text: /* Copyright 2017 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.
   */
  
  import { assert, ImageKind, OPS, shadow, warn } from "../shared/util.js";
  
  function addState(parentState, pattern, checkFn, iterateFn, processFn) {
    let state = parentState;
    for (let i = 0, ii = pattern.length - 1; i < ii; i++) {
      const item = pattern[i];
      state = state[item] || (state[item] = []);
    }
    state[pattern[pattern.length - 1]] = {
      checkFn,
      iterateFn,
      processFn,
    };
  }
  
  function handlePaintSolidColorImageMask(iFirstSave, count, fnArray, argsArray) {
    // Handles special case of mainly LaTeX documents which use image masks to
    // draw lines with the current fill style.
    // 'count' groups of (save, transform, paintImageMaskXObject, restore)+
    // have been found at iFirstSave.
    const iFirstPIMXO = iFirstSave + 2;
    let i;
    for (i = 0; i < count; i++) {
      const arg = argsArray[iFirstPIMXO + 4 * i];
      const imageMask = arg.length === 1 && arg[0];
      if (
        imageMask &&
        imageMask.width === 1 &&
        imageMask.height === 1 &&
        (!imageMask.data.length ||
          (imageMask.data.length === 1 && imageMask.data[0] === 0))
      ) {
        fnArray[iFirstPIMXO + 4 * i] = OPS.paintSolidColorImageMask;
        continue;
      }
      break;
    }
    return count - i;
  }
  
  const InitialState = [];
  
  // This replaces (save, transform, paintInlineImageXObject, restore)+
  // sequences with one |paintInlineImageXObjectGroup| operation.
  addState(
    InitialState,
    [OPS.save, OPS.transform, OPS.paintInlineImageXObject, OPS.restore],
    null,
    function iterateInlineImageGroup(context, i) {
      const fnArray = context.fnArray;
      const iFirstSave = context.iCurr - 3;
      const pos = (i - iFirstSave) % 4;
      switch (pos) {
        case 0:
          return fnArray[i] === OPS.save;
        case 1:
          return fnArray[i] === OPS.transform;
        case 2:
          return fnArray[i] === OPS.paintInlineImageXObject;
        case 3:
          return fnArray[i] === OPS.restore;
      }
      throw new Error(`iterateInlineImageGroup - invalid pos: ${pos}`);
    },
    function foundInlineImageGroup(context, i) {
      const MIN_IMAGES_IN_INLINE_IMAGES_BLOCK = 10;
      const MAX_IMAGES_IN_INLINE_IMAGES_BLOCK = 200;
      const MAX_WIDTH = 1000;
      const IMAGE_PADDING = 1;
  
      const fnArray = context.fnArray,
        argsArray = context.argsArray;
      const curr = context.iCurr;
      const iFirstSave = curr - 3;
      const iFirstTransform = curr - 2;
      const iFirstPIIXO = curr - 1;
  
      const count = Math.min(
        Math.floor((i - iFirstSave) / 4),
        MAX_IMAGES_IN_INLINE_IMAGES_BLOCK
      );
      if (count < MIN_IMAGES_IN_INLINE_IMAGES_BLOCK) {
        return i - ((i - iFirstSave) % 4);
      }
  
      // assuming that heights of those image is too small (~1 pixel)
      // packing as much as possible by lines
      let maxX = 0;
      const map = [];
      let maxLineHeight = 0;
      let currentX = IMAGE_PADDING,
        currentY = IMAGE_PADDING;
      for (let q = 0; q < count; q++) {
        const transform = argsArray[iFirstTransform + (q << 2)];
        const img = argsArray[iFirstPIIXO + (q << 2)][0];
        if (currentX + img.width > MAX_WIDTH) {
          // starting new line
          maxX = Math.max(maxX, currentX);
          currentY += maxLineHeight + 2 * IMAGE_PADDING;
          currentX = 0;
          maxLineHeight = 0;
        }
        map.push({
          transform,
          x: currentX,
          y: currentY,
          w: img.width,
          h: img.height,
        });
        currentX += img.width + 2 * IMAGE_PADDING;
        maxLineHeight = Math.max(maxLineHeight, img.height);
      }
      const imgWidth = Math.max(maxX, currentX) + IMAGE_PADDING;
      const imgHeight = currentY + maxLineHeight + IMAGE_PADDING;
      const imgData = new Uint8ClampedArray(imgWidth * imgHeight * 4);
      const imgRowSize = imgWidth << 2;
      for (let q = 0; q < count; q++) {
        const data = argsArray[iFirstPIIXO + (q << 2)][0].data;
        // Copy image by lines and extends pixels into padding.
        const rowSize = map[q].w << 2;
        let dataOffset = 0;
        let offset = (map[q].x + map[q].y * imgWidth) << 2;
        imgData.set(data.subarray(0, rowSize), offset - imgRowSize);
        for (let k = 0, kk = map[q].h; k < kk; k++) {
          imgData.set(data.subarray(dataOffset, dataOffset + rowSize), offset);
          dataOffset += rowSize;
          offset += imgRowSize;
        }
        imgData.set(data.subarray(dataOffset - rowSize, dataOffset), offset);
        while (offset >= 0) {
          data[offset - 4] = data[offset];
          data[offset - 3] = data[offset + 1];
          data[offset - 2] = data[offset + 2];
          data[offset - 1] = data[offset + 3];
          data[offset + rowSize] = data[offset + rowSize - 4];
          data[offset + rowSize + 1] = data[offset + rowSize - 3];
          data[offset + rowSize + 2] = data[offset + rowSize - 2];
          data[offset + rowSize + 3] = data[offset + rowSize - 1];
          offset -= imgRowSize;
        }
      }
  
      // Replace queue items.
      fnArray.splice(iFirstSave, count * 4, OPS.paintInlineImageXObjectGroup);
      argsArray.splice(iFirstSave, count * 4, [
        {
          width: imgWidth,
          height: imgHeight,
          kind: ImageKind.RGBA_32BPP,
          data: imgData,
        },
        map,
      ]);
  
      return iFirstSave + 1;
    }
  );
  
  // This replaces (save, transform, paintImageMaskXObject, restore)+
  // sequences with one |paintImageMaskXObjectGroup| or one
  // |paintImageMaskXObjectRepeat| operation.
  addState(
    InitialState,
    [OPS.save, OPS.transform, OPS.paintImageMaskXObject, OPS.restore],
    null,
    function iterateImageMaskGroup(context, i) {
      const fnArray = context.fnArray;
      const iFirstSave = context.iCurr - 3;
      const pos = (i - iFirstSave) % 4;
      switch (pos) {
        case 0:
          return fnArray[i] === OPS.save;
        case 1:
          return fnArray[i] === OPS.transform;
        case 2:
          return fnArray[i] === OPS.paintImageMaskXObject;
        case 3:
          return fnArray[i] === OPS.restore;
      }
      throw new Error(`iterateImageMaskGroup - invalid pos: ${pos}`);
    },
    function foundImageMaskGroup(context, i) {
      const MIN_IMAGES_IN_MASKS_BLOCK = 10;
      const MAX_IMAGES_IN_MASKS_BLOCK = 100;
      const MAX_SAME_IMAGES_IN_MASKS_BLOCK = 1000;
  
      const fnArray = context.fnArray,
        argsArray = context.argsArray;
      const curr = context.iCurr;
      const iFirstSave = curr - 3;
      const iFirstTransform = curr - 2;
      const iFirstPIMXO = curr - 1;
  
      // At this point, i is the index of the first op past the last valid
      // quartet.
      let count = Math.floor((i - iFirstSave) / 4);
      count = handlePaintSolidColorImageMask(
        iFirstSave,
        count,
        fnArray,
        argsArray
      );
      if (count < MIN_IMAGES_IN_MASKS_BLOCK) {
        return i - ((i - iFirstSave) % 4);
      }
  
      let isSameImage = false;
      let iTransform, transformArgs;
      const firstPIMXOArg0 = argsArray[iFirstPIMXO][0];
      const firstTransformArg0 = argsArray[iFirstTransform][0],
        firstTransformArg1 = argsArray[iFirstTransform][1],
        firstTransformArg2 = argsArray[iFirstTransform][2],
        firstTransformArg3 = argsArray[iFirstTransform][3];
  
      if (firstTransformArg1 === firstTransformArg2) {
        isSameImage = true;
        iTransform = iFirstTransform + 4;
        let iPIMXO = iFirstPIMXO + 4;
        for (let q = 1; q < count; q++, iTransform += 4, iPIMXO += 4) {
          transformArgs = argsArray[iTransform];
          if (
            argsArray[iPIMXO][0] !== firstPIMXOArg0 ||
            transformArgs[0] !== firstTransformArg0 ||
            transformArgs[1] !== firstTransformArg1 ||
            transformArgs[2] !== firstTransformArg2 ||
            transformArgs[3] !== firstTransformArg3
          ) {
            if (q < MIN_IMAGES_IN_MASKS_BLOCK) {
              isSameImage = false;
            } else {
              count = q;
            }
            break; // different image or transform
          }
        }
      }
  
      if (isSameImage) {
        count = Math.min(count, MAX_SAME_IMAGES_IN_MASKS_BLOCK);
        const positions = new Float32Array(count * 2);
        iTransform = iFirstTransform;
        for (let q = 0; q < count; q++, iTransform += 4) {
          transformArgs = argsArray[iTransform];
          positions[q << 1] = transformArgs[4];
          positions[(q << 1) + 1] = transformArgs[5];
        }
  
        // Replace queue items.
        fnArray.splice(iFirstSave, count * 4, OPS.paintImageMaskXObjectRepeat);
        argsArray.splice(iFirstSave, count * 4, [
          firstPIMXOArg0,
          firstTransformArg0,
          firstTransformArg1,
          firstTransformArg2,
          firstTransformArg3,
          positions,
        ]);
      } else {
        count = Math.min(count, MAX_IMAGES_IN_MASKS_BLOCK);
        const images = [];
        for (let q = 0; q < count; q++) {
          transformArgs = argsArray[iFirstTransform + (q << 2)];
          const maskParams = argsArray[iFirstPIMXO + (q << 2)][0];
          images.push({
            data: maskParams.data,
            width: maskParams.width,
            height: maskParams.height,
            transform: transformArgs,
          });
        }
  
        // Replace queue items.
        fnArray.splice(iFirstSave, count * 4, OPS.paintImageMaskXObjectGroup);
        argsArray.splice(iFirstSave, count * 4, [images]);
      }
  
      return iFirstSave + 1;
    }
  );
  
  // This replaces (save, transform, paintImageXObject, restore)+ sequences
  // with one paintImageXObjectRepeat operation, if the |transform| and
  // |paintImageXObjectRepeat| ops are appropriate.
  addState(
    InitialState,
    [OPS.save, OPS.transform, OPS.paintImageXObject, OPS.restore],
    function (context) {
      const argsArray = context.argsArray;
      const iFirstTransform = context.iCurr - 2;
      return (
        argsArray[iFirstTransform][1] === 0 && argsArray[iFirstTransform][2] === 0
      );
    },
    function iterateImageGroup(context, i) {
      const fnArray = context.fnArray,
        argsArray = context.argsArray;
      const iFirstSave = context.iCurr - 3;
      const pos = (i - iFirstSave) % 4;
      switch (pos) {
        case 0:
          return fnArray[i] === OPS.save;
        case 1:
          if (fnArray[i] !== OPS.transform) {
            return false;
          }
          const iFirstTransform = context.iCurr - 2;
          const firstTransformArg0 = argsArray[iFirstTransform][0];
          const firstTransformArg3 = argsArray[iFirstTransform][3];
          if (
            argsArray[i][0] !== firstTransformArg0 ||
            argsArray[i][1] !== 0 ||
            argsArray[i][2] !== 0 ||
            argsArray[i][3] !== firstTransformArg3
          ) {
            return false; // transforms don't match
          }
          return true;
        case 2:
          if (fnArray[i] !== OPS.paintImageXObject) {
            return false;
          }
          const iFirstPIXO = context.iCurr - 1;
          const firstPIXOArg0 = argsArray[iFirstPIXO][0];
          if (argsArray[i][0] !== firstPIXOArg0) {
            return false; // images don't match
          }
          return true;
        case 3:
          return fnArray[i] === OPS.restore;
      }
      throw new Error(`iterateImageGroup - invalid pos: ${pos}`);
    },
    function (context, i) {
      const MIN_IMAGES_IN_BLOCK = 3;
      const MAX_IMAGES_IN_BLOCK = 1000;
  
      const fnArray = context.fnArray,
        argsArray = context.argsArray;
      const curr = context.iCurr;
      const iFirstSave = curr - 3;
      const iFirstTransform = curr - 2;
      const iFirstPIXO = curr - 1;
      const firstPIXOArg0 = argsArray[iFirstPIXO][0];
      const firstTransformArg0 = argsArray[iFirstTransform][0];
      const firstTransformArg3 = argsArray[iFirstTransform][3];
  
      // At this point, i is the index of the first op past the last valid
      // quartet.
      const count = Math.min(
        Math.floor((i - iFirstSave) / 4),
        MAX_IMAGES_IN_BLOCK
      );
      if (count < MIN_IMAGES_IN_BLOCK) {
        return i - ((i - iFirstSave) % 4);
      }
  
      // Extract the (x,y) positions from all of the matching transforms.
      const positions = new Float32Array(count * 2);
      let iTransform = iFirstTransform;
      for (let q = 0; q < count; q++, iTransform += 4) {
        const transformArgs = argsArray[iTransform];
        positions[q << 1] = transformArgs[4];
        positions[(q << 1) + 1] = transformArgs[5];
      }
  
      // Replace queue items.
      const args = [
        firstPIXOArg0,
        firstTransformArg0,
        firstTransformArg3,
        positions,
      ];
      fnArray.splice(iFirstSave, count * 4, OPS.paintImageXObjectRepeat);
      argsArray.splice(iFirstSave, count * 4, args);
  
      return iFirstSave + 1;
    }
  );
  
  // This replaces (beginText, setFont, setTextMatrix, showText, endText)+
  // sequences with (beginText, setFont, (setTextMatrix, showText)+, endText)+
  // sequences, if the font for each one is the same.
  addState(
    InitialState,
    [OPS.beginText, OPS.setFont, OPS.setTextMatrix, OPS.showText, OPS.endText],
    null,
    function iterateShowTextGroup(context, i) {
      const fnArray = context.fnArray,
        argsArray = context.argsArray;
      const iFirstSave = context.iCurr - 4;
      const pos = (i - iFirstSave) % 5;
      switch (pos) {
        case 0:
          return fnArray[i] === OPS.beginText;
        case 1:
          return fnArray[i] === OPS.setFont;
        case 2:
          return fnArray[i] === OPS.setTextMatrix;
        case 3:
          if (fnArray[i] !== OPS.showText) {
            return false;
          }
          const iFirstSetFont = context.iCurr - 3;
          const firstSetFontArg0 = argsArray[iFirstSetFont][0];
          const firstSetFontArg1 = argsArray[iFirstSetFont][1];
          if (
            argsArray[i][0] !== firstSetFontArg0 ||
            argsArray[i][1] !== firstSetFontArg1
          ) {
            return false; // fonts don't match
          }
          return true;
        case 4:
          return fnArray[i] === OPS.endText;
      }
      throw new Error(`iterateShowTextGroup - invalid pos: ${pos}`);
    },
    function (context, i) {
      const MIN_CHARS_IN_BLOCK = 3;
      const MAX_CHARS_IN_BLOCK = 1000;
  
      const fnArray = context.fnArray,
        argsArray = context.argsArray;
      const curr = context.iCurr;
      const iFirstBeginText = curr - 4;
      const iFirstSetFont = curr - 3;
      const iFirstSetTextMatrix = curr - 2;
      const iFirstShowText = curr - 1;
      const iFirstEndText = curr;
      const firstSetFontArg0 = argsArray[iFirstSetFont][0];
      const firstSetFontArg1 = argsArray[iFirstSetFont][1];
  
      // At this point, i is the index of the first op past the last valid
      // quintet.
      let count = Math.min(
        Math.floor((i - iFirstBeginText) / 5),
        MAX_CHARS_IN_BLOCK
      );
      if (count < MIN_CHARS_IN_BLOCK) {
        return i - ((i - iFirstBeginText) % 5);
      }
  
      // If the preceding quintet is (<something>, setFont, setTextMatrix,
      // showText, endText), include that as well. (E.g. <something> might be
      // |dependency|.)
      let iFirst = iFirstBeginText;
      if (
        iFirstBeginText >= 4 &&
        fnArray[iFirstBeginText - 4] === fnArray[iFirstSetFont] &&
        fnArray[iFirstBeginText - 3] === fnArray[iFirstSetTextMatrix] &&
        fnArray[iFirstBeginText - 2] === fnArray[iFirstShowText] &&
        fnArray[iFirstBeginText - 1] === fnArray[iFirstEndText] &&
        argsArray[iFirstBeginText - 4][0] === firstSetFontArg0 &&
        argsArray[iFirstBeginText - 4][1] === firstSetFontArg1
      ) {
        count++;
        iFirst -= 5;
      }
  
      // Remove (endText, beginText, setFont) trios.
      let iEndText = iFirst + 4;
      for (let q = 1; q < count; q++) {
        fnArray.splice(iEndText, 3);
        argsArray.splice(iEndText, 3);
        iEndText += 2;
      }
  
      return iEndText + 1;
    }
  );
  
  class NullOptimizer {
    constructor(queue) {
      this.queue = queue;
    }
  
    _optimize() {}
  
    push(fn, args) {
      this.queue.fnArray.push(fn);
      this.queue.argsArray.push(args);
      this._optimize();
    }
  
    flush() {}
  
    reset() {}
  }
  
  class QueueOptimizer extends NullOptimizer {
    constructor(queue) {
      super(queue);
      this.state = null;
      this.context = {
        iCurr: 0,
        fnArray: queue.fnArray,
        argsArray: queue.argsArray,
      };
      this.match = null;
      this.lastProcessed = 0;
    }
  
    _optimize() {
      // Process new fnArray item(s) chunk.
      const fnArray = this.queue.fnArray;
      let i = this.lastProcessed,
        ii = fnArray.length;
      let state = this.state;
      let match = this.match;
      if (!state && !match && i + 1 === ii && !InitialState[fnArray[i]]) {
        // Micro-optimization for the common case: last item is not
        // optimizable, just skipping it.
        this.lastProcessed = ii;
        return;
      }
  
      const context = this.context;
      while (i < ii) {
        if (match) {
          // Already find a block of potentially optimizable items, iterating...
          const iterate = (0, match.iterateFn)(context, i);
          if (iterate) {
            i++;
            continue;
          }
          // Found last items for the block, processing...
          i = (0, match.processFn)(context, i + 1);
          ii = fnArray.length;
          match = null;
          state = null;
          if (i >= ii) {
            break;
          }
        }
        // Find the potentially optimizable items.
        state = (state || InitialState)[fnArray[i]];
        if (!state || Array.isArray(state)) {
          i++;
          continue;
        }
        // Found a start of the block based on addState rules.
        context.iCurr = i;
        i++;
        if (state.checkFn && !(0, state.checkFn)(context)) {
          // Check failed, continue search...
          state = null;
          continue;
        }
        match = state;
        state = null;
      }
      this.state = state;
      this.match = match;
      this.lastProcessed = i;
    }
  
    flush() {
      while (this.match) {
        const length = this.queue.fnArray.length;
        this.lastProcessed = (0, this.match.processFn)(this.context, length);
        this.match = null;
        this.state = null;
        // Repeat optimization until all chunks are exhausted.
        this._optimize();
      }
    }
  
    reset() {
      this.state = null;
      this.match = null;
      this.lastProcessed = 0;
    }
  }
  
  class OperatorList {
    static get CHUNK_SIZE() {
      return shadow(this, "CHUNK_SIZE", 1000);
    }
  
    // Close to chunk size.
    static get CHUNK_SIZE_ABOUT() {
      return shadow(this, "CHUNK_SIZE_ABOUT", this.CHUNK_SIZE - 5);
    }
  
    constructor(intent, streamSink) {
      this._streamSink = streamSink;
      this.fnArray = [];
      this.argsArray = [];
      if (streamSink && !(intent && intent.startsWith("oplist-"))) {
        this.optimizer = new QueueOptimizer(this);
      } else {
        this.optimizer = new NullOptimizer(this);
      }
      this.dependencies = new Set();
      this._totalLength = 0;
      this.weight = 0;
      this._resolved = streamSink ? null : Promise.resolve();
    }
  
    get length() {
      return this.argsArray.length;
    }
  
    get ready() {
      return this._resolved || this._streamSink.ready;
    }
  
    /**
     * @type {number} The total length of the entire operator list, since
     *                `this.length === 0` after flushing.
     */
    get totalLength() {
      return this._totalLength + this.length;
    }
  
    addOp(fn, args) {
      this.optimizer.push(fn, args);
      this.weight++;
      if (this._streamSink) {
        if (this.weight >= OperatorList.CHUNK_SIZE) {
          this.flush();
        } else if (
          this.weight >= OperatorList.CHUNK_SIZE_ABOUT &&
          (fn === OPS.restore || fn === OPS.endText)
        ) {
          // Heuristic to flush on boundary of restore or endText.
          this.flush();
        }
      }
    }
  
    addDependency(dependency) {
      if (this.dependencies.has(dependency)) {
        return;
      }
      this.dependencies.add(dependency);
      this.addOp(OPS.dependency, [dependency]);
    }
  
    addDependencies(dependencies) {
      for (const dependency of dependencies) {
        this.addDependency(dependency);
      }
    }
  
    addOpList(opList) {
      if (!(opList instanceof OperatorList)) {
        warn('addOpList - ignoring invalid "opList" parameter.');
        return;
      }
      for (const dependency of opList.dependencies) {
        this.dependencies.add(dependency);
      }
      for (let i = 0, ii = opList.length; i < ii; i++) {
        this.addOp(opList.fnArray[i], opList.argsArray[i]);
      }
    }
  
    getIR() {
      return {
        fnArray: this.fnArray,
        argsArray: this.argsArray,
        length: this.length,
      };
    }
  
    get _transfers() {
      const transfers = [];
      const { fnArray, argsArray, length } = this;
      for (let i = 0; i < length; i++) {
        switch (fnArray[i]) {
          case OPS.paintInlineImageXObject:
          case OPS.paintInlineImageXObjectGroup:
          case OPS.paintImageMaskXObject:
            const arg = argsArray[i][0]; // First parameter in imgData.
  
            if (
              typeof PDFJSDev === "undefined" ||
              PDFJSDev.test("!PRODUCTION || TESTING")
            ) {
              assert(
                arg.data instanceof Uint8ClampedArray,
                'OperatorList._transfers: Unsupported "arg.data" type.'
              );
            }
            if (!arg.cached) {
              transfers.push(arg.data.buffer);
            }
            break;
        }
      }
      return transfers;
    }
  
    flush(lastChunk = false) {
      this.optimizer.flush();
      const length = this.length;
      this._totalLength += length;
  
      this._streamSink.enqueue(
        {
          fnArray: this.fnArray,
          argsArray: this.argsArray,
          lastChunk,
          length,
        },
        1,
        this._transfers
      );
  
      this.dependencies.clear();
      this.fnArray.length = 0;
      this.argsArray.length = 0;
      this.weight = 0;
      this.optimizer.reset();
    }
  }
  
  export { OperatorList };
  