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

import java.util.Iterator;
import java.util.Arrays;

import org.pentaho.reporting.libraries.base.util.ObjectUtilities;
import org.pentaho.reporting.libraries.base.util.LinkedMap;

/**
 * The attribute list is used by a writer to specify the attributes of an XML
 * element in a certain order.
 *
 * @author Thomas Morgner
 */
public class AttributeList
{
  /**
   * A constant containing the XML-Namespace namespace identifier.
   */
  public static final String XMLNS_NAMESPACE = "http://www.w3.org/2000/xmlns/";
  /**
   * A constant containing the XML namespace identifier.
   */
  public static final String XML_NAMESPACE = "http://www.w3.org/XML/1998/namespace";

  /**
   * A name/value pair of the attribute list.
   */
  public static class AttributeEntry
  {
    /**
     * The namespace of the attribute entry.
     */
    private String namespace;

    /**
     * The name of the attribute entry.
     */
    private String name;

    /**
     * The value of the attribute entry.
     */
    private String value;

    /**
     * Creates a new attribute entry for the given name and value.
     *
     * @param namespace the namespace of the attribute.
     * @param name      the attribute name (<code>null</code> not permitted).
     * @param value     the attribute value (<code>null</code> not permitted).
     */
    public AttributeEntry(final String namespace,
                          final String name,
                          final String value)
    {
      if (name == null)
      {
        throw new NullPointerException("Name must not be null. ["
            + name + ", " + value + ']');
      }
      if (value == null)
      {
        throw new NullPointerException("Value must not be null. ["
            + name + ", " + value + ']');
      }
      this.namespace = namespace;
      this.name = name;
      this.value = value;
    }

    /**
     * Returns the attribute name.
     *
     * @return the name.
     */
    public String getName()
    {
      return this.name;
    }

    /**
     * Returns the value of this attribute entry.
     *
     * @return the value of the entry.
     */
    public String getValue()
    {
      return this.value;
    }

    /**
     * Returns the attribute namespace (which can be null).
     *
     * @return the namespace.
     */
    public String getNamespace()
    {
      return namespace;
    }

    /**
     * Compares this attribute entry for equality with an other object.
     *
     * @param o the other object.
     * @return true, if this object is equal, false otherwise.
     */
    public boolean equals(final Object o)
    {
      if (this == o)
      {
        return true;
      }
      if (o == null || getClass() != o.getClass())
      {
        return false;
      }

      final AttributeEntry that = (AttributeEntry) o;

      if (!name.equals(that.name))
      {
        return false;
      }
      if (namespace != null ? !namespace.equals(that.namespace) : that.namespace != null)
      {
        return false;
      }

      return true;
    }

    /**
     * Computes a hashcode for this attribute entry.
     *
     * @return the attribute entry's hashcode.
     */
    public int hashCode()
    {
      int result = (namespace != null ? namespace.hashCode() : 0);
      result = 29 * result + name.hashCode();
      return result;
    }

    protected void update(final String namespace, final String name, final String value)
    {
      if (name == null)
      {
        throw new NullPointerException("Name must not be null. ["
            + name + ", " + value + ']');
      }
      if (value == null)
      {
        throw new NullPointerException("Value must not be null. ["
            + name + ", " + value + ']');
      }
      this.namespace = namespace;
      this.name = name;
      this.value = value;
    }
  }

  /**
   * The storage for all entries of this list.
   */
  private LinkedMap entryList;
  private AttributeEntry lookupKey;
  private transient AttributeEntry[] arrayCache;

  /**
   * Creates an empty attribute list with no default values.
   */
  public AttributeList()
  {
    this.entryList = new LinkedMap();
    this.lookupKey = new AttributeEntry(null, "lookup", "value");
  }

  /**
   * Returns an iterator over the entry list. The iterator returns
   * AttributeList.AttributeEntry objects.
   *
   * @return the iterator over the entries contained in this list.
   * @deprecated use toArray instead.
   */
  public Iterator iterator()
  {
    return Arrays.asList(entryList.values()).iterator();
  }

  public AttributeEntry[] toArray()
  {
    if (arrayCache == null)
    {
      arrayCache = (AttributeEntry[]) entryList.values(new AttributeEntry[entryList.size()]);
    }
    return (AttributeEntry[]) arrayCache.clone();
  }

