let { todo, sleep } = require("./util.js")

const { Subprocess, SpawnInfo, PipeID, ReadPipeRequest, WritePipeRequest } = require("../../lib/subprocess/index.js")
const { Readable, Writable } = require('readable-stream');
const { EventEmitter } = require("events")

const { promisify } = require("util");

const M = new Subprocess()

function waitEvent(e, name) {
  return new Promise((resolve)=>{
    e.on(name, resolve)
  })
}

const MAX_BUFFER = 1024 * 1024;
const kIsUsedAsStdio = Symbol('kIsUsedAsStdio');

function validateTimeout(timeout) {
  if (timeout != null && !(Number.isInteger(timeout) && timeout >= 0)) {
    throw new ERR_OUT_OF_RANGE('timeout', 'an unsigned integer', timeout);
  }
}


function validateMaxBuffer(maxBuffer) {
  if (maxBuffer != null && !(typeof maxBuffer === 'number' && maxBuffer >= 0)) {
    throw new ERR_OUT_OF_RANGE('options.maxBuffer',
                               'a positive number',
                               maxBuffer);
  }
}


function flushStdio(subprocess) {
  const stdio = subprocess.stdio;
  if (stdio == null) return;

  for (let i = 0; i < stdio.length; i++) {
    const stream = stdio[i];
    // TODO(snyh): support stdio
    if (!stream || !stream.readable) { // || stream[kIsUsedAsStdio]) {
      continue;
    }
    stream.resume();
  }
}

class ChildProcess extends EventEmitter {
  constructor(command, args = [], options) {
    super();

    this.pid = -1;
    this.exitCode = 0;
    this.killed = false;
    this.connected = false;
    this.signalCode = null;

    this.spawnfile = command;
    this.spawnargs = args;
    this._env = options.env;

    //this.stdio = TODO

    this._handle = new Promise((resolve) => {
      this._handle_resolve = resolve;
    })

    this.stdio = [
      new ProcessWriteStream(this._handle, 0),
      new ProcessReadStream(this._handle, 1),
      new ProcessReadStream(this._handle, 2),
    ]
    this.stdin = this.stdio[0]
    this.stdout = this.stdio[1]
    this.stderr = this.stdio[2]
    for (let i=0;i<3;i++) this.stdio[i][kIsUsedAsStdio] = true;

    const env = this._env || [];
    const tty = false;
    const cwd = null;

    let sinfo = new SpawnInfo()
    sinfo.setCommand(this.spawnfile)
    sinfo.setWorkdir(cwd)
    sinfo.setTty(options.tty)

    if (options.shell) {
      if (typeof options.shell === String) {
	sinfo.setShell(options.shell)
      }  else {
	sinfo.setShell("/bin/sh")
      }
    }
    if (tty) {
      sinfo.setTty(true)
    }

    for (let i=0; i<env.length; i++) {
      sinfo.addEnvironment(env[i])
    }
    for (let i=0; i<args.length; i++) {
      sinfo.addArguments(args[i])
    }

    this._start(sinfo)
  }

  async _start(sinfo) {
    try {
      await M.Spawn(sinfo, (handle) =>{
        this._handle_resolve(handle)
      });
      this.exitCode = 0;
    } catch (e) {
      this.exitCode = 1;
    }

    this.emit("exit", this.exitCode, null)

    process.nextTick(flushStdio, this);

    // To wait stdio terminated and then emit ChildProcess's close event
    Promise.all([
      // waitEvent(this.stdio[0], "end"),
      waitEvent(this.stdio[1], "end"),
      waitEvent(this.stdio[2], "end"),
    ]).then(()=>{
      this.emit("close", this.exitCode, null)
    })
  }

  async kill(code) {
    await M.kill(await this._handle)
    this.killed = true;
  }
}

class ProcessWriteStream extends Writable {
  constructor(pending_pid, num) {
    super({highWaterMark:4096, autoDestroy:true})

    this.__init__ = new Promise(async (resolve)=>{
      let p = new PipeID();
      p.setPid(await pending_pid)
      p.setFd(num)
      this._pipe = p;
      resolve();
    })
  }

