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

import java.io.OutputStream;
import java.security.AccessController;
import java.security.PrivilegedAction;

import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;

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

import org.apache.xmlgraphics.util.QName;

import org.apache.fop.accessibility.fo.FO2StructureTreeConverter;
import org.apache.fop.apps.FOPException;
import org.apache.fop.apps.FOUserAgent;
import org.apache.fop.apps.FormattingResults;
import org.apache.fop.fo.ElementMapping.Maker;
import org.apache.fop.fo.extensions.ExtensionElementMapping;
import org.apache.fop.fo.pagination.Root;
import org.apache.fop.render.pdf.extensions.PDFElementMapping;
import org.apache.fop.util.ContentHandlerFactory;
import org.apache.fop.util.ContentHandlerFactory.ObjectBuiltListener;
import org.apache.fop.util.ContentHandlerFactory.ObjectSource;

/**
 * SAX Handler that passes parsed data to the various
 * FO objects, where they can be used either to build
 * an FO Tree, or used by Structure Renderers to build
 * other data structures.
 */
public class FOTreeBuilder extends DefaultHandler {

    /** logging instance */
    private static final Log LOG = LogFactory.getLog(FOTreeBuilder.class);

    /** The registry for ElementMapping instances */
    protected ElementMappingRegistry elementMappingRegistry;

    /** The root of the formatting object tree */
    protected Root rootFObj;

    /** Main DefaultHandler that handles the FO namespace. */
    protected MainFOHandler mainFOHandler;

    /** Current delegate ContentHandler to receive the SAX events */
    protected ContentHandler delegate;

    /** Provides information used during tree building stage. */
    private FOTreeBuilderContext builderContext;

    /** The object that handles formatting and rendering to a stream */
    private FOEventHandler foEventHandler;

    /** The SAX locator object managing the line and column counters */
    private Locator locator;

    /** The user agent for this processing run. */
    private FOUserAgent userAgent;

    private boolean used;
    private boolean empty = true;

    private int depth;
    private boolean errorinstart;

    /**
     * <code>FOTreeBuilder</code> constructor
     *
     * @param outputFormat the MIME type of the output format to use (ex. "application/pdf").
     * @param foUserAgent   the {@link FOUserAgent} in effect for this process
     * @param stream    the <code>OutputStream</code> to direct the results to
     * @throws FOPException if the <code>FOTreeBuilder</code> cannot be properly created
     */
    public FOTreeBuilder(
                String outputFormat,
                FOUserAgent foUserAgent,
                OutputStream stream)
            throws FOPException {

        this.userAgent = foUserAgent;
        this.elementMappingRegistry = userAgent.getElementMappingRegistry();
        //This creates either an AreaTreeHandler and ultimately a Renderer, or
        //one of the RTF-, MIF- etc. Handlers.
        foEventHandler = foUserAgent.getRendererFactory().createFOEventHandler(
                foUserAgent, outputFormat, stream);
        if (userAgent.isAccessibilityEnabled()) {
            foEventHandler = new FO2StructureTreeConverter(
                    foUserAgent.getStructureTreeEventHandler(), foEventHandler);
        }
        builderContext = new FOTreeBuilderContext();
        builderContext.setPropertyListMaker(new PropertyListMaker() {
            public PropertyList make(FObj fobj, PropertyList parentPropertyList) {
                return new StaticPropertyList(fobj, parentPropertyList);
            }
        });
    }

    /** {@inheritDoc} */
    public void setDocumentLocator(Locator locator) {
        this.locator = locator;
    }

    /**
     * @return a {@link Locator} instance if it is available and not disabled
     */
    protected Locator getEffectiveLocator() {
        return (userAgent.isLocatorEnabled() ? this.locator : null);
    }

    /** {@inheritDoc} */
    public void characters(char[] data, int start, int length)
                throws SAXException {
        delegate.characters(data, start, length);
    }

    /** {@inheritDoc} */
    public void startDocument() throws SAXException {
        if (used) {
            throw new IllegalStateException("FOTreeBuilder (and the Fop class) cannot be reused."
                    + " Please instantiate a new instance.");
        }

        used = true;
        empty = true;
        rootFObj = null;    // allows FOTreeBuilder to be reused
        if (LOG.isDebugEnabled()) {
            LOG.debug("Building formatting object tree");
        }
        foEventHandler.startDocument();
        this.mainFOHandler = new MainFOHandler();
        this.mainFOHandler.startDocument();
        this.delegate = this.mainFOHandler;
    }

