text: // The Font object
  
  import Path from './path';
  import sfnt from './tables/sfnt';
  import { DefaultEncoding } from './encoding';
  import glyphset from './glyphset';
  import Position from './position';
  import Substitution from './substitution';
  import { isBrowser, checkArgument, arrayBufferToNodeBuffer } from './util';
  import HintingTrueType from './hintingtt';
  
  /**
   * @typedef FontOptions
   * @type Object
   * @property {Boolean} empty - whether to create a new empty font
   * @property {string} familyName
   * @property {string} styleName
   * @property {string=} fullName
   * @property {string=} postScriptName
   * @property {string=} designer
   * @property {string=} designerURL
   * @property {string=} manufacturer
   * @property {string=} manufacturerURL
   * @property {string=} license
   * @property {string=} licenseURL
   * @property {string=} version
   * @property {string=} description
   * @property {string=} copyright
   * @property {string=} trademark
   * @property {Number} unitsPerEm
   * @property {Number} ascender
   * @property {Number} descender
   * @property {Number} createdTimestamp
   * @property {string=} weightClass
   * @property {string=} widthClass
   * @property {string=} fsSelection
   */
  
  /**
   * A Font represents a loaded OpenType font file.
   * It contains a set of glyphs and methods to draw text on a drawing context,
   * or to get a path representing the text.
   * @exports opentype.Font
   * @class
   * @param {FontOptions}
   * @constructor
   */
  function Font(options) {
      options = options || {};
  
      if (!options.empty) {
          // Check that we've provided the minimum set of names.
          checkArgument(options.familyName, 'When creating a new Font object, familyName is required.');
          checkArgument(options.styleName, 'When creating a new Font object, styleName is required.');
          checkArgument(options.unitsPerEm, 'When creating a new Font object, unitsPerEm is required.');
          checkArgument(options.ascender, 'When creating a new Font object, ascender is required.');
          checkArgument(options.descender, 'When creating a new Font object, descender is required.');
          checkArgument(options.descender < 0, 'Descender should be negative (e.g. -512).');
  
          // OS X will complain if the names are empty, so we put a single space everywhere by default.
          this.names = {
              fontFamily: {en: options.familyName || ' '},
              fontSubfamily: {en: options.styleName || ' '},
              fullName: {en: options.fullName || options.familyName + ' ' + options.styleName},
              postScriptName: {en: options.postScriptName || options.familyName + options.styleName},
              designer: {en: options.designer || ' '},
              designerURL: {en: options.designerURL || ' '},
              manufacturer: {en: options.manufacturer || ' '},
              manufacturerURL: {en: options.manufacturerURL || ' '},
              license: {en: options.license || ' '},
              licenseURL: {en: options.licenseURL || ' '},
              version: {en: options.version || 'Version 0.1'},
              description: {en: options.description || ' '},
              copyright: {en: options.copyright || ' '},
              trademark: {en: options.trademark || ' '}
          };
          this.unitsPerEm = options.unitsPerEm || 1000;
          this.ascender = options.ascender;
          this.descender = options.descender;
          this.createdTimestamp = options.createdTimestamp;
          this.tables = { os2: {
              usWeightClass: options.weightClass || this.usWeightClasses.MEDIUM,
              usWidthClass: options.widthClass || this.usWidthClasses.MEDIUM,
              fsSelection: options.fsSelection || this.fsSelectionValues.REGULAR
          } };
      }
  
      this.supported = true; // Deprecated: parseBuffer will throw an error if font is not supported.
      this.glyphs = new glyphset.GlyphSet(this, options.glyphs || []);
      this.encoding = new DefaultEncoding(this);
      this.position = new Position(this);
      this.substitution = new Substitution(this);
      this.tables = this.tables || {};
  
      Object.defineProperty(this, 'hinting', {
          get: function() {
              if (this._hinting) return this._hinting;
              if (this.outlinesFormat === 'truetype') {
                  return (this._hinting = new HintingTrueType(this));
              }
          }
      });
  }
  
  /**
   * Check if the font has a glyph for the given character.
   * @param  {string}
   * @return {Boolean}
   */
  Font.prototype.hasChar = function(c) {
      return this.encoding.charToGlyphIndex(c) !== null;
  };
  
  /**
   * Convert the given character to a single glyph index.
   * Note that this function assumes that there is a one-to-one mapping between
   * the given character and a glyph; for complex scripts this might not be the case.
   * @param  {string}
   * @return {Number}
   */
  Font.prototype.charToGlyphIndex = function(s) {
      return this.encoding.charToGlyphIndex(s);
  };
  
  /**
   * Convert the given character to a single Glyph object.
   * Note that this function assumes that there is a one-to-one mapping between
   * the given character and a glyph; for complex scripts this might not be the case.
   * @param  {string}
   * @return {opentype.Glyph}
   */
  Font.prototype.charToGlyph = function(c) {
      const glyphIndex = this.charToGlyphIndex(c);
      let glyph = this.glyphs.get(glyphIndex);
      if (!glyph) {
          // .notdef
          glyph = this.glyphs.get(0);
      }
  
      return glyph;
  };
  
  /**
   * Convert the given text to a list of Glyph objects.
   * Note that there is no strict one-to-one mapping between characters and
   * glyphs, so the list of returned glyphs can be larger or smaller than the
   * length of the given string.
   * @param  {string}
   * @param  {GlyphRenderOptions} [options]
   * @return {opentype.Glyph[]}
   */
  Font.prototype.stringToGlyphs = function(s, options) {
      options = options || this.defaultRenderOptions;
      // Get glyph indexes
      const indexes = [];
      for (let i = 0; i < s.length; i += 1) {
          const c = s[i];
          indexes.push(this.charToGlyphIndex(c));
      }
      let length = indexes.length;
  
      // Apply substitutions on glyph indexes
      if (options.features) {
          const script = options.script || this.substitution.getDefaultScriptName();
          let manyToOne = [];
          if (options.features.liga) manyToOne = manyToOne.concat(this.substitution.getFeature('liga', script, options.language));
          if (options.features.rlig) manyToOne = manyToOne.concat(this.substitution.getFeature('rlig', script, options.language));
          for (let i = 0; i < length; i += 1) {
              for (let j = 0; j < manyToOne.length; j++) {
                  const ligature = manyToOne[j];
                  const components = ligature.sub;
                  const compCount = components.length;
                  let k = 0;
                  while (k < compCount && components[k] === indexes[i + k]) k++;
                  if (k === compCount) {
                      indexes.splice(i, compCount, ligature.by);
                      length = length - compCount + 1;
                  }
              }
          }
      }
  
      // convert glyph indexes to glyph objects
      const glyphs = new Array(length);
      const notdef = this.glyphs.get(0);
      for (let i = 0; i < length; i += 1) {
          glyphs[i] = this.glyphs.get(indexes[i]) || notdef;
      }
      return glyphs;
  };
  
  /**
   * @param  {string}
   * @return {Number}
   */
  Font.prototype.nameToGlyphIndex = function(name) {
      return this.glyphNames.nameToGlyphIndex(name);
  };
  
  /**
   * @param  {string}
   * @return {opentype.Glyph}
   */
  Font.prototype.nameToGlyph = function(name) {
      const glyphIndex = this.nameToGlyphIndex(name);
      let glyph = this.glyphs.get(glyphIndex);
      if (!glyph) {
          // .notdef
          glyph = this.glyphs.get(0);
      }
  
      return glyph;
  };
  
  /**
   * @param  {Number}
   * @return {String}
   */
  Font.prototype.glyphIndexToName = function(gid) {
      if (!this.glyphNames.glyphIndexToName) {
          return '';
      }
  
      return this.glyphNames.glyphIndexToName(gid);
  };
  
  /**
   * Retrieve the value of the kerning pair between the left glyph (or its index)
   * and the right glyph (or its index). If no kerning pair is found, return 0.
   * The kerning value gets added to the advance width when calculating the spacing
   * between glyphs.
   * @param  {opentype.Glyph} leftGlyph
   * @param  {opentype.Glyph} rightGlyph
   * @return {Number}
   */
  Font.prototype.getKerningValue = function(leftGlyph, rightGlyph) {
      leftGlyph = leftGlyph.index || leftGlyph;
      rightGlyph = rightGlyph.index || rightGlyph;
      return this.kerningPairs[leftGlyph + ',' + rightGlyph] || 0;
  };
  
  /**
   * @typedef GlyphRenderOptions
   * @type Object
   * @property {string} [script] - script used to determine which features to apply. By default, 'DFLT' or 'latn' is used.
   *                               See https://www.microsoft.com/typography/otspec/scripttags.htm
   * @property {string} [language='dflt'] - language system used to determine which features to apply.
   *                                        See https://www.microsoft.com/typography/developers/opentype/languagetags.aspx
   * @property {boolean} [kerning=true] - whether to include kerning values
   * @property {object} [features] - OpenType Layout feature tags. Used to enable or disable the features of the given script/language system.
   *                                 See https://www.microsoft.com/typography/otspec/featuretags.htm
   */
  Font.prototype.defaultRenderOptions = {
      kerning: true,
      features: {
          liga: true,
          rlig: true
      }
  };
  
  /**
   * Helper function that invokes the given callback for each glyph in the given text.
   * The callback gets `(glyph, x, y, fontSize, options)`.* @param  {string} text
   * @param {string} text - The text to apply.
   * @param  {number} [x=0] - Horizontal position of the beginning of the text.
   * @param  {number} [y=0] - Vertical position of the *baseline* of the text.
   * @param  {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`.
   * @param  {GlyphRenderOptions=} options
   * @param  {Function} callback
   */
  Font.prototype.forEachGlyph = function(text, x, y, fontSize, options, callback) {
      x = x !== undefined ? x : 0;
      y = y !== undefined ? y : 0;
      fontSize = fontSize !== undefined ? fontSize : 72;
      options = options || this.defaultRenderOptions;
      const fontScale = 1 / this.unitsPerEm * fontSize;
      const glyphs = this.stringToGlyphs(text, options);
      let kerningLookups;
      if (options.kerning) {
          const script = options.script || this.position.getDefaultScriptName();
          kerningLookups = this.position.getKerningTables(script, options.language);
      }
      for (let i = 0; i < glyphs.length; i += 1) {
          const glyph = glyphs[i];
          callback.call(this, glyph, x, y, fontSize, options);
          if (glyph.advanceWidth) {
              x += glyph.advanceWidth * fontScale;
          }
  
          if (options.kerning && i < glyphs.length - 1) {
              // We should apply position adjustment lookups in a more generic way.
              // Here we only use the xAdvance value.
              const kerningValue = kerningLookups ?
                    this.position.getKerningValue(kerningLookups, glyph.index, glyphs[i + 1].index) :
                    this.getKerningValue(glyph, glyphs[i + 1]);
              x += kerningValue * fontScale;
          }
  
          if (options.letterSpacing) {
              x += options.letterSpacing * fontSize;
          } else if (options.tracking) {
              x += (options.tracking / 1000) * fontSize;
          }
      }
      return x;
  };
  
  /**
   * Create a Path object that represents the given text.
   * @param  {string} text - The text to create.
   * @param  {number} [x=0] - Horizontal position of the beginning of the text.
   * @param  {number} [y=0] - Vertical position of the *baseline* of the text.
   * @param  {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`.
   * @param  {GlyphRenderOptions=} options
   * @return {opentype.Path}
   */
  Font.prototype.getPath = function(text, x, y, fontSize, options) {
      const fullPath = new Path();
      this.forEachGlyph(text, x, y, fontSize, options, function(glyph, gX, gY, gFontSize) {
          const glyphPath = glyph.getPath(gX, gY, gFontSize, options, this);
          fullPath.extend(glyphPath);
      });
      return fullPath;
  };
  
  /**
   * Create an array of Path objects that represent the glyphs of a given text.
   * @param  {string} text - The text to create.
   * @param  {number} [x=0] - Horizontal position of the beginning of the text.
   * @param  {number} [y=0] - Vertical position of the *baseline* of the text.
   * @param  {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`.
   * @param  {GlyphRenderOptions=} options
   * @return {opentype.Path[]}
   */
  Font.prototype.getPaths = function(text, x, y, fontSize, options) {
      const glyphPaths = [];
      this.forEachGlyph(text, x, y, fontSize, options, function(glyph, gX, gY, gFontSize) {
          const glyphPath = glyph.getPath(gX, gY, gFontSize, options, this);
          glyphPaths.push(glyphPath);
      });
  
      return glyphPaths;
  };
  
  /**
   * Returns the advance width of a text.
   *
   * This is something different than Path.getBoundingBox() as for example a
   * suffixed whitespace increases the advanceWidth but not the bounding box
   * or an overhanging letter like a calligraphic 'f' might have a quite larger
   * bounding box than its advance width.
   *
   * This corresponds to canvas2dContext.measureText(text).width
   *
   * @param  {string} text - The text to create.
   * @param  {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`.
   * @param  {GlyphRenderOptions=} options
   * @return advance width
   */
  Font.prototype.getAdvanceWidth = function(text, fontSize, options) {
      return this.forEachGlyph(text, 0, 0, fontSize, options, function() {});
  };
  
  /**
   * Draw the text on the given drawing context.
   * @param  {CanvasRenderingContext2D} ctx - A 2D drawing context, like Canvas.
   * @param  {string} text - The text to create.
   * @param  {number} [x=0] - Horizontal position of the beginning of the text.
   * @param  {number} [y=0] - Vertical position of the *baseline* of the text.
   * @param  {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`.
   * @param  {GlyphRenderOptions=} options
   */
  Font.prototype.draw = function(ctx, text, x, y, fontSize, options) {
      this.getPath(text, x, y, fontSize, options).draw(ctx);
  };
  
  /**
   * Draw the points of all glyphs in the text.
   * On-curve points will be drawn in blue, off-curve points will be drawn in red.
   * @param {CanvasRenderingContext2D} ctx - A 2D drawing context, like Canvas.
   * @param {string} text - The text to create.
   * @param {number} [x=0] - Horizontal position of the beginning of the text.
   * @param {number} [y=0] - Vertical position of the *baseline* of the text.
   * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`.
   * @param {GlyphRenderOptions=} options
   */
  Font.prototype.drawPoints = function(ctx, text, x, y, fontSize, options) {
      this.forEachGlyph(text, x, y, fontSize, options, function(glyph, gX, gY, gFontSize) {
          glyph.drawPoints(ctx, gX, gY, gFontSize);
      });
  };
  
  /**
   * Draw lines indicating important font measurements for all glyphs in the text.
   * Black lines indicate the origin of the coordinate system (point 0,0).
   * Blue lines indicate the glyph bounding box.
   * Green line indicates the advance width of the glyph.
   * @param {CanvasRenderingContext2D} ctx - A 2D drawing context, like Canvas.
   * @param {string} text - The text to create.
   * @param {number} [x=0] - Horizontal position of the beginning of the text.
   * @param {number} [y=0] - Vertical position of the *baseline* of the text.
   * @param {number} [fontSize=72] - Font size in pixels. We scale the glyph units by `1 / unitsPerEm * fontSize`.
   * @param {GlyphRenderOptions=} options
   */
  Font.prototype.drawMetrics = function(ctx, text, x, y, fontSize, options) {
      this.forEachGlyph(text, x, y, fontSize, options, function(glyph, gX, gY, gFontSize) {
          glyph.drawMetrics(ctx, gX, gY, gFontSize);
      });
  };
  
  /**
   * @param  {string}
   * @return {string}
   */
  Font.prototype.getEnglishName = function(name) {
      const translations = this.names[name];
      if (translations) {
          return translations.en;
      }
  };
  
  /**
   * Validate
   */
  Font.prototype.validate = function() {
      const warnings = [];
      const _this = this;
  
      function assert(predicate, message) {
          if (!predicate) {
              warnings.push(message);
          }
      }
  
      function assertNamePresent(name) {
          const englishName = _this.getEnglishName(name);
          assert(englishName && englishName.trim().length > 0,
                 'No English ' + name + ' specified.');
      }
  
      // Identification information
      assertNamePresent('fontFamily');
      assertNamePresent('weightName');
      assertNamePresent('manufacturer');
      assertNamePresent('copyright');
      assertNamePresent('version');
  
      // Dimension information
      assert(this.unitsPerEm > 0, 'No unitsPerEm specified.');
  };
  
  /**
   * Convert the font object to a SFNT data structure.
   * This structure contains all the necessary tables and metadata to create a binary OTF file.
   * @return {opentype.Table}
   */
  Font.prototype.toTables = function() {
      return sfnt.fontToTable(this);
  };
  /**
   * @deprecated Font.toBuffer is deprecated. Use Font.toArrayBuffer instead.
   */
  Font.prototype.toBuffer = function() {
      console.warn('Font.toBuffer is deprecated. Use Font.toArrayBuffer instead.');
      return this.toArrayBuffer();
  };
  /**
   * Converts a `opentype.Font` into an `ArrayBuffer`
   * @return {ArrayBuffer}
   */
  Font.prototype.toArrayBuffer = function() {
      const sfntTable = this.toTables();
      const bytes = sfntTable.encode();
      const buffer = new ArrayBuffer(bytes.length);
      const intArray = new Uint8Array(buffer);
      for (let i = 0; i < bytes.length; i++) {
          intArray[i] = bytes[i];
      }
  
      return buffer;
  };
  
  /**
   * Initiate a download of the OpenType font.
   */
  Font.prototype.download = function(fileName) {
      const familyName = this.getEnglishName('fontFamily');
      const styleName = this.getEnglishName('fontSubfamily');
      fileName = fileName || familyName.replace(/\s/g, '') + '-' + styleName + '.otf';
      const arrayBuffer = this.toArrayBuffer();
  
      if (isBrowser()) {
          window.requestFileSystem = window.requestFileSystem || window.webkitRequestFileSystem;
          window.requestFileSystem(window.TEMPORARY, arrayBuffer.byteLength, function(fs) {
              fs.root.getFile(fileName, {create: true}, function(fileEntry) {
                  fileEntry.createWriter(function(writer) {
                      const dataView = new DataView(arrayBuffer);
                      const blob = new Blob([dataView], {type: 'font/opentype'});
                      writer.write(blob);
  
                      writer.addEventListener('writeend', function() {
                          // Navigating to the file will download it.
                          location.href = fileEntry.toURL();
                      }, false);
                  });
              });
          },
          function(err) {
              throw new Error(err.name + ': ' + err.message);
          });
      } else {
          const fs = require('fs');
          const buffer = arrayBufferToNodeBuffer(arrayBuffer);
          fs.writeFileSync(fileName, buffer);
      }
  };
  /**
   * @private
   */
  Font.prototype.fsSelectionValues = {
      ITALIC:              0x001, //1
      UNDERSCORE:          0x002, //2
      NEGATIVE:            0x004, //4
      OUTLINED:            0x008, //8
      STRIKEOUT:           0x010, //16
      BOLD:                0x020, //32
      REGULAR:             0x040, //64
      USER_TYPO_METRICS:   0x080, //128
      WWS:                 0x100, //256
      OBLIQUE:             0x200  //512
  };
  
  /**
   * @private
   */
  Font.prototype.usWidthClasses = {
      ULTRA_CONDENSED: 1,
      EXTRA_CONDENSED: 2,
      CONDENSED: 3,
      SEMI_CONDENSED: 4,
      MEDIUM: 5,
      SEMI_EXPANDED: 6,
      EXPANDED: 7,
      EXTRA_EXPANDED: 8,
      ULTRA_EXPANDED: 9
  };
  
  /**
   * @private
   */
  Font.prototype.usWeightClasses = {
      THIN: 100,
      EXTRA_LIGHT: 200,
      LIGHT: 300,
      NORMAL: 400,
      MEDIUM: 500,
      SEMI_BOLD: 600,
      BOLD: 700,
      EXTRA_BOLD: 800,
      BLACK:    900
  };
  
  export default Font;
  