UNPKG

bpmn-js-element-templates

Version:
7,956 lines 214 kB
'use strict';

var ModelUtil = require('bpmn-js/lib/util/ModelUtil');
var uuid = require('uuid');
var minDash = require('min-dash');
var semver = require('semver');
var semverCompare = require('semver-compare');
var elementTemplatesValidator = require('@bpmn-io/element-templates-validator');
var Ids = require('ids');
var LabelUtil = require('bpmn-js/lib/features/label-editing/LabelUtil');
var DrilldownUtil = require('bpmn-js/lib/util/DrilldownUtil');
var DiUtil = require('bpmn-js/lib/util/DiUtil');
var CommandInterceptor = require('diagram-js/lib/command/CommandInterceptor');

/**
 * Check if the property is cast to FEEL expression:
 * - Boolean and Number properties with feel set to 'optional' or 'static'
 * - Boolean and Number input/output parameters have default feel=static
 *
 * @returns {boolean}
 */
const shouldCastToFeel = (property) => {
  const feel = getFeelValue(property);

  return [ 'optional', 'static' ].includes(feel) && [ 'Boolean', 'Number' ].includes(property.type);
};

const ALWAYS_CAST_TO_FEEL = [
  'zeebe:input',
  'zeebe:output'
];

function getFeelValue(property) {
  if (ALWAYS_CAST_TO_FEEL.includes(property.binding.type)) {
    return property.feel || 'static';
  }

  return property.feel;
}

const toFeelExpression = (value, type) => {
  if (typeof value === 'string' && value.startsWith('=')) {
    return value;
  }

  if (type === 'Boolean') {
    value = value === 'false' ? false : value;
    return '=' + !!value;
  }

  if (typeof value === 'undefined') {
    return value;
  }

  return '=' + value.toString();
};

/**
 * The BPMN 2.0 extension attribute name under
 * which the element template ID is stored.
 *
 * @type {String}
 */
const TEMPLATE_ID_ATTR$1 = 'zeebe:modelerTemplate';

/**
 * The BPMN 2.0 extension attribute name under
 * which the element template version is stored.
 *
 * @type {String}
 */
const TEMPLATE_VERSION_ATTR$1 = 'zeebe:modelerTemplateVersion';

/**
 * The BPMN 2.0 extension attribute name under
 * which the element template icon is stored.
 *
 * @type {String}
 */
const TEMPLATE_ICON_ATTR = 'zeebe:modelerTemplateIcon';


/**
 * Get template id for a given diagram element.
 *
 * @param {djs.model.Base} element
 *
 * @return {String}
 */
function getTemplateId$1(element) {
  const businessObject = ModelUtil.getBusinessObject(element);

  if (businessObject) {
    return businessObject.get(TEMPLATE_ID_ATTR$1);
  }
}

/**
 * Get template version for a given diagram element.
 *
 * @param {djs.model.Base} element
 *
 * @return {String}
 */
function getTemplateVersion$1(element) {
  const businessObject = ModelUtil.getBusinessObject(element);

  if (businessObject) {
    return businessObject.get(TEMPLATE_VERSION_ATTR$1);
  }
}

/**
 * Get template icon for a given diagram element.
 *
 * @param {djs.model.Base} element
 *
 * @return {String}
 */
function getTemplateIcon(element) {
  const businessObject = ModelUtil.getBusinessObject(element);

  if (businessObject) {
    return businessObject.get(TEMPLATE_ICON_ATTR);
  }
}

/**
 * Find extension with given type in
 * BPMN element, diagram element or ExtensionElement.
 *
 * @param {ModdleElement|djs.model.Base} element
 * @param {String} type
 *
 * @return {ModdleElement} the extension
 */
function findExtension$1(element, type) {
  const businessObject = ModelUtil.getBusinessObject(element);

  let extensionElements;

  if (ModelUtil.is(businessObject, 'bpmn:ExtensionElements')) {
    extensionElements = businessObject;
  } else {
    extensionElements = businessObject.get('extensionElements');
  }

  if (!extensionElements) {
    return;
  }

  return extensionElements.get('values').find((value) => {
    return ModelUtil.is(value, type);
  });
}

function findZeebeProperty(zeebeProperties, binding) {
  return zeebeProperties.get('properties').find((value) => {
    return value.name === binding.name;
  });
}

function findInputParameter(ioMapping, binding) {
  const parameters = ioMapping.get('inputParameters');

  return parameters.find((parameter) => {
    return parameter.target === binding.name;
  });
}

function findOutputParameter(ioMapping, binding) {
  const parameters = ioMapping.get('outputParameters');

  return parameters.find((parameter) => {
    return parameter.source === binding.source;
  });
}

function findTaskHeader(taskHeaders, binding) {
  const headers = taskHeaders.get('values');

  return headers.find((header) => {
    return header.key === binding.key;
  });
}

function findMessage(businessObject) {
  if (ModelUtil.is(businessObject, 'bpmn:Event')) {
    const eventDefinitions = businessObject.get('eventDefinitions');

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

    businessObject = eventDefinitions[0];
  }

  if (!businessObject) {
    return;
  }

  return businessObject.get('messageRef');
}

function getDefaultValue(property) {

  if (
    shouldCastToFeel(property) || property.feel === 'required'
  ) {
    return toFeelExpression(property.value, property.type);
  }

  if (property.value !== undefined) {
    return property.value;
  }

  if (property.generatedValue) {
    const { type } = property.generatedValue;

    if (type === 'uuid') {
      return uuid.v4();
    }
  }
}

/**
 * The BPMN 2.0 extension attribute name under
 * which the element template ID is stored.
 *
 * @type {String}
 */
const TEMPLATE_ID_ATTR = 'camunda:modelerTemplate';

/**
 * The BPMN 2.0 extension attribute name under
 * which the element template version is stored.
 *
 * @type {String}
 */
const TEMPLATE_VERSION_ATTR = 'camunda:modelerTemplateVersion';


/**
 * Get template id for a given diagram element.
 *
 * @param {djs.model.Base} element
 *
 * @return {String}
 */
function getTemplateId(element) {
  const businessObject = ModelUtil.getBusinessObject(element);

  if (businessObject) {
    return businessObject.get(TEMPLATE_ID_ATTR);
  }
}

/**
 * Get template version for a given diagram element.
 *
 * @param {djs.model.Base} element
 *
 * @return {String}
 */
function getTemplateVersion(element) {
  const businessObject = ModelUtil.getBusinessObject(element);

  if (businessObject) {
    return businessObject.get(TEMPLATE_VERSION_ATTR);
  }
}

/**
 * Find extension with given type in
 * BPMN element, diagram element or ExtensionElement.
 *
 * @param {ModdleElement|djs.model.Base} element
 * @param {String} type
 *
 * @return {ModdleElement} the extension
 */
function findExtension(element, type) {
  const businessObject = ModelUtil.getBusinessObject(element);

  let extensionElements;

  if (ModelUtil.is(businessObject, 'bpmn:ExtensionElements')) {
    extensionElements = businessObject;
  } else {
    extensionElements = businessObject.get('extensionElements');
  }

  if (!extensionElements) {
    return null;
  }

  return extensionElements.get('values').find((value) => {
    return ModelUtil.is(value, type);
  });
}

function findExtensions(element, types) {
  const extensionElements = getExtensionElements(element);

  if (!extensionElements) {
    return [];
  }

  return extensionElements.get('values').filter((value) => {
    return ModelUtil.isAny(value, types);
  });
}

function findCamundaErrorEventDefinition(element, errorRef) {
  const errorEventDefinitions = findExtensions(element, [ 'camunda:ErrorEventDefinition' ]);

  let error;

  // error ID has to start with <Error_${ errorRef }_>
  return errorEventDefinitions.find((definition) => {
    error = definition.get('bpmn:errorRef');

    if (error) {
      return error.get('bpmn:id').startsWith(`Error_${ errorRef }`);
    }
  });
}


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

function getExtensionElements(element) {
  const businessObject = ModelUtil.getBusinessObject(element);

  if (ModelUtil.is(businessObject, 'bpmn:ExtensionElements')) {
    return businessObject;
  } else {
    return businessObject.get('extensionElements');
  }
}

// eslint-disable-next-line no-undef
const packageVersion = "2.9.1";


/**
 * Registry for element templates.
 */
let ElementTemplates$1 = class ElementTemplates {
  constructor(commandStack, eventBus, modeling, injector, config) {
    this._commandStack = commandStack;
    this._eventBus = eventBus;
    this._injector = injector;
    this._modeling = modeling;

    this._templatesById = {};
    this._templates = [];

    config = config || {};

    this._engines = this._coerceEngines(config.engines || {});

    eventBus.on('elementTemplates.engines.changed', event => {
      this.set(this._templates);
    });
  }

  /**
   * Get template with given ID and optional version or for element.
   *
   * @param {String|djs.model.Base} id
   * @param {number} [version]
   *
   * @return {ElementTemplate}
   */
  get(id, version) {
    const templates = this._templatesById;

    let element;

    if (minDash.isUndefined(id)) {
      return null;
    } else if (minDash.isString(id)) {

      if (minDash.isUndefined(version)) {
        version = '_';
      }

      if (templates[ id ] && templates[ id ][ version ]) {
        return templates[ id ][ version ];
      } else {
        return null;
      }
    } else {
      element = id;

      return this.get(this._getTemplateId(element), this._getTemplateVersion(element));
    }
  }

  /**
   * Get default template for given element.
   *
   * @param {djs.model.Base} element
   *
   * @return {ElementTemplate}
   */
  getDefault(element) {
    return minDash.find(this.getAll(element), function(template) {
      return template.isDefault;
    }) || null;
  }

  /**
   * Get all templates (with given ID or applicable to element).
   *
   * @param {string|djs.model.Base} [id]
   * @return {Array<ElementTemplate>}
   */
  getAll(id) {
    return this._getTemplateVerions(id, { includeDeprecated: true });
  }


  /**
   * Get all templates (with given ID or applicable to element) with the latest
   * version.
   *
   * @param {String|djs.model.Base} [id]
   * @param {{ deprecated?: boolean }} [options]
   *
   * @return {Array<ElementTemplate>}
   */
  getLatest(id, options = {}) {
    return this._getTemplateVerions(id, {
      ...options,
      latest: true
    });
  }

  /**
   * Set templates.
   *
   * @param {Array<ElementTemplate>} templates
   */
  set(templates) {
    this._templatesById = {};
    this._templates = templates;

    templates.forEach((template) => {
      const id = template.id;
      const version = minDash.isUndefined(template.version) ? '_' : template.version;

      if (!this._templatesById[ id ]) {
        this._templatesById[ id ] = { };
      }

      this._templatesById[ id ][ version ] = template;

      const latest = this._templatesById[ id ].latest;

      if (this.isCompatible(template)) {
        if (!latest || minDash.isUndefined(latest.version) || latest.version < version) {
          this._templatesById[ id ].latest = template;
        }
      }
    });

    this._fire('changed');
  }

  getEngines() {
    return this._engines;
  }

  setEngines(engines) {

    this._engines = this._coerceEngines(engines);

    this._fire('engines.changed');
  }

  /**
   * Ensures that only valid engines are kept around
   *
   * @param { Record<string, string> } engines
   *
   * @return { Record<string, string> } filtered, valid engines
   */
  _coerceEngines(engines) {

    // we provide <elementTemplates> engine with the current
    // package version; templates may use that engine to declare
    // compatibility with this library
    engines = {
      elementTemplates: packageVersion,
      ...engines
    };

    return minDash.reduce(engines, (validEngines, version, engine) => {

      const coercedVersion = semver.coerce(version);

      if (!semver.valid(coercedVersion)) {
        console.error(
          new Error(`Engine <${ engine }> specifies unparseable version <${version}>`)
        );

        return validEngines;
      }

      return {
        ...validEngines,
        [ engine ]: coercedVersion.raw
      };
    }, {});
  }

  /**
   * Check if template is compatible with currently set engine version.
   *
   * @param {ElementTemplate} template
   *
   * @return {boolean} - true if compatible or no engine is set for elementTemplates or template.
   */
  isCompatible(template) {
    return !Object.keys(this.getIncompatibleEngines(template)).length;
  }

  /**
   * Get engines that are incompatible with the template.
   *
   * @param {any} template
   *
   * @return { Record<string, { required: string, found: string } } - incompatible engines along with their template and local versions
   */
  getIncompatibleEngines(template) {
    const localEngines = this._engines;
    const templateEngines = template.engines;

    return minDash.reduce(templateEngines, (result, _, engine) => {

      if (!minDash.has(localEngines, engine)) {
        return result;
      }

      if (!semver.satisfies(localEngines[engine], templateEngines[engine])) {
        result[engine] = {
          actual: localEngines[engine],
          required: templateEngines[engine]
        };
      }

      return result;
    }, {});
  }

  /**
   * @param {object|string|null} id
   * @param { { latest?: boolean, deprecated?: boolean } [options]
   *
   * @return {Array<ElementTemplate>}
   */
  _getTemplateVerions(id, options = {}) {

    const {
      latest: includeLatestOnly,
      deprecated: includeDeprecated
    } = options;

    const templatesById = this._templatesById;
    const getVersions = (template) => {
      const { latest, ...versions } = template;
      return includeLatestOnly ? (
        !includeDeprecated && (latest && latest.deprecated) ? [] : (latest ? [ latest ] : [])
      ) : minDash.values(versions) ;
    };

    if (minDash.isNil(id)) {
      return minDash.flatten(minDash.values(templatesById).map(getVersions));
    }

    if (minDash.isObject(id)) {
      const element = id;

      return minDash.filter(this._getTemplateVerions(null, options), function(template) {
        return ModelUtil.isAny(element, template.appliesTo);
      }) || [];
    }

    if (minDash.isString(id)) {
      return templatesById[ id ] && getVersions(templatesById[ id ]);
    }

    throw new Error('argument must be of type {string|djs.model.Base|undefined}');
  }

  _getTemplateId(element) {
    return getTemplateId(element);
  }

  _getTemplateVersion(element) {
    return getTemplateVersion(element);
  }

  /**
   * Apply element template to a given element.
   *
   * @param {djs.model.Base} element
   * @param {ElementTemplate} newTemplate
   *
   * @return {djs.model.Base} the updated element
   */
  applyTemplate(element, newTemplate) {

    let action = 'apply';
    let payload = { element, newTemplate };
    const oldTemplate = this.get(element);

    if (oldTemplate && !newTemplate) {
      action = 'unlink';
      payload = { element };
    }

    if (newTemplate && oldTemplate && (newTemplate.id === oldTemplate.id)) {
      action = 'update';
    }

    const context = {
      element,
      newTemplate,
      oldTemplate
    };

    this._commandStack.execute('propertiesPanel.camunda.changeTemplate', context);

    this._fire(action, payload);

    return context.element;
  }

  _fire(action, payload) {
    return this._eventBus.fire(`elementTemplates.${action}`, payload);
  }

  /**
   * Remove template from a given element.
   *
   * @param {djs.model.Base} element
   *
   * @return {djs.model.Base} the updated element
   */
  removeTemplate(element) {
    this._fire('remove', { element });

    const context = {
      element
    };

    this._commandStack.execute('propertiesPanel.removeTemplate', context);

    return context.newElement;

  }

  /**
   * Unlink template from a given element.
   *
   * @param {djs.model.Base} element
   *
   * @return {djs.model.Base} the updated element
   */
  unlinkTemplate(element) {
    return this.applyTemplate(element, null);
  }

};

ElementTemplates$1.$inject = [
  'commandStack',
  'eventBus',
  'modeling',
  'injector',
  'config.elementTemplates'
];

/**
 * Registry for element templates.
 */
class ElementTemplates extends ElementTemplates$1 {
  constructor(templateElementFactory, commandStack, eventBus, modeling, injector, config) {
    super(commandStack, eventBus, modeling, injector, config);

    this._templateElementFactory = templateElementFactory;
  }

  _getTemplateId(element) {
    return getTemplateId$1(element);
  }

  _getTemplateVersion(element) {
    return getTemplateVersion$1(element);
  }

  /**
   * Create an element based on an element template. This is, for example,
   * called from the create-append anything menu.
   *
   * @param {ElementTemplate} template
   * @returns {djs.model.Base}
   */
  createElement(template) {
    if (!template) {
      throw new Error('template is missing');
    }

    const element = this._templateElementFactory.create(template);

    return element;
  }

  /**
   * Apply element template to a given element.
   *
   * @param {djs.model.Base} element
   * @param {ElementTemplate} newTemplate
   *
   * @return {djs.model.Base} the updated element
   */
  applyTemplate(element, newTemplate) {

    let action = 'apply';
    let payload = { element, newTemplate };

    const oldTemplate = this.get(element);

    if (oldTemplate && !newTemplate) {
      action = 'unlink';
      payload = { element };
    }

    if (newTemplate && oldTemplate && (newTemplate.id === oldTemplate.id)) {
      action = 'update';
    }

    const context = {
      element,
      newTemplate,
      oldTemplate
    };

    this._commandStack.execute('propertiesPanel.zeebe.changeTemplate', context);

    this._eventBus.fire(`elementTemplates.${action}`, payload);

    return context.element;
  }

}

ElementTemplates.$inject = [
  'templateElementFactory',
  'commandStack',
  'eventBus',
  'modeling',
  'injector',
  'config.elementTemplates',
];

const SUPPORTED_SCHEMA_VERSION$1 = elementTemplatesValidator.getSchemaVersion();
const MORPHABLE_TYPES = [ 'bpmn:Activity', 'bpmn:Event', 'bpmn:Gateway' ];


/**
 * A element template validator.
 */
let Validator$1 = class Validator {
  constructor(moddle) {
    this._templatesById = {};

    this._validTemplates = [];
    this._errors = [];
    this._moddle = moddle;
  }

  /**
   * Adds the templates.
   *
   * @param {Array<TemplateDescriptor>} templates
   *
   * @return {Validator}
   */
  addAll(templates) {
    if (!minDash.isArray(templates)) {
      this._logError('templates must be []');
    } else {
      templates.forEach(this.add, this);
    }

    return this;
  }

  /**
   * Add the given element template, if it is valid.
   *
   * @param {TemplateDescriptor} template
   *
   * @return {Validator}
   */
  add(template) {
    const err = this._validateTemplate(template);

    let id, version;

    if (!err) {
      id = template.id;
      version = template.version || '_';

      if (!this._templatesById[ id ]) {
        this._templatesById[ id ] = {};
      }

      this._templatesById[ id ][ version ] = template;

      this._validTemplates.push(template);
    }

    return this;
  }

  /**
   * Validate given template and return error (if any).
   *
   * @param {TemplateDescriptor} template
   *
   * @return {Error} validation error, if any
   */
  _validateTemplate(template) {
    const id = template.id,
          version = template.version || '_',
          schemaVersion = template.$schema && getSchemaVersion(template.$schema);

    // (1) compatibility
    if (schemaVersion && (semverCompare(SUPPORTED_SCHEMA_VERSION$1, schemaVersion) < 0)) {
      return this._logError(
        `unsupported element template schema version <${ schemaVersion }>. Your installation only supports up to version <${ SUPPORTED_SCHEMA_VERSION$1 }>. Please update your installation`,
        template
      );
    }

    // (2) versioning
    if (this._templatesById[ id ] && this._templatesById[ id ][ version ]) {
      if (version === '_') {
        return this._logError(`template id <${ id }> already used`, template);
      } else {
        return this._logError(`template id <${ id }> and version <${ version }> already used`, template);
      }
    }

    // (3) elementType validation
    const elementTypeError = this._validateElementType(template);

    if (elementTypeError) {
      return elementTypeError;
    }

    // (4) JSON schema compliance
    const schemaValidationResult = elementTemplatesValidator.validate(template);

    const {
      errors: schemaErrors,
      valid
    } = schemaValidationResult;

    if (!valid) {
      filteredSchemaErrors(schemaErrors).forEach((error) => {
        this._logError(error.message, template);
      });

      return new Error('invalid template');
    }

    // (5) engines validation
    const enginesError = this._validateEngines(template);

    if (enginesError) {
      return enginesError;
    }

    return null;
  }

  _validateEngines(template) {

    let err;

    minDash.forEach(template.engines, (rangeStr, engine) => {

      if (!semver.validRange(rangeStr)) {
        err = this._logError(new Error(
          `Engine <${engine}> specifies invalid semver range <${rangeStr}>`
        ), template);
      }
    });

    return err;
  }

  /**
   * Validate elementType for given template and return error (if any).
   *
   * @param {TemplateDescriptor} template
   *
   * @return {Error} validation error, if any
   */
  _validateElementType(template) {
    if (template.elementType && template.appliesTo) {

      const elementType = template.elementType.value,
            appliesTo = template.appliesTo;

      // (3.1) template can be applied to elementType
      // prevents cases where the elementType is not part of appliesTo
      if (!appliesTo.find(type => this._isType(elementType, type))) {
        return this._logError(`template does not apply to requested element type <${ elementType }>`, template);
      }

      // (3.2) template only applies to same type of element
      // prevent elementTemplates to morph into incompatible types, e.g. Task -> SequenceFlow
      for (const sourceType of appliesTo) {
        if (!this._canMorph(sourceType, elementType)) {
          return this._logError(`can not morph <${sourceType}> into <${elementType}>`, template);
        }
      }
    }
  }


  /**
   * Check if given type is a subtype of given base type.
   *
   * @param {String} type
   * @param {String} baseType
   * @returns {Boolean}
   */
  _isType(type, baseType) {
    const moddleType = this._moddle.getType(type);

    return moddleType && (baseType in this._moddle.getElementDescriptor(moddleType).allTypesByName);
  }


  /**
   * Checks if a given type can be morphed into another type.
   *
   * @param {String} sourceType
   * @param {String} targetType
   * @returns {Boolean}
   */
  _canMorph(sourceType, targetType) {

    if (sourceType === targetType) {
      return true;
    }

    const baseType = MORPHABLE_TYPES.find(type => this._isType(sourceType, type));

    if (!baseType) {
      return false;
    }

    return this._isType(targetType, baseType);
  }

  /**
   * Log an error for the given template
   *
   * @param {(String|Error)} err
   * @param {TemplateDescriptor} template
   *
   * @return {Error} logged validation errors
   */
  _logError(err, template) {
    if (minDash.isString(err)) {

      if (template) {
        const {
          id,
          name
        } = template;

        err = `template(id: <${ id }>, name: <${ name }>): ${ err }`;
      }

      err = new Error(err);
    }

    this._errors.push(err);

    return err;
  }

  getErrors() {
    return this._errors;
  }

  getValidTemplates() {
    return this._validTemplates;
  }
};


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

/**
 * Extract schema version from schema URI
 *
 * @param {String} schemaUri - for example https://unpkg.com/@camunda/element-templates-json-schema@99.99.99/resources/schema.json
 *
 * @return {String} for example '99.99.99'
 */
function getSchemaVersion(schemaUri) {
  const re = /\d+\.\d+\.\d+/g;

  const match = schemaUri.match(re);

  return match === null ? undefined : match[ 0 ];
}

/**
 * Extract only relevant errors of the validation result.
 *
 * The JSON Schema we use under the hood produces more errors than we need for a
 * detected schema violation (for example, unmatched sub-schemas, if-then-rules,
 * `oneOf`-definitions ...).
 *
 * We call these errors "relevant" that have a custom error message defined by us OR
 * are basic data type errors.
 *
 * @param {Array} schemaErrors
 *
 * @return {Array}
 */
function filteredSchemaErrors(schemaErrors) {
  return minDash.filter(schemaErrors, (err) => {
    const {
      instancePath,
      keyword
    } = err;

    // (1) regular errors are customized from the schema
    if (keyword === 'errorMessage') {
      return true;
    }

    // (2) data type errors
    // ignore type errors nested in scopes
    if (keyword === 'type' && instancePath && !instancePath.startsWith('/scopes/')) {
      return true;
    }

    return false;
  });
}

const SUPPORTED_SCHEMA_VERSION = elementTemplatesValidator.getZeebeSchemaVersion();
const SUPPORTED_SCHEMA_PACKAGE = elementTemplatesValidator.getZeebeSchemaPackage();

/**
 * A Camunda Cloud element template validator.
 */
class Validator extends Validator$1 {
  constructor(moddle) {
    super(moddle);
  }

  /**
   * Validate given template and return error (if any).
   *
   * @param {TemplateDescriptor} template
   *
   * @return {Error} validation error, if any
   */
  _validateTemplate(template) {
    const id = template.id,
          version = template.version || '_',
          schema = template.$schema,
          schemaVersion = schema && getSchemaVersion(schema);

    // (1) $schema attribute defined
    if (!schema) {
      return this._logError(
        'missing $schema attribute.',
        template
      );
    }

    if (!this.isSchemaValid(schema)) {
      return this._logError(
        `unsupported $schema attribute <${ schema }>.`,
        template
      );
    }

    // (2) compatibility
    if (schemaVersion && (semverCompare(SUPPORTED_SCHEMA_VERSION, schemaVersion) < 0)) {
      return this._logError(
        `unsupported element template schema version <${ schemaVersion }>. Your installation only supports up to version <${ SUPPORTED_SCHEMA_VERSION }>. Please update your installation`,
        template
      );
    }

    // (3) versioning
    if (this._templatesById[ id ] && this._templatesById[ id ][ version ]) {
      if (version === '_') {
        return this._logError(`template id <${ id }> already used`, template);
      } else {
        return this._logError(`template id <${ id }> and version <${ version }> already used`, template);
      }
    }

    // (4) elementType validation
    const elementTypeError = this._validateElementType(template);

    if (elementTypeError) {
      return elementTypeError;
    }

    // (5) JSON schema compliance
    const schemaValidationResult = elementTemplatesValidator.validateZeebe(template);

    const {
      errors: schemaErrors,
      valid
    } = schemaValidationResult;

    if (!valid) {
      filteredSchemaErrors(schemaErrors).forEach((error) => {
        this._logError(error.message, template);
      });

      return new Error('invalid template');
    }

    // (6) engines validation
    const enginesError = this._validateEngines(template);

    if (enginesError) {
      return enginesError;
    }

    return null;
  }

