
/**
 *  generate and launch a guard
 */

import { Bcs, ContextType, ERROR, Errors, IsValidU8, OperatorType, ValueType, GUARD_QUERIES, IsValidAddress, 
    concatenate, TransactionBlock, Protocol, FnCallType, hasDuplicates, insertAtHead,
    IsValidDesription, PassportObject, IsValidGuardIdentifier, GuardQuery, 
    } from "wowok";
import { CallBase, CallResult, Namedbject } from "./base.js";
import { LocalMark } from "../local/local.js";

export interface GuardConst {
    identifier: number; // 1-255, the same identifier to represent the same data in different nodes
    bWitness: boolean; // witness(verifiee provides while verifying in the future) or normal(verifier provides now)
    value_type: ValueType; // data value type
    value?: any; // if bWitness true, value ignores; otherwise, data.
}

// parameters: Child nodes arranged in parameter order, with each child node providing the type and data for that parameter
export type GuardNode = { identifier: number; } // Data from GuardConst
    | {query: number, object: string | number; parameters: GuardNode[];} // object: address string or identifier in GuardConst that value_type = ValueType.address
    | {logic: OperatorType.TYPE_LOGIC_AS_U256_GREATER | OperatorType.TYPE_LOGIC_AS_U256_GREATER_EQUAL
        | OperatorType.TYPE_LOGIC_AS_U256_LESSER | OperatorType.TYPE_LOGIC_AS_U256_LESSER_EQUAL 
        | OperatorType.TYPE_LOGIC_AS_U256_EQUAL | OperatorType.TYPE_LOGIC_EQUAL | OperatorType.TYPE_LOGIC_HAS_SUBSTRING 
        | OperatorType.TYPE_LOGIC_NOT | OperatorType.TYPE_LOGIC_AND | OperatorType.TYPE_LOGIC_OR;  parameters: GuardNode[];}
    | {calc: OperatorType.TYPE_NUMBER_ADD | OperatorType.TYPE_NUMBER_DEVIDE | OperatorType.TYPE_NUMBER_MOD | OperatorType.TYPE_NUMBER_ADDRESS 
        | OperatorType.TYPE_NUMBER_MULTIPLY | OperatorType.TYPE_NUMBER_SUBTRACT; parameters: GuardNode[];}
    | {value_type: ValueType; value:any; } // Data 
    | {context: ContextType.TYPE_CLOCK | ContextType.TYPE_GUARD | ContextType.TYPE_SIGNER }; // Data from run-time environment

export interface CallGuard_Data {
    namedNew?: Namedbject;
    description?: string;
    table?: GuardConst[]; //  data used by multiple logical guard nodes
    root?: GuardNode; // root must return ValueType.TYPE_BOOL     
}
export class CallGuard extends CallBase {
    data: CallGuard_Data;
    constructor(data: CallGuard_Data) {
        super();
        this.data = data;
    }
    async call(account?:string) : Promise<CallResult> {
        return await this.exec(account)
    }
    
