import ElementService from '@baserow/modules/builder/services/element'
import PublicBuilderService from '@baserow/modules/builder/services/publishedBuilder'
import { calculateTempOrder } from '@baserow/modules/core/utils/order'
import BigNumber from 'bignumber.js'

const populateElement = (element) => {
  element._ = {
    contentLoading: false,
    content: [],
    hasNextPage: false,
    reset: 0,
    shouldBeFocused: false,
  }

  return element
}

const state = {
  // The currently selected element
  selected: null,
}

const updateContext = {
  updateTimeout: null,
  promiseResolve: null,
  lastUpdatedValues: null,
  valuesToUpdate: {},
}

const mutations = {
  SET_ITEMS(state, { page, elements }) {
    state.selected = null
    page.elements = elements.map(populateElement)
  },
  ADD_ITEM(state, { page, element, beforeId = null }) {
    page.elements.push(populateElement(element))
  },
  UPDATE_ITEM(state, { page, element: elementToUpdate, values }) {
    page.elements.forEach((element) => {
      if (element.id === elementToUpdate.id) {
        Object.assign(element, values)
      }
    })
    if (state.selected?.id === elementToUpdate.id) {
      Object.assign(state.selected, values)
    }
  },
  DELETE_ITEM(state, { page, elementId }) {
    const index = page.elements.findIndex((element) => element.id === elementId)
    if (index > -1) {
      page.elements.splice(index, 1)
    }
  },
  MOVE_ITEM(state, { page, index, oldIndex }) {
    page.elements.splice(index, 0, page.elements.splice(oldIndex, 1)[0])
  },
  SELECT_ITEM(state, { element }) {
    state.selected = element
  },
  CLEAR_ITEMS(state, { page }) {
    page.elements = []
  },
}

