/*
 * Copyright (c) 2000, 2017, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code 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 General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package javax.print.attribute;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.HashMap;

/**
 * Class {@code HashAttributeSet} provides an {@code AttributeSet}
 * implementation with characteristics of a hash map.
 *
 * @author Alan Kaminsky
 */
public class HashAttributeSet implements AttributeSet, Serializable {

    /**
     * Use serialVersionUID from JDK 1.4 for interoperability.
     */
    private static final long serialVersionUID = 5311560590283707917L;

    /**
     * The interface of which all members of this attribute set must be an
     * instance. It is assumed to be interface {@link Attribute Attribute} or a
     * subinterface thereof.
     *
     * @serial
     */
    private Class<?> myInterface;

    /**
     * A {@code HashMap} used by the implementation. The serialised form doesn't
     * include this instance variable.
     */
    private transient HashMap<Class<?>, Attribute> attrMap = new HashMap<>();

    /**
     * Write the instance to a stream (ie serialize the object).
     *
     * @param  s the output stream
     * @throws IOException if an I/O exception has occurred
     * @serialData The serialized form of an attribute set explicitly writes the
     *             number of attributes in the set, and each of the attributes.
     *             This does not guarantee equality of serialized forms since
     *             the order in which the attributes are written is not defined.
     */
    private void writeObject(ObjectOutputStream s) throws IOException {

        s.defaultWriteObject();
        Attribute [] attrs = toArray();
        s.writeInt(attrs.length);
        for (int i = 0; i < attrs.length; i++) {
            s.writeObject(attrs[i]);
        }
    }

    /**
     * Reconstitute an instance from a stream that is, deserialize it).
     *
     * @param  s the input stream
     * @throws ClassNotFoundException if the class is not found
     * @throws IOException if an I/O exception has occurred
     */
    private void readObject(ObjectInputStream s)
        throws ClassNotFoundException, IOException {

        s.defaultReadObject();
        attrMap = new HashMap<>();
        int count = s.readInt();
        Attribute attr;
        for (int i = 0; i < count; i++) {
            attr = (Attribute)s.readObject();
            add(attr);
        }
    }

    /**
     * Construct a new, empty attribute set.
     */
    public HashAttributeSet() {
        this(Attribute.class);
    }

    /**
     * Construct a new attribute set, initially populated with the given
     * attribute.
     *
     * @param  attribute attribute value to add to the set
     * @throws NullPointerException if {@code attribute} is {@code null}
     */
    public HashAttributeSet(Attribute attribute) {
        this (attribute, Attribute.class);
    }

    /**
     * Construct a new attribute set, initially populated with the values from
     * the given array. The new attribute set is populated by adding the
     * elements of {@code attributes} array to the set in sequence, starting at
     * index 0. Thus, later array elements may replace earlier array elements if
     * the array contains duplicate attribute values or attribute categories.
     *
     * @param  attributes array of attribute values to add to the set. If
     *         {@code null}, an empty attribute set is constructed.
     * @throws NullPointerException if any element of {@code attributes} is
     *         {@code null}
     */
    public HashAttributeSet(Attribute[] attributes) {
        this (attributes, Attribute.class);
    }

    /**
     * Construct a new attribute set, initially populated with the values from
     * the given set.
     *
     * @param  attributes set of attributes from which to initialise this set.
     *         If {@code null}, an empty attribute set is constructed.
     */
    public HashAttributeSet(AttributeSet attributes) {
        this (attributes, Attribute.class);
    }

    /**
     * Construct a new, empty attribute set, where the members of the attribute
     * set are restricted to the given interface.
     *
     * @param  interfaceName the interface of which all members of this
     *         attribute set must be an instance. It is assumed to be interface
     *         {@link Attribute Attribute} or a subinterface thereof.
     * @throws NullPointerException if {@code interfaceName} is {@code null}
     */
    protected HashAttributeSet(Class<?> interfaceName) {
        if (interfaceName == null) {
            throw new NullPointerException("null interface");
        }
        myInterface = interfaceName;
    }

    /**
     * Construct a new attribute set, initially populated with the given
     * attribute, where the members of the attribute set are restricted to the
     * given interface.
     *
     * @param  attribute attribute value to add to the set
     * @param  interfaceName the interface of which all members of this
     *         attribute set must be an instance. It is assumed to be interface
     *         {@link Attribute Attribute} or a subinterface thereof.
     * @throws NullPointerException if {@code attribute} or
     *         {@code interfaceName} are {@code null}
     * @throws ClassCastException if {@code attribute} is not an instance of
     *         {@code interfaceName}
     */
    protected HashAttributeSet(Attribute attribute, Class<?> interfaceName) {
        if (interfaceName == null) {
            throw new NullPointerException("null interface");
        }
        myInterface = interfaceName;
        add (attribute);
    }

