import { Rect, Node, Txt, Line, NodeProps } from "@motion-canvas/2d";
import { createRef, all, range, makeRef } from "@motion-canvas/core";

import { CodeBlock } from "@motion-canvas/2d/lib/components/CodeBlock";
import { theme } from "../theme";

import { transpile } from "typescript";

export interface FunctionBoxProps extends NodeProps {
  source?: string;
  fn?: Function;
  padding?: number;
  delta?: number;

  inputFontSize?: number;
  outputFontSize?: number;

  workingText?: string;
  idlingText?: string;

  arity?: number;

  isChild?: boolean;
}

type FunctionArgs = { node?: Node; val: any }[];

export class FunctionBox extends Node {
  private readonly source: string;
  private readonly workingText: string;
  private readonly idlingText: string;
  private readonly function: any;
  private readonly delta: number;
  private readonly arity: number;
  private readonly padding: number;

  private readonly block = createRef<CodeBlock>();
  private readonly node = createRef<Node>();
  private readonly rect = createRef<Rect>();

  private readonly boxMoji = createRef<Txt>();
  private readonly inputSegments: Line[] = [];
  private readonly inputs: Rect[] = [];
  private readonly outputSegment = createRef<Line>();
  private readonly output = createRef<Rect>();
  private readonly child = createRef<FunctionBox>();

  private readonly isChild: boolean;

  private readonly inputFontSize: number;
  private readonly outputFontSize: number;

  private currentArgs: FunctionArgs = [];

  public constructor(props?: FunctionBoxProps) {
    super({
      ...props,
    });

    this.arity = props?.arity ?? 1;
    if (props.fn) {
      this.source = props.fn.toString();
      this.function = props.fn;
    } else {
      this.source = props?.source ?? `(x: number): number => x + 2`;

      const functionCode = transpile(this.source);
      this.function = eval(functionCode);
    }

    this.delta = props?.delta ?? 20;
    this.padding = props?.padding ?? 100;

    this.workingText = props?.workingText ?? "👷‍♀️⚙️";
    this.idlingText = props?.idlingText ?? "😴";

    this.isChild = props?.isChild ?? false;

    this.outputFontSize = props?.outputFontSize ?? 30;
    this.inputFontSize = props?.inputFontSize ?? 30;

    this.add(
      <Rect
        opacity={this.opacity}
        direction={"row"}
        alignItems={"center"}
        layout
      >
        <Rect direction={"column"} alignItems={"end"}>
          {range(this.arity).map((i) => (
            <Rect direction={"row"} alignItems={"center"} gap={10}>
              <Rect
                direction={"row"}
                fontSize={0}
                ref={makeRef(this.inputs, i)}
                justifyContent={"end"}
                opacity={1}
              ></Rect>

              <Line
                points={[]}
                stroke={theme.green.hex}
                ref={makeRef(this.inputSegments, i)}
                lineWidth={5}
                arrowSize={10}
                endArrow
              ></Line>
            </Rect>
          ))}
        </Rect>

        <Rect
          ref={this.rect}
          radius={4}
          stroke={theme.overlay0.hex}
          fill={theme.crust.hex}
          lineWidth={4}
          padding={60}
          direction={"row"}
          height={"100%"}
        >
          <Txt fontFamily={theme.font} fill={theme.text.hex} ref={this.boxMoji}>
            {this.idlingText}
          </Txt>
          <Node ref={this.node} opacity={0}>
            <CodeBlock
              fontFamily={theme.font}
              language="typescript"
              ref={this.block}
              fontSize={0}
              code={this.source}
            ></CodeBlock>
          </Node>
        </Rect>

        <Rect
          direction="column"
          height={"100%"}
          alignItems={"end"}
          justifyContent="center"
        >
          <Rect direction="row" alignItems="center" gap={10}>
            <Line
              points={[]}
              stroke={theme.red.hex}
              lineWidth={5}
              arrowSize={10}
              ref={this.outputSegment}
              endArrow
            ></Line>
            <Rect
              direction={"row"}
              ref={this.output}
              justifyContent={"end"}
              opacity={1}
              fontSize={0}
            ></Rect>
          </Rect>
        </Rect>
      </Rect>,
    );
  }

