import '@glideapps/glide-data-grid/dist/index.css'

import { useTheme } from '@context-providers/theme-context-provider'
import {
  type CellClickedEventArgs,
  DataEditor,
  type DataEditorProps,
  type DataEditorRef,
  type GridCell,
  GridCellKind,
  type GridColumn,
  type GridMouseEventArgs,
  type Item,
  type LoadingCell,
  type Rectangle,
  Theme,
} from '@glideapps/glide-data-grid'
import { ParentSize } from '@visx/responsive'

import { type Dispatch, ReactNode, type SetStateAction, useCallback, useEffect, useRef, useState } from 'react'

import { Loader } from '@components/ui/loader/loader'

import { countryArrayDataTableRenderer } from './renderers/country-array-cell-renderer'
import { countryDataTableRenderer } from './renderers/country-cell-renderer'
import { favoriteDataTableRenderer } from './renderers/favorite-cell-renderer'
import { linksTableRenderer } from './renderers/links-cell-renderer'
import { getTableTheme, isHoverableRenderer } from './utils/data-table.utils'
import type { ContextMenuAction, ContextMenuConfig } from './context-menu-simple'

export type TableSort<FieldName extends string> = {
  field: FieldName
  order: 'asc' | 'desc' | null
}

export type DataTableProps<T extends object, ColumnId extends string> = {
  data: T[]
  columns: DataTableColumn<T, ColumnId>[]
  totalRows?: number
  onEndReached?: () => void
  sortHandler?: (columnId: ColumnId, sort: 'asc' | 'desc' | null) => void
  freezeColumns?: number
  currentSort?: TableSort<ColumnId>
  isLoading?: boolean
  isLoadingLabel?: string
  hasHeaderGroup?: boolean
  footer?: ReactNode
  /**
   * E2E test_id to identify the button.
   */
  __e2e_test_id__?: string

  /**
   * Callback function to be called when the user right-clicks on a cell.
   * This function should be used to show and position the context menu.
   */
  setContextMenu?: Dispatch<SetStateAction<ContextMenuConfig>>
} & Omit<DataEditorProps, 'columns' | 'rows' | 'getCellContent'>

/**
 * We take the `GridColumn` type from the `@glideapps/glide-data-grid` so the
 * user can use all of its props and extend it to include the `toCell`,
 * `onFilter`, and `onSort` functions.
 */
export type DataTableColumn<Row extends object, ColumnId extends string> = GridColumn & {
  id: ColumnId

  tooltip?: string

  /**
   * We use this to render the cell content to the column. This then uses the
   * `GridCell` type to render the content/data.
   */
  toCell: (item: Row) => GridCell

  /**
   * The function that performs an action when the user clicks on a cell in this column.
   */
  onClick?: (row: Row, event: CellClickedEventArgs) => void

  /**
   * The function that returns context actions for a given row. Will be called when a cell in this column is right-clicked.
   */
  onContextMenu?: OnContextMenuCallback<Row>

  sortComparator: (row1: Row, row2: Row, direction: 'asc' | 'desc' | null) => number
}

export type OnContextMenuCallback<Row> = (row: Row, event: CellClickedEventArgs) => ContextMenuAction[]

/**
 * We take the `GridColumn` type from the `@glideapps/glide-data-grid` so the
 * user can use all of its props and extend it to include the `toCell`,
 * `onFilter`, and `onSort` functions.
 */
export type DataGridColumn<Row extends object, ColumnId extends string> = GridColumn & {
  id: ColumnId
  /**
   * We use this to render the cell content to the column. This then uses the
   * `GridCell` type to render the content/data.
   */
  toCell: (item: Row) => GridCell

  onClick?: (rowData: Row, event: CellClickedEventArgs) => void
}

type ManagedDataTableColumn<Row extends object, ColumnId extends string> = DataTableColumn<Row, ColumnId> & {
  activeSort: 'asc' | 'desc' | null
}

