/*
 * This program is free software; you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software
 * Foundation.
 *
 * You should have received a copy of the GNU Lesser General Public License along with this
 * program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
 * or from the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Lesser General Public License for more details.
 *
 * Copyright (c) 2006 - 2009 Object Refinery Ltd, Pentaho Corporation and Contributors.  All rights reserved.
 */

package org.pentaho.reporting.libraries.xmlns.parser;

import java.util.HashMap;

import org.pentaho.reporting.libraries.resourceloader.DependencyCollector;
import org.pentaho.reporting.libraries.resourceloader.ResourceKey;
import org.pentaho.reporting.libraries.resourceloader.ResourceManager;
import org.pentaho.reporting.libraries.base.config.DefaultConfiguration;
import org.pentaho.reporting.libraries.base.util.FastStack;
import org.pentaho.reporting.libraries.base.util.DebugLog;
import org.xml.sax.Attributes;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

/**
 * A base root SAX handler.
 *
 * @author Peter Becker
 * @author Thomas Morgner
 */
public class RootXmlReadHandler extends DefaultHandler
{
  /**
   * Storage for the parser configuration.
   */
  private DefaultConfiguration parserConfiguration;

  /**
   * The DocumentLocator can be used to resolve the current parse position.
   */
  private Locator documentLocator;

  /**
   * The current handlers.
   */
  private FastStack currentHandlers;

  /**
   * The list of parent handlers.
   */
  private FastStack outerScopes;

  /**
   * The root handler.
   */
  private XmlReadHandler rootHandler;

  /**
   * The object registry.
   */
  private HashMap objectRegistry;

  /**
   * A flag indicating whether this handler has initialized the root-element.
   */
  private boolean rootHandlerInitialized;

  /**
   * The current comment handler used to receive xml comments.
   */
  private CommentHandler commentHandler;

  private DependencyCollector dependencyCollector;
  private ResourceKey source;
  private ResourceKey context;
  private ResourceManager manager;
  private FastStack namespaces;
  private boolean firstCall;

  /**
   * Creates a new root-handler using the given versioning information and
   * resource-manager.
   *
   * @param manager the resource manager that loaded this xml-file.
   * @param source  the source-key that idenfies from where the file was loaded.
   * @param version the versioning information for the root-file.
   */
  public RootXmlReadHandler(final ResourceManager manager,
                            final ResourceKey source,
                            final long version)
  {
    this(manager, source, source, version);
  }

  /**
   * Creates a new root-handler using the given versioning information and
   * resource-manager.
   *
   * @param manager the resource manager that loaded this xml-file.
   * @param source  the source-key that idenfies from where the file was loaded.
   * @param context the key that should be used to resolve relative paths.
   * @param version the versioning information for the root-file.
   */
  public RootXmlReadHandler(final ResourceManager manager,
                            final ResourceKey source,
                            final ResourceKey context,
                            final long version)
  {
    if (manager == null)
    {
      throw new NullPointerException();
    }
    if (source == null)
    {
      throw new NullPointerException();
    }
    this.firstCall = true;
    this.manager = manager;
    this.source = source;
    this.context = context;
    this.dependencyCollector = new DependencyCollector(source, version);
    this.objectRegistry = new HashMap();
    this.parserConfiguration = new DefaultConfiguration();
    this.commentHandler = new CommentHandler();
    this.namespaces = new FastStack();
  }

  /**
   * Returns the context key. This key may specity a base context for loading
   * resources. (It behaves like the 'base-url' setting of HTML and allows to
   * reference external resources as relative paths without being bound to the
   * original location of the xml file.)
   *
   * @return the context.
   */
  public ResourceKey getContext()
  {
    return context;
  }

  /**
   * Returns the resource-manager that is used to load external resources.
   *
   * @return the resource-manager.
   */
  public ResourceManager getResourceManager()
  {
    return manager;
  }

  /**
   * Checks, whether this is the first call to the handler.
   *
   * @return true, if this is the first call, false otherwise.
   */
  public boolean isFirstCall()
  {
    return firstCall;
  }

  /**
   * Returns the source key. This key points to the file or stream that is
   * currently parsed.
   *
   * @return the source key.
   */
  public ResourceKey getSource()
  {
    return source;
  }

  /**
   * Returns the current dependency collector for this parse-operation.
   * The Collector allows to check compound-keys for changes.
   *
   * @return the dependency collector.
   */
  public DependencyCollector getDependencyCollector()
  {
    return dependencyCollector;
  }

  /**
   * Returns the comment handler that is used to collect comments.
   *
   * @return the comment handler.
   */
  public CommentHandler getCommentHandler()
  {
    return this.commentHandler;
  }

  /**
   * Returns the parser-configuration. This can be use to configure
   * the parsing process.
   *
   * @return the parser's configuration.
   */
  public DefaultConfiguration getParserConfiguration()
  {
    return parserConfiguration;
  }

