import fs from "fs";
import {MvInfo, MvTable} from "./comm_interface";
import {logger} from "./logger";

interface Input {
    internalType: string;
    name: string;
    type: string;
}

interface Output {
    internalType: string;
    name: string;
    type: string;
}

interface FunctionDefinition {
    inputs: Input[];
    name: string;
    outputs: Output[];
    stateMutability: string;
    type: string;
    opcodes: string[];
}

interface ContractDefinition {
    owner: string;
    name: string;
    functions: FunctionDefinition[];
}

const EVM_TO_MOVE_TYPE = new Map()
EVM_TO_MOVE_TYPE.set("uint64", 3) // u64

const EVM_TO_MOVE_VISIBILITY = new Map()
EVM_TO_MOVE_VISIBILITY.set("view", 1) // public
EVM_TO_MOVE_VISIBILITY.set("pure", 1)//  public

const SUPPORT_CODE = ["ADD", "RETURN", "SUB"]


function getMoveSignature(evmType: string): string {
    const t = EVM_TO_MOVE_TYPE.get(evmType)
    if (!t) throw  new Error(`parse signature error, now not support ${evmType}`)
    return t
}

function getFunVisibility(evmType: string): string {
    const t = EVM_TO_MOVE_VISIBILITY.get(evmType)
    if (!t) throw  new Error(`parse visibility error, now not support ${evmType}`)
    return t
}

function normalizeAddress(address: string): string {
    let ret = address
    if (address.startsWith("0x"))
        ret = address.slice(2)
    return ret.toLowerCase();
}

function numToHex(num: number): string {
    let str = num.toString(16)
    if (str.length % 2 !== 0) {
        str = `0${str}`
    }
    return str
}

function strToHex(str: string): string {
    let hexStr = Buffer.from(str, "utf8").toString("hex")
    if (hexStr.length % 2 !== 0) {
        hexStr = `0${hexStr}`
    }
    return hexStr
}

export class BuildCode {
    private schema: ContractDefinition;

    private info: MvInfo

    private code: string = ""

    constructor(schema: ContractDefinition) {
        this.schema = schema;
        this.info = {magicValue: "", selfModule: 0, tableCount: 0, tables: [], version: 0}
    }

    private static getBaseTable(): MvTable {
        return {
            kind: "00",
            offset: 0,
            size: 0,
            content: "",
            name: "",
            index: [],
            payload: []
        }
    }

    build() {
        // We must maintain the following function call order
        this.header();
        this.module_handles()
        this.function_handles()
        this.signature()
        this.identifiers()
        this.address_identifiers()
        this.metadata()
        this.function_definitions()
        this.parse_fun()
        this.inner_build();
    }

    private inner_build() {
        logger.info("movement build bytecode processing")
        const buf = Buffer.alloc(4)
        buf.writeUint32LE(this.info.version, 0)
        this.code = this.info.magicValue.concat(
            buf.toString("hex"),
            numToHex(this.info.tableCount))
        const base = this.info.tables[0]
        base.offset = 0
        Object.values<number>(base.index).forEach((it) => {
            base.content += numToHex(it)
            base.size += 1
        })
        this.code += base.kind.concat(numToHex(base.offset), numToHex(base.size))
        for (let i = 1; i < this.info.tables.length; i += 1) {
            const table = this.info.tables[i]
            const preTable = this.info.tables[i - 1]

            if (table.kind === "03") {
                table.payload.forEach((it: any) => {
                    table.content += numToHex(it.module).concat(
                        numToHex(it.name),
                        numToHex(it.parameters),
                        numToHex(it.return),
                        numToHex(it.type_parameters.length),
                    )
                    // TODO add type_parameters content
                })
            }
            if (table.kind === "05") {
                table.payload.forEach((it: any) => {
                    table.content += numToHex(it.length)
                    it.forEach((el: any) => {
                        table.content += numToHex(el)
                    })
                })
            }
            if (table.kind === "07") {
                table.payload.forEach((it: any) => {
                    const content = strToHex(it)
                    table.content += numToHex(content.length / 2).concat(content)
                })
            }
            if (table.kind === "08") {
                table.content = table.payload.join("")
            }
            if (table.kind === "10") {
                const key = strToHex(table.payload.key)
                const {value} = table.payload
                table.content += numToHex(key.length / 2).concat(key)
                table.content += numToHex(value.length / 2).concat(value)
            }
            if (table.kind === "0c") {
                const doubleBytes = ["0b"]
                table.payload.forEach((it: any) => {
                    table.content += numToHex(it.function_handle).concat(
                        numToHex(it.visibility),
                        numToHex(it.is_entry),
                        numToHex(it.acquires_global_resources.length), // TODO:add the array content
                        numToHex(it.locals),
                        numToHex(it.bytecode.filter((x: string) => !doubleBytes.includes(x)).length),
                        it.bytecode.join("")
                    )
                })
            }
            table.offset = preTable.offset + preTable.size
            table.size = table.content.length / 2;
            this.code += table.kind.concat(numToHex(table.offset), numToHex(table.size))
        }
        for (let i = 0; i < this.info.tables.length; i += 1) {
            this.code += this.info.tables[i].content
        }
        // Todo why?
        const selfModuleIndex = "00"
        this.code = this.code.concat(selfModuleIndex)
        logger.success("movement build bytecode successfully")
    }

    save(p: string) {
        fs.writeFileSync(p, Buffer.from(this.code, "hex"));
        logger.success(`movement save bytecode to ${p} successfully`)
    }

