"use strict";

exports.__esModule = true;
exports.default = void 0;
var _ElementWriter = _interopRequireDefault(require("./ElementWriter"));
var _PageSize = require("./PageSize");
var _DocumentContext = _interopRequireDefault(require("./DocumentContext"));
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
/**
 * An extended ElementWriter which can handle:
 * - page-breaks (it adds new pages when there's not enough space left),
 * - repeatable fragments (like table-headers, which are repeated everytime
 *                         a page-break occurs)
 * - transactions (used for unbreakable-blocks when we want to make sure
 *                 whole block will be rendered on the same page)
 */
class PageElementWriter extends _ElementWriter.default {
  /**
   * @param {DocumentContext} context
   */
  constructor(context) {
    super(context);
    this.transactionLevel = 0;
    this.repeatables = [];
  }
  addLine(line, dontUpdateContextPosition, index) {
    return this._fitOnPage(() => super.addLine(line, dontUpdateContextPosition, index));
  }
  addImage(image, index) {
    return this._fitOnPage(() => super.addImage(image, index));
  }
  addCanvas(image, index) {
    return this._fitOnPage(() => super.addCanvas(image, index));
  }
  addSVG(image, index) {
    return this._fitOnPage(() => super.addSVG(image, index));
  }
  addQr(qr, index) {
    return this._fitOnPage(() => super.addQr(qr, index));
  }
  addAttachment(attachment, index) {
    return this._fitOnPage(() => super.addAttachment(attachment, index));
  }
  addVector(vector, ignoreContextX, ignoreContextY, index, forcePage) {
    return super.addVector(vector, ignoreContextX, ignoreContextY, index, forcePage);
  }
  beginClip(width, height) {
    return super.beginClip(width, height);
  }
  endClip() {
    return super.endClip();
  }
  beginVerticalAlignment(verticalAlignment) {
    return super.beginVerticalAlignment(verticalAlignment);
  }
  endVerticalAlignment(verticalAlignment) {
    return super.endVerticalAlignment(verticalAlignment);
  }
  addFragment(fragment, useBlockXOffset, useBlockYOffset, dontUpdateContextPosition) {
    return this._fitOnPage(() => super.addFragment(fragment, useBlockXOffset, useBlockYOffset, dontUpdateContextPosition));
  }
  moveToNextPage(pageOrientation) {
    let nextPage = this.context().moveToNextPage(pageOrientation);

    // moveToNextPage is called multiple times for table, because is called for each column
    // and repeatables are inserted only in the first time. If columns are used, is needed
    // call for table in first column and then for table in the second column (is other repeatables).
    this.repeatables.forEach(function (rep) {
      if (rep.insertedOnPages[this.context().page] === undefined) {
        rep.insertedOnPages[this.context().page] = true;
        this.addFragment(rep, true);
      } else {
        this.context().moveDown(rep.height);
      }
    }, this);
    this.emit('pageChanged', {
      prevPage: nextPage.prevPage,
      prevY: nextPage.prevY,
      y: this.context().y
    });
  }
  addPage(pageSize, pageOrientation, pageMargin, customProperties = {}) {
    let prevPage = this.page;
    let prevY = this.y;
    this.context().addPage((0, _PageSize.normalizePageSize)(pageSize, pageOrientation), (0, _PageSize.normalizePageMargin)(pageMargin), customProperties);
    this.emit('pageChanged', {
      prevPage: prevPage,
      prevY: prevY,
      y: this.context().y
    });
  }
  beginUnbreakableBlock(width, height) {
    if (this.transactionLevel++ === 0) {
      this.originalX = this.context().x;
      this.pushContext(width, height);
    }
  }
  commitUnbreakableBlock(forcedX, forcedY) {
    if (--this.transactionLevel === 0) {
      let unbreakableContext = this.context();
      this.popContext();
      let nbPages = unbreakableContext.pages.length;
      if (nbPages > 0) {
        // no support for multi-page unbreakableBlocks
        let fragment = unbreakableContext.pages[0];
        fragment.xOffset = forcedX;
        fragment.yOffset = forcedY;

        //TODO: vectors can influence height in some situations
        if (nbPages > 1) {
          // on out-of-context blocs (headers, footers, background) height should be the whole DocumentContext height
          if (forcedX !== undefined || forcedY !== undefined) {
            fragment.height = unbreakableContext.getCurrentPage().pageSize.height - unbreakableContext.pageMargins.top - unbreakableContext.pageMargins.bottom;
          } else {
            fragment.height = this.context().getCurrentPage().pageSize.height - this.context().pageMargins.top - this.context().pageMargins.bottom;
            for (let i = 0, l = this.repeatables.length; i < l; i++) {
              fragment.height -= this.repeatables[i].height;
            }
          }
        } else {
          fragment.height = unbreakableContext.y;
        }
        if (forcedX !== undefined || forcedY !== undefined) {
          super.addFragment(fragment, true, true, true);
        } else {
          this.addFragment(fragment);
        }
      }
    }
  }
  currentBlockToRepeatable() {
    let unbreakableContext = this.context();
    let rep = {
      items: []
    };
    unbreakableContext.pages[0].items.forEach(item => {
      rep.items.push(item);
    });
    rep.xOffset = this.originalX;

    //TODO: vectors can influence height in some situations
    rep.height = unbreakableContext.y;
    rep.insertedOnPages = [];
    return rep;
  }
  pushToRepeatables(rep) {
    this.repeatables.push(rep);
  }
  popFromRepeatables() {
    this.repeatables.pop();
  }

