// LICENSE_CODE ZON
'use strict'; /*jslint node:true*/
require('./config.js');
const etask = require('./etask.js');
const file = require('./file.js');
const string = require('./string.js');
const array = require('./array.js');
const child_process = require('child_process');
const fs = require('fs');
const E = exports;
const assign = Object.assign;

E.process_opt = opt=>{
    opt = assign({}, opt);
    if (opt.out=='stdall')
        opt.stdall = true;
    if (opt.stdall)
        opt.stdout = opt.stderr = true;
    if (!opt.out && (opt.stdout || opt.stderr))
        opt.out = 'obj';
    if (!opt.out)
        opt.out = 'unix';
    if (opt.out=='stdout')
        opt.stdout = true;
    if (opt.out=='stderr')
        opt.stderr = true;
    // XXX sergey: must use pipes only on required ios
    if (opt.stdin || opt.stdout || opt.stderr)
        opt.piped = true;
    return opt;
};
E.get_cmd = (cmd, opt)=>{
    let args = array.copy(array.to_array(cmd));
    // XXX vladimir: rm process.zon from hutil
    if (opt.node && process.zon && process.zon.main)
        throw 'Unable to spawn node process from embedded Node';
    let command = opt.shell ? '/bin/bash' :
        opt.node ? process.argv[0] : args.shift();
    command = file.normalize(command);
    if (opt.shell)
        args = ['-c', args.join(' ')];
    else if (opt.node&&args.length)
        args[0] = file.normalize(args[0]);
    if (opt.node&&opt.node_args)
        args.unshift(...opt.node_args);
    return {cmd: command, argv: args};
};
E.get_env = opt=>{
    if (!opt.opt||!opt.opt.env)
        return process.env;
    if (!opt.node)
        return opt.opt.env;
    let nave_env = {};
    string.qw`NAVEPATH NAVEBIN NAVELIB NAVEVERSION NAVENAME NAVE_DIR NAVE
        npm_config_binroot npm_config_root npm_config_manroot
        npm_config_prefix NODE_MODULES NODE_PATH`.forEach(v=>{
        nave_env[v] = process.env[v]; });
    return assign(nave_env, opt.opt.env);
};
E.get_redir = (input, piped, ipc)=>{
    let redir_modes = {'<': 'r', '>': 'w', '>>': 'a'};
    let outputs = [0, 1, 2], fds = [];
    input = input||'';
    if (piped)
        input = '0</dev/pipe 1>/dev/pipe 2>/dev/pipe '+input;
    let redirs = string.qw(input), match;
    for (let r=0; r<redirs.length; r++)
    {
        if (!(match = /^(\d*)(<|>|>>)(&(\d+)|[^>&].*)$/.exec(redirs[r])))
            throw new Error('Invalid redirection '+redirs[r]);
        let src, dst;
        src = match[1] ? +match[1] : match[2]=='<' ? 0 : 1;
        if (match[4]) // to fd
        {
            dst = +match[4];
            if (dst in outputs)
                dst = outputs[dst];
        }
        else // to file
        {
            dst = match[3];
            if (/^\/dev\/pipe$/.test(dst))
                dst += src; // remember src fd
            if (!/^\/dev\/(null|pipe)/.test(dst)) // real file
            {
                fds.push(dst = fs.openSync(dst, redir_modes[match[2]]));
            }
        }
        outputs[src] = dst;
    }
    let pipes = {};
    for (let i=0; i<outputs.length; i++)
    {
        let o = outputs[i], m;
        if (o=='/dev/null')
            o = 'ignore';
        else if (m = /^\/dev\/pipe(\d)$/.exec(o))
        {
            o = 'pipe';
            if (+m[1]!=i)
                pipes[i] = +m[1];
        }
        outputs[i] = o;
    }
    if (ipc)
        outputs.push('ipc');
    return {stdio: outputs, pipes: pipes, fds: fds};
};
E.get_redir_sync = (redir, stdio)=>{
    if (redir)
        return E.get_redir(redir);
    return {stdio: stdio||'pipe', fds: []};
};
function unshift_pipe(stdio, data){
    // XXX sergey: find out what causes stream to close
    if (stdio && !stdio._readableState.ended)
        stdio.unshift(data);
}
function handle_stdio(child, redir, logger){
    let io_names = ['stdin', 'stdout', 'stderr'];
    child.stdio.forEach((stdio, i)=>{
        if (i<1||!stdio)
           return;
        let method = !(i in redir) ? data=>logger(io_names[i], data) :
            data=>unshift_pipe(child.stdio[redir[i]], data);
        stdio.on('data', method);
    });
}
function close_fds(child, fds){
    fds.forEach(fs.closeSync);
    if (!child)
        return;
    child.stdio.forEach(stdio=>{
        if (stdio)
            stdio.destroy();
    });
}

