import type { Spectrum } from '@zakodium/nmrium-core'; import dlv from 'dlv'; import type { ReactNode } from 'react'; import { createContext, useCallback, useContext, useMemo, useState, } from 'react'; import nucleusToString from '../utility/nucleusToString.js'; type SortDirection = 'asc' | 'desc' | null; type SpectraReference = { id: string } & Partial>; interface BaseSortOptions { sortDirection?: SortDirection; } interface SortByReferenceOptions extends BaseSortOptions { path: string; sortByReferences: SpectraReference[]; sortType: 'sortByReference'; } interface SortByReferenceIndexesOptions { sortByReferences: SpectraReference[]; sortType: 'sortByReferenceIndexes'; } interface SortByPathOptions extends BaseSortOptions { path: string; nucleus?: string; sortType: 'sortByPath'; } type SortOptions = | SortByPathOptions | SortByReferenceOptions | SortByReferenceIndexesOptions; type SortFuncOptions = SortOptions & { activeSort?: string }; interface SortSpectraContextState { sortOptions: SortOptions | null; sort: (options: SortFuncOptions) => void; reset: () => void; activeSort: string | null; } const SortSpectraContext = createContext(null); export function useSortSpectra(): SortSpectraContextState { const context = useContext(SortSpectraContext); if (!context) { throw new Error('useSpectra must be used within a SpectraProvider'); } return context; } interface SpectraProviderProps { children: ReactNode; } function sortSpectraByPath( originSpectra: Spectrum[], sortOptions: SortByPathOptions, ) { const outSpectra = [...originSpectra]; const { path, sortDirection, nucleus } = sortOptions; const sortedSpectra: SortItem[] = []; const originIndexes: number[] = []; for (let index = 0; index < originSpectra.length; index++) { const spectrum = originSpectra[index]; if ( !nucleus || (nucleus && nucleusToString(spectrum.info.nucleus) === nucleus) ) { sortedSpectra.push({ spectrum, sortValue: dlv(spectrum, path), }); originIndexes.push(index); } } sortArray(sortedSpectra, sortDirection); for (let index = 0; index < sortedSpectra.length; index++) { outSpectra[originIndexes[index]] = sortedSpectra[index].spectrum; } return outSpectra; } function sortSpectraByReferences( originSpectra: Spectrum[], sortOptions: SortByReferenceOptions, ) { const outSpectra = [...originSpectra]; const { path, sortDirection, sortByReferences } = sortOptions; const spectraReferenceMap = new Map(); const sortedSpectra: SortItem[] = []; const originIndexes: number[] = []; for (const reference of sortByReferences || []) { spectraReferenceMap.set(reference.id, reference); } for (let index = 0; index < originSpectra.length; index++) { const spectrum = originSpectra[index]; if (spectraReferenceMap.size > 0 && spectraReferenceMap.has(spectrum.id)) { sortedSpectra.push({ spectrum, sortValue: dlv(spectraReferenceMap.get(spectrum.id), path), }); originIndexes.push(index); } } sortArray(sortedSpectra, sortDirection); for (let index = 0; index < sortedSpectra.length; index++) { outSpectra[originIndexes[index]] = sortedSpectra[index].spectrum; } return outSpectra; } function sortSpectraByReferenceIndexes( originSpectra: Spectrum[], sortOptions: SortByReferenceIndexesOptions, ) { const { sortByReferences } = sortOptions; const outSpectra = [...originSpectra]; const spectraReferenceMap = new Map(); const originIndexes: number[] = []; const sortedSpectra = new Map(); for (const reference of sortByReferences || []) { spectraReferenceMap.set(reference.id, reference); } for (let index = 0; index < originSpectra.length; index++) { const spectrum = originSpectra[index]; if (spectraReferenceMap.size > 0 && spectraReferenceMap.has(spectrum.id)) { originIndexes.push(index); sortedSpectra.set(spectrum.id, spectrum); } } for (let index = 0; index < originIndexes.length; index++) { const spectrum = sortedSpectra.get(sortByReferences[index].id); if (spectrum) { outSpectra[originIndexes[index]] = spectrum; } } return outSpectra; } interface SortItem { spectrum: Spectrum; sortValue: string | number | boolean | null | undefined; } function sortArray(data: SortItem[], sortDirection?: SortDirection): void { const direction = sortDirection === 'asc' ? 1 : -1; if (!sortDirection) { return; } data.sort((a, b) => { const valueA = a.sortValue; const valueB = b.sortValue; if (valueA == null && valueB != null) return -1 * direction; if (valueB == null && valueA != null) return 1 * direction; if (valueA == null && valueB == null) return 0; if (typeof valueA === 'string' && typeof valueB === 'string') { return direction * valueA.localeCompare(valueB); } if (typeof valueA === 'number' && typeof valueB === 'number') { return direction * (valueA - valueB); } if (typeof valueA === 'boolean' && typeof valueB === 'boolean') { return direction * (Number(valueA) - Number(valueB)); } return 0; }); } export function sortSpectra( originSpectra: Spectrum[], sortOptions: SortOptions, ) { const { sortType } = sortOptions; switch (sortType) { case 'sortByPath': return sortSpectraByPath(originSpectra, sortOptions); case 'sortByReference': return sortSpectraByReferences(originSpectra, sortOptions); case 'sortByReferenceIndexes': return sortSpectraByReferenceIndexes(originSpectra, sortOptions); default: return originSpectra; } } function getNextSortType(currentSort: SortDirection): SortDirection { if (currentSort === null) return 'asc'; if (currentSort === 'asc') return 'desc'; return null; } export function SortSpectraProvider(props: SpectraProviderProps) { const { children } = props; const [sortOptions, setSortOptions] = useState(null); const [activeSort, setActiveSort] = useState(null); const sort = useCallback( (options: SortFuncOptions) => { setActiveSort(options?.activeSort ?? null); setSortOptions((prevOptions) => { if (options.sortType !== 'sortByReferenceIndexes') { let previousOptions = { ...prevOptions }; if (activeSort !== options.activeSort) { previousOptions = { ...prevOptions, sortDirection: null }; } const newOptions: SortOptions = { ...previousOptions, ...options, sortDirection: options.sortDirection ?? getNextSortType( (previousOptions as BaseSortOptions)?.sortDirection ?? null, ), }; return newOptions; } return { ...prevOptions, ...options, }; }); }, [activeSort], ); const state = useMemo(() => { function reset() { setSortOptions(null); } return { activeSort, sortOptions, sort, reset, }; }, [activeSort, sort, sortOptions]); return ( {children} ); }