import { onClickOutside } from '@baserow/modules/core/utils/dom'
import baseField from '@baserow/modules/database/mixins/baseField'
import copyPasteHelper from '@baserow/modules/database/mixins/copyPasteHelper'

/**
 * A mixin that can be used by a field grid component. It introduces the props that
 * will be passed by the GridViewField component and it created methods that are
 * going to be called.
 */
export default {
  mixins: [baseField, copyPasteHelper],
  props: {
    /**
     * Indicates if the grid field is in a selected state.
     */
    selected: {
      type: Boolean,
      required: true,
    },
    readOnly: {
      type: Boolean,
      required: true,
    },
    storePrefix: {
      type: String,
      required: true,
    },
  },
  watch: {
    /**
     * It could happen that the cell is not select, but still being kept alive to
     * finish a task. This for example happens when the user selects another cell
     * while still uploading a file. When the selected state changes, we do want to
     * add and remove the event listeners to prevent conflicts.
     */
    selected(value) {
      if (value) {
        this._select()
      } else {
        this._beforeUnSelect()
      }
    },
  },
  mounted() {
    if (this.selected) {
      this._select()
    }
  },
  beforeDestroy() {
    // It could be that the cell has already been unselected, in that case we don't
    // have to before unselect twice.
    if (this.selected) {
      this._beforeUnSelect()
    }
  },
  methods: {
    /**
     * This method adds an event listener to the given element. It also
     * automatically removes the event listeners when the cell is unselected
     * (emitted from `_beforeUnSelect`) so there's no need to do that manually.
     */
    addEventListenerWithAutoRemove(el, event, eventHandler) {
      el.addEventListener(event, eventHandler)
      this.$once('unselected', () => {
        el.removeEventListener(event, eventHandler)
      })
    },
    /**
     * This method is called when the cell is selected to add all the event
     * listeners needed to handle the user interaction.
     * It uses the `addEventListenerWithAutoRemove` method to automatically
     * remove the event listeners when the cell is unselected.
     */
    setupAllEventListenersOnCellSelected() {
      this.addEventListenerWithAutoRemove(
        this.$el,
        'dblclick',
        this.doubleClick
      )

      // Register a body click event listener so that we can detect if a user has
      // clicked outside the field. If that happens we want to unselect the field and
      // possibly save the value.
      const clickOutsideEventCancel = onClickOutside(
        this.$el,
        (target, event) => {
          if (
            // Check if the event has the 'preventFieldCellUnselect' attribute which
            // if true should prevent the field from being unselected.
            !(
              'preventFieldCellUnselect' in event &&
              event.preventFieldCellUnselect
            ) &&
            // If the child field allows to unselect when clicked outside.
            this.canUnselectByClickingOutside(event)
          ) {
            this.$emit('unselect')
          }
        }
      )
      this.$once('unselected', clickOutsideEventCancel)

      // Event that is called when a key is pressed while the field is selected.
      const keyDownEventListener = (event) => {
        // When for example a related modal is open all the key combinations must be
        // ignored because the focus is not in the cell.
        if (!this.canKeyDown(event)) {
          return
        }

        // If the tab or arrow keys are pressed we want to select the next field. This
        // is however out of the scope of this component so we emit the selectNext
        // event that the GridView can handle.
        const { key, shiftKey } = event
        const arrowKeysMapping = {
          ArrowLeft: 'selectPrevious',
          ArrowUp: 'selectAbove',
          ArrowRight: 'selectNext',
          ArrowDown: 'selectBelow',
        }
        if (this.canSelectNext(event)) {
          if (Object.keys(arrowKeysMapping).includes(key) && !shiftKey) {
            event.preventDefault()
            this.$emit(arrowKeysMapping[key])
          } else if (key === 'Tab') {
            event.preventDefault()
            this.$emit(shiftKey ? 'selectPrevious' : 'selectNext')
          } else if (key === 'Enter' && shiftKey && !this.readOnly) {
            event.preventDefault()
            event.preventFieldCellUnselect = true
            this.$emit('add-row-after')
            this.$emit('selectBelow')
            return
          }
        }

        // Removes the value if the backspace/delete key is pressed.
        if (
          (key === 'Delete' || key === 'Backspace') &&
          this.canKeyboardShortcut(event)
        ) {
          event.preventDefault()
          const value = this.$registry
            .get('field', this.field.type)
            .getEmptyValue(this.field)
          const oldValue = this.value
          if (
            value !== oldValue &&
            !this.readOnly &&
            !this.field._.type.isReadOnly
          ) {
            this.$emit('update', value, oldValue)
          }
        }

        // Space bar should enlarge the row.
        if (key === ' ' && this.canKeyboardShortcut(event)) {
          event.preventDefault()
          this.$emit('edit-modal')
        }
      }
      this.addEventListenerWithAutoRemove(
        document.body,
        'keydown',
        keyDownEventListener
      )

      const copyEventListener = async (event) => {
        if (!this.canKeyDown(event) || !this.canKeyboardShortcut(event)) return

        await this.copySelectionToClipboard(
          Promise.resolve([
            [this.field],
            [{ [`field_${this.field.id}`]: this.value }],
          ])
        )
        // prevent Safari from beeping since the window.getSelection() is empty
        event.preventDefault()
      }
      this.addEventListenerWithAutoRemove(window, 'copy', copyEventListener)

      // Updates the value of the field when a user pastes something in the field.
      const pasteEventListener = async (event) => {
        if (!this.canKeyboardShortcut(event)) {
          return
        }

        // Try to call the field handler if one exists
        if (this.onPaste) {
          // If the return value of onPaste is true then we must stop event handling
          // here. It means the event has already been handled.
          if (this.onPaste(event)) {
            return
          }
        }

        try {
          const [data, jsonData] = await this.extractClipboardData(event)
          // A grid field cell can only handle one single value. We try to extract
          // that from the clipboard and update the cell, otherwise we emit the
          // paste event up.
          if (data.length === 1 && data[0].length === 1) {
            const value = this.$registry
              .get('field', this.field.type)
              .prepareValueForPaste(
                this.field,
                data[0][0],
                jsonData !== null ? jsonData[0][0] : undefined
              )
            const oldValue = this.value

            if (
              value !== undefined &&
              value !== oldValue &&
              !this.readOnly &&
              !this.field._.type.isReadOnly
            ) {
              this.$emit('update', value, oldValue)
            }
          } else {
            // This is a multi cell paste
            event.stopPropagation()
            this.$emit('paste', { textData: data, jsonData })
          }
        } catch (e) {}
      }
      this.addEventListenerWithAutoRemove(document, 'paste', pasteEventListener)
    },
    /**
     * Adds all the event listeners related to all the field types, for example when a
     * user presses the one of the arrow keys, tab, backspace, double clicks etc. This
     * method is not meant to be overwritten.
     */
    _select() {
      this.setupAllEventListenersOnCellSelected()
      this.select()

      // Emit the selected event so that the parent component can take an action like
      // making sure that the element fits in the viewport.
      this.$emit('selected', { component: this })
    },
    /**
     * Removes all the listeners related to all field types.
     */
    _beforeUnSelect() {
      this.beforeUnSelect()
      this.$emit('unselected', { component: this })
    },
    /**
     * Method that is called when the column is selected. For example when clicked
     * on the field. This is the moment to register event listeners if they are needed.
     */
    select() {},
    /**
     * Method that is called when the column is unselected. For example when clicked
     * outside the field. This is the moment to remove any event listeners.
     */
    beforeUnSelect() {},
    /**
     * Method that is called when the column is double clicked. Some grid fields want
     * to do something here apart from triggering the selected state. A boolean
     * toggles its value for example.
     */
    doubleClick() {},
    /**
     * There are keyboard shortcuts to select the next or previous field. For
     * example when the arrow or tab keys are pressed. The GridViewField component
     * first asks if this is allowed by calling this function. If false is returned
     * the next field is not going to be selected.
     */
    canSelectNext() {
      return true
    },
    /**
     * If the user clicks outside the cell, the cell is automatically unselected. In
     * some cases, for example when you have a context menu as helper, you might not
     * want to unselect when the user clicks in the context menu. The can be
     * prevented by returned false here. The context menu lives at the root of the
     * body element and not inside the cell.
     */
    canUnselectByClickingOutside() {
      return true
    },
    /**
     * It must be possible for a field to ignore all key combinations. For example when
     * it is possible for a field to open a modal to select some data, the
     * backspace/delete key should not empty the field at that moment.
     */
    canKeyDown() {
      return true
    },
    /**
     * If the user presses a keyboard shortcut like ctrl/cmd + v or spacebar while a
     * field is selected In some cases, for example when the user is editing the
     * value, we do not want to allow the keyboard shortcut to work. If false it will
     * be disabled.
     */
    canKeyboardShortcut() {
      return true
    },
  },
}