/*
 * Copyright (c) 2010, 2026 BSI Business Systems Integration AG
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 */
import {arrays, Dimension, HtmlComponent, HtmlCompPrefSizeOptions, InitModelOf, Insets, LayoutConstants, LogicalGridData, LogicalGridLayoutInfoModel, Rectangle, TreeSet} from '../../index';
import $ from 'jquery';

/**
 * JavaScript port of org.eclipse.scout.rt.ui.swing.LogicalGridLayoutInfo.
 */
export class LogicalGridLayoutInfo implements LogicalGridLayoutInfoModel {
  declare model: LogicalGridLayoutInfoModel;

  gridDatas: LogicalGridData[];
  cons: LogicalGridData[];
  cols: number;
  compSize: Dimension[];
  rows: number;
  width: number[][];
  widthHints: number[];
  height: number[][];
  weightX: number[];
  weightY: number[];
  hgap: number;
  vgap: number;
  rowHeight: number;
  columnWidth: number;
  cellBounds: Rectangle[][];
  widthHint: number;
  widthOnly: boolean;
  $components: JQuery[];

  constructor(model: InitModelOf<LogicalGridLayoutInfo>) {
    this.gridDatas = [];
    this.$components = null;
    this.cols = 0;
    this.compSize = [];
    this.rows = 0;
    this.width = [];
    this.widthHints = [];
    this.height = [];
    this.weightX = [];
    this.weightY = [];
    this.hgap = 0;
    this.vgap = 0;
    this.rowHeight = 0;
    this.columnWidth = 0;
    this.cellBounds = [];
    this.widthHint = null;
    this.widthOnly = false;
    $.extend(this, model);

    // create a modifiable copy of the grid datas
    let i, gd, x, y;
    for (i = 0; i < this.cons.length; i++) {
      this.gridDatas[i] = new LogicalGridData(this.cons[i]);
    }
    if (this.$components.length === 0) {
      return;
    }
    // eliminate unused rows and columns
    let usedCols = new TreeSet();
    let usedRows = new TreeSet();
    // ticket 86645 use member gridDatas instead of param cons
    for (i = 0; i < this.gridDatas.length; i++) {
      gd = this.gridDatas[i];
      if (gd.gridx < 0) {
        gd.gridx = 0;
      }
      if (gd.gridy < 0) {
        gd.gridy = 0;
      }
      if (gd.gridw < 1) {
        gd.gridw = 1;
      }
      if (gd.gridh < 1) {
        gd.gridh = 1;
      }
      for (x = gd.gridx; x < gd.gridx + gd.gridw; x++) {
        usedCols.add(x);
      }
      for (y = gd.gridy; y < gd.gridy + gd.gridh; y++) {
        usedRows.add(y);
      }
    }
    let maxCol = usedCols.last();
    for (x = maxCol; x >= 0; x--) {
      if (!usedCols.contains(x)) {
        // eliminate column
        // ticket 86645 use member gridDatas instead of param cons
        for (i = 0; i < this.gridDatas.length; i++) {
          gd = this.gridDatas[i];
          if (gd.gridx > x) {
            gd.gridx--;
          }
        }
      }
    }
    let maxRow = usedRows.last();
    for (y = maxRow; y >= 0; y--) {
      if (!usedRows.contains(y)) {
        // eliminate row
        // ticket 86645 use member gridDatas instead of param cons
        for (i = 0; i < this.gridDatas.length; i++) {
          gd = this.gridDatas[i];
          if (gd.gridy > y) {
            // ticket 86645
            gd.gridy--;
          }
        }
      }
    }
    this.cols = usedCols.size();
    this.rows = usedRows.size();

    $.log.isTraceEnabled() && $.log.trace('(LogicalGridLayoutInfo#CTOR) $components.length=' + this.$components.length + ' usedCols=' + this.cols + ' usedRows=' + this.rows);
    this._initializeInfo();
  }

