package com.limegroup.gnutella;

import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;

/**
 * This class defines the types of URNs supported in the application and 
 * provides utility functions for handling urn types.
 *
 * @see URN
 * @see UrnCache
 */
public class UrnType implements Serializable {

	private static final long serialVersionUID = -8211681448456483713L;

	/**
	 * Identifier string for the SHA1 type.
	 */
	public static final String SHA1_STRING = "sha1:";

    /**
     * Identifier string for the BITPRINT type.
     */
    public static final String BITPRINT_STRING = "bitprint:";

    /**
     * The <tt>UrnType</tt> for an invalid UrnType.
     */
    public static final UrnType INVALID = new UrnType("invalid");

	/**
	 * The <tt>UrnType</tt> for SHA1 hashes.
	 */
	public static final UrnType SHA1 = new UrnType(SHA1_STRING);
	
	/**
	 * The <tt>UrnType</tt> for bitprint hashes.
	 */
	public static final UrnType BITPRINT = new UrnType(BITPRINT_STRING);

	/**
	 * The <tt>UrnType</tt> for specifying any URN type.
	 */
	public static final UrnType ANY_TYPE = new UrnType("");

	/**
	 * Constant for specifying SHA1 URNs in replies.
	 */
	public static transient final Set SHA1_SET = new HashSet();		

	/**
	 * Constant for specifying any type of URN for replies.
	 */
	public static transient final Set ANY_TYPE_SET = new HashSet();	

	/**
	 * Statically add the SHA1 type to the set. 
	 */
	static {
		SHA1_SET.add(UrnType.SHA1);
		ANY_TYPE_SET.add(UrnType.ANY_TYPE);
	}

	/**
	 * Constant for the leading URN string identifier, as specified in
	 * RFC 2141.  This is equal to "urn:", although note that this
	 * should be used in a case-insensitive manner in compliance with
	 * the URN specification (RFC 2141).
	 */
	public static final String URN_NAMESPACE_ID = "urn:";

	/**
	 * Constant string for the URN type. INVARIANT: this cannot be null
	 */
	private transient String _urnType;


	/**
	 * Private constructor ensures that this class can never be constructed 
	 * from outside the class.  This assigns the _urnType string.
	 * 
	 * @param typeString the string representation of the URN type
	 * @throws <tt>NullPointerException</tt> if the <tt>typeString</tt>
	 *  argument is <tt>null</tt>
	 */
	private UrnType(String typeString) {
		if(typeString == null) {
			throw new NullPointerException("UrnTypes cannot except null strings");
		}
		_urnType = typeString;
	}

	/**
	 * Returns whether or not this URN type is SHA1.  
	 *
	 * @return <tt>true</tt> if this is a SHA1 URN type, <tt>false</tt> 
	 *  otherwise
	 */
	public boolean isSHA1() {
		return _urnType.equals(SHA1_STRING);
	}

	/**
	 * Returns the string representation of this URN type.
	 *
	 * @return the string representation of this URN type
	 */
	public String toString() {
		return URN_NAMESPACE_ID+_urnType;
	}

	/**
	 * It is necessary for this class to override equals because the 
	 * readResolve method was not added to the serialization API until 
	 * Java 1.2, which means that we cannot use it to ensure that the
	 * <tt>UrnType</tt> enum constants are actually the same instances upon
	 * deserialization.  Therefore, we must rely on Object.equals instead
	 * of upon "==".  
	 *
	 * @param o the <tt>Object</tt> to compare for equality
	 * @return <tt>true</tt> if these represent the same UrnType, <tt>false</tt>
	 *  otherwise
	 * @see java.lang.Object#equals(Object)
	 */
	public boolean equals(Object o) {
		if(o == this) return true;
		if(!(o instanceof UrnType)) return false;
		UrnType type = (UrnType)o;
		return _urnType.equals(type._urnType);
	}

	/**
	 * Overridden to meet the contract of Object.hashCode.
	 *
	 * @return the unique hashcode for this <tt>UrnType</tt>, in accordance with
	 *  Object.equals
	 * @see java.lang.Object#hashCode
	 */
	public int hashCode() {
		int result = 17;
		result = 37*result + _urnType.hashCode();
		return result;
	}

	/**
	 * Serializes this instance.
	 *
	 * @serialData the string representation of the URN type
	 */
	private void writeObject(ObjectOutputStream s) 
		throws IOException {
		s.defaultWriteObject();
		s.writeObject(_urnType);
	}

	/**
	 * Deserializes this <tt>UrnType</tt> instance, validating the input string.
	 */
	private void readObject(ObjectInputStream s) 
		throws IOException, ClassNotFoundException {
		s.defaultReadObject();
		_urnType = (String)s.readObject();
		if(!_urnType.equals("") &&
		   !_urnType.equals(SHA1_STRING) &&
		   !_urnType.equals(BITPRINT_STRING)) {
			throw new InvalidObjectException("invalid urn type: "+_urnType);
		}
	}

	/**
	 * Factory method for obtaining <tt>UrnType</tt> instances from strings.
	 * If the isSupportedUrnType method returns <tt>true</tt> this is
	 * guaranteed to return a non-null UrnType.
	 *
	 * @param type the string representation of the urn type
	 * @return the <tt>UrnType</tt> instance corresponding with the specified
	 *  string, or <tt>null</tt> if the type is not supported
	 */
	public static UrnType createUrnType(String type) {
		String lowerCaseType = type.toLowerCase().trim();
		if(lowerCaseType.equals(SHA1.toString())) { 
			return SHA1;
		} else if(lowerCaseType.equals(ANY_TYPE.toString())) {
			return ANY_TYPE;
		} else if(lowerCaseType.equals(BITPRINT.toString())) {
		    return BITPRINT;
        } else {
			return null;
		}
	}

	/**
	 * Returns whether or not the string argument is a urn type that
	 * we know about.
	 *
	 * @param urnString to string to check 
	 * @return <tt>true</tt> if it is a valid URN type, <tt>false</tt>
	 *  otherwise
	 */
	public static boolean isSupportedUrnType(final String urnString) {
		UrnType type = UrnType.createUrnType(urnString);
		if(type == null) return false;
		return true;
	}
}
