<template>
  <div
    class="filters__items"
    :class="{
      'filters__container--dark': variant === 'dark',
      'filters__items--full-width': fullWidth,
    }"
  >
    <div
      v-for="(filter, index) in filtersTree.filtersOrdered()"
      :key="index"
      class="filters__item-wrapper"
    >
      <div class="filters__item filters__item--level-1">
        <ViewFilterFormOperator
          :index="index"
          :filter-type="filterType"
          :disable-filter="disableFilter"
          @select-boolean-operator="$emit('selectOperator', $event)"
        />
        <ViewFieldConditionItem
          :ref="`condition-${filter.id}`"
          :filter="filter"
          :view="view"
          :is-public-view="isPublicView"
          :fields="fields"
          :disable-filter="disableFilter"
          :read-only="readOnly"
          @updateFilter="updateFilter(filter, $event)"
          @deleteFilter="deleteFilter(filter, $event)"
        >
          <template #filterInputComponent="{ slotProps }">
            <slot name="filterInputComponent" :slot-props="slotProps"></slot>
          </template>
          <template #afterValueInput="{ slotProps }">
            <slot name="afterValueInput" :slot-props="slotProps"></slot>
          </template>
        </ViewFieldConditionItem>
      </div>
    </div>
    <div
      v-for="(groupNode, groupIndex) in filtersTree.groupsOrdered()"
      :key="filtersTree.filters.length + groupIndex"
      class="filters__group-item-wrapper"
    >
      <ViewFilterFormOperator
        :index="filtersTree.filters.length + groupIndex"
        :filter-type="filterType"
        :disable-filter="disableFilter"
        @select-boolean-operator="$emit('selectOperator', $event)"
      />
      <div class="filters__group-item">
        <div class="filters__group-item-filters">
          <div
            v-for="(filter, index) in groupNode.filtersOrdered()"
            :key="`${groupIndex}-${index}`"
            class="filters__item-wrapper"
          >
            <div class="filters__item filters__item--level-2">
              <ViewFilterFormOperator
                :index="index"
                :filter-type="groupNode.group.filter_type"
                :disable-filter="disableFilter"
                @select-boolean-operator="
                  $emit('selectFilterGroupOperator', {
                    value: $event,
                    filterGroup: groupNode.group,
                  })
                "
              />
              <ViewFieldConditionItem
                :ref="`condition-${filter.id}`"
                :filter="filter"
                :view="view"
                :is-public-view="isPublicView"
                :fields="fields"
                :disable-filter="disableFilter"
                :read-only="readOnly"
                @updateFilter="updateFilter(filter, $event)"
                @deleteFilter="deleteFilter(filter, $event)"
              >
                <template #filterInputComponent="{ slotProps }">
                  <slot
                    name="filterInputComponent"
                    :slot-props="slotProps"
                  ></slot>
                </template>
                <template #afterValueInput="{ slotProps }">
                  <slot name="afterValueInput" :slot-props="slotProps"></slot>
                </template>
              </ViewFieldConditionItem>
            </div>
          </div>
        </div>
        <div v-if="!disableFilter" class="filters__group-item-actions">
          <a
            class="filters__add"
            @click.prevent="$emit('addFilter', groupNode.group.id)"
          >
            <i class="filters__add-icon iconoir-plus"></i>
            {{ addConditionLabel }}</a
          >
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import ViewFilterFormOperator from '@baserow/modules/database/components/view/ViewFilterFormOperator'
import ViewFieldConditionItem from '@baserow/modules/database/components/view/ViewFieldConditionItem'
import { sortNumbersAndUuid1Asc } from '@baserow/modules/core/utils/sort'

const GroupNode = class {
  constructor(group, parent = null, sorted = false) {
    this.group = group
    this.parent = parent
    this.children = []
    this.filters = []
    this.sorted = sorted
    if (parent) {
      parent.children.push(this)
    }
  }

  groupsOrdered() {
    if (this.sorted) {
      return this.children
    }

    return this.children.sort((a, b) => {
      return sortNumbersAndUuid1Asc(a.group, b.group)
    })
  }

  filtersOrdered() {
    if (this.sorted) {
      return this.filters
    }

    return this.filters.sort(sortNumbersAndUuid1Asc)
  }

  findGroup(id) {
    if (this.group && this.group.id === id) {
      return this
    }
    for (const groupNode of this.children) {
      const found = groupNode.findGroup(id)
      if (found) {
        return found
      }
    }
    return null
  }

  addFilter(filter) {
    this.filters.push(filter)
  }

  remove() {
    if (this.parent) {
      this.parent.children = this.parent.children.filter((g) => g !== this)
    }
  }
}