  protected _initializeInfo() {
    let compCount = this.$components.length;
    let uiHeightElements = [];
    for (let i = 0; i < compCount; i++) {
      // cleanup constraints
      let $comp = this.$components[i];
      let cons = this.gridDatas[i];
      if (cons.gridx < 0) {
        cons.gridx = 0;
      }
      if (cons.gridy < 0) {
        cons.gridy = 0;
      }
      if (cons.gridw < 1) {
        cons.gridw = 1;
      }
      if (cons.gridh < 1) {
        cons.gridh = 1;
      }
      if (cons.gridx >= this.cols) {
        cons.gridx = this.cols - 1;
      }
      if (cons.gridy >= this.rows) {
        cons.gridy = this.rows - 1;
      }
      if (cons.gridx + cons.gridw - 1 >= this.cols) {
        cons.gridw = this.cols - cons.gridx;
      }
      if (cons.gridy + cons.gridh >= this.rows) {
        cons.gridh = this.rows - cons.gridy;
      }

      // Calculate and cache component size
      let size = new Dimension(0, 0);
      if (cons.widthHint > 0) {
        // Use explicit width hint, if set
        size.width = cons.widthHint;
      } else if (cons.useUiWidth || !cons.fillHorizontal) {
        // Calculate preferred width otherwise
        // This size is needed by _initializeColumns
        // But only if really needed by the logical grid layout (because it is expensive)
        size = this.uiSizeInPixel($comp);
      }
      if (cons.heightHint > 0) {
        // Use explicit height hint, if set
        size.height = cons.heightHint;
      } else if (cons.useUiHeight || !cons.fillVertical) {
        // Otherwise check if preferred height should be calculated.
        // Don't do it now because weightX need to be calculated first to get the correct width hints
        uiHeightElements.push({
          cons: cons,
          $comp: $comp,
          index: i
        });
      }
      this.compSize[i] = size;
    }

    // Calculate this.width and this.weightX
    this._initializeColumns();

    if (this.widthOnly) {
      // Abort here if only width is of interest
      this.height = arrays.init(this.rows, [0, 0, 0]);
      return;
    }

    // Calculate preferred heights using the width hints
    if (this.widthHint && uiHeightElements.length > 0) {
      let totalHGap = Math.max(0, (this.cols - 1) * this.hgap);
      this.widthHints = this.layoutSizes(this.widthHint - totalHGap, this.width, this.weightX);
    }
    uiHeightElements.forEach(elem => {
      let $comp = elem.$comp;
      let cons = elem.cons;
      let widthHint = this.widthHintForGridData(cons);
      if (!cons.fillHorizontal) {
        widthHint = Math.min(widthHint, this.compSize[elem.index].width);
      }
      this.compSize[elem.index] = this.uiSizeInPixel($comp, {
        widthHint: widthHint
      });
    });

    // Calculate this.height and this.weightY
    this._initializeRows();
  }

