"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 _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 _logger() {
  const data = _interopRequireDefault(require("../../logger/logger"));

  _logger = function () {
    return data;
  };

  return data;
}

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

  _constants = function () {
    return data;
  };

  return data;
}

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

  _utils = function () {
    return data;
  };

  return data;
}

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

  _bitId = function () {
    return data;
  };

  return data;
}

function _addComponents() {
  const data = _interopRequireDefault(require("../component-ops/add-components"));

  _addComponents = function () {
    return data;
  };

  return data;
}

function _exceptions() {
  const data = require("../component-ops/add-components/exceptions");

  _exceptions = function () {
    return data;
  };

  return data;
}

function _generalError() {
  const data = _interopRequireDefault(require("../../error/general-error"));

  _generalError = function () {
    return data;
  };

  return data;
}

function _validationError() {
  const data = _interopRequireDefault(require("../../error/validation-error"));

  _validationError = function () {
    return data;
  };

  return data;
}

function _componentNotFoundInPath() {
  const data = _interopRequireDefault(require("../component/exceptions/component-not-found-in-path"));

  _componentNotFoundInPath = function () {
    return data;
  };

  return data;
}

function _configDir() {
  const data = _interopRequireDefault(require("./config-dir"));

  _configDir = function () {
    return data;
  };

  return data;
}

class ComponentMap {
  // always set for IMPORTED and NESTED.
  // reason why trackDir and not re-use rootDir is because using rootDir requires all paths to be
  // relative to rootDir for consistency, then, when saving into the model changing them back to
  // be relative to consumer-root. (we can't save in the model relative to rootDir, otherwise the
  // dependencies paths won't work).
  // relevant for AUTHORED only when a component was added as a directory, used for tracking changes in that dir
  // directory shared among a component and its dependencies by the original author. Relevant for IMPORTED only
  // a wrapper directory needed when a user adds a package.json file to the component root so then it won't collide with Bit generated one
  // wether the compiler / tester are detached from the workspace global configuration
  // relevant for authored components only, it helps finding out whether a component has a scope
  constructor({
    id,
    files,
    mainFile,
    rootDir,
    trackDir,
    configDir,
    origin,
    originallySharedDir,
    wrapDir
  }) {
    let confDir;

    if (configDir && typeof configDir === 'string') {
      confDir = new (_configDir().default)(configDir);
    } else if (configDir && configDir instanceof _configDir().default) {
      confDir = configDir.clone();
    }

    this.id = id;
    this.files = files;
    this.mainFile = mainFile;
    this.rootDir = rootDir;
    this.trackDir = trackDir;
    this.configDir = confDir;
    this.origin = origin;
    this.originallySharedDir = originallySharedDir;
    this.wrapDir = wrapDir;
  }

  static fromJson(componentMapObj) {
    return new ComponentMap(componentMapObj);
  }

  toPlainObject() {
    let res = {
      id: this.id,
      files: this.files,
      mainFile: this.mainFile,
      rootDir: this.rootDir,
      trackDir: this.trackDir,
      configDir: this.configDir ? this.configDir.linuxDirPath : undefined,
      origin: this.origin,
      originallySharedDir: this.originallySharedDir,
      wrapDir: this.wrapDir,
      exported: this.exported
    };

    const notNil = val => {
      return !_ramda().default.isNil(val);
    };

    res = _ramda().default.filter(notNil, res);
    return res;
  }

  static getPathWithoutRootDir(rootDir, filePath) {
    const newPath = (0, _utils().pathRelativeLinux)(rootDir, filePath);

    if (newPath.startsWith('..')) {
      // this is forbidden for security reasons. Allowing files to be written outside the components directory may
      // result in overriding OS files.
      throw new (_generalError().default)(`unable to add file ${filePath} because it's located outside the component root dir ${rootDir}`);
    }

    return newPath;
  }