  isSchemaValid(schema) {
    return schema && schema.includes(SUPPORTED_SCHEMA_PACKAGE);
  }

  _validateEngines(template) {

    let err;

    minDash.forEach(template.engines, (rangeStr, engine) => {

      if (!semver.validRange(rangeStr)) {
        err = this._logError(new Error(
          `Engine <${engine}> specifies invalid semver range <${rangeStr}>`
        ), template);
      }
    });

    return err;
  }
}

/**
 * The guy responsible for template loading.
 *
 * Provide the actual templates via the `config.elementTemplates`.
 *
 * That configuration can either be an array of template
 * descriptors or a node style callback to retrieve
 * the templates asynchronously.
 *
 * @param {Array<TemplateDescriptor>|Function} config
 * @param {EventBus} eventBus
 * @param {ElementTemplates} elementTemplates
 * @param {Moddle} moddle
 */
let ElementTemplatesLoader$1 = class ElementTemplatesLoader {
  constructor(config, eventBus, elementTemplates, moddle) {
    this._loadTemplates;
    this._eventBus = eventBus;
    this._elementTemplates = elementTemplates;
    this._moddle = moddle;

    if (minDash.isArray(config) || minDash.isFunction(config)) {
      this._loadTemplates = config;
    }

    if (config && config.loadTemplates) {
      this._loadTemplates = config.loadTemplates;
    }

    eventBus.on('diagram.init', () => {
      this.reload();
    });
  }

  reload() {
    const loadTemplates = this._loadTemplates;

    // no templates specified
    if (minDash.isUndefined(loadTemplates)) {
      return;
    }

    // template loader function specified
    if (minDash.isFunction(loadTemplates)) {

      return loadTemplates((err, templates) => {

        if (err) {
          return this._templateErrors([ err ]);
        }

        this.setTemplates(templates);
      });
    }

    // templates array specified
    if (loadTemplates.length) {
      return this.setTemplates(loadTemplates);
    }
  }

  setTemplates(templates) {
    const elementTemplates = this._elementTemplates,
          moddle = this._moddle;

    const validator = new Validator$1(moddle).addAll(templates);

    const errors = validator.getErrors(),
          validTemplates = validator.getValidTemplates();

    elementTemplates.set(validTemplates);

    if (errors.length) {
      this._templateErrors(errors);
    }
  }

  _templateErrors(errors) {
    this._elementTemplates._fire('errors', {
      errors: errors
    });
  }
};

ElementTemplatesLoader$1.$inject = [
  'config.elementTemplates',
  'eventBus',
  'elementTemplates',
  'moddle'
];

/**
 * @param {Object|Array<TemplateDescriptor>|Function} config
 * @param {EventBus} eventBus
 * @param {ElementTemplates} elementTemplates
 * @param {Moddle} moddle
 */
class ElementTemplatesLoader extends ElementTemplatesLoader$1 {
  constructor(config, eventBus, elementTemplates, moddle) {

    super(config, eventBus, elementTemplates, moddle);

    this._elementTemplates = elementTemplates;
  }

  setTemplates(templates) {
    const elementTemplates = this._elementTemplates,
          moddle = this._moddle;

    const validator = new Validator(moddle).addAll(templates);

    const errors = validator.getErrors(),
          validTemplates = validator.getValidTemplates();

    elementTemplates.set(validTemplates);

    if (errors.length) {
      this._templateErrors(errors);
    }
  }
}

ElementTemplatesLoader.$inject = [
  'config.elementTemplates',
  'eventBus',
  'elementTemplates',
  'moddle'
];

/**
 * Create a new element and set its parent.
 *
 * @param {String} elementType of the new element
 * @param {Object} properties of the new element in key-value pairs
 * @param {moddle.object} parent of the new element
 * @param {BpmnFactory} factory which creates the new element
 *
 * @returns {djs.model.Base} element which is created
 */
function createElement(elementType, properties, parent, factory) {
  const element = factory.create(elementType, properties);

  if (parent) {
    element.$parent = parent;
  }

  return element;
}

/**
 * generate a semantic id with given prefix
 */
function nextId(prefix) {
  const ids = new Ids([ 32,32,1 ]);

  return ids.nextPrefixed(prefix);
}

function getRoot(businessObject) {
  let parent = businessObject;

  while (parent.$parent) {
    parent = parent.$parent;
  }

  return parent;
}

/**
 * Create an input parameter representing the given
 * binding and value.
 *
 * @param {PropertyBinding} binding
 * @param {String} value
 * @param {BpmnFactory} bpmnFactory
 *
 * @return {ModdleElement}
 */
function createInputParameter$1(binding, value, bpmnFactory) {
  const {
    name
  } = binding;

  return bpmnFactory.create('zeebe:Input', {
    source: value,
    target: name
  });
}

/**
 * Create an output parameter representing the given
 * binding and value.
 *
 * @param {PropertyBinding} binding
 * @param {String} value
 * @param {BpmnFactory} bpmnFactory
 *
 * @return {ModdleElement}
 */
function createOutputParameter$1(binding, value, bpmnFactory) {
  const {
    source
  } = binding;

  return bpmnFactory.create('zeebe:Output', {
    source,
    target: value
  });
}

/**
 * Create a task header representing the given
 * binding and value.
 *
 * @param {PropertyBinding} binding
 * @param {String} value
 * @param {BpmnFactory} bpmnFactory
 *
 * @return {ModdleElement}
 */
function createTaskHeader(binding, value, bpmnFactory) {
  const {
    key
  } = binding;

  return bpmnFactory.create('zeebe:Header', {
    key,
    value
  });
}

/**
 * Create a task definition representing the given value.
 *
 * @param {object} attrs
 * @param {BpmnFactory} bpmnFactory
 *
 * @return {ModdleElement}
 */
function createTaskDefinition(attrs = {}, bpmnFactory) {
  return bpmnFactory.create('zeebe:TaskDefinition', attrs);
}

/**
 * Create zeebe:Property from the given binding.
 *
 * @param {PropertyBinding} binding
 * @param {String} value
 * @param {BpmnFactory} bpmnFactory
 *
 * @return {ModdleElement}
 */
function createZeebeProperty(binding, value = '', bpmnFactory) {
  const { name } = binding;

  return bpmnFactory.create('zeebe:Property', {
    name,
    value
  });
}

/**
 * Retrieves whether an element should be updated for a given property.
 *
 * That matches once
 * a) the property value is not empty, or
 * b) the property is not optional
 *
 * @param {String} value
 * @param {Object} property
 * @returns {Boolean}
 */
function shouldUpdate(value, property) {
  const { optional } = property;

  return value || !optional;
}

/**
 * Gets or, in case not existent, creates extension element for given element.
 *
 * @param {djs.model.Base} element
 * @param {String} type
 * @param {BpmnFactory} bpmnFactory
 * @returns {ModdleElement}
 */
function ensureExtension(element, type, bpmnFactory) {
  const businessObject = ModelUtil.getBusinessObject(element);
  let extensionElements = businessObject.get('extensionElements');

  if (!extensionElements) {
    extensionElements = createElement('bpmn:ExtensionElements', {}, businessObject, bpmnFactory);
    businessObject.set('extensionElements', extensionElements);
  }

  let extension = findExtension$1(extensionElements, type);

  if (!extension) {
    extension = bpmnFactory.create(type);
    extension.$parent = extensionElements;
    extensionElements.get('values').push(extension);
  }

  return extension;
}

const PROPERTY_TYPE = 'property';

const ZEBBE_PROPERTY_TYPE = 'zeebe:property';
const ZEBBE_INPUT_TYPE = 'zeebe:input';
const ZEEBE_OUTPUT_TYPE = 'zeebe:output';
const ZEEBE_PROPERTY_TYPE = 'zeebe:property';
const ZEEBE_TASK_DEFINITION_TYPE_TYPE = 'zeebe:taskDefinition:type';
const ZEEBE_TASK_DEFINITION = 'zeebe:taskDefinition';
const ZEEBE_TASK_HEADER_TYPE = 'zeebe:taskHeader';
const MESSAGE_PROPERTY_TYPE = 'bpmn:Message#property';
const MESSAGE_ZEEBE_SUBSCRIPTION_PROPERTY_TYPE = 'bpmn:Message#zeebe:subscription#property';
const ZEEBE_CALLED_ELEMENT = 'zeebe:calledElement';
const ZEEBE_LINKED_RESOURCE_PROPERTY = 'zeebe:linkedResource';
const ZEEBE_USER_TASK = 'zeebe:userTask';
const ZEEBE_CALLED_DECISION = 'zeebe:calledDecision';
const ZEEBE_FORM_DEFINITION = 'zeebe:formDefinition';
const ZEEBE_SCRIPT_TASK = 'zeebe:script';
const ZEEBE_ASSIGNMENT_DEFINITION = 'zeebe:assignmentDefinition';

const EXTENSION_BINDING_TYPES = [
  MESSAGE_ZEEBE_SUBSCRIPTION_PROPERTY_TYPE,
  ZEBBE_INPUT_TYPE,
  ZEEBE_OUTPUT_TYPE,
  ZEEBE_PROPERTY_TYPE,
  ZEEBE_TASK_DEFINITION_TYPE_TYPE,
  ZEEBE_TASK_DEFINITION,
  ZEEBE_TASK_HEADER_TYPE,
  ZEEBE_CALLED_ELEMENT,
  ZEEBE_LINKED_RESOURCE_PROPERTY,
  ZEEBE_CALLED_DECISION,
  ZEEBE_FORM_DEFINITION,
  ZEEBE_SCRIPT_TASK,
  ZEEBE_ASSIGNMENT_DEFINITION
];

const TASK_DEFINITION_TYPES = [
  ZEEBE_TASK_DEFINITION_TYPE_TYPE,
  ZEEBE_TASK_DEFINITION
];

const IO_BINDING_TYPES = [
  ZEBBE_INPUT_TYPE,
  ZEEBE_OUTPUT_TYPE
];

const MESSAGE_BINDING_TYPES = [
  MESSAGE_PROPERTY_TYPE,
  MESSAGE_ZEEBE_SUBSCRIPTION_PROPERTY_TYPE
];

const PROPERTY_BINDING_TYPES = [
  PROPERTY_TYPE,
  MESSAGE_PROPERTY_TYPE
];

function getTaskDefinitionPropertyName(binding) {
  return binding.type === ZEEBE_TASK_DEFINITION_TYPE_TYPE ? 'type' : binding.property;
}

function removeRootElement(rootElement, injector) {
  const modeling = injector.get('modeling'),
        canvas = injector.get('canvas'),
        bpmnjs = injector.get('bpmnjs');

  const element = canvas.getRootElement(),
        definitions = bpmnjs.getDefinitions(),
        rootElements = definitions.get('rootElements');

  const newRootElements = rootElements.filter(e => e !== rootElement);

  // short-circuit to prevent unnecessary updates
  if (newRootElements.length === rootElements.length) {
    return;
  }

  modeling.updateModdleProperties(element, definitions, {
    rootElements: newRootElements
  });
}

/**
 * Remove message from element and the diagram.
 *
 * @param {import('bpmn-js/lib/model/Types').Element} element
 * @param {import('didi').Injector} injector
 */
function removeMessage(element, injector) {
  const modeling = injector.get('modeling');

  const bo = getReferringElement(element);

  // Event does not have an event definition
  if (!bo) {
    return;
  }

  const message = findMessage(bo);

  if (!message) {
    return;
  }

  modeling.updateModdleProperties(element, bo, {
    messageRef: undefined
  });
  removeRootElement(message, injector);
}


function getReferringElement(element) {
  const bo = ModelUtil.getBusinessObject(element);

  if (ModelUtil.is(bo, 'bpmn:Event')) {
    return bo.get('eventDefinitions')[0];
  }

  return bo;
}

const EXPRESSION_TYPES$1 = [
  'bpmn:Expression',
  'bpmn:FormalExpression'
];

function isExpression(element, propertyName) {
  const bo = ModelUtil.getBusinessObject(element);

  const descriptor = bo.$descriptor.propertiesByName[propertyName];

  return descriptor && EXPRESSION_TYPES$1.includes(descriptor.type);
}

function createExpression(value, parent, bpmnFactory) {
  const expression = createElement('bpmn:FormalExpression', { body: value }, parent, bpmnFactory);

  return expression;
}

function getExpressionValue(element, propertyName) {
  const bo = ModelUtil.getBusinessObject(element);
  const expression = bo.get(propertyName);

  return expression?.get('body') || undefined;
}

/**
 * Applies an element template to an element. Sets `zeebe:modelerTemplate` and
 * `zeebe:modelerTemplateVersion`.
 */
