/*
 * 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.ArrayList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;

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

import org.apache.xmlgraphics.util.QName;

import org.apache.fop.area.Area;
import org.apache.fop.area.AreaTreeObject;
import org.apache.fop.area.PageViewport;
import org.apache.fop.fo.Constants;
import org.apache.fop.fo.FONode;
import org.apache.fop.fo.FObj;
import org.apache.fop.fo.flow.Marker;
import org.apache.fop.fo.flow.RetrieveMarker;

/**
 * The base class for most LayoutManagers.
 */
public abstract class AbstractLayoutManager extends AbstractBaseLayoutManager implements Constants {

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

    /** Parent LayoutManager for this LayoutManager */
    protected LayoutManager parentLayoutManager;
    /** List of child LayoutManagers */
    protected List<LayoutManager> childLMs;
    /** Iterator for child LayoutManagers */
    protected ListIterator fobjIter;
    /** Marker map for markers related to this LayoutManager */
    private Map<String, Marker> markers;

    /** True if this LayoutManager has handled all of its content. */
    private boolean isFinished;

    /** child LM during getNextKnuthElement phase */
    protected LayoutManager curChildLM;

    /** child LM iterator during getNextKnuthElement phase */
    protected ListIterator<LayoutManager> childLMiter;

    private int lastGeneratedPosition = -1;
    private int smallestPosNumberChecked = Integer.MAX_VALUE;

    private boolean preserveChildrenAtEndOfLayout = true;

    /**
     * Abstract layout manager.
     */
    public AbstractLayoutManager() {
    }

    /**
     * Abstract layout manager.
     *
     * @param fo the formatting object for this layout manager
     */
    public AbstractLayoutManager(FObj fo) {
        super(fo);
        markers = fo.getMarkers();
        fobjIter = fo.getChildNodes();
        childLMiter = new LMiter(this);
    }

    /** {@inheritDoc} */
    public void setParent(LayoutManager lm) {
        this.parentLayoutManager = lm;
    }

    /** {@inheritDoc} */
    public LayoutManager getParent() {
        return this.parentLayoutManager;
    }

    /** {@inheritDoc} */
    public void initialize() {
        // Empty
    }

    /**
     * Return currently active child LayoutManager or null if
     * all children have finished layout.
     * Note: child must implement LayoutManager! If it doesn't, skip it
     * and print a warning.
     * @return the current child LayoutManager
     */
    protected LayoutManager getChildLM() {
        if (curChildLM != null && !curChildLM.isFinished()) {
            return curChildLM;
        }
        if (childLMiter.hasNext()) {
            curChildLM = childLMiter.next();
            curChildLM.initialize();
            return curChildLM;
        }
        return null;
    }

    /**
     * Set currently active child layout manager.
     * @param childLM the child layout manager
     */
    protected void setCurrentChildLM(LayoutManager childLM) {
        curChildLM = childLM;
        childLMiter = new LMiter(this);
        do {
            curChildLM = childLMiter.next();
        } while (curChildLM != childLM);
    }

    /**
     * Return indication if getChildLM will return another LM.
     * @return true if another child LM is still available
     */
    protected boolean hasNextChildLM() {
        return childLMiter.hasNext();
    }

    /**
     * Tell whether this LayoutManager has handled all of its content.
     * @return True if there are no more break possibilities,
     * ie. the last one returned represents the end of the content.
     */
    public boolean isFinished() {
        return isFinished;
    }

    /**
     * Set the flag indicating the LayoutManager has handled all of its content.
     * @param fin the flag value to be set
     */
    public void setFinished(boolean fin) {
        isFinished = fin;
    }

    /** {@inheritDoc} */
    public void addAreas(PositionIterator posIter, LayoutContext context) {
    }

    /** {@inheritDoc} */
    public List getNextKnuthElements(LayoutContext context, int alignment) {
        log.warn("null implementation of getNextKnuthElements() called!");
        setFinished(true);
        return null;
    }

    /** {@inheritDoc} */
    public List<ListElement> getChangedKnuthElements(List<ListElement> oldList, int alignment) {
        log.warn("null implementation of getChangeKnuthElement() called!");
        return null;
    }

    /**
     * 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 for which the parent area is wanted
     * @return the parent area for the given child
     */
    public Area getParentArea(Area childArea) {
        return null;
    }

    /**
     * Add a child area to the current area. If this causes the maximum
     * dimension of the current area to be exceeded, the parent LM is called
     * to add it.
     * @param childArea the child area to be added
     */
    public void addChildArea(Area childArea) {
    }

    /**
     * Create the LM instances for the children of the
     * formatting object being handled by this LM.
     * @param size the requested number of child LMs
     * @return the list with the preloaded child LMs
     */
    protected List<LayoutManager> createChildLMs(int size) {
        if (fobjIter == null) {
            return null;
        }
        List<LayoutManager> newLMs = new ArrayList<LayoutManager>(size);
        while (fobjIter.hasNext() && newLMs.size() < size) {
            Object theobj = fobjIter.next();
            if (theobj instanceof FONode) {
                FONode foNode = (FONode) theobj;
                if (foNode instanceof RetrieveMarker) {
                    foNode = getPSLM().resolveRetrieveMarker(
                        (RetrieveMarker) foNode);
                }
                if (foNode != null) {
                    getPSLM().getLayoutManagerMaker()
                        .makeLayoutManagers(foNode, newLMs);
                }
            }
        }
        return newLMs;
    }