// XXX vladimir: check about platforms other than x86/arm from signal(7)
const sigstring_int = {
    // posix.1-1990
    SIGHUP: 1,
    SIGINT: 2,
    SIGQUIT: 3,
    SIGILL: 4,
    SIGABRT: 6,
    SIGFPE: 8,
    SIGKILL: 9,
    SIGSEGV: 11,
    SIGPIPE: 13,
    SIGALRM: 14,
    SIGTERM: 15,
    SIGUSR1: 10,
    SIGUSR2: 12,
    SIGCHLD: 17,
    SIGCONT: 18,
    SIGSTOP: 19,
    SIGTSTP: 20,
    SIGTTIN: 21,
    SIGTTOU: 22,
    // posix.1-2001
    SIGBUS: 7,
    SIGPOLL: 29,
    SIGPROF: 27,
    SIGSYS: 31,
    SIGTRAP: 5,
    SIGURG: 23,
    SIGVTALRM: 26,
    SIGXCPU: 24,
    SIGXFSZ: 25,
    // various
    SIGIOT: 6,
    SIGSTKFLT: 16,
    SIGIO: 29,
    SIGPWR: 30,
    SIGINFO: 30,
    SIGWINCH: 28,
    SIGUNUSED: 31,
};
E.sys = (cmd, opt)=>etask(function*exec(){
    opt = E.process_opt(opt||{});
    // XXX vladimir: rm process.zon from hutil
    if (opt.fork && process.zon && process.zon.main)
        throw 'Unable to spawn node process from embedded Node';
    cmd = E.get_cmd(cmd, opt);
    let status = {signal: 0, code: 0};
    let log = {stdout: '', stderr: '', stdall: ''};
    let kill_signal = opt.kill_signal||'SIGINT';
    let child;
    let redir_str = opt.redir||'';
    this.info.cmd = ()=>cmd.cmd+' '+cmd.argv.join(' ')+redir_str;
    if (opt.verbose)
        console.log(cmd.cmd, cmd.argv.join(' '), redir_str);
    let redir = E.get_redir(redir_str, opt.piped, opt.fork);
    let opts = assign({stdio: redir.stdio, cwd: opt.cwd}, opt.opt);
    this.finally(()=>{
        close_fds(child, redir.fds);
        if (child && !child.killed && child.exitCode===null)
            child.kill(kill_signal);
    });
    let error;
    let on_error = err=>{
        error = err;
        this.continue();
    };
    // To kill children of process it needed to be run in detached state
    if (opt.timeout||opt.detached)
        opts.detached = true;
    opts.env = E.get_env(opt);
    if (opt.fork)
        child = child_process.fork(cmd.cmd, cmd.argv, opts);
    else
        child = child_process.spawn(cmd.cmd, cmd.argv, opts);
    child.on('error', on_error);
    child.on('close', (code, signal)=>{
        status.code = code;
        status.signal = signal;
        this.continue();
    });
    if (opt.on_msg)
        child.on('message', msg=>opt.on_msg(child, msg));
    if (opt.on_disc)
        child.on('disconnect', msg=>opt.on_disc(child));
    if (opt.timeout||opt.detached)
        this.child_process = child;
    // Process stdio redirection
    let logger = (name, data)=>{
        if (opt[name])
            log[name] += data;
        if (opt.stdall)
            log.stdall += data;
        if (opt.log)
            opt.log(data, name);
    };
    handle_stdio(child, redir.pipes, logger);
    if (opt.stdin&&child.stdio[0])
    {
        child.stdio[0].end(opt.stdin);
        child.stdio[0].once('error', on_error);
    }
    if (opt.timeout)
        this.alarm(opt.timeout, on_error.bind(null, 'timeout'));
    yield this.wait();
    if (error==='timeout' && !child.killed)
        process.kill(-child.pid);
    let rv = error ? -1 : status.code;
    let ret = {stdout: log.stdout, stderr: log.stderr, stdall: log.stdall,
        signal: status.signal, retval: rv&0xff, error: error,
        status_code: status.code};
    ret.unix = error ? -257 :
        ret.signal ? -(sigstring_int[ret.signal]||258) : ret.retval;
    return opt.out=='retval' ? ret.retval :
        opt.out=='unix' ? ret.unix :
        opt.out=='stdout' ? ret.stdout :
        opt.out=='stderr' ? ret.stderr :
        opt.out=='stdall' ? ret.stdall :
        ret;
});

