/*
 * 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$ */

package org.apache.fop.layoutmgr.table;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

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

import org.apache.fop.area.Area;
import org.apache.fop.area.Block;
import org.apache.fop.datatypes.LengthBase;
import org.apache.fop.fo.Constants;
import org.apache.fop.fo.FONode;
import org.apache.fop.fo.FObj;
import org.apache.fop.fo.flow.AbstractGraphics;
import org.apache.fop.fo.flow.Marker;
import org.apache.fop.fo.flow.Markers;
import org.apache.fop.fo.flow.RetrieveTableMarker;
import org.apache.fop.fo.flow.table.Table;
import org.apache.fop.fo.flow.table.TableColumn;
import org.apache.fop.fo.properties.CommonBorderPaddingBackground;
import org.apache.fop.fo.properties.KeepProperty;
import org.apache.fop.layoutmgr.BlockLevelEventProducer;
import org.apache.fop.layoutmgr.BreakElement;
import org.apache.fop.layoutmgr.BreakOpportunity;
import org.apache.fop.layoutmgr.KnuthElement;
import org.apache.fop.layoutmgr.KnuthGlue;
import org.apache.fop.layoutmgr.LayoutContext;
import org.apache.fop.layoutmgr.LeafPosition;
import org.apache.fop.layoutmgr.ListElement;
import org.apache.fop.layoutmgr.Position;
import org.apache.fop.layoutmgr.PositionIterator;
import org.apache.fop.layoutmgr.SpacedBorderedPaddedBlockLayoutManager;
import org.apache.fop.layoutmgr.TraitSetter;
import org.apache.fop.traits.MinOptMax;
import org.apache.fop.traits.SpaceVal;
import org.apache.fop.util.BreakUtil;

/**
 * LayoutManager for a table FO.
 * A table consists of columns, table header, table footer and multiple
 * table bodies.
 * The header, footer and body add the areas created from the table cells.
 * The table then creates areas for the columns, bodies and rows
 * the render background.
 */