  protected _initializeColumns() {
    let compSize = this.compSize;
    let compCount = compSize.length;
    let prefWidths = arrays.init(this.cols, 0);
    let maxWidths = arrays.init(this.cols, 10240);
    let fixedWidths = arrays.init(this.cols, false);
    for (let i = 0; i < compCount; i++) {
      let cons = this.gridDatas[i];
      if (cons.gridw === 1) {
        let prefw;
        if (cons.widthHint > 0) {
          prefw = cons.widthHint;
        } else if (cons.useUiWidth) {
          prefw = compSize[i].width;
        } else {
          prefw = this.logicalWidthInPixel(cons);
        }
        prefw = Math.floor(prefw);
        let x = cons.gridx;
        if (x < this.cols) {
          prefWidths[x] = Math.max(prefWidths[x], prefw);
          maxWidths[x] = Math.min(maxWidths[x], cons.maxWidth);
          if (cons.weightx === 0) {
            fixedWidths[x] = true;
          }
        }
      }
    }
    const lc = LayoutConstants;
    for (let i = 0; i < compCount; i++) {
      let cons = this.gridDatas[i];
      if (cons.gridw > 1) {
        let hSpan = cons.gridw;
        let spanWidth = 0;
        let distWidth;
        for (let j = cons.gridx; j < cons.gridx + cons.gridw && j < this.cols; j++) {
          if (!fixedWidths[j]) {
            spanWidth += prefWidths[j];
          }
        }
        if (cons.widthHint > 0) {
          distWidth = cons.widthHint;
        } else if (cons.useUiWidth) {
          distWidth = compSize[i].width;
        } else {
          distWidth = this.logicalWidthInPixel(cons);
        }
        let hGaps = (hSpan - 1) * this.hgap;
        distWidth -= hGaps;
        if (distWidth > spanWidth) {
          this._distributeWidth(cons, distWidth, prefWidths, fixedWidths, Math.max.bind(Math));
        }
        this._distributeWidth(cons, cons.maxWidth - hGaps, maxWidths, fixedWidths, Math.min.bind(Math));
      }
    }

    for (let i = 0; i < this.cols; i++) {
      this.width[i] = [];
      if (fixedWidths[i]) {
        this.width[i][lc.MIN] = prefWidths[i];
        this.width[i][lc.PREF] = prefWidths[i];
        this.width[i][lc.MAX] = Math.min(prefWidths[i], maxWidths[i]);
      } else {
        this.width[i][lc.MIN] = 0; // must be exactly 0!
        this.width[i][lc.PREF] = prefWidths[i];
        this.width[i][lc.MAX] = maxWidths[i];
      }
    }

    // averaged column weights, normalized so that sum of weights is equal to
    // 1.0
    for (let i = 0; i < this.cols; i++) {
      if (fixedWidths[i]) {
        this.weightX[i] = 0;
      } else {
        let weightSum = 0;
        let weightCount = 0;
        for (let k = 0; k < compCount; k++) {
          let cons = this.gridDatas[k];
          if (cons.weightx > 0 && cons.gridx <= i && i <= cons.gridx + cons.gridw - 1) {
            weightSum += (cons.weightx / cons.gridw);
            weightCount++;
          }
        }
        this.weightX[i] = (weightCount > 0 ? weightSum / weightCount : 0);
      }
    }
    let sumWeightX = 0;
    for (let i = 0; i < this.cols; i++) {
      sumWeightX += this.weightX[i];
    }
    if (sumWeightX >= 1e-6) {
      let f = 1.0 / sumWeightX;
      for (let i = 0; i < this.cols; i++) {
        this.weightX[i] = this.weightX[i] * f;
      }
    }
  }

  /**
   * @param cons the current grid data
   * @param distWidth the width to distribute to the columns. The width is distributed to the columns equally.
   * @param widths the column widths that have been distributed so far when the previous rows were visited
   * @param fixedWidths the columns with a fixed width (weightx = 0)
   * @param calc a function that is called for each element of the given `widths` array, e.g. to decide whether to use the newly calculated column width or the existing one.
   */
  protected _distributeWidth(cons: LogicalGridData, distWidth: number, widths: number[], fixedWidths: boolean[], calc: (equalWidth: number, width: number) => number) {
    let hSpan = cons.gridw;
    let equalWidth = Math.floor(distWidth / hSpan);
    let remainder = distWidth % hSpan;
    let last = -1;
    for (let j = cons.gridx; j < cons.gridx + cons.gridw && j < this.cols; j++) {
      last = j;
      if (!fixedWidths[j]) {
        widths[j] = calc(equalWidth, widths[j]);
      }
      if (cons.weightx === 0) {
        fixedWidths[j] = true;
      }
    }
    if (last > -1) {
      widths[last] += remainder;
    }
  }

  /**
   * @param cons the current grid data
   * @param distHeight the height to distribute to the rows. The height is distributed to the rows equally.
   * @param widths the row heights that have been distributed so far when the previous column were visited
   * @param fixedHeights the rows with a fixed height (weighty = 0)
   * @param calc a function that is called for each element of the given `heights` array, e.g. to decide whether to use the newly calculated row height or the existing one.
   */
  protected _distributeHeight(cons: LogicalGridData, distHeight: number, heights: number[], fixedHeights: boolean[], calc: (equalWidth: number, width: number) => number) {
    let vSpan = cons.gridh;
    let equalHeight = Math.floor(distHeight / vSpan);
    let remainder = distHeight % vSpan;
    let last = -1;
    for (let j = cons.gridy; j < cons.gridy + cons.gridh && j < this.rows; j++) {
      last = j;
      if (!fixedHeights[j]) {
        heights[j] = calc(equalHeight, heights[j]);
      }
      if (cons.weighty === 0) {
        fixedHeights[j] = true;
      }
    }
    if (last > -1) {
      heights[last] += remainder;
    }
  }

