import { IApp, IObject } from 'interfaces'
import { throttle } from 'lodash'
import PropTypes from 'prop-types'
import React from 'react'
import {
  LayoutChangeEvent,
  StyleProp,
  StyleSheet,
  View,
  ViewStyle,
} from 'react-native'
import { BaseObjectProps } from '../BaseObject'
import { TableContextProvider } from './context'
import EmptyState from './EmptyState'
import FixedColumn from './FixedColumn'
import LoadingIndicator from './LoadingIndicator'
import PaginatedFetchComponent, {
  connectPaginatedFetch,
  PaginatedFetchProps,
} from './PaginatedFetchComponent'
import PaginationControls from './PaginationControls'
import TableHeader from './TableHeader'
import TableRow, { Row } from './TableRow'
import TableScroll from './TableScroll'
import type { BindingDataItem, CellData } from './TableTypes'
import { makeTheme, type TableTheme } from './theme'
import { getBindingData, getShouldFreezeFirstColumn } from './utils'

interface TableObjectProps extends BaseObjectProps {
  bindingData: BindingDataItem[]
  object: IObject
  getApp: () => IApp
}

type TableData = {
  id: number
  cells: CellData[]
}[]

const DEFAULT_PAGE_LIMIT = 10
const MAX_REQUESTS_PER_SECOND = 7
const NEXT_PAGE_THROTTLE = Math.floor(1000 / MAX_REQUESTS_PER_SECOND)

type Props = TableObjectProps & PaginatedFetchProps
class Table extends PaginatedFetchComponent<Props> {
  constructor(props: Props) {
    super(props)

    this.goToNextPage = throttle(
      this.goToNextPage.bind(this),
      NEXT_PAGE_THROTTLE
    )

    this.goToPreviousPage = throttle(
      this.goToPreviousPage.bind(this),
      NEXT_PAGE_THROTTLE
    )
  }

  static childContextTypes = {
    getBindings: PropTypes.func,
    getStore: PropTypes.func,
    getParams: PropTypes.func,
  }

  getPageSize() {
    const { object } = this.props
    const { attributes } = object

    const pageSize = attributes?.pageSize ?? DEFAULT_PAGE_LIMIT

    return pageSize
  }

  getCurrentPage() {
    return this.state.page
  }

  getTableColumns() {
    const { bindingData } = this.props

    if (bindingData) {
      const { object } = this.props
      const { columns } = getBindingData(object, bindingData)

      return columns
    }

    return []
  }

  getTableDataRowsForPage(page: number, pageSize: number): TableData {
    const { bindingData } = this.props

    if (bindingData) {
      const { object } = this.props
      const { data } = getBindingData(object, bindingData)

      let pageData = data

      if (this.isManuallyPaginated()) {
        pageData = data.slice(page * pageSize, (page + 1) * pageSize)
      }

      return pageData
    }

    return []
  }

  goToFirstPage(): void {
    this.setState({ page: 0, nextPageLoading: true })
    const pageSize = this.getPageSize()

    this.fetchPageData(0, pageSize).then(() => {
      // When fetching data from the first page, the data in the store gets reset,
      // so we need to prefetch the next page of data to make sure we know if the next page is available.
      this.fetchPageData(1, pageSize).finally(() =>
        this.setState({ nextPageLoading: false })
      )
    })
  }

  getTableDataRows() {
    const { page } = this.state
    const pageSize = this.getPageSize()
    return this.getTableDataRowsForPage(page, pageSize)
  }

  goToPreviousPage() {
    const { page } = this.state

    if (page !== 0) {
      const previousPage = page - 1
      const pageSize = this.getPageSize()
      this.setState({ page: previousPage })
      // Re-fetch the current page, and fetch the next page again to keep "next" enabled
      this.fetchPageData(previousPage, pageSize).then(() => {
        // Fetching the next page should happen after fetching the current page is done
        this.fetchPageData(page, pageSize)
      })
    }
  }

