/*
 * 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: RowPainter.java 1835810 2018-07-13 10:29:57Z ssteiner $ */

package org.apache.fop.layoutmgr.table;

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

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

import org.apache.fop.area.Block;
import org.apache.fop.area.Trait;
import org.apache.fop.fo.flow.table.ConditionalBorder;
import org.apache.fop.fo.flow.table.EffRow;
import org.apache.fop.fo.flow.table.EmptyGridUnit;
import org.apache.fop.fo.flow.table.GridUnit;
import org.apache.fop.fo.flow.table.PrimaryGridUnit;
import org.apache.fop.fo.flow.table.Table;
import org.apache.fop.fo.flow.table.TableColumn;
import org.apache.fop.fo.flow.table.TablePart;
import org.apache.fop.fo.properties.CommonBorderPaddingBackground;
import org.apache.fop.fo.properties.CommonBorderPaddingBackground.BorderInfo;
import org.apache.fop.layoutmgr.ElementListUtils;
import org.apache.fop.layoutmgr.KnuthElement;
import org.apache.fop.layoutmgr.KnuthPossPosIter;
import org.apache.fop.layoutmgr.LayoutContext;
import org.apache.fop.layoutmgr.SpaceResolver;
import org.apache.fop.layoutmgr.TraitSetter;

class RowPainter {
    private static Log log = LogFactory.getLog(RowPainter.class);
    private int colCount;
    private int currentRowOffset;
    /** Currently handled row (= last encountered row). */
    private EffRow currentRow;
    private LayoutContext layoutContext;
    /**
     * Index of the first row of the current part present on the current page.
     */
    private int firstRowIndex;

    /**
     * Index of the very first row on the current page. Needed to properly handle
     * {@link BorderProps#COLLAPSE_OUTER}. This is not the same as {@link #firstRowIndex}
     * when the table has headers!
     */
    private int firstRowOnPageIndex;

    /**
     * Keeps track of the y-offsets of each row on a page.
     * This is particularly needed for spanned cells where you need to know the y-offset
     * of the starting row when the area is generated at the time the cell is closed.
     */
    private List rowOffsets = new ArrayList();

    private int[] cellHeights;
    private boolean[] firstCellOnPage;
    private CellPart[] firstCellParts;
    private CellPart[] lastCellParts;

    /** y-offset of the current table part. */
    private int tablePartOffset;
    /** See {@link RowPainter#registerPartBackgroundArea(Block)}. */
    private CommonBorderPaddingBackground tablePartBackground;
    /** See {@link RowPainter#registerPartBackgroundArea(Block)}. */
    private List tablePartBackgroundAreas;

    private TableContentLayoutManager tclm;

    RowPainter(TableContentLayoutManager tclm, LayoutContext layoutContext) {
        this.tclm = tclm;
        this.layoutContext = layoutContext;
        this.colCount = tclm.getColumns().getColumnCount();
        this.cellHeights = new int[colCount];
        this.firstCellOnPage = new boolean[colCount];
        this.firstCellParts = new CellPart[colCount];
        this.lastCellParts = new CellPart[colCount];
        this.firstRowIndex = -1;
        this.firstRowOnPageIndex = -1;
    }

    void startTablePart(TablePart tablePart) {
        CommonBorderPaddingBackground background = tablePart.getCommonBorderPaddingBackground();
        if (background.hasBackground()) {
            tablePartBackground = background;
            if (tablePartBackgroundAreas == null) {
                tablePartBackgroundAreas = new ArrayList();
            }
        }
        tablePartOffset = currentRowOffset;
    }

    /**
     * Signals that the end of the current table part is reached.
     *
     * @param lastInBody true if the part is the last table-body element to be displayed
     * on the current page. In which case all the cells must be flushed even if they
     * aren't finished, plus the proper collapsed borders must be selected (trailing
     * instead of normal, or rest if the cell is unfinished)
     * @param lastOnPage true if the part is the last to be displayed on the current page.
     * In which case collapsed after borders for the cells on the last row must be drawn
     * in the outer mode
     */
    void endTablePart(boolean lastInBody, boolean lastOnPage) {
        addAreasAndFlushRow(lastInBody, lastOnPage);

        if (tablePartBackground != null) {
            TableLayoutManager tableLM = tclm.getTableLM();
            for (Object tablePartBackgroundArea : tablePartBackgroundAreas) {
                Block backgroundArea = (Block) tablePartBackgroundArea;
                TraitSetter.addBackground(backgroundArea, tablePartBackground, tableLM,
                        -backgroundArea.getXOffset(), tablePartOffset - backgroundArea.getYOffset(),
                        tableLM.getContentAreaIPD(), currentRowOffset - tablePartOffset);
            }
            tablePartBackground = null;
            tablePartBackgroundAreas.clear();
        }
    }