    /**
     * Construct a new attribute set, where the members of the attribute set are
     * restricted to the given interface. The new attribute set is populated by
     * adding the elements of {@code attributes} array to the set in sequence,
     * starting at index 0. Thus, later array elements may replace earlier array
     * elements if the array contains duplicate attribute values or attribute
     * categories.
     *
     * @param  attributes array of attribute values to add to the set. If
     *         {@code null}, an empty attribute set is constructed.
     * @param  interfaceName the interface of which all members of this
     *         attribute set must be an instance. It is assumed to be interface
     *         {@link Attribute Attribute} or a subinterface thereof.
     * @throws NullPointerException if {@code interfaceName} is {@code null}, or
     *         if any element of {@code attributes} is {@code null}
     * @throws ClassCastException if any element of {@code attributes} is not an
     *         instance of {@code interfaceName}
     */
    protected HashAttributeSet(Attribute[] attributes, Class<?> interfaceName) {
        if (interfaceName == null) {
            throw new NullPointerException("null interface");
        }
        myInterface = interfaceName;
        int n = attributes == null ? 0 : attributes.length;
        for (int i = 0; i < n; ++ i) {
            add (attributes[i]);
        }
    }

    /**
     * Construct a new attribute set, initially populated with the values from
     * the given set where the members of the attribute set are restricted to
     * the given interface.
     *
     * @param  attributes set of attribute values to initialise the set. If
     *         {@code null}, an empty attribute set is constructed.
     * @param  interfaceName The interface of which all members of this
     *         attribute set must be an instance. It is assumed to be interface
     *         {@link Attribute Attribute} or a subinterface thereof.
     * @throws ClassCastException if any element of {@code attributes} is not an
     *         instance of {@code interfaceName}
     */
    protected HashAttributeSet(AttributeSet attributes, Class<?> interfaceName) {
      myInterface = interfaceName;
      if (attributes != null) {
        Attribute[] attribArray = attributes.toArray();
        int n = attribArray == null ? 0 : attribArray.length;
        for (int i = 0; i < n; ++ i) {
          add (attribArray[i]);
        }
      }
    }

    /**
     * Returns the attribute value which this attribute set contains in the
     * given attribute category. Returns {@code null} if this attribute set does
     * not contain any attribute value in the given attribute category.
     *
     * @param  category attribute category whose associated attribute value is
     *         to be returned. It must be a {@link Class Class} that implements
     *         interface {@link Attribute Attribute}.
     * @return the attribute value in the given attribute category contained in
     *         this attribute set, or {@code null} if this attribute set does
     *         not contain any attribute value in the given attribute category
     * @throws NullPointerException if the {@code category} is {@code null}
     * @throws ClassCastException if the {@code category} is not a
     *         {@link Class Class} that implements interface
     *         {@link Attribute Attribute}
     */
    public Attribute get(Class<?> category) {
        return attrMap.get(AttributeSetUtilities.
                           verifyAttributeCategory(category,
                                                   Attribute.class));
    }

    /**
     * Adds the specified attribute to this attribute set if it is not already
     * present, first removing any existing in the same attribute category as
     * the specified attribute value.
     *
     * @param  attribute attribute value to be added to this attribute set
     * @return {@code true} if this attribute set changed as a result of the
     *         call, i.e., the given attribute value was not already a member of
     *         this attribute set
     * @throws NullPointerException if the {@code attribute} is {@code null}
     * @throws UnmodifiableSetException if this attribute set does not support
     *         the {@code add()} operation
     */
    public boolean add(Attribute attribute) {
        Object oldAttribute =
            attrMap.put(attribute.getCategory(),
                        AttributeSetUtilities.
                        verifyAttributeValue(attribute, myInterface));
        return (!attribute.equals(oldAttribute));
    }

    /**
     * Removes any attribute for this category from this attribute set if
     * present. If {@code category} is {@code null}, then {@code remove()} does
     * nothing and returns {@code false}.
     *
     * @param  category attribute category to be removed from this attribute set
     * @return {@code true} if this attribute set changed as a result of the
     *         call, i.e., the given attribute category had been a member of
     *         this attribute set
     * @throws UnmodifiableSetException if this attribute set does not support
     *         the {@code remove()} operation
     */
    public boolean remove(Class<?> category) {
        return
            category != null &&
            AttributeSetUtilities.
            verifyAttributeCategory(category, Attribute.class) != null &&
            attrMap.remove(category) != null;
    }

    /**
     * Removes the specified attribute from this attribute set if present. If
     * {@code attribute} is {@code null}, then {@code remove()} does nothing and
     * returns {@code false}.
     *
     * @param  attribute attribute value to be removed from this attribute set
     * @return {@code true} if this attribute set changed as a result of the
     *         call, i.e., the given attribute value had been a member of this
     *         attribute set
     * @throws UnmodifiableSetException if this attribute set does not support
     *         the {@code remove()} operation
     */
    public boolean remove(Attribute attribute) {
        return
            attribute != null &&
            attrMap.remove(attribute.getCategory()) != null;
    }

