import { useState } from "react";

import { apply, UnifyError, assignFn, getType, getSymbol, symbolFunction } from "dope2";

import { Editor, type EditorState } from "./Editor";
import { Value } from "./Value";
import type { Dynamic, State2Props } from "./util/extra";

import "./CallBlock.css";

export interface CallBlockState<
  FnState=EditorState,
  InputState=EditorState,
> {
  kind: "call";
  env: any;
  fn: FnState;
  input: InputState;
  resolved: undefined | Dynamic;
  // focus: boolean;
}

interface CallBlockProps<
  FnState=EditorState,
  InputState=EditorState,
> extends State2Props<CallBlockState<FnState,InputState>> {
  onResolve: (resolved: EditorState) => void;
}

function headlessCallBlock({state, setState, onResolve}: CallBlockProps) {
  const [unifyError, setUnifyError] = useState<UnifyError | undefined>(undefined);
  const {fn, input } = state;
  const setFn = (fn: EditorState) => {
    setState({...state, fn});
  }
  const setInput = (input: EditorState) => {
    setState({...state, input});
  }
  const setResolved = (resolved?: Dynamic) => {
    setState({...state, resolved});
  }
  const makeTheCall = (input, fn) => {
    try {
      const outputResolved = apply(input.resolved)(fn.resolved);
      setResolved(outputResolved);
      onResolve({
        ...state, resolved: outputResolved
      });
      setUnifyError(undefined);
    }
    catch (e) {
      if (!(e instanceof UnifyError)) {
        throw e;
      }
      setUnifyError(e);
      onResolve({
        ...state, resolved: undefined
      })
    }
  };
  const onFnResolve = (fnState) => {
    if (input.resolved) {
      makeTheCall(input, fnState);
    }
    else {
      // setFn(fnState);
      setResolved(undefined);
      onResolve({
        ...state, resolved: undefined
      });
    }
  };
  const onInputResolve = (inputState) => {
    if (fn.resolved) {
      makeTheCall(inputState, fn);
    }
    else {
      setResolved(undefined);
      onResolve({
        ...state, resolved: undefined
      });
    }
  };
  const onFnCancel = () => {
    setState(input);
  }
  const onInputCancel = () => {
    setState(fn);
  }
  return {unifyError, setFn, setInput, onFnResolve, onInputResolve, onFnCancel, onInputCancel};
}

export function CallBlock({ state, setState, onResolve }: CallBlockProps) {
  const {unifyError, setFn, setInput, onFnResolve, onInputResolve, onInputCancel}
    = headlessCallBlock({ state, setState, onResolve });
  return <span className={"functionBlock" + (unifyError ? " unifyError" : "")}>
    <FunctionHeader
      fn={state.fn}
      setFn={setFn}
      onFnResolve={onFnResolve}
      input={state.input} />
    <div className="functionParams">
      <div className="outputParam">
        {/* Sequence of input parameters */}
        <InputParams
          fn={state.fn}       setFn={setFn}
          input={state.input} setInput={setInput}
          onFnResolve={onFnResolve}
          onInputResolve={onInputResolve} onInputCancel={onInputCancel} />
          
        {/* Output (or Error) */}
        {state.resolved && <Value dynamic={state.resolved} />}
        {unifyError && unifyError.toString()}
      </div>
    </div>
  </span>;
}

function FunctionHeader({ fn, setFn, input, onFnResolve }) {
  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 {
      onFnResolve   : onFnFnResolve
    } = headlessCallBlock({state: fn, setState: setFn, onResolve: onFnResolve});

    return <FunctionHeader
      fn={fn.fn}
      setFn={fnFn => setFn({...fn, fn: fnFn})}
      onFnResolve={onFnFnResolve}
      input={fn.input} />;
  }
  else {
    const filterCompatibleFns = ([_name, dynamic]: [string, Dynamic]) => {
      if (input.resolved) {
        try {
          const type = getType(dynamic);
          if (getSymbol(type) !== symbolFunction) {
            return false;
          }
          assignFn(type, getType(input.resolved));
        } catch (e) {
          if (!(e instanceof UnifyError)) {
            throw e;
          }
          return false;
        }  
      }
      return true;
    }
  
    //  end of recursion - draw function name
    return <div className="functionName">
      &#119891;&#119899;&nbsp;
      <Editor
        state={fn}
        setState={setFn}
        onResolve={onFnResolve}
        onCancel={() => {/*todo*/}}
        filter={filterCompatibleFns} />
    </div>;
  }
}

function InputParams({ fn, setFn, input, setInput, onFnResolve, onInputResolve, onInputCancel }) {
  const filterCompatibleInputs = ([_name, dynamic]: [string, Dynamic]) => {
    if (fn.resolved) {
      try {
        assignFn(getType(fn.resolved), getType(dynamic));
      } catch (e) {
        if (!(e instanceof UnifyError)) {
          throw e;
        }
        return false;
      }  
    }
    return true;
  }
  return <div className="inputParam">
    {(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

      // Input(s) of the function we're calling:
      <NestedInputParams fn={fn} setFn={setFn} onFnResolve={onFnResolve} />
    }
    {/* Our own input */}
    <Editor
      state={input}
      setState={setInput}
      onResolve={onInputResolve}
      onCancel={onInputCancel}
      filter={filterCompatibleInputs}
      />
  </div>;
}

function NestedInputParams({fn, setFn, onFnResolve}) {
  const {
    onInputResolve: onFnInputResolve,
    onFnResolve   : onFnFnResolve,
  } = headlessCallBlock({state: fn, setState: setFn, onResolve: onFnResolve});
  return <InputParams
    fn={fn.fn}
    setFn={fnFn => setFn({...fn, fn: fnFn})}
    input={fn.input}
    setInput={fnInput => setFn({...fn, input: fnInput})}
    onFnResolve={onFnFnResolve}
    onInputResolve={onFnInputResolve}
    onInputCancel={() => {/*todo*/}}
    />;
}
