import inherits from 'inherits';

import {
  assign
} from 'min-dash';

import BaseLayouter from 'diagram-js/lib/layout/BaseLayouter';

import {
  repairConnection,
  withoutRedundantPoints
} from 'diagram-js/lib/layout/ManhattanLayout';

import {
  getMid,
  getOrientation
} from 'diagram-js/lib/layout/LayoutUtil';

import {
  isExpanded
} from '../../util/DiUtil';

import { is } from '../../util/ModelUtil';


var BOUNDARY_TO_HOST_THRESHOLD = 40;

export default function BpmnLayouter() {}

inherits(BpmnLayouter, BaseLayouter);


BpmnLayouter.prototype.layoutConnection = function(connection, hints) {
  hints = hints || {};

  var source = hints.source || connection.source,
      target = hints.target || connection.target,
      waypoints = connection.waypoints,
      start = hints.connectionStart,
      end = hints.connectionEnd;

  var manhattanOptions,
      updatedWaypoints;

  if (!start) {
    start = getConnectionDocking(waypoints && waypoints[0], source);
  }

  if (!end) {
    end = getConnectionDocking(waypoints && waypoints[waypoints.length - 1], target);
  }

  // TODO(nikku): support vertical modeling
  // and invert preferredLayouts accordingly

  if (is(connection, 'bpmn:Association') ||
      is(connection, 'bpmn:DataAssociation')) {

    if (waypoints && !isCompensationAssociation(source, target)) {
      return [].concat([ start ], waypoints.slice(1, -1), [ end ]);
    }
  }

  // manhattan layout sequence / message flows
  if (is(connection, 'bpmn:MessageFlow')) {
    manhattanOptions = getMessageFlowManhattanOptions(source, target);

  } else


  // layout all connection between flow elements h:h,
  //
  // except for
  //
  // (1) outgoing of BoundaryEvents -> layout based on attach orientation and target orientation
  // (2) incoming / outgoing of Gateway -> v:h (outgoing), h:v (incoming)
  // (3) loops from / to the same element
  //
  if (is(connection, 'bpmn:SequenceFlow') ||
      isCompensationAssociation(source, target)) {

    if (source === target) {
      manhattanOptions = {
        preferredLayouts: [ 'b:l' ]
      };
    } else

    if (is(source, 'bpmn:BoundaryEvent')) {

      manhattanOptions = {
        preferredLayouts: getBoundaryEventPreferredLayouts(source, target, end)
      };

    } else

    if (is(source, 'bpmn:Gateway')) {

      manhattanOptions = {
        preferredLayouts: [ 'v:h' ]
      };
    } else

    if (is(target, 'bpmn:Gateway')) {

      manhattanOptions = {
        preferredLayouts: [ 'h:v' ]
      };
    }

    else {
      manhattanOptions = {
        preferredLayouts: [ 'h:h' ]
      };
    }

  }

  if (manhattanOptions) {

    manhattanOptions = assign(manhattanOptions, hints);

    updatedWaypoints =
      withoutRedundantPoints(
        repairConnection(
          source, target,
          start, end,
          waypoints,
          manhattanOptions
        )
      );
  }

  return updatedWaypoints || [ start, end ];
};


function getAttachOrientation(attachedElement) {

  var hostElement = attachedElement.host,
      padding = -10;

  return getOrientation(getMid(attachedElement), hostElement, padding);
}


function getMessageFlowManhattanOptions(source, target) {
  return {
    preferredLayouts: [ 'straight', 'v:v' ],
    preserveDocking: getMessageFlowPreserveDocking(source, target)
  };
}


function getMessageFlowPreserveDocking(source, target) {

  // (1) docking element connected to participant has precedence

  if (is(target, 'bpmn:Participant')) {
    return 'source';
  }

  if (is(source, 'bpmn:Participant')) {
    return 'target';
  }

  // (2) docking element connected to expanded sub-process has precedence

  if (isExpandedSubProcess(target)) {
    return 'source';
  }

  if (isExpandedSubProcess(source)) {
    return 'target';
  }

  // (3) docking event has precedence

  if (is(target, 'bpmn:Event')) {
    return 'target';
  }

  if (is(source, 'bpmn:Event')) {
    return 'source';
  }

  return null;
}


function getConnectionDocking(point, shape) {
  return point ? (point.original || point) : getMid(shape);
}

function isCompensationAssociation(source, target) {
  return is(target, 'bpmn:Activity') &&
         is(source, 'bpmn:BoundaryEvent') &&
         target.businessObject.isForCompensation;
}


function isExpandedSubProcess(element) {
  return is(element, 'bpmn:SubProcess') && isExpanded(element);
}

function isSame(a, b) {
  return a === b;
}

function isAnyOrientation(orientation, orientations) {
  return orientations.indexOf(orientation) !== -1;
}

var oppositeOrientationMapping = {
  'top': 'bottom',
  'top-right': 'bottom-left',
  'top-left': 'bottom-right',
  'right': 'left',
  'bottom': 'top',
  'bottom-right': 'top-left',
  'bottom-left': 'top-right',
  'left': 'right'
};

var orientationDirectionMapping = {
  top: 't',
  right: 'r',
  bottom: 'b',
  left: 'l'
};