    /**
     * Returns {@code true} if this attribute set contains an attribute for the
     * specified category.
     *
     * @param  category whose presence in this attribute set is to be tested
     * @return {@code true} if this attribute set contains an attribute value
     *         for the specified category
     */
    public boolean containsKey(Class<?> category) {
        return
            category != null &&
            AttributeSetUtilities.
            verifyAttributeCategory(category, Attribute.class) != null &&
            attrMap.get(category) != null;
    }

    /**
     * Returns {@code true} if this attribute set contains the given attribute.
     *
     * @param  attribute value whose presence in this attribute set is to be
     *         tested
     * @return {@code true} if this attribute set contains the given attribute
     *         value
     */
    public boolean containsValue(Attribute attribute) {
        return
           attribute != null &&
           attribute instanceof Attribute &&
           attribute.equals(attrMap.get(attribute.getCategory()));
    }

    /**
     * Adds all of the elements in the specified set to this attribute. The
     * outcome is the same as if the {@link #add(Attribute) add(Attribute)}
     * operation had been applied to this attribute set successively with each
     * element from the specified set. The behavior of the
     * {@code addAll(AttributeSet)} operation is unspecified if the specified
     * set is modified while the operation is in progress.
     * <p>
     * If the {@code addAll(AttributeSet)} operation throws an exception, the
     * effect on this attribute set's state is implementation dependent;
     * elements from the specified set before the point of the exception may or
     * may not have been added to this attribute set.
     *
     * @param  attributes whose elements are to be added to this attribute set
     * @return {@code true} if this attribute set changed as a result of the
     *         call
     * @throws UnmodifiableSetException if this attribute set does not support
     *         the {@code addAll(AttributeSet)} method
     * @throws NullPointerException if some element in the specified set is
     *         {@code null}, or the set is {@code null}
     * @see #add(Attribute)
     */
    public boolean addAll(AttributeSet attributes) {

        Attribute []attrs = attributes.toArray();
        boolean result = false;
        for (int i=0; i<attrs.length; i++) {
            Attribute newValue =
                AttributeSetUtilities.verifyAttributeValue(attrs[i],
                                                           myInterface);
            Object oldValue = attrMap.put(newValue.getCategory(), newValue);
            result = (! newValue.equals(oldValue)) || result;
        }
        return result;
    }

    /**
     * Returns the number of attributes in this attribute set. If this attribute
     * set contains more than {@code Integer.MAX_VALUE} elements, returns
     * {@code Integer.MAX_VALUE}.
     *
     * @return the number of attributes in this attribute set
     */
    public int size() {
        return attrMap.size();
    }

    /**
     * Returns an array of the attributes contained in this set.
     *
     * @return the attributes contained in this set as an array, zero length if
     *         the {@code AttributeSet} is empty
     */
    public Attribute[] toArray() {
        Attribute []attrs = new Attribute[size()];
        attrMap.values().toArray(attrs);
        return attrs;
    }

    /**
     * Removes all attributes from this attribute set.
     *
     * @throws UnmodifiableSetException if this attribute set does not support
     *         the {@code clear()} operation
     */
    public void clear() {
        attrMap.clear();
    }

    /**
     * Returns {@code true} if this attribute set contains no attributes.
     *
     * @return {@code true} if this attribute set contains no attributes
     */
    public boolean isEmpty() {
        return attrMap.isEmpty();
    }

    /**
     * Compares the specified object with this attribute set for equality.
     * Returns {@code true} if the given object is also an attribute set and the
     * two attribute sets contain the same attribute category-attribute value
     * mappings. This ensures that the {@code equals()} method works properly
     * across different implementations of the {@code AttributeSet} interface.
     *
     * @param  object to be compared for equality with this attribute set
     * @return {@code true} if the specified object is equal to this attribute
     *         set
     */
    public boolean equals(Object object) {
        if (object == null || !(object instanceof AttributeSet)) {
            return false;
        }

        AttributeSet aset = (AttributeSet)object;
        if (aset.size() != size()) {
            return false;
        }

        Attribute[] attrs = toArray();
        for (int i=0;i<attrs.length; i++) {
            if (!aset.containsValue(attrs[i])) {
                return false;
            }
        }
        return true;
    }

    /**
     * Returns the hash code value for this attribute set. The hash code of an
     * attribute set is defined to be the sum of the hash codes of each entry in
     * the {@code AttributeSet}. This ensures that {@code t1.equals(t2)} implies
     * that {@code t1.hashCode()==t2.hashCode()} for any two attribute sets
     * {@code t1} and {@code t2}, as required by the general contract of
     * {@link Object#hashCode() Object.hashCode()}.
     *
     * @return the hash code value for this attribute set
     */
    public int hashCode() {
        int hcode = 0;
        Attribute[] attrs = toArray();
        for (int i=0;i<attrs.length; i++) {
            hcode += attrs[i].hashCode();
        }
        return hcode;
    }
}
