/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */
import cluster from 'cluster';
import { cpus } from 'os';
import { dirname, join } from 'path';
import { fileURLToPath } from 'url';
import { BaseRunner } from './BaseRunner.js';
import { State } from './enums.js';
const numCPUs = cpus().length;
let fileDir = '';
try {
    if (import.meta && import.meta.url) {
        fileDir = dirname(fileURLToPath(import.meta.url));
    }
}
catch {
    fileDir = __dirname;
}
/**
 * Runs a project in parallel.
 * It creates a number of workers determined by the number of CPUs available on the current machine
 * and the number of iterations defined in the configuration options.
 *
 * When the number of iterations is greater then the number of CPUs then
 * the program distributes the remaining iterations among created workers.
 *
 * The program dispatched the `status` event. It is dispatched each time when the worker status
 * change. This event can be user to refresh the UI to reflect the newest state.
 */
// eslint-disable-next-line no-redeclare
export class ProjectParallelRunner extends BaseRunner {
    project;
    options;
    workers = [];
    mainResolver;
    mainRejecter;
    constructor(project, opts = {}) {
        super();
        this.project = project;
        this.options = opts;
        this._exitHandler = this._exitHandler.bind(this);
        this._abortHandler = this._abortHandler.bind(this);
        if (opts.signal) {
            this.signal = opts.signal;
        }
    }
    execute() {
        return new Promise((resolve, reject) => {
            this.mainResolver = resolve;
            this.mainRejecter = reject;
            this._execute();
        });
    }
    _state = State.Idle;
    get state() {
        return this._state;
    }
    _signal;
    /**
     * The abort signal to set on this request.
     * Aborts the request when the signal fires.
     * @type {(AbortSignal | undefined)}
     */
    get signal() {
        return this._signal;
    }
    set signal(value) {
        const old = this._signal;
        if (old === value) {
            return;
        }
        this._signal = value;
        if (old) {
            old.removeEventListener('abort', this._abortHandler);
        }
        if (value) {
            value.addEventListener('abort', this._abortHandler);
        }
    }
    /**
     * Aborts the current run.
     * The promise returned by the `execute()` method will reject if not yet resolved.
     */
    abort() {
        this._state = State.Aborted;
        const { workers } = this;
        workers.forEach((info) => {
            if (info.status !== 'error') {
                try {
                    info.worker?.destroy();
                }
                catch {
                    // ...
                }
                info.status = 'error';
            }
        });
        if (this.mainRejecter) {
            this.mainRejecter(new Error(`The execution has been aborted.`));
            this.mainRejecter = undefined;
            this.mainResolver = undefined;
        }
    }
    /**
     * Handler for the `abort` event on the `AbortSignal`.
     */
    _abortHandler() {
        this.abort();
    }
    _execute() {
        this._state = State.Running;
        try {
            cluster.setupPrimary({
                exec: join(fileDir, 'ProjectRunnerWorker.js'),
                silent: true,
            });
            const { iterations = 1 } = this.options;
            const poolSize = Math.min(iterations, numCPUs);
            for (let i = 0; i < poolSize; i++) {
                const worker = cluster.fork();
                this.setupWorker(worker);
            }
            this.distributeIterations();
            this.emit('status', this.getStatusWorkers());
            cluster.on('exit', this._exitHandler);
        }
        catch (e) {
            const cause = e;
            if (this.mainRejecter) {
                this.mainRejecter(cause);
            }
            this.mainResolver = undefined;
            this.mainRejecter = undefined;
            this._state = State.Idle;
        }
    }
    getStatusWorkers() {
        const { workers } = this;
        const result = [];
        workers.forEach((info) => {
            const cp = { ...info };
            delete cp.worker;
            result.push(cp);
        });
        return result;
    }
    distributeIterations() {
        const workers = this.workers.length;
        const { iterations = 1 } = this.options;
        let iterationsRemaining = iterations - workers;
        let currentIndex = 0;
        while (iterationsRemaining > 0) {
            this.workers[currentIndex].iterations += 1;
            iterationsRemaining--;
            currentIndex++;
            if (currentIndex + 1 === workers) {
                currentIndex = 0;
            }
        }
    }
    setupWorker(worker) {
        this.workers.push({
            worker,
            online: false,
            iterations: 1,
            status: 'initializing',
        });
        worker.on('message', this._messageHandler.bind(this, worker));
    }
    _messageHandler(worker, message) {
        switch (message.cmd) {
            case 'online':
                this.setOnline(worker);
                break;
            case 'result':
                this.setRunResult(worker, message);
                break;
            case 'error':
                this.setRunError(worker, message);
                break;
        }
    }
    _exitHandler(worker) {
        const info = this.workers.find((i) => i.worker === worker);
        if (!info) {
            return;
        }
        this.finishWhenReady();
    }
    setOnline(worker) {
        const info = this.workers.find((i) => i.worker === worker);
        if (!info) {
            return;
        }
        info.online = true;
        info.status = 'ready';
        this.runWhenReady();
        this.emit('status', this.getStatusWorkers());
    }
    setRunResult(worker, message) {
        const reports = message.data;
        this.executed = this.executed.concat(reports);
        worker.destroy();
        const info = this.workers.find((i) => i.worker === worker);
        if (!info) {
            return;
        }
        info.status = 'finished';
        this.emit('status', this.getStatusWorkers());
    }
    runWhenReady() {
        const waiting = this.workers.some((i) => !i.online);
        if (waiting) {
            return;
        }
        this.startTime = Date.now();
        this.workers.forEach((info) => {
            const opts = { ...this.options, project: this.project.toJSON() };
            opts.iterations = info.iterations;
            info.status = 'running';
            info.worker?.send({
                cmd: 'run',
                data: opts,
            });
        });
    }
    async finishWhenReady() {
        if (this.endTime) {
            return;
        }
        const working = this.workers.some((i) => !['finished', 'error'].includes(i.status));
        if (working || !this.mainResolver) {
            return;
        }
        this.endTime = Date.now();
        const report = await this.createReport();
        this.mainResolver(report);
        this.mainResolver = undefined;
        this.mainRejecter = undefined;
        cluster.off('exit', this._exitHandler);
        this._state = State.Idle;
    }
    setRunError(worker, message) {
        worker.destroy();
        const info = this.workers.find((i) => i.worker === worker);
        if (!info) {
            return;
        }
        info.status = 'error';
        info.message = message.data;
        this.emit('status', this.getStatusWorkers());
    }
}
//# sourceMappingURL=ProjectParallelRunner.js.map