const isWindows =
  process.platform === 'win32' ||
  process.env.OSTYPE === 'cygwin' ||
  process.env.OSTYPE === 'msys';

const path = require('path');
const COLON = isWindows ? ';' : ':';
const isexe = require('isexe');

const getNotFoundError = (cmd) =>
  Object.assign(new Error(`not found: ${cmd}`), { code: 'ENOENT' });

const getPathInfo = (cmd, opt) => {
  const colon = opt.colon || COLON;

  // If it has a slash, then we don't bother searching the pathenv.
  // just check the file itself, and that's it.
  const pathEnv =
    cmd.match(/\//) || (isWindows && cmd.match(/\\/)) ?
      ['']
    : [
        // windows always checks the cwd first
        ...(isWindows ? [process.cwd()] : []),
        ...(
          opt.path ||
          process.env.PATH ||
          /* istanbul ignore next: very unusual */ ''
        ).split(colon),
      ];
  const pathExtExe =
    isWindows ?
      opt.pathExt || process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM'
    : '';
  const pathExt = isWindows ? pathExtExe.split(colon) : [''];

  if (isWindows) {
    if (cmd.indexOf('.') !== -1 && pathExt[0] !== '') pathExt.unshift('');
  }

  return {
    pathEnv,
    pathExt,
    pathExtExe,
  };
};

const which = (cmd, opt, cb) => {
  if (typeof opt === 'function') {
    cb = opt;
    opt = {};
  }
  if (!opt) opt = {};

  const { pathEnv, pathExt, pathExtExe } = getPathInfo(cmd, opt);
  const found = [];

  const step = (i) =>
    new Promise((resolve, reject) => {
      if (i === pathEnv.length)
        return opt.all && found.length ?
            resolve(found)
          : reject(getNotFoundError(cmd));

      const ppRaw = pathEnv[i];
      const pathPart = /^".*"$/.test(ppRaw) ? ppRaw.slice(1, -1) : ppRaw;

      const pCmd = path.join(pathPart, cmd);
      const p =
        !pathPart && /^\.[\\\/]/.test(cmd) ? cmd.slice(0, 2) + pCmd : pCmd;

      resolve(subStep(p, i, 0));
    });

  const subStep = (p, i, ii) =>
    new Promise((resolve, reject) => {
      if (ii === pathExt.length) return resolve(step(i + 1));
      const ext = pathExt[ii];
      isexe(p + ext, { pathExt: pathExtExe }, (er, is) => {
        if (!er && is) {
          if (opt.all) found.push(p + ext);
          else return resolve(p + ext);
        }
        return resolve(subStep(p, i, ii + 1));
      });
    });

  return cb ? step(0).then((res) => cb(null, res), cb) : step(0);
};

const whichSync = (cmd, opt) => {
  opt = opt || {};

  const { pathEnv, pathExt, pathExtExe } = getPathInfo(cmd, opt);
  const found = [];

  for (let i = 0; i < pathEnv.length; i++) {
    const ppRaw = pathEnv[i];
    const pathPart = /^".*"$/.test(ppRaw) ? ppRaw.slice(1, -1) : ppRaw;

    const pCmd = path.join(pathPart, cmd);
    const p =
      !pathPart && /^\.[\\\/]/.test(cmd) ? cmd.slice(0, 2) + pCmd : pCmd;

    for (let j = 0; j < pathExt.length; j++) {
      const cur = p + pathExt[j];
      try {
        const is = isexe.sync(cur, { pathExt: pathExtExe });
        if (is) {
          if (opt.all) found.push(cur);
          else return cur;
        }
      } catch (ex) {}
    }
  }

  if (opt.all && found.length) return found;

  if (opt.nothrow) return null;

  throw getNotFoundError(cmd);
};

module.exports = which;
which.sync = whichSync;