    /** {@inheritDoc} */
    public PageSequenceLayoutManager getPSLM() {
        return parentLayoutManager.getPSLM();
    }

    /**
     * @see PageSequenceLayoutManager#getCurrentPage()
     * @return the {@link Page} instance corresponding to the current page
     */
    public Page getCurrentPage() {
        return getPSLM().getCurrentPage();
    }

    /** @return the current page viewport */
    public PageViewport getCurrentPV() {
        return getPSLM().getCurrentPage().getPageViewport();
    }

    /** {@inheritDoc} */
    public boolean createNextChildLMs(int pos) {
        List<LayoutManager> newLMs = createChildLMs(pos + 1 - childLMs.size());
        addChildLMs(newLMs);
        return pos < childLMs.size();
    }

    /** {@inheritDoc} */
    public List<LayoutManager> getChildLMs() {
        if (childLMs == null) {
            childLMs = new java.util.ArrayList<LayoutManager>(10);
        }
        return childLMs;
    }

    /** {@inheritDoc} */
    public void addChildLM(LayoutManager lm) {
        if (lm == null) {
            return;
        }
        lm.setParent(this);
        if (childLMs == null) {
            childLMs = new java.util.ArrayList<LayoutManager>(10);
        }
        childLMs.add(lm);
        if (log.isTraceEnabled()) {
            log.trace(this.getClass().getName()
                    + ": Adding child LM " + lm.getClass().getName());
        }
    }

    /** {@inheritDoc} */
    public void addChildLMs(List newLMs) {
        if (newLMs == null || newLMs.size() == 0) {
            return;
        }
        for (LayoutManager newLM : (Iterable<LayoutManager>) newLMs) {
            addChildLM(newLM);
        }
    }

    /**
     * Adds a Position to the Position participating in the first|last determination by assigning
     * it a unique position index.
     * @param pos the Position
     * @return the same Position but with a position index
     */
    public Position notifyPos(Position pos) {
        if (pos.getIndex() >= 0) {
            throw new IllegalStateException("Position already got its index");
        }

        pos.setIndex(++lastGeneratedPosition);
        return pos;
    }

    private void verifyNonNullPosition(Position pos) {
        if (pos == null || pos.getIndex() < 0) {
            throw new IllegalArgumentException(
                    "Only non-null Positions with an index can be checked");
        }
    }

    /**
     * Indicates whether the given Position is the first area-generating Position of this LM.
     * @param pos the Position (must be one with a position index)
     * @return True if it is the first Position
     */
    public boolean isFirst(Position pos) {
        if (pos == null) {
            return false;
        }
        verifyNonNullPosition(pos);
        if (pos.getIndex() == this.smallestPosNumberChecked) {
            return true;
        } else if (pos.getIndex() < this.smallestPosNumberChecked) {
            this.smallestPosNumberChecked = pos.getIndex();
            return true;
        } else {
            return false;
        }
    }

    /**
     * Indicates whether the given Position is the last area-generating Position of this LM.
     * @param pos the Position (must be one with a position index)
     * @return True if it is the last Position
     */
    public boolean isLast(Position pos) {
        if (pos == null) {
            return false;
        }
        verifyNonNullPosition(pos);
        return (pos.getIndex() == this.lastGeneratedPosition
                && isFinished());
    }

    public boolean hasLineAreaDescendant() {
        if (childLMs == null || childLMs.isEmpty()) {
            return false;
        } else {
            for (LayoutManager childLM : childLMs) {
                if (childLM.hasLineAreaDescendant()) {
                    return true;
                }
            }
        }
        return false;
    }

    public int getBaselineOffset() {
        if (childLMs != null) {
            for (LayoutManager childLM : childLMs) {
                if (childLM.hasLineAreaDescendant()) {
                    return childLM.getBaselineOffset();
                }
            }
        }
        throw newNoLineAreaDescendantException();
    }

    protected IllegalStateException newNoLineAreaDescendantException() {
        return new IllegalStateException("getBaselineOffset called on an object that has no line-area descendant");
    }

    /**
     * Transfers foreign attributes from the formatting object to the area.
     * @param targetArea the area to set the attributes on
     */
    protected void transferForeignAttributes(AreaTreeObject targetArea) {
        Map<QName, String> atts = fobj.getForeignAttributes();
        targetArea.setForeignAttributes(atts);
    }

    /**
     * Transfers extension attachments from the formatting object to the area.
     * @param targetArea the area to set the extensions on
     */
    protected void transferExtensionAttachments(AreaTreeObject targetArea) {
        if (fobj.hasExtensionAttachments()) {
            targetArea.setExtensionAttachments(fobj.getExtensionAttachments());
        }
    }