  static changeFilesPathAccordingToItsRootDir(existingRootDir, files) {
    const changes = [];
    files.forEach(file => {
      const newPath = this.getPathWithoutRootDir(existingRootDir, file.relativePath);
      changes.push({
        from: file.relativePath,
        to: newPath
      });
      file.relativePath = newPath;
    });
    return changes;
  }

  setMarkAsChangedCb(markAsChangedBinded) {
    this.markBitMapChangedCb = markAsChangedBinded;
  }

  _findFile(fileName) {
    return this.files.find(file => {
      const filePath = this.rootDir ? (0, _utils().pathJoinLinux)(this.rootDir, file.relativePath) : file.relativePath;
      return filePath === fileName;
    });
  }

  updateFileLocation(fileFrom, fileTo) {
    fileFrom = (0, _utils().pathNormalizeToLinux)(fileFrom);
    fileTo = (0, _utils().pathNormalizeToLinux)(fileTo);

    const currentFile = this._findFile(fileFrom);

    const changes = [];

    if (currentFile) {
      const rootDir = this.rootDir;
      const newLocation = rootDir ? ComponentMap.getPathWithoutRootDir(rootDir, fileTo) : fileTo;

      _logger().default.debug(`updating file location from ${currentFile.relativePath} to ${newLocation}`);

      if (this.mainFile === currentFile.relativePath) this.mainFile = newLocation;
      changes.push({
        from: currentFile.relativePath,
        to: newLocation
      });
      currentFile.relativePath = newLocation;
      currentFile.name = _path().default.basename(newLocation);
    }

    this.validate();
    return changes;
  }

  updateDirLocation(dirFrom, dirTo) {
    dirFrom = (0, _utils().pathNormalizeToLinux)(dirFrom);
    dirTo = (0, _utils().pathNormalizeToLinux)(dirTo);
    const changes = [];

    if (this.rootDir && this.rootDir.startsWith(dirFrom)) {
      const rootDir = this.rootDir;
      const newRootDir = rootDir.replace(dirFrom, dirTo);
      const newRootDirNormalized = (0, _utils().pathNormalizeToLinux)(newRootDir);
      changes.push({
        from: rootDir,
        to: newRootDirNormalized
      });

      _logger().default.debug(`updating rootDir location from ${rootDir} to ${newRootDirNormalized}`);

      this.rootDir = newRootDirNormalized;
      return changes;
    }

    this.files.forEach(file => {
      const filePath = this.rootDir ? _path().default.join(this.rootDir, file.relativePath) : file.relativePath;

      if (filePath.startsWith(dirFrom)) {
        const fileTo = filePath.replace(dirFrom, dirTo);
        const newLocation = this.rootDir ? ComponentMap.getPathWithoutRootDir(this.rootDir, fileTo) : fileTo;

        _logger().default.debug(`updating file location from ${file.relativePath} to ${newLocation}`);

        if (this.mainFile === file.relativePath) this.mainFile = newLocation;
        changes.push({
          from: file.relativePath,
          to: newLocation
        });
        file.relativePath = newLocation;
      }
    });

    if (this.origin === _constants().COMPONENT_ORIGINS.AUTHORED && this.trackDir && this.trackDir === dirFrom) {
      this.trackDir = dirTo;
    }

    this.validate();
    return changes;
  }

  getFilesRelativeToConsumer() {
    return this.files.map(file => {
      return this.rootDir ? (0, _utils().pathJoinLinux)(this.rootDir, file.relativePath) : file.relativePath;
    });
  }

  getAllFilesPaths() {
    return this.files.map(file => file.relativePath);
  }

  getFilesGroupedByBeingTests() {
    const allFiles = [];
    const nonTestsFiles = [];
    const testsFiles = [];
    this.files.forEach(file => {
      allFiles.push(file.relativePath);
      if (file.test) testsFiles.push(file.relativePath);else nonTestsFiles.push(file.relativePath);
    });
    return {
      allFiles,
      nonTestsFiles,
      testsFiles
    };
  }
  /**
   * if one of the added files is outside of the trackDir, remove the trackDir attribute
   */