    protected async operate(txb:TransactionBlock, passport?:PassportObject, account?:string) {
        if (!this.data?.root) {
            ERROR(Errors.InvalidParam, 'guard root node invalid')
        }
        if (this.data?.description && !IsValidDesription(this.data?.description)) {
            ERROR(Errors.IsValidDesription, 'build_guard - '+this.data.description)
        }
    
        // check const
        this.data?.table?.forEach(v => {
            if (!IsValidU8(v.identifier) || v.identifier < 1) ERROR(Errors.InvalidParam, 'table.identifer invalid');
            if (!v.bWitness && v.value === undefined) ERROR(Errors.InvalidParam, 'table.value');
        })
    
        if (this.data?.table && hasDuplicates(this.data?.table?.map(v => v.identifier))) {
            ERROR(Errors.InvalidParam, 'table.identifer duplicates')
        }
    
        // check root
        var output : Uint8Array[]= [];
        buildNode(this.data.root!, ValueType.TYPE_BOOL, this.data?.table ?? [], output);
        const bytes = (concatenate(Uint8Array, ...output) as Uint8Array); 

        const obj = txb.moveCall({
            target: Protocol.Instance().guardFn('new') as FnCallType,
            arguments: [txb.pure.string(this.data.description ?? ''), txb.pure.vector('u8', [].slice.call(bytes.reverse()))],  
        });
        if (this.data.table) {
            for (let i = 0; i < this.data?.table?.length; ++ i) {
                const v = this.data.table[i];
                if (v.bWitness) {
                    const n = new Uint8Array(1); n.set([v.value_type], 0);
                    txb.moveCall({
                        target:Protocol.Instance().guardFn("constant_add") as FnCallType,
                        arguments:[txb.object(obj), txb.pure.u8(v.identifier), txb.pure.bool(true), txb.pure.vector('u8', [].slice.call(n)), txb.pure.bool(false)]
                    }) 
                } else {
                    if (v.value_type === ValueType.TYPE_ADDRESS) {
                        v.value = await LocalMark.Instance().get_address(v.value);
                        if (!v.value) { ERROR(Errors.InvalidParam, `CallGuard_Data.data.table address`)}
                    };
                    const tmp = Uint8Array.from(Bcs.getInstance().ser(v.value_type, v.value));
                    const n = insertAtHead(tmp, v.value_type);
                    txb.moveCall({
                        target:Protocol.Instance().guardFn("constant_add") as FnCallType,
                        arguments:[txb.object(obj), txb.pure.u8(v.identifier), txb.pure.bool(false),  txb.pure.vector('u8', [].slice.call(n)), txb.pure.bool(false)]
                    }) 
                }
            }
        }

        const addr = txb.moveCall({
            target:Protocol.Instance().guardFn("create") as FnCallType,
            arguments:[txb.object(obj)]
        });
        await this.new_with_mark('Guard', txb, addr, this.data?.namedNew, account);
    }
}

//export const MAX_CHILD_NODE_COUNT = 6;

