/*
 * 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: ActiveCell.java 1890190 2021-05-25 09:08:58Z ssteiner $ */

package org.apache.fop.layoutmgr.table;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;

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.ConditionalBorder;
import org.apache.fop.fo.flow.table.EffRow;
import org.apache.fop.fo.flow.table.PrimaryGridUnit;
import org.apache.fop.fo.properties.CommonBorderPaddingBackground;
import org.apache.fop.layoutmgr.ElementListUtils;
import org.apache.fop.layoutmgr.Keep;
import org.apache.fop.layoutmgr.KnuthBlockBox;
import org.apache.fop.layoutmgr.KnuthBox;
import org.apache.fop.layoutmgr.KnuthElement;
import org.apache.fop.layoutmgr.KnuthPenalty;
import org.apache.fop.traits.MinOptMax;

/**
 * A cell playing in the construction of steps for a row-group.
 */
class ActiveCell {

    private static Log log = LogFactory.getLog(ActiveCell.class);

    private PrimaryGridUnit pgu;
    /** Knuth elements for this active cell. */
    private List elementList;
    /** Iterator over the Knuth element list. */
    private ListIterator knuthIter;
    /** Number of the row where the row-span ends, zero-based. */
    private int endRowIndex;
    /** Length of the Knuth elements not yet included in the steps. */
    private int remainingLength;
    /** Total length of this cell's content plus the lengths of the previous rows. */
    private int totalLength;
    /** Length of the Knuth elements already included in the steps. */
    private int includedLength;

    private int paddingBeforeNormal;
    private int paddingBeforeLeading;
    private int paddingAfterNormal;
    private int paddingAfterTrailing;

    private int bpBeforeNormal;
    private int bpBeforeLeading;
    private int bpAfterNormal;
    private int bpAfterTrailing;

    /** True if the next CellPart that will be created will be the last one for this cell. */
    private boolean lastCellPart;

    private Keep keepWithNext;

    private int spanIndex;

    private Step previousStep;
    private Step nextStep;
    /**
     * The step following nextStep. Computing it early allows to calculate
     * {@link Step#condBeforeContentLength}, thus to easily determine the remaining
     * length. That also helps for {@link #increaseCurrentStep(int)}.
     */
    private Step afterNextStep;

    /**
     * Auxiliary class to store all the informations related to a breaking step.
     */
    private static class Step {
        /** Index, in the list of Knuth elements, of the element starting this step. */
        private int start;
        /** Index, in the list of Knuth elements, of the element ending this step. */
        private int end;
        /** Length of the Knuth elements up to this step. */
        private int contentLength;
        /** Total length up to this step, including paddings and borders. */
        private int totalLength;
        /** Length of the penalty ending this step, if any. */
        private int penaltyLength;
        /** Value of the penalty ending this step, 0 if the step does not end on a penalty. */
        private int penaltyValue;
        /** List of footnotes for this step. */
        private List footnoteList;
        /**
         * One of {@link Constants#EN_AUTO}, {@link Constants#EN_COLUMN},
         * {@link Constants#EN_PAGE}, {@link Constants#EN_EVEN_PAGE},
         * {@link Constants#EN_ODD_PAGE}. Set to auto if the break isn't at a penalty
         * element.
         */
        private int breakClass;
        /**
         * Length of the optional content at the beginning of the step. That is, content
         * that will not appear if this step starts a new page.
         */
        private int condBeforeContentLength;

        Step(int contentLength) {
            this.contentLength = contentLength;
            this.end = -1;
        }

        Step(Step other) {
            set(other);
        }

        void set(Step other) {
            this.start         = other.start;
            this.end           = other.end;
            this.contentLength = other.contentLength;
            this.totalLength   = other.totalLength;
            this.penaltyLength = other.penaltyLength;
            this.penaltyValue  = other.penaltyValue;
            if (other.footnoteList != null) {
                if (this.footnoteList == null) {
                    this.footnoteList = new ArrayList();
                }
                this.footnoteList.addAll(other.footnoteList);
            }
            this.condBeforeContentLength = other.condBeforeContentLength;
            this.breakClass    = other.breakClass;
        }

        /** {@inheritDoc} */
        public String toString() {
            return "Step: start=" + start + " end=" + end + " length=" + totalLength;
        }
    }

