/* Copyright 2012 Mozilla Foundation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/* globals CFFDictDataMap, CFFDictPrivateDataMap, CFFEncodingMap, CFFStrings,
           Components, Dict, dump, FormatError, isNum, netscape, Stream */

'use strict';

/*
 * The Type2 reader code below is only used for debugging purpose since Type2
 * is only a CharString format and is never used directly as a Font file.
 *
 * So the code here is useful for dumping the data content of a .cff file in
 * order to investigate the similarity between a Type1 CharString and a Type2
 * CharString or to understand the structure of the CFF format.
 */

/*
 * Build a charset by assigning the glyph name and the human readable form
 * of the glyph data.
 */
function readCharset(aStream, aCharstrings) {
  var charset = {};

  var format = aStream.getByte();
  var count = aCharstrings.length - 1;
  var i, sid;
  if (format === 0) {
    charset['.notdef'] = readCharstringEncoding(aCharstrings[0]);

    for (i = 1; i < count + 1; i++) {
      sid = aStream.getByte() << 8 | aStream.getByte();
      charset[CFFStrings[sid]] = readCharstringEncoding(aCharstrings[i]);
    }
  } else if (format === 1) {
    for (i = 1; i < count + 1; i++) {
      var first = aStream.getByte();
      first = (first << 8) | aStream.getByte();
      var numLeft = aStream.getByte();
      for (var j = 0; j <= numLeft; j++) {
        sid = first++;
        charset[CFFStrings[sid]] = readCharstringEncoding(aCharstrings[j]);
      }
    }
  } else {
    throw new FormatError('Invalid charset format');
  }

  return charset;
}

/*
 * Take a Type2 binary charstring as input and transform it to a human
 * readable representation as specified by the 'The Type 2 Charstring Format',
 * chapter 3.1.
 */
function readCharstringEncoding(aString) {
  if (!aString) {
    return '';
  }

  var charstringTokens = [];

  var count = aString.length;
  for (var i = 0; i < count; ) {
    var value = aString[i++] | 0;
    var token = null;

    if (value < 0) {
      continue;
    } else if (value <= 11) {
      token = CFFEncodingMap[value];
    } else if (value === 12) {
      token = CFFEncodingMap[value][aString[i++]];
    } else if (value <= 18) {
      token = CFFEncodingMap[value];
    } else if (value <= 20) {
      ++i; // var mask = aString[i++];
      token = CFFEncodingMap[value];
    } else if (value <= 27) {
      token = CFFEncodingMap[value];
    } else if (value === 28) {
      token = aString[i++] << 8 | aString[i++];
    } else if (value <= 31) {
      token = CFFEncodingMap[value];
    } else if (value < 247) {
      token = parseInt(value, 10) - 139;
    } else if (value < 251) {
      token = (value - 247) * 256 + aString[i++] + 108;
    } else if (value < 255) {
      token = -(value - 251) * 256 - aString[i++] - 108;
    } else { // value === 255
      token = aString[i++] << 24 | aString[i++] << 16 |
              aString[i++] << 8 | aString[i];
    }

    charstringTokens.push(token);
  }

  return charstringTokens;
}


/*
 * Take a binary DICT Data as input and transform it into a human readable
 * form as specified by 'The Compact Font Format Specification', chapter 5.
 */
function readFontDictData(aString, aMap) {
  var fontDictDataTokens = [];

  var count = aString.length;
  for (var i = 0; i < count; i) {
    var value = aString[i++] | 0;
    var token = null;

    if (value === 12) {
      token = aMap[value][aString[i++]];
    } else if (value === 28) {
      token = aString[i++] << 8 | aString[i++];
    } else if (value === 29) {
      token = aString[i++] << 24 |
              aString[i++] << 16 |
              aString[i++] << 8 |
              aString[i++];
    } else if (value === 30) {
      token = '';
      var parsed = false;
      while (!parsed) {
        var octet = aString[i++];

        var nibbles = [parseInt(octet / 16, 10), parseInt(octet % 16, 10)];
        for (var j = 0; j < nibbles.length; j++) {
          var nibble = nibbles[j];
          switch (nibble) {
            case 0xA:
              token += '.';
              break;
            case 0xB:
              token += 'E';
              break;
            case 0xC:
              token += 'E-';
              break;
            case 0xD:
              break;
            case 0xE:
              token += '-';
              break;
            case 0xF:
              parsed = true;
              break;
            default:
              token += nibble;
              break;
          }
        }
      }
      token = parseFloat(token);
    } else if (value <= 31) {
      token = aMap[value];
    } else if (value <= 246) {
      token = parseInt(value, 10) - 139;
    } else if (value <= 250) {
      token = (value - 247) * 256 + aString[i++] + 108;
    } else if (value <= 254) {
      token = -(value - 251) * 256 - aString[i++] - 108;
    } else if (value === 255) {
      throw new FormatError('255 is not a valid DICT command');
    }

    fontDictDataTokens.push(token);
  }

  return fontDictDataTokens;
}

