"use strict";

var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard");

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 _ramda() {
  const data = _interopRequireDefault(require("ramda"));

  _ramda = function () {
    return data;
  };

  return data;
}

function _objects() {
  const data = require("../objects");

  _objects = function () {
    return data;
  };

  return data;
}

function _componentObjects() {
  const data = _interopRequireDefault(require("../component-objects"));

  _componentObjects = 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 _models() {
  const data = require("../models");

  _models = function () {
    return data;
  };

  return data;
}

function _bitId() {
  const data = require("../../bit-id");

  _bitId = function () {
    return data;
  };

  return data;
}

function globalConfig() {
  const data = _interopRequireWildcard(require("../../api/consumer/lib/global-config"));

  globalConfig = function () {
    return data;
  };

  return data;
}

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

  _logger = function () {
    return data;
  };

  return data;
}

function _repository() {
  const data = _interopRequireDefault(require("../objects/repository"));

  _repository = function () {
    return data;
  };

  return data;
}

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

  _abstractVinyl = function () {
    return data;
  };

  return data;
}

function _manipulateDir() {
  const data = require("../../consumer/component-ops/manipulate-dir");

  _manipulateDir = function () {
    return data;
  };

  return data;
}

class SourceRepository {
  constructor(scope) {
    this.scope = scope;
  }

  objects() {
    return this.scope.objects;
  }

  getMany(ids) {
    _logger().default.debug(`sources.getMany, Ids: ${ids.join(', ')}`);

    return Promise.all(ids.map(id => {
      return this.get(id).then(component => {
        return {
          id,
          component
        };
      });
    }));
  }

  get(bitId) {
    var _this = this;

    return (0, _bluebird().coroutine)(function* () {
      const component = _models().ModelComponent.fromBitId(bitId);

      const foundComponent = yield _this._findComponent(component);

      if (foundComponent && bitId.hasVersion()) {
        // $FlowFixMe
        const msg = `found ${bitId.toStringWithoutVersion()}, however version ${bitId.getVersion().versionNum}`; // $FlowFixMe

        if (!foundComponent.versions[bitId.version]) {
          _logger().default.debugAndAddBreadCrumb('sources.get', `${msg} is not in the component versions array`);

          return null;
        } // $FlowFixMe


        const version = yield _this.objects().load(foundComponent.versions[bitId.version]);

        if (!version) {
          _logger().default.debugAndAddBreadCrumb('sources.get', `${msg} object was not found on the filesystem`);

          return null;
        }
      }

      return foundComponent;
    })();
  }

  _findComponent(component) {
    var _this2 = this;

    return (0, _bluebird().coroutine)(function* () {
      try {
        const foundComponent = yield _this2.objects().load(component.hash());

        if (foundComponent instanceof _models().Symlink) {
          return _this2._findComponentBySymlink(foundComponent);
        }

        if (foundComponent) return foundComponent;
      } catch (err) {
        _logger().default.error(`findComponent got an error ${err}`);
      }

      _logger().default.debug(`failed finding a component ${component.id()} with hash: ${component.hash().toString()}`);

      return null;
    })();
  }

  _findComponentBySymlink(symlink) {
    var _this3 = this;

    return (0, _bluebird().coroutine)(function* () {
      const realComponentId = symlink.getRealComponentId();

      const realModelComponent = _models().ModelComponent.fromBitId(realComponentId);

      const foundComponent = yield _this3.objects().load(realModelComponent.hash());

      if (!foundComponent) {
        throw new Error(`error: found a symlink object "${symlink.id()}" that references to a non-exist component "${realComponentId.toString()}".
if you have the steps to reproduce the issue, please open a Github issue with the details.
to quickly fix the issue, please delete the object at "${_this3.objects().objectPath(symlink.hash())}"`);
      }

      return foundComponent;
    })();
  }

  getObjects(id) {
    return this.get(id).then(component => {
      if (!component) throw new (_exceptions().ComponentNotFound)(id.toString());
      return component.collectObjects(this.objects());
    });
  }

  findOrAddComponent(props) {
    const comp = _models().ModelComponent.from(props);

    return this._findComponent(comp).then(component => {
      if (!component) return comp;
      return component;
    });
  }

