import { R2PipeSync } from "./r2pipe.js";

declare let console: any;
declare let r2: R2PipeSync;

// ("this is just a comment"), -- comments are also part of the runtime

/*
=("//", {
	=(obj, {}())
	=([obj, comment], 32)
	if(eq([obj,comment], 32), 
		ret()
	)
	ret(obj)
})
*/

export class EsilToken {
    label: string = "";
    comment: string = "";
    text: string = "";
    addr: string = "0"; // for ut64 we use strings for numbers :<
    position: number = 0;
    constructor(text: string = "", position: number = 0) {
        this.text = text;
        this.position = position;
    }
    toString(): string {
        return this.text;
    }
}

export type EsilNodeType =
    | "number"
    | "flag"
    | "register"
    | "operation"
    | "none"
    | "block"
    | "goto"
    | "label";

export class EsilNode {
    lhs: EsilNode | undefined;
    rhs: EsilNode | undefined;
    children: EsilNode[];
    token: EsilToken;
    type: EsilNodeType = "none";
    constructor(
        token: EsilToken = new EsilToken(),
        type: EsilNodeType = "none"
    ) {
        this.token = token;
        this.children = [];
    }
    setSides(lhs: EsilNode, rhs: EsilNode): void {
        this.lhs = lhs;
        this.rhs = rhs;
    }
    addChildren(ths: EsilNode, fhs: EsilNode): void {
        if (ths !== undefined) {
            this.children.push(ths);
        }
        if (fhs !== undefined) {
            this.children.push(fhs);
        }
    }
    toEsil(): string {
        if (this.lhs !== undefined && this.rhs !== undefined) {
            // XXX handle ?{ }{ }
            let left = this.lhs.toEsil();
            if (left !== "") {
                left += ",";
            }
            const right = this.rhs.toEsil();
            return `${right},${left}${this.token}`;
        }
        return ""; // this.token.text;
    }
    toString(): string {
        let str = "";
        if (this.token.label !== "") {
            str += this.token.label + ":\n";
        }
        if (this.token.addr !== "0") {
            // str += "//  @ " + this.token.addr + "\n";
        }
        if (this.token.comment !== "") {
            str += "/*" + this.token.comment + "*/\n";
        }
        if (this.token.toString() === "GOTO") {
            if (this.children.length > 0) {
                const children = this.children[0];
                str += "goto label_" + children.token.position + ";\n";
            } else {
                // console.log(JSON.stringify(this,null, 2));
                const pos = 0;
                str += `goto label_${pos};\n`;
            }
        }
        if (this.children.length > 0) {
            str += `  (if (${this.rhs})\n`;
            for (const children of this.children) {
                if (children !== null) {
                    const x = children.toString();
                    if (x != "") {
                        str += `  ${x}\n`;
                    }
                }
            }
            str += "  )\n";
        }
        if (this.lhs !== undefined && this.rhs !== undefined) {
            return str + `    ( ${this.lhs} ${this.token} ${this.rhs} )`;
            // return str + `${this.lhs} ${this.token} ${this.rhs}`;
        }
        return str + this.token.toString();
    }
}

export class EsilParser {
    r2: R2PipeSync;
    stack: EsilNode[]; // must be a stack or a list.. to parse sub expressions we must reset
    nodes: EsilNode[];
    root: EsilNode;
    tokens: EsilToken[];
    cur: number = 0;