    int getAccumulatedBPD() {
        return currentRowOffset;
    }

    /**
     * Records the fragment of row represented by the given position. If it belongs to
     * another (grid) row than the current one, that latter is painted and flushed first.
     *
     * @param tcpos a position representing the row fragment
     */
    void handleTableContentPosition(TableContentPosition tcpos) {
        if (log.isDebugEnabled()) {
            log.debug("===handleTableContentPosition(" + tcpos);
        }
        if (currentRow == null) {
            currentRow = tcpos.getNewPageRow();
        } else {
            EffRow row = tcpos.getRow();
            if (row.getIndex() > currentRow.getIndex()) {
                addAreasAndFlushRow(false, false);
                currentRow = row;
            }
        }
        if (firstRowIndex < 0) {
            firstRowIndex = currentRow.getIndex();
            if (firstRowOnPageIndex < 0) {
                firstRowOnPageIndex = firstRowIndex;
            }
        }
        //Iterate over all grid units in the current step
        for (Object cellPart1 : tcpos.cellParts) {
            CellPart cellPart = (CellPart) cellPart1;
            if (log.isDebugEnabled()) {
                log.debug(">" + cellPart);
            }
            int colIndex = cellPart.pgu.getColIndex();
            if (firstCellParts[colIndex] == null) {
                firstCellParts[colIndex] = cellPart;
                cellHeights[colIndex] = cellPart.getBorderPaddingBefore(firstCellOnPage[colIndex]);
            } else {
                assert firstCellParts[colIndex].pgu == cellPart.pgu;
                cellHeights[colIndex] += cellPart.getConditionalBeforeContentLength();
            }
            cellHeights[colIndex] += cellPart.getLength();
            lastCellParts[colIndex] = cellPart;
        }
    }

