import r from 'restructure';
import { cache } from './decorators';
import fontkit from './base';
import Directory from './tables/directory';
import tables from './tables';
import CmapProcessor from './CmapProcessor';
import LayoutEngine from './layout/LayoutEngine';
import TTFGlyph from './glyph/TTFGlyph';
import CFFGlyph from './glyph/CFFGlyph';
import SBIXGlyph from './glyph/SBIXGlyph';
import COLRGlyph from './glyph/COLRGlyph';
import GlyphVariationProcessor from './glyph/GlyphVariationProcessor';
import TTFSubset from './subset/TTFSubset';
import CFFSubset from './subset/CFFSubset';
import BBox from './glyph/BBox';

/**
 * This is the base class for all SFNT-based font formats in fontkit.
 * It supports TrueType, and PostScript glyphs, and several color glyph formats.
 */
export default class TTFFont {
  static probe(buffer) {
    let format = buffer.toString('ascii', 0, 4);
    return format === 'true' || format === 'OTTO' || format === String.fromCharCode(0, 1, 0, 0);
  }

  constructor(stream, variationCoords = null) {
    this.stream = stream;
    this.variationCoords = variationCoords;

    this._directoryPos = this.stream.pos;
    this._tables = {};
    this._glyphs = {};
    this._decodeDirectory();

    // define properties for each table to lazily parse
    for (let tag in this.directory.tables) {
      let table = this.directory.tables[tag];
      if (tables[tag] && table.length > 0) {
        Object.defineProperty(this, tag, {
          get: this._getTable.bind(this, table)
        });
      }
    }
  }

  _getTable(table) {
    if (!(table.tag in this._tables)) {
      try {
        this._tables[table.tag] = this._decodeTable(table);
      } catch (e) {
        if (fontkit.logErrors) {
          console.error(`Error decoding table ${table.tag}`);
          console.error(e.stack);
        }
      }
    }

    return this._tables[table.tag];
  }

  _getTableStream(tag) {
    let table = this.directory.tables[tag];
    if (table) {
      this.stream.pos = table.offset;
      return this.stream;
    }

    return null;
  }

  _decodeDirectory() {
    return this.directory = Directory.decode(this.stream, {_startOffset: 0});
  }

  _decodeTable(table) {
    let pos = this.stream.pos;

    let stream = this._getTableStream(table.tag);
    let result = tables[table.tag].decode(stream, this, table.length);

    this.stream.pos = pos;
    return result;
  }

  /**
   * The unique PostScript name for this font
   * @type {string}
   */
  get postscriptName() {
    let name = this.name.records.postscriptName;
    if (name) {
      let lang = Object.keys(name)[0];
      return name[lang];
    }

    return null;
  }

  /**
   * Gets a string from the font's `name` table
   * `lang` is a BCP-47 language code.
   * @return {string}
   */
  getName(key, lang = 'en') {
    let record = this.name.records[key];
    if (record) {
      return record[lang];
    }

    return null;
  }

  /**
   * The font's full name, e.g. "Helvetica Bold"
   * @type {string}
   */
  get fullName() {
    return this.getName('fullName');
  }

  /**
   * The font's family name, e.g. "Helvetica"
   * @type {string}
   */
  get familyName() {
    return this.getName('fontFamily');
  }

  /**
   * The font's sub-family, e.g. "Bold".
   * @type {string}
   */
  get subfamilyName() {
    return this.getName('fontSubfamily');
  }

  /**
   * The font's copyright information
   * @type {string}
   */
  get copyright() {
    return this.getName('copyright');
  }

  /**
   * The font's version number
   * @type {string}
   */
  get version() {
    return this.getName('version');
  }

  /**
   * The font’s [ascender](https://en.wikipedia.org/wiki/Ascender_(typography))
   * @type {number}
   */
  get ascent() {
    return this.hhea.ascent;
  }

  /**
   * The font’s [descender](https://en.wikipedia.org/wiki/Descender)
   * @type {number}
   */
  get descent() {
    return this.hhea.descent;
  }

  /**
   * The amount of space that should be included between lines
   * @type {number}
   */
  get lineGap() {
    return this.hhea.lineGap;
  }

  /**
   * The offset from the normal underline position that should be used
   * @type {number}
   */
  get underlinePosition() {
    return this.post.underlinePosition;
  }

  /**
   * The weight of the underline that should be used
   * @type {number}
   */
  get underlineThickness() {
    return this.post.underlineThickness;
  }

  /**
   * If this is an italic font, the angle the cursor should be drawn at to match the font design
   * @type {number}
   */
  get italicAngle() {
    return this.post.italicAngle;
  }

