/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.
 */

/* $Id: TableStepper.java 1872250 2020-01-02 16:28:29Z ssteiner $ */

package org.apache.fop.layoutmgr.table;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.apache.fop.fo.Constants;
import org.apache.fop.fo.flow.table.EffRow;
import org.apache.fop.fo.flow.table.GridUnit;
import org.apache.fop.fo.flow.table.PrimaryGridUnit;
import org.apache.fop.layoutmgr.BlockStackingLayoutManager;
import org.apache.fop.layoutmgr.BreakElement;
import org.apache.fop.layoutmgr.Keep;
import org.apache.fop.layoutmgr.KnuthBlockBox;
import org.apache.fop.layoutmgr.KnuthBox;
import org.apache.fop.layoutmgr.KnuthGlue;
import org.apache.fop.layoutmgr.KnuthPenalty;
import org.apache.fop.layoutmgr.LayoutContext;
import org.apache.fop.layoutmgr.LayoutManager;
import org.apache.fop.layoutmgr.Position;
import org.apache.fop.util.BreakUtil;

/**
 * This class processes row groups to create combined element lists for tables.
 */
public class TableStepper {

    /** Logger **/
    private static Log log = LogFactory.getLog(TableStepper.class);

    private TableContentLayoutManager tclm;

    private EffRow[] rowGroup;
    /** Number of columns in the row group. */
    private int columnCount;
    private int totalHeight;
    private int previousRowsLength;
    private int activeRowIndex;

    private boolean rowFinished;

    /** Cells spanning the current row. */
    private List activeCells = new LinkedList();

    /** Cells that will start the next row. */
    private List nextActiveCells = new LinkedList();

    /**
     * True if the next row is being delayed, that is, if cells spanning the current and
     * the next row have steps smaller than the next row's first step. In this case the
     * next row may be extended to offer additional break possibilities.
     */
    private boolean delayingNextRow;

    /**
     * The first step for a row. This is the minimal step necessary to include some
     * content from all the cells starting the row.
     */
    private int rowFirstStep;

    /**
     * Flag used to produce an infinite penalty if the height of the current row is
     * smaller than the first step for that row (may happen with row-spanning cells).
     *
     * @see #considerRowLastStep(int)
     */
    private boolean rowHeightSmallerThanFirstStep;

    /**
     * The class of the next break. One of {@link Constants#EN_AUTO},
     * {@link Constants#EN_COLUMN}, {@link Constants#EN_PAGE},
     * {@link Constants#EN_EVEN_PAGE}, {@link Constants#EN_ODD_PAGE}. Defaults to
     * EN_AUTO.
     */
    private int nextBreakClass;

    /**
     * Main constructor
     * @param tclm The parent TableContentLayoutManager
     */
    public TableStepper(TableContentLayoutManager tclm) {
        this.tclm = tclm;
        this.columnCount = tclm.getTableLM().getTable().getNumberOfColumns();
    }

    /**
     * Initializes the fields of this instance to handle a new row group.
     *
     * @param rows the new row group to handle
     */
    private void setup(EffRow[] rows) {
        rowGroup = rows;
        previousRowsLength = 0;
        activeRowIndex = 0;
        activeCells.clear();
        nextActiveCells.clear();
        delayingNextRow = false;
        rowFirstStep = 0;
        rowHeightSmallerThanFirstStep = false;
    }

    private void calcTotalHeight() {
        totalHeight = 0;
        for (EffRow aRowGroup : rowGroup) {
            totalHeight += aRowGroup.getHeight().getOpt();
        }
        if (log.isDebugEnabled()) {
            log.debug("totalHeight=" + totalHeight);
        }
    }

    private int getMaxRemainingHeight() {
        int maxW = 0;
        for (Object activeCell1 : activeCells) {
            ActiveCell activeCell = (ActiveCell) activeCell1;
            int remain = activeCell.getRemainingLength();
            PrimaryGridUnit pgu = activeCell.getPrimaryGridUnit();
            for (int i = activeRowIndex + 1; i < pgu.getRowIndex() - rowGroup[0].getIndex()
                    + pgu.getCell().getNumberRowsSpanned(); i++) {
                remain -= rowGroup[i].getHeight().getOpt();
            }
            maxW = Math.max(maxW, remain);
        }
        for (int i = activeRowIndex + 1; i < rowGroup.length; i++) {
            maxW += rowGroup[i].getHeight().getOpt();
        }
        return maxW;
    }

