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

package org.apache.fop.layoutmgr.inline;

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

import org.apache.fop.area.Area;
import org.apache.fop.area.inline.Space;
import org.apache.fop.fo.FObj;
import org.apache.fop.fo.properties.SpaceProperty;
import org.apache.fop.layoutmgr.AbstractLayoutManager;
import org.apache.fop.layoutmgr.BreakOpportunity;
import org.apache.fop.layoutmgr.BreakOpportunityHelper;
import org.apache.fop.layoutmgr.KnuthElement;
import org.apache.fop.layoutmgr.LayoutContext;
import org.apache.fop.layoutmgr.NonLeafPosition;
import org.apache.fop.layoutmgr.Position;
import org.apache.fop.traits.MinOptMax;

/**
 * Class modelling the commonalities of layoutmanagers for objects
 * which stack children in the inline direction, such as Inline or
 * Line. It should not be instantiated directly.
 */
public abstract class InlineStackingLayoutManager extends AbstractLayoutManager implements
        InlineLevelLayoutManager, BreakOpportunity {

    /**
     * Size of border and padding in BPD (ie, before and after).
     */
    protected MinOptMax extraBPD;

    private Area currentArea; // LineArea or InlineParent

    /** The child layout context */
    protected LayoutContext childLC;

    /**
     * Create an inline stacking layout manager.
     * This is used for fo's that create areas that
     * contain inline areas.
     *
     * @param node the formatting object that creates the area
     */
    protected InlineStackingLayoutManager(FObj node) {
        super(node);
        extraBPD = MinOptMax.ZERO;  // @SuppressFBWarnings("URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
    }

    /**
     * Set the iterator.
     *
     * @param iter the iterator for this LM
     */
    public void setLMiter(ListIterator iter) {
        childLMiter = iter;
    }

    /**
     * Returns the extra IPD needed for any leading or trailing fences for the
     * current area.
     * @param bNotFirst true if not the first area for this layout manager
     * @param bNotLast true if not the last area for this layout manager
     * @return the extra IPD as a MinOptMax spec
     */
    protected MinOptMax getExtraIPD(boolean bNotFirst, boolean bNotLast) {
        return MinOptMax.ZERO;
    }


    /**
     * Indication if the current area has a leading fence.
     * @param bNotFirst true if not the first area for this layout manager
     * @return the leading fence flag
     */
    protected boolean hasLeadingFence(boolean bNotFirst) {
        return false;
    }

    /**
     * Indication if the current area has a trailing fence.
     * @param bNotLast true if not the last area for this layout manager
     * @return the trailing fence flag
     */
    protected boolean hasTrailingFence(boolean bNotLast) {
        return false;
    }

    /**
     * Get the space at the start of the inline area.
     * @return the space property describing the space
     */
    protected SpaceProperty getSpaceStart() {
        return null;
    }

    /**
     * Get the space at the end of the inline area.
     * @return the space property describing the space
     */
    protected SpaceProperty getSpaceEnd() {
        return null;
    }

    /**
     * Returns the current area.
     * @return the current area
     */
    protected Area getCurrentArea() {
        return currentArea;
    }

    /**
     * Set the current area.
     * @param area the current area
     */
    protected void setCurrentArea(Area area) {
        currentArea = area;
    }

    /**
     * Trait setter to be overridden by subclasses.
     * @param bNotFirst true if this is not the first child area added
     * @param bNotLast true if this is not the last child area added
     */
    protected void setTraits(boolean bNotFirst, boolean bNotLast) {
    }

    /**
     * Set the current child layout context
     * @param lc the child layout context
     */
    protected void setChildContext(LayoutContext lc) {
        childLC = lc;
    }

    /**
     * Current child layout context
     * @return the current child layout context
     */
    protected LayoutContext getContext() {
        return childLC;
    }

    /**
     * Adds a space to the area.
     *
     * @param parentArea the area to which to add the space
     * @param spaceRange the space range specifier
     * @param spaceAdjust the factor by which to stretch or shrink the space
     */
    protected void addSpace(Area parentArea, MinOptMax spaceRange, double spaceAdjust) {
        if (spaceRange != null) {
            int iAdjust = spaceRange.getOpt();
            if (spaceAdjust > 0.0) {
                // Stretch by factor
                iAdjust += (int) (spaceRange.getStretch() * spaceAdjust);
            } else if (spaceAdjust < 0.0) {
                // Shrink by factor
                iAdjust += (int) (spaceRange.getShrink() * spaceAdjust);
            }
            if (iAdjust != 0) {
                //getLogger().debug("Add leading space: " + iAdjust);
                Space ls = new Space();
                ls.setChangeBarList(getChangeBarList());
                ls.setIPD(iAdjust);
                int level = parentArea.getBidiLevel();
                if (level >= 0) {
                    ls.setBidiLevel(level);
                }
                parentArea.addChildArea(ls);
            }
        }
    }

    /** {@inheritDoc} */
    public List addALetterSpaceTo(List oldList) {
        return addALetterSpaceTo(oldList, 0);
    }

    /** {@inheritDoc} */
    public List addALetterSpaceTo(List oldList, int thisDepth) {
        // old list contains only a box, or the sequence: box penalty glue box

        ListIterator oldListIterator = oldList.listIterator(oldList.size());
        KnuthElement element = (KnuthElement) oldListIterator.previous();
        int depth = thisDepth + 1;

        // The last element may not have a layout manager (its position == null);
        // this may happen if it is a padding box; see bug 39571.
        Position pos = element.getPosition();
        InlineLevelLayoutManager lm = null;
        if (pos != null) {
            lm = (InlineLevelLayoutManager) pos.getLM(depth);
        }
        if (lm == null) {
            return oldList;
        }
        oldList = lm.addALetterSpaceTo(oldList, depth);
        // "wrap" the Position stored in new elements of oldList
        oldListIterator = oldList.listIterator();
        while (oldListIterator.hasNext()) {
            element = (KnuthElement) oldListIterator.next();
            pos = element.getPosition();
            lm = null;
            if (pos != null) {
                lm = (InlineLevelLayoutManager) pos.getLM(thisDepth);
            }
            // in old elements the position at thisDepth is a position for this LM
            // only wrap new elements
            if (lm != this) {
                // new element, wrap position
                element.setPosition(notifyPos(new NonLeafPosition(this, element.getPosition())));
            }
        }

        return oldList;
    }

    /** {@inheritDoc} */
    public String getWordChars(Position pos) {
        Position newPos = pos.getPosition();
        return ((InlineLevelLayoutManager) newPos.getLM()).getWordChars(newPos);
    }

    /** {@inheritDoc} */
    public void hyphenate(Position pos, HyphContext hc) {
        Position newPos = pos.getPosition();
        ((InlineLevelLayoutManager)
         newPos.getLM()).hyphenate(newPos, hc);
    }

    /** {@inheritDoc} */
    public boolean applyChanges(List oldList) {
        return applyChanges(oldList, 0);
    }

    /** {@inheritDoc} */
    public boolean applyChanges(List oldList, int depth) {
        ListIterator oldListIterator = oldList.listIterator();
        KnuthElement oldElement;
        depth += 1;

        InlineLevelLayoutManager prevLM = null;
        InlineLevelLayoutManager currLM;
        int fromIndex = 0;

        boolean bSomethingChanged = false;
        while (oldListIterator.hasNext()) {
            oldElement = (KnuthElement) oldListIterator.next();
            Position pos = oldElement.getPosition();
            if (pos == null) {
                currLM = null;
            } else {
                currLM = (InlineLevelLayoutManager) pos.getLM(depth);
            }

            // initialize prevLM
            if (prevLM == null) {
                prevLM = currLM;
            }

            if (currLM != prevLM || !oldListIterator.hasNext()) {
                if (prevLM == this || currLM == this) {
                    prevLM = currLM;
                } else if (oldListIterator.hasNext()) {
                    bSomethingChanged
                        = prevLM.applyChanges(oldList.subList(fromIndex,
                                                              oldListIterator.previousIndex()),
                                                              depth)
                        || bSomethingChanged;
                    prevLM = currLM;
                    fromIndex = oldListIterator.previousIndex();
                } else if (currLM == prevLM) {
                    bSomethingChanged
                        = (prevLM != null)
                            && prevLM.applyChanges(oldList.subList(fromIndex,
                                                                   oldList.size()), depth)
                            || bSomethingChanged;
                } else {
                    bSomethingChanged
                        = prevLM.applyChanges(oldList.subList(fromIndex,
                                                              oldListIterator.previousIndex()),
                                                              depth)
                            || bSomethingChanged;
                    if (currLM != null) {
                        bSomethingChanged
                            = currLM.applyChanges(oldList.subList(oldListIterator.previousIndex(),
                                                                  oldList.size()), depth)
                            || bSomethingChanged;
                    }
                }
            }
        }

        return bSomethingChanged;
    }

    /**
     * {@inheritDoc}
     */
    public List getChangedKnuthElements(List oldList, int alignment) {
        return getChangedKnuthElements(oldList, alignment, 0);
    }

    /** {@inheritDoc} */
    public List getChangedKnuthElements(List oldList, int alignment, int depth) {
        // "unwrap" the Positions stored in the elements
        ListIterator oldListIterator = oldList.listIterator();
        KnuthElement oldElement;
        depth += 1;

        KnuthElement returnedElement;
        LinkedList returnedList = new LinkedList();
        LinkedList returnList = new LinkedList();
        InlineLevelLayoutManager prevLM = null;
        InlineLevelLayoutManager currLM;
        int fromIndex = 0;

        while (oldListIterator.hasNext()) {
            oldElement = (KnuthElement) oldListIterator.next();
            Position pos = oldElement.getPosition();
            if (pos == null) {
                currLM = null;
            } else {
                currLM = (InlineLevelLayoutManager) pos.getLM(depth);
            }
            if (prevLM == null) {
                prevLM = currLM;
            }

            if (currLM != prevLM || !oldListIterator.hasNext()) {
                if (oldListIterator.hasNext()) {
                    returnedList.addAll(
                        prevLM.getChangedKnuthElements(
                         oldList.subList(fromIndex, oldListIterator.previousIndex()),
                          alignment, depth));
                    prevLM = currLM;
                    fromIndex = oldListIterator.previousIndex();
                } else if (currLM == prevLM) {
                    returnedList.addAll(
                        prevLM.getChangedKnuthElements(
                         oldList.subList(fromIndex, oldList.size()),
                          alignment, depth));
                } else {
                    returnedList.addAll(
                        prevLM.getChangedKnuthElements(
                         oldList.subList(fromIndex, oldListIterator.previousIndex()),
                          alignment, depth));
                    if (currLM != null) {
                        returnedList.addAll(
                            currLM.getChangedKnuthElements(
                             oldList.subList(oldListIterator.previousIndex(), oldList.size()),
                              alignment, depth));
                    }
                }
            }
        }

        // this is a new list
        // "wrap" the Position stored in each element of returnedList
        for (Object aReturnedList : returnedList) {
            returnedElement = (KnuthElement) aReturnedList;
            returnedElement.setPosition(
                    notifyPos(new NonLeafPosition(this, returnedElement.getPosition())));
            returnList.add(returnedElement);
        }

        return returnList;
    }

    public int getBreakBefore() {
        return BreakOpportunityHelper.getBreakBefore(this);
    }

}
