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

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

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

import org.apache.fop.area.Area;
import org.apache.fop.area.BlockParent;
import org.apache.fop.fo.pagination.Flow;
import org.apache.fop.util.ListUtil;

/**
 * LayoutManager for an fo:flow object.
 * Its parent LM is the PageSequenceLayoutManager.
 * This LM is responsible for getting columns of the appropriate size
 * and filling them with block-level areas generated by its children.
 * TODO Reintroduce emergency counter (generate error to avoid endless loop)
 */
public class FlowLayoutManager extends BlockStackingLayoutManager {

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

    /** Array of areas currently being filled stored by area class */
    private final BlockParent[] currentAreas = new BlockParent[Area.CLASS_MAX];

    private boolean handlingFloat;

    /**
     * This is the top level layout manager.
     * It is created by the PageSequence FO.
     * @param pslm parent PageSequenceLayoutManager object
     * @param node Flow object
     */
    public FlowLayoutManager(PageSequenceLayoutManager pslm, Flow node) {
        super(node);
        setGeneratesBlockArea(true);
        setParent(pslm);
    }

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

    /**
     * Get a sequence of KnuthElements representing the content
     * of the node assigned to the LM.
     * @param context   the LayoutContext used to store layout information
     * @param alignment the desired text alignment
     * @param restartPosition   {@link Position} to restart from
     * @param restartLM {@link LayoutManager} to restart from
     * @return the list of KnuthElements
     * @see LayoutManager#getNextKnuthElements(LayoutContext,int)
     */
    List<ListElement> getNextKnuthElements(LayoutContext context, int alignment,
            Position restartPosition, LayoutManager restartLM) {

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

        boolean isRestart = (restartPosition != null);
        // always reset in case of restart (exception: see below)
        boolean doReset = isRestart;
        LayoutManager currentChildLM;
        Stack<LayoutManager> lmStack = new Stack<LayoutManager>();
        if (isRestart) {
            currentChildLM = restartPosition.getLM();
            if (currentChildLM == null) {
                throw new IllegalStateException("Cannot find layout manager to restart from");
            }
            if (restartLM != null && restartLM.getParent() == this) {
                currentChildLM = restartLM;
            } else {
                while (currentChildLM.getParent() != this) {
                    lmStack.push(currentChildLM);
                    currentChildLM = currentChildLM.getParent();
                }
                doReset = false;
            }
            setCurrentChildLM(currentChildLM);
        } else {
            currentChildLM = getChildLM();
        }

        while (currentChildLM != null) {
            if (!isRestart || doReset) {
                if (doReset) {
                    currentChildLM.reset(); // TODO won't work with forced breaks
                }
                if (addChildElements(elements, currentChildLM, context, alignment,
                        null, null, null) != null) {
                    return elements;
                }
            } else {
                if (addChildElements(elements, currentChildLM, context, alignment, lmStack,
                        restartPosition, restartLM) != null) {
                    return elements;
                }
                // restarted; force reset as of next child
                doReset = true;
            }
            currentChildLM = getChildLM();
        }

        SpaceResolver.resolveElementList(elements);
        setFinished(true);

        assert !elements.isEmpty();
        return elements;
    }

    private List<ListElement> addChildElements(List<ListElement> elements,
            LayoutManager childLM, LayoutContext context, int alignment,
            Stack<LayoutManager> lmStack, Position position, LayoutManager restartAtLM) {
        if (handleSpanChange(childLM, context) && position == null) {
            SpaceResolver.resolveElementList(elements);
            return elements;
        }

        LayoutContext childLC = makeChildLayoutContext(context);
        List<ListElement> childElements
                = getNextChildElements(childLM, context, childLC, alignment, lmStack,
                    position, restartAtLM);
        if (elements.isEmpty()) {
            context.updateKeepWithPreviousPending(childLC.getKeepWithPreviousPending());
        }
        if (!elements.isEmpty()
                && !ElementListUtils.startsWithForcedBreak(childElements)) {
            addInBetweenBreak(elements, context, childLC);
        }
        context.updateKeepWithNextPending(childLC.getKeepWithNextPending());

        elements.addAll(childElements);

        if (ElementListUtils.endsWithForcedBreak(elements)) {
            // a descendant of this flow has break-before or break-after
            if (childLM.isFinished() && !hasNextChildLM()) {
                setFinished(true);
            }
            SpaceResolver.resolveElementList(elements);
            return elements;
        }
        return null;
    }

