import { useContext } from "react";

import { Editor, type EditorState } from "./Editor";
import { Value } from "./Value";
import { type SetStateFn, type State2Props } from "./Editor";
import { DeepError, evalCallBlock, evalEditorBlock, haveValue } from "./eval";
import { type ResolvedType } from "./eval";
import "./CallBlock.css";
import { EnvContext } from "./EnvContext";
import type { SuggestionType } from "./InputBlock";
import { getType, NotAFunctionError, unify, UnifyError } from "dope2";

export interface CallBlockState<
  FnState=EditorState,
  InputState=EditorState,
> {
  kind: "call";
  fn: FnState;
  input: InputState;
}

interface CallBlockProps<
  FnState=EditorState,
  InputState=EditorState,
> extends State2Props<CallBlockState<FnState,InputState>,EditorState> {
  suggestionPriority: (suggestion: SuggestionType) => number;
}

function headlessCallBlock(setState: (callback: SetStateFn<CallBlockState,EditorState>) => void) {
  const setFn = (callback: SetStateFn) => {
    setState(state => ({...state, fn: callback(state.fn)}));
  }
  const setInput = (callback: SetStateFn) => {
    setState(state => ({...state, input: callback(state.input)}));
  }
  const onFnCancel = () => {
    setState(state => state.input); // we become our input
  }
  const onInputCancel = () => {
    setState(state => state.fn); // we become our function
  }
  return {setFn, setInput, onFnCancel, onInputCancel};
}

export function CallBlock({ state, setState, suggestionPriority }: CallBlockProps) {
  const {setFn, setInput, onFnCancel, onInputCancel}
    = headlessCallBlock(setState);
  const env = useContext(EnvContext);
  const resolved = evalEditorBlock(state, env);
  return <span className={"functionBlock" + ((resolved instanceof DeepError) ? " unifyError" : "")}>
    <FunctionHeader
      fn={state.fn}
      setFn={setFn}
      onFnCancel={onFnCancel}
      input={state.input}
      suggestionPriority={suggestionPriority}
      />
    <div className="functionParams">
      <div className="outputParam">
        {/* Sequence of input parameters */}
        <InputParams
          fn={state.fn}       setFn={setFn}
          input={state.input} setInput={setInput}
          onInputCancel={onInputCancel}
          depth={0}
          errorDepth={resolved instanceof DeepError ? (resolved.depth) : -1}
          suggestionPriority={suggestionPriority}
          />
        {/* Output (or Error) */}
        { resolved instanceof DeepError && resolved.e.toString()
        || resolved && <><Value dynamic={resolved} />
            </>}
      </div>
    </div>
  </span>;
}

function computePriority(fn: ResolvedType, input: ResolvedType, outPriority: (s: SuggestionType) => number) {
  const resolved = evalCallBlock(fn, input);
  if ((resolved && !(resolved instanceof DeepError))) {
      // The computed output becomes an input for the surrounding block, which may also have a priority function defined, for instance our output is the input to another function
      return 1 + outPriority(['literal', '<computed>', resolved]);
  }
  return 0; // no fit
}

function FunctionHeader({ fn, setFn, input, onFnCancel, suggestionPriority }) {
  const env = useContext(EnvContext);
  if (fn.kind === "call") {
    // if the function we're calling is itself the result of a function call,
    // then we are anonymous, and so we don't draw a function name

    // recurse:
    const {
      setFn         : setFnFn,
      onFnCancel    : onFnFnCancel,
    } = headlessCallBlock(setFn);

    return <FunctionHeader
      fn={fn.fn}
      setFn={setFnFn}
      onFnCancel={onFnFnCancel}
      input={fn.input}
      suggestionPriority={(fnSuggestion: SuggestionType) => computePriority(
        fnSuggestion[2],
        evalEditorBlock(fn.input, env),
        suggestionPriority,
      )}
      />;
  }
  else {
    //  end of recursion - draw function name
    return <span className="functionName">
      &nbsp;&#119891;&#119899;&nbsp;
      <Editor
        state={fn}
        setState={setFn}
        onCancel={onFnCancel}
        suggestionPriority={
          (fnSuggestion: SuggestionType) => computePriority(
            fnSuggestion[2],             // suggestions will be for function
            evalEditorBlock(input, env), // input *may* be set
            suggestionPriority,          // priority function we get from parent block
          )}
        />
    </span>;
  }
}

function InputParams({ fn, setFn, input, setInput, onInputCancel, depth, errorDepth, suggestionPriority }) {
  const env = useContext(EnvContext);
  let nestedParams;
  if (fn.kind === "call") {
    // Nest input of nested function
    const {
      setFn         : setFnFn,
      setInput      : setFnInput,
    } = headlessCallBlock(setFn);
    nestedParams = <InputParams
      fn={fn.fn}
      setFn={setFnFn}
      input={fn.input}
      setInput={setFnInput}
      onInputCancel={() => {/*todo*/}}
      depth={depth+1}
      errorDepth={errorDepth}
      suggestionPriority={
        (inputSuggestion: SuggestionType) => computePriority(
          evalEditorBlock(fn.fn, env),
          inputSuggestion[2],
          suggestionPriority,
        )}
    />;
  }
  else {
    nestedParams = <></>;
  }
  const isOffending = depth === errorDepth;
  return <div className={"inputParam" + (isOffending ? " offending" : "")}>
    {nestedParams}
    <Editor
      state={input}
      setState={setInput}
      onCancel={onInputCancel}
      suggestionPriority={
        (inputSuggestion: SuggestionType) => computePriority(
          evalEditorBlock(fn, env), // fn *may* be set
          inputSuggestion[2],       // suggestions will be for input
          suggestionPriority,       // priority function we get from parent block
        )}
      />
  </div>;
  //   {(fn.kind === "call") &&
  //     // if the function we're calling is itself the result of a function call,
  //     // then we render its input parameter nested in our own input parameter box, which is way more readable

  //     // recurse:
  //     <NestedParams
  //       fn={fn}
  //       setFn={setFn}
  //       depth={depth}
  //       errorDepth={errorDepth}
  //       suggestionPriority={suggestionPriority}
  //     />
  //   }
  //   {/* Our own input */}
  //   <Editor
  //     state={input}
  //     setState={setInput}
  //     onCancel={onInputCancel}
  //     suggestionPriority={
  //       (inputSuggestion: SuggestionType) => computePriority(
  //         evalEditorBlock(fn, env), // fn *may* be set
  //         inputSuggestion[2],       // suggestions will be for input
  //         suggestionPriority,       // priority function we get from parent block
  //       )}
  //     />
  // </div>;
}

// function NestedParams({fn, setFn, depth, errorDepth, suggestionPriority}) {
//   const env = useContext(EnvContext);
//   const {
//     setFn         : setFnFn,
//     setInput      : setFnInput,
//   } = headlessCallBlock(setFn);
//   return <InputParams
//     fn={fn.fn}
//     setFn={setFnFn}
//     input={fn.input}
//     setInput={setFnInput}
//     onInputCancel={() => {/*todo*/}}
//     depth={depth+1}
//     errorDepth={errorDepth}
//     suggestionPriority={
//       (inputSuggestion: SuggestionType) => computePriority(
//         evalEditorBlock(fn.fn, env), // fn *may* be set
//         inputSuggestion[2],       // suggestions will be for input
//         suggestionPriority,       // priority function we get from parent block
//       )}
//   />;
// }