// Copyright (c) 2020-2021 Drew Lemmy
// This file is part of TenebraWeb 2 under AGPL-3.0.
// Full details: https://github.com/tmpim/TenebraWeb2/blob/master/LICENSE.txt
import {
  useState, useEffect, useMemo, useRef, Dispatch, SetStateAction, Ref,
  MutableRefObject
} from "react";
import { Select, Form, Input, Button } from "antd";
import { RefSelectProps } from "antd/lib/select";

import { useTranslation, TFunction } from "react-i18next";

import { useSelector } from "react-redux";
import { RootState } from "@store";

import { WalletAddressMap, useWallets } from "@wallets";
import { NameOptionGroup, fetchNames } from "./lookupNames";

import { useNameSuffix } from "@utils/tenebra";
import shallowEqual from "shallowequal";

import { throttle } from "lodash-es";

import Debug from "debug";
const debug = Debug("tenebraweb:name-picker");

const FETCH_THROTTLE = 2000;
export async function _fetchNames(
  t: TFunction,
  nameSuffix: string,
  wallets: WalletAddressMap,
  setOptions: Dispatch<SetStateAction<NameOptionGroup[] | null>>,
  isMounted: MutableRefObject<boolean>
): Promise<void> {
  if (!isMounted.current) {
    debug("unmounted name picker lookup result skipped");
    return;
  }

  setOptions(await fetchNames(t, nameSuffix, wallets));
}

interface Props {
  formName: string;
  label?: string;
  tabIndex?: number;

  value?: string[];
  setValue?: (value: string[]) => void;

  filterOwner?: string;
  suppressUpdates?: boolean;

  multiple?: boolean;
  allowAll?: boolean;

  inputRef?: Ref<RefSelectProps>;
}

export function NamePicker({
  formName,
  label,
  tabIndex,

  value,
  setValue,

  filterOwner,
  suppressUpdates,

  multiple,
  allowAll,

  inputRef,
  ...props
}: Props): JSX.Element {
  const { t } = useTranslation();

  // Used for clean-up
  const isMounted = useRef(true);

  // Used to fetch the list of available names
  const { walletAddressMap, joinedAddressList } = useWallets();
  const throttledFetchNames = useMemo(() =>
    throttle(_fetchNames, FETCH_THROTTLE, { leading: true }), []);
  const nameSuffix = useNameSuffix();

  // The actual list of available names (pre-filtered, not rendered yet)
  const [nameOptions, setNameOptions]
    = useState<NameOptionGroup[] | null>(null);
  const [filteredOptions, setFilteredOptions]
    = useState<JSX.Element[] | null>(null);

  // Used for auto-refreshing the names if they happen to update
  const refreshID = useSelector((s: RootState) => s.node.lastOwnNameTransactionID);

  // Whether or not to show the 'All' button for bulk name management. On by
  // default.
  const showAllButton = allowAll !== false && multiple !== false;

  // Fetch the name list on mount/when the address list changes, or when one of
  // our wallets receives a name transaction.
  useEffect(() => {
    // Skip doing anything when unmounted to avoid illegal state updates
    if (!isMounted.current) return debug("unmounted skipped lookup useEffect");
    if (suppressUpdates) return debug("name picker lookup suppressed");

    debug(
      "addressList updated (%s, %s, %d)",
      joinedAddressList, nameSuffix, refreshID
    );

    throttledFetchNames(
      t,
      nameSuffix,
      walletAddressMap,
      setNameOptions,
      isMounted
    );
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [
    throttledFetchNames, t, nameSuffix, refreshID, joinedAddressList,
    suppressUpdates
  ]);

  // If passed an address, filter out that address from the results. Used to
  // prevent sending names to the existing owner. Renders the name options.
  useEffect(() => {
    // Skip doing anything when unmounted to avoid illegal state updates
    if (!isMounted.current) return debug("unmounted skipped filter useEffect");

    if (!nameOptions) {
      setFilteredOptions(null);
      return;
    }

    const filteredGroups = nameOptions
      .filter(group => filterOwner ? group.key !== filterOwner : true);

    // If there are any invalid names in the form value, remove them here
    if (value && setValue) {
      // Convert the available names to a lookup table
      const filteredNames = filteredGroups
        .flatMap(g => g.names)
        .reduce((out, o) => {
          out[o.key] = true;
          return out;
        }, {} as Record<string, boolean>);

      // Remove any names in the value that aren't in the lookup table
      const newValue = value?.filter(v => !!filteredNames[v]);
      const equal = shallowEqual(value, newValue);

      debug(
        "updating value (filterOwner: %s) to %o (equal: %b)",
        filterOwner, newValue, equal
      );

      // If the values are different, set the new one
      if (!equal) setValue(newValue);
    }

    setFilteredOptions(filteredGroups.map(renderNameOptions));
  }, [nameOptions, filterOwner, value, setValue]);

  // Clean up the debounced function when unmounting
  useEffect(() => {
    return () => {
      debug("unmounting name picker");
      isMounted.current = false;
      throttledFetchNames?.cancel();
    };
  }, [throttledFetchNames]);

  // Select all available names
  function selectAll() {
    if (!nameOptions || !setValue) return;

    // Filter out names from filterOwner if applicable
    const filteredGroups = nameOptions
      .filter(group => filterOwner ? group.key !== filterOwner : true);
    const names = filteredGroups
      .flatMap(g => g.names)
      .map(n => n.value);

    setValue(names);
  }

  return <Form.Item
    label={label}
    required
    {...props}
  >
    <Input.Group compact style={{ display: "flex" }}>
      {/* Name select */}
      <Form.Item
        name={formName}
        style={{ flex: 1, marginBottom: 0 }}

        validateFirst
        rules={[
          { required: true, message: t("nameTransfer.errorNameRequired") }
        ]}
      >
        <Select
          showSearch
          placeholder={multiple !== false
            ? t("namePicker.placeholderMultiple")
            : t("namePicker.placeholder")}

          style={{ width: "100%" }}

          allowClear
          mode={multiple !== false ? "multiple" : undefined}
          maxTagCount={5}

          loading={!nameOptions}

          // Filter by name with suffix case insensitively
          filterOption={(input, option) => {
            // Display all groups
            if (option?.options) return false;

            const name = option?.["data-name"] || option?.value;
            if (!name) return false;

            return name.toUpperCase().indexOf(input.toUpperCase()) >= 0;
          }}

          ref={inputRef}
          tabIndex={tabIndex}
        >
          {filteredOptions}
        </Select>
      </Form.Item>

      {/* "All" button */}
      {showAllButton && <div>
        <Button onClick={selectAll} >
          {t("namePicker.buttonAll")}
        </Button>
      </div>}
    </Input.Group>
  </Form.Item>;
}

function renderNameOptions(group: NameOptionGroup): JSX.Element {
  // Group by owning wallet
  return <Select.OptGroup key={group.key} label={group.label}>
    {/* Each individual name */}
    {group.names.map(name => (
      <Select.Option
        key={name.key}
        value={name.value}
        data-name={name.name}
      >
        {name.name}
      </Select.Option>
    ))}
  </Select.OptGroup>;
}