    // TODO to be removed along with the RowPainter#computeContentLength method
    /** See {@link ActiveCell#handleExplicitHeight(MinOptMax, MinOptMax)}. */
    private static class FillerPenalty extends KnuthPenalty {

        private int contentLength;

        FillerPenalty(KnuthPenalty p, int length) {
            super(length, p.getPenalty(), p.isPenaltyFlagged(), p.getBreakClass(),
                    p.getPosition(), p.isAuxiliary());
            contentLength = p.getWidth();
        }

        FillerPenalty(int length) {
            super(length, 0, false, null, true);
            contentLength = 0;
        }
    }

    /** See {@link ActiveCell#handleExplicitHeight(MinOptMax, MinOptMax)}. */
    private static class FillerBox extends KnuthBox {
        FillerBox(int length) {
            super(length, null, true);
        }
    }

    /**
     * Returns the actual length of the content represented by the given element. In the
     * case where this element is used as a filler to match a row's fixed height, the
     * value returned by the getW() method will be higher than the actual content.
     *
     * @param el an element
     * @return the actual content length corresponding to the element
     */
    static int getElementContentLength(KnuthElement el) {
        if (el instanceof FillerPenalty) {
            return ((FillerPenalty) el).contentLength;
        } else if (el instanceof FillerBox) {
            return 0;
        } else {
            return el.getWidth();
        }
    }

    ActiveCell(PrimaryGridUnit pgu, EffRow row, int rowIndex, int previousRowsLength,
            TableLayoutManager tableLM) {
        this.pgu = pgu;
        CommonBorderPaddingBackground bordersPaddings = pgu.getCell()
                .getCommonBorderPaddingBackground();
        TableCellLayoutManager cellLM = pgu.getCellLM();
        paddingBeforeNormal = bordersPaddings.getPaddingBefore(false, cellLM);
        paddingBeforeLeading = bordersPaddings.getPaddingBefore(true, cellLM);
        paddingAfterNormal = bordersPaddings.getPaddingAfter(false, cellLM);
        paddingAfterTrailing = bordersPaddings.getPaddingAfter(true, cellLM);
        bpBeforeNormal = paddingBeforeNormal
                + pgu.getBeforeBorderWidth(0, ConditionalBorder.NORMAL);
        bpBeforeLeading = paddingBeforeLeading
                + pgu.getBeforeBorderWidth(0, ConditionalBorder.REST);
        bpAfterNormal = paddingAfterNormal + pgu.getAfterBorderWidth(ConditionalBorder.NORMAL);
        bpAfterTrailing = paddingAfterTrailing + pgu.getAfterBorderWidth(0, ConditionalBorder.REST);
        elementList = pgu.getElements();
        handleExplicitHeight(pgu.getCell().getBlockProgressionDimension().toMinOptMax(tableLM),
                row.getExplicitHeight());
        knuthIter = elementList.listIterator();
        includedLength = -1;  // Avoid troubles with cells having content of zero length
        totalLength = previousRowsLength + ElementListUtils.calcContentLength(elementList);
        endRowIndex = rowIndex + pgu.getCell().getNumberRowsSpanned() - 1;
        keepWithNext = Keep.KEEP_AUTO;
        remainingLength = totalLength - previousRowsLength;

        afterNextStep = new Step(previousRowsLength);
        previousStep = new Step(afterNextStep);
        gotoNextLegalBreak();
        nextStep = new Step(afterNextStep);
        if (afterNextStep.end < elementList.size() - 1) {
            gotoNextLegalBreak();
        }
    }

