/**
 * ColumnRenderInputModel
 *
 * Render-time representation of a column or group node used by header/body renderers.
 * Built from a ColumnStructureModel tree, it carries computed layout metadata required
 * for rendering and pinned/sticky calculations (colSpan, rowSpan, pinned boundaries, etc).
 *
 * Instances form a forest (root-level nodes in an array) that mirrors the structure model
 * but contains additional render-specific properties and parent/child links for renderers.
 */
export class ColumnRenderInputModel {
  /**
   * Create a new render input node.
   *
   * @param {string} key - Unique key (matches ColumnStructureModel.key).
   * @param {string} label - Optional display label (from structure model).
   * @param {VegaTablePinnedType} pinned - Pinned type for this node ('left' | 'right' | null).
   * @param {Nullable<number>} colSpan - Number of visual columns spanned by this node's subtree.
   * @param {Nullable<number>} rowSpan - Number of header rows this node should span.
   * @param {boolean} isViewStart - True when this node is the start of a visible view range.
   * @param {boolean} isViewEnd - True when this node is the end of a visible view range.
   * @param {boolean} isLeftEndPinned - True when node is last left-pinned sibling.
   * @param {boolean} isRightStartPinned - True when node is first right-pinned sibling.
   * @param {boolean} isNoPinnedEnd - True when node is last non-pinned sibling.
   * @param {boolean} headHasGroupColumn - Header contains group columns (affects rowSpan).
   * @param {boolean} headHasPinnedColumn - Header contains pinned columns.
   * @param {number} headMaxDepth - Maximum header depth (number of header rows).
   * @param {number} level - Level/index of this node within header rows (0-based).
   * @param {number} columnIndex - Sequential leaf index (used for ordering).
   * @param {Nullable<ColumnRenderInputModel>} parent - Parent render input node (null for root-level).
   * @param {ColumnRenderInputModel[]} children - Child render input nodes.
   */
  constructor(key, label, pinned, colSpan, rowSpan, isViewStart, isViewEnd, isLeftEndPinned, isRightStartPinned, isNoPinnedEnd, headHasGroupColumn, headHasPinnedColumn, headMaxDepth, level, columnIndex, parent, children = []) {
    this.key = key;
    this.label = label;
    this.pinned = pinned;
    this.colSpan = colSpan;
    this.rowSpan = rowSpan;
    this.isViewStart = isViewStart;
    this.isViewEnd = isViewEnd;
    this.isLeftEndPinned = isLeftEndPinned;
    this.isRightStartPinned = isRightStartPinned;
    this.isNoPinnedEnd = isNoPinnedEnd;
    this.headHasGroupColumn = headHasGroupColumn;
    this.headHasPinnedColumn = headHasPinnedColumn;
    this.headMaxDepth = headMaxDepth;
    this.level = level;
    this.columnIndex = columnIndex;
    this.parent = parent;
    this.children = children;
    this.iteratorCacheMap = new Map();
    this.leaves = [];
  }
  /**
   * Build a forest of ColumnRenderInputModel nodes from a ColumnStructureModel root.
   *
   * @param {ColumnStructureModel} rootModel - Root ColumnStructureModel that supplies tree and level data.
   * @returns {ColumnRenderInputModel[]} Array of root-level ColumnRenderInputModel nodes (the render forest).
   */
  static from(rootModel) {
    const renderInputs = [];
    const parentStack = [];
    const maxLevel = rootModel.getMaxDepth();
    const hasGroupColumn = rootModel.hasGroupColumn();
    const hasPinnedColumn = rootModel.hasLeftPinnedColumn() || rootModel.hasRightPinnedColumn();
    let columnIndex = 0;
    // Iterate structure model (DFS by default) and construct render nodes.
    rootModel.iterator().forEach((model) => {
      const parent = model.level > 0 ? parentStack[model.level - 1] : null;
      const isParentStart = hasGroupColumn && this.isParentStart(model);
      const isParentEnd = hasGroupColumn && this.isParentEnd(model);
      const renderInput = new ColumnRenderInputModel(model.key, model.label, parent ? parent.pinned : model.pinned, hasGroupColumn ? this.getColSpan(model) : null, hasGroupColumn ? (model.isLeaf() ? maxLevel - model.level : 1) : null, parent ? parent.isViewStart && isParentStart : isParentStart, parent ? parent.isViewEnd && isParentEnd : isParentEnd, parent
        ? parent.isLeftEndPinned && isParentEnd
        : hasPinnedColumn && this.isLeftEndPinned(model), parent
        ? parent.isRightStartPinned && isParentStart
        : hasPinnedColumn && this.isRightStartPinned(model), parent ? parent.isNoPinnedEnd && isParentEnd : hasPinnedColumn && this.isNoPinnedEnd(model), hasGroupColumn, hasPinnedColumn, maxLevel, model.level, columnIndex, parent);
      // Attach to parent if present, otherwise add to root-level array.
      parent ? parent.children.push(renderInput) : renderInputs.push(renderInput);
      // Maintain parent stack keyed by level to allow next nodes to find their parent.
      parentStack[model.level] = renderInput;
      parentStack.length = model.level + 1;
      // Increment leaf index when encountering a leaf structure node.
      if (model.isLeaf())
        columnIndex++;
    });
    return renderInputs;
  }
  /**
   * True when the provided structure model is the last left-pinned sibling in its parent's children.
   * Used to mark boundary nodes for left-pinned groups.
   *
   * @param {ColumnStructureModel} model - Structure model to check.
   * @returns {boolean} True when model is the last left-pinned sibling.
   */
  static isLeftEndPinned(model) {
    const siblings = model.parent
      .getOrderedChildren()
      .filter((child) => child.pinned === 'left');
    return siblings[siblings.length - 1] === model;
  }
  /**
   * True when the provided structure model is the first right-pinned sibling in its parent's children.
   * Used to mark boundary nodes for right-pinned groups.
   *
   * @param {ColumnStructureModel} model - Structure model to check.
   * @returns {boolean} True when model is the first right-pinned sibling.
   */
  static isRightStartPinned(model) {
    const siblings = model.parent
      .getOrderedChildren()
      .filter((child) => child.pinned === 'right');
    return siblings[0] === model;
  }
  /**
   * True when the provided structure model is the last non-pinned sibling in its parent's children.
   * Useful to handle the 'no-pinned-end' condition.
   *
   * @param {ColumnStructureModel} model - Structure model to check.
   * @returns {boolean} True when model is the last non-pinned sibling.
   */
  static isNoPinnedEnd(model) {
    const siblings = model.parent
      .getOrderedChildren()
      .filter((child) => !child.pinned);
    return siblings[siblings.length - 1] === model;
  }
  /**
   * True when the provided model is the first child of its parent.
   *
   * @param {ColumnStructureModel} model - Structure model to check.
   * @returns {boolean} True when model is the first child.
   */
  static isParentStart(model) {
    return model.parent.getOrderedChildren()[0] === model;
  }
  /**
   * True when the provided model is the last child of its parent.
   *
   * @param {ColumnStructureModel} model - Structure model to check.
   * @returns {boolean} True when model is the last child.
   */
  static isParentEnd(model) {
    const parentChildren = model.parent.getOrderedChildren();
    return parentChildren[parentChildren.length - 1] === model;
  }
  /**
   * Compute a node's colSpan by iterating its subtree and counting the maximum number
   * of nodes present at any single level (visual column span).
   *
   * @param {ColumnStructureModel} mode - ColumnStructureModel node to examine.
   * @returns {number} Number of visual columns spanned by the node.
   */
  static getColSpan(mode) {
    const children = mode.iterator();
    const rowMap = {};
    children.forEach((node) => {
      rowMap[node.level] = (rowMap[node.level] || 0) + 1;
    });
    return Math.max(...Object.values(rowMap));
  }
  /**
   * Return cached leaf render nodes for this subtree.
   * Caches the result on first call and returns cached array on subsequent calls.
   *
   * @returns {ColumnRenderInputModel[]} Array of leaf render input nodes.
   */
  getLeaves() {
    if (this.leaves.length > 0) {
      return this.leaves;
    }
    const leaves = this.iterator().filter((model) => model.isLeaf());
    this.leaves = leaves;
    return leaves;
  }
  /**
   * Generic iterator that supports traversal modes ('dfs' currently implemented).
   *
   * @param {IteratorMode} [mode='dfs'] - Iterator mode ('dfs' | 'bfs').
   * @returns {ColumnRenderInputModel[]} Traversal-ordered array of nodes.
   */
  iterator(mode = 'dfs') {
    if (mode === 'dfs') {
      return this.dfsIterator();
    }
    // Future: implement BFS mode here.
    return [];
  }
  /**
   * Depth-first pre-order traversal of this node and its descendants.
   * Caches the result for the 'dfs' mode to avoid repeated recursion.
   *
   * @returns {ColumnRenderInputModel[]} Array of nodes in DFS order (includes this node).
   */
  dfsIterator() {
    if (this.iteratorCacheMap.has('dfs')) {
      return this.iteratorCacheMap.get('dfs');
    }
    const localResult = [];
    localResult.push(this);
    if (this.children && this.children.length > 0) {
      this.children.forEach((child) => {
        localResult.push(...child.dfsIterator());
      });
    }
    this.iteratorCacheMap.set('dfs', localResult);
    return localResult;
  }
  /**
   * True when this render node has no children (visual leaf).
   *
   * @returns {boolean} True when node is a leaf.
   */
  isLeaf() {
    return !this.children || this.children.length === 0;
  }
}
