/*
 * 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 java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.pentaho.reporting.libraries.resourceloader.DependencyCollector;
import org.pentaho.reporting.libraries.resourceloader.FactoryParameterKey;
import org.pentaho.reporting.libraries.resourceloader.Resource;
import org.pentaho.reporting.libraries.resourceloader.ResourceException;
import org.pentaho.reporting.libraries.resourceloader.ResourceKey;
import org.pentaho.reporting.libraries.resourceloader.ResourceLoadingException;
import org.pentaho.reporting.libraries.resourceloader.ResourceManager;
import org.pentaho.reporting.libraries.base.util.ObjectUtilities;
import org.xml.sax.Attributes;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;

/**
 * A base class for implementing an {@link org.pentaho.reporting.libraries.xmlns.parser.XmlReadHandler}.
 * This class takes care of all the delegation management.
 */
public abstract class AbstractXmlReadHandler implements XmlReadHandler
{
  private static final Log logger = LogFactory.getLog(AbstractXmlReadHandler.class);

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

  /**
   * The tag name.
   */
  private String tagName;

  /**
   * THe namespace URI.
   */
  private String uri;

  /**
   * A flag indicating the first call.
   */
  private boolean firstCall;

  /**
   * Creates a new handler.
   */
  protected AbstractXmlReadHandler()
  {
  }

  /**
   * Initialises the handler.
   *
   * @param rootHandler the root handler.
   * @param tagName     the tag name.
   */
  public void init(final RootXmlReadHandler rootHandler,
                   final String uri,
                   final String tagName)
  {
    if (rootHandler == null)
    {
      throw new NullPointerException("Root handler must not be null");
    }
    if (tagName == null)
    {
      throw new NullPointerException("Tag name must not be null");
    }
    this.uri = uri;
    this.rootHandler = rootHandler;
    this.tagName = tagName;
    this.firstCall = true;
  }

  /**
   * This method is called at the start of an element.
   *
   * @param tagName the tag name.
   * @param attrs   the attributes.
   * @throws SAXException if there is a parsing error.
   */
  public final void startElement(final String uri,
                                 final String tagName,
                                 final Attributes attrs)
      throws SAXException
  {
    if (this.firstCall)
    {
      if (!this.tagName.equals(tagName) || !this.uri.equals(uri))
      {
        throw new ParseException(
            "Expected <" + this.tagName + ">, found <" + tagName + '>', getLocator());
      }
      this.firstCall = false;
      startParsing(attrs);
    }
    else
    {
      final XmlReadHandler childHandler = getHandlerForChild(uri, tagName, attrs);
      if (childHandler == null)
      {
        logger.warn("Unknown tag <" + uri + ':' + tagName + ">: Start to ignore this element and all of its childs. " + getLocatorString());
        logger.debug(this.getClass());
        final IgnoreAnyChildReadHandler ignoreAnyChildReadHandler =
            new IgnoreAnyChildReadHandler();
        ignoreAnyChildReadHandler.init(getRootHandler(), uri, tagName);
        this.rootHandler.recurse(ignoreAnyChildReadHandler, uri, tagName, attrs);
      }
      else
      {
        childHandler.init(getRootHandler(), uri, tagName);
        this.rootHandler.recurse(childHandler, uri, tagName, attrs);
      }
    }
  }

  /**
   * This method is called to process the character data between element tags.
   *
   * @param ch     the character buffer.
   * @param start  the start index.
   * @param length the length.
   * @throws SAXException if there is a parsing error.
   */
  public void characters(final char[] ch, final int start, final int length)
      throws SAXException
  {
    // nothing required
  }

  /**
   * This method is called at the end of an element.
   *
   * @param tagName the tag name.
   * @throws SAXException if there is a parsing error.
   */
  public final void endElement(final String uri,
                               final String tagName) throws SAXException
  {
    if (this.tagName.equals(tagName) && this.uri.equals(uri))
    {
      doneParsing();
      this.rootHandler.unwind(uri, tagName);
    }
    else
    {
      throw new ParseException("Illegal Parser State." + toString(), getLocator());
    }
  }

  /**
   * Computes a string containing the current parse location or an empty string if there is no locator.
   *
   * @return the location as debug-text.
   */
  private String getLocatorString()
  {
    final Locator locator = getLocator();
    if (locator == null)
    {
      return "";
    }
    final StringBuffer message = new StringBuffer(100);
    message.append(" [Location: Line=");
    message.append(locator.getLineNumber());
    message.append(" Column=");
    message.append(locator.getColumnNumber());
    message.append("] ");
    return message.toString();

  }