    /**
     * Creates ActiveCell instances for cells starting on the row at the given index.
     *
     * @param activeCellList the list that will hold the active cells
     * @param rowIndex the index of the row from which cells must be activated
     */
    private void activateCells(List activeCellList, int rowIndex) {
        EffRow row = rowGroup[rowIndex];
        for (int i = 0; i < columnCount; i++) {
            GridUnit gu = row.getGridUnit(i);
            if (!gu.isEmpty() && gu.isPrimary()) {
                assert (gu instanceof PrimaryGridUnit);
                activeCellList.add(new ActiveCell((PrimaryGridUnit) gu, row, rowIndex,
                        previousRowsLength, getTableLM()));
            }
        }
    }

    /**
     * Creates the combined element list for a row group.
     * @param context Active LayoutContext
     * @param rows the row group
     * @param bodyType Indicates what type of body is processed (body, header or footer)
     * @return the combined element list
     */
    public LinkedList getCombinedKnuthElementsForRowGroup(LayoutContext context, EffRow[] rows,
            int bodyType) {
        setup(rows);
        activateCells(activeCells, 0);
        calcTotalHeight();

        int cumulateLength = 0; // Length of the content accumulated before the break
        TableContentPosition lastTCPos = null;
        LinkedList returnList = new LinkedList();
        int laststep = 0;
        int step = getFirstStep();
        do {
            int maxRemainingHeight = getMaxRemainingHeight();
            int penaltyOrGlueLen = step + maxRemainingHeight - totalHeight;
            int boxLen = step - cumulateLength - Math.max(0, penaltyOrGlueLen)/* penalty, if any */;
            cumulateLength += boxLen + Math.max(0, -penaltyOrGlueLen)/* the glue, if any */;

            if (log.isDebugEnabled()) {
                log.debug("Next step: " + step + " (+" + (step - laststep) + ")");
                log.debug("           max remaining height: " + maxRemainingHeight);
                if (penaltyOrGlueLen >= 0) {
                    log.debug("           box = " + boxLen + " penalty = " + penaltyOrGlueLen);
                } else {
                    log.debug("           box = " + boxLen + " glue = " + (-penaltyOrGlueLen));
                }
            }

            LinkedList footnoteList = new LinkedList();
            //Put all involved grid units into a list
            List cellParts = new java.util.ArrayList(activeCells.size());
            for (Object activeCell2 : activeCells) {
                ActiveCell activeCell = (ActiveCell) activeCell2;
                CellPart part = activeCell.createCellPart();
                cellParts.add(part);
                activeCell.addFootnotes(footnoteList);
            }

            //Create elements for step
            TableContentPosition tcpos = new TableContentPosition(getTableLM(),
                    cellParts, rowGroup[activeRowIndex]);
            if (delayingNextRow) {
                tcpos.setNewPageRow(rowGroup[activeRowIndex + 1]);
            }
            if (returnList.size() == 0) {
                tcpos.setFlag(TableContentPosition.FIRST_IN_ROWGROUP, true);
            }
            lastTCPos = tcpos;

            // TODO TableStepper should remain as footnote-agnostic as possible
            if (footnoteList.isEmpty()) {
                returnList.add(new KnuthBox(boxLen, tcpos, false));
            } else {
                returnList.add(new KnuthBlockBox(boxLen, footnoteList, tcpos, false));
            }

            int effPenaltyLen = Math.max(0, penaltyOrGlueLen);
            TableHFPenaltyPosition penaltyPos = new TableHFPenaltyPosition(getTableLM());
            if (bodyType == TableRowIterator.BODY) {
                if (!getTableLM().getTable().omitHeaderAtBreak()) {
                    effPenaltyLen += tclm.getHeaderNetHeight();
                    penaltyPos.headerElements = tclm.getHeaderElements();
                }
                if (!getTableLM().getTable().omitFooterAtBreak()) {
                    effPenaltyLen += tclm.getFooterNetHeight();
                    penaltyPos.footerElements = tclm.getFooterElements();
                }
            }

            Keep keep = getTableLM().getKeepTogether();
            int stepPenalty = 0;
            for (Object activeCell1 : activeCells) {
                ActiveCell activeCell = (ActiveCell) activeCell1;
                keep = keep.compare(activeCell.getKeepWithNext());
                stepPenalty = Math.max(stepPenalty, activeCell.getPenaltyValue());
            }
            if (!rowFinished) {
                keep = keep.compare(rowGroup[activeRowIndex].getKeepTogether());
            } else if (activeRowIndex < rowGroup.length - 1) {
                keep = keep.compare(rowGroup[activeRowIndex].getKeepWithNext());
                keep = keep.compare(rowGroup[activeRowIndex + 1].getKeepWithPrevious());
                nextBreakClass = BreakUtil.compareBreakClasses(nextBreakClass,
                        rowGroup[activeRowIndex].getBreakAfter());
                nextBreakClass = BreakUtil.compareBreakClasses(nextBreakClass,
                        rowGroup[activeRowIndex + 1].getBreakBefore());
            }
            int p = keep.getPenalty();
            if (rowHeightSmallerThanFirstStep) {
                rowHeightSmallerThanFirstStep = false;
                p = KnuthPenalty.INFINITE;
            }
            p = Math.max(p, stepPenalty);
            int breakClass = keep.getContext();
            if (nextBreakClass != Constants.EN_AUTO) {
                log.trace("Forced break encountered");
                p = -KnuthPenalty.INFINITE; //Overrides any keeps (see 4.8 in XSL 1.0)
                breakClass = nextBreakClass;
            }
            returnList.add(new BreakElement(penaltyPos, effPenaltyLen, p, breakClass, context));

            laststep = step;
            step = getNextStep();
            if (penaltyOrGlueLen < 0) {
                int shrink = 0;
                int stretch = 0;
                int width = -penaltyOrGlueLen;
                LayoutManager bslm = getTableLM().getParent();
                if (bslm instanceof BlockStackingLayoutManager && ((BlockStackingLayoutManager)bslm).isRestartAtLM()
                        && keep.getPenalty() == KnuthPenalty.INFINITE) {
                    width = 0;
                }
                returnList.add(new KnuthGlue(width, stretch, shrink, new Position(null), true));
            }
        } while (step >= 0);
        assert !returnList.isEmpty();
        lastTCPos.setFlag(TableContentPosition.LAST_IN_ROWGROUP, true);
        return returnList;
    }

