"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 _defineProperty2() {
  const data = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));

  _defineProperty2 = function () {
    return data;
  };

  return data;
}

function _ramda() {
  const data = _interopRequireDefault(require("ramda"));

  _ramda = function () {
    return data;
  };

  return data;
}

function _fsExtra() {
  const data = _interopRequireDefault(require("fs-extra"));

  _fsExtra = function () {
    return data;
  };

  return data;
}

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

  _path = function () {
    return data;
  };

  return data;
}

function _lodash() {
  const data = _interopRequireDefault(require("lodash.uniqby"));

  _lodash = function () {
    return data;
  };

  return data;
}

function _object() {
  const data = _interopRequireDefault(require("./object"));

  _object = function () {
    return data;
  };

  return data;
}

function _rawObject() {
  const data = _interopRequireDefault(require("./raw-object"));

  _rawObject = function () {
    return data;
  };

  return data;
}

function _ref3() {
  const data = _interopRequireDefault(require("./ref"));

  _ref3 = function () {
    return data;
  };

  return data;
}

function _constants() {
  const data = require("../../constants");

  _constants = function () {
    return data;
  };

  return data;
}

function _exceptions() {
  const data = require("../exceptions");

  _exceptions = function () {
    return data;
  };

  return data;
}

function _utils() {
  const data = require("../../utils");

  _utils = function () {
    return data;
  };

  return data;
}

function _fsRemoveFile() {
  const data = _interopRequireDefault(require("../../utils/fs-remove-file"));

  _fsRemoveFile = function () {
    return data;
  };

  return data;
}

function _scopeMeta() {
  const data = _interopRequireDefault(require("../models/scopeMeta"));

  _scopeMeta = function () {
    return data;
  };

  return data;
}

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

  _logger = function () {
    return data;
  };

  return data;
}

function _componentsIndex() {
  const data = _interopRequireDefault(require("./components-index"));

  _componentsIndex = function () {
    return data;
  };

  return data;
}

function _scopeJson() {
  const data = require("../scope-json");

  _scopeJson = function () {
    return data;
  };

  return data;
}

function _objectRegistrar() {
  const data = require("../object-registrar");

  _objectRegistrar = function () {
    return data;
  };

  return data;
}

function _models() {
  const data = require("../models");

  _models = function () {
    return data;
  };

  return data;
}

const OBJECTS_BACKUP_DIR = `${_constants().OBJECTS_DIR}.bak`;

class Repository {
  constructor(scopePath, scopeJson, types = _objectRegistrar().typesObj) {
    (0, _defineProperty2().default)(this, "objects", {});
    (0, _defineProperty2().default)(this, "objectsToRemove", []);
    (0, _defineProperty2().default)(this, "_cache", {});
    this.scopePath = scopePath;
    this.scopeJson = scopeJson;
    this.types = types;
  }

  static load({
    scopePath,
    scopeJson
  }) {
    return (0, _bluebird().coroutine)(function* () {
      const repository = new Repository(scopePath, scopeJson, _objectRegistrar().typesObj);
      const componentsIndex = yield repository.loadOptionallyCreateComponentsIndex();
      repository.componentsIndex = componentsIndex;
      return repository;
    })();
  }

  static create({
    scopePath,
    scopeJson
  }) {
    const repository = new Repository(scopePath, scopeJson, _objectRegistrar().typesObj);

    const componentsIndex = _componentsIndex().default.create(scopePath);

    repository.componentsIndex = componentsIndex;
    return repository;
  }

  static reset(scopePath) {
    return _componentsIndex().default.reset(scopePath);
  }

  static getPathByScopePath(scopePath) {
    return _path().default.join(scopePath, _constants().OBJECTS_DIR);
  }

  ensureDir() {
    return (0, _utils().mkdirp)(this.getPath());
  }

  getPath() {
    return Repository.getPathByScopePath(this.scopePath);
  }

  getBackupPath(dirName) {
    const backupPath = _path().default.join(this.scopePath, OBJECTS_BACKUP_DIR);

    return dirName ? _path().default.join(backupPath, dirName) : backupPath;
  }

  getLicense() {
    return this.scopeJson.getPopulatedLicense();
  }

  getScopeMetaObject() {
    return this.getLicense().then(license => _scopeMeta().default.fromObject({
      license,
      name: this.scopeJson.name
    }).compress());
  }

  objectPath(ref) {
    const hash = ref.toString();
    return _path().default.join(this.getPath(), hash.slice(0, 2), hash.slice(2));
  }

  load(ref, throws = false) {
    if (this.getCache(ref)) return Promise.resolve(this.getCache(ref));
    return _fsExtra().default.readFile(this.objectPath(ref)).then(fileContents => {
      return _object().default.parseObject(fileContents, this.types);
    }).then(parsedObject => {
      this.setCache(parsedObject);
      return parsedObject;
    }).catch(err => {
      if (err.code === 'ENOENT') {
        _logger().default.debug(`Failed finding a ref file ${this.objectPath(ref)}.`);
      } else {
        _logger().default.error(`Failed reading a ref file ${this.objectPath(ref)}. Error: ${err.message}`);
      }

      if (throws) throw err;
      return null;
    });
  }

