const fs = require('fs');
const path = require('path');
const os = require('os');
const axios = require('axios/index');
const exec = require('child_process').exec;
const execFile = require('child_process').execFile;
const spawn = require('child_process').spawn;
const Constants = require('./constants');
const RandomString = require('randomstring');

let vcRuntimeExists;
const _vcRuntimeExists = () => {
  return new Promise((resolve, reject) => {
    if (os.platform() !== 'win32') {
      reject('Windows OS is not being used');
    } else {
      if (vcRuntimeExists) {
        resolve(true);
      } else {
        const cmd = path.join(process.env.windir, 'system32', 'reg.exe');
        const args = ['QUERY', 'HKLM\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall'];
        _execProcessGetOutput(cmd, null, args)
        .then(lines => {
          const parseLine = index => {
            if (index < lines.length) {
              const line = lines[index];
              if (line.startsWith('HKEY_LOCAL_MACHINE\\')) {
                let args2 = JSON.parse(JSON.stringify(args));
                args2[1] = 'HKLM\\' + line.substr(19);
                args2.push('/v');
                args2.push('DisplayName');
                args2.push('/t');
                args2.push('REG_SZ');
                _execProcessGetOutput(cmd, null, args2)
                .then(lines => {
                  const value = lines[2].trim().substr(args2[3].length).trim().substr(6).trim();
                  if (value.includes('Microsoft Visual C++ 2015-2019 Redistributable (x64)')) {
                    vcRuntimeExists = true;
                    resolve(true);
                  } else {
                    parseLine(++index);
                  }
                })
                .catch(() => {
                  parseLine(++index);
                });
              } else {
                parseLine(++index);
              }
            } else {
              resolve(false);
            }
          };
          parseLine(0);
        })
        .catch(err => {
          reject(err);
        });
      }
    }
  });
};

// https://stackoverflow.com/questions/19531453/transform-file-directory-structure-into-tree-in-javascript
const _createTreeNodes = fileList => {
  let tree = {}

  const directorySort = (a, b) => {
    return !!a.directory === !!b.directory ? a.name.localeCompare(b.name) : a.directory ? -1 : 1;
  };

  const addNode = obj => {
    let fullPath;
    const idx = obj.skylink.indexOf('/');
    if (idx > -1) {
      fullPath = path.join(obj.directory, obj.skylink.substr(idx + 1)).replace(/\\/g, '/');
    } else {
      fullPath = path.join(obj.directory, obj.filename).replace(/\\/g, '/');
    }
    const pathParts = fullPath.replace(/^\/|\/$/g, '').split('/');
    let ptr = tree;
    for (let i = 0; i < pathParts.length; i++) {
      const node = {
        directory: true,
        name: pathParts[i],
      };
      if (i === pathParts.length - 1) {
        node.directory = false;
        node.path = fullPath;
      }
      ptr[pathParts[i]] = ptr[pathParts[i]] || node;
      ptr[pathParts[i]].children = ptr[pathParts[i]].children || {};
      ptr = ptr[pathParts[i]].children;
    }
  }

  const objectToArray = node => {
    Object.keys(node || {}).map((k) => {
      if (node[k].children) {
        objectToArray(node[k])
      }
    })
    if (node.children) {
      node.children = Object.values(node.children);
      node.children.forEach(objectToArray)
      node.children = node.children.sort(directorySort);
    }
  }

  fileList.map(addNode);
  objectToArray(tree);
  return Object.values(tree).sort(directorySort);
};

const _exportAllSkylinks = version => {
  return new Promise((resolve, reject) => {
    const repertoryExec = _getRepertoryExec(version);
    const processOptions = {
      cwd: repertoryExec.working,
      detached: true,
      shell: false,
      windowsHide: true,
    };

    const args = _getDefaultRepertoryArgs('Skynet');
    args.push('-ea');

    let result = '';
    const process = new spawn(repertoryExec.cmd, args, processOptions);

    process.on('error', (err) => {
      reject(err);
    });

    process.stdout.on('data', (d) => {
      result += d;
    });

    process.stderr.on('data', (d) => {
      result += d;
    });

    process.on('exit', code => {
      if (code === 0) {
        result = result.substr(result.indexOf('{'));
        resolve(JSON.parse(result));
      } else {
        reject(new Error('Failed to import: ' + code + ':' + result));
      }
    });

    process.unref();
  });
};

