import { createContext, FC, useCallback, useContext, useMemo, useState } from 'react'
import { Group } from '../../src/types'
import { ReactNodeLike } from 'prop-types'
import pako from 'pako'
import { Dict } from '../../src/types'
import { fixedEncodeURIComponent } from '../../src/utils'
import {
  GroupRenameBody,
  GroupSetFactoryArrayBody,
  SetFactoryArrayBody
} from '../../src/types/ApiSchemasFrontend'

interface Props {
  children: ReactNodeLike
  id: string
  initial: {
    groups: Dict<Group>
    ignored: string[]
    base: string[]
  }
}

interface GroupContextType {
  doNotSuggest: Set<string>
  exportedFactories: Set<string>

  ignoredFactories: string[]
  setIgnoredFactories(factories: string[]): void

  baseFactories: string[]
  setBaseFactories(factories: string[]): void

  groups: Dict<Group>
  addGroup(name: string, exported?: string[], malls?: string[]): void
  removeGroup(name: string): void
  renameGroup(name: string, newName: string): void

  setFactories(name: string, factories: string[], type: 'exports' | 'malls'): void
  getInputType(uid: string): 'base' | 'produced' | 'unknown'

  store(): Uint8Array
  load(str: Uint8Array): void
}

const defaultValues: GroupContextType = {
  doNotSuggest: new Set(),
  exportedFactories: new Set(),

  ignoredFactories: [],
  setIgnoredFactories() {
    return
  },

  baseFactories: [],
  setBaseFactories() {
    return
  },

  groups: {},
  addGroup() {
    return
  },
  removeGroup() {
    return
  },
  renameGroup() {
    return
  },

  setFactories() {
    return
  },
  getInputType() {
    return 'unknown'
  },

  store() {
    return new Uint8Array(0)
  },
  load() {
    return
  }
}

const GroupContext = createContext<GroupContextType>(defaultValues)
export const useGroups = () => useContext(GroupContext)

interface StoredFile {
  groups: Dict<Group>
  basicValues: string[]
  excludedSuggestions: string[]
}

export const postFetchJson = async (url: string, body: unknown) => {
  const res = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(body)
  })
  return res.json()
}

