/*
 * 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: PageBreaker.java 1229622 2012-01-10 16:14:05Z cbowditch $ */

package org.apache.fop.layoutmgr;

import java.util.List;
import java.util.ListIterator;

import org.apache.fop.area.Block;
import org.apache.fop.area.BodyRegion;
import org.apache.fop.area.Footnote;
import org.apache.fop.area.PageViewport;
import org.apache.fop.fo.Constants;
import org.apache.fop.fo.FObj;
import org.apache.fop.fo.pagination.Region;
import org.apache.fop.fo.pagination.RegionBody;
import org.apache.fop.fo.pagination.StaticContent;
import org.apache.fop.layoutmgr.PageBreakingAlgorithm.PageBreakingLayoutListener;
import org.apache.fop.traits.MinOptMax;

/**
 * Handles the breaking of pages in an fo:flow
 */
public class PageBreaker extends AbstractBreaker {

    private PageSequenceLayoutManager pslm;
    private boolean firstPart = true;
    private boolean pageBreakHandled;
    private boolean needColumnBalancing;
    private PageProvider pageProvider;
    private Block separatorArea;
    private boolean spanAllActive;

    /**
     * The FlowLayoutManager object, which processes
     * the single fo:flow of the fo:page-sequence
     */
    private FlowLayoutManager childFLM = null;

    private StaticContentLayoutManager footnoteSeparatorLM = null;

    /**
     * Construct page breaker.
     * @param pslm the page sequence layout manager
     */
    public PageBreaker(PageSequenceLayoutManager pslm) {
        this.pslm = pslm;
        this.pageProvider = pslm.getPageProvider();
        this.childFLM = pslm.getLayoutManagerMaker().makeFlowLayoutManager(
                pslm, pslm.getPageSequence().getMainFlow());
    }

    /** {@inheritDoc} */
    protected void updateLayoutContext(LayoutContext context) {
        int flowIPD = pslm.getCurrentPV().getCurrentSpan().getColumnWidth();
        context.setRefIPD(flowIPD);
    }

    /** {@inheritDoc} */
    protected LayoutManager getTopLevelLM() {
        return pslm;
    }

    /** {@inheritDoc} */
    protected PageProvider getPageProvider() {
        return pslm.getPageProvider();
    }

    /**
     * Starts the page breaking process.
     * @param flowBPD the constant available block-progression-dimension (used for every part)
     */
    void doLayout(int flowBPD) {
        doLayout(flowBPD, false);
    }

    /** {@inheritDoc} */
    protected PageBreakingLayoutListener createLayoutListener() {
        return new PageBreakingLayoutListener() {

            public void notifyOverflow(int part, int amount, FObj obj) {
                Page p = pageProvider.getPage(
                            false, part, PageProvider.RELTO_CURRENT_ELEMENT_LIST);
                RegionBody body = (RegionBody)p.getSimplePageMaster().getRegion(
                        Region.FO_REGION_BODY);
                BlockLevelEventProducer eventProducer = BlockLevelEventProducer.Provider.get(
                        body.getUserAgent().getEventBroadcaster());

                boolean canRecover = (body.getOverflow() != Constants.EN_ERROR_IF_OVERFLOW);
                boolean needClip = (body.getOverflow() == Constants.EN_HIDDEN
                        || body.getOverflow() == Constants.EN_ERROR_IF_OVERFLOW);
                eventProducer.regionOverflow(this, body.getName(),
                        p.getPageViewport().getPageNumberString(),
                        amount, needClip, canRecover,
                        body.getLocator());
            }

        };
    }

    /** {@inheritDoc} */
    protected int handleSpanChange(LayoutContext childLC, int nextSequenceStartsOn) {
        needColumnBalancing = false;
        if (childLC.getNextSpan() != Constants.NOT_SET) {
            //Next block list will have a different span.
            nextSequenceStartsOn = childLC.getNextSpan();
            needColumnBalancing = childLC.getNextSpan() == Constants.EN_ALL
                    && childLC.getDisableColumnBalancing() == Constants.EN_FALSE;

        }
        if (needColumnBalancing) {
            AbstractBreaker.log.debug(
                    "Column balancing necessary for the next element list!!!");
        }
        return nextSequenceStartsOn;
    }

    /** {@inheritDoc} */
    protected int getNextBlockList(LayoutContext childLC,
            int nextSequenceStartsOn) {
        return getNextBlockList(childLC, nextSequenceStartsOn, null, null, null);
    }