const _executeProcess = (command, working, args = []) => {
  return new Promise((resolve, reject) => {
    let processOptions = {
      detached: true,
      shell: false,
    };
    if (working) {
      processOptions.cwd = working;
    }

    const process = new spawn(command, args, processOptions);
    const pid = process.pid;

    process.on('error', (err) => {
      reject(err, pid);
    });

    process.on('exit', (code) => {
      resolve(code);
    });

    process.unref();
  });
};

const _execProcessGetOutput = (cmd, working, args) => {
  return new Promise((resolve, reject) => {
    let processOptions = {
      env: process.env,
      stdio: ['ignore', 'pipe', 'pipe']
    };
    if (working) {
      processOptions.cwd = working;
    }
    const proc = spawn(cmd, args, processOptions);

    let output;
    proc.stdout.on('data', data => {
      output += data.toString();
    });

    proc.on('error', (err) => {
      reject(err);
    });

    proc.on('exit', () => {
      const lines = output
      .replace(/\r\n/g, '\n')
      .split('\n');
      resolve(lines);
    });

    proc.unref();
  });
};

const _getDataDirectory = () => {
  return _resolvePath(Constants.DATA_LOCATIONS[os.platform()]);
};

const _getDefaultRepertoryArgs = (provider, remote, s3) => {
  const providerLower = provider.toLowerCase();
  const args = [];
  if (s3) {
    args.push('-s3');
    args.push('-na');
    args.push(provider.substr(2));
  } else if (remote) {
    args.push('-rm');
    args.push(provider.substr(6));
  } else if (Constants.PROVIDER_ARG[providerLower] && (Constants.PROVIDER_ARG[providerLower].length > 0)) {
    args.push(Constants.PROVIDER_ARG[providerLower]);
  }
  return args;
};

const _getRepertoryExec = version => {
  return {
    cmd: (os.platform() === 'win32') ? 'repertory.exe' : './repertory',
    working: path.join(_getDataDirectory(), version),
  };
};

const _removeDirectoryRecursively = dir => {
  if (fs.existsSync(dir)) {
    fs
    .readdirSync(dir)
    .forEach(file => {
      const curPath = path.join(dir, file);
      if (fs.lstatSync(curPath).isDirectory()) {
        module.exports.removeDirectoryRecursively(curPath);
      } else {
        fs.unlinkSync(curPath);
      }
    });
    fs.rmdirSync(dir);
  }
};

const _resolvePath = str => {
  if (os.platform() === 'win32') {
    return str.replace(/%([^%]+)%/g, (_, n) => {
      return process.env[n];
    });
  } else {
    return str.replace('~', os.homedir());
  }
};

const _tryParse = (j, def) => {
  try {
    return JSON.parse(j);
  } catch (e) {
    return def;
  }
};

module.exports.checkDaemonVersion = (version, provider) => {
  return new Promise((resolve, reject) => {
    const repertoryExec = _getRepertoryExec(version);
    const processOptions = {
      cwd: repertoryExec.working,
      detached: true,
      shell: false,
      windowsHide: true,
    };

    const args = _getDefaultRepertoryArgs(provider);
    args.push('-cv');
    const process = new spawn(repertoryExec.cmd, args, processOptions);

    process.on('error', err => {
      reject(err);
    });

    process.on('exit', code => {
      resolve(code);
    });
    process.unref();
  });
};

module.exports.cleanupOldReleases = versionList => {
  return new Promise((resolve, reject) => {
    try {
      if (versionList && versionList.length > 0) {
        const dataDir = _getDataDirectory();
        const directoryList = fs
        .readdirSync(dataDir, {withFileTypes: true})
        .filter(dirent => dirent.isDirectory())
        .map(dirent => dirent);

        const removeList = directoryList
        .filter(dirent => !versionList.includes(dirent.name))
        .map(dirent => dirent.name);

        for (const dir of removeList) {
          try {
            _removeDirectoryRecursively(path.join(dataDir, dir));
          } catch (e) {
            console.log(e);
          }
        }
        resolve();
      }
    } catch (e) {
      reject(e);
      console.log(e);
    }
  });
};