    /**
     * Modifies the cell's element list by putting filler elements, so that the cell's or
     * row's explicit height is always reached.
     *
     * TODO this will work properly only for the first break. Then the limitation
     * explained on http://wiki.apache.org/xmlgraphics-fop/TableLayout/KnownProblems
     * occurs. The list of elements needs to be re-adjusted after each break.
     */
    private void handleExplicitHeight(MinOptMax cellBPD, MinOptMax rowBPD) {
        int minBPD = Math.max(cellBPD.getMin(), rowBPD.getMin());
        if (minBPD > 0) {
            int cumulateLength = 0;
            boolean prevIsBox = false;
            for (int i = 0; i < elementList.size() && cumulateLength < minBPD; i++) {
                KnuthElement el = (KnuthElement) elementList.get(i);
                if (el.isBox()) {
                    prevIsBox = true;
                    cumulateLength += el.getWidth();
                } else if (el.isGlue()) {
                    if (prevIsBox) {
                        elementList.add(i, new FillerPenalty(minBPD - cumulateLength));
                    }
                    prevIsBox = false;
                    cumulateLength += el.getWidth();
                } else {
                    prevIsBox = false;
                    if (cumulateLength + el.getWidth() < minBPD) {
                        elementList.set(i, new FillerPenalty((KnuthPenalty) el, minBPD - cumulateLength));
                    }
                }
            }
        }
        int optBPD = Math.max(minBPD, Math.max(cellBPD.getOpt(), rowBPD.getOpt()));
        if (pgu.getContentLength() < optBPD) {
            elementList.add(new FillerBox(optBPD - pgu.getContentLength()));
        }
    }

    PrimaryGridUnit getPrimaryGridUnit() {
        return pgu;
    }

    /**
     * Returns true if this cell ends on the given row.
     *
     * @param rowIndex index of a row in the row-group, zero-based
     * @return true if this cell ends on the given row
     */
    boolean endsOnRow(int rowIndex) {
        return rowIndex == endRowIndex;
    }

    /**
     * Returns the length of this cell's content not yet included in the steps, plus the
     * cell's borders and paddings if applicable.
     *
     * @return the remaining length, zero if the cell is finished
     */
    int getRemainingLength() {
        if (includedInLastStep() && (nextStep.end == elementList.size() - 1)) {
            // The cell is finished
            return 0;
        } else {
            return bpBeforeLeading + remainingLength + bpAfterNormal;
        }
    }

    private void gotoNextLegalBreak() {
        afterNextStep.penaltyLength = 0;
        afterNextStep.penaltyValue = 0;
        afterNextStep.condBeforeContentLength = 0;
        afterNextStep.breakClass = Constants.EN_AUTO;
        if (afterNextStep.footnoteList != null) {
            afterNextStep.footnoteList.clear();
        }
        boolean breakFound = false;
        boolean prevIsBox = false;
        boolean boxFound = false;
        while (!breakFound && knuthIter.hasNext()) {
            KnuthElement el = (KnuthElement) knuthIter.next();
            if (el.isPenalty()) {
                prevIsBox = false;
                if (el.getPenalty() < KnuthElement.INFINITE
                        || ((KnuthPenalty) el).getBreakClass() == Constants.EN_PAGE) {
                    // TODO too much is being done in that test, only to handle
                    // keep.within-column properly.

                    // First legal break point
                    breakFound = true;
                    KnuthPenalty p = (KnuthPenalty) el;
                    afterNextStep.penaltyLength = p.getWidth();
                    afterNextStep.penaltyValue = p.getPenalty();
                    if (p.isForcedBreak()) {
                        afterNextStep.breakClass = p.getBreakClass();
                    }
                }
            } else if (el.isGlue()) {
                if (prevIsBox) {
                    // Second legal break point
                    breakFound = true;
                } else {
                    afterNextStep.contentLength += el.getWidth();
                    if (!boxFound) {
                        afterNextStep.condBeforeContentLength += el.getWidth();
                    }
                }
                prevIsBox = false;
            } else {
                if (el instanceof KnuthBlockBox && ((KnuthBlockBox) el).hasAnchors()) {
                    if (afterNextStep.footnoteList == null) {
                        afterNextStep.footnoteList = new LinkedList();
                    }
                    afterNextStep.footnoteList.addAll(((KnuthBlockBox) el).getFootnoteBodyLMs());
                }
                prevIsBox = true;
                boxFound = true;
                afterNextStep.contentLength += el.getWidth();
            }
        }
        afterNextStep.end = knuthIter.nextIndex() - 1;
        afterNextStep.totalLength = bpBeforeNormal
                + afterNextStep.contentLength + afterNextStep.penaltyLength
                + bpAfterTrailing;
    }

    /**
     * Returns the minimal step that is needed for this cell to contribute some content.
     *
     * @return the step for this cell's first legal break
     */
    int getFirstStep() {
        log.debug(this + ": min first step = " + nextStep.totalLength);
        return nextStep.totalLength;
    }

