"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    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) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
    result["default"] = mod;
    return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
const zen_observable_ts_1 = require("zen-observable-ts");
const R = __importStar(require("ramda"));
const Environment_1 = require("../environment/Environment");
const getContract_1 = require("./getContract");
const ensure_1 = require("../guards/ensure");
const token_math_1 = require("@melonproject/token-math");
const defaultPrepareArgs = (environment, params, contractAddress) => Object.values(params || {}).map(v => v.toString());
const defaultPostProcess = (environment, result, prepared) => result;
const defaultProcessors = {
    postProcess: defaultPostProcess,
    prepareArgs: defaultPrepareArgs,
};
const callFactory = (name, contract, processors = defaultProcessors) => {
    const { postProcess, prepareArgs } = Object.assign({}, defaultProcessors, processors);
    const prepare = (environment, contractAddress, params = {}) => {
        ensure_1.ensure(token_math_1.isAddress(contractAddress), `Invalid contract address provided: ${contractAddress}`);
        const log = environment.logger('melon:protocol:utils:solidity:callFactory');
        const args = prepareArgs(environment, params, contractAddress);
        const txId = `${contract}@${contractAddress}.${name}(${args.join(',')})`;
        try {
            log(Environment_1.LogLevels.INFO, 'Prepare call', txId);
            const contractInstance = getContract_1.getContract(environment, contract, contractAddress);
            const txObject = contractInstance.methods[name](...args);
            const prepared = {
                contractAddress,
                params,
                txObject,
            };
            return prepared;
        }
        catch (error) {
            log(Environment_1.LogLevels.ERROR, txId, error);
            throw new Error(
            // tslint:disable-next-line:max-line-length
            `Error in prepare call ${txId}): ${error.message}`);
        }
    };
    const call = (environment, prepared) => __awaiter(this, void 0, void 0, function* () {
        const log = environment.logger('melon:protocol:utils:solidity:callFactory');
        const txId = `${contract}@${prepared.contractAddress}.${name}(${prepared.txObject.arguments.join(',')})`;
        let result;
        try {
            result = yield prepared.txObject.call();
        }
        catch (error) {
            log(Environment_1.LogLevels.ERROR, txId, error);
            throw new Error(`Call failed. ${txId}: ${error.message}`);
        }
        try {
            const postProcessed = yield postProcess(environment, result, prepared);
            log(Environment_1.LogLevels.DEBUG, 'Call processed', txId, { result, postProcessed });
            return postProcessed;
        }
        catch (error) {
            log(Environment_1.LogLevels.ERROR, txId, result, error);
            throw error;
            // throw new Error(`Postprocess failed. ${txId}: ${error.message}`);
        }
    });
    // TODO: Possibility to specify custom filters
    // TODO: Check if newBlockHeaders & multiple subscriptions lead to
    // performance problems?
    const observable = (environment, contractAddress, params) => new zen_observable_ts_1.Observable(observer => {
        let lastResult;
        const prepared = prepare(environment, contractAddress, params);
        const subscription = environment.eth.subscribe('newBlockHeaders');
        subscription.on('data', (block) => __awaiter(this, void 0, void 0, function* () {
            if (block.number) {
                const result = yield call(environment, prepared);
                if (!R.equals(result, lastResult)) {
                    observer.next(result);
                    lastResult = result;
                }
            }
        }));
        // TODO: Better error handling (what kind of errors do we expect?)
        subscription.on('error', error => {
            observer.error(error);
        });
        return () => subscription.unsubscribe();
    });
    const execute = (environment, contractAddress, params = {}) => __awaiter(this, void 0, void 0, function* () {
        const prepared = prepare(environment, contractAddress, params);
        const result = yield call(environment, prepared);
        return result;
    });
    execute.prepare = prepare;
    execute.call = call;
    execute.observable = observable;
    return execute;
};
exports.callFactory = callFactory;
const callFactoryWithoutParams = (name, contract, processors) => {
    const withParams = callFactory(name, contract, processors);
    const prepare = (environment, contractAddress) => withParams.prepare(environment, contractAddress, {});
    const call = withParams.call;
    const observable = (environment, contractAddress) => withParams.observable(environment, contractAddress, {});
    const execute = (environment, contractAddress) => __awaiter(this, void 0, void 0, function* () {
        const prepared = prepare(environment, contractAddress);
        const result = yield call(environment, prepared);
        return result;
    });
    execute.prepare = prepare;
    execute.call = call;
    execute.observable = observable;
    return execute;
};
exports.callFactoryWithoutParams = callFactoryWithoutParams;
