import {
  pick,
  assign,
  filter,
  forEach,
  isArray,
  isUndefined,
  has
} from 'min-dash';

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

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

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

import { getPropertyNames } from '../copy-paste/ModdleCopy';

function copyProperties(source, target, properties) {
  if (!isArray(properties)) {
    properties = [ properties ];
  }

  forEach(properties, function(property) {
    if (!isUndefined(source[property])) {
      target[property] = source[property];
    }
  });
}


var CUSTOM_PROPERTIES = [
  'cancelActivity',
  'instantiate',
  'eventGatewayType',
  'triggeredByEvent',
  'isInterrupting'
];

/**
 * Check if element should be collapsed or expanded.
 */
function shouldToggleCollapsed(element, targetElement) {

  var oldCollapsed = (
    element && has(element, 'collapsed') ? element.collapsed : !isExpanded(element)
  );

  var targetCollapsed;

  if (targetElement && (has(targetElement, 'collapsed') || has(targetElement, 'isExpanded'))) {

    // property is explicitly set so use it
    targetCollapsed = (
      has(targetElement, 'collapsed') ? targetElement.collapsed : !targetElement.isExpanded
    );
  } else {

    // keep old state
    targetCollapsed = oldCollapsed;
  }

  if (oldCollapsed !== targetCollapsed) {
    return true;
  }

  return false;
}


/**
 * This module takes care of replacing BPMN elements
 */