const actions = {
  clearAll({ commit }, { page }) {
    commit('CLEAR_ITEMS', { page })
  },
  forceCreate({ commit }, { page, element }) {
    commit('ADD_ITEM', { page, element })

    const elementType = this.$registry.get('element', element.type)
    elementType.afterCreate(element, page)
  },
  forceUpdate({ commit }, { page, element, values }) {
    commit('UPDATE_ITEM', { page, element, values })
    const elementType = this.$registry.get('element', element.type)
    elementType.afterUpdate(element, page)
  },
  forceDelete({ commit, getters }, { page, elementId }) {
    const elementsOfPage = getters.getElements(page)
    const elementIndex = elementsOfPage.findIndex(
      (element) => element.id === elementId
    )
    const elementToDelete = elementsOfPage[elementIndex]

    if (getters.getSelected?.id === elementId) {
      commit('SELECT_ITEM', { element: null })
    }
    commit('DELETE_ITEM', { page, elementId })

    const elementType = this.$registry.get('element', elementToDelete.type)
    elementType.afterDelete(elementToDelete, page)
  },
  forceMove(
    { commit, getters },
    { page, elementId, beforeElementId, parentElementId, placeInContainer }
  ) {
    const beforeElement = getters.getElementById(page, beforeElementId)
    const afterOrder = beforeElement?.order || null
    const beforeOrder =
      getters.getPreviousElement(
        page,
        beforeElement,
        parentElementId,
        placeInContainer
      )?.order || null
    const tempOrder = calculateTempOrder(beforeOrder, afterOrder)

    commit('UPDATE_ITEM', {
      page,
      element: getters.getElementById(page, elementId),
      values: {
        order: tempOrder,
        parent_element_id: parentElementId,
        place_in_container: placeInContainer,
      },
    })
  },
  select({ commit }, { element }) {
    updateContext.lastUpdatedValues = null
    commit('SELECT_ITEM', { element })
  },
  async create(
    { dispatch },
    {
      page,
      elementType: elementTypeName,
      beforeId = null,
      configuration = null,
      forceCreate = true,
    }
  ) {
    const elementType = this.$registry.get('element', elementTypeName)
    const { data: element } = await ElementService(this.$client).create(
      page.id,
      elementTypeName,
      beforeId,
      elementType.getDefaultValues(page, configuration)
    )

    if (forceCreate) {
      await dispatch('forceCreate', { page, element })
      await dispatch('select', { element })
    }

    return element
  },
  async update({ dispatch }, { page, element, values }) {
    const oldValues = {}
    const newValues = {}
    Object.keys(values).forEach((name) => {
      if (Object.prototype.hasOwnProperty.call(element, name)) {
        oldValues[name] = element[name]
        newValues[name] = values[name]
      }
    })

    await dispatch('forceUpdate', { page, element, values: newValues })

    try {
      await ElementService(this.$client).update(element.id, values)
    } catch (error) {
      await dispatch('forceUpdate', { page, element, values: oldValues })
      throw error
    }
  },

  async debouncedUpdateSelected({ dispatch, getters }, { page, values }) {
    const element = getters.getSelected

    const oldValues = {}
    Object.keys(values).forEach((name) => {
      if (Object.prototype.hasOwnProperty.call(element, name)) {
        oldValues[name] = element[name]
        // Accumulate the changed values to send all the ongoing changes with the
        // final request
        updateContext.valuesToUpdate[name] = values[name]
      }
    })

    await dispatch('forceUpdate', {
      page,
      element,
      values: updateContext.valuesToUpdate,
    })

    return new Promise((resolve, reject) => {
      const fire = async () => {
        const toUpdate = updateContext.valuesToUpdate
        updateContext.valuesToUpdate = {}
        try {
          await ElementService(this.$client).update(element.id, toUpdate)
          updateContext.lastUpdatedValues = null
          resolve()
        } catch (error) {
          // Revert to old values on error
          await dispatch('forceUpdate', {
            page,
            element,
            values: updateContext.lastUpdatedValues,
          })
          updateContext.lastUpdatedValues = null
          reject(error)
        }
      }

      if (updateContext.promiseResolve) {
        updateContext.promiseResolve()
        updateContext.promiseResolve = null
      }

      clearTimeout(updateContext.updateTimeout)

      if (!updateContext.lastUpdatedValues) {
        updateContext.lastUpdatedValues = oldValues
      }

      updateContext.updateTimeout = setTimeout(fire, 500)
      updateContext.promiseResolve = resolve
    })
  },
  async delete({ dispatch, getters }, { page, elementId }) {
    const elementsOfPage = getters.getElements(page)
    const elementIndex = elementsOfPage.findIndex(
      (element) => element.id === elementId
    )
    const elementToDelete = elementsOfPage[elementIndex]

    const descendants = getters.getDescendants(page, elementToDelete)

    // First delete all children
    await Promise.all(
      descendants.map((descendant) =>
        dispatch('forceDelete', { page, elementId: descendant.id })
      )
    )

    await dispatch('forceDelete', { page, elementId })

    try {
      await ElementService(this.$client).delete(elementId)
    } catch (error) {
      await dispatch('forceCreate', {
        page,
        element: elementToDelete,
      })
      // Then restore all children
      await Promise.all(
        descendants.map((descendant) =>
          dispatch('forceCreate', { page, element: descendant })
        )
      )
      throw error
    }
  },
  async fetch({ commit }, { page }) {
    const { data: elements } = await ElementService(this.$client).fetchAll(
      page.id
    )

    commit('SET_ITEMS', { page, elements })

    return elements
  },
  async fetchPublished({ commit }, { page }) {
    const { data: elements } = await PublicBuilderService(
      this.$client
    ).fetchElements(page)

    commit('SET_ITEMS', { page, elements })

    return elements
  },
  async move(
    { commit, dispatch, getters },
    {
      page,
      elementId,
      beforeElementId,
      parentElementId = null,
      placeInContainer = null,
    }
  ) {
    const element = getters.getElementById(page, elementId)

    await dispatch('forceMove', {
      page,
      elementId,
      beforeElementId,
      parentElementId,
      placeInContainer,
    })

    try {
      const { data: elementUpdated } = await ElementService(this.$client).move(
        elementId,
        beforeElementId,
        parentElementId,
        placeInContainer
      )

      dispatch('forceUpdate', {
        page,
        element: elementUpdated,
        values: { ...elementUpdated },
      })
    } catch (error) {
      await dispatch('forceUpdate', {
        page,
        element,
        values: element,
      })
      throw error
    }
  },
  async duplicate({ commit, dispatch, getters }, { page, elementId }) {
    const {
      data: { elements, workflow_actions: workflowActions },
    } = await ElementService(this.$client).duplicate(elementId)

    const elementPromises = elements.map((element) =>
      dispatch('forceCreate', { page, element })
    )
    const workflowActionPromises = workflowActions.map((workflowAction) =>
      dispatch(
        'workflowAction/forceCreate',
        { page, workflowAction },
        { root: true }
      )
    )

    await Promise.all(elementPromises.concat(workflowActionPromises))

    const elementToDuplicate = getters.getElementById(page, elementId)
    const elementToSelect = elements.find(
      ({ parent_element_id: parentId }) =>
        parentId === elementToDuplicate.parent_element_id
    )

    commit('SELECT_ITEM', { element: elementToSelect })

    return elements
  },
  emitElementEvent({ getters }, { event, page, ...rest }) {
    const elements = getters.getElements(page)

    elements.forEach((element) => {
      const elementType = this.$registry.get('element', element.type)
      elementType.onElementEvent(event, { page, element, ...rest })
    })
  },
}