  protected _initializeRows() {
    let compSize = this.compSize;
    let compCount = compSize.length;
    let prefHeights = arrays.init(this.rows, 0);
    let maxHeights = arrays.init(this.rows, 10240);
    let fixedHeights = arrays.init(this.rows, false);
    for (let i = 0; i < compCount; i++) {
      let cons = this.gridDatas[i];
      if (cons.gridh === 1) {
        let prefh;
        if (cons.heightHint > 0) {
          prefh = cons.heightHint;
        } else if (cons.useUiHeight) {
          prefh = compSize[i].height;
        } else {
          prefh = this.logicalHeightInPixel(cons);
        }
        prefh = Math.floor(prefh);
        let y = cons.gridy;
        if (y < this.rows) {
          prefHeights[y] = Math.max(prefHeights[y], prefh);
          maxHeights[y] = Math.min(maxHeights[y], cons.maxHeight);
          if (cons.weighty === 0) {
            fixedHeights[y] = true;
          }
        }
      }
    }
    const lc = LayoutConstants;
    for (let i = 0; i < compCount; i++) {
      let cons = this.gridDatas[i];
      if (cons.gridh > 1) {
        let vSpan = cons.gridh;
        let spanHeight = 0;
        let distHeight;
        for (let j = cons.gridy; j < cons.gridy + cons.gridh && j < this.rows; j++) {
          if (!fixedHeights[j]) {
            spanHeight += prefHeights[j];
          }
        }
        let vGaps = (vSpan - 1) * this.vgap;
        if (cons.heightHint > 0) {
          distHeight = cons.heightHint;
        } else if (cons.useUiHeight) {
          distHeight = compSize[i].height;
        } else {
          distHeight = this.logicalHeightInPixel(cons);
        }
        distHeight -= vGaps;
        if (distHeight > spanHeight) {
          this._distributeHeight(cons, distHeight, prefHeights, fixedHeights, Math.max.bind(this));
        }
        this._distributeHeight(cons, cons.maxHeight - vGaps, maxHeights, fixedHeights, Math.min.bind(this));
      }
    }

    for (let i = 0; i < this.rows; i++) {
      this.height[i] = [];
      if (fixedHeights[i]) {
        this.height[i][lc.MIN] = prefHeights[i];
        this.height[i][lc.PREF] = prefHeights[i];
        this.height[i][lc.MAX] = Math.min(prefHeights[i], maxHeights[i]);
      } else {
        this.height[i][lc.MIN] = 0; // must be exactly 0!
        this.height[i][lc.PREF] = prefHeights[i];
        this.height[i][lc.MAX] = maxHeights[i];
      }
    }

    // averaged row weights, normalized so that sum of weights is equal to 1.0
    for (let i = 0; i < this.rows; i++) {
      if (fixedHeights[i]) {
        this.weightY[i] = 0;
      } else {
        let weightSum = 0;
        let weightCount = 0;
        for (let k = 0; k < compCount; k++) {
          let cons = this.gridDatas[k];
          if (cons.weighty > 0 && cons.gridy <= i && i <= cons.gridy + cons.gridh - 1) {
            weightSum += (cons.weighty / cons.gridh);
            weightCount++;
          }
        }
        this.weightY[i] = (weightCount > 0 ? weightSum / weightCount : 0);
      }
    }
    let sumWeightY = 0;
    for (let i = 0; i < this.rows; i++) {
      sumWeightY += this.weightY[i];
    }
    if (sumWeightY >= 1e-6) {
      let f = 1.0 / sumWeightY;
      for (let i = 0; i < this.rows; i++) {
        this.weightY[i] = this.weightY[i] * f;
      }
    }
  }

  layoutCellBounds(size: Dimension, insets: Insets): Rectangle[][] {
    let w = this.layoutSizes(size.width - insets.horizontal() - Math.max(0, (this.cols - 1) * this.hgap), this.width, this.weightX);
    let h = this.layoutSizes(size.height - insets.vertical() - Math.max(0, (this.rows - 1) * this.vgap), this.height, this.weightY);
    this.cellBounds = arrays.init(this.rows, null);
    let y = insets.top;
    for (let r = 0; r < this.rows; r++) {
      let x = insets.left;
      this.cellBounds[r] = arrays.init(this.cols, null);
      for (let c = 0; c < this.cols; c++) {
        this.cellBounds[r][c] = new Rectangle(x, y, w[c], h[r]);
        x += w[c];
        x += this.hgap;
      }
      y += h[r];
      y += this.vgap;
    }
    return this.cellBounds;
  }

