/*
 * 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: CollapsingBorderResolver.java 1762060 2016-09-23 12:57:46Z ssteiner $ */

package org.apache.fop.fo.flow.table;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.apache.fop.fo.properties.CommonBorderPaddingBackground;
import org.apache.fop.layoutmgr.table.CollapsingBorderModel;

/**
 * A class that implements the border-collapsing model.
 */
class CollapsingBorderResolver implements BorderResolver {

    private Table table;

    private CollapsingBorderModel collapsingBorderModel;

    /**
     * The previously registered row, either in the header or the body(-ies), but not in
     * the footer (handled separately).
     */
    private List<GridUnit> previousRow;

    private boolean firstInTable;

    private List<GridUnit> footerFirstRow;

    /** The last currently registered footer row. */
    private List<GridUnit> footerLastRow;

    private Resolver delegate;

    // Re-use the same ResolverInBody for every table-body
    // Important to properly handle firstInBody!!
    private Resolver resolverInBody = new ResolverInBody();

    private Resolver resolverInFooter;

    private List<ConditionalBorder> leadingBorders;

    private List<ConditionalBorder> trailingBorders;

    /* TODO Temporary hack for resolved borders in header */
    /* Currently the normal border is always used. */
    private List<GridUnit> headerLastRow;
    /* End of temporary hack */

    /**
     * Base class for delegate resolvers. Implementation of the State design pattern: the
     * treatment differs slightly whether we are in the table's header, footer or body. To
     * avoid complicated if statements, specialised delegate resolvers will be used
     * instead.
     */
    private abstract class Resolver {

        protected TablePart tablePart;

        protected boolean firstInPart;

        private BorderSpecification borderStartTableAndBody;
        private BorderSpecification borderEndTableAndBody;

        /**
         * Integrates border-before specified on the table and its column.
         *
         * @param row the first row of the table (in the header, or in the body if the
         * table has no header)
         * @param withNormal
         * @param withLeadingTrailing
         * @param withRest
         */
        void resolveBordersFirstRowInTable(List<GridUnit> row, boolean withNormal,
                boolean withLeadingTrailing, boolean withRest) {
            assert firstInTable;
            for (int i = 0; i < row.size(); i++) {
                TableColumn column = table.getColumn(i);
                row.get(i).integrateBorderSegment(
                        CommonBorderPaddingBackground.BEFORE, column, withNormal,
                        withLeadingTrailing, withRest);
            }
            firstInTable = false;
        }

        /**
         * Resolves border-after for the first row, border-before for the second one.
         *
         * @param rowBefore
         * @param rowAfter
         */
        void resolveBordersBetweenRows(List<GridUnit> rowBefore, List<GridUnit> rowAfter) {
            assert rowBefore != null && rowAfter != null;
            for (int i = 0; i < rowAfter.size(); i++) {
                GridUnit gu = rowAfter.get(i);
                if (gu.getRowSpanIndex() == 0) {
                    GridUnit beforeGU = rowBefore.get(i);
                    gu.resolveBorder(beforeGU, CommonBorderPaddingBackground.BEFORE);
                }
            }
        }

        /** Integrates the border-after of the part. */
        void resolveBordersLastRowInPart(List<GridUnit> row, boolean withNormal,
                boolean withLeadingTrailing, boolean withRest) {
            for (Object aRow : row) {
                ((GridUnit) aRow).integrateBorderSegment(CommonBorderPaddingBackground.AFTER,
                        tablePart, withNormal, withLeadingTrailing, withRest);
            }
        }

        /**
         * Integrates border-after specified on the table and its columns.
         *
         * @param row the last row of the footer, or of the last body if the table has no
         * footer
         * @param withNormal
         * @param withLeadingTrailing
         * @param withRest
         */
        void resolveBordersLastRowInTable(List<GridUnit> row, boolean withNormal,
                boolean withLeadingTrailing, boolean withRest) {
            for (int i = 0; i < row.size(); i++) {
                TableColumn column = table.getColumn(i);
                row.get(i).integrateBorderSegment(CommonBorderPaddingBackground.AFTER,
                        column, withNormal, withLeadingTrailing, withRest);
            }
        }

