import {
  Dispatch,
  SetStateAction,
  useCallback,
  useEffect,
  useLayoutEffect,
  useMemo,
  useRef,
  useState,
} from "react";
import SuperJSON from "superjson";

export interface UseQueryParamStateConfig {
  /**
   * Optionally disable rendering of query param.
   * This is to allow this hook to act as either a query param useState or normal useState based on configuration.
   * @default false
   */
  disableQueryParam?: boolean;

  /**
   * Optionally show default state.
   */
  showDefaultValue?: boolean;
}

/**
 * Hook to link object to specific query param.
 * Meant to be used when displaying specific items in modal form.
 *
 * @param param Identifier of query parameter being used
 * @param paramConverter Callback to convert advanced object to string for query param storage.
 * @param onInitialize Method called on page load if parameter is already populated (bookmark etc.)
 * @returns
 */
export function useQueryParamState<T>(
  param: string,
  paramConverter: (val: T) => string | undefined,
  onInitialize: (paramVal: string | null) => Promise<T> | T,
  config?: UseQueryParamStateConfig,
): [T, Dispatch<SetStateAction<T>>, string | undefined] {
  const valToQueryRef = useRef(paramConverter);
  const queryToValRef = useRef(onInitialize);
  const configRef = useRef(config);

  useLayoutEffect(() => {
    valToQueryRef.current = paramConverter;
    queryToValRef.current = onInitialize;
    configRef.current = config;
  });

  const valToQuery = useCallback((val: T) => valToQueryRef.current(val), []);
  const queryToVal = useCallback(
    (query: string | null) => queryToValRef.current(query),
    [],
  );

  //Internal state management
  const [value, setValue] = useState<T | undefined>();

  const hasInitialized = useRef<boolean>(false);
  // @ts-ignore till everything is on React 19
  const defaultVal = useRef<T | Promise<T>>();

  //Callback to update/remove query param
  const updateHistoryWithParam = useCallback(
    (param: string, value: string | undefined) => {
      if (configRef.current?.disableQueryParam) {
        return;
      }

      const currentSearchParams = new URLSearchParams(window.location.search);
      //Update query parameter with current state value
      if (value !== null && value !== "" && value !== undefined) {
        currentSearchParams.set(param, value);
      } else {
        currentSearchParams.delete(param);
      }

      //Get new url
      const newUrl = [window.location.pathname, currentSearchParams.toString()]
        .filter(Boolean)
        .join("?");

      //Update browser history
      window.history.replaceState(window.history.state, "", newUrl);
    },
    [],
  );

  const getEncodedValue = useCallback(
    (value: T | undefined) => {
      if (value === "" || value === null || value === undefined) {
        return undefined;
      }
      //Synchronous loads should hide if default value.
      if (
        !configRef.current?.showDefaultValue &&
        defaultVal.current === value
      ) {
        return undefined;
      }
      return valToQuery(value);
    },
    [valToQuery],
  );

  const encodedValue = useMemo(
    () => getEncodedValue(value),
    [value, getEncodedValue],
  );

  //Perform initial load if present
  useEffect(() => {
    if (hasInitialized.current) {
      return;
    }
    hasInitialized.current = true;
    const initVal = new URLSearchParams(window.location.search).get(param);
    Promise.resolve(queryToVal(initVal)).then((val) => {
      //Loaded new value, set value
      setValue(val);
    });
  }, [param, queryToVal]);

  useEffect(() => {
    if(configRef.current?.showDefaultValue) {
      defaultVal.current = queryToVal(null);
    }
  }, [queryToVal]);

  //On state change, update parameter
  useEffect(() => {
    //Only update param once async init has finished (if running)
    if (hasInitialized.current) {
      updateHistoryWithParam(param, encodedValue);
    }
  }, [param, value, encodedValue, updateHistoryWithParam]);

  return [
    value!,
    setValue as Dispatch<SetStateAction<T>>,
    //Output stored value in case it needs to be used
    encodedValue,
  ];
}

export function Base64QueryParamEncoder<T>(value: T): string | undefined {
  try {
    //Uses superjson to handle advanced data types (Map, Date etc.)

    //Convert to utf8 to handle emoji etc.
    const utf8String = String.fromCharCode(
      ...new TextEncoder().encode(SuperJSON.stringify(value)),
    );

    return value ? btoa(utf8String) : undefined;
  } catch {
    return undefined;
  }
}

export function Base64QueryParamDecoder<T>(
  param: string | null,
): T | undefined {
  try {
    if (param == null) {
      return undefined;
    }

    //Uses superjson to handle advanced data types (Map, Date etc.)

    const decodedText = new TextDecoder().decode(
      Uint8Array.from(atob(param), (c) => c.charCodeAt(0)),
    );

    return SuperJSON.parse(decodedText);
  } catch {
    return undefined;
  }
}
