import {
  Img,
  Rect,
  Node,
  Video,
  makeScene2D,
  Txt,
  Line,
  LineSegment,
  NodeProps,
} from "@motion-canvas/2d";
import {
  Direction,
  beginSlide,
  createRef,
  map,
  slideTransition,
  tween,
  all,
  waitFor,
  range,
  makeRef,
} from "@motion-canvas/core";

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

import * as ts from "typescript";

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

  workingText?: string;
  idlingText?: string;

  arity?: number;

  isChild?: boolean;
}

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

/*
<Node ref={this.node} opacity={0}>
            <CodeBlock
              fontFamily={theme.font}
              language="typescript"
              ref={this.block}
              fontSize={1}
              code={this.source}
            ></CodeBlock>
          </Node>
 */

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<Node>();
  private readonly child = createRef<FunctionBox>();

  private readonly isChild: boolean;

  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 = ts.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.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"}
                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%"}
          gap={40}
        >
          <Txt fontFamily={theme.font} fill={theme.text.hex} ref={this.boxMoji}>
            {this.idlingText}
          </Txt>
        </Rect>

        <Rect direction={"column"} height={"100%"} alignItems={"end"}>
          <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}
            ></Rect>
          </Rect>
        </Rect>
      </Rect>,
    );
  }

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

  public *resetOutput(duration: number) {
    yield* all(
      this.output().opacity(0, duration),
      this.outputSegment().points([], duration),
    );
    yield 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} fontSize={30} fill={theme.text.hex}>
            {args[i].val.toString()}
          </Txt>
        ),
      );
    });
    yield* all(
      ...this.inputSegments.map((segment) =>
        segment.points(
          [
            { x: -this.padding, y: 0 },
            { x: -this.delta, y: 0 },
          ],
          duration,
        ),
      ),
      ...this.inputs.map((input) =>
        all(
          input.height(40, duration),
          input.width(40, duration),
          input.opacity(1, duration),
        ),
      ),
    );
  }

  public *propogateInput(duration: number) {
    const opacityChangeDuration = 0.1;

    yield* all(
      ...this.inputSegments.map((segment) =>
        segment.opacity(0.2, opacityChangeDuration),
      ),
    );

    yield* all(
      ...this.inputSegments.map((segment) => segment.points([], duration)),
    );

    yield* all(
      ...this.inputSegments.map((segment) =>
        segment.opacity(1, opacityChangeDuration),
      ),
      ...this.inputs.map((input) => input.opacity(0, opacityChangeDuration)),
      this.boxMoji().text(this.workingText, duration),
    );
  }

  public *propogateOutput(duration: number) {
    const output = this.function(...this.currentArgs.map((input) => input.val));
    if (typeof output === "function") {
      this.output().add(
        <FunctionBox
          opacity={0}
          isChild={true}
          ref={this.child}
          fn={output}
        ></FunctionBox>,
      );
    } else {
      this.output().add(
        <Txt fontFamily={theme.font} fontSize={30} fill={theme.text.hex}>
          {output.toString()}
        </Txt>,
      );
    }

    yield* this.boxMoji().text(this.idlingText, duration);

    yield* all(
      this.outputSegment().points(
        [
          { x: -this.delta, y: 0 },
          { x: this.padding, y: 0 },
        ],
        duration,
      ),
      this.child()?.opacity(1, duration),
      this.output().opacity(1, duration),
      this.outputSegment().opacity(1, duration),
    );
  }
}
