"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 _ = require("lodash");
const log_1 = require("../log/log");
const Validator_1 = require("../validation/Validator");
const UniversalSynchronizer_1 = require("./UniversalSynchronizer");
class SingleDaemon {
    constructor(api, protocol, delegator, notifier) {
        this.rules = [];
        this.api = api;
        this.protocol = protocol;
        this.universalSynchronizer = new UniversalSynchronizer_1.UniversalSynchronizer(api, protocol, {
            onSetRules: (setRules, wiseOp) => this.onSetRules(setRules, wiseOp),
            onVoteorder: (voteorder, wiseOp) => this.onVoteorder(voteorder, wiseOp),
            onStart: () => this.onStart(),
            onError: (error, proceeding) => this.onError(error, proceeding),
            onFinished: () => this.onFinished(),
            onBlockProcessingStart: (blockNum) => this.onBlockProcessingStart(blockNum),
            onBlockProcessingFinished: (blockNum) => this.onBlockProcessingFinished(blockNum)
        });
        this.delegator = delegator;
        this.notifier = notifier;
    }
    // this function only starts the loop via processBlock, which then calls processBlock(blockNum+1)
    start(since) {
        (() => __awaiter(this, void 0, void 0, function* () {
            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.universalSynchronizer.start(since);
        }))();
        return this;
    }
    stop() {
        this.universalSynchronizer.stop();
    }
    onStart() {
    }
    onFinished() {
        this.notify(undefined, {
            type: SingleDaemon.EventType.SynchronizationStop,
            moment: this.universalSynchronizer.getLastProcessedOperation(),
            message: "Synchronization stop"
        });
    }
    onBlockProcessingStart(blockNum) {
        this.notify(undefined, {
            type: SingleDaemon.EventType.StartBlockProcessing, blockNum: blockNum,
            message: "Start processing block " + blockNum
        });
    }
    onBlockProcessingFinished(blockNum) {
        this.notify(undefined, {
            type: SingleDaemon.EventType.EndBlockProcessing, blockNum: blockNum,
            message: "End processing block " + blockNum
        });
    }
    onError(error, proceeding) {
        if (proceeding) {
            this.notify(undefined, {
                type: SingleDaemon.EventType.ReversibleError,
                error: error, moment: this.universalSynchronizer.getLastProcessedOperation(),
                message: " Reversible error: " + error.message + ". Retrying in 3 seconds..."
            });
        }
        else {
            this.notify(undefined, {
                type: SingleDaemon.EventType.UnhandledError,
                error: error, moment: this.universalSynchronizer.getLastProcessedOperation(),
                message: " Reversible error: " + error.message + ". Retrying in 3 seconds..."
            });
        }
    }
    // update preoaded rules to keep them up-to-date without calling blockchain every voteorder
    onSetRules(setRules, op) {
        if (op.delegator === this.delegator) {
            const es = {
                moment: op.moment,
                voter: op.voter,
                delegator: op.delegator,
                rulesets: setRules.rulesets
            };
            this.rules.push(es);
            log_1.Log.log().debug("SYNCHRONIZER_UPDATED_RULES=" + JSON.stringify(setRules.rulesets));
            this.notify(undefined, { type: SingleDaemon.EventType.RulesUpdated, moment: op.moment,
                message: "Change of rules on blockchain. Local rules were updated." });
        }
    }
    onVoteorder(cmd, op) {
        return __awaiter(this, void 0, void 0, function* () {
            if (op.delegator !== this.delegator)
                return;
            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().warn("@" + op.voter + " tried to vote with ruleset "
                    + "\"" + cmd.rulesetName + "\", but there are no rulesets for him.");
                return;
            }
            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);
            try { // errors inside this try will be published to the blockchain! (be cautious)
                const validationResult = yield v.validate(this.delegator, op.voter, cmd, op.moment);
                if (validationResult === true) {
                    yield this.voteAndConfirm(op, cmd);
                }
                else {
                    yield this.rejectVoteorder(op, cmd, validationResult.message);
                }
            }
            catch (error) {
                if (error.name === "FetchError")
                    throw error;
                else
                    yield 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: SingleDaemon.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: SingleDaemon.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: SingleDaemon.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: SingleDaemon.EventType.OperarionsPushed,
                operations: opsToSend, moment: moment, message: "Sent operations to blockchain: " + _.join(opsToSend.map(op => op[0]), ",") });
        });
    }
    notify(error, event) {
        (() => __awaiter(this, void 0, void 0, function* () {
            this.notifier(error, event);
            if (error)
                log_1.Log.log().error(JSON.stringify(error));
            log_1.Log.log().cheapInfo(() => "SYNCHRONIZER_EVENT=" + JSON.stringify(event));
        }))();
    }
}
exports.SingleDaemon = SingleDaemon;
(function (SingleDaemon) {
    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 = SingleDaemon.EventType || (SingleDaemon.EventType = {}));
})(SingleDaemon = exports.SingleDaemon || (exports.SingleDaemon = {}));
//# sourceMappingURL=SingleDaemon.js.map