    constructor(r2: R2PipeSync) {
        this.r2 = r2;
        this.cur = 0;
        this.stack = [];
        this.nodes = [];
        this.tokens = [];
        this.root = new EsilNode(new EsilToken("function", 0), "block");
    }
    toJSON(): string {
        if (this.stack.length > 0) {
            // return JSON.stringify (this.stack, null, 2);
            throw new Error("The ESIL stack is not empty");
        }
        return JSON.stringify(this.root, null, 2);
    }
    toEsil(): string {
        return this.nodes.map((x) => x.toEsil()).join(",");
    }
    private optimizeFlags(node: EsilNode) {
        if (node.rhs !== undefined) {
            this.optimizeFlags(node.rhs);
        }
        if (node.lhs !== undefined) {
            this.optimizeFlags(node.lhs);
        }
        for (let i = 0; i < node.children.length; i++) {
            this.optimizeFlags(node.children[i]);
        }
        const addr: string = node.toString();
        if (+addr > 4096) {
            const cname = r2.cmd(`fd.@ ${addr}`);
            const fname = cname.trim().split("\n")[0].trim();
            if (fname != "" && fname.indexOf("+") === -1) {
                node.token.text = fname;
            }
        }
    }
    optimize(options: string): void {
        if (options.indexOf("flag") != -1) {
            this.optimizeFlags(this.root);
        }
    }
    toString(): string {
        return this.root.children.map((x) => x.toString()).join(";\n");
    }
    reset(): void {
        this.nodes = [];
        this.stack = [];
        this.tokens = [];
        this.cur = 0;
        this.root = new EsilNode(new EsilToken("function", 0), "block");
    }
    parseRange(from: number, to: number) {
        let pos = from;
        while (pos < this.tokens.length && pos < to) {
            const token = this.peek(pos);
            if (!token) {
                // console.log("BREAK");
                break;
            }
            // console.log(pos, token);
            this.cur = pos;
            this.pushToken(token);
            pos = this.cur;
            pos++;
        }
        // console.log("done");
    }
    parseFunction(addr?: string): void {
        const ep = this;
        function parseAmount(n: number): void {
            // console.log("PDQ "+n);
            const output = r2.cmd("pie " + n + " @e:scr.color=0");
            const lines = output.trim().split("\n");
            for (const line of lines) {
                if (line.length === 0) {
                    console.log("Empty");
                    continue;
                }
                // console.log("parse", r2.cmd("?v:$$"));
                const kv = line.split(" ");
                if (kv.length > 1) {
                    // line != "") {
                    // console.log("// @ " + kv[0]);
                    //ep.reset ();
                    r2.cmd(`s ${kv[0]}`);
                    ep.parse(kv[1], kv[0]);
                    ep.optimize("flags,labels");
                    //console.log(ep.toString());
                }
            }
            // console.log(ep.toString());
        }
        const oaddr = (r2.cmd("?v $$")).trim();
        // const func = r2.cmdj("pdrj"); // XXX this command changes the current seek
        if (addr === undefined) {
            addr = oaddr;
        }
        const bbs = r2.cmdj(`afbj@${addr}`); // XXX this command changes the current seek
        for (const bb of bbs) {
            // console.log("bb_" + bb.addr + ":");
            r2.cmd(`s ${bb.addr}`);
            parseAmount(bb.ninstr);
        }
        r2.cmd(`s ${oaddr}`);
    }
    parse(expr: string, addr?: string): void | never {
        const tokens = expr
            .trim()
            .split(",")
            .map((x) => x.trim());
        const from = this.tokens.length;
        for (const tok of tokens) {
            const token = new EsilToken(tok, this.tokens.length);
            if (addr !== undefined) {
                token.addr = addr;
            }
            this.tokens.push(token);
        }
        const to = this.tokens.length;
        this.parseRange(from, to);
    }

    peek(a: number): EsilToken | undefined {
        return this.tokens[a];
    }

