/**
 * Hook to manage a value in localStorage with optional serialization and
 * deserialization. Ported from the source design system's kernel.
 *
 * @param {string} key            The key to store the value under in localStorage.
 * @param {*}      [initialValue] The initial value or a function to compute it.
 * @param {Object} [options]      Optional configuration for serialization,
 *                                deserialization, and initialization.
 * @return {Array} The stored value, a setter function, and a remover function.
 */
import { useCallback, useState } from '@wordpress/element';
import { useEventListener } from './useEventListener';

export function useLocalStorage(key, initialValue = undefined, options = {}) {
    const {
        initializeWithValue = true,
        serializer = JSON.stringify,
        deserializer = JSON.parse,
        onError = (error, errorKey) =>
            console.warn(
                `Error with localStorage key "${errorKey}": ${error.message}`,
            ),
    } = options;

    const isLocalStorageAvailable = useCallback(() => {
        return typeof window !== 'undefined' && !!window.localStorage;
    }, []);

    const getInitialValue = useCallback(() => {
        if (typeof initialValue === 'function') {
            return initialValue();
        }
        return initialValue;
    }, [initialValue]);

    const readValueFromStorage = useCallback(() => {
        if (!isLocalStorageAvailable()) {
            return getInitialValue();
        }

        try {
            const storedItem = window.localStorage.getItem(key);

            if (storedItem === null) {
                return getInitialValue();
            }

            return deserializer(storedItem);
        } catch (error) {
            const errorObj =
                error instanceof Error ? error : new Error('Unknown error');
            onError(errorObj, key);
            return getInitialValue();
        }
    }, [key, deserializer, onError, isLocalStorageAvailable, getInitialValue]);

    const [storedValue, setStoredValue] = useState(() => {
        if (initializeWithValue) {
            return readValueFromStorage();
        }
        return getInitialValue();
    });

    /** Function to update localStorage and state with error handling. */
    const setValue = useCallback(
        (value) => {
            if (!isLocalStorageAvailable()) {
                onError(new Error('localStorage not available'), key);
                return;
            }

            try {
                const newValue =
                    typeof value === 'function' ? value(storedValue) : value;

                window.localStorage.setItem(key, serializer(newValue));
                setStoredValue(newValue);
            } catch (error) {
                const errorObj =
                    error instanceof Error ? error : new Error('Unknown error');
                onError(errorObj, key);
            }
        },
        [key, storedValue, serializer, onError, isLocalStorageAvailable],
    );

    /** Function to remove value from localStorage and reset state to initial value. */
    const removeValue = useCallback(() => {
        if (isLocalStorageAvailable()) {
            window.localStorage.removeItem(key);
        }

        setStoredValue(getInitialValue());
    }, [key, isLocalStorageAvailable, getInitialValue]);

    /** Handle storage events from other tabs/windows when our key changes. */
    const handleStorageChange = useCallback(
        (event) => {
            if (event.key === key) {
                setStoredValue(readValueFromStorage());
            }
        },
        [key, readValueFromStorage],
    );

    /** Listen for storage changes from other tabs/windows using the custom hook. */
    useEventListener(
        'storage',
        handleStorageChange,
        typeof window !== 'undefined' ? window : null,
    );

    return [storedValue, setValue, removeValue];
}
