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

// Java
import java.io.OutputStream;
import java.util.List;
import java.util.Locale;

import org.xml.sax.SAXException;

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

import org.apache.fop.apps.FOPException;
import org.apache.fop.apps.FOUserAgent;
import org.apache.fop.apps.FormattingResults;
import org.apache.fop.datatypes.Numeric;
import org.apache.fop.fo.FOEventHandler;
import org.apache.fop.fo.extensions.ExtensionAttachment;
import org.apache.fop.fo.extensions.ExternalDocument;
import org.apache.fop.fo.extensions.destination.Destination;
import org.apache.fop.fo.pagination.AbstractPageSequence;
import org.apache.fop.fo.pagination.PageSequence;
import org.apache.fop.fo.pagination.Root;
import org.apache.fop.fo.pagination.bookmarks.BookmarkTree;
import org.apache.fop.layoutmgr.ExternalDocumentLayoutManager;
import org.apache.fop.layoutmgr.LayoutManagerMaker;
import org.apache.fop.layoutmgr.LayoutManagerMapping;
import org.apache.fop.layoutmgr.PageSequenceLayoutManager;
import org.apache.fop.layoutmgr.TopLevelLayoutManager;

/**
 * Area tree handler for formatting objects.
 *
 * Concepts: The area tree is to be as small as possible. With minimal classes
 * and data to fully represent an area tree for formatting objects. The area
 * tree needs to be simple to render and follow the spec closely. This area tree
 * has the concept of page sequences. Wherever possible information is discarded
 * or optimized to keep memory use low. The data is also organized to make it
 * possible for renderers to minimize their output. A page can be saved if not
 * fully resolved and once rendered a page contains only size and id reference
 * information. The area tree pages are organized in a model that depends on the
 * type of renderer.
 */
public class AreaTreeHandler extends FOEventHandler {

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

    // Recorder of debug statistics
    private Statistics statistics;

    // The LayoutManager maker
    private LayoutManagerMaker lmMaker;

    /** The AreaTreeModel in use */
    protected AreaTreeModel model;

    // Flag for controlling complex script features (default: true).
    private boolean useComplexScriptFeatures = true;

    // Keeps track of all meaningful id references
    private IDTracker idTracker;

    // The fo:root node of the document
    private Root rootFObj;

    // The formatting results to be handed back to the caller.
    private FormattingResults results = new FormattingResults();

    private TopLevelLayoutManager prevPageSeqLM;

    private int idGen;

    /**
     * Constructor.
     *
     * @param userAgent FOUserAgent object for process
     * @param outputFormat the MIME type of the output format to use (ex.
     * "application/pdf").
     * @param stream OutputStream
     * @throws FOPException if the RenderPagesModel cannot be created
     */
    public AreaTreeHandler(FOUserAgent userAgent, String outputFormat,
            OutputStream stream) throws FOPException {
        super(userAgent);

        setupModel(userAgent, outputFormat, stream);

        this.lmMaker = userAgent.getLayoutManagerMakerOverride();
        if (lmMaker == null) {
            lmMaker = new LayoutManagerMapping(userAgent);
        }

        this.idTracker = new IDTracker();

        this.useComplexScriptFeatures = userAgent.isComplexScriptFeaturesEnabled();

        if (log.isDebugEnabled()) {
            statistics = new Statistics();
        }
    }

    /**
     * Sets up the AreaTreeModel instance for use by the AreaTreeHandler.
     *
     * @param userAgent FOUserAgent object for process
     * @param outputFormat the MIME type of the output format to use (ex.
     * "application/pdf").
     * @param stream OutputStream
     * @throws FOPException if the RenderPagesModel cannot be created
     */
    protected void setupModel(FOUserAgent userAgent, String outputFormat,
            OutputStream stream) throws FOPException {
        if (userAgent.isConserveMemoryPolicyEnabled()) {
            this.model = new CachedRenderPagesModel(userAgent, outputFormat, fontInfo, stream);
        } else {
            this.model = new RenderPagesModel(userAgent, outputFormat, fontInfo, stream);
        }
    }