    /** {@inheritDoc} */
    protected int getNextBlockList(LayoutContext childLC, int nextSequenceStartsOn,
            Position positionAtIPDChange, LayoutManager restartLM, List firstElements) {
        if (!firstPart) {
            // if this is the first page that will be created by
            // the current BlockSequence, it could have a break
            // condition that must be satisfied;
            // otherwise, we may simply need a new page
            handleBreakTrait(nextSequenceStartsOn);
        }
        firstPart = false;
        pageBreakHandled = true;

        pageProvider.setStartOfNextElementList(pslm.getCurrentPageNum(),
                pslm.getCurrentPV().getCurrentSpan().getCurrentFlowIndex(), this.spanAllActive);
        return super.getNextBlockList(childLC, nextSequenceStartsOn, positionAtIPDChange,
                restartLM, firstElements);
    }

    private boolean containsFootnotes(List contentList, LayoutContext context) {

        boolean containsFootnotes = false;
        if (contentList != null) {
            ListIterator contentListIterator = contentList.listIterator();
            while (contentListIterator.hasNext()) {
                ListElement element = (ListElement) contentListIterator.next();
                if (element instanceof KnuthBlockBox
                    && ((KnuthBlockBox) element).hasAnchors()) {
                    // element represents a line with footnote citations
                    containsFootnotes = true;
                    LayoutContext footnoteContext = new LayoutContext(context);
                    footnoteContext.setStackLimitBP(context.getStackLimitBP());
                    footnoteContext.setRefIPD(pslm.getCurrentPV()
                            .getRegionReference(Constants.FO_REGION_BODY).getIPD());
                    List footnoteBodyLMs = ((KnuthBlockBox) element).getFootnoteBodyLMs();
                    ListIterator footnoteBodyIterator = footnoteBodyLMs.listIterator();
                    // store the lists of elements representing the footnote bodies
                    // in the box representing the line containing their references
                    while (footnoteBodyIterator.hasNext()) {
                        FootnoteBodyLayoutManager fblm
                            = (FootnoteBodyLayoutManager) footnoteBodyIterator.next();
                        fblm.setParent(childFLM);
                        fblm.initialize();
                        ((KnuthBlockBox) element).addElementList(
                                fblm.getNextKnuthElements(footnoteContext, alignment));
                    }
                }
            }
        }
        return containsFootnotes;
    }

    private void handleFootnoteSeparator() {
        StaticContent footnoteSeparator;
        footnoteSeparator = pslm.getPageSequence().getStaticContent("xsl-footnote-separator");
        if (footnoteSeparator != null) {
            // the footnote separator can contain page-dependent content such as
            // page numbers or retrieve markers, so its areas cannot simply be
            // obtained now and repeated in each page;
            // we need to know in advance the separator bpd: the actual separator
            // could be different from page to page, but its bpd would likely be
            // always the same

            // create a Block area that will contain the separator areas
            separatorArea = new Block();
            separatorArea.setIPD(pslm.getCurrentPV()
                        .getRegionReference(Constants.FO_REGION_BODY).getIPD());
            // create a StaticContentLM for the footnote separator
            footnoteSeparatorLM
                    = pslm.getLayoutManagerMaker().makeStaticContentLayoutManager(
                        pslm, footnoteSeparator, separatorArea);
            footnoteSeparatorLM.doLayout();

            footnoteSeparatorLength = MinOptMax.getInstance(separatorArea.getBPD());
        }
    }

    /** {@inheritDoc} */
    protected List getNextKnuthElements(LayoutContext context, int alignment) {
        List contentList = null;

        while (!childFLM.isFinished() && contentList == null) {
            contentList = childFLM.getNextKnuthElements(context, alignment);
        }

        // scan contentList, searching for footnotes
        if (containsFootnotes(contentList, context)) {
            // handle the footnote separator
            handleFootnoteSeparator();
        }
        return contentList;
    }

    /** {@inheritDoc} */
    protected List getNextKnuthElements(LayoutContext context, int alignment,
            Position positionAtIPDChange, LayoutManager restartAtLM) {
        List contentList = null;

        do {
            contentList = childFLM.getNextKnuthElements(context, alignment, positionAtIPDChange,
                    restartAtLM);
        } while (!childFLM.isFinished() && contentList == null);

        // scan contentList, searching for footnotes
        if (containsFootnotes(contentList, context)) {
            // handle the footnote separator
            handleFootnoteSeparator();
        }
        return contentList;
    }

