import {
  createContext,
  useContext,
  useState,
  useEffect,
  useMemo,
  useCallback,
  ReactNode,
  Dispatch,
  SetStateAction,
} from 'react'
import { Token } from '@uniswap/sdk'

import { DEFAULT_APPROVE_MAX, DEFAULT_DEADLINE, DEFAULT_SLIPPAGE } from './constants'

enum LocalStorageKeys {
  Version = 'version',
  ApproveMax = 'approveMax',
  Deadline = 'deadline',
  Slippage = 'slippage',
  Transactions = 'transactions',
  Tokens = 'tokens',
}

const NO_VERSION = -1
const CURRENT_VERSION = 0

function useLocalStorage<T, S = T>(
  key: LocalStorageKeys,
  defaultValue: T,
  overrideLookup = false,
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  { serialize, deserialize }: { serialize: (toSerialize: T) => S; deserialize: (toDeserialize: S) => T } = {
    serialize: (toSerialize): S => (toSerialize as unknown) as S,
    deserialize: (toDeserialize): T => (toDeserialize as unknown) as T,
  }
): [T, Dispatch<SetStateAction<T>>] {
  const [value, setValue] = useState<T>(() => {
    if (overrideLookup) {
      return defaultValue
    } else {
      try {
        const item = window.localStorage.getItem(key)
        return item === null ? defaultValue : deserialize(JSON.parse(item)) ?? defaultValue
      } catch {
        return defaultValue
      }
    }
  })

  useEffect(() => {
    try {
      window.localStorage.setItem(key, JSON.stringify(serialize(value)))
    } catch {}
  }, [key, serialize, value])

  return [value, setValue]
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function serializeTokens(
  tokens: Token[]
): { chainId: number; address: string; decimals: number; symbol?: string; name?: string }[] {
  return tokens.map((token) => ({
    chainId: token.chainId,
    address: token.address,
    decimals: token.decimals,
    symbol: token.symbol,
    name: token.name,
  }))
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function deserializeTokens(serializedTokens: ReturnType<typeof serializeTokens>): Token[] {
  return serializedTokens.map(
    (serializedToken) =>
      new Token(
        serializedToken.chainId,
        serializedToken.address,
        serializedToken.decimals,
        serializedToken.symbol,
        serializedToken.name
      )
  )
}

interface Transaction {
  chainId: number
  hash: string
}

const HypertextContext = createContext<
  [
    {
      firstToken: Token | undefined
      secondToken: Token | undefined
      showUSD: boolean
      approveMax: boolean
      deadline: number
      slippage: number
      transactions: Transaction[]
      tokens: Token[]
    },
    {
      setFirstToken: Dispatch<SetStateAction<Token | undefined>>
      setSecondToken: Dispatch<SetStateAction<Token | undefined>>
      setShowUSD: Dispatch<SetStateAction<boolean>>
      setApproveMax: Dispatch<SetStateAction<boolean>>
      setDeadline: Dispatch<SetStateAction<number>>
      setSlippage: Dispatch<SetStateAction<number>>
      setTransactions: Dispatch<SetStateAction<Transaction[]>>
      setTokens: Dispatch<SetStateAction<Token[]>>
    }
  ]
>([{}, {}] as any) // eslint-disable-line @typescript-eslint/no-explicit-any

// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
function useHypertextContext() {
  return useContext(HypertextContext)
}

export default function Provider({ children }: { children: ReactNode }): JSX.Element {
  // global state
  const [firstToken, setFirstToken] = useState<Token | undefined>()
  const [secondToken, setSecondToken] = useState<Token | undefined>()
  const [showUSD, setShowUSD] = useState<boolean>(false)

  // versioning
  const [version, setVersion] = useLocalStorage<number>(LocalStorageKeys.Version, NO_VERSION)
  // after it's been used to sychronously + selectively override localstorage keys, bump the version as soon as we can
  useEffect(() => {
    setVersion(CURRENT_VERSION)
  }, [setVersion])

  // global localstorage state
  const [approveMax, setApproveMax] = useLocalStorage<boolean>(LocalStorageKeys.ApproveMax, DEFAULT_APPROVE_MAX)
  const [deadline, setDeadline] = useLocalStorage<number>(LocalStorageKeys.Deadline, DEFAULT_DEADLINE)
  const [slippage, setSlippage] = useLocalStorage<number>(LocalStorageKeys.Slippage, DEFAULT_SLIPPAGE)
  const [transactions, setTransactions] = useLocalStorage<Transaction[]>(
    LocalStorageKeys.Transactions,
    [],
    version < 0 ? true : false // pre-version0 localstorage transactions didn't include chainId and must be overriden
  )
  const [tokens, setTokens] = useLocalStorage<Token[], ReturnType<typeof serializeTokens>>(
    LocalStorageKeys.Tokens,
    [],
    false,
    {
      serialize: serializeTokens,
      deserialize: deserializeTokens,
    }
  )

  return (
    <HypertextContext.Provider
      value={useMemo(
        () => [
          { firstToken, secondToken, showUSD, approveMax, deadline, slippage, transactions, tokens },
          {
            setFirstToken,
            setSecondToken,
            setShowUSD,
            setApproveMax,
            setDeadline,
            setSlippage,
            setTransactions,
            setTokens,
          },
        ],
        [
          firstToken,
          secondToken,
          showUSD,
          approveMax,
          deadline,
          slippage,
          transactions,
          tokens,
          setFirstToken,
          setSecondToken,
          setShowUSD,
          setApproveMax,
          setDeadline,
          setSlippage,
          setTransactions,
          setTokens,
        ]
      )}
    >
      {children}
    </HypertextContext.Provider>
  )
}

export function useFirstToken(): [Token | undefined, ReturnType<typeof useHypertextContext>[1]['setFirstToken']] {
  const [{ firstToken }, { setFirstToken }] = useHypertextContext()
  return [firstToken, setFirstToken]
}

export function useSecondToken(): [Token | undefined, ReturnType<typeof useHypertextContext>[1]['setSecondToken']] {
  const [{ secondToken }, { setSecondToken }] = useHypertextContext()
  return [secondToken, setSecondToken]
}

export function useShowUSD(): [boolean, ReturnType<typeof useHypertextContext>[1]['setShowUSD']] {
  const [{ showUSD }, { setShowUSD }] = useHypertextContext()
  return [showUSD, setShowUSD]
}

export function useApproveMax(): [boolean, () => void] {
  const [{ approveMax }, { setApproveMax }] = useHypertextContext()
  const toggleApproveMax = useCallback(() => {
    setApproveMax(!approveMax)
  }, [approveMax, setApproveMax])
  return [approveMax, toggleApproveMax]
}

export function useDeadline(): [number, ReturnType<typeof useHypertextContext>[1]['setDeadline']] {
  const [{ deadline }, { setDeadline }] = useHypertextContext()
  return [deadline, setDeadline]
}

export function useSlippage(): [number, ReturnType<typeof useHypertextContext>[1]['setSlippage']] {
  const [{ slippage }, { setSlippage }] = useHypertextContext()
  return [slippage, setSlippage]
}

export function useTransactions(): [
  Transaction[],
  {
    addTransaction: (chainId: number, hash: string) => void
    removeTransaction: (chainId: number, hash: string) => void
  }
] {
  const [{ transactions }, { setTransactions }] = useHypertextContext()

  const addTransaction = useCallback(
    (chainId: number, hash: string) => {
      setTransactions((transactions) =>
        transactions
          .filter((transaction) => !(transaction.chainId === chainId && transaction.hash === hash))
          .concat([{ chainId, hash }])
      )
    },
    [setTransactions]
  )
  const removeTransaction = useCallback(
    (chainId: number, hash: string) => {
      setTransactions((transactions) =>
        transactions.filter((transaction) => !(transaction.chainId === chainId && transaction.hash === hash))
      )
    },
    [setTransactions]
  )

  return [transactions, { addTransaction, removeTransaction }]
}

export function useLocalStorageTokens(): [
  Token[],
  {
    addToken: (token: Token) => void
    removeToken: (token: Token) => void
  }
] {
  const [{ tokens }, { setTokens }] = useHypertextContext()

  const addToken = useCallback(
    async (token: Token) => {
      setTokens((tokens) => tokens.filter((currentToken) => !currentToken.equals(token)).concat([token]))
    },
    [setTokens]
  )

  const removeToken = useCallback(
    (token: Token) => {
      setTokens((tokens) => tokens.filter((currentToken) => !currentToken.equals(token)))
    },
    [setTokens]
  )

  return [tokens, { addToken, removeToken }]
}