  /**
   * Move to the next column in a column group (snaking columns).
   * Handles repeatables and emits columnChanged event.
   */
  moveToNextColumn() {
    let nextColumn = this.context().moveToNextColumn();

    // Handle repeatables (like table headers) for the new column
    this.repeatables.forEach(function (rep) {
      // In snaking columns, we WANT headers to repeat.
      // However, in Standard Page Breaks, headers are drawn using useBlockXOffset=true (original absolute X).
      // This works for page breaks because margins are consistent.
      // In Snaking Columns, the X position changes for each column.
      // If we use true, the header is drawn at the *original* X position (Col 1), overlapping/invisible.
      // We MUST use false to force drawing relative to the CURRENT context X (new column start).
      this.addFragment(rep, false);
    }, this);
    this.emit('columnChanged', {
      prevY: nextColumn.prevY,
      y: this.context().y
    });
  }

  /**
   * Check if currently in a column group that can move to next column.
   * Only returns true if snakingColumns is enabled for the column group.
   * @returns {boolean}
   */
  canMoveToNextColumn() {
    let ctx = this.context();
    let snakingSnapshot = ctx.getSnakingSnapshot();
    if (snakingSnapshot) {
      // Check if we're inside a nested (non-snaking) column group.
      // If so, don't allow a snaking column move — it would corrupt
      // the inner row's layout (e.g. product name in col 1, price in col 2).
      // The inner row should complete via normal page break instead.
      for (let i = ctx.snapshots.length - 1; i >= 0; i--) {
        let snap = ctx.snapshots[i];
        if (snap.snakingColumns) {
          break; // Reached the snaking snapshot, no inner groups found
        }
        if (!snap.overflowed) {
          return false; // Found a non-snaking, non-overflowed inner group
        }
      }
      let overflowCount = 0;
      for (let i = ctx.snapshots.length - 1; i >= 0; i--) {
        if (ctx.snapshots[i].overflowed) {
          overflowCount++;
        } else {
          break;
        }
      }
      if (snakingSnapshot.columnWidths && overflowCount >= snakingSnapshot.columnWidths.length - 1) {
        return false;
      }
      let currentColumnWidth = ctx.availableWidth || ctx.lastColumnWidth || 0;
      let nextColumnWidth = snakingSnapshot.columnWidths ? snakingSnapshot.columnWidths[overflowCount + 1] : currentColumnWidth;
      let nextX = ctx.x + currentColumnWidth + (snakingSnapshot.gap || 0);
      let page = ctx.getCurrentPage();
      let pageWidth = page.pageSize.width;
      let rightMargin = page.pageMargins ? page.pageMargins.right : 0;
      let parentRightMargin = ctx.marginXTopParent ? ctx.marginXTopParent[1] : 0;
      let rightBoundary = pageWidth - rightMargin - parentRightMargin;
      return nextX + nextColumnWidth <= rightBoundary + 1;
    }
    return false;
  }
  _fitOnPage(addFct) {
    let position = addFct();
    if (!position) {
      if (this.canMoveToNextColumn()) {
        this.moveToNextColumn();
        position = addFct();
      }
      if (!position) {
        let ctx = this.context();
        let snakingSnapshot = ctx.getSnakingSnapshot();
        if (snakingSnapshot) {
          if (ctx.isInNestedNonSnakingGroup()) {
            // Inside a table cell within snaking columns — use standard page break.
            // Don't reset snaking state; the table handles its own breaks.
            // Column breaks happen between rows in processTable instead.
            this.moveToNextPage();
          } else {
            this.moveToNextPage();

            // Save lastColumnWidth before reset — if we're inside a nested
            // column group (e.g. product/price row), the reset would overwrite
            // it with the snaking column width, corrupting inner column layout.
            let savedLastColumnWidth = ctx.lastColumnWidth;
            ctx.resetSnakingColumnsForNewPage();
            ctx.lastColumnWidth = savedLastColumnWidth;
          }
          position = addFct();
        } else {
          while (ctx.snapshots.length > 0 && ctx.snapshots[ctx.snapshots.length - 1].overflowed) {
            let popped = ctx.snapshots.pop();
            let prevSnapshot = ctx.snapshots[ctx.snapshots.length - 1];
            if (prevSnapshot) {
              ctx.x = prevSnapshot.x;
              ctx.y = prevSnapshot.y;
              ctx.availableHeight = prevSnapshot.availableHeight;
              ctx.availableWidth = popped.availableWidth;
              ctx.lastColumnWidth = prevSnapshot.lastColumnWidth;
            }
          }
          this.moveToNextPage();
          position = addFct();
        }
      }
    }
    return position;
  }
}
var _default = exports.default = PageElementWriter;