    /**
     * Returns the last step for this cell. This includes the normal border- and
     * padding-before, the whole content, the normal padding-after, and the
     * <em>trailing</em> after border. Indeed, if the normal border is taken instead,
     * and appears to be smaller than the trailing one, the last step may be smaller than
     * the current step (see TableStepper#considerRowLastStep). This will produce a wrong
     * infinite penalty, plus the cell's content won't be taken into account since the
     * final step will be smaller than the current one (see {@link #signalNextStep(int)}).
     * This actually means that the content will be swallowed.
     *
     * @return the length of last step
     */
    int getLastStep() {
        assert nextStep.end == elementList.size() - 1;
        assert nextStep.contentLength == totalLength && nextStep.penaltyLength == 0;
        int lastStep = bpBeforeNormal + totalLength + paddingAfterNormal
                + pgu.getAfterBorderWidth(ConditionalBorder.LEADING_TRAILING);
        log.debug(this + ": last step = " + lastStep);
        return lastStep;
    }

    /**
     * Increases the next step up to the given limit.
     *
     * @param limit the length up to which the next step is allowed to increase
     * @see #signalRowFirstStep(int)
     * @see #signalRowLastStep(int)
     */
    private void increaseCurrentStep(int limit) {
        if (nextStep.end < elementList.size() - 1) {
            while (afterNextStep.totalLength <= limit && nextStep.breakClass == Constants.EN_AUTO) {
                int condBeforeContentLength = nextStep.condBeforeContentLength;
                nextStep.set(afterNextStep);
                nextStep.condBeforeContentLength = condBeforeContentLength;
                if (afterNextStep.end >= elementList.size() - 1) {
                    break;
                }
                gotoNextLegalBreak();
            }
        }
    }

    /**
     * Gets the selected first step for the current row. If this cell's first step is
     * smaller, then it may be able to add some more of its content, since there will be
     * no break before the given step anyway.
     *
     * @param firstStep the current row's first step
     */
    void signalRowFirstStep(int firstStep) {
        increaseCurrentStep(firstStep);
        if (log.isTraceEnabled()) {
            log.trace(this + ": first step increased to " + nextStep.totalLength);
        }
    }

    /** See {@link #signalRowFirstStep(int)}. */
    void signalRowLastStep(int lastStep) {
        increaseCurrentStep(lastStep);
        if (log.isTraceEnabled()) {
            log.trace(this + ": next step increased to " + nextStep.totalLength);
        }
    }

    /**
     * Returns the total length up to the next legal break, not yet included in the steps.
     *
     * @return the total length up to the next legal break (-1 signals no further step)
     */
    int getNextStep() {
        if (includedInLastStep()) {
            previousStep.set(nextStep);
            if (nextStep.end >= elementList.size() - 1) {
                nextStep.start = elementList.size();
                return -1;
            } else {
                nextStep.set(afterNextStep);
                nextStep.start = previousStep.end + 1;
                afterNextStep.start = nextStep.start;
                if (afterNextStep.end < elementList.size() - 1) {
                    gotoNextLegalBreak();
                }
            }
        }
        return nextStep.totalLength;
    }

    private boolean includedInLastStep() {
        return includedLength == nextStep.contentLength;
    }

    /**
     * Signals the length of the chosen next step, so that this cell determines whether
     * its own step may be included or not.
     *
     * @param minStep length of the chosen next step
     * @return the break class of the step, if any. One of {@link Constants#EN_AUTO},
     * {@link Constants#EN_COLUMN}, {@link Constants#EN_PAGE},
     * {@link Constants#EN_EVEN_PAGE}, {@link Constants#EN_ODD_PAGE}. EN_AUTO if this
     * cell's step is not included in the next step.
     */
    int signalNextStep(int minStep) {
        if (nextStep.totalLength <= minStep) {
            includedLength = nextStep.contentLength;
            remainingLength = totalLength - includedLength - afterNextStep.condBeforeContentLength;
            return nextStep.breakClass;
        } else {
            return Constants.EN_AUTO;
        }
    }

