/**
 * 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 {
  Fragment,
  useCallback,
  useContext,
  useLayoutEffect,
  useReducer,
  useRef,
  useState,
} from 'react';
import Button from '../Button';
import ButtonIcon from '../ButtonIcon';
import Toggle from '../Toggle';
import ElementBadges from './ElementBadges';
import {OwnersListContext, useChangeOwnerAction} from './OwnersListContext';
import {TreeDispatcherContext, TreeStateContext} from './TreeContext';
import {useIsOverflowing} from '../hooks';
import {StoreContext} from '../context';
import Tooltip from '../Components/reach-ui/tooltip';
import {
  Menu,
  MenuList,
  MenuButton,
  MenuItem,
} from '../Components/reach-ui/menu-button';

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

import styles from './OwnersStack.css';

type SelectOwner = (owner: SerializedElement | null) => void;

type ACTION_UPDATE_OWNER_ID = {
  type: 'UPDATE_OWNER_ID',
  ownerID: number | null,
  owners: Array<SerializedElement>,
};
type ACTION_UPDATE_SELECTED_INDEX = {
  type: 'UPDATE_SELECTED_INDEX',
  selectedIndex: number,
};

type Action = ACTION_UPDATE_OWNER_ID | ACTION_UPDATE_SELECTED_INDEX;

type State = {
  ownerID: number | null,
  owners: Array<SerializedElement>,
  selectedIndex: number,
};

function dialogReducer(state: State, action: Action) {
  switch (action.type) {
    case 'UPDATE_OWNER_ID':
      const selectedIndex = action.owners.findIndex(
        owner => owner.id === action.ownerID,
      );
      return {
        ownerID: action.ownerID,
        owners: action.owners,
        selectedIndex,
      };
    case 'UPDATE_SELECTED_INDEX':
      return {
        ...state,
        selectedIndex: action.selectedIndex,
      };
    default:
      throw new Error(`Invalid action "${action.type}"`);
  }
}

export default function OwnerStack(): React.Node {
  const read = useContext(OwnersListContext);
  const {ownerID} = useContext(TreeStateContext);
  const treeDispatch = useContext(TreeDispatcherContext);
  const changeOwnerAction = useChangeOwnerAction();

  const [state, dispatch] = useReducer<State, State, Action>(dialogReducer, {
    ownerID: null,
    owners: [],
    selectedIndex: 0,
  });

  // When an owner is selected, we either need to update the selected index, or we need to fetch a new list of owners.
  // We use a reducer here so that we can avoid fetching a new list unless the owner ID has actually changed.
  if (ownerID === null) {
    dispatch({
      type: 'UPDATE_OWNER_ID',
      ownerID: null,
      owners: [],
    });
  } else if (ownerID !== state.ownerID) {
    const isInStore =
      state.owners.findIndex(owner => owner.id === ownerID) >= 0;
    dispatch({
      type: 'UPDATE_OWNER_ID',
      ownerID,
      owners: isInStore ? state.owners : read(ownerID) || [],
    });
  }

  const {owners, selectedIndex} = state;

  const selectOwner = useCallback<SelectOwner>(
    (owner: SerializedElement | null) => {
      if (owner !== null) {
        const index = owners.indexOf(owner);
        dispatch({
          type: 'UPDATE_SELECTED_INDEX',
          selectedIndex: index >= 0 ? index : 0,
        });
        changeOwnerAction(owner.id);
      } else {
        dispatch({
          type: 'UPDATE_SELECTED_INDEX',
          selectedIndex: 0,
        });
        treeDispatch({type: 'RESET_OWNER_STACK'});
      }
    },
    [owners, treeDispatch],
  );

  const [elementsTotalWidth, setElementsTotalWidth] = useState(0);
  const elementsBarRef = useRef<HTMLDivElement | null>(null);
  const isOverflowing = useIsOverflowing(elementsBarRef, elementsTotalWidth);

  const selectedOwner = owners[selectedIndex];

  useLayoutEffect(() => {
    // If we're already overflowing, then we don't need to re-measure items.
    // That's because once the owners stack is open, it can only get larger (by drilling in).
    // A totally new stack can only be reached by exiting this mode and re-entering it.
    if (elementsBarRef.current === null || isOverflowing) {
      return () => {};
    }

    let totalWidth = 0;
    for (let i = 0; i < owners.length; i++) {
      const element = elementsBarRef.current.children[i];
      const computedStyle = getComputedStyle(element);

      totalWidth +=
        element.offsetWidth +
        parseInt(computedStyle.marginLeft, 10) +
        parseInt(computedStyle.marginRight, 10);
    }

    setElementsTotalWidth(totalWidth);
  }, [elementsBarRef, isOverflowing, owners.length]);

  return (
    <div className={styles.OwnerStack}>
      <div className={styles.Bar} ref={elementsBarRef}>
        {isOverflowing && (
          <Fragment>
            <ElementsDropdown
              owners={owners}
              selectedIndex={selectedIndex}
              selectOwner={selectOwner}
            />
            <BackToOwnerButton
              owners={owners}
              selectedIndex={selectedIndex}
              selectOwner={selectOwner}
            />
            {selectedOwner != null && (
              <ElementView
                owner={selectedOwner}
                isSelected={true}
                selectOwner={selectOwner}
              />
            )}
          </Fragment>
        )}
        {!isOverflowing &&
          owners.map((owner, index) => (
            <ElementView
              key={index}
              owner={owner}
              isSelected={index === selectedIndex}
              selectOwner={selectOwner}
            />
          ))}
      </div>
      <div className={styles.VRule} />
      <Button onClick={() => selectOwner(null)} title="Back to tree view">
        <ButtonIcon type="close" />
      </Button>
    </div>
  );
}

type ElementsDropdownProps = {
  owners: Array<SerializedElement>,
  selectedIndex: number,
  selectOwner: SelectOwner,
};
function ElementsDropdown({owners, selectOwner}: ElementsDropdownProps) {
  const store = useContext(StoreContext);

  const menuItems = [];
  for (let index = owners.length - 1; index >= 0; index--) {
    const owner = owners[index];
    const isInStore = store.containsElement(owner.id);
    menuItems.push(
      <MenuItem
        key={owner.id}
        className={`${styles.Component} ${isInStore ? '' : styles.NotInStore}`}
        onSelect={() => (isInStore ? selectOwner(owner) : null)}>
        {owner.displayName}

        <ElementBadges
          hocDisplayNames={owner.hocDisplayNames}
          environmentName={owner.env}
          compiledWithForget={owner.compiledWithForget}
          className={styles.BadgesBlock}
        />
      </MenuItem>,
    );
  }

  return (
    <Menu>
      <MenuButton className={styles.MenuButton}>
        <Tooltip label="Open elements dropdown">
          <span className={styles.MenuButtonContent} tabIndex={-1}>
            <ButtonIcon type="more" />
          </span>
        </Tooltip>
      </MenuButton>
      <MenuList className={styles.Modal}>{menuItems}</MenuList>
    </Menu>
  );
}

type ElementViewProps = {
  isSelected: boolean,
  owner: SerializedElement,
  selectOwner: SelectOwner,
  ...
};
function ElementView({isSelected, owner, selectOwner}: ElementViewProps) {
  const store = useContext(StoreContext);

  const {displayName, hocDisplayNames, compiledWithForget} = owner;
  const isInStore = store.containsElement(owner.id);

  const handleChange = useCallback(() => {
    if (isInStore) {
      selectOwner(owner);
    }
  }, [isInStore, selectOwner, owner]);

  return (
    <Toggle
      className={`${styles.Component} ${isInStore ? '' : styles.NotInStore}`}
      isChecked={isSelected}
      onChange={handleChange}>
      {displayName}

      <ElementBadges
        hocDisplayNames={hocDisplayNames}
        environmentName={owner.env}
        compiledWithForget={compiledWithForget}
        className={styles.BadgesBlock}
      />
    </Toggle>
  );
}

type BackToOwnerButtonProps = {
  owners: Array<SerializedElement>,
  selectedIndex: number,
  selectOwner: SelectOwner,
};
function BackToOwnerButton({
  owners,
  selectedIndex,
  selectOwner,
}: BackToOwnerButtonProps) {
  const store = useContext(StoreContext);

  if (selectedIndex <= 0) {
    return null;
  }

  const owner = owners[selectedIndex - 1];
  const isInStore = store.containsElement(owner.id);

  return (
    <Button
      className={isInStore ? undefined : styles.NotInStore}
      onClick={() => (isInStore ? selectOwner(owner) : null)}
      title={`Up to ${owner.displayName || 'owner'}`}>
      <ButtonIcon type="previous" />
    </Button>
  );
}
