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

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

import { useEffect } from "react";
import "./CallBlock.css";

type ResolvedType = Dynamic | Error | undefined;

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

interface CallBlockProps<
  FnState=EditorState,
  InputState=EditorState,
> extends State2Props<CallBlockState<FnState,InputState>,EditorState> {
}


function have(resolved: ResolvedType) {
  return resolved && !(resolved instanceof Error);
}

function headlessCallBlock({state, setState}: CallBlockProps) {
  const {fn, input} = state;
  const setFn = (callback: SetStateFn) => {
    setState(state => ({...state, fn: callback(state.fn)}));
  }
  const setInput = (callback: SetStateFn) => {
    setState(state => ({...state, input: callback(state.input)}));
  }
  const setResolved = (callback: SetStateFn<ResolvedType>) => {
    setState(state => ({...state, resolved: callback(state.resolved)}));
  }
  useEffect(() => {
    // Here we do something spooky: we update the state in response to state change...
    // The reason this shouldn't give problems is because we update the state in such a way that the changes only 'trickle up', rather than getting stuck in a cycle.
    if (have(input.resolved) && have(fn.resolved)) {
      try {
        const outputResolved = apply(input.resolved)(fn.resolved); // may throw
        setResolved(() => outputResolved); // success
      }
      catch (e) {
        if (!(e instanceof UnifyError) && !(e instanceof NotAFunctionError)) {
          throw e;
        }
        setResolved(() => e as Error); // eval error
      }
    }
    else if (input.resolved instanceof Error) {
      setResolved(() => input.resolved); // bubble up the error
    }
    else if (fn.resolved instanceof Error) {
      // @ts-ignore
      setResolved(() => {
        // @ts-ignore
        return Object.assign(fn.resolved, {depth: (fn.resolved.depth || 0) + 1});
      }); // bubble up the error
    }
    else {
      // no errors and at least one is undefined:
      setResolved(() => undefined); // chill out
    }
  }, [input.resolved, fn.resolved]);
  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 }: CallBlockProps) {
  const {setFn, setInput, onFnCancel, onInputCancel}
    = headlessCallBlock({ state, setState });


  // @ts-ignore
  console.log('depth:', state.resolved?.depth);
  
  return <span className={"functionBlock" + ((state.resolved instanceof Error) ? " unifyError" : "")}>
    <FunctionHeader
      fn={state.fn}
      setFn={setFn}
      onFnCancel={onFnCancel}
      input={state.input} />
    <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}
          // @ts-ignore
          errorDepth={state.resolved instanceof Error ? (state.resolved.depth || 0) : -1}
          />
        {/* Output (or Error) */}
        { state.resolved instanceof Error && state.resolved.toString()
        || state.resolved && <><Value dynamic={state.resolved} />&#x2611;</>}
      </div>
    </div>
  </span>;
}

function filterFnInputs(fn: Dynamic|Error|undefined, input: Dynamic|Error|undefined) {
  if (!have(fn) || !have(input)) {
    return false;
  }
  const fnType = getType(fn);
  if (getSymbol(fnType) !== symbolFunction) {
    return false; // filter out non-functions already
  }
  try {
    assignFn(fnType, getType(input)); // may throw
    return true;
  } catch (e) {
    if (!(e instanceof UnifyError)) {
      throw e;
    }
    return false;
  }
}

function FunctionHeader({ fn, setFn, input, onFnCancel }) {
  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({state: fn, setState: setFn});

    return <FunctionHeader
      fn={fn.fn}
      setFn={setFnFn}
      onFnCancel={onFnFnCancel}
      input={fn.input} />;
  }
  else {
    //  end of recursion - draw function name
    return <span className="functionName">
      &nbsp;&#119891;&#119899;&nbsp;
      <Editor
        state={fn}
        setState={setFn}
        onCancel={onFnCancel}
        filter={([_, fnCandidate]) => filterFnInputs(fnCandidate, input.resolved)} />
    </span>;
  }
}

function InputParams({ fn, setFn, input, setInput, onInputCancel, depth, errorDepth }) {
  return <div className={"inputParam" + (depth === errorDepth ? " offending" : "")}>
    {(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}/>
    }
    {/* Our own input */}
    <Editor
      state={input}
      setState={setInput}
      onCancel={onInputCancel}
      filter={([_, inputCandidate]) => filterFnInputs(fn.resolved, inputCandidate)}
      />
  </div>;
}

function NestedParams({fn, setFn, depth, errorDepth}) {
  const {
      setFn         : setFnFn,
      setInput      : setFnInput,
    } = headlessCallBlock({state: fn, setState: setFn});
  return <InputParams
    fn={fn.fn}
    setFn={setFnFn}
    input={fn.input}
    setInput={setFnInput}
    onInputCancel={() => {/*todo*/}}
    depth={depth+1}
    errorDepth={errorDepth}
    />;
}