  /**
   * Receive an object for locating the origin of SAX document events.
   * <p/>
   * The documentLocator allows the application to determine the end position of
   * any document-related event, even if the parser is not reporting an error.
   * Typically, the application will use this information for reporting its own
   * errors (such as character content that does not match an application's
   * business rules). The information returned by the documentLocator is
   * probably not sufficient for use with a search engine.
   *
   * @param locator the documentLocator.
   */
  public void setDocumentLocator(final Locator locator)
  {
    this.documentLocator = locator;
  }

  /**
   * Returns the current documentLocator.
   *
   * @return the documentLocator.
   */
  public Locator getDocumentLocator()
  {
    return this.documentLocator;
  }

  /**
   * Adds an object to the registry.
   *
   * @param key   the key.
   * @param value the object.
   */
  public void setHelperObject(final String key, final Object value)
  {
    if (value == null)
    {
      this.objectRegistry.remove(key);
    }
    else
    {
      this.objectRegistry.put(key, value);
    }
  }

  /**
   * Returns an object from the registry.
   *
   * @param key the key.
   * @return The object.
   */
  public Object getHelperObject(final String key)
  {
    return this.objectRegistry.get(key);
  }

  /**
   * Returns the array of all currently registered helper-objects. Helper
   * objects are used as simple communication process between the various
   * handler implementations.
   *
   * @return the helper object names.
   */
  public String[] getHelperObjectNames()
  {
    return (String[]) this.objectRegistry.keySet().toArray
        (new String[objectRegistry.size()]);
  }

  /**
   * Sets the root SAX handler.
   *
   * @param handler the SAX handler.
   */
  protected void setRootHandler(final XmlReadHandler handler)
  {
    if (handler == null)
    {
      throw new NullPointerException();
    }
    this.rootHandler = handler;
    this.rootHandlerInitialized = false;
  }

  /**
   * Returns the root SAX handler.
   *
   * @return the root SAX handler.
   */
  protected XmlReadHandler getRootHandler()
  {
    return this.rootHandler;
  }

  /**
   * Start a new handler stack and delegate to another handler.
   *
   * @param handler the handler.
   * @param uri     the namespace uri of the current tag.
   * @param tagName the tag name.
   * @param attrs   the attributes.
   * @throws SAXException if there is a problem with the parser.
   */
  public void recurse(final XmlReadHandler handler,
                      final String uri,
                      final String tagName,
                      final Attributes attrs)
      throws SAXException
  {
    if (handler == null)
    {
      throw new NullPointerException();
    }

    this.outerScopes.push(this.currentHandlers);
    this.currentHandlers = new FastStack();
    this.currentHandlers.push(handler);
    handler.startElement(uri, tagName, attrs);

  }

  /**
   * Delegate to another handler.
   *
   * @param handler the new handler.
   * @param tagName the tag name.
   * @param uri     the namespace uri of the current tag.
   * @param attrs   the attributes.
   * @throws SAXException if there is a problem with the parser.
   */
  public void delegate(final XmlReadHandler handler,
                       final String uri,
                       final String tagName,
                       final Attributes attrs)
      throws SAXException
  {
    if (handler == null)
    {
      throw new NullPointerException();
    }
    this.currentHandlers.push(handler);
    handler.init(this, uri, tagName);
    handler.startElement(uri, tagName, attrs);
  }

  /**
   * Hand control back to the previous handler.
   *
   * @param tagName the tagname.
   * @param uri     the namespace uri of the current tag.
   * @throws SAXException if there is a problem with the parser.
   */
  public void unwind(final String uri, final String tagName)
      throws SAXException
  {
    // remove current handler from stack ..
    this.currentHandlers.pop();
    if (this.currentHandlers.isEmpty() && !this.outerScopes.isEmpty())
    {
      // if empty, but "recurse" had been called, then restore the old handler stack ..
      // but do not end the recursed element ..
      this.currentHandlers = (FastStack) this.outerScopes.pop();
    }
    else if (!this.currentHandlers.isEmpty())
    {
      // if there are some handlers open, close them too (these handlers must be delegates)..
      getCurrentHandler().endElement(uri, tagName);
    }
  }

  /**
   * Returns the current handler.
   *
   * @return The current handler.
   */
  protected XmlReadHandler getCurrentHandler()
  {
    return (XmlReadHandler) this.currentHandlers.peek();
  }

  /**
   * Starts processing a document.
   *
   * @throws SAXException not in this implementation.
   */
  public void startDocument() throws SAXException
  {
    this.outerScopes = new FastStack();
    this.currentHandlers = new FastStack();
    if (rootHandler != null)
    {
      // When dealing with the multiplexing beast, we cant define a
      // root handler unless we've seen the first element and all its
      // namespace declarations ...
      this.currentHandlers.push(this.rootHandler);
    }
  }

