import process from 'node:process';
import Rx from 'rxjs';
export class Command {
    killProcess;
    spawn;
    spawnOpts;
    index;
    /** @inheritdoc */
    name;
    /** @inheritdoc */
    command;
    /** @inheritdoc */
    prefixColor;
    /** @inheritdoc */
    env;
    /** @inheritdoc */
    cwd;
    /** @inheritdoc */
    ipc;
    close = new Rx.Subject();
    error = new Rx.Subject();
    stdout = new Rx.Subject();
    stderr = new Rx.Subject();
    timer = new Rx.Subject();
    /**
     * A stream of changes to the `#state` property.
     *
     * Note that the command never goes back to the `stopped` state, therefore it's not a value
     * that's emitted by this stream.
     */
    stateChange = new Rx.Subject();
    messages = {
        incoming: new Rx.Subject(),
        outgoing: new Rx.ReplaySubject(),
    };
    process;
    // TODO: Should exit/error/stdio subscriptions be added here?
    subscriptions = [];
    stdin;
    pid;
    killed = false;
    exited = false;
    state = 'stopped';
    constructor({ index, name, command, prefixColor, env, cwd, ipc }, spawnOpts, spawn, killProcess) {
        this.index = index;
        this.name = name;
        this.command = command;
        this.prefixColor = prefixColor;
        this.env = env || {};
        this.cwd = cwd;
        this.ipc = ipc;
        this.killProcess = killProcess;
        this.spawn = spawn;
        this.spawnOpts = spawnOpts;
    }
    /**
     * Starts this command, piping output, error and close events onto the corresponding observables.
     */
    start() {
        const child = this.spawn(this.command, this.spawnOpts);
        this.changeState('started');
        this.process = child;
        this.pid = child.pid;
        const startDate = new Date(Date.now());
        const highResStartTime = process.hrtime();
        this.timer.next({ startDate });
        this.subscriptions = [...this.maybeSetupIPC(child)];
        Rx.fromEvent(child, 'error').subscribe((event) => {
            this.cleanUp();
            const endDate = new Date(Date.now());
            this.timer.next({ startDate, endDate });
            this.error.next(event);
            this.changeState('errored');
        });
        Rx.fromEvent(child, 'close')
            .pipe(Rx.map((event) => event))
            .subscribe(([exitCode, signal]) => {
            this.cleanUp();
            // Don't override error event
            if (this.state !== 'errored') {
                this.changeState('exited');
            }
            const endDate = new Date(Date.now());
            this.timer.next({ startDate, endDate });
            const [durationSeconds, durationNanoSeconds] = process.hrtime(highResStartTime);
            this.close.next({
                command: this,
                index: this.index,
                exitCode: exitCode ?? String(signal),
                killed: this.killed,
                timings: {
                    startDate,
                    endDate,
                    durationSeconds: durationSeconds + durationNanoSeconds / 1e9,
                },
            });
        });
        if (child.stdout) {
            pipeTo(Rx.fromEvent(child.stdout, 'data').pipe(Rx.map((event) => event)), this.stdout);
        }
        if (child.stderr) {
            pipeTo(Rx.fromEvent(child.stderr, 'data').pipe(Rx.map((event) => event)), this.stderr);
        }
        this.stdin = child.stdin || undefined;
    }
    changeState(state) {
        this.state = state;
        this.stateChange.next(state);
    }
    maybeSetupIPC(child) {
        if (!this.ipc) {
            return [];
        }
        return [
            pipeTo(Rx.fromEvent(child, 'message').pipe(Rx.map((event) => {
                const [message, handle] = event;
                return { message, handle };
            })), this.messages.incoming),
            this.messages.outgoing.subscribe((message) => {
                if (!child.send) {
                    return message.onSent(new Error('Command does not have an IPC channel'));
                }
                child.send(message.message, message.handle, message.options, (error) => {
                    message.onSent(error);
                });
            }),
        ];
    }
    /**
     * Sends a message to the underlying process once it starts.
     *
     * @throws  If the command doesn't have an IPC channel enabled
     * @returns Promise that resolves when the message is sent,
     *          or rejects if it fails to deliver the message.
     */
    send(message, handle, options) {
        if (this.ipc == null) {
            throw new Error('Command IPC is disabled');
        }
        return new Promise((resolve, reject) => {
            this.messages.outgoing.next({
                message,
                handle,
                options,
                onSent(error) {
                    if (error) {
                        reject(error);
                    }
                    else {
                        resolve();
                    }
                },
            });
        });
    }
    /**
     * Kills this command, optionally specifying a signal to send to it.
     */
    kill(code) {
        if (Command.canKill(this)) {
            this.killed = true;
            this.killProcess(this.pid, code);
        }
    }
    cleanUp() {
        this.subscriptions?.forEach((sub) => sub.unsubscribe());
        this.messages.outgoing = new Rx.ReplaySubject();
        this.process = undefined;
    }
    /**
     * Detects whether a command can be killed.
     *
     * Also works as a type guard on the input `command`.
     */
    static canKill(command) {
        return !!command.pid && !!command.process;
    }
}
/**
 * Pipes all events emitted by `stream` into `subject`.
 */
function pipeTo(stream, subject) {
    return stream.subscribe((event) => subject.next(event));
}
