/**
 * 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 * as React from 'react';
import {
  useContext,
  useMemo,
  useCallback,
  memo,
  useState,
  useEffect,
} from 'react';
import styles from './HookChangeSummary.css';
import ButtonIcon from '../ButtonIcon';
import {InspectedElementContext} from '../Components/InspectedElementContext';
import {StoreContext} from '../context';

import {
  getAlreadyLoadedHookNames,
  getHookSourceLocationKey,
} from 'react-devtools-shared/src/hookNamesCache';
import Toggle from '../Toggle';
import type {HooksNode} from 'react-debug-tools/src/ReactDebugHooks';
import type {ChangeDescription} from './types';

// $FlowFixMe: Flow doesn't know about Intl.ListFormat
const hookListFormatter = new Intl.ListFormat('en', {
  style: 'long',
  type: 'conjunction',
});

type HookProps = {
  hook: HooksNode,
  hookNames: Map<string, string> | null,
};

const Hook: React.AbstractComponent<HookProps> = memo(({hook, hookNames}) => {
  const hookSource = hook.hookSource;
  const hookName = useMemo(() => {
    if (!hookSource || !hookNames) return null;
    const key = getHookSourceLocationKey(hookSource);
    return hookNames.get(key) || null;
  }, [hookSource, hookNames]);

  return (
    <ul className={styles.Hook}>
      <li>
        {hook.id !== null && (
          <span className={styles.PrimitiveHookNumber}>
            {String(hook.id + 1)}
          </span>
        )}
        <span
          className={hook.id !== null ? styles.PrimitiveHookName : styles.Name}>
          {hook.name}
          {hookName && <span className={styles.HookName}>({hookName})</span>}
        </span>
        {hook.subHooks?.map((subHook, index) => (
          <Hook key={hook.id} hook={subHook} hookNames={hookNames} />
        ))}
      </li>
    </ul>
  );
});

const shouldKeepHook = (
  hook: HooksNode,
  hooksArray: Array<number>,
): boolean => {
  if (hook.id !== null && hooksArray.includes(hook.id)) {
    return true;
  }
  const subHooks = hook.subHooks;
  if (subHooks == null) {
    return false;
  }

  return subHooks.some(subHook => shouldKeepHook(subHook, hooksArray));
};

const filterHooks = (
  hook: HooksNode,
  hooksArray: Array<number>,
): HooksNode | null => {
  if (!shouldKeepHook(hook, hooksArray)) {
    return null;
  }

  const subHooks = hook.subHooks;
  if (subHooks == null) {
    return hook;
  }

  const filteredSubHooks = subHooks
    .map(subHook => filterHooks(subHook, hooksArray))
    .filter(Boolean);
  return filteredSubHooks.length > 0
    ? {...hook, subHooks: filteredSubHooks}
    : hook;
};

type Props = {|
  fiberID: number,
  hooks: $PropertyType<ChangeDescription, 'hooks'>,
  state: $PropertyType<ChangeDescription, 'state'>,
  displayMode?: 'detailed' | 'compact',
|};

const HookChangeSummary: React.AbstractComponent<Props> = memo(
  ({hooks, fiberID, state, displayMode = 'detailed'}: Props) => {
    const {parseHookNames, toggleParseHookNames, inspectedElement} = useContext(
      InspectedElementContext,
    );
    const store = useContext(StoreContext);

    const [parseHookNamesOptimistic, setParseHookNamesOptimistic] =
      useState<boolean>(parseHookNames);

    useEffect(() => {
      setParseHookNamesOptimistic(parseHookNames);
    }, [inspectedElement?.id, parseHookNames]);

    const handleOnChange = useCallback(() => {
      setParseHookNamesOptimistic(!parseHookNames);
      toggleParseHookNames();
    }, [toggleParseHookNames, parseHookNames]);

    const element = fiberID !== null ? store.getElementByID(fiberID) : null;
    const hookNames =
      element != null ? getAlreadyLoadedHookNames(element) : null;

    const filteredHooks = useMemo(() => {
      if (!hooks || !inspectedElement?.hooks) return null;
      return inspectedElement.hooks
        .map(hook => filterHooks(hook, hooks))
        .filter(Boolean);
    }, [inspectedElement?.hooks, hooks]);

    const hookParsingFailed = parseHookNames && hookNames === null;

    if (!hooks?.length) {
      return <span>No hooks changed</span>;
    }

    if (
      inspectedElement?.id !== element?.id ||
      filteredHooks?.length !== hooks.length ||
      displayMode === 'compact'
    ) {
      const hookIds = hooks.map(hookId => String(hookId + 1));
      const hookWord = hookIds.length === 1 ? '• Hook' : '• Hooks';
      return (
        <span>
          {hookWord} {hookListFormatter.format(hookIds)} changed
        </span>
      );
    }

    let toggleTitle: string;
    if (hookParsingFailed) {
      toggleTitle = 'Hook parsing failed';
    } else if (parseHookNamesOptimistic) {
      toggleTitle = 'Parsing hook names ...';
    } else {
      toggleTitle = 'Parse hook names (may be slow)';
    }

    if (filteredHooks == null) {
      return null;
    }

    return (
      <div>
        {filteredHooks.length > 1 ? '• Hooks changed:' : '• Hook changed:'}
        {(!parseHookNames || hookParsingFailed) && (
          <Toggle
            className={
              hookParsingFailed
                ? styles.ToggleError
                : styles.LoadHookNamesToggle
            }
            isChecked={parseHookNamesOptimistic}
            isDisabled={parseHookNamesOptimistic || hookParsingFailed}
            onChange={handleOnChange}
            title={toggleTitle}>
            <ButtonIcon type="parse-hook-names" />
          </Toggle>
        )}
        {filteredHooks.map(hook => (
          <Hook
            key={`${inspectedElement?.id ?? 'unknown'}-${hook.id}`}
            hook={hook}
            hookNames={hookNames}
          />
        ))}
      </div>
    );
  },
);

export default HookChangeSummary;