package hirondelle.web4j.config;

import java.util.*;
import java.util.logging.*;

import hirondelle.web4j.database.DAOException; 
import hirondelle.web4j.ui.translate.Translator;
import hirondelle.web4j.util.Util;
import hirondelle.web4j.ui.translate.Translation;
import hirondelle.web4j.ui.translate.Translation.LookupResult;

import hirondelle.fish.translate.translation.TranslationDAO;
import hirondelle.fish.translate.unknown.UnknownBaseTextDAO;

/**
 Implementation of {@link Translator}, required by WEB4J.
 
 <P>This implementation uses a database to store translations. As part of 
 {@link hirondelle.web4j.StartupTasks}, the translations are {@link #read()}, and cached in 
 an in-memory structure, which is used to perform the actual runtime look up of translations.

 <P><span class="highlight">This class can also help to find items that need translation, 
 simply by exercising the application.</span> The steps are as follows :
 <ul>
 <li>upon startup, this class starts to "record" in memory all unknown base text items 
 (see {@link Translator} for a definition of 'base text').
 <li>the application is exercised over the desired pages. An effort is made to 
 generate all possible error messages (it is useful to perform all these tasks during regular unit testing).
 <li>{@link #stopRecordingUnknowns} is then called. This class will stop recording unknowns,
 and flush all existing unknowns to the database.
 <li>the screens provided in the <tt>translate</tt> module are used to evaluate each item : add it as 
 a natural language key, add it as a coder key, or delete it from further consideration. 
 <li>{@link #read} is called to refresh the in-memory translations. 
 <li>one may then call call {@link #startRecordingUnknowns} to repeat the process. 
 </ul>
 
 <P>The above can be performed both during application development and during production, 
 to find items that may have been missed. Screens are provided for all of the above tasks.
 
 <P><span class="highlight">The start/stop action for recording unknowns is global, and applies to all users.</span> 
 If more than one person is simultaneously working on finding and evaluating unknown base text, 
 then they should coordinate their efforts, to know when recording is on/off. 
*/
public final class TranslatorImpl implements Translator {  

  /** 
   Look up all translations from a database, and store them in a static member.
   
   <P>Must be called by {@link hirondelle.web4j.StartupTasks}. May also be called 
   after startup, to refresh the data.
  */
  public static synchronized void  read() throws DAOException {
    TranslationDAO dao = new TranslationDAO();
    fTranslations = dao.getTranslations();
    fLogger.finest("Fetched Translations : " + Util.logOnePerLine(fTranslations));
  }

  /** Return the number of translations.  */
  public static synchronized Integer getNumTranslations(){
    return fTranslations.size();
  }
  
  /**
   Start recording unknown base text.
   
   <P>Recording can start only if : 
   <ul>
   <li>this class is not already recording.
   <li>there are no entries in the database for unknown base text. That is, starting to record can only be done after all
   unknown base text items already persisted to the database have been evaluated and processed, thus removing them 
   from the unknowns 'queue'. 
   </ul>
   
   <P>Items are recorded to an in-memory structure only. They are saved to the database by 
   calling {@link #stopRecordingUnknowns()}. 
  */
  public static synchronized void startRecordingUnknowns() throws DAOException {
    if( fIsRecording ){
      throw new IllegalArgumentException("Recording of Unknowns has already started.");
    }
    
    if( numPersistedUnknownEntries() == 0) {
      fLogger.fine("Starting to record Unknown Base Text in memory, for later persistence.");
      fIsRecording = true;
      fUnknownBaseText.clear();
    }
    else {
      fLogger.fine("Cannot start recording. Must have 0 entries in Unknown Base Text table before recording can start.");
      fIsRecording = false;
    }
  }
  
  /**
   Stop recording of unknown base text, and persist unknown items to the database.
   
   <P>This method fails if this class is not already recording.
  */
  public static synchronized void stopRecordingUnknowns() throws DAOException {
    if( ! fIsRecording ){
      throw new IllegalArgumentException("Recording of Unknowns has not yet started.");
    }
    fLogger.fine("Stop recording of Unknown Base Text. Save to database, and clear in-memory cache of recorded items.");
    storeUnknownsInTable();
    fUnknownBaseText.clear();
    fIsRecording = false;
  }

  /** Return <tt>true</tt> only if this class is currently recording unknown base text. */
  public static synchronized boolean isRecording(){
    return fIsRecording;
  }
   
  /**
   Look up the translation for <tt>aBaseText</tt> and <tt>aLocale</tt>. 
    
   <P>If <tt>aBaseText</tt> is not known, or if there is no explicit translation for that 
   {@link Locale}, then return <tt>aBaseText</tt> as is.
   
   <P>If <tt>aBaseText</tt> is not known, and this class is 'recording', then it is added to the 
   unknown items.
  */
  public String get(String aBaseText, Locale aLocale) {
    String result = null;
    LookupResult lookup = Translation.lookUp(aBaseText, aLocale, fTranslations);
    if( lookup.hasSucceeded() ){ 
      result = lookup.getText();
    }
    else {
      result = aBaseText;
      if(LookupResult.UNKNOWN_BASE_TEXT == lookup){
        addUnknown(aBaseText);
      }
      else if (LookupResult.UNKNOWN_LOCALE == lookup){
        //do nothing in this implementation; since the base text exists, any "missing" translations
        //for a given locale can always be retrieved by an ordinary query
      }
    }
    fLogger.finest("Translation of " + Util.quote(aBaseText) + " for Locale " + aLocale + ": " + Util.quote(result));
    return result;
  }
  
  // PRIVATE //
  
  /**
   Holds all translations in memory, as a  
   Map[BaseText, Map[Locale, Translation]].
   
   (Empty object provided here; without an object, you will have an unnecessary NullPointerException 
   if the database is down upon startup.) 
  */
  private static Map<String, Map<String, String>> fTranslations = new HashMap<String, Map<String, String>>();
  
  /**  On/off indicator for recording.  */
  private static boolean fIsRecording;
  
  /** In-memory store of base text items that are currently unknown to fTranslations.  */
  private static final Set<String> fUnknownBaseText = new HashSet<String>();
  
  private static final Logger fLogger = Util.getLogger(TranslatorImpl.class);
  
  private static synchronized void addUnknown(String aBaseText){
    if (fIsRecording){
      fUnknownBaseText.add(aBaseText);
    }
  }
  
  private static synchronized void storeUnknownsInTable() throws DAOException {
    UnknownBaseTextDAO dao = new UnknownBaseTextDAO();
    dao.addAll(fUnknownBaseText);
  }
  
  private static synchronized int numPersistedUnknownEntries() throws DAOException {
    UnknownBaseTextDAO dao = new UnknownBaseTextDAO();
    return dao.count().intValue();
  }
}