import Muuri, {
  DraggerCancelEvent,
  DraggerEndEvent,
  DraggerMoveEvent,
  DragAutoScrollTarget,
  DraggerStartEvent,
  LayoutData,
  LayoutFunctionCancel,
  Item,
} from 'muuri'
import Cookies from 'js-cookie'
import create from './dom/create'
import Grid from 'muuri'
import onFind from './dom/onFind'

const colGrids: Muuri[] = []
let boardGrid: Grid
let boardContainer: HTMLDivElement

const getDragContainer = (): HTMLElement => {
  let dragContainer = document.querySelector<HTMLElement>(
    '.SearchResult-dragContainer',
  )

  if (!dragContainer) {
    dragContainer = create('div', {
      className: 'SearchResult-dragContainer',
    })

    document.body.appendChild(dragContainer)
  }

  return dragContainer
}

const doLoadMoreObservationX = (): void => {
  const board = boardContainer.querySelector(':scope .SearchResult-board')
  const onIntersectionX = (entries: IntersectionObserverEntry[]): void => {
    for (const entry of entries) {
      const { isIntersecting, target } = entry
      if (isIntersecting && !target.classList.contains('frame')) {
        observerX.unobserve(target)

        const moreColumnsButton =
          boardContainer.querySelector<HTMLButtonElement>(
            ':scope button.moreColumns-button',
          )

        if (moreColumnsButton) {
          moreColumnsButton.click()
        }
      }
    }
  }

  const onLayoutEnd = (): void => {
    // Get the sibling before any child that's a frame element.
    if (board) {
      const boardChild = board.lastChild

      if (boardChild instanceof Element) {
        const lastChild = boardChild.classList.contains('frame')
          ? board.querySelector(
              '.SearchResult-board-column:nth-last-child(-n+2)',
            )
          : board.lastChild

        if (lastChild instanceof Element) {
          observerX.observe(lastChild)
        }

        boardGrid.off('layoutEnd', onLayoutEnd)

        // Inside a Popup?
        const popup = board.closest('.Popup')

        if (popup instanceof HTMLDivElement) {
          document.body.style.setProperty(
            '--BoardContainer-zIndex',
            (parseInt(popup.style.zIndex) + 1).toString(),
          )
        }
      }
    }
  }

  const observerX = new IntersectionObserver(onIntersectionX, {
    root: boardContainer,
    rootMargin: '25px',
    threshold: 0.75,
  })

  boardGrid.on('layoutEnd', onLayoutEnd)
}

const doLoadMoreObservationY = (grid: Muuri, items: HTMLOListElement): void => {
  const listContainer = items.parentNode

  if (listContainer instanceof HTMLDivElement) {
    const onIntersectionY = (entries: IntersectionObserverEntry[]): void => {
      for (const entry of entries) {
        const { isIntersecting, target } = entry
        if (isIntersecting) {
          observerY.unobserve(target)

          const moreCardsButton =
            listContainer?.querySelector<HTMLButtonElement>(
              ':scope button.moreCards-button',
            )

          if (moreCardsButton) {
            moreCardsButton.click()
          }
        }
      }
    }

    const observerY = new IntersectionObserver(onIntersectionY, {
      root: listContainer,
      rootMargin: '25px',
      threshold: 0.75,
    })

    const onLayoutEnd = (): void => {
      const lastChild = items.lastChild

      if (lastChild instanceof Element) {
        observerY.observe(lastChild)
        grid.off('layoutEnd', onLayoutEnd)
      }
    }

    grid.on('layoutEnd', onLayoutEnd)
  }
}

// Columns within the Board.
onFind(
  '.is-muuri .SearchResult-boardContainer',
  (container: HTMLDivElement): void => {
    boardContainer = container
    const board = container.querySelector<HTMLDivElement>(
      ':scope div.SearchResult-board',
    )

    if (!board) return

    boardGrid = new Muuri(board, {
      items: '.SearchResult-board-column',
      layoutDuration: 300,
      layoutEasing: 'cubic-bezier(0.625, 0.225, 0.100, 0.890)',
      layout: (
        grid: Grid,
        layoutId: number,
        items: Item[],
        width: number,
        height: number,
        callback,
      ): void | LayoutFunctionCancel => {
        Muuri.defaultPacker.setOptions({ horizontal: true })
        return Muuri.defaultPacker.createLayout(
          grid,
          layoutId,
          items,
          width,
          height,
          (layoutData: LayoutData): void => {
            delete layoutData.styles
            callback(layoutData)
          },
        )
      },
      dragEnabled: false,
      dragAxis: 'x',
      dragSortHeuristics: {
        sortInterval: 0,
      },
      dragHandle: '.BoardView-title',
      dragRelease: {
        duration: 300,
        easing: 'cubic-bezier(0.625, 0.225, 0.100, 0.890)',
        useDragContainer: false,
      },
      dragAutoScroll: {
        targets: [{ element: boardContainer, axis: Muuri.AutoScroller.AXIS_X }],
      },
    })

    const errorElement = boardContainer.querySelector(
      '.SearchResult-board-errors',
    )

    onFind('.SearchResult-board-errors-close', (element: HTMLElement): void => {
      element.onclick = (): void => {
        if (errorElement) {
          errorElement.innerHTML = ''
        }
      }
    })

    doLoadMoreObservationX()
  },
)

