/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 */

import type {ReactContext} from 'shared/ReactTypes';

import * as React from 'react';
import {
  createContext,
  useContext,
  useEffect,
  useLayoutEffect,
  useMemo,
} from 'react';
import {
  LOCAL_STORAGE_BROWSER_THEME,
  LOCAL_STORAGE_PARSE_HOOK_NAMES_KEY,
  LOCAL_STORAGE_SHOULD_BREAK_ON_CONSOLE_ERRORS,
  LOCAL_STORAGE_SHOULD_APPEND_COMPONENT_STACK_KEY,
  LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY,
  LOCAL_STORAGE_SHOW_INLINE_WARNINGS_AND_ERRORS_KEY,
  LOCAL_STORAGE_HIDE_CONSOLE_LOGS_IN_STRICT_MODE,
} from 'react-devtools-shared/src/constants';
import {
  COMFORTABLE_LINE_HEIGHT,
  COMPACT_LINE_HEIGHT,
} from 'react-devtools-shared/src/devtools/constants';
import {useLocalStorage} from '../hooks';
import {BridgeContext} from '../context';
import {logEvent} from 'react-devtools-shared/src/Logger';

import type {BrowserTheme} from 'react-devtools-shared/src/frontend/types';

export type DisplayDensity = 'comfortable' | 'compact';
export type Theme = 'auto' | 'light' | 'dark';

type Context = {
  displayDensity: DisplayDensity,
  setDisplayDensity(value: DisplayDensity): void,

  // Derived from display density.
  // Specified as a separate prop so it can trigger a re-render of FixedSizeList.
  lineHeight: number,

  appendComponentStack: boolean,
  setAppendComponentStack: (value: boolean) => void,

  breakOnConsoleErrors: boolean,
  setBreakOnConsoleErrors: (value: boolean) => void,

  parseHookNames: boolean,
  setParseHookNames: (value: boolean) => void,

  hideConsoleLogsInStrictMode: boolean,
  setHideConsoleLogsInStrictMode: (value: boolean) => void,

  showInlineWarningsAndErrors: boolean,
  setShowInlineWarningsAndErrors: (value: boolean) => void,

  theme: Theme,
  setTheme(value: Theme): void,

  browserTheme: Theme,

  traceUpdatesEnabled: boolean,
  setTraceUpdatesEnabled: (value: boolean) => void,
};

const SettingsContext: ReactContext<Context> = createContext<Context>(
  ((null: any): Context),
);
SettingsContext.displayName = 'SettingsContext';

function useLocalStorageWithLog<T>(
  key: string,
  initialValue: T | (() => T),
): [T, (value: T | (() => T)) => void] {
  return useLocalStorage<T>(key, initialValue, (v, k) => {
    logEvent({
      event_name: 'settings-changed',
      metadata: {
        source: 'localStorage setter',
        key: k,
        value: v,
      },
    });
  });
}

type DocumentElements = Array<HTMLElement>;

type Props = {
  browserTheme: BrowserTheme,
  children: React$Node,
  componentsPortalContainer?: Element,
  profilerPortalContainer?: Element,
};