  /**
   * The height of capital letters above the baseline.
   * See [here](https://en.wikipedia.org/wiki/Cap_height) for more details.
   * @type {number}
   */
  get capHeight() {
    let os2 = this['OS/2'];
    return os2 ? os2.capHeight : this.ascent;
  }

  /**
   * The height of lower case letters in the font.
   * See [here](https://en.wikipedia.org/wiki/X-height) for more details.
   * @type {number}
   */
  get xHeight() {
    let os2 = this['OS/2'];
    return os2 ? os2.xHeight : 0;
  }

  /**
   * The number of glyphs in the font.
   * @type {number}
   */
  get numGlyphs() {
    return this.maxp.numGlyphs;
  }

  /**
   * The size of the font’s internal coordinate grid
   * @type {number}
   */
  get unitsPerEm() {
    return this.head.unitsPerEm;
  }

  /**
   * The font’s bounding box, i.e. the box that encloses all glyphs in the font.
   * @type {BBox}
   */
  @cache
  get bbox() {
    return Object.freeze(new BBox(this.head.xMin, this.head.yMin, this.head.xMax, this.head.yMax));
  }

  @cache
  get _cmapProcessor() {
    return new CmapProcessor(this.cmap);
  }

  /**
   * An array of all of the unicode code points supported by the font.
   * @type {number[]}
   */
  @cache
  get characterSet() {
    return this._cmapProcessor.getCharacterSet();
  }

  /**
   * Returns whether there is glyph in the font for the given unicode code point.
   *
   * @param {number} codePoint
   * @return {boolean}
   */
  hasGlyphForCodePoint(codePoint) {
    return !!this._cmapProcessor.lookup(codePoint);
  }

  /**
   * Maps a single unicode code point to a Glyph object.
   * Does not perform any advanced substitutions (there is no context to do so).
   *
   * @param {number} codePoint
   * @return {Glyph}
   */
  glyphForCodePoint(codePoint) {
    return this.getGlyph(this._cmapProcessor.lookup(codePoint), [codePoint]);
  }

  /**
   * Returns an array of Glyph objects for the given string.
   * This is only a one-to-one mapping from characters to glyphs.
   * For most uses, you should use font.layout (described below), which
   * provides a much more advanced mapping supporting AAT and OpenType shaping.
   *
   * @param {string} string
   * @return {Glyph[]}
   */
  glyphsForString(string) {
    let glyphs = [];
    let len = string.length;
    let idx = 0;
    let last = -1;
    let state = -1;

    while (idx <= len) {
      let code = 0;
      let nextState = 0;

      if (idx < len) {
        // Decode the next codepoint from UTF 16
        code = string.charCodeAt(idx++);
        if (0xd800 <= code && code <= 0xdbff && idx < len) {
          let next = string.charCodeAt(idx);
          if (0xdc00 <= next && next <= 0xdfff) {
            idx++;
            code = ((code & 0x3ff) << 10) + (next & 0x3ff) + 0x10000;
          }
        }

        // Compute the next state: 1 if the next codepoint is a variation selector, 0 otherwise.
        nextState = ((0xfe00 <= code && code <= 0xfe0f) || (0xe0100 <= code && code <= 0xe01ef)) ? 1 : 0;
      } else {
        idx++;
      }

      if (state === 0 && nextState === 1) {
        // Variation selector following normal codepoint.
        glyphs.push(this.getGlyph(this._cmapProcessor.lookup(last, code), [last, code]));
      } else if (state === 0 && nextState === 0) {
        // Normal codepoint following normal codepoint.
        glyphs.push(this.glyphForCodePoint(last));
      }

      last = code;
      state = nextState;
    }

    return glyphs;
  }

  @cache
  get _layoutEngine() {
    return new LayoutEngine(this);
  }

  /**
   * Returns a GlyphRun object, which includes an array of Glyphs and GlyphPositions for the given string.
   *
   * @param {string} string
   * @param {string[]} [userFeatures]
   * @param {string} [script]
   * @param {string} [language]
   * @param {string} [direction]
   * @return {GlyphRun}
   */
  layout(string, userFeatures, script, language, direction) {
    return this._layoutEngine.layout(string, userFeatures, script, language, direction);
  }

  /**
   * Returns an array of strings that map to the given glyph id.
   * @param {number} gid - glyph id
   */
  stringsForGlyph(gid) {
    return this._layoutEngine.stringsForGlyph(gid);
  }

  /**
   * An array of all [OpenType feature tags](https://www.microsoft.com/typography/otspec/featuretags.htm)
   * (or mapped AAT tags) supported by the font.
   * The features parameter is an array of OpenType feature tags to be applied in addition to the default set.
   * If this is an AAT font, the OpenType feature tags are mapped to AAT features.
   *
   * @type {string[]}
   */
  get availableFeatures() {
    return this._layoutEngine.getAvailableFeatures();
  }