export default function BpmnReplace(
    bpmnFactory,
    elementFactory,
    moddleCopy,
    modeling,
    replace,
    rules,
    selection
) {

  /**
   * Prepares a new business object for the replacement element
   * and triggers the replace operation.
   *
   * @param  {djs.model.Base} element
   * @param  {Object} target
   * @param  {Object} [hints]
   *
   * @return {djs.model.Base} the newly created element
   */
  function replaceElement(element, target, hints) {

    hints = hints || {};

    var type = target.type,
        oldBusinessObject = element.businessObject;

    if (isSubProcess(oldBusinessObject) && type === 'bpmn:SubProcess') {
      if (shouldToggleCollapsed(element, target)) {

        // expanding or collapsing process
        modeling.toggleCollapse(element);

        return element;
      }
    }

    var newBusinessObject = bpmnFactory.create(type);

    var newElement = {
      type: type,
      businessObject: newBusinessObject,
    };

    newElement.di = {};

    // colors will be set to DI
    copyProperties(element.di, newElement.di, [
      'fill',
      'stroke',
      'background-color',
      'border-color',
      'color'
    ]);

    var elementProps = getPropertyNames(oldBusinessObject.$descriptor),
        newElementProps = getPropertyNames(newBusinessObject.$descriptor, true),
        copyProps = intersection(elementProps, newElementProps);

    // initialize special properties defined in target definition
    assign(newBusinessObject, pick(target, CUSTOM_PROPERTIES));

    var properties = filter(copyProps, function(propertyName) {

      // copying event definitions, unless we replace
      if (propertyName === 'eventDefinitions') {
        return hasEventDefinition(element, target.eventDefinitionType);
      }

      // retain loop characteristics if the target element
      // is not an event sub process
      if (propertyName === 'loopCharacteristics') {
        return !isEventSubProcess(newBusinessObject);
      }

      // so the applied properties from 'target' don't get lost
      if (has(newBusinessObject, propertyName)) {
        return false;
      }

      if (propertyName === 'processRef' && target.isExpanded === false) {
        return false;
      }

      if (propertyName === 'triggeredByEvent') {
        return false;
      }

      return true;
    });

    newBusinessObject = moddleCopy.copyElement(
      oldBusinessObject,
      newBusinessObject,
      properties
    );

    // initialize custom BPMN extensions
    if (target.eventDefinitionType) {

      // only initialize with new eventDefinition
      // if we did not set an event definition yet,
      // i.e. because we copied it
      if (!hasEventDefinition(newBusinessObject, target.eventDefinitionType)) {
        newElement.eventDefinitionType = target.eventDefinitionType;
        newElement.eventDefinitionAttrs = target.eventDefinitionAttrs;
      }
    }

    if (is(oldBusinessObject, 'bpmn:Activity')) {

      if (isSubProcess(oldBusinessObject)) {

        // no toggeling, so keep old state
        newElement.isExpanded = isExpanded(element);
      }

      // else if property is explicitly set, use it
      else if (target && has(target, 'isExpanded')) {
        newElement.isExpanded = target.isExpanded;

        // assign default size of new expanded element
        var defaultSize = elementFactory.getDefaultSize(newBusinessObject, {
          isExpanded: newElement.isExpanded
        });

        newElement.width = defaultSize.width;
        newElement.height = defaultSize.height;

        // keep element centered
        newElement.x = element.x - (newElement.width - element.width) / 2;
        newElement.y = element.y - (newElement.height - element.height) / 2;
      }

      // TODO: need also to respect min/max Size
      // copy size, from an expanded subprocess to an expanded alternative subprocess
      // except bpmn:Task, because Task is always expanded
      if ((isExpanded(element) && !is(oldBusinessObject, 'bpmn:Task')) && newElement.isExpanded) {
        newElement.width = element.width;
        newElement.height = element.height;
      }
    }

    // remove children if not expanding sub process
    if (isSubProcess(oldBusinessObject) && !isSubProcess(newBusinessObject)) {
      hints.moveChildren = false;
    }

    // transform collapsed/expanded pools
    if (is(oldBusinessObject, 'bpmn:Participant')) {

      // create expanded pool
      if (target.isExpanded === true) {
        newBusinessObject.processRef = bpmnFactory.create('bpmn:Process');
      } else {

        // remove children when transforming to collapsed pool
        hints.moveChildren = false;
      }

      // apply same width and default height
      newElement.width = element.width;
      newElement.height = elementFactory.getDefaultSize(newElement).height;
    }

    if (!rules.allowed('shape.resize', { shape: newBusinessObject })) {
      newElement.height = elementFactory.getDefaultSize(newElement).height;
      newElement.width = elementFactory.getDefaultSize(newElement).width;
    }

    newBusinessObject.name = oldBusinessObject.name;

    // retain default flow's reference between inclusive <-> exclusive gateways and activities
    if (
      isAny(oldBusinessObject, [
        'bpmn:ExclusiveGateway',
        'bpmn:InclusiveGateway',
        'bpmn:Activity'
      ]) &&
      isAny(newBusinessObject, [
        'bpmn:ExclusiveGateway',
        'bpmn:InclusiveGateway',
        'bpmn:Activity'
      ])
    ) {
      newBusinessObject.default = oldBusinessObject.default;
    }

    if (
      target.host &&
      !is(oldBusinessObject, 'bpmn:BoundaryEvent') &&
      is(newBusinessObject, 'bpmn:BoundaryEvent')
    ) {
      newElement.host = target.host;
    }

    // The DataStoreReference element is 14px wider than the DataObjectReference element
    // This ensures that they stay centered on the x axis when replaced
    if (
      newElement.type === 'bpmn:DataStoreReference' ||
      newElement.type === 'bpmn:DataObjectReference'
    ) {
      newElement.x = element.x + (element.width - newElement.width) / 2;
    }


    newElement = replace.replaceElement(element, newElement, hints);

    if (hints.select !== false) {
      selection.select(newElement);
    }

    return newElement;
  }

  this.replaceElement = replaceElement;
}

BpmnReplace.$inject = [
  'bpmnFactory',
  'elementFactory',
  'moddleCopy',
  'modeling',
  'replace',
  'rules',
  'selection'
];


function isSubProcess(bo) {
  return is(bo, 'bpmn:SubProcess');
}

function hasEventDefinition(element, type) {

  var bo = getBusinessObject(element);

  return type && bo.get('eventDefinitions').some(function(definition) {
    return is(definition, type);
  });
}

/**
 * Compute intersection between two arrays.
 */
function intersection(a1, a2) {
  return a1.filter(function(el) {
    return a2.indexOf(el) !== -1;
  });
}