module.exports.createSignatureFiles = (signature, publicKey) => {
  const fileName1 = RandomString.generate({
    length: 12,
    charset: 'alphabetic'
  });
  const fileName2 = RandomString.generate({
    length: 12,
    charset: 'alphabetic'
  });

  const signatureFile = path.join(os.tmpdir(), fileName1 + '.sig');
  const publicKeyFile = path.join(os.tmpdir(), fileName2 + '.pub');

  const buffer = Buffer.from(signature, 'base64');
  fs.writeFileSync(signatureFile, buffer);
  fs.writeFileSync(publicKeyFile, publicKey);

  return {
    PublicKeyFile: publicKeyFile,
    SignatureFile: signatureFile,
  };
};

module.exports.detectRepertoryMounts = (version, providerList) => {
  return new Promise((resolve, reject) => {
    let mountState = {};
    const defaultData = {};
    for (const provider of providerList) {
      defaultData[provider] = {
        Active: false,
        Location: '',
        PID: -1,
      };
    }
    const grabStatus = index => {
      if (index >= providerList.length) {
        resolve(mountState);
      } else {
        const provider = providerList[index];
        const repertoryExec = _getRepertoryExec(version);
        const processOptions = {
          cwd: repertoryExec.working,
          detached: true,
          shell: false,
          windowsHide: true,
        };

        const args = _getDefaultRepertoryArgs(provider,
          !Constants.PROVIDER_LIST.includes(provider) && provider.toLowerCase().startsWith('remote'),
          !Constants.PROVIDER_LIST.includes(provider) && provider.toLowerCase().startsWith('s3'));
        args.push('-status');

        const process = new spawn(repertoryExec.cmd, args, processOptions);
        let result = '';

        process.on('error', (err) => {
          reject(err);
        });

        process.stdout.on('data', (d) => {
          result += d;
        });

        process.on('exit', () => {
          mountState[provider] = _tryParse(result, defaultData)[provider] || defaultData;
          if (mountState[provider].Active &&
            ((mountState[provider].Location === 'elevating') || (mountState[provider].Location === ''))) {
            setTimeout(() => {
              grabStatus(index);
            }, 2000);
          } else {
            grabStatus(++index);
          }
        });
        process.unref();
      }
    };
    grabStatus(0);
  });
};

module.exports.downloadFile = (url, destination, progressCallback, completeCallback) => {
  try {
    if (fs.existsSync(destination)) {
      fs.unlinkSync(destination);
    }
  } catch (e) {
    completeCallback(e);
    return;
  }

  axios
  .get(url, {
    responseType: 'stream',
  })
  .then(response => {
    try {
      const total = parseInt(response.headers['content-length'], 10);
      if (total === 0) {
        completeCallback(new Error('No data available for download'));
      } else {
        const stream = fs.createWriteStream(destination);

        let downloaded = 0;
        response.data.on('data', (chunk) => {
          stream.write(Buffer.from(chunk));
          downloaded += chunk.length;
          if (progressCallback) {
            progressCallback((downloaded / total * 100.0).toFixed(2));
          }
        });

        response.data.on('end', () => {
          stream.end(() => {
            if (downloaded === 0) {
              completeCallback(new Error('Received 0 bytes'));
            } else if (downloaded !== total) {
              completeCallback(new Error('Received incorrect number of bytes'));
            } else {
              completeCallback();
            }
          });
        });

        response.data.on('error', error => {
          stream.end(() => {
            completeCallback(error);
          });
        });
      }
    } catch (error) {
      completeCallback(error);
    }
  })
  .catch(error => {
    completeCallback(error);
  });
};

module.exports.executeAndWait = (command, ignoreResult) => {
  return new Promise((resolve, reject) => {
    const retryExecute = (count, lastError) => {
      if (++count <= 5) {
        exec(command, error => {
          if (error) {
            if (!ignoreResult && (error.code === 1)) {
              setTimeout(() => {
                retryExecute(count, error);
              }, 1000);
            } else {
              reject(error);
            }
          } else {
            resolve();
          }
        });
      } else {
        reject(lastError);
      }
    };
    retryExecute(0);
  });
};

