import React, {
  createContext,
  ReactElement,
  useContext,
  useEffect,
  useState,
} from "react";

type BuiltInThemes = "light" | "dark";

export interface Theme {
  name: BuiltInThemes | "custom";
  palette: Palette;
}

export type SetThemeInput = BuiltInThemes | Partial<Palette>;

export const PALETTE_NAMES = [
  "--primary-background",
  "--secondary-background",

  "--primary-accent",
  "--primary-accent-soft",
  "--primary-accent-light",
  "--primary-accent-lighter",
  "--primary-accent-lightest",

  "--secondary-accent",
  "--secondary-accent-light",

  "--primary-heading",

  "--primary-title",
  "--primary-subtitle",
  "--secondary-subtitle",

  "--primary-text",
  "--text",
  "--text-light",
  "--author-text",
  "--sidebar-text",
  "--mini-event-card-text",

  "--form-invalid",
  "--warning-background",
  "--warning-text",

  "--input-background",
  "--input-placeholder-text",
  "--input-text",

  "--icon",

  "--code-background",
  "--button-background",
  "--footer-background",
  "--card-background",
  "--dark-card-background",
  "--table-header",
  "--table-section",

  "--navbar-page-overlay",

  "--link",
  "--link-hover",

  "--blue-gradient",
  "--border",
  "--marker",
] as const;

export const emptyPalette = PALETTE_NAMES.reduce(
  (partial, varName) => ({
    ...partial,
    [varName]: "#c4e0f8",
  }),
  {} as Palette
);

const ThemeContext =
  createContext<
    | {
        theme: Theme;
        setTheme(input: SetThemeInput): void;
        save(): void;
        clearSaved(): void;
      }
    | undefined
  >(undefined);

export interface Props {
  children?: ReactElement;
}

export function ThemeProvider(props: Props) {
  const update = useForcedUpdate();
  const [themeName, setThemeName] =
    useState<Theme["name"] | undefined>(undefined);

  const setTheme = (input: SetThemeInput) => {
    if (typeof input === "string") {
      PALETTE_NAMES.forEach((name) =>
        document.body.style.setProperty(name, "var(--" + input + name + ")")
      );
      savePalette(getCurrentPalette(input));
      setThemeName(input);
    } else {
      const properties = Object.keys(input) as PaletteNames[];

      properties.forEach((property) =>
        document.body.style.setProperty(
          property,
          input[property]?.trim() ?? null
        )
      );

      setThemeName("custom");
    }

    update();
  };

  useEffect(() => {
    const customPalette = getSavedPalette();

    if (customPalette == null) {
      const prefersDark = window.matchMedia(
        "(prefers-color-scheme: dark)"
      ).matches;
      if (prefersDark) {
        setTheme("dark");
      } else {
        setTheme("light");
      }
    } else {
      setTheme(customPalette);
      setThemeName("custom");
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <ThemeContext.Provider
      value={
        themeName == null
          ? undefined
          : {
              theme: {
                name: themeName,
                get palette() {
                  return getCurrentPalette(themeName);
                },
              },
              setTheme,
              save: () => savePalette(getCurrentPalette(themeName)),
              clearSaved: clearSavedPalette,
            }
      }
    >
      {props.children}
    </ThemeContext.Provider>
  );
}

export function useThemeContext() {
  return useContext(ThemeContext);
}

export type PaletteNames = typeof PALETTE_NAMES[number];

export type Palette = {
  [key in PaletteNames]: string;
};

function themePropertyName(
  name: PaletteNames,
  themeName: BuiltInThemes
): string {
  return `--${themeName}${name}`;
}

function getCurrentPalette(themeName: string) {
  const styles = getComputedStyle(document.body);
  if (themeName === "light" || themeName === "dark") {
    return PALETTE_NAMES.reduce(
      (partial, name) => ({
        ...partial,
        [name]: styles
          .getPropertyValue(themePropertyName(name, themeName))
          .trim(),
      }),
      {} as Palette
    );
  }
  return PALETTE_NAMES.reduce(
    (partial, name) => ({
      ...partial,
      [name]: styles.getPropertyValue(name).trim(),
    }),
    {} as Palette
  );
}

function useForcedUpdate() {
  const [fakeState, setFakeState] = useState(true);

  return () => setFakeState(!fakeState);
}

const STORAGE_KEY = "csc-theme-palette";

function getSavedPalette() {
  const raw = localStorage.getItem(STORAGE_KEY);

  return raw == null ? undefined : (JSON.parse(raw) as Palette);
}

function savePalette(palette: Palette) {
  localStorage.setItem(STORAGE_KEY, JSON.stringify(palette));
}

function clearSavedPalette() {
  localStorage.removeItem(STORAGE_KEY);
}