export const GroupProvider: FC<Props> = ({ children, id, initial }) => {
  const [excludedSuggestions, _setExcludedSuggestions] = useState<string[]>(initial.ignored)
  const [basicValues, _setBasicValues] = useState<string[]>(initial.base)
  const [groups, setGroups] = useState<Dict<Group>>(initial.groups)

  const doNotSuggest = useMemo<Set<string>>(() => {
    return new Set([
      ...Object.values(groups).flatMap(group => [...group.exports, ...group.malls]),
      ...excludedSuggestions,
      ...basicValues
    ])
  }, [basicValues, groups, excludedSuggestions])

  const exportedFactories = useMemo<Set<string>>(() => {
    return new Set([...Object.values(groups).flatMap(group => [...group.exports])])
  }, [groups])

  const setExcludedSuggestions = useCallback<typeof _setExcludedSuggestions>(
    val => {
      _setExcludedSuggestions(val)
      postFetchJson(`/api/${fixedEncodeURIComponent(id)}/factories`, {
        type: 'ignored',
        factories: val
      } as SetFactoryArrayBody).catch(console.error)
    },
    [id]
  )

  const setBasicValues = useCallback<typeof _setBasicValues>(
    val => {
      _setBasicValues(val)
      postFetchJson(`/api/${fixedEncodeURIComponent(id)}/factories`, {
        type: 'base',
        factories: val
      } as SetFactoryArrayBody).catch(console.error)
    },
    [id]
  )

  const addGroup = useCallback(
    (name: string, exports: string[] = [], malls: string[] = []) => {
      name = name.replace(/[.$]/g, '')
      if (name in groups) return false
      setGroups(g => {
        g[name] = { name, exports, malls }
        return { ...g }
      })
      ;(async () => {
        await postFetchJson(
          `/api/${fixedEncodeURIComponent(id)}/group/${fixedEncodeURIComponent(name)}/add`,
          {}
        )
        if (exports.length) {
          await postFetchJson(
            `/api/${fixedEncodeURIComponent(id)}/group/${fixedEncodeURIComponent(name)}/factories`,
            {
              type: 'exports',
              factories: exports
            } as GroupSetFactoryArrayBody
          )
        }
        if (malls.length) {
          await postFetchJson(
            `/api/${fixedEncodeURIComponent(id)}/group/${fixedEncodeURIComponent(name)}/factories`,
            {
              type: 'malls',
              factories: exports
            } as GroupSetFactoryArrayBody
          )
        }
      })().catch(console.error)
      return true
    },
    [groups, id]
  )
  const removeGroup = useCallback(
    (name: string) => {
      name = name.replace(/[.$]/g, '')
      setGroups(g => {
        delete g[name]
        return { ...g }
      })
      postFetchJson(
        `/api/${fixedEncodeURIComponent(id)}/group/${fixedEncodeURIComponent(name)}/remove`,
        {}
      ).catch(console.error)
    },
    [id]
  )
  const renameGroup = useCallback(
    (name: string, newName: string) => {
      name = name.replace(/[.$]/g, '')
      newName = newName.replace(/[.$]/g, '')
      if (newName in groups) return
      setGroups(g => {
        g[newName] = { ...g[name], name: newName }
        delete g[name]
        return { ...g }
      })
      postFetchJson(
        `/api/${fixedEncodeURIComponent(id)}/group/${fixedEncodeURIComponent(name)}/rename`,
        {
          newName
        } as GroupRenameBody
      ).catch(console.error)
    },
    [groups, id]
  )

  const setFactories = useCallback(
    (name: string, factories: string[], type: 'exports' | 'malls') => {
      name = name.replace(/[.$]/g, '')
      setGroups(g => {
        g[name] = { ...g[name], [type]: factories }
        return { ...g }
      })
      postFetchJson(
        `/api/${fixedEncodeURIComponent(id)}/group/${fixedEncodeURIComponent(name)}/factories`,
        {
          type,
          factories
        } as GroupSetFactoryArrayBody
      ).catch(console.error)
    },
    [id]
  )
  const getInputType = useCallback(
    (uid: string) => {
      if (basicValues.includes(uid)) return 'base'
      else if (exportedFactories.has(uid)) return 'produced'
      else return 'unknown'
    },
    [basicValues, exportedFactories]
  )

  const store = useCallback(() => {
    // const btoa = (bin: Uint8Array) => Buffer.from(bin).toString('base64')
    const value: StoredFile = {
      groups,
      basicValues,
      excludedSuggestions
    }
    const uncompressed = JSON.stringify(value)
    return pako.deflate(uncompressed)
  }, [basicValues, excludedSuggestions, groups])

  const load = useCallback(
    (compressed: Uint8Array) => {
      // const atob = (str: string) => Buffer.from(str, 'base64')
      const uncompressed = pako.inflate(compressed, { to: 'string' })
      const value: StoredFile = JSON.parse(uncompressed)
      if (!value.groups || !value.basicValues || !value.excludedSuggestions) return
      setGroups(value.groups)
      setBasicValues(value.basicValues)
      setExcludedSuggestions(value.excludedSuggestions)
    },
    [setBasicValues, setExcludedSuggestions]
  )

  const value: GroupContextType = useMemo(
    () => ({
      doNotSuggest,
      exportedFactories,

      ignoredFactories: excludedSuggestions,
      setIgnoredFactories: setExcludedSuggestions,

      baseFactories: basicValues,
      setBaseFactories: setBasicValues,

      groups,
      addGroup,
      removeGroup,
      renameGroup,

      setFactories,
      getInputType,

      store,
      load
    }),
    [
      addGroup,
      basicValues,
      doNotSuggest,
      excludedSuggestions,
      exportedFactories,
      getInputType,
      groups,
      load,
      removeGroup,
      renameGroup,
      setBasicValues,
      setExcludedSuggestions,
      setFactories,
      store
    ]
  )
  return <GroupContext.Provider value={value}>{children}</GroupContext.Provider>
}