  removeTrackDirIfNeeded() {
    if (!this.trackDir) return;

    if (this.origin !== _constants().COMPONENT_ORIGINS.AUTHORED) {
      this.trackDir = undefined;
      return;
    }

    for (const file of this.files) {
      if (!file.relativePath.startsWith(this.trackDir)) {
        // Make sure we are not getting to case where we have config dir with {COMPONENT_DIR} but there is no component dir
        // This might happen in the following case:
        // User add a folder to bit which create the track dir
        // Then the user eject the config to that dir
        // Then the user adding a new file to that component which is outside of the component dir
        // This will remove the trackDir, so just before the remove we resolve it
        if (this.configDir) {
          this.configDir = this.configDir.getResolved({
            componentDir: this.trackDir
          });
        }

        this.trackDir = undefined;
        return;
      }
    }
  }
  /**
   * directory to track for changes (such as files added/renamed)
   */


  getTrackDir() {
    if (this.origin === _constants().COMPONENT_ORIGINS.AUTHORED) return this.trackDir;

    if (this.origin === _constants().COMPONENT_ORIGINS.IMPORTED) {
      return this.wrapDir ? (0, _utils().pathJoinLinux)(this.rootDir, this.wrapDir) : this.rootDir;
    } // DO NOT track nested components!


    return null;
  }
  /**
   * this.rootDir is not defined for author. instead, the current workspace is the rootDir
   * also, for imported environments (compiler/tester) components the rootDir is empty
   */


  getRootDir() {
    return this.rootDir || '.';
  }
  /**
   * directory of the component (root / track)
   */


  getComponentDir() {
    if (this.origin === _constants().COMPONENT_ORIGINS.AUTHORED) return this.trackDir;
    return this.rootDir;
  }

  setConfigDir(val) {
    if (val === null || val === undefined) {
      delete this.configDir;
      this.markBitMapChangedCb();
      return;
    }

    this.markBitMapChangedCb();
    this.configDir = new (_configDir().default)(val);
  }

  deleteConfigDirIfNotExists() {
    var _this = this;

    return (0, _bluebird().coroutine)(function* () {
      const resolvedDir = _this.getBaseConfigDir();

      if (resolvedDir) {
        const isExist = yield _fsExtra().default.exists(resolvedDir);

        if (!isExist) {
          _this.setConfigDir(null);
        }
      }
    })();
  }
  /**
   * Get resolved base config dir (the dir where the bit.json is) after resolving the DSL
   */


  getBaseConfigDir() {
    if (!this.configDir) return null;
    const componentDir = this.getComponentDir();
    const configDir = this.configDir && this.configDir.getResolved({
      componentDir
    }).getEnvTypeCleaned().linuxDirPath;
    return configDir;
  }
  /**
   * in case new files were created in the track-dir directory, add them to the component-map
   * so then they'll be tracked by bitmap
   */


  trackDirectoryChanges(consumer, id) {
    var _this2 = this;

    return (0, _bluebird().coroutine)(function* () {
      const trackDir = _this2.getTrackDir();

      if (trackDir) {
        const trackDirAbsolute = _path().default.join(consumer.getPath(), trackDir);

        const trackDirRelative = _path().default.relative(process.cwd(), trackDirAbsolute);

        if (!_fsExtra().default.existsSync(trackDirAbsolute)) throw new (_componentNotFoundInPath().default)(trackDirRelative);
        const addParams = {
          componentPaths: [trackDirRelative || '.'],
          id: id.toString(),
          override: false,
          // this makes sure to not override existing files of componentMap
          trackDirFeature: true,
          origin: _this2.origin
        };
        const numOfFilesBefore = _this2.files.length;
        const addContext = {
          consumer
        };
        const addComponents = new (_addComponents().default)(addContext, addParams);

        try {
          yield addComponents.add();
        } catch (err) {
          if (err instanceof _exceptions().NoFiles || err instanceof _exceptions().EmptyDirectory) {// it might happen that a component is imported and current .gitignore configuration
            // are effectively removing all files from bitmap. we should ignore the error in that
            // case
          } else {
            throw err;
          }
        }

        if (_this2.files.length > numOfFilesBefore) {
          _logger().default.info(`new file(s) have been added to .bitmap for ${id.toString()}`);

          consumer.bitMap.hasChanged = true;
        }
      }
    })();
  }

