"use strict";
// Copyright 2017-2020 @polkadot/api 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 __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const bn_js_1 = __importDefault(require("bn.js"));
const rxjs_1 = require("rxjs");
const operators_1 = require("rxjs/operators");
const api_derive_1 = __importDefault(require("../../api-derive"));
const getHasher_1 = __importDefault(require("../../metadata/Decorated/storage/fromMetadata/getHasher"));
const rpc_core_1 = __importDefault(require("../../rpc-core"));
const rpc_provider_1 = require("../../rpc-provider");
const types_1 = require("../../types");
const Linkage_1 = require("../../types/codec/Linkage");
const constants_1 = require("../../types/primitive/Extrinsic/constants");
const util_1 = require("@polkadot/util");
const submittable_1 = require("../submittable");
const decorate_1 = require("../util/decorate");
const Events_1 = __importDefault(require("./Events"));
class Decorate extends Events_1.default {
    /**
     * @description Create an instance of the class
     *
     * @param options Options object to create API instance or a Provider instance
     *
     * @example
     * <BR>
     *
     * ```javascript
     * import Api from '@polkadot/api/promise';
     *
     * const api = new Api().isReady();
     *
     * api.rpc.subscribeNewHeads((header) => {
     *   console.log(`new block #${header.number.toNumber()}`);
     * });
     * ```
     */
    constructor(options, type, decorateMethod) {
        super();
        // HACK Use BN import so decorateDerive works... yes, wtf.
        this.__phantom = new bn_js_1.default(0);
        this._extrinsicType = constants_1.DEFAULT_VERSION;
        this._isReady = false;
        this._rx = {};
        /**
         * Put the `this.onCall` function of ApiRx here, because it is needed by
         * `api._rx`.
         */
        this.rxDecorateMethod = (method) => {
            return method;
        };
        this.registry = options.registry || new types_1.TypeRegistry();
        const thisProvider = options.source
            ? options.source._rpcCore.provider.clone()
            : (options.provider || new rpc_provider_1.WsProvider());
        this.decorateMethod = decorateMethod;
        this._options = options;
        this._type = type;
        this._rpcCore = new rpc_core_1.default(this.registry, thisProvider, this._options.rpc);
        this._isConnected = new rxjs_1.BehaviorSubject(this._rpcCore.provider.isConnected());
        this._rx.hasSubscriptions = this._rpcCore.provider.hasSubscriptions;
        this._rx.registry = this.registry;
    }
    /**
     * @returns `true` if the API operates with subscriptions
     */
    get hasSubscriptions() {
        return this._rpcCore.provider.hasSubscriptions;
    }
    decorateFunctionMeta(input, output) {
        output.meta = input.meta;
        output.method = input.method;
        output.section = input.section;
        output.toJSON = input.toJSON;
        if (input.callIndex) {
            output.callIndex = input.callIndex;
        }
        return output;
    }
    // Filter all RPC methods based on the results of the rpc_methods call. We do this in the following
    // manner to cater for both old and new:
    //   - when the number of entries are 0, only remove the ones with isOptional (account & contracts)
    //   - when non-zero, remove anything that is not in the array (we don't do this)
    filterRpc() {
        return __awaiter(this, void 0, void 0, function* () {
            let methods;
            try {
                // we ignore the version (adjust as versions change, for now only "1")
                methods = (yield this._rpcCore.rpc.methods().toPromise()).methods.map((t) => t.toString());
            }
            catch (error) {
                // the method is not there, we adjust accordingly
                methods = [];
            }
            this.filterRpcMethods(methods);
        });
    }
    filterRpcMethods(methods) {
        // this is true when the RPC has entries
        const hasResults = methods.length !== 0;
        // loop through all entries we have (populated in decorate) and filter as required
        [...this._rpcCore.mapping.entries()]
            .filter(([key, { isOptional }]) => 
        // only remove when we have results and method missing, or with no results if optional
        hasResults
            ? !methods.includes(key) && key !== 'rpc_methods' // rpc_methods doesn't appear, v1
            : isOptional || key === 'rpc_methods' // we didn't find this one, remove
        )
            // eslint-disable-next-line @typescript-eslint/no-unused-vars
            .forEach(([_, { method, section }]) => {
            delete this._rpc[section][method];
            delete this._rx.rpc[section][method];
        });
    }
    decorateRpc(rpc, decorateMethod) {
        return rpc.sections.reduce((out, _sectionName) => {
            const sectionName = _sectionName;
            // out and section here are horrors to get right from a typing perspective :(
            out[sectionName] = Object.entries(rpc[sectionName]).reduce((section, [methodName, method]) => {
                //  skip subscriptions where we have a non-subscribe interface
                if (this.hasSubscriptions || !(methodName.startsWith('subscribe') || methodName.startsWith('unsubscribe'))) {
                    section[methodName] = decorateMethod(method, { methodName });
                }
                return section;
            }, {});
            return out;
        }, {});
    }
    decorateMulti(decorateMethod) {
        return decorateMethod((calls) => this._rpcCore.state.subscribeStorage(calls.map((arg) => 
        // the input is a QueryableStorageEntry, convert to StorageEntry
        Array.isArray(arg)
            ? [arg[0].creator, ...arg.slice(1)]
            : [arg.creator])));
    }
    decorateExtrinsics(extrinsics, decorateMethod) {
        const creator = submittable_1.createSubmittable(this._type, this._rx, decorateMethod);
        return Object.entries(extrinsics).reduce((out, [name, section]) => {
            out[name] = Object.entries(section).reduce((out, [name, method]) => {
                out[name] = this.decorateExtrinsicEntry(method, creator);
                return out;
            }, {});
            return out;
        }, creator);
    }
    decorateExtrinsicEntry(method, creator) {
        const decorated = (...params) => creator(method(...params));
        return this.decorateFunctionMeta(method, decorated);
    }
    decorateStorage(storage, decorateMethod) {
        return Object.entries(storage).reduce((out, [name, section]) => {
            out[name] = Object.entries(section).reduce((out, [name, method]) => {
                out[name] = this.decorateStorageEntry(method, decorateMethod);
                return out;
            }, {});
            return out;
        }, {});
    }
    decorateStorageEntry(creator, decorateMethod) {
        // get the storage arguments, with DoubleMap as an array entry, otherwise spread
        const getArgs = (...args) => creator.meta.type.isDoubleMap
            ? [creator, args]
            : [creator, ...args];
        // FIXME We probably want to be able to query the full list with non-subs as well
        const decorated = this.hasSubscriptions && creator.iterKey && creator.meta.type.isMap && creator.meta.type.asMap.linked.isTrue
            ? this.decorateStorageLinked(creator, decorateMethod)
            : decorateMethod((...args) => (this.hasSubscriptions
                ? this._rpcCore.state
                    // Unfortunately for one-shot calls we also use .subscribeStorage here
                    .subscribeStorage([getArgs(...args)])
                    // state_storage returns an array of values, since we have just subscribed to
                    // a single entry, we pull that from the array and return it as-is
                    .pipe(operators_1.map(([data]) => data))
                : this._rpcCore.state.getStorage(getArgs(...args))), { methodName: creator.method });
        decorated.creator = creator;
        decorated.at = decorateMethod((hash, arg1, arg2) => this._rpcCore.state.getStorage(getArgs(arg1, arg2), hash));
        decorated.entries = decorateMethod((doubleMapArg) => this.retrieveMapEntries(creator, doubleMapArg));
        decorated.hash = decorateMethod((arg1, arg2) => this._rpcCore.state.getStorageHash(getArgs(arg1, arg2)));
        decorated.key = (arg1, arg2) => util_1.u8aToHex(util_1.compactStripLength(creator(creator.meta.type.isDoubleMap ? [arg1, arg2] : arg1))[1]);
        decorated.keyPrefix = () => util_1.u8aToHex(creator.keyPrefix);
        if (this.hasSubscriptions) {
            // When using double map storage function, user need to pass double map key as an array
            decorated.multi = decorateMethod((args) => this._rpcCore.state.subscribeStorage(args.map((arg) => [creator, arg])));
        }
        decorated.size = decorateMethod((arg1, arg2) => this._rpcCore.state.getStorageSize(getArgs(arg1, arg2)));
        return this.decorateFunctionMeta(creator, decorated);
    }
    decorateStorageLinked(creator, decorateMethod) {
        const result = new Map();
        let subject;
        let head = null;
        // retrieve a value based on the key, iterating if it has a next entry. Since
        // entries can be re-linked in the middle of a list, we subscribe here to make
        // sure we catch any updates, no matter the list position
        const getNext = (key) => this._rpcCore.state.subscribeStorage([[creator, key]]).pipe(operators_1.switchMap(([_data]) => {
            var _a;
            const data = creator.meta.modifier.isOptional
                ? _data.unwrapOr(null)
                : _data;
            result.set(key, data);
            // iterate from this key to the linkages, constructing entries for all
            // those found and available
            if (data && data[1].next.isSome) {
                return getNext(data[1].next.unwrap());
            }
            const [keys, vals] = [[], []];
            let nextKey = head;
            // loop through the results collected, starting at the head an re-creating
            // the list. Our map may have old entries, based on the linking these will
            // not be returned in the final result
            while (nextKey) {
                const entry = result.get(nextKey);
                if (!entry) {
                    break;
                }
                const [item, linkage] = entry;
                keys.push(nextKey);
                vals.push(item);
                nextKey = (_a = linkage.next) === null || _a === void 0 ? void 0 : _a.unwrapOr(null);
            }
            const nextResult = vals.length
                ? new Linkage_1.LinkageResult(this.registry, [keys[0].constructor, keys], [vals[0].constructor, vals])
                : new Linkage_1.LinkageResult(this.registry, [types_1.Null, []], [types_1.Null, []]);
            // we set our result into a subject so we have a single observable for
            // which the value changes over time. Initially create, follow-up next
            if (subject) {
                subject.next(nextResult);
            }
            else {
                subject = new rxjs_1.BehaviorSubject(nextResult);
            }
            return subject;
        }));
        // this handles the case where the head changes effectively, i.e. a new entry
        // appears at the top of the list, the new getNext gets kicked off
        return decorateMethod((...args) => args.length
            ? this._rpcCore.state
                .subscribeStorage([[creator, ...args]])
                .pipe(operators_1.map(([data]) => data))
            : this._rpcCore.state
                .subscribeStorage([creator.iterKey])
                .pipe(operators_1.switchMap(([key]) => getNext(head = key))));
    }
    retrieveMapEntries(creator, arg) {
        util_1.assert(creator.iterKey && (creator.meta.type.isMap || creator.meta.type.isDoubleMap), 'entries can only be retrieved on maps, linked maps and double maps');
        const outputType = creator.meta.type.isMap
            ? creator.meta.type.asMap.value.toString()
            : creator.meta.type.asDoubleMap.value.toString();
        return this._rpcCore.state
            // FIXME This should really be some sort of subscription, so we can get stuff as
            // it changes (as of now it is a one-shot query). Not sure how to do this though...
            .getKeys(this.createType('Raw', util_1.u8aConcat(creator.iterKey, creator.meta.type.isDoubleMap && !util_1.isUndefined(arg) && !util_1.isNull(arg)
            ? getHasher_1.default(creator.meta.type.asDoubleMap.hasher)(this.createType(creator.meta.type.asDoubleMap.key1.toString(), arg).toU8a())
            : new Uint8Array([]))))
            .pipe(operators_1.switchMap((keys) => rxjs_1.combineLatest([
            rxjs_1.of(keys),
            // Not 100% sure about this, surely this is just a vec?
            this._rpcCore.state.subscribeStorage(keys.map((key) => key.setOutputType(outputType)))
        ])), operators_1.map(([keys, values]) => keys.map((key, index) => [key, values[index]])));
    }
    decorateDeriveRx(decorateMethod) {
        // Pull in derive from api-derive
        const derive = api_derive_1.default(this._rx, this._options.derives);
        return decorate_1.decorateSections(derive, decorateMethod);
    }
    decorateDerive(decorateMethod) {
        return decorate_1.decorateSections(this._rx.derive, decorateMethod);
    }
}
exports.default = Decorate;