function SettingsContextController({
  browserTheme,
  children,
  componentsPortalContainer,
  profilerPortalContainer,
}: Props): React.Node {
  const bridge = useContext(BridgeContext);

  const [displayDensity, setDisplayDensity] =
    useLocalStorageWithLog<DisplayDensity>(
      'React::DevTools::displayDensity',
      'compact',
    );
  const [theme, setTheme] = useLocalStorageWithLog<Theme>(
    LOCAL_STORAGE_BROWSER_THEME,
    'auto',
  );
  const [appendComponentStack, setAppendComponentStack] =
    useLocalStorageWithLog<boolean>(
      LOCAL_STORAGE_SHOULD_APPEND_COMPONENT_STACK_KEY,
      true,
    );
  const [breakOnConsoleErrors, setBreakOnConsoleErrors] =
    useLocalStorageWithLog<boolean>(
      LOCAL_STORAGE_SHOULD_BREAK_ON_CONSOLE_ERRORS,
      false,
    );
  const [parseHookNames, setParseHookNames] = useLocalStorageWithLog<boolean>(
    LOCAL_STORAGE_PARSE_HOOK_NAMES_KEY,
    false,
  );
  const [hideConsoleLogsInStrictMode, setHideConsoleLogsInStrictMode] =
    useLocalStorageWithLog<boolean>(
      LOCAL_STORAGE_HIDE_CONSOLE_LOGS_IN_STRICT_MODE,
      false,
    );
  const [showInlineWarningsAndErrors, setShowInlineWarningsAndErrors] =
    useLocalStorageWithLog<boolean>(
      LOCAL_STORAGE_SHOW_INLINE_WARNINGS_AND_ERRORS_KEY,
      true,
    );
  const [traceUpdatesEnabled, setTraceUpdatesEnabled] =
    useLocalStorageWithLog<boolean>(
      LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY,
      false,
    );

  const documentElements = useMemo<DocumentElements>(() => {
    const array: Array<HTMLElement> = [
      ((document.documentElement: any): HTMLElement),
    ];
    if (componentsPortalContainer != null) {
      array.push(
        ((componentsPortalContainer.ownerDocument
          .documentElement: any): HTMLElement),
      );
    }
    if (profilerPortalContainer != null) {
      array.push(
        ((profilerPortalContainer.ownerDocument
          .documentElement: any): HTMLElement),
      );
    }
    return array;
  }, [componentsPortalContainer, profilerPortalContainer]);

  useLayoutEffect(() => {
    switch (displayDensity) {
      case 'comfortable':
        updateDisplayDensity('comfortable', documentElements);
        break;
      case 'compact':
        updateDisplayDensity('compact', documentElements);
        break;
      default:
        throw Error(`Unsupported displayDensity value "${displayDensity}"`);
    }
  }, [displayDensity, documentElements]);

  useLayoutEffect(() => {
    switch (theme) {
      case 'light':
        updateThemeVariables('light', documentElements);
        break;
      case 'dark':
        updateThemeVariables('dark', documentElements);
        break;
      case 'auto':
        updateThemeVariables(browserTheme, documentElements);
        break;
      default:
        throw Error(`Unsupported theme value "${theme}"`);
    }
  }, [browserTheme, theme, documentElements]);

  useEffect(() => {
    bridge.send('updateConsolePatchSettings', {
      appendComponentStack,
      breakOnConsoleErrors,
      showInlineWarningsAndErrors,
      hideConsoleLogsInStrictMode,
      browserTheme,
    });
  }, [
    bridge,
    appendComponentStack,
    breakOnConsoleErrors,
    showInlineWarningsAndErrors,
    hideConsoleLogsInStrictMode,
    browserTheme,
  ]);

  useEffect(() => {
    bridge.send('setTraceUpdatesEnabled', traceUpdatesEnabled);
  }, [bridge, traceUpdatesEnabled]);

  const value = useMemo(
    () => ({
      appendComponentStack,
      breakOnConsoleErrors,
      displayDensity,
      lineHeight:
        displayDensity === 'compact'
          ? COMPACT_LINE_HEIGHT
          : COMFORTABLE_LINE_HEIGHT,
      parseHookNames,
      setAppendComponentStack,
      setBreakOnConsoleErrors,
      setDisplayDensity,
      setParseHookNames,
      setTheme,
      setTraceUpdatesEnabled,
      setShowInlineWarningsAndErrors,
      showInlineWarningsAndErrors,
      setHideConsoleLogsInStrictMode,
      hideConsoleLogsInStrictMode,
      theme,
      browserTheme,
      traceUpdatesEnabled,
    }),
    [
      appendComponentStack,
      breakOnConsoleErrors,
      displayDensity,
      parseHookNames,
      setAppendComponentStack,
      setBreakOnConsoleErrors,
      setDisplayDensity,
      setParseHookNames,
      setTheme,
      setTraceUpdatesEnabled,
      setShowInlineWarningsAndErrors,
      showInlineWarningsAndErrors,
      setHideConsoleLogsInStrictMode,
      hideConsoleLogsInStrictMode,
      theme,
      browserTheme,
      traceUpdatesEnabled,
    ],
  );

  return (
    <SettingsContext.Provider value={value}>
      {children}
    </SettingsContext.Provider>
  );
}

export function updateDisplayDensity(
  displayDensity: DisplayDensity,
  documentElements: DocumentElements,
): void {
  // Sizes and paddings/margins are all rem-based,
  // so update the root font-size as well when the display preference changes.
  const computedStyle = getComputedStyle((document.body: any));
  const fontSize = computedStyle.getPropertyValue(
    `--${displayDensity}-root-font-size`,
  );
  const root = ((document.querySelector(':root'): any): HTMLElement);
  root.style.fontSize = fontSize;
}

export function updateThemeVariables(
  theme: Theme,
  documentElements: DocumentElements,
): void {
  // Update scrollbar color to match theme.
  // this CSS property is currently only supported in Firefox,
  // but it makes a significant UI improvement in dark mode.
  // https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-color
  documentElements.forEach(documentElement => {
    // $FlowFixMe[prop-missing] scrollbarColor is missing in CSSStyleDeclaration
    documentElement.style.scrollbarColor = `var(${`--${theme}-color-scroll-thumb`}) var(${`--${theme}-color-scroll-track`})`;
  });
}

export {SettingsContext, SettingsContextController};
