import {
  find,
  forEach,
  isArray,
  isDefined,
  isObject,
  matchPattern,
  reduce,
  has,
  sortBy
} from 'min-dash';

var DISALLOWED_PROPERTIES = [
  'artifacts',
  'dataInputAssociations',
  'dataOutputAssociations',
  'default',
  'flowElements',
  'lanes',
  'incoming',
  'outgoing',
  'categoryValue'
];

/**
 * @typedef {Function} <moddleCopy.canCopyProperties> listener
 *
 * @param {Object} context
 * @param {Array<string>} context.propertyNames
 * @param {ModdleElement} context.sourceElement
 * @param {ModdleElement} context.targetElement
 *
 * @returns {Array<string>|boolean} - Return properties to be copied or false to disallow
 * copying.
 */

/**
 * @typedef {Function} <moddleCopy.canCopyProperty> listener
 *
 * @param {Object} context
 * @param {ModdleElement} context.parent
 * @param {*} context.property
 * @param {string} context.propertyName
 *
 * @returns {*|boolean} - Return copied property or false to disallow
 * copying.
 */

/**
 * @typedef {Function} <moddleCopy.canSetCopiedProperty> listener
 *
 * @param {Object} context
 * @param {ModdleElement} context.parent
 * @param {*} context.property
 * @param {string} context.propertyName
 *
 * @returns {boolean} - Return false to disallow
 * setting copied property.
 */

/**
 * Utility for copying model properties from source element to target element.
 *
 * @param {EventBus} eventBus
 * @param {BpmnFactory} bpmnFactory
 * @param {BpmnModdle} moddle
 */
export default function ModdleCopy(eventBus, bpmnFactory, moddle) {
  this._bpmnFactory = bpmnFactory;
  this._eventBus = eventBus;
  this._moddle = moddle;

  // copy extension elements last
  eventBus.on('moddleCopy.canCopyProperties', function(context) {
    var propertyNames = context.propertyNames;

    if (!propertyNames || !propertyNames.length) {
      return;
    }

    return sortBy(propertyNames, function(propertyName) {
      return propertyName === 'extensionElements';
    });
  });

  // default check whether property can be copied
  eventBus.on('moddleCopy.canCopyProperty', function(context) {
    var parent = context.parent,
        parentDescriptor = isObject(parent) && parent.$descriptor,
        propertyName = context.propertyName;

    if (propertyName && DISALLOWED_PROPERTIES.indexOf(propertyName) !== -1) {

      // disallow copying property
      return false;
    }

    if (propertyName &&
      parentDescriptor &&
      !find(parentDescriptor.properties, matchPattern({ name: propertyName }))) {

      // disallow copying property
      return false;
    }
  });

  // do NOT allow to copy empty extension elements
  eventBus.on('moddleCopy.canSetCopiedProperty', function(context) {
    var property = context.property;

    if (is(property, 'bpmn:ExtensionElements') && (!property.values || !property.values.length)) {

      // disallow setting copied property
      return false;
    }
  });
}

ModdleCopy.$inject = [
  'eventBus',
  'bpmnFactory',
  'moddle'
];

/**
 * Copy model properties of source element to target element.
 *
 * @param {ModdleElement} sourceElement
 * @param {ModdleElement} targetElement
 * @param {Array<string>} [propertyNames]
 * @param {boolean} clone
 *
 * @param {ModdleElement}
 */
ModdleCopy.prototype.copyElement = function(sourceElement, targetElement, propertyNames, clone) {
  var self = this;

  if (propertyNames && !isArray(propertyNames)) {
    propertyNames = [ propertyNames ];
  }

  propertyNames = propertyNames || getPropertyNames(sourceElement.$descriptor);

  var canCopyProperties = this._eventBus.fire('moddleCopy.canCopyProperties', {
    propertyNames: propertyNames,
    sourceElement: sourceElement,
    targetElement: targetElement,
    clone: clone
  });

  if (canCopyProperties === false) {
    return targetElement;
  }

  if (isArray(canCopyProperties)) {
    propertyNames = canCopyProperties;
  }

  // copy properties
  forEach(propertyNames, function(propertyName) {
    var sourceProperty;

    if (has(sourceElement, propertyName)) {
      sourceProperty = sourceElement.get(propertyName);
    }

    var copiedProperty = self.copyProperty(sourceProperty, targetElement, propertyName, clone);

    if (!isDefined(copiedProperty)) {
      return;
    }

    var canSetProperty = self._eventBus.fire('moddleCopy.canSetCopiedProperty', {
      parent: targetElement,
      property: copiedProperty,
      propertyName: propertyName
    });

    if (canSetProperty === false) {
      return;
    }

    // TODO(nikku): unclaim old IDs if ID property is copied over
    // this._moddle.getPropertyDescriptor(parent, propertyName)
    targetElement.set(propertyName, copiedProperty);
  });

  return targetElement;
};

/**
 * Copy model property.
 *
 * @param {*} property
 * @param {ModdleElement} parent
 * @param {string} propertyName
 * @param {boolean} clone
 *
 * @returns {*}
 */
ModdleCopy.prototype.copyProperty = function(property, parent, propertyName, clone) {
  var self = this;

  // allow others to copy property
  var copiedProperty = this._eventBus.fire('moddleCopy.canCopyProperty', {
    parent: parent,
    property: property,
    propertyName: propertyName,
    clone: clone
  });

  // return if copying is NOT allowed
  if (copiedProperty === false) {
    return;
  }

  if (copiedProperty) {
    if (isObject(copiedProperty) && copiedProperty.$type && !copiedProperty.$parent) {
      copiedProperty.$parent = parent;
    }

    return copiedProperty;
  }

  var propertyDescriptor = this._moddle.getPropertyDescriptor(parent, propertyName);

  // do NOT copy references
  if (propertyDescriptor.isReference) {
    return;
  }

  // copy id
  if (propertyDescriptor.isId) {
    return property && this._copyId(property, parent, clone);
  }

  // copy arrays
  if (isArray(property)) {
    return reduce(property, function(childProperties, childProperty) {

      // recursion
      copiedProperty = self.copyProperty(childProperty, parent, propertyName, clone);

      // copying might NOT be allowed
      if (copiedProperty) {
        return childProperties.concat(copiedProperty);
      }

      return childProperties;
    }, []);
  }

  // copy model elements
  if (isObject(property) && property.$type) {
    if (this._moddle.getElementDescriptor(property).isGeneric) {
      return;
    }

    copiedProperty = self._bpmnFactory.create(property.$type);

    copiedProperty.$parent = parent;

    // recursion
    copiedProperty = self.copyElement(property, copiedProperty, null, clone);

    return copiedProperty;
  }

  // copy primitive properties
  return property;
};

ModdleCopy.prototype._copyId = function(id, element, clone) {

  if (clone) {
    return id;
  }

  // disallow if already taken
  if (this._moddle.ids.assigned(id)) {
    return;
  } else {

    this._moddle.ids.claim(id, element);
    return id;
  }
};

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

export function getPropertyNames(descriptor, keepDefaultProperties) {
  return reduce(descriptor.properties, function(properties, property) {

    if (keepDefaultProperties && property.default) {
      return properties;
    }

    return properties.concat(property.name);
  }, []);
}

function is(element, type) {
  return element && (typeof element.$instanceOf === 'function') && element.$instanceOf(type);
}