    /**
     * Receives indication that the next row is about to start, and that (collapse)
     * borders must be updated accordingly.
     */
    void nextRowStarts() {
        spanIndex++;
        // Subtract the old value of bpAfterTrailing...
        nextStep.totalLength -= bpAfterTrailing;
        afterNextStep.totalLength -= bpAfterTrailing;

        bpAfterTrailing = paddingAfterTrailing
                + pgu.getAfterBorderWidth(spanIndex, ConditionalBorder.REST);

        // ... and add the new one
        nextStep.totalLength += bpAfterTrailing;
        afterNextStep.totalLength += bpAfterTrailing;
        // TODO if the new after border is greater than the previous one the next step may
        // increase further than the row's first step, which can lead to wrong output in
        // some cases
    }

    /**
     * Receives indication that the current row is ending, and that (collapse) borders
     * must be updated accordingly.
     *
     * @param rowIndex the index of the ending row
     */
    void endRow(int rowIndex) {
        if (endsOnRow(rowIndex)) {
            // Subtract the old value of bpAfterTrailing...
            nextStep.totalLength -= bpAfterTrailing;
            bpAfterTrailing = paddingAfterNormal
                    + pgu.getAfterBorderWidth(ConditionalBorder.LEADING_TRAILING);
            // ... and add the new one
            nextStep.totalLength += bpAfterTrailing;
            lastCellPart = true;
        } else {
            bpBeforeLeading = paddingBeforeLeading
                    + pgu.getBeforeBorderWidth(spanIndex + 1, ConditionalBorder.REST);
        }
    }

    /**
     * Returns true if this cell would be finished after the given step. That is, it would
     * be included in the step and the end of its content would be reached.
     *
     * @param step the next step
     * @return true if this cell finishes at the given step
     */
    boolean finishes(int step) {
        return nextStep.totalLength <= step && (nextStep.end == elementList.size() - 1);
    }

    /**
     * Creates and returns a CellPart instance for the content of this cell which
     * is included in the next step.
     *
     * @return a CellPart instance
     */
    CellPart createCellPart() {
        if (nextStep.end + 1 == elementList.size()) {
            keepWithNext = pgu.getKeepWithNext();
            // TODO if keep-with-next is set on the row, must every cell of the row
            // contribute some content from children blocks?
            // see http://mail-archives.apache.org/mod_mbox/xmlgraphics-fop-dev/200802.mbox/
            // %3c47BDA379.4050606@anyware-tech.com%3e
            // Assuming no, but if yes the following code should enable this behaviour
//            if (pgu.getRow() != null && pgu.getRow().mustKeepWithNext()) {
//                keepWithNextSignal = true; //to be converted to integer strengths
//            }
        }
        int bpBeforeFirst;
        if (nextStep.start == 0) {
            bpBeforeFirst = pgu.getBeforeBorderWidth(0, ConditionalBorder.LEADING_TRAILING)
                    + paddingBeforeNormal;
        } else {
            bpBeforeFirst = bpBeforeLeading;
        }
        int length = nextStep.contentLength - nextStep.condBeforeContentLength
                - previousStep.contentLength;
        if (!includedInLastStep() || nextStep.start == elementList.size()) {
            return new CellPart(pgu, nextStep.start, previousStep.end, lastCellPart,
                    0, 0, previousStep.penaltyLength,
                    bpBeforeNormal, bpBeforeFirst, bpAfterNormal, bpAfterTrailing);
        } else {
            return new CellPart(pgu, nextStep.start, nextStep.end, lastCellPart,
                    nextStep.condBeforeContentLength, length, nextStep.penaltyLength,
                    bpBeforeNormal, bpBeforeFirst, bpAfterNormal, bpAfterTrailing);
        }
    }

    /**
     * Adds the footnotes (if any) that are part of the next step, if this cell
     * contributes content to the next step.
     *
     * @param footnoteList the list to which this cell must add its footnotes
     */
    void addFootnotes(List footnoteList) {
        if (includedInLastStep() && nextStep.footnoteList != null) {
            footnoteList.addAll(nextStep.footnoteList);
            nextStep.footnoteList.clear();
        }
    }

    Keep getKeepWithNext() {
        return keepWithNext;
    }

    int getPenaltyValue() {
        if (includedInLastStep()) {
            return nextStep.penaltyValue;
        } else {
            return previousStep.penaltyValue;
        }
    }

    /** {@inheritDoc} */
    public String toString() {
        return "Cell " + (pgu.getRowIndex() + 1) + "." + (pgu.getColIndex() + 1);
    }
}