    /** {@inheritDoc} */
    public void endDocument() throws SAXException {
        this.delegate.endDocument();
        if (this.rootFObj == null && empty) {
            FOValidationEventProducer eventProducer
                = FOValidationEventProducer.Provider.get(userAgent.getEventBroadcaster());
            eventProducer.emptyDocument(this);
        }
        rootFObj = null;
        if (LOG.isDebugEnabled()) {
            LOG.debug("Parsing of document complete");
        }
        foEventHandler.endDocument();
    }

    /** {@inheritDoc} */
    public void startElement(final String namespaceURI, final String localName, final String rawName,
                             final Attributes attlist) throws SAXException {
        this.depth++;
        errorinstart = false;
        final ContentHandler contentHandler = delegate;
        SAXException saxException = AccessController.doPrivileged(
            new PrivilegedAction<SAXException>() {
                public SAXException run() {
                    try {
                        contentHandler.startElement(namespaceURI, localName, rawName, attlist);
                    } catch (SAXException e) {
                        return e;
                    }
                    return null;
                }
            }
        );
        if (saxException != null) {
            errorinstart = true;
            throw saxException;
        }
    }

    /** {@inheritDoc} */
    public void endElement(final String uri, final String localName, final String rawName) throws SAXException {
        if (!errorinstart) {
            final ContentHandler contentHandler = delegate;
            SAXException saxException = AccessController.doPrivileged(
                new PrivilegedAction<SAXException>() {
                    public SAXException run() {
                        try {
                            contentHandler.endElement(uri, localName, rawName);
                        } catch (SAXException e) {
                            return e;
                        }
                        return null;
                    }
                }
            );
            if (saxException != null) {
                throw saxException;
            }

            this.depth--;
            if (depth == 0) {
                if (delegate != mainFOHandler) {
                    //Return from sub-handler back to main handler
                    delegate.endDocument();
                    delegate = mainFOHandler;
                    delegate.endElement(uri, localName, rawName);
                }
            }
        }
    }

    /** {@inheritDoc} */
    public void warning(SAXParseException e) {
        LOG.warn(e.getLocalizedMessage());
    }

    /** {@inheritDoc} */
    public void error(SAXParseException e) {
        LOG.error(e.toString());
    }

    /** {@inheritDoc} */
    public void fatalError(SAXParseException e) throws SAXException {
        LOG.error(e.toString());
        throw e;
    }

    /**
     * Provides access to the underlying {@link FOEventHandler} object.
     *
     * @return the FOEventHandler object
     */
    public FOEventHandler getEventHandler() {
        return foEventHandler;
    }

    /**
     * Returns the results of the rendering process. Information includes
     * the total number of pages generated and the number of pages per
     * page-sequence.
     *
     * @return the results of the rendering process.
     */
    public FormattingResults getResults() {
        return getEventHandler().getResults();
    }

    /**
     * Main <code>DefaultHandler</code> implementation which builds the FO tree.
     */
    private class MainFOHandler extends DefaultHandler {

        /** Current formatting object being handled */
        protected FONode currentFObj;

        /** Current propertyList for the node being handled */
        protected PropertyList currentPropertyList;

        /** Current marker nesting-depth */
        private int nestedMarkerDepth;

