import {
  Dispatch,
  SetStateAction,
  useCallback,
  useEffect,
  useRef,
  useState,
} from "react";
import { useQueryParamState as newVersion } from "./UseQueryParamState.strict";

export interface UseQueryParamStateConfig<T> {
  /**
   * 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;

  /**
   * Fallback values if param isn't yet set.
   * Consider moving to part of onInit.
   */
  defaultValues?: T;
}

/**
 * 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.)
 * 
 * @deprecated use {@link newVersion useQueryParamState} (strict)
 * @returns
 */
export function useQueryParamState<T>(
  param: string,
  paramConverter?: (val: T) => string | undefined,
  onInitialize?: (
    paramVal: string,
  ) => Promise<T | undefined | false> | (T | undefined | false),
  config?: UseQueryParamStateConfig<T>,
): [
  T | undefined,
  Dispatch<SetStateAction<T | undefined>>,
  string | undefined,
] {
  //Callback to update/remove query param
  const updateHistoryWithParam = useCallback(
    (param: string, value: string | undefined) => {
      if (config?.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);
    },
    [config],
  );

  const getEncodedValue = useCallback(
    (value: T | undefined) => {
      if (!value) {
        return undefined;
      }

      if (paramConverter) {
        return paramConverter(value);
      } else {
        return JSON.stringify(value);
      }
    },
    [paramConverter],
  );

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

  const hasInitialized = useRef<boolean>(false);

  //Perform initial load if present
  useEffect(() => {
    if (hasInitialized.current === false) {
      const initSearchParams = new URLSearchParams(window.location.search);

      const initVal = initSearchParams.get(param);
      if (initVal != null) {
        if (onInitialize != null) {
          Promise.resolve(onInitialize?.(initVal)).then((val) => {
            if (val !== false) {
              //Loaded new value, set value
              setValue(val);
            } else {
              //Init load failed, remove param
              updateHistoryWithParam(param, undefined);
              setValue(undefined);
            }
            hasInitialized.current = true;
          });
        } else {
          //default load
          try {
            setValue(JSON.parse(initVal));
          } catch {
            //Init load failed, remove param
            updateHistoryWithParam(param, undefined);
            setValue(undefined);
          } finally {
            hasInitialized.current = true;
          }
        }
      } else {
        //No init value, ignore
        if(config?.defaultValues) {
          setValue(config?.defaultValues);
        }
        hasInitialized.current = true;
      }
    }
  }, []);

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

  return [
    value,
    setValue,
    //Output stored value in case it needs to be used
    getEncodedValue(value),
  ];
}
