package hirondelle.web4j.database;

import java.util.regex.Pattern;
import hirondelle.web4j.model.Check;
import hirondelle.web4j.model.ModelCtorException;
import hirondelle.web4j.model.ModelUtil;
import hirondelle.web4j.util.EscapeChars;
import hirondelle.web4j.util.Util;
import hirondelle.web4j.util.Consts;
import hirondelle.web4j.util.Regex;

/**
<span class="highlight">Identifier of an SQL statement block in an <tt>.sql</tt> file.</span>
(Such identifiers must be unique.)
 
 <P>This class does <em>not</em> contain the text of the underlying SQL statement.
 Rather, this class allows a code friendly way of <em>referencing</em> SQL statements. 
 Since <tt>.sql</tt> files are simple text files, there is a need to build a bridge between these text files
 and java code. This class is that bridge. 
 
 <P> Please see the package summary for important information regarding <tt>.sql</tt> files.
  
 <P>Typical use case :
<PRE>public static final SqlId MEMBER_FETCH = new SqlId("MEMBER_FETCH");</PRE>
This corresponds to an entry in an <tt>.sql</tt> file :
<PRE>
MEMBER_FETCH {
  SELECT Id, Name, IsActive, DispositionFK 
  FROM Member WHERE Id=?
}
</PRE>

 <P>This class is unusual, since there is only one way to use these objects. 
 That is, they <span class="highlight">must be declared  
 as <tt>public static final</tt> fields in a <tt>public</tt> class.</span> 
 They should never appear <i>only</i> as local objects in the body of a method. (This unusual restriction 
 exists to allow the framework to find and examine such fields using reflection.)
 The text passed to the constructor must correspond to the identifier of some SQL 
 statement block in an <tt>.sql</tt> file. Such identifiers must match a specific 
 {@link #FORMAT}.
 
 <P><a name="StartupChecks"></a><b>Startup Checks</b><br>
 To discover simple typographical errors as quickly as possible, 
 the framework will run diagnostics upon startup : <span class="highlight">there must be an exact, one-to-one 
 correspondence between the SQL statement identifiers defined in the <tt>.sql</tt> file(s), 
 and the <tt>public static final SqlId</tt> fields declared by the 
 application.</span> Any mismatch will result in an error. (Running such diagnostics 
 upon startup is highly advantageous, since the only alternative is discovery during 
 actual use, upon the first execution of a particular operation.) 
 
 <P><a name="DeclarationLocation"></a><b>Where To Declare <tt>SqlId</tt> Fields</b><br>
 Where should <tt>SqlId</tt> fields be declared? The only real restriction is that 
 they must be declared in a <tt>public</tt> class. With the most recommended first, one may declare 
 <tt>SqlId</tt> fields in :
 <ul>
 <li>a <tt>public</tt> {@link hirondelle.web4j.action.Action}
 <li>a <tt>public</tt> Data Access Object
 <li>a <tt>public</tt> constants class, one per package/feature. 
 <li>a <tt>public</tt> constants class, one per application. If more than one developer at a time 
 works on the application, then this style will result in a lot of developer contention. It is not recommended. 
 </ul>
 
 <P><em>Design Note</em>
 <br>The justification for recommending that <tt>SqlId</tt> fields appear in a 
 {@link hirondelle.web4j.action.Action} is as follows : 
 <ul>
 <li>it is highly satisfying to have mostly <tt>package-private</tt> classes in an application, since it 
 takes advantage of a principal technique for "information hiding" - one of the guiding principles of 
 lasting value in object programming. For instance, it is usually possible to have a Data Access Object 
 (DAO) as package-private. If a <tt>SqlId</tt> is declared in a DAO, however, then that DAO must be 
 changed to <tt>public</tt>, just to render the <tt>SqlId</tt> fields accessible by reflection, 
 which is distasteful.
 <li>the {@link hirondelle.web4j.action.Action} is always <tt>public</tt> anyway, so adding a <tt>SqlId</tt> will 
 not change its scope.
 <li>{@link hirondelle.web4j.action.Action} is intended as the <tt>public</tt> face of each feature. Therefore, 
 all important items related to the feature should be documented there - what it does, when is it called, and 
 how it shows a response. One can argue with some force that the single most important thing about a 
 feature is <em>"What does it do?"</em>. In a typical database application, the answer to that 
 question is usually <em>"these SQL operations"</em>.  
 </ul>
*/
public final class SqlId {