    private boolean handleSpanChange(LayoutManager childLM, LayoutContext context) {
        int span = EN_NONE;
        int disableColumnBalancing = EN_FALSE;
        if (childLM instanceof BlockLayoutManager) {
            span = ((BlockLayoutManager)childLM).getBlockFO().getSpan();
            disableColumnBalancing = ((BlockLayoutManager) childLM).getBlockFO()
                    .getDisableColumnBalancing();
        } else if (childLM instanceof BlockContainerLayoutManager) {
            span = ((BlockContainerLayoutManager)childLM).getBlockContainerFO().getSpan();
            disableColumnBalancing = ((BlockContainerLayoutManager) childLM).getBlockContainerFO()
                    .getDisableColumnBalancing();
        }

        int currentSpan = context.getCurrentSpan();
        if (currentSpan != span) {
            if (span == EN_ALL) {
                context.setDisableColumnBalancing(disableColumnBalancing);
            }
            log.debug("span change from " + currentSpan + " to " + span);
            context.signalSpanChange(span);
            return true;
        } else {
            return false;
        }
    }

    /**
     * Overridden to take into account the current page-master's
     * writing-mode
     * {@inheritDoc}
     */
    @Override
    protected LayoutContext makeChildLayoutContext(LayoutContext context) {
        LayoutContext childLC = LayoutContext.newInstance();
        childLC.setStackLimitBP(context.getStackLimitBP());
        childLC.setRefIPD(context.getRefIPD());
        childLC.setWritingMode(getCurrentPage().getSimplePageMaster().getWritingMode());
        return childLC;
    }

    /**
     * Overridden to wrap the child positions before returning the list
     * {@inheritDoc}
     */
    @Override
    protected List<ListElement> getNextChildElements(LayoutManager childLM, LayoutContext context,
            LayoutContext childLC, int alignment, Stack<LayoutManager> lmStack,
            Position restartPosition, LayoutManager restartLM) {

        List<ListElement> childElements;
        if (lmStack == null) {
            childElements = childLM.getNextKnuthElements(childLC, alignment);
        } else {
            childElements = childLM.getNextKnuthElements(childLC, alignment,
                    lmStack, restartPosition, restartLM);
        }
        assert !childElements.isEmpty();

        // "wrap" the Position inside each element
        List tempList = childElements;
        childElements = new LinkedList<ListElement>();
        wrapPositionElements(tempList, childElements);
        return childElements;
    }

    /** {@inheritDoc} */
    @Override
    public int negotiateBPDAdjustment(int adj, KnuthElement lastElement) {
        log.debug(" FLM.negotiateBPDAdjustment> " + adj);

        Position lastPosition = lastElement.getPosition();
        if (lastPosition instanceof NonLeafPosition) {
            // this element was not created by this FlowLM
            NonLeafPosition savedPos = (NonLeafPosition) lastPosition;
            lastElement.setPosition(savedPos.getPosition());
            int returnValue = ((BlockLevelLayoutManager)lastElement.getLayoutManager())
                    .negotiateBPDAdjustment(adj, lastElement);
            lastElement.setPosition(savedPos);
            log.debug(" FLM.negotiateBPDAdjustment> result " + returnValue);
            return returnValue;
        } else {
            return 0;
        }
    }

    /** {@inheritDoc} */
    @Override
    public void discardSpace(KnuthGlue spaceGlue) {
        log.debug(" FLM.discardSpace> ");

        Position gluePosition = spaceGlue.getPosition();
        if (gluePosition instanceof NonLeafPosition) {
            // this element was not created by this FlowLM
            NonLeafPosition savedPos = (NonLeafPosition) gluePosition;
            spaceGlue.setPosition(savedPos.getPosition());
            ((BlockLevelLayoutManager) spaceGlue.getLayoutManager()).discardSpace(spaceGlue);
            spaceGlue.setPosition(savedPos);
        }
    }

    /** {@inheritDoc} */
    @Override
    public Keep getKeepTogether() {
        return Keep.KEEP_AUTO;
    }

    /** {@inheritDoc} */
    @Override
    public Keep getKeepWithNext() {
        return Keep.KEEP_AUTO;
    }

    /** {@inheritDoc} */
    @Override
    public Keep getKeepWithPrevious() {
        return Keep.KEEP_AUTO;
    }