        /**
         * Integrates either border-before specified on the table and its columns if the
         * table has no header, or border-after specified on the cells of the header's
         * last row. For the case the grid unit are at the top of a page.
         *
         * @param row
         */
        void integrateLeadingBorders(List<GridUnit> row) {
            for (int i = 0; i < table.getNumberOfColumns(); i++) {
                GridUnit gu = row.get(i);
                ConditionalBorder border = leadingBorders.get(i);
                gu.integrateCompetingBorder(CommonBorderPaddingBackground.BEFORE, border,
                        false, true, true);
            }
        }

        /**
         * Integrates either border-after specified on the table and its columns if the
         * table has no footer, or border-before specified on the cells of the footer's
         * first row. For the case the grid unit are at the bottom of a page.
         *
         * @param row
         */
        void integrateTrailingBorders(List<GridUnit> row) {
            for (int i = 0; i < table.getNumberOfColumns(); i++) {
                GridUnit gu = row.get(i);
                ConditionalBorder border = trailingBorders.get(i);
                gu.integrateCompetingBorder(CommonBorderPaddingBackground.AFTER, border,
                        false, true, true);
            }
        }

        void startPart(TablePart part) {
            tablePart = part;
            firstInPart = true;
            borderStartTableAndBody = collapsingBorderModel.determineWinner(table.borderStart,
                    tablePart.borderStart);
            borderEndTableAndBody = collapsingBorderModel.determineWinner(table.borderEnd,
                    tablePart.borderEnd);
        }

        /**
         * Resolves the applicable borders for the given row.
         * <ul>
         * <li>Integrates the border-before/after of the containing table-row if any;</li>
         * <li>Integrates the border-before of the containing part, if first row;</li>
         * <li>Resolves border-start/end between grid units.</li>
         * </ul>
         *
         * @param row the row being finished
         * @param container the containing element
         */
        void endRow(List<GridUnit> row, TableCellContainer container) {
            BorderSpecification borderStart = borderStartTableAndBody;
            BorderSpecification borderEnd = borderEndTableAndBody;
            // Resolve before- and after-borders for the table-row
            if (container instanceof TableRow) {
                TableRow tableRow = (TableRow) container;
                for (Object aRow : row) {
                    GridUnit gu = (GridUnit) aRow;
                    boolean first = (gu.getRowSpanIndex() == 0);
                    boolean last = gu.isLastGridUnitRowSpan();
                    gu.integrateBorderSegment(CommonBorderPaddingBackground.BEFORE, tableRow,
                            first, first, true);
                    gu.integrateBorderSegment(CommonBorderPaddingBackground.AFTER, tableRow,
                            last, last, true);
                }
                borderStart = collapsingBorderModel.determineWinner(borderStart,
                        tableRow.borderStart);
                borderEnd = collapsingBorderModel.determineWinner(borderEnd,
                        tableRow.borderEnd);
            }
            if (firstInPart) {
                // Integrate the border-before of the part
                for (Object aRow : row) {
                    ((GridUnit) aRow).integrateBorderSegment(
                            CommonBorderPaddingBackground.BEFORE, tablePart, true, true, true);
                }
                firstInPart = false;
            }
            // Resolve start/end borders in the row
            Iterator guIter = row.iterator();
            GridUnit gu = (GridUnit) guIter.next();
            Iterator colIter = table.getColumns().iterator();
            TableColumn col = (TableColumn) colIter.next();
            gu.integrateBorderSegment(CommonBorderPaddingBackground.START, col);
            gu.integrateBorderSegment(CommonBorderPaddingBackground.START, borderStart);
            while (guIter.hasNext()) {
                GridUnit nextGU = (GridUnit) guIter.next();
                TableColumn nextCol = (TableColumn) colIter.next();
                if (gu.isLastGridUnitColSpan()) {
                    gu.integrateBorderSegment(CommonBorderPaddingBackground.END, col);
                    nextGU.integrateBorderSegment(CommonBorderPaddingBackground.START, nextCol);
                    gu.resolveBorder(nextGU, CommonBorderPaddingBackground.END);
                }
                gu = nextGU;
                col = nextCol;
            }
            gu.integrateBorderSegment(CommonBorderPaddingBackground.END, col);
            gu.integrateBorderSegment(CommonBorderPaddingBackground.END, borderEnd);
        }

