/**
 * @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.XRef = void 0;

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

var _primitives = require("./primitives.js");

var _parser = require("./parser.js");

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

var _crypto = require("./crypto.js");

class XRef {
  constructor(stream, pdfManager) {
    this.stream = stream;
    this.pdfManager = pdfManager;
    this.entries = [];
    this.xrefstms = Object.create(null);
    this._cacheMap = new Map();
    this.stats = {
      streamTypes: Object.create(null),
      fontTypes: Object.create(null)
    };
    this._newRefNum = null;
  }

  getNewRef() {
    if (this._newRefNum === null) {
      this._newRefNum = this.entries.length;
    }

    return _primitives.Ref.get(this._newRefNum++, 0);
  }

  resetNewRef() {
    this._newRefNum = null;
  }

  setStartXRef(startXRef) {
    this.startXRefQueue = [startXRef];
  }

  parse(recoveryMode = false) {
    let trailerDict;

    if (!recoveryMode) {
      trailerDict = this.readXRef();
    } else {
      (0, _util.warn)("Indexing all PDF objects");
      trailerDict = this.indexObjects();
    }

    trailerDict.assignXref(this);
    this.trailer = trailerDict;
    let encrypt;

    try {
      encrypt = trailerDict.get("Encrypt");
    } catch (ex) {
      if (ex instanceof _core_utils.MissingDataException) {
        throw ex;
      }

      (0, _util.warn)(`XRef.parse - Invalid "Encrypt" reference: "${ex}".`);
    }

    if ((0, _primitives.isDict)(encrypt)) {
      const ids = trailerDict.get("ID");
      const fileId = ids && ids.length ? ids[0] : "";
      encrypt.suppressEncryption = true;
      this.encrypt = new _crypto.CipherTransformFactory(encrypt, fileId, this.pdfManager.password);
    }

    let root;

    try {
      root = trailerDict.get("Root");
    } catch (ex) {
      if (ex instanceof _core_utils.MissingDataException) {
        throw ex;
      }

      (0, _util.warn)(`XRef.parse - Invalid "Root" reference: "${ex}".`);
    }

    if ((0, _primitives.isDict)(root) && root.has("Pages")) {
      this.root = root;
    } else {
      if (!recoveryMode) {
        throw new _core_utils.XRefParseException();
      }

      throw new _util.FormatError("Invalid root reference");
    }
  }

  processXRefTable(parser) {
    if (!("tableState" in this)) {
      this.tableState = {
        entryNum: 0,
        streamPos: parser.lexer.stream.pos,
        parserBuf1: parser.buf1,
        parserBuf2: parser.buf2
      };
    }

    const obj = this.readXRefTable(parser);

    if (!(0, _primitives.isCmd)(obj, "trailer")) {
      throw new _util.FormatError("Invalid XRef table: could not find trailer dictionary");
    }

    let dict = parser.getObj();

    if (!(0, _primitives.isDict)(dict) && dict.dict) {
      dict = dict.dict;
    }

    if (!(0, _primitives.isDict)(dict)) {
      throw new _util.FormatError("Invalid XRef table: could not parse trailer dictionary");
    }

    delete this.tableState;
    return dict;
  }

  readXRefTable(parser) {
    const stream = parser.lexer.stream;
    const tableState = this.tableState;
    stream.pos = tableState.streamPos;
    parser.buf1 = tableState.parserBuf1;
    parser.buf2 = tableState.parserBuf2;
    let obj;

    while (true) {
      if (!("firstEntryNum" in tableState) || !("entryCount" in tableState)) {
        if ((0, _primitives.isCmd)(obj = parser.getObj(), "trailer")) {
          break;
        }

        tableState.firstEntryNum = obj;
        tableState.entryCount = parser.getObj();
      }

      let first = tableState.firstEntryNum;
      const count = tableState.entryCount;

      if (!Number.isInteger(first) || !Number.isInteger(count)) {
        throw new _util.FormatError("Invalid XRef table: wrong types in subsection header");
      }

      for (let i = tableState.entryNum; i < count; i++) {
        tableState.streamPos = stream.pos;
        tableState.entryNum = i;
        tableState.parserBuf1 = parser.buf1;
        tableState.parserBuf2 = parser.buf2;
        const entry = {};
        entry.offset = parser.getObj();
        entry.gen = parser.getObj();
        const type = parser.getObj();

        if (type instanceof _primitives.Cmd) {
          switch (type.cmd) {
            case "f":
              entry.free = true;
              break;

            case "n":
              entry.uncompressed = true;
              break;
          }
        }

        if (!Number.isInteger(entry.offset) || !Number.isInteger(entry.gen) || !(entry.free || entry.uncompressed)) {
          throw new _util.FormatError(`Invalid entry in XRef subsection: ${first}, ${count}`);
        }

        if (i === 0 && entry.free && first === 1) {
          first = 0;
        }

        if (!this.entries[i + first]) {
          this.entries[i + first] = entry;
        }
      }

      tableState.entryNum = 0;
      tableState.streamPos = stream.pos;
      tableState.parserBuf1 = parser.buf1;
      tableState.parserBuf2 = parser.buf2;
      delete tableState.firstEntryNum;
      delete tableState.entryCount;
    }

    if (this.entries[0] && !this.entries[0].free) {
      throw new _util.FormatError("Invalid XRef table: unexpected first object");
    }

    return obj;
  }

  processXRefStream(stream) {
    if (!("streamState" in this)) {
      const streamParameters = stream.dict;
      const byteWidths = streamParameters.get("W");
      let range = streamParameters.get("Index");

      if (!range) {
        range = [0, streamParameters.get("Size")];
      }

      this.streamState = {
        entryRanges: range,
        byteWidths,
        entryNum: 0,
        streamPos: stream.pos
      };
    }

    this.readXRefStream(stream);
    delete this.streamState;
    return stream.dict;
  }

  readXRefStream(stream) {
    let i, j;
    const streamState = this.streamState;
    stream.pos = streamState.streamPos;
    const byteWidths = streamState.byteWidths;
    const typeFieldWidth = byteWidths[0];
    const offsetFieldWidth = byteWidths[1];
    const generationFieldWidth = byteWidths[2];
    const entryRanges = streamState.entryRanges;

    while (entryRanges.length > 0) {
      const first = entryRanges[0];
      const n = entryRanges[1];

      if (!Number.isInteger(first) || !Number.isInteger(n)) {
        throw new _util.FormatError(`Invalid XRef range fields: ${first}, ${n}`);
      }

      if (!Number.isInteger(typeFieldWidth) || !Number.isInteger(offsetFieldWidth) || !Number.isInteger(generationFieldWidth)) {
        throw new _util.FormatError(`Invalid XRef entry fields length: ${first}, ${n}`);
      }

      for (i = streamState.entryNum; i < n; ++i) {
        streamState.entryNum = i;
        streamState.streamPos = stream.pos;
        let type = 0,
            offset = 0,
            generation = 0;

        for (j = 0; j < typeFieldWidth; ++j) {
          type = type << 8 | stream.getByte();
        }

        if (typeFieldWidth === 0) {
          type = 1;
        }

        for (j = 0; j < offsetFieldWidth; ++j) {
          offset = offset << 8 | stream.getByte();
        }

        for (j = 0; j < generationFieldWidth; ++j) {
          generation = generation << 8 | stream.getByte();
        }

        const entry = {};
        entry.offset = offset;
        entry.gen = generation;

        switch (type) {
          case 0:
            entry.free = true;
            break;

          case 1:
            entry.uncompressed = true;
            break;

          case 2:
            break;

          default:
            throw new _util.FormatError(`Invalid XRef entry type: ${type}`);
        }

        if (!this.entries[first + i]) {
          this.entries[first + i] = entry;
        }
      }

      streamState.entryNum = 0;
      streamState.streamPos = stream.pos;
      entryRanges.splice(0, 2);
    }
  }

  indexObjects() {
    const TAB = 0x9,
          LF = 0xa,
          CR = 0xd,
          SPACE = 0x20;
    const PERCENT = 0x25,
          LT = 0x3c;

    function readToken(data, offset) {
      let token = "",
          ch = data[offset];

      while (ch !== LF && ch !== CR && ch !== LT) {
        if (++offset >= data.length) {
          break;
        }

        token += String.fromCharCode(ch);
        ch = data[offset];
      }

      return token;
    }

    function skipUntil(data, offset, what) {
      const length = what.length,
            dataLength = data.length;
      let skipped = 0;

      while (offset < dataLength) {
        let i = 0;

        while (i < length && data[offset + i] === what[i]) {
          ++i;
        }

        if (i >= length) {
          break;
        }

        offset++;
        skipped++;
      }

      return skipped;
    }

    const objRegExp = /^(\d+)\s+(\d+)\s+obj\b/;
    const endobjRegExp = /\bendobj[\b\s]$/;
    const nestedObjRegExp = /\s+(\d+\s+\d+\s+obj[\b\s<])$/;
    const CHECK_CONTENT_LENGTH = 25;
    const trailerBytes = new Uint8Array([116, 114, 97, 105, 108, 101, 114]);
    const startxrefBytes = new Uint8Array([115, 116, 97, 114, 116, 120, 114, 101, 102]);
    const objBytes = new Uint8Array([111, 98, 106]);
    const xrefBytes = new Uint8Array([47, 88, 82, 101, 102]);
    this.entries.length = 0;
    const stream = this.stream;
    stream.pos = 0;
    const buffer = stream.getBytes(),
          length = buffer.length;
    let position = stream.start;
    const trailers = [],
          xrefStms = [];

    while (position < length) {
      let ch = buffer[position];

      if (ch === TAB || ch === LF || ch === CR || ch === SPACE) {
        ++position;
        continue;
      }

      if (ch === PERCENT) {
        do {
          ++position;

          if (position >= length) {
            break;
          }

          ch = buffer[position];
        } while (ch !== LF && ch !== CR);

        continue;
      }

      const token = readToken(buffer, position);
      let m;

      if (token.startsWith("xref") && (token.length === 4 || /\s/.test(token[4]))) {
        position += skipUntil(buffer, position, trailerBytes);
        trailers.push(position);
        position += skipUntil(buffer, position, startxrefBytes);
      } else if (m = objRegExp.exec(token)) {
        const num = m[1] | 0,
              gen = m[2] | 0;
        let contentLength,
            startPos = position + token.length,
            updateEntries = false;

        if (!this.entries[num]) {
          updateEntries = true;
        } else if (this.entries[num].gen === gen) {
          try {
            const parser = new _parser.Parser({
              lexer: new _parser.Lexer(stream.makeSubStream(startPos))
            });
            parser.getObj();
            updateEntries = true;
          } catch (ex) {
            if (ex instanceof _core_utils.ParserEOFException) {
              (0, _util.warn)(`indexObjects -- checking object (${token}): "${ex}".`);
            } else {
              updateEntries = true;
            }
          }
        }

        if (updateEntries) {
          this.entries[num] = {
            offset: position - stream.start,
            gen,
            uncompressed: true
          };
        }

        while (startPos < buffer.length) {
          const endPos = startPos + skipUntil(buffer, startPos, objBytes) + 4;
          contentLength = endPos - position;
          const checkPos = Math.max(endPos - CHECK_CONTENT_LENGTH, startPos);
          const tokenStr = (0, _util.bytesToString)(buffer.subarray(checkPos, endPos));

          if (endobjRegExp.test(tokenStr)) {
            break;
          } else {
            const objToken = nestedObjRegExp.exec(tokenStr);

            if (objToken && objToken[1]) {
              (0, _util.warn)('indexObjects: Found new "obj" inside of another "obj", ' + 'caused by missing "endobj" -- trying to recover.');
              contentLength -= objToken[1].length;
              break;
            }
          }

          startPos = endPos;
        }

        const content = buffer.subarray(position, position + contentLength);
        const xrefTagOffset = skipUntil(content, 0, xrefBytes);

        if (xrefTagOffset < contentLength && content[xrefTagOffset + 5] < 64) {
          xrefStms.push(position - stream.start);
          this.xrefstms[position - stream.start] = 1;
        }

        position += contentLength;
      } else if (token.startsWith("trailer") && (token.length === 7 || /\s/.test(token[7]))) {
        trailers.push(position);
        position += skipUntil(buffer, position, startxrefBytes);
      } else {
        position += token.length + 1;
      }
    }

    for (let i = 0, ii = xrefStms.length; i < ii; ++i) {
      this.startXRefQueue.push(xrefStms[i]);
      this.readXRef(true);
    }

    let trailerDict;

    for (let i = 0, ii = trailers.length; i < ii; ++i) {
      stream.pos = trailers[i];
      const parser = new _parser.Parser({
        lexer: new _parser.Lexer(stream),
        xref: this,
        allowStreams: true,
        recoveryMode: true
      });
      const obj = parser.getObj();

      if (!(0, _primitives.isCmd)(obj, "trailer")) {
        continue;
      }

      const dict = parser.getObj();

      if (!(0, _primitives.isDict)(dict)) {
        continue;
      }

      try {
        const rootDict = dict.get("Root");

        if (!(rootDict instanceof _primitives.Dict)) {
          continue;
        }

        const pagesDict = rootDict.get("Pages");

        if (!(pagesDict instanceof _primitives.Dict)) {
          continue;
        }

        const pagesCount = pagesDict.get("Count");

        if (!Number.isInteger(pagesCount)) {
          continue;
        }
      } catch (ex) {
        continue;
      }

      if (dict.has("ID")) {
        return dict;
      }

      trailerDict = dict;
    }

    if (trailerDict) {
      return trailerDict;
    }

    throw new _util.InvalidPDFException("Invalid PDF structure.");
  }

  readXRef(recoveryMode = false) {
    const stream = this.stream;
    const startXRefParsedCache = new Set();

    try {
      while (this.startXRefQueue.length) {
        const startXRef = this.startXRefQueue[0];

        if (startXRefParsedCache.has(startXRef)) {
          (0, _util.warn)("readXRef - skipping XRef table since it was already parsed.");
          this.startXRefQueue.shift();
          continue;
        }

        startXRefParsedCache.add(startXRef);
        stream.pos = startXRef + stream.start;
        const parser = new _parser.Parser({
          lexer: new _parser.Lexer(stream),
          xref: this,
          allowStreams: true
        });
        let obj = parser.getObj();
        let dict;

        if ((0, _primitives.isCmd)(obj, "xref")) {
          dict = this.processXRefTable(parser);

          if (!this.topDict) {
            this.topDict = dict;
          }

          obj = dict.get("XRefStm");

          if (Number.isInteger(obj)) {
            const pos = obj;

            if (!(pos in this.xrefstms)) {
              this.xrefstms[pos] = 1;
              this.startXRefQueue.push(pos);
            }
          }
        } else if (Number.isInteger(obj)) {
          if (!Number.isInteger(parser.getObj()) || !(0, _primitives.isCmd)(parser.getObj(), "obj") || !(0, _primitives.isStream)(obj = parser.getObj())) {
            throw new _util.FormatError("Invalid XRef stream");
          }

          dict = this.processXRefStream(obj);

          if (!this.topDict) {
            this.topDict = dict;
          }

          if (!dict) {
            throw new _util.FormatError("Failed to read XRef stream");
          }
        } else {
          throw new _util.FormatError("Invalid XRef stream header");
        }

        obj = dict.get("Prev");

        if (Number.isInteger(obj)) {
          this.startXRefQueue.push(obj);
        } else if ((0, _primitives.isRef)(obj)) {
          this.startXRefQueue.push(obj.num);
        }

        this.startXRefQueue.shift();
      }

      return this.topDict;
    } catch (e) {
      if (e instanceof _core_utils.MissingDataException) {
        throw e;
      }

      (0, _util.info)("(while reading XRef): " + e);
    }

    if (recoveryMode) {
      return undefined;
    }

    throw new _core_utils.XRefParseException();
  }

  getEntry(i) {
    const xrefEntry = this.entries[i];

    if (xrefEntry && !xrefEntry.free && xrefEntry.offset) {
      return xrefEntry;
    }

    return null;
  }

  fetchIfRef(obj, suppressEncryption = false) {
    if (obj instanceof _primitives.Ref) {
      return this.fetch(obj, suppressEncryption);
    }

    return obj;
  }

  fetch(ref, suppressEncryption = false) {
    if (!(ref instanceof _primitives.Ref)) {
      throw new Error("ref object is not a reference");
    }

    const num = ref.num;

    const cacheEntry = this._cacheMap.get(num);

    if (cacheEntry !== undefined) {
      if (cacheEntry instanceof _primitives.Dict && !cacheEntry.objId) {
        cacheEntry.objId = ref.toString();
      }

      return cacheEntry;
    }

    let xrefEntry = this.getEntry(num);

    if (xrefEntry === null) {
      this._cacheMap.set(num, xrefEntry);

      return xrefEntry;
    }

    if (xrefEntry.uncompressed) {
      xrefEntry = this.fetchUncompressed(ref, xrefEntry, suppressEncryption);
    } else {
      xrefEntry = this.fetchCompressed(ref, xrefEntry, suppressEncryption);
    }

    if ((0, _primitives.isDict)(xrefEntry)) {
      xrefEntry.objId = ref.toString();
    } else if ((0, _primitives.isStream)(xrefEntry)) {
      xrefEntry.dict.objId = ref.toString();
    }

    return xrefEntry;
  }

  fetchUncompressed(ref, xrefEntry, suppressEncryption = false) {
    const gen = ref.gen;
    let num = ref.num;

    if (xrefEntry.gen !== gen) {
      throw new _core_utils.XRefEntryException(`Inconsistent generation in XRef: ${ref}`);
    }

    const stream = this.stream.makeSubStream(xrefEntry.offset + this.stream.start);
    const parser = new _parser.Parser({
      lexer: new _parser.Lexer(stream),
      xref: this,
      allowStreams: true
    });
    const obj1 = parser.getObj();
    const obj2 = parser.getObj();
    const obj3 = parser.getObj();

    if (obj1 !== num || obj2 !== gen || !(obj3 instanceof _primitives.Cmd)) {
      throw new _core_utils.XRefEntryException(`Bad (uncompressed) XRef entry: ${ref}`);
    }

    if (obj3.cmd !== "obj") {
      if (obj3.cmd.startsWith("obj")) {
        num = parseInt(obj3.cmd.substring(3), 10);

        if (!Number.isNaN(num)) {
          return num;
        }
      }

      throw new _core_utils.XRefEntryException(`Bad (uncompressed) XRef entry: ${ref}`);
    }

    if (this.encrypt && !suppressEncryption) {
      xrefEntry = parser.getObj(this.encrypt.createCipherTransform(num, gen));
    } else {
      xrefEntry = parser.getObj();
    }

    if (!(0, _primitives.isStream)(xrefEntry)) {
      this._cacheMap.set(num, xrefEntry);
    }

    return xrefEntry;
  }

  fetchCompressed(ref, xrefEntry, suppressEncryption = false) {
    const tableOffset = xrefEntry.offset;
    const stream = this.fetch(_primitives.Ref.get(tableOffset, 0));

    if (!(0, _primitives.isStream)(stream)) {
      throw new _util.FormatError("bad ObjStm stream");
    }

    const first = stream.dict.get("First");
    const n = stream.dict.get("N");

    if (!Number.isInteger(first) || !Number.isInteger(n)) {
      throw new _util.FormatError("invalid first and n parameters for ObjStm stream");
    }

    let parser = new _parser.Parser({
      lexer: new _parser.Lexer(stream),
      xref: this,
      allowStreams: true
    });
    const nums = new Array(n);
    const offsets = new Array(n);

    for (let i = 0; i < n; ++i) {
      const num = parser.getObj();

      if (!Number.isInteger(num)) {
        throw new _util.FormatError(`invalid object number in the ObjStm stream: ${num}`);
      }

      const offset = parser.getObj();

      if (!Number.isInteger(offset)) {
        throw new _util.FormatError(`invalid object offset in the ObjStm stream: ${offset}`);
      }

      nums[i] = num;
      offsets[i] = offset;
    }

    const start = (stream.start || 0) + first;
    const entries = new Array(n);

    for (let i = 0; i < n; ++i) {
      const length = i < n - 1 ? offsets[i + 1] - offsets[i] : undefined;

      if (length < 0) {
        throw new _util.FormatError("Invalid offset in the ObjStm stream.");
      }

      parser = new _parser.Parser({
        lexer: new _parser.Lexer(stream.makeSubStream(start + offsets[i], length, stream.dict)),
        xref: this,
        allowStreams: true
      });
      const obj = parser.getObj();
      entries[i] = obj;

      if ((0, _primitives.isStream)(obj)) {
        continue;
      }

      const num = nums[i],
            entry = this.entries[num];

      if (entry && entry.offset === tableOffset && entry.gen === i) {
        this._cacheMap.set(num, obj);
      }
    }

    xrefEntry = entries[xrefEntry.gen];

    if (xrefEntry === undefined) {
      throw new _core_utils.XRefEntryException(`Bad (compressed) XRef entry: ${ref}`);
    }

    return xrefEntry;
  }

  async fetchIfRefAsync(obj, suppressEncryption) {
    if (obj instanceof _primitives.Ref) {
      return this.fetchAsync(obj, suppressEncryption);
    }

    return obj;
  }

  async fetchAsync(ref, suppressEncryption) {
    try {
      return this.fetch(ref, suppressEncryption);
    } catch (ex) {
      if (!(ex instanceof _core_utils.MissingDataException)) {
        throw ex;
      }

      await this.pdfManager.requestRange(ex.begin, ex.end);
      return this.fetchAsync(ref, suppressEncryption);
    }
  }

  getCatalogObj() {
    return this.root;
  }

}

exports.XRef = XRef;