import path from '../paths/path.js';
import dir from '../paths/dir.js';
import Debug, { DebugThread } from '../meta/Debug.js';

declare const Neutralino;

type ProcessOptions = {
    /**
     * Environment variables
     */
    env?: object;

    /**
     * Current working directory for the running process
     */
    cwd?: string;

    /**
     * Interval between tries to find started process id
     * 
     * @default 50
     */
    childInterval?: number;
};

class Process
{
    /**
     * Process ID
     */
    public readonly id: number;

    /**
     * Interval in ms between process status update
     * 
     * null if you don't want to update process status
     * 
     * @default 200
     */
    public runningInterval: number|null = 200;

    /**
     * Interval in ms between process output update
     * 
     * null if you don't want to update process output
     * 
     * @default 500
     */
    public outputInterval: number|null = 500;

    protected outputFile: string|null;
    protected outputOffset: number = 0;

    protected _finished: boolean = false;

    /**
     * Whether the process was finished
     */
    public get finished(): boolean
    {
        return this._finished;
    };

    protected onOutput?: (output: string, process: Process) => void;
    protected onFinish?: (process: Process) => void;

    public constructor(pid: number, outputFile: string|null = null)
    {
        this.id = pid;
        this.outputFile = outputFile;

        const debugThread = new DebugThread('Process/Stream', `Opened process ${pid} stream`);

        const updateStatus = () => {
            this.running().then((running) => {
                // The process is still running
                if (running)
                {
                    if (this.runningInterval)
                        setTimeout(updateStatus, this.runningInterval);
                }

                // Otherwise the process was stopped
                else
                {
                    this._finished = true;

                    debugThread.log('Process stopped');

                    if (this.onFinish)
                        this.onFinish(this);
                }
            });
        };

        if (this.runningInterval)
            setTimeout(updateStatus, this.runningInterval);

        if (this.outputFile)
        {
            const updateOutput = () => {
                Neutralino.filesystem.readFile(this.outputFile)
                    .then((output: string) => {
                        if (this.onOutput)
                            this.onOutput(output.substring(this.outputOffset), this);

                        this.outputOffset = output.length;

                        if (this._finished)
                            Neutralino.filesystem.removeFile(this.outputFile);

                        else if (this.outputInterval)
                            setTimeout(updateOutput, this.outputInterval);
                    })
                    .catch(() => {
                        if (this.outputInterval && !this._finished)
                            setTimeout(updateOutput, this.outputInterval);
                    });
            };

            if (this.outputInterval)
                setTimeout(updateOutput, this.outputInterval);
        }
    }

    /**
     * Specify callback to run when the process will be finished
     */
    public finish(callback: (process: Process) => void)
    {
        this.onFinish = callback;

        if (this._finished)
            callback(this);

        // If user stopped process status auto-checking
        // then we should check it manually when this method was called
        else if (this.runningInterval === null)
        {
            this.running().then((running) => {
                if (!running)
                {
                    this._finished = true;

                    callback(this);
                }
            });
        }
    }

    public output(callback: (output: string, process: Process) => void)
    {
        this.onOutput = callback;
    }

    /**
     * Kill process
     */
    public kill(forced: boolean = false): Promise<void>
    {
        Neutralino.filesystem.removeFile(this.outputFile);

        return Process.kill(this.id, forced);
    }

    /**
     * Returns whether the process is running
     * 
     * This method doesn't call onFinish event
     */
    public running(): Promise<boolean>
    {
        return new Promise((resolve) => {
            Neutralino.os.execCommand(`ps -p ${this.id} -S`).then((output) => {
                resolve(output.stdOut.includes(this.id) && !output.stdOut.includes('Z   '));
            });
        });
    }

    /**
     * Run shell command
     */
    public static run(command: string, options: ProcessOptions = {}): Promise<Process>
    {
        return new Promise(async (resolve) => {
            const tmpFile = `${await dir.temp}/${10000 + Math.round(Math.random() * 89999)}.tmp`;

            // Set env variables
            if (options.env)
                for (const key of Object.keys(options.env))
                    command = `${key}="${path.addSlashes(options.env![key].toString())}" ${command}`;

            // Set output redirection to the temp file
            command = `${command} > "${path.addSlashes(tmpFile)}" 2>&1`;

            // Set current working directory
            if (options.cwd)
                command = `cd "${path.addSlashes(options.cwd)}" && ${command}`;

            // And run the command
            const process = await Neutralino.os.execCommand(command, {
                background: true
            });

            const childFinder = async () => {
                const childProcess = await Neutralino.os.execCommand(`pgrep -P ${process.pid}`);

                // Child wasn't found
                if (childProcess.stdOut == '')
                    setTimeout(childFinder, options.childInterval ?? 50);

                // Otherwise return its id
                else
                {
                    const processId = parseInt(childProcess.stdOut.substring(0, childProcess.stdOut.length - 1));

                    Debug.log({
                        function: 'Process.run',
                        message: {
                            'running command': command,
                            'cwd': options.cwd,
                            'initial process id': process.pid,
                            'real process id': processId,
                            ...options.env
                        }
                    });
        
                    resolve(new Process(processId, tmpFile));
                }
            };

            setTimeout(childFinder, options.childInterval ?? 50);
        });
    }

    public static kill(id: number, forced: boolean = false): Promise<void>
    {
        return new Promise((resolve) => {
            Neutralino.os.execCommand(`kill ${forced ? '-9' : '-15'} ${id}`).then(() => resolve());
        });
    }
}

export type { ProcessOptions };

export default Process;
