"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = _default;

var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime/helpers/asyncToGenerator"));

var _common = require("../prelude/common.js");

var _follow = require("./follow.js");

var _log = require("./log.js");

var _assert = _interopRequireDefault(require("assert"));

var _detector = _interopRequireDefault(require("./detector.js"));

var _fsExtra = _interopRequireDefault(require("fs-extra"));

var _globby = _interopRequireDefault(require("globby"));

var _path = _interopRequireDefault(require("path"));

/* eslint-disable require-atomic-updates */
const win32 = process.platform === 'win32';

function unlikelyJavascript(file) {
  return ['.css', '.html', '.json'].includes(_path.default.extname(file));
}

function isPublic(config) {
  if (config.private) return false;
  let {
    license,
    licenses
  } = config;

  if (licenses) {
    license = licenses;
  }

  if (license) {
    license = license.type || license;
  }

  if (Array.isArray(license)) {
    license = license.map(c => String(c.type || c)).join(',');
  }

  if (!license) return false;
  if (/^\(/.test(license)) license = license.slice(1);
  if (/\)$/.test(license)) license = license.slice(0, -1);
  license = license.toLowerCase();
  licenses = Array.prototype.concat(license.split(' or '), license.split(' and '), license.split('/'), license.split(','));
  let result = false;
  const foss = ['isc', 'mit', 'apache-2.0', 'apache 2.0', 'public domain', 'bsd', 'bsd-2-clause', 'bsd-3-clause', 'wtfpl', 'cc-by-3.0', 'x11', 'artistic-2.0', 'gplv3', 'mpl', 'mplv2.0', 'unlicense', 'apache license 2.0', 'zlib', 'mpl-2.0', 'nasa-1.3', 'apache license, version 2.0', 'lgpl-2.1+', 'cc0-1.0'];

  for (const c of licenses) {
    result = foss.indexOf(c) >= 0;
    if (result) break;
  }

  return result;
}

function upon(p, base) {
  if (typeof p !== 'string') {
    throw (0, _log.wasReported)('Config items must be strings. See examples');
  }

  let negate = false;

  if (p[0] === '!') {
    p = p.slice(1);
    negate = true;
  }

  p = _path.default.join(base, p);

  if (win32) {
    p = p.replace(/\\/g, '/');
  }

  if (negate) {
    p = '!' + p;
  }

  return p;
}

function collect(ps) {
  return _globby.default.sync(ps, {
    dot: true
  });
}

function expandFiles(efs, base) {
  if (!Array.isArray(efs)) {
    efs = [efs];
  }

  efs = collect(efs.map(p => upon(p, base)));
  return efs;
}

class Walker {
  appendRecord(task) {
    const {
      file
    } = task;
    if (this.records[file]) return;
    this.records[file] = {
      file
    };
  }

  append(task) {
    task.file = (0, _common.normalizePath)(task.file);
    this.appendRecord(task);
    this.tasks.push(task);
    const what = {
      [_common.STORE_BLOB]: 'Bytecode of',
      [_common.STORE_CONTENT]: 'Content of',
      [_common.STORE_LINKS]: 'Directory',
      [_common.STORE_STAT]: 'Stat info of'
    }[task.store];

    if (task.reason) {
      _log.log.debug(what + ' %1 is added to queue. It was required from %2', ['%1: ' + task.file, '%2: ' + task.reason]);
    } else {
      _log.log.debug(what + ' %1 is added to queue', ['%1: ' + task.file]);
    }
  }