let ChangeElementTemplateHandler$1 = class ChangeElementTemplateHandler {
  constructor(bpmnFactory, bpmnReplace, commandStack, modeling, moddleCopy, injector) {
    this._bpmnFactory = bpmnFactory;
    this._bpmnReplace = bpmnReplace;

    this._modeling = modeling;
    this._moddleCopy = moddleCopy;
    this._commandStack = commandStack;

    this._injector = injector;
  }

  /**
   * Change an element's template and update its properties as specified in `newTemplate`. Specify
   * `oldTemplate` to update from one template to another. If `newTemplate` isn't specified the
   * `zeebe:modelerTemplate` and `zeebe:modelerTemplateVersion` properties will be removed from
   * the element.
   *
   * @param {Object} context
   * @param {Object} context.element
   * @param {Object} [context.oldTemplate]
   * @param {Object} [context.newTemplate]
   */
  preExecute(context) {
    let newTemplate = context.newTemplate,
        oldTemplate = context.oldTemplate;

    let element = context.element;

    // update zeebe:modelerTemplate attribute
    this._updateZeebeModelerTemplate(element, newTemplate);

    // update zeebe:modelerTemplateIcon
    this._updateZeebeModelerTemplateIcon(element, newTemplate);

    if (newTemplate) {

      // update element type
      element = context.element = this._updateElementType(element, oldTemplate, newTemplate);

      // update properties
      this._updateProperties(element, oldTemplate, newTemplate);

      // update zeebe:TaskDefinition
      this._updateZeebeTaskDefinition(element, oldTemplate, newTemplate);

      // update zeebe:Input and zeebe:Output properties
      this._updateZeebeInputOutputParameterProperties(element, oldTemplate, newTemplate);

      // update zeebe:Header properties
      this._updateZeebeTaskHeaderProperties(element, oldTemplate, newTemplate);

      // update zeebe:Property properties
      this._updateZeebePropertyProperties(element, oldTemplate, newTemplate);

      this._updateMessage(element, oldTemplate, newTemplate);

      this._updateCalledElement(element, oldTemplate, newTemplate);

      this._updateLinkedResources(element, oldTemplate, newTemplate);

      this._updateZeebeUserTask(element, newTemplate);

      this._updateCalledDecision(element, oldTemplate, newTemplate);

      this._updateZeebeFormDefinition(element, oldTemplate, newTemplate);

      this._updateScriptTask(element, oldTemplate, newTemplate);

      this._updateZeebeAssignmentDefinition(element, oldTemplate, newTemplate);
    }
  }

  _getOrCreateExtensionElements(element, businessObject = ModelUtil.getBusinessObject(element)) {
    const bpmnFactory = this._bpmnFactory,
          modeling = this._modeling;

    let extensionElements = businessObject.get('extensionElements');

    if (!extensionElements) {
      extensionElements = bpmnFactory.create('bpmn:ExtensionElements', {
        values: []
      });

      extensionElements.$parent = businessObject;

      modeling.updateModdleProperties(element, businessObject, {
        extensionElements: extensionElements
      });
    }

    return extensionElements;
  }

  _updateZeebeModelerTemplate(element, newTemplate) {
    const modeling = this._modeling;

    const newId = newTemplate && newTemplate.id;
    const newVersion = newTemplate && newTemplate.version;

    if (getTemplateId$1(element) !== newId || getTemplateVersion$1(element) !== newVersion) {
      modeling.updateProperties(element, {
        'zeebe:modelerTemplate': newId,
        'zeebe:modelerTemplateVersion': newVersion
      });
    }
  }

  _updateZeebeModelerTemplateIcon(element, newTemplate) {
    const modeling = this._modeling;

    const newIcon = newTemplate && newTemplate.icon;
    const newIconContents = newIcon && newIcon.contents;

    if (getTemplateIcon(element) !== newIconContents) {
      modeling.updateProperties(element, {
        'zeebe:modelerTemplateIcon': newIconContents
      });
    }
  }

  _updateProperties(element, oldTemplate, newTemplate) {
    const commandStack = this._commandStack;
    const businessObject = ModelUtil.getBusinessObject(element);

    const newProperties = newTemplate.properties.filter((newProperty) => {
      const newBinding = newProperty.binding,
            newBindingType = newBinding.type;

      return newBindingType === 'property';
    });

    // Remove old Properties if no new Properties specified
    const propertiesToRemove = oldTemplate && oldTemplate.properties.filter((oldProperty) => {
      const oldBinding = oldProperty.binding,
            oldBindingType = oldBinding.type;

      return oldBindingType === 'property' && !newProperties.find((newProperty) => newProperty.binding.name === oldProperty.binding.name);
    }) || [];

    if (propertiesToRemove.length) {
      const payload = propertiesToRemove.reduce((properties, property) => {
        properties[property.binding.name] = undefined;
        return properties;
      }, {});

      commandStack.execute('element.updateModdleProperties', {
        element,
        moddleElement: businessObject,
        properties: payload
      });
    }

    if (!newProperties.length) {
      return;
    }

    newProperties.forEach((newProperty) => {
      const oldProperty = findOldProperty$1(oldTemplate, newProperty),
            newBinding = newProperty.binding,
            newBindingName = newBinding.name,
            newPropertyValue = getDefaultValue(newProperty),
            changedElement = businessObject;

      let properties = {};

      if (shouldKeepValue(changedElement, oldProperty, newProperty)) {
        return;
      }

      let assignedValue = newPropertyValue;
      if (isExpression(businessObject, newBindingName)) {
        assignedValue = createExpression(newPropertyValue, businessObject, this._bpmnFactory);
      }

      properties[ newBindingName ] = assignedValue;

      commandStack.execute('element.updateModdleProperties', {
        element,
        moddleElement: businessObject,
        properties
      });
    });
  }

  /**
   * Update `zeebe:TaskDefinition` properties of specified business object. This
   * can only exist in `bpmn:ExtensionElements`.
   *
   * @param {djs.model.Base} element
   * @param {Object} oldTemplate
   * @param {Object} newTemplate
   */
  _updateZeebeTaskDefinition(element, oldTemplate, newTemplate) {
    this._updateSingleExtensionElement(
      element,
      oldTemplate,
      newTemplate,
      {
        bindingTypes: TASK_DEFINITION_TYPES,
        extensionType: 'zeebe:TaskDefinition',
        getPropertyName: getTaskDefinitionPropertyName
      }
    );
  }

  /**
   * Update `zeebe:Input` and `zeebe:Output` properties of specified business
   * object. Both can only exist in `zeebe:ioMapping` which can exist in `bpmn:ExtensionElements`.
   *
   * @param {djs.model.Base} element
   * @param {Object} oldTemplate
   * @param {Object} newTemplate
   */
  _updateZeebeInputOutputParameterProperties(element, oldTemplate, newTemplate) {
    const bpmnFactory = this._bpmnFactory,
          commandStack = this._commandStack;

    const newProperties = newTemplate.properties.filter((newProperty) => {
      const newBinding = newProperty.binding,
            newBindingType = newBinding.type;

      return newBindingType === 'zeebe:input' || newBindingType === 'zeebe:output';
    });

    const businessObject = this._getOrCreateExtensionElements(element);

    let ioMapping = findExtension$1(businessObject, 'zeebe:IoMapping');

    // (1) remove old mappings if no new specified
    if (!newProperties.length) {
      if (!ioMapping) {
        return;
      }

      commandStack.execute('element.updateModdleProperties', {
        element,
        moddleElement: businessObject,
        properties: {
          values: minDash.without(businessObject.get('values'), ioMapping)
        }
      });
    }

    if (!ioMapping) {
      ioMapping = bpmnFactory.create('zeebe:IoMapping');

      ioMapping.$parent = businessObject;

      commandStack.execute('element.updateModdleProperties', {
        element,
        moddleElement: businessObject,
        properties: {
          values: [ ...businessObject.get('values'), ioMapping ]
        }
      });
    }

    const oldInputs = ioMapping.get('zeebe:inputParameters')
      ? ioMapping.get('zeebe:inputParameters').slice()
      : [];

    const oldOutputs = ioMapping.get('zeebe:outputParameters')
      ? ioMapping.get('zeebe:outputParameters').slice()
      : [];

    let propertyName;

    newProperties.forEach((newProperty) => {
      const oldProperty = findOldProperty$1(oldTemplate, newProperty),
            inputOrOutput = findBusinessObject(businessObject, newProperty),
            newPropertyValue = getDefaultValue(newProperty),
            newBinding = newProperty.binding,
            newBindingType = newBinding.type;

      let newInputOrOutput,
          properties;

      // (2) update old inputs and outputs
      if (inputOrOutput) {

        // (2a) exclude old inputs and outputs from cleanup, unless
        // a) optional and has empty value, and
        // b) not changed
        if (
          shouldUpdate(newPropertyValue, newProperty) ||
          shouldKeepValue(inputOrOutput, oldProperty, newProperty)
        ) {
          if (ModelUtil.is(inputOrOutput, 'zeebe:Input')) {
            remove$1(oldInputs, inputOrOutput);
          } else {
            remove$1(oldOutputs, inputOrOutput);
          }
        }

        // (2a) do updates (unless changed)
        if (!shouldKeepValue(inputOrOutput, oldProperty, newProperty)) {

          if (ModelUtil.is(inputOrOutput, 'zeebe:Input')) {
            properties = {
              source: newPropertyValue
            };
          } else {
            properties = {
              target: newPropertyValue
            };
          }

          commandStack.execute('element.updateModdleProperties', {
            element,
            moddleElement: inputOrOutput,
            properties
          });
        }
      }

      // (3) add new inputs and outputs (unless optional)
      else if (shouldUpdate(newPropertyValue, newProperty)) {

        if (newBindingType === 'zeebe:input') {
          propertyName = 'inputParameters';

          newInputOrOutput = createInputParameter$1(newBinding, newPropertyValue, bpmnFactory);
        } else {
          propertyName = 'outputParameters';

          newInputOrOutput = createOutputParameter$1(newBinding, newPropertyValue, bpmnFactory);
        }

        newInputOrOutput.$parent = ioMapping;

        commandStack.execute('element.updateModdleProperties', {
          element,
          moddleElement: ioMapping,
          properties: {
            [ propertyName ]: [ ...ioMapping.get(propertyName), newInputOrOutput ]
          }
        });
      }
    });

    // (4) remove old inputs and outputs
    if (oldInputs.length) {
      commandStack.execute('element.updateModdleProperties', {
        element,
        moddleElement: ioMapping,
        properties: {
          inputParameters: minDash.without(ioMapping.get('inputParameters'), inputParameter => oldInputs.includes(inputParameter))
        }
      });
    }

    if (oldOutputs.length) {
      commandStack.execute('element.updateModdleProperties', {
        element,
        moddleElement: ioMapping,
        properties: {
          outputParameters: minDash.without(ioMapping.get('outputParameters'), outputParameter => oldOutputs.includes(outputParameter))
        }
      });
    }
  }

  /**
   * Update `zeebe:Header` properties of specified business
   * object. Those can only exist in `zeebe:taskHeaders` which can exist in `bpmn:ExtensionElements`.
   *
   * @param {djs.model.Base} element
   * @param {Object} oldTemplate
   * @param {Object} newTemplate
   */
  _updateZeebeTaskHeaderProperties(element, oldTemplate, newTemplate) {
    const bpmnFactory = this._bpmnFactory,
          commandStack = this._commandStack;

    const newProperties = newTemplate.properties.filter((newProperty) => {
      const newBinding = newProperty.binding,
            newBindingType = newBinding.type;

      return newBindingType === 'zeebe:taskHeader';
    });

    const businessObject = this._getOrCreateExtensionElements(element);

    let taskHeaders = findExtension$1(businessObject, 'zeebe:TaskHeaders');

    // (1) remove old headers if no new specified
    if (!newProperties.length) {
      if (!taskHeaders) {
        return;
      }

      commandStack.execute('element.updateModdleProperties', {
        element,
        moddleElement: businessObject,
        properties: {
          values: minDash.without(businessObject.get('values'), taskHeaders)
        }
      });
    }

    if (!taskHeaders) {
      taskHeaders = bpmnFactory.create('zeebe:TaskHeaders');

      taskHeaders.$parent = businessObject;

      commandStack.execute('element.updateModdleProperties', {
        element,
        moddleElement: businessObject,
        properties: {
          values: [ ...businessObject.get('values'), taskHeaders ]
        }
      });
    }

    const oldHeaders = taskHeaders.get('zeebe:values')
      ? taskHeaders.get('zeebe:values').slice()
      : [];

    newProperties.forEach((newProperty) => {
      const oldProperty = findOldProperty$1(oldTemplate, newProperty),
            oldHeader = findBusinessObject(businessObject, newProperty),
            newPropertyValue = getDefaultValue(newProperty),
            newBinding = newProperty.binding;

      // (2) update old headers
      if (oldHeader) {

        if (!shouldKeepValue(oldHeader, oldProperty, newProperty)) {
          const properties = {
            value: newPropertyValue
          };

          commandStack.execute('element.updateModdleProperties', {
            element,
            moddleElement: oldHeader,
            properties
          });
        }

        remove$1(oldHeaders, oldHeader);
      }

      // (3) add new (non-empty) headers
      else if (newPropertyValue) {
        const newHeader = createTaskHeader(newBinding, newPropertyValue, bpmnFactory);

        newHeader.$parent = taskHeaders;

        commandStack.execute('element.updateModdleProperties', {
          element,
          moddleElement: taskHeaders,
          properties: {
            values: [ ...taskHeaders.get('values'), newHeader ]
          }
        });
      }
    });

    // (4) remove old headers
    if (oldHeaders.length) {
      commandStack.execute('element.updateModdleProperties', {
        element,
        moddleElement: taskHeaders,
        properties: {
          values: minDash.without(taskHeaders.get('values'), header => oldHeaders.includes(header))
        }
      });
    }
  }

  /**
   * Update zeebe:Property properties of zeebe:Properties extension element.
   *
   * @param {djs.model.Base} element
   * @param {Object} oldTemplate
   * @param {Object} newTemplate
   */
  _updateZeebePropertyProperties(element, oldTemplate, newTemplate) {
    const bpmnFactory = this._bpmnFactory,
          commandStack = this._commandStack;

    const newProperties = newTemplate.properties.filter((newProperty) => {
      const newBinding = newProperty.binding,
            newBindingType = newBinding.type;

      return newBindingType === 'zeebe:property';
    });

    const businessObject = this._getOrCreateExtensionElements(element);

    let zeebeProperties = findExtension$1(businessObject, 'zeebe:Properties');

    // (1) remove old zeebe:Properties if no new zeebe:Property properties
    if (!newProperties.length) {
      if (!zeebeProperties) {
        return;
      }

      commandStack.execute('element.updateModdleProperties', {
        element,
        moddleElement: businessObject,
        properties: {
          values: minDash.without(businessObject.get('values'), zeebeProperties)
        }
      });
    }

    if (!zeebeProperties) {
      zeebeProperties = bpmnFactory.create('zeebe:Properties');

      zeebeProperties.$parent = businessObject;

      commandStack.execute('element.updateModdleProperties', {
        element,
        moddleElement: businessObject,
        properties: {
          values: [ ...businessObject.get('values'), zeebeProperties ]
        }
      });
    }

    const oldZeebeProperties = zeebeProperties.get('properties')
      ? zeebeProperties.get('properties').slice()
      : [];

    newProperties.forEach((newProperty) => {
      const oldProperty = findOldProperty$1(oldTemplate, newProperty),
            oldZeebeProperty = findBusinessObject(businessObject, newProperty),
            newPropertyValue = getDefaultValue(newProperty),
            newBinding = newProperty.binding;

      // (2) update old zeebe:Property
      if (oldZeebeProperty) {
        if (shouldUpdate(newPropertyValue, newProperty)
          || shouldKeepValue(oldZeebeProperty, oldProperty, newProperty)) {
          remove$1(oldZeebeProperties, oldZeebeProperty);
        }

        if (!shouldKeepValue(oldZeebeProperty, oldProperty, newProperty)) {
          commandStack.execute('element.updateModdleProperties', {
            element,
            moddleElement: oldZeebeProperty,
            properties: {
              value: newPropertyValue
            }
          });
        }
      }

      // (3) add new zeebe:Property
      else if (shouldUpdate(newPropertyValue, newProperty)) {
        const newProperty = createZeebeProperty(newBinding, newPropertyValue, bpmnFactory);

        newProperty.$parent = zeebeProperties;

        commandStack.execute('element.updateModdleProperties', {
          element,
          moddleElement: zeebeProperties,
          properties: {
            properties: [ ...zeebeProperties.get('properties'), newProperty ]
          }
        });
      }
    });

    // (4) remove old zeebe:Property
    if (oldZeebeProperties.length) {
      commandStack.execute('element.updateModdleProperties', {
        element,
        moddleElement: zeebeProperties,
        properties: {
          properties: minDash.without(zeebeProperties.get('properties'), zeebeProperty => oldZeebeProperties.includes(zeebeProperty))
        }
      });
    }
  }

  _updateMessage(element, oldTemplate, newTemplate) {

    // update bpmn:Message properties
    this._updateMessageProperties(element, oldTemplate, newTemplate);

    // update bpmn:Message zeebe:subscription properties
    this._updateMessageZeebeSubscriptionProperties(element, oldTemplate, newTemplate);

    this._updateZeebeModelerTemplateOnReferencedElement(element, oldTemplate, newTemplate);

    if (!hasMessageProperties(newTemplate)) {
      removeMessage(element, this._injector);
    }
  }

  /**
   * Update bpmn:Message properties.
   *
   * @param {djs.model.Base} element
   * @param {Object} oldTemplate
   * @param {Object} newTemplate
   */
  _updateMessageProperties(element, oldTemplate, newTemplate) {
    const newProperties = newTemplate.properties.filter((newProperty) => {
      const newBinding = newProperty.binding,
            newBindingType = newBinding.type;

      return newBindingType === MESSAGE_PROPERTY_TYPE;
    });

    const removedProperties = oldTemplate && oldTemplate.properties.filter((oldProperty) => {
      const oldBinding = oldProperty.binding,
            oldBindingType = oldBinding.type;

      return oldBindingType === MESSAGE_PROPERTY_TYPE && !newProperties.find((newProperty) => newProperty.binding.name === oldProperty.binding.name);
    }) || [];

    let message = this._getMessage(element);
    message && removedProperties.forEach((removedProperty) => {

      this._modeling.updateModdleProperties(element, message, {
        [removedProperty.binding.name]: undefined
      });
    });

    if (!newProperties.length) {
      return;
    }

    message = this._ensureMessage(element, newTemplate);

    newProperties.forEach((newProperty) => {
      const oldProperty = findOldProperty$1(oldTemplate, newProperty),
            newBinding = newProperty.binding,
            newBindingName = newBinding.name,
            newPropertyValue = getDefaultValue(newProperty),
            changedElement = message;

      let properties = {};

      if (shouldKeepValue(changedElement, oldProperty, newProperty)) {
        return;
      }

      properties[ newBindingName ] = newPropertyValue;

      this._modeling.updateModdleProperties(element, changedElement, properties);
    });
  }

  /**
   * Update bpmn:Message#zeebe:subscription properties.
   *
   * @param {djs.model.Base} element
   * @param {Object} oldTemplate
   * @param {Object} newTemplate
   */
  _updateMessageZeebeSubscriptionProperties(element, oldTemplate, newTemplate) {
    const newProperties = newTemplate.properties.filter((newProperty) => {
      const newBinding = newProperty.binding,
            newBindingType = newBinding.type;

      return newBindingType === MESSAGE_ZEEBE_SUBSCRIPTION_PROPERTY_TYPE;
    });

    const removedProperties = oldTemplate && oldTemplate.properties.filter((oldProperty) => {
      const oldBinding = oldProperty.binding,
            oldBindingType = oldBinding.type;

      return oldBindingType === MESSAGE_ZEEBE_SUBSCRIPTION_PROPERTY_TYPE && !newProperties.find((newProperty) => newProperty.binding.name === oldProperty.binding.name);
    }) || [];

    if (!newProperties.length && !removedProperties.length) {
      return;
    }

    let message = this._getMessage(element);

    // no new properties and no message -> nothing to do
    if (!newProperties.length && !message) {
      return;
    }

    message = this._ensureMessage(element, newTemplate);

    const messageExtensionElements = this._getOrCreateExtensionElements(element, message);
    const zeebeSubscription = this._getSubscription(element, message);

    const propertiesToSet = newProperties.reduce((properties, newProperty) => {
      const oldProperty = findOldProperty$1(oldTemplate, newProperty),
            newBinding = newProperty.binding,
            newBindingName = newBinding.name,
            newPropertyValue = getDefaultValue(newProperty),
            changedElement = zeebeSubscription;

      if (shouldKeepValue(changedElement, oldProperty, newProperty)) {
        return properties;
      }

      properties[ newBindingName ] = newPropertyValue;
      return properties;
    }, {});

    // Update zeebe Subscription
    if (zeebeSubscription) {
      this._modeling.updateModdleProperties(element, zeebeSubscription,
        propertiesToSet
      );
    } else {

      // create new Subscription
      const newSubscription = createElement('zeebe:Subscription', propertiesToSet, message, this._bpmnFactory);
      this._modeling.updateModdleProperties(element, messageExtensionElements, {
        values: [ ...messageExtensionElements.get('values'), newSubscription ]
      });
    }

    // Remove old properties
    if (!oldTemplate || !zeebeSubscription) {
      return;
    }

    const propertiesToRemove = removedProperties.reduce((properties, removedProperty) => {
      properties[ removedProperty.binding.name ] = undefined;
      return properties;
    }, {});

    this._modeling.updateModdleProperties(element, zeebeSubscription,
      propertiesToRemove
    );
  }

  _updateZeebeModelerTemplateOnReferencedElement(element, oldTemplate, newTemplate) {
    const businessObject = ModelUtil.getBusinessObject(element);

    const message = findMessage(businessObject);

    if (!message) {
      return;
    }

    if (getTemplateId$1(message) === newTemplate.id) {
      return;
    }

    this._modeling.updateModdleProperties(element, message, {
      'zeebe:modelerTemplate': newTemplate.id
    });
  }

  _getSubscription(element, bo) {
    const extensionElements = this._getOrCreateExtensionElements(element, bo);

    const extension = findExtension$1(extensionElements, 'zeebe:Subscription');

    if (extension) {
      return extension;
    }
  }

  _createMessage(element, template) {
    let bo = ModelUtil.getBusinessObject(element);

    if (ModelUtil.is(bo, 'bpmn:Event')) {
      bo = bo.get('eventDefinitions')[0];
    }

    const message = this._bpmnFactory.create('bpmn:Message', { 'zeebe:modelerTemplate': template.id });

    message.$parent = getRoot(bo);

    this._modeling.updateModdleProperties(element, bo, { messageRef: message });

    return message;
  }

  _getMessage(element) {
    let bo = ModelUtil.getBusinessObject(element);

    if (ModelUtil.is(bo, 'bpmn:Event')) {
      bo = bo.get('eventDefinitions')[0];
    }

    return bo && bo.get('messageRef');
  }

  _ensureMessage(element, template) {

    const message = this._getMessage(element);

    // message is already templated, so we use it
    if (message && message.get('zeebe:modelerTemplate')) {
      return message;
    }

    const newMessage = this._createMessage(element, template);

    // no message is set on the element, so no properties to copy
    if (!message) {
      return newMessage;
    }

    return this._moddleCopy.copyElement(message, newMessage, [ 'name', 'extensionElements' ]);
  }


  /**
   * Update `zeebe:CalledElement` properties of specified business object. This
   * can only exist in `bpmn:ExtensionElements`.
   *
   * @param {djs.model.Base} element
   * @param {Object} oldTemplate
   * @param {Object} newTemplate
   */
  _updateCalledElement(element, oldTemplate, newTemplate) {
    this._updateSingleExtensionElement(
      element,
      oldTemplate,
      newTemplate,
      {
        bindingTypes: [ ZEEBE_CALLED_ELEMENT ],
        extensionType: 'zeebe:CalledElement',
        getPropertyName: (binding) => binding.property
      }
    );
  }

  /**
   * Update `zeebe:CalledDecision` properties of specified business object. This
   * can only exist in `bpmn:ExtensionElements`.
   *
   * @param {djs.model.Base} element
   * @param {Object} oldTemplate
   * @param {Object} newTemplate
   */
  _updateCalledDecision(element, oldTemplate, newTemplate) {
    this._updateSingleExtensionElement(
      element,
      oldTemplate,
      newTemplate,
      {
        bindingTypes: [ ZEEBE_CALLED_DECISION ],
        extensionType: 'zeebe:CalledDecision',
        getPropertyName: (binding) => binding.property
      }
    );
  }

  /**
   * Update `zeebe:Script` properties of specified business object. This
   * can only exist in `bpmn:ExtensionElements`.
   *
   * @param {djs.model.Base} element
   * @param {Object} oldTemplate
   * @param {Object} newTemplate
   */
  _updateScriptTask(element, oldTemplate, newTemplate) {
    this._updateSingleExtensionElement(
      element,
      oldTemplate,
      newTemplate,
      {
        bindingTypes: [ ZEEBE_SCRIPT_TASK ],
        extensionType: 'zeebe:Script',
        getPropertyName: (binding) => binding.property
      }
    );
  }

  _updateZeebeAssignmentDefinition(element, oldTemplate, newTemplate) {
    this._updateSingleExtensionElement(
      element,
      oldTemplate,
      newTemplate,
      {
        bindingTypes: [ ZEEBE_ASSIGNMENT_DEFINITION ],
        extensionType: 'zeebe:AssignmentDefinition',
        getPropertyName: (binding) => binding.property
      }
    );
  }

  /**
   * Replaces the element with the specified elementType.
   * Takes into account the eventDefinition for events.
   *
   * @param {djs.model.Base} element
   * @param {Object} newTemplate
   */
  _updateElementType(element, oldTemplate, newTemplate) {

    // determine new task type
    const newType = newTemplate.elementType;

    if (!newType) {
      return element;
    }


    // Do not replace if the element type did not change
    if (!shouldUpdateElementType(element, oldTemplate, newType)) {
      return element;
    }

    const replacement = { type: newType.value };

    if (newType.eventDefinition) {
      replacement.eventDefinitionType = newType.eventDefinition;
    }

    const replacedElement = this._bpmnReplace.replaceElement(element, replacement);

    return replacedElement;
  }


  _updateLinkedResources(element, oldTemplate, newTemplate) {
    const bpmnFactory = this._bpmnFactory,
          commandStack = this._commandStack;

    const newLinkedResources = newTemplate.properties.filter((newProperty) => {
      const newBinding = newProperty.binding,
            newBindingType = newBinding.type;

      return newBindingType === 'zeebe:linkedResource';
    });

    const extensionElements = this._getOrCreateExtensionElements(element);

    let linkedResources = findExtension$1(extensionElements, 'zeebe:LinkedResources');

    // (1) remove linkedResourcess if no new specified
    if (!newLinkedResources.length) {
      if (!linkedResources) {
        return;
      }

      commandStack.execute('element.updateModdleProperties', {
        element,
        moddleElement: extensionElements,
        properties: {
          values: minDash.without(extensionElements.get('values'), linkedResources)
        }
      });
      return;
    }

    if (!linkedResources) {
      linkedResources = bpmnFactory.create('zeebe:LinkedResources');

      linkedResources.$parent = extensionElements;

      commandStack.execute('element.updateModdleProperties', {
        element,
        moddleElement: extensionElements,
        properties: {
          values: [ ...extensionElements.get('values'), linkedResources ]
        }
      });
    }

    const unusedLinkedResources = linkedResources.get('values').slice();
    const unusedResourceProperties = (oldTemplate?.properties.slice() || [])
      .filter((property) => property.binding.type === ZEEBE_LINKED_RESOURCE_PROPERTY);

    newLinkedResources.forEach((newLinkedResource) => {
      const oldProperty = findOldProperty$1(oldTemplate, newLinkedResource),
            oldLinkedResource = findBusinessObject(extensionElements, newLinkedResource),
            newPropertyValue = getDefaultValue(newLinkedResource),
            newBinding = newLinkedResource.binding;

      if (oldProperty) {
        remove$1(unusedResourceProperties, oldProperty);
      }

      // (2) update old LinkesResources
      if (oldLinkedResource) {
        if (
          shouldUpdate(newPropertyValue, newLinkedResource)
          || shouldKeepValue(oldLinkedResource, oldProperty, newLinkedResource)
        ) {
          remove$1(unusedLinkedResources, oldLinkedResource);
        }

        if (!shouldKeepValue(oldLinkedResource, oldProperty, newLinkedResource)) {
          commandStack.execute('element.updateModdleProperties', {
            element,
            moddleElement: oldLinkedResource,
            properties: {
              [newBinding.property]: newPropertyValue
            }
          });
        }
      }

      // (3) add new linkedResources
      else if (shouldUpdate(newPropertyValue, newLinkedResource)) {
        const newProperties = {
          linkName: newBinding.linkName,
          [newBinding.property]: newPropertyValue
        };

        const newLinkedResource = createElement('zeebe:LinkedResource', newProperties, extensionElements, bpmnFactory);

        commandStack.execute('element.updateModdleProperties', {
          element,
          moddleElement: linkedResources,
          properties: {
            values: [ ...linkedResources.get('values'), newLinkedResource ]
          }
        });
      }
    });


    // (4) remove unused linkedResources
    if (unusedLinkedResources.length) {
      commandStack.execute('element.updateModdleProperties', {
        element,
        moddleElement: linkedResources,
        properties: {
          values: minDash.without(linkedResources.get('values'), linkedResource => unusedLinkedResources.includes(linkedResource))
        }
      });
    }

    // (5) remove unused resource properties
    unusedResourceProperties.forEach((unusedResourceProperty) => {
      const oldLinkedResource = findBusinessObject(extensionElements, unusedResourceProperty);

      const oldBinding = unusedResourceProperty.binding;

      // No property was reused and element was removed in previous step
      if (!oldLinkedResource) {
        return;
      }

      commandStack.execute('element.updateModdleProperties', {
        element,
        moddleElement: oldLinkedResource,
        properties: {
          [oldBinding.property]: undefined
        }
      });
    });
  }

  _updateZeebeUserTask = function(element, newTemplate) {

    const commandStack = this._commandStack;
    const bpmnFactory = this._bpmnFactory;

    // check if template has zeebe:userTask binding property
    const hasBinding = newTemplate.properties.some((property) =>
      property.binding.type === ZEEBE_USER_TASK);

    // check if element has zeebe:UserTask extension element
    const extensionElements = ModelUtil.getBusinessObject(element).get('extensionElements');
    const userTaskExtension = findExtension$1(element, 'zeebe:UserTask');

    if (newTemplate.elementType?.value !== 'bpmn:UserTask') {
      return;
    }

    // remove zeebe:UserTask if no binding
    if (userTaskExtension) {

      !hasBinding && commandStack.execute('element.updateModdleProperties', {
        element,
        moddleElement: extensionElements,
        properties: {
          values: minDash.without(extensionElements.get('values'), userTaskExtension)
        }
      });

      return;
    }

    if (!hasBinding) {
      return;
    }

    // create new zeebe:UserTask extension element
    const zeebeUserTask = bpmnFactory.create('zeebe:UserTask');
    zeebeUserTask.$parent = extensionElements;

    commandStack.execute('element.updateModdleProperties', {
      element,
      moddleElement: extensionElements,
      properties: {
        values: [ ...extensionElements.get('values'), zeebeUserTask ]
      }
    });
  };

  /**
   * @param {djs.model.Base} element
   * @param {Object} oldTemplate
   * @param {Object} newTemplate
   */
  _updateZeebeFormDefinition = function(element, oldTemplate, newTemplate) {
    this._updateSingleExtensionElement(
      element,
      oldTemplate,
      newTemplate,
      {
        bindingTypes: [ ZEEBE_FORM_DEFINITION ],
        extensionType: 'zeebe:FormDefinition',
        getPropertyName: (binding) => binding.property
      }
    );
  };

  /**
   * Generic handler for updating extension elements that are single instances with properties.
   *
   * @example
   * For this template
   * ```json
   * {
   *   properties: [
   *    { binding: { type: 'zeebe:taskDefinition', property: 'type' }, value: 'myTaskType' },
   *    {binding: { type: 'zeebe:taskDefinition', property: 'retries' }, value: 3}
   *   ]
   * }
   *```
   * `getPropertyName` should return `binding.property`.
   *
   * `bindingTypes` should be an array of binding types to consider, e.g. `['zeebe:taskDefinition']`.
   *
   * `extensionType` should be the type of the extension element to update, e.g. `'zeebe:TaskDefinition'`.
   *
   * The resulting XML will look like this:
   *
   * ```xml
   * <bpmn:extensionElements>
   *   <zeebe:taskDefinition type="myTaskType" retries="3" />
   * </bpmn:extensionElements>
   *```
   * @param {djs.model.Base} element
   * @param {Object} oldTemplate
   * @param {Object} newTemplate
   * @param {Object} opts
   * @param {Array<string>} opts.bindingTypes - binding types to consider
   * @param {string} opts.extensionType - type of the extension element to update
   * @param {(binding:object) => string} opts.getPropertyName - function to get the property name for the binding
   */
  _updateSingleExtensionElement(element, oldTemplate, newTemplate, opts) {
    const { bindingTypes, extensionType, getPropertyName } = opts;
    const bpmnFactory = this._bpmnFactory,
          commandStack = this._commandStack;

    const newProperties = newTemplate.properties.filter((newProperty) => {
      const newBinding = newProperty.binding,
            newBindingType = newBinding.type;

      return bindingTypes.includes(newBindingType);
    });

    const businessObject = this._getOrCreateExtensionElements(element);
    let extensionElement = findExtension$1(businessObject, extensionType);

    // (1) Remove extension if no new properties

    if (!newProperties.length) {
      if (!extensionElement) {
        return;
      }

      commandStack.execute('element.updateModdleProperties', {
        element,
        moddleElement: businessObject,
        properties: {
          values: minDash.without(businessObject.get('values'), extensionElement)
        }
      });

      return;
    }

    // If there are new properties:
    newProperties.forEach((newProperty) => {
      const oldProperty = findOldProperty$1(oldTemplate, newProperty),
            newPropertyValue = getDefaultValue(newProperty),
            newBinding = newProperty.binding,
            propertyName = getPropertyName(newBinding);

      // (2) Update old extension with new property values
      if (extensionElement) {

        if (!shouldKeepValue(extensionElement, oldProperty, newProperty)) {
          const properties = {
            [propertyName]: newPropertyValue
          };
          commandStack.execute('element.updateModdleProperties', {
            element,
            moddleElement: extensionElement,
            properties
          });
        }
      }

      // (3) Add new extension with properties if it does not exist
      else {
        const properties = {
          [propertyName]: newPropertyValue
        };

        extensionElement = bpmnFactory.create(extensionType, properties);

        extensionElement.$parent = businessObject;

        commandStack.execute('element.updateModdleProperties', {
          element,
          moddleElement: businessObject,
          properties: {
            values: [ ...businessObject.get('values'), extensionElement ]
          }
        });
      }
    });

    // (4) Remove properties no longer templated
    const oldProperties = oldTemplate && oldTemplate.properties.filter((oldProperty) => {
      const oldBinding = oldProperty.binding,
            oldBindingType = oldBinding.type,
            oldPropertyName = getPropertyName(oldBinding);

      return bindingTypes.includes(oldBindingType) &&
        !newProperties.find((newProperty) => oldPropertyName === getPropertyName(newProperty.binding));
    }) || [];

    oldProperties.forEach((oldProperty) => {
      const properties = {
        [ getPropertyName(oldProperty.binding) ]: undefined
      };

      commandStack.execute('element.updateModdleProperties', {
        element,
        moddleElement: extensionElement,
        properties
      });
    });
  }
};

ChangeElementTemplateHandler$1.$inject = [
  'bpmnFactory',
  'bpmnReplace',
  'commandStack',
  'modeling',
  'moddleCopy',
  'injector'
];


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

/**
 * Find business object matching specified property.
 *
 * @param {djs.model.Base|ModdleElement} element
 * @param {Object} property
 *
 * @returns {ModdleElement}
 */
function findBusinessObject(element, property) {
  const businessObject = ModelUtil.getBusinessObject(element);

  const binding = property.binding,
        bindingType = binding.type;

  if (TASK_DEFINITION_TYPES.includes(bindingType)) {
    return findExtension$1(businessObject, 'zeebe:TaskDefinition');
  }

  if (bindingType === 'zeebe:input' || bindingType === 'zeebe:output') {

    const extensionElements = findExtension$1(businessObject, 'zeebe:IoMapping');

    if (!extensionElements) {
      return;
    }

    if (bindingType === 'zeebe:input') {
      return minDash.find(extensionElements.get('zeebe:inputParameters'), function(input) {
        return input.get('zeebe:target') === binding.name;
      });
    } else {
      return minDash.find(extensionElements.get('zeebe:outputParameters'), function(output) {
        return output.get('zeebe:source') === binding.source;
      });
    }

  }

  if (bindingType === 'zeebe:taskHeader') {
    const extensionElements = findExtension$1(businessObject, 'zeebe:TaskHeaders');

    if (!extensionElements) {
      return;
    }

    return minDash.find(extensionElements.get('zeebe:values'), function(value) {
      return value.get('zeebe:key') === binding.key;
    });
  }

  if (bindingType === 'zeebe:property') {
    const zeebeProperties = findExtension$1(businessObject, 'zeebe:Properties');

    if (!zeebeProperties) {
      return;
    }

    return zeebeProperties.get('properties').find((value) => {
      return value.get('name') === binding.name;
    });
  }

  if (bindingType === ZEEBE_LINKED_RESOURCE_PROPERTY) {
    const linkedResources = findExtension$1(businessObject, 'zeebe:LinkedResources');

    if (!linkedResources) {
      return;
    }

    return linkedResources.get('values').find((value) => {
      return value.get('linkName') === binding.linkName;
    });
  }
}

/**
 * Find old property matching specified new property.
 *
 * @param {Object} oldTemplate
 * @param {Object} newProperty
 *
 * @returns {Object}
 */