        void endPart() {
            resolveBordersLastRowInPart(previousRow, true, true, true);
        }

        abstract void endTable();
    }

    private class ResolverInHeader extends Resolver {

        void endRow(List<GridUnit> row, TableCellContainer container) {
            super.endRow(row, container);
            if (previousRow != null) {
                resolveBordersBetweenRows(previousRow, row);
            } else {
                /*
                 * This is a bit hacky...
                 * The two only sensible values for border-before on the header's first row are:
                 * - at the beginning of the table (normal case)
                 * - if the header is repeated after each page break
                 * To represent those values we (ab)use the normal and the rest fields of
                 * ConditionalBorder. But strictly speaking this is not their purposes.
                 */
                for (Object aRow : row) {
                    ConditionalBorder borderBefore = ((GridUnit) aRow).borderBefore;
                    borderBefore.leadingTrailing = borderBefore.normal;
                    borderBefore.rest = borderBefore.normal;
                }
                resolveBordersFirstRowInTable(row, true, false, true);
            }
            previousRow = row;
        }

        void endPart() {
            super.endPart();
            leadingBorders = new ArrayList(table.getNumberOfColumns());
            /*
             * Another hack...
             * The border-after of a header is always the same. Leading and rest don't
             * apply to cells in the header since they are never broken. To ease
             * resolution we override the (normally unused) leadingTrailing and rest
             * fields of ConditionalBorder with the only sensible normal field. That way
             * grid units from the body will always resolve against the same, normal
             * header border.
             */
            for (Object aPreviousRow : previousRow) {
                ConditionalBorder borderAfter = ((GridUnit) aPreviousRow).borderAfter;
                borderAfter.leadingTrailing = borderAfter.normal;
                borderAfter.rest = borderAfter.normal;
                leadingBorders.add(borderAfter);
            }
            /* TODO Temporary hack for resolved borders in header */
            headerLastRow = previousRow;
            /* End of temporary hack */
        }

        void endTable() {
            throw new IllegalStateException();
        }
    }

    private class ResolverInFooter extends Resolver {

        void endRow(List<GridUnit> row, TableCellContainer container) {
            super.endRow(row, container);
            if (footerFirstRow == null) {
                footerFirstRow = row;
            } else {
                // There is a previous row
                resolveBordersBetweenRows(footerLastRow, row);
            }
            footerLastRow = row;
        }

        void endPart() {
            resolveBordersLastRowInPart(footerLastRow, true, true, true);
            trailingBorders = new ArrayList(table.getNumberOfColumns());
            // See same method in ResolverInHeader for an explanation of the hack
            for (Object aFooterFirstRow : footerFirstRow) {
                ConditionalBorder borderBefore = ((GridUnit) aFooterFirstRow).borderBefore;
                borderBefore.leadingTrailing = borderBefore.normal;
                borderBefore.rest = borderBefore.normal;
                trailingBorders.add(borderBefore);
            }
        }

        void endTable() {
            // Resolve after/before border between the last row of table-body and the
            // first row of table-footer
            resolveBordersBetweenRows(previousRow, footerFirstRow);
            // See endRow method in ResolverInHeader for an explanation of the hack
            for (Object aFooterLastRow : footerLastRow) {
                ConditionalBorder borderAfter = ((GridUnit) aFooterLastRow).borderAfter;
                borderAfter.leadingTrailing = borderAfter.normal;
                borderAfter.rest = borderAfter.normal;
            }
            resolveBordersLastRowInTable(footerLastRow, true, false, true);
        }
    }

    private class ResolverInBody extends Resolver {

        private boolean firstInBody = true;

