/**
 * @licstart The following is the entire license notice for the
 * Javascript code in this page
 *
 * Copyright 2021 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.
 *
 * @licend The above is the entire license notice for the
 * Javascript code in this page
 */
"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.Type1Parser = void 0;

var _encodings = require("./encodings.js");

var _core_utils = require("./core_utils.js");

var _stream = require("./stream.js");

var _util = require("../shared/util.js");

const HINTING_ENABLED = false;

const Type1CharString = function Type1CharStringClosure() {
  const 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]
  };

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

    convert(encoded, subrs, seacAnalysisEnabled) {
      const count = encoded.length;
      let error = false;
      let wx, sbx, subrNumber;

      for (let i = 0; i < count; i++) {
        let value = encoded[i];

        if (value < 32) {
          if (value === 12) {
            value = (value << 8) + encoded[++i];
          }

          switch (value) {
            case 1:
              if (!HINTING_ENABLED) {
                this.stack = [];
                break;
              }

              error = this.executeCommand(2, COMMAND_MAP.hstem);
              break;

            case 3:
              if (!HINTING_ENABLED) {
                this.stack = [];
                break;
              }

              error = this.executeCommand(2, COMMAND_MAP.vstem);
              break;

            case 4:
              if (this.flexing) {
                if (this.stack.length < 1) {
                  error = true;
                  break;
                }

                const dy = this.stack.pop();
                this.stack.push(0, dy);
                break;
              }

              error = this.executeCommand(1, COMMAND_MAP.vmoveto);
              break;

            case 5:
              error = this.executeCommand(2, COMMAND_MAP.rlineto);
              break;

            case 6:
              error = this.executeCommand(1, COMMAND_MAP.hlineto);
              break;

            case 7:
              error = this.executeCommand(1, COMMAND_MAP.vlineto);
              break;

            case 8:
              error = this.executeCommand(6, COMMAND_MAP.rrcurveto);
              break;

            case 9:
              this.stack = [];
              break;

            case 10:
              if (this.stack.length < 1) {
                error = true;
                break;
              }

              subrNumber = this.stack.pop();

              if (!subrs[subrNumber]) {
                error = true;
                break;
              }

              error = this.convert(subrs[subrNumber], subrs, seacAnalysisEnabled);
              break;

            case 11:
              return error;

            case 13:
              if (this.stack.length < 2) {
                error = true;
                break;
              }

              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:
              this.output.push(COMMAND_MAP.endchar[0]);
              break;

            case 21:
              if (this.flexing) {
                break;
              }

              error = this.executeCommand(2, COMMAND_MAP.rmoveto);
              break;

            case 22:
              if (this.flexing) {
                this.stack.push(0);
                break;
              }

              error = this.executeCommand(1, COMMAND_MAP.hmoveto);
              break;

            case 30:
              error = this.executeCommand(4, COMMAND_MAP.vhcurveto);
              break;

            case 31:
              error = this.executeCommand(4, COMMAND_MAP.hvcurveto);
              break;

            case (12 << 8) + 0:
              this.stack = [];
              break;

            case (12 << 8) + 1:
              if (!HINTING_ENABLED) {
                this.stack = [];
                break;
              }

              error = this.executeCommand(2, COMMAND_MAP.vstem);
              break;

            case (12 << 8) + 2:
              if (!HINTING_ENABLED) {
                this.stack = [];
                break;
              }

              error = this.executeCommand(2, COMMAND_MAP.hstem);
              break;

            case (12 << 8) + 6:
              if (seacAnalysisEnabled) {
                const asb = this.stack[this.stack.length - 5];
                this.seac = this.stack.splice(-4, 4);
                this.seac[0] += this.lsb - asb;
                error = this.executeCommand(0, COMMAND_MAP.endchar);
              } else {
                error = this.executeCommand(4, COMMAND_MAP.endchar);
              }

              break;

            case (12 << 8) + 7:
              if (this.stack.length < 4) {
                error = true;
                break;
              }

              this.stack.pop();
              wx = this.stack.pop();
              const 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:
              if (this.stack.length < 2) {
                error = true;
                break;
              }

              const num2 = this.stack.pop();
              const num1 = this.stack.pop();
              this.stack.push(num1 / num2);
              break;

            case (12 << 8) + 16:
              if (this.stack.length < 2) {
                error = true;
                break;
              }

              subrNumber = this.stack.pop();
              const numArgs = this.stack.pop();

              if (subrNumber === 0 && numArgs === 3) {
                const flexArgs = this.stack.splice(this.stack.length - 17, 17);
                this.stack.push(flexArgs[2] + flexArgs[0], flexArgs[3] + flexArgs[1], flexArgs[4], flexArgs[5], flexArgs[6], flexArgs[7], flexArgs[8], flexArgs[9], flexArgs[10], flexArgs[11], flexArgs[12], flexArgs[13], flexArgs[14]);
                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:
              break;

            case (12 << 8) + 33:
              this.stack = [];
              break;

            default:
              (0, _util.warn)('Unknown type 1 charstring command of "' + value + '"');
              break;
          }

          if (error) {
            break;
          }

          continue;
        } else if (value <= 246) {
          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(howManyArgs, command, keepStack) {
      const stackLength = this.stack.length;

      if (howManyArgs > stackLength) {
        return true;
      }

      const start = stackLength - howManyArgs;

      for (let i = start; i < stackLength; i++) {
        let value = this.stack[i];

        if (Number.isInteger(value)) {
          this.output.push(28, value >> 8 & 0xff, value & 0xff);
        } else {
          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;
}();

const Type1Parser = function Type1ParserClosure() {
  const EEXEC_ENCRYPT_KEY = 55665;
  const CHAR_STRS_ENCRYPT_KEY = 4330;

  function isHexDigit(code) {
    return code >= 48 && code <= 57 || code >= 65 && code <= 70 || code >= 97 && code <= 102;
  }

  function decrypt(data, key, discardNumber) {
    if (discardNumber >= data.length) {
      return new Uint8Array(0);
    }

    const c1 = 52845,
          c2 = 22719;
    let r = key | 0,
        i,
        j;

    for (i = 0; i < discardNumber; i++) {
      r = (data[i] + r) * c1 + c2 & (1 << 16) - 1;
    }

    const count = data.length - discardNumber;
    const decrypted = new Uint8Array(count);

    for (i = discardNumber, j = 0; j < count; i++, j++) {
      const value = data[i];
      decrypted[j] = value ^ r >> 8;
      r = (value + r) * c1 + c2 & (1 << 16) - 1;
    }

    return decrypted;
  }

  function decryptAscii(data, key, discardNumber) {
    const c1 = 52845,
          c2 = 22719;
    let r = key | 0;
    const count = data.length,
          maybeLength = count >>> 1;
    const decrypted = new Uint8Array(maybeLength);
    let i, j;

    for (i = 0, j = 0; i < count; i++) {
      const digit1 = data[i];

      if (!isHexDigit(digit1)) {
        continue;
      }

      i++;
      let digit2;

      while (i < count && !isHexDigit(digit2 = data[i])) {
        i++;
      }

      if (i < count) {
        const value = parseInt(String.fromCharCode(digit1, digit2), 16);
        decrypted[j++] = value ^ r >> 8;
        r = (value + r) * c1 + c2 & (1 << 16) - 1;
      }
    }

    return decrypted.slice(discardNumber, j);
  }

  function isSpecial(c) {
    return c === 0x2f || c === 0x5b || c === 0x5d || c === 0x7b || c === 0x7d || c === 0x28 || c === 0x29;
  }

  class Type1Parser {
    constructor(stream, encrypted, seacAnalysisEnabled) {
      if (encrypted) {
        const data = stream.getBytes();
        const isBinary = !((isHexDigit(data[0]) || (0, _core_utils.isWhiteSpace)(data[0])) && isHexDigit(data[1]) && isHexDigit(data[2]) && isHexDigit(data[3]) && isHexDigit(data[4]) && isHexDigit(data[5]) && isHexDigit(data[6]) && isHexDigit(data[7]));
        stream = new _stream.Stream(isBinary ? decrypt(data, EEXEC_ENCRYPT_KEY, 4) : decryptAscii(data, EEXEC_ENCRYPT_KEY, 4));
      }

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

    readNumberArray() {
      this.getToken();
      const array = [];

      while (true) {
        const token = this.getToken();

        if (token === null || token === "]" || token === "}") {
          break;
        }

        array.push(parseFloat(token || 0));
      }

      return array;
    }

    readNumber() {
      const token = this.getToken();
      return parseFloat(token || 0);
    }

    readInt() {
      const token = this.getToken();
      return parseInt(token || 0, 10) | 0;
    }

    readBoolean() {
      const token = this.getToken();
      return token === "true" ? 1 : 0;
    }

    nextChar() {
      return this.currentChar = this.stream.getByte();
    }

    getToken() {
      let comment = false;
      let 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 (!(0, _core_utils.isWhiteSpace)(ch)) {
          break;
        }

        ch = this.nextChar();
      }

      if (isSpecial(ch)) {
        this.nextChar();
        return String.fromCharCode(ch);
      }

      let token = "";

      do {
        token += String.fromCharCode(ch);
        ch = this.nextChar();
      } while (ch >= 0 && !(0, _core_utils.isWhiteSpace)(ch) && !isSpecial(ch));

      return token;
    }

    readCharStrings(bytes, lenIV) {
      if (lenIV === -1) {
        return bytes;
      }

      return decrypt(bytes, CHAR_STRS_ENCRYPT_KEY, lenIV);
    }

    extractFontProgram(properties) {
      const stream = this.stream;
      const subrs = [],
            charstrings = [];
      const privateData = Object.create(null);
      privateData.lenIV = 4;
      const program = {
        subrs: [],
        charstrings: [],
        properties: {
          privateData
        }
      };
      let token, length, data, lenIV, encoded;

      while ((token = this.getToken()) !== null) {
        if (token !== "/") {
          continue;
        }

        token = this.getToken();

        switch (token) {
          case "CharStrings":
            this.getToken();
            this.getToken();
            this.getToken();
            this.getToken();

            while (true) {
              token = this.getToken();

              if (token === null || token === "end") {
                break;
              }

              if (token !== "/") {
                continue;
              }

              const glyph = this.getToken();
              length = this.readInt();
              this.getToken();
              data = length > 0 ? stream.getBytes(length) : new Uint8Array(0);
              lenIV = program.properties.privateData.lenIV;
              encoded = this.readCharStrings(data, lenIV);
              this.nextChar();
              token = this.getToken();

              if (token === "noaccess") {
                this.getToken();
              }

              charstrings.push({
                glyph,
                encoded
              });
            }

            break;

          case "Subrs":
            this.readInt();
            this.getToken();

            while (this.getToken() === "dup") {
              const index = this.readInt();
              length = this.readInt();
              this.getToken();
              data = length > 0 ? stream.getBytes(length) : new Uint8Array(0);
              lenIV = program.properties.privateData.lenIV;
              encoded = this.readCharStrings(data, lenIV);
              this.nextChar();
              token = this.getToken();

              if (token === "noaccess") {
                this.getToken();
              }

              subrs[index] = encoded;
            }

            break;

          case "BlueValues":
          case "OtherBlues":
          case "FamilyBlues":
          case "FamilyOtherBlues":
            const blueArray = this.readNumberArray();

            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 (let i = 0; i < charstrings.length; i++) {
        const glyph = charstrings[i].glyph;
        encoded = charstrings[i].encoded;
        const charString = new Type1CharString();
        const error = charString.convert(encoded, subrs, this.seacAnalysisEnabled);
        let output = charString.output;

        if (error) {
          output = [14];
        }

        const charStringObject = {
          glyphName: glyph,
          charstring: output,
          width: charString.width,
          lsb: charString.lsb,
          seac: charString.seac
        };

        if (glyph === ".notdef") {
          program.charstrings.unshift(charStringObject);
        } else {
          program.charstrings.push(charStringObject);
        }

        if (properties.builtInEncoding) {
          const index = properties.builtInEncoding.indexOf(glyph);

          if (index > -1 && properties.widths[index] === undefined && index >= properties.firstChar && index <= properties.lastChar) {
            properties.widths[index] = charString.width;
          }
        }
      }

      return program;
    }

    extractFontHeader(properties) {
      let token;

      while ((token = this.getToken()) !== null) {
        if (token !== "/") {
          continue;
        }

        token = this.getToken();

        switch (token) {
          case "FontMatrix":
            const matrix = this.readNumberArray();
            properties.fontMatrix = matrix;
            break;

          case "Encoding":
            const encodingArg = this.getToken();
            let encoding;

            if (!/^\d+$/.test(encodingArg)) {
              encoding = (0, _encodings.getEncoding)(encodingArg);
            } else {
              encoding = [];
              const size = parseInt(encodingArg, 10) | 0;
              this.getToken();

              for (let j = 0; j < size; j++) {
                token = this.getToken();

                while (token !== "dup" && token !== "def") {
                  token = this.getToken();

                  if (token === null) {
                    return;
                  }
                }

                if (token === "def") {
                  break;
                }

                const index = this.readInt();
                this.getToken();
                const glyph = this.getToken();
                encoding[index] = glyph;
                this.getToken();
              }
            }

            properties.builtInEncoding = encoding;
            break;

          case "FontBBox":
            const fontBBox = this.readNumberArray();
            properties.ascent = Math.max(fontBBox[3], fontBBox[1]);
            properties.descent = Math.min(fontBBox[1], fontBBox[3]);
            properties.ascentScaled = true;
            break;
        }
      }
    }

  }

  return Type1Parser;
}();

exports.Type1Parser = Type1Parser;