"use strict";

const _ = require('lodash');
const la = require('lazy-ass');
const is = require('check-more-types');
const cp = require('child_process');
const os = require('os');
const yauzl = require('yauzl');
const debug = require('debug')('cypress:cli:unzip');
const extract = require('extract-zip');
const Promise = require('bluebird');
const readline = require('readline');
const {
  throwFormErrorText,
  errors
} = require('../errors');
const fs = require('../fs');
const util = require('../util');
const unzipTools = {
  extract
};

// expose this function for simple testing
const unzip = ({
  zipFilePath,
  installDir,
  progress
}) => {
  debug('unzipping from %s', zipFilePath);
  debug('into', installDir);
  if (!zipFilePath) {
    throw new Error('Missing zip filename');
  }
  const startTime = Date.now();
  let yauzlDoneTime = 0;
  return fs.ensureDirAsync(installDir).then(() => {
    return new Promise((resolve, reject) => {
      return yauzl.open(zipFilePath, (err, zipFile) => {
        yauzlDoneTime = Date.now();
        if (err) {
          debug('error using yauzl %s', err.message);
          return reject(err);
        }
        const total = zipFile.entryCount;
        debug('zipFile entries count', total);
        const started = new Date();
        let percent = 0;
        let count = 0;
        const notify = percent => {
          const elapsed = +new Date() - +started;
          const eta = util.calculateEta(percent, elapsed);
          progress.onProgress(percent, util.secsRemaining(eta));
        };
        const tick = () => {
          count += 1;
          percent = count / total * 100;
          const displayPercent = percent.toFixed(0);
          return notify(displayPercent);
        };
        const unzipWithNode = () => {
          debug('unzipping with node.js (slow)');
          const opts = {
            dir: installDir,
            onEntry: tick
          };
          debug('calling Node extract tool %s %o', zipFilePath, opts);
          return unzipTools.extract(zipFilePath, opts).then(() => {
            debug('node unzip finished');
            return resolve();
          }).catch(err => {
            const error = err || new Error('Unknown error with Node extract tool');
            debug('error %s', error.message);
            return reject(error);
          });
        };
        const unzipFallback = _.once(unzipWithNode);
        const unzipWithUnzipTool = () => {
          debug('unzipping via `unzip`');
          const inflatingRe = /inflating:/;
          const sp = cp.spawn('unzip', ['-o', zipFilePath, '-d', installDir]);
          sp.on('error', err => {
            debug('unzip tool error: %s', err.message);
            unzipFallback();
          });
          sp.on('close', code => {
            debug('unzip tool close with code %d', code);
            if (code === 0) {
              percent = 100;
              notify(percent);
              return resolve();
            }
            debug('`unzip` failed %o', {
              code
            });
            return unzipFallback();
          });
          sp.stdout.on('data', data => {
            if (inflatingRe.test(data)) {
              return tick();
            }
          });
          sp.stderr.on('data', data => {
            debug('`unzip` stderr %s', data);
          });
        };

        // we attempt to first unzip with the native osx
        // ditto because its less likely to have problems
        // with corruption, symlinks, or icons causing failures
        // and can handle resource forks
        // http://automatica.com.au/2011/02/unzip-mac-os-x-zip-in-terminal/
        const unzipWithOsx = () => {
          debug('unzipping via `ditto`');
          const copyingFileRe = /^copying file/;
          const sp = cp.spawn('ditto', ['-xkV', zipFilePath, installDir]);

          // f-it just unzip with node
          sp.on('error', err => {
            debug(err.message);
            unzipFallback();
          });
          sp.on('close', code => {
            if (code === 0) {
              // make sure we get to 100% on the progress bar
              // because reading in lines is not really accurate
              percent = 100;
              notify(percent);
              return resolve();
            }
            debug('`ditto` failed %o', {
              code
            });
            return unzipFallback();
          });
          return readline.createInterface({
            input: sp.stderr
          }).on('line', line => {
            if (copyingFileRe.test(line)) {
              return tick();
            }
          });
        };
        switch (os.platform()) {
          case 'darwin':
            return unzipWithOsx();
          case 'linux':
            return unzipWithUnzipTool();
          case 'win32':
            return unzipWithNode();
          default:
            return;
        }
      });
    }).tap(() => {
      debug('unzip completed %o', {
        yauzlMs: yauzlDoneTime - startTime,
        unzipMs: Date.now() - yauzlDoneTime
      });
    });
  });
};
function isMaybeWindowsMaxPathLengthError(err) {
  return os.platform() === 'win32' && err.code === 'ENOENT' && err.syscall === 'realpath';
}
const start = async ({
  zipFilePath,
  installDir,
  progress
}) => {
  la(is.unemptyString(installDir), 'missing installDir');
  if (!progress) {
    progress = {
      onProgress: () => {
        return {};
      }
    };
  }
  try {
    const installDirExists = await fs.pathExists(installDir);
    if (installDirExists) {
      debug('removing existing unzipped binary', installDir);
      await fs.removeAsync(installDir);
    }
    await unzip({
      zipFilePath,
      installDir,
      progress
    });
  } catch (err) {
    const errorTemplate = isMaybeWindowsMaxPathLengthError(err) ? errors.failedUnzipWindowsMaxPathLength : errors.failedUnzip;
    await throwFormErrorText(errorTemplate)(err);
  }
};
module.exports = {
  start,
  utils: {
    unzip,
    unzipTools
  }
};