        /** {@inheritDoc} */
        public void startElement(String namespaceURI, String localName, String rawName,
                                 Attributes attlist) throws SAXException {

            /* the node found in the FO document */
            FONode foNode;
            PropertyList propertyList = null;

            // Check to ensure first node encountered is an fo:root
            if (rootFObj == null) {
                empty = false;
                if (!namespaceURI.equals(FOElementMapping.URI)
                        || !localName.equals("root")) {
                    FOValidationEventProducer eventProducer
                        = FOValidationEventProducer.Provider.get(
                                userAgent.getEventBroadcaster());
                    eventProducer.invalidFORoot(this, FONode.getNodeString(namespaceURI, localName),
                            getEffectiveLocator());
                }
            } else { // check that incoming node is valid for currentFObj
                if (currentFObj.getNamespaceURI().equals(FOElementMapping.URI)
                    || currentFObj.getNamespaceURI().equals(ExtensionElementMapping.URI)
                    || currentFObj.getNamespaceURI().equals(PDFElementMapping.NAMESPACE)) {
                    if (!currentFObj.isChangeBarElement(namespaceURI, localName)) {
                        currentFObj.validateChildNode(locator, namespaceURI, localName);
                    }
                }
            }

            ElementMapping.Maker fobjMaker = findFOMaker(namespaceURI, localName);

            try {
                foNode = fobjMaker.make(currentFObj);
                if (rootFObj == null) {
                    rootFObj = (Root) foNode;
                    rootFObj.setBuilderContext(builderContext);
                    rootFObj.setFOEventHandler(foEventHandler);
                }
                propertyList = foNode.createPropertyList(
                                    currentPropertyList, foEventHandler);
                foNode.processNode(localName, getEffectiveLocator(),
                                    attlist, propertyList);
                if (foNode.getNameId() == Constants.FO_MARKER) {
                    if (builderContext.inMarker()) {
                        nestedMarkerDepth++;
                    } else {
                        builderContext.switchMarkerContext(true);
                    }
                }
                if (foNode.getNameId() == Constants.FO_PAGE_SEQUENCE) {
                    builderContext.getXMLWhiteSpaceHandler().reset();
                }
            } catch (IllegalArgumentException e) {
                throw new SAXException(e);
            }

            ContentHandlerFactory chFactory = foNode.getContentHandlerFactory();
            if (chFactory != null) {
                ContentHandler subHandler = chFactory.createContentHandler();
                if (subHandler instanceof ObjectSource
                        && foNode instanceof ObjectBuiltListener) {
                    ((ObjectSource) subHandler).setObjectBuiltListener(
                            (ObjectBuiltListener) foNode);
                }

                subHandler.startDocument();
                subHandler.startElement(namespaceURI, localName,
                        rawName, attlist);
                depth = 1;
                delegate = subHandler;
            }

            if (currentFObj != null) {
                currentFObj.addChildNode(foNode);
            }

            currentFObj = foNode;
            if (propertyList != null && !builderContext.inMarker()) {
                currentPropertyList = propertyList;
            }

            // fo:characters can potentially be removed during
            // white-space handling.
            // Do not notify the FOEventHandler.
            if (currentFObj.getNameId() != Constants.FO_CHARACTER
                    && (!builderContext.inMarker() || currentFObj.getNameId() == Constants.FO_MARKER)) {
                currentFObj.startOfNode();
            }
        }

        /** {@inheritDoc} */
        public void endElement(String uri, String localName, String rawName)
                    throws SAXException {
            if (currentFObj == null) {
                throw new SAXException(
                        "endElement() called for " + rawName
                            + " where there is no current element.");
            } else if (!currentFObj.getLocalName().equals(localName)
                    || !currentFObj.getNamespaceURI().equals(uri)) {
                throw new SAXException("Mismatch: " + currentFObj.getLocalName()
                        + " (" + currentFObj.getNamespaceURI()
                        + ") vs. " + localName + " (" + uri + ")");
            }

            // fo:characters can potentially be removed during
            // white-space handling.
            // Do not notify the FOEventHandler.
            if (currentFObj.getNameId() != Constants.FO_CHARACTER
                    && (!builderContext.inMarker() || currentFObj.getNameId() == Constants.FO_MARKER)) {
                currentFObj.endOfNode();
            }

            if (currentPropertyList != null
                    && currentPropertyList.getFObj() == currentFObj
                    && !builderContext.inMarker()) {
                currentPropertyList = currentPropertyList.getParentPropertyList();
            }

            if (currentFObj.getNameId() == Constants.FO_MARKER) {
                if (nestedMarkerDepth == 0) {
                    builderContext.switchMarkerContext(false);
                } else {
                    nestedMarkerDepth--;
                }
            }

            if (currentFObj.getParent() == null) {
                LOG.debug("endElement for top-level " + currentFObj.getName());
            }

            currentFObj = currentFObj.getParent();
        }

        /** {@inheritDoc} */
        public void characters(char[] data, int start, int length)
            throws FOPException {
            if (currentFObj != null) {
                currentFObj.characters(data, start, length,
                        currentPropertyList, getEffectiveLocator());
            }
        }

        /** {@inheritDoc} */
        public void endDocument() throws SAXException {
            currentFObj = null;
        }

        /**
         * Finds the {@link Maker} used to create {@link FONode} objects of a particular type
         *
         * @param namespaceURI URI for the namespace of the element
         * @param localName name of the Element
         * @return the ElementMapping.Maker that can create an FO object for this element
         * @throws FOPException if a Maker could not be found for a bound namespace.
         */
        private Maker findFOMaker(String namespaceURI, String localName) throws FOPException {
            Maker maker = elementMappingRegistry.findFOMaker(namespaceURI, localName, locator);
            if (maker instanceof UnknownXMLObj.Maker) {
                FOValidationEventProducer eventProducer
                    = FOValidationEventProducer.Provider.get(
                        userAgent.getEventBroadcaster());
                String name = (currentFObj != null ? currentFObj.getName()
                                                   : "{" + namespaceURI + "}" + localName);
                eventProducer.unknownFormattingObject(this, name,
                        new QName(namespaceURI, localName),
                        getEffectiveLocator());
            }
            return maker;
        }

    }
}