    /**
     * Transfers extensions (foreign attributes and extension attachments) from
     * the formatting object to the area.
     * @param targetArea the area to set the extensions on
     */
    protected void transferExtensions(AreaTreeObject targetArea) {
        transferForeignAttributes(targetArea);
        transferExtensionAttachments(targetArea);
    }

    /**
     * Registers the FO's markers on the current PageViewport, and if applicable on the parent TableLM.
     *
     * @param isStarting    boolean indicating whether the markers qualify as 'starting'
     * @param isFirst   boolean indicating whether the markers qualify as 'first'
     * @param isLast    boolean indicating whether the markers qualify as 'last'
     */
    protected void registerMarkers(boolean isStarting, boolean isFirst, boolean isLast) {
        if (this.markers != null) {
            getCurrentPV().registerMarkers(
                    this.markers,
                    isStarting,
                    isFirst,
                    isLast);
            possiblyRegisterMarkersForTables(markers, isStarting, isFirst, isLast);
        }
    }

    /**
     * Registers the FO's id on the current PageViewport
     */
    protected void addId() {
        if (fobj != null) {
            getPSLM().addIDToPage(fobj.getId());
        }
    }

    /**
     * Notifies the {@link PageSequenceLayoutManager} that layout
     * for this LM has ended.
     */
    protected void notifyEndOfLayout() {
        if (fobj != null) {
            getPSLM().notifyEndOfLayout(fobj.getId());
        }
    }

    /**
     * Checks to see if the incoming {@link Position}
     * is the last one for this LM, and if so, calls
     * {@link #notifyEndOfLayout()} and cleans up.
     *
     * @param pos   the {@link Position} to check
     */
    protected void checkEndOfLayout(Position pos) {
        if (pos != null
            && pos.getLM() == this
            && this.isLast(pos)) {

            notifyEndOfLayout();

            if (!preserveChildrenAtEndOfLayout) {
                // References to the child LMs are no longer needed
                childLMs = null;
                curChildLM = null;
                childLMiter = null;
            }

            /* markers that qualify have been transferred to the page
             */
            markers = null;

            /* References to the FO's children can be released if the
             * LM is a descendant of the FlowLM. For static-content
             * the FO may still be needed on following pages.
             */
            LayoutManager lm = this.parentLayoutManager;
            while (!(lm instanceof FlowLayoutManager
                        || lm instanceof PageSequenceLayoutManager)) {
                lm = lm.getParent();
            }
            if (lm instanceof FlowLayoutManager && !preserveChildrenAtEndOfLayout) {
                fobj.clearChildNodes();
                fobjIter = null;
            }
        }
    }

    /*
     * Preserves the children LMs at the end of layout. This is necessary if the layout is expected to be
     * repeated, as when using retrieve-table-markers.
     */
    public void preserveChildrenAtEndOfLayout() {
        preserveChildrenAtEndOfLayout = true;
    }

    /** {@inheritDoc} */
    @Override
    public String toString() {
        return (super.toString() + (fobj != null ? "{fobj = " + fobj.toString() + "}" : ""));
    }

    /** {@inheritDoc} */
    @Override
    public void reset() {
        isFinished = false;
        curChildLM = null;
        childLMiter = new LMiter(this);
        /* Reset all the children LM that have been created so far. */
        for (LayoutManager childLM : getChildLMs()) {
            childLM.reset();
        }
        if (fobj != null) {
            markers = fobj.getMarkers();
        }
        lastGeneratedPosition = -1;
    }

    public void recreateChildrenLMs() {
        childLMs = new ArrayList<>();
        isFinished = false;
        if (fobj == null) {
            return;
        }
        fobjIter = fobj.getChildNodes();
        int position = 0;
        while (createNextChildLMs(position++)) {
            //
        }
        childLMiter = new LMiter(this);
        for (LMiter iter = new LMiter(this); iter.hasNext();) {
            AbstractBaseLayoutManager alm = (AbstractBaseLayoutManager) iter.next();
            alm.initialize();
            alm.recreateChildrenLMs();
            alm.preserveChildrenAtEndOfLayout();
        }
        curChildLM = getChildLM();
    }

    protected void possiblyRegisterMarkersForTables(Map<String, Marker> markers, boolean isStarting,
            boolean isFirst, boolean isLast) {
        LayoutManager lm = this.parentLayoutManager;
        if (lm instanceof FlowLayoutManager || lm instanceof PageSequenceLayoutManager
                || !(lm instanceof AbstractLayoutManager)) {
            return;
        }
        ((AbstractLayoutManager) lm).possiblyRegisterMarkersForTables(markers, isStarting, isFirst, isLast);
    }

    public boolean handlingFloat() {
        if (parentLayoutManager != null && parentLayoutManager instanceof AbstractLayoutManager) {
            return ((AbstractLayoutManager) parentLayoutManager).handlingFloat();
        }
        return false;
    }
}
