"use strict";

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

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

function _bluebird() {
  const data = require("bluebird");

  _bluebird = function () {
    return data;
  };

  return data;
}

function _path() {
  const data = _interopRequireDefault(require("path"));

  _path = function () {
    return data;
  };

  return data;
}

function _capsule() {
  const data = _interopRequireDefault(require("../../../../components/core/capsule"));

  _capsule = function () {
    return data;
  };

  return data;
}

function _abstractVinyl() {
  const data = _interopRequireDefault(require("./abstract-vinyl"));

  _abstractVinyl = function () {
    return data;
  };

  return data;
}

function _symlink() {
  const data = _interopRequireDefault(require("../../../links/symlink"));

  _symlink = function () {
    return data;
  };

  return data;
}

function _logger() {
  const data = _interopRequireDefault(require("../../../logger/logger"));

  _logger = function () {
    return data;
  };

  return data;
}

function _removePath() {
  const data = _interopRequireDefault(require("./remove-path"));

  _removePath = function () {
    return data;
  };

  return data;
}

function _removeFilesAndEmptyDirsRecursively() {
  const data = _interopRequireDefault(require("../../../utils/fs/remove-files-and-empty-dirs-recursively"));

  _removeFilesAndEmptyDirsRecursively = function () {
    return data;
  };

  return data;
}

class DataToPersist {
  constructor() {
    this.files = [];
    this.symlinks = [];
    this.remove = [];
  }

  addFile(file) {
    if (!file) throw new Error('failed adding an empty file into DataToPersist');

    if (!file.path) {
      throw new Error('failed adding a file into DataToPersist as it does not have a path property');
    }

    const existingFileIndex = this.files.findIndex(existingFile => existingFile.path === file.path);

    if (existingFileIndex !== -1) {
      if (file.override) {
        // delete existing file
        this.files.splice(existingFileIndex, 1);
      } else {
        // don't push this one. keep the existing file
        return;
      }
    }

    this._throwForDirectoryCollision(file);

    this.files.push(file);
  }

  addManyFiles(files = []) {
    files.forEach(file => this.addFile(file));
  }

  removePath(pathToRemove) {
    if (!pathToRemove) throw new Error('failed adding a path to remove into DataToPersist');

    if (!this.remove.includes(pathToRemove)) {
      this.remove.push(pathToRemove);
    }
  }

  removeManyPaths(pathsToRemove = []) {
    pathsToRemove.forEach(pathToRemove => this.removePath(pathToRemove));
  }

  addSymlink(symlink) {
    if (!symlink.src) throw new Error('failed adding a symlink into DataToPersist, src is empty');
    if (!symlink.dest) throw new Error('failed adding a symlink into DataToPersist, dest is empty');
    this.symlinks.push(symlink);
  }

  addManySymlinks(symlinks = []) {
    symlinks.forEach(symlink => this.addSymlink(symlink));
  }

  merge(dataToPersist) {
    if (!dataToPersist) return;
    this.addManyFiles(dataToPersist.files);
    this.removeManyPaths(dataToPersist.remove);
    this.addManySymlinks(dataToPersist.symlinks);
  }

  persistAllToFS() {
    var _this = this;

    return (0, _bluebird().coroutine)(function* () {
      _this._log();

      _this._validateAbsolute(); // the order is super important. first remove, then create and finally symlink


      yield _this._deletePathsFromFS();
      yield _this._persistFilesToFS();
      yield _this._persistSymlinksToFS();
    })();
  }

  persistAllToCapsule(capsule) {
    var _this2 = this;

    return (0, _bluebird().coroutine)(function* () {
      _this2._log();

      _this2._validateRelative();

      yield Promise.all(_this2.remove.map(pathToRemove => capsule.removePath(pathToRemove.path)));
      yield Promise.all(_this2.files.map(file => capsule.outputFile(file.path, file.contents)));
      yield Promise.all(_this2.symlinks.map(symlink => _this2.atomicSymlink(capsule, symlink)));
    })();
  }

  atomicSymlink(capsule, symlink) {
    return (0, _bluebird().coroutine)(function* () {
      try {
        yield capsule.symlink(symlink.src, symlink.dest);
      } catch (e) {
        // On windows when the link already created by npm we got EPERM error
        // TODO: We should handle this better and avoid creating the symlink if it's already exists
        if (e.code !== 'EEXIST' && e.code !== 'EPERM') {
          throw e;
        } else {
          _logger().default.debug(`ignoring ${e.code} error on atomicSymlink creation`);
        }
      }
    })();
  }

  addBasePath(basePath) {
    this.files.forEach(file => {
      this._assertRelative(file.base);

      file.updatePaths({
        newBase: _path().default.join(basePath, file.base)
      });
    });
    this.symlinks.forEach(symlink => {
      this._assertRelative(symlink.src);

      this._assertRelative(symlink.dest);

      symlink.src = _path().default.join(basePath, symlink.src);
      symlink.dest = _path().default.join(basePath, symlink.dest);
    });
    this.remove.forEach(removePath => {
      this._assertRelative(removePath.path);

      removePath.path = _path().default.join(basePath, removePath.path);
    });
  }
  /**
   * helps for debugging
   */


