import React from "react";
import { Popover } from "./Popover";
import { isTransparent } from "../utils";

import "./ColorPicker.scss";
import { isArrowKey, KEYS } from "../keys";
import { t, getLanguage } from "../i18n";
import { isWritableElement } from "../utils";
import colors from "../colors";
import { ExcalidrawElement } from "../element/types";
import { AppState } from "../types";

const MAX_CUSTOM_COLORS = 5;
const MAX_DEFAULT_COLORS = 15;

export const getCustomColors = (
  elements: readonly ExcalidrawElement[],
  type: "elementBackground" | "elementStroke",
) => {
  const customColors: string[] = [];
  const updatedElements = elements
    .filter((element) => !element.isDeleted)
    .sort((ele1, ele2) => ele2.updated - ele1.updated);

  let index = 0;
  const elementColorTypeMap = {
    elementBackground: "backgroundColor",
    elementStroke: "strokeColor",
  };
  const colorType = elementColorTypeMap[type] as
    | "backgroundColor"
    | "strokeColor";
  while (
    index < updatedElements.length &&
    customColors.length < MAX_CUSTOM_COLORS
  ) {
    const element = updatedElements[index];

    if (
      customColors.length < MAX_CUSTOM_COLORS &&
      isCustomColor(element[colorType], type) &&
      !customColors.includes(element[colorType])
    ) {
      customColors.push(element[colorType]);
    }
    index++;
  }
  return customColors;
};

const isCustomColor = (
  color: string,
  type: "elementBackground" | "elementStroke",
) => {
  return !colors[type].includes(color);
};

const isValidColor = (color: string) => {
  const style = new Option().style;
  style.color = color;
  return !!style.color;
};

const getColor = (color: string): string | null => {
  if (isTransparent(color)) {
    return color;
  }

  // testing for `#` first fixes a bug on Electron (more specfically, an
  // Obsidian popout window), where a hex color without `#` is (incorrectly)
  // considered valid
  return isValidColor(`#${color}`)
    ? `#${color}`
    : isValidColor(color)
    ? color
    : null;
};

// This is a narrow reimplementation of the awesome react-color Twitter component
// https://github.com/casesandberg/react-color/blob/master/src/components/twitter/Twitter.js

// Unfortunately, we can't detect keyboard layout in the browser. So this will
// only work well for QWERTY but not AZERTY or others...
const keyBindings = [
  ["1", "2", "3", "4", "5"],
  ["q", "w", "e", "r", "t"],
  ["a", "s", "d", "f", "g"],
  ["z", "x", "c", "v", "b"],
].flat();

const Picker = ({
  colors,
  color,
  onChange,
  onClose,
  label,
  showInput = true,
  type,
  elements,
}: {
  colors: string[];
  color: string | null;
  onChange: (color: string) => void;
  onClose: () => void;
  label: string;
  showInput: boolean;
  type: "canvasBackground" | "elementBackground" | "elementStroke";
  elements: readonly ExcalidrawElement[];
}) => {
  const firstItem = React.useRef<HTMLButtonElement>();
  const activeItem = React.useRef<HTMLButtonElement>();
  const gallery = React.useRef<HTMLDivElement>();
  const colorInput = React.useRef<HTMLInputElement>();

  const [customColors] = React.useState(() => {
    if (type === "canvasBackground") {
      return [];
    }
    return getCustomColors(elements, type);
  });

  React.useEffect(() => {
    // After the component is first mounted focus on first input
    if (activeItem.current) {
      activeItem.current.focus();
    } else if (colorInput.current) {
      colorInput.current.focus();
    } else if (gallery.current) {
      gallery.current.focus();
    }
  }, []);

  const handleKeyDown = (event: React.KeyboardEvent) => {
    let handled = false;
    if (isArrowKey(event.key)) {
      handled = true;
      const { activeElement } = document;
      const isRTL = getLanguage().rtl;
      let isCustom = false;
      let index = Array.prototype.indexOf.call(
        gallery.current!.querySelector(".color-picker-content--default")
          ?.children,
        activeElement,
      );
      if (index === -1) {
        index = Array.prototype.indexOf.call(
          gallery.current!.querySelector(".color-picker-content--canvas-colors")
            ?.children,
          activeElement,
        );
        if (index !== -1) {
          isCustom = true;
        }
      }
      const parentElement = isCustom
        ? gallery.current?.querySelector(".color-picker-content--canvas-colors")
        : gallery.current?.querySelector(".color-picker-content--default");

      if (parentElement && index !== -1) {
        const length = parentElement.children.length - (showInput ? 1 : 0);
        const nextIndex =
          event.key === (isRTL ? KEYS.ARROW_LEFT : KEYS.ARROW_RIGHT)
            ? (index + 1) % length
            : event.key === (isRTL ? KEYS.ARROW_RIGHT : KEYS.ARROW_LEFT)
            ? (length + index - 1) % length
            : !isCustom && event.key === KEYS.ARROW_DOWN
            ? (index + 5) % length
            : !isCustom && event.key === KEYS.ARROW_UP
            ? (length + index - 5) % length
            : index;
        (parentElement.children[nextIndex] as HTMLElement | undefined)?.focus();
      }
      event.preventDefault();
    } else if (
      keyBindings.includes(event.key.toLowerCase()) &&
      !event[KEYS.CTRL_OR_CMD] &&
      !event.altKey &&
      !isWritableElement(event.target)
    ) {
      handled = true;
      const index = keyBindings.indexOf(event.key.toLowerCase());
      const isCustom = index >= MAX_DEFAULT_COLORS;
      const parentElement = isCustom
        ? gallery?.current?.querySelector(
            ".color-picker-content--canvas-colors",
          )
        : gallery?.current?.querySelector(".color-picker-content--default");
      const actualIndex = isCustom ? index - MAX_DEFAULT_COLORS : index;
      (
        parentElement?.children[actualIndex] as HTMLElement | undefined
      )?.focus();

      event.preventDefault();
    } else if (event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) {
      handled = true;
      event.preventDefault();
      onClose();
    }
    if (handled) {
      event.nativeEvent.stopImmediatePropagation();
      event.stopPropagation();
    }
  };

  const renderColors = (colors: Array<string>, custom: boolean = false) => {
    return colors.map((_color, i) => {
      const _colorWithoutHash = _color.replace("#", "");
      const keyBinding = custom
        ? keyBindings[i + MAX_DEFAULT_COLORS]
        : keyBindings[i];
      const label = custom
        ? _colorWithoutHash
        : t(`colors.${_colorWithoutHash}`);
      return (
        <button
          className="color-picker-swatch"
          onClick={(event) => {
            (event.currentTarget as HTMLButtonElement).focus();
            onChange(_color);
          }}
          title={`${label}${
            !isTransparent(_color) ? ` (${_color})` : ""
          } — ${keyBinding.toUpperCase()}`}
          aria-label={label}
          aria-keyshortcuts={keyBindings[i]}
          style={{ color: _color }}
          key={_color}
          ref={(el) => {
            if (!custom && el && i === 0) {
              firstItem.current = el;
            }
            if (el && _color === color) {
              activeItem.current = el;
            }
          }}
          onFocus={() => {
            onChange(_color);
          }}
        >
          {isTransparent(_color) ? (
            <div className="color-picker-transparent"></div>
          ) : undefined}
          <span className="color-picker-keybinding">{keyBinding}</span>
        </button>
      );
    });
  };

  return (
    <div
      className={`color-picker color-picker-type-${type}`}
      role="dialog"
      aria-modal="true"
      aria-label={t("labels.colorPicker")}
      onKeyDown={handleKeyDown}
    >
      <div className="color-picker-triangle color-picker-triangle-shadow"></div>
      <div className="color-picker-triangle"></div>
      <div
        className="color-picker-content"
        ref={(el) => {
          if (el) {
            gallery.current = el;
          }
        }}
        // to allow focusing by clicking but not by tabbing
        tabIndex={-1}
      >
        <div className="color-picker-content--default">
          {renderColors(colors)}
        </div>
        {!!customColors.length && (
          <div className="color-picker-content--canvas">
            <span className="color-picker-content--canvas-title">
              {t("labels.canvasColors")}
            </span>
            <div className="color-picker-content--canvas-colors">
              {renderColors(customColors, true)}
            </div>
          </div>
        )}

        {showInput && (
          <ColorInput
            color={color}
            label={label}
            onChange={(color) => {
              onChange(color);
            }}
            ref={colorInput}
          />
        )}
      </div>
    </div>
  );
};

