// Copyright (c) 2018 Jordi Baylina
// License: LGPL-3.0+
//


const Web3 = require("web3");
const assert = require("assert");

class Contract {
    constructor() {
        this.code = [];
        this.labels = {};
        this.pendingLabels = {};
    }

    createTxData() {
        let C;

        // Check all labels are defined
        const pendingLabels = Object.keys(this.pendingLabels);
        if (pendingLabels.length>0) {
            throw new Error("Lables not defined: "+ pendingLabels.join(", "));
        }

        let setLoaderLength = 0;
        let genLoadedLength = -1;

        while  (genLoadedLength!=setLoaderLength) {
            setLoaderLength = genLoadedLength;
            C = new module.exports();
            C.codesize();
            C.push(setLoaderLength);
            C.push(0);
            C.codecopy();

            C.push(this.code.length);
            C.push(0);
            C.return();
            genLoadedLength = C.code.length;
        }

        return Web3.utils.bytesToHex(C.code.concat(this.code));
    }

    stop() { this.code.push(0x00); }
    add()  { this.code.push(0x01); }
    mul()  { this.code.push(0x02); }
    sub()  { this.code.push(0x03); }
    div()  { this.code.push(0x04); }
    sdiv() { this.code.push(0x05); }
    mod()  { this.code.push(0x06); }
    smod()  { this.code.push(0x07); }
    addmod()  { this.code.push(0x08); }
    mulmod()  { this.code.push(0x09); }
    exp()  { this.code.push(0x0a); }
    signextend()  { this.code.push(0x0b); }

    lt()   { this.code.push(0x10); }
    gt()   { this.code.push(0x11); }
    slt()  { this.code.push(0x12); }
    sgt()  { this.code.push(0x13); }
    eq()   { this.code.push(0x14); }
    iszero()  { this.code.push(0x15); }
    and()  { this.code.push(0x16); }
    or()  { this.code.push(0x17); }
    shor()  { this.code.push(0x18); }
    not()  { this.code.push(0x19); }
    byte()  { this.code.push(0x1a); }

    keccak() { this.code.push(0x20); }
    sha3() { this.code.push(0x20); }   // alias

    address()  { this.code.push(0x30); }
    balance()  { this.code.push(0x31); }
    origin()  { this.code.push(0x32); }
    caller()  { this.code.push(0x33); }
    callvalue()  { this.code.push(0x34); }
    calldataload()  { this.code.push(0x35); }
    calldatasize()  { this.code.push(0x36); }
    calldatacopy()  { this.code.push(0x37); }
    codesize()  { this.code.push(0x38); }
    codecopy()  { this.code.push(0x39); }
    gasprice()  { this.code.push(0x3a); }
    extcodesize()  { this.code.push(0x3b); }
    extcodecopy()  { this.code.push(0x3c); }
    returndatasize()  { this.code.push(0x3d); }
    returndatacopy()  { this.code.push(0x3e); }

    blockhash()  { this.code.push(0x40); }
    coinbase()  { this.code.push(0x41); }
    timestamp()  { this.code.push(0x42); }
    number()  { this.code.push(0x43); }
    difficulty()  { this.code.push(0x44); }
    gaslimit()  { this.code.push(0x45); }

    pop()  { this.code.push(0x50); }
    mload()  { this.code.push(0x51); }
    mstore()  { this.code.push(0x52); }
    mstore8()  { this.code.push(0x53); }
    sload()  { this.code.push(0x54); }
    sstore()  { this.code.push(0x55); }

    _pushLabel(label) {
        if (typeof this.labels[label] != "undefined") {
            this.push(this.labels[label]);
        } else {
            this.pendingLabels[label] = this.pendingLabels[label] || [];
            this.pendingLabels[label].push(this.code.length);
            this.push("0x000000");
        }
    }

    _fillLabel(label) {
        if (!this.pendingLabels[label]) return;

        let dst = this.labels[label];

        const dst3 = [dst >> 16, (dst >> 8) & 0xFF, dst & 0xFF];

        this.pendingLabels[label].forEach((p) => {
            for (let i=0; i<3; i++) {
                this.code[p+i+1] = dst3[i];
            }
        });

        delete this.pendingLabels[label];
    }


    jmp(label)  {
        this._pushLabel(label);
        this.code.push(0x56);
    }

    jmpi(label)  {
        this._pushLabel(label);
        this.code.push(0x57);
    }

    pc()  { this.code.push(0x58); }
    msize()  { this.code.push(0x59); }
    gas()  { this.code.push(0x5a); }
    label(name)  {
        assert(typeof this.labels[name] == "undefined", "Label already defined");
        this.labels[name] = this.code.length;
        this.code.push(0x5b);

        this._fillLabel(name);
    }

    push(data) {
        const d = Web3.utils.hexToBytes(Web3.utils.toHex(data));
        assert(d.length>0);
        assert(d.length<=32);
        this.code = this.code.concat([0x5F + d.length], d);
    }

    dup(n) {
        assert(n>=0);
        assert(n<16);
        this.code.push(0x80 + n);
    }

    swap(n) {
        assert(n>=1);
        assert(n<=16);
        this.code.push(0x8f + n);
    }

    log0()  { this.code.push(0xa0); }
    log1()  { this.code.push(0xa1); }
    log2()  { this.code.push(0xa2); }
    log3()  { this.code.push(0xa3); }
    log4()  { this.code.push(0xa4); }

    create()  { this.code.push(0xf0); }
    call()  { this.code.push(0xf1); }
    callcode()  { this.code.push(0xf2); }
    return()  { this.code.push(0xf3); }
    delegatecall()  { this.code.push(0xf4); }

    staticcall()  { this.code.push(0xfa); }
    revert()  { this.code.push(0xfd); }
    invalid()  { this.code.push(0xfe); }
    selfdestruct()  { this.code.push(0xff); }
}

module.exports = Contract;

