/**
 * FontGen.js -- a tiny font generator, forming TTF fonts with a
 * single zero-width character implemented, written by Mike "Pomax"
 * Kamermans, with the help of some functions that implement the
 * PHP functions that the original PHP code relied on.
 *
 * LICENSE INFORMATION
 *
 * This code is (c) Mike "Pomax" Kamermans [2012] but is
 * released under the MIT ("expat" flavour) license.
 **/

// we're going to hand-craft the hell out of this font =P
TinyFontGenerator = {

  /**
   * table ordering (ASCII-sorted, rather than alpha)
   */
  ordering: ["OS/2", "cmap", "glyf", "head", "hhea", "hmtx", "loca", "maxp", "name", "post"],

  /**
   * See http://phpjs.org/functions/dechex:382
   */
  dechex: function(number) {
    if (number < 0) { number = 0xFFFFFFFF + number + 1; }
    return parseInt(number, 10).toString(16);
  },

  /**
   * Shorthand hexdec() function
   */
  hexdec: function(hex) {
    return parseInt(hex, 16);
  },

  /**
   * Shorthand chr() function. 
   */
  chr: function(number) { return String.fromCharCode(number); },

  /**
   * See http://phpjs.org/functions/ord:483
   */
  ord: function(string) {
    var str = string + '', code = str.charCodeAt(0);
    if (0xD800 <= code && code <= 0xDBFF) { // High surrogate
      var hi = code;
      if (str.length === 1) { return code; }
      var low = str.charCodeAt(1);
      return ((hi - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000; }
    if (0xDC00 <= code && code <= 0xDFFF) { return code; } // Low surrogate
    return code;
  },

  /**
   * shorthand helper for turning a hex string into a byte
   */
  asByte: function(val) {
    return this.chr(this.hexdec(val));
  },

  /**
   * simplified pack('N',num)
   */
  toULONG: function(num) {
    result = "";
    result += this.chr(num >> 24 & 0xFF);
    result += this.chr(num >> 16 & 0xFF);
    result += this.chr(num >> 8 & 0xFF);
    result += this.chr(num & 0xFF);
    return result;
  },

  /**
   * Turn string data into byte code
   */
  convertData: function(data) {
    var buffer = "";
    data = data.replace(/\s+/g," ").trim().split(" ");
    // convert from string to byte code
    for(var bt=0, e=data.length; bt<e; bt++) {
      buffer += this.asByte(data[bt].trim()); }
    // return the bytecode data
    return buffer;
  },
  
  /**
   * Compute the LONG checksum for a data block
   */
  computeChecksum: function(data) {
    // we don't actually care about checksums =)
    return this.chr(0) + this.chr(0) + this.chr(0) + this.chr(0);
  },

  /**
   * IE9 does not have binary-to-ascii built in O_O
   */
  btoa: function(data) {
    if(window.btoa) {
      return window.btoa(data);
    } else {
      // see http://phpjs.org/functions/base64_encode:358
      var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
      var o1, o2, o3, h1, h2, h3, h4, bits, i = 0, ac = 0, enc = "", tmp_arr = [];
      if (!data) { return data; }
      do { // pack three octets into four hexets
          o1 = data.charCodeAt(i++);
          o2 = data.charCodeAt(i++);
          o3 = data.charCodeAt(i++);
          bits = o1 << 16 | o2 << 8 | o3;
          h1 = bits >> 18 & 0x3f;
          h2 = bits >> 12 & 0x3f;
          h3 = bits >> 6 & 0x3f;
          h4 = bits & 0x3f;
          // use hexets to index into b64, and append result to encoded string
          tmp_arr[ac++] = b64.charAt(h1) + b64.charAt(h2) + b64.charAt(h3) + b64.charAt(h4);
      } while (i < data.length);
      enc = tmp_arr.join('');
      var r = data.length % 3;
      return (r ? enc.slice(0, r - 3) : enc) + '==='.slice(r || 3);
    }
  },

  // ------ TABLE DEFINITIONS ------

  // table data - do not access these values directly, use get[...] instead
  OS2data: false,
  CMAPdata: false, codedCharacter: false,
  GLYFdata: false,
  HEADdata: false,
  HHEAdata: false,
  HMTXdata: false,
  LOCAdata: false,
  MAXPdata: false,
  NAMEdata: false,
  POSTdata: false,

  /**
   * Get the OS/2 table data, or set it up if it doesn't exist yet.
   */ 
  getOS2Data : function() {
    if(this.OS2data!==false) { return this.OS2data; }

    var data = "";
    data += " 00 04";  // The current (Jan 2012) OS/2 table version is 4
    data += " 00 01";  // xAvgCharWidth
    data += " 00 64";  // weight class: 100 ("thin")
    data += " 00 01";  // width class: 1 ("ultra condensed")
    data += " 00 00";  // fsType: Installable Embedding  

    data += " 00 00";  // subscript sizes are irrelevant
    data += " 00 00";  //   "
    data += " 00 00";  //   "
    data += " 00 00";  //   "
    data += " 00 00";  // superscript sizes are irrelevant
    data += " 00 00";  //   "
    data += " 00 00";  //   "
    data += " 00 00";  //   "
    data += " 00 00";  // strikeout values are irrelevant
    data += " 00 00";  //   "
    
    data += " 00 00";  // sfamilyclass - this is 0000 because this font will not be publically catalogued

    data += " 00 00 00 00 00 00 00 00 00 00"; // panose bytes - we set all of these to 0, because this font will not be publically catalogued

    data += " 00 00 00 00";  // ulUnicodeRange 1 (if browsers supported OS/2 v0, we wouldn't need this field)
    data += " 00 00 00 00";  // ulUnicodeRange 2 (idem dito)
    data += " 00 00 00 00";  // ulUnicodeRange 3 (idem)
    data += " 00 00 00 00";  // ulUnicodeRange 4 (idem)

    data += " 00 00 00 00";  // vendor ID - we set this to 0x00000000 because this font will not be publically catalogued
    data += " 00 00";  // font indicator bits - we set this to 0x0000 because this font will not be publically catalogued

    data += " 00 00";  // first character index - for "real" fonts this would correspond to the first CMAP entry
    data += " 00 00";  // last character index  - for "real" fonts this would correspond to the last CMAP entry

    data += " 00 00";  // typographical ascender value
    data += " 00 00";  // typographical descender value
    data += " 00 00";  // typographical linegap value

    data += " 01 00";  // usWinAscent.
    
    /*
      Note: any value for usWinAscent below 0x0100 breaks Opera, I do not know why.
      The OpenType documentation states this:
      
        "For platform 3 encoding 0 fonts, it is the same as yMax"
      
      But in this font, yMax is 0x0000, and the value seems tied to xMax instead.
      However, changing both of these so that they match still leads to Opera
      refusing to load the font if the value is below 0x0100...
    */
    
    data += " 00 00";  // usWinDescent. 
    data += " 00 00 00 00";  // ulCodePageRange 1 (if browsers supported OS/2 v0, we wouldn't need this field)
    data += " 00 00 00 00";  // ulCodePageRange 2 (idem dito)
    this.OS2data = this.convertData(data);
    return this.OS2data;
  },
  
  /**
   * Get the CMAP table data, or set it up if it doesn't exist yet.
   */
  getCMAPData : function(charnum, hexchar) {
    if(this.CMAPdata!==false && this.codedCharacter===hexchar) { return this.CMAPdata; }
    this.codedCharacter = hexchar;

    var data = "";

    data += " 00 00";        // CMAP table format 0
    data += " 00 01";        // we only have one subtable

    // what are we coding for?
    data += " 00 03";        // platform: 3 (Windows)
    data += " 00 01";        // encoding: 1 (Unicode)
    data += " 00 00 00 0C";  // subtable offset: 12 bytes from start of data block

    // We'll use a format 4 subtable, since OTS currently rejects any other subtable format
    // (hopefully this will change in the future because that's a VERY severe restriction)
    data += " 00 04";
    data += " 00 20";        // table length: 32 byte
    data += " 00 00";
    data += " 00 04";        // segCount x 2
    data += " 00 04";        // Note: even though knowing segCount means searchRange can be computed by the engine,
                             //       setting it to a wrong value breaks Chrome and Firefox
    data += " 00 01";        // idem dito for entrySelector
    data += " 00 00";

    // actual character information
    data += " " + hexchar + " FF FF"; // end character codes in each segment range
    data += " 00 00";
    data += " " + hexchar + " FF FF"; // start character codes in each segment range

    // Make sure we get the correct idDelta value. Because our "glyph"
    // is always at index 1 (index 0 is reserved for .notdef) we need
    // to set up the delta value such that <character code> + <delta> == 1
    var corrective = -(charnum - 1) + 65536;
    var idDelta = this.dechex(corrective);
    data += " "+idDelta[0] + idDelta[1] + " " + idDelta[2] + idDelta[3];

    // Of course, there are two values in this segment: the real
    // glyph, and the terminator 0xFFFF. The idDelta for 0xFFFF
    // must be 1.
    data += " 00 01";

    // We do not need to work with idRangeOffset (THANK GOD!), so
    // the idRangeOffset values are 0 for both segment entries.
    data += " 00 00 00 00";

    this.CMAPdata = this.convertData(data);
    return this.CMAPdata;
  },
  
  /**
   * Get the GLYF table data, or set it up if it doesn't exist yet.
   */
  getGLYFData : function() {
    if(this.GLYFdata!==false) { return this.GLYFdata; }
    var data = "";

    // glyph 1: .notdef
    data += " 00 01";         // simple glyph
    data += " 00 00  00 00";  // x/y min
    data += " 00 00  00 00";  // y/x max
    data += " 00 00";         // end point(s): coordinate 0 is the last point in the path
    data += " 00 00";         // instructions: there are no TTF instructions
    /*
      Outline flags for the only point in the glyph:

       1  coordinate represents an on-curve point
       0  x coordinate is encoded as SHORT
       0  y coordinate is encoded as SHORT
       1  x coordinate is 'same as last', which for the first coordinate means it's zero.
       1  y coordinate is also 'same as last'
       0  reserved
       0  reserved
    */
    data += " 31";
    data += " 00"; // padding byte to make sure the table is LONG aligned

    this.GLYFdata = this.convertData(data);
    return this.GLYFdata;
  },
  
  /**
   * Get the HEAD table data, or set it up if it doesn't exist yet.
   */
  getHEADData : function() {
    if(this.HEADdata!==false) { return this.HEADdata; }
    var data  = "";

    data += " 00 01 00 00";  // table version
    data += " 00 00 00 00";  // font revision 0
    data += " 00 00 00 00";  // as this font will never be installed, its checksum is irrelevant
    data += " 5F 0F 3C F5";  // even though the TTF magic number is fixed, we HAVE to include it =(
    data += " 00 00";        // special flags: we do not care about them
    data += " 20 00";        // EM quad size of 8192 - any higher and Opera breaks
    data += " 00 00 00 00 00 00 00 00"; // Number of seconds since 12:00 midnight, January 1, 1904 (64-bit integer)
    data += " 00 00 00 00 00 00 00 00"; // idem dito
    data += " 00 00";        // xmin
    data += " 00 00";        // ymin
    data += " 01 00";        // xMax for this font. For some reason, Opera breaks when this value is lower than 0x0100
    data += " 00 00";        // ymax
    data += " 00 00";        // macstyle - irrelevant, because this font will never be catalogued
    data += " 00 01";        // smallest readable size in pixels: one.
    data += " 00 02";        // (fontDirectionHint is deprecated and should be 2)
    data += " 00 00";        // indices for the LOCA data are encoded as SHORT, rather than LONG (which is 0x0001)
    data += " 00 00";        // use "current" glyph data format

    this.HEADdata = this.convertData(data);
    return this.HEADdata;
  },

  /**
   * Get the HEAD table data, or set it up if it doesn't exist yet.
   */
  getHHEAData : function() {
    if(this.HHEAdata!==false) { return this.HHEAdata; }

    var data = "";
    data += " 00 01 00 00";  // table version
    data += " 00 01";        // typographical ascender
    data += " 00 00";        // typographical descender
    data += " 00 01";        // typographical line gap
    data += " 00 00";        // advanceWidthMax
    data += " 00 00";        // minimum LSB
    data += " 00 00";        // minimum RSB
    data += " 00 00";        // Max(lsb + (xMax - xMin))
    data += " 00 00";        // caretSlopeRise
    data += " 00 00";        // caretSlopeRun
    data += " 00 00";        // caretOffset
    data += " 00 00";        // must be 0
    data += " 00 00";        //  "
    data += " 00 00";        //  "
    data += " 00 00";        //  "
    data += " 00 00";        // use "current" metric data format
    data += " 00 01";        // number of hmetrics

    this.HHEAdata = this.convertData(data);
    return this.HHEAdata;
  },

  /**
   * Get the HMTX table data, or set it up if it doesn't exist yet.
   */
  getHMTXData : function() {
    if(this.HMTXdata!==false) { return this.HMTXdata; }

    var data = "";
    /*
      First, an array of {advanceWidth,	lsb} values, encoded as {USHORT,SHORT}.
      There is only one value here, as per the hmetric value from the HHEA table
    */
    data += " 00 00";
    data += " 00 00";
    /*
      Then, a SHORT[n] for leftSideBearing values, with n being: (maxp.numGlyphs - hhea.hmetrics) = (2-1) = 1
      Since side bearings are meaningless for this minimal font, we set it to 0.
    */
    data += " 00 00";

    this.HMTXdata = this.convertData(data);
    return this.HMTXdata;
  },

  /**
   * Get the LOCA table data, or set it up if it doesn't exist yet.
   */
  getLOCAData : function() {
    if(this.LOCAdata!==false) { return this.LOCAdata; }

    var data = "";
    data += " 00 00";        // location for .notdef: no glyph
    data += " 00 00";        // location for our custom character: also no glyph
    data += " 00 08";        // end of GLYF table: 16 bytes [encoded value '8'], although I don't know why we need this value, as the font engine can get this value from the SFNT 'glyf' tag.

    this.LOCAdata = this.convertData(data);
    return this.LOCAdata;
  },
  
  /**
   * Get the MAXP table data, or set it up if it doesn't exist yet.
   */
  getMAXPData : function() {
    if(this.MAXPdata!==false) { return this.MAXPdata; }

    var data = "";
    data += " 00 01 00 00";  // table version must be 0x00010000 for TTF (CFF uses 0x00005000)
    data += " 00 02";        // two 'glyphs'; .notdef and our custom character
    data += " 00 01";        // contours have at most one point.
    data += " 00 01";        // glyphs have at most one contour.
    data += " 00 00";        // maximum number of points in composite glyphs = zero, because there are no composite glyphs
    data += " 00 00";        // maximum number of contours in composite glyphs = zero, because there are no composite glyphs
    data += " 00 02";        // max_zones should be either 0x0001 ("uses twilight zone") or 0x0002 ("does not use twilight zone"). This is relevant for TTF instructions only
    data += " 00 00";        // maxTwilightPoints is zero
    data += " 00 00";        // Number of Storage Area locations
    data += " 00 00";        // Number of FDEFs
    data += " 00 00";        // Number of IDEFs
    data += " 00 00";        // Maximum stack depth
    data += " 00 00";        // Maximum byte count for glyph instructions
    data += " 00 00";        // Maximum number of components referenced at “top level” for any composite glyph
    data += " 00 00";        // Maximum level of recursion

    this.MAXPdata = this.convertData(data);
    return this.MAXPdata;
  },

  /**
   * Get the MAXP table data, or set it up if it doesn't exist yet.
   */
  getNAMEData : function() {
    if(this.NAMEdata!==false) { return this.NAMEdata; }
    var data = "";
    data += " 00 00";
    data += " 00 02";        // two entries
    data += " 00 1E";        // offset for the string heap
    data += " 00 03  00 01  04 09  00 01  00 00  00 00"; // windows font family name entry
    data += " 00 03  00 01  04 09  00 02  00 02  00 00"; // windows font subfamily name entry
    // string heap
    data += " 00 00";
    this.NAMEdata = this.convertData(data);
    return this.NAMEdata;
  },
  
  /**
   * Get the POST table data, or set it up if it doesn't exist yet.
   */
  getPOSTData : function() {
    if(this.POSTdata!==false) { return this.POSTdata; }

    var data = ""; 
    data += " 00 01 00 00";  // version 1
    data += " 00 00 00 00";  // italicAngle
    data += " 00 00";        // underlinePosition
    data += " 00 00";        // underlineThickness
    data += " 00 00 00 00";  // isFixedPitch
    this.POSTdata = this.convertData(data);
    return this.POSTdata;
  },

  /**
   * Generate the minimal font for our desired character
   */
  generateFont: function(character) {
    var tables = {}, font="";

    // convert the provided character to its hex code
    // equivalent, taking spacing into account.
    var charnum = this.ord(character);
    var hexchar = this.dechex(charnum);
    if(hexchar.length==2) {
      hexchar = "00 "+hexchar;
    } else if(hexchar.length==4) {
      hexchar = hexchar.substring(0,2) + " " + hexchar.substring(2,4);
    }

    tables["OS/2"] = this.getOS2Data();
    tables["cmap"] = this.getCMAPData(charnum, hexchar);
    tables["glyf"] = this.getGLYFData();
    tables["head"] = this.getHEADData();
    tables["hhea"] = this.getHHEAData();
    tables["hmtx"] = this.getHMTXData();
    tables["loca"] = this.getLOCAData();
    tables["maxp"] = this.getMAXPData();
    tables["name"] = this.getNAMEData();
    tables["post"] = this.getPOSTData();

    // Write the spline font (SFNT) header
    font  = this.chr(0) + this.chr(1) + this.chr(0) + this.chr(0);  // TrueType format font = 0x0100 (CFF would be 'OTTO')
    font += this.chr(0) + this.chr(10);  // 10 tables
    font += this.chr(0) + this.chr(128); // search range   (determined by table count)
    font += this.chr(0) + this.chr(3);   // entry selector (determined by table count)
    font += this.chr(0) + this.chr(32);  // range shift    (determined by table count)

    // Write the OpenType table definitions
    var ordering = this.ordering, // local alias
        olen = ordering.length,    // length of table ordering
        offset = 12 + 16*olen,     // offset tracker for table offset values
        tablename,                 // iteration value
        len;                       // iteration value
    for(var i=0; i<olen; i++) {
      tablename = ordering[i];
      // table directory should record the table's actual (not padded) length
      len = tables[tablename].length;
      // the table data itself should be long-aligned
      while(tables[tablename].length % 4 != 0) { tables[tablename] += this.chr(0); }
      font += tablename;
      font += this.computeChecksum(tables[tablename]);  // table checksum
      font += this.toULONG(offset);                     // offset for this table
      font += this.toULONG(len);                        // table length
      offset += tables[tablename].length;
    }

    // Finally, write the actual table data blocks
    for(var i=0; i<olen; i++) { font += tables[ordering[i]]; }

    // return this font as a glorious base64 encoded string
    return this.btoa(font);
  }
}