/* 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.
 */

'use strict';

(function(root, factory) {
    if (typeof define === 'function' && define.amd) {
        define('pdfjs/core/type1_parser', ['exports', 'pdfjs/shared/util',
                'pdfjs/core/stream', 'pdfjs/core/parser', 'pdfjs/core/encodings'],
            factory);
    } else if (typeof exports !== 'undefined') {
        factory(exports, require('../shared/util.js'), require('./stream.js'),
            require('./parser.js'), require('./encodings.js'));
    } else {
        factory((root.pdfjsCoreType1Parser = {}), root.pdfjsSharedUtil,
            root.pdfjsCoreStream, root.pdfjsCoreParser, root.pdfjsCoreEncodings);
    }
}(this, function(exports, sharedUtil, coreStream, coreParser, coreEncodings) {

    var warn = sharedUtil.warn;
    var Stream = coreStream.Stream;
    var Lexer = coreParser.Lexer;
    var getEncoding = coreEncodings.getEncoding;

// Hinting is currently disabled due to unknown problems on windows
// in tracemonkey and various other pdfs with type1 fonts.
    var HINTING_ENABLED = false;

    /*
     * CharStrings are encoded following the the CharString Encoding sequence
     * describe in Chapter 6 of the "Adobe Type1 Font Format" specification.
     * The value in a byte indicates a command, a number, or subsequent bytes
     * that are to be interpreted in a special way.
     *
     * CharString Number Encoding:
     *  A CharString byte containing the values from 32 through 255 inclusive
     *  indicate an integer. These values are decoded in four ranges.
     *
     * 1. A CharString byte containing a value, v, between 32 and 246 inclusive,
     * indicate the integer v - 139. Thus, the integer values from -107 through
     * 107 inclusive may be encoded in single byte.
     *
     * 2. A CharString byte containing a value, v, between 247 and 250 inclusive,
     * indicates an integer involving the next byte, w, according to the formula:
     * [(v - 247) x 256] + w + 108
     *
     * 3. A CharString byte containing a value, v, between 251 and 254 inclusive,
     * indicates an integer involving the next byte, w, according to the formula:
     * -[(v - 251) * 256] - w - 108
     *
     * 4. A CharString containing the value 255 indicates that the next 4 bytes
     * are a two complement signed integer. The first of these bytes contains the
     * highest order bits, the second byte contains the next higher order bits
     * and the fourth byte contain the lowest order bits.
     *
     *
     * CharString Command Encoding:
     *  CharStrings commands are encoded in 1 or 2 bytes.
     *
     *  Single byte commands are encoded in 1 byte that contains a value between
     *  0 and 31 inclusive.
     *  If a command byte contains the value 12, then the value in the next byte
     *  indicates a command. This "escape" mechanism allows many extra commands
     * to be encoded and this encoding technique helps to minimize the length of
     * the charStrings.
     */
    var Type1CharString = (function Type1CharStringClosure() {
        var COMMAND_MAP = {
            'hstem'     : [1],
            'vstem'     : [3],
            'vmoveto'   : [4],
            'rlineto'   : [5],
            'hlineto'   : [6],
            'vlineto'   : [7],
            'rrcurveto' : [8],
            'callsubr'  : [10],
            'flex'      : [12, 35],
            'drop'      : [12, 18],
            'endchar'   : [14],
            'rmoveto'   : [21],
            'hmoveto'   : [22],
            'vhcurveto' : [30],
            'hvcurveto' : [31]
        };

        function Type1CharString() {
            this.width = 0;
            this.lsb = 0;
            this.flexing = false;
            this.output = [];
            this.stack = [];
        }

        Type1CharString.prototype = {
            convert : function Type1CharString_convert(encoded, subrs,
                                                       seacAnalysisEnabled) {
                var count = encoded.length;
                var error = false;
                var wx, sbx, subrNumber;
                for (var i = 0; i < count; i++) {
                    var value = encoded[i];
                    if (value < 32) {
                        if (value === 12) {
                            value = (value << 8) + encoded[++i];
                        }
                        switch (value) {
                            case 1: // hstem
                                if (!HINTING_ENABLED) {
                                    this.stack = [];
                                    break;
                                }
                                error = this.executeCommand(2, COMMAND_MAP.hstem);
                                break;
                            case 3: // vstem
                                if (!HINTING_ENABLED) {
                                    this.stack = [];
                                    break;
                                }
                                error = this.executeCommand(2, COMMAND_MAP.vstem);
                                break;
                            case 4: // vmoveto
                                if (this.flexing) {
                                    if (this.stack.length < 1) {
                                        error = true;
                                        break;
                                    }
                                    // Add the dx for flex and but also swap the values so they are
                                    // the right order.
                                    var dy = this.stack.pop();
                                    this.stack.push(0, dy);
                                    break;
                                }
                                error = this.executeCommand(1, COMMAND_MAP.vmoveto);
                                break;
                            case 5: // rlineto
                                error = this.executeCommand(2, COMMAND_MAP.rlineto);
                                break;
                            case 6: // hlineto
                                error = this.executeCommand(1, COMMAND_MAP.hlineto);
                                break;
                            case 7: // vlineto
                                error = this.executeCommand(1, COMMAND_MAP.vlineto);
                                break;
                            case 8: // rrcurveto
                                error = this.executeCommand(6, COMMAND_MAP.rrcurveto);
                                break;
                            case 9: // closepath
                                // closepath is a Type1 command that does not take argument and is
                                // useless in Type2 and it can simply be ignored.
                                this.stack = [];
                                break;
                            case 10: // callsubr
                                if (this.stack.length < 1) {
                                    error = true;
                                    break;
                                }
                                subrNumber = this.stack.pop();
                                error = this.convert(subrs[subrNumber], subrs,
                                    seacAnalysisEnabled);
                                break;
                            case 11: // return
                                return error;
                            case 13: // hsbw
                                if (this.stack.length < 2) {
                                    error = true;
                                    break;
                                }
                                // To convert to type2 we have to move the width value to the
                                // first part of the charstring and then use hmoveto with lsb.
                                wx = this.stack.pop();
                                sbx = this.stack.pop();
                                this.lsb = sbx;
                                this.width = wx;
                                this.stack.push(wx, sbx);
                                error = this.executeCommand(2, COMMAND_MAP.hmoveto);
                                break;
                            case 14: // endchar
                                this.output.push(COMMAND_MAP.endchar[0]);
                                break;
                            case 21: // rmoveto
                                if (this.flexing) {
                                    break;
                                }
                                error = this.executeCommand(2, COMMAND_MAP.rmoveto);
                                break;
                            case 22: // hmoveto
                                if (this.flexing) {
                                    // Add the dy for flex.
                                    this.stack.push(0);
                                    break;
                                }
                                error = this.executeCommand(1, COMMAND_MAP.hmoveto);
                                break;
                            case 30: // vhcurveto
                                error = this.executeCommand(4, COMMAND_MAP.vhcurveto);
                                break;
                            case 31: // hvcurveto
                                error = this.executeCommand(4, COMMAND_MAP.hvcurveto);
                                break;
                            case (12 << 8) + 0: // dotsection
                                // dotsection is a Type1 command to specify some hinting feature
                                // for dots that do not take a parameter and it can safely be
                                // ignored for Type2.
                                this.stack = [];
                                break;
                            case (12 << 8) + 1: // vstem3
                                if (!HINTING_ENABLED) {
                                    this.stack = [];
                                    break;
                                }
                                // [vh]stem3 are Type1 only and Type2 supports [vh]stem with
                                // multiple parameters, so instead of returning [vh]stem3 take a
                                // shortcut and return [vhstem] instead.
                                error = this.executeCommand(2, COMMAND_MAP.vstem);
                                break;
                            case (12 << 8) + 2: // hstem3
                                if (!HINTING_ENABLED) {
                                    this.stack = [];
                                    break;
                                }
                                // See vstem3.
                                error = this.executeCommand(2, COMMAND_MAP.hstem);
                                break;
                            case (12 << 8) + 6: // seac
                                // seac is like type 2's special endchar but it doesn't use the
                                // first argument asb, so remove it.
                                if (seacAnalysisEnabled) {
                                    this.seac = this.stack.splice(-4, 4);
                                    error = this.executeCommand(0, COMMAND_MAP.endchar);
                                } else {
                                    error = this.executeCommand(4, COMMAND_MAP.endchar);
                                }
                                break;
                            case (12 << 8) + 7: // sbw
                                if (this.stack.length < 4) {
                                    error = true;
                                    break;
                                }
                                // To convert to type2 we have to move the width value to the
                                // first part of the charstring and then use rmoveto with
                                // (dx, dy). The height argument will not be used for vmtx and
                                // vhea tables reconstruction -- ignoring it.
                                var wy = this.stack.pop();
                                wx = this.stack.pop();
                                var sby = this.stack.pop();
                                sbx = this.stack.pop();
                                this.lsb = sbx;
                                this.width = wx;
                                this.stack.push(wx, sbx, sby);
                                error = this.executeCommand(3, COMMAND_MAP.rmoveto);
                                break;
                            case (12 << 8) + 12: // div
                                if (this.stack.length < 2) {
                                    error = true;
                                    break;
                                }
                                var num2 = this.stack.pop();
                                var num1 = this.stack.pop();
                                this.stack.push(num1 / num2);
                                break;
                            case (12 << 8) + 16: // callothersubr
                                if (this.stack.length < 2) {
                                    error = true;
                                    break;
                                }
                                subrNumber = this.stack.pop();
                                var numArgs = this.stack.pop();
                                if (subrNumber === 0 && numArgs === 3) {
                                    var flexArgs = this.stack.splice(this.stack.length - 17, 17);
                                    this.stack.push(
                                        flexArgs[2] + flexArgs[0], // bcp1x + rpx
                                        flexArgs[3] + flexArgs[1], // bcp1y + rpy
                                        flexArgs[4], // bcp2x
                                        flexArgs[5], // bcp2y
                                        flexArgs[6], // p2x
                                        flexArgs[7], // p2y
                                        flexArgs[8], // bcp3x
                                        flexArgs[9], // bcp3y
                                        flexArgs[10], // bcp4x
                                        flexArgs[11], // bcp4y
                                        flexArgs[12], // p3x
                                        flexArgs[13], // p3y
                                        flexArgs[14] // flexDepth
                                        // 15 = finalx unused by flex
                                        // 16 = finaly unused by flex
                                    );
                                    error = this.executeCommand(13, COMMAND_MAP.flex, true);
                                    this.flexing = false;
                                    this.stack.push(flexArgs[15], flexArgs[16]);
                                } else if (subrNumber === 1 && numArgs === 0) {
                                    this.flexing = true;
                                }
                                break;
                            case (12 << 8) + 17: // pop
                                // Ignore this since it is only used with othersubr.
                                break;
                            case (12 << 8) + 33: // setcurrentpoint
                                // Ignore for now.
                                this.stack = [];
                                break;
                            default:
                                warn('Unknown type 1 charstring command of "' + value + '"');
                                break;
                        }
                        if (error) {
                            break;
                        }
                        continue;
                    } else if (value <= 246) {
                        value = value - 139;
                    } else if (value <= 250) {
                        value = ((value - 247) * 256) + encoded[++i] + 108;
                    } else if (value <= 254) {
                        value = -((value - 251) * 256) - encoded[++i] - 108;
                    } else {
                        value = (encoded[++i] & 0xff) << 24 | (encoded[++i] & 0xff) << 16 |
                            (encoded[++i] & 0xff) << 8 | (encoded[++i] & 0xff) << 0;
                    }
                    this.stack.push(value);
                }
                return error;
            },

            executeCommand : function(howManyArgs, command, keepStack) {
                var stackLength = this.stack.length;
                if (howManyArgs > stackLength) {
                    return true;
                }
                var start = stackLength - howManyArgs;
                for (var i = start; i < stackLength; i++) {
                    var value = this.stack[i];
                    if (value === (value | 0)) { // int
                        this.output.push(28, (value >> 8) & 0xff, value & 0xff);
                    } else { // fixed point
                        value = (65536 * value) | 0;
                        this.output.push(255,
                            (value >> 24) & 0xFF,
                            (value >> 16) & 0xFF,
                            (value >> 8) & 0xFF,
                            value & 0xFF);
                    }
                }
                this.output.push.apply(this.output, command);
                if (keepStack) {
                    this.stack.splice(start, howManyArgs);
                } else {
                    this.stack.length = 0;
                }
                return false;
            }
        };

        return Type1CharString;
    })();

    /*
     * Type1Parser encapsulate the needed code for parsing a Type1 font
     * program. Some of its logic depends on the Type2 charstrings
     * structure.
     * Note: this doesn't really parse the font since that would require evaluation
     * of PostScript, but it is possible in most cases to extract what we need
     * without a full parse.
     */
    var Type1Parser = (function Type1ParserClosure() {
        /*
         * Decrypt a Sequence of Ciphertext Bytes to Produce the Original Sequence
         * of Plaintext Bytes. The function took a key as a parameter which can be
         * for decrypting the eexec block of for decoding charStrings.
         */
        var EEXEC_ENCRYPT_KEY = 55665;
        var CHAR_STRS_ENCRYPT_KEY = 4330;

        function isHexDigit(code) {
            return code >= 48 && code <= 57 || // '0'-'9'
                code >= 65 && code <= 70 || // 'A'-'F'
                code >= 97 && code <= 102;  // 'a'-'f'
        }

        function decrypt(data, key, discardNumber) {
            if (discardNumber >= data.length) {
                return new Uint8Array(0);
            }
            var r = key | 0, c1 = 52845, c2 = 22719, i, j;
            for (i = 0; i < discardNumber; i++) {
                r = ((data[i] + r) * c1 + c2) & ((1 << 16) - 1);
            }
            var count = data.length - discardNumber;
            var decrypted = new Uint8Array(count);
            for (i = discardNumber, j = 0; j < count; i++, j++) {
                var value = data[i];
                decrypted[j] = value ^ (r >> 8);
                r = ((value + r) * c1 + c2) & ((1 << 16) - 1);
            }
            return decrypted;
        }

        function decryptAscii(data, key, discardNumber) {
            var r = key | 0, c1 = 52845, c2 = 22719;
            var count = data.length, maybeLength = count >>> 1;
            var decrypted = new Uint8Array(maybeLength);
            var i, j;
            for (i = 0, j = 0; i < count; i++) {
                var digit1 = data[i];
                if (!isHexDigit(digit1)) {
                    continue;
                }
                i++;
                var digit2;
                while (i < count && !isHexDigit(digit2 = data[i])) {
                    i++;
                }
                if (i < count) {
                    var value = parseInt(String.fromCharCode(digit1, digit2), 16);
                    decrypted[j++] = value ^ (r >> 8);
                    r = ((value + r) * c1 + c2) & ((1 << 16) - 1);
                }
            }
            return Array.prototype.slice.call(decrypted, discardNumber, j);
        }

        function isSpecial(c) {
            return c === 0x2F || // '/'
                c === 0x5B || c === 0x5D || // '[', ']'
                c === 0x7B || c === 0x7D || // '{', '}'
                c === 0x28 || c === 0x29; // '(', ')'
        }

        function Type1Parser(stream, encrypted, seacAnalysisEnabled) {
            if (encrypted) {
                var data = stream.getBytes();
                var isBinary = !(isHexDigit(data[0]) && isHexDigit(data[1]) &&
                    isHexDigit(data[2]) && isHexDigit(data[3]));
                stream = new Stream(isBinary ? decrypt(data, EEXEC_ENCRYPT_KEY, 4) :
                    decryptAscii(data, EEXEC_ENCRYPT_KEY, 4));
            }
            this.seacAnalysisEnabled = !!seacAnalysisEnabled;

            this.stream = stream;
            this.nextChar();
        }

        Type1Parser.prototype = {
            readNumberArray : function Type1Parser_readNumberArray() {
                this.getToken(); // read '[' or '{' (arrays can start with either)
                var array = [];
                while (true) {
                    var token = this.getToken();
                    if (token === null || token === ']' || token === '}') {
                        break;
                    }
                    array.push(parseFloat(token || 0));
                }
                return array;
            },

            readNumber : function Type1Parser_readNumber() {
                var token = this.getToken();
                return parseFloat(token || 0);
            },

            readInt : function Type1Parser_readInt() {
                // Use '| 0' to prevent setting a double into length such as the double
                // does not flow into the loop variable.
                var token = this.getToken();
                return parseInt(token || 0, 10) | 0;
            },

            readBoolean : function Type1Parser_readBoolean() {
                var token = this.getToken();

                // Use 1 and 0 since that's what type2 charstrings use.
                return token === 'true' ? 1 : 0;
            },

            nextChar : function Type1_nextChar() {
                return (this.currentChar = this.stream.getByte());
            },

            getToken : function Type1Parser_getToken() {
                // Eat whitespace and comments.
                var comment = false;
                var ch = this.currentChar;
                while (true) {
                    if (ch === -1) {
                        return null;
                    }

                    if (comment) {
                        if (ch === 0x0A || ch === 0x0D) {
                            comment = false;
                        }
                    } else if (ch === 0x25) { // '%'
                        comment = true;
                    } else if (!Lexer.isSpace(ch)) {
                        break;
                    }
                    ch = this.nextChar();
                }
                if (isSpecial(ch)) {
                    this.nextChar();
                    return String.fromCharCode(ch);
                }
                var token = '';
                do {
                    token += String.fromCharCode(ch);
                    ch = this.nextChar();
                } while (ch >= 0 && !Lexer.isSpace(ch) && !isSpecial(ch));
                return token;
            },

            /*
             * Returns an object containing a Subrs array and a CharStrings
             * array extracted from and eexec encrypted block of data
             */
            extractFontProgram : function Type1Parser_extractFontProgram() {
                var stream = this.stream;

                var subrs = [], charstrings = [];
                var privateData = Object.create(null);
                privateData['lenIV'] = 4;
                var program = {
                    subrs       : [],
                    charstrings : [],
                    properties  : {
                        'privateData' : privateData
                    }
                };
                var token, length, data, lenIV, encoded;
                while ((token = this.getToken()) !== null) {
                    if (token !== '/') {
                        continue;
                    }
                    token = this.getToken();
                    switch (token) {
                        case 'CharStrings':
                            // The number immediately following CharStrings must be greater or
                            // equal to the number of CharStrings.
                            this.getToken();
                            this.getToken(); // read in 'dict'
                            this.getToken(); // read in 'dup'
                            this.getToken(); // read in 'begin'
                            while (true) {
                                token = this.getToken();
                                if (token === null || token === 'end') {
                                    break;
                                }

                                if (token !== '/') {
                                    continue;
                                }
                                var glyph = this.getToken();
                                length = this.readInt();
                                this.getToken(); // read in 'RD' or '-|'
                                data = stream.makeSubStream(stream.pos, length);
                                lenIV = program.properties.privateData['lenIV'];
                                encoded = decrypt(data.getBytes(), CHAR_STRS_ENCRYPT_KEY, lenIV);
                                // Skip past the required space and binary data.
                                stream.skip(length);
                                this.nextChar();
                                token = this.getToken(); // read in 'ND' or '|-'
                                if (token === 'noaccess') {
                                    this.getToken(); // read in 'def'
                                }
                                charstrings.push({
                                    glyph   : glyph,
                                    encoded : encoded
                                });
                            }
                            break;
                        case 'Subrs':
                            var num = this.readInt();
                            this.getToken(); // read in 'array'
                            while ((token = this.getToken()) === 'dup') {
                                var index = this.readInt();
                                length = this.readInt();
                                this.getToken(); // read in 'RD' or '-|'
                                data = stream.makeSubStream(stream.pos, length);
                                lenIV = program.properties.privateData['lenIV'];
                                encoded = decrypt(data.getBytes(), CHAR_STRS_ENCRYPT_KEY, lenIV);
                                // Skip past the required space and binary data.
                                stream.skip(length);
                                this.nextChar();
                                token = this.getToken(); // read in 'NP' or '|'
                                if (token === 'noaccess') {
                                    this.getToken(); // read in 'put'
                                }
                                subrs[index] = encoded;
                            }
                            break;
                        case 'BlueValues':
                        case 'OtherBlues':
                        case 'FamilyBlues':
                        case 'FamilyOtherBlues':
                            var blueArray = this.readNumberArray();
                            // *Blue* values may contain invalid data: disables reading of
                            // those values when hinting is disabled.
                            if (blueArray.length > 0 && (blueArray.length % 2) === 0 &&
                                HINTING_ENABLED) {
                                program.properties.privateData[token] = blueArray;
                            }
                            break;
                        case 'StemSnapH':
                        case 'StemSnapV':
                            program.properties.privateData[token] = this.readNumberArray();
                            break;
                        case 'StdHW':
                        case 'StdVW':
                            program.properties.privateData[token] =
                                this.readNumberArray()[0];
                            break;
                        case 'BlueShift':
                        case 'lenIV':
                        case 'BlueFuzz':
                        case 'BlueScale':
                        case 'LanguageGroup':
                        case 'ExpansionFactor':
                            program.properties.privateData[token] = this.readNumber();
                            break;
                        case 'ForceBold':
                            program.properties.privateData[token] = this.readBoolean();
                            break;
                    }
                }

                for (var i = 0; i < charstrings.length; i++) {
                    glyph = charstrings[i].glyph;
                    encoded = charstrings[i].encoded;
                    var charString = new Type1CharString();
                    var error = charString.convert(encoded, subrs,
                        this.seacAnalysisEnabled);
                    var output = charString.output;
                    if (error) {
                        // It seems when FreeType encounters an error while evaluating a glyph
                        // that it completely ignores the glyph so we'll mimic that behaviour
                        // here and put an endchar to make the validator happy.
                        output = [14];
                    }
                    program.charstrings.push({
                        glyphName  : glyph,
                        charstring : output,
                        width      : charString.width,
                        lsb        : charString.lsb,
                        seac       : charString.seac
                    });
                }

                return program;
            },

            extractFontHeader : function Type1Parser_extractFontHeader(properties) {
                var token;
                while ((token = this.getToken()) !== null) {
                    if (token !== '/') {
                        continue;
                    }
                    token = this.getToken();
                    switch (token) {
                        case 'FontMatrix':
                            var matrix = this.readNumberArray();
                            properties.fontMatrix = matrix;
                            break;
                        case 'Encoding':
                            var encodingArg = this.getToken();
                            var encoding;
                            if (!/^\d+$/.test(encodingArg)) {
                                // encoding name is specified
                                encoding = getEncoding(encodingArg);
                            } else {
                                encoding = [];
                                var size = parseInt(encodingArg, 10) | 0;
                                this.getToken(); // read in 'array'

                                for (var j = 0; j < size; j++) {
                                    token = this.getToken();
                                    // skipping till first dup or def (e.g. ignoring for statement)
                                    while (token !== 'dup' && token !== 'def') {
                                        token = this.getToken();
                                        if (token === null) {
                                            return; // invalid header
                                        }
                                    }
                                    if (token === 'def') {
                                        break; // read all array data
                                    }
                                    var index = this.readInt();
                                    this.getToken(); // read in '/'
                                    var glyph = this.getToken();
                                    encoding[index] = glyph;
                                    this.getToken(); // read the in 'put'
                                }
                            }
                            properties.builtInEncoding = encoding;
                            break;
                        case 'FontBBox':
                            var fontBBox = this.readNumberArray();
                            // adjusting ascent/descent
                            properties.ascent = fontBBox[3];
                            properties.descent = fontBBox[1];
                            properties.ascentScaled = true;
                            break;
                    }
                }
            }
        };

        return Type1Parser;
    })();

    exports.Type1Parser = Type1Parser;
}));