const buildNode = (guard_node:GuardNode, type_required:ValueType | 'number' | 'variable', table:GuardConst[], output:Uint8Array[]) => {
    const node: any = guard_node as any;
    if (node?.identifier !== undefined) {
        const f = table.find(v=>v.identifier === node.identifier);
        if (f) {
            checkType(f.value_type, type_required, node);
            output.push(Bcs.getInstance().ser(ValueType.TYPE_U8, ContextType.TYPE_CONSTANT)); 
            output.push(Bcs.getInstance().ser(ValueType.TYPE_U8, node.identifier))
        } else {
            ERROR(Errors.InvalidParam, 'node identifier - ' + JSON.stringify(node));
        }
    } else if (node?.query !== undefined) {
        var q: GuardQuery | undefined;
        if (typeof(node.query) === 'string') {
            q = GUARD_QUERIES.find(v=>v.query_name === node.query);
        } else if (typeof(node.query) === 'number') {
            q = GUARD_QUERIES.find(v=>v.query_id === node.query);
        }
        if (!q) ERROR(Errors.InvalidParam, 'query invalid - ' + node?.query);
        
        checkType(q!.return, type_required, node); // Return type checking
        if (q!.parameters.length === node.parameters.length) {
            for (let i = node.parameters.length - 1; i >= 0; --i) { // stack: first in, last out
                buildNode(node.parameters[i], q!.parameters[i], table, output); // Recursive check
            }
        } else {
            ERROR(Errors.InvalidParam, 'node query parameters length not match - ' + JSON.stringify(node))
        }

        output.push(Bcs.getInstance().ser(ValueType.TYPE_U8, OperatorType.TYPE_QUERY)); // QUERY TYPE + addr + cmd
        if (typeof(node.object) === 'string') {
            if (!IsValidAddress(node.object)) {
                ERROR(Errors.InvalidParam, 'node object from address string - ' + JSON.stringify(node))
            }
            output.push(Bcs.getInstance().ser(ValueType.TYPE_U8, ValueType.TYPE_ADDRESS)); 
            output.push(Bcs.getInstance().ser(ValueType.TYPE_ADDRESS, node.object)); // object address             
        } else {
            const f = table.find(v=>v.identifier === node.object);
            if (f) {
                checkType(f.value_type, ValueType.TYPE_ADDRESS, node);
                output.push(Bcs.getInstance().ser(ValueType.TYPE_U8, ContextType.TYPE_CONSTANT));
                output.push(Bcs.getInstance().ser(ValueType.TYPE_U8, node.object)); // object id
            } else {
                ERROR(Errors.InvalidParam, 'node object from identifier - ' + JSON.stringify(node));
            }
        }
        output.push(Bcs.getInstance().ser('u16', q!.query_id)); // cmd(u16)
    } else if (node?.logic !== undefined) {
        checkType(ValueType.TYPE_BOOL, type_required, node); // bool
        switch (node?.logic) {
            case OperatorType.TYPE_LOGIC_AND:
            case OperatorType.TYPE_LOGIC_OR:
                if (node.parameters.length < 2) ERROR(Errors.InvalidParam, 'node logic parameters length must >= 2'+ JSON.stringify(node)); 
                (node.parameters as GuardNode[]).reverse().forEach(v => buildNode(v, ValueType.TYPE_BOOL, table, output)); // reserve
                output.push(Bcs.getInstance().ser(ValueType.TYPE_U8, node.logic)); // TYPE 
                output.push((Bcs.getInstance().ser(ValueType.TYPE_U8, node.parameters.length)));
                break;
            case OperatorType.TYPE_LOGIC_NOT:
                if (node.parameters.length !== 1) ERROR(Errors.InvalidParam, 'node logic parameters length must be 1'+ JSON.stringify(node));
                (node.parameters as GuardNode[]).reverse().forEach(v => buildNode(v, ValueType.TYPE_BOOL, table, output)); // reserve
                output.push(Bcs.getInstance().ser(ValueType.TYPE_U8, node.logic)); // TYPE 
                break;
            case OperatorType.TYPE_LOGIC_AS_U256_GREATER:
            case OperatorType.TYPE_LOGIC_AS_U256_GREATER_EQUAL:
            case OperatorType.TYPE_LOGIC_AS_U256_LESSER:
            case OperatorType.TYPE_LOGIC_AS_U256_LESSER_EQUAL:
            case OperatorType.TYPE_LOGIC_AS_U256_EQUAL:
                if (node.parameters.length < 2) ERROR(Errors.InvalidParam, 'node logic parameters length must >= 2'+ JSON.stringify(node));
                (node.parameters as GuardNode[]).reverse().forEach(v => buildNode(v, 'number', table, output)); 
                output.push(Bcs.getInstance().ser(ValueType.TYPE_U8, node.logic)); // TYPE 
                output.push((Bcs.getInstance().ser(ValueType.TYPE_U8, node.parameters.length)));
                break;
            case OperatorType.TYPE_LOGIC_EQUAL:
                if (node.parameters.length < 2) ERROR(Errors.InvalidParam, 'node logic parameters length must >= 2'+ JSON.stringify(node));
                var any_type: any = 'variable';
                (node.parameters as GuardNode[]).reverse().forEach(v => buildNode(v, any_type, table, output)); 
                output.push(Bcs.getInstance().ser(ValueType.TYPE_U8, node.logic)); // TYPE 
                output.push((Bcs.getInstance().ser(ValueType.TYPE_U8, node.parameters.length)));
                break;               
            case OperatorType.TYPE_LOGIC_HAS_SUBSTRING:
                if (node.parameters.length < 2) ERROR(Errors.InvalidParam, 'node logic parameters length must >= 2'+ JSON.stringify(node));
                (node.parameters as GuardNode[]).reverse().forEach(v => buildNode(v, ValueType.TYPE_STRING, table, output)); 
                output.push(Bcs.getInstance().ser(ValueType.TYPE_U8, node.logic)); // TYPE 
                output.push((Bcs.getInstance().ser(ValueType.TYPE_U8, node.parameters.length)));
                break;
        }
    } else if (node?.calc !== undefined) {
        if (node?.calc === OperatorType.TYPE_NUMBER_ADDRESS) {
            checkType(ValueType.TYPE_ADDRESS, type_required, node);
            if (node.parameters.length !== 1) ERROR(Errors.InvalidParam, 'node TYPE_NUMBER_ADDRESS parameters length must == 1'+ JSON.stringify(node));
            (node.parameters as GuardNode[]).reverse().forEach(v => buildNode(v, 'number', table, output)); 
            output.push(Bcs.getInstance().ser(ValueType.TYPE_U8, node.calc)); // TYPE 
        } else {
            checkType(ValueType.TYPE_U256, type_required, node);
            if (node.parameters.length < 2) ERROR(Errors.InvalidParam, 'node calc parameters length must >= 2'+ JSON.stringify(node));
            (node.parameters as GuardNode[]).reverse().forEach(v => buildNode(v, 'number', table, output)); 
            output.push(Bcs.getInstance().ser(ValueType.TYPE_U8, node.calc)); // TYPE 
            output.push((Bcs.getInstance().ser(ValueType.TYPE_U8, node.parameters.length)));            
        }
    } else if (node?.value_type !== undefined) {
        checkType(node?.value_type, type_required, node);
        if (node?.value === undefined) ERROR(Errors.InvalidParam, 'node value undefined - ' + JSON.stringify(node));
        output.push(Bcs.getInstance().ser(ValueType.TYPE_U8, node.value_type)); // TYPE

        if (node.value_type == ValueType.TYPE_STRING || node.value_type === ValueType.TYPE_VEC_U8) {
            if (typeof(node.value) == 'string') {
                output.push(Bcs.getInstance().ser(ValueType.TYPE_STRING, node.value));
            } else {
                output.push(Bcs.getInstance().ser(ValueType.TYPE_VEC_U8, node.value));
            }
        } else {
            output.push(Bcs.getInstance().ser(node?.value_type, node.value));            
        }
    } else if (node?.context !== undefined) {
        output.push(Bcs.getInstance().ser(ValueType.TYPE_U8, node.context));
        switch (node.context) {
            case ContextType.TYPE_CLOCK:
                checkType(ValueType.TYPE_U64, type_required, node);
                break;
            case ContextType.TYPE_GUARD: 
            case ContextType.TYPE_SIGNER:
                checkType(ValueType.TYPE_ADDRESS, type_required, node);
                break;
        }
    } else if (node?.identifier !== undefined) {
        if (!IsValidGuardIdentifier(node.identifier)) ERROR(Errors.IsValidGuardIdentifier, 'node - '+JSON.stringify(node));
        const i = table.find(v => v.identifier === node.identifier);
        if (!i) ERROR(Errors.InvalidParam, 'identifier not found. node - '+JSON.stringify(node));
        checkType(i!.value_type, type_required, node);
        output.push(Bcs.getInstance().ser(ValueType.TYPE_U8, ContextType.TYPE_CONSTANT)); 
        output.push(Bcs.getInstance().ser(ValueType.TYPE_U8, node.identifier)); 
    } else {
        ERROR(Errors.InvalidParam, 'node - ' + JSON.stringify(node))
    }
}


const checkType = (type: ValueType | ContextType.TYPE_CLOCK | ContextType.TYPE_GUARD | ContextType.TYPE_SIGNER, 
    type_required:ValueType | 'number' | 'variable', node?: GuardNode) => {
    if (type_required === 'variable') {
        type_required = type as number;
    } else if (type_required === 'number') {
        if (type === ValueType.TYPE_U128 || type === ValueType.TYPE_U256 || type === ValueType.TYPE_U8 ||
            type === ValueType.TYPE_U64 || type === ContextType.TYPE_CLOCK) {
                return
            }
    } else if (type_required === ValueType.TYPE_ADDRESS) {
        if (type === ContextType.TYPE_SIGNER || type === ContextType.TYPE_GUARD) {
            return
        }
    } /*else if (type_required === ValueType.TYPE_STRING) {
        if (type === ValueType.TYPE_VEC_U8) {
            return
        }
    } else if (type_required === ValueType.TYPE_VEC_U8) {
        if (type === ValueType.TYPE_STRING) {
            return 
        }
    } */

    if (type !== type_required) {
        var str = '';
        if (node) str = ' - ' + JSON.stringify(node);
        ERROR(Errors.InvalidParam, 'checkType: ' + type + ' require type: ' + type_required + str);
    }
}