  /**
   * Starts parsing.
   *
   * @param attrs the attributes.
   * @throws SAXException if there is a parsing error.
   */
  protected void startParsing(final Attributes attrs) throws SAXException
  {
    // nothing required
  }

  /**
   * Done parsing.
   *
   * @throws SAXException if there is a parsing error.
   */
  protected void doneParsing() throws SAXException
  {
    // nothing required
  }

  /**
   * Checks whether the given url denotes the same namespace as the element's namespace.
   *
   * @param namespaceURI the namespace that should be tested.
   * @return true, if the namespace matches the element's namespace,false otherwise.
   */
  protected boolean isSameNamespace(final String namespaceURI)
  {
    return ObjectUtilities.equal(namespaceURI, getUri());
  }

  /**
   * Returns the handler for a child element.
   *
   * @param uri     the URI of the namespace of the current element.
   * @param tagName the tag name.
   * @param atts    the attributes.
   * @return the handler or null, if the tagname is invalid.
   * @throws SAXException if there is a parsing error.
   */
  protected XmlReadHandler getHandlerForChild(final String uri,
                                              final String tagName,
                                              final Attributes atts)
      throws SAXException
  {
    return null;
  }

  /**
   * Returns the tag name.
   *
   * @return the tag name.
   */
  public String getTagName()
  {
    return this.tagName;
  }

  /**
   * Returns the uri of the element. The URI identifies the namespace.
   *
   * @return the element's URI.
   */
  public String getUri()
  {
    return uri;
  }

  /**
   * Returns the root handler for the parsing.
   *
   * @return the root handler.
   */
  public RootXmlReadHandler getRootHandler()
  {
    return this.rootHandler;
  }

  /**
   * Returns the locator as provided by the XML parser. This method may return null if the XML parser has no
   * locator support.
   *
   * @return the locator or null.
   */
  public Locator getLocator()
  {
    return rootHandler.getDocumentLocator();
  }

  /**
   * Parses an external file using LibLoader and returns the parsed result as an object of type
   * <code>targetClass</code>. The file is given as relative pathname (relative to the current source file).
   * The current helper-methods are used as parse-parameters for the external parsing.
   *
   * @param file the file to be parsed.
   * @param targetClass the target type of the parse operation.
   * @return the result, never null.
   * @throws ParseException if parsing the result failed for some reason.
   * @throws ResourceLoadingException if there was an IO error loading the resource.
   * @see #deriveParseParameters()
   * @see #performExternalParsing(String, Class, Map)  
   */
  protected Object performExternalParsing(final String file, final Class targetClass)
      throws ParseException, ResourceLoadingException
  {
    return performExternalParsing(file, targetClass, deriveParseParameters());
  }

  /**
   * Parses an external file using LibLoader and returns the parsed result as an object of type
   * <code>targetClass</code>. The file is given as relative pathname (relative to the current source file).
   * The current helper-methods are used as parse-parameters for the external parsing.
   *
   * @param file the file to be parsed.
   * @param targetClass the target type of the parse operation.
   * @param map the map of parse parameters.
   * @return the result, never null.
   * @throws ParseException if parsing the result failed for some reason.
   * @throws ResourceLoadingException if there was an IO error loading the resource.
   * @see #deriveParseParameters()
   */
  protected Object performExternalParsing(final String file, final Class targetClass, final Map map)
      throws ParseException, ResourceLoadingException
  {
    try
    {
      final ResourceManager resourceManager = rootHandler.getResourceManager();
      final ResourceKey source = rootHandler.getSource();

      final ResourceKey target = resourceManager.deriveKey(source, file, map);
      final DependencyCollector dc = rootHandler.getDependencyCollector();

      final Resource resource = resourceManager.create(target, rootHandler.getContext(), targetClass);
      dc.add(resource);
      return resource.getResource();
    }
    catch (ResourceLoadingException rle)
    {
      throw rle;
    }
    catch (ResourceException e)
    {
      throw new ParseException("Failure while loading data: " + file, e, getLocator());
    }

  }

  /**
   * Creates a working copy of the current parse state.
   *
   * @noinspection ObjectAllocationInLoop as this is a cloning operation.
   * @return the derived parse-parameters.
   */
  protected Map deriveParseParameters()
  {
    final RootXmlReadHandler rootHandler = getRootHandler();
    final HashMap map = new HashMap();
    final String[] names = rootHandler.getHelperObjectNames();
    final int length = names.length;
    for (int i = 0; i < length; i++)
    {
      final String name = names[i];
      final FactoryParameterKey key = new FactoryParameterKey(name);
      map.put(key, rootHandler.getHelperObject(name));
    }
    return map;
  }
}