public class TableLayoutManager extends SpacedBorderedPaddedBlockLayoutManager
        implements BreakOpportunity {

    /**
     * logging instance
     */
    private static Log log = LogFactory.getLog(TableLayoutManager.class);

    private TableContentLayoutManager contentLM;
    private ColumnSetup columns;

    private Block curBlockArea;

    private double tableUnit;
    private double oldTableUnit;
    private boolean autoLayout = true;

    private int halfBorderSeparationBPD;
    private int halfBorderSeparationIPD;

    /** See {@link TableLayoutManager#registerColumnBackgroundArea(TableColumn, Block, int)}. */
    private List<ColumnBackgroundInfo> columnBackgroundAreas;

    private Position auxiliaryPosition;

    // this holds a possible list of TCLMs that needed to have their addAreas() repeated
    private List<TableCellLayoutManager> savedTCLMs;
    private boolean areAllTCLMsSaved;

    private Markers tableMarkers;
    private Markers tableFragmentMarkers;

    private boolean hasRetrieveTableMarker;

    private boolean repeatedHeader;

    private List<List<KnuthElement>> headerFootnotes = Collections.emptyList();

    private List<List<KnuthElement>> footerFootnotes = Collections.emptyList();

    /**
     * Temporary holder of column background informations for a table-cell's area.
     *
     * @see TableLayoutManager#registerColumnBackgroundArea(TableColumn, Block, int)
     */
    private static final class ColumnBackgroundInfo {
        private TableColumn column;
        private Block backgroundArea;
        private int xShift;

        private ColumnBackgroundInfo(TableColumn column, Block backgroundArea, int xShift) {
            this.column = column;
            this.backgroundArea = backgroundArea;
            this.xShift = xShift;
        }
    }

    /**
     * Create a new table layout manager.
     * @param node the table FO
     */
    public TableLayoutManager(Table node) {
        super(node);
        this.columns = new ColumnSetup(node);
    }


    @Override
    protected CommonBorderPaddingBackground getCommonBorderPaddingBackground() {
        return getTable().getCommonBorderPaddingBackground();
    }


    /** @return the table FO */
    public Table getTable() {
        return (Table)this.fobj;
    }

    /**
     * @return the column setup for this table.
     */
    public ColumnSetup getColumns() {
        return this.columns;
    }

    /** {@inheritDoc} */
    public void initialize() {
        foSpaceBefore = new SpaceVal(
                getTable().getCommonMarginBlock().spaceBefore, this).getSpace();
        foSpaceAfter = new SpaceVal(
                getTable().getCommonMarginBlock().spaceAfter, this).getSpace();
        startIndent = getTable().getCommonMarginBlock().startIndent.getValue(this);
        endIndent = getTable().getCommonMarginBlock().endIndent.getValue(this);

        if (getTable().isSeparateBorderModel()) {
            this.halfBorderSeparationBPD = getTable().getBorderSeparation().getBPD().getLength()
                    .getValue(this) / 2;
            this.halfBorderSeparationIPD = getTable().getBorderSeparation().getIPD().getLength()
                    .getValue(this) / 2;
        } else {
            this.halfBorderSeparationBPD = 0;
            this.halfBorderSeparationIPD = 0;
        }

        if (!getTable().isAutoLayout()
                && getTable().getInlineProgressionDimension().getOptimum(this).getEnum()
                    != EN_AUTO) {
            autoLayout = false;
        }
    }

    private void resetSpaces() {
        this.discardBorderBefore = false;
        this.discardBorderAfter = false;
        this.discardPaddingBefore = false;
        this.discardPaddingAfter = false;
        this.effSpaceBefore = null;
        this.effSpaceAfter = null;
    }

    /**
     * @return half the value of border-separation.block-progression-dimension, or 0 if
     * border-collapse="collapse".
     */
    public int getHalfBorderSeparationBPD() {
        return halfBorderSeparationBPD;
    }

    /**
     * @return half the value of border-separation.inline-progression-dimension, or 0 if
     * border-collapse="collapse".
     */
    public int getHalfBorderSeparationIPD() {
        return halfBorderSeparationIPD;
    }

    /** {@inheritDoc} */
    public List<ListElement> getNextKnuthElements(LayoutContext context, int alignment) {

        List<ListElement> returnList = new LinkedList<>();

        /*
         * Compute the IPD and adjust it if necessary (overconstrained)
         */
        referenceIPD = context.getRefIPD();
        if (getTable().getInlineProgressionDimension().getOptimum(this).getEnum() != EN_AUTO) {
            int contentIPD = getTable().getInlineProgressionDimension().getOptimum(this)
                    .getLength().getValue(this);
            updateContentAreaIPDwithOverconstrainedAdjust(contentIPD);
        } else {
            if (!getTable().isAutoLayout()) {
                BlockLevelEventProducer eventProducer = BlockLevelEventProducer.Provider.get(
                        getTable().getUserAgent().getEventBroadcaster());
                eventProducer.tableFixedAutoWidthNotSupported(this, getTable().getLocator());
            }
            updateContentAreaIPDwithOverconstrainedAdjust();
        }
        int sumOfColumns = columns.getSumOfColumnWidths(this);
        if (!autoLayout && sumOfColumns > getContentAreaIPD()) {
            log.debug(FONode.decorateWithContextInfo(
                    "The sum of all column widths is larger than the specified table width.",
                    getTable()));
            updateContentAreaIPDwithOverconstrainedAdjust(sumOfColumns);
        }
        int availableIPD = referenceIPD - getIPIndents();
        if (getContentAreaIPD() > availableIPD) {
            BlockLevelEventProducer eventProducer = BlockLevelEventProducer.Provider.get(
                    getTable().getUserAgent().getEventBroadcaster());
            eventProducer.objectTooWide(this, getTable().getName(),
                    getContentAreaIPD(), context.getRefIPD(),
                    getTable().getLocator());
        }

        /* initialize unit to determine computed values
         * for proportional-column-width()
         */
        if (tableUnit == 0.0) {
            tableUnit = columns.computeTableUnit(this);
            if (oldTableUnit > tableUnit && supportResize(fobj)) {
                tableUnit = oldTableUnit;
            }
        }

        if (!firstVisibleMarkServed) {
            addKnuthElementsForSpaceBefore(returnList, alignment);
        }

        if (getTable().isSeparateBorderModel()) {
            addKnuthElementsForBorderPaddingBefore(returnList, !firstVisibleMarkServed);
            firstVisibleMarkServed = true;
            // Border and padding to be repeated at each break
            // This must be done only in the separate-border model, as in collapsing
            // tables have no padding and borders are determined at the cell level
            addPendingMarks(context);
        }


        // Elements for the table-header/footer/body
        List contentKnuthElements;
        contentLM = new TableContentLayoutManager(this);
        LayoutContext childLC = LayoutContext.newInstance();
        /*
        childLC.setStackLimit(
              MinOptMax.subtract(context.getStackLimit(),
                                 stackSize));*/
        childLC.setRefIPD(context.getRefIPD());
        childLC.copyPendingMarksFrom(context);

        contentKnuthElements = contentLM.getNextKnuthElements(childLC, alignment);
        //Set index values on elements coming from the content LM
        for (Object contentKnuthElement : contentKnuthElements) {
            ListElement el = (ListElement) contentKnuthElement;
            notifyPos(el.getPosition());
        }
        log.debug(contentKnuthElements);
        wrapPositionElements(contentKnuthElements, returnList);

        context.updateKeepWithPreviousPending(getKeepWithPrevious());
        context.updateKeepWithPreviousPending(childLC.getKeepWithPreviousPending());

        context.updateKeepWithNextPending(getKeepWithNext());
        context.updateKeepWithNextPending(childLC.getKeepWithNextPending());

        if (getTable().isSeparateBorderModel()) {
            addKnuthElementsForBorderPaddingAfter(returnList, true);
        }
        addKnuthElementsForSpaceAfter(returnList, alignment);

        if (!context.suppressBreakBefore()) {
            //addKnuthElementsForBreakBefore(returnList, context);
            int breakBefore = BreakUtil.compareBreakClasses(getTable().getBreakBefore(),
                    childLC.getBreakBefore());
            if (breakBefore != Constants.EN_AUTO) {
                returnList.add(0, new BreakElement(new LeafPosition(getParent(), 0), 0,
                        -KnuthElement.INFINITE, breakBefore, context));
            }
        }

        //addKnuthElementsForBreakAfter(returnList, context);
        int breakAfter = BreakUtil.compareBreakClasses(getTable().getBreakAfter(),
                childLC.getBreakAfter());
        if (breakAfter != Constants.EN_AUTO) {
            returnList.add(new BreakElement(new LeafPosition(getParent(), 0),
                    0, -KnuthElement.INFINITE, breakAfter, context));
        }

        setFinished(true);
        resetSpaces();
        return returnList;
    }

    private boolean supportResize(FONode node) {
        if (node instanceof AbstractGraphics || getPSLM().getPageProvider().getCurrentColumnCount() == 1) {
            return false;
        }
        FONode.FONodeIterator iterator = node.getChildNodes();
        while (iterator != null && iterator.hasNext()) {
            FONode x = iterator.next();
            if (!supportResize(x)) {
                return false;
            }
        }
        return true;
    }

    /** {@inheritDoc} */
    public Position getAuxiliaryPosition() {
        /*
         * Redefined to return a LeafPosition instead of a NonLeafPosition. The
         * SpaceResolver.SpaceHandlingBreakPosition constructors unwraps all
         * NonLeafPositions, which can lead to a NPE when a break in a table occurs at a
         * page with different ipd.
         */
        if (auxiliaryPosition == null) {
            auxiliaryPosition = new LeafPosition(this, 0);
        }
        return auxiliaryPosition;
    }

    /**
     * Registers the given area, that will be used to render the part of column background
     * covered by a table-cell. If percentages are used to place the background image, the
     * final bpd of the (fraction of) table 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
     * TableLM's {@link #addAreas(PositionIterator, LayoutContext)} method.
     *
     * @param column the table-column element from which the cell gets background
     * informations
     * @param backgroundArea the block of the cell's dimensions that will hold the column
     * background
     * @param xShift additional amount by which the image must be shifted to be correctly
     * placed (to counterbalance the cell's start border)
     */
    void registerColumnBackgroundArea(TableColumn column, Block backgroundArea, int xShift) {
        addBackgroundArea(backgroundArea);
        if (columnBackgroundAreas == null) {
            columnBackgroundAreas = new ArrayList<>();
        }
        columnBackgroundAreas.add(new ColumnBackgroundInfo(column, backgroundArea, xShift));
    }

    /**
     * The table area is a reference area that contains areas for
     * columns, bodies, rows and the contents are in cells.
     *
     * @param parentIter the position iterator
     * @param layoutContext the layout context for adding areas
     */
    public void addAreas(PositionIterator parentIter,
                         LayoutContext layoutContext) {
        getParentArea(null);
        addId();

        // add space before, in order to implement display-align = "center" or "after"
        if (layoutContext.getSpaceBefore() != 0) {
            addBlockSpacing(0.0, MinOptMax.getInstance(layoutContext.getSpaceBefore()));
        }

        int startXOffset = getTable().getCommonMarginBlock().startIndent.getValue(this);

        // add column, body then row areas

        // BPD of the table, i.e., height of its content; table's borders and paddings not counted
        int tableHeight = 0;
        //Body childLM;
        LayoutContext lc = LayoutContext.offspringOf(layoutContext);


        lc.setRefIPD(getContentAreaIPD());
        contentLM.setStartXOffset(startXOffset);
        contentLM.addAreas(parentIter, lc);

        if (fobj.getUserAgent().isTableBorderOverpaint()) {
            new OverPaintBorders(curBlockArea);
        }

        tableHeight += contentLM.getUsedBPD();

        curBlockArea.setBPD(tableHeight);

        if (columnBackgroundAreas != null) {
            for (Object columnBackgroundArea : columnBackgroundAreas) {
                ColumnBackgroundInfo b = (ColumnBackgroundInfo) columnBackgroundArea;
                TraitSetter.addBackground(b.backgroundArea,
                        b.column.getCommonBorderPaddingBackground(), this,
                        b.xShift, -b.backgroundArea.getYOffset(),
                        b.column.getColumnWidth().getValue(this), tableHeight);
            }
            columnBackgroundAreas.clear();
        }

        if (getTable().isSeparateBorderModel()) {
            TraitSetter.addBorders(curBlockArea,
                    getTable().getCommonBorderPaddingBackground(),
                    discardBorderBefore, discardBorderAfter, false, false, this);
            TraitSetter.addPadding(curBlockArea,
                    getTable().getCommonBorderPaddingBackground(),
                    discardPaddingBefore, discardPaddingAfter, false, false, this);
        }
        TraitSetter.addBackground(curBlockArea,
                getTable().getCommonBorderPaddingBackground(),
                this);
        TraitSetter.addMargins(curBlockArea,
                getTable().getCommonBorderPaddingBackground(),
                startIndent, endIndent,
                this);
        TraitSetter.addBreaks(curBlockArea,
                getTable().getBreakBefore(), getTable().getBreakAfter());
        TraitSetter.addSpaceBeforeAfter(curBlockArea, layoutContext.getSpaceAdjust(),
                effSpaceBefore, effSpaceAfter);

        flush();

        resetSpaces();
        curBlockArea = null;

        notifyEndOfLayout();
    }

    /**
     * Return an Area which can contain the passed childArea. The childArea
     * may not yet have any content, but it has essential traits set.
     * In general, if the LayoutManager already has an Area it simply returns
     * it. Otherwise, it makes a new Area of the appropriate class.
     * It gets a parent area for its area by calling its parent LM.
     * Finally, based on the dimensions of the parent area, it initializes
     * its own area. This includes setting the content IPD and the maximum
     * BPD.
     *
     * @param childArea the child area
     * @return the parent area of the child
     */
    public Area getParentArea(Area childArea) {
        if (curBlockArea == null) {
            curBlockArea = new Block();
            curBlockArea.setChangeBarList(getChangeBarList());

            // Set up dimensions
            // Must get dimensions from parent area
            /*Area parentArea =*/ parentLayoutManager.getParentArea(curBlockArea);

            TraitSetter.setProducerID(curBlockArea, getTable().getId());

            curBlockArea.setIPD(getContentAreaIPD());

            setCurrentArea(curBlockArea);
        }
        return curBlockArea;
    }

    /**
     * Add the child area to this layout manager.
     *
     * @param childArea the child area to add
     */
    public void addChildArea(Area childArea) {
        if (curBlockArea != null) {
            curBlockArea.addBlock((Block) childArea);
        }
    }

    /**
     * Adds the given area to this layout manager's area, without updating the used bpd.
     *
     * @param background an area
     */
    void addBackgroundArea(Block background) {
        curBlockArea.addChildArea(background);
    }

    /** {@inheritDoc} */
    public int negotiateBPDAdjustment(int adj, KnuthElement lastElement) {
        // TODO Auto-generated method stub
        return 0;
    }

    /** {@inheritDoc} */
    public void discardSpace(KnuthGlue spaceGlue) {
        // TODO Auto-generated method stub

    }

    /** {@inheritDoc} */
    public KeepProperty getKeepTogetherProperty() {
        return getTable().getKeepTogether();
    }

    /** {@inheritDoc} */
    public KeepProperty getKeepWithPreviousProperty() {
        return getTable().getKeepWithPrevious();
    }

    /** {@inheritDoc} */
    public KeepProperty getKeepWithNextProperty() {
        return getTable().getKeepWithNext();
    }

    // --------- Property Resolution related functions --------- //

    /**
     * {@inheritDoc}
     */
    public int getBaseLength(int lengthBase, FObj fobj) {
        // Special handler for TableColumn width specifications
        if (fobj instanceof TableColumn && fobj.getParent() == getFObj()) {
            switch (lengthBase) {
            case LengthBase.CONTAINING_BLOCK_WIDTH:
                return getContentAreaIPD();
            case LengthBase.TABLE_UNITS:
                return (int) this.tableUnit;
            default:
                log.error("Unknown base type for LengthBase.");
                return 0;
            }
        } else {
            switch (lengthBase) {
            case LengthBase.TABLE_UNITS:
                return (int) this.tableUnit;
            default:
                return super.getBaseLength(lengthBase, fobj);
            }
        }
    }

    /** {@inheritDoc} */
    public void reset() {
        super.reset();
        curBlockArea = null;
        oldTableUnit = tableUnit;
        tableUnit = 0.0;
    }

    /**
     * Saves a TableCellLayoutManager for later use.
     *
     * @param tclm a TableCellLayoutManager that has a RetrieveTableMarker
     */
    protected void saveTableHeaderTableCellLayoutManagers(TableCellLayoutManager tclm) {
        if (savedTCLMs == null) {
            savedTCLMs = new ArrayList<TableCellLayoutManager>();
        }
        if (!areAllTCLMsSaved) {
            savedTCLMs.add(tclm);
        }
    }

    /**
     * Calls addAreas() for each of the saved TableCellLayoutManagers.
     */
    protected void repeatAddAreasForSavedTableHeaderTableCellLayoutManagers() {
        if (savedTCLMs == null) {
            return;
        }
        // if we get to this stage then we are at the footer of the table fragment; this means that no more
        // different TCLM need to be saved (we already have all); we flag the list as being complete then
        areAllTCLMsSaved = true;
        for (TableCellLayoutManager tclm : savedTCLMs) {
            if (this.repeatedHeader) {
                tclm.setHasRepeatedHeader(true);
            }
            tclm.repeatAddAreas();
        }
    }

    /**
     * Resolves a RetrieveTableMarker by finding a qualifying Marker to which it is bound to.
     * @param rtm the RetrieveTableMarker to be resolved
     * @return a bound RetrieveTableMarker instance or null if no qualifying Marker found
     */
    public RetrieveTableMarker resolveRetrieveTableMarker(RetrieveTableMarker rtm) {
        String name = rtm.getRetrieveClassName();
        int originalPosition = rtm.getPosition();
        boolean changedPosition = false;

        Marker mark = null;
        // try the primary retrieve scope area, which is the same as table-fragment
        mark = (tableFragmentMarkers == null) ? null : tableFragmentMarkers.resolve(rtm);
        if (mark == null && rtm.getBoundary() != Constants.EN_TABLE_FRAGMENT) {
            rtm.changePositionTo(Constants.EN_LAST_ENDING);
            changedPosition = true;
            // try the page scope area
            mark = getCurrentPV().resolveMarker(rtm);
            if (mark == null && rtm.getBoundary() != Constants.EN_PAGE) {
                // try the table scope area
                mark = (tableMarkers == null) ? null : tableMarkers.resolve(rtm);
            }
        }
        if (changedPosition) {
            // so that the next time it is called looks unchanged
            rtm.changePositionTo(originalPosition);
        }
        if (mark == null) {
            log.debug("found no marker with name: " + name);
            return null;
        } else {
            rtm.bindMarker(mark);
            return rtm;
        }
    }

    /**
     * Register the markers for this table.
     *
     * @param marks the map of markers to add
     * @param starting if the area being added is starting or ending
     * @param isfirst if the area being added has is-first trait
     * @param islast if the area being added has is-last trait
     */
    public void registerMarkers(Map<String, Marker> marks, boolean starting, boolean isfirst,
            boolean islast) {
        if (tableMarkers == null) {
            tableMarkers = new Markers();
        }
        tableMarkers.register(marks, starting, isfirst, islast);
        if (tableFragmentMarkers == null) {
            tableFragmentMarkers = new Markers();
        }
        tableFragmentMarkers.register(marks, starting, isfirst, islast);
    }

    /**
     * Clears the list of markers in the current table fragment. Should be called just before starting a new
     * header (that belongs to the next table fragment).
     */
    protected void clearTableFragmentMarkers() {
        tableFragmentMarkers = null;
    }

    public void flagAsHavingRetrieveTableMarker() {
        hasRetrieveTableMarker = true;
    }

    protected void possiblyRegisterMarkersForTables(Map<String, Marker> markers, boolean isStarting,
            boolean isFirst, boolean isLast) {
        // note: if we allow table-footer after a table-body this check should not be made and the markers
        // should be registered regardless because the retrieval may be done only in the footer
        if (hasRetrieveTableMarker) {
            registerMarkers(markers, isStarting, isFirst, isLast);
        }
        super.possiblyRegisterMarkersForTables(markers, isStarting, isFirst, isLast);
    }

    void setHeaderFootnotes(List<List<KnuthElement>> footnotes) {
        this.headerFootnotes = footnotes;
    }

    List<List<KnuthElement>> getHeaderFootnotes() {
        return headerFootnotes;
    }

    void setFooterFootnotes(List<List<KnuthElement>> footnotes) {
        this.footerFootnotes = footnotes;
    }

    public void setRepeateHeader(boolean repeateHeader) {
        this.repeatedHeader = repeateHeader;
    }

    List<List<KnuthElement>> getFooterFootnotes() {
        return footerFootnotes;
    }

}