  /**
   * Defines an attribute.
   *
   * @param namespace the namespace of the attribute.
   * @param name      the name of the attribute to be defined
   * @param value     the value of the attribute.
   */
  public void setAttribute(final String namespace,
                           final String name,
                           final String value)
  {
    if (value == null)
    {
      removeAttribute(namespace, name);
      return;
    }

    final AttributeEntry entry = new AttributeEntry(namespace, name, value);
    this.arrayCache = null;
    this.entryList.put(entry, entry);
  }

  /**
   * Returns the attribute value for the given attribute name or null, if the
   * attribute is not defined in this list.
   *
   * @param namespace the namespace of the attribute.
   * @param name      the name of the attribute
   * @return the attribute value or null.
   */
  public String getAttribute(final String namespace,
                             final String name)
  {
    return getAttribute(namespace, name, null);
  }

  /**
   * Returns the attribute value for the given attribute name or the given
   * defaultvalue, if the attribute is not defined in this list.
   *
   * @param namespace    the namespace of the attribute.
   * @param name         the name of the attribute.
   * @param defaultValue the default value.
   * @return the attribute value or the defaultValue.
   */
  public String getAttribute(final String namespace,
                             final String name,
                             final String defaultValue)
  {
    lookupKey.update(namespace, name, "");
    final AttributeEntry entry = (AttributeEntry) this.entryList.get(lookupKey);
    if (entry != null)
    {
      return entry.getValue();
    }
    return defaultValue;
  }

  /**
   * Removes the attribute with the given name from the list.
   *
   * @param namespace the namespace of the attribute that should be removed.
   * @param name      the name of the attribute which should be removed..
   */
  public void removeAttribute(final String namespace,
                              final String name)
  {
    lookupKey.update(namespace, name, "");
    entryList.remove(lookupKey);
    this.arrayCache = null;
  }

  /**
   * Checks, whether this list is empty.
   *
   * @return true, if the list is empty, false otherwise.
   */
  public boolean isEmpty()
  {
    return this.entryList.isEmpty();
  }

  /**
   * Adds a namespace declaration. In XML, Namespaces are declared by
   * using a special attribute-syntax. As this syntax is confusing and
   * complicated, this method encapsulates this and make defining
   * namespaces less confusing.
   *
   * @param prefix       the desired namespace prefix (can be null or empty to
   *                     define the default namespace.
   * @param namespaceUri the URI of the namespace.
   */
  public void addNamespaceDeclaration(final String prefix,
                                      final String namespaceUri)
  {
    if (namespaceUri == null)
    {
      throw new NullPointerException();
    }

    if (prefix == null || "".equals(prefix))
    {
      setAttribute(AttributeList.XMLNS_NAMESPACE, "", namespaceUri);
    }
    else
    {
      setAttribute(AttributeList.XMLNS_NAMESPACE, prefix, namespaceUri);
    }
  }

  /**
   * Removes a namespace declaration from this attribute list.
   *
   * @param prefix the declared namespace prefix.
   */
  public void removeNamespaceDeclaration(final String prefix)
  {
    if (prefix == null || "".equals(prefix))
    {
      removeAttribute(AttributeList.XMLNS_NAMESPACE, "");
    }
    else
    {
      removeAttribute(AttributeList.XMLNS_NAMESPACE, prefix);
    }
  }

  /**
   * Checks, whether the given prefix is defined.
   *
   * @param prefix the namespace prefix.
   * @return true, if the prefix is defined, false otherwise.
   */
  public boolean isNamespacePrefixDefined(final String prefix)
  {
    return getAttribute(AttributeList.XMLNS_NAMESPACE, prefix) != null;
  }

  /**
   * Checks, whether the given namespace URI has a defined prefix.
   *
   * @param uri the uri.
   * @return true, if there is at least one namespace declaration matching
   *         the given URI, false otherwise.
   */
  public boolean isNamespaceUriDefined(final String uri)
  {
    if (this.entryList.isEmpty())
    {
      return false;
    }

    final AttributeEntry[] objects = toArray();
    for (int i = 0; i < objects.length; i++)
    {
      final AttributeEntry ae = objects[i];
      if (ObjectUtilities.equal(ae.getValue(), uri) &&
          AttributeList.XMLNS_NAMESPACE.equals(ae.getNamespace()))
      {
        return true;
      }
    }
    return false;
  }
}
