import AposInputMixin from 'Modules/@apostrophecms/schema/mixins/AposInputMixin';
import AposInputFollowingMixin from 'Modules/@apostrophecms/schema/mixins/AposInputFollowingMixin';
import AposInputConditionalFieldsMixin from 'Modules/@apostrophecms/schema/mixins/AposInputConditionalFieldsMixin';
import { getConditionTypesObject, hasParentConditionalField } from 'Modules/@apostrophecms/schema/lib/conditionalFields';

import { createId } from '@paralleldrive/cuid2';
import { klona } from 'klona';
import { get } from 'lodash';
import { Sortable } from 'sortablejs-vue3';
import newInstance from 'apostrophe/modules/@apostrophecms/schema/lib/newInstance.js';

export default {
  name: 'AposInputArray',
  components: { draggable: Sortable },
  mixins: [
    AposInputMixin,
    AposInputFollowingMixin,
    AposInputConditionalFieldsMixin
  ],
  emits: [ 'validate' ],
  props: {
    generation: {
      type: Number,
      required: false,
      default: null
    }
  },
  data() {
    const next = this.getNext();
    // this.schema is a computed property and is not available in data, that's
    // why we use this.field.schema here instead
    const items = modelItems(next, this.field, this.field.schema);
    return {
      next,
      items,
      isDragging: false,
      itemsConditionalFields: Object
        .fromEntries(items.map(({ _id }) => [ _id, getConditionTypesObject() ])),
      emptyWhenIcon: this.field?.whenEmpty?.icon || 'text-box-multiple-icon',
      emptyWhenLabel: this.field?.whenEmpty?.label || 'apostrophe:noItemsAdded'
    };
  },
  computed: {
    isInlineTable() {
      return this.field.style === 'table' && this.isInline;
    },
    isInlineStandard() {
      return this.field.style !== 'table' && this.isInline;
    },
    isInline() {
      return !!this.field.inline;
    },
    shouldResetConditionalFields() {
      if (!this.isInline) {
        return false;
      }

      return hasParentConditionalField(this.schema);
    },
    isDraggable() {
      if (this.field.draggable === false) {
        return false;
      }
      if (this.field.readOnly) {
        return false;
      }
      if (this.next.length <= 1) {
        return false;
      }
      return true;
    },
    isAddDisabled() {
      return this.field.readOnly ||
        (this.field.max && (this.items.length >= this.field.max));
    },
    inlineContextMenu() {
      return [
        ...(this.isDraggable
          ? [
            {
              label: this.$t('apostrophe:moveUp'),
              action: 'move-up'
            },
            {
              label: this.$t('apostrophe:moveDown'),
              action: 'move-down'
            }
          ]
          : []),
        ...(this.field.duplicate !== false
          ? [ {
            label: this.$t('apostrophe:duplicate'),
            action: 'duplicate'
          } ]
          : []),
        {
          label: this.$t('apostrophe:remove'),
          action: 'remove',
          modifiers: [ 'danger' ]
        }
      ];
    },
    // required by the conditional fields mixin
    schema() {
      return this.field.schema;
    },
    listId() {
      return `sortableList-${createId()}`;
    },
    dragOptions() {
      return {
        disabled: !this.isDraggable,
        ghostClass: 'apos-is-ghost',
        handle: this.isInlineTable ? '.apos-drag-handle' : '.apos-input-array-inline-header',
        dragClass: 'apos-is-dragging',
        forceFallback: true,
        fallbackTolerance: 5
      };
    },
    itemLabel() {
      return this.field.itemLabel
        ? {
          key: 'apostrophe:addType',
          type: this.$t(this.field.itemLabel)
        }
        : 'apostrophe:addItem';
    },
    editLabel() {
      return {
        key: 'apostrophe:editType',
        type: this.$t(this.field.label)
      };
    },
    effectiveError() {
      const error = this.error || this.serverError;
      // Server-side errors behave differently
      const name = error?.name || error;
      if (name === 'invalid' && !this.serverError) {
        // Not always due to a subproperty which will display its own error,
        // don't confuse the user if so
        return false;
      }
      return error;
    },
    arrayMeta() {
      return this.convertMetaToItems(this.items);
    }
  },
  watch: {
    generation() {
      this.next = this.getNext();
      this.items = modelItems(this.next, this.field, this.schema);
    },
    items: {
      deep: true,
      handler() {
        const erroneous = this.items.filter(item => item.schemaInput.hasErrors);
        if (erroneous.length) {
          erroneous.forEach(item => {
            if (!item.open) {
              // Make errors visible
              item.open = true;
            }
          });
        } else {
          const next = this.items.map(item => ({
            ...item.schemaInput.data,
            _id: item._id,
            metaType: 'arrayItem',
            scopedArrayName: this.field.scopedArrayName
          }));
          this.next = next;
        }
        // Our validate method was called first before that of
        // the subfields, so remedy that by calling again on any
        // change to the subfield state during validation
        if (this.triggerValidation) {
          this.validateAndEmit();
        }
      }
    },
    followingValues: {
      // Re-evaluate following values when the parentFollowingValues prop changes
      async handler(values) {
        if (this.shouldResetConditionalFields) {
          this.setItemsConditionalFields();
        }
      },
      deep: true
    }
  },
  async mounted() {
    if (this.isInline) {
      await this.evaluateExternalConditions();
      this.setItemsConditionalFields();
    }
  },
  methods: {
    toggleAll(open) {
      this.items = this.items.map(item => ({
        ...item,
        open
      }));
    },
    startDragging(event) {
      this.isDragging = true;
      this.disengageAll();
      this.toggleEngage({ target: event.item });
    },
    stopDragging(event) {
      this.isDragging = false;
      document.getSelection().removeAllRanges();
      this.focusElement(event.item.getAttribute('data-id'));
    },
    getInlineMenuItems(index) {
      const menu = klona(this.inlineContextMenu);
      if (index === 0 && menu.some(i => i.action === 'move-up')) {
        menu.find(i => i.action === 'move-up').modifiers = [ 'disabled' ];
      }
      if (index + 1 === this.items.length && menu.some(i => i.action === 'move-down')) {
        menu.find(i => i.action === 'move-down').modifiers = [ 'disabled' ];
      }
      return menu;
    },
    getTableHeaderClass(field, baseClass) {
      const validChars = /[^a-zA-Z0-9_-]/g;
      const label = this.$t(field.label)
        .replace(validChars, '-')
        .toLowerCase();
      return `${baseClass}--${label}`;
    },
    toggleEngage(event, options = {}) {
      let elId = event.target.getAttribute('data-id');
      if (!elId && !options.exact) {
        elId = event.target.closest('[data-id]').getAttribute('data-id');
      }

      if (!elId) {
        return;
      }

      const item = this.items.find(i => i._id === elId);
      const wasEngaged = item.engaged;
      this.disengageAll();
      item.engaged = !wasEngaged;

      if (options.prevent) {
        event.preventDefault();
      }
    },
    disengageAll() {
      this.items.forEach(i => {
        i.engaged = false;
      });
    },
    moveUpdate({
      oldIndex, newIndex
    }) {
      this.items.splice(newIndex, 0, this.items.splice(oldIndex, 1)[0]);
    },
    getItemData(_id) {
      return (this.items.find((item) => item._id === _id))?.schemaInput.data;
    },
    getItemDataWithFollowingValues(itemId) {
      const data = this.getItemData(itemId) || {};
      const followingValues = this.computeFollowingValues(data, true);
      return {
        ...followingValues,
        ...data
      };
    },
    setItemsConditionalFields(itemId) {
      if (itemId) {
        this.itemsConditionalFields[itemId] = this.getConditionalFields(
          this.getItemDataWithFollowingValues(itemId)
        );
        return;
      }

      for (const _id of Object.keys(this.itemsConditionalFields)) {
        this.itemsConditionalFields[_id] = this.getConditionalFields(
          this.getItemDataWithFollowingValues(_id)
        );
      }
    },
    emitValidate() {
      this.$emit('validate');
    },
    validate(value) {
      if (this.items.find(item => item.schemaInput.hasErrors)) {
        return 'invalid';
      }
      if (this.field.required && !value.length) {
        return 'required';
      }
      if (this.field.min && value.length < this.field.min) {
        return 'min';
      }
      if (this.field.max && value.length > this.field.max) {
        return 'max';
      }
      if (value.length && this.field.fields && this.field.fields.add) {
        const [ uniqueFieldName, uniqueFieldSchema ] = Object
          .entries(this.field.fields.add)
          .find(([ , subfield ]) => subfield.unique) || [];
        if (uniqueFieldName) {
          const duplicates = this.next
            .map(item =>
              Array.isArray(item[uniqueFieldName])
                ? item[uniqueFieldName].map(i => i._id).sort().join('|')
                : item[uniqueFieldName])
            .filter((item, index, array) => array.indexOf(item) !== index);

          if (duplicates.length) {
            duplicates.forEach(duplicate => {
              this.items.forEach(item => {
                uniqueFieldSchema.type === 'relationship'
                  ? item.schemaInput.data[uniqueFieldName]?.forEach(datum => {
                    item.schemaInput.fieldState[uniqueFieldName].duplicate = duplicate
                      .split('|').find(i => i === datum._id);
                  })
                  : item.schemaInput.fieldState[uniqueFieldName].duplicate = item
                    .schemaInput.data[uniqueFieldName] === duplicate;
              });
            });

            return {
              name: 'duplicate',
              message: `${this.$t('apostrophe:duplicateError')} ${this.$t(uniqueFieldSchema.label) || uniqueFieldName}`
            };
          }
        }
      }
      return false;
    },
    async edit() {
      const result = await apos.modal.execute('AposArrayEditor', {
        field: this.field,
        inputSchema: this.schema,
        items: this.next,
        serverError: this.serverError,
        docId: this.docId,
        parentFollowingValues: this.followingValues,
        meta: this.arrayMeta
      });
      if (result) {
        this.next = result;
      }
    },
    getNext() {
      // Next should consistently be an array.
      return (this.modelValue && Array.isArray(this.modelValue.data))
        ? this.modelValue.data
        : (this.field.def || []);
    },
    disableAdd() {
      return this.field.max && (this.items.length >= this.field.max);
    },
    remove(_id) {
      this.items = this.items.filter(item => item._id !== _id);
      delete this.itemsConditionalFields[_id];
    },
    add() {
      const _id = createId();
      this.items.push({
        _id,
        schemaInput: {
          data: this.newInstance()
        },
        open: true,
        engaged: false
      });
      this.setItemsConditionalFields(_id);
      this.focusElement(_id);
    },
    duplicate(originalId, originalIndex) {
      const original = this.items.find(i => i._id === originalId);
      const titleField = this.field.titleField || null;
      const id = createId();
      const dup = {
        _id: id,
        schemaInput: klona(original.schemaInput),
        open: false,
        engaged: false
      };
      const titleFieldVal = get(dup.schemaInput.data, titleField);
      if (titleField) {
        dup.schemaInput.data[titleField] = `${this.$t('apostrophe:duplicateOf')} ${titleFieldVal}`;
      }

      if (originalIndex + 1 === this.items.length) {
        this.items.push(dup);
      } else {
        this.items.splice(originalIndex + 1, 0, dup);
      }
      this.focusElement(id);
    },
    newInstance() {
      return newInstance(this.schema);
    },
    getLabel(id, index) {
      const titleField = this.field.titleField || 'title';
      const item = this.items.find(item => item._id === id);
      return get(item.schemaInput.data, titleField) || `Item ${index + 1}`;
    },
    toggleOpenInlineItem(event) {
      if (!event) {
        return;
      }

      const elId =
        event.target.getAttribute('data-id')
          ? event.target.getAttribute('data-id')
          : event.target.closest('[data-id]').getAttribute('data-id');
      const item = this.items.find(i => i._id === elId);
      if (item) {
        item.open = !item.open;
      }
    },
    moveEngaged(event, id, direction, options = {}) {
      const item = this.items.find(i => i._id === id);
      const index = this.items.indexOf(item);

      if (
        ((index + direction) === this.items.length) ||
        ((index + direction < 0))
      ) {
        // already first or last, don't move
        return;
      }

      if (item.engaged) {
        this.moveUpdate({
          oldIndex: index,
          newIndex: index + direction
        });
        this.focusElement(id);

        if (options.prevent) {
          event.preventDefault();
        }
      }
    },
    focusElement(id) {
      this.$nextTick(() => {
        const el = this.$refs.root.$el.querySelector(`[data-id="${id}"]`);
        if (el) {
          el.focus();
        }
      });
    },
    getFollowingValues(item) {
      return this.computeFollowingValues(item.schemaInput.data);
    },
    // Retrieve table heading fields from the schema, based on the currently
    // opened item. Available only when the field style is `table`.
    visibleSchema() {
      if (this.field.style !== 'table') {
        return this.schema;
      }
      const currentItem = this.items.find(item => item.open) ||
        this.items[this.items.length - 1];

      return this.schema.filter(
        field => this.itemsConditionalFields[currentItem._id]?.if[field.name] !== false
      );
    },
    inlineMenuHandler(event, { index, id }) {
      switch (event.action) {
        case 'move-up':
          this.moveUpdate({
            oldIndex: index,
            newIndex: index - 1
          });
          break;
        case 'move-down':
          this.moveUpdate({
            oldIndex: index,
            newIndex: index + 1
          });
          break;
        case 'remove':
          this.remove(id);
          break;
        case 'duplicate':
          this.duplicate(id, index);
          break;
      }
    }
  }
};

function modelItems(items, field, schema) {
  return items.map(item => {
    return {
      _id: item._id || createId(),
      schemaInput: {
        data: item || {}
      },
      open: false,
      engaged: false
    };
  });
}
