package hirondelle.web4j.model;

import java.io.Serializable;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.IOException;
import hirondelle.web4j.model.ModelUtil;
import hirondelle.web4j.util.Consts;
import hirondelle.web4j.util.Util;
import hirondelle.web4j.security.SafeText;

/**
 Building block class for identifiers. 
 
 <P>Identifiers are both common and important. Unfortunately, there is no class in the 
 JDK specifically for identifiers. 
 
 <P>An <tt>Id</tt> class is useful for these reasons :
<ul>
 <li>it allows model classes to read at a higher level of abstraction. Identifiers are 
 labeled as such, and stand out very clearly from other items
 <li>it avoids a common <a href="http://www.javapractices.com/Topic192.cjp">problem</a> 
 in modeling identifiers as numbers.
</ul>

 <P>The underlying database column may be modeled as either text or as a number.
 If the underlying column is of a numeric type, however, then a Data Access Object
 will need to pass <tt>Id</tt> parameters to {@link hirondelle.web4j.database.Db} 
 using {@link #asInteger} or {@link #asLong}.

 <P><em>Design Note :</em><br>
 This class is <tt>final</tt>, immutable, {@link Serializable}, 
 and {@link Comparable}, in imitation of the other building block classes 
 such as {@link String}, {@link Integer}, and so on.
*/
public final class Id implements Serializable, Comparable<Id> {

  /**
   Construct an identifier using an arbitrary {@link String}.
    
   This class uses a {@link SafeText} object internally.
   @param aText is non-null, and contains characters that are allowed 
   by {@link hirondelle.web4j.security.PermittedCharacters}.
  */
  public Id(String aText) {
    fId = new SafeText(aText);
    validateState();
  }
  
  /**
   Factory method.
   
   Simply a slightly more compact way of building an object, as opposed to 'new'.
  */
  public static Id from(String aText){
    return new Id(aText);
  }

  /**
   Return this id as an {@link Integer}, if possible.
   
   <P>See class comment.
   
   <P>If this <tt>Id</tt> is not convertible to an {@link Integer}, then a {@link RuntimeException} is 
   thrown.
  */
  public Integer asInteger(){
    return new Integer(fId.getRawString());
  }

  /**
   Return this id as a {@link Long}, if possible.
   
   <P>See class comment.
   
   <P>If this <tt>Id</tt> is not convertible to a {@link Long}, 
   then a {@link RuntimeException} is thrown.
  */
  public Long asLong(){
    return new Long(fId.getRawString());
  }
  
  /**
   Return the id, with special characters escaped. 
   
   <P>The return value either has content (with no leading or trailing spaces), 
   or is empty.
   See {@link hirondelle.web4j.util.EscapeChars#forHTML(String)} for a list of escaped
   characters.
  */
  @Override public String toString(){
    return fId.toString();
  }
  
  /** Return the text passed to the constructor. */
  public String getRawString(){
    return fId.getRawString();
  }
  
  /** Return the text with special  XML characters esacped. See {@link SafeText#getXmlSafe()}.  */
  public String getXmlSafe() {
    return fId.getXmlSafe();
  }
  
  @Override public boolean equals(Object aThat){
    Boolean result = ModelUtil.quickEquals(this, aThat);
    if ( result == null ){
      Id that = (Id) aThat;
      result = ModelUtil.equalsFor(this.getSignificantFields(), that.getSignificantFields());
    }
    return result;    
  }

  @Override public int hashCode(){
    return ModelUtil.hashCodeFor(getSignificantFields());
  }
  
  public int compareTo(Id aThat) {
    final int EQUAL = 0;
    if ( this == aThat ) return EQUAL;
    int comparison = this.fId.compareTo(aThat.fId);
    if ( comparison != EQUAL ) return comparison;
    return EQUAL;
  }
  
  // PRIVATE 
  
  /** @serial  */
  private SafeText fId;
  
  /**
   For evolution of this class, see Sun guidelines : 
   http://java.sun.com/j2se/1.5.0/docs/guide/serialization/spec/version.html#6678 
  */
  private static final long serialVersionUID = 7526472295633676147L;
  
  /**
   Always treat de-serialization as a full-blown constructor, by
   validating the final state of the de-serialized object.
  */
  private void readObject(ObjectInputStream aInputStream) throws ClassNotFoundException, IOException {
     //always perform the default de-serialization first
     aInputStream.defaultReadObject();
     //make defensive copy of mutable fields (none here)
     //ensure that object state has not been corrupted or tampered with maliciously
     validateState();
  }

  /**
   This is the default implementation of writeObject.
   Customise if necessary.
  */
  private void writeObject(ObjectOutputStream aOutputStream) throws IOException {
    //perform the default serialization for all non-transient, non-static fields
    aOutputStream.defaultWriteObject();
  }
  
  private void validateState() {
    if( ! Util.textHasContent(fId) ) {
      if ( ! Consts.EMPTY_STRING.equals(fId.getRawString()) ) {
        throw new IllegalArgumentException(
          "Id must have content, or be the empty String. Erroneous Value : " + Util.quote(fId)
        );
      }
    }
  }
  
  private Object[] getSignificantFields(){
    return new Object[] {fId};
  }
}