  async _write(chunk, encoding, callback) {
    await this.__init__;

    const req = new WritePipeRequest();
    req.setPipe(this._pipe);

    if (chunk instanceof String) {
      req.setData((new TextEncoder(encoding).encode(chunk)))
    } else {
      if (chunk.constructor != Uint8Array) {
        req.setData(new Uint8Array(chunk))
      } else {
        req.setData(chunk)
      }
    }

    try {
      await M.WritePipe(req)
      callback()
    } catch (e) {
      callback(e)
    }
  }

  async _final(callback) {
    await this.__init__;

    try {
      await M.ClosePipe(this._pipe)
      callback()
    } catch (e) {
      callback(e)
    }
  }
}

class ProcessReadStream extends Readable {
  constructor(pending_pid, num) {
    super({highWaterMark:4096, autoDestroy:true})

    this.__init__ = new Promise(async (resolve)=>{
      let p = new PipeID();
      p.setPid(await pending_pid)
      p.setFd(num)
      this._pipe = p;
      resolve();
    })
  }

  async _read(size) {
    if (this._running) {
      return
    }
    this._running = true;
    await this.__init__;

    const req = new ReadPipeRequest();
    req.setPipe(this._pipe)
    req.setSize(size);
    try {
      let ok = false;
      do {
        ok = false;
        let source = await M.ReadPipe(req);
        const data = source.getData()
        if (data.length > 0) {
          ok = this.push(data);
        } else {
          ok = false;
          this.push(null);
        }
      } while (ok);
    } catch (e) {
      throw(e)
    }
  }

  async readAll() {
    await this.__init__;

    let result = '';
    for await (const chunk of this) {
      result += chunk;
    }
    return result;
  }
}


function spawn(command, args=[], options={}) {
  return new ChildProcess(command, args, options);
}

function normalizeExecArgs(command, options, callback) {
  if (typeof options === 'function') {
    callback = options;
    options = undefined;
  }

  // Make a shallow copy so we don't clobber the user's options object.
  options = { ...options };
  options.shell = typeof options.shell === 'string' ? options.shell : true;

  return {
    file: command,
    options: options,
    callback: callback
  };
}

function exec(command, options, callback) {
  const opts = normalizeExecArgs(command, options, callback);
  return module.exports.execFile(opts.file,
                                 opts.options,
                                 opts.callback);
}