  appendFilesFromConfig(marker) {
    var _this = this;

    return (0, _asyncToGenerator2.default)(function* () {
      const {
        config,
        configPath,
        base
      } = marker;
      const pkgConfig = config.pkg;

      if (pkgConfig) {
        let {
          scripts
        } = pkgConfig;

        if (scripts) {
          scripts = expandFiles(scripts, base);

          for (const script of scripts) {
            const stat = yield _fsExtra.default.stat(script);

            if (stat.isFile()) {
              if (!(0, _common.isDotJS)(script) && !(0, _common.isDotJSON)(script) & !(0, _common.isDotNODE)(script)) {
                _log.log.warn('Non-javascript file is specified in \'scripts\'.', ['Pkg will probably fail to parse. Specify *.js in glob.', script]);
              }

              _this.append({
                file: script,
                marker,
                store: _common.STORE_BLOB,
                reason: configPath
              });
            }
          }
        }

        let {
          assets
        } = pkgConfig;

        if (assets) {
          assets = expandFiles(assets, base);

          for (const asset of assets) {
            const stat = yield _fsExtra.default.stat(asset);

            if (stat.isFile()) {
              _this.append({
                file: asset,
                marker,
                store: _common.STORE_CONTENT,
                reason: configPath
              });
            }
          }
        }
      } else {
        let {
          files
        } = config;

        if (files) {
          files = expandFiles(files, base);

          for (const file of files) {
            const stat = yield _fsExtra.default.stat(file);

            if (stat.isFile()) {
              // 1) remove sources of top-level(!) package 'files' i.e. ship as BLOB
              // 2) non-source (non-js) files of top-level package are shipped as CONTENT
              // 3) parsing some js 'files' of non-top-level packages fails, hence all CONTENT
              if (marker.toplevel) {
                _this.append({
                  file,
                  marker,
                  store: (0, _common.isDotJS)(file) ? _common.STORE_BLOB : _common.STORE_CONTENT,
                  reason: configPath
                });
              } else {
                _this.append({
                  file,
                  marker,
                  store: _common.STORE_CONTENT,
                  reason: configPath
                });
              }
            }
          }
        }
      }
    })();
  }

  stepActivate(marker, derivatives) {
    var _this2 = this;

    return (0, _asyncToGenerator2.default)(function* () {
      if (!marker) (0, _assert.default)(false);
      if (marker.activated) return;
      const {
        config,
        base
      } = marker;
      if (!config) (0, _assert.default)(false);
      const {
        name
      } = config;

      if (name) {
        const d = _this2.dictionary[name];

        if (d) {
          if (typeof config.dependencies === 'object' && typeof d.dependencies === 'object') {
            Object.assign(config.dependencies, d.dependencies);
            delete d.dependencies;
          }

          Object.assign(config, d);
          marker.hasDictionary = true;
        }
      }

      const {
        dependencies
      } = config;

      if (typeof dependencies === 'object') {
        for (const dependency in dependencies) {
          // it may be `undefined` - overridden
          // in dictionary (see publicsuffixlist)
          if (dependencies[dependency]) {
            derivatives.push({
              alias: dependency,
              aliasType: _common.ALIAS_AS_RESOLVABLE,
              fromDependencies: true
            });
            derivatives.push({
              alias: dependency + '/package.json',
              aliasType: _common.ALIAS_AS_RESOLVABLE,
              fromDependencies: true
            });
          }
        }
      }

      const pkgConfig = config.pkg;

      if (pkgConfig) {
        const {
          patches
        } = pkgConfig;

        if (patches) {
          for (const key in patches) {
            const p = _path.default.join(base, key);

            _this2.patches[p] = patches[key];
          }
        }

        const {
          deployFiles
        } = pkgConfig;

        if (deployFiles) {
          marker.hasDeployFiles = true;

          for (const deployFile of deployFiles) {
            const type = deployFile[2] || 'file';

            _log.log.warn(`Cannot include ${type} %1 into executable.`, [`The ${type} must be distributed with executable as %2.`, '%1: ' + _path.default.relative(process.cwd(), _path.default.join(base, deployFile[0])), '%2: path-to-executable/' + deployFile[1]]);
          }
        }

        if (pkgConfig.log) {
          pkgConfig.log(_log.log, {
            packagePath: base
          });
        }
      }

      yield _this2.appendFilesFromConfig(marker);
      marker.public = isPublic(config);

      if (!marker.public && marker.toplevel) {
        marker.public = _this2.params.publicToplevel;
      }

      if (!marker.public && !marker.toplevel && _this2.params.publicPackages) {
        marker.public = _this2.params.publicPackages[0] === '*' || _this2.params.publicPackages.indexOf(name) !== -1;
      }

      marker.activated = true; // assert no further work with config

      delete marker.config;
    })();
  }

  stepRead(record) {
    return (0, _asyncToGenerator2.default)(function* () {
      let body;

      try {
        body = yield _fsExtra.default.readFile(record.file);
      } catch (error) {
        _log.log.error('Cannot read file, ' + error.code, record.file);

        throw (0, _log.wasReported)(error);
      }

      record.body = body;
    })();
  }