  modifyCIProps({
    source,
    ciProps
  }) {
    const objectRepo = this.objects();
    return this.findOrAddComponent(source).then(component => {
      return component.loadVersion(component.latest(), objectRepo).then(version => {
        version.setCIProps(ciProps);
        return objectRepo._writeOne(version);
      });
    });
  }

  modifySpecsResults({
    source,
    specsResults
  }) {
    const objectRepo = this.objects();
    return this.findOrAddComponent(source).then(component => {
      return component.loadVersion(component.latest(), objectRepo).then(version => {
        version.setSpecsResults(specsResults);
        return objectRepo._writeOne(version);
      });
    });
  } // TODO: This should treat dist as an array


  updateDist({
    source
  }) {
    const objectRepo = this.objects();
    return this.findOrAddComponent(source).then(component => {
      return component.loadVersion(component.latest(), objectRepo).then(version => {
        const dist = source.dist ? _models().Source.from(Buffer.from(source.dist.toString())) : null;
        version.setDist(dist);
        objectRepo.add(dist).add(version);
        return objectRepo.persist();
      });
    });
  }
  /**
   * given a consumer-component object, returns the Version representation.
   * useful for saving into the model or calculation the hash for comparing with other Version object.
   * among other things, it reverts the path manipulation that was done when a component was loaded
   * from the filesystem. it adds the originallySharedDir and strip the wrapDir.
   *
   * warning: Do not change anything on the consumerComponent instance! Only use its clone.
   *
   * @see model-components.toConsumerComponent() for the opposite action. (converting Version to
   * ConsumerComponent).
   */


  consumerComponentToVersion({
    consumerComponent,
    consumer,
    versionFromModel,
    message,
    flattenedDependencies,
    flattenedDevDependencies,
    flattenedCompilerDependencies,
    flattenedTesterDependencies,
    specsResults
  }) {
    return (0, _bluebird().coroutine)(function* () {
      // $FlowFixMe
      const clonedComponent = consumerComponent.clone();

      const setEol = files => {
        if (!files) return;
        const result = files.map(file => {
          file.file = file.toSourceAsLinuxEOL();
          return file;
        });
        return result;
      };

      const manipulateDirs = pathStr => {
        return (0, _manipulateDir().revertDirManipulationForPath)(pathStr, clonedComponent.originallySharedDir, clonedComponent.wrapDir);
      };

      const files = consumerComponent.files.map(file => {
        return {
          name: file.basename,
          relativePath: manipulateDirs(file.relative),
          file: file.toSourceAsLinuxEOL(),
          test: file.test
        };
      });
      const {
        dists,
        mainDistFile
      } = clonedComponent.dists.toDistFilesModel(consumer, consumerComponent.originallySharedDir, consumerComponent.compiler);
      const compilerFiles = setEol(_ramda().default.path(['compiler', 'files'], consumerComponent));
      const testerFiles = setEol(_ramda().default.path(['tester', 'files'], consumerComponent));
      const [username, email] = yield Promise.all([globalConfig().get(_constants().CFG_USER_NAME_KEY), globalConfig().get(_constants().CFG_USER_EMAIL_KEY)]);
      clonedComponent.mainFile = manipulateDirs(clonedComponent.mainFile);
      clonedComponent.getAllDependencies().forEach(dependency => {
        // ignoreVersion because when persisting the tag is higher than currently exist in .bitmap
        const depFromBitMap = consumer.bitMap.getComponentIfExist(dependency.id, {
          ignoreVersion: true
        });
        dependency.relativePaths.forEach(relativePath => {
          if (!relativePath.isCustomResolveUsed) {
            // for isCustomResolveUsed it was never stripped
            relativePath.sourceRelativePath = manipulateDirs(relativePath.sourceRelativePath);
          }

          if (depFromBitMap && depFromBitMap.origin !== _constants().COMPONENT_ORIGINS.AUTHORED) {
            // when a dependency is not authored, we need to also change the
            // destinationRelativePath, which is the path written in the link file, however, the
            // dir manipulation should be according to this dependency component, not the
            // consumerComponent passed to this function
            relativePath.destinationRelativePath = (0, _manipulateDir().revertDirManipulationForPath)(relativePath.destinationRelativePath, depFromBitMap.originallySharedDir, depFromBitMap.wrapDir);
          }
        });
      });
      clonedComponent.overrides.addOriginallySharedDir(clonedComponent.originallySharedDir);

      const version = _models().Version.fromComponent({
        component: clonedComponent,
        versionFromModel,
        files,
        dists,
        mainDistFile,
        flattenedDependencies,
        flattenedDevDependencies,
        flattenedCompilerDependencies,
        flattenedTesterDependencies,
        specsResults,
        message,
        username,
        email
      }); // $FlowFixMe it's ok to override the pendingVersion attribute


      consumerComponent.pendingVersion = version; // helps to validate the version against the consumer-component

      return {
        version,
        files,
        dists,
        compilerFiles,
        testerFiles
      };
    })();
  }

