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

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

import org.apache.fop.area.AreaTreeHandler;
import org.apache.fop.area.AreaTreeModel;
import org.apache.fop.area.IDTracker;
import org.apache.fop.area.PageViewport;
import org.apache.fop.area.Resolvable;
import org.apache.fop.datatypes.Numeric;
import org.apache.fop.fo.Constants;
import org.apache.fop.fo.flow.Marker;
import org.apache.fop.fo.flow.RetrieveMarker;
import org.apache.fop.fo.pagination.AbstractPageSequence;

/**
 * Abstract base class for a page sequence layout manager.
 */
public abstract class AbstractPageSequenceLayoutManager extends AbstractLayoutManager
            implements TopLevelLayoutManager {

    private static Log log = LogFactory.getLog(AbstractPageSequenceLayoutManager.class);

    /**
     * AreaTreeHandler which activates the PSLM and controls
     * the rendering of its pages.
     */
    protected AreaTreeHandler areaTreeHandler;

    /** ID tracker supplied by the AreaTreeHandler */
    protected IDTracker idTracker;

    /** page sequence formatting object being processed by this class */
    protected AbstractPageSequence pageSeq;

    /** Current page with page-viewport-area being filled by the PSLM. */
    protected Page curPage;

    /** the current page number */
    protected int currentPageNum;
    /** The stating page number */
    protected int startPageNum;

    /**
     * Constructor
     *
     * @param ath the area tree handler object
     * @param pseq fo:page-sequence to process
     */
    public AbstractPageSequenceLayoutManager(AreaTreeHandler ath, AbstractPageSequence pseq) {
        super(pseq);
        this.areaTreeHandler = ath;
        this.idTracker = ath.getIDTracker();
        this.pageSeq = pseq;
    }

    /**
     * @return the LayoutManagerMaker object associated to the areaTreeHandler
     */
    public LayoutManagerMaker getLayoutManagerMaker() {
        return areaTreeHandler.getLayoutManagerMaker();
    }

    /**
     * Provides access to the current page.
     * @return the current Page
     */
    public Page getCurrentPage() {
        return curPage;
    }

    /**
     * Provides access for setting the current page.
     * @param currentPage the new current Page
     */
    protected void setCurrentPage(Page currentPage) {
        this.curPage = currentPage;
    }

    /**
     * Provides access to the current page number
     * @return the current page number
     */
    protected int getCurrentPageNum() {
        return currentPageNum;
    }

    /** {@inheritDoc} */
    public void initialize() {
        startPageNum = pageSeq.getStartingPageNumber();
        currentPageNum = startPageNum - 1;
        curPage = null;
    }

    /**
     * This returns the first PageViewport that contains an id trait
     * matching the idref argument, or null if no such PV exists.
     *
     * @param idref the idref trait needing to be resolved
     * @return the first PageViewport that contains the ID trait
     */
    public PageViewport getFirstPVWithID(String idref) {
        List<PageViewport> list = idTracker.getPageViewportsContainingID(idref);
        if (list != null && list.size() > 0) {
            return list.get(0);
        }
        return null;
    }

    /**
     * This returns the last PageViewport that contains an id trait
     * matching the idref argument, or null if no such PV exists.
     *
     * @param idref the idref trait needing to be resolved
     * @return the last PageViewport that contains the ID trait
     */
    public PageViewport getLastPVWithID(String idref) {
        List<PageViewport> list = idTracker.getPageViewportsContainingID(idref);
        if (list != null && list.size() > 0) {
            return list.get(list.size() - 1);
        }
        return null;
    }

    /**
     * Add an ID reference to the current page.
     * When adding areas the area adds its ID reference.
     * For the page layout manager it adds the id reference
     * with the current page to the area tree.
     *
     * @param id the ID reference to add
     */
    public void addIDToPage(String id) {
        if (id != null && id.length() > 0) {
            idTracker.associateIDWithPageViewport(id, curPage.getPageViewport());
        }
    }

    /**
     * Add an id reference of the layout manager in the AreaTreeHandler,
     * if the id hasn't been resolved yet
     * @param id the id to track
     * @return a boolean indicating if the id has already been resolved
     * TODO Maybe give this a better name
     */
    public boolean associateLayoutManagerID(String id) {
        if (log.isDebugEnabled()) {
            log.debug("associateLayoutManagerID(" + id + ")");
        }
        if (!idTracker.alreadyResolvedID(id)) {
            idTracker.signalPendingID(id);
            return false;
        } else {
            return true;
        }
    }

    /**
     * Notify the areaTreeHandler that the LayoutManagers containing
     * idrefs have finished creating areas
     * @param id the id for which layout has finished
     */
    public void notifyEndOfLayout(String id) {
        idTracker.signalIDProcessed(id);
    }

    /**
     * Identify an unresolved area (one needing an idref to be
     * resolved, e.g. the internal-destination of an fo:basic-link)
     * for both the AreaTreeHandler and PageViewport object.
     *
     * The IDTracker keeps a document-wide list of idref's
     * and the PV's needing them to be resolved.  It uses this to
     * send notifications to the PV's when an id has been resolved.
     *
     * The PageViewport keeps lists of id's needing resolving, along
     * with the child areas (page-number-citation, basic-link, etc.)
     * of the PV needing their resolution.
     *
     * @param id the ID reference to add
     * @param res the resolvable object that needs resolving
     */
    public void addUnresolvedArea(String id, Resolvable res) {
        curPage.getPageViewport().addUnresolvedIDRef(id, res);
        idTracker.addUnresolvedIDRef(id, curPage.getPageViewport());
    }

    /**
     * Bind the RetrieveMarker to the corresponding Marker subtree.
     * If the boundary is page then it will only check the
     * current page. For page-sequence and document it will
     * lookup preceding pages from the area tree and try to find
     * a marker.
     * If we retrieve a marker from a preceding page,
     * then the containing page does not have a qualifying area,
     * and all qualifying areas have ended.
     * Therefore we use last-ending-within-page (Constants.EN_LEWP)
     * as the position.
     *
     * @param rm the RetrieveMarker instance whose properties are to
     * used to find the matching Marker.
     * @return a bound RetrieveMarker instance, or null if no Marker
     * could be found.
     */
    public RetrieveMarker resolveRetrieveMarker(RetrieveMarker rm) {
        AreaTreeModel areaTreeModel = areaTreeHandler.getAreaTreeModel();
        String name = rm.getRetrieveClassName();
        int boundary = rm.getRetrieveBoundary();

        // get marker from the current markers on area tree
        Marker mark = getCurrentPV().resolveMarker(rm);
        if (mark == null && boundary != EN_PAGE) {
            // go back over pages until mark found
            // if document boundary then keep going
            boolean doc = (boundary == EN_DOCUMENT);
            int seq = areaTreeModel.getPageSequenceCount();
            int page = areaTreeModel.getPageCount(seq) - 1;
            while (page < 0 && doc && seq > 1) {
                seq--;
                page = areaTreeModel.getPageCount(seq) - 1;
            }
            while (page >= 0) {
                PageViewport pv = areaTreeModel.getPage(seq, page);
                int originalPosition = rm.getPosition();
                rm.changePositionTo(Constants.EN_LEWP);
                mark = pv.resolveMarker(rm);
                // this is probably not necessary since the RM will not be used again, but to be safe...
                rm.changePositionTo(originalPosition);
                if (mark != null) {
                    break;
                }
                page--;
                if (page < 0 && doc && seq > 1) {
                    seq--;
                    page = areaTreeModel.getPageCount(seq) - 1;
                }
            }
        }

        if (mark == null) {
            log.debug("found no marker with name: " + name);
            return null;
        } else {
            rm.bindMarker(mark);
            return rm;
        }
    }

    /**
     * Creates and returns a new page.
     * @param pageNumber the page number
     * @param isBlank true if it's a blank page
     * @return the newly created page
     */
    protected abstract Page createPage(int pageNumber, boolean isBlank);

    /**
     * Makes a new page
     *
     * @param isBlank whether this page is blank or not
     * @return a new page
     */
    protected Page makeNewPage(boolean isBlank) {
        if (curPage != null) {
            finishPage();
        }

        currentPageNum++;

        curPage = createPage(currentPageNum, isBlank);

        if (log.isDebugEnabled()) {
            log.debug("[" + curPage.getPageViewport().getPageNumberString()
                    + (isBlank ? "*" : "") + "]");
        }

        addIDToPage(pageSeq.getRoot().getId());
        addIDToPage(pageSeq.getId());
        return curPage;
    }

    /**
     * Finishes a page in preparation for a new page.
     */
    protected void finishPage() {
        if (log.isTraceEnabled()) {
            curPage.getPageViewport().dumpMarkers();
        }

        // Try to resolve any unresolved IDs for the current page.
        //
        idTracker.tryIDResolution(curPage.getPageViewport());
        // Queue for ID resolution and rendering
        areaTreeHandler.getAreaTreeModel().addPage(curPage.getPageViewport());
        if (log.isDebugEnabled()) {
            log.debug("page finished: " + curPage.getPageViewport().getPageNumberString()
                    + ", current num: " + currentPageNum);
        }
        curPage = null;
    }

    /** {@inheritDoc} */
    public void doForcePageCount(Numeric nextPageSeqInitialPageNumber) {

        int forcePageCount = pageSeq.getForcePageCount();

        // xsl-spec version 1.0   (15.oct 2001)
        // auto | even | odd | end-on-even | end-on-odd | no-force | inherit
        // auto:
        // Force the last page in this page-sequence to be an odd-page
        // if the initial-page-number of the next page-sequence is even.
        // Force it to be an even-page
        // if the initial-page-number of the next page-sequence is odd.
        // If there is no next page-sequence
        // or if the value of its initial-page-number is "auto" do not force any page.

        // if force-page-count is auto then set the value of forcePageCount
        // depending on the initial-page-number of the next page-sequence
        if (nextPageSeqInitialPageNumber != null && forcePageCount == Constants.EN_AUTO) {
            if (nextPageSeqInitialPageNumber.getEnum() != 0) {
                // auto | auto-odd | auto-even
                int nextPageSeqPageNumberType = nextPageSeqInitialPageNumber.getEnum();
                if (nextPageSeqPageNumberType == Constants.EN_AUTO_ODD) {
                    forcePageCount = Constants.EN_END_ON_EVEN;
                } else if (nextPageSeqPageNumberType == Constants.EN_AUTO_EVEN) {
                    forcePageCount = Constants.EN_END_ON_ODD;
                } else {   // auto
                    forcePageCount = Constants.EN_NO_FORCE;
                }
            } else { // <integer> for explicit page number
                int nextPageSeqPageStart = nextPageSeqInitialPageNumber.getValue();
                // spec rule
                nextPageSeqPageStart = (nextPageSeqPageStart > 0) ? nextPageSeqPageStart : 1;
                if (nextPageSeqPageStart % 2 == 0) {   // explicit even startnumber
                    forcePageCount = Constants.EN_END_ON_ODD;
                } else {    // explicit odd startnumber
                    forcePageCount = Constants.EN_END_ON_EVEN;
                }
            }
        }

        if (forcePageCount == Constants.EN_EVEN) {
            if ((currentPageNum - startPageNum + 1) % 2 != 0) { // we have an odd number of pages
                curPage = makeNewPage(true);
            }
        } else if (forcePageCount == Constants.EN_ODD) {
            if ((currentPageNum - startPageNum + 1) % 2 == 0) { // we have an even number of pages
                curPage = makeNewPage(true);
            }
        } else if (forcePageCount == Constants.EN_END_ON_EVEN) {
            if (currentPageNum % 2 != 0) { // we are now on an odd page
                curPage = makeNewPage(true);
            }
        } else if (forcePageCount == Constants.EN_END_ON_ODD) {
            if (currentPageNum % 2 == 0) { // we are now on an even page
                curPage = makeNewPage(true);
            }
        } /*  else if (forcePageCount == Constants.EN_NO_FORCE) {
            // i hope: nothing special at all
        } */

        if (curPage != null) {
            finishPage();
        }

        while (forcePageCount != Constants.EN_NO_FORCE && getCurrentPageNum() < getLastPageNumber()) {
            curPage = makeNewPage(true);
            finishPage();
        }
    }

    /** {@inheritDoc} */
    public void reset() {
        throw new IllegalStateException();
    }

    protected int getLastPageNumber() {
        return currentPageNum;
    }
}