  goToNextPage() {
    const { page } = this.state
    const { object } = this.props
    const { dataBinding } = object

    if (!dataBinding) {
      throw new Error(
        `Tried to fetch data from Table without dataBinding. Object: ${object.id}`
      )
    }

    const pageSize = this.getPageSize()
    const nextPage = page + 1
    const nextPageDataLoaded = this.getTableDataRowsForPage(nextPage, pageSize)

    this.setState({ nextPageLoading: true })

    if (nextPageDataLoaded.length === 0) {
      // Only re-fetch the current page if we don't have the data loaded in the state
      this.fetchPageData(nextPage, pageSize)
    }

    this.fetchPageData(nextPage + 1, pageSize)
    this.setState({ page: nextPage })

    // Clear the loading state after a short delay. This is just an artificial delay to make the loading state visible for a short time.
    const clearNextPageLoading = () => this.setState({ nextPageLoading: false })
    setTimeout(clearNextPageLoading, NEXT_PAGE_THROTTLE / 2)
  }

  renderPaginationControls(theme: TableTheme) {
    // Only render paginated controls for manually paginated tables (external collections aren't "manually" paginated)
    if (!this.isManuallyPaginated()) {
      return null
    }

    const { page, nextPageLoading } = this.state
    const pageSize = this.getPageSize()
    const nextPageData = this.getTableDataRowsForPage(page + 1, pageSize)
    const nextEnabled = !nextPageLoading && nextPageData.length > 0

    return (
      <PaginationControls
        nextDisabled={!nextEnabled}
        currentPage={page}
        goToNextPage={this.goToNextPage}
        goToPreviousPage={this.goToPreviousPage}
        theme={theme}
      />
    )
  }

  renderEmptyState(data: TableData, theme: TableTheme) {
    // if no attempt has been made to populate bindingData
    // then it's undefined, meaning we can differentiate
    // between an empty table and a table that hasn't loaded yet

    const hasNoBindingData = typeof this.props.bindingData === 'undefined'

    if (hasNoBindingData === true) {
      return <LoadingIndicator object={this.props.object} theme={theme} />
    } else if (hasNoBindingData === false && data.length === 0) {
      const { object } = this.props

      return <EmptyState object={object} />
    }

    return null
  }

  getDatasource = () => {
    const { object } = this.props
    const { dataBinding } = object

    return dataBinding?.source
  }

  onUpdateTableWidth = (ev: LayoutChangeEvent) => {
    const { width } = ev.nativeEvent.layout

    this.setState({ tableWidth: width })
  }

  render() {
    const data = this.getTableDataRows()
    const columns = this.getTableColumns()
    const datasource = this.getDatasource()

    if (!datasource) {
      return null
    }

    const { object, isResponsiveComponent, getApp } = this.props
    const theme = makeTheme(getApp(), object)

    const emptyState = this.renderEmptyState(data, theme)
    const tableHasData = emptyState === null

    const fixedFirstColumn = getShouldFreezeFirstColumn(object)

    const borderlessTableWidth =
      this.state.tableWidth - theme.tableContainer.borderWidth * 2

    const wrapperStyles: StyleProp<ViewStyle> = [
      styles.tableContainer,
      theme.tableContainer,
      isResponsiveComponent === false && object.layout,
    ]

    const innerStyles: StyleProp<ViewStyle> = [
      styles.tableContainerInner,
      theme.tableContainerInner,
    ]

    return (
      <TableContextProvider>
        <View style={wrapperStyles} onLayout={this.onUpdateTableWidth}>
          <View style={innerStyles}>
            {fixedFirstColumn && tableHasData && (
              <FixedColumn
                firstColumn={columns[0]}
                data={data}
                tableObject={object}
                datasource={datasource}
                theme={theme}
              />
            )}
            <TableScroll
              columns={columns}
              tableObjectId={object.id}
              scrollEnabled={tableHasData}
              tableWidth={borderlessTableWidth}
            >
              <TableHeader
                object={object}
                columns={columns}
                fixedFirstColumn={fixedFirstColumn}
                theme={theme}
              />
              {tableHasData &&
                data.map((rowData: Row) => (
                  <TableRow
                    datasource={datasource}
                    data={rowData}
                    theme={theme}
                    tableObject={object}
                    hideFirstColumn={fixedFirstColumn}
                  />
                ))}
            </TableScroll>
            {emptyState}
            {this.renderPaginationControls(theme)}
          </View>
        </View>
      </TableContextProvider>
    )
  }
}

const styles = StyleSheet.create({
  tableContainer: {
    display: 'flex',
    flexDirection: 'column',
  },
  tableContainerInner: {
    width: '100%',
    overflow: 'hidden',
  },
})

// @ts-expect-error https://github.com/AdaloHQ/runner/pull/873/files#r1656132138
export default connectPaginatedFetch(Table)