    private header() {
        logger.info("movement start build bytecode header")
        // now this is fixed value
        this.info.magicValue = "a11ceb0b"
        this.info.version = 6
        this.info.tableCount = 7
    }

    private module_handles() {
        logger.info("movement start build bytecode module_handles")
        // now we think this table is fixed
        const table = BuildCode.getBaseTable();
        table.kind = "01"
        table.name = "MODULE_HANDLES"
        table.offset = 0
        table.index = {address: 0, name: 0}
        this.info.tables.push(table)
    }

    private function_handles() {
        logger.info("movement start build bytecode function_handles")
        const table = BuildCode.getBaseTable()
        table.kind = "03"
        table.name = "FUNCTION_HANDLES"
        table.offset = 0
        this.info.tables.push(table)
    }

    private signature() {
        logger.info("movement start build bytecode signature")
        const table = BuildCode.getBaseTable()
        table.kind = "05"
        table.name = "SIGNATURES"
        // this always exist empty parameters or return values, so add to this table at the first
        table.payload.push([])
        this.info.tables.push(table)
    }

    private identifiers() {
        logger.info("movement start build bytecode identifiers")
        const table = BuildCode.getBaseTable()
        table.kind = "07"
        table.name = "IDENTIFIERS"
        table.payload.push(this.schema.name)
        this.info.tables.push(table)
    }

    private address_identifiers() {
        logger.info("movement start build bytecode address_identifiers")
        // now we think this table is fixed
        const table = BuildCode.getBaseTable()
        table.kind = "08"
        table.name = "ADDRESS_IDENTIFIERS"
        table.payload.push(normalizeAddress(this.schema.owner))
        this.info.tables.push(table)
    }

    private metadata() {
        logger.info("movement start build bytecode metadata")
        const table = BuildCode.getBaseTable()
        table.kind = "10"
        table.name = "METADATA"
        // now we think this table is fixed
        table.payload = {
            key: "aptos::metadata_v1",
            value: ""
        }
        this.info.tables.push(table)
    }

    private function_definitions() {
        logger.info("movement start build bytecode function_definitions")
        const table = BuildCode.getBaseTable()
        table.kind = "0c"
        table.name = "FUNCTION_DEFINITIONS"
        this.info.tables.push(table)
    }

    private parse_fun() {
        logger.info("movement start parse bytecode function")
        const tableIdentifiers = this.info.tables.find(it => it.kind === "07")!
        const tableMetadata = this.info.tables.find(it => it.kind === "10")!
        const tableSignatures = this.info.tables.find(it => it.kind === "05")!
        const tableFunHandler = this.info.tables.find(it => it.kind === "03")!
        const tableFunDefinitions = this.info.tables.find(it => it.kind === "0c")!

        function signatureIndex(t: string[]) {
            return tableSignatures.payload.findIndex((it: any) => JSON.stringify(it) === JSON.stringify(t))
        }

        const functions = this.schema.functions.filter(it => it.type === "function")
        for (let i = 0; i < functions.length; i += 1) {
            const fun = functions[i]
            tableIdentifiers.payload.push(fun.name)
            const funHex = strToHex(fun.name)
            tableMetadata.payload.value += `000001${numToHex(funHex.length / 2)}${funHex}010100`

            let parametersIndex = 0;
            let returnIndex = 0;
            const funInputType = fun.inputs.map(it => getMoveSignature(it.type))
            if (funInputType.length > 0) {
                const index = signatureIndex(funInputType)
                if (index > -1) {
                    parametersIndex = index
                } else {
                    parametersIndex = tableSignatures.payload.push(funInputType) - 1
                }
            }
            const funOutputType = fun.outputs.map(it => getMoveSignature(it.type))
            if (funOutputType.length > 0) {
                const index = signatureIndex(funOutputType)
                if (index > -1) {
                    returnIndex = index
                } else {
                    returnIndex = tableSignatures.payload.push(funOutputType) - 1
                }
            }

            tableFunHandler.payload.push({
                module: 0, // now we think it is fixed value,
                name: i + 1,  // as the module name is first, so add 1,
                type_parameters: [], // TODO decode more type parameters,
                parameters: parametersIndex,
                return: returnIndex,
            })
            const def: {
                function_handle: any,
                visibility: string,
                is_entry: number,
                acquires_global_resources: [],
                locals: number,
                bytecode: string[]
            } = {
                function_handle: tableFunHandler.payload.length - 1,
                visibility: getFunVisibility(fun.stateMutability),
                is_entry: 0,// TODO parse why ?
                acquires_global_resources: [], // TODO parse why ?,
                //  ULEB128 index into the SIGNATURES table for the types of the locals of the function,
                //  this should parse according to code ,but now we just set it as fixed value
                locals: 0,
                bytecode: []
            }
            const codes = fun.opcodes;
            for (let i1 = 0; i1 < codes.length; i1 += 1) {
                const code = codes[i1];
                if (!SUPPORT_CODE.includes(code)) {
                    throw new Error(`current not support ${code}`)
                }
                if (code === "ADD") {
                    for (let j = 0; j < funInputType.length; j += 1) {
                        def.bytecode.push("0b")
                        def.bytecode.push(`0${j}`)
                    }
                    def.bytecode.push("16")
                }
                if (code === "SUB") {
                    for (let j = 0; j < funInputType.length; j += 1) {
                        def.bytecode.push("0b")
                        def.bytecode.push(`0${j}`)
                    }
                    def.bytecode.push("17")
                }
                if (code === "RETURN") {
                    def.bytecode.push("02")
                }
            }
            tableFunDefinitions.payload.push(def)
            logger.success("movement start parse bytecode function successfully")
        }
    }
}