module.exports.executeAsync = (command, args = []) => {
  return new Promise((resolve, reject) => {
    const launchProcess = (count, timeout) => {
      let cmd = path.basename(command);
      const working = cmd.length === command.length ? null : command.substr(0, command.length - cmd.length);
      let processOptions = {
        detached: true,
        shell: false,
      };
      if (working) {
        processOptions.cwd = working;
        if (os.platform() !== 'win32') {
          cmd = './' + cmd;
        }
      }

      const process = new spawn(cmd, args, processOptions);
      const pid = process.pid;

      process.on('error', err => {
        if (++count === 5) {
          reject(err, pid);
        } else {
          clearTimeout(timeout);
          setTimeout(() => launchProcess(count, setTimeout(() => resolve(), 3000)), 1000);
        }
      });

      process.on('exit', (code) => {
        if (code !== 0) {
          if (++count === 5) {
            reject(code, pid);
          } else {
            clearTimeout(timeout);
            setTimeout(() => launchProcess(count, setTimeout(() => resolve(), 3000)), 1000);
          }
        }
      });

      process.unref();
    };

    launchProcess(0, setTimeout(() => resolve(), 3000));
  });
};

module.exports.executeScript = script => {
  return new Promise((resolve, reject) => {
    const processOptions = {
      detached: false,
      shell: true,
      windowsHide: true,
    };

    const command = '/bin/sh';
    const args = [
      script
    ];

    const process = new spawn(command, args, processOptions);
    let result = '';

    process.on('error', (err) => {
      reject(err);
    });

    process.stdout.on('data', (d) => {
      result += d;
    });

    process.on('exit', () => {
      resolve(result);
    });
    process.unref();
  });
};

module.exports.executeMount = (version, provider, remote, s3, location, exitCallback) => {
  return new Promise((resolve) => {
    const repertoryExec = _getRepertoryExec(version);
    const processOptions = {
      cwd: repertoryExec.working,
      detached: false,
      shell: os.platform() !== 'darwin',
      stdio: 'ignore',
    };

    const args = _getDefaultRepertoryArgs(provider, remote, s3);

    if ((os.platform() === 'linux') || (os.platform() === 'darwin')) {
      args.push('-o');
      args.push('big_writes');
      args.push('-f');
      args.push('-nc');
    } else if (os.platform() === 'win32') {
      args.push('-hidden');
    }
    args.push(location);

    let process = new spawn(repertoryExec.cmd, args, processOptions);
    const pid = process.pid;

    const timeout = setTimeout(() => {
      resolve(pid);
    }, 3000);

    process.on('error', (err) => {
      clearTimeout(timeout);
      exitCallback(err, pid);
    });

    process.on('exit', (code) => {
      clearTimeout(timeout);
      exitCallback(code, pid);
    });
  });
};

module.exports.exportAllSkylinks = _exportAllSkylinks;

module.exports.exportSkylinks = (version, paths) => {
  return new Promise((resolve, reject) => {
    const repertoryExec = _getRepertoryExec(version);
    const processOptions = {
      cwd: repertoryExec.working,
      detached: true,
      shell: false,
      windowsHide: true,
    };

    const args = _getDefaultRepertoryArgs('Skynet');
    args.push('-ex');
    args.push(paths.join(','));

    let result = '';
    const process = new spawn(repertoryExec.cmd, args, processOptions);

    process.on('error', (err) => {
      reject(err);
    });

    process.stdout.on('data', (d) => {
      result += d;
    });

    process.stderr.on('data', (d) => {
      result += d;
    });

    process.on('exit', code => {
      if (code === 0) {
        result = result.substr(result.indexOf('{'));
        resolve(JSON.parse(result));
      } else {
        reject(new Error('Failed to import: ' + code + ':' + result));
      }
    });

    process.unref();
  });
};