    /**
     * Returns the first step for the current row group.
     *
     * @return the first step for the current row group
     */
    private int getFirstStep() {
        computeRowFirstStep(activeCells);
        signalRowFirstStep();
        int minStep = considerRowLastStep(rowFirstStep);
        signalNextStep(minStep);
        return minStep;
    }

    /**
     * Returns the next break possibility.
     *
     * @return the next step
     */
    private int getNextStep() {
        if (rowFinished) {
            if (activeRowIndex == rowGroup.length - 1) {
                // The row group is finished, no next step
                return -1;
            }
            rowFinished = false;
            removeCellsEndingOnCurrentRow();
            log.trace("Delaying next row");
            delayingNextRow = true;
        }
        if (delayingNextRow) {
            int minStep = computeMinStep();
            if (minStep < 0 || minStep >= rowFirstStep
                    || minStep > rowGroup[activeRowIndex].getExplicitHeight().getMax()) {
                if (log.isTraceEnabled()) {
                    log.trace("Step = " + minStep);
                }
                delayingNextRow = false;
                minStep = rowFirstStep;
                switchToNextRow();
                signalRowFirstStep();
                minStep = considerRowLastStep(minStep);
            }
            signalNextStep(minStep);
            return minStep;
        } else {
            int minStep = computeMinStep();
            minStep = considerRowLastStep(minStep);
            signalNextStep(minStep);
            return minStep;
        }
    }

    /**
     * Computes the minimal necessary step to make the next row fit. That is, so such as
     * cell on the next row can contribute some content.
     *
     * @param cells the cells occupying the next row (may include cells starting on
     * previous rows and spanning over this one)
     */
    private void computeRowFirstStep(List cells) {
        for (Object cell : cells) {
            ActiveCell activeCell = (ActiveCell) cell;
            rowFirstStep = Math.max(rowFirstStep, activeCell.getFirstStep());
        }
    }

    /**
     * Computes the next minimal step.
     *
     * @return the minimal step from the active cells, &lt; 0 if there is no such step
     */
    private int computeMinStep() {
        int minStep = Integer.MAX_VALUE;
        boolean stepFound = false;
        for (Object activeCell1 : activeCells) {
            ActiveCell activeCell = (ActiveCell) activeCell1;
            int nextStep = activeCell.getNextStep();
            if (nextStep >= 0) {
                stepFound = true;
                minStep = Math.min(minStep, nextStep);
            }
        }
        if (stepFound) {
            return minStep;
        } else {
            return -1;
        }
    }