function findOldProperty$1(oldTemplate, newProperty) {
  if (!oldTemplate) {
    return;
  }

  const oldProperties = oldTemplate.properties,
        newBinding = newProperty.binding,
        newBindingName = newBinding.name,
        newBindingType = newBinding.type;

  if (newBindingType === 'property') {
    return minDash.find(oldProperties, function(oldProperty) {
      const oldBinding = oldProperty.binding,
            oldBindingName = oldBinding.name,
            oldBindingType = oldBinding.type;

      return oldBindingType === 'property' && oldBindingName === newBindingName;
    });
  }

  if (TASK_DEFINITION_TYPES.includes(newBindingType)) {
    return minDash.find(oldProperties, function(oldProperty) {
      const oldBinding = oldProperty.binding,
            oldPropertyName = getTaskDefinitionPropertyName(oldBinding),
            newPropertyName = getTaskDefinitionPropertyName(newBinding);

      return oldPropertyName === newPropertyName;
    });
  }

  if (newBindingType === 'zeebe:input') {
    return minDash.find(oldProperties, function(oldProperty) {
      const oldBinding = oldProperty.binding,
            oldBindingName = oldBinding.name,
            oldBindingType = oldBinding.type;

      if (oldBindingType !== 'zeebe:input') {
        return;
      }

      return oldBindingName === newBindingName;
    });
  }

  if (newBindingType === 'zeebe:output') {
    return minDash.find(oldProperties, function(oldProperty) {
      const oldBinding = oldProperty.binding,
            oldBindingType = oldBinding.type;

      if (oldBindingType !== 'zeebe:output') {
        return;
      }

      return oldBinding.source === newBinding.source;
    });
  }

  if (newBindingType === 'zeebe:taskHeader') {
    return minDash.find(oldProperties, function(oldProperty) {
      const oldBinding = oldProperty.binding,
            oldBindingType = oldBinding.type;

      if (oldBindingType !== 'zeebe:taskHeader') {
        return;
      }

      return oldBinding.key === newBinding.key;
    });
  }

  if (newBindingType === 'zeebe:property') {
    return oldProperties.find((oldProperty) => {
      const oldBinding = oldProperty.binding,
            oldBindingType = oldBinding.type;

      if (oldBindingType !== 'zeebe:property') {
        return;
      }

      return oldBinding.name === newBinding.name;
    });
  }

  if (newBindingType === MESSAGE_PROPERTY_TYPE) {
    return oldProperties.find(oldProperty => {
      const oldBinding = oldProperty.binding,
            oldBindingType = oldBinding.type;

      if (oldBindingType !== MESSAGE_PROPERTY_TYPE) {
        return;
      }

      return oldBinding.name === newBinding.name;
    });
  }

  if (newBindingType === MESSAGE_ZEEBE_SUBSCRIPTION_PROPERTY_TYPE) {
    return oldProperties.find(oldProperty => {
      const oldBinding = oldProperty.binding,
            oldBindingType = oldBinding.type;

      if (oldBindingType !== MESSAGE_ZEEBE_SUBSCRIPTION_PROPERTY_TYPE) {
        return;
      }

      return oldBinding.name === newBinding.name;
    });
  }

  if (newBindingType === ZEEBE_LINKED_RESOURCE_PROPERTY) {
    return oldProperties.find(oldProperty => {
      const oldBinding = oldProperty.binding,
            oldBindingType = oldBinding.type;

      if (oldBindingType !== ZEEBE_LINKED_RESOURCE_PROPERTY) {
        return;
      }

      return oldBinding.linkName === newBinding.linkName && oldBinding.property === newBinding.property;
    });
  }

  if (newBindingType === ZEEBE_CALLED_DECISION) {
    return oldProperties.find(oldProperty => {
      const oldBinding = oldProperty.binding,
            oldBindingType = oldBinding.type;

      if (oldBindingType !== ZEEBE_CALLED_DECISION) {
        return;
      }

      return oldBindingType === newBindingType && oldBinding.property === newBinding.property;
    });
  }

  if (newBindingType === ZEEBE_SCRIPT_TASK) {
    return oldProperties.find(oldProperty => {
      const oldBinding = oldProperty.binding,
            oldBindingType = oldBinding.type;

      if (oldBindingType !== ZEEBE_SCRIPT_TASK) {
        return;
      }

      return oldBindingType === newBindingType && oldBinding.property === newBinding.property;
    });
  }

  if (newBindingType === ZEEBE_FORM_DEFINITION) {
    return oldProperties.find(oldProperty => {
      const oldBinding = oldProperty.binding,
            oldBindingType = oldBinding.type;

      if (oldBindingType !== ZEEBE_FORM_DEFINITION) {
        return;
      }

      return oldBindingType === newBindingType && oldBinding.property === newBinding.property;
    });
  }

  if (newBindingType === ZEEBE_ASSIGNMENT_DEFINITION) {
    return oldProperties.find(oldProperty => {
      const oldBinding = oldProperty.binding,
            oldBindingType = oldBinding.type;

      if (oldBindingType !== ZEEBE_ASSIGNMENT_DEFINITION) {
        return;
      }

      return oldBindingType === newBindingType && oldBinding.property === newBinding.property;
    });
  }
}

/**
 * Check whether the existing property should be kept. This is the case if
 *  - an old template was set and the value differs from the default
 *  - no template was set but the property was set manually
 *
 * @param {djs.model.Base|ModdleElement} element
 * @param {Object} oldProperty
 * @param {Object} newProperty
 *
 * @returns {boolean}
 */
function shouldKeepValue(element, oldProperty, newProperty) {

  // "Hidden" values are treated as a constant
  if (newProperty.type === 'Hidden') {
    return false;
  }

  // Dropdowns should keep existing configuration
  // cf. https://github.com/bpmn-io/bpmn-js-properties-panel/issues/767
  if (newProperty.type === 'Dropdown') {

    const currentValue = getPropertyValue$1(element, newProperty);

    // only keep value if old value is a valid option
    return newProperty.choices && newProperty.choices.some(
      (choice) => choice.value === currentValue
    );
  }

  // keep existing old property if
  // user changed it from the original
  if (oldProperty) {
    return propertyChanged$1(element, oldProperty);
  }

  // keep existing property value
  return !!(getPropertyValue$1(element, newProperty));
}

/**
 * Check whether property was changed after being set by template.
 *
 * @param {djs.model.Base|ModdleElement} element
 * @param {Object} oldProperty
 *
 * @returns {boolean}
 */
function propertyChanged$1(element, oldProperty) {
  const oldPropertyValue = oldProperty.value;

  return getPropertyValue$1(element, oldProperty) !== oldPropertyValue;
}

function getPropertyValue$1(element, property) {
  const businessObject = ModelUtil.getBusinessObject(element);

  if (!businessObject) {
    return;
  }

  const binding = property.binding,
        bindingName = binding.name,
        bindingType = binding.type,
        bindingProperty = binding.property;


  if (bindingType === 'property') {
    return businessObject.get(bindingName);
  }

  if (TASK_DEFINITION_TYPES.includes(bindingType)) {
    return businessObject.get(getTaskDefinitionPropertyName(binding));
  }

  if (bindingType === 'zeebe:input') {
    return businessObject.get('zeebe:source');
  }

  if (bindingType === 'zeebe:output') {
    return businessObject.get('zeebe:target');
  }

  if (bindingType === 'zeebe:taskHeader') {
    return businessObject.get('zeebe:value');
  }

  if (bindingType === 'zeebe:property') {
    return businessObject.get('zeebe:value');
  }

  if (bindingType === MESSAGE_PROPERTY_TYPE) {
    return businessObject.get(bindingName);
  }

  if (bindingType === MESSAGE_ZEEBE_SUBSCRIPTION_PROPERTY_TYPE) {
    return businessObject.get(bindingName);
  }

  if (bindingType === ZEEBE_LINKED_RESOURCE_PROPERTY) {
    return businessObject.get(bindingProperty);
  }

  if (bindingType === ZEEBE_CALLED_DECISION) {
    return businessObject.get(bindingProperty);
  }

  if (bindingType === ZEEBE_CALLED_ELEMENT) {
    return businessObject.get(bindingProperty);
  }

  if (bindingType === ZEEBE_FORM_DEFINITION) {
    return businessObject.get(bindingProperty);
  }

  if (bindingType === ZEEBE_SCRIPT_TASK) {
    return businessObject.get(bindingProperty);
  }

  if (bindingType === ZEEBE_ASSIGNMENT_DEFINITION) {
    return businessObject.get(bindingProperty);
  }
}

function remove$1(array, item) {
  const index = array.indexOf(item);

  if (index < 0) {
    return array;
  }

  array.splice(index, 1);

  return array;
}


function hasMessageProperties(template) {
  return template.properties.some(p => MESSAGE_BINDING_TYPES.includes(p.binding.type));
}

function shouldUpdateElementType(element, oldTemplate, newType) {

  // Never reuse existing eventDefinition when applying a new template
  if (!oldTemplate && newType.eventDefinition) {
    return true;
  }

  const oldType = oldTemplate && oldTemplate.elementType || {
    value: element.type,
    eventDefinition: getEventDefinitionType(element)
  };

  // Do not update if the element type did not change
  if (oldType && oldType.value === newType.value && oldType.eventDefinition === newType.eventDefinition) {
    return false;
  }

  return true;
}

function getEventDefinitionType(element) {
  const businessObject = ModelUtil.getBusinessObject(element);
  if (!businessObject.eventDefinitions) {
    return;
  }

  const eventDefinition = businessObject.eventDefinitions[ 0 ];

  if (!eventDefinition) {
    return;
  }

  return eventDefinition.$type;
}

let RemoveElementTemplateHandler$1 = class RemoveElementTemplateHandler {
  constructor(
      modeling,
      elementFactory,
      elementRegistry,
      canvas,
      bpmnFactory,
      replace,
      commandStack,
      moddleCopy
  ) {
    this._modeling = modeling;
    this._elementFactory = elementFactory;
    this._elementRegistry = elementRegistry;
    this._canvas = canvas;
    this._bpmnFactory = bpmnFactory;
    this._replace = replace;
    this._commandStack = commandStack;
    this._moddleCopy = moddleCopy;
  }

  preExecute(context) {
    const {
      element
    } = context;

    if (element.parent) {
      context.newElement = this._removeTemplate(element);
    } else {
      context.newElement = this._removeRootTemplate(element);
    }
  }

  _removeTemplate(element) {
    const replace = this._replace;

    const businessObject = ModelUtil.getBusinessObject(element);

    const type = businessObject.$type,
          eventDefinitionType = this._getEventDefinitionType(businessObject);

    const newBusinessObject = this._createNewBusinessObject(element);

    return replace.replaceElement(element,
      {
        type: type,
        businessObject: newBusinessObject,
        eventDefinitionType: eventDefinitionType,
      },
      {
        createElementsBehavior: false
      }
    );
  }

  /**
   * Remove template from a given element.
   *
   * @param {djs.model.Base} element
   *
   * @return {djs.model.Base} the updated element
   */
  _removeRootTemplate(element) {
    var modeling = this._modeling,
        elementFactory = this._elementFactory,
        elementRegistry = this._elementRegistry,
        canvas = this._canvas;

    // We are inside a collapsed subprocess, move up to the parent before replacing the collapsed object
    if (DrilldownUtil.isPlane(element)) {
      const shapeId = DrilldownUtil.getShapeIdFromPlane(element);
      const shape = elementRegistry.get(shapeId);

      if (shape && shape !== element) {
        canvas.setRootElement(canvas.findRoot(shape));
        return this._removeTemplate(shape);
      }
    }

    const businessObject = ModelUtil.getBusinessObject(element);

    const type = businessObject.$type;

    const newBusinessObject = this._createNewBusinessObject(element);

    const newRoot = elementFactory.create('root', {
      type: type,
      businessObject: newBusinessObject
    });

    this._commandStack.execute('canvas.updateRoot', {
      newRoot: newRoot,
      oldRoot: element
    });

    modeling.moveElements(element.children, { x: 0, y: 0 }, newRoot);

    return newRoot;
  }

  _getEventDefinitionType(businessObject) {
    if (!businessObject.eventDefinitions) {
      return null;
    }

    const eventDefinition = businessObject.eventDefinitions[ 0 ];

    if (!eventDefinition) {
      return null;
    }

    return eventDefinition.$type;
  }

  _createNewBusinessObject(element) {
    const bpmnFactory = this._bpmnFactory;

    const bo = ModelUtil.getBusinessObject(element),
          newBo = bpmnFactory.create(bo.$type),
          label = LabelUtil.getLabel(element);

    // Make we we keep general properties unrelated to the template.
    this._copyProperties(bo, newBo, [ 'documentation' ]);
    this._copyExtensionElements(bo, newBo, [ 'zeebe:ExecutionListeners' ]);

    if (!label) {
      return newBo;
    }

    if (ModelUtil.is(element, 'bpmn:Group')) {
      newBo.categoryValueRef = bpmnFactory.create('bpmn:CategoryValue');
    }

    LabelUtil.setLabel({ businessObject: newBo }, label);

    return newBo;
  }

  /**
   * Copy specified properties to the target business object.
   *
   * @param {ModdleElement} source
   * @param {ModdleElement} target
   * @param {Array<string>} properties
   */
  _copyProperties(source, target, properties) {
    const copy = this._moddleCopy;

    properties.forEach(propertyName => {

      const property = source.get(propertyName);

      if (property) {
        const propertyCopy = copy.copyProperty(property, target, propertyName);
        target.set(propertyName, propertyCopy);
      }
    });
  }

  /**
   * Copy extension elements of specified types to the target business object.
   *
   * @param {ModdleElement} source
   * @param {ModdleElement} target
   * @param {Array<string>} extensionElements
   */
  _copyExtensionElements(source, target, extensionElements) {
    const bpmnFactory = this._bpmnFactory;

    // No extension elements in the source business object.
    if (!source.extensionElements || !source.extensionElements.values) return;

    const newExtensionElements = source.extensionElements.values.filter(value =>
      extensionElements.some(extensionElement => ModelUtil.is(value, extensionElement))
    );

    // Nothing to copy.
    if (!newExtensionElements.length) return;

    target.extensionElements = bpmnFactory.create('bpmn:ExtensionElements', { values: newExtensionElements });
  }
};


RemoveElementTemplateHandler$1.$inject = [
  'modeling',
  'elementFactory',
  'elementRegistry',
  'canvas',
  'bpmnFactory',
  'replace',
  'commandStack',
  'moddleCopy'
];

/**
 * A handler that combines and executes multiple commands.
 *
 * All updates are bundled on the command stack and executed in one step.
 * This also makes it possible to revert the changes in one step.
 *
 * Example use case: remove the camunda:formKey attribute and in addition
 * add all form fields needed for the camunda:formData property.
 */
class MultiCommandHandler {
  constructor(commandStack) {
    this._commandStack = commandStack;
  }

  preExecute(context) {
    const commandStack = this._commandStack;

    minDash.forEach(context, function(command) {
      commandStack.execute(command.cmd, command.context);
    });
  }
}

MultiCommandHandler.$inject = [ 'commandStack' ];

let ElementTemplatesCommands$1 = class ElementTemplatesCommands {
  constructor(commandStack, elementTemplates, eventBus) {
    commandStack.registerHandler(
      'element-templates.multi-command-executor',
      MultiCommandHandler
    );

    commandStack.registerHandler(
      'propertiesPanel.zeebe.changeTemplate',
      ChangeElementTemplateHandler$1
    );

    commandStack.registerHandler(
      'propertiesPanel.removeTemplate',
      RemoveElementTemplateHandler$1
    );

    // apply default element templates on shape creation
    eventBus.on([ 'commandStack.shape.create.postExecuted' ], function(event) {
      const {
        context: {
          hints = {},
          shape
        }
      } = event;

      if (hints.createElementsBehavior !== false) {
        applyDefaultTemplate$1(shape, elementTemplates, commandStack);
      }
    });

    // apply default element templates on connection creation
    eventBus.on([ 'commandStack.connection.create.postExecuted' ], function(event) {
      const {
        context: {
          hints = {},
          connection
        }
      } = event;

      if (hints.createElementsBehavior !== false) {
        applyDefaultTemplate$1(connection, elementTemplates, commandStack);
      }
    });
  }
};

ElementTemplatesCommands$1.$inject = [ 'commandStack', 'elementTemplates', 'eventBus' ];

function applyDefaultTemplate$1(element, elementTemplates, commandStack) {

  if (!elementTemplates.get(element) && elementTemplates.getDefault(element)) {

    const command = 'propertiesPanel.zeebe.changeTemplate';
    const commandContext = {
      element: element,
      newTemplate: elementTemplates.getDefault(element)
    };

    commandStack.execute(command, commandContext);
  }
}

var commandsModule$1 = {
  __init__: [ 'elementTemplateCommands' ],
  elementTemplateCommands: [ 'type', ElementTemplatesCommands$1 ]
};

class PropertyBindingProvider {
  static create(element, options) {
    const {
      bpmnFactory,
      property
    } = options;

    const {
      binding
    } = property;

    const {
      name
    } = binding;

    let value = getDefaultValue(property);

    const businessObject = ModelUtil.getBusinessObject(element);

    if (isExpression(businessObject, name)) {
      const expression = createExpression(value, businessObject, bpmnFactory);

      value = expression;
    }

    businessObject[name] = value;
  }
}

class TaskDefinitionTypeBindingProvider {
  static create(element, options) {
    const {
      property,
      bpmnFactory
    } = options;

    const value = getDefaultValue(property);
    const propertyName = getTaskDefinitionPropertyName(property.binding);

    const taskDefinition = ensureExtension(element, 'zeebe:TaskDefinition', bpmnFactory);
    taskDefinition.set(propertyName, value);
  }
}

class InputBindingProvider {
  static create(element, options) {
    const {
      property,
      bpmnFactory
    } = options;

    const {
      binding
    } = property;

    const value = getDefaultValue(property);

    const ioMapping = ensureExtension(element, 'zeebe:IoMapping', bpmnFactory);

    if (!shouldUpdate(value, property)) {
      return;
    }

    const input = createInputParameter$1(binding, value, bpmnFactory);
    input.$parent = ioMapping;
    ioMapping.get('inputParameters').push(input);
  }
}

class OutputBindingProvider {
  static create(element, options) {
    const {
      property,
      bpmnFactory
    } = options;

    const {
      binding
    } = property;

    const value = getDefaultValue(property);

    const ioMapping = ensureExtension(element, 'zeebe:IoMapping', bpmnFactory);

    if (!shouldUpdate(value, property)) {
      return;
    }

    const output = createOutputParameter$1(binding, value, bpmnFactory);
    output.$parent = ioMapping;
    ioMapping.get('outputParameters').push(output);
  }
}

class TaskHeaderBindingProvider {
  static create(element, options) {
    const {
      property,
      bpmnFactory
    } = options;

    const {
      binding
    } = property;

    const value = getDefaultValue(property);

    const taskHeaders = ensureExtension(element, 'zeebe:TaskHeaders', bpmnFactory);

    const header = createTaskHeader(binding, value, bpmnFactory);
    header.$parent = taskHeaders;
    taskHeaders.get('values').push(header);
  }
}

/**
 * A provider that sets up the `zeebe:property` binding
 * on a newly created moddle element.
 */
class ZeebePropertiesProvider {
  static create(element, options) {
    const {
      property,
      bpmnFactory
    } = options;

    const {
      binding,
    } = property;

    const value = getDefaultValue(property);

    const zeebeProperties = ensureExtension(element, 'zeebe:Properties', bpmnFactory);

    if (!shouldUpdate(value, property)) {
      return;
    }

    const zeebeProperty = createZeebeProperty(binding, value, bpmnFactory);
    zeebeProperty.$parent = zeebeProperties;
    zeebeProperties.get('properties').push(zeebeProperty);
  }
}

class MessagePropertyBindingProvider {
  static create(element, options) {
    const {
      bpmnFactory,
      property
    } = options;

    const {
      binding
    } = property;

    const {
      name
    } = binding;

    const value = getDefaultValue(property);

    let businessObject = ModelUtil.getBusinessObject(element);

    if (ModelUtil.is(businessObject, 'bpmn:Event')) {
      businessObject = businessObject.get('eventDefinitions')[0];
    }

    let message = businessObject.get('messageRef');

    if (!message) {
      message = bpmnFactory.create('bpmn:Message', { 'zeebe:modelerTemplate': getTemplateId$1(element) });
      businessObject.set('messageRef', message);
    }

    message.set(name, value);
  }
}

class MessageZeebeSubscriptionBindingProvider {
  static create(element, options) {
    const {
      bpmnFactory,
      property
    } = options;

    const {
      binding
    } = property;

    const {
      name
    } = binding;

    const value = getDefaultValue(property);

    let businessObject = ModelUtil.getBusinessObject(element);
    if (ModelUtil.is(businessObject, 'bpmn:Event')) {
      businessObject = businessObject.get('eventDefinitions')[0];
    }

    let message = businessObject.get('messageRef');
    if (!message) {
      message = bpmnFactory.create('bpmn:Message', { 'zeebe:modelerTemplate': getTemplateId$1(element) });
      businessObject.set('messageRef', message);
    }

    const subscription = ensureExtension(message, 'zeebe:Subscription', bpmnFactory);

    if (!shouldUpdate(value, property)) {
      return;
    }

    subscription.set(name, value);
  }
}

class CalledElementBindingProvider {
  static create(element, options) {
    const {
      property,
      bpmnFactory
    } = options;

    const {
      binding
    } = property;

    const {
      property: propertyName
    } = binding;

    const value = getDefaultValue(property);

    const calledElement = ensureExtension(element, 'zeebe:CalledElement', bpmnFactory);

    // TODO(@barmac): remove if we decide to support propagation in templates
    ensureNoPropagation(calledElement);

    calledElement.set(propertyName, value);
  }
}

function ensureNoPropagation(calledElement) {
  calledElement.set('propagateAllChildVariables', false);
  calledElement.set('propagateAllParentVariables', false);
}

class LinkedResourcePropertyBindingProvider {
  static create(element, options) {
    const {
      property,
      bpmnFactory
    } = options;

    const {
      binding: {
        property: bindingProperty,
        linkName
      }
    } = property;

    const value = getDefaultValue(property);

    const bo = ModelUtil.getBusinessObject(element);
    const linkedResources = ensureExtension(element, 'zeebe:LinkedResources', bpmnFactory);

    let linkedResource = linkedResources.get('values').find(linkedResource => linkedResource.get('linkName') === linkName);

    if (!linkedResource) {
      linkedResource = createElement('zeebe:LinkedResource', { linkName }, bo, bpmnFactory);
      linkedResources.get('values').push(linkedResource);
    }

    linkedResource.set(bindingProperty, value);
  }
}

class ZeebeUserTaskBindingProvider {
  static create(element, options) {
    const {
      bpmnFactory
    } = options;

    ensureExtension(element, 'zeebe:UserTask', bpmnFactory);
  }
}

class CalledDecisionBindingProvider {
  static create(element, options) {
    const {
      property,
      bpmnFactory
    } = options;

    const {
      binding
    } = property;

    const {
      property: propertyName
    } = binding;

    const value = getDefaultValue(property);

    const calledDecision = ensureExtension(element, 'zeebe:CalledDecision', bpmnFactory);

    calledDecision.set(propertyName, value);
  }
}

class ScriptTaskBindingProvider {
  static create(element, options) {
    const {
      property,
      bpmnFactory
    } = options;

    const {
      binding
    } = property;

    const {
      property: propertyName
    } = binding;

    const value = getDefaultValue(property);

    const scriptTask = ensureExtension(element, 'zeebe:Script', bpmnFactory);

    scriptTask.set(propertyName, value);
  }
}

class ZeebeFormDefinitionBindingProvider {
  static create(element, options) {
    const {
      property,
      bpmnFactory
    } = options;

    const {
      binding
    } = property;

    const {
      property: propertyName
    } = binding;

    const value = getDefaultValue(property);

    const formDefinition = ensureExtension(element, 'zeebe:FormDefinition', bpmnFactory);

    formDefinition.set(propertyName, value);
  }
}

class AssignmentDefinitionBindingProvider {
  static create(element, options) {
    const {
      property,
      bpmnFactory
    } = options;

    const {
      binding
    } = property;

    const {
      property: propertyName
    } = binding;

    const value = getDefaultValue(property);

    const assignmentDefinition = ensureExtension(element, 'zeebe:AssignmentDefinition', bpmnFactory);

    assignmentDefinition.set(propertyName, value);
  }
}

const EXPRESSION_TYPES = [
  'bpmn:Expression',
  'bpmn:FormalExpression'
];

function getPropertyValue(element, property) {
  const rawValue = getRawPropertyValue(element, property);

  const { type } = property;

  if (type === 'Boolean') {
    return getBooleanPropertyValue(rawValue);
  }

  return rawValue;
}