module.exports.getConfig = (version, provider, remote, s3) => {
  return new Promise((resolve, reject) => {
    const repertoryExec = _getRepertoryExec(version);
    const processOptions = {
      cwd: repertoryExec.working,
      detached: true,
      shell: false,
      windowsHide: true,
    };

    const args = _getDefaultRepertoryArgs(provider, remote, s3);
    args.push('-dc');

    const process = new spawn(repertoryExec.cmd, args, processOptions);
    let result = '';

    process.on('error', (err) => {
      reject(err);
    });

    process.stdout.on('data', (d) => {
      result += d;
    });

    process.on('exit', () => {
      const lines = result
      .replace(/\r\n/g, '\n')
      .split('\n');

      const code = parseInt(lines[0], 10);
      if (code === 0) {
        lines.shift();
        resolve({
          Code: code,
          Data: JSON.parse(lines.join('\n')),
        });
      } else {
        resolve(code);
      }
    });
    process.unref();
  });
};

module.exports.getConfigTemplate = (version, provider, remote, s3) => {
  return new Promise((resolve, reject) => {
    const repertoryExec = _getRepertoryExec(version);
    const processOptions = {
      cwd: repertoryExec.working,
      detached: true,
      shell: false,
      windowsHide: true,
    };

    const args = _getDefaultRepertoryArgs(provider, remote, s3);
    args.push('-gt');

    const process = new spawn(repertoryExec.cmd, args, processOptions);
    let result = '';

    process.on('error', (err) => {
      reject(err);
    });

    process.stdout.on('data', (d) => {
      result += d;
    });

    process.on('exit', () => {
      resolve(JSON.parse(result));
    });
    process.unref();
  });
};

module.exports.getDataDirectory = _getDataDirectory;

module.exports.getMissingDependencies = dependencies => {
  return new Promise((resolve, reject) => {
    if (!dependencies) {
      reject(new Error('Dependency list is invalid'));
    }

    let missing = [];
    if (os.platform() === 'win32') {
      let count = 0;
      const resolveIfComplete = () => {
        if (++count === dependencies.length) {
          resolve(missing);
        }
      };

      const Registry = require('winreg');
      const checkRegistry = (dep, index) => {
        if (index >= dep.registry.length) {
          if (dep.display === 'VC Runtime 2015-2019') {
            _vcRuntimeExists()
            .then(exists => {
              if (!exists) {
                missing.push(dep);
              }
              resolveIfComplete();
            })
            .catch(() => {
              missing.push(dep);
              resolveIfComplete();
            })
          } else {
            missing.push(dep);
            resolveIfComplete();
          }
        } else {
          let hive = null;
          const hiveName = dep.registry[index].split('\\')[0];
          switch (hiveName) {
            case 'HKEY_CLASSES_ROOT':
              hive = Registry.HKCR;
              break;
            case 'HKEY_CURRENT_CONFIG':
              hive = Registry.HKCC;
              break;
            case 'HKEY_CURRENT_USER':
              hive = Registry.HKCU;
              break;
            case 'HKEY_LOCAL_MACHINE':
              hive = Registry.HKLM;
              break;
            case 'HKEY_USERS':
              hive = Registry.HKU;
              break;
            default:
              throw new Error('Invalid registry hive: ' + hiveName);
          }

          const key = dep.registry[index].substr(hiveName.length);
          const regKey = new Registry({
            hive: hive,
            key: key
          });
          regKey.valueExists('DisplayName', (err, exists) => {
            if (err || !exists) {
              regKey.valueExists('ProductName', (err, exists) => {
                if (err || !exists) {
                  checkRegistry(dep, ++index);
                } else {
                  resolveIfComplete();
                }
              });
            } else {
              resolveIfComplete();
            }
          });
        }
      };

      for (const dependency of dependencies) {
        checkRegistry(dependency, 0);
      }
    } else {
      for (const dep of dependencies) {
        try {
          if (!(fs.lstatSync(dep.file).isFile() || fs.lstatSync(dep.file).isSymbolicLink())) {
            missing.push(dep);
          }
        } catch (e) {
          missing.push(dep);
        }
      }
      resolve(missing);
    }
  });
};

module.exports.grabSkynetFileTree = version => {
  return new Promise((resolve, reject) => {
    _exportAllSkylinks(version)
    .then(results => {
      resolve([{
        name: '/',
        directory: true,
        children: _createTreeNodes(results.success),
      }]);
    })
    .catch(e => {
      reject(e);
    });
  });
};

