/* eslint-disable eqeqeq */
import React, { useState } from "react";
import "./App.css";

export default function App() {
    let [input, setInput] = useState("");
    let [activeStateIdx, setActiveStateIdx] = useState(0);
    let parsed = somes(input.split(/[^a-z0-9-]+/).map(parse_op));
    let states = steps(INITIAL_STATE, parsed);
    return (
        <>
            <textarea
                onChange={e => setInput(e.target.value)}
                value={input}
                className="program"
                title={`\
type Op =
    | number
    | "add"
    | "sub"
    | "mul"
    | "div"
    | "put"
    | "jmp"
    | "dup"
    | "del"
    | "cmp"
    | "swp";`}
            ></textarea>
            <div className="info">
                <Ops
                    parsed={parsed}
                    ip={(states[activeStateIdx] ?? INITIAL_STATE).ip}
                    // // hack
                    // top={document.getElementById("input")?.clientHeight ?? 0}
                />
                <States
                    states={states}
                    activeStateIdx={activeStateIdx}
                    setActiveStateIdx={n => setActiveStateIdx(n)}
                />
            </div>
        </>
    );
}

function Ops({
    parsed,
    ip,
}: // top,
{
    parsed: Op[];
    ip: number;
    // top: number;
}): JSX.Element {
    return (
        <div
            className="ops"
            children={parsed.map((op, i) => (
                <Op op={op} highlighted={i == ip} num={i} key={i} />
            ))}
            // style={{
            //     top: `${top + 5}px`,
            // }}
        />
    );
}

function Op({
    op,
    highlighted,
    num,
    ...props
}: {
    op: Op;
    num: number;
    highlighted: boolean;
}): JSX.Element {
    return (
        <div
            className={`op ${highlighted ? "active" : ""}`}
            key={num}
            {...props}
        >
            <div className="op-num">{num}: </div>
            {op}
        </div>
    );
}

function States({
    states,
    activeStateIdx,
    setActiveStateIdx,
}: {
    states: State[];
    activeStateIdx: number;
    setActiveStateIdx: (_: number) => any;
}): JSX.Element {
    return (
        <div
            className="states"
            children={states.map((state, i) => (
                <State
                    state={state}
                    key={i}
                    active={activeStateIdx == i}
                    onMouseEnter={() => setActiveStateIdx(i)}
                />
            ))}
        />
    );
}

function State({
    state,
    active,
    onMouseEnter,
    ...props
}: {
    state: State;
    active: boolean;
    onMouseEnter: () => any;
}): JSX.Element {
    return (
        <div
            className={`state ${active ? "active" : ""}`}
            onMouseEnter={onMouseEnter}
            {...props}
        >
            <div className="ip">Ip: {state.ip}</div>
            <span>Stack:</span> <Stack stack={state.stack} />
            <span>Output:</span> <Output output={state.output} />
        </div>
    );
}

function Stack({ stack }: { stack: number[] }): JSX.Element {
    let ns = [...stack];
    ns.reverse();
    return (
        <div
            className="stack"
            children={ns.map((item, i) => (
                <StackItem item={item} key={i} />
            ))}
        />
    );
}

function StackItem({ item, ...props }: { item: number }): JSX.Element {
    return (
        <div className="stack-item" {...props}>
            {item}
        </div>
    );
}

// bad
function Output({ output }: { output: number[] }): JSX.Element {
    return (
        <div
            className="output"
            children={output.map((item, i) => (
                <StackItem item={item} key={i} />
            ))}
        ></div>
    );
}

type Option<T> = T | undefined;

function is_some<T>(a: Option<T>): a is T {
    return typeof a != "undefined";
}

function is_none<T>(a: Option<T>): a is undefined {
    return typeof a == "undefined";
}

function cons<T>(x: T, xs: T[]): T[] {
    return [x, ...xs];
}

function somes<T>(os: Option<T>[]): T[] {
    if (os.length == 0) {
        return [];
    } else {
        let first = os[0];
        if (is_some(first)) {
            return cons(first, somes(os.slice(1)));
        } else {
            return somes(os.slice(1));
        }
    }
}

type Op =
    | number
    | "add"
    | "sub"
    | "mul"
    | "div"
    | "put"
    | "jmp"
    | "dup"
    | "del"
    | "cmp"
    | "swp";

function parse_int(n: string, radix?: number): Option<number> {
    let maybe_int = parseInt(n, radix);
    // eslint-disable-next-line no-self-compare
    if (maybe_int == maybe_int) {
        return maybe_int;
    }
}

function parse_op(op: string): Option<Op> {
    if (parse_int(op)) {
        return parse_int(op);
    }
    switch (op) {
        case "add":
        case "sub":
        case "mul":
        case "div":
        case "put":
        case "jmp":
        case "dup":
        case "del":
        case "cmp":
        case "swp":
            return op;
    }
}

type State = {
    ip: number;
    stack: number[];
    output: number[];
};

const INITIAL_STATE: State = {
    ip: 0,
    stack: [],
    output: [],
};

function steps(state: State, ops: Op[], limit = 100, num_steps = 0): State[] {
    if (!ops[state.ip] || num_steps > limit) {
        return [state];
    }
    return cons(
        state,
        steps(step(state, ops[state.ip]), ops, limit, num_steps + 1)
    );
}

// all but jmp advance the ip by one
// cmp: lt => -1, eq => 0, gt => 1
function step(state: State, op: Op): State {
    let ns = JSON.parse(JSON.stringify(state));
    ns.ip++;

    function binary_op(
        default_a: number,
        default_b: number,
        f: (a: number, b: number) => number
    ) {
        let [b, a] = [ns.stack.pop() ?? default_b, ns.stack.pop() ?? default_a];
        ns.stack.push(f(a, b));
    }

    if (typeof op == "number") {
        ns.stack.push(op);
    } else {
        switch (op) {
            case "add":
                binary_op(0, 0, (a, b) => a + b);
                break;
            case "sub":
                binary_op(0, 0, (a, b) => a - b);
                break;
            case "mul":
                binary_op(0, 1, (a, b) => a * b);
                break;
            case "div":
                binary_op(0, 1, (a, b) => a / b);
                break;
            case "put": {
                let popped = ns.stack.pop();
                if (typeof popped != "undefined") {
                    ns.output.push(popped);
                }
                break;
            }
            case "jmp":
                ns.ip--;
                ns.ip += ns.stack.pop() ?? 1;
                break;
            case "dup": {
                let popped = ns.stack.pop();
                if (typeof popped != "undefined") {
                    ns.stack.push(popped, popped);
                }
                break;
            }
            case "del":
                ns.stack.pop();
                break;
            case "cmp": {
                let [a, b] = [ns.stack.pop() ?? 0, ns.stack.pop() ?? 0];
                // matter of taste
                ns.stack.push(cmp(b, a));
                break;
            }
            case "swp": {
                let [a, b] = [ns.stack.pop(), ns.stack.pop()];
                if (typeof a == "undefined" || typeof b == "undefined") {
                    break;
                }
                ns.stack.push(a, b);
                break;
            }
        }
    }

    return ns;
}

function cmp(a: number, b: number): number {
    if (a < b) {
        return -1;
    } else if (a == b) {
        return 0;
    } else {
        return 1;
    }
}