  layoutSizes(targetSize: number, sizes: number[][], weights: number[]): number[] {
    let outSizes = arrays.init(sizes.length, 0);
    if (targetSize <= 0) {
      for (let i = 0; i < sizes.length; i++) {
        outSizes[i] = sizes[i][LayoutConstants.MIN];
      }
      return outSizes;
    }
    let sumSize = 0;
    let tmpWeight = arrays.init(weights.length, 0.0);
    let sumWeight = 0;
    for (let i = 0; i < sizes.length; i++) {
      outSizes[i] = Math.min(Math.max(sizes[i][LayoutConstants.PREF], sizes[i][LayoutConstants.MIN]), sizes[i][LayoutConstants.MAX]);
      sumSize += outSizes[i];
      tmpWeight[i] = weights[i];
      /**
       * autocorrection: if weight is 0 and min / max sizes are NOT equal then
       * set weight to 1; if weight<eps set it to 0
       */
      if (tmpWeight[i] < LayoutConstants.EPS) {
        if (sizes[i][LayoutConstants.MAX] > sizes[i][LayoutConstants.MIN]) {
          tmpWeight[i] = 1;
        } else {
          tmpWeight[i] = 0;
        }
      }
      sumWeight += tmpWeight[i];
    }
    // normalize weights
    if (sumWeight > 0) {
      for (let i = 0; i < tmpWeight.length; i++) {
        tmpWeight[i] = tmpWeight[i] / sumWeight;
      }
    }
    let deltaInt = targetSize - sumSize;
    // expand or shrink
    if (Math.abs(deltaInt) > 0) {
      // setup accumulators
      /* float[] */
      let accWeight = arrays.init(tmpWeight.length, 0.0);
      let hasTargets;
      if (deltaInt > 0) {
        // expand, if delta is > 0
        hasTargets = true;
        while (deltaInt > 0 && hasTargets) {
          hasTargets = false;
          for (let i = 0; i < outSizes.length && deltaInt > 0; i++) {
            if (tmpWeight[i] > 0 && outSizes[i] < sizes[i][LayoutConstants.MAX]) {
              hasTargets = true;
              accWeight[i] += tmpWeight[i];
              if (accWeight[i] > 0) {
                accWeight[i] -= 1;
                outSizes[i] += 1;
                deltaInt -= 1;
              }
            }
          }
        }
      } else {
        // shrink, if delta is <= 0
        hasTargets = true;
        while (deltaInt < 0 && hasTargets) {
          hasTargets = false;
          for (let i = 0; i < outSizes.length && deltaInt < 0; i++) {
            if (tmpWeight[i] > 0 && outSizes[i] > sizes[i][LayoutConstants.MIN]) {
              hasTargets = true;
              accWeight[i] += tmpWeight[i];
              if (accWeight[i] > 0) {
                accWeight[i] -= 1;
                outSizes[i] -= 1;
                deltaInt += 1;
              }
            }
          }
        }
      }
    }
    return outSizes;
  }

  logicalWidthInPixel(cons: LogicalGridData): number {
    let gridW = cons.gridw;
    return (this.columnWidth * gridW) + (this.hgap * Math.max(0, gridW - 1));
  }

  logicalHeightInPixel(cons: LogicalGridData): number {
    let gridH = cons.gridh;
    let addition = cons.logicalRowHeightAddition || 0;
    return (this.rowHeight * gridH) + (this.vgap * Math.max(0, gridH - 1)) + addition;
  }

  uiSizeInPixel($comp: JQuery, options?: HtmlCompPrefSizeOptions): Dimension {
    let htmlComp = HtmlComponent.get($comp);
    return htmlComp.prefSize(options).add(htmlComp.margins());
  }

  /**
   * @returns the width hint for the given gridData
   */
  widthHintForGridData(gridData: LogicalGridData): number | null {
    if (this.widthHints.length === 0) {
      return null;
    }
    let widthHint = (gridData.gridw - 1) * this.hgap;
    for (let i = gridData.gridx; i < gridData.gridx + gridData.gridw; i++) {
      widthHint += this.widthHints[i];
    }
    return widthHint;
  }
}
