<template>
  <RecursiveWrapper
    :components="
      wrapperDecorations.map((comp) => ({
        ...comp,
        props: comp.propsFn(row),
      }))
    "
    first-component-class="grid-view__row-background-wrapper"
  >
    <div
      class="grid-view__row"
      :class="{
        'grid-view__row--selected': row._.selectedBy.length > 0,
        'grid-view__row--loading': row._.loading,
        'grid-view__row--hover': row._.hover,
        'grid-view__row--warning':
          !row._.matchFilters || !row._.matchSortings || !row._.matchSearch,
      }"
      @mouseover="$emit('row-hover', { row, value: true })"
      @mouseleave="$emit('row-hover', { row, value: false })"
      @contextmenu.prevent="$emit('row-context', { row, event: $event })"
    >
      <template v-if="includeRowDetails">
        <div
          v-if="
            !row._.matchFilters || !row._.matchSortings || !row._.matchSearch
          "
          class="grid-view__row-warning"
        >
          <template v-if="!row._.matchFilters">
            {{ $t('gridViewRow.rowNotMatchingFilters') }}
          </template>
          <template v-else-if="!row._.matchSearch">
            {{ $t('gridViewRow.rowNotMatchingSearch') }}
          </template>
          <template v-else-if="!row._.matchSortings">{{
            $t('gridViewRow.rowHasMoved')
          }}</template>
        </div>
        <div
          class="grid-view__column grid-view__column--no-border-right"
          :class="{ 'grid-view__column--group-end': groupEnd }"
          :style="{ width: gridViewRowDetailsWidth + 'px' }"
        >
          <div
            class="grid-view__row-info"
            :class="{
              'grid-view__row-info--matches-search':
                row._.matchSearch &&
                row._.fieldSearchMatches.includes('row_id'),
            }"
          >
            <div
              class="grid-view__row-count"
              :class="{ 'grid-view__row-count--small': rowIdentifier > 9999 }"
              :title="rowIdentifier"
            >
              {{ rowIdentifier }}
            </div>
            <div
              v-if="!readOnly && canDrag"
              class="grid-view__row-drag"
              @mousedown="startDragging($event, row)"
            ></div>
            <component
              :is="rowExpandButton"
              v-if="!row._.loading"
              :row="row"
              :workspace-id="workspaceId"
              :table="view.table"
              @edit-modal="$emit('edit-modal', row)"
            ></component>
            <component
              :is="dec.component"
              v-for="dec in firstCellDecorations"
              :key="dec.decoration.id"
              v-bind="dec.propsFn(row)"
            />
          </div>
        </div>
      </template>
      <!--
      Somehow re-declaring all the events instead of using v-on="$listeners" speeds
      everything up because the rows don't need to be updated everytime a new one is
      rendered, which happens a lot when scrolling.
      -->
      <GridViewCell
        v-for="field in fieldsToRender"
        :key="'row-field-' + row._.persistentId + '-' + field.id.toString()"
        :workspace-id="workspaceId"
        :field="field"
        :row="row"
        :all-fields-in-table="allFieldsInTable"
        :state="state"
        :multi-select-position="getMultiSelectPosition(row.id, field)"
        :read-only="readOnly || field.read_only"
        :store-prefix="storePrefix"
        :group-end="groupEnd"
        :style="{
          width: fieldWidths[field.id] + 'px',
          ...getSelectedCellStyle(field),
        }"
        @update="$emit('update', $event)"
        @paste="$emit('paste', $event)"
        @edit="$emit('edit', $event)"
        @select="$emit('select', $event)"
        @unselect="$emit('unselect', $event)"
        @selected="$emit('selected', $event)"
        @unselected="$emit('unselected', $event)"
        @select-next="$emit('select-next', $event)"
        @refresh-row="$emit('refresh-row', $event)"
        @cell-mousedown-left="$emit('cell-mousedown-left', { row, field })"
        @cell-mouseover="$emit('cell-mouseover', { row, field })"
        @cell-mouseup-left="$emit('cell-mouseup-left', { row, field })"
        @cell-shift-click="$emit('cell-shift-click', { row, field })"
        @add-row-after="$emit('add-row-after', $event)"
        @edit-modal="$emit('edit-modal', row)"
      ></GridViewCell>
    </div>
  </RecursiveWrapper>
