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

import { isCmd } from 'diagram-js/lib/features/keyboard/KeyboardUtil';

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

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

import { isAny } from '../modeling/util/ModelingUtil';

import { some } from 'min-dash';

var HIGHER_PRIORITY = 1250;

var BOUNDARY_TO_HOST_THRESHOLD = 40;

var TARGET_BOUNDS_PADDING = 20,
    TASK_BOUNDS_PADDING = 10;

var TARGET_CENTER_PADDING = 20;

var AXES = [ 'x', 'y' ];

var abs = Math.abs;

/**
 * Snap during connect.
 *
 * @param {EventBus} eventBus
 */
export default function BpmnConnectSnapping(eventBus) {
  eventBus.on([
    'connect.hover',
    'connect.move',
    'connect.end',
  ], HIGHER_PRIORITY, function(event) {
    var context = event.context,
        canExecute = context.canExecute,
        start = context.start,
        hover = context.hover,
        source = context.source,
        target = context.target;

    // do NOT snap on CMD
    if (event.originalEvent && isCmd(event.originalEvent)) {
      return;
    }

    if (!context.initialConnectionStart) {
      context.initialConnectionStart = context.connectionStart;
    }

    // snap hover
    if (canExecute && hover) {
      snapToShape(event, hover, getTargetBoundsPadding(hover));
    }

    if (hover && isAnyType(canExecute, [
      'bpmn:Association',
      'bpmn:DataInputAssociation',
      'bpmn:DataOutputAssociation',
      'bpmn:SequenceFlow'
    ])) {
      context.connectionStart = mid(start);

      // snap hover
      if (isAny(hover, [ 'bpmn:Event', 'bpmn:Gateway' ])) {
        snapToPosition(event, mid(hover));
      }

      // snap hover
      if (isAny(hover, [ 'bpmn:Task', 'bpmn:SubProcess' ])) {
        snapToTargetMid(event, hover);
      }

      // snap source and target
      if (is(source, 'bpmn:BoundaryEvent') && target === source.host) {
        snapBoundaryEventLoop(event);
      }

    } else if (isType(canExecute, 'bpmn:MessageFlow')) {

      if (is(start, 'bpmn:Event')) {

        // snap start
        context.connectionStart = mid(start);
      }

      if (is(hover, 'bpmn:Event')) {

        // snap hover
        snapToPosition(event, mid(hover));
      }

    } else {

      // un-snap source
      context.connectionStart = context.initialConnectionStart;
    }
  });
}

BpmnConnectSnapping.$inject = [ 'eventBus' ];


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

// snap to target if event in target
function snapToShape(event, target, padding) {
  AXES.forEach(function(axis) {
    var dimensionForAxis = getDimensionForAxis(axis, target);

    if (event[ axis ] < target[ axis ] + padding) {
      setSnapped(event, axis, target[ axis ] + padding);
    } else if (event[ axis ] > target[ axis ] + dimensionForAxis - padding) {
      setSnapped(event, axis, target[ axis ] + dimensionForAxis - padding);
    }
  });
}

// snap to target mid if event in target mid
function snapToTargetMid(event, target) {
  var targetMid = mid(target);

  AXES.forEach(function(axis) {
    if (isMid(event, target, axis)) {
      setSnapped(event, axis, targetMid[ axis ]);
    }
  });
}

// snap to prevent loop overlapping boundary event
function snapBoundaryEventLoop(event) {
  var context = event.context,
      source = context.source,
      target = context.target;

  if (isReverse(context)) {
    return;
  }

  var sourceMid = mid(source),
      orientation = getOrientation(sourceMid, target, -10),
      axes = [];

  if (/top|bottom/.test(orientation)) {
    axes.push('x');
  }

  if (/left|right/.test(orientation)) {
    axes.push('y');
  }

  axes.forEach(function(axis) {
    var coordinate = event[ axis ], newCoordinate;

    if (abs(coordinate - sourceMid[ axis ]) < BOUNDARY_TO_HOST_THRESHOLD) {
      if (coordinate > sourceMid[ axis ]) {
        newCoordinate = sourceMid[ axis ] + BOUNDARY_TO_HOST_THRESHOLD;
      }
      else {
        newCoordinate = sourceMid[ axis ] - BOUNDARY_TO_HOST_THRESHOLD;
      }

      setSnapped(event, axis, newCoordinate);
    }
  });
}

function snapToPosition(event, position) {
  setSnapped(event, 'x', position.x);
  setSnapped(event, 'y', position.y);
}

function isType(attrs, type) {
  return attrs && attrs.type === type;
}

function isAnyType(attrs, types) {
  return some(types, function(type) {
    return isType(attrs, type);
  });
}

function getDimensionForAxis(axis, element) {
  return axis === 'x' ? element.width : element.height;
}

function getTargetBoundsPadding(target) {
  if (is(target, 'bpmn:Task')) {
    return TASK_BOUNDS_PADDING;
  } else {
    return TARGET_BOUNDS_PADDING;
  }
}

function isMid(event, target, axis) {
  return event[ axis ] > target[ axis ] + TARGET_CENTER_PADDING
    && event[ axis ] < target[ axis ] + getDimensionForAxis(axis, target) - TARGET_CENTER_PADDING;
}

function isReverse(context) {
  var hover = context.hover,
      source = context.source;

  return hover && source && hover === source;
}