    /**
     * @return current display alignment
     */
    protected int getCurrentDisplayAlign() {
        return pslm.getCurrentPage().getSimplePageMaster().getRegion(
                Constants.FO_REGION_BODY).getDisplayAlign();
    }

    /**
     * @return whether or not this flow has more page break opportunities
     */
    protected boolean hasMoreContent() {
        return !childFLM.isFinished();
    }

    /**
     * Adds an area to the flow layout manager
     * @param posIter the position iterator
     * @param context the layout context
     */
    protected void addAreas(PositionIterator posIter, LayoutContext context) {
        if (footnoteSeparatorLM != null) {
            StaticContent footnoteSeparator = pslm.getPageSequence().getStaticContent(
                    "xsl-footnote-separator");
            // create a Block area that will contain the separator areas
            separatorArea = new Block();
            separatorArea.setIPD(
                    pslm.getCurrentPV().getRegionReference(Constants.FO_REGION_BODY).getIPD());
            // create a StaticContentLM for the footnote separator
            footnoteSeparatorLM = pslm.getLayoutManagerMaker().makeStaticContentLayoutManager(
            pslm, footnoteSeparator, separatorArea);
            footnoteSeparatorLM.doLayout();
        }

        childFLM.addAreas(posIter, context);
    }

    /**
     * {@inheritDoc}
     * This implementation checks whether to trigger column-balancing,
     * or whether to take into account a 'last-page' condition.
     */
    protected void doPhase3(PageBreakingAlgorithm alg, int partCount,
            BlockSequence originalList, BlockSequence effectiveList) {

        if (needColumnBalancing) {
            //column balancing for the last part
            redoLayout(alg, partCount, originalList, effectiveList);
            return;
        }

        boolean lastPageMasterDefined = pslm.getPageSequence().hasPagePositionLast();
        if (!hasMoreContent()) {
            //last part is reached
            if (lastPageMasterDefined) {
                //last-page condition
                redoLayout(alg, partCount, originalList, effectiveList);
                return;
            }
        }

        //nothing special: just add the areas now
        addAreas(alg, partCount, originalList, effectiveList);
    }

    /**
     * Restart the algorithm at the break corresponding to the given partCount. Used to
     * re-do the part after the last break in case of either column-balancing or a last
     * page-master.
     */
    private void redoLayout(PageBreakingAlgorithm alg, int partCount,
            BlockSequence originalList, BlockSequence effectiveList) {

        int newStartPos = 0;
        int restartPoint = pageProvider.getStartingPartIndexForLastPage(partCount);
        if (restartPoint > 0) {
            //Add definitive areas for the parts before the
            //restarting point
            addAreas(alg, restartPoint, originalList, effectiveList);
            //Get page break from which we restart
            PageBreakPosition pbp = (PageBreakPosition)
                    alg.getPageBreaks().get(restartPoint - 1);
            newStartPos = pbp.getLeafPos() + 1;
            //Handle page break right here to avoid any side-effects
            if (newStartPos > 0) {
                handleBreakTrait(Constants.EN_PAGE);
            }
        }

        AbstractBreaker.log.debug("Restarting at " + restartPoint
                + ", new start position: " + newStartPos);

        pageBreakHandled = true;
        //Update so the available BPD is reported correctly
        int currentPageNum = pslm.getCurrentPageNum();

        pageProvider.setStartOfNextElementList(currentPageNum,
                pslm.getCurrentPV().getCurrentSpan().getCurrentFlowIndex(), this.spanAllActive);

        //Make sure we only add the areas we haven't added already
        effectiveList.ignoreAtStart = newStartPos;

        PageBreakingAlgorithm algRestart;
        if (needColumnBalancing) {
            AbstractBreaker.log.debug("Column balancing now!!!");
            AbstractBreaker.log.debug("===================================================");

            //Restart last page
            algRestart = new BalancingColumnBreakingAlgorithm(
                    getTopLevelLM(), getPageProvider(), createLayoutListener(),
                    alignment, Constants.EN_START, footnoteSeparatorLength,
                    isPartOverflowRecoveryActivated(),
                    pslm.getCurrentPV().getBodyRegion().getColumnCount());
            AbstractBreaker.log.debug("===================================================");
        } else  {
            // Handle special page-master for last page
            BodyRegion currentBody = pageProvider.getPage(false, currentPageNum)
                    .getPageViewport().getBodyRegion();

            setLastPageIndex(currentPageNum);

            BodyRegion lastBody = pageProvider.getPage(false, currentPageNum)
                    .getPageViewport().getBodyRegion();
            lastBody.getMainReference().setSpans(currentBody.getMainReference().getSpans());
            AbstractBreaker.log.debug("Last page handling now!!!");
            AbstractBreaker.log.debug("===================================================");
            //Restart last page
            algRestart = new PageBreakingAlgorithm(
                    getTopLevelLM(), getPageProvider(), createLayoutListener(),
                    alg.getAlignment(), alg.getAlignmentLast(),
                    footnoteSeparatorLength,
                    isPartOverflowRecoveryActivated(), false, false);
            AbstractBreaker.log.debug("===================================================");
        }

        int optimalPageCount = algRestart.findBreakingPoints(effectiveList,
                    newStartPos,
                    1, true, BreakingAlgorithm.ALL_BREAKS);
        AbstractBreaker.log.debug("restart: optimalPageCount= " + optimalPageCount
                + " pageBreaks.size()= " + algRestart.getPageBreaks().size());

        boolean fitsOnePage
            = optimalPageCount <= pslm.getCurrentPV()
                .getBodyRegion().getMainReference().getCurrentSpan().getColumnCount();

        if (needColumnBalancing) {
            if (!fitsOnePage) {
                AbstractBreaker.log.warn(
                        "Breaking algorithm produced more columns than are available.");
                /* reenable when everything works
                throw new IllegalStateException(
                        "Breaking algorithm must not produce more columns than available.");
                */
            }
        } else {
            if (fitsOnePage) {
                //Replace last page
                pslm.setCurrentPage(pageProvider.getPage(false, currentPageNum));
            } else {
                //Last page-master cannot hold the content.
                //Add areas now...
                addAreas(alg, restartPoint, partCount - restartPoint, originalList, effectiveList);
                //...and add a blank last page
                setLastPageIndex(currentPageNum + 1);
                pslm.setCurrentPage(pslm.makeNewPage(true));
                return;
            }
        }

        addAreas(algRestart, optimalPageCount, originalList, effectiveList);
    }