  list() {
    var _this = this;

    return (0, _bluebird().coroutine)(function* () {
      const refs = yield _this.listRefs();
      return Promise.all(refs.map(ref => _this.load(ref)));
    })();
  }

  listRefs() {
    var _this2 = this;

    return (0, _bluebird().coroutine)(function* () {
      const matches = yield (0, _utils().glob)(_path().default.join('*', '*'), {
        cwd: _this2.getPath()
      });
      const refs = matches.map(str => {
        const hash = str.replace(_path().default.sep, '');
        return new (_ref3().default)(hash);
      });
      return refs;
    })();
  }

  listRawObjects() {
    var _this3 = this;

    return (0, _bluebird().coroutine)(function* () {
      const refs = yield _this3.listRefs();
      return Promise.all(refs.map(
      /*#__PURE__*/
      function () {
        var _ref = (0, _bluebird().coroutine)(function* (ref) {
          try {
            const buffer = yield _this3.loadRaw(ref);
            const bitRawObject = yield _rawObject().default.fromDeflatedBuffer(buffer, ref.hash, _this3.types);
            return bitRawObject;
          } catch (err) {
            _logger().default.error(`Couldn't load the ref ${ref} this object is probably corrupted and should be delete`);

            return null;
          }
        });

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

  listComponentsIncludeSymlinks() {
    var _this4 = this;

    return (0, _bluebird().coroutine)(function* () {
      const hashes = _this4.componentsIndex.getHashesIncludeSymlinks();

      return _this4._getBitObjectsByHashes(hashes);
    })();
  }

  listComponents() {
    var _this5 = this;

    return (0, _bluebird().coroutine)(function* () {
      const hashes = _this5.componentsIndex.getHashes();

      return _this5._getBitObjectsByHashes(hashes);
    })();
  }

  _getBitObjectsByHashes(hashes) {
    var _this6 = this;

    return (0, _bluebird().coroutine)(function* () {
      const bitObjects = yield Promise.all(hashes.map(
      /*#__PURE__*/
      function () {
        var _ref2 = (0, _bluebird().coroutine)(function* (hash) {
          const bitObject = yield _this6.load(new (_ref3().default)(hash));

          if (!bitObject) {
            const componentId = _this6.componentsIndex.getIdByHash(hash);

            const indexJsonPath = _this6.componentsIndex.getPath();

            if (_this6.componentsIndex.isFileOnBitHub()) {
              _logger().default.error(`repository._getBitObjectsByHashes, indexJson at "${indexJsonPath}" is outdated and needs to be deleted`);

              return null;
            } // $FlowFixMe componentId must be set as it was retrieved from indexPath before


            throw new (_exceptions().OutdatedIndexJson)(componentId, indexJsonPath);
          }

          return bitObject;
        });

        return function (_x2) {
          return _ref2.apply(this, arguments);
        };
      }())); // $FlowFixMe

      return bitObjects.filter(b => b); // remove nulls;
    })();
  }

  loadOptionallyCreateComponentsIndex() {
    var _this7 = this;

    return (0, _bluebird().coroutine)(function* () {
      try {
        const componentsIndex = yield _componentsIndex().default.load(_this7.scopePath);
        return componentsIndex;
      } catch (err) {
        if (err.code === 'ENOENT') {
          const bitObjects = yield _this7.list();

          const componentsIndex = _componentsIndex().default.create(_this7.scopePath);

          componentsIndex.addMany(bitObjects);
          yield componentsIndex.write();
          return componentsIndex;
        }

        throw err;
      }
    })();
  }

  loadRaw(ref) {
    return _fsExtra().default.readFile(this.objectPath(ref));
  }

  loadRawObject(ref) {
    var _this8 = this;

    return (0, _bluebird().coroutine)(function* () {
      const buffer = yield _this8.loadRaw(ref);
      const bitRawObject = yield _rawObject().default.fromDeflatedBuffer(buffer, ref.hash, _this8.types);
      return bitRawObject;
    })();
  }
  /**
   * prefer using `this.load()` for an async version, which also writes to the cache
   */


  loadSync(ref, throws = true) {
    try {
      const objectFile = _fsExtra().default.readFileSync(this.objectPath(ref));

      return _object().default.parseSync(objectFile, this.types);
    } catch (err) {
      if (throws) {
        throw new (_exceptions().HashNotFound)(ref.toString());
      }

      return null;
    }
  }

  setCache(object) {
    this._cache[object.hash().toString()] = object;
    return this;
  }

  getCache(ref) {
    return this._cache[ref.toString()];
  }

  removeFromCache(ref) {
    delete this._cache[ref.toString()];
  }

  backup(dirName) {
    const backupDir = this.getBackupPath(dirName);
    const objectsDir = this.getPath();

    _logger().default.debug(`making a backup of all objects from ${objectsDir} to ${backupDir}`);

    _fsExtra().default.emptyDirSync(backupDir);

    _fsExtra().default.copySync(objectsDir, backupDir);
  }

  add(object) {
    if (!object) return this; // leave the following commented log message, it is very useful for debugging but too verbose when not needed.
    // logger.debug(`repository: adding object ${object.hash().toString()} which consist of the following id: ${object.id()}`);

    this.objects[object.hash().toString()] = object;
    this.setCache(object);
    return this;
  }

  addMany(objects) {
    if (!objects || !objects.length) return this;
    objects.forEach(obj => this.add(obj));
    return this;
  }

  removeObject(ref) {
    this.objectsToRemove.push(ref);
  }

  removeManyObjects(refs) {
    if (!refs || !refs.length) return;
    refs.forEach(ref => this.removeObject(ref));
  }

  findMany(refs) {
    return Promise.all(refs.map(ref => this.load(ref)));
  }
  /**
   * persist objects changes (added and removed) into the filesystem
   * do not call this function multiple times in parallel, otherwise, it'll damage the index.json file.
   * call this function only once after you added and removed all applicable objects.
   */


  persist(validate = true) {
    var _this9 = this;

    return (0, _bluebird().coroutine)(function* () {
      _logger().default.debug(`Repository.persist, validate = ${validate.toString()}`);

      yield _this9._deleteMany();

      _this9._validateObjects(validate);

      yield _this9._writeMany();
    })();
  }
  /**
   * normally, the validation step takes place just before the acutal writing of the file.
   * however, this can be an issue where a component has an invalid version. the component could
   * be saved before validating the version (see #1727). that's why we validate here before writing
   * anything to the filesystem.
   * the open question here is whether should we validate again before the actual writing or it
   * should be enough to validate here?
   * for now, it does validate again before saving, only to be 100% sure nothing happens in a few
   * lines of code until the actual writing. however, if the performance penalty is noticeable, we
   * can easily revert it by changing `bitObject.validateBeforePersist = false` line run regardless
   * the `validate` argument.
   */


  _validateObjects(validate) {
    Object.keys(this.objects).forEach(hash => {
      const bitObject = this.objects[hash]; // $FlowFixMe some BitObject classes have validate() method

      if (validate && bitObject.validate) {
        bitObject.validate();
      }

      if (!validate) {
        bitObject.validateBeforePersist = false;
      }
    });
  }

  _deleteMany() {
    var _this10 = this;

    return (0, _bluebird().coroutine)(function* () {
      if (!_this10.objectsToRemove.length) return;
      const uniqRefs = (0, _lodash().default)(_this10.objectsToRemove, 'hash');

      _logger().default.debug(`Repository._deleteMany: deleting ${uniqRefs.length} objects`);

      yield Promise.all(uniqRefs.map(ref => _this10._deleteOne(ref)));

      const removed = _this10.componentsIndex.removeMany(uniqRefs);

      if (removed) yield _this10.componentsIndex.write();
    })();
  }

  _writeMany() {
    var _this11 = this;

    return (0, _bluebird().coroutine)(function* () {
      if (_ramda().default.isEmpty(_this11.objects)) return;

      _logger().default.debug(`Repository._writeMany: writing ${Object.keys(_this11.objects).length} objects`); // @TODO handle failures


      yield Promise.all(Object.keys(_this11.objects).map(hash => _this11._writeOne(_this11.objects[hash])));

      const added = _this11.componentsIndex.addMany(_ramda().default.values(_this11.objects));

      if (added) yield _this11.componentsIndex.write();
    })();
  }
  /**
   * do not call this method directly. always call this.removeObject() and once done with all objects,
   * call this.persist()
   */


  _deleteOne(ref) {
    this.removeFromCache(ref);
    const pathToDelete = this.objectPath(ref);

    _logger().default.debug(`repository._deleteOne: deleting ${pathToDelete}`);

    return (0, _fsRemoveFile().default)(pathToDelete, true);
  }
  /**
   * always prefer this.persist().
   * this method doesn't write to componentsIndex. so using this method for ModelComponent or
   * Symlink makes the index outdated.
   */


  _writeOne(object) {
    var _this12 = this;

    return (0, _bluebird().coroutine)(function* () {
      const contents = yield object.compress();
      const options = {};
      if (_this12.scopeJson.groupName) options.gid = yield (0, _utils().resolveGroupId)(_this12.scopeJson.groupName);

      const objectPath = _this12.objectPath(object.hash());

      _logger().default.debug(`repository._writeOne: ${objectPath}`);

      return (0, _utils().writeFile)(objectPath, contents, options);
    })();
  }

}

exports.default = Repository;