  addSource({
    source,
    consumer,
    flattenedDependencies,
    flattenedDevDependencies,
    flattenedCompilerDependencies,
    flattenedTesterDependencies,
    message,
    exactVersion,
    releaseType,
    specsResults
  }) {
    var _this4 = this;

    return (0, _bluebird().coroutine)(function* () {
      const objectRepo = _this4.objects(); // if a component exists in the model, add a new version. Otherwise, create a new component on the model


      const component = yield _this4.findOrAddComponent(source); // TODO: instead of doing that like this we should use:
      // const versionFromModel = await component.loadVersion(source.usedVersion, this.scope.objects);
      // it looks like it's exactly the same code but it's not working from some reason

      const versionRef = component.versions[source.usedVersion];
      let versionFromModel;

      if (versionRef) {
        versionFromModel = yield _this4.scope.getObject(versionRef.hash);
      }

      const {
        version,
        files,
        dists,
        compilerFiles,
        testerFiles
      } = yield _this4.consumerComponentToVersion({
        consumerComponent: source,
        consumer,
        versionFromModel,
        message,
        flattenedDependencies,
        flattenedDevDependencies,
        flattenedCompilerDependencies,
        flattenedTesterDependencies,
        specsResults
      });
      component.addVersion(version, releaseType, exactVersion);
      objectRepo.add(version).add(component);
      files.forEach(file => objectRepo.add(file.file));
      if (dists) dists.forEach(dist => objectRepo.add(dist.file));
      if (compilerFiles) compilerFiles.forEach(file => objectRepo.add(file.file));
      if (testerFiles) testerFiles.forEach(file => objectRepo.add(file.file));
      return component;
    })();
  }

  putAdditionalVersion(component, version, message, releaseType = _constants().DEFAULT_BIT_RELEASE_TYPE) {
    var _this5 = this;

    return (0, _bluebird().coroutine)(function* () {
      const [username, email] = yield Promise.all([globalConfig().get(_constants().CFG_USER_NAME_KEY), globalConfig().get(_constants().CFG_USER_EMAIL_KEY)]);
      version.log = {
        message,
        username,
        email,
        date: Date.now().toString()
      };
      component.addVersion(version, releaseType);
      return _this5.put({
        component,
        objects: [version]
      });
    })();
  }

  put({
    component,
    objects
  }) {
    _logger().default.debug(`sources.put, id: ${component.id()}, versions: ${component.listVersions().join(', ')}`);

    const repo = this.objects();
    repo.add(component);

    const isObjectShouldBeAdded = obj => {
      // don't add a component if it's already exist locally with more versions
      if (obj instanceof _models().ModelComponent) {
        const loaded = repo.loadSync(obj.hash(), false);

        if (loaded) {
          if (Object.keys(loaded.versions) > Object.keys(obj.versions)) {
            return false;
          }
        }
      }

      return true;
    };

    objects.forEach(obj => {
      if (isObjectShouldBeAdded(obj)) repo.add(obj);
    });
    return component;
  }
  /**
   * remove specified component versions from component.
   * if all versions of a component were deleted, delete also the component.
   * it doesn't persist anything to the filesystem.
   * (repository.persist() needs to be called at the end of the operation)
   */


  removeComponentVersions(component, versions) {
    _logger().default.debug(`removeComponentVersion, component ${component.id()}, versions ${versions.join(', ')}`);

    const objectRepo = this.objects();
    versions.forEach(version => {
      const ref = component.removeVersion(version);
      objectRepo.removeObject(ref);
    });

    if (component.versionArray.length) {
      objectRepo.add(component); // add the modified component object
    } else {
      // if all versions were deleted, delete also the component itself from the model
      objectRepo.removeObject(component.hash());
    }
  }
  /**
   * @see this.removeComponent()
   *
   */