    /**
     * Get the area tree model for this area tree.
     *
     * @return AreaTreeModel the model being used for this area tree
     */
    public AreaTreeModel getAreaTreeModel() {
        return this.model;
    }

    /**
     * Get the LayoutManager maker for this area tree.
     *
     * @return LayoutManagerMaker the LayoutManager maker being used for this
     *         area tree
     */
    public LayoutManagerMaker getLayoutManagerMaker() {
        return this.lmMaker;
    }

    /**
     * Get the IDTracker for this area tree.
     *
     * @return IDTracker used to track reference ids for items in this area tree
     */
    public IDTracker getIDTracker() {
        return this.idTracker;
    }

    /**
     * Get information about the rendered output, like number of pages created.
     *
     * @return the results structure
     */
    public FormattingResults getResults() {
        return this.results;
    }

    /**
     * Check whether complex script features are enabled.
     *
     * @return true if using complex script features
     */
    public boolean isComplexScriptFeaturesEnabled() {
        return useComplexScriptFeatures;
    }

    /**
     * Prepare AreaTreeHandler for document processing This is called from
     * FOTreeBuilder.startDocument()
     *
     * @throws SAXException
     *             if there is an error
     */
    @Override
    public void startDocument() throws SAXException {
        // Initialize statistics
        if (statistics != null) {
            statistics.start();
        }
    }

    @Override
    public void startRoot(Root root) {
        Locale locale = root.getLocale();
        if (locale != null) {
            model.setDocumentLocale(locale);
        }
    }

    /**
     * finish the previous pageSequence
     */
    private void finishPrevPageSequence(Numeric initialPageNumber) {
        if (prevPageSeqLM != null) {
            prevPageSeqLM.doForcePageCount(initialPageNumber);
            prevPageSeqLM.finishPageSequence();
            prevPageSeqLM = null;
        }
    }

    /** {@inheritDoc} */
    @Override
    public void startPageSequence(PageSequence pageSequence) {
        startAbstractPageSequence(pageSequence);
    }

    private void startAbstractPageSequence(AbstractPageSequence pageSequence) {
        rootFObj = pageSequence.getRoot();

        //Before the first page-sequence...
        if (this.prevPageSeqLM == null) {
            // extension attachments from fo:root
            wrapAndAddExtensionAttachments(rootFObj.getExtensionAttachments());
            // extension attachments from fo:declarations
            if (rootFObj.getDeclarations() != null) {
                wrapAndAddExtensionAttachments(
                        rootFObj.getDeclarations().getExtensionAttachments());
            }
        }

        finishPrevPageSequence(pageSequence.getInitialPageNumber());
        pageSequence.initPageNumber();
    }

    private void wrapAndAddExtensionAttachments(List<ExtensionAttachment> list) {
        for (ExtensionAttachment attachment : list) {
            addOffDocumentItem(new OffDocumentExtensionAttachment(attachment));
        }
    }

    /**
     * End the PageSequence. The PageSequence formats Pages and adds them to the
     * AreaTree. The area tree then handles what happens with the pages.
     *
     * @param pageSequence the page sequence ending
     */
    @Override
    public void endPageSequence(PageSequence pageSequence) {

        if (statistics != null) {
            statistics.end();
        }

        // If no main flow, nothing to layout!
        if (pageSequence.getMainFlow() != null) {
            PageSequenceLayoutManager pageSLM;
            pageSLM = getLayoutManagerMaker().makePageSequenceLayoutManager(
                    this, pageSequence);
            pageSLM.activateLayout();
            // preserve the current PageSequenceLayoutManger for the
            // force-page-count check at the beginning of the next PageSequence
            prevPageSeqLM = pageSLM;
        }
    }

    /** {@inheritDoc} */
    @Override
    public void startExternalDocument(ExternalDocument document) {
        startAbstractPageSequence(document);
    }