const ColorInput = React.forwardRef(
  (
    {
      color,
      onChange,
      label,
    }: {
      color: string | null;
      onChange: (color: string) => void;
      label: string;
    },
    ref,
  ) => {
    const [innerValue, setInnerValue] = React.useState(color);
    const inputRef = React.useRef(null);

    React.useEffect(() => {
      setInnerValue(color);
    }, [color]);

    React.useImperativeHandle(ref, () => inputRef.current);

    const changeColor = React.useCallback(
      (inputValue: string) => {
        const value = inputValue.toLowerCase();
        const color = getColor(value);
        if (color) {
          onChange(color);
        }
        setInnerValue(value);
      },
      [onChange],
    );

    return (
      <label className="color-input-container">
        <div className="color-picker-hash">#</div>
        <input
          spellCheck={false}
          className="color-picker-input"
          aria-label={label}
          onChange={(event) => changeColor(event.target.value)}
          value={(innerValue || "").replace(/^#/, "")}
          onBlur={() => setInnerValue(color)}
          ref={inputRef}
        />
      </label>
    );
  },
);

ColorInput.displayName = "ColorInput";

export const ColorPicker = ({
  type,
  color,
  onChange,
  label,
  isActive,
  setActive,
  elements,
  appState,
}: {
  type: "canvasBackground" | "elementBackground" | "elementStroke";
  color: string | null;
  onChange: (color: string) => void;
  label: string;
  isActive: boolean;
  setActive: (active: boolean) => void;
  elements: readonly ExcalidrawElement[];
  appState: AppState;
}) => {
  const pickerButton = React.useRef<HTMLButtonElement>(null);
  const coords = pickerButton.current?.getBoundingClientRect();

  return (
    <div>
      <div className="color-picker-control-container">
        <div className="color-picker-label-swatch-container">
          <button
            className="color-picker-label-swatch"
            aria-label={label}
            style={color ? { "--swatch-color": color } : undefined}
            onClick={() => setActive(!isActive)}
            ref={pickerButton}
          />
        </div>
        <ColorInput
          color={color}
          label={label}
          onChange={(color) => {
            onChange(color);
          }}
        />
      </div>
      <React.Suspense fallback="">
        {isActive ? (
          <div
            className="color-picker-popover-container"
            style={{
              position: "fixed",
              top: coords?.top,
              left: coords?.right,
              zIndex: 1,
            }}
          >
            <Popover
              onCloseRequest={(event) =>
                event.target !== pickerButton.current && setActive(false)
              }
            >
              <Picker
                colors={colors[type]}
                color={color || null}
                onChange={(changedColor) => {
                  onChange(changedColor);
                }}
                onClose={() => {
                  setActive(false);
                  pickerButton.current?.focus();
                }}
                label={label}
                showInput={false}
                type={type}
                elements={elements}
              />
            </Popover>
          </div>
        ) : null}
      </React.Suspense>
    </div>
  );
};
