import inherits from 'inherits';

import CreateMoveSnapping from 'diagram-js/lib/features/snapping/CreateMoveSnapping';

import {
  isSnapped,
  setSnapped,
  topLeft,
  bottomRight
} from 'diagram-js/lib/features/snapping/SnapUtil';

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

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

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

import { getBoundaryAttachment } from './BpmnSnappingUtil';

import { forEach } from 'min-dash';

var HIGH_PRIORITY = 1500;


/**
 * Snap during create and move.
 *
 * @param {BpmnRules} bpmnRules
 * @param {EventBus} eventBus
 * @param {Injector} injector
 */
export default function BpmnCreateMoveSnapping(bpmnRules, eventBus, injector) {
  injector.invoke(CreateMoveSnapping, this);

  // creating first participant
  eventBus.on([ 'create.move', 'create.end' ], HIGH_PRIORITY, setSnappedIfConstrained);

  function canAttach(shape, target, position) {
    return bpmnRules.canAttach([ shape ], target, null, position) === 'attach';
  }

  // snap boundary events
  eventBus.on([
    'create.move',
    'create.end',
    'shape.move.move',
    'shape.move.end'
  ], HIGH_PRIORITY, function(event) {
    var context = event.context,
        target = context.target,
        shape = context.shape;

    if (target && canAttach(shape, target, event) && !isSnapped(event)) {
      snapBoundaryEvent(event, target);
    }
  });
}

inherits(BpmnCreateMoveSnapping, CreateMoveSnapping);

BpmnCreateMoveSnapping.$inject = [
  'bpmnRules',
  'eventBus',
  'injector'
];

BpmnCreateMoveSnapping.prototype.initSnap = function(event) {
  var snapContext = CreateMoveSnapping.prototype.initSnap.call(this, event);

  var shape = event.shape;

  // snap to docking points
  forEach(shape.outgoing, function(connection) {
    var docking = connection.waypoints[0];

    docking = docking.original || docking;

    snapContext.setSnapOrigin(connection.id + '-docking', {
      x: docking.x - event.x,
      y: docking.y - event.y
    });
  });

  forEach(shape.incoming, function(connection) {
    var docking = connection.waypoints[connection.waypoints.length - 1];

    docking = docking.original || docking;

    snapContext.setSnapOrigin(connection.id + '-docking', {
      x: docking.x - event.x,
      y: docking.y - event.y
    });
  });

  if (is(shape, 'bpmn:Participant')) {

    // snap to borders with higher priority
    snapContext.setSnapLocations([ 'top-left', 'bottom-right', 'mid' ]);
  }

  return snapContext;
};

BpmnCreateMoveSnapping.prototype.addSnapTargetPoints = function(snapPoints, shape, target) {
  CreateMoveSnapping.prototype.addSnapTargetPoints.call(this, snapPoints, shape, target);

  var snapTargets = this.getSnapTargets(shape, target);

  forEach(snapTargets, function(snapTarget) {

    // handle TRBL alignment
    //
    // * with container elements
    // * with text annotations
    if (isContainer(snapTarget) || areAll([ shape, snapTarget ], 'bpmn:TextAnnotation')) {
      snapPoints.add('top-left', topLeft(snapTarget));
      snapPoints.add('bottom-right', bottomRight(snapTarget));
    }
  });

  // snap to docking points
  forEach(shape.incoming, function(connection) {

    if (!includes(snapTargets, connection.source)) {
      snapPoints.add('mid', getMid(connection.source));
    }

    var docking = connection.waypoints[0];

    snapPoints.add(connection.id + '-docking', docking.original || docking);
  });


  forEach(shape.outgoing, function(connection) {

    if (!includes(snapTargets, connection.target)) {
      snapPoints.add('mid', getMid(connection.target));
    }

    var docking = connection.waypoints[ connection.waypoints.length - 1 ];

    snapPoints.add(connection.id + '-docking', docking.original || docking);
  });

  // add sequence flow parents as snap targets
  if (is(target, 'bpmn:SequenceFlow')) {
    snapPoints = this.addSnapTargetPoints(snapPoints, shape, target.parent);
  }

  return snapPoints;
};

BpmnCreateMoveSnapping.prototype.getSnapTargets = function(shape, target) {
  return CreateMoveSnapping.prototype.getSnapTargets.call(this, shape, target)
    .filter(function(snapTarget) {

      // do not snap to lanes
      return !is(snapTarget, 'bpmn:Lane');
    });
};

// helpers //////////

function snapBoundaryEvent(event, target) {
  var targetTRBL = asTRBL(target);

  var direction = getBoundaryAttachment(event, target);

  if (/top/.test(direction)) {
    setSnapped(event, 'y', targetTRBL.top);
  } else
  if (/bottom/.test(direction)) {
    setSnapped(event, 'y', targetTRBL.bottom);
  }

  if (/left/.test(direction)) {
    setSnapped(event, 'x', targetTRBL.left);
  } else
  if (/right/.test(direction)) {
    setSnapped(event, 'x', targetTRBL.right);
  }
}

function areAll(elements, type) {
  return elements.every(function(el) {
    return is(el, type);
  });
}

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

  return is(element, 'bpmn:Participant');
}


function setSnappedIfConstrained(event) {
  var context = event.context,
      createConstraints = context.createConstraints;

  if (!createConstraints) {
    return;
  }

  var top = createConstraints.top,
      right = createConstraints.right,
      bottom = createConstraints.bottom,
      left = createConstraints.left;

  if ((left && left >= event.x) || (right && right <= event.x)) {
    setSnapped(event, 'x', event.x);
  }

  if ((top && top >= event.y) || (bottom && bottom <= event.y)) {
    setSnapped(event, 'y', event.y);
  }
}

function includes(array, value) {
  return array.indexOf(value) !== -1;
}