"use strict";
/* eslint-disable @typescript-eslint/camelcase */
// Copyright 2017-2020 @polkadot/rpc-provider 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 eventemitter3_1 = __importDefault(require("eventemitter3"));
const Decorated_1 = __importDefault(require("../../metadata/Decorated"));
const static_1 = __importDefault(require("../../metadata/Metadata/static"));
const jsonrpc_1 = __importDefault(require("../../type-jsonrpc"));
const testing_1 = __importDefault(require("@polkadot/keyring/testing"));
const Header_004_json_1 = __importDefault(require("@polkadot/types/json/Header.004.json"));
const SignedBlock_004_immortal_json_1 = __importDefault(require("@polkadot/types/json/SignedBlock.004.immortal.json"));
const types_1 = require("../../types");
const util_1 = require("@polkadot/util");
const util_crypto_1 = require("@polkadot/util-crypto");
const INTERVAL = 1000;
const SUBSCRIPTIONS = Array.prototype.concat.apply([], Object.values(jsonrpc_1.default).map((area) => Object
    .values(area.methods)
    .filter((method) => method.isSubscription)
    .map(({ method, section }) => `${section}_${method}`)
    .concat('chain_subscribeNewHead')));
const keyring = testing_1.default({ type: 'ed25519' });
const l = util_1.logger('api-mock');
/**
 * A mock provider mainly used for testing.
 * @return {ProviderInterface} The mock provider
 * @internal
 */
class Mock {
    constructor(registry) {
        this.db = {};
        this.emitter = new eventemitter3_1.default();
        this.isUpdating = true;
        this.prevNumber = new bn_js_1.default(-1);
        this.requests = {
            // eslint-disable-next-line @typescript-eslint/no-unused-vars
            chain_getBlock: (hash) => types_1.createType(this.registry, 'SignedBlock', SignedBlock_004_immortal_json_1.default.result).toJSON(),
            // eslint-disable-next-line @typescript-eslint/no-unused-vars
            chain_getBlockHash: (blockNumber) => '0x1234',
            chain_getHeader: () => types_1.createType(this.registry, 'Header', Header_004_json_1.default.result).toJSON(),
            state_getRuntimeVersion: () => types_1.createType(this.registry, 'RuntimeVersion').toHex(),
            state_getMetadata: () => static_1.default,
            state_getStorage: (storage, params) => util_1.u8aToHex(storage[params[0]]),
            system_chain: () => 'mockChain',
            system_name: () => 'mockClient',
            system_properties: () => ({ ss58Format: 42 }),
            system_version: () => '9.8.7'
        };
        this.subscriptions = SUBSCRIPTIONS.reduce((subs, name) => {
            subs[name] = {
                callbacks: {},
                lastValue: null
            };
            return subs;
        }, {});
        this.subscriptionId = 0;
        this.subscriptionMap = {};
        this.registry = registry;
        this.init();
    }
    get hasSubscriptions() {
        return true;
    }
    clone() {
        throw new Error('Unimplemented');
    }
    disconnect() {
        // noop
    }
    isConnected() {
        return true;
    }
    on(type, sub) {
        this.emitter.on(type, sub);
        return () => {
            this.emitter.removeListener(type, sub);
        };
    }
    // eslint-disable-next-line @typescript-eslint/require-await
    send(method, params) {
        return __awaiter(this, void 0, void 0, function* () {
            if (!this.requests[method]) {
                throw new Error(`provider.send: Invalid method '${method}'`);
            }
            return this.requests[method](this.db, params);
        });
    }
    // eslint-disable-next-line @typescript-eslint/require-await
    subscribe(type, method, ...params) {
        return __awaiter(this, void 0, void 0, function* () {
            l.debug(() => ['subscribe', method, params]);
            if (this.subscriptions[method]) {
                const callback = params.pop();
                const id = ++this.subscriptionId;
                this.subscriptions[method].callbacks[id] = callback;
                this.subscriptionMap[id] = method;
                if (this.subscriptions[method].lastValue !== null) {
                    callback(null, this.subscriptions[method].lastValue);
                }
                return id;
            }
            throw new Error(`provider.subscribe: Invalid method '${method}'`);
        });
    }
    // eslint-disable-next-line @typescript-eslint/require-await
    unsubscribe(type, method, id) {
        return __awaiter(this, void 0, void 0, function* () {
            const sub = this.subscriptionMap[id];
            l.debug(() => ['unsubscribe', id, sub]);
            if (!sub) {
                throw new Error(`Unable to find subscription for ${id}`);
            }
            delete this.subscriptionMap[id];
            delete this.subscriptions[sub].callbacks[id];
            return true;
        });
    }
    init() {
        const emitEvents = ['connected', 'disconnected'];
        let emitIndex = 0;
        let newHead = this.makeBlockHeader();
        let counter = -1;
        const metadata = new Decorated_1.default(this.registry, static_1.default);
        // Do something every 1 seconds
        setInterval(() => {
            if (!this.isUpdating) {
                return;
            }
            // create a new header (next block)
            newHead = this.makeBlockHeader();
            // increment the balances and nonce for each account
            keyring.getPairs().forEach(({ publicKey }, index) => {
                // this.setStateBn(metadata.query.balances.freeBalance(publicKey), newHead.number.toBn().muln(3).iaddn(index));
                this.setStateBn(metadata.query.system.accountNonce(publicKey), newHead.number.toBn().addn(index));
            });
            // set the timestamp for the current block
            this.setStateBn(metadata.query.timestamp.now(), Math.floor(Date.now() / 1000));
            this.updateSubs('chain_subscribeNewHead', newHead);
            // We emit connected/disconnected at intervals
            if (++counter % 2 === 1) {
                if (++emitIndex === emitEvents.length) {
                    emitIndex = 0;
                }
                this.emitter.emit(emitEvents[emitIndex]);
            }
        }, INTERVAL);
    }
    makeBlockHeader() {
        const blockNumber = this.prevNumber.addn(1);
        const header = types_1.createType(this.registry, 'Header', {
            digest: {
                logs: []
            },
            extrinsicsRoot: util_crypto_1.randomAsU8a(),
            number: blockNumber,
            parentHash: blockNumber.isZero()
                ? new Uint8Array(32)
                : util_1.bnToU8a(this.prevNumber, 256, false),
            stateRoot: util_1.bnToU8a(blockNumber, 256, false)
        });
        this.prevNumber = blockNumber;
        return header;
    }
    setStateBn(key, value) {
        this.db[util_1.u8aToHex(key)] = util_1.bnToU8a(value, 64, true);
    }
    updateSubs(method, value) {
        this.subscriptions[method].lastValue = value;
        Object
            .values(this.subscriptions[method].callbacks)
            .forEach((cb) => {
            try {
                cb(null, value.toJSON());
            }
            catch (error) {
                console.error(`Error on '${method}' subscription`, error);
            }
        });
    }
}
exports.default = Mock;