module.exports.importSkylinks = (version, jsonArray) => {
  return new Promise((resolve, reject) => {
    const repertoryExec = _getRepertoryExec(version);
    const processOptions = {
      cwd: repertoryExec.working,
      detached: true,
      shell: false,
      windowsHide: true,
    };

    const args = _getDefaultRepertoryArgs('Skynet');
    args.push('-ij');
    args.push(JSON.stringify(jsonArray));

    let result = '';
    const process = new spawn(repertoryExec.cmd, args, processOptions);

    process.on('error', (err) => {
      reject(err);
    });

    process.stdout.on('data', (d) => {
      result += d;
    });

    process.stderr.on('data', (d) => {
      result += d;
    });

    process.on('exit', code => {
      if (code === 0) {
        result = result.substr(result.indexOf('{'));
        resolve(JSON.parse(result));
      } else {
        reject(new Error('Failed to import: ' + code + ':' + result));
      }
    });

    process.unref();
  });
};

//https://stackoverflow.com/questions/31645738/how-to-create-full-path-with-nodes-fs-mkdirsync
module.exports.mkDirByPathSync = (targetDir, {isRelativeToScript = false} = {}) => {
  const sep = path.sep;
  const initDir = path.isAbsolute(targetDir) ? sep : '';
  const baseDir = isRelativeToScript ? __dirname : '.';

  return targetDir.split(sep).reduce((parentDir, childDir) => {
    const curDir = path.resolve(baseDir, parentDir, childDir);
    try {
      fs.mkdirSync(curDir);
    } catch (err) {
      if (err.code === 'EEXIST') { // curDir already exists!
        return curDir;
      }

      // To avoid `EISDIR` error on Mac and `EACCES`-->`ENOENT` and `EPERM` on Windows.
      if (err.code === 'ENOENT') { // Throw the original parentDir error on curDir `ENOENT` failure.
        throw new Error(`EACCES: permission denied, mkdir '${parentDir}'`);
      }

      const caughtErr = ['EACCES', 'EPERM', 'EISDIR'].indexOf(err.code) > -1;
      if (!caughtErr || (caughtErr && (targetDir === curDir))) {
        throw err; // Throw if it's just the last created dir.
      }
    }

    return curDir;
  }, initDir);
};

module.exports.performWindowsUninstall = names => {
  return new Promise((resolve, reject) => {
    if (os.platform() !== 'win32') {
      reject('Windows OS is not being used');
    } else {
      const cmd = path.join(process.env.windir, 'system32', 'reg.exe');
      const args = ['QUERY', 'HKLM\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall'];
      _execProcessGetOutput(cmd, null, args)
      .then(lines => {
        const parseLine = index => {
          if (index < lines.length) {
            const line = lines[index];
            if (line.startsWith('HKEY_LOCAL_MACHINE\\')) {
              let args2 = JSON.parse(JSON.stringify(args));
              args2[1] = 'HKLM\\' + line.substr(19);
              args2.push('/v');
              args2.push('DisplayName');
              args2.push('/t');
              args2.push('REG_SZ');
              _execProcessGetOutput(cmd, null, args2)
              .then(lines => {
                const value = lines[2].trim().substr(args2[3].length).trim().substr(6).trim();
                if (names.includes(value)) {
                  const items = line.split('\\');
                  const productCode = items[items.length - 1];
                  _executeProcess('msiexec.exe', null, ['/x', productCode, '/norestart'])
                  .then(code => {
                    if ((code === 0) || (code === 3010) || (code === 1641)) {
                      resolve(true);
                    } else {
                      reject('[' + value + '] uninstall failed: ' + code);
                    }
                  })
                  .catch(err => {
                    reject(err);
                  });
                } else {
                  parseLine(++index);
                }
              })
              .catch(() => {
                parseLine(++index);
              });
            } else {
              parseLine(++index);
            }
          } else {
            resolve(false);
          }
        };
        parseLine(0);
      })
      .catch(err => {
        reject(err);
      });
    }
  });
};

module.exports.removeDirectoryRecursively = _removeDirectoryRecursively;

module.exports.resolvePath = _resolvePath;