  removeFiles(files) {
    const relativePaths = files.map(file => file.relativePath);
    this.files = this.files.reduce((accumulator, file) => {
      return relativePaths.includes(file.relativePath) ? accumulator : accumulator.concat(file);
    }, []);
    this.validate();
  }

  sort() {
    this.files = _ramda().default.sortBy(_ramda().default.prop('relativePath'), this.files);
  }

  clone() {
    // $FlowFixMe - there is some issue with the config dir type
    return new ComponentMap(this);
  }

  validate() {
    const errorMessage = `failed adding or updating a ${_constants().BIT_MAP} record of ${this.id.toString()}.`;
    if (!this.mainFile) throw new (_validationError().default)(`${errorMessage} mainFile attribute is missing`);

    if (!(0, _utils().isValidPath)(this.mainFile)) {
      throw new (_validationError().default)(`${errorMessage} mainFile attribute ${this.mainFile} is invalid`);
    } // if it's an environment component (such as compiler) the rootDir is empty


    if (!this.rootDir && this.origin === _constants().COMPONENT_ORIGINS.NESTED) {
      throw new (_validationError().default)(`${errorMessage} rootDir attribute is missing`);
    } // $FlowFixMe


    if (this.rootDir && !(0, _utils().isValidPath)(this.rootDir)) {
      throw new (_validationError().default)(`${errorMessage} rootDir attribute ${this.rootDir} is invalid`);
    }

    if (this.rootDir && this.origin === _constants().COMPONENT_ORIGINS.AUTHORED) {
      throw new (_validationError().default)(`${errorMessage} rootDir attribute should not be set for AUTHORED component`);
    }

    if (this.trackDir && this.origin !== _constants().COMPONENT_ORIGINS.AUTHORED) {
      throw new (_validationError().default)(`${errorMessage} trackDir attribute should be set for AUTHORED component only`);
    }

    if (this.originallySharedDir && this.origin === _constants().COMPONENT_ORIGINS.AUTHORED) {
      throw new (_validationError().default)(`${errorMessage} originallySharedDir attribute should be set for non AUTHORED components only`);
    }

    if (!this.files || !this.files.length) throw new (_validationError().default)(`${errorMessage} files list is missing`);
    this.files.forEach(file => {
      if (!(0, _utils().isValidPath)(file.relativePath)) {
        throw new (_validationError().default)(`${errorMessage} file path ${file.relativePath} is invalid`);
      }
    });
    const foundMainFile = this.files.find(file => file.relativePath === this.mainFile);

    if (!foundMainFile || _ramda().default.isEmpty(foundMainFile)) {
      throw new (_validationError().default)(`${errorMessage} mainFile ${this.mainFile} is not in the files list`);
    }

    const filesPaths = this.files.map(file => file.relativePath);
    const duplicateFiles = filesPaths.filter(file => filesPaths.filter(f => file.toLowerCase() === f.toLowerCase()).length > 1);

    if (duplicateFiles.length) {
      throw new (_validationError().default)(`${errorMessage} the following files are duplicated ${duplicateFiles.join(', ')}`);
    }

    if (this.trackDir) {
      const trackDir = this.trackDir;
      this.files.forEach(file => {
        if (!file.relativePath.startsWith(trackDir)) {
          throw new (_validationError().default)(`${errorMessage} a file path ${file.relativePath} is not in the trackDir ${trackDir}`);
        }
      });
    }
  }

}

exports.default = ComponentMap;