function getRawPropertyValue(element, property) {
  let businessObject = ModelUtil.getBusinessObject(element);

  const defaultValue = '';

  const {
    binding
  } = property;

  const {
    name,
    property: bindingProperty,
    type,
    linkName
  } = binding;

  // property
  if (type === 'property') {
    const value = isExpression(element, name) ? getExpressionValue(element, name) : businessObject.get(name);

    if (!minDash.isUndefined(value)) {
      return value;
    }

    return defaultValue;
  }

  // zeebe:taskDefinition
  if (TASK_DEFINITION_TYPES.includes(type)) {

    const taskDefinition = findExtension$1(businessObject, 'zeebe:TaskDefinition');

    if (taskDefinition) {
      if (type === ZEEBE_TASK_DEFINITION_TYPE_TYPE) {
        return taskDefinition.get('type');
      } else if (type === ZEEBE_TASK_DEFINITION) {
        return taskDefinition.get(bindingProperty);
      }
    }

    return defaultValue;
  }

  if (IO_BINDING_TYPES.includes(type)) {
    const ioMapping = findExtension$1(businessObject, 'zeebe:IoMapping');

    if (!ioMapping) {
      return defaultValue;
    }

    // zeebe:Input
    if (type === ZEBBE_INPUT_TYPE) {
      const inputParameter = findInputParameter(ioMapping, binding);

      if (inputParameter) {
        return inputParameter.get('source');
      }

      return defaultValue;
    }

    // zeebe:Output
    if (type === ZEEBE_OUTPUT_TYPE) {
      const outputParameter = findOutputParameter(ioMapping, binding);

      if (outputParameter) {
        return outputParameter.get('target');
      }

      return defaultValue;
    }
  }

  // zeebe:taskHeaders
  if (type === ZEEBE_TASK_HEADER_TYPE) {
    const taskHeaders = findExtension$1(businessObject, 'zeebe:TaskHeaders');

    if (!taskHeaders) {
      return defaultValue;
    }

    const header = findTaskHeader(taskHeaders, binding);

    if (header) {
      return header.get('value');
    }

    return defaultValue;
  }

  // zeebe:Property
  if (type === ZEEBE_PROPERTY_TYPE) {
    const zeebeProperties = findExtension$1(businessObject, 'zeebe:Properties');

    if (zeebeProperties) {
      const zeebeProperty = findZeebeProperty(zeebeProperties, binding);

      if (zeebeProperty) {
        return zeebeProperty.get('value');
      }
    }

    return defaultValue;
  }

  // bpmn:Message#property
  if (type === MESSAGE_PROPERTY_TYPE) {
    const message = findMessage(businessObject);

    const value = message ? message.get(name) : undefined;

    if (!minDash.isUndefined(value)) {
      return value;
    }

    return defaultValue;
  }

  // bpmn:Message#zeebe:subscription#property
  if (type === MESSAGE_ZEEBE_SUBSCRIPTION_PROPERTY_TYPE) {
    const message = findMessage(businessObject);

    if (message) {
      const subscription = findExtension$1(message, 'zeebe:Subscription');

      const value = subscription ? subscription.get(name) : undefined;

      if (!minDash.isUndefined(value)) {
        return subscription.get(name);
      }
    }

    return defaultValue;
  }

  // zeebe:calledElement
  if (type === ZEEBE_CALLED_ELEMENT) {
    const calledElement = findExtension$1(businessObject, 'zeebe:CalledElement');

    return calledElement ? calledElement.get(bindingProperty) : defaultValue;
  }

  if (type === ZEEBE_LINKED_RESOURCE_PROPERTY) {
    const linkedResources = findExtension$1(businessObject, 'zeebe:LinkedResources');

    if (!linkedResources) {
      return defaultValue;
    }

    const linkedResource = linkedResources.get('values').find((value) => value.get('linkName') === linkName);

    return linkedResource ? linkedResource.get(bindingProperty) : defaultValue;
  }

  // zeebe:userTask
  if (type === ZEEBE_USER_TASK) {
    const userTask = findExtension$1(businessObject, 'zeebe:userTask');

    return userTask ? userTask.get(bindingProperty) : defaultValue;
  }

  // zeebe:calledDecision
  if (type === ZEEBE_CALLED_DECISION) {

    const calledDecision = findExtension$1(businessObject, 'zeebe:CalledDecision');

    return calledDecision ? calledDecision.get(bindingProperty) : defaultValue;
  }

  // zeebe:formDefinition
  if (type === ZEEBE_FORM_DEFINITION) {
    const formDefinition = findExtension$1(businessObject, 'zeebe:FormDefinition');

    return formDefinition ? formDefinition.get(bindingProperty) : defaultValue;
  }

  // zeebe:script
  if (type === ZEEBE_SCRIPT_TASK) {

    const scriptTask = findExtension$1(businessObject, 'zeebe:Script');

    return scriptTask ? scriptTask.get(bindingProperty) : defaultValue;
  }

  if (type === ZEEBE_ASSIGNMENT_DEFINITION) {
    const assignmentDefinition = findExtension$1(businessObject, 'zeebe:AssignmentDefinition');

    return assignmentDefinition ? assignmentDefinition.get(bindingProperty) : defaultValue;
  }

  // should never throw as templates are validated beforehand
  throw unknownBindingError(element, property);
}

/**
 * Cast a string value to a boolean if possible. Otherwise return the value.
 * Cannot always cast due to FEEL expressions.
 *
 * @param {string|boolean} value
 */
function getBooleanPropertyValue(value) {
  switch (value) {
  case 'true':
    return true;
  case 'false':
    return false;
  }

  return value;
}

const NO_OP = null;

function setPropertyValue(bpmnFactory, commandStack, element, property, value) {
  let businessObject = ModelUtil.getBusinessObject(element);

  const {
    binding,
  } = property;

  const {
    name,
    type,
    property: bindingProperty,
    linkName
  } = binding;

  let extensionElements;

  let propertyValue;

  const commands = [];

  const context = {
    element,
    property
  };

  // ensure message exists
  if (MESSAGE_BINDING_TYPES.includes(type)) {
    if (ModelUtil.is(businessObject, 'bpmn:Event')) {
      businessObject = businessObject.get('eventDefinitions')[0];
    }

    let message = findMessage(businessObject);

    if (!message) {
      message = bpmnFactory.create('bpmn:Message', { 'zeebe:modelerTemplate': getTemplateId$1(element) });

      commands.push({
        cmd: 'element.updateModdleProperties',
        context: {
          ...context,
          moddleElement: businessObject,
          properties: { messageRef: message }
        }
      });
    }

    businessObject = message;
  }

  // ensure extension elements
  if (EXTENSION_BINDING_TYPES.includes(type)) {
    extensionElements = businessObject.get('extensionElements');

    if (!extensionElements) {
      extensionElements = createElement('bpmn:ExtensionElements', null, businessObject, bpmnFactory);

      commands.push({
        cmd: 'element.updateModdleProperties',
        context: {
          ...context,
          moddleElement: businessObject,
          properties: { extensionElements }
        }
      });
    } else {
      commands.push(NO_OP);
    }
  }

  // property
  if (PROPERTY_BINDING_TYPES.includes(type)) {

    const propertyType = businessObject.$descriptor.propertiesByName[ name ]?.type;
    let propertyName = name;

    if (!propertyType || propertyType === 'String') {

      // make sure we create and don't remove the property
      propertyValue = value || '';
    } else if (propertyType === 'Boolean') {
      propertyValue = !!value;
    } else if (propertyType === 'Integer') {
      propertyValue = parseInt(value, 10);

      if (isNaN(propertyValue)) {

        // do not set NaN value
        propertyValue = undefined;
      }
    } else if (EXPRESSION_TYPES.includes(propertyType)) {
      const existingExpression = businessObject.get(name);

      if (existingExpression && ModelUtil.is(existingExpression, 'bpmn:FormalExpression')) {

        // re-use existing expression
        businessObject = existingExpression;
        propertyName = 'body';
        propertyValue = value || '';
      } else {
        propertyValue = createExpression(value, businessObject, bpmnFactory);
      }
    } else {

      // unsupported non-primitive types cannot be set
      throw new Error(`cannot set property of type <${ propertyType }>`);
    }

    if (!minDash.isUndefined(propertyValue)) {
      commands.push({
        cmd: 'element.updateModdleProperties',
        context: {
          ...context,
          moddleElement: businessObject,
          properties: { [ propertyName ]: propertyValue }
        }
      });
    } else {
      commands.push(NO_OP);
    }
  }

  // zeebe:taskDefinition
  if (TASK_DEFINITION_TYPES.includes(type)) {
    const oldTaskDefinition = findExtension$1(extensionElements, 'zeebe:TaskDefinition'),
          propertyName = getTaskDefinitionPropertyName(binding),
          properties = {
            [ propertyName ]: value || ''
          };

    if (oldTaskDefinition) {
      commands.push({
        cmd: 'element.updateModdleProperties',
        context: {
          ...context,
          properties,
          moddleElement: oldTaskDefinition
        }
      });
    } else {
      const newTaskDefinition = createTaskDefinition(properties, bpmnFactory);
      newTaskDefinition.$parent = businessObject;

      const values = extensionElements.get('values');

      commands.push({
        cmd: 'element.updateModdleProperties',
        context: {
          ...context,
          moddleElement: extensionElements,
          properties: { values: [ ...values, newTaskDefinition ] }
        }
      });
    }
  }

  if (IO_BINDING_TYPES.includes(type)) {
    let ioMapping = findExtension$1(extensionElements, 'zeebe:IoMapping');

    if (!ioMapping) {
      ioMapping = createElement('zeebe:IoMapping', null, businessObject, bpmnFactory);

      commands.push({
        cmd: 'element.updateModdleProperties',
        context: {
          ...context,
          moddleElement: extensionElements,
          properties: { values: [ ...extensionElements.get('values'), ioMapping ] }
        }
      });
    }

    // zeebe:Input
    if (type === ZEBBE_INPUT_TYPE) {
      const oldZeebeInputParameter = findInputParameter(ioMapping, binding);
      const values = ioMapping.get('inputParameters').filter((value) => value !== oldZeebeInputParameter);

      // do not persist empty parameters when configured as <optional>
      if (shouldUpdate(value, property)) {
        const newZeebeInputParameter = createInputParameter$1(binding, value, bpmnFactory);
        values.push(newZeebeInputParameter);
      }

      commands.push({
        cmd: 'element.updateModdleProperties',
        context: {
          ...context,
          moddleElement: ioMapping,
          properties: { inputParameters: [ ...values ] }
        }
      });
    }

    // zeebe:Output
    if (type === ZEEBE_OUTPUT_TYPE) {
      const oldZeebeOutputParameter = findOutputParameter(ioMapping, binding);
      const values = ioMapping.get('outputParameters').filter((value) => value !== oldZeebeOutputParameter);

      // do not persist empty parameters when configured as <optional>
      if (shouldUpdate(value, property)) {
        const newZeebeOutputParameter = createOutputParameter$1(binding, value, bpmnFactory);
        values.push(newZeebeOutputParameter);
      }

      commands.push({
        cmd: 'element.updateModdleProperties',
        context: {
          ...context,
          moddleElement: ioMapping,
          properties: { 'outputParameters': [ ...values ] }
        }
      });
    }
  }

  // zeebe:taskHeaders
  if (type === ZEEBE_TASK_HEADER_TYPE) {
    let taskHeaders = findExtension$1(extensionElements, 'zeebe:TaskHeaders');

    if (!taskHeaders) {
      taskHeaders = createElement('zeebe:TaskHeaders', null, businessObject, bpmnFactory);

      commands.push({
        cmd: 'element.updateModdleProperties',
        context: {
          ...context,
          moddleElement: extensionElements,
          properties: { values: [ ...extensionElements.get('values'), taskHeaders ] }
        }
      });
    }

    const oldTaskHeader = findTaskHeader(taskHeaders, binding);

    const values = taskHeaders.get('values').filter((value) => value !== oldTaskHeader);

    // do not persist task headers with empty value
    if (!value) {
      commands.push({
        cmd: 'element.updateModdleProperties',
        context: {
          ...context,
          moddleElement: taskHeaders,
          properties: { values }
        }
      });
    } else {
      const newTaskHeader = createTaskHeader(binding, value, bpmnFactory);

      commands.push({
        cmd: 'element.updateModdleProperties',
        context: {
          ...context,
          moddleElement: taskHeaders,
          properties: { values: [ ...values, newTaskHeader ] }
        }
      });
    }
  }

  // zeebe:Property
  if (type === ZEEBE_PROPERTY_TYPE) {
    let zeebeProperties = findExtension$1(extensionElements, 'zeebe:Properties');

    if (!zeebeProperties) {
      zeebeProperties = createElement('zeebe:Properties', null, businessObject, bpmnFactory);

      commands.push({
        cmd: 'element.updateModdleProperties',
        context: {
          element,
          moddleElement: extensionElements,
          properties: {
            values: [ ...extensionElements.get('values'), zeebeProperties ]
          }
        }
      });
    }

    const oldZeebeProperty = findZeebeProperty(zeebeProperties, binding);

    const properties = zeebeProperties.get('properties').filter((property) => property !== oldZeebeProperty);

    if (shouldUpdate(value, property)) {
      const newZeebeProperty = createZeebeProperty(binding, value, bpmnFactory);

      properties.push(newZeebeProperty);
    }

    commands.push({
      cmd: 'element.updateModdleProperties',
      context: {
        element,
        moddleElement: zeebeProperties,
        properties: {
          properties
        }
      }
    });
  }

  // bpmn:Message#zeebe:subscription#property
  if (type === MESSAGE_ZEEBE_SUBSCRIPTION_PROPERTY_TYPE) {
    let subscription = findExtension$1(extensionElements, 'zeebe:Subscription');
    const properties = {
      [ name ]: value || ''
    };

    if (subscription) {
      commands.push({
        cmd: 'element.updateModdleProperties',
        context: {
          element,
          properties,
          moddleElement: subscription
        }
      });
    } else {
      subscription = createElement('zeebe:Subscription', properties, extensionElements, bpmnFactory);

      commands.push({
        cmd: 'element.updateModdleProperties',
        context: {
          ...context,
          moddleElement: extensionElements,
          properties: { values: [ ...extensionElements.get('values'), subscription ] }
        }
      });
    }
  }

  // zeebe:calledElement
  if (type === ZEEBE_CALLED_ELEMENT) {
    let calledElement = findExtension$1(element, 'zeebe:CalledElement');
    const propertyName = binding.property;

    const properties = {
      [ propertyName ]: value || ''
    };

    if (calledElement) {
      commands.push({
        cmd: 'element.updateModdleProperties',
        context: {
          element,
          properties,
          moddleElement: calledElement
        }
      });
    } else {
      calledElement = createElement('zeebe:CalledElement', properties, extensionElements, bpmnFactory);

      commands.push({
        cmd: 'element.updateModdleProperties',
        context: {
          ...context,
          moddleElement: extensionElements,
          properties: { values: [ ...extensionElements.get('values'), calledElement ] }
        }
      });
    }
  }

  if (type === ZEEBE_LINKED_RESOURCE_PROPERTY) {
    let linkedResources = findExtension$1(businessObject, 'zeebe:LinkedResources');

    if (!linkedResources) {
      linkedResources = createElement('zeebe:LinkedResources', null, businessObject, bpmnFactory);

      commands.push({
        cmd: 'element.updateModdleProperties',
        context: {
          ...context,
          moddleElement: extensionElements,
          properties: { values: [ ...extensionElements.get('values'), linkedResources ] }
        }
      });
    }

    let linkedResource = linkedResources.get('values').find((value) => value.get('linkName') === linkName);

    if (!linkedResource) {
      linkedResource = createElement('zeebe:LinkedResource', { linkName }, businessObject, bpmnFactory);

      commands.push({
        cmd: 'element.updateModdleProperties',
        context: {
          ...context,
          moddleElement: linkedResources,
          properties: { values: [ ...linkedResources.get('values'), linkedResource ] }
        }
      });
    }

    commands.push({
      cmd: 'element.updateModdleProperties',
      context: {
        ...context,
        moddleElement: linkedResource,
        properties: { [ bindingProperty ]: value }
      }
    });
  }

  // zeebe:calledDecision
  if (type === ZEEBE_CALLED_DECISION) {
    let calledDecision = findExtension$1(element, 'zeebe:CalledDecision');
    const propertyName = binding.property;

    const properties = {
      [ propertyName ]: value || ''
    };

    if (calledDecision) {
      commands.push({
        cmd: 'element.updateModdleProperties',
        context: {
          element,
          properties,
          moddleElement: calledDecision
        }
      });
    } else {
      calledDecision = createElement('zeebe:CalledDecision', properties, extensionElements, bpmnFactory);

      commands.push({
        cmd: 'element.updateModdleProperties',
        context: {
          ...context,
          moddleElement: extensionElements,
          properties: { values: [ ...extensionElements.get('values'), calledDecision ] }
        }
      });
    }
  }

  // zeebe:script
  if (type === ZEEBE_SCRIPT_TASK) {
    let scriptTask = findExtension$1(element, 'zeebe:Script');
    const propertyName = binding.property;

    const properties = {
      [ propertyName ]: value || ''
    };

    if (scriptTask) {
      commands.push({
        cmd: 'element.updateModdleProperties',
        context: {
          element,
          properties,
          moddleElement: scriptTask
        }
      });
    } else {
      scriptTask = createElement('zeebe:Script', properties, extensionElements, bpmnFactory);

      commands.push({
        cmd: 'element.updateModdleProperties',
        context: {
          ...context,
          moddleElement: extensionElements,
          properties: { values: [ ...extensionElements.get('values'), scriptTask ] }
        }
      });
    }
  }


  if (type === ZEEBE_FORM_DEFINITION) {
    let formDefinition = findExtension$1(element, 'zeebe:FormDefinition');
    const propertyName = binding.property;

    const properties = {
      [ propertyName ]: value || ''
    };

    if (formDefinition) {
      commands.push({
        cmd: 'element.updateModdleProperties',
        context: {
          element,
          properties,
          moddleElement: formDefinition
        }
      });
    } else {
      formDefinition = createElement('zeebe:FormDefinition', properties, extensionElements, bpmnFactory);

      commands.push({
        cmd: 'element.updateModdleProperties',
        context: {
          ...context,
          moddleElement: extensionElements,
          properties: { values: [ ...extensionElements.get('values'), formDefinition ] }
        }
      });
    }
  }

  // zeebe:assignmentDefinition
  if (type === ZEEBE_ASSIGNMENT_DEFINITION) {
    let assignmentDefinition = findExtension$1(element, 'zeebe:AssignmentDefinition');
    const propertyName = binding.property;

    const properties = {
      [ propertyName ]: value || ''
    };

    if (assignmentDefinition) {
      commands.push({
        cmd: 'element.updateModdleProperties',
        context: {
          element,
          properties,
          moddleElement: assignmentDefinition
        }
      });
    } else {
      assignmentDefinition = createElement('zeebe:AssignmentDefinition', properties, extensionElements, bpmnFactory);

      commands.push({
        cmd: 'element.updateModdleProperties',
        context: {
          ...context,
          moddleElement: extensionElements,
          properties: { values: [ ...extensionElements.get('values'), assignmentDefinition ] }
        }
      });
    }
  }



  if (commands.length) {
    const commandsToExecute = commands.filter((command) => command !== NO_OP);

    commandsToExecute.length && commandStack.execute(
      'element-templates.multi-command-executor',
      commandsToExecute
    );

    return;
  }

  // should never throw as templates are validated beforehand
  throw unknownBindingError(element, property);
}

// helpers
function unknownBindingError(element, property) {
  const businessObject = ModelUtil.getBusinessObject(element);

  const id = businessObject.get('id');

  const { binding } = property;

  const { type } = binding;

  return new Error(`unknown binding <${ type }> for element <${ id }>, this should never happen`);
}

/**
 * Based on conditions, remove properties from the template.
 */
function applyConditions(element, elementTemplate) {
  const { properties } = elementTemplate;

  const filteredProperties = properties.filter(property => {
    return isPropertyAllowed(element, property) && isConditionMet(element, properties, property);
  });

  return {
    ...elementTemplate,
    properties: filteredProperties
  };
}

function isConditionMet(element, properties, property) {
  const { condition } = property;

  // If no condition is defined, return true.
  if (!condition) {
    return true;
  }

  // multiple ("and") conditions
  if (condition.allMatch) {
    const conditions = condition.allMatch;

    return conditions.every(
      condition => isSimpleConditionMet(element, properties, condition)
    );
  }

  // single condition
  return isSimpleConditionMet(element, properties, condition);
}

function isSimpleConditionMet(element, properties, condition) {
  const { property, equals, oneOf, isActive } = condition;

  if (typeof isActive !== 'undefined') {
    const relatedCondition = properties.find(p => p.id === property);

    if (!relatedCondition) {
      return !isActive;
    }

    return isActive ? isConditionMet(element, properties, relatedCondition) : !isConditionMet(element, properties, relatedCondition);
  }

  const propertyObject = getProperty(properties, property);

  if (propertyObject) {

    // if the property is a FEEL [optional, static, required] property,
    // we need to get the value removing the '='

    const propertyValue = shouldCastToFeel(propertyObject) || propertyObject.feel === 'required' ?
      getPropertyValue(element, propertyObject).slice(1) :
      getPropertyValue(element, propertyObject);

    if (hasProperty(condition, 'equals')) {
      return compareProperty(propertyObject, propertyValue, equals);
    }

    if (oneOf) {
      return oneOf.includes(propertyValue);
    }
  }

  return false;
}

function getProperty(properties, propertyId) {
  const property = properties.find(p => p.id === propertyId);

  if (!property) {
    return;
  }

  return property;
}

function isPropertyAllowed(element, property) {
  const { binding } = property;
  const { type } = binding;

  if (
    type === 'bpmn:Message#zeebe:subscription#property' &&
    binding.name === 'correlationKey' &&
    ModelUtil.is(element, 'bpmn:StartEvent') &&
    !DiUtil.isEventSubProcess(element.parent)
  ) {
    return false;
  }

  return true;
}

function compareProperty(propertyObj, propertyValue, value) {

  // Number
  if (propertyObj.type === 'Number') {
    return Number(propertyValue) === value;
  }

  // Boolean
  else if (propertyObj.type === 'Boolean') {
    return Boolean(propertyValue) === value;
  }

  return propertyValue === value;
}

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

function hasProperty(obj, key) {
  return Object.prototype.hasOwnProperty.call(obj, key);
}

class TemplateElementFactory {

  constructor(bpmnFactory, elementFactory) {
    this._bpmnFactory = bpmnFactory;
    this._elementFactory = elementFactory;

    this._providers = {
      [PROPERTY_TYPE]: PropertyBindingProvider,
      [ZEEBE_TASK_DEFINITION_TYPE_TYPE]: TaskDefinitionTypeBindingProvider,
      [ZEEBE_TASK_DEFINITION]: TaskDefinitionTypeBindingProvider,
      [ZEBBE_PROPERTY_TYPE]: ZeebePropertiesProvider,
      [ZEBBE_INPUT_TYPE]: InputBindingProvider,
      [ZEEBE_OUTPUT_TYPE]: OutputBindingProvider,
      [ZEEBE_TASK_HEADER_TYPE]: TaskHeaderBindingProvider,
      [MESSAGE_PROPERTY_TYPE]: MessagePropertyBindingProvider,
      [MESSAGE_ZEEBE_SUBSCRIPTION_PROPERTY_TYPE]: MessageZeebeSubscriptionBindingProvider,
      [ZEEBE_CALLED_ELEMENT]: CalledElementBindingProvider,
      [ZEEBE_LINKED_RESOURCE_PROPERTY]: LinkedResourcePropertyBindingProvider,
      [ZEEBE_USER_TASK]: ZeebeUserTaskBindingProvider,
      [ZEEBE_CALLED_DECISION]: CalledDecisionBindingProvider,
      [ZEEBE_FORM_DEFINITION]: ZeebeFormDefinitionBindingProvider,
      [ZEEBE_SCRIPT_TASK]: ScriptTaskBindingProvider,
      [ZEEBE_ASSIGNMENT_DEFINITION]: AssignmentDefinitionBindingProvider
    };
  }

  /**
   * Create an element based on an element template.
   *
   * @param {ElementTemplate} template
   * @returns {djs.model.Base}
   */
  create(template) {

    const {
      properties
    } = template;

    // (1) base shape
    const element = this._createShape(template);

    // (2) apply template
    this._setModelerTemplate(element, template);

    // (3) apply icon
    if (hasIcon(template)) {
      this._setModelerTemplateIcon(element, template);
    }

    // (4) apply properties
    this._applyProperties(element, properties);

    return element;
  }

  _createShape(template) {
    const {
      appliesTo,
      elementType = {}
    } = template;
    const elementFactory = this._elementFactory;

    const attrs = {
      type: elementType.value || appliesTo[0]
    };

    // apply eventDefinition
    if (elementType.eventDefinition) {
      attrs.eventDefinitionType = elementType.eventDefinition;
    }

    const element = elementFactory.createShape(attrs);

    return element;
  }

  _ensureExtensionElements(element) {
    const bpmnFactory = this._bpmnFactory;
    const businessObject = ModelUtil.getBusinessObject(element);

    let extensionElements = businessObject.get('extensionElements');

    if (!extensionElements) {
      extensionElements = bpmnFactory.create('bpmn:ExtensionElements', {
        values: []
      });

      extensionElements.$parent = businessObject;
      businessObject.set('extensionElements', extensionElements);
    }

    return extensionElements;
  }

  _setModelerTemplate(element, template) {
    const {
      id,
      version
    } = template;

    const businessObject = ModelUtil.getBusinessObject(element);

    businessObject.set('zeebe:modelerTemplate', id);
    businessObject.set('zeebe:modelerTemplateVersion', version);
  }

  _setModelerTemplateIcon(element, template) {
    const {
      icon
    } = template;

    const {
      contents
    } = icon;

    const businessObject = ModelUtil.getBusinessObject(element);

    businessObject.set('zeebe:modelerTemplateIcon', contents);
  }

  /**
   * Apply properties to a given element.
   *
   * @param {djs.model.Base} element
   * @param {Array<Object>} properties
   */
  _applyProperties(element, properties) {
    const processedProperties = [];

    properties.forEach(
      property => this._applyProperty(element, property, properties, processedProperties)
    );
  }