</template>

<script>
import GridViewCell from '@baserow/modules/database/components/view/grid/GridViewCell'
import gridViewHelpers from '@baserow/modules/database/mixins/gridViewHelpers'
import GridViewRowExpandButton from '@baserow/modules/database/components/view/grid/GridViewRowExpandButton'
import RecursiveWrapper from '@baserow/modules/database/components/RecursiveWrapper'

export default {
  name: 'GridViewRow',
  components: {
    GridViewRowExpandButton,
    GridViewCell,
    RecursiveWrapper,
  },
  mixins: [gridViewHelpers],
  provide() {
    return {
      $hasPermission: this.$hasPermission,
    }
  },
  props: {
    view: {
      type: Object,
      required: true,
    },
    workspaceId: {
      type: Number,
      required: true,
    },
    row: {
      type: Object,
      required: true,
    },
    groupEnd: {
      type: Boolean,
      required: false,
      default: () => false,
    },
    renderedFields: {
      type: Array,
      required: true,
    },
    decorationsByPlace: {
      type: Object,
      required: true,
    },
    visibleFields: {
      type: Array,
      required: true,
    },
    allFieldsInTable: {
      type: Array,
      required: true,
    },
    fieldWidths: {
      type: Object,
      required: true,
    },
    includeRowDetails: {
      type: Boolean,
      required: false,
      default: () => false,
    },
    includeGroupBy: {
      type: Boolean,
      required: false,
      default: () => false,
    },
    readOnly: {
      type: Boolean,
      required: true,
    },
    canDrag: {
      type: Boolean,
      required: true,
    },
    rowIdentifierType: {
      type: String,
      required: true,
      default: 'count',
    },
    count: {
      type: Number,
      required: true,
    },
    primaryFieldIsSticky: {
      type: Boolean,
      required: false,
      default: () => true,
    },
  },
  data() {
    return {
      // The state can be used by functional components to make changes to the dom.
      // This is for example used by the functional file field component to enable the
      // drop effect without having the cell selected.
      state: {},
      // A list containing field id's of field cells that must not be converted to the
      // functional component even though the user has selected another cell. This is
      // for example used by the file field to finish the uploading task if the user
      // has selected another cell while uploading.
      alive: [],
      rowExpandButton: this.$registry
        .get('application', 'database')
        .getRowExpandButtonComponent(),
    }
  },
  computed: {
    /**
     * This component already accepts a `renderedFields` property containing the fields
     * that must be rendered based on the viewport width and horizontal scroll offset,
     * meaning it only renders the fields that are in the viewport. Because a selected
     * field must always be rendered, this computed property checks if there is a
     * selected field and if so, it's added to the array. This doesn't influence the
     * position of the other cells because the position will be absolute. The selected
     * field must always be rendered, otherwise the arrow keys and other functionality
     * won't work.
     */
    fieldsToRender() {
      // If the row doesn't have a selected field, we can safely return the fields
      // because we just want to render the fields inside of the view port.
      if (!this.row._.selected) {
        return this.renderedFields
      }

      // Check if the selected field exists in the all fields array, so not just the to
      // be rendered ones.
      const selectedField = this.visibleFields.find(
        (field) => field.id === this.row._.selectedFieldId
      )

      // If it doesn't exist or if it's already in the fields array, we don't have to
      // add it because it's already rendered.
      if (
        selectedField === undefined ||
        this.renderedFields.find((field) => field.id === selectedField.id) !==
          undefined
      ) {
        return this.renderedFields
      }

      // If the selected field exists in all fields, but not in fields it must be added
      // to the fields array because we want to render it. It won't influence the other
      // cells because it's positioned absolute.
      const fields = this.renderedFields.slice()
      fields.unshift(selectedField)
      return fields
    },
    firstCellDecorations() {
      return this.decorationsByPlace?.first_cell || []
    },
    wrapperDecorations() {
      return this.decorationsByPlace?.wrapper || []
    },
    rowIdentifier() {
      switch (this.rowIdentifierType) {
        case 'count':
          return this.count
        default:
          return this.row.id
      }
    },
  },
  methods: {
    isCellSelected(fieldId) {
      return this.row._.selected && this.row._.selectedFieldId === fieldId
    },
    selectCell(fieldId, rowId = this.row.id) {
      this.$emit('cell-selected', { fieldId, rowId })
    },
    // Return an object that represents if a cell is selected,
    // and it's current position in the selection grid
    getMultiSelectPosition(rowId, field) {
      const position = {
        selected: false,
        top: false,
        right: false,
        bottom: false,
        left: false,
      }
      if (
        this.$store.getters[this.storePrefix + 'view/grid/isMultiSelectActive']
      ) {
        const rowIndex =
          this.$store.getters[this.storePrefix + 'view/grid/getRowIndexById'](
            rowId
          )

        const allFieldIds = this.visibleFields.map((field) => field.id)
        let fieldIndex = allFieldIds.findIndex((id) => field.id === id)
        fieldIndex += !field.primary && this.primaryFieldIsSticky ? 1 : 0

        const [minRow, maxRow] =
          this.$store.getters[
            this.storePrefix + 'view/grid/getMultiSelectRowIndexSorted'
          ]
        const [minField, maxField] =
          this.$store.getters[
            this.storePrefix + 'view/grid/getMultiSelectFieldIndexSorted'
          ]

        if (rowIndex >= minRow && rowIndex <= maxRow) {
          if (fieldIndex >= minField && fieldIndex <= maxField) {
            position.selected = true
            if (rowIndex === minRow) {
              position.top = true
            }
            if (rowIndex === maxRow) {
              position.bottom = true
            }
            if (fieldIndex === minField) {
              position.left = true
            }
            if (fieldIndex === maxField) {
              position.right = true
            }
          }
        }
      }
      return position
    },
    setState(value) {
      this.state = value
    },
    addKeepAlive(fieldId) {
      if (!this.alive.includes(fieldId)) {
        this.alive.push(fieldId)
      }
    },
    removeKeepAlive(fieldId) {
      const index = this.alive.findIndex((id) => id === fieldId)
      if (index > -1) {
        this.alive.splice(index, 1)
      }
    },
    startDragging(event, row) {
      if (this.readOnly) {
        return
      }

      event.preventDefault()
      this.$emit('row-dragging', { row, event })
    },
    /**
     * Returns an object with additional styling if the field is selected and outside
     * of the viewport. This is because selected fields must always be rendered because
     * otherwise certain functionality won't work.
     */
    getSelectedCellStyle(field) {
      const exists =
        this.renderedFields.find((f) => f.id === field.id) !== undefined

      // If the field already exists in the field list it means that it's already
      // rendered. In that case we don't have to provide any other styling because it's
      // already in the position it's supposed to be in.
      if (exists) {
        return {}
      }

      // If the field doesn't exist in the fields array, it's being rendered because
      // it's selected. In that case, the element must be positioned without influencing
      // the other cells.
      const styling = { position: 'absolute' }

      const selectedFieldIndex = this.visibleFields.findIndex(
        (field) => field.id === this.row._.selectedFieldId
      )
      const firstVisibleFieldIndex = this.visibleFields.findIndex(
        (field) => field.id === this.renderedFields[0].id
      )
      const lastVisibleFieldIndex = this.visibleFields.findIndex(
        (field) =>
          field.id === this.renderedFields[this.renderedFields.length - 1].id
      )

      // Positions the selected field cell on the right position without influencing the
      // position of the rendered cells. This is needed because other components depend
      // on the cell to be in the right position, for example when using the arrow key
      // navigation.
      if (selectedFieldIndex < firstVisibleFieldIndex) {
        // If the selected field must be positioned before the other fields
        let spaceBetween = 0
        for (let i = selectedFieldIndex; i < firstVisibleFieldIndex; i++) {
          spaceBetween += this.fieldWidths[this.visibleFields[i].id]
        }
        styling.left = -spaceBetween + 'px'
      } else if (selectedFieldIndex > lastVisibleFieldIndex) {
        // If the selected field must be positioned after the other fields.
        let spaceBetween = 0
        for (let i = lastVisibleFieldIndex; i < selectedFieldIndex; i++) {
          spaceBetween += this.fieldWidths[this.visibleFields[i].id]
        }
        styling.right = -spaceBetween + 'px'
      }

      return styling
    },
  },
}
</script>