export default {
  name: 'ViewFieldConditionsForm',
  components: {
    ViewFilterFormOperator,
    ViewFieldConditionItem,
  },
  props: {
    filters: {
      type: Array,
      required: true,
    },
    filterGroups: {
      type: Array,
      required: false,
      default: () => [],
    },
    disableFilter: {
      type: Boolean,
      required: true,
    },
    filterType: {
      type: String,
      required: true,
    },
    fields: {
      type: Array,
      required: true,
    },
    // A view is optional, but may be required by some specific
    // field components, such as `ViewFilterTypeLinkRow`.
    view: {
      type: Object,
      required: false,
      default: () => {},
    },
    isPublicView: {
      type: Boolean,
      required: false,
      default: false,
    },
    readOnly: {
      type: Boolean,
      required: true,
    },
    addConditionString: {
      type: String,
      required: false,
      default: null,
    },
    variant: {
      type: String,
      required: false,
      default: 'light',
    },
    fullWidth: {
      type: Boolean,
      required: false,
      default: false,
    },
    sorted: {
      type: Boolean,
      required: false,
      default: false,
    },
    /*
     * Allows the parent component to provide a function that will be called
     * when a filter is updated. This is useful for components that need to
     * do additional processing when a filter is updated.
     */
    prepareValue: {
      type: Function,
      required: false,
      default: (value, filter, field, filterType) => {
        return filterType.prepareValue(value, field, true)
      },
    },
  },
  data() {
    return {
      groups: {},
    }
  },
  computed: {
    addConditionLabel() {
      return (
        this.addConditionString ||
        this.$t('viewFieldConditionsForm.addCondition')
      )
    },
    filterTypes() {
      return this.$registry.getAll('viewFilter')
    },
    filtersTree() {
      const root = new GroupNode(null, null, this.sorted)
      const groups = { '': root }
      for (const filterGroup of this.filterGroups) {
        const parentId = filterGroup.parent || ''
        const parent = groups[parentId]
        const node = new GroupNode(filterGroup, parent, this.sorted)
        groups[filterGroup.id] = node
      }
      for (const filter of this.filters) {
        const groupId = filter.group != null ? filter.group : ''
        const group = groups[groupId]
        if (group) {
          group.addFilter(filter)
        }
      }
      return root
    },
  },
  watch: {
    'view._.focusFilter': {
      handler(filterId) {
        const filter =
          filterId !== null && this.filters.find((f) => f.id === filterId)
        if (filter) {
          this.focusFilterValue(filter)
        }
      },
      immediate: true,
    },
  },
  methods: {
    focusFilterValue(filter) {
      this.$nextTick(() => {
        const ref = `condition-${filter.id}`
        if (this.$refs[ref] && this.$refs[ref].length > 0) {
          this.$refs[ref][0]?.focusValue()
        }
        this.$emit('filterFocused', filter)
      })
    },
    /**
     * Returns a list of filter types that are allowed for the given fieldId.
     */
    allowedFilters(filterTypes, fields, fieldId) {
      const field = fields.find((f) => f.id === fieldId)
      return Object.values(filterTypes).filter((filterType) => {
        return field !== undefined && filterType.fieldIsCompatible(field)
      })
    },
    deleteFilter(filter, event) {
      event.deletedFilterEvent = true
      const groupNode = this.filtersTree.findGroup(filter.group)
      const lastInGroup = groupNode && groupNode.filters.length === 1
      if (lastInGroup) {
        this.$emit('deleteFilterGroup', groupNode)
      } else {
        this.$emit('deleteFilter', filter)
      }
    },
    /**
     * Updates a filter with the given values. Some data manipulation will also be done
     * because some filter types are not compatible with certain field types.
     */
    updateFilter(filter, values) {
      const fieldId = Object.prototype.hasOwnProperty.call(values, 'field')
        ? values.field
        : filter.field
      const type = Object.prototype.hasOwnProperty.call(values, 'type')
        ? values.type
        : filter.type
      const value = Object.prototype.hasOwnProperty.call(values, 'value')
        ? values.value
        : filter.value

      // If the field has changed we need to check if the filter type is compatible
      // and if not we are going to choose the first compatible type.
      if (Object.prototype.hasOwnProperty.call(values, 'field')) {
        const allowedFilterTypes = this.allowedFilters(
          this.filterTypes,
          this.fields,
          fieldId
        ).map((filter) => filter.type)
        if (!allowedFilterTypes.includes(type)) {
          values.type = allowedFilterTypes[0]
        }
      }

      // If the type or value has changed it could be that the value needs to be
      // formatted or prepared.
      if (
        Object.prototype.hasOwnProperty.call(values, 'field') ||
        Object.prototype.hasOwnProperty.call(values, 'type') ||
        Object.prototype.hasOwnProperty.call(values, 'value')
      ) {
        const filterType = this.$registry.get('viewFilter', type)
        const field = this.fields.find(({ id }) => id === fieldId)

        values.value = this.prepareValue(value, filter, field, filterType)
      }

      this.$emit('updateFilter', { filter, values })
    },
  },
}
</script>