    pushToken(tok: EsilToken): void | never {
        if (this.isNumber(tok)) {
            const node = new EsilNode(tok, "number");
            this.stack.push(node);
            this.nodes.push(node);
        } else if (this.isInternal(tok)) {
            const node = new EsilNode(tok, "flag");
            this.stack.push(node);
            this.nodes.push(node);
        } else if (this.isOperation(tok)) {
            // run the operation login
        } else {
            // assume it's a register, so just push the string
            const node = new EsilNode(tok, "register");
            this.stack.push(node);
            this.nodes.push(node);
        }
        // we need a list of register names to do this check properly
        // throw new Error ("Unknown token");
    }
    private isNumber(expr: EsilToken): boolean {
        if (expr.toString().startsWith("0")) {
            return true;
        }
        return +expr > 0;
    }
    private isInternal(expr: EsilToken): boolean {
        const text = expr.toString();
        return text.startsWith("$") && text.length > 1;
    }
    private parseUntil(start: number): EsilNode | null {
        const from = start + 1;
        let pos = from;
        const origStack: any[] = [];
        const this_nodes_length = this.nodes.length;
        this.stack.forEach((x) => origStack.push(x));
        while (pos < this.tokens.length) {
            const token = this.peek(pos);
            if (!token) {
                break;
            }
            if (token.toString() === "}") {
                break;
            }
            if (token.toString() === "}{") {
                // return token;
                break;
            }
            // console.log("peek ", this.tokens[pos]);
            pos++;
        }
        this.stack = origStack;
        const to = pos;
        this.parseRange(from, to);
        const same = this.nodes.length == this_nodes_length;
        // console.log("BLOCK ("+ ep.toString());
        if (same) {
            return null;
        }
        return this.nodes[this.nodes.length - 1]; // this.tokens.length - 1];
    }
    private getNodeFor(index: number): null | EsilNode {
        const tok = this.peek(index);
        if (tok === undefined) {
            return null;
        }
        for (const node of this.nodes) {
            if (node.token.position === index) {
                return node;
            }
        }
        this.nodes.push(new EsilNode(new EsilToken("label", index), "label"));
        return null;
    }
    private findNodeFor(index: number): null | EsilNode {
        for (const node of this.nodes) {
            if (node.token.position === index) {
                return node;
            }
        }
        return null;
    }
    private isOperation(expr: EsilToken): never | boolean {
        switch (expr.toString()) {
            // 1pop1push
            case "[1]":
            case "[2]":
            case "[4]":
            case "[8]":
                if (this.stack.length >= 1) {
                    const i1 = this.stack.pop()!;
                    // TODO: MemoryReferenceNode(i1));
                    const mn = new EsilNode(i1.token, "operation"); // expr.toString());
                    this.stack.push(i1); // mn);
                } else {
                    throw new Error("Stack needs more items");
                }
                return true;
            // 1pop1push
            case "!":
                if (this.stack.length >= 1) {
                    const i0 = new EsilNode(
                        new EsilToken("", expr.position),
                        "none"
                    );
                    const i1 = this.stack.pop()!;
                    const nn = new EsilNode(expr, "operation");
                    nn.setSides(i0, i1);
                    this.stack.push(nn);
                } else {
                    throw new Error("Stack needs more items");
                }
                return true;
            case "":
            case "}":
            case "}{":
                // no pops or nothing, just does nothing
                return true;
            case "DUP":
                if (this.stack.length < 1) {
                    throw new Error("goto cant pop");
                } else {
                    const destNode = this.stack.pop()!;
                    this.stack.push(destNode);
                    this.stack.push(destNode);
                }
                return true;
            case "GOTO":
                // take previous statement which should be const and add a label
                {
                    const prev = this.peek(expr.position - 1);
                    if (prev !== null) {
                        // TODO: check stack
                        if (this.stack.length < 1) {
                            throw new Error("goto cant pop");
                        }
                        const destNode = this.stack.pop()!;
                        if (destNode !== null) {
                            const value: number = 0 | +destNode.toString();
                            if (value > 0) {
                                const destToken = this.peek(value);
                                if (destToken !== undefined) {
                                    destToken.label = "label_" + value;
                                    destToken.comment = "hehe";
                                    const nn = new EsilNode(expr, "goto");
                                    const gn = this.getNodeFor(
                                        destToken.position
                                    );
                                    if (gn != null) {
                                        nn.children.push(gn);
                                    }
                                    this.root.children.push(nn);
                                } else {
                                    console.error("Cannot find goto node");
                                }
                            } else {
                                console.error("Cannot find dest node for goto");
                            }
                        }
                    }
                }
                return true;
            // controlflow
            case "?{": // ESIL_TOKEN_IF
                if (this.stack.length >= 1) {
                    const i0 = new EsilNode(
                        new EsilToken("if", expr.position),
                        "none"
                    );
                    const i1 = this.stack.pop()!;
                    const nn = new EsilNode(expr, "operation");
                    nn.setSides(i0, i1); // left side can be ignored for now.. but we can express this somehow
                    const trueBlock = this.parseUntil(expr.position);
                    let falseBlock = null;
                    // nn.addChildren(trueBlock, falseBlock);
                    if (trueBlock !== null) {
                        nn.children.push(trueBlock);
                        this.nodes.push(trueBlock);
                        falseBlock = this.parseUntil(
                            trueBlock.token.position + 1
                        );
                        if (falseBlock !== null) {
                            nn.children.push(falseBlock);
                            this.nodes.push(falseBlock);
                        }
                    }
                    // console.log("true", trueBlock);
                    // console.log("false", falseBlock);
                    // this.stack.push(nn);
                    this.nodes.push(nn);
                    this.root.children.push(nn);
                    if (falseBlock !== null) {
                        this.cur = falseBlock.token.position;
                    }
                } else {
                    throw new Error("Stack needs more items");
                }
                return true;
            case "-":
                if (this.stack.length >= 2) {
                    const i0 = this.stack.pop()!;
                    const i1 = this.stack.pop()!;
                    const nn = new EsilNode(expr, "operation");
                    nn.setSides(i0, i1);
                    if (this.stack.length === 0) {
                        //	this.root.children.push(nn);
                    }
                    this.stack.push(nn);
                    this.nodes.push(nn);
                } else {
                    throw new Error("Stack needs more items");
                }
                return true;
            // 2pop1push
            case "<":
            case ">":
            case "^":
            case "&":
            case "|":
            case "+":
            case "*":
            case "/":
            case ">>=":
            case "<<=":
            case ">>>=":
            case "<<<=":
            case ">>>>=":
            case "<<<<=":
                if (this.stack.length >= 2) {
                    const i0 = this.stack.pop()!;
                    const i1 = this.stack.pop()!;
                    const nn = new EsilNode(expr, "operation");
                    nn.setSides(i0, i1);
                    if (this.stack.length === 0) {
                        //	this.root.children.push(nn);
                    }
                    this.stack.push(nn);
                    this.nodes.push(nn);
                } else {
                    throw new Error("Stack needs more items");
                }
                return true;
            // 2pop0push
            case "=":
            case ":=":
            case "-=":
            case "+=":
            case "==":
            case "=[1]":
            case "=[2]":
            case "=[4]":
            case "=[8]":
                if (this.stack.length >= 2) {
                    const i0 = this.stack.pop()!;
                    const i1 = this.stack.pop()!;
                    const nn = new EsilNode(expr, "operation");
                    nn.setSides(i0, i1);
                    if (this.stack.length === 0) {
                        this.root.children.push(nn);
                    }
                    this.nodes.push(nn);
                } else {
                    throw new Error("Stack needs more items");
                }
                return true;
        }
        return false;
    }
}
