/**
 * @licstart The following is the entire license notice for the
 * JavaScript code in this page
 *
 * Copyright 2022 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 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.at(-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(...command);
    if (keepStack) {
      this.stack.splice(start, howManyArgs);
    } else {
      this.stack.length = 0;
    }
    return false;
  }
}
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();
  }
  prevChar() {
    this.stream.skip(-2);
    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;
    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;
            const encoded = this.readCharStrings(data, lenIV);
            this.nextChar();
            token = this.getToken();
            if (token === "noaccess") {
              this.getToken();
            } else if (token === "/") {
              this.prevChar();
            }
            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;
            const 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 (const {
      encoded,
      glyph
    } of charstrings) {
      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;
      }
    }
  }
}
exports.Type1Parser = Type1Parser;