/*
 * Take a stream as input and return an array of objects.
 * In CFF an INDEX is a structure with the following format:
 *  {
 *    count: 2 bytes (Number of objects stored in INDEX),
 *    offsize: 1 byte (Offset array element size),
 *    offset: [count + 1] bytes (Offsets array),
 *    data: - (Objects data)
 *  }
 *
 *  More explanation are given in the 'CFF Font Format Specification',
 *  chapter 5.
 */
function readFontIndexData(aStream, aIsByte) {
  var count = aStream.getByte() << 8 | aStream.getByte();
  var offsize = aStream.getByte();

  function getNextOffset() {
    switch (offsize) {
      case 0:
        return 0;
      case 1:
        return aStream.getByte();
      case 2:
        return aStream.getByte() << 8 | aStream.getByte();
      case 3:
        return aStream.getByte() << 16 | aStream.getByte() << 8 |
               aStream.getByte();
      case 4:
      return aStream.getByte() << 24 | aStream.getByte() << 16 |
             aStream.getByte() << 8 | aStream.getByte();
    }
    throw new FormatError(offsize + ' is not a valid offset size');
  }

  var offsets = [];
  var i;
  for (i = 0; i < count + 1; i++) {
    offsets.push(getNextOffset());
  }

  dump('Found ' + count + ' objects at offsets :' +
       offsets + ' (offsize: ' + offsize + ')');

  // Now extract the objects
  var relativeOffset = aStream.pos;
  var objects = [];
  for (i = 0; i < count; i++) {
    var offset = offsets[i];
    aStream.pos = relativeOffset + offset - 1;

    var data = [];
    var length = offsets[i + 1] - 1;
    for (var j = offset - 1; j < length; j++) {
      data.push(aIsByte ? aStream.getByte() : aStream.getChar());
    }
    objects.push(data);
  }

  return objects;
}