    /** {@inheritDoc} */
    @Override
    public void endExternalDocument(ExternalDocument document) {
        if (statistics != null) {
            statistics.end();
        }

        ExternalDocumentLayoutManager edLM;
        edLM = getLayoutManagerMaker().makeExternalDocumentLayoutManager(this, document);
        edLM.activateLayout();
        // preserve the current PageSequenceLayoutManger for the
        // force-page-count check at the beginning of the next PageSequence
        prevPageSeqLM = edLM;

    }

    /**
     * Called by the PageSequenceLayoutManager when it is finished with a
     * page-sequence.
     *
     * @param pageSequence the page-sequence just finished
     * @param pageCount The number of pages generated for the page-sequence
     */
    public void notifyPageSequenceFinished(AbstractPageSequence pageSequence,
            int pageCount) {
        this.results.haveFormattedPageSequence(pageSequence, pageCount);
        if (log.isDebugEnabled()) {
            log.debug("Last page-sequence produced " + pageCount + " pages.");
        }
    }

    /**
     * End the document.
     *
     * @throws SAXException if there is some error
     */
    @Override
    public void endDocument() throws SAXException {

        finishPrevPageSequence(null);
        // process fox:destination elements
        if (rootFObj != null) {
            List<Destination> destinationList = rootFObj.getDestinationList();
            if (destinationList != null) {
                while (destinationList.size() > 0) {
                    Destination destination = destinationList.remove(0);
                    DestinationData destinationData = new DestinationData(destination);
                    addOffDocumentItem(destinationData);
                }
            }
            // process fo:bookmark-tree
            BookmarkTree bookmarkTree = rootFObj.getBookmarkTree();
            if (bookmarkTree != null) {
                BookmarkData data = new BookmarkData(bookmarkTree);
                addOffDocumentItem(data);
                if (!data.isResolved()) {
                    // bookmarks did not fully resolve, add anyway. (hacky? yeah)
                    model.handleOffDocumentItem(data);
                }
            }
            idTracker.signalIDProcessed(rootFObj.getId());
        }
        model.endDocument();

        if (statistics != null) {
            statistics.logResults();
        }
    }

    /**
     * Add a OffDocumentItem to the area tree model. This checks if the
     * OffDocumentItem is resolvable and attempts to resolve or add the
     * resolvable ids for later resolution.
     *
     * @param odi the OffDocumentItem to add.
     */
    private void addOffDocumentItem(OffDocumentItem odi) {
        if (odi instanceof Resolvable) {
            Resolvable res = (Resolvable) odi;
            String[] ids = res.getIDRefs();
            for (String id : ids) {
                List<PageViewport> pageVPList = idTracker.getPageViewportsContainingID(id);
                if (pageVPList != null && !pageVPList.isEmpty()) {
                    res.resolveIDRef(id, pageVPList);
                } else {
                    AreaEventProducer eventProducer = AreaEventProducer.Provider.get(
                            getUserAgent().getEventBroadcaster());
                    eventProducer.unresolvedIDReference(this, odi.getName(), id);
                    idTracker.addUnresolvedIDRef(id, res);
                }
            }
            // check to see if ODI is now fully resolved, if so process it
            if (res.isResolved()) {
                model.handleOffDocumentItem(odi);
            }
        } else {
            model.handleOffDocumentItem(odi);
        }
    }

    /**
     * Generates and returns a unique key for a page viewport.
     *
     * @return the generated key.
     */
    public String generatePageViewportKey() {
        this.idGen++;
        return "P" + this.idGen;
    }

    /**
     * Tie a PageViewport with an ID found on a child area of the PV. Note that
     * an area with a given ID may be on more than one PV, hence an ID may have
     * more than one PV associated with it.
     *
     * @param id the property ID of the area
     * @param pv a page viewport that contains the area with this ID
     * @deprecated use getIDTracker().associateIDWithPageViewport(id, pv) instead
     */
    @Deprecated
    public void associateIDWithPageViewport(String id, PageViewport pv) {
        idTracker.associateIDWithPageViewport(id, pv);
    }

