"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 () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.UtxoMultiThreadSync = void 0;
const events_1 = require("events");
const os = __importStar(require("os"));
const worker_threads_1 = require("worker_threads");
const logger_1 = __importStar(require("../../logger"));
class UtxoMultiThreadSync extends events_1.EventEmitter {
    constructor({ chain, network, config, callbacks }) {
        super();
        this.threads = [];
        this.stopping = false;
        this.syncing = false;
        this.currentHeight = 0;
        // Ordered block processing
        this.blockBuffer = new Map();
        this.headerQueue = [];
        this.headerQueueIdx = 0;
        this.nextProcessHeight = 0;
        this.isProcessing = false;
        this.batchTargetHeight = 0;
        this.startHeight = 0;
        this.startTime = 0;
        this.chain = chain;
        this.network = network;
        this.config = config;
        this.callbacks = callbacks;
    }
    async sync() {
        if (this.syncing) {
            return false;
        }
        this.syncing = true;
        const { chain, network } = this;
        try {
            const tip = await this.callbacks.getLocalTip();
            this.currentHeight = tip ? tip.height : this.config.syncStartHeight || 0;
            this.startHeight = this.currentHeight;
            await this.initializeThreads();
            this.startTime = Date.now();
            const oneSecond = 1000;
            this.syncInterval = setInterval(() => {
                const blocksProcessed = this.currentHeight - this.startHeight;
                const elapsedMinutes = (Date.now() - this.startTime) / (60 * oneSecond);
                logger_1.default.info(`${(0, logger_1.timestamp)()} | MT Syncing... | Chain: ${chain} | Network: ${network} |` +
                    `${(blocksProcessed / elapsedMinutes).toFixed(2).padStart(8)} blocks/min | ` +
                    `Height: ${this.currentHeight.toString().padStart(7)} | ` +
                    `Buffer: ${this.blockBuffer.size} | Threads: ${this.threads.length}`);
            }, oneSecond);
            await this.syncLoop();
        }
        catch (err) {
            logger_1.default.error(`Error in multi-thread sync for ${chain} ${network}: ${err.message}`);
            this.syncing = false;
            clearInterval(this.syncInterval);
            throw err;
        }
        return true;
    }
    async syncLoop() {
        let headers = await this.callbacks.getHeaders();
        while (headers.length > 0 && !this.stopping) {
            const tip = await this.callbacks.getLocalTip();
            const baseHeight = tip ? tip.height : this.config.syncStartHeight || 0;
            // Build header queue with pre-assigned heights
            this.headerQueue = headers.map((h, i) => ({ hash: h.hash, height: baseHeight + 1 + i }));
            this.headerQueueIdx = 0;
            this.nextProcessHeight = baseHeight + 1;
            this.batchTargetHeight = baseHeight + headers.length;
            logger_1.default.info(`${(0, logger_1.timestamp)()} | MT Syncing ${headers.length} blocks | Chain: ${this.chain} | ` +
                `Network: ${this.network} | Threads: ${this.threads.length}`);
            // Kick off all workers
            for (const thread of this.threads) {
                this.assignNextBlock(thread);
            }
            // Wait for all blocks in this header batch to be processed
            await new Promise(resolve => {
                this.batchResolve = resolve;
                // Check if already done (edge case)
                if (this.currentHeight >= this.batchTargetHeight) {
                    resolve();
                }
            });
            headers = await this.callbacks.getHeaders();
        }
        this.finishSync();
    }
    assignNextBlock(thread) {
        if (this.headerQueueIdx < this.headerQueue.length && !this.stopping) {
            const { hash, height } = this.headerQueue[this.headerQueueIdx++];
            thread.postMessage({ message: 'sync', hash, height });
        }
    }
    threadMessageHandler(thread) {
        return (msg) => {
            if (msg.message === 'ready') {
                this.emit('THREADREADY');
            }
            else {
                this.onBlockFetched(thread, msg);
            }
        };
    }
    async onBlockFetched(thread, msg) {
        if (msg.error || msg.notFound) {
            logger_1.default.warn(`Sync thread ${msg.threadId} error at height ${msg.height}: ${msg.error || 'not found'}. Retrying.`);
            // Retry the same block
            thread.postMessage({ message: 'sync', hash: msg.hash, height: msg.height });
            return;
        }
        // Buffer the fetched block
        this.blockBuffer.set(msg.height, msg.rawBlock);
        // Process buffered blocks in order
        await this.processOrderedBlocks();
        // Assign next block to this worker
        this.assignNextBlock(thread);
    }
    async processOrderedBlocks() {
        if (this.isProcessing)
            return;
        this.isProcessing = true;
        try {
            while (this.blockBuffer.has(this.nextProcessHeight) && !this.stopping) {
                const rawHex = this.blockBuffer.get(this.nextProcessHeight);
                this.blockBuffer.delete(this.nextProcessHeight);
                const block = this.callbacks.deserializeBlock(rawHex);
                await this.callbacks.processBlock(block);
                this.currentHeight = this.nextProcessHeight;
                this.nextProcessHeight++;
                // Check if batch is complete
                if (this.currentHeight >= this.batchTargetHeight && this.batchResolve) {
                    this.batchResolve();
                }
            }
        }
        catch (err) {
            logger_1.default.error(`Error processing block at height ${this.nextProcessHeight}: ${err.message}`);
            throw err;
        }
        finally {
            this.isProcessing = false;
            // Re-check buffer in case blocks arrived while processing
            if (this.blockBuffer.has(this.nextProcessHeight) && !this.stopping) {
                this.processOrderedBlocks();
            }
        }
    }
    getWorkerThread(workerData) {
        return new worker_threads_1.Worker(__dirname + '/syncWorker.js', {
            workerData
        });
    }
    async initializeThreads() {
        if (this.threads.length > 0) {
            return;
        }
        const threadCnt = this.config.threads || os.cpus().length - 1;
        if (threadCnt <= 0) {
            throw new Error('Invalid number of syncing threads.');
        }
        logger_1.default.info(`Initializing ${threadCnt} UTXO syncing threads for ${this.chain}:${this.network}.`);
        const workerData = { chain: this.chain, network: this.network, rpc: this.config.rpc };
        for (let i = 0; i < threadCnt; i++) {
            const thread = this.getWorkerThread(workerData);
            this.threads.push(thread);
            thread.on('message', this.threadMessageHandler(thread));
            thread.on('exit', (code) => {
                this.threads.splice(this.threads.findIndex(t => t.threadId === thread.threadId), 1);
                if (code !== 0) {
                    logger_1.default.error('UTXO sync thread exited with non-zero code: %o', code);
                }
                if (this.threads.length === 0) {
                    logger_1.default.info('All UTXO syncing threads stopped.');
                }
            });
            thread.postMessage({ message: 'start' });
        }
        await new Promise(resolve => this.once('THREADREADY', resolve));
        logger_1.default.info('UTXO syncing threads ready.');
    }
    finishSync() {
        clearInterval(this.syncInterval);
        if (this.stopping) {
            return;
        }
        logger_1.default.info(`${this.chain}:${this.network} multi-thread sync finished. Switching to single-thread P2P sync.`);
        this.emit('INITIALSYNCDONE');
        this.shutdownThreads();
        this.syncing = false;
    }
    shutdownThreads() {
        for (const thread of this.threads) {
            thread.postMessage({ message: 'shutdown' });
        }
        clearInterval(this.syncInterval);
    }
    stop() {
        this.shutdownThreads();
        this.stopping = true;
    }
}
exports.UtxoMultiThreadSync = UtxoMultiThreadSync;
//# sourceMappingURL=sync.js.map