    /**
     * Creates the areas corresponding to the last row. That is, an area with background
     * for the row, plus areas for all the cells that finish on the row (not spanning over
     * further rows).
     *
     * @param lastInPart true if the row is the last from its table part to be displayed
     * on the current page. In which case all the cells must be flushed even if they
     * aren't finished, plus the proper collapsed borders must be selected (trailing
     * instead of normal, or rest if the cell is unfinished)
     * @param lastOnPage true if the row is the very last row of the table that will be
     * displayed on the current page. In which case collapsed after borders must be drawn
     * in the outer mode
     */
    private void addAreasAndFlushRow(boolean lastInPart, boolean lastOnPage) {
        if (log.isDebugEnabled()) {
            log.debug("Remembering yoffset for row " + currentRow.getIndex() + ": "
                    + currentRowOffset);
        }
        recordRowOffset(currentRow.getIndex(), currentRowOffset);

        // Need to compute the actual row height first
        // and determine border behaviour for empty cells
        boolean firstCellPart = true;
        boolean lastCellPart = true;
        int actualRowHeight = 0;
        for (int i = 0; i < colCount; i++) {
            GridUnit currentGU = currentRow.getGridUnit(i);
            if (currentGU.isEmpty()) {
                continue;
            }
            if (currentGU.getColSpanIndex() == 0
                    && (lastInPart || currentGU.isLastGridUnitRowSpan())
                    && firstCellParts[i] != null) {
                // TODO
                // The last test above is a workaround for the stepping algorithm's
                // fundamental flaw making it unable to produce the right element list for
                // multiple breaks inside a same row group.
                // (see http://wiki.apache.org/xmlgraphics-fop/TableLayout/KnownProblems)
                // In some extremely rare cases (forced breaks, very small page height), a
                // TableContentPosition produced during row delaying may end up alone on a
                // page. It will not contain the CellPart instances for the cells starting
                // the next row, so firstCellParts[i] will still be null for those ones.
                int cellHeight = cellHeights[i];
                cellHeight += lastCellParts[i].getConditionalAfterContentLength();
                cellHeight += lastCellParts[i].getBorderPaddingAfter(lastInPart);
                int cellOffset = getRowOffset(Math.max(firstCellParts[i].pgu.getRowIndex(),
                        firstRowIndex));
                actualRowHeight = Math.max(actualRowHeight, cellOffset + cellHeight
                        - currentRowOffset);
            }

            if (firstCellParts[i] != null && !firstCellParts[i].isFirstPart()) {
                firstCellPart = false;
            }
            if (lastCellParts[i] != null && !lastCellParts[i].isLastPart()) {
                lastCellPart = false;
            }
        }

        // Then add areas for cells finishing on the current row
        for (int i = 0; i < colCount; i++) {
            GridUnit currentGU = currentRow.getGridUnit(i);
            if (currentGU.isEmpty() && !tclm.isSeparateBorderModel()) {
                int borderBeforeWhich;
                if (firstCellPart) {
                    if (firstCellOnPage[i]) {
                        borderBeforeWhich = ConditionalBorder.LEADING_TRAILING;
                    } else {
                        borderBeforeWhich = ConditionalBorder.NORMAL;
                    }
                } else {
                    borderBeforeWhich = ConditionalBorder.REST;
                }
                int borderAfterWhich;
                if (lastCellPart) {
                    if (lastInPart) {
                        borderAfterWhich = ConditionalBorder.LEADING_TRAILING;
                    } else {
                        borderAfterWhich = ConditionalBorder.NORMAL;
                    }
                } else {
                    borderAfterWhich = ConditionalBorder.REST;
                }
                assert (currentGU instanceof EmptyGridUnit);
                addAreaForEmptyGridUnit((EmptyGridUnit)currentGU,
                        currentRow.getIndex(), i,
                        actualRowHeight,
                        borderBeforeWhich, borderAfterWhich,
                        lastOnPage);

                firstCellOnPage[i] = false;
            } else if (currentGU.getColSpanIndex() == 0
                    && (lastInPart || currentGU.isLastGridUnitRowSpan())
                    && firstCellParts[i] != null) {
                assert firstCellParts[i].pgu == currentGU.getPrimary();

                int borderBeforeWhich;
                if (firstCellParts[i].isFirstPart()) {
                    if (firstCellOnPage[i]) {
                        borderBeforeWhich = ConditionalBorder.LEADING_TRAILING;
                    } else {
                        borderBeforeWhich = ConditionalBorder.NORMAL;
                    }
                } else {
                    assert firstCellOnPage[i];
                    borderBeforeWhich = ConditionalBorder.REST;
                }
                int borderAfterWhich;
                if (lastCellParts[i].isLastPart()) {
                    if (lastInPart) {
                        borderAfterWhich = ConditionalBorder.LEADING_TRAILING;
                    } else {
                        borderAfterWhich = ConditionalBorder.NORMAL;
                    }
                } else {
                    borderAfterWhich = ConditionalBorder.REST;
                }

                // when adding the areas for the TableCellLayoutManager this helps with the isLast trait
                // if, say, the first cell of a row has content that fits in the page, but the content of
                // the second cell does not fit this will assure that the isLast trait for the first cell
                // will also be false
                lastCellParts[i].pgu.getCellLM().setLastTrait(lastCellParts[i].isLastPart());
                addAreasForCell(firstCellParts[i].pgu,
                        firstCellParts[i].start, lastCellParts[i].end,
                        actualRowHeight, borderBeforeWhich, borderAfterWhich,
                        lastOnPage);
                firstCellParts[i] = null; // why? what about the lastCellParts[i]?
                Arrays.fill(firstCellOnPage, i, i + currentGU.getCell().getNumberColumnsSpanned(),
                        false);
            }
        }
        currentRowOffset += actualRowHeight;
        if (lastInPart) {
            /*
             * Either the end of the page is reached, then this was the last call of this
             * method and we no longer care about currentRow; or the end of a table-part
             * (header, footer, body) has been reached, and the next row will anyway be
             * different from the current one, and this is unnecessary to call this method
             * again in the first lines of handleTableContentPosition, so we may reset the
             * following variables.
             */
            currentRow = null;
            firstRowIndex = -1;
            rowOffsets.clear();
            /*
             * The current table part has just been handled. Be it the first one or not,
             * the header or the body, in any case the borders-before of the next row
             * (i.e., the first row of the next part if any) must be painted in
             * COLLAPSE_INNER mode. So the firstRowOnPageIndex indicator must be kept
             * disabled. The following way is not the most elegant one but will be good
             * enough.
             */
            firstRowOnPageIndex = Integer.MAX_VALUE;
        }
    }