        void endRow(List<GridUnit> row, TableCellContainer container) {
            super.endRow(row, container);
            if (firstInTable) {
                resolveBordersFirstRowInTable(row, true, true, true);
            } else {
                // Either there is a header, and then previousRow is set to the header's last row,
                // or this is not the first row in the body, and previousRow is not null
                resolveBordersBetweenRows(previousRow, row);
                integrateLeadingBorders(row);
            }
            integrateTrailingBorders(row);
            previousRow = row;
            if (firstInBody) {
                firstInBody = false;
                for (Object aRow : row) {
                    GridUnit gu = (GridUnit) aRow;
                    gu.borderBefore.leadingTrailing = gu.borderBefore.normal;
                }
            }
        }

        void endTable() {
            if (resolverInFooter != null) {
                resolverInFooter.endTable();
            } else {
                // Trailing and rest borders already resolved with integrateTrailingBorders
                resolveBordersLastRowInTable(previousRow, true, false, false);
            }
            for (Object aPreviousRow : previousRow) {
                GridUnit gu = (GridUnit) aPreviousRow;
                gu.borderAfter.leadingTrailing = gu.borderAfter.normal;
            }
        }
    }

    CollapsingBorderResolver(Table table) {
        this.table = table;
        collapsingBorderModel = CollapsingBorderModel.getBorderModelFor(table.getBorderCollapse());
        firstInTable = true;
        // Resolve before and after borders between the table and each table-column
        int index = 0;
        do {
            TableColumn col = table.getColumn(index);
            // See endRow method in ResolverInHeader for an explanation of the hack
            col.borderBefore.integrateSegment(table.borderBefore, true, false, true);
            col.borderBefore.leadingTrailing = col.borderBefore.rest;
            col.borderAfter.integrateSegment(table.borderAfter, true, false, true);
            col.borderAfter.leadingTrailing = col.borderAfter.rest;
            /*
             * TODO The border resolution must be done only once for each table column,
             * even if it's repeated; otherwise, re-resolving against the table's borders
             * will lead to null border specifications.
             *
             * Eventually table columns should probably be cloned instead.
             */
            index += col.getNumberColumnsRepeated();
        } while (index < table.getNumberOfColumns());
    }

    /** {@inheritDoc} */
    public void endRow(List<GridUnit> row, TableCellContainer container) {
        delegate.endRow(row, container);
    }

    /** {@inheritDoc} */
    public void startPart(TablePart part) {
        if (part instanceof TableHeader) {
            delegate = new ResolverInHeader();
        } else {
            if (leadingBorders == null || table.omitHeaderAtBreak()) {
                // No header, leading borders determined by the table
                leadingBorders = new ArrayList(table.getNumberOfColumns());
                for (Object o : table.getColumns()) {
                    ConditionalBorder border = ((TableColumn) o).borderBefore;
                    leadingBorders.add(border);
                }
            }
            if (part instanceof TableFooter) {
                resolverInFooter = new ResolverInFooter();
                delegate = resolverInFooter;
            } else {
                if (trailingBorders == null || table.omitFooterAtBreak()) {
                    // No footer, trailing borders determined by the table
                    trailingBorders = new ArrayList(table.getNumberOfColumns());
                    for (Object o : table.getColumns()) {
                        ConditionalBorder border = ((TableColumn) o).borderAfter;
                        trailingBorders.add(border);
                    }
                }
                delegate = resolverInBody;
            }
        }
        delegate.startPart(part);
    }

    /** {@inheritDoc} */
    public void endPart() {
        delegate.endPart();
    }

    /** {@inheritDoc} */
    public void endTable() {
        delegate.endTable();
        delegate = null;
        /* TODO Temporary hack for resolved borders in header */
        if (headerLastRow != null) {
            for (Object aHeaderLastRow : headerLastRow) {
                GridUnit gu = (GridUnit) aHeaderLastRow;
                gu.borderAfter.leadingTrailing = gu.borderAfter.normal;
            }
        }
        if (footerLastRow != null) {
            for (Object aFooterLastRow : footerLastRow) {
                GridUnit gu = (GridUnit) aFooterLastRow;
                gu.borderAfter.leadingTrailing = gu.borderAfter.normal;
            }
        }
        /* End of temporary hack */
    }
}
