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

export default function App() {
    let [hash, setHash] = useHash();

    let input = decodeURI(hash.slice(1));
    function setInput(s: string) {
        setHash(encodeURI(s));
    }

    let [activeStateIdx, setActiveStateIdx] = useState(0);
    let parsed = somes(input.split(/[^a-z0-9-]+/).map(parse_op));
    let states = steps(INITIAL_STATE, parsed);
    let state = states[activeStateIdx] ?? INITIAL_STATE;

    return (
        <>
            <TextArea
                onChange={(e: any) => 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}
                    state={state}
                    // // hack
                    // top={document.getElementById("input")?.clientHeight ?? 0}
                />
                <Stack state={state} just_popped={state.just_popped} />
                <States
                    states={states}
                    activeStateIdx={activeStateIdx}
                    setActiveStateIdx={n => setActiveStateIdx(n)}
                />
            </div>
        </>
    );
}

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

function Op({
    op,
    highlighted,
    num,
    jumped_from,
    ...props
}: {
    op: Op;
    num: number;
    highlighted: boolean;
    jumped_from: boolean;
}): JSX.Element {
    return (
        <div
            className={`op ${highlighted ? "active" : ""} ${
                jumped_from ? "jumped-from" : ""
            }`}
            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}
            onFocus={onMouseEnter}
            onClick={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({
    state,
    just_popped,
    ...props
}: {
    state: State;
    just_popped?: number[];
}): JSX.Element {
    let ns = [...state.stack];
    ns.reverse();
    console.log(just_popped);
    let children = (just_popped ?? []).map((just_popped, i) => (
        <StackItem
            item={just_popped}
            key={-(i + 1)}
            highlighted={false}
            just_popped={true}
        />
    ));
    ns.map((item, i) => (
        <StackItem
            item={item}
            key={i}
            highlighted={
                (state.just_pushed && i == 0) || (state.just_swapped && i < 2)
            }
        />
    )).forEach(child => children.push(child));

    return <div className="stack" children={children} {...props} />;
}

function StackItem({
    item,
    highlighted = false,
    just_popped = false,
    ...props
}: {
    item: number;
    highlighted: boolean;
    just_popped?: boolean;
}): JSX.Element {
    return (
        <div
            className={`stack-item ${highlighted ? "active" : ""} ${
                just_popped ? "just-popped" : ""
            }`}
            {...props}
        >
            {item}
        </div>
    );
}

// bad
function Output({ output }: { output: number[] }): JSX.Element {
    return (
        <div
            className="output"
            children={output.map((item, i) => (
                <span key={i}>{String.fromCharCode(item)}</span>
            ))}
        ></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> {
    let maybe_int = parse_int(op);
    if (is_some(maybe_int)) {
        return maybe_int;
    }
    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[];
    jumped_from: Option<number>;
    just_pushed: boolean;
    just_swapped: boolean;
    just_popped?: number[];
};

const INITIAL_STATE: State = {
    ip: 0,
    stack: [],
    output: [],
    jumped_from: undefined,
    just_pushed: false,
    just_swapped: false,
};

function steps(state: State, ops: Op[], limit = 300, num_steps = 0): State[] {
    if (!(state.ip in ops) || 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)) as State;
    ns.jumped_from = ns.ip;
    ns.just_pushed = false;
    ns.just_swapped = false;
    ns.just_popped = undefined;
    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);
    }

    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.jumped_from = 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.just_swapped = true;
            ns.stack.push(a, b);
            break;
        }
    }

    if (ns.stack.length > state.stack.length) {
        ns.just_pushed = true;
    } else if (ns.stack.length < state.stack.length) {
        ns.just_popped = state.stack.slice(ns.stack.length - 1).reverse();
        if (state.stack.length - ns.stack.length == 1) {
            ns.just_pushed = true;
        }
    }

    return ns;
}

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