'use strict';

Object.defineProperty(exports, '__esModule', {
  value: true
});

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }

var _lodash = require('lodash');

var _lodash2 = _interopRequireDefault(_lodash);

var _attributes = require('./attributes');

var SUPER_SUB_FONT_RATIO = 0.65; // matches MS word according to http://en.wikipedia.org/wiki/Subscript_and_superscript

function calcFontScale(fontSize, unitsPerEm) {
  return 1 / unitsPerEm * fontSize;
}

function calcSuperSubFontSize(fontSize, minFontSize) {
  var superSubFontSize = Math.round(fontSize * SUPER_SUB_FONT_RATIO);
  return superSubFontSize > minFontSize ? superSubFontSize : minFontSize;
}

function charFontStyle(char) {
  var attrs = char.attributes;
  if (!attrs) return 'regular';

  var bold = false;
  var italic = false;

  if (!_lodash2['default'].isUndefined(attrs[_attributes.ATTR.BOLD])) bold = attrs[_attributes.ATTR.BOLD];
  if (!_lodash2['default'].isUndefined(attrs[_attributes.ATTR.ITALIC])) italic = attrs[_attributes.ATTR.ITALIC];

  if (bold && italic) return 'boldItalic';else if (bold) return 'bold';else if (italic) return 'italic';else return 'regular';
}

function calcFontSizeFromAttributes(fontSize, minFontSize, attributes) {
  var hasAttribute = (0, _attributes.hasAttributeFor)(attributes);

  // superscript and subscript affect the font size
  var superscript = hasAttribute(_attributes.ATTR.SUPERSCRIPT);
  var subscript = hasAttribute(_attributes.ATTR.SUBSCRIPT);

  return superscript || subscript ? calcSuperSubFontSize(fontSize, minFontSize) : fontSize;
}

function fontSpec(fontSize, font) {
  var styleSpec = font.styleName === 'Regular' ? '' : font.styleName + ' ';
  var fontSizeSpec = fontSize + 'px ';
  var name = font.familyName;
  return styleSpec + fontSizeSpec + name;
}

function calcCharAdvanceOpenType(char, fontSize, font, unitsPerEm) {
  var glyph = font.charToGlyph(char);
  return glyph.unicode ? glyph.advanceWidth * calcFontScale(fontSize, unitsPerEm) : 0;
}

var canvas = _lodash2['default'].memoize(function () {
  return document.createElement('canvas');
});

var canvasContext = _lodash2['default'].memoize(function () {
  return canvas().getContext('2d');
});

function clearCanvas() {
  var c = canvas();
  canvasContext().clearRect(0, 0, c.width, c.height);
}

function calcTextAdvanceCanvas(text, fontSize, font) {
  // need to override newline handling, measureText doesn't handle it correctly (returns a non-zero width)
  if (text === '\n') {
    return 0;
  }
  var context = canvasContext();
  context.font = fontSpec(fontSize, font);
  return context.measureText(text).width;
}

/**
 * Tests various string widths and compares the advance width (in pixels) results between OpenType.js and
 * the canvas fallback mechanism which returns the browser's actual rendered font width.
 * @type {Function}
 */
