"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());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
const BluebirdPromise = require("bluebird");
const _ = require("lodash");
const log_1 = require("../log/log");
const SteemOperationNumber_1 = require("../blockchain/SteemOperationNumber");
const SetRules_1 = require("../protocol/SetRules");
const SendVoteorder_1 = require("../protocol/SendVoteorder");
const Validator_1 = require("../validation/Validator");
class LegacySynchronizer {
    constructor(api, protocol, delegator, notifier) {
        this.timeoutMs = 12000;
        this.isRunning = true;
        this.rules = [];
        this.lastProcessedOperationNum = new SteemOperationNumber_1.SteemOperationNumber(0, 0, 0);
        this.api = api;
        this.protocol = protocol;
        this.delegator = delegator;
        this.notifier = notifier;
    }
    setTimeout(timeoutMs) {
        this.timeoutMs = timeoutMs;
    }
    // this function only starts the loop via processBlock, which then calls processBlock(blockNum+1)
    start(since) {
        log_1.Log.log().debug("SYNCHRONIZER_RUN_LOOP=" + JSON.stringify({ since: since }));
        this.lastProcessedOperationNum = since;
        (() => __awaiter(this, void 0, void 0, function* () {
            try {
                const rules = yield this.api.loadRulesets({ delegator: this.delegator }, since);
                log_1.Log.log().debug("SYNCHRONIZER_INITIAL_RULESETS_LOADED=" + JSON.stringify(rules));
                this.rules = rules;
                this.processBlock(since.blockNum); // the loop is started
            }
            catch (error) {
                this.notifier(error, { type: Synchronizer.EventType.UnhandledError,
                    error: error, moment: this.lastProcessedOperationNum, message: "Unhandled error in synchronizer loop: " + error.message });
            }
        }))();
        return this;
    }
    stop() {
        // when isRunning=false, continueIfRunning() will not call it's callback parameter
        this.isRunning = false;
    }
    processBlock(blockNum) {
        this.notify(undefined, { type: Synchronizer.EventType.StartBlockProcessing, blockNum: blockNum, message: "Start processing block " + blockNum });
        this.continueIfRunning(() => BluebirdPromise.resolve()
            .then(() => this.api.getWiseOperationsRelatedToDelegatorInBlock(this.delegator, blockNum))
            .mapSeries((op /* bug in bluebird */) => this.processOperation(op))
            .timeout(this.timeoutMs, new Error("Timeout (> " + this.timeoutMs + "ms while processing operations)"))
            // when timeout occurs an error is thrown. It is then catched few lines below
            // (already processed operations will not be processed second time, as is described below).
            .then(() => {
            this.notify(undefined, { type: Synchronizer.EventType.EndBlockProcessing, blockNum: blockNum, message: "End processing block " + blockNum });
            this.continueIfRunning(() => // loop continues only if synchronizer wasn't stopped
             this.processBlock(blockNum + 1) // loop occurs here through recursion
            );
        }, (error) => {
            this.notify(undefined, { type: Synchronizer.EventType.ReversibleError,
                error: error, moment: this.lastProcessedOperationNum, message: " Reversible error: " + error.message + ". Retrying in 3 seconds..." });
            // note that operations are processed only if currentOpNum > this.lastProcessedOperationNum,
            // so if block processing is retried - already processed operations will not be processed second time
            this.continueIfRunning(() => setTimeout(() => this.processBlock(blockNum), 3000));
        }));
    }
    processOperation(op) {
        return __awaiter(this, void 0, void 0, function* () {
            const currentOpNum = op.moment;
            if (currentOpNum.isGreaterThan(this.lastProcessedOperationNum)) {
                if (op.delegator === this.delegator) {
                    if (SetRules_1.SetRules.isSetRules(op.command)) {
                        this.updateRulesArray(op, op.command);
                    }
                    else if (SendVoteorder_1.SendVoteorder.isSendVoteorder(op.command)) {
                        yield this.processVoteorder(op, op.command);
                    }
                    // confirmVote does not need to be processed
                }
            }
            this.lastProcessedOperationNum = op.moment;
        });
    }
    // update preoaded rules to keep them up-to-date without calling blockchain every voteorder
    updateRulesArray(op, cmd) {
        const es = {
            moment: op.moment,
            voter: op.voter,
            delegator: op.delegator,
            rulesets: cmd.rulesets
        };
        this.rules.push(es);
        log_1.Log.log().debug("SYNCHRONIZER_UPDATED_RULES=" + JSON.stringify(cmd.rulesets));
        this.notify(undefined, { type: Synchronizer.EventType.RulesUpdated, moment: op.moment,
            message: "Change of rules on blockchain. Local rules were updated." });
    }
    processVoteorder(op, cmd) {
        log_1.Log.log().cheapDebug(() => "SYNCHRONIZER_START_PROCESSING_VOTEORDER= " + JSON.stringify(op));
        const rules = this.determineRules(op, cmd);
        log_1.Log.log().cheapDebug(() => "SYNCHRONIZER_DETERMINED_RULES=" + JSON.stringify(rules));
        if (!rules) {
            log_1.Log.log().info("@" + op.voter + " tried to vote with ruleset "
                + "\"" + cmd.rulesetName + "\", but there are no rulesets for him.");
            return Promise.resolve();
        }
        const v = new Validator_1.Validator(this.api);
        // provide already loaded rulesets (there is no need to call blockchain for them every single voteorder)
        v.provideRulesets(rules);
        return v.validate(this.delegator, op.voter, cmd, op.moment)
            .then((result) => {
            if (result === true) {
                return this.voteAndConfirm(op, cmd);
            }
            else {
                return this.rejectVoteorder(op, cmd, result.message);
            }
        })
            .catch((error) => {
            if (error.name === "FetchError")
                throw error;
            else
                this.rejectVoteorder(op, cmd, error.message);
        });
    }
    determineRules(op, cmd) {
        let out = undefined;
        const moment = op.moment;
        for (let i = 0; i < this.rules.length; i++) {
            const r = this.rules[i];
            if (r.voter === op.voter) {
                if (r.moment.isLesserThan_solveOpInTrxBug(moment)) {
                    if (!out || out.moment.isLesserThan_solveOpInTrxBug(r.moment)) {
                        out = r;
                    }
                }
            }
        }
        return out;
    }
    voteAndConfirm(op, cmd) {
        log_1.Log.log().cheapDebug(() => "SYNCHRONIZER_ACCEPT_VOTEORDER= " + JSON.stringify({ op: op, voteorder: cmd }));
        const opsToSend = [];
        const confirmCmd = {
            voteorderTxId: op.transaction_id,
            accepted: true,
            msg: "",
        };
        const wiseOp = {
            voter: op.voter,
            delegator: this.delegator,
            command: confirmCmd
        };
        opsToSend.push(...this.protocol.serializeToBlockchain(wiseOp));
        const voteOp = {
            voter: this.delegator,
            author: cmd.author,
            permlink: cmd.permlink,
            weight: cmd.weight
        };
        opsToSend.push(["vote", voteOp]);
        this.notify(undefined, { type: Synchronizer.EventType.VoteorderPassed, voteorder: cmd, voteorderTxId: op.transaction_id, moment: op.moment, voter: op.voter, message: "Voteorder passed" });
        return this.api.sendToBlockchain(opsToSend).then((moment) => {
            this.notify(undefined, { type: Synchronizer.EventType.OperarionsPushed,
                operations: opsToSend, moment: moment, message: "Sent operations to blockchain: " + _.join(opsToSend.map(op => op[0]), ",") });
        });
    }
    rejectVoteorder(op, cmd, msg) {
        log_1.Log.log().cheapDebug(() => "SYNCHRONIZER_REJECT_VOTEORDER= " + JSON.stringify({ op: op, voteorder: cmd, msg: msg }));
        const confirmCmd = {
            voteorderTxId: op.transaction_id,
            accepted: false,
            msg: msg,
        };
        const wiseOp = {
            voter: op.voter,
            delegator: this.delegator,
            command: confirmCmd
        };
        const opsToSend = this.protocol.serializeToBlockchain(wiseOp);
        this.notify(undefined, { type: Synchronizer.EventType.VoteorderRejected, voteorder: cmd, voteorderTxId: op.transaction_id, moment: op.moment, voter: op.voter, message: "Voteorder rejected: " + msg, validationException: undefined });
        return this.api.sendToBlockchain(opsToSend).then((moment) => {
            this.notify(undefined, { type: Synchronizer.EventType.OperarionsPushed,
                operations: opsToSend, moment: moment, message: "Sent operations to blockchain: " + _.join(opsToSend.map(op => op[0]), ",") });
        });
    }
    notify(error, event) {
        return __awaiter(this, void 0, void 0, function* () {
            (() => __awaiter(this, void 0, void 0, function* () { return this.notifier(error, event); }))();
            if (error)
                log_1.Log.log().error(JSON.stringify(error));
            log_1.Log.log().cheapInfo(() => "SYNCHRONIZER_EVENT=" + JSON.stringify(event));
        });
    }
    continueIfRunning(fn) {
        if (this.isRunning)
            fn();
        else {
            this.notify(undefined, { type: Synchronizer.EventType.SynchronizationStop, moment: this.lastProcessedOperationNum, message: "Synchronization stopped" });
        }
    }
}
exports.LegacySynchronizer = LegacySynchronizer;
var Synchronizer;
(function (Synchronizer) {
    let EventType;
    (function (EventType) {
        EventType["StartBlockProcessing"] = "start-block-processing";
        EventType["EndBlockProcessing"] = "end-block-processing";
        EventType["OperarionsPushed"] = "operations-pushed";
        EventType["VoteorderRejected"] = "voteorder-rejected";
        EventType["VoteorderPassed"] = "voteorder-passed";
        EventType["ReversibleError"] = "reversible-error";
        EventType["UnhandledError"] = "unhandled-error";
        EventType["SynchronizationStop"] = "synchronization-stop";
        EventType["RulesUpdated"] = "rules-updated";
    })(EventType = Synchronizer.EventType || (Synchronizer.EventType = {}));
})(Synchronizer = exports.Synchronizer || (exports.Synchronizer = {}));
//# sourceMappingURL=LegacySynchronizer.js.map