Source: manufacturer.js

const Puzzle = require('./puzzle');
const Piece = require('./piece');
const {Anchor} = require('./anchor');
const {anchor} = require('./anchor');
const {fixed, InsertSequence} = require('./sequence')
const Metadata = require('./metadata');

/**
 * A manufacturer allows to create rectangular
 * puzzles by automatically generating inserts
 */
class Manufacturer {
  constructor() {
    this.insertsGenerator = fixed;
    this.metadata = [];
    /** @type {Anchor} */
    this.headAnchor = null;
  }

  /**
   * Attach metadata to each piece
   *
   * @param {object[]} metadata list of metadata that will be attached to each generated piece
   */
  withMetadata(metadata) {
    this.metadata = metadata;
  }

  /**
   * @param {import('./sequence').InsertsGenerator} generator
   */
  withInsertsGenerator(generator) {
    this.insertsGenerator = generator || this.insertsGenerator;
  }

  /**
   * Sets the central anchor. If not specified, puzzle will be positioned
   * at the distance of a whole piece from the origin
   *
   * @param {Anchor} anchor
   */
  withHeadAt(anchor) {
    this.headAnchor = anchor;
  }

  /**
   * If nothing is configured, default Puzzle structured is assumed
   *
   * @param {import('../src/puzzle').Settings} structure
   */
  withStructure(structure) {
    this.structure = structure
  }

  /**
   *
   * @param {number} width
   * @param {number} height
   */
  withDimensions(width, height) {
    this.width = width;
    this.height = height;
  }

   /**
   * @returns {Puzzle}
   */
  build() {
    const puzzle = new Puzzle(this.structure);
    const positioner = new Positioner(puzzle, this.headAnchor);

    let verticalSequence = this._newSequence();
    let horizontalSequence;

    for (let y = 0; y < this.height; y++) {
      horizontalSequence = this._newSequence();
      verticalSequence.next();

      for (let x = 0; x < this.width; x++) {
        horizontalSequence.next();
        const piece = this._buildPiece(puzzle, horizontalSequence, verticalSequence);
        piece.centerAround(positioner.naturalAnchor(x, y));
      }
    }
    this._annotateAll(puzzle.pieces);
    return puzzle;
  }

  /**
   * @param {Piece[]} pieces
   */
  _annotateAll(pieces) {
    pieces.forEach((piece, index) => this._annotate(piece, index));
  }

  /**
   * @param {Piece} piece
   * @param {number} index
   */
  _annotate(piece, index) {
    const baseMetadata = this.metadata[index];
    const metadata = baseMetadata ? Metadata.copy(baseMetadata) : {};
    metadata.id = metadata.id || String(index + 1);
    piece.annotate(metadata);
  }

  _newSequence() {
    return new InsertSequence(this.insertsGenerator);
  }

  /**
   * @param {Puzzle} puzzle
   * @param {InsertSequence} horizontalSequence
   * @param {InsertSequence} verticalSequence
   */
  _buildPiece(puzzle, horizontalSequence, verticalSequence) {
    return puzzle.newPiece({
      left: horizontalSequence.previousComplement(),
      up: verticalSequence.previousComplement(),
      right: horizontalSequence.current(this.width),
      down: verticalSequence.current(this.height)
    });
  }
}

class Positioner {
  /**
   *
   * @param {Puzzle} puzzle
   * @param {Anchor} headAnchor
   */
  constructor(puzzle, headAnchor) {
    this.puzzle = puzzle;
    this.initializeOffset(headAnchor);
  }

  /**
   * @param {Anchor} headAnchor
   */
  initializeOffset(headAnchor) {
    if (headAnchor) {
      /** @type {import('./vector').Vector} */
      this.offset = headAnchor.asVector();
    }
    else {
      this.offset = this.pieceDiameter;
    }
  }

  get pieceDiameter() {
    return this.puzzle.pieceDiameter;
  }

    /**
   * @param {number} x
   * @param {number} y
   */
  naturalAnchor(x, y) {
    return anchor(
      x * this.pieceDiameter.x + this.offset.x,
      y * this.pieceDiameter.y + this.offset.y);
  }
}

module.exports = Manufacturer;