text: import CollapsibleEntry from '@bpmn-io/properties-panel/lib/components/entries/Collapsible';
  import ListEntry from '@bpmn-io/properties-panel/lib/components/entries/List';
  import SelectEntry from '@bpmn-io/properties-panel/lib/components/entries/Select';
  import TextField from '@bpmn-io/properties-panel/lib/components/entries/TextField';
  
  import FormFieldConstraint from './FormFieldConstraint';
  import FormFieldProperty from './FormFieldProperty';
  import FormFieldValue from './FormFieldValue';
  
  import {
    getBusinessObject
  } from 'bpmn-js/lib/util/ModelUtil';
  
  import {
    createElement
  } from '../../../utils/ElementUtil';
  
  import {
    useService
  } from '../../../hooks';
  
  
  const CUSTOM_TYPE_VALUE = '',
        DEFINED_TYPE_VALUES = [ 'boolean', 'date', 'enum', 'long', 'string', undefined ];
  
  export default function FormField(props) {
    const {
      idPrefix,
      element,
      formField
    } = props;
  
    const entries = [
      {
        id: idPrefix + '-formFieldID',
        component: <Id idPrefix={ idPrefix } element={ element } formField={ formField } />
      },
      {
        id: idPrefix + '-formFieldLabel',
        component: <Label idPrefix={ idPrefix } element={ element } formField={ formField } />
      },
      {
        id: idPrefix + '-formFieldType',
        component: <Type idPrefix={ idPrefix } element={ element } formField={ formField } />
      },
      DEFINED_TYPE_VALUES.includes(formField.get('type')) ? {} :
        {
          id: idPrefix + '-formFieldCustomType',
          component: <CustomType idPrefix={ idPrefix } element={ element } formField={ formField } />
        },
      {
        id: idPrefix + '-formFieldDefaultValue',
        component: <DefaultValue idPrefix={ idPrefix } element={ element } formField={ formField } />
      },
      formField.get('type') !== 'enum' ? {} :
        {
          id: idPrefix + '-formFieldValues',
          component: <ValueList id={ idPrefix + '-values' } element={ element } formField={ formField } />
        },
      {
        id: idPrefix + '-formFieldConstraints',
        component: <ConstraintList id={ idPrefix + '-constraints' } element={ element } formField={ formField } />
      },
      {
        id: idPrefix + '-formFieldProperties',
        component: <PropertiesList id={ idPrefix + '-properties' } element={ element } formField={ formField } />
      }
    ];
  
    return entries;
  }
  
  function Id(props) {
    const {
      idPrefix,
      element,
      formField
    } = props;
  
    const commandStack = useService('commandStack');
    const translate = useService('translate');
    const debounce = useService('debounceInput');
  
    const setValue = (value) => {
      commandStack.execute('properties-panel.update-businessobject', {
        element,
        businessObject: formField,
        properties: {
          id: value
        }
      });
    };
  
    const getValue = () => {
      return formField.get('id');
    };
  
    return TextField({
      element: formField,
      id: idPrefix + '-formFieldID',
      label: translate('ID'),
      description: translate('Refers to the process variable name'),
      getValue,
      setValue,
      debounce
    });
  }
  
  function Label(props) {
    const {
      idPrefix,
      element,
      formField
    } = props;
  
    const commandStack = useService('commandStack');
    const translate = useService('translate');
    const debounce = useService('debounceInput');
  
    const setValue = (value) => {
      commandStack.execute('properties-panel.update-businessobject', {
        element,
        businessObject: formField,
        properties: {
          label: value
        }
      });
    };
  
    const getValue = () => {
      return formField.get('label');
    };
  
    return TextField({
      element: formField,
      id: idPrefix + '-formFieldLabel',
      label: translate('Label'),
      getValue,
      setValue,
      debounce
    });
  }
  
  function Type(props) {
    const {
      idPrefix,
      element,
      formField
    } = props;
  
    const commandStack = useService('commandStack');
    const translate = useService('translate');
  
    const setValue = (value) => {
      commandStack.execute('properties-panel.update-businessobject', {
        element,
        businessObject: formField,
        properties: {
          type: value
        }
      });
    };
  
    const getValue = () => {
      const type = formField.get('type');
  
      return DEFINED_TYPE_VALUES.includes(type) ?
        type :
        CUSTOM_TYPE_VALUE;
    };
  
    const getOptions = () => {
      const options = [
        { label: translate('boolean'), value: 'boolean' },
        { label: translate('date'), value: 'date' },
        { label: translate('enum'), value: 'enum' },
        { label: translate('long'), value: 'long' },
        { label: translate('string'), value: 'string' },
        { label: translate('<custom type>'), value: CUSTOM_TYPE_VALUE }
      ];
  
      // for the initial state only, we want to show an empty state
      if (formField.get('type') === undefined) {
        options.unshift({ label: translate('<none>'), value: '' });
      }
  
      return options;
    };
  
    return SelectEntry({
      element: formField,
      id: idPrefix + '-formFieldType',
      label: translate('Type'),
      getValue,
      setValue,
      getOptions
    });
  }
  
  function CustomType(props) {
    const {
      idPrefix,
      element,
      formField
    } = props;
  
    const commandStack = useService('commandStack');
    const translate = useService('translate');
    const debounce = useService('debounceInput');
  
    const setValue = (value) => {
      const type = value || '';
  
      commandStack.execute('properties-panel.update-businessobject', {
        element,
        businessObject: formField,
        properties: {
          type
        }
      });
    };
  
    const getValue = () => {
      return formField.get('type');
    };
  
    return TextField({
      element: formField,
      id: idPrefix + '-formFieldCustomType',
      label: translate('Custom type'),
      getValue,
      setValue,
      debounce
    });
  }
  
  function DefaultValue(props) {
    const {
      idPrefix,
      element,
      formField
    } = props;
  
    const commandStack = useService('commandStack');
    const translate = useService('translate');
    const debounce = useService('debounceInput');
  
    const setValue = (value) => {
      commandStack.execute('properties-panel.update-businessobject', {
        element,
        businessObject: formField,
        properties: {
          defaultValue: value
        }
      });
    };
  
    const getValue = () => {
      return formField.get('defaultValue');
    };
  
    return TextField({
      element: formField,
      id: idPrefix + '-formFieldDefaultValue',
      label: translate('Default value'),
      getValue,
      setValue,
      debounce
    });
  }
  
  function ValueList(props) {
    const {
      id,
      element,
      formField
    } = props;
  
    const bpmnFactory = useService('bpmnFactory');
    const commandStack = useService('commandStack');
    const translate = useService('translate');
  
    const values = formField.get('values') || [];
  
    function Value(value, index, open) {
      return (
        <CollapsibleEntry
          id={ `${id}-value-${index}` }
          entries={ FormFieldValue({ idPrefix: `${id}-field-${index}`, element, value }) }
          label={ value.get('id') || translate('<empty>') }
          open={ open }
        />
      );
    }
  
    function addValue() {
      const value = createElement(
        'camunda:Value',
        { id: undefined, name: undefined },
        formField,
        bpmnFactory
      );
  
      commandStack.execute('properties-panel.update-businessobject-list', {
        element: element,
        currentObject: formField,
        objectsToAdd: [ value ],
        propertyName: 'values'
      });
    }
  
    function removeValue(value) {
      commandStack.execute('properties-panel.update-businessobject-list', {
        element: element,
        currentObject: formField,
        objectsToRemove: [ value ],
        propertyName: 'values'
      });
    }
  
    return <ListEntry
      element={ element }
      autoFocusEntry={ true }
      id={ id }
      label={ translate('Values') }
      items={ values }
      renderItem={ Value }
      onAdd={ addValue }
      onRemove={ removeValue }
    />;
  }
  
  function ConstraintList(props) {
    const {
      id,
      element,
      formField
    } = props;
  
    const bpmnFactory = useService('bpmnFactory');
    const commandStack = useService('commandStack');
    const translate = useService('translate');
  
    const businessObject = getBusinessObject(element);
  
    let validation = formField.get('validation');
  
    const constraints = (validation && validation.get('constraints')) || [];
  
    function Constraint(constraint, index, open) {
      return (
        <CollapsibleEntry
          id={ `${id}-constraint-${index}` }
          entries={ FormFieldConstraint({ idPrefix: `${id}-field-${index}`, element, constraint }) }
          label={ constraint.get('name') || translate('<empty>') }
          open={ open }
        />
      );
    }
  
    function addConstraint() {
      const commands = [];
  
      // (1) ensure validation
      if (!validation) {
        validation = createElement(
          'camunda:Validation',
          { },
          businessObject,
          bpmnFactory
        );
  
        commands.push({
          cmd: 'properties-panel.update-businessobject',
          context: {
            element: element,
            businessObject: formField,
            properties: { validation }
          }
        });
      }
  
      // (2) add constraint
      const constraint = createElement(
        'camunda:Constraint',
        { name: undefined, config: undefined },
        validation,
        bpmnFactory
      );
  
      commands.push({
        cmd: 'properties-panel.update-businessobject-list',
        context: {
          element: element,
          currentObject: validation,
          objectsToAdd: [ constraint ],
          propertyName: 'constraints'
        }
      });
  
      // (3) commit updates
      commandStack.execute('properties-panel.multi-command-executor', commands);
    }
  
    function removeConstraint(constraint) {
      commandStack.execute('properties-panel.update-businessobject-list', {
        element: element,
        currentObject: validation,
        objectsToRemove: [ constraint ],
        propertyName: 'constraints'
      });
    }
  
    return <ListEntry
      element={ element }
      autoFocusEntry={ `[data-entry-id="${id}-constraint-${constraints.length - 1}"] input` }
      id={ id }
      label={ translate('Constraints') }
      items={ constraints }
      renderItem={ Constraint }
      onAdd={ addConstraint }
      onRemove={ removeConstraint } />;
  }
  
  function PropertiesList(props) {
    const {
      id,
      element,
      formField
    } = props;
  
    const bpmnFactory = useService('bpmnFactory');
    const commandStack = useService('commandStack');
    const translate = useService('translate');
  
    const businessObject = getBusinessObject(element);
  
    let properties = formField.get('properties');
  
    const propertyEntries = (properties && properties.get('values')) || [];
  
    function Property(property, index, open) {
      return (
        <CollapsibleEntry
          id={ `${id}-constraint-${index}` }
          entries={ FormFieldProperty({ idPrefix: `${id}-field-${index}`, element, property }) }
          label={ property.get('id') || translate('<empty>') }
          open={ open }
        />
      );
    }
  
    function addProperty() {
      const commands = [];
  
      // (1) ensure properties
      if (!properties) {
        properties = createElement(
          'camunda:Properties',
          { },
          businessObject,
          bpmnFactory
        );
  
        commands.push({
          cmd: 'properties-panel.update-businessobject',
          context: {
            element: element,
            businessObject: formField,
            properties: { properties }
          }
        });
      }
  
      // (2) add property
      const property = createElement(
        'camunda:Property',
        { id: undefined, value: undefined },
        properties,
        bpmnFactory
      );
  
      commands.push({
        cmd: 'properties-panel.update-businessobject-list',
        context: {
          element: element,
          currentObject: properties,
          objectsToAdd: [ property ],
          propertyName: 'values'
        }
      });
  
      // (3) commit updates
      commandStack.execute('properties-panel.multi-command-executor', commands);
    }
  
    function removeProperty(property) {
      commandStack.execute('properties-panel.update-businessobject-list', {
        element: element,
        currentObject: properties,
        objectsToRemove: [ property ],
        propertyName: 'values'
      });
    }
  
    return <ListEntry
      element={ element }
      autoFocusEntry={ true }
      id={ id }
      compareFn={ createAlphanumericCompare('id') }
      label={ translate('Properties') }
      items={ propertyEntries }
      renderItem={ Property }
      onAdd={ addProperty }
      onRemove={ removeProperty }
    />;
  }
  
  // helper //////////////////
  
  function createAlphanumericCompare(field) {
    return function(entry, anotherEntry) {
      const [ key = '', anotherKey = '' ] = [ entry[field], anotherEntry[field] ];
  
      return key === anotherKey ? 0 : key > anotherKey ? 1 : -1;
    };
  }
  