    // TODO this is not very efficient and should probably be done another way
    // this method is only necessary when display-align = center or after, in which case
    // the exact content length is needed to compute the size of the empty block that will
    // be used as padding.
    // This should be handled automatically by a proper use of Knuth elements
    private int computeContentLength(PrimaryGridUnit pgu, int startIndex, int endIndex) {
        if (startIndex > endIndex) {
             // May happen if the cell contributes no content on the current page (empty
             // cell, in most cases)
            return 0;
        } else {
            ListIterator iter = pgu.getElements().listIterator(startIndex);
            // Skip from the content length calculation glues and penalties occurring at the
            // beginning of the page
            boolean nextIsBox = false;
            while (iter.nextIndex() <= endIndex && !nextIsBox) {
                nextIsBox = ((KnuthElement) iter.next()).isBox();
            }
            int len = 0;
            if (((KnuthElement) iter.previous()).isBox()) {
                while (iter.nextIndex() < endIndex) {
                    KnuthElement el = (KnuthElement) iter.next();
                    if (el.isBox() || el.isGlue()) {
                        len += el.getWidth();
                    }
                }
                len += ActiveCell.getElementContentLength((KnuthElement) iter.next());
            }
            return len;
        }
    }

    private void addAreasForCell(PrimaryGridUnit pgu, int startPos, int endPos,
            int rowHeight, int borderBeforeWhich, int borderAfterWhich, boolean lastOnPage) {
        /*
         * Determine the index of the first row of this cell that will be displayed on the
         * current page.
         */
        int currentRowIndex = currentRow.getIndex();
        int startRowIndex;
        int firstRowHeight;
        if (pgu.getRowIndex() >= firstRowIndex) {
            startRowIndex = pgu.getRowIndex();
            if (startRowIndex < currentRowIndex) {
                firstRowHeight = getRowOffset(startRowIndex + 1) - getRowOffset(startRowIndex);
            } else {
                firstRowHeight = rowHeight;
            }
        } else {
            startRowIndex = firstRowIndex;
            firstRowHeight = 0;
        }

        /*
         * In collapsing-border model, if the cell spans over several columns/rows then
         * dedicated areas will be created for each grid unit to hold the corresponding
         * borders. For that we need to know the height of each grid unit, that is of each
         * grid row spanned over by the cell
         */
        int[] spannedGridRowHeights = null;
        if (!tclm.getTableLM().getTable().isSeparateBorderModel() && pgu.hasSpanning()) {
            spannedGridRowHeights = new int[currentRowIndex - startRowIndex + 1];
            int prevOffset = getRowOffset(startRowIndex);
            for (int i = 0; i < currentRowIndex - startRowIndex; i++) {
                int newOffset = getRowOffset(startRowIndex + i + 1);
                spannedGridRowHeights[i] = newOffset - prevOffset;
                prevOffset = newOffset;
            }
            spannedGridRowHeights[currentRowIndex - startRowIndex] = rowHeight;
        }
        int cellOffset = getRowOffset(startRowIndex);
        int cellTotalHeight = rowHeight + currentRowOffset - cellOffset;
        if (log.isDebugEnabled()) {
            log.debug("Creating area for cell:");
            log.debug("  start row: " + pgu.getRowIndex() + " " + currentRowOffset + " "
                    + cellOffset);
            log.debug(" rowHeight=" + rowHeight + " cellTotalHeight=" + cellTotalHeight);
        }
        TableCellLayoutManager cellLM = pgu.getCellLM();
        cellLM.setXOffset(tclm.getXOffsetOfGridUnit(pgu));
        cellLM.setYOffset(cellOffset);
        cellLM.setContentHeight(computeContentLength(pgu, startPos, endPos));
        cellLM.setTotalHeight(cellTotalHeight);
        int prevBreak = ElementListUtils.determinePreviousBreak(pgu.getElements(), startPos);
        if (endPos >= 0) {
            SpaceResolver.performConditionalsNotification(pgu.getElements(),
                    startPos, endPos, prevBreak);
        }
        cellLM.addAreas(new KnuthPossPosIter(pgu.getElements(), startPos, endPos + 1),
                layoutContext, spannedGridRowHeights, startRowIndex - pgu.getRowIndex(),
                currentRowIndex - pgu.getRowIndex(), borderBeforeWhich, borderAfterWhich,
                startRowIndex == firstRowOnPageIndex, lastOnPage, this, firstRowHeight);
    }

