"use strict";

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

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = exports.nonPackageJsonFields = exports.overridesBitInternalFields = exports.overridesForbiddenFields = exports.dependenciesFields = 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 _bitId() {
  const data = _interopRequireDefault(require("../../bit-id/bit-id"));

  _bitId = function () {
    return data;
  };

  return data;
}

function _hasWildcard() {
  const data = _interopRequireDefault(require("../../utils/string/has-wildcard"));

  _hasWildcard = function () {
    return data;
  };

  return data;
}

function _isBitIdMatchByWildcards() {
  const data = _interopRequireDefault(require("../../utils/bit/is-bit-id-match-by-wildcards"));

  _isBitIdMatchByWildcards = function () {
    return data;
  };

  return data;
}

function _validateType() {
  const data = require("../../utils/validate-type");

  _validateType = function () {
    return data;
  };

  return data;
}

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

  _generalError = function () {
    return data;
  };

  return data;
}

function _abstractConfig() {
  const data = _interopRequireDefault(require("./abstract-config"));

  _abstractConfig = function () {
    return data;
  };

  return data;
}

const dependenciesFields = ['dependencies', 'devDependencies', 'peerDependencies'];
exports.dependenciesFields = dependenciesFields;
const overridesForbiddenFields = ['name', 'main', 'version', 'bit'];
exports.overridesForbiddenFields = overridesForbiddenFields;
const overridesBitInternalFields = ['propagate', 'exclude', 'env'];
exports.overridesBitInternalFields = overridesBitInternalFields;
const nonPackageJsonFields = [...dependenciesFields, ...overridesBitInternalFields];
exports.nonPackageJsonFields = nonPackageJsonFields;

class ConsumerOverrides {
  // whether the overrides has been changed (so then it should write them to fs)
  constructor(overrides) {
    this.overrides = overrides;
  }

  static load(overrides = {}) {
    return new ConsumerOverrides(overrides);
  }

  getOverrideComponentData(bitId) {
    const matches = this._getAllRulesMatchedById(bitId);

    if (!matches.length) {
      return null;
    }

    const overrideValues = matches.map(match => this.overrides[match]);
    let stopPropagation = false;
    return overrideValues.reduce((acc, current) => {
      if (stopPropagation) return acc;

      if (!current.propagate) {
        stopPropagation = true;
      }

      this._updateSpecificOverridesWithGeneralOverrides(current, acc);

      return acc;
    }, {});
  }

  _updateSpecificOverridesWithGeneralOverrides(generalOverrides, specificOverrides) {
    const isObjectAndNotArray = val => typeof val === 'object' && !Array.isArray(val);

    Object.keys(generalOverrides).forEach(field => {
      switch (field) {
        case 'env':
          if (!specificOverrides[field]) specificOverrides[field] = {};
          ['compiler', 'tester'].forEach(envField => {
            if (specificOverrides.env[envField] || !generalOverrides.env[envField]) return;
            specificOverrides.env[envField] = generalOverrides.env[envField];
          });
          break;

        case 'propagate':
        case 'exclude':
          // it's a system field, do nothing
          break;

        default:
          if (isObjectAndNotArray(specificOverrides[field]) && isObjectAndNotArray(generalOverrides[field])) {
            specificOverrides[field] = Object.assign(generalOverrides[field], specificOverrides[field]);
          } else if (!specificOverrides[field]) {
            specificOverrides[field] = generalOverrides[field];
          }

        // when specificOverrides[field] is set and not an object, do not override it by the general one
      }
    });
  }

  _getAllRulesMatchedById(bitId) {
    const exactMatch = this.findExactMatch(bitId);
    const matchByGlobPattern = Object.keys(this.overrides).filter(idStr => this._isMatchByWildcard(bitId, idStr));
    const nonExcluded = matchByGlobPattern.filter(match => !this._isExcluded(this.overrides[match], bitId));
    const allMatches = nonExcluded.sort(ConsumerOverrides.sortWildcards);
    if (exactMatch) allMatches.unshift(exactMatch);
    return allMatches;
  }

  _isMatchByWildcard(bitId, idWithPossibleWildcard) {
    if (!(0, _hasWildcard().default)(idWithPossibleWildcard)) return false;
    return (0, _isBitIdMatchByWildcards().default)(bitId, idWithPossibleWildcard);
  }