function getHorizontalOrientation(orientation) {
  var matches = /right|left/.exec(orientation);

  return matches && matches[0];
}

function getVerticalOrientation(orientation) {
  var matches = /top|bottom/.exec(orientation);

  return matches && matches[0];
}

function isOppositeOrientation(a, b) {
  return oppositeOrientationMapping[a] === b;
}

function isOppositeHorizontalOrientation(a, b) {
  var horizontalOrientation = getHorizontalOrientation(a);

  var oppositeHorizontalOrientation = oppositeOrientationMapping[horizontalOrientation];

  return b.indexOf(oppositeHorizontalOrientation) !== -1;
}

function isOppositeVerticalOrientation(a, b) {
  var verticalOrientation = getVerticalOrientation(a);

  var oppositeVerticalOrientation = oppositeOrientationMapping[verticalOrientation];

  return b.indexOf(oppositeVerticalOrientation) !== -1;
}

function isHorizontalOrientation(orientation) {
  return orientation === 'right' || orientation === 'left';
}

function getBoundaryEventPreferredLayouts(source, target, end) {
  var sourceMid = getMid(source),
      targetMid = getMid(target),
      attachOrientation = getAttachOrientation(source),
      sourceLayout,
      targetLayout;

  var isLoop = isSame(source.host, target);

  var attachedToSide = isAnyOrientation(attachOrientation, [ 'top', 'right', 'bottom', 'left' ]);

  var targetOrientation = getOrientation(targetMid, sourceMid, {
    x: source.width / 2 + target.width / 2,
    y: source.height / 2 + target.height / 2
  });

  if (isLoop) {
    return getBoundaryEventLoopLayout(attachOrientation, attachedToSide, source, target, end);
  }

  // source layout
  sourceLayout = getBoundaryEventSourceLayout(attachOrientation, targetOrientation, attachedToSide);

  // target layout
  targetLayout = getBoundaryEventTargetLayout(attachOrientation, targetOrientation, attachedToSide);

  return [ sourceLayout + ':' + targetLayout ];
}

function getBoundaryEventLoopLayout(attachOrientation, attachedToSide, source, target, end) {

  var sourceLayout = orientationDirectionMapping[attachedToSide ? attachOrientation : getVerticalOrientation(attachOrientation)],
      targetLayout;

  if (attachedToSide) {
    if (isHorizontalOrientation(attachOrientation)) {
      targetLayout = shouldConnectToSameSide('y', source, target, end) ? 'h' : 'b';
    } else {
      targetLayout = shouldConnectToSameSide('x', source, target, end) ? 'v' : 'l';
    }
  } else {
    targetLayout = 'v';
  }

  return [ sourceLayout + ':' + targetLayout ];
}

function shouldConnectToSameSide(axis, source, target, end) {
  var threshold = BOUNDARY_TO_HOST_THRESHOLD;

  return !(
    areCloseOnAxis(axis, end, target, threshold) ||
    areCloseOnAxis(axis, end, { x: target.x + target.width, y: target.y + target.height }, threshold) ||
    areCloseOnAxis(axis, end, getMid(source), threshold)
  );
}

function areCloseOnAxis(axis, a, b, threshold) {
  return Math.abs(a[axis] - b[axis]) < threshold;
}

function getBoundaryEventSourceLayout(attachOrientation, targetOrientation, attachedToSide) {

  // attached to either top, right, bottom or left side
  if (attachedToSide) {
    return orientationDirectionMapping[attachOrientation];
  }

  // attached to either top-right, top-left, bottom-right or bottom-left corner

  // same vertical or opposite horizontal orientation
  if (isSame(
    getVerticalOrientation(attachOrientation), getVerticalOrientation(targetOrientation)
  ) || isOppositeOrientation(
    getHorizontalOrientation(attachOrientation), getHorizontalOrientation(targetOrientation)
  )) {
    return orientationDirectionMapping[getVerticalOrientation(attachOrientation)];
  }

  // fallback
  return orientationDirectionMapping[getHorizontalOrientation(attachOrientation)];
}

function getBoundaryEventTargetLayout(attachOrientation, targetOrientation, attachedToSide) {

  // attached to either top, right, bottom or left side
  if (attachedToSide) {
    if (isHorizontalOrientation(attachOrientation)) {
      // orientation is 'right' or 'left'

      // opposite horizontal orientation or same orientation
      if (
        isOppositeHorizontalOrientation(attachOrientation, targetOrientation) ||
        isSame(attachOrientation, targetOrientation)
      ) {
        return 'h';
      }

      // fallback
      return 'v';
    } else {
      // orientation is 'top' or 'bottom'

      // opposite vertical orientation or same orientation
      if (
        isOppositeVerticalOrientation(attachOrientation, targetOrientation) ||
        isSame(attachOrientation, targetOrientation)
      ) {
        return 'v';
      }

      // fallback
      return 'h';
    }
  }
  // attached to either top-right, top-left, bottom-right or bottom-left corner

  // orientation is 'right', 'left'
  // or same vertical orientation but also 'right' or 'left'
  if (isHorizontalOrientation(targetOrientation) ||
    (isSame(getVerticalOrientation(attachOrientation), getVerticalOrientation(targetOrientation)) &&
      getHorizontalOrientation(targetOrientation))) {
    return 'h';
  } else {
    return 'v';
  }
}
