import { memo, useContext, useEffect, useMemo, useRef, useState } from "react";

import { trie } from "dope2";

import { CallContext } from "../../context/CallContext";
import { GlobalContext } from "../../context/GlobalContext";
import { inferTypeInput, type StaticEnvironment, type Type, type TypeInfoInput } from "../../eval/infer_type";
import { getActions } from "../app/actions";
import { Input } from "../other/Input";
import { Type as TypeBlock, TypeInfoBlock } from "../other/Type";
import type { ExprBlockState, State2Props } from "./ExprBlock";

import "./InputBlock.css";
import { evalExpr } from "../../eval/eval";

interface Literal {
  kind: "literal";
  type: string; // todo: store (and serialize) real type
};
interface Name {
  kind: "name";
}
interface Gibberish {
  kind: "gibberish";
}
export type InputValueType = Literal | Name | Gibberish;

export interface InputBlockState {
  kind: "input";
  text: string;
  value: InputValueType;
  focus?: boolean;
}

export type PrioritizedSuggestionType = [number, Type, InputBlockState];

export interface InputBlockProps extends State2Props<InputBlockState,ExprBlockState> {
  onCancel: () => void;
  typeInfo: TypeInfoInput;
}

const attemptLiterals = [
  ["Double", text => (text !== '') && !Number.isNaN(Number(text))],
  ["Int"   , text => /^-?[0-9]+$/.test(text)]
] as [string, (text: string) => boolean][];

const computeSuggestions = (
  text: string,
  env: StaticEnvironment,
  score: InputBlockProps['score'],
): PrioritizedSuggestionType[] => {
  const startTime = performance.now();

  const ls = [
    ...attemptLiterals
      .filter(([_, test]) => test(text))
      .map(([typename]) => ({
        kind: "input",
        text,
        value: {
          kind: "literal",
          type: typename,
        },
      })),
    ...trie.suggest(env.names)(text)(Infinity)
      .map(([name]) => ({
        kind: "input",
        text: name,
        value: {
          kind: "name",
        },
      })),
  ];
  const midTime = performance.now();

  // return []; // <-- uncomment to disable suggestions (useful for debugging)
  const result = ls.map((state) => [score(state), inferTypeInput(state, env).type, state] as PrioritizedSuggestionType)
    .sort(([a],[b]) => b-a);

  const endTime = performance.now();

  console.log(`computing suggestions took ${midTime-startTime} + ${endTime - midTime}`);

  return result;
}

export function InputBlock({ state, setState, score, onCancel, typeInfo, evalResult }: InputBlockProps) {
  const {text, focus} = state;
  const globalContext = useContext(GlobalContext);
  const env = typeInfo.env;
  const callContext = useContext(CallContext);
  const inputRef = useRef<HTMLInputElement>(null);
  const [i, setI] = useState(0); // selected suggestion idx

  const singleSuggestion = trie.growPrefix(env.names)(text);
  const suggestions = useMemo(() => computeSuggestions(text, env, score), [text, score, env]);

  useEffect(() => {
    if (focus) {
      inputRef.current?.focus();
    }
  }, [focus]);

  useEffect(() => {
    if (suggestions.length >= i) {
      setI(0);
    }
  }, [suggestions.length]);

  const onTextChange = newText => {
    setState(state => ({...state,
      text: newText,
      value: (trie.get(env.names)(newText) ? {
        kind: "name",
      } : state.value),
    }));
  }

  const onSelectSuggestion = () => {
    const [_priority, _type, inputState] = suggestions[i];
    setState(_ => inputState);
  };

  const extraHandlers = {
    ArrowDown: (e) => {
      setI((i + 1) % suggestions.length);
      e.preventDefault();
    },
    ArrowUp: (e) => {
      setI((suggestions.length + i - 1) % suggestions.length);
      e.preventDefault();
    },
    " ": (e) => {
      if (text.length > 0) {
        if (callContext.addParam) {
          callContext.addParam();
        }
        else {
          const actions = getActions(globalContext, setState);
          actions.c();
        }
      }
      e.preventDefault();
    },
  };

  const err = typeInfo.err || evalResult.err;

  return <>
    <Input
      placeholder="<name or literal>"
      onCancel={onCancel}
      onEnter={onSelectSuggestion}
      onTextChange={onTextChange}
      text={text}
      suggestion={singleSuggestion}
      extraHandlers={extraHandlers}
      >
      <span className="dropdownContainer">
        <span className="dropdown">
          {(err !== undefined) &&
            (<div className="errorMessage">
              {err.message.trim()}
            </div>)}
          <Suggestions
            suggestions={suggestions}
            onSelect={onSelectSuggestion}
            i={i} setI={setI} />
        </span>
      </span>
    </Input>
    ::<TypeInfoBlock typeInfo={typeInfo} />
  </>;
}

function Suggestions({ suggestions, onSelect, i, setI }) {
  return <>{(suggestions.length > 0) &&
    <div className={"suggestionsScrollableBox"}>
      {suggestions.map((suggestion, j) =>
        <SuggestionMemo key={j}
          {...{setI, j,
            onSelect,
            highlighted: i===j,
            suggestion}}/>)}
    </div>
  }</>;
}

interface SuggestionProps {
  setI: any;
  j: number;
  onSelect: any;
  highlighted: boolean;
  suggestion: PrioritizedSuggestionType;
}

function Suggestion({ setI, j, onSelect, highlighted, suggestion: [priority, type, {text, value: {kind} }] }: SuggestionProps) {
  const onMouseEnter = j => () => {
    setI(j);
  };
  const onMouseDown = j => () => {
    setI(j);
    onSelect();
  };
  return <div
    key={`${j}_${name}`}
    className={(highlighted ? " selected" : "")}
    onMouseEnter={onMouseEnter(j)}
    onMouseDown={onMouseDown(j)}>
      ({priority}) ({kind}) {text} :: <TypeBlock type={type} />
  </div>;
}

const SuggestionMemo = memo<SuggestionProps>(Suggestion);