  _isExcluded(overridesValues, bitId) {
    if (!overridesValues.exclude || !overridesValues.exclude.length) {
      return false;
    }

    return overridesValues.exclude.some(excludeRule => this._isMatchByWildcard(bitId, excludeRule) || bitId.toStringWithoutVersion() === excludeRule || bitId.toStringWithoutScopeAndVersion() === excludeRule);
  }
  /**
   * sort from the more specific (more namespaces) to the more generic (less namespaces)
   * e.g.
   * src/utils/javascript/*
   * src/utils/javascript/*
   * src/utils/*
   * src/*
   *
   * more namespaces (slashes) === more specific
   * more wildcards === less specific
   *
   * if both have the same number of namespaces (slashes), the one with less wildcards is first.
   * if both have the same number of wildcards, the one with more namespaces is first.
   *
   * a reminder about compare function:
   * If the result is negative a is sorted before b.
   * If the result is positive b is sorted before a.
   * If the result is 0 no changes is done with the sort order of the two values.
   */


  static sortWildcards(a, b) {
    const numOfNamespaces = str => (str.match(/\//g) || []).length;

    const numOfWildcards = str => (str.match(/\*/g) || []).length;

    const indexOfFirstWildcard = str => str.indexOf('*');

    const byNamespaces = numOfNamespaces(b) - numOfNamespaces(a);
    if (byNamespaces !== 0) return byNamespaces;
    const byWildcards = numOfWildcards(a) - numOfWildcards(b);
    if (byWildcards !== 0) return byWildcards; // both have the same number of namespaces and the same number of wildcards
    // e.g. a component `utils/is-string` matches two rules: `utils/*` and `*/is-string`.
    // the one with the wildcard more left should be first as it is more specific.

    return indexOfFirstWildcard(a) - indexOfFirstWildcard(b);
  }

  updateOverridesIfChanged(component, areEnvsChanged) {
    var _this = this;

    return (0, _bluebird().coroutine)(function* () {
      const overrides = component.overrides.componentOverridesData;
      const id = component.id;

      const existingOverrides = _this.getOverrideComponentData(id);

      if (!areEnvsChanged && _this.areOverridesObjectsEqual(existingOverrides, overrides)) return false;

      const exactMatch = _this.findExactMatch(id);

      const key = exactMatch || id.toStringWithoutVersion();
      const env = {};

      if (component.compiler) {
        env.compiler = _abstractConfig().default.convertEnvToStringIfPossible(component.compiler.toBitJsonObject('.'));
      }

      if (component.tester) {
        env.tester = _abstractConfig().default.convertEnvToStringIfPossible(component.tester.toBitJsonObject('.'));
      }

      if (!_ramda().default.isEmpty(env)) overrides.env = env;
      _this.overrides[key] = overrides;
      _this.hasChanged = true;
      return true;
    })();
  }

  areOverridesObjectsEqual(overridesA, overridesB) {
    // seems like R.equals does a great job here. it compares objects by values (not by reference).
    // also it disregards the keys order.
    return _ramda().default.equals(overridesA, overridesB);
  }

  findExactMatch(bitId) {
    return Object.keys(this.overrides).find(idStr => bitId.toStringWithoutVersion() === idStr || bitId.toStringWithoutScopeAndVersion() === idStr);
  }

  removeExactMatch(bitId) {
    const exactMatch = this.findExactMatch(bitId);
    if (!exactMatch) return false;
    delete this.overrides[exactMatch];
    return true;
  }

  static validate(overrides) {
    if (typeof overrides === 'undefined') return;
    const message = 'consumer-config (either bit.json or package.json "bit")';
    (0, _validateType().validateUserInputType)(message, overrides, 'overrides', 'object');
    Object.keys(overrides).forEach(id => validateComponentOverride(id, overrides[id]));

    function validateComponentOverride(id, override) {
      (0, _validateType().validateUserInputType)(message, override, `overrides.${id}`, 'object');
      Object.keys(override).forEach(field => {
        if (overridesForbiddenFields.includes(field)) {
          throw new (_generalError().default)(`${message} found a forbidden field "${field}" inside "overrides.${id}" property.
the following fields are not allowed: ${overridesForbiddenFields.join(', ')}.`);
        }

        if (dependenciesFields.includes(field)) {
          validateDependencyField(field, override, id);
        } else if (field === 'env') {
          validateEnv(override, id);
        } else if (field === 'exclude') {
          (0, _validateType().validateUserInputType)(message, override.exclude, `overrides.${id}.exclude`, 'array');
        }
      });
    }

    function validateDependencyField(field, override, id) {
      (0, _validateType().validateUserInputType)(message, override[field], `overrides.${id}.${field}`, 'object');
      Object.keys(override[field]).forEach(rule => {
        (0, _validateType().validateUserInputType)(message, override[field][rule], `overrides.${id}.${field}.${rule}`, 'string');
      });
    }

    function validateEnv(override, id) {
      (0, _validateType().validateUserInputType)(message, override.env, `overrides.${id}.env`, 'object');
    }
  }

}

exports.default = ConsumerOverrides;