  /**
   * Apply a property and its parent properties to an element based on conditions.
   *
   * @param {djs.model.Base} element
   * @param {Object} property
   * @param {Array<Object>} properties
   * @param {Array<Object>} processedProperties
   */
  _applyProperty(element, property, properties, processedProperties) {

    // skip if already processed
    if (processedProperties.includes(property)) {
      return;
    }

    // apply dependant property first if not already applied
    const dependentProperties = findDependentProperties(property, properties);

    dependentProperties.forEach(
      property => this._applyProperty(element, property, properties, processedProperties)
    );

    // check condition and apply property if condition is met
    if (isConditionMet(element, properties, property)) {
      this._bindProperty(property, element);
    }

    processedProperties.push(property);
  }

  /**
   * Bind property to element.
   * @param {Object} property
   * @param {djs.Model.Base} element
   */
  _bindProperty(property, element) {
    const {
      binding
    } = property;

    const {
      type: bindingType
    } = binding;

    const bindingProvider = this._providers[bindingType];

    bindingProvider.create(element, {
      property,
      bpmnFactory: this._bpmnFactory
    });
  }
}

TemplateElementFactory.$inject = [ 'bpmnFactory', 'elementFactory' ];


// helper ////////////////

function hasIcon(template) {
  const {
    icon
  } = template;

  return !!(icon && icon.contents);
}

function findDependentProperties(property, properties) {

  const {
    condition
  } = property;

  if (!condition) {
    return [];
  }

  const dependentProperty = findProperyById(properties, condition.property);

  if (dependentProperty) {
    return [ dependentProperty ];
  }

  return [];
}

function findProperyById(properties, id) {
  return minDash.find(properties, function(property) {
    return property.id === id;
  });
}

var createModule = {
  __init__: [
    'templateElementFactory'
  ],
  templateElementFactory: [ 'type', TemplateElementFactory ]
};

/**
 * Checks the conditions of an element template and sets/resets the
 * corresponding properties on the element.
 */
class ConditionalBehavior extends CommandInterceptor {

  /**
   * @param {import('diagram-js/lib/core/EventBus').default} eventBus
   * @param {import('../ElementTemplates').default} elementTemplates
   * @param {import('diagram-js/lib/command/CommandStack').default} commandStack
   * @param {import('bpmn-js/lib/features/modeling/BpmnFactory').default} bpmnFactory
   * @param {import('didi').Injector} injector
   */
  constructor(eventBus, elementTemplates, commandStack, bpmnFactory, injector) {
    super(eventBus);

    this._eventBus = eventBus;
    this._elementTemplates = elementTemplates;
    this._commandStack = commandStack;
    this._bpmnFactory = bpmnFactory;
    this._injector = injector;

    // (1) save pre-conditional state before updating a property
    this.preExecute([
      'element.updateProperties',
      'element.updateModdleProperties',
      'element.move'
    ], this._saveConditionalState, true, this);

    // (2) check if we need to apply post-conditional updates
    //
    //   if [additional bindings activate] then
    //     (re-)trigger setting the template
    //   else
    //     else we're done
    //
    this.postExecute([
      'element.updateProperties',
      'element.updateModdleProperties',
      'propertiesPanel.zeebe.changeTemplate',
      'element.move'
    ], this._applyConditions, true, this);

    // (3) set conditions before changing the template
    this.preExecute([
      'propertiesPanel.zeebe.changeTemplate'
    ], this._ensureConditional, true, this);
  }

  _saveConditionalState(context) {
    const {
      element
    } = context;

    const template = this._elementTemplates.get(element);

    if (!template) {
      return;
    }

    context.oldTemplateWithConditions = applyConditions(element, template);
  }

  _applyConditions(context, event) {
    const {
      element,
      newTemplate,
      oldTemplateWithConditions
    } = context;

    // do not apply conditions if we only update meta information
    if (isMetaUpdate(event, context)) {
      return;
    }

    const template = this._elementTemplates.get(element);

    // new Template is persisted before applying default values,
    // new conditions might apply after the defaults are present.
    const oldTemplate = oldTemplateWithConditions || newTemplate;

    if (!template || !oldTemplate) {
      return;
    }

    const newTemplateWithConditions = applyConditions(element, template);

    // verify that new bindings were activated
    if (!hasDifferentPropertyBindings(newTemplateWithConditions, oldTemplate)) {
      return;
    }

    // do another pass to apply further conditional bindings
    // newTemplate will always be the original template; it is filtered
    // at a later step (3)
    const changeContext = {
      element,
      newTemplate: template,
      oldTemplate
    };

    this._commandStack.execute('propertiesPanel.zeebe.changeTemplate', changeContext);
  }

  _ensureConditional(context) {
    const {
      element,
      newTemplate
    } = context;

    if (!newTemplate) {
      return;
    }

    // ensure conditions are applied before changing the template.
    // `newTemplate` will always be the original template.
    context.newTemplate = applyConditions(element, newTemplate);
  }

}


ConditionalBehavior.$inject = [
  'eventBus',
  'elementTemplates',
  'commandStack',
  'bpmnFactory',
  'injector'
];


// helpers

function hasDifferentPropertyBindings(sourceTemplate, targetTemplate) {
  return hasNewProperties(sourceTemplate, targetTemplate) || hasRemovedProperties(sourceTemplate, targetTemplate);
}

function hasNewProperties(sourceTemplate, targetTemplate) {
  let properties = targetTemplate.properties;

  return properties.some(targetProp =>!(
    sourceTemplate.properties.find(sourceProp => compareProps(sourceProp, targetProp))
  ));
}

function hasRemovedProperties(oldTemplate, newTemplate) {
  const oldProperties = getMissingProperties(newTemplate, oldTemplate);

  // ensure XML properties are mantained for properties with
  // different conditions but same bindings
  return oldProperties.some(property =>
    !findPropertyWithBinding(newTemplate, property)
  );
}

function getMissingProperties(sourceTemplate, targetTemplate) {

  let properties = targetTemplate.properties;

  return properties.filter(targetProp =>!(
    sourceTemplate.properties.find(sourceProp => compareProps(sourceProp, targetProp))
  ));
}

function compareProps(sourceProp, targetProp) {
  return (
    areBindingsEqual(sourceProp.binding, targetProp.binding) &&
    equals(sourceProp.condition, targetProp.condition)
  );
}

function findPropertyWithBinding(template, prop1) {
  return template.properties.some(
    prop2 => areBindingsEqual(prop1.binding, prop2.binding)
  );
}

function normalizeReplacer$1(key, value) {

  if (minDash.isObject(value)) {
    const keys = Object.keys(value).sort();

    return keys.reduce((obj, key) => {
      obj[key] = value[key];

      return obj;
    }, {});
  }

  return value;
}

function areBindingsEqual(binding1, binding2) {
  binding1 = normalizeBinding(binding1);
  binding2 = normalizeBinding(binding2);

  return equals(binding1, binding2);
}

/**
 * Convert deprecated binding type to new type.
 */
function normalizeBinding(binding) {
  if (binding.type === ZEEBE_TASK_DEFINITION_TYPE_TYPE) {
    return {
      ...binding,
      type: ZEEBE_TASK_DEFINITION,
      property: 'type'
    };
  }

  return binding;
}

function equals(a, b) {
  return JSON.stringify(a, normalizeReplacer$1) === JSON.stringify(b, normalizeReplacer$1);
}

/**
 * Checks if the event only updates "meta" properties:
 *
 *   * `zeebe:modelerTemplate`
 *   * `zeebe:modelerTemplateVersion`
 *
 * @param {string} event
 * @param {any} context
 *
 * @return {boolean}
 */
function isMetaUpdate(event, context) {
  return event === 'element.updateProperties' && (
    Object.keys(context.properties).every(
      key => [ 'zeebe:modelerTemplate', 'zeebe:modelerTemplateVersion' ].includes(key)
    ));
}

/**
 * This Behavior checks if the new element's type is in
 * the list of elements the template applies to and unlinks
 * it if not.
 */
let ReplaceBehavior$1 = class ReplaceBehavior extends CommandInterceptor {
  constructor(elementTemplates, injector) {
    super(injector.get('eventBus'));

    this.postExecuted('shape.replace', function(e) {
      var context = e.context,
          oldShape = context.oldShape,
          oldBo = ModelUtil.getBusinessObject(oldShape),
          newShape = context.newShape,
          newBo = ModelUtil.getBusinessObject(newShape);

      if (!oldBo.modelerTemplate) {
        return;
      }

      const template = newBo.modelerTemplate;
      const version = newBo.modelerTemplateVersion;

      const elementTemplate = elementTemplates.get(template, version);

      if (!elementTemplate) {
        elementTemplates.unlinkTemplate(newShape);
        return;
      }

      const { appliesTo, elementType } = elementTemplate;

      if (elementType) {
        if (!ModelUtil.is(newShape, elementType.value) || shouldUnlinkEvent(newShape, elementType)) {
          elementTemplates.unlinkTemplate(newShape);
        }

        return;
      }

      const allowed = appliesTo.reduce((allowed, type) => {
        return allowed || ModelUtil.is(newBo, type);
      }, false);

      if (!allowed) {
        elementTemplates.unlinkTemplate(newShape);
      }
    });
  }
};

ReplaceBehavior$1.$inject = [
  'elementTemplates',
  'injector'
];

function shouldUnlinkEvent(newShape, elementType) {
  if (!ModelUtil.is(newShape, 'bpmn:Event')) {
    return false;
  }

  const { eventDefinition } = elementType,
        bo = ModelUtil.getBusinessObject(newShape),
        eventDefinitions = bo.get('eventDefinitions');

  if (!eventDefinition) {
    return eventDefinitions.length !== 0;
  }

  return !ModelUtil.is(eventDefinitions[0], eventDefinition);
}

/**
 * Restores the original order of the template properties
 * on the moddle element.
 */
class UpdateTemplatePropertiesOrder extends CommandInterceptor {
  constructor(eventBus, elementTemplates, commandStack, bpmnFactory) {
    super(eventBus);

    this._eventBus = eventBus;
    this._elementTemplates = elementTemplates;
    this._commandStack = commandStack;
    this._bpmnFactory = bpmnFactory;

    this.postExecute([
      'element.updateProperties', 'element.updateModdleProperties'
    ], this._updatePropertiesOrder, true, this);
  }

  _updatePropertiesOrder(context) {
    const {
      element
    } = context;

    const template = this._elementTemplates.get(element);
    const businessObject = ModelUtil.getBusinessObject(element);
    const commands = [];

    if (!template) {
      return;
    }

    const templateProperties = applyConditions(element, template).properties;

    // zeebe:Property
    const zeebeProperties = findExtension$1(businessObject, 'zeebe:Properties');

    if (zeebeProperties) {
      this._updateZeebePropertiesOrder(zeebeProperties, templateProperties, commands, context);
    }

    // zeebe:IoMapping
    const ioMapping = findExtension$1(businessObject, 'zeebe:IoMapping');

    if (ioMapping) {

      // zeebe:Input
      this._updateInputOrder(ioMapping, templateProperties, commands, context);

      // zeebe:Output
      this._updateOutputOrder(ioMapping, templateProperties, commands, context);
    }

    // zeebe:TaskHeaders
    const taskHeaders = findExtension$1(businessObject, 'zeebe:TaskHeaders');

    if (taskHeaders) {
      this._updateTaskHeadersOrder(taskHeaders, templateProperties, commands, context);
    }


    if (commands.length) {
      const commandsToExecute = commands.filter((command) => command !== null);

      commandsToExecute.length && this._commandStack.execute(
        'element-templates.multi-command-executor',
        commandsToExecute
      );

      return;
    }
  }

  _updateZeebePropertiesOrder(zeebeProperties, templateProperties, commands, context) {
    const findIndex = (properties, propertyToFind) =>
      properties.findIndex(prop =>
        prop.binding.type == 'zeebe:property' && prop.binding.name === propertyToFind.get('name')
      );

    const properties = zeebeProperties.get('properties');

    if (properties.length < 1)
      return;

    let newPropertiesOrder = [ ...properties ];

    sortProperties(newPropertiesOrder, findIndex, templateProperties);

    if (!arrayEquals(newPropertiesOrder, properties)) {

      commands.push({
        cmd: 'element.updateModdleProperties',
        context: {
          ...context,
          moddleElement: zeebeProperties,
          properties: {
            properties: newPropertiesOrder
          }
        }
      });
    }
  }

  _updateInputOrder(ioMapping, templateProperties, commands, context) {

    const findIndex = (properties, propertyToFind) =>
      properties.findIndex(prop =>
        prop.binding.type == 'zeebe:input' && prop.binding.name === propertyToFind.get('target')
      );

    const inputParameters = ioMapping.get('inputParameters');

    if (inputParameters.length < 1)
      return;

    let newInputOrder = [ ...inputParameters ];

    sortProperties(newInputOrder, findIndex, templateProperties);

    if (!arrayEquals(newInputOrder,inputParameters)) {

      commands.push({
        cmd: 'element.updateModdleProperties',
        context: {
          ...context,
          moddleElement: ioMapping,
          properties: { inputParameters: newInputOrder }
        }
      });
    }
  }

  _updateOutputOrder(ioMapping, templateProperties, commands, context) {

    const findIndex = (properties, propertyToFind) =>
      properties.findIndex(prop =>
        prop.binding.type == 'zeebe:output' && prop.binding.source === propertyToFind.get('source')
      );

    const outputParameters = ioMapping.get('outputParameters');

    if (outputParameters.length < 1)
      return;

    let newOutputOrder = [ ...outputParameters ];

    sortProperties(newOutputOrder, findIndex, templateProperties);

    if (!arrayEquals(newOutputOrder, outputParameters)) {

      commands.push({
        cmd: 'element.updateModdleProperties',
        context: {
          ...context,
          moddleElement: ioMapping,
          properties: { outputParameters: newOutputOrder }
        }
      });
    }
  }

  _updateTaskHeadersOrder(taskHeaders, templateProperties, commands, context) {

    const findIndex = (properties, propertyToFind) =>
      properties.findIndex(prop =>
        prop.binding.type == 'zeebe:taskHeader' && prop.binding.key === propertyToFind.get('key')
      );

    const headers = taskHeaders.get('zeebe:values');

    if (headers.length < 1)
      return;

    let newHeadersOrder = [ ...headers ];

    sortProperties(newHeadersOrder, findIndex, templateProperties);

    if (!arrayEquals(newHeadersOrder, headers)) {
      commands.push({
        cmd: 'element.updateModdleProperties',
        context: {
          ...context,
          moddleElement: taskHeaders,
          properties: { values: newHeadersOrder }
        }
      });
    }
  }

}

UpdateTemplatePropertiesOrder.$inject = [
  'eventBus',
  'elementTemplates',
  'commandStack',
  'bpmnFactory'
];


// helpers

function normalizeReplacer(key, value) {

  if (minDash.isObject(value)) {
    const keys = Object.keys(value).sort();

    return keys.reduce((obj, key) => {
      obj[key] = value[key];

      return obj;
    }, {});
  }

  return value;
}

function objectEquals(a, b) {
  return JSON.stringify(a, normalizeReplacer) === JSON.stringify(b, normalizeReplacer);
}

function arrayEquals(a, b) {
  return a.every((element, idx) => objectEquals(element, b[idx]));
}

function sortProperties(array, findIndex, templateProperties) {
  return array.sort((a, b) => {
    const aIndex = findIndex(templateProperties, a);
    const bIndex = findIndex(templateProperties, b);

    return aIndex - bIndex;
  });
}

/**
 * Handles referenced elements.
 */
class ReferencedElementBehavior extends CommandInterceptor {
  constructor(eventBus, elementTemplates, modeling, injector, moddleCopy, bpmnFactory) {
    super(eventBus);

    this._eventBus = eventBus;
    this._elementTemplates = elementTemplates;
    this._modeling = modeling;
    this._injector = injector;

    this.postExecuted([
      'element.updateProperties', 'element.updateModdleProperties'
    ], this._handlePropertiesUpdate, true, this);

    this.postExecuted('shape.replace', this._handleReplacement, true, this);

    this.postExecuted('shape.delete', this._handleRemoval, true, this);


    // copy templated root element when pasting
    eventBus.on('copyPaste.pasteElement', function(context) {
      const {
        referencedRootElement
      } = context.descriptor;

      if (!referencedRootElement) {
        return;
      }

      if (!getTemplateId$1(referencedRootElement)) {
        return;
      }

      context.descriptor.referencedRootElement = moddleCopy.copyElement(
        referencedRootElement,
        bpmnFactory.create(referencedRootElement.$type)
      );
    });
  }

  /**
   * Unlink referenced element when template is unlinked.
   */
  _handlePropertiesUpdate(context) {
    const { element, properties } = context;

    if (!canHaveReferencedElement(element)) {
      return;
    }

    if (!(TEMPLATE_ID_ATTR$1 in properties) || minDash.isString(properties[TEMPLATE_ID_ATTR$1])) {
      return;
    }

    const bo = ModelUtil.getBusinessObject(element);
    const message = findMessage(bo);

    if (message && getTemplateId$1(message)) {
      this._modeling.updateModdleProperties(element, message, {
        [TEMPLATE_ID_ATTR$1]: null
      });
    }
  }

  /**
   * Remove referenced element when template is removed.
   * Keep referenced element when template is replaced.
   */
  _handleReplacement(context) {
    const { oldShape, newShape } = context;
    const oldTemplate = getTemplateId$1(oldShape),
          newTemplate = getTemplateId$1(newShape);

    if (!canHaveReferencedElement(oldShape) || !oldTemplate) {
      return;
    }

    const bo = ModelUtil.getBusinessObject(oldShape);
    const message = findMessage(bo);

    if (!message || !getTemplateId$1(message)) {
      return;
    }

    if (!canHaveReferencedElement(newShape) || !newTemplate) {
      removeRootElement(message, this._injector);
      return;
    }

    this._addMessage(newShape, message);
  }

  _handleRemoval(context) {
    const { shape } = context;

    if (isLabel(shape)) {
      return;
    }

    if (!canHaveReferencedElement(shape)) {
      return;
    }

    if (!getTemplateId$1(shape)) {
      return;
    }

    const bo = ModelUtil.getBusinessObject(shape);
    const message = findMessage(bo);

    if (message && getTemplateId$1(message)) {
      removeRootElement(message, this._injector);
    }
  }

  _addMessage(element, message) {
    const bo = getReferringElement(element);

    this._modeling.updateModdleProperties(element, bo, {
      'messageRef': message
    });
  }
}

ReferencedElementBehavior.$inject = [
  'eventBus',
  'elementTemplates',
  'modeling',
  'injector',
  'moddleCopy',
  'bpmnFactory'
];

function canHaveReferencedElement(element) {

  // Blank-Events can't have referenced elements
  if (ModelUtil.is(element, 'bpmn:Event')) {
    const bo = ModelUtil.getBusinessObject(element);
    return bo.get('eventDefinitions') && bo.get('eventDefinitions')[0];
  }

  return ModelUtil.isAny(element, [
    'bpmn:ReceiveTask',
    'bpmn:SendTask'
  ]);
}

function isLabel(element) {
  return element.type === 'label';
}

/**
 * Handles generated value properties.
 */
class GeneratedValueBehavior extends CommandInterceptor {
  constructor(eventBus, elementTemplates, modeling, commandStack, bpmnFactory) {
    super(eventBus);

    this._eventBus = eventBus;
    this._elementTemplates = elementTemplates;
    this._modeling = modeling;

    this.preExecute('shape.create', context => {
      const element = context.shape;
      const template = elementTemplates.get(element);

      if (!template) {
        return;
      }

      const generatedProps = template.properties.filter(p => p.generatedValue);

      generatedProps.forEach(p => {
        if (!getPropertyValue(element, p)) {
          return;
        }

        const value = getDefaultValue(p);

        setPropertyValue(bpmnFactory, commandStack, element, p, value);
      });
    }, true);
  }
}

GeneratedValueBehavior.$inject = [
  'eventBus',
  'elementTemplates',
  'modeling',
  'commandStack',
  'bpmnFactory'
];

/**
 * Enforces no variable propagation for templated call activities.
 */
class CalledElementBehavior extends CommandInterceptor {

  /**
   * @param {*} eventBus
   * @param {*} modeling
   * @param {import('../ElementTemplates').default} elementTemplates
   */
  constructor(eventBus, modeling, elementTemplates) {
    super(eventBus);

    this._modeling = modeling;
    this._elementTemplates = elementTemplates;

    this.postExecuted([
      'element.updateProperties', 'element.updateModdleProperties'
    ], this._ensureNoPropagation, true, this);
  }

  _ensureNoPropagation(context) {
    const { element } = context;

    if (!this._elementTemplates.get(element)) {
      return;
    }

    if (!ModelUtil.is(element, 'bpmn:CallActivity')) {
      return;
    }

    const calledElement = findExtension$1(element, 'zeebe:CalledElement');

    if (!calledElement) {
      return;
    }

    for (const property of [
      'propagateAllChildVariables',
      'propagateAllParentVariables'
    ]) {
      if (calledElement.get(property) !== false) {
        this._modeling.updateModdleProperties(element, calledElement, {
          [property]: false
        });
      }
    }
  }
}

CalledElementBehavior.$inject = [
  'eventBus',
  'modeling',
  'elementTemplates'
];

/**
 * Disable the default Camunda behavior for a newly created element
 * if it is a templated `<bpmn:UserTask>`.
 */
class UserTaskBehavior extends CommandInterceptor {
  constructor(eventBus, elementTemplates) {
    super(eventBus);

    this.preExecute('shape.create', function(event) {
      const {
        context: {
          shape,
          hints = {},
        }
      } = event;

      if (ModelUtil.is(shape, 'bpmn:UserTask') && elementTemplates.get(shape)) {
        minDash.assign(hints, {
          createElementsBehavior: false
        });
      }
    });
  }
}

UserTaskBehavior.$inject = [
  'eventBus',
  'elementTemplates'
];

var behaviorModule$1 = {
  __init__: [
    'elementTemplatesReplaceBehavior',
    'elementTemplatesConditionalBehavior',
    'elementTemplatesGeneratedValueBehavior',
    'elementTemplatesReferencedElementBehavior',
    'elementTemplatesUpdatePropertiesOrderBehavior',
    'elementTemplatesCalledElementBehavior',
    'elementTemplatesUserTaskBehavior'
  ],
  elementTemplatesReplaceBehavior: [ 'type', ReplaceBehavior$1 ],
  elementTemplatesConditionalBehavior: [ 'type', ConditionalBehavior ],
  elementTemplatesGeneratedValueBehavior: [ 'type', GeneratedValueBehavior ],
  elementTemplatesReferencedElementBehavior: [ 'type', ReferencedElementBehavior ],
  elementTemplatesUpdatePropertiesOrderBehavior: [ 'type', UpdateTemplatePropertiesOrder ],
  elementTemplatesCalledElementBehavior: [ 'type', CalledElementBehavior ],
  elementTemplatesUserTaskBehavior: [ 'type', UserTaskBehavior ]
};

var index$1 = {
  __depends__: [
    commandsModule$1,
    behaviorModule$1,
    createModule
  ],
  __init__: [
    'elementTemplatesLoader'
  ],
  elementTemplates: [ 'type', ElementTemplates ],
  elementTemplatesLoader: [ 'type', ElementTemplatesLoader ]
};

/**
 * Converts legacy scopes descriptor to newer supported array structure.
 *
 * For example, it transforms
 *
 * scopes: {
 *   'camunda:Connector':
 *     { properties: []
 *   }
 * }
 *
 * to
 *
 * scopes: [
 *   {
 *     type: 'camunda:Connector',
 *     properties: []
 *   }
 * ]
 *
 * @param {ScopesDescriptor} scopes
 *
 * @returns {Array}
 */
function handleLegacyScopes(scopes = []) {
  const scopesAsArray = [];

  if (!minDash.isObject(scopes)) {
    return scopes;
  }

  minDash.forEach(minDash.keys(scopes), function(scopeName) {
    scopesAsArray.push(minDash.assign({
      type: scopeName
    }, scopes[scopeName]));
  });

  return scopesAsArray;
}

/**
 * Create an input parameter representing the given
 * binding and value.
 *
 * @param {PropertyBinding} binding
 * @param {String} value
 * @param {BpmnFactory} bpmnFactory
 *
 * @return {ModdleElement}
 */
function createInputParameter(binding, value, bpmnFactory) {
  const {
    name,
    scriptFormat
  } = binding;

  let parameterValue,
      parameterDefinition;

  if (scriptFormat) {
    parameterDefinition = bpmnFactory.create('camunda:Script', {
      scriptFormat,
      value
    });
  } else {
    parameterValue = value;
  }

  return bpmnFactory.create('camunda:InputParameter', {
    name,
    value: parameterValue,
    definition: parameterDefinition
  });
}


/**
 * Create an output parameter representing the given
 * binding and value.
 *
 * @param {PropertyBinding} binding
 * @param {String} value
 * @param {BpmnFactory} bpmnFactory
 *
 * @return {ModdleElement}
 */
function createOutputParameter(binding, value, bpmnFactory) {
  const {
    scriptFormat,
    source
  } = binding;

  let parameterValue,
      parameterDefinition;

  if (scriptFormat) {
    parameterDefinition = bpmnFactory.create('camunda:Script', {
      scriptFormat,
      value: source
    });
  } else {
    parameterValue = source;
  }

  return bpmnFactory.create('camunda:OutputParameter', {
    name: value,
    value: parameterValue,
    definition: parameterDefinition
  });
}


/**
 * Create camunda property from the given binding.
 *
 * @param {PropertyBinding} binding
 * @param {String} value
 * @param {BpmnFactory} bpmnFactory
 *
 * @return {ModdleElement}
 */
