Source: validator.js

const Puzzle = require('./puzzle');
const Piece = require('./piece');
const Pair = require('./pair');

/**
 * @typedef {PieceValidator | PuzzleValidator | NullValidator} Validator
 **/

/**
 * @callback ValidationListener
 * @param {Puzzle} puzzle
 */

/** An abstract base validator */
class AbstractValidator {

  constructor() {
    /** @type {ValidationListener[]} */
    this.validListeners = [];
    this._valid = undefined;
  }

  /**
   * Validates the puzzle, updating the validity state and
   * firing validation events
   *
   * @param {Puzzle} puzzle
   */
  validate(puzzle) {
    const wasValid = this._valid;
    this.updateValidity(puzzle);
    if (this._valid && !wasValid) {
      this.fireValid(puzzle);
    }
  }

  /**
   * Updates the valid state.
   *
   * @param {Puzzle} puzzle
   */
  updateValidity(puzzle) {
    // @ts-ignore
    this._valid = this.isValid(puzzle);
  }

  /**
   * @param {Puzzle} puzzle
   */
  fireValid(puzzle) {
    this.validListeners.forEach(it => it(puzzle));
  }

  /**
   * Registers a validation listener
   *
   * @param {ValidationListener} f
   */
  onValid(f) {
    this.validListeners.push(f);
  }

  /**
   * Answers the current validity status of this validator. This
   * property neither alters the current status nor triggers new validity checks
   *
   * @type {boolean}
   */
  get valid() {
    return this._valid;
  }

  /**
   * Answers wether this is the {@link NullValidator}
   *
   * @type {boolean}
   */
  get isNull() {
    return false;
  }
}

/**
 * @callback PieceCondition
 * @param {Piece} puzzle
 * @returns {boolean}
 */


/**
 * @callback PuzzleCondition
 * @param {Puzzle} puzzle
 * @returns {boolean}
 */

/** A validator that evaluates each piece independently */
class PieceValidator extends AbstractValidator {

  /**
   * @param {PieceCondition} f
   */
  constructor(f) {
    super();
    this.condition = f;
  }

  /**
   * @param {Puzzle} puzzle
   * @returns {boolean}
   */
  isValid(puzzle) {
    return puzzle.pieces.every(it => this.condition(it));
  }
}

/** A validator that evaluates the whole puzzle */
class PuzzleValidator extends AbstractValidator {

  /**
   * @param {PuzzleCondition} f
   */
  constructor(f) {
    super();
    this.condition = f;
  }

  /**
   * @param {Puzzle} puzzle
   */
  isValid(puzzle) {
    return this.condition(puzzle);
  }

  /**
   * Compares two pairs
   *
   * @param {import('./pair').Pair} param0
   * @param {import('./pair').Pair} param1
   *
   * @returns {boolean}
   */
  static equalDiffs([dx0, dy0], [dx, dy]) {
    return Pair.equal(dx0, dy0, dx, dy, PuzzleValidator.DIFF_DELTA);
  }
}

/** A validator that always is invalid */
class NullValidator extends AbstractValidator {

  /**
   * @param {Puzzle} puzzle
   */
  isValid(puzzle) {
    return false;
  }

  /**
   * @type {boolean}
   */
  get isNull() {
    return true;
  }
};

/**
 * The delta used to compare distances
 *
 * @type {number}
 */
PuzzleValidator.DIFF_DELTA = 0.01;

/**
* @type {PuzzleCondition}
*/
PuzzleValidator.connected = (puzzle) => puzzle.connected;

/**
 * @param {import('./pair').Pair[]} expected the expected relative refs
 * @returns {PuzzleCondition}
 */
PuzzleValidator.relativeRefs = (expected) => {
  return (puzzle) => {
    function diff(x, y, index) {
      return Pair.diff(x, y, ...expected[index]);
    }
    const refs = puzzle.refs;
    const [x0, y0] = refs[0];
    const diff0 = diff(x0, y0, 0);
    return refs.every(([x, y], index) => PuzzleValidator.equalDiffs(diff0, diff(x, y, index)));
  };
};

module.exports = {
  PuzzleValidator,
  PieceValidator,
  NullValidator
}