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();
  }
}