  /**
   Format of SQL statement identifiers.
    
   <P>Matching examples include : 
  <ul>
   <li><tt>ADD_MESSAGE</tt>
   <li><tt>fetch_member</tt>
   <li><tt>LIST_RESTAURANTS_2</tt>
  </ul>
  
   <P>One or more letters/underscores, with possible trailing digits. 
   <P>To scope an SQL statement to a particular database, simply prefix the identifier with a second 
   such identifier to represent the database, separated by a period, 
   as in <tt>'TRANSLATION_DB.ADD_BASE_TEXT'</tt>. 
  */
  public static final String FORMAT = Regex.SIMPLE_IDENTIFIER;
   
  /**
   Constructor for statement against the <em>default</em> database. 
    
   @param aStatementName identifier of an SQL statement, satisfies  {@link #FORMAT}, 
   and matches the name attached to an SQL statement appearing in an <tt>.sql</tt> file.
  */
  public SqlId(String aStatementName) { 
    fStatementName = aStatementName;
    fDatabaseName = null;
    validateState();
  }
  
  /**
   Constructor for statement against a <em>named</em> database.
   
   @param aDatabaseName identifier for the target database, 
   satisfies {@link #FORMAT}, 
   matches one of the return values of {@link ConnectionSource#getDatabaseNames()},
   and also matches the prefix for a <tt>aStatementName</tt>. See package overview for more information. 
   @param aStatementName identifier of an SQL statement, satisfies  {@link #FORMAT}, 
   and matches the name attached to an SQL statement appearing in an <tt>.sql</tt> file.
  */
  public SqlId(String aDatabaseName, String aStatementName) {
    fStatementName = aStatementName;
    fDatabaseName = aDatabaseName;
    validateState();
  }
  
  /**
   Factory method for building an <tt>SqlId</tt> from a <tt>String</tt> which may or may 
   not be qualified by the database name. 
   
   @param aSqlId which may or may not be qualified by the database name.
  */
  public static SqlId fromStringId(String aSqlId){
    SqlId result = null;
    String SEPARATOR = ".";
    if( aSqlId.contains(SEPARATOR) ){
      String[] parts = aSqlId.split(EscapeChars.forRegex(SEPARATOR));
      String database = parts[0];
      String statement = parts[1];
      result = new SqlId(database, statement);
    }
    else {
      result = new SqlId(aSqlId);
    }
    return result;
  }
  
  /**
   Return <tt>aDatabaseName</tt> passed to the constructor. 
   
   <P>If no database name was passed to the constructor, then return an empty {@link String} 
   (corresponds to the 'default' database).
   
  */
  public String getDatabaseName(){
    return Util.textHasContent(fDatabaseName) ? fDatabaseName : Consts.EMPTY_STRING;
  }
  
  /** Return <tt>aStatementName</tt> passed to the constructor.  */
  public String getStatementName(){
    return fStatementName;
  }
  
  /**
   Return the SQL statement identifier as it appears in the <tt>.sql</tt> file.
   
   <P>Example return values :
  <ul>
   <li><tt>MEMBER_FETCH</tt> (against the default database)
   <li><tt>TRANSLATION.FETCH_ALL_TRANSLATIONS</tt> (against a database named <tt>TRANSLATION</tt>)
  </ul> 
  */
  @Override public String toString() { 
    return Util.textHasContent(fDatabaseName) ? fDatabaseName + "." + fStatementName : fStatementName;  
  } 
  
  @Override public boolean equals(Object aThat){
    Boolean result = ModelUtil.quickEquals(this, aThat);
    if( result == null ) {
      SqlId that = (SqlId) aThat;
      result = ModelUtil.equalsFor(this.getSignificantFields(), that.getSignificantFields());
    }
    return result;
  }
 
  @Override public int hashCode(){
    if(fHashCode == 0){
      fHashCode =  ModelUtil.hashCodeFor(getSignificantFields()); 
    }
    return fHashCode;
  }
   
  // PRIVATE //
  private final String fStatementName;
  private final String fDatabaseName;
  private int fHashCode;

  /**
   Does NOT throw ModelCtorException, since errors here represent bugs. 
  */
  private void validateState(){
    ModelCtorException ex = new ModelCtorException();
    Pattern simpleId = Pattern.compile(FORMAT);
    if ( ! Check.required(fStatementName, Check.pattern(simpleId)) ) {
      ex.add("Statement Name is required, and must match SqlId.FORMAT.");
    }
    if ( ! Check.optional(fDatabaseName, Check.pattern(simpleId)) ) {
      ex.add("Database Name is optional, and must match SqlId.FORMAT.");
    }
    if ( ! ex.isEmpty() ) {
      throw new IllegalArgumentException(Util.logOnePerLine(ex.getMessages()));
    }
  }
  
  private Object[] getSignificantFields(){
    return new Object[]{fStatementName, fDatabaseName};
  }
}
