"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 });
/* END_PROMISE_DEF */
const _ = require("lodash");
const SteemOperationNumber_1 = require("./blockchain/SteemOperationNumber");
const RulePrototyper_1 = require("./rules/RulePrototyper");
const Ruleset_1 = require("./protocol/Ruleset");
const SetRulesForVoter_1 = require("./protocol/SetRulesForVoter");
class RulesUpdater {
    static getUploadRulesetsForVoterOps(protocol, delegator, voter, rulesets_) {
        if (!voter || !voter.length)
            throw new Error("Voter cannot be undefined or empty");
        if (!rulesets_)
            throw new Error("Rulesets cannot be undefined");
        if (!Array.isArray(rulesets_))
            throw new Error("Rulesets must be an array");
        if (rulesets_.filter(ruleset => !Ruleset_1.Ruleset.validateRuleset(ruleset)).length > 0)
            throw new Error("Rulesets must contain valid rulesets. Invalid rulesets: "
                + JSON.stringify(rulesets_.filter(ruleset => !Ruleset_1.Ruleset.validateRuleset(ruleset))));
        /**
         * Prototype
         */
        const rulesets = rulesets_.map(ruleset => RulePrototyper_1.RulePrototyper.prototypeRuleset(ruleset));
        // the above method will throw error on invalid rule object
        // it also validates the structure of a ruleset
        /**
         * Send to blockchain
         */
        const wiseOp = {
            voter: voter,
            delegator: delegator,
            command: { rulesets: rulesets }
        };
        const steemOps = protocol.serializeToBlockchain(wiseOp);
        if (steemOps.length !== 1)
            throw new Error("SetRules should be a single blockchain operation");
        if (!protocol.validateOperation(steemOps[0]))
            throw new Error("Operation object has invalid structure");
        return steemOps;
    }
    static uploadRulesetsForVoter(api, protocol, delegator, voter, rulesets_, proggressCallback) {
        return __awaiter(this, void 0, void 0, function* () {
            const steemOps = RulesUpdater.getUploadRulesetsForVoterOps(protocol, delegator, voter, rulesets_);
            if (!protocol.validateOperation(steemOps[0]))
                throw new Error("Operation object has invalid structure");
            if (proggressCallback)
                proggressCallback("Sending rules to blockchain...", 0.0);
            const resultSon = yield api.sendToBlockchain(steemOps);
            if (proggressCallback)
                proggressCallback("Sent to blockchain (" + resultSon.toString() + ")", 1.0);
            return resultSon;
        });
    }
    static downloadAllRulesets(api, delegator, moment = SteemOperationNumber_1.SteemOperationNumber.FUTURE) {
        return __awaiter(this, void 0, void 0, function* () {
            const currentRulesets = yield api.loadRulesets({ delegator: delegator }, moment);
            if (currentRulesets.length == 0)
                return [];
            const rulesByVoter = _.toPairs(_.groupBy(currentRulesets, (r) => r.voter));
            const rules = rulesByVoter.map((votersRuleHistory) => {
                return votersRuleHistory[1].reduce((newestRules, testedRules) => {
                    if (testedRules.moment.isGreaterThan(newestRules.moment))
                        return testedRules;
                    else
                        return newestRules;
                }, votersRuleHistory[1][0]);
            });
            // the following filter removes voters with cleared (empty) rules
            return _.filter(rules, rulesForSingleVoter => rulesForSingleVoter.rulesets.length > 0);
        });
    }
    static uploadAllRulesets(api, protocol, delegator, newRules, proggressCallback) {
        return __awaiter(this, void 0, void 0, function* () {
            /**
             * Validate input
             */
            const invalidRulesetsForVoter = newRules.filter(r => !SetRulesForVoter_1.SetRulesForVoter.validateSetRulesForVoter(r));
            if (invalidRulesetsForVoter.length > 0)
                throw new Error("There are invalid rulesets: " + invalidRulesetsForVoter); // can throw inside promise
            /**
             * Validate rulesets
             */
            newRules.forEach((rulesetsForVoter) => {
                if (!SetRulesForVoter_1.SetRulesForVoter.validateSetRulesForVoter(rulesetsForVoter))
                    throw new Error("Invalid ruleset: " + JSON.stringify(rulesetsForVoter));
                rulesetsForVoter.rulesets.forEach(ruleset => ruleset.rules.forEach(rule => {
                    const prototypedRule = RulePrototyper_1.RulePrototyper.fromUnprototypedRule(rule);
                    try {
                        prototypedRule.validateRuleObject(rule);
                    }
                    catch (error) {
                        throw new Error("Invalid rule " + JSON.stringify(rule) + " in ruleset \"" + ruleset.name
                            + "\" for voter " + rulesetsForVoter.voter + ": " + error.message);
                    }
                }));
            });
            /**
             * Download current rules
             */
            const currentRules = yield RulesUpdater.downloadAllRulesets(api, delegator);
            // ensure each Rule has prototype in newRules
            newRules.forEach((srfv) => srfv.rulesets.forEach(rulesInRuleset => rulesInRuleset.rules.forEach((rule, index) => rulesInRuleset.rules[index] = RulePrototyper_1.RulePrototyper.fromUnprototypedRule(rule))));
            const rulesByVoter = _.concat(currentRules.map(r => [r.voter, { current: r, updated: undefined }]), newRules.map(r => [r.voter, { current: undefined, updated: r }])).reduce((obj, elem) => {
                /*if (obj[elem[0]]) (obj[elem[0]] as object []).push(elem[1]);
                else obj[]
                return obj;*/
                const newObj = {};
                newObj[elem[0]] = elem[1];
                return _.merge(obj, newObj);
            }, {});
            /**
             * Create a list of operations to perform
             */
            const operationsToPerform = [];
            _.forOwn(rulesByVoter, (situation, voter, object) => {
                if (situation.current && !situation.updated && situation.current.rulesets.length > 0 /* if length == 0 it means that the rules were voided */) {
                    // delete rules
                    proggressCallback("Planning updating rules: rules for " + voter + " will be voided.", 0);
                    operationsToPerform.push(RulesUpdater.sendRules(voter, delegator, { rulesets: [] }));
                }
                else if (!situation.current && situation.updated) {
                    // create rules
                    proggressCallback("Planning updating rules: rules for " + voter + " will be created: " + JSON.stringify(situation.updated), 0);
                    operationsToPerform.push(RulesUpdater.sendRules(voter, delegator, situation.updated));
                }
                else if (situation.current && situation.updated) {
                    // compare rules, send if differ
                    if (RulesUpdater.diffRules(situation.current, situation.updated)) {
                        proggressCallback("Planning updating rules: rules for " + voter + " will be updated: current="
                            + JSON.stringify(situation.current) + ", new=" + JSON.stringify(situation.updated), 0);
                        operationsToPerform.push(RulesUpdater.sendRules(voter, delegator, situation.updated));
                    }
                }
                return operationsToPerform;
            });
            if (operationsToPerform.length === 0)
                return true;
            else {
                proggressCallback("Updating rules: " + operationsToPerform.length + " operations to send...", 0);
                let moment = SteemOperationNumber_1.SteemOperationNumber.NEVER;
                for (let i = 0; i < operationsToPerform.length; i++) {
                    const op = operationsToPerform[i];
                    const num = i + 1;
                    proggressCallback("Updating rules: Sending operation " + num + "/" + operationsToPerform.length + "...", (num / operationsToPerform.length));
                    moment = yield api.sendToBlockchain(protocol.serializeToBlockchain(op));
                    proggressCallback("Updating rules: Sent operation " + num + "/" + operationsToPerform.length
                        + " (" + moment + ")", ((num + 1) / operationsToPerform.length));
                }
                proggressCallback("Done updating rules.", 1);
                return moment;
            }
        });
    }
    static sendRules(voter, delegator, rulesCmd) {
        return {
            voter: voter,
            delegator: delegator,
            command: rulesCmd
        };
    }
    static diffRules(a, b) {
        return !_.isEqual(a.rulesets, b.rulesets);
        // we have to compare a.rulesets to b.rulesets instead of comparing a to b,
        // because either a or be can be an EffectuatedSetRules object, which contains additional properties
    }
}
exports.RulesUpdater = RulesUpdater;
//# sourceMappingURL=RulesUpdater.js.map