  removeComponentById(bitId) {
    var _this6 = this;

    return (0, _bluebird().coroutine)(function* () {
      _logger().default.debug(`sources.removeComponentById: ${bitId.toString()}`);

      const component = yield _this6.get(bitId);
      if (!component) return;

      _this6.removeComponent(component);
    })();
  }
  /**
   * remove all versions objects of the component from local scope.
   * if deepRemove is true, it removes also the refs associated with the removed versions.
   * finally, it removes the component object itself
   * it doesn't physically delete from the filesystem.
   * the actual delete is done at a later phase, once Repository.persist() is called.
   *
   * @param {ModelComponent} component
   * @param {boolean} [deepRemove=false] - whether remove all the refs or only the version array
   */


  removeComponent(component) {
    const repo = this.objects();

    _logger().default.debug(`sources.removeComponent: removing a component ${component.id()} from a local scope`);

    const objectRefs = component.versionArray;
    objectRefs.push(component.hash());
    repo.removeManyObjects(objectRefs);
  }
  /**
   * merge the existing component with the data from the incoming component
   * here, we assume that there is no conflict between the two, otherwise, this.merge() would throw
   * a MergeConflict exception.
   */


  mergeTwoComponentsObjects(existingComponent, incomingComponent) {
    // the base component to save is the existingComponent because it might contain local data that
    // is not available in the remote component, such as the "state" property.
    const mergedComponent = existingComponent;
    const mergedVersions = []; // in case the existing version hash is different than incoming version hash, use the incoming
    // version because we hold the incoming component from a remote as the source of truth

    Object.keys(existingComponent.versions).forEach(existingVersion => {
      if (incomingComponent.versions[existingVersion] && existingComponent.versions[existingVersion].toString() !== incomingComponent.versions[existingVersion].toString()) {
        mergedComponent.versions[existingVersion] = incomingComponent.versions[existingVersion];
        mergedVersions.push(existingVersion);
      }
    }); // in case the incoming component has versions that are not in the existing component, copy them

    Object.keys(incomingComponent.versions).forEach(incomingVersion => {
      if (!existingComponent.versions[incomingVersion]) {
        mergedComponent.versions[incomingVersion] = incomingComponent.versions[incomingVersion];
        mergedVersions.push(incomingVersion);
      }
    });
    return {
      mergedComponent,
      mergedVersions
    };
  }
  /**
   * Adds the objects into scope.object array, in-memory. It doesn't save anything to the file-system.
   *
   * When this function gets called originally from import command, the 'local' parameter is true. Otherwise, if it was
   * originated from export command, it'll be false.
   * If the 'local' is true and the existing component wasn't changed locally, it doesn't check for
   * discrepancies, but simply override the existing component.
   * In this context, "discrepancy" means, same version but different hashes.
   * When using import command, it makes sense to override a component in case of discrepancies because the source of
   * truth should be the remote scope from where the import fetches the component.
   * When the same component has different versions in the remote and the local, it merges the two
   * by calling this.mergeTwoComponentsObjects().
   */


  merge({
    component,
    objects
  }, inScope = false, local = true) {
    var _this7 = this;

    return (0, _bluebird().coroutine)(function* () {
      if (inScope) component.scope = _this7.scope.name;
      const existingComponent = yield _this7._findComponent(component);

      if (!existingComponent) {
        _this7.put({
          component,
          objects
        });

        return {
          mergedComponent: component,
          mergedVersions: component.listVersions()
        };
      }

      const locallyChanged = existingComponent.isLocallyChanged();

      if (local && !locallyChanged || component.compatibleWith(existingComponent, local)) {
        _logger().default.debug(`sources.merge component ${component.id()}`);

        const {
          mergedComponent,
          mergedVersions
        } = _this7.mergeTwoComponentsObjects(existingComponent, component);

        _this7.put({
          component: mergedComponent,
          objects
        });

        return {
          mergedComponent,
          mergedVersions
        };
      }

      const conflictVersions = component.diffWith(existingComponent, local);
      throw new (_exceptions().MergeConflict)(component.id(), conflictVersions);
    })();
  }

}

exports.default = SourceRepository;