VexFlow - Copyright (c) Mohit Muthanna 2010.

import { Vex } from './vex';
import { Element } from './element';
import { BoundingBoxComputation } from './boundingboxcomputation';
import { BoundingBox } from './boundingbox';
import { Font } from './fonts/vexflow_font';

function processOutline(outline, originX, originY, scaleX, scaleY, outlineFns) {
  let command;
  let x;
  let y;
  let i = 0;

  function nextX() { return originX + outline[i++] * scaleX; }
  function nextY() { return originY + outline[i++] * scaleY; }

  while (i < outline.length) {
    command = outline[i++];
    switch (command) {
      case 'm':
      case 'l':
        outlineFns[command](nextX(), nextY());
        break;
      case 'q':
        x = nextX();
        y = nextY();
        outlineFns.q(nextX(), nextY(), x, y);
        break;
      case 'b':
        x = nextX();
        y = nextY();
        outlineFns.b(nextX(), nextY(), nextX(), nextY(), x, y);
        break;
      default:
        break;
    }
  }
}

export class Glyph extends Element {
  /* Static methods used to implement loading / unloading of glyphs */
  static loadMetrics(font, code, cache) {
    const glyph = font.glyphs[code];
    if (!glyph) {
      throw new Vex.RERR('BadGlyph', `Glyph ${code} does not exist in font.`);
    }

    const x_min = glyph.x_min;
    const x_max = glyph.x_max;
    const ha = glyph.ha;

    let outline;

    if (glyph.o) {
      if (cache) {
        if (glyph.cached_outline) {
          outline = glyph.cached_outline;
        } else {
          outline = glyph.o.split(' ');
          glyph.cached_outline = outline;
        }
      } else {
        if (glyph.cached_outline) delete glyph.cached_outline;
        outline = glyph.o.split(' ');
      }

      return {
        x_min,
        x_max,
        ha,
        outline,
      };
    } else {
      throw new Vex.RERR('BadGlyph', `Glyph ${code} has no outline defined.`);
    }
  }

  /**
   * A quick and dirty static glyph renderer. Renders glyphs from the default
   * font defined in Vex.Flow.Font.
   *
   * @param {!Object} ctx The canvas context.
   * @param {number} x_pos X coordinate.
   * @param {number} y_pos Y coordinate.
   * @param {number} point The point size to use.
   * @param {string} val The glyph code in Vex.Flow.Font.
   * @param {boolean} nocache If set, disables caching of font outline.
   */
  static renderGlyph(ctx, x_pos, y_pos, point, val, nocache) {
    const scale = point * 72.0 / (Font.resolution * 100.0);
    const metrics = Glyph.loadMetrics(Font, val, !nocache);
    Glyph.renderOutline(ctx, metrics.outline, scale, x_pos, y_pos);
  }

  static renderOutline(ctx, outline, scale, x_pos, y_pos) {
    ctx.beginPath();
    ctx.moveTo(x_pos, y_pos);
    processOutline(outline, x_pos, y_pos, scale, -scale, {
      m: ctx.moveTo.bind(ctx),
      l: ctx.lineTo.bind(ctx),
      q: ctx.quadraticCurveTo.bind(ctx),
      b: ctx.bezierCurveTo.bind(ctx),
    });
    ctx.fill();
  }

  static getOutlineBoundingBox(outline, scale, x_pos, y_pos) {
    const bboxComp = new BoundingBoxComputation();

    processOutline(outline, x_pos, y_pos, scale, -scale, {
      m: bboxComp.addPoint.bind(bboxComp),
      l: bboxComp.addPoint.bind(bboxComp),
      q: bboxComp.addQuadraticCurve.bind(bboxComp),
      b: bboxComp.addBezierCurve.bind(bboxComp),
    });

    return new BoundingBox(
      bboxComp.x1,
      bboxComp.y1,
      bboxComp.width(),
      bboxComp.height()
    );
  }

  /**
   * @constructor
   */
  constructor(code, point, options) {
    super();
    this.attrs.type = 'Glyph';

    this.code = code;
    this.point = point;
    this.options = {
      cache: true,
      font: Font,
    };

    this.metrics = null;
    this.x_shift = 0;
    this.y_shift = 0;

    this.originShift = {
      x: 0,
      y: 0,
    };

    if (options) {
      this.setOptions(options);
    } else {
      this.reset();
    }
  }

  setOptions(options) {
    Vex.Merge(this.options, options);
    this.reset();
  }

  setPoint(point) { this.point = point; return this; }
  setStave(stave) { this.stave = stave; return this; }
  setXShift(x_shift) { this.x_shift = x_shift; return this; }
  setYShift(y_shift) { this.y_shift = y_shift; return this; }

  reset() {
    this.scale = this.point * 72 / (this.options.font.resolution * 100);
    this.metrics = Glyph.loadMetrics(
      this.options.font,
      this.code,
      this.options.cache
    );
    this.bbox = Glyph.getOutlineBoundingBox(
      this.metrics.outline,
      this.scale,
      0,
      0
    );
  }

  getMetrics() {
    if (!this.metrics) {
      throw new Vex.RuntimeError('BadGlyph', `Glyph ${this.code} is not initialized.`);
    }

    return {
      x_min: this.metrics.x_min * this.scale,
      x_max: this.metrics.x_max * this.scale,
      width: this.bbox.getW(),
      height: this.bbox.getH(),
    };
  }

  setOrigin(x, y) {
    const { bbox } = this;
    const originX = Math.abs(bbox.getX() / bbox.getW());
    const originY = Math.abs(bbox.getY() / bbox.getH());

    const xShift = (x - originX) * bbox.getW();
    const yShift = (y - originY) * bbox.getH();

    this.originShift = {
      x: -xShift,
      y: -yShift,
    };
  }

  render(ctx, x, y) {
    if (!this.metrics) {
      throw new Vex.RuntimeError('BadGlyph', `Glyph ${this.code} is not initialized.`);
    }

    const outline = this.metrics.outline;
    const scale = this.scale;

    Glyph.renderOutline(ctx, outline, scale, x + this.originShift.x, y + this.originShift.y);
  }

  renderToStave(x) {
    this.checkContext();

    if (!this.metrics) {
      throw new Vex.RuntimeError('BadGlyph', `Glyph ${this.code} is not initialized.`);
    }

    if (!this.stave) {
      throw new Vex.RuntimeError('GlyphError', 'No valid stave');
    }

    const outline = this.metrics.outline;
    const scale = this.scale;

    Glyph.renderOutline(this.context, outline, scale,
        x + this.x_shift, this.stave.getYForGlyphs() + this.y_shift);
  }
}
h