/** Recursively order the elements from up to down and left to right */
const orderElements = (elements, parentElementId = null) => {
  return elements
    .filter(
      ({ parent_element_id: curentParentElementId }) =>
        curentParentElementId === parentElementId
    )
    .sort((a, b) => {
      if (a.place_in_container !== b.place_in_container) {
        return a.place_in_container > b.place_in_container ? 1 : -1
      }

      return a.order.gt(b.order) ? 1 : -1
    })
    .map((element) => [element, ...orderElements(elements, element.id)])
    .flat()
}

const getters = {
  getElements: (state) => (page) => {
    return page.elements.map((element) => ({
      ...element,
      order: new BigNumber(element.order),
    }))
  },
  getElementById: (state, getters) => (page, id) => {
    return getters.getElements(page).find((e) => e.id === id)
  },
  getElementsOrdered: (state, getters) => (page) => {
    return orderElements(getters.getElements(page))
  },
  getRootElements: (state, getters) => (page) => {
    return getters
      .getElementsOrdered(page)
      .filter((e) => e.parent_element_id === null)
  },
  getChildren: (state, getters) => (page, element) => {
    return getters
      .getElementsOrdered(page)
      .filter((e) => e.parent_element_id === element.id)
  },
  getDescendants: (state, getters) => (page, element) => {
    return getters
      .getChildren(page, element)
      .map((child) => [...getters.getChildren(page, child), child])
      .flat()
  },
  getAncestors: (state, getters) => (page, element) => {
    const getElementAncestors = (element) => {
      if (element.parent_element_id === null) {
        return []
      } else {
        const parentElement = getters.getElementById(
          page,
          element.parent_element_id
        )
        return [...getElementAncestors(parentElement), parentElement]
      }
    }
    return getElementAncestors(element)
  },
  getSiblings: (state, getters) => (page, element) => {
    return getters
      .getElementsOrdered(page)
      .filter((e) => e.parent_element_id === element.parent_element_id)
  },
  getElementPosition:
    (state, getters) =>
    (page, element, sameType = false) => {
      const elements = getters.getElementsOrdered(page)

      return (
        (sameType
          ? elements.filter(({ type }) => type === element.type)
          : elements
        ).findIndex(({ id }) => id === element.id) + 1
      )
    },
  getElementsInPlace:
    (state, getters) => (page, parentId, placeInContainer) => {
      return getters
        .getElementsOrdered(page)
        .filter(
          (e) =>
            e.parent_element_id === parentId &&
            e.place_in_container === placeInContainer
        )
    },
  getPreviousElement:
    (state, getters) => (page, before, parentId, placeInContainer) => {
      const elementsInPlace = getters.getElementsInPlace(
        page,
        parentId,
        placeInContainer
      )
      return before
        ? elementsInPlace.reverse().find((e) => e.order.lt(before.order)) ||
            null
        : elementsInPlace.at(-1)
    },
  getSelected(state) {
    return state.selected
  },
}

export default {
  namespaced: true,
  state,
  getters,
  actions,
  mutations,
}