function execFile(file /* , args, options, callback */) {
  let args = [];
  let callback;
  let options;

  // Parse the optional positional parameters.
  let pos = 1;
  if (pos < arguments.length && Array.isArray(arguments[pos])) {
    args = arguments[pos++];
  } else if (pos < arguments.length && arguments[pos] == null) {
    pos++;
  }

  if (pos < arguments.length && typeof arguments[pos] === 'object') {
    options = arguments[pos++];
  } else if (pos < arguments.length && arguments[pos] == null) {
    pos++;
  }

  if (pos < arguments.length && typeof arguments[pos] === 'function') {
    callback = arguments[pos++];
  }

  if (!callback && pos < arguments.length && arguments[pos] != null) {
    throw new ERR_INVALID_ARG_VALUE('args', arguments[pos]);
  }

  options = {
    encoding: 'utf8',
    timeout: 0,
    maxBuffer: MAX_BUFFER,
    killSignal: 'SIGTERM',
    cwd: null,
    env: null,
    shell: false,
    ...options
  };

  // Validate the timeout, if present.
  validateTimeout(options.timeout);

  // Validate maxBuffer, if present.
  validateMaxBuffer(options.maxBuffer);

//  options.killSignal = sanitizeKillSignal(options.killSignal);

  const child = spawn(file, args, {
    cwd: options.cwd,
    env: options.env,
    gid: options.gid,
    uid: options.uid,
    shell: options.shell,
    windowsHide: !!options.windowsHide,
    windowsVerbatimArguments: !!options.windowsVerbatimArguments
  });

  let encoding;
  const _stdout = [];
  const _stderr = [];
  if (options.encoding !== 'buffer' && Buffer.isEncoding(options.encoding)) {
    encoding = options.encoding;
  } else {
    encoding = null;
  }
  let stdoutLen = 0;
  let stderrLen = 0;
  let killed = false;
  let exited = false;
  let timeoutId;

  let ex = null;

  let cmd = file;

  function exithandler(code, signal) {
    if (exited) return;
    exited = true;

    if (timeoutId) {
      clearTimeout(timeoutId);
      timeoutId = null;
    }

    if (!callback) return;

    // merge chunks
    let stdout;
    let stderr;
    if (encoding ||
      (
        child.stdout &&
        child.stdout.readableEncoding
      )) {
      stdout = _stdout.join('');
    } else {
      stdout = Buffer.concat(_stdout);
    }
    if (encoding ||
      (
        child.stderr &&
        child.stderr.readableEncoding
      )) {
      stderr = _stderr.join('');
    } else {
      stderr = Buffer.concat(_stderr);
    }

    if (!ex && code === 0 && signal === null) {
      callback(null, stdout, stderr);
      return;
    }

    if (args.length !== 0)
      cmd += ` ${args.join(' ')}`;

    if (!ex) {
      debugger;
      // eslint-disable-next-line no-restricted-syntax
      ex = new Error('Command failed: ' + cmd + '\n' + stderr);
      ex.killed = child.killed || killed;
      ex.code = code < 0 ? getSystemErrorName(code) : code;
      ex.signal = signal;
    }

    ex.cmd = cmd;
    callback(ex, stdout, stderr);
  }

  function errorhandler(e) {
    ex = e;

    if (child.stdout)
      child.stdout.destroy();

    if (child.stderr)
      child.stderr.destroy();

    exithandler();
  }

  function kill() {
    if (child.stdout)
      child.stdout.destroy();

    if (child.stderr)
      child.stderr.destroy();

    killed = true;
    try {
      child.kill(options.killSignal);
    } catch (e) {
      ex = e;
      exithandler();
    }
  }

  if (options.timeout > 0) {
    timeoutId = setTimeout(function delayedKill() {
      kill();
      timeoutId = null;
    }, options.timeout);
  }

  if (child.stdout) {
    if (encoding)
      child.stdout.setEncoding(encoding);
    child.stdout.on('data', function onChildStdout(chunk) {
      const encoding = child.stdout.readableEncoding;
      const length = encoding ?
        Buffer.byteLength(chunk, encoding) :
        chunk.length;
      stdoutLen += length;

      if (stdoutLen > options.maxBuffer) {
        const truncatedLen = options.maxBuffer - (stdoutLen - length);
        _stdout.push(chunk.slice(0, truncatedLen));

        ex = new ERR_CHILD_PROCESS_STDIO_MAXBUFFER('stdout');
        kill();
      } else {
        _stdout.push(chunk);
      }
    });
  }

  if (child.stderr) {
    if (encoding)
      child.stderr.setEncoding(encoding);

    child.stderr.on('data', function onChildStderr(chunk) {
      const encoding = child.stderr.readableEncoding;
      const length = encoding ?
        Buffer.byteLength(chunk, encoding) :
        chunk.length;
      stderrLen += length;

      if (stderrLen > options.maxBuffer) {
        const truncatedLen = options.maxBuffer - (stderrLen - length);
        _stderr.push(chunk.slice(0, truncatedLen));

        ex = new ERR_CHILD_PROCESS_STDIO_MAXBUFFER('stderr');
        kill();
      } else {
        _stderr.push(chunk);
      }
    });
  }

  child.addListener('close', exithandler);
  child.addListener('error', errorhandler);

  return child;
}

const util = require("util")
exec[util.promisify.custom] = (command, opt) => {
  let p = new Promise((resolve, reject) => {
    try {
      const child = exec(command, opt, (err, stdout, stderr) => {
	p.child = child;
	if (err) {
	  reject(err)
	} else {
	  resolve({stdout, stderr})
	}
      });
    } catch (e) {
      reject(e, stdout, stderr);
    }
  })
  return p;
}
execFile[util.promisify.custom] = (file, args, opt) => {
  let p = new Promise((resolve, reject) => {
    try {
      const child = exec(file, args, opt, (err, stdout, stderr) => {
	p.child = child;
	if (err) {
	  reject(err)
	} else {
	  resolve({stdout, stderr})
	}
      });
    } catch (e) {
      reject(e, stdout, stderr);
    }
  })
  return p;
}


module.exports = todo("child_process", {
  spawn,
  exec: exec,
  execFile: execFile,
})