    /**
     * This method tie an ID to the areaTreeHandler until this one is ready to
     * be processed. This is used in page-number-citation-last processing so we
     * know when an id can be resolved.
     *
     * @param id the id of the object being processed
     * @deprecated use getIDTracker().signalPendingID(id) instead
     */
    @Deprecated
    public void signalPendingID(String id) {
        idTracker.signalPendingID(id);
    }

    /**
     * Signals that all areas for the formatting object with the given ID have
     * been generated. This is used to determine when page-number-citation-last
     * ref-ids can be resolved.
     *
     * @param id the id of the formatting object which was just finished
     * @deprecated use getIDTracker().signalIDProcessed(id) instead
     */
    @Deprecated
    public void signalIDProcessed(String id) {
        idTracker.signalIDProcessed(id);
    }

    /**
     * Check if an ID has already been resolved
     *
     * @param id the id to check
     * @return true if the ID has been resolved
     * @deprecated use getIDTracker().alreadyResolvedID(id) instead
     */
    @Deprecated
    public boolean alreadyResolvedID(String id) {
        return idTracker.alreadyResolvedID(id);
    }

    /**
     * Tries to resolve all unresolved ID references on the given page.
     *
     * @param pv page viewport whose ID refs to resolve
     * @deprecated use getIDTracker().tryIDResolution(pv) instead
     */
    @Deprecated
    public void tryIDResolution(PageViewport pv) {
        idTracker.tryIDResolution(pv);
    }

    /**
     * Get the set of page viewports that have an area with a given id.
     *
     * @param id the id to lookup
     * @return the list of PageViewports
     * @deprecated use getIDTracker().getPageViewportsContainingID(id) instead
     */
    @Deprecated
    public List<PageViewport> getPageViewportsContainingID(String id) {
        return idTracker.getPageViewportsContainingID(id);
    }

    /**
     * Add an Resolvable object with an unresolved idref
     *
     * @param idref the idref whose target id has not yet been located
     * @param res the Resolvable object needing the idref to be resolved
     * @deprecated use getIDTracker().addUnresolvedIDRef(idref, res) instead
     */
    @Deprecated
    public void addUnresolvedIDRef(String idref, Resolvable res) {
        idTracker.addUnresolvedIDRef(idref, res);
    }

    private class Statistics {
        // for statistics gathering
        private Runtime runtime;

        // heap memory allocated (for statistics)
        private long initialMemory;

        // time used in rendering (for statistics)
        private long startTime;

        /**
         * Default constructor
         */
        protected Statistics() {
            this.runtime = Runtime.getRuntime();
        }

        /**
         * starts the area tree handler statistics gathering
         */
        protected void start() {
            this.initialMemory = runtime.totalMemory() - runtime.freeMemory();
            this.startTime = System.currentTimeMillis();
        }

        /**
         * ends the area tree handler statistics gathering
         */
        protected void end() {
            long memoryNow = runtime.totalMemory() - runtime.freeMemory();
            log.debug("Current heap size: " + (memoryNow / 1024L) + "KB");
        }

        /**
         * logs the results of the area tree handler statistics gathering
         */
        protected void logResults() {
            long memoryNow = runtime.totalMemory() - runtime.freeMemory();
            long memoryUsed = (memoryNow - initialMemory) / 1024L;
            long timeUsed = System.currentTimeMillis() - startTime;
            int pageCount = rootFObj.getTotalPagesGenerated();
            log.debug("Initial heap size: " + (initialMemory / 1024L) + "KB");
            log.debug("Current heap size: " + (memoryNow / 1024L) + "KB");
            log.debug("Total memory used: " + memoryUsed + "KB");
            log.debug("Total time used: " + timeUsed + "ms");
            log.debug("Pages rendered: " + pageCount);
            if (pageCount > 0) {
                long perPage = (timeUsed / pageCount);
                long ppm = (timeUsed != 0 ? Math.round(60000 * pageCount
                        / (double) timeUsed) : -1);
                log.debug("Avg render time: " + perPage + "ms/page (" + ppm + "pages/min)");
            }
        }
    }
}