    private void setLastPageIndex(int currentPageNum) {
        int lastPageIndex = pslm.getForcedLastPageNum(currentPageNum);
        pageProvider.setLastPageIndex(lastPageIndex);
    }

    /** {@inheritDoc} */
    protected void startPart(BlockSequence list, int breakClass) {
        AbstractBreaker.log.debug("startPart() breakClass=" + getBreakClassName(breakClass));
        if (pslm.getCurrentPage() == null) {
            throw new IllegalStateException("curPage must not be null");
        }
        if (!pageBreakHandled) {

            //firstPart is necessary because we need the first page before we start the
            //algorithm so we have a BPD and IPD. This may subject to change later when we
            //start handling more complex cases.
            if (!firstPart) {
                // if this is the first page that will be created by
                // the current BlockSequence, it could have a break
                // condition that must be satisfied;
                // otherwise, we may simply need a new page
                handleBreakTrait(breakClass);
            }
            pageProvider.setStartOfNextElementList(pslm.getCurrentPageNum(),
                    pslm.getCurrentPV().getCurrentSpan().getCurrentFlowIndex(),
                    this.spanAllActive);
        }
        pageBreakHandled = false;
        // add static areas and resolve any new id areas
        // finish page and add to area tree
        firstPart = false;
    }

    /** {@inheritDoc} */
    protected void handleEmptyContent() {
        pslm.getCurrentPV().getPage().fakeNonEmpty();
    }

    /** {@inheritDoc} */
    protected void finishPart(PageBreakingAlgorithm alg, PageBreakPosition pbp) {
        // add footnote areas
        if (pbp.footnoteFirstListIndex < pbp.footnoteLastListIndex
            || pbp.footnoteFirstElementIndex <= pbp.footnoteLastElementIndex) {
            // call addAreas() for each FootnoteBodyLM
            for (int i = pbp.footnoteFirstListIndex; i <= pbp.footnoteLastListIndex; i++) {
                List elementList = alg.getFootnoteList(i);
                int firstIndex = (i == pbp.footnoteFirstListIndex
                        ? pbp.footnoteFirstElementIndex : 0);
                int lastIndex = (i == pbp.footnoteLastListIndex
                        ? pbp.footnoteLastElementIndex : elementList.size() - 1);

                SpaceResolver.performConditionalsNotification(elementList,
                        firstIndex, lastIndex, -1);
                LayoutContext childLC = new LayoutContext(0);
                AreaAdditionUtil.addAreas(null,
                        new KnuthPossPosIter(elementList, firstIndex, lastIndex + 1),
                        childLC);
            }
            // set the offset from the top margin
            Footnote parentArea = pslm.getCurrentPV().getBodyRegion().getFootnote();
            int topOffset = pslm.getCurrentPV().getBodyRegion().getBPD() - parentArea.getBPD();
            if (separatorArea != null) {
                topOffset -= separatorArea.getBPD();
            }
            parentArea.setTop(topOffset);
            parentArea.setSeparator(separatorArea);
        }
        pslm.getCurrentPV().getCurrentSpan().notifyFlowsFinished();
    }