var isOpenTypeJsReliable = _lodash2['default'].memoize(function (fontSize, font, unitsPerEm) {
  var strings = ['111111111111111111111111111111', 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', 'iiiiiiiiiiiiiiiiiiiiiiiiiiiiii', 'wwwwwwwwwwwwwwwwwwwwwwwwwwwwww', 'Lorem ipsum dolor sit amet, libris essent labitur duo cu.'];

  var reduceOt = function reduceOt(currentAdvance, char) {
    return currentAdvance + calcCharAdvanceOpenType(char, fontSize, font, unitsPerEm);
  };

  var reduceCanvas = function reduceCanvas(currentAdvance, char) {
    return currentAdvance + calcTextAdvanceCanvas(char, fontSize, font);
  };

  var reliable = true;
  var _iteratorNormalCompletion = true;
  var _didIteratorError = false;
  var _iteratorError = undefined;

  try {
    for (var _iterator = strings[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
      var candidate = _step.value;

      var chars = candidate.split('');
      var advanceOt = chars.reduce(reduceOt, 0);
      var advanceCanvas = calcTextAdvanceCanvas(candidate, fontSize, font);
      var delta = Math.abs(advanceOt - advanceCanvas);

      if (delta > 1) {
        console.warn('OpenType.js NOT reliable on this browser/OS (or font not loaded):\n  Candidate = [' + candidate + '], Fontspec = ' + fontSpec(fontSize, font) + ', Δ = ' + delta + 'px\n  Falling back to slower canvas measurement mechanism.');

        // test if canvas char-by-char width additions are the same as canvas total text width
        // if this ever returns false, then the current approach will need to be refactored, see docs on calcCharAdvance
        var advanceCanvasByChar = chars.reduce(reduceCanvas, 0);
        var deltaCanvas = Math.abs(advanceCanvas - advanceCanvasByChar);

        if (deltaCanvas > 1) {
          console.error('Canvas char-by-char width != canvas text width, oops!\n  Candidate = [' + candidate + '], Fontspec = ' + fontSpec(fontSize, font) + ', Δ ot = ' + delta + 'px, Δ canvas = ' + deltaCanvas + 'px\n  Please report this along with your browser/OS details.');
        }

        reliable = false;
        break;
      }
    }

    // clear the canvas, not really necessary as measureText shouldn't write anything there
  } catch (err) {
    _didIteratorError = true;
    _iteratorError = err;
  } finally {
    try {
      if (!_iteratorNormalCompletion && _iterator['return']) {
        _iterator['return']();
      }
    } finally {
      if (_didIteratorError) {
        throw _iteratorError;
      }
    }
  }

  clearCanvas();
  return reliable;
}, function (fontSize, font, unitsPerEm) {
  return fontSpec(fontSize, font) + ' ' + unitsPerEm;
});

/**
 * Calculate the advance in pixels for a given char. In some browsers/platforms/font sizes, the fonts are not
 * rendered according to the specs in the font (see
 * http://stackoverflow.com/questions/30922573/firefox-rendering-of-opentype-font-does-not-match-the-font-specification).
 * Therefore, ensure the font spec matches the actual rendered width (via the canvas `measureText` method), and use
 * the font spec if it matches, otherwise fall back to the (slower) measuredText option.
 *
 * NOTE there may still be one difference between the browser's rendering and canvas-based calculations here: the
 * browser renders entire strings within elements, whereas this calculation renders one character to the canvas at
 * a time and adds up the widths. These two approaches seem to be equivalent except for IE in compatibility mode.
 *
 * TODO refactor mixin to deal with chunks of styled text rather than chars for IE in compatibility mode
 */
function calcCharAdvance(char, fontSize, font, unitsPerEm) {
  return isOpenTypeJsReliable(fontSize, font, unitsPerEm) ? calcCharAdvanceOpenType(char, fontSize, font, unitsPerEm) : calcTextAdvanceCanvas(char, fontSize, font);
}

function calcReplicaCharAdvance(replicaChar, fontSize, fonts, minFontSize, unitsPerEm) {
  var style = charFontStyle(replicaChar);
  var charFontSize = calcFontSizeFromAttributes(fontSize, minFontSize, replicaChar.attributes);
  return calcCharAdvance(replicaChar.char, charFontSize, fonts[style], unitsPerEm);
}

exports['default'] = {
  setConfig: function setConfig(config) {
    this.config = config;
  },

  /**
   * Get the font scale to convert between font units and pixels for the given font size.
   * @param fontSize
   * @return {number}
   */
  fontScale: function fontScale(fontSize) {
    return calcFontScale(fontSize, this.config.unitsPerEm);
  },

  /**
   * Return the font size given the default font size and current attributes.
   * @param fontSize
   * @param attributes
   */
  fontSizeFromAttributes: function fontSizeFromAttributes(fontSize, attributes) {
    return calcFontSizeFromAttributes(fontSize, this.config.minFontSize, attributes);
  },

  /**
   * Determines the advance for a given replica char.
   * @param char The replica char object.
   * @param fontSize
   * @return {number}
   */
  replicaCharAdvance: function replicaCharAdvance(char, fontSize) {
    return calcReplicaCharAdvance(char, fontSize, this.config.fonts, this.config.minFontSize, this.config.unitsPerEm);
  },

  /**
   * Determines the advance for a given char. Since it is not a replica char, the font style and other attribute
   * information cannot be determined. A normal weight, non-decorated font with no special attributes is assumed.
   */
  charAdvance: function charAdvance(char, fontSize, font) {
    return calcCharAdvance(char, fontSize, font, this.config.unitsPerEm);
  },

  /**
   * Returns the advance width in pixels for a space character in the normal style.
   */
  advanceXForSpace: function advanceXForSpace(fontSize) {
    return this.charAdvance(' ', fontSize, this.config.fonts.regular);
  },

  /**
   * Obtain an Object with the char id and cursor position for a given pixel value. This is used to
   * set the current character and position the cursor correctly on a mouse click. If the target position
   * is past the last character, the index of the last character is returned.
   * @param {number} fontSize
   * @param {number} pixelValue
   * @param {Array} chars The characters used to compare against the given pixel value.
   * @return {Object} The cursor x position (cursorX) between characters, and the 0-based character index
   *   (index) for the given x value.
   */
  indexAndCursorForXValue: function indexAndCursorForXValue(fontSize, pixelValue, chars) {
    var minFontSize = this.config.minFontSize;
    fontSize = fontSize > minFontSize ? fontSize : minFontSize;
    var currentWidthPx = 0;
    var index = 0;
    for (var i = 0; i < chars.length; i++) {
      var glyphAdvancePx = this.replicaCharAdvance(chars[i], fontSize);
      if (pixelValue < currentWidthPx + glyphAdvancePx / 2) {
        return {
          cursorX: currentWidthPx,
          index: index
        };
      } else {
        currentWidthPx += glyphAdvancePx;
        if (glyphAdvancePx > 0) index++;
      }
    }
    return {
      cursorX: currentWidthPx,
      index: index
    };
  },

  /**
   * Get the advance width in pixels for the given char or chars.
   * @param {number} fontSize
   * @param {object|Array} chars
   * @return {number}
   */
  advanceXForChars: function advanceXForChars(fontSize, chars) {
    var _this = this;

    var minFontSize = this.config.minFontSize;
    fontSize = fontSize > minFontSize ? fontSize : minFontSize;
    if (_lodash2['default'].isArray(chars)) {
      return chars.reduce(function (currentWidthPx, char) {
        return currentWidthPx + _this.replicaCharAdvance(char, fontSize);
      }, 0);
    } else {
      return this.replicaCharAdvance(chars, fontSize);
    }
  },

  /**
   * Gets the line height in pixels for a given font size, using the bold font.
   */
  lineHeight: function lineHeight(fontSize) {
    var fontHeader = this.config.fonts.bold.tables.head;
    return (fontHeader.yMax - fontHeader.yMin) * this.fontScale(fontSize);
  },

  /**
   * Gets the height in pixels of the top of the font, relative to the baseline, using the bold font.
   */
  top: function top(fontSize) {
    var fontHeader = this.config.fonts.bold.tables.head;
    return fontHeader.yMax * this.fontScale(fontSize);
  }
};
module.exports = exports['default'];