E.sys_sync = (cmd, opt)=>{
    opt = E.process_opt(opt||{});
    cmd = E.get_cmd(cmd, opt);
    if (opt.verbose)
        console.log(cmd.cmd, cmd.argv.join(' '));
    let redir = E.get_redir_sync(opt.redir, opt.stdio);
    let opts = assign({stdio: redir.stdio, cwd: opt.cwd}, opt.opt);
    opts.env = E.get_env(opt);
    let child = child_process.spawnSync(cmd.cmd, cmd.argv, opts);
    close_fds(null, redir.fds);
    let rv = child.error ? -1 : child.status;
    // XXX sergey: stdall must be mixed, not concatinated
    let stdout = (child.stdout||'').toString();
    let stderr = (child.stderr||'').toString();
    let ret = {stdout: stdout, stderr: stderr, stdall: stdout+stderr,
        signal: child.signal, retval: rv&0xff, error: child.error,
        status_code: child.status};
    ret.unix = ret.error ? -257 :
        ret.signal ? -(sigstring_int[ret.signal]||258) : ret.retval;
    return opt.out=='retval' ? ret.retval :
        opt.out=='unix' ? ret.unix :
        opt.out=='stdout' ? stdout :
        opt.out=='stderr' ? stderr :
        opt.out=='stdall' ? stdout+stderr :
        ret;
};

let which_cache = {};
E.which = bin=>{
    let cache = which_cache;
    if (bin in cache && file.is_exec(cache[bin]))
        return cache[bin];
    let ret = file.which(bin)||'';
    let sys_opts = {out: 'stdout'};
    if (ret&&file.is_win)
        ret = string.chomp(E.sys_sync(['cygpath', '-w', ret], sys_opts));
    if (ret&&file.is_win&&file.is_symlink(ret))
        ret = fs.readlinkSync(ret);
    cache[bin] = ret;
    return ret;
};

E.get = (cmd, opt)=>{
    opt = assign(Array.isArray(cmd) ? {} :
         {shell: true, stdio: 'inherit'}, opt);
    if (opt.dry_run)
    {
        if (opt.verbose)
            console.log(array.to_array(cmd).join(' '));
        return;
    }
    return E.sys_sync(cmd, opt);
};
E.get_lines = (cmd, opt)=>{
     let ret = E.get(cmd, assign({out: 'stdout', stdio: 'pipe'}, opt));
     return string.split_crlf(ret);
};
E.get_line = (cmd, opt)=>E.get_lines(cmd, opt)[0];