var Type2Parser = function type2Parser(aFilePath) {
  var font = new Dict(null);

  var xhr = new XMLHttpRequest();
  xhr.open('GET', aFilePath, false);
  xhr.responseType = 'arraybuffer';
  xhr.expected = document.URL.startsWith('file:') ? 0 : 200;
  xhr.send(null);
  this.data = new Stream(xhr.response);

  // Turn on this flag for additional debugging logs
  var debug = false;

  function dump(aStr) {
    if (debug) {
      console.log(aStr);
    }
  }

  function parseAsToken(aString, aMap) {
    var decoded = readFontDictData(aString, aMap);

    var stack = [];
    var count = decoded.length;
    for (var i = 0; i < count; i++) {
      var token = decoded[i];
      if (isNum(token)) {
        stack.push(token);
      } else {
        switch (token.operand) {
          case 'SID':
            font.set(token.name, CFFStrings[stack.pop()]);
            break;
          case 'number number':
            font.set(token.name, {
              offset: stack.pop(),
              size: stack.pop()
            });
            break;
          case 'boolean':
            font.set(token.name, stack.pop());
            break;
          case 'delta':
            font.set(token.name, stack.pop());
            break;
          default:
            if (token.operand && token.operand.length) {
              var array = [];
              for (var j = 0; j < token.operand.length; j++) {
                array.push(stack.pop());
              }
              font.set(token.name, array);
            } else {
              font.set(token.name, stack.pop());
            }
            break;
        }
      }
    }
  }

  this.parse = function type2ParserParse(aStream) {
    font.set('major', aStream.getByte());
    font.set('minor', aStream.getByte());
    font.set('hdrSize', aStream.getByte());
    font.set('offsize', aStream.getByte());

    // Read the NAME Index
    dump('Reading Index: Names');
    font.set('Names', readFontIndexData(aStream));
    dump('Names: ' + font.get('Names'));

    // Read the Top Dict Index
    dump('Reading Index: TopDict');
    var topDict = readFontIndexData(aStream, true);
    dump('TopDict: ' + topDict);

    // Read the String Index
    dump('Reading Index: Strings');
    var strings = readFontIndexData(aStream);
    dump('strings: ' + strings);

    // Fill up the Strings dictionary with the new unique strings
    var i;
    for (i = 0; i < strings.length; i++) {
      CFFStrings.push(strings[i].join(''));
    }

    // Parse the TopDict operator
    var count = topDict.length;
    for (i = 0; i < count; i++) {
      parseAsToken(topDict[i], CFFDictDataMap);
    }

    // Read the Global Subr Index that comes just after the Strings Index
    // (cf. "The Compact Font Format Specification" Chapter 16)
    dump('Reading Global Subr Index');
    var subrs = readFontIndexData(aStream, true);
    dump(subrs);

    // Reading Private Dict
    var priv = font.get('Private');
    dump('Reading Private Dict (offset: ' + priv.offset +
        ' size: ' + priv.size + ')');
    aStream.pos = priv.offset;

    var privateDict = [];
    for (i = 0; i < priv.size; i++) {
      privateDict.push(aStream.getByte());
    }
    dump('privateData:' + privateDict);
    parseAsToken(privateDict, CFFDictPrivateDataMap);

    font.forEach(function(key, value) {
      dump(key + '::' + value);
    });

    // Read CharStrings Index
    var charStringsOffset = font.get('CharStrings');
    dump('Read CharStrings Index (offset: ' + charStringsOffset + ')');
    aStream.pos = charStringsOffset;
    var charStrings = readFontIndexData(aStream, true);

    // Read Charset
    dump('Read Charset for ' + charStrings.length + ' glyphs');
    var charsetEntry = font.get('charset');
    if (charsetEntry === 0) {
      throw new FormatError('Need to support CFFISOAdobeCharset');
    } else if (charsetEntry === 1) {
      throw new FormatError('Need to support CFFExpert');
    } else if (charsetEntry === 2) {
      throw new FormatError('Need to support CFFExpertSubsetCharset');
    } else {
      aStream.pos = charsetEntry;
      readCharset(aStream, charStrings);
    }
  };
};

/*
 * To try the Type2 decoder on a local file in the current directory:
 *
 *  var cff = new Type2Parser("file.cff");
 *  cff.parse(this.data);
 *
 * To try the Type2 decoder on a custom built CFF array:
 *
 *  var file = new Uint8Array(cffFileArray, 0, cffFileSize);
 *  var parser = new Type2Parser();
 *  parser.parse(new Stream(file));
 */

/*
 * Write to a file to the disk (works only on Firefox in privilege mode)
 * but this is useful for dumping a font file to the disk and check with
 * fontforge or the ots program what's wrong with the file.
 *
 * writeToFile(fontData, "/tmp/pdf.js." + fontCount + ".cff");
 */
function writeToFile(aBytes, aFilePath) {
  if (!('netscape' in window)) {
    return;
  }

  netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
  var Cc = Components.classes,
      Ci = Components.interfaces;
  var file = Cc['@mozilla.org/file/local;1'].createInstance(Ci.nsILocalFile);
  file.initWithPath(aFilePath);

  var stream = Cc['@mozilla.org/network/file-output-stream;1']
                 .createInstance(Ci.nsIFileOutputStream);
  stream.init(file, 0x04 | 0x08 | 0x20, 0x180, 0);

  var bos = Cc['@mozilla.org/binaryoutputstream;1']
              .createInstance(Ci.nsIBinaryOutputStream);
  bos.setOutputStream(stream);
  bos.writeByteArray(aBytes, aBytes.length);
  stream.close();
}