  hasPatch(record) {
    const patch = this.patches[record.file];
    if (!patch) return;
    return true;
  }

  stepPatch(record) {
    const patch = this.patches[record.file];
    if (!patch) return;
    let body = record.body.toString('utf8');

    for (let i = 0; i < patch.length; i += 2) {
      if (typeof patch[i] === 'object') {
        if (patch[i].do === 'erase') {
          body = patch[i + 1];
        } else if (patch[i].do === 'prepend') {
          body = patch[i + 1] + body;
        } else if (patch[i].do === 'append') {
          body += patch[i + 1];
        }
      } else if (typeof patch[i] === 'string') {
        // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
        // function escapeRegExp
        const esc = patch[i].replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
        const regexp = new RegExp(esc, 'g');
        body = body.replace(regexp, patch[i + 1]);
      }
    }

    record.body = body;
  }

  stepStrip(record) {
    let body = record.body.toString('utf8');

    if (/^\ufeff/.test(body)) {
      body = body.replace(/^\ufeff/, '');
    }

    if (/^#!/.test(body)) {
      body = body.replace(/^#![^\n]*\n/, '\n');
    }

    record.body = body;
  }

  stepDetect(record, marker, derivatives) {
    const body = record.body;

    try {
      _detector.default.detect(body, (node, trying) => {
        const {
          toplevel
        } = marker;

        let d = _detector.default.visitor_SUCCESSFUL(node);

        if (d) {
          if (d.mustExclude) return false;
          d.mayExclude = d.mayExclude || trying;
          derivatives.push(d);
          return false;
        }

        d = _detector.default.visitor_NONLITERAL(node);

        if (d) {
          if (d.mustExclude) return false;
          const debug = !toplevel || d.mayExclude || trying;
          const level = debug ? 'debug' : 'warn';

          _log.log[level](`Cannot resolve '${d.alias}'`, [record.file, 'Dynamic require may fail at run time, because the requested file', 'is unknown at compilation time and not included into executable.', 'Use a string literal as an argument for \'require\', or leave it', 'as is and specify the resolved file name in \'scripts\' option.']);

          return false;
        }

        d = _detector.default.visitor_MALFORMED(node);

        if (d) {
          // there is no 'mustExclude'
          const debug = !toplevel || trying;
          const level = debug ? 'debug' : 'warn'; // there is no 'mayExclude'

          _log.log[level](`Malformed requirement for '${d.alias}'`, [record.file]);

          return false;
        }

        d = _detector.default.visitor_USESCWD(node);

        if (d) {
          // there is no 'mustExclude'
          const level = 'debug'; // there is no 'mayExclude'

          _log.log[level](`Path.resolve(${d.alias}) is ambiguous`, [record.file, 'It resolves relatively to \'process.cwd\' by default, however', 'you may want to use \'path.dirname(require.main.filename)\'']);

          return false;
        }

        return true; // can i go inside?
      });
    } catch (error) {
      _log.log.error(error.message, record.file);

      throw (0, _log.wasReported)(error);
    }
  }

  stepDerivatives_ALIAS_AS_RELATIVE(record, marker, derivative) {
    var _this3 = this;

    return (0, _asyncToGenerator2.default)(function* () {
      // eslint-disable-line camelcase
      const file = _path.default.join(_path.default.dirname(record.file), derivative.alias);

      let stat;

      try {
        stat = yield _fsExtra.default.stat(file);
      } catch (error) {
        const {
          toplevel
        } = marker;
        const debug = !toplevel && error.code === 'ENOENT';
        const level = debug ? 'debug' : 'warn';

        _log.log[level]('Cannot stat, ' + error.code, [file, 'The file was required from \'' + record.file + '\'']);
      }

      if (stat && stat.isFile()) {
        _this3.append({
          file,
          marker,
          store: _common.STORE_CONTENT,
          reason: record.file
        });
      }
    })();
  }

  stepDerivatives_ALIAS_AS_RESOLVABLE(record, marker, derivative) {
    var _this4 = this;

    return (0, _asyncToGenerator2.default)(function* () {
      // eslint-disable-line camelcase
      const newPackages = [];

      const catchReadFile = file => {
        (0, _assert.default)((0, _common.isPackageJson)(file), 'walker: ' + file + ' must be package.json');
        newPackages.push({
          packageJson: file
        });
      };

      const catchPackageFilter = (config, base) => {
        const newPackage = newPackages[newPackages.length - 1];
        newPackage.marker = {
          config,
          configPath: newPackage.packageJson,
          base
        };
      };

      let newFile, failure;

      try {
        newFile = yield (0, _follow.follow)(derivative.alias, {
          basedir: _path.default.dirname(record.file),
          // default is extensions: ['.js'], but
          // it is not enough because 'typos.json'
          // is not taken in require('./typos')
          // in 'normalize-package-data/lib/fixer.js'
          extensions: ['.js', '.json', '.node'],
          readFile: catchReadFile,
          packageFilter: catchPackageFilter
        });
      } catch (error) {
        failure = error;
      }

      if (failure) {
        const {
          toplevel
        } = marker;
        const mainNotFound = newPackages.length > 0 && !newPackages[0].marker.config.main;
        const debug = !toplevel || derivative.mayExclude || mainNotFound && derivative.fromDependencies;
        const level = debug ? 'debug' : 'warn';

        if (mainNotFound) {
          const message = 'Entry \'main\' not found in %1';

          _log.log[level](message, ['%1: ' + newPackages[0].packageJson, '%2: ' + record.file]);
        } else {
          _log.log[level](failure.message, ['%1: ' + record.file]);
        }

        return;
      }

      let newPackageForNewRecords;

      for (const newPackage of newPackages) {
        let newFile2;

        try {
          newFile2 = yield (0, _follow.follow)(derivative.alias, {
            basedir: _path.default.dirname(record.file),
            extensions: ['.js', '.json', '.node'],
            ignoreFile: newPackage.packageJson
          });
        } catch (_) {// not setting is enough
        }

        if (newFile2 !== newFile) {
          newPackageForNewRecords = newPackage;
          break;
        }
      }

      if (newPackageForNewRecords) {
        _this4.append({
          file: newPackageForNewRecords.packageJson,
          marker: newPackageForNewRecords.marker,
          store: _common.STORE_CONTENT,
          reason: record.file
        });
      }

      _this4.append({
        file: newFile,
        marker: newPackageForNewRecords ? newPackageForNewRecords.marker : marker,
        store: _common.STORE_BLOB,
        reason: record.file
      });
    })();
  }

  stepDerivatives(record, marker, derivatives) {
    var _this5 = this;

    return (0, _asyncToGenerator2.default)(function* () {
      for (const derivative of derivatives) {
        if (_follow.natives[derivative.alias]) continue;

        if (derivative.aliasType === _common.ALIAS_AS_RELATIVE) {
          yield _this5.stepDerivatives_ALIAS_AS_RELATIVE(record, marker, derivative);
        } else if (derivative.aliasType === _common.ALIAS_AS_RESOLVABLE) {
          yield _this5.stepDerivatives_ALIAS_AS_RESOLVABLE(record, marker, derivative);
        } else {
          (0, _assert.default)(false, 'walker: unknown aliasType ' + derivative.aliasType);
        }
      }
    })();
  }

  step_STORE_ANY(record, marker, store) {
    var _this6 = this;

    return (0, _asyncToGenerator2.default)(function* () {
      // eslint-disable-line camelcase
      if (record[store] !== undefined) return;
      record[store] = false; // default is discard

      _this6.append({
        file: record.file,
        store: _common.STORE_STAT
      });

      if ((0, _common.isDotNODE)(record.file)) {
        // provide explicit deployFiles to override
        // native addon deployment place. see 'sharp'
        if (!marker.hasDeployFiles) {
          _log.log.warn('Cannot include addon %1 into executable.', ['The addon must be distributed with executable as %2.', '%1: ' + record.file, '%2: path-to-executable/' + _path.default.basename(record.file)]);
        }

        return; // discard
      }

      const derivatives1 = [];
      yield _this6.stepActivate(marker, derivatives1);
      yield _this6.stepDerivatives(record, marker, derivatives1);

      if (store === _common.STORE_BLOB) {
        if (unlikelyJavascript(record.file)) {
          _this6.append({
            file: record.file,
            marker,
            store: _common.STORE_CONTENT
          });

          return; // discard
        }

        if (marker.public || marker.hasDictionary) {
          _this6.append({
            file: record.file,
            marker,
            store: _common.STORE_CONTENT
          });
        }
      }

      if (store === _common.STORE_BLOB || _this6.hasPatch(record)) {
        if (!record.body) {
          yield _this6.stepRead(record);

          _this6.stepPatch(record);

          if (store === _common.STORE_BLOB) {
            _this6.stepStrip(record);
          }
        }

        if (store === _common.STORE_BLOB) {
          const derivatives2 = [];

          _this6.stepDetect(record, marker, derivatives2);

          yield _this6.stepDerivatives(record, marker, derivatives2);
        }
      }

      record[store] = true;
    })();
  }

  step_STORE_LINKS(record, data) {
    // eslint-disable-line camelcase
    if (record[_common.STORE_LINKS]) {
      record[_common.STORE_LINKS].push(data);

      return;
    }

    record[_common.STORE_LINKS] = [data];
    this.append({
      file: record.file,
      store: _common.STORE_STAT
    });
  }

  step_STORE_STAT(record) {
    var _this7 = this;

    return (0, _asyncToGenerator2.default)(function* () {
      // eslint-disable-line camelcase
      if (record[_common.STORE_STAT]) return;

      try {
        record[_common.STORE_STAT] = yield _fsExtra.default.stat(record.file);
      } catch (error) {
        _log.log.error('Cannot stat, ' + error.code, record.file);

        throw (0, _log.wasReported)(error);
      }

      if (_path.default.dirname(record.file) !== record.file) {
        // root directory
        _this7.append({
          file: _path.default.dirname(record.file),
          store: _common.STORE_LINKS,
          data: _path.default.basename(record.file)
        });
      }
    })();
  }

  step(task) {
    var _this8 = this;

    return (0, _asyncToGenerator2.default)(function* () {
      const {
        file,
        store,
        data
      } = task;
      const record = _this8.records[file];

      if (store === _common.STORE_BLOB || store === _common.STORE_CONTENT) {
        yield _this8.step_STORE_ANY(record, task.marker, store);
      } else if (store === _common.STORE_LINKS) {
        _this8.step_STORE_LINKS(record, data);
      } else if (store === _common.STORE_STAT) {
        yield _this8.step_STORE_STAT(record);
      } else {
        (0, _assert.default)(false, 'walker: unknown store ' + store);
      }
    })();
  }

  readDictionary() {
    var _this9 = this;

    return (0, _asyncToGenerator2.default)(function* () {
      const dd = _path.default.join(__dirname, '../dictionary');

      const files = yield _fsExtra.default.readdir(dd);

      for (const file of files) {
        if (/\.js$/.test(file)) {
          const name = file.slice(0, -3);

          const config = require(_path.default.join(dd, file));

          _this9.dictionary[name] = config;
        }
      }
    })();
  }

  start(marker, entrypoint, addition, params) {
    var _this10 = this;

    return (0, _asyncToGenerator2.default)(function* () {
      _this10.tasks = [];
      _this10.records = {};
      _this10.dictionary = {};
      _this10.patches = {};
      _this10.params = params;
      yield _this10.readDictionary();

      _this10.append({
        file: entrypoint,
        marker,
        store: _common.STORE_BLOB
      });

      if (addition) {
        _this10.append({
          file: addition,
          marker,
          store: _common.STORE_CONTENT
        });
      }

      const tasks = _this10.tasks;

      for (let i = 0; i < tasks.length; i += 1) {
        // NO MULTIPLE WORKERS! THIS WILL LEAD TO NON-DETERMINISTIC
        // ORDER. one-by-one fifo is the only way to iterate tasks
        yield _this10.step(tasks[i]);
      }

      return {
        records: _this10.records,
        entrypoint: (0, _common.normalizePath)(entrypoint)
      };
    })();
  }

}

function _default() {
  return _ref.apply(this, arguments);
}

function _ref() {
  _ref = (0, _asyncToGenerator2.default)(function* (...args) {
    const w = new Walker();
    return yield w.start(...args);
  });
  return _ref.apply(this, arguments);
}