"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.cleanOld = exports.clean = exports.processMessagesInGossipQueue = exports.newCollector = exports.initGossip = exports.Collector = exports.forwardedGossips = void 0;
const events_1 = require("events");
const Comms_1 = require("../p2p/Comms");
const Comm = __importStar(require("../p2p/Comms"));
const NodeList = __importStar(require("../p2p/NodeList"));
const logger_1 = require("../logger");
const profiler_1 = require("../utils/profiler");
const promises_1 = require("../utils/functions/promises");
const queue = new Map();
const collectors = new Map();
const gossipCounterByCycle = new Map();
let gossipCounterByPartition;
let readyPartitions;
let parititionShardDataMap;
exports.forwardedGossips = new Map();
// A class responsible for collecting and processing partition gossip for a given Cycle
class Collector extends events_1.EventEmitter {
    constructor(shard) {
        super();
        this.shard = shard;
        this.allDataHashes = new Map();
        this.allReceiptMapHashes = new Map();
        this.allSummaryHashes = new Map();
        this.dataHashCounter = new Map();
        this.receiptHashCounter = new Map();
        this.summaryHashCounter = new Map();
    }
    process(messages) {
        let cycle;
        // Loop through messages and add to hash tally
        for (let messageIndex = 0; messageIndex < messages.length; messageIndex++) {
            // only iterating through messages, so probably safe
            // eslint-disable-next-line security/detect-object-injection
            const message = messages[messageIndex];
            const partitionHashData = message.data.partitionHash;
            const receiptHashData = message.data.receiptMapHash;
            const summaryHashData = message.data.summaryHash;
            if (!cycle)
                cycle = message.cycle;
            // forward snapshot gossip if gossip cycle is same as current cycle
            if (this.shard.cycleNumber === message.cycle) {
                if (!exports.forwardedGossips.has(message.sender)) {
                    (0, promises_1.fireAndForget)(() => Comm.sendGossip('snapshot_gossip', message, '', null, NodeList.byIdOrder, false));
                    exports.forwardedGossips.set(message.sender, true);
                }
                else if (exports.forwardedGossips.has(message.sender)) {
                    continue;
                }
            }
            if (!parititionShardDataMap) {
                if (logger_1.logFlags.console)
                    console.log('No partition shard data map');
                return;
            }
            // Record number of gossip recieved for each partition
            if (gossipCounterByCycle.has(cycle)) {
                gossipCounterByCycle.set(cycle, gossipCounterByCycle.get(cycle) + 1);
            }
            else {
                gossipCounterByCycle.set(cycle, 1);
            }
            for (let i = 0; i < Object.keys(partitionHashData).length; i++) {
                // only accessing keys of an object, so probably safe
                // eslint-disable-next-line security/detect-object-injection
                const partitionId = parseInt(Object.keys(partitionHashData)[i]);
                const partitionShardData = parititionShardDataMap.get(partitionId);
                if (!partitionShardData) {
                    continue;
                }
                const coveredBy = partitionShardData.coveredBy;
                const isSenderCoverThePartition = coveredBy[message.sender];
                if (!isSenderCoverThePartition) {
                    // eslint-disable-next-line security/detect-object-injection
                    delete partitionHashData[partitionId];
                    continue;
                }
                const currentCount = gossipCounterByPartition.get(partitionId);
                const requiredCount = Math.ceil(Object.keys(coveredBy).length / 2);
                if (currentCount) {
                    const newCount = currentCount + 1;
                    gossipCounterByPartition.set(partitionId, newCount);
                    if (newCount >= requiredCount) {
                        readyPartitions.set(partitionId, true);
                    }
                }
                else {
                    gossipCounterByPartition.set(partitionId, 1);
                }
            }
            const dataHashes = convertObjectToHashMap(partitionHashData);
            const receiptHashes = convertObjectToHashMap(receiptHashData);
            const summaryHashes = convertObjectToHashMap(summaryHashData);
            // Add partition hashes into partition hashTally
            for (const [partitionId, hash] of dataHashes) {
                if (!this.dataHashCounter.has(partitionId)) {
                    this.dataHashCounter.set(partitionId, new Map([[hash, 1]]));
                }
                else if (this.dataHashCounter.has(partitionId)) {
                    const counterMap = this.dataHashCounter.get(partitionId);
                    const currentCount = counterMap.get(hash);
                    if (currentCount)
                        counterMap.set(hash, currentCount + 1);
                    else
                        counterMap.set(hash, 1);
                }
            }
            // Add receipt hashes into receipt hashTally
            for (const [partitionId, hash] of receiptHashes) {
                if (!this.receiptHashCounter.has(partitionId)) {
                    this.receiptHashCounter.set(partitionId, new Map([[hash, 1]]));
                }
                else if (this.receiptHashCounter.has(partitionId)) {
                    const counterMap = this.receiptHashCounter.get(partitionId);
                    const currentCount = counterMap.get(hash);
                    if (currentCount)
                        counterMap.set(hash, currentCount + 1);
                    else
                        counterMap.set(hash, 1);
                }
            }
            // Add summary hashes into receipt hashTally
            for (const [partitionId, hash] of summaryHashes) {
                if (!this.summaryHashCounter.has(partitionId)) {
                    this.summaryHashCounter.set(partitionId, new Map([[hash, 1]]));
                }
                else if (this.summaryHashCounter.has(partitionId)) {
                    const counterMap = this.summaryHashCounter.get(partitionId);
                    const currentCount = counterMap.get(hash);
                    if (currentCount)
                        counterMap.set(hash, currentCount + 1);
                    else
                        counterMap.set(hash, 1);
                }
            }
        }
        if (readyPartitions.size >= this.shard.shardGlobals.numPartitions &&
            this.dataHashCounter.size === this.shard.shardGlobals.numPartitions + 1) {
            // +1 is for virtual global partition
            // decide winner partition hashes based on hash tally
            for (const [partitionId, counterMap] of this.dataHashCounter) {
                let selectedHash;
                let maxCount = 0;
                let possibleHashes = [];
                for (const [, count] of counterMap) {
                    if (count > maxCount) {
                        maxCount = count;
                    }
                }
                for (const [hash, count] of counterMap) {
                    if (count === maxCount) {
                        possibleHashes.push(hash);
                    }
                }
                possibleHashes = possibleHashes.sort();
                /* prettier-ignore */ if (logger_1.logFlags.console)
                    console.log(`DATA HASH COUNTER: Cycle ${cycle}, Partition ${partitionId} => `, counterMap);
                if (possibleHashes.length > 0)
                    selectedHash = possibleHashes[0];
                if (selectedHash)
                    this.allDataHashes.set(partitionId, selectedHash);
            }
            // decide winner receipt hashes based on hash tally
            for (const [partitionId, counterMap] of this.receiptHashCounter) {
                let selectedHash;
                let maxCount = 0;
                let possibleHashes = [];
                for (const [, count] of counterMap) {
                    if (count > maxCount) {
                        maxCount = count;
                    }
                }
                for (const [hash, count] of counterMap) {
                    if (count === maxCount) {
                        possibleHashes.push(hash);
                    }
                }
                possibleHashes = possibleHashes.sort();
                if (possibleHashes.length > 0)
                    selectedHash = possibleHashes[0];
                if (selectedHash)
                    this.allReceiptMapHashes.set(partitionId, selectedHash);
            }
            // decide winner summary hashes based on hash tally
            for (const [partitionId, counterMap] of this.summaryHashCounter) {
                let selectedHash;
                let maxCount = 0;
                let possibleHashes = [];
                for (const [, count] of counterMap) {
                    if (count > maxCount) {
                        maxCount = count;
                    }
                }
                for (const [hash, count] of counterMap) {
                    if (count === maxCount) {
                        possibleHashes.push(hash);
                    }
                }
                possibleHashes = possibleHashes.sort();
                if (possibleHashes.length > 0)
                    selectedHash = possibleHashes[0];
                if (selectedHash)
                    this.allSummaryHashes.set(partitionId, selectedHash);
            }
            // Emit an event once allHashes are collected
            this.emit('gotAllHashes', {
                partitionHashes: this.allDataHashes,
                receiptHashes: this.allReceiptMapHashes,
                summaryHashes: this.allSummaryHashes,
            });
        }
    }
}
exports.Collector = Collector;
/** FUNCTIONS */
// Registers partition gossip handler
function initGossip() {
    if (logger_1.logFlags.console)
        console.log('registering gossip handler...');
    (0, Comms_1.registerGossipHandler)('snapshot_gossip', (message) => {
        profiler_1.profilerInstance.scopedProfileSectionStart('snapshot_gossip');
        try {
            const { cycle } = message;
            const collector = collectors.get(cycle);
            if (collector) {
                collector.process([message]);
            }
            else {
                if (queue.has(cycle)) {
                    const messageList = queue.get(cycle);
                    messageList.push(message);
                }
                else {
                    queue.set(cycle, [message]);
                }
            }
        }
        finally {
            profiler_1.profilerInstance.scopedProfileSectionEnd('snapshot_gossip');
        }
    });
}
exports.initGossip = initGossip;
// Make a Collector to handle gossip for the given cycle
function newCollector(shard) {
    if (logger_1.logFlags.console)
        console.log(`Initiating a new collector for cycle ${shard.cycleNumber}`);
    gossipCounterByPartition = new Map();
    readyPartitions = new Map();
    exports.forwardedGossips = new Map();
    parititionShardDataMap = shard.parititionShardDataMap;
    // Creates a new Collector instance
    const collector = new Collector(shard);
    // Add it to collectors map by shard cycle number
    collectors.set(shard.cycleNumber, collector);
    return collector;
}
exports.newCollector = newCollector;
function processMessagesInGossipQueue(shard, collector) {
    // Pass any messages in the queue for the given cycle to this collector
    const messages = queue.get(shard.cycleNumber);
    if (messages) {
        collector.process([...messages]);
    }
    queue.delete(shard.cycleNumber);
}
exports.processMessagesInGossipQueue = processMessagesInGossipQueue;
function convertObjectToHashMap(obj) {
    const convertedMap = new Map();
    for (const [key, value] of Object.entries(obj)) {
        convertedMap.set(parseInt(key), value);
    }
    return convertedMap;
}
// Cleans the collector and any remaining gossip in the queue for the given cycle
function clean(cycle) {
    collectors.delete(cycle);
    queue.delete(cycle);
}
exports.clean = clean;
// Cleans partition gossip for cycles older than current - age
function cleanOld(current, age) {
    if (age > current)
        return;
    for (const [cycle] of collectors) {
        if (cycle <= current - age)
            collectors.delete(cycle);
    }
    for (const [cycle] of queue) {
        if (cycle <= current - age)
            queue.delete(cycle);
    }
}
exports.cleanOld = cleanOld;
//# sourceMappingURL=partition-gossip.js.map