module.exports.setConfigValue = (name, value, provider, remote, s3, version) => {
  return new Promise((resolve, reject) => {
    const repertoryExec = _getRepertoryExec(version);
    const processOptions = {
      cwd: repertoryExec.working,
      detached: true,
      shell: false,
      windowsHide: true,
    };

    const args = _getDefaultRepertoryArgs(provider, remote, s3);
    args.push('-set');
    args.push(name);
    args.push(value);

    const process = new spawn(repertoryExec.cmd, args, processOptions);

    process.on('error', (err) => {
      reject(err);
    });

    process.on('exit', code => {
      if (code !== 0) {
        reject(new Error('Failed to set configuration value: ' + code));
      } else {
        resolve();
      }
    });

    process.unref();
  });
};

module.exports.stopMountProcess = (version, provider, remote, s3) => {
  return new Promise((resolve, reject) => {
    const repertoryExec = _getRepertoryExec(version);
    const processOptions = {
      cwd: repertoryExec.working,
      detached: os.platform() === 'darwin',
      shell: os.platform() !== 'darwin',
      windowsHide: true,
    };

    const args = _getDefaultRepertoryArgs(provider, remote, s3);
    args.push('-unmount');

    const process = new spawn(repertoryExec.cmd, args, processOptions);
    const pid = process.pid;
    process.on('error', (err) => {
      reject(err);
    });
    process.on('exit', (code) => {
      resolve({
        PID: pid,
        Code: code,
      });
    });

    if (os.platform() === 'darwin') {
      process.unref();
    }
  });
};

module.exports.stopMountProcessSync = (version, provider, remote, s3) => {
  const repertoryExec = _getRepertoryExec(version);
  const processOptions = {
    cwd: repertoryExec.working,
    detached: true,
    shell: os.platform() !== 'darwin',
    windowsHide: true,
  };

  const args = _getDefaultRepertoryArgs(provider, remote, s3);
  args.push('-unmount');

  const process = new spawn(repertoryExec.cmd, args, processOptions);
  process.unref();
};

module.exports.testRepertoryBinary = version => {
  return new Promise((resolve, reject) => {
    const repertoryExec = _getRepertoryExec(version);
    _executeProcess(repertoryExec.cmd, repertoryExec.working, ['-dc'])
    .then(code => {
      if (code === 0) {
        resolve();
      } else {
        reject(new Error('Invalid exit code: ' + code));
      }
    })
    .catch(error => {
      reject(error);
    });
  });
};

module.exports.verifyHash = (file, hash) => {
  return new Promise((resolve, reject) => {
    const platform = os.platform();
    let command;
    let args;
    if (platform === 'darwin') {
      command = 'shasum';
      args = ['-b', '-a', '256', file];
    } else if (platform === 'linux') {
      command = 'sha256sum';
      args = ['-b', file, '-z'];
    } else {
      reject(new Error('Platform not supported: ' + os.platform()))
    }
    if (command) {
      execFile(command, args, (err, stdout) => {
        if (err) {
          reject(err);
        } else {
          const hash2 = stdout.split(' ')[0].trim().toLowerCase();
          if (hash2 === hash.toLowerCase()) {
            resolve(hash2);
          } else {
            reject(new Error('Checksum failed for file'));
          }
        }
      });
    }
  });
};

module.exports.verifySignature = (file, signatureFile, publicKeyFile) => {
  return new Promise((resolve, reject) => {
    const executeVerify = openssl => {
      execFile(openssl, ['dgst', '-sha256', '-verify', publicKeyFile, '-signature', signatureFile, file], (err, stdout) => {
        if (err) {
          reject(err);
        } else {
          resolve(stdout);
        }
      });
    };

    if (os.platform() === 'win32') {
      const Registry = require('winreg');
      const regKey = new Registry({
        hive: Registry.HKLM,
        key: 'SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\OpenSSL (64-bit)_is1'
      });
      regKey.valueExists('InstallLocation', (err, exists) => {
        if (err) {
          reject(err);
        } else if (exists) {
          regKey.get('InstallLocation', (err, item) => {
            if (err) {
              reject(err);
            } else {
              executeVerify(path.join(item.value(), 'bin', 'openssl.exe'));
            }
          });
        } else {
          reject(new Error('Failed to locate \'openssl.exe\''));
        }
      });
    } else if (os.platform() === 'linux') {
      executeVerify('openssl');
    } else {
      reject(new Error('Platform not supported: ' + os.platform()))
    }
  });
};
