"use strict";
// Copyright 2017-2020 @polkadot/api-contract authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
var __rest = (this && this.__rest) || function (s, e) {
    var t = {};
    for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
        t[p] = s[p];
    if (s != null && typeof Object.getOwnPropertySymbols === "function")
        for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
            if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
                t[p[i]] = s[p[i]];
        }
    return t;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const types_1 = require("../types");
const util_1 = require("@polkadot/util");
const MetaRegistry_1 = __importDefault(require("./MetaRegistry"));
// parse a selector, this can be a number (older) or of [<hex>, <hex>, ...]. However,
// just catch everything (since this is now non-standard for u32 anyway)
function parseSelector(registry, fnname, input) {
    if (util_1.isNumber(input)) {
        return types_1.createType(registry, 'u32', input);
    }
    else if (util_1.isHex(input)) {
        return types_1.createType(registry, 'u32', util_1.hexToU8a(input));
    }
    else if (typeof input === 'string') {
        try {
            const array = JSON.parse(input);
            util_1.assert(array.length === 4, `${fnname}: Invalid selector length`);
            return types_1.createType(registry, 'u32', Uint8Array.from(
            // the as number[] is here to pacify TS, it doesn't quite know how to handle the cb
            array.map((value) => util_1.isHex(value)
                ? util_1.hexToNumber(value.toString())
                : value)));
        }
        catch (e) {
            console.error(e);
        }
    }
    throw new Error(`${fnname}: Unable to parse selector`);
}
function createArgClass(registry, args, baseDef) {
    return types_1.createClass(registry, JSON.stringify(args.reduce((base, { name, type }) => {
        base[name] = type.displayName || types_1.encodeType(type);
        return base;
    }, baseDef)));
}
class ContractRegistry extends MetaRegistry_1.default {
    // Contract ABI helpers
    validateArgs(name, args) {
        util_1.assert(Array.isArray(args), `Expected 'args' to exist on ${name}`);
        args.forEach((arg) => {
            const unknownKeys = Object.keys(arg).filter((key) => !['name', 'type'].includes(key));
            util_1.assert(unknownKeys.length === 0, `Unknown keys ${unknownKeys.join(', ')} found in ABI args for ${name}`);
            util_1.assert(util_1.isNumber(arg.name) && util_1.isString(this.stringAt(arg.name)), `${name} args should have valid name `);
            util_1.assert(util_1.isNumber(arg.type.ty) && util_1.isObject(this.typeDefAt(arg.type.ty)), `${name} args should have valid type`);
        });
    }
    validateConstructors({ contract: { constructors } }) {
        constructors.forEach((constructor, index) => {
            const unknownKeys = Object.keys(constructor).filter((key) => !['args', 'docs', 'name', 'selector'].includes(key));
            util_1.assert(unknownKeys.length === 0, `Unknown keys ${unknownKeys.join(', ')} found in ABI constructor`);
            this.validateArgs(`constructor ${index}`, constructor.args);
        });
    }
    validateMessages({ contract: { messages } }) {
        messages.forEach((message) => {
            const unknownKeys = Object.keys(message).filter((key) => !['args', 'docs', 'mutates', 'name', 'selector', 'return_type'].includes(key));
            const fnname = `messages.${message.name}`;
            util_1.assert(unknownKeys.length === 0, `Unknown keys ${unknownKeys.join(', ')} found in ABI args for messages.${message.name}`);
            const { name, selector, return_type: returnType } = message;
            util_1.assert(util_1.isNumber(name) && util_1.isString(this.stringAt(name)), `Expected name for ${fnname}`);
            util_1.assert(util_1.isNull(returnType) || (util_1.isNumber(returnType.ty) && util_1.isObject(this.typeDefAt(returnType.ty))), `Expected return_type for ${fnname}`);
            parseSelector(this.registry, fnname, selector);
            this.validateArgs(fnname, message.args);
        });
    }
    validateAbi(abi) {
        const unknownKeys = Object.keys(abi.contract).filter((key) => !['constructors', 'docs', 'events', 'messages', 'name'].includes(key));
        util_1.assert(unknownKeys.length === 0, `Unknown fields ${unknownKeys.join(', ')} found in ABI`);
        const { contract: { constructors, messages, name } } = abi;
        util_1.assert(constructors && messages && util_1.isString(this.stringAt(name)), 'ABI should have constructors, messages & name sections');
        this.validateConstructors(abi);
        this.validateMessages(abi);
    }
    createMessage(name, message) {
        const args = message.args.map(({ name, type }) => {
            util_1.assert(util_1.isObject(type), `Invalid type at index ${type}`);
            return {
                name: util_1.stringCamelCase(name),
                type
            };
        });
        const Clazz = createArgClass(this.registry, args, util_1.isUndefined(message.selector) ? {} : { __selector: 'u32' });
        const baseStruct = { __selector: util_1.isUndefined(message.selector) ? undefined : parseSelector(this.registry, name, message.selector) };
        const encoder = (...params) => {
            util_1.assert(params.length === args.length, `Expected ${args.length} arguments to contract ${name}, found ${params.length}`);
            const u8a = new Clazz(this.registry, args.reduce((mapped, { name }, index) => {
                mapped[name] = params[index];
                return mapped;
            }, Object.assign({}, baseStruct))).toU8a();
            return types_1.Compact.addLengthPrefix(u8a);
        };
        const fn = encoder;
        fn.args = args;
        fn.isConstant = !(message.mutates || false);
        fn.type = message.returnType || null;
        return fn;
    }
    convertAbi({ contract, storage }) {
        return {
            contract: this.convertContract(contract),
            storage: this.convertStorage(storage)
        };
    }
    convertArgs(args) {
        return args.map((_a) => {
            var { name, type } = _a, arg = __rest(_a, ["name", "type"]);
            return (Object.assign(Object.assign({}, arg), { name: this.stringAt(name), type: this.convertType(type) }));
        });
    }
    convertType({ ty, display_name: displayNameIndices }) {
        const displayName = this.stringsAt(displayNameIndices).join('::');
        return this.typeDefAt(ty, { displayName });
    }
    convertContract(_a) {
        var { constructors, messages, name, events } = _a, contract = __rest(_a, ["constructors", "messages", "name", "events"]);
        return Object.assign(Object.assign({ constructors: this.convertConstructors(constructors), messages: messages.map(message => this.convertMessage(message)), name: this.stringAt(name) }, (events
            ? { events: events.map(event => this.convertEvent(event)) }
            : {})), contract);
    }
    convertConstructors(constructors) {
        return constructors.map((constructor) => {
            return this.convertMessage(constructor);
        });
    }
    convertMessage(_a) {
        var { args, name, return_type: returnType } = _a, message = __rest(_a, ["args", "name", "return_type"]);
        return Object.assign(Object.assign({}, message), { args: this.convertArgs(args), name: this.stringAt(name), returnType: returnType ? this.convertType(returnType) : null });
    }
    convertEvent({ args }) {
        return {
            args: this.convertArgs(args)
        };
    }
    convertStorage(storage) {
        return this.convertStorageStruct(storage);
    }
    convertStorageLayout(storageLayout) {
        if (storageLayout['struct.type']) {
            return this.convertStorageStruct(storageLayout);
        }
        else {
            return this.convertStorageRange(storageLayout);
        }
    }
    convertStorageStruct({ 'struct.type': structType, 'struct.fields': structFields }) {
        return {
            'struct.type': this.typeDefAt(structType),
            'struct.fields': structFields.map(({ name, layout }) => ({
                name: this.stringAt(name),
                layout: this.convertStorageLayout(layout)
            }))
        };
    }
    convertStorageRange(_a) {
        var { 'range.elem_type': type } = _a, range = __rest(_a, ['range.elem_type']);
        return Object.assign(Object.assign({}, range), { 'range.elem_type': this.typeDefAt(type) });
    }
}
exports.default = ContractRegistry;