  getAvailableFeatures(script, language) {
    return this._layoutEngine.getAvailableFeatures(script, language);
  }

  _getBaseGlyph(glyph, characters = []) {
    if (!this._glyphs[glyph]) {
      if (this.directory.tables.glyf) {
        this._glyphs[glyph] = new TTFGlyph(glyph, characters, this);

      } else if (this.directory.tables['CFF '] || this.directory.tables.CFF2) {
        this._glyphs[glyph] = new CFFGlyph(glyph, characters, this);
      }
    }

    return this._glyphs[glyph] || null;
  }

  /**
   * Returns a glyph object for the given glyph id.
   * You can pass the array of code points this glyph represents for
   * your use later, and it will be stored in the glyph object.
   *
   * @param {number} glyph
   * @param {number[]} characters
   * @return {Glyph}
   */
  getGlyph(glyph, characters = []) {
    if (!this._glyphs[glyph]) {
      if (this.directory.tables.sbix) {
        this._glyphs[glyph] = new SBIXGlyph(glyph, characters, this);

      } else if ((this.directory.tables.COLR) && (this.directory.tables.CPAL)) {
        this._glyphs[glyph] = new COLRGlyph(glyph, characters, this);

      } else {
        this._getBaseGlyph(glyph, characters);
      }
    }

    return this._glyphs[glyph] || null;
  }

  /**
   * Returns a Subset for this font.
   * @return {Subset}
   */
  createSubset() {
    if (this.directory.tables['CFF ']) {
      return new CFFSubset(this);
    }

    return new TTFSubset(this);
  }

  /**
   * Returns an object describing the available variation axes
   * that this font supports. Keys are setting tags, and values
   * contain the axis name, range, and default value.
   *
   * @type {object}
   */
  @cache
  get variationAxes() {
    let res = {};
    if (!this.fvar) {
      return res;
    }

    for (let axis of this.fvar.axis) {
      res[axis.axisTag.trim()] = {
        name: axis.name.en,
        min: axis.minValue,
        default: axis.defaultValue,
        max: axis.maxValue
      };
    }

    return res;
  }

  /**
   * Returns an object describing the named variation instances
   * that the font designer has specified. Keys are variation names
   * and values are the variation settings for this instance.
   *
   * @type {object}
   */
  @cache
  get namedVariations() {
    let res = {};
    if (!this.fvar) {
      return res;
    }

    for (let instance of this.fvar.instance) {
      let settings = {};
      for (let i = 0; i < this.fvar.axis.length; i++) {
        let axis = this.fvar.axis[i];
        settings[axis.axisTag.trim()] = instance.coord[i];
      }

      res[instance.name.en] = settings;
    }

    return res;
  }

  /**
   * Returns a new font with the given variation settings applied.
   * Settings can either be an instance name, or an object containing
   * variation tags as specified by the `variationAxes` property.
   *
   * @param {object} settings
   * @return {TTFFont}
   */
  getVariation(settings) {
    if (!(this.directory.tables.fvar && ((this.directory.tables.gvar && this.directory.tables.glyf) || this.directory.tables.CFF2))) {
      throw new Error('Variations require a font with the fvar, gvar and glyf, or CFF2 tables.');
    }

    if (typeof settings === 'string') {
      settings = this.namedVariations[settings];
    }

    if (typeof settings !== 'object') {
      throw new Error('Variation settings must be either a variation name or settings object.');
    }

    // normalize the coordinates
    let coords = this.fvar.axis.map((axis, i) => {
      let axisTag = axis.axisTag.trim();
      if (axisTag in settings) {
        return Math.max(axis.minValue, Math.min(axis.maxValue, settings[axisTag]));
      } else {
        return axis.defaultValue;
      }
    });

    let stream = new r.DecodeStream(this.stream.buffer);
    stream.pos = this._directoryPos;

    let font = new TTFFont(stream, coords);
    font._tables = this._tables;

    return font;
  }

  @cache
  get _variationProcessor() {
    if (!this.fvar) {
      return null;
    }

    let variationCoords = this.variationCoords;

    // Ignore if no variation coords and not CFF2
    if (!variationCoords && !this.CFF2) {
      return null;
    }

    if (!variationCoords) {
      variationCoords = this.fvar.axis.map(axis => axis.defaultValue);
    }

    return new GlyphVariationProcessor(this, variationCoords);
  }

  // Standardized format plugin API
  getFont(name) {
    return this.getVariation(name);
  }
}