    /** {@inheritDoc} */
    @Override
    public List<ListElement> getChangedKnuthElements(List oldList, int alignment) {
        ListIterator<KnuthElement> oldListIterator = oldList.listIterator();
        KnuthElement returnedElement;
        List<KnuthElement> returnedList = new LinkedList<KnuthElement>();
        List<ListElement> returnList = new LinkedList<>();
        KnuthElement prevElement = null;
        KnuthElement currElement = null;
        int fromIndex = 0;

        // "unwrap" the Positions stored in the elements
        KnuthElement oldElement;
        while (oldListIterator.hasNext()) {
            oldElement = oldListIterator.next();
            if (oldElement.getPosition() instanceof NonLeafPosition) {
                // oldElement was created by a descendant of this FlowLM
                oldElement.setPosition((oldElement.getPosition()).getPosition());
            } else {
                // thisElement was created by this FlowLM, remove it
                oldListIterator.remove();
            }
        }
        // reset the iterator
        oldListIterator = oldList.listIterator();


        while (oldListIterator.hasNext()) {
            currElement = oldListIterator.next();
            if (prevElement != null
                && prevElement.getLayoutManager() != currElement.getLayoutManager()) {
                // prevElement is the last element generated by the same LM
                BlockLevelLayoutManager prevLM = (BlockLevelLayoutManager)
                                                 prevElement.getLayoutManager();
                BlockLevelLayoutManager currLM = (BlockLevelLayoutManager)
                                                 currElement.getLayoutManager();
                returnedList.addAll(prevLM.getChangedKnuthElements(
                        oldList.subList(fromIndex, oldListIterator.previousIndex()), alignment));
                fromIndex = oldListIterator.previousIndex();

                // there is another block after this one
                if (prevLM.mustKeepWithNext()
                    || currLM.mustKeepWithPrevious()) {
                    // add an infinite penalty to forbid a break between blocks
                    returnedList.add(new KnuthPenalty(0, KnuthElement.INFINITE, false,
                            new Position(this), false));
                } else if (!ListUtil.getLast(returnedList).isGlue()) {
                    // add a null penalty to allow a break between blocks
                    returnedList.add(new KnuthPenalty(0, 0, false, new Position(this), false));
                }
            }
            prevElement = currElement;
        }
        if (currElement != null) {
            BlockLevelLayoutManager currLM = (BlockLevelLayoutManager)
                                             currElement.getLayoutManager();
            returnedList.addAll(currLM.getChangedKnuthElements(
                    oldList.subList(fromIndex, oldList.size()), alignment));
        }

        // "wrap" the Position stored in each element of returnedList
        // and add elements to returnList
        for (KnuthElement aReturnedList : returnedList) {
            returnedElement = aReturnedList;
            if (returnedElement.getLayoutManager() != this) {
                returnedElement.setPosition(
                        new NonLeafPosition(this, returnedElement.getPosition()));
            }
            returnList.add(returnedElement);
        }

        return returnList;
    }

    /** {@inheritDoc} */
    @Override
    public void addAreas(PositionIterator parentIter, LayoutContext layoutContext) {
        AreaAdditionUtil.addAreas(this, parentIter, layoutContext);
        flush();
    }

    /**
     * Add child area to a the correct container, depending on its
     * area class. A Flow can fill at most one area container of any class
     * at any one time. The actual work is done by BlockStackingLM.
     *
     * @param childArea the area to add
     */
    @Override
    public void addChildArea(Area childArea) {
        if (childArea instanceof BlockParent && handlingFloat()) {
            BlockParent bp = (BlockParent) childArea;
            bp.setXOffset(getPSLM().getStartIntrusionAdjustment());
        }
        getParentArea(childArea);
        addChildToArea(childArea,
                          this.currentAreas[childArea.getAreaClass()]);
    }

    /** {@inheritDoc} */
    @Override
    public Area getParentArea(Area childArea) {
        BlockParent parentArea = null;
        int aclass = childArea.getAreaClass();

        if (aclass == Area.CLASS_NORMAL || aclass == Area.CLASS_SIDE_FLOAT) {
            parentArea = getCurrentPV().getCurrentFlow();
        } else if (aclass == Area.CLASS_BEFORE_FLOAT) {
            parentArea = getCurrentPV().getBodyRegion().getBeforeFloat();
        } else if (aclass == Area.CLASS_FOOTNOTE) {
            parentArea = getCurrentPV().getBodyRegion().getFootnote();
        } else {
            throw new IllegalStateException("(internal error) Invalid "
                    + "area class (" + aclass + ") requested.");
        }

        this.currentAreas[aclass] = parentArea;
        setCurrentArea(parentArea);
        return parentArea;
    }

    /**
     * Returns the IPD of the content area
     * @return the IPD of the content area
     */
    @Override
    public int getContentAreaIPD() {
        int flowIPD = getPSLM().getCurrentColumnWidth();
        return flowIPD;
    }

    /**
     * Returns the BPD of the content area
     * @return the BPD of the content area
     */
    @Override
    public int getContentAreaBPD() {
        return getCurrentPV().getBodyRegion().getBPD();
    }

    /** {@inheritDoc} */
    @Override
    public boolean isRestartable() {
        return true;
    }

    public void handleFloatOn() {
        handlingFloat = true;
    }

    public void handleFloatOff() {
        handlingFloat = false;
    }

    public boolean handlingFloat() {
        return handlingFloat;
    }
}