  /**
   * Starts processing an element.
   *
   * @param originalUri the URI.
   * @param localName   the local name.
   * @param qName       the qName.
   * @param attributes  the attributes.
   * @throws SAXException if there is a parsing problem.
   */
  public final void startElement(final String originalUri,
                                 final String localName,
                                 final String qName,
                                 final Attributes attributes)
      throws SAXException
  {
    // Check the default-namespace ..
    if (firstCall)
    {
      firstCall = false;
      interceptFirstStartElement(originalUri, localName, qName, attributes);
      return;
    }

    final String defaultNamespace;
    final String nsuri = attributes.getValue("xmlns");
    if (nsuri != null)
    {
      defaultNamespace = nsuri;
    }
    else if (namespaces.isEmpty())
    {
      defaultNamespace = "";
    }
    else
    {
      defaultNamespace = (String) namespaces.peek();
    }

    pushDefaultNamespace(defaultNamespace);

    final String uri;
    if ((originalUri == null || "".equals(originalUri)) &&
        defaultNamespace != null)
    {
      uri = defaultNamespace;
    }
    else
    {
      uri = originalUri;
    }

    if (rootHandlerInitialized == false)
    {
      rootHandler.init(this, uri, localName);
      rootHandlerInitialized = true;
    }

    final XmlReadHandler currentHandler = getCurrentHandler();
    currentHandler.startElement(uri, localName,
        wrapAttributes(new FixNamespaceUriAttributes(uri, attributes)));
  }

  protected Attributes wrapAttributes(final Attributes attributes)
  {
    return attributes;
  }

  /**
   * A helper call that allows to override the first call to the startElememt
   * method. This allows the implementation of an multiplexing parser, which
   * requires the information from the root-level elements.
   *
   * @param uri        the namespace uri of the current tag.
   * @param localName  the unqualified tag-name.
   * @param qName      the qualified tag-name.
   * @param attributes the attributes of the current element.
   * @throws SAXException if something goes wrong.
   */
  protected void interceptFirstStartElement(final String uri,
                                            final String localName,
                                            final String qName,
                                            final Attributes attributes)
      throws SAXException
  {
    startElement(uri, localName, qName, attributes);
  }

  /**
   * Updates the current default namespace.
   *
   * @param nsuri the uri of the current namespace.
   */
  protected final void pushDefaultNamespace(final String nsuri)
  {
    namespaces.push(nsuri);
  }

  /**
   * Sets and configures the root handle for the given root-level element.
   *
   * @param handler    the read handler for the root element.
   * @param uri        the uri of the root elements namespace.
   * @param localName  the local tagname of the root element.
   * @param attributes the attributes of the root element.
   * @throws SAXException if something goes wrong.
   */
  protected void installRootHandler(final XmlReadHandler handler,
                                    final String uri,
                                    final String localName,
                                    final Attributes attributes)
      throws SAXException
  {
    if (handler == null)
    {
      throw new NullPointerException();
    }
    this.rootHandler = handler;
    this.rootHandler.init(this, uri, localName);
    this.currentHandlers.push(handler);
    this.rootHandlerInitialized = true;
    this.rootHandler.startElement(uri, localName, attributes);
  }

  /**
   * Process character data.
   *
   * @param ch     the character buffer.
   * @param start  the start index.
   * @param length the length of the character data.
   * @throws SAXException if there is a parsing error.
   */
  public void characters(final char[] ch, final int start, final int length)
      throws SAXException
  {
    try
    {
      getCurrentHandler().characters(ch, start, length);
    }
    catch (SAXException se)
    {
      throw se;
    }
    catch (Exception e)
    {
      throw new ParseException
          ("Failed at handling character data", e, getDocumentLocator());
    }
  }

  /**
   * Finish processing an element.
   *
   * @param originalUri the URI.
   * @param localName   the local name.
   * @param qName       the qName.
   * @throws SAXException if there is a parsing error.
   */
  public final void endElement(final String originalUri,
                               final String localName,
                               final String qName)
      throws SAXException
  {
    final String defaultNamespace = (String) namespaces.pop();
    final String uri;
    if ((originalUri == null || "".equals(originalUri)) &&
        defaultNamespace != null)
    {
      uri = defaultNamespace;
    }
    else
    {
      uri = originalUri;
    }

    final XmlReadHandler currentHandler = getCurrentHandler();
    currentHandler.endElement(uri, localName);
  }

  /**
   * Tries to return the parse-result of the selected root-handler.
   *
   * @return the parse-result.
   * @throws SAXException if an error occurs.
   */
  public Object getResult() throws SAXException
  {
    if (this.rootHandler != null)
    {
      return this.rootHandler.getObject();
    }
    return null;
  }
}
