import { useEffect, useMemo, useState } from 'react';
import { useFirstMountState } from '../useFirstMountState/index.js';
import { useIsomorphicLayoutEffect } from '../useIsomorphicLayoutEffect/index.js';
import { useSyncedRef } from '../useSyncedRef/index.js';
import { useUpdateEffect } from '../useUpdateEffect/index.js';
import { isBrowser } from '../util/const.js';
import { off, on } from '../util/misc.js';
import { resolveHookState } from '../util/resolveHookState.js';
const storageListeners = new Map();
const invokeStorageKeyListeners = (s, key, value, skipListener) => {
    storageListeners
        .get(s)
        ?.get(key)
        ?.forEach((listener) => {
        if (listener !== skipListener) {
            listener(value);
        }
    });
};
const storageEventHandler = (evt) => {
    if (evt.storageArea && evt.key && evt.newValue) {
        invokeStorageKeyListeners(evt.storageArea, evt.key, evt.newValue);
    }
};
const addStorageListener = (s, key, listener) => {
    if (isBrowser && storageListeners.size === 0) {
        on(window, 'storage', storageEventHandler, { passive: true });
    }
    let keys = storageListeners.get(s);
    if (!keys) {
        keys = new Map();
        storageListeners.set(s, keys);
    }
    let listeners = keys.get(key);
    if (!listeners) {
        listeners = new Set();
        keys.set(key, listeners);
    }
    listeners.add(listener);
};
const removeStorageListener = (s, key, listener) => {
    const keys = storageListeners.get(s);
    if (!keys) {
        return;
    }
    const listeners = keys.get(key);
    if (!listeners) {
        return;
    }
    listeners.delete(listener);
    if (listeners.size === 0) {
        keys.delete(key);
    }
    if (keys.size === 0) {
        storageListeners.delete(s);
    }
    if (isBrowser && storageListeners.size === 0) {
        off(window, 'storage', storageEventHandler);
    }
};
const DEFAULT_OPTIONS = {
    defaultValue: null,
    initializeWithValue: true,
};
export function useStorageValue(storage, key, options) {
    const optionsRef = useSyncedRef({ ...DEFAULT_OPTIONS, ...options });
    const parse = (str, fallback) => {
        const parseFunction = optionsRef.current.parse ?? defaultParse;
        return parseFunction(str, fallback);
    };
    const stringify = (data) => {
        const stringifyFunction = optionsRef.current.stringify ?? defaultStringify;
        return stringifyFunction(data);
    };
    const storageActions = useSyncedRef({
        fetchRaw: () => storage.getItem(key),
        fetch: () => parse(storageActions.current.fetchRaw(), optionsRef.current.defaultValue),
        remove() {
            storage.removeItem(key);
        },
        store(value) {
            const stringified = stringify(value);
            if (stringified !== null) {
                storage.setItem(key, stringified);
            }
            return stringified;
        },
    });
    const isFirstMount = useFirstMountState();
    const [state, setState] = useState(optionsRef.current?.initializeWithValue && isFirstMount
        ? storageActions.current.fetch()
        : undefined);
    const stateRef = useSyncedRef(state);
    const stateActions = useSyncedRef({
        fetch() {
            setState(storageActions.current.fetch());
        },
        setRawVal(value) {
            setState(parse(value, optionsRef.current.defaultValue));
        },
    });
    useUpdateEffect(() => {
        stateActions.current.fetch();
    }, [key]);
    useEffect(() => {
        if (!optionsRef.current.initializeWithValue) {
            stateActions.current.fetch();
        }
    }, []);
    useIsomorphicLayoutEffect(() => {
        const handler = stateActions.current.setRawVal;
        addStorageListener(storage, key, handler);
        return () => {
            removeStorageListener(storage, key, handler);
        };
    }, [storage, key]);
    const actions = useSyncedRef({
        set(value) {
            if (!isBrowser)
                return;
            const s = resolveHookState(value, stateRef.current);
            const storeValue = storageActions.current.store(s);
            if (storeValue !== null) {
                invokeStorageKeyListeners(storage, key, storeValue);
            }
        },
        delete() {
            if (!isBrowser)
                return;
            storageActions.current.remove();
            invokeStorageKeyListeners(storage, key, null);
        },
        fetch() {
            if (!isBrowser)
                return;
            invokeStorageKeyListeners(storage, key, storageActions.current.fetchRaw());
        },
    });
    const staticActions = useMemo(() => ({
        set: ((v) => {
            actions.current.set(v);
        }),
        remove() {
            actions.current.delete();
        },
        fetch() {
            actions.current.fetch();
        },
    }), []);
    return useMemo(() => ({
        value: state,
        ...staticActions,
    }), [state]);
}
const defaultStringify = (data) => {
    if (data === null) {
        if (process.env.NODE_ENV === 'development') {
            console.warn(`'null' is not a valid data for useStorageValue hook, this operation will take no effect`);
        }
        return null;
    }
    try {
        return JSON.stringify(data);
    }
    catch (error) {
        console.warn(error);
        return null;
    }
};
const defaultParse = (str, fallback) => {
    if (str === null)
        return fallback;
    try {
        return JSON.parse(str);
    }
    catch (error) {
        console.warn(error);
        return fallback;
    }
};