function createCamundaProperty(binding, value = '', bpmnFactory) {
  const { name } = binding;

  return bpmnFactory.create('camunda:Property', {
    name,
    value
  });
}


/**
 * Create camunda:in element from given binding.
 *
 * @param {PropertyBinding} binding
 * @param {String} value
 * @param {BpmnFactory} bpmnFactory
 *
 * @return {ModdleElement}
 */
function createCamundaIn(binding, value, bpmnFactory) {
  const attrs = createCamundaInOutAttrs(binding, value);

  return bpmnFactory.create('camunda:In', attrs);
}


/**
 * Create camunda:in with businessKey element from given binding.
 *
 * @param {String} value
 * @param {BpmnFactory} bpmnFactory
 *
 * @return {ModdleElement}
 */
function createCamundaInWithBusinessKey(value, bpmnFactory) {
  return bpmnFactory.create('camunda:In', {
    businessKey: value
  });
}


/**
 * Create camunda:out element from given binding.
 *
 * @param {PropertyBinding} binding
 * @param {String} value
 * @param {BpmnFactory} bpmnFactory
 *
 * @return {ModdleElement}
 */
function createCamundaOut(binding, value, bpmnFactory) {
  const attrs = createCamundaInOutAttrs(binding, value);

  return bpmnFactory.create('camunda:Out', attrs);
}


/**
 * Create camunda:executionListener element containing an inline script from given binding.
 *
 * @param {PropertyBinding} binding
 * @param {String} value
 * @param {BpmnFactory} bpmnFactory
 *
 * @return {ModdleElement}
 */
function createCamundaExecutionListener(binding, value, bpmnFactory) {
  const {
    event,
    implementationType,
    scriptFormat
  } = binding;

  // To guarantee backwards compatibility scriptFormat is taken into account and has precedence before any other type
  if (implementationType === 'script' || scriptFormat) {
    return bpmnFactory.create('camunda:ExecutionListener', {
      event,
      script: bpmnFactory.create('camunda:Script', {
        scriptFormat,
        value
      })
    });
  }

  return bpmnFactory.create('camunda:ExecutionListener', {
    event,
    [implementationType]: value
  });
}

/**
 * Create camunda:field element containing string or expression from given binding.
 *
 * @param {PropertyBinding} binding
 * @param {String} value
 * @param {BpmnFactory} bpmnFactory
 *
 * @return {ModdleElement}
 */
function createCamundaFieldInjection(binding, value, bpmnFactory) {
  const DEFAULT_PROPS = {
    'string': undefined,
    'expression': undefined,
    'name': undefined
  };

  const props = minDash.assign({}, DEFAULT_PROPS);

  const {
    expression,
    name
  } = binding;

  if (!expression) {
    props.string = value;
  } else {
    props.expression = value;
  }
  props.name = name;

  return bpmnFactory.create('camunda:Field', props);
}


/**
 * Create camunda:errorEventDefinition element containing expression and errorRef
 * from given binding.
 *
 * @param {String} expression
 * @param {ModdleElement} errorRef
 * @param {ModdleElement} parent
 * @param {BpmnFactory} bpmnFactory
 *
 * @return {ModdleElement}
 */
function createCamundaErrorEventDefinition(expression, errorRef, parent, bpmnFactory) {
  const errorEventDefinition = bpmnFactory.create('camunda:ErrorEventDefinition', {
    errorRef,
    expression
  });

  errorEventDefinition.$parent = parent;

  return errorEventDefinition;
}


/**
 * Create bpmn:error element containing a specific error id given by a binding.
 *
 * @param {String} bindingErrorRef
 * @param {ModdleElement} parent
 * @param {BpmnFactory} bpmnFactory
 *
 * @return { ModdleElement }
 */
function createError(bindingErrorRef, parent, bpmnFactory) {
  const error = bpmnFactory.create('bpmn:Error', {

    // we need to later retrieve the error from a binding
    id: nextId('Error_' + bindingErrorRef + '_')
  });

  error.$parent = parent;

  return error;
}


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

/**
 * Create properties for camunda:in and camunda:out types.
 */
function createCamundaInOutAttrs(binding, value) {
  const properties = {};

  const {
    expression,
    source,
    sourceExpression,
    target,
    type,
    variables
  } = binding;

  // explicitly cover all conditions as specified here:
  // https://github.com/camunda/camunda-modeler/blob/develop/docs/element-templates/README.md#camundain
  if (type === 'camunda:in') {
    if (target && !expression && !variables) {
      properties.target = target;
      properties.source = value;

    } else if (target && expression === true && !variables) {
      properties.target = target;
      properties.sourceExpression = value;

    } else if (!target && !expression && variables === 'local') {
      properties.local = true;
      properties.variables = 'all';

    } else if (target && !expression && variables === 'local') {
      properties.local = true;
      properties.source = value;
      properties.target = target;

    } else if (target && expression && variables === 'local') {
      properties.local = true;
      properties.sourceExpression = value;
      properties.target = target;

    } else if (!target && !expression && variables === 'all') {
      properties.variables = 'all';
    } else {
      throw new Error('invalid configuration for camunda:in element template binding');
    }
  }

  // explicitly cover all conditions as specified here:
  // https://github.com/camunda/camunda-modeler/blob/develop/docs/element-templates/README.md#camundaout
  if (type === 'camunda:out') {
    if (source && !sourceExpression && !variables) {
      properties.target = value;
      properties.source = source;

    } else if (!source && sourceExpression && !variables) {
      properties.target = value;
      properties.sourceExpression = sourceExpression;

    } else if (!source && !sourceExpression && variables === 'all') {
      properties.variables = 'all';

    } else if (source && !sourceExpression && variables === 'local') {
      properties.local = true;
      properties.source = source;
      properties.target = value;

    } else if (!source && sourceExpression && variables === 'local') {
      properties.local = true;
      properties.sourceExpression = sourceExpression;
      properties.target = value;

    } else if (!source && !sourceExpression && variables === 'local') {
      properties.local = true;
      properties.variables = 'all';
    } else {
      throw new Error('invalid configuration for camunda:out element template binding');
    }
  }

  return properties;
}

function getEventDefinition(element, eventType) {
  const businessObject = ModelUtil.getBusinessObject(element);

  const eventDefinitions = businessObject.get('eventDefinitions') || [];

  return minDash.find(eventDefinitions, function(definition) {
    return ModelUtil.is(definition, eventType);
  });
}

function getSignalEventDefinition(element) {
  return getEventDefinition(element, 'bpmn:SignalEventDefinition');
}

const CAMUNDA_SERVICE_TASK_LIKE = [
  'camunda:class',
  'camunda:delegateExpression',
  'camunda:expression'
];

/**
 * Applies an element template to an element. Sets `camunda:modelerTemplate` and
 * `camunda:modelerTemplateVersion`.
 */
class ChangeElementTemplateHandler {
  constructor(bpmnFactory, bpmnReplace, commandStack, modeling) {
    this._bpmnFactory = bpmnFactory;
    this._bpmnReplace = bpmnReplace;
    this._commandStack = commandStack;
    this._modeling = modeling;
  }

  /**
   * Change an element's template and update its properties as specified in `newTemplate`. Specify
   * `oldTemplate` to update from one template to another. If `newTemplate` isn't specified the
   * `camunda:modelerTemplate` and `camunda:modelerTemplateVersion` properties will be removed from
   * the element.
   *
   * @param {Object} context
   * @param {Object} context.element
   * @param {Object} [context.oldTemplate]
   * @param {Object} [context.newTemplate]
   */
  preExecute(context) {
    const newTemplate = context.newTemplate,
          oldTemplate = context.oldTemplate;

    let element = context.element;

    // update camunda:modelerTemplate attribute
    this._updateCamundaModelerTemplate(element, newTemplate);

    if (newTemplate) {

      element = context.element = this._updateTaskType(element, newTemplate);

      // update properties
      this._updateProperties(element, oldTemplate, newTemplate);

      // update camunda:ExecutionListener properties
      this._updateCamundaExecutionListenerProperties(element, newTemplate);

      // update camunda:Field properties
      this._updateCamundaFieldProperties(element, oldTemplate, newTemplate);

      // update camunda:In and camunda:Out properties
      this._updateCamundaInOutProperties(element, oldTemplate, newTemplate);

      // update camunda:InputParameter and camunda:OutputParameter properties
      this._updateCamundaInputOutputParameterProperties(element, oldTemplate, newTemplate);

      // update camunda:Property properties
      this._updateCamundaPropertyProperties(element, oldTemplate, newTemplate);

      // update camunda:ErrorEventDefinition properties
      this._updateCamundaErrorEventDefinitionProperties(element, oldTemplate, newTemplate);

      // update properties for each scope
      handleLegacyScopes(newTemplate.scopes).forEach((newScopeTemplate) => {
        this._updateScopeProperties(element, oldTemplate, newScopeTemplate, newTemplate);
      });

    }
  }

  _getOrCreateExtensionElements(element) {
    const bpmnFactory = this._bpmnFactory,
          modeling = this._modeling;

    const businessObject = ModelUtil.getBusinessObject(element);

    let extensionElements = businessObject.get('extensionElements');

    if (!extensionElements) {
      extensionElements = bpmnFactory.create('bpmn:ExtensionElements', {
        values: []
      });

      extensionElements.$parent = businessObject;

      modeling.updateProperties(element, {
        extensionElements: extensionElements
      });
    }

    return extensionElements;
  }

  /**
   * Update `camunda:ErrorEventDefinition` properties of specified business object. Event
   * definitions can only exist in `bpmn:ExtensionElements`.
   *
   * Ensures an bpmn:Error exists for the event definition.
   *
   * @param {djs.model.Base} element
   * @param {Object} oldTemplate
   * @param {Object} newTemplate
   */
  _updateCamundaErrorEventDefinitionProperties(element, oldTemplate, newTemplate) {
    const bpmnFactory = this._bpmnFactory,
          commandStack = this._commandStack;

    const newProperties = newTemplate.properties.filter((newProperty) => {
      const newBinding = newProperty.binding,
            newBindingType = newBinding.type;

      return newBindingType === 'camunda:errorEventDefinition';
    });

    // (1) do not override if no updates
    if (!newProperties.length) {
      return;
    }

    const extensionElements = this._getOrCreateExtensionElements(element);

    const oldErrorEventDefinitions = findExtensions(element, [ 'camunda:ErrorEventDefinition' ]);

    newProperties.forEach((newProperty) => {
      const oldProperty = findOldProperty(oldTemplate, newProperty),
            oldEventDefinition = oldProperty && findOldBusinessObject(extensionElements, oldProperty),
            newBinding = newProperty.binding;

      // (2) update old event definitions
      if (oldProperty && oldEventDefinition) {

        if (!propertyChanged(oldEventDefinition, oldProperty)) {
          commandStack.execute('element.updateModdleProperties', {
            element,
            moddleElement: oldEventDefinition,
            properties: {
              expression: newProperty.value
            }
          });
        }

        remove(oldErrorEventDefinitions, oldEventDefinition);
      }

      // (3) create new event definition + error
      else {
        const rootElement = getRoot(ModelUtil.getBusinessObject(element)),
              newError = createError(newBinding.errorRef, rootElement, bpmnFactory),
              newEventDefinition = createCamundaErrorEventDefinition(newProperty.value, newError, extensionElements, bpmnFactory);

        commandStack.execute('element.updateModdleProperties', {
          element,
          moddleElement: rootElement,
          properties: {
            rootElements: [ ...rootElement.get('rootElements'), newError ]
          }
        });

        commandStack.execute('element.updateModdleProperties', {
          element,
          moddleElement: extensionElements,
          properties: {
            values: [ ...extensionElements.get('values'), newEventDefinition ]
          }
        });
      }

    });

    // (4) remove old event definitions
    if (oldErrorEventDefinitions.length) {
      commandStack.execute('element.updateModdleProperties', {
        element,
        moddleElement: extensionElements,
        properties: {
          values: minDash.without(extensionElements.get('values'), value => oldErrorEventDefinitions.includes(value))
        }
      });
    }
  }

  /**
   * Update `camunda:ExecutionListener` properties of specified business object. Execution listeners
   * will always be overridden. Execution listeners can only exist in `bpmn:ExtensionElements`.
   *
   * @param {djs.model.Base} element
   * @param {Object} newTemplate
   */
  _updateCamundaExecutionListenerProperties(element, newTemplate) {
    const bpmnFactory = this._bpmnFactory,
          commandStack = this._commandStack;

    const newProperties = newTemplate.properties.filter((newProperty) => {
      const newBinding = newProperty.binding,
            newBindingType = newBinding.type;

      return newBindingType === 'camunda:executionListener';
    });

    // (1) do not override old execution listeners if no new execution listeners specified
    if (!newProperties.length) {
      return;
    }

    const extensionElements = this._getOrCreateExtensionElements(element);

    // (2) remove old execution listeners
    const oldExecutionListeners = findExtensions(element, [ 'camunda:ExecutionListener' ]);

    // (3) add new execution listeners
    const newExecutionListeners = newProperties.map((newProperty) => {
      const newBinding = newProperty.binding,
            propertyValue = newProperty.value;

      return createCamundaExecutionListener(newBinding, propertyValue, bpmnFactory);
    });

    commandStack.execute('element.updateModdleProperties', {
      element,
      moddleElement: extensionElements,
      properties: {
        values: [
          ...minDash.without(extensionElements.get('values'), value => oldExecutionListeners.includes(value)),
          ...newExecutionListeners
        ]
      }
    });
  }

  /**
   * Update `camunda:Field` properties of specified business object.
   * If business object is `camunda:ExecutionListener` or `camunda:TaskListener` `fields` property
   * will be updated. Otherwise `extensionElements.values` property will be updated.
   *
   * @param {djs.model.Base} element
   * @param {Object} oldTemplate
   * @param {Object} newTemplate
   * @param {ModdleElement} businessObject
   */
  _updateCamundaFieldProperties(element, oldTemplate, newTemplate, businessObject) {
    const bpmnFactory = this._bpmnFactory,
          commandStack = this._commandStack;

    const newProperties = newTemplate.properties.filter((newProperty) => {
      const newBinding = newProperty.binding,
            newBindingType = newBinding.type;

      return newBindingType === 'camunda:field';
    });

    // (1) do not override old fields if no new fields specified
    if (!newProperties.length) {
      return;
    }

    if (!businessObject) {
      businessObject = this._getOrCreateExtensionElements(element);
    }

    const propertyName = ModelUtil.isAny(businessObject, [ 'camunda:ExecutionListener', 'camunda:TaskListener' ])
      ? 'fields'
      : 'values';

    const oldFields = findExtensions(element, [ 'camunda:Field' ]);

    newProperties.forEach((newProperty) => {
      const oldProperty = findOldProperty(oldTemplate, newProperty),
            oldField = oldProperty && findOldBusinessObject(businessObject, oldProperty),
            newBinding = newProperty.binding;

      // (2) update old fields
      if (oldProperty && oldField) {

        if (!propertyChanged(oldField, oldProperty)) {
          commandStack.execute('element.updateModdleProperties', {
            element,
            moddleElement: oldField,
            properties: {
              string: newProperty.value
            }
          });
        }

        remove(oldFields, oldField);
      }

      // (3) add new fields
      else {
        const newCamundaFieldInjection = createCamundaFieldInjection(newBinding, newProperty.value, bpmnFactory);

        commandStack.execute('element.updateModdleProperties', {
          element,
          moddleElement: businessObject,
          properties: {
            [ propertyName ]: [ ...businessObject.get(propertyName), newCamundaFieldInjection ]
          }
        });
      }
    });

    // (4) remove old fields
    if (oldFields.length) {
      commandStack.execute('element.updateModdleProperties', {
        element,
        moddleElement: businessObject,
        properties: {
          [ propertyName ]: minDash.without(businessObject.get(propertyName), value => oldFields.includes(value))
        }
      });
    }
  }

  /**
   * Update `camunda:In` and `camunda:Out` properties of specified business object. Only
   * `bpmn:CallActivity` and events with `bpmn:SignalEventDefinition` can have ins. Only
   * `camunda:CallActivity` can have outs.
   *
   * @param {djs.model.Base} element
   * @param {Object} oldTemplate
   * @param {Object} newTemplate
   */
  _updateCamundaInOutProperties(element, oldTemplate, newTemplate) {
    const bpmnFactory = this._bpmnFactory,
          commandStack = this._commandStack;

    const newProperties = newTemplate.properties.filter((newProperty) => {
      const newBinding = newProperty.binding,
            newBindingType = newBinding.type;

      return newBindingType === 'camunda:in'
      || newBindingType === 'camunda:in:businessKey'
      || newBindingType === 'camunda:out';
    });

    // (1) do not override old fields if no new fields specified
    if (!newProperties.length) {
      return;
    }

    // get extension elements of either signal event definition or call activity
    const extensionElements = this._getOrCreateExtensionElements(getSignalEventDefinition(element) || element);

    const oldInsAndOuts = findExtensions(extensionElements, [ 'camunda:In', 'camunda:Out' ]);

    newProperties.forEach((newProperty) => {
      const oldProperty = findOldProperty(oldTemplate, newProperty),
            oldBinding = oldProperty && oldProperty.binding,
            oldInOurOut = oldProperty && findOldBusinessObject(extensionElements, oldProperty),
            newPropertyValue = newProperty.value,
            newBinding = newProperty.binding,
            newBindingType = newBinding.type,
            properties = {};

      let newInOrOut;

      // (2) update old ins and outs
      if (oldProperty && oldInOurOut) {

        if (!propertyChanged(oldInOurOut, oldProperty)) {
          if (newBindingType === 'camunda:in') {
            if (newBinding.expression) {
              properties[ 'camunda:sourceExpression' ] = newPropertyValue;
            } else {
              properties[ 'camunda:source' ] = newPropertyValue;
            }
          } else if (newBindingType === 'camunda:in:businessKey') {
            properties[ 'camunda:businessKey' ] = newPropertyValue;
          } else if (newBindingType === 'camunda:out') {
            properties[ 'camunda:target' ] = newPropertyValue;
          }
        }

        // update camunda:local property if it changed
        if ((oldBinding.local && !newBinding.local) || !oldBinding.local && newBinding.local) {
          properties.local = newBinding.local;
        }

        if (minDash.keys(properties)) {
          commandStack.execute('element.updateModdleProperties', {
            element,
            moddleElement: oldInOurOut,
            properties
          });
        }

        remove(oldInsAndOuts, oldInOurOut);
      }

      // (3) add new ins and outs
      else {
        if (newBindingType === 'camunda:in') {
          newInOrOut = createCamundaIn(newBinding, newPropertyValue, bpmnFactory);
        } else if (newBindingType === 'camunda:out') {
          newInOrOut = createCamundaOut(newBinding, newPropertyValue, bpmnFactory);
        } else if (newBindingType === 'camunda:in:businessKey') {
          newInOrOut = createCamundaInWithBusinessKey(newPropertyValue, bpmnFactory);
        }

        commandStack.execute('element.updateModdleProperties', {
          element,
          moddleElement: extensionElements,
          properties: {
            values: [ ...extensionElements.get('values'), newInOrOut ]
          }
        });
      }
    });

    // (4) remove old ins and outs
    if (oldInsAndOuts.length) {
      commandStack.execute('element.updateModdleProperties', {
        element,
        moddleElement: extensionElements,
        properties: {
          values: minDash.without(extensionElements.get('values'), value => oldInsAndOuts.includes(value))
        }
      });
    }
  }

  /**
   * Update `camunda:InputParameter` and `camunda:OutputParameter` properties of specified business
   * object. Both can only exist in `camunda:InputOutput` which can exist in `bpmn:ExtensionElements`
   * or `camunda:Connector`.
   *
   * @param {djs.model.Base} element
   * @param {Object} oldTemplate
   * @param {Object} newTemplate
   */
  _updateCamundaInputOutputParameterProperties(element, oldTemplate, newTemplate, businessObject) {
    const bpmnFactory = this._bpmnFactory,
          commandStack = this._commandStack;

    const newProperties = newTemplate.properties.filter((newProperty) => {
      const newBinding = newProperty.binding,
            newBindingType = newBinding.type;

      return newBindingType === 'camunda:inputParameter' || newBindingType === 'camunda:outputParameter';
    });

    // (1) do not override old inputs and outputs if no new inputs and outputs specified
    if (!newProperties.length) {
      return;
    }

    if (!businessObject) {
      businessObject = this._getOrCreateExtensionElements(element);
    }

    let inputOutput;

    if (ModelUtil.is(businessObject, 'camunda:Connector')) {
      inputOutput = businessObject.get('camunda:inputOutput');

      if (!inputOutput) {
        inputOutput = bpmnFactory.create('camunda:InputOutput');

        commandStack.execute('element.updateModdleProperties', {
          element,
          moddleElement: businessObject,
          properties: {
            inputOutput
          }
        });
      }
    } else {
      inputOutput = findExtension(businessObject, 'camunda:InputOutput');

      if (!inputOutput) {
        inputOutput = bpmnFactory.create('camunda:InputOutput');

        commandStack.execute('element.updateModdleProperties', {
          element,
          moddleElement: businessObject,
          properties: {
            values: [ ...businessObject.get('values'), inputOutput ]
          }
        });
      }
    }

    const oldInputs = inputOutput.get('camunda:inputParameters')
      ? inputOutput.get('camunda:inputParameters').slice()
      : [];

    const oldOutputs = inputOutput.get('camunda:outputParameters')
      ? inputOutput.get('camunda:outputParameters').slice()
      : [];

    let propertyName;

    newProperties.forEach((newProperty) => {
      const oldProperty = findOldProperty(oldTemplate, newProperty),
            oldInputOrOutput = oldProperty && findOldBusinessObject(businessObject, oldProperty),
            newPropertyValue = newProperty.value,
            newBinding = newProperty.binding,
            newBindingType = newBinding.type;

      let newInputOrOutput,
          properties;

      // (2) update old inputs and outputs
      if (oldProperty && oldInputOrOutput) {

        if (!propertyChanged(oldInputOrOutput, oldProperty)) {
          if (ModelUtil.is(oldInputOrOutput, 'camunda:InputParameter')) {
            properties = {
              value: newPropertyValue
            };
          } else {
            properties = {
              name: newPropertyValue
            };
          }

          commandStack.execute('element.updateModdleProperties', {
            element,
            moddleElement: oldInputOrOutput,
            properties
          });
        }

        if (ModelUtil.is(oldInputOrOutput, 'camunda:InputParameter')) {
          remove(oldInputs, oldInputOrOutput);
        } else {
          remove(oldOutputs, oldInputOrOutput);
        }
      }

      // (3) add new inputs and outputs
      else {
        if (newBindingType === 'camunda:inputParameter') {
          propertyName = 'inputParameters';

          newInputOrOutput = createInputParameter(newBinding, newPropertyValue, bpmnFactory);
        } else {
          propertyName = 'outputParameters';

          newInputOrOutput = createOutputParameter(newBinding, newPropertyValue, bpmnFactory);
        }

        commandStack.execute('element.updateModdleProperties', {
          element,
          moddleElement: inputOutput,
          properties: {
            [ propertyName ]: [ ...inputOutput.get(propertyName), newInputOrOutput ]
          }
        });
      }
    });

    // (4) remove old inputs and outputs
    if (oldInputs.length) {
      commandStack.execute('element.updateModdleProperties', {
        element,
        moddleElement: inputOutput,
        properties: {
          inputParameters: minDash.without(inputOutput.get('inputParameters'), inputParameter => oldInputs.includes(inputParameter))
        }
      });
    }

    if (oldOutputs.length) {
      commandStack.execute('element.updateModdleProperties', {
        element,
        moddleElement: inputOutput,
        properties: {
          outputParameters: minDash.without(inputOutput.get('outputParameters'), outputParameter => oldOutputs.includes(outputParameter))
        }
      });
    }
  }

  _updateCamundaModelerTemplate(element, newTemplate) {
    const modeling = this._modeling;

    const newId = newTemplate && newTemplate.id;
    const newVersion = newTemplate && newTemplate.version;

    if (getTemplateId(element) !== newId || getTemplateVersion(element) !== newVersion) {
      modeling.updateProperties(element, {
        'camunda:modelerTemplate': newId,
        'camunda:modelerTemplateVersion': newVersion
      });
    }
  }