  public *resetInput(duration: number) {
    yield* all(
      ...this.inputs.map((x) =>
        all(x.opacity(0, duration), x.fontSize(0, duration)),
      ),
      ...this.inputSegments.map((segment) =>
        all(segment.points([], duration), segment.opacity(0, duration)),
      ),
    );

    this.inputs.forEach((x) => x.removeChildren());
  }

  public *resetOutput(duration: number) {
    yield* all(
      this.output().opacity(0, duration),
      this.output().fontSize(0, duration),
      this.outputSegment().points([], duration),
      this.outputSegment().opacity(0, duration),
    );

    this.output().removeChildren();
  }

  public *reset(duration: number) {
    yield* all(this.resetInput(duration), this.resetOutput(duration));
  }

  public *setInputs(args: FunctionArgs, duration: number) {
    if (args.length != this.arity)
      throw new Error("input length must equal function arity");
    this.currentArgs = args;

    this.inputs.forEach((input, i) => {
      input.removeChildren();
      input.add(
        args[i].node ?? (
          <Txt fontFamily={theme.font} fill={theme.text.hex}>
            {args[i].val.toString()}
          </Txt>
        ),
      );
    });
    yield* all(
      ...this.inputSegments.map((segment) =>
        all(
          segment.points(
            [
              { x: -this.padding, y: 0 },
              { x: -this.delta, y: 0 },
            ],
            duration,
          ),
          segment.opacity(1, duration),
        ),
      ),
      ...this.inputs.map((input) =>
        all(
          input.opacity(1, duration),
          input.fontSize(this.inputFontSize, duration),
        ),
      ),
    );
  }

  public *propogateInput(duration: number) {
    yield* all(
      ...this.inputSegments.map((segment) =>
        all(segment.opacity(0.2, duration), segment.points([], duration)),
      ),
    );
    yield* all(
      this.resetInput(duration),
      ...this.inputs.map((input) =>
        all(input.opacity(0.2, duration), input.fontSize(0, duration)),
      ),
      this.boxMoji().text(this.workingText, duration),
    );
  }

  public *propogateOutput(duration: number) {
    yield* all(
      this.child()?.opacity(0.2, duration),
      this.output().opacity(0.2, duration),
      this.outputSegment().opacity(0, duration),
    );

    const output = this.function(...this.currentArgs.map((input) => input.val));
    switch (typeof output) {
      case "function":
        yield this.output().add(
          <FunctionBox
            opacity={0}
            isChild={true}
            ref={this.child}
            fn={output}
          ></FunctionBox>,
        );
        break;
      case "number":
      case "string":
        yield this.output().add(
          <Txt fontFamily={theme.font} fill={theme.text.hex}>
            {output.toString()}
          </Txt>,
        );
        break;
      default:
        yield this.output().add(
          <CodeBlock
            fontFamily={theme.font}
            language="typescript"
            fontSize={15}
            code={JSON.stringify(output, null, 2)}
          ></CodeBlock>,
        );
    }

    yield* all(
      this.boxMoji().text(this.idlingText, duration),
      this.output().fontSize(this.outputFontSize, duration),
      this.outputSegment().points(
        [
          { x: 0, y: 0 },
          { x: this.padding, y: 0 },
        ],
        duration,
      ),
      this.outputSegment().opacity(1, duration),
      this.output().fontSize(this.outputFontSize, duration),
      this.child()?.opacity(1, duration),
      this.output().opacity(1, duration),
    );
  }

  public *showCode(duration: number) {
    yield* all(
      this.boxMoji().text("", duration),
      this.boxMoji().opacity(0, duration),
      this.block().fontSize(30, duration),
      this.node().opacity(1, duration),
    );
  }

  public *hideCode(duration: number) {
    yield* this.boxMoji().text(this.idlingText, 0);
    yield* all(
      this.block().fontSize(0, duration),
      this.node().opacity(0, duration),
      this.boxMoji().opacity(1, duration),
    );
  }

  public getChild() {
    return this.child;
  }
}