export function DataTable<Row extends object, ColumnId extends string = string>({
  data,
  columns,
  onEndReached,
  sortHandler,
  freezeColumns,
  currentSort,
  __e2e_test_id__,
  isLoading,
  isLoadingLabel = 'Loading...',
  hasHeaderGroup,
  footer,
  setContextMenu,
  ...delegated
}: DataTableProps<Row, ColumnId>) {
  const sortOrder = currentSort?.order === 'asc' ? 'sortAscending' : 'sortDescending'
  const [managedColumns, setManagedColumns] = useState<ManagedDataTableColumn<Row, ColumnId>[]>([])
  const { theme, isThemeReady } = useTheme()
  const [tableTheme, setTableTheme] = useState<Partial<Theme> | undefined>()

  const getTheme = useCallback(() => getTableTheme(theme), [theme])

  useEffect(() => {
    // we need this timeout to make sure 'getTableTheme' returns the correct css variables
    if (isThemeReady) {
      setTimeout(() => {
        const newTheme = getTheme()
        setTableTheme(newTheme)
      }, 0)
    }
  }, [getTheme, isThemeReady, theme])

  useEffect(() => {
    setManagedColumns(
      columns.map((col) => ({
        ...col,
        activeSort: currentSort?.field === col.id ? currentSort.order : null,
        isDraggable: true,
        icon: currentSort?.field === col.id ? sortOrder : undefined,
      })),
    )
  }, [columns, currentSort, sortOrder])

  const [visibleRegion, setVisibleRegion] = useState<Rectangle>({
    x: 0,
    y: 0,
    width: 0,
    height: 0,
  })

  // const dataEditorTheme: Partial<Theme> = useMemo(() => getTableTheme(), [theme === 'dark'])

  useEffect(() => {
    if (data.length > 1 && visibleRegion.y + visibleRegion.height > data.length - 20) {
      onEndReached?.()
    }
  }, [visibleRegion, onEndReached, data.length])

  const getRowContent = useCallback(
    (row: number) => {
      return data[row]
    },
    [data],
  )

  const getCellContent = useCallback(
    ([col, row]: Item) => {
      const rowData = getRowContent(row)

      if (!rowData) {
        return { kind: GridCellKind.Loading, allowOverlay: true } satisfies LoadingCell
      }

      const toCell = managedColumns[col].toCell

      return toCell(rowData)
    },
    [getRowContent, managedColumns],
  )

  const dataEditorRef = useRef<DataEditorRef>(null)
  const tableWrapperRef = useRef<HTMLDivElement>(null)

  const onRightClick = useCallback(
    ([col, row]: Item, event: CellClickedEventArgs) => {
      if (!dataEditorRef?.current) {
        return
      }
      const cellBounds: Rectangle | undefined = dataEditorRef.current.getBounds(col, row)
      if (!cellBounds) {
        return
      }
      const position = {
        x: cellBounds.x + event.localEventX,
        y: cellBounds.y + event.localEventY,
      }

      // Uncomment to get coordinates during test creation
      // console.log('absolute', position)
      // console.log('relative', { x: event.localEventX, y: event.localEventY })

      if (!setContextMenu) {
        return
      }
      const rowData = getRowContent(row)
      if (!rowData) {
        return { kind: GridCellKind.Loading, allowOverlay: true } satisfies LoadingCell
      }

      const onContextMenuFn = managedColumns[col].onContextMenu
      if (!onContextMenuFn) {
        return
      }

      const actions = onContextMenuFn(rowData, event)
      if (!actions) {
        return
      }
      event.preventDefault()

      setContextMenu({
        position,
        actions,
      })
    },
    [dataEditorRef, setContextMenu, getRowContent, managedColumns],
  )

  const [hoveredCellData, setHoveredCellData] = useState<{
    x: number
    y: number
    data: any
    hoverOverlay?: (data: any) => React.ReactNode
  } | null>(null)

  const [hoveredHeader, setHoveredHeader] = useState<{
    columnId: number
    x: number
    y: number
  } | null>(null)
  const onItemHovered = useCallback(
    (args: GridMouseEventArgs) => {
      const tableBounds = tableWrapperRef.current?.getBoundingClientRect()

      if (args.kind === 'cell') {
        setHoveredHeader(null)
        const [col, row] = args.location
        const cell = getCellContent([col, row])
        if (!cell || cell.kind !== GridCellKind.Custom) {
          setHoveredCellData(null)
          return
        }
        const x = args.bounds.x - (tableBounds?.x ?? 0) + 10
        const y = args.bounds.y - (tableBounds?.y ?? 0) + args.bounds.height
        const hoverableRenderers = [countryArrayDataTableRenderer]
        const renderer = hoverableRenderers.find((r) => r.isMatch(cell))
        const data = cell.data
        const hoverOverlay = isHoverableRenderer(renderer) ? renderer.hoverOverlay : undefined

        setHoveredCellData({ x, y, data, hoverOverlay })
        return
      }

      if (args.kind === 'header') {
        setHoveredCellData(null)
        const [columnId] = args.location
        const x = args.bounds.x - (tableBounds?.x ?? 0) + 10
        const y = args.bounds.height

        setHoveredHeader({ columnId, x, y: y + (hasHeaderGroup ? 20 : 0) })
        return
      }

      setHoveredCellData(null)
      setHoveredHeader(null)
    },
    [getCellContent, hasHeaderGroup],
  )

  const onHeaderMenuClick = useCallback(
    (colIndex: number) => {
      if (!sortHandler) {
        // If sorting is not supported, do nothing on header click
        return
      }
      setManagedColumns((cols) =>
        cols.map((col, i) => {
          if (i !== colIndex) {
            return { ...col, activeSort: null, icon: undefined }
          }
          let newSort: 'asc' | 'desc' | null
          let icon: 'sortAscending' | 'sortDescending' | undefined = undefined

          switch (col.activeSort) {
            case null:
              newSort = 'asc'
              icon = 'sortAscending'
              break
            case 'asc':
              newSort = 'desc'
              icon = 'sortDescending'
              break
            case 'desc':
              newSort = null
              icon = undefined
              break
          }

          console.log(`Column ${col.id} sort state: ${col.activeSort} -> ${newSort}`)

          if (sortHandler) {
            sortHandler(col.id, newSort)
          }
          return { ...col, activeSort: newSort, icon }
        }),
      )
    },
    [sortHandler],
  )

  const onCellClicked = useCallback(
    ([col, row]: Item, event: CellClickedEventArgs) => {
      const rowData = getRowContent(row)
      const columnId = managedColumns[col].id
      const columnDefinition = managedColumns.find((col) => col.id === columnId)!
      if (columnDefinition.onClick) {
        columnDefinition.onClick(rowData, event)
      }
    },
    [getRowContent, managedColumns],
  )

  const onColumnResize = useCallback(
    ({ id }: { id?: string }, width: number) => {
      setManagedColumns((oldColumns) =>
        oldColumns.map((oldColumn) => (oldColumn.id === id ? { ...oldColumn, width } : oldColumn)),
      )
    },
    [setManagedColumns],
  )
  return (
    <ParentSize className="flex h-full min-h-[inherit] relative">
      {({ height }) => (
        <div
          className={`h-[${height}px] w-full flex flex-col relative`}
          ref={tableWrapperRef}
          data-test-id={__e2e_test_id__}
        >
          <DataEditor
            ref={dataEditorRef}
            freezeColumns={freezeColumns ?? 0}
            customRenderers={[
              linksTableRenderer,
              countryDataTableRenderer,
              countryArrayDataTableRenderer,
              favoriteDataTableRenderer,
            ]}
            columns={managedColumns}
            getCellContent={getCellContent}
            rows={data.length}
            onHeaderClicked={onHeaderMenuClick}
            onCellActivated={(cell) => console.log('cell activated', cell)}
            onItemHovered={onItemHovered}
            onHeaderMenuClick={onHeaderMenuClick}
            onColumnResize={onColumnResize}
            onCellClicked={onCellClicked}
            verticalBorder={false}
            headerIcons={{ sortAscending, sortDescending, noSort }}
            {...delegated}
            onCellContextMenu={onRightClick}
            theme={tableTheme}
            onVisibleRegionChanged={setVisibleRegion}
            rowHeight={22}
            headerHeight={22}
            smoothScrollX
            smoothScrollY
          />

          {hoveredHeader && managedColumns[hoveredHeader.columnId]?.tooltip && (
            <div
              style={
                {
                  '--header-x': `${hoveredHeader.x}px`,
                  '--header-y': `${hoveredHeader.y}px`,
                } as React.CSSProperties
              }
              className={
                'absolute left-(--header-x) top-(--header-y) bg-background p-2 m-0.5 border border-(--discreet-border) shadow-md max-w-[400px]'
              }
              dangerouslySetInnerHTML={{ __html: managedColumns[hoveredHeader.columnId].tooltip ?? '' }}
            />
          )}

          {hoveredCellData?.hoverOverlay && (
            <div
              style={
                {
                  '--header-x': `${hoveredCellData.x}px`,
                  '--header-y': `${hoveredCellData.y}px`,
                } as React.CSSProperties
              }
              className={
                'absolute left-(--header-x) top-(--header-y) bg-background p-2 m-0.5 border border-(--discreet-border) shadow-md max-w-[400px]'
              }
            >
              {hoveredCellData.hoverOverlay(hoveredCellData.data)}
            </div>
          )}

          {footer ? (
            <div className="flex absolute bottom-0 w-full items-center px-1 py-4 font-bold bg-background text-muted-foreground">
              {footer}
            </div>
          ) : null}

          {isLoading && <Loader label={isLoadingLabel} />}
        </div>
      )}
    </ParentSize>
  )
}

// CUSTOM ICONS ----------------------------------------------------------------

const noSort = () => `
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
  <path transform="scale(0.75)"  stroke-linecap="round" stroke-linejoin="round" d="M3 7.5 7.5 3m0 0L12 7.5M7.5 3v13.5m13.5 0L16.5 21m0 0L12 16.5m4.5 4.5V7.5" />
</svg>
`

const sortAscending = () => `
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M4 9H11L7.5 4.5L4 9Z" fill="currentColor"></path></svg>

`

const sortDescending = () => `
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M4 6H11L7.5 10.5L4 6Z" fill="currentColor"></path></svg>
`