  /**
   * Update `camunda:Property` properties of specified business object. `camunda:Property` can only
   * exist in `camunda:Properties`.
   *
   * @param {djs.model.Base} element
   * @param {Object} oldTemplate
   * @param {Object} newTemplate
   * @param {ModdleElement} businessObject
   */
  _updateCamundaPropertyProperties(element, oldTemplate, newTemplate, businessObject) {
    const bpmnFactory = this._bpmnFactory,
          commandStack = this._commandStack;

    const newProperties = newTemplate.properties.filter((newProperty) => {
      const newBinding = newProperty.binding,
            newBindingType = newBinding.type;

      return newBindingType === 'camunda:property';
    });

    // (1) do not override old properties if no new properties specified
    if (!newProperties.length) {
      return;
    }

    if (businessObject) {
      businessObject = this._getOrCreateExtensionElements(businessObject);
    } else {
      businessObject = this._getOrCreateExtensionElements(element);
    }

    let camundaProperties = findExtension(businessObject, 'camunda:Properties');

    if (!camundaProperties) {
      camundaProperties = bpmnFactory.create('camunda:Properties');

      commandStack.execute('element.updateModdleProperties', {
        element,
        moddleElement: businessObject,
        properties: {
          values: [ ...businessObject.get('values'), camundaProperties ]
        }
      });
    }

    const oldCamundaProperties = camundaProperties.get('camunda:values')
      ? camundaProperties.get('camunda:values').slice()
      : [];

    newProperties.forEach((newProperty) => {
      const oldProperty = findOldProperty(oldTemplate, newProperty),
            oldCamundaProperty = oldProperty && findOldBusinessObject(businessObject, oldProperty),
            newPropertyValue = newProperty.value,
            newBinding = newProperty.binding;

      // (2) update old properties
      if (oldProperty && oldCamundaProperty) {

        if (!propertyChanged(oldCamundaProperty, oldProperty)) {
          commandStack.execute('element.updateModdleProperties', {
            element,
            moddleElement: oldCamundaProperty,
            properties: {
              value: newPropertyValue
            }
          });
        }

        remove(oldCamundaProperties, oldCamundaProperty);
      }

      // (3) add new properties
      else {
        const newCamundaProperty = createCamundaProperty(newBinding, newPropertyValue, bpmnFactory);

        commandStack.execute('element.updateModdleProperties', {
          element,
          moddleElement: camundaProperties,
          properties: {
            values: [ ...camundaProperties.get('values'), newCamundaProperty ]
          }
        });
      }
    });

    // (4) remove old properties
    if (oldCamundaProperties.length) {
      commandStack.execute('element.updateModdleProperties', {
        element,
        moddleElement: camundaProperties,
        properties: {
          values: minDash.without(camundaProperties.get('values'), value => oldCamundaProperties.includes(value))
        }
      });
    }
  }

  /**
   * Update `bpmn:conditionExpression` property of specified element. Since condition expression is
   * is not primitive it needs special handling.
   *
   * @param {djs.model.Base} element
   * @param {Object} oldProperty
   * @param {Object} newProperty
   */
  _updateConditionExpression(element, oldProperty, newProperty) {
    const bpmnFactory = this._bpmnFactory,
          commandStack = this._commandStack,
          modeling = this._modeling;

    const newBinding = newProperty.binding,
          newPropertyValue = newProperty.value;

    if (!oldProperty) {
      modeling.updateProperties(element, {
        conditionExpression: bpmnFactory.create('bpmn:FormalExpression', {
          body: newPropertyValue,
          language: newBinding.scriptFormat
        })
      });

      return;
    }

    const oldBinding = oldProperty.binding,
          oldPropertyValue = oldProperty.value;

    const businessObject = ModelUtil.getBusinessObject(element),
          conditionExpression = businessObject.get('bpmn:conditionExpression');

    const properties = {};

    if (conditionExpression.get('body') === oldPropertyValue) {
      properties.body = newPropertyValue;
    }

    if (conditionExpression.get('language') === oldBinding.scriptFormat) {
      properties.language = newBinding.scriptFormat;
    }

    if (!minDash.keys(properties).length) {
      return;
    }

    commandStack.execute('element.updateModdleProperties', {
      element,
      moddleElement: conditionExpression,
      properties
    });
  }

  _updateProperties(element, oldTemplate, newTemplate, businessObject) {
    const commandStack = this._commandStack;

    const newProperties = newTemplate.properties.filter((newProperty) => {
      const newBinding = newProperty.binding,
            newBindingType = newBinding.type;

      return newBindingType === 'property';
    });

    const oldProperties = oldTemplate && oldTemplate.properties.filter((oldProperty) => {
      const oldBinding = oldProperty.binding,
            oldBindingType = oldBinding.type;

      return oldBindingType === 'property';
    });

    if (!newProperties.length) {
      return;
    }

    if (!businessObject) {
      businessObject = ModelUtil.getBusinessObject(element);
    }

    newProperties.forEach((newProperty) => {
      const oldProperty = findOldProperty(oldTemplate, newProperty),
            newBinding = newProperty.binding,
            newBindingName = newBinding.name,
            newPropertyValue = newProperty.value;

      let changedElement,
          properties;

      if (oldProperty) {
        remove(oldProperties, oldProperty);
      }

      if (newBindingName === 'conditionExpression') {
        this._updateConditionExpression(element, oldProperty, newProperty);
      } else {

        if (ModelUtil.is(businessObject, 'bpmn:Error')) {
          changedElement = businessObject;
        } else {
          changedElement = element;
        }

        if (oldProperty && propertyChanged(changedElement, oldProperty)) {
          return;
        }

        properties = {};

        properties[ newBindingName ] = newPropertyValue;

        // only one of `camunda:class`, `camunda:delegateExpression` and `camunda:expression` can be set
        // TODO(philippfromme): ensuring only one of these properties is set at a time should be
        // implemented in a behavior and not in this handler and properties panel UI
        if (CAMUNDA_SERVICE_TASK_LIKE.indexOf(newBindingName) !== -1) {
          CAMUNDA_SERVICE_TASK_LIKE.forEach((camundaServiceTaskLikeProperty) => {
            if (camundaServiceTaskLikeProperty !== newBindingName) {
              properties[ camundaServiceTaskLikeProperty ] = undefined;
            }
          });
        }

        commandStack.execute('element.updateModdleProperties', {
          element,
          moddleElement: businessObject,
          properties
        });
      }
    });

    // remove old properties not present in new template
    oldProperties && oldProperties.forEach((oldProperty) => {
      commandStack.execute('element.updateModdleProperties', {
        element,
        moddleElement: businessObject,
        properties: {
          [oldProperty.binding.name]: null
        }
      });
    });
  }

  /**
   * Update properties for a specified scope.
   *
   * @param {djs.model.Base} element
   * @param {Object} oldTemplate
   * @param {Object} newScopeTemplate
   * @param {Object} newTemplate
   */
  _updateScopeProperties(element, oldTemplate, newScopeTemplate, newTemplate) {
    const bpmnFactory = this._bpmnFactory,
          commandStack = this._commandStack;

    const scopeName = newScopeTemplate.type;

    let scopeElement;

    scopeElement = findOldScopeElement(element, newScopeTemplate, newTemplate);

    if (!scopeElement) {

      scopeElement = bpmnFactory.create(scopeName);
    }

    const oldScopeTemplate = findOldScopeTemplate(newScopeTemplate, oldTemplate);

    // update properties
    this._updateProperties(element, oldScopeTemplate, newScopeTemplate, scopeElement);

    // update camunda:ExecutionListener properties
    this._updateCamundaExecutionListenerProperties(element, newScopeTemplate);

    // update camunda:In and camunda:Out properties
    this._updateCamundaInOutProperties(element, oldScopeTemplate, newScopeTemplate);

    // update camunda:InputParameter and camunda:OutputParameter properties
    this._updateCamundaInputOutputParameterProperties(element, oldScopeTemplate, newScopeTemplate, scopeElement);

    // update camunda:Field properties
    this._updateCamundaFieldProperties(element, oldScopeTemplate, newScopeTemplate, scopeElement);

    // update camunda:Property properties
    this._updateCamundaPropertyProperties(element, oldScopeTemplate, newScopeTemplate, scopeElement);

    // assume that root elements were already created in root by referenced event definition binding
    if (isRootElementScope(scopeName)) {
      return;
    }

    const extensionElements = this._getOrCreateExtensionElements(element);

    commandStack.execute('element.updateModdleProperties', {
      element,
      moddleElement: extensionElements,
      properties: {
        values: [ ...extensionElements.get('values'), scopeElement ]
      }
    });
  }

  /**
   * Replaces the element with the specified elementType
   *
   * @param {djs.model.Base} element
   * @param {Object} newTemplate
   */
  _updateTaskType(element, newTemplate) {

    // determine new task type
    const newType = newTemplate.elementType;

    if (!newType) {
      return element;
    }

    // don't replace Task that is already the correct type
    if (element.$type === newType.value) {
      return element;
    }

    return this._bpmnReplace.replaceElement(element, { type: newType.value });
  }
}

ChangeElementTemplateHandler.$inject = [
  'bpmnFactory',
  'bpmnReplace',
  'commandStack',
  'modeling'
];


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

/**
 * Find old business object matching specified old property.
 *
 * @param {djs.model.Base|ModdleElement} element
 * @param {Object} oldProperty
 *
 * @returns {ModdleElement}
 */
function findOldBusinessObject(element, oldProperty) {
  let businessObject = ModelUtil.getBusinessObject(element),
      propertyName;

  const oldBinding = oldProperty.binding,
        oldBindingType = oldBinding.type;

  if (oldBindingType === 'camunda:field') {

    if (ModelUtil.isAny(businessObject, [ 'camunda:ExecutionListener', 'camunda:TaskListener' ])) {
      propertyName = 'camunda:fields';
    } else {
      propertyName = 'bpmn:values';
    }

    if (!businessObject || !businessObject.get(propertyName) || !businessObject.get(propertyName).length) {
      return;
    }

    return minDash.find(businessObject.get(propertyName), function(oldBusinessObject) {
      return oldBusinessObject.get('camunda:name') === oldBinding.name;
    });
  }

  if (oldBindingType === 'camunda:in') {
    return minDash.find(businessObject.get('values'), function(oldBusinessObject) {
      return oldBusinessObject.get('target') === oldBinding.target;
    });
  }

  if (oldBindingType === 'camunda:in:businessKey') {
    return minDash.find(businessObject.get('values'), function(oldBusinessObject) {
      return minDash.isString(oldBusinessObject.get('businessKey'));
    });
  }

  if (oldBindingType === 'camunda:out') {
    return minDash.find(businessObject.get('values'), function(oldBusinessObject) {
      return oldBusinessObject.get('source') === oldBinding.source ||
        oldBusinessObject.get('sourceExpression') || oldBinding.sourceExpression;
    });
  }

  if (oldBindingType === 'camunda:inputParameter' || oldBindingType === 'camunda:outputParameter') {

    if (ModelUtil.is(businessObject, 'camunda:Connector')) {
      businessObject = businessObject.get('camunda:inputOutput');

      if (!businessObject) {
        return;
      }
    } else {
      businessObject = findExtension(businessObject, 'camunda:InputOutput');

      if (!businessObject) {
        return;
      }
    }

    if (oldBindingType === 'camunda:inputParameter') {
      return minDash.find(businessObject.get('camunda:inputParameters'), function(oldBusinessObject) {
        return oldBusinessObject.get('camunda:name') === oldBinding.name;
      });
    } else {
      return minDash.find(businessObject.get('camunda:outputParameters'), function(oldBusinessObject) {
        if (oldBinding.scriptFormat) {
          const definition = oldBusinessObject.get('camunda:definition');

          return definition && definition.get('camunda:value') === oldBinding.source;
        } else {
          return oldBusinessObject.get('camunda:value') === oldBinding.source;
        }
      });
    }

  }

  if (oldBindingType === 'camunda:property') {
    if (!businessObject || !businessObject.get('values') || !businessObject.get('values').length) {
      return;
    }

    businessObject = findExtension(businessObject, 'camunda:Properties');

    if (!businessObject) {
      return;
    }

    return minDash.find(businessObject.get('values'), function(oldBusinessObject) {
      return oldBusinessObject.get('camunda:name') === oldBinding.name;
    });
  }

  if (oldBindingType === 'camunda:errorEventDefinition') {
    return findCamundaErrorEventDefinition(element, oldBinding.errorRef);
  }
}

/**
 * Find old property matching specified new property.
 *
 * @param {Object} oldTemplate
 * @param {Object} newProperty
 *
 * @returns {Object}
 */
function findOldProperty(oldTemplate, newProperty) {
  if (!oldTemplate) {
    return;
  }

  const oldProperties = oldTemplate.properties,
        newBinding = newProperty.binding,
        newBindingName = newBinding.name,
        newBindingType = newBinding.type;

  if (newBindingType === 'property') {
    return minDash.find(oldProperties, function(oldProperty) {
      const oldBinding = oldProperty.binding,
            oldBindingName = oldBinding.name,
            oldBindingType = oldBinding.type;

      return oldBindingType === 'property' && oldBindingName === newBindingName;
    });
  }

  if (newBindingType === 'camunda:field') {
    return minDash.find(oldProperties, function(oldProperty) {
      const oldBinding = oldProperty.binding,
            oldBindingName = oldBinding.name,
            oldBindingType = oldBinding.type;

      return oldBindingType === 'camunda:field' && oldBindingName === newBindingName;
    });
  }

  if (newBindingType === 'camunda:in') {
    return minDash.find(oldProperties, function(oldProperty) {
      const oldBinding = oldProperty.binding,
            oldBindingType = oldBinding.type;

      if (oldBindingType !== 'camunda:in') {
        return;
      }

      // always override if change from source to source expression or vice versa
      if ((oldBinding.expression && !newBinding.expression) ||
        !oldBinding.expression && newBinding.expression) {
        return;
      }

      return oldBinding.target === newBinding.target;
    });
  }

  if (newBindingType === 'camunda:in:businessKey') {
    return minDash.find(oldProperties, function(oldProperty) {
      const oldBinding = oldProperty.binding,
            oldBindingType = oldBinding.type;

      return oldBindingType === 'camunda:in:businessKey';
    });
  }

  if (newBindingType === 'camunda:out') {
    return minDash.find(oldProperties, function(oldProperty) {
      const oldBinding = oldProperty.binding,
            oldBindingType = oldBinding.type;

      return oldBindingType === 'camunda:out' && (
        oldBinding.source === newBinding.source ||
        oldBinding.sourceExpression === newBinding.sourceExpression
      );
    });
  }

  if (newBindingType === 'camunda:inputParameter') {
    return minDash.find(oldProperties, function(oldProperty) {
      const oldBinding = oldProperty.binding,
            oldBindingName = oldBinding.name,
            oldBindingType = oldBinding.type;

      if (oldBindingType !== 'camunda:inputParameter') {
        return;
      }

      return oldBindingName === newBindingName
        && oldBinding.scriptFormat === newBinding.scriptFormat;
    });
  }

  if (newBindingType === 'camunda:outputParameter') {
    return minDash.find(oldProperties, function(oldProperty) {
      const oldBinding = oldProperty.binding,
            oldBindingType = oldBinding.type;

      if (oldBindingType !== 'camunda:outputParameter') {
        return;
      }

      return oldBinding.source === newBinding.source
        && oldBinding.scriptFormat === newBinding.scriptFormat;
    });
  }

  if (newBindingType === 'camunda:property') {
    return minDash.find(oldProperties, function(oldProperty) {
      const oldBinding = oldProperty.binding,
            oldBindingName = oldBinding.name,
            oldBindingType = oldBinding.type;

      return oldBindingType === 'camunda:property' && oldBindingName === newBindingName;
    });
  }

  if (newBindingType === 'camunda:errorEventDefinition') {
    return minDash.find(oldProperties, function(oldProperty) {
      const newBindingRef = newBinding.errorRef,
            oldBinding = oldProperty.binding,
            oldBindingRef = oldBinding.errorRef,
            oldBindingType = oldBinding.type;

      return oldBindingType === 'camunda:errorEventDefinition'
        && oldBindingRef === newBindingRef;
    });
  }
}

function findOldScopeElement(element, scopeTemplate, template) {
  const scopeName = scopeTemplate.type,
        id = scopeTemplate.id;

  if (scopeName === 'camunda:Connector') {
    return findExtension(element, 'camunda:Connector');
  }

  if (scopeName === 'bpmn:Error') {

    // (1) find by error event definition binding
    const errorEventDefinitionBinding = findErrorEventDefinitionBinding(template, id);

    if (!errorEventDefinitionBinding) {
      return;
    }

    // (2) find error event definition
    const errorEventDefinition = findOldBusinessObject(element, errorEventDefinitionBinding);

    if (!errorEventDefinition) {
      return;
    }

    // (3) retrieve referenced error
    return errorEventDefinition.errorRef;
  }
}

function isRootElementScope(scopeName) {
  return [ 'bpmn:Error' ].includes(scopeName);
}

function findOldScopeTemplate(scopeTemplate, oldTemplate) {
  const scopeName = scopeTemplate.type,
        scopeId = scopeTemplate.id,
        scopes = oldTemplate && handleLegacyScopes(oldTemplate.scopes);

  return scopes && minDash.find(scopes, function(scope) {

    if (isRootElementScope(scopeName)) {
      return scope.id === scopeId;
    }

    return scope.type === scopeName;
  });
}

function findErrorEventDefinitionBinding(template, templateErrorId) {
  return minDash.find(template.properties, function(property) {
    return property.binding.errorRef === templateErrorId;
  });
}

/**
 * Check whether property was changed after being set by template.
 *
 * @param {djs.model.Base|ModdleElement} element
 * @param {Object} oldProperty
 *
 * @returns {boolean}
 */
function propertyChanged(element, oldProperty) {
  const businessObject = ModelUtil.getBusinessObject(element);

  const oldBinding = oldProperty.binding,
        oldBindingName = oldBinding.name,
        oldBindingType = oldBinding.type,
        oldPropertyValue = oldProperty.value;

  let conditionExpression,
      definition;

  if (oldBindingType === 'property') {
    if (oldBindingName === 'conditionExpression') {
      conditionExpression = businessObject.get('bpmn:conditionExpression');

      return conditionExpression.get('bpmn:body') !== oldPropertyValue;
    }

    return businessObject.get(oldBindingName) !== oldPropertyValue;
  }

  if (oldBindingType === 'camunda:field') {
    return businessObject.get('camunda:string') !== oldPropertyValue;
  }

  if (oldBindingType === 'camunda:in') {
    if (oldBinding.expression) {
      return businessObject.get('sourceExpression') !== oldPropertyValue;
    } else {
      return businessObject.get('camunda:source') !== oldPropertyValue;
    }
  }

  if (oldBindingType === 'camunda:in:businessKey') {
    return businessObject.get('camunda:businessKey') !== oldPropertyValue;
  }

  if (oldBindingType === 'camunda:out') {
    return businessObject.get('camunda:target') !== oldPropertyValue;
  }

  if (oldBindingType === 'camunda:inputParameter') {
    if (oldBinding.scriptFormat) {
      definition = businessObject.get('camunda:definition');

      return definition && definition.get('camunda:value') !== oldPropertyValue;
    } else {
      return businessObject.get('camunda:value') !== oldPropertyValue;
    }
  }

  if (oldBindingType === 'camunda:outputParameter') {
    return businessObject.get('camunda:name') !== oldPropertyValue;
  }

  if (oldBindingType === 'camunda:property') {
    return businessObject.get('camunda:value') !== oldPropertyValue;
  }

  if (oldBindingType === 'camunda:errorEventDefinition') {
    return businessObject.get('expression') !== oldPropertyValue;
  }
}

function remove(array, item) {
  const index = array.indexOf(item);

  if (minDash.isUndefined(index)) {
    return array;
  }

  array.splice(index, 1);

  return array;
}

class RemoveElementTemplateHandler {
  constructor(
      modeling,
      elementFactory,
      elementRegistry,
      canvas,
      bpmnFactory,
      replace,
      commandStack
  ) {
    this._modeling = modeling;
    this._elementFactory = elementFactory;
    this._elementRegistry = elementRegistry;
    this._canvas = canvas;
    this._bpmnFactory = bpmnFactory;
    this._replace = replace;
    this._commandStack = commandStack;
  }

  preExecute(context) {
    const {
      element
    } = context;

    if (element.parent) {
      context.newElement = this._removeTemplate(element);
    } else {
      context.newElement = this._removeRootTemplate(element);
    }
  }

  _removeTemplate(element) {
    const replace = this._replace;

    const businessObject = ModelUtil.getBusinessObject(element);

    const type = businessObject.$type,
          eventDefinitionType = this._getEventDefinitionType(businessObject);

    const newBusinessObject = this._createBlankBusinessObject(element);

    return replace.replaceElement(element,
      {
        type: type,
        businessObject: newBusinessObject,
        eventDefinitionType: eventDefinitionType,
      },
      {
        createElementsBehavior: false
      }
    );
  }

  /**
   * Remove template from a given element.
   *
   * @param {djs.model.Base} element
   *
   * @return {djs.model.Base} the updated element
   */
  _removeRootTemplate(element) {
    var modeling = this._modeling,
        elementFactory = this._elementFactory,
        elementRegistry = this._elementRegistry,
        canvas = this._canvas;

    // We are inside a collapsed subprocess, move up to the parent before replacing the collapsed object
    if (DrilldownUtil.isPlane(element)) {
      const shapeId = DrilldownUtil.getShapeIdFromPlane(element);
      const shape = elementRegistry.get(shapeId);

      if (shape && shape !== element) {
        canvas.setRootElement(canvas.findRoot(shape));
        return this._removeTemplate(shape);
      }
    }

    const businessObject = ModelUtil.getBusinessObject(element);

    const type = businessObject.$type;

    const newBusinessObject = this._createBlankBusinessObject(element);

    const newRoot = elementFactory.create('root', {
      type: type,
      businessObject: newBusinessObject
    });

    this._commandStack.execute('canvas.updateRoot', {
      newRoot: newRoot,
      oldRoot: element
    });

    modeling.moveElements(element.children, { x: 0, y: 0 }, newRoot);

    return newRoot;
  }

  _getEventDefinitionType(businessObject) {
    if (!businessObject.eventDefinitions) {
      return null;
    }

    const eventDefinition = businessObject.eventDefinitions[ 0 ];

    if (!eventDefinition) {
      return null;
    }

    return eventDefinition.$type;
  }

  _createBlankBusinessObject(element) {
    const bpmnFactory = this._bpmnFactory;

    const bo = ModelUtil.getBusinessObject(element),
          newBo = bpmnFactory.create(bo.$type),
          label = LabelUtil.getLabel(element);

    if (!label) {
      return newBo;
    }

    if (ModelUtil.is(element, 'bpmn:Group')) {
      newBo.categoryValueRef = bpmnFactory.create('bpmn:CategoryValue');
    }

    LabelUtil.setLabel({ businessObject: newBo }, label);

    return newBo;
  }
}


RemoveElementTemplateHandler.$inject = [
  'modeling',
  'elementFactory',
  'elementRegistry',
  'canvas',
  'bpmnFactory',
  'replace',
  'commandStack'
];

class ElementTemplatesCommands {
  constructor(commandStack, elementTemplates, eventBus) {
    commandStack.registerHandler(
      'element-templates.multi-command-executor',
      MultiCommandHandler
    );

    commandStack.registerHandler(
      'propertiesPanel.camunda.changeTemplate',
      ChangeElementTemplateHandler
    );

    commandStack.registerHandler(
      'propertiesPanel.removeTemplate',
      RemoveElementTemplateHandler
    );

    // apply default element templates on shape creation
    eventBus.on([ 'commandStack.shape.create.postExecuted' ], function(event) {
      const {
        context: {
          hints = {},
          shape
        }
      } = event;

      if (hints.createElementsBehavior !== false) {
        applyDefaultTemplate(shape, elementTemplates, commandStack);
      }
    });

    // apply default element templates on connection creation
    eventBus.on([ 'commandStack.connection.create.postExecuted' ], function(event) {
      const {
        context: {
          hints = {},
          connection
        }
      } = event;

      if (hints.createElementsBehavior !== false) {
        applyDefaultTemplate(connection, elementTemplates, commandStack);
      }
    });
  }
}

ElementTemplatesCommands.$inject = [ 'commandStack', 'elementTemplates', 'eventBus' ];

function applyDefaultTemplate(element, elementTemplates, commandStack) {

  if (!elementTemplates.get(element) && elementTemplates.getDefault(element)) {

    const command = 'propertiesPanel.camunda.changeTemplate';
    const commandContext = {
      element: element,
      newTemplate: elementTemplates.getDefault(element)
    };

    commandStack.execute(command, commandContext);
  }
}

var commandsModule = {
  __init__: [ 'elementTemplateCommands' ],
  elementTemplateCommands: [ 'type', ElementTemplatesCommands ]
};

/**
 * This Behavior checks if the new element's type is in
 * the list of elements the template applies to and unlinks
 * it if not.
 */
class ReplaceBehavior extends CommandInterceptor {
  constructor(elementTemplates, injector) {
    super(injector.get('eventBus'));

    this.postExecuted('shape.replace', function(e) {
      var context = e.context,
          oldShape = context.oldShape,
          oldBo = ModelUtil.getBusinessObject(oldShape),
          newShape = context.newShape,
          newBo = ModelUtil.getBusinessObject(newShape);

      if (!oldBo.modelerTemplate) {
        return;
      }

      const template = newBo.modelerTemplate;
      const version = newBo.modelerTemplateVersion;

      const elementTemplate = elementTemplates.get(template, version);

      if (!elementTemplate) {
        elementTemplates.unlinkTemplate(newShape, injector);
        return;
      }

      const { appliesTo, elementType } = elementTemplate;

      if (elementType) {
        if (!ModelUtil.is(newShape, elementType.value)) {
          elementTemplates.unlinkTemplate(newShape, injector);
        }

        return;
      }

      const allowed = appliesTo.reduce((allowed, type) => {
        return allowed || ModelUtil.is(newBo, type);
      }, false);

      if (!allowed) {
        elementTemplates.unlinkTemplate(newShape, injector);
      }
    });
  }
}

ReplaceBehavior.$inject = [
  'elementTemplates',
  'injector'
];

var behaviorModule = {
  __init__: [
    'elementTemplatesReplaceBehavior'
  ],
  elementTemplatesReplaceBehavior: [ 'type', ReplaceBehavior ]
};

var index = {
  __depends__: [
    commandsModule,
    behaviorModule
  ],
  __init__: [
    'elementTemplatesLoader'
  ],
  elementTemplates: [ 'type', ElementTemplates$1 ],
  elementTemplatesLoader: [ 'type', ElementTemplatesLoader$1 ]
};

exports.CloudElementTemplatesCoreModule = index$1;
exports.ElementTemplatesCoreModule = index;
//# sourceMappingURL=core.js.map