    /**
     * Signals the first step to the active cells, to allow them to add more content to
     * the step if possible.
     *
     * @see ActiveCell#signalRowFirstStep(int)
     */
    private void signalRowFirstStep() {
        for (Object activeCell1 : activeCells) {
            ActiveCell activeCell = (ActiveCell) activeCell1;
            activeCell.signalRowFirstStep(rowFirstStep);
        }
    }

    /**
     * Signals the next selected step to the active cells.
     *
     * @param step the next step
     */
    private void signalNextStep(int step) {
        nextBreakClass = Constants.EN_AUTO;
        for (Object activeCell1 : activeCells) {
            ActiveCell activeCell = (ActiveCell) activeCell1;
            nextBreakClass = BreakUtil.compareBreakClasses(nextBreakClass,
                    activeCell.signalNextStep(step));
        }
    }

    /**
     * Determines if the given step will finish the current row, and if so switch to the
     * last step for this row.
     * <p>If the row is finished then the after borders for the cell may change (their
     * conditionalities no longer apply for the cells ending on the current row). Thus the
     * final step may grow with respect to the given one.</p>
     * <p>In more rare occasions, the given step may correspond to the first step of a
     * row-spanning cell, and may be greater than the height of the current row (consider,
     * for example, an unbreakable cell spanning three rows). In such a case the returned
     * step will correspond to the row height and a flag will be set to produce an
     * infinite penalty for this step. This will prevent the breaking algorithm from
     * choosing this break, but still allow to create the appropriate TableContentPosition
     * for the cells ending on the current row.</p>
     *
     * @param step the next step
     * @return the updated step if any
     */
    private int considerRowLastStep(int step) {
        rowFinished = true;
        for (Object activeCell3 : activeCells) {
            ActiveCell activeCell = (ActiveCell) activeCell3;
            if (activeCell.endsOnRow(activeRowIndex)) {
                if (!activeCell.finishes(step)) {
                    rowFinished = false;
                }
            }
        }
        if (rowFinished) {
            if (log.isTraceEnabled()) {
                log.trace("Step = " + step);
                log.trace("Row finished, computing last step");
            }
            int maxStep = 0;
            for (Object activeCell2 : activeCells) {
                ActiveCell activeCell = (ActiveCell) activeCell2;
                if (activeCell.endsOnRow(activeRowIndex)) {
                    maxStep = Math.max(maxStep, activeCell.getLastStep());
                }
            }
            if (log.isTraceEnabled()) {
                log.trace("Max step: " + maxStep);
            }
            for (Object activeCell1 : activeCells) {
                ActiveCell activeCell = (ActiveCell) activeCell1;
                activeCell.endRow(activeRowIndex);
                if (!activeCell.endsOnRow(activeRowIndex)) {
                    activeCell.signalRowLastStep(maxStep);
                }
            }
            if (maxStep < step) {
                log.trace("Row height smaller than first step, produced penalty will be infinite");
                rowHeightSmallerThanFirstStep = true;
            }
            step = maxStep;
            prepareNextRow();
        }
        return step;
    }

    /**
     * Pre-activates the cells that will start the next row, and computes the first step
     * for that row.
     */
    private void prepareNextRow() {
        if (activeRowIndex < rowGroup.length - 1) {
            previousRowsLength += rowGroup[activeRowIndex].getHeight().getOpt();
            activateCells(nextActiveCells, activeRowIndex + 1);
            if (log.isTraceEnabled()) {
                log.trace("Computing first step for row " + (activeRowIndex + 2));
            }
            computeRowFirstStep(nextActiveCells);
            if (log.isTraceEnabled()) {
                log.trace("Next first step = " + rowFirstStep);
            }
        }
    }

    private void removeCellsEndingOnCurrentRow() {
        for (Iterator iter = activeCells.iterator(); iter.hasNext();) {
            ActiveCell activeCell = (ActiveCell) iter.next();
            if (activeCell.endsOnRow(activeRowIndex)) {
                iter.remove();
            }
        }
    }

    /**
     * Actually switches to the next row, increasing activeRowIndex and transferring to
     * activeCells the cells starting on the next row.
     */
    private void switchToNextRow() {
        activeRowIndex++;
        if (log.isTraceEnabled()) {
            log.trace("Switching to row " + (activeRowIndex + 1));
        }
        for (Object activeCell1 : activeCells) {
            ActiveCell activeCell = (ActiveCell) activeCell1;
            activeCell.nextRowStarts();
        }
        activeCells.addAll(nextActiveCells);
        nextActiveCells.clear();
    }

    /** @return the table layout manager */
    private TableLayoutManager getTableLM() {
        return this.tclm.getTableLM();
    }
}
