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, UploadDataBody } from '../../src/types/ApiSchemasFrontend' import { useRouter } from 'next/router' interface Props { children: ReactNodeLike id: string initial: { groups: Dict ignored: string[] base: string[] } } interface GroupContextType { doNotSuggest: Set exportedFactories: Set ignoredFactories: string[] setIgnoredFactories(factories: string[]): void baseFactories: string[] setBaseFactories(factories: string[]): void groups: Dict addGroup(name: string, exported?: string[], malls?: string[]): boolean 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 false }, removeGroup() { return }, renameGroup() { return }, setFactories() { return }, getInputType() { return 'unknown' }, store() { return new Uint8Array(0) }, load() { return } } const GroupContext = createContext(defaultValues) export const useGroups = () => useContext(GroupContext) interface StoredFile { groups: Dict basicValues: string[] excludedSuggestions: string[] } export const postFetchJson = async (base: string | undefined, url: string, body: unknown) => { const res = await fetch((base ?? '') + url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }) return res.json() } export const GroupProvider: FC = ({ children, id, initial }) => { const { basePath, reload, query } = useRouter() const [excludedSuggestions, _setExcludedSuggestions] = useState(initial.ignored) const [basicValues, _setBasicValues] = useState(initial.base) const [groups, setGroups] = useState>(initial.groups) const doNotSuggest = useMemo>(() => { return new Set([ ...Object.values(groups).flatMap(group => [...group.exports, ...group.malls]), ...excludedSuggestions, ...basicValues ]) }, [basicValues, groups, excludedSuggestions]) const exportedFactories = useMemo>(() => { return new Set([...Object.values(groups).flatMap(group => [...group.exports])]) }, [groups]) const setExcludedSuggestions = useCallback( val => { _setExcludedSuggestions(val) postFetchJson(basePath, `/api/${fixedEncodeURIComponent(id)}/factories`, { type: 'ignored', factories: val } as SetFactoryArrayBody).catch(console.error) }, [id, basePath] ) const setBasicValues = useCallback( val => { _setBasicValues(val) postFetchJson(basePath, `/api/${fixedEncodeURIComponent(id)}/factories`, { type: 'base', factories: val } as SetFactoryArrayBody).catch(console.error) }, [id, basePath] ) 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( basePath, `/api/${fixedEncodeURIComponent(id)}/group/${fixedEncodeURIComponent(name)}/add`, {} ) if (exports.length) { await postFetchJson( basePath, `/api/${fixedEncodeURIComponent(id)}/group/${fixedEncodeURIComponent(name)}/factories`, { type: 'exports', factories: exports } as GroupSetFactoryArrayBody ) } if (malls.length) { await postFetchJson( basePath, `/api/${fixedEncodeURIComponent(id)}/group/${fixedEncodeURIComponent(name)}/factories`, { type: 'malls', factories: malls } as GroupSetFactoryArrayBody ) } })().catch(console.error) return true }, [groups, id, basePath] ) const removeGroup = useCallback( (name: string) => { name = name.replace(/[.$]/g, '') setGroups(g => { delete g[name] return { ...g } }) postFetchJson( basePath, `/api/${fixedEncodeURIComponent(id)}/group/${fixedEncodeURIComponent(name)}/remove`, {} ).catch(console.error) }, [id, basePath] ) 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( basePath, `/api/${fixedEncodeURIComponent(id)}/group/${fixedEncodeURIComponent(name)}/rename`, { newName } as GroupRenameBody ).catch(console.error) }, [groups, id, basePath] ) 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( basePath, `/api/${fixedEncodeURIComponent(id)}/group/${fixedEncodeURIComponent(name)}/factories`, { type, factories } as GroupSetFactoryArrayBody ).catch(console.error) }, [id, basePath] ) 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( async (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 await postFetchJson(basePath, `/api/${query.id}/upload`, { groups: value.groups, ignored: value.excludedSuggestions, base: value.basicValues } as UploadDataBody) reload() }, [basePath, query.id, reload] ) 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 {children} }