// *****************************************************************************
// Copyright (C) 2017 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************

import * as path from 'path';
import * as fs from 'fs-extra';
import * as cp from 'child_process';
import { ApplicationPackage } from '@theia/application-package';

export class ApplicationProcess {

    protected readonly defaultOptions = {
        cwd: this.pck.projectPath,
        env: process.env
    };

    constructor(
        protected readonly pck: ApplicationPackage,
        protected readonly binProjectPath: string
    ) { }

    spawn(command: string, args?: string[], options?: cp.SpawnOptions): cp.ChildProcess {
        // Build a single quoted command line rather than passing an args array together with `shell: true`.
        // Node.js 24 deprecates the latter (DEP0190) because it does not escape the arguments.
        const commandLine = this.buildCommandLine(command, args || []);
        return cp.spawn(commandLine, [], Object.assign({}, this.defaultOptions, {
            ...options,
            shell: true
        }));
    }

    protected buildCommandLine(command: string, args: string[]): string {
        return [command, ...args].map(arg => this.quoteArg(arg)).join(' ');
    }

    protected quoteArg(arg: string): string {
        if (process.platform === 'win32') {
            // cmd.exe: wrap in double quotes and escape embedded double quotes by doubling them.
            return `"${arg.replace(/"/g, '""')}"`;
        }
        // POSIX shell: single-quote and escape embedded single quotes (' becomes '\'').
        return `'${arg.replace(/'/g, '\'\\\'\'')}'`;
    }

    fork(modulePath: string, args?: string[], options?: cp.ForkOptions): cp.ChildProcess {
        return cp.fork(modulePath, args, Object.assign({}, this.defaultOptions, options));
    }

    canRun(command: string): boolean {
        const binPath = this.resolveBin(this.binProjectPath, command);
        return !!binPath && fs.existsSync(binPath);
    }

    run(command: string, args: string[], options?: cp.SpawnOptions): Promise<void> {
        const commandProcess = this.spawnBin(command, args, options);
        return this.promisify(command, commandProcess);
    }

    spawnBin(command: string, args: string[], options?: cp.SpawnOptions): cp.ChildProcess {
        const binPath = this.resolveBin(this.binProjectPath, command);
        if (!binPath) {
            throw new Error(`Could not resolve ${command} relative to ${this.binProjectPath}`);
        }
        return this.spawn(binPath, args, options);
    }

    protected resolveBin(rootPath: string, command: string): string | undefined {
        let commandPath = path.resolve(rootPath, 'node_modules', '.bin', command);
        if (process.platform === 'win32') {
            commandPath = commandPath + '.cmd';
        }
        if (fs.existsSync(commandPath)) {
            return commandPath;
        }
        const parentDir = path.dirname(rootPath);
        if (parentDir === rootPath) {
            return undefined;
        }
        return this.resolveBin(parentDir, command);
    }

    promisify(command: string, p: cp.ChildProcess): Promise<void> {
        return new Promise((resolve, reject) => {
            p.stdout!.on('data', data => this.pck.log(data.toString()));
            p.stderr!.on('data', data => this.pck.error(data.toString()));
            p.on('error', reject);
            p.on('close', (code, signal) => {
                if (signal) {
                    reject(new Error(`${command} exited with an unexpected signal: ${signal}.`));
                    return;
                }
                if (code === 0) {
                    resolve();
                } else {
                    reject(new Error(`${command} exited with an unexpected code: ${code}.`));
                }
            });
        });
    }

}