  toConsole() {
    console.log(`\nfiles: ${this.files.map(f => f.path).join('\n')}`); // eslint-disable-line no-console

    console.log(`\nsymlinks: ${this.symlinks.map(s => `src: ${s.src}, dest: ${s.dest}`).join('\n')}`); // eslint-disable-line no-console

    console.log(`remove: ${this.remove.map(r => r.path).join('\n')}`); // eslint-disable-line no-console
  }

  filterByPath(filterFunc) {
    const dataToPersist = new DataToPersist();
    dataToPersist.addManyFiles(this.files.filter(f => filterFunc(f.path)));
    dataToPersist.removeManyPaths(this.remove.filter(r => filterFunc(r.path)));
    dataToPersist.addManySymlinks(this.symlinks.filter(s => filterFunc(s.dest)));
    return dataToPersist;
  }

  _persistFilesToFS() {
    var _this3 = this;

    return (0, _bluebird().coroutine)(function* () {
      return Promise.all(_this3.files.map(file => file.write()));
    })();
  }

  _persistSymlinksToFS() {
    var _this4 = this;

    return (0, _bluebird().coroutine)(function* () {
      return Promise.all(_this4.symlinks.map(symlink => symlink.write()));
    })();
  }

  _deletePathsFromFS() {
    var _this5 = this;

    return (0, _bluebird().coroutine)(function* () {
      const pathWithRemoveItsDirIfEmptyEnabled = _this5.remove.filter(p => p.removeItsDirIfEmpty).map(p => p.path);

      const restPaths = _this5.remove.filter(p => !p.removeItsDirIfEmpty);

      if (pathWithRemoveItsDirIfEmptyEnabled.length) {
        yield (0, _removeFilesAndEmptyDirsRecursively().default)(pathWithRemoveItsDirIfEmptyEnabled);
      }

      return Promise.all(restPaths.map(removePath => removePath.persistToFS()));
    })();
  }

  _validateAbsolute() {
    // it's important to make sure that all paths are absolute before writing them to the
    // filesystem. relative paths won't work when running bit commands from an inner dir
    const validateAbsolutePath = pathToValidate => {
      if (!_path().default.isAbsolute(pathToValidate)) {
        throw new Error(`DataToPersist expects ${pathToValidate} to be absolute, got relative`);
      }
    };

    this.files.forEach(file => {
      validateAbsolutePath(file.path);
    });
    this.remove.forEach(removePath => {
      validateAbsolutePath(removePath.path);
    });
    this.symlinks.forEach(symlink => {
      validateAbsolutePath(symlink.src);
      validateAbsolutePath(symlink.dest);
    });
  }

  _validateRelative() {
    // it's important to make sure that all paths are relative before writing them to the capsule
    const validateRelativePath = pathToValidate => {
      if (_path().default.isAbsolute(pathToValidate)) {
        throw new Error(`DataToPersist expects ${pathToValidate} to be relative, got absolute`);
      }
    };

    this.files.forEach(file => {
      validateRelativePath(file.path);
    });
    this.remove.forEach(removePath => {
      validateRelativePath(removePath.path);
    });
    this.symlinks.forEach(symlink => {
      validateRelativePath(symlink.src);
      validateRelativePath(symlink.dest);
    });
  }

  _log() {
    if (this.remove.length) {
      const pathToDeleteStr = this.remove.map(r => r.path).join('\n');

      _logger().default.debug(`DateToPersist, paths-to-delete:\n${pathToDeleteStr}`);
    }

    if (this.files.length) {
      const filesToWriteStr = this.files.map(f => f.path).join('\n');

      _logger().default.debug(`DateToPersist, paths-to-write:\n${filesToWriteStr}`);
    }

    if (this.symlinks.length) {
      const symlinksStr = this.symlinks.map(symlink => `src (existing): ${symlink.src}\ndest (new): ${symlink.dest}`).join('\n');

      _logger().default.debug(`DateToPersist, symlinks:\n${symlinksStr}`);
    }
  }

  _assertRelative(pathToCheck) {
    if (_path().default.isAbsolute(pathToCheck)) {
      throw new Error(`DataToPersist expects ${pathToCheck} to be relative, but found it absolute`);
    }
  }
  /**
   * prevent adding a file which later on will cause an error "EEXIST: file already exists, mkdir {dirname}".
   * this happens one a file is a directory name of the other file.
   * e.g. adding these two files, will cause the error above: "bar/foo" and "bar"
   *
   * to check for this possibility, we need to consider two scenarios:
   * 1) "bar/foo" is there and now adding "bar" => check whether one of the files starts with "bar/"
   * 2) "bar" is there and now adding "bar/foo" => check whether this file "bar/foo" starts with one of the files with '/'
   * practically, it runs `("bar/foo".startsWith("bar/"))` for both cases above.
   */


  _throwForDirectoryCollision(file) {
    const directoryCollision = this.files.find(f => f.path.startsWith(`${file.path}${_path().default.sep}`) || `${file.path}`.startsWith(`${f.path}${_path().default.sep}`));

    if (directoryCollision) {
      throw new Error(`unable to add the file "${file.path}", because another file "${directoryCollision.path}" is going to be written.
one of them is a directory of the other one, and is not possible to have them both`);
    }
  }

}

exports.default = DataToPersist;