    /** {@inheritDoc} */
    protected LayoutManager getCurrentChildLM() {
        return childFLM;
    }

    /** {@inheritDoc} */
    protected void observeElementList(List elementList) {
        ElementListObserver.observe(elementList, "breaker",
                pslm.getFObj().getId());
    }

    /**
     * Depending on the kind of break condition, move to next column
     * or page. May need to make an empty page if next page would
     * not have the desired "handedness".
     * @param breakVal - value of break-before or break-after trait.
     */
    private void handleBreakTrait(int breakVal) {
        Page curPage = pslm.getCurrentPage();
        switch (breakVal) {
        case Constants.EN_ALL:
            //break due to span change in multi-column layout
            curPage.getPageViewport().createSpan(true);
            this.spanAllActive = true;
            return;
        case Constants.EN_NONE:
            curPage.getPageViewport().createSpan(false);
            this.spanAllActive = false;
            return;
        case Constants.EN_COLUMN:
        case Constants.EN_AUTO:
        case Constants.EN_PAGE:
        case -1:
            PageViewport pv = curPage.getPageViewport();

            //Check if previous page was spanned
            boolean forceNewPageWithSpan = false;
            RegionBody rb = (RegionBody)curPage.getSimplePageMaster().getRegion(
                    Constants.FO_REGION_BODY);
            forceNewPageWithSpan
                    = (rb.getColumnCount() > 1
                        && pv.getCurrentSpan().getColumnCount() == 1);

            if (forceNewPageWithSpan) {
                log.trace("Forcing new page with span");
                curPage = pslm.makeNewPage(false);
                curPage.getPageViewport().createSpan(true);
            } else if (pv.getCurrentSpan().hasMoreFlows()) {
                log.trace("Moving to next flow");
                pv.getCurrentSpan().moveToNextFlow();
            } else {
                log.trace("Making new page");
                /*curPage = */pslm.makeNewPage(false);
            }
            return;
        default:
            log.debug("handling break-before after page " + pslm.getCurrentPageNum()
                + " breakVal=" + getBreakClassName(breakVal));
            if (needBlankPageBeforeNew(breakVal)) {
                log.trace("Inserting blank page");
                /*curPage = */pslm.makeNewPage(true);
            }
            if (needNewPage(breakVal)) {
                log.trace("Making new page");
                /*curPage = */pslm.makeNewPage(false);
            }
        }
    }

    /**
     * Check if a blank page is needed to accomodate
     * desired even or odd page number.
     * @param breakVal - value of break-before or break-after trait.
     */
    private boolean needBlankPageBeforeNew(int breakVal) {
        if (breakVal == Constants.EN_PAGE
                || (pslm.getCurrentPage().getPageViewport().getPage().isEmpty())) {
            // any page is OK or we already have an empty page
            return false;
        } else {
            /* IF we are on the kind of page we need, we'll need a new page. */
            if (pslm.getCurrentPageNum() % 2 == 0) { // even page
                return (breakVal == Constants.EN_EVEN_PAGE);
            } else { // odd page
                return (breakVal == Constants.EN_ODD_PAGE);
            }
        }
    }

    /**
     * See if need to generate a new page
     * @param breakVal - value of break-before or break-after trait.
     */
    private boolean needNewPage(int breakVal) {
        if (pslm.getCurrentPage().getPageViewport().getPage().isEmpty()) {
            if (breakVal == Constants.EN_PAGE) {
                return false;
            } else if (pslm.getCurrentPageNum() % 2 == 0) { // even page
                return (breakVal == Constants.EN_ODD_PAGE);
            } else { // odd page
                return (breakVal == Constants.EN_EVEN_PAGE);
            }
        } else {
            return true;
        }
    }
}
