import { useEffect, useState } from "react";

/**
 * The `useStorage` hook is a generic hook for reactively storing state in a
 * similar fashion to reacts usual {@link useState} hook. There is however some
 * caveats which are good to keep in mind:
 *   - Any value of type `T` needs to be JSON (de-)serializeable. That means
 *     storing certain non-serializeable values can result in unexpected
 *     behavior, for example storing `Date` objects will stringify them.
 *   - Setting the value to `undefined` is equivalent to deleting the storage
 *     entry, not storing an `undefined` value. The reasoning behind this is
 *     that because JSON doesn't support `undefined` it should instead indicate
 *     an `undefined` value.
 *   - The hook is designed to listen to the global `storage` event which
 *     should make state changes propagate within the same context. If the
 *     storage is for example modified from another tab that change should
 *     trigger a state update in the current tab too. The behaviour of this may
 *     be browser dependant.
 */
export function useStorage<T>(
  storage: Storage,
  key: string,
  initial: T,
): [T, React.Dispatch<React.SetStateAction<T>>] {
  const stored = storage.getItem(key);
  const value = stored === null ? initial : JSON.parse(stored);
  const [state, setState] = useState(value);

  useEffect(() => {
    if (state === undefined) {
      storage.removeItem(key);
    } else {
      storage.setItem(key, JSON.stringify(state));
    }
  }, [state]);

  useEffect(() => {
    const listener = (event: StorageEvent) => {
      if (event.storageArea !== storage) {
        return;
      }

      if (event.key === null) {
        setState(initial);
        return;
      }

      if (event.key === key && event.newValue !== event.oldValue && event.newValue !== state) {
        setState(event.newValue !== null ? JSON.parse(event.newValue) : null);
      }
    };

    addEventListener("storage", listener);

    return () => removeEventListener("storage", listener);
  }, [storage]);

  return [state, setState];
}

/**
 * A hook for storing react state in `localStorage`.
 *
 * @see useStorage
 */
export function useLocalStorage<T>(key: string, initial: T) {
  return useStorage(localStorage, key, initial);
}

/**
 * A hook for storing react state in `sessionStorage`.
 *
 * @see useStorage
 */
export function useSessionStorage<T>(key: string, initial?: T) {
  return useStorage(sessionStorage, key, initial);
}
