// License: Apache-2.0
// Copyright Vladimir Sitnikov, 2021
// See https://github.com/vlsi/github-actions-random-matrix

class Axis {
  constructor({name, title, values}) {
    this.name = name;
    this.title = title;
    this.values = values;
    // If all entries have same weight, the axis has uniform distribution
    this.uniform = values.reduce((a, b) => a === (b.weight || 1) ? a : 0, values[0].weight || 1) !== 0
    this.totalWeigth = this.uniform ? values.length : values.reduce((a, b) => a + (b.weight || 1), 0);
  }

  static matches(row, filter) {
    if (typeof filter === 'function') {
      return filter(row);
    }
    if (Array.isArray(filter)) {
      // e.g. row={os: 'windows'}; filter=[{os: 'linux'}, {os: 'linux'}]
      return filter.find(v => Axis.matches(row, v));
    }
    if (typeof filter === 'object') {
      // e.g. row={jdk: {name: 'openjdk', version: 8}}; filter={jdk: {version: 8}}
      for (const [key, value] of Object.entries(filter)) {
        if (!row.hasOwnProperty(key) || !Axis.matches(row[key], value)) {
          return false;
        }
      }
      return true;
    }
    return row == filter;
  }

  pickValue(filter) {
    let values = this.values;
    if (filter) {
      values = values.filter(v => Axis.matches(v, filter));
    }
    if (values.length == 0) {
      const filterStr = typeof filter === 'string' ? filter.toString() : JSON.stringify(filter);
      throw Error(`No values produces for axis '${this.name}' from ${JSON.stringify(this.values)}, filter=${filterStr}`);
    }
    if (values.length == 1) {
      return values[0];
    }
    if (this.uniform) {
      return values[Math.floor(Math.random() * values.length)];
    }
    const totalWeight = !filter ? this.totalWeigth : values.reduce((a, b) => a + (b.weight || 1), 0);
    let weight = Math.random() * totalWeight;
    for (let i = 0; i < values.length; i++) {
      const value = values[i];
      weight -= value.weight || 1;
      if (weight <= 0) {
        return value;
      }
    }
    return values[values.length - 1];
  }
}

class MatrixBuilder {
  constructor() {
    this.axes = [];
    this.axisByName = {};
    this.rows = [];
    this.duplicates = {};
    this.excludes = [];
    this.includes = [];
  }

  /**
   * Specifies include filter (all the generated rows would comply with all the include filters)
   * @param filter
   */
  include(filter) {
    this.includes.push(filter);
  }

  /**
   * Specifies exclude filter (e.g. exclude a forbidden combination)
   * @param filter
   */
  exclude(filter) {
    this.excludes.push(filter);
  }

  addAxis({name, title, values}) {
    const axis = new Axis({name, title, values});
    this.axes.push(axis);
    this.axisByName[name] = axis;
    return axis;
  }

  setNamePattern(names) {
    this.namePattern = names;
  }

  /**
   * Adds a row that matches the given filter to the resulting matrix.
   * filter values could be
   *  - literal values: filter={os: 'windows-latest'}
   *  - arrays: filter={os: ['windows-latest', 'linux-latest']}
   *  - functions: filter={os: x => x!='windows-latest'}
   * @param filter object with keys matching axes names
   * @returns {*}
   */
  generateRow(filter) {
    let res;
    if (filter) {
      // If matching row already exists, no need to generate more
      res = this.rows.find(v => Axis.matches(v, filter));
      if (res) {
        return res;
      }
    }
    for (let i = 0; i < 142; i++) {
      res = this.axes.reduce(
        (prev, next) =>
          Object.assign(prev, {
            [next.name]: next.pickValue(filter ? filter[next.name] : undefined)
          }),
        {}
      );
      if (this.excludes.length > 0 && this.excludes.find(f => Axis.matches(res, f)) ||
        this.includes.length > 0 && !this.includes.find(f => Axis.matches(res, f))) {
        continue;
      }
      const key = JSON.stringify(res);
      if (!this.duplicates.hasOwnProperty(key)) {
        this.duplicates[key] = true;
        res.name =
          this.namePattern.map(axisName => {
            let value = res[axisName];
            const title = value.title;
            if (typeof title != 'undefined') {
              return title;
            }
            const computeTitle = this.axisByName[axisName].title;
            return computeTitle ? computeTitle(value) : value;
          }).filter(Boolean).join(", ");
        this.rows.push(res);
        return res;
      }
    }
    throw Error(`Unable to generate row. Please check include and exclude filters`);
  }

  generateRows(maxRows, filter) {
    for (let i = 0; this.rows.length < maxRows && i < maxRows; i++) {
      this.generateRow(filter);
    }
    return this.rows;
  }
}

module.exports = {Axis, MatrixBuilder};