    private void addAreaForEmptyGridUnit(EmptyGridUnit gu, int rowIndex, int colIndex,
            int actualRowHeight,
            int borderBeforeWhich, int borderAfterWhich, boolean lastOnPage) {

        //get effective borders
        BorderInfo borderBefore = gu.getBorderBefore(borderBeforeWhich);
        BorderInfo borderAfter = gu.getBorderAfter(borderAfterWhich);
        BorderInfo borderStart = gu.getBorderStart();
        BorderInfo borderEnd = gu.getBorderEnd();
        if (borderBefore.getRetainedWidth() == 0
                && borderAfter.getRetainedWidth() == 0
                && borderStart.getRetainedWidth() == 0
                && borderEnd.getRetainedWidth() == 0) {
            return; //no borders, no area necessary
        }

        TableLayoutManager tableLM = tclm.getTableLM();
        Table table = tableLM.getTable();
        TableColumn col = tclm.getColumns().getColumn(colIndex + 1);

        //position information
        boolean firstOnPage = (rowIndex == firstRowOnPageIndex);
        boolean inFirstColumn = (colIndex == 0);
        boolean inLastColumn = (colIndex == table.getNumberOfColumns() - 1);

        //determine the block area's size
        int ipd = col.getColumnWidth().getValue(tableLM);
        ipd -= (borderStart.getRetainedWidth() + borderEnd.getRetainedWidth()) / 2;
        int bpd = actualRowHeight;
        bpd -= (borderBefore.getRetainedWidth() + borderAfter.getRetainedWidth()) / 2;

        //generate the block area
        Block block = new Block();
        block.setChangeBarList(tclm.getTableLM().getFObj().getChangeBarList());
        block.setPositioning(Block.ABSOLUTE);
        block.addTrait(Trait.IS_REFERENCE_AREA, Boolean.TRUE);
        block.setIPD(ipd);
        block.setBPD(bpd);
        block.setXOffset(tclm.getXOffsetOfGridUnit(colIndex, 1)
                + (borderStart.getRetainedWidth() / 2));
        block.setYOffset(getRowOffset(rowIndex)
                - (borderBefore.getRetainedWidth() / 2));
        boolean[] outer = new boolean[] {firstOnPage, lastOnPage, inFirstColumn,
                inLastColumn};
        TraitSetter.addCollapsingBorders(block,
                borderBefore,
                borderAfter,
                borderStart,
                borderEnd, outer);
        tableLM.addChildArea(block);
    }

    /**
     * Registers the given area, that will be used to render the part of
     * table-header/footer/body background covered by a table-cell. If percentages are
     * used to place the background image, the final bpd of the (fraction of) table part
     * that will be rendered on the current page must be known. The traits can't then be
     * set when the areas for the cell are created since at that moment this bpd is yet
     * unknown. So they will instead be set in
     * {@link #addAreasAndFlushRow(boolean, boolean)}.
     *
     * @param backgroundArea the block of the cell's dimensions that will hold the part
     * background
     */
    void registerPartBackgroundArea(Block backgroundArea) {
        tclm.getTableLM().addBackgroundArea(backgroundArea);
        tablePartBackgroundAreas.add(backgroundArea);
    }

    /**
     * Records the y-offset of the row with the given index.
     *
     * @param rowIndex index of the row
     * @param offset y-offset of the row on the page
     */
    private void recordRowOffset(int rowIndex, int offset) {
        /*
         * In some very rare cases a row may be skipped. See for example Bugzilla #43633:
         * in a two-column table, a row contains a row-spanning cell and a missing cell.
         * In TableStepper#goToNextRowIfCurrentFinished this row will immediately be
         * considered as finished, since it contains no cell ending on this row. Thus no
         * TableContentPosition will be created for this row. Thus its index will never be
         * recorded by the #handleTableContentPosition method.
         *
         * The offset of such a row is the same as the next non-empty row. It's needed
         * to correctly offset blocks for cells starting on this row. Hence the loop
         * below.
         */
        for (int i = rowOffsets.size(); i <= rowIndex - firstRowIndex; i++) {
            rowOffsets.add(offset);
        }
    }

    /**
     * Returns the offset of the row with the given index.
     *
     * @param rowIndex index of the row
     * @return its y-offset on the page
     */
    private int getRowOffset(int rowIndex) {
        return (Integer) rowOffsets.get(rowIndex - firstRowIndex);
    }

    // TODO get rid of that
    /** Signals that the first table-body instance has started. */
    void startBody() {
        Arrays.fill(firstCellOnPage, true);
    }

    // TODO get rid of that
    /** Signals that the last table-body instance has ended. */
    void endBody() {
        Arrays.fill(firstCellOnPage, false);
    }
}
