package hirondelle.web4j.readconfig;

import static hirondelle.web4j.util.Consts.NEW_LINE;
import hirondelle.web4j.util.Regex;
import hirondelle.web4j.util.Util;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.StringReader;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 (UNPUBLISHED) Helper for {@link ConfigReader}, for reading {@link ConfigReader.FileType#TEXT_BLOCK} 
 files.

 <P>See {@link ConfigReader.FileType#TEXT_BLOCK} for specification of the format of such files.
*/
final class TextBlockReader {
 
  /**
   @param aInput has an underlying <tt>TextBlock</tt> file as source
   @param aConfigFileName the underlying source file name
  */
  TextBlockReader(InputStream aInput, String aConfigFileName) {
    fReader = new LineNumberReader(new InputStreamReader(aInput));
    fConfigFileName = aConfigFileName;
    fConstants = new Properties();
  }
  
  TextBlockReader(String aRawSqlFile) {
    fReader = new LineNumberReader(new StringReader(aRawSqlFile));
    fConfigFileName = "<file name unspecified>";
    fConstants = new Properties();
  }
  
  /**
   Parse the underlying <tt>TEXT_BLOCK</tt> file into a {@link Properties} object, which 
   uses key-value pairs of <tt>String</tt>s.
  
   <P>Using this example entry in a <tt>*.sql</tt> file : 
   <PRE>
   FETCH_NUM_MSGS_FOR_USER {
     SELECT COUNT(Id) FROM MyMessage WHERE LoginName=?
   }
  </PRE> 
   the key is <P><tt>FETCH_NUM_MSGS_FOR_USER</tt> <P> while the value is 
   <P><tt>SELECT COUNT(Id) FROM MyMessage WHERE LoginName=?</tt>.
  */
  Properties read() throws IOException {
    fLogger.fine("Reading text block file : " + Util.quote(fConfigFileName));
    final Properties result = new Properties();
    String line = null;
    while (( line = fReader.readLine()) != null){
      if ( isIgnorable(line) ) {
        continue;
      }
      if ( ! isInBlock() ){
        startBlock(line);
      }
      else {
        if ( ! isEndOfBlock(line) ){
          addLine(line);
        }
        else {
          endBlock(result);
        }
      }
    }
    return result;
  }
  
  // PRIVATE 
  private final LineNumberReader fReader;
  private final String fConfigFileName;
  private StringBuilder fBlockBody;
  
  /*
   These two distinguish between regular blocks and 'constants' blocks. 
   sql statements, and those for constants.
  */
  private boolean fIsReadingBlockBody;
  private boolean fIsReadingConstants;
  
  /**
   For regular Blocks, refers to the Block Name.
   For constants, refers not to the name of the containing block (which is always 
   the same), but to the identifier for the constant itself; this is line-level, 
   not block-level.
  */
  private String fKey;
  
  /**
   Remembers where the current block started. Used for debugging messages.
  */
  private int fBlockStartLineNumber;
  
  /**
   Stores the constants, which are always intended for substitutions appearing 
   later in the file. These items are not added to the final Properties object 
   returned by this class; rather, they assist in performing substitutions of 
   simple constants. 
  
   <P>There can be multiple Constants blocks in a TEXT_BLOCK file. This item 
   collects them all into one data structure.
  */
  private final Properties fConstants;
  
  private static final String fCOMMENT = "--";
  private static final String fEND_BLOCK = "}";
  private static final String fSTART_BLOCK = "{";
  private static final Pattern fKEY_NAME_PATTERN = Pattern.compile(Regex.SIMPLE_SCOPED_IDENTIFIER);
  private static final Pattern fSUBSTITUTION_PATTERN = Pattern.compile(
    "(?:\\$\\{)" + Regex.SIMPLE_IDENTIFIER +  "(?:\\})"
  );
  private static final String fCONSTANTS_BLOCK = "constants"; //ignore case
  private static final String fCONSTANTS_SEPARATOR = "="; 
  private static final int fNOT_FOUND = -1;
  
  private static final Logger fLogger = Util.getLogger(TextBlockReader.class);  
  
  private boolean isIgnorable(String aLine){
    boolean result;
    if ( isInBlock() ) {
      //no empty lines within blocks allowed
      result = isComment(aLine);
    }
    else {
      result = ! Util.textHasContent(aLine) || isComment(aLine);
    }
    return result;
  }

  private boolean isInBlock(){
    return fIsReadingBlockBody || fIsReadingConstants;
  }
  
  private boolean isComment(String aLine){
    return aLine.trim().startsWith(fCOMMENT);
  }
  
  private void startBlock(String aLine){
    fBlockStartLineNumber = fReader.getLineNumber();
    fKey = getBlockName(aLine);
    if ( fCONSTANTS_BLOCK.equalsIgnoreCase(fKey) ) {
      fIsReadingConstants = true;
      fIsReadingBlockBody = false;
    }
    else {
      fBlockBody = new StringBuilder();
      fIsReadingBlockBody = true;
      fIsReadingConstants = false;
    }
  }
  
  private String getBlockName(String aLine){
    int indexOfBrace = aLine.indexOf(fSTART_BLOCK);
    if ( indexOfBrace == -1 ){
      throw new IllegalArgumentException(
        reportLineNumber() + 
        "Expecting to find line defining a block, containing a trailing " + 
        Util.quote(fSTART_BLOCK) + ". Found this line instead : " + Util.quote(aLine)
      );
    }
    String candidateKey = aLine.substring(0, indexOfBrace).trim();
    return verifiedKeyName(candidateKey);
  }
  
  private String verifiedKeyName(String aCandidateKey){
    if (Util.matches(fKEY_NAME_PATTERN, aCandidateKey)) {
      return aCandidateKey;
    }
    String message = reportLineNumber() + "The name " + 
      Util.quote(aCandidateKey) + " is not in the expected syntax. " + 
      "It does not match the regular expression " + fKEY_NAME_PATTERN.pattern()
    ;
    throw new IllegalArgumentException(message);
  }
  
  private boolean isEndOfBlock(String aLine){
    return fEND_BLOCK.equals(aLine.trim());
  }
  
  private void addLine(String aLine){
    if ( fIsReadingBlockBody) {
      addToBlockBody(aLine);
    }
    else if ( fIsReadingConstants ){
      addConstant(aLine);
    }
  }
  
  private void addToBlockBody(String aLine){
    fBlockBody.append(aLine);
    fBlockBody.append(NEW_LINE);
  }
  
  private void addConstant(String aLine){
    int idxFirstSeparator = aLine.indexOf(fCONSTANTS_SEPARATOR);
    if ( idxFirstSeparator == fNOT_FOUND ) {
      String message = 
        reportLineNumber() + "Cannot find expected constants separator character : " + 
        Util.quote(fCONSTANTS_SEPARATOR)
      ;
      throw new IllegalAccessError(message);
    }
    String key = verifiedKeyName(aLine.substring(0, idxFirstSeparator).trim());
    String value = aLine.substring(idxFirstSeparator + 1).trim();
    addToResult(key, value, fConstants);
  }
  
  /**
   Allow a duplicate key, but log as SEVERE.
  */
  private void addToResult(String aKey, String aValue, Map aResult){
    if ( aResult.containsKey(aKey) ) {
      fLogger.severe(
        "DUPLICATE Value found for this Block Name or constant name " + Util.quote(aKey) + 
        ". This almost always indicates an error."
      );
    }
    aResult.put(aKey, aValue);
  }
  
  private void endBlock(Properties aResult){
    if (fIsReadingBlockBody){
      String finalBlockBody = resolveAnySubstitutions(fBlockBody.toString().trim(), aResult);
      addToResult(fKey, finalBlockBody, aResult);
    }
    fIsReadingBlockBody = false;
    fIsReadingConstants = false;
    fBlockStartLineNumber = 0;
  }
  
  private String resolveAnySubstitutions(String aRawBlockBody, Properties aResult){
    StringBuffer result = new StringBuffer();
    Matcher matcher = fSUBSTITUTION_PATTERN.matcher(aRawBlockBody);
    while ( matcher.find() ){
      matcher.appendReplacement(result, getReplacement(matcher, aResult));
    }
    matcher.appendTail(result);
    return result.toString();
  }
  
  private String getReplacement(Matcher aMatcher, Properties aProperties){
    String replacementKey = aMatcher.group(1);
    //the replacement may be another Block Body, or it may be a constant
    String replacement = aProperties.getProperty(replacementKey);
    if ( ! Util.textHasContent(replacement) ) {
      replacement = fConstants.getProperty(replacementKey);
    }
    if ( ! Util.textHasContent(replacement) ) {
      throw new IllegalArgumentException(
        reportBlockStartLineNumber() + 
        "The substitution variable ${" + replacementKey + 
        "} is not defined. Substitution variables must be defined before they " + 
        "are referenced. Please correct the ordering problem."
      );
    }
    fLogger.finest(
      "Replacement for " + Util.quote(replacementKey) + " is " + Util.quote(replacement)
    );
    return replacement;
  }
  
  private String reportLineNumber(){
    return "[" + fConfigFileName + ":" + Integer.toString(fReader.getLineNumber()) + "] ";
  }
  
  private String reportBlockStartLineNumber(){
    return "[" + fConfigFileName + ":" + Integer.toString(fBlockStartLineNumber) + "] ";
  }
}