onFind('.SearchResult-items .SearchResult-boardContainer', (board): void => {
  const items = board.closest('.SearchResult-items')
  items?.classList.add('is-muuri')
})

let initialFromGrid: Muuri
let initialFromIndex: number
let previousBoardFieldValue: string

// Items within the Columns.
onFind('ol.BoardView-list', (items: HTMLOListElement): void => {
  const board = items.closest('.SearchResult-board')
  const dragEnabled = board?.hasAttribute('data-dragEnabled') ? true : false
  const colGrid: Muuri = new Muuri(items, {
    items: '.BoardView-card',
    showDuration: 300,
    showEasing: 'ease',
    hideDuration: 300,
    hideEasing: 'ease',
    layoutDuration: 300,
    layoutEasing: 'cubic-bezier(0.625, 0.225, 0.100, 0.890)',
    dragEnabled: dragEnabled,
    dragSort: (): Grid[] =>
      colGrids.filter((item): boolean | undefined =>
        board?.contains(item.getElement()),
      ),
    dragContainer: getDragContainer(),
    dragRelease: {
      duration: 300,
      easing: 'cubic-bezier(0.625, 0.225, 0.100, 0.890)',
      useDragContainer: true,
    },
    dragStartPredicate: (
      item: Item,
      event:
        | DraggerStartEvent
        | DraggerMoveEvent
        | DraggerEndEvent
        | DraggerCancelEvent,
    ): boolean => {
      // Clicking on <a>'s should follow links and not initiate dragging.
      // Archived objects and Quick View button should also not be draggable.

      const target = event.target
      const element = item.getElement()

      return !(
        target.tagName === 'A' ||
        target.className.includes('ContentSummary-info') ||
        (element && element.hasAttribute('data-archived'))
      )
    },
    dragPlaceholder: {
      enabled: true,
      createElement: (item: Item): HTMLElement => {
        const element = item.getElement()

        return element?.cloneNode(true) as HTMLElement
      },
    },
    dragAutoScroll: {
      targets: (
        item: Item,
      ): (HTMLElement | Window | DragAutoScrollTarget)[] => {
        return [
          window,
          {
            element: boardContainer,
            priority: 1,
            axis: Muuri.AutoScroller.AXIS_X,
          },
          {
            element: item.getGrid()?.getElement().parentNode as HTMLElement,
            priority: 1,
            axis: Muuri.AutoScroller.AXIS_Y,
          },
        ]
      },
      sortDuringScroll: false,
    },
  })

  let lastReceivedItem: Item | null

  // Resize the card according to any DOM additions/removals at runtime.
  const observer = new MutationObserver(
    (mutationList: MutationRecord[]): void => {
      for (const mutation of mutationList) {
        const target = mutation.target
        if (mutation.type === 'childList' && target instanceof HTMLElement) {
          /*
            I'm using a resizeObserver here because some components might not
            have an initial height at the time of calculating the refresh and layout.
            eg: "edit field update" viewer avatars are transitioned-in with CSS transforms.
        */
          const resizeObserver = new ResizeObserver(
            (
              entries: ResizeObserverEntry[],
              observer: ResizeObserver,
            ): void => {
              colGrid.refreshItems().layout()
              observer.unobserve(target)
            },
          )
          resizeObserver.observe(target)
        }
      }
    },
  )
  observer.observe(items, { childList: true, subtree: true })

  colGrid.on('dragReleaseEnd', (item: Item): void => {
    if (lastReceivedItem === item) {
      const element = item.getElement()
      if (element instanceof HTMLElement) {
        element.classList.add('is-saving')
      }

      save(item)

      // Decrement-Increment counts for display.
      let fromCount = null
      let toCount = null

      if (initialFromGrid) {
        const fromCountEl = initialFromGrid.getElement()
        const column = fromCountEl.closest('.SearchResult-board-column')
        fromCount = column?.querySelector('.BoardView-title')
      }

      if (colGrid) {
        const toCountEl = colGrid.getElement()
        const column = toCountEl.closest('.SearchResult-board-column')
        toCount = column?.querySelector('.BoardView-title')
      }

      if (fromCount instanceof HTMLElement) {
        const count = fromCount.dataset.count

        if (count) {
          fromCount.dataset.count = (parseInt(count) - 1).toString()
        }
      }

      if (toCount instanceof HTMLElement) {
        const count = toCount.dataset.count

        if (count) {
          toCount.dataset.count = (parseInt(count) + 1).toString()
        }
      }
    }

    lastReceivedItem = null
    initialFromIndex = 0
  })

  colGrid.on('dragStart', (item: Item): void => {
    const grid = item.getGrid()

    if (grid instanceof Grid) {
      initialFromGrid = grid
      initialFromIndex = colGrid.getItems().indexOf(item)

      const colGridElement = colGrid.getElement()

      if (colGridElement) {
        const column = colGridElement.closest('.SearchResult-board-column')
        const boardFieldValue = column?.getAttribute('data-boardFieldValue')

        if (boardFieldValue) {
          previousBoardFieldValue = boardFieldValue
        }
      }

      const itemElement = item.getElement()

      if (itemElement) {
        itemElement.style.width = item.getWidth() + 'px'
        itemElement.style.height = item.getHeight() + 'px'
      }
    }
  })

  colGrid.on('dragMove', (item: Item): void => {
    if (colGrid === initialFromGrid && initialFromIndex !== null) {
      colGrid.move(item, initialFromIndex, { layout: 'instant' })
    }
  })

  colGrid.on('layoutStart', (): void => {
    if (!boardGrid) return
    boardGrid.refreshItems().layout()
  })

  colGrid.on(
    'receive',
    (data: {
      item: Item
      fromGrid: Grid
      fromIndex: number
      toGrid: Grid
      toIndex: number
    }): void => {
      lastReceivedItem = data.item
    },
  )

  const save = async (item: Item): Promise<void> => {
    const itemEl = item.getElement()
    const objectId = itemEl?.getAttribute('data-objectId')
    const originalObject = document.querySelector<HTMLLIElement>(
      'li[data-objectId="' + objectId + '"]',
    )

    let boardField
    let boardFieldValue
    let searchData

    if (originalObject) {
      const originalObjectBoard = originalObject.closest('.SearchResult-board')

      if (originalObjectBoard) {
        boardField = originalObjectBoard.getAttribute('data-boardField')
        searchData = originalObjectBoard.getAttribute('data-search')
      }
    }

    if (itemEl) {
      const column = itemEl.closest('.SearchResult-board-column')
      boardFieldValue = column?.getAttribute('data-boardFieldValue')
    }

    const headers = new Headers()
    const cookie = Cookies.get('bsp.csrf')

    if (cookie) {
      headers.append('Brightspot-CSRF', cookie)
    }

    const body = new FormData()
    if (objectId) body.append('objectId', objectId)
    if (boardField) body.append('boardField', boardField)
    if (boardFieldValue) body.append('boardFieldValue', boardFieldValue)
    if (searchData) body.append('search', searchData)
    body.append('previousBoardFieldValue', previousBoardFieldValue)

    const response = await fetch(window.CONTEXT_PATH + 'board/fieldUpdate', {
      method: 'post',
      credentials: 'include',
      headers,
      body,
    })

    const responseText = await response.text()
    if (response.ok) {
      // Remove the dragged item and replace it with response.
      const index = colGrid.getItems().indexOf(item)
      colGrid.remove([item], { removeElements: true })
      const template = document.createElement('template')
      template.innerHTML = responseText
      const child = template.content.firstChild

      if (child instanceof HTMLElement) {
        colGrid.add(child, { index: index })
      }
    } else {
      if (responseText) {
        colGrid.send(item, initialFromGrid, initialFromIndex)
        const errors = document.querySelector('.SearchResult-board-errors')

        if (errors) {
          errors.innerHTML = responseText
        }
      }
    }
  }

  doLoadMoreObservationY(colGrid, items)
  colGrids.push(colGrid)
})

// Get the lazy loaded items from the ajaxed Frame
// and insert them into the correct columns.
window.addEventListener('brightspot-frame-load', (e: Event): void => {
  const frame = e.target
  if (frame instanceof HTMLElement) {
    if (frame.classList.contains('BoardResults')) {
      const items = [
        ...frame.querySelectorAll<HTMLLIElement>('li.BoardView-card'),
      ]
      const container = frame.closest('.BoardView-listContainer')
      const gridEl =
        container?.querySelector<HTMLOListElement>('ol.BoardView-list')

      colGrids.forEach((colGrid: Muuri): void => {
        if (colGrid instanceof Object && colGrid.getElement() === gridEl) {
          doLoadMoreObservationY(colGrid, gridEl)
          colGrid.add(items)
        }
      })
    } else if (frame.classList.contains('BoardResults-Horiz')) {
      doLoadMoreObservationX()
      boardGrid.add([
        ...frame.querySelectorAll<HTMLDivElement>(
          'div.SearchResult-board-column',
        ),
      ])
    }
  }
})
