content: /**
   * Copyright 2008 DFKI GmbH.
   * All Rights Reserved.  Use is subject to license terms.
   * <p/>
   * This file is part of MARY TTS.
   * <p/>
   * MARY TTS is free software: you can redistribute it and/or modify
   * it under the terms of the GNU Lesser General Public License as published by
   * the Free Software Foundation, version 3 of the License.
   * <p/>
   * This program is distributed in the hope that it will be useful,
   * but WITHOUT ANY WARRANTY; without even the implied warranty of
   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   * GNU Lesser General Public License for more details.
   * <p/>
   * You should have received a copy of the GNU Lesser General Public License
   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
   */
  package marytts.modules;
  
  import android.util.Log;
  
  
  import org.apache.commons.collections4.map.MultiKeyMap;
  
  import java.util.ArrayList;
  import java.util.Collections;
  import java.util.Iterator;
  import java.util.LinkedHashMap;
  import java.util.LinkedList;
  import java.util.List;
  import java.util.Locale;
  
  import marytts.datatypes.MaryDataType;
  import marytts.exceptions.MaryConfigurationException;
  import marytts.modules.synthesis.Voice;
  import marytts.server.Mary;
  import marytts.util.MaryRuntimeUtils;
  
  //import org.apache.log4j.Logger;
  
  /**
   * A hierarchical repository for Mary modules,
   * allowing the flexible indexing by an ordered hierarchy of datatype, locale and voice.
   * A given lookup will search for a combination of datatype, locale and voice
   * first; if it does not find a value, it will look for datatype, locale, and null;
   * if it does notfind that, it will look for datatype, null, and null.
   *
   * @author marc
   */
  public class ModuleRegistry {
      private static MultiKeyMap mkm;
      private static List<MaryModule> allModules;
      private static boolean registrationComplete;
      //  private static Logger logger;
  
      /**
       * Create a new, empty module repository.
       */
      static {
          mkm = new MultiKeyMap();
          allModules = new ArrayList<MaryModule>();
          registrationComplete = false;
          //  logger = MaryUtils.getLogger("ModuleRegistry");
      }
  
      private ModuleRegistry() {
      }
  
      //////////////////////////////////////////////////////////////////
      ///////////////////////// instantiation //////////////////////////
      //////////////////////////////////////////////////////////////////
  
      /**
       * From the given module init info, instantiate a new mary module.
       *
       * @param moduleInitInfo a string description of the module to instantiate.
       *                       The moduleInitInfo is expected to have one of the following forms:
       *                       <ol>
       *                       <li> my.class.which.extends.MaryModule</li>
       *                       <li> my.class.which.extends.MaryModule(any,string,args,without,spaces)</li>
       *                       <li>my.class.which.extends.MaryModule(arguments,$my.special.property,other,args)</li>
       *                       </ol>
       *                       where 'my.special.property' is a property in the property file.
       * @throws MaryConfigurationException if the module cannot be instantiated
       */
      public static MaryModule instantiateModule(String moduleInitInfo)
              throws MaryConfigurationException {
          Log.i(Mary.LOG, "Now initiating mary module '" + moduleInitInfo + "'");
  
          MaryModule m = (MaryModule) MaryRuntimeUtils.instantiateObject(moduleInitInfo);
          //Log.i(Mary.LOG,"Module '"+moduleInitInfo+" finished process initialisation. ");
          return m;
      }
  
  
      //////////////////////////////////////////////////////////////////
      ///////////////////////// registration ///////////////////////////
      //////////////////////////////////////////////////////////////////
  
  
      /**
       * Register a MaryModule as an appropriate module to process the given
       * combination of MaryDataType for the input data, locale of the input data,
       * and voice requested for processing.
       * Note that it is possible to register more than one module for a given
       * combination of input type, locale and voice; in that case, all of them
       * will be remembered, and will be returned as a List<MaryModule> by get().
       *
       * @param module the module to add to the registry, under its input type and the given locale and voice.
       * @param locale the locale (language or language-COUNTRY) of the input data; can be null to signal that
       *               the module is locale-independent.
       * @param voice  a voice for which this module is suited. Can be null to indicate
       *               that the module is not specific to any voice.
       * @throws IllegalStateException if called after registration is complete.
       */
      @SuppressWarnings("unchecked")
      public static void registerModule(MaryModule module, Locale locale, Voice voice)
              throws IllegalStateException {
          if (registrationComplete)
              throw new IllegalStateException("cannot register modules after registration is complete");
          MaryDataType type = module.inputType();
          Object o = mkm.get(type, locale, voice);
          List<MaryModule> l;
          if (o != null) {
              assert o instanceof List : "Expected List of MaryModules, got " + o.getClass();
              l = (List<MaryModule>) o;
          } else {
              l = new ArrayList<MaryModule>(1);
              mkm.put(type, locale, voice, l);
          }
          assert l != null;
          l.add(module);
  
          allModules.add(module);
  
          MaryDataType.registerDataType(type);
          MaryDataType.registerDataType(module.outputType());
      }
  
      /**
       * Determine whether or not the registration is complete.
       * When the registration is not (yet) complete, calls to
       *
       * @return false when the registration is still open, true when it is complete.
       * @see #registerModule(MaryModule, Locale, Voice) are possible;
       * when the registration is complete, calls to the other methods are possible.
       */
      public static boolean getRegistrationComplete() {
          return registrationComplete;
      }
  
      /**
       * Indicate that the registration is now complete. No further calls to
       * registerModules() will be possible.
       *
       * @throws IllegalStateException if called when registration was already completed before.
       */
      public static void setRegistrationComplete()
              throws IllegalStateException {
          if (registrationComplete)
              throw new IllegalStateException("Registration has already completed, cannot do that a second time");
  
          registrationComplete = true;
          MaryDataType.setRegistrationComplete();
      }
  
  
      //////////////////////////////////////////////////////////////////
      /////////////////////////  modules   /////////////////////////////
      //////////////////////////////////////////////////////////////////
  
  
      /**
       * Provide a list containing all MaryModules instances.
       * The order is not important.
       *
       * @throws IllegalStateException if called while registration is not yet complete.
       */
      public static List<MaryModule> getAllModules() {
          if (!registrationComplete)
              throw new IllegalStateException("Cannot inquire about modules while registration is ongoing");
          return Collections.unmodifiableList(allModules);
      }
  
  
      /**
       * Find an active module by its class.
       *
       * @return the module instance if found, or null if not found.
       * @throws IllegalStateException if called while registration is not yet complete.
       */
      // TODO: what should happen with this method when we parameterise modules, so that there can be several instances of the same class?
      public static MaryModule getModule(Class<?> moduleClass) {
          if (!registrationComplete)
              throw new IllegalStateException("Cannot inquire about modules while registration is ongoing");
          for (Iterator<MaryModule> it = allModules.iterator(); it.hasNext(); ) {
              MaryModule m = it.next();
              if (moduleClass.isInstance(m)) {
                  return m;
              }
          }
          // Not found:
          return null;
      }
  
  
      /**
       * A method for determining the list of modules required to transform
       * the given source data type into the requested target data type.
       *
       * @return the (ordered) list of modules required, or null if no such
       * list could be found.
       * @throws IllegalStateException if called while registration is not yet complete.
       * @throws NullPointerException  if source data type, target data type or locale is null.
       */
      public static LinkedList<MaryModule> modulesRequiredForProcessing(
              MaryDataType sourceType, MaryDataType targetType, Locale locale) {
          return modulesRequiredForProcessing(sourceType, targetType, locale, null);
      }
  
      /**
       * A method for determining the list of modules required to transform
       * the given source data type into the requested target data type. If the
       * voice given is not null, any preferred modules it may have are taken into account.
       *
       * @return the (ordered) list of modules required, or null if no such
       * list could be found.
       * @throws IllegalStateException if called while registration is not yet complete.
       * @throws NullPointerException  if source data type, target data type or locale is null.
       */
      public static LinkedList<MaryModule> modulesRequiredForProcessing(
              MaryDataType sourceType, MaryDataType targetType, Locale locale, Voice voice) {
          if (!registrationComplete)
              throw new IllegalStateException("Cannot inquire about modules while registration is ongoing");
          if (sourceType == null)
              throw new NullPointerException("Received null source type");
          if (targetType == null)
              throw new NullPointerException("Received null target type");
          //if (locale == null)
          //    throw new NullPointerException("Received null locale");
          LinkedList<MaryDataType> seenTypes = new LinkedList<MaryDataType>();
          seenTypes.add(sourceType);
          return modulesRequiredForProcessing(
                  sourceType, targetType, locale, voice, seenTypes);
      }
  
      /**
       * This method recursively calls itself.
       * It forward-constructs a list of seen types (upon test),
       * and backward-constructs a list of required modules (upon success).
       *
       * @param sourceType
       * @param targetType
       * @param locale
       * @param voice
       * @param seenTypes
       * @return
       */
      private static LinkedList<MaryModule> modulesRequiredForProcessing(
              MaryDataType sourceType, MaryDataType targetType, Locale locale, Voice voice,
              LinkedList<MaryDataType> seenTypes) {
          // Terminating condition:
          if (sourceType.equals(targetType)) {
              Log.d(Mary.LOG, "found path through modules");
              return new LinkedList<MaryModule>();
          }
          // Recursion step:
          // Any voice-specific modules?
          List<MaryModule> candidates = null;
          // TODO: the following should be obsolete as soon as we are properly using the voice index in ModuleRegistry
          if (voice != null) candidates = voice.getPreferredModulesAcceptingType(sourceType);
          if (candidates == null || candidates.isEmpty()) { // default: use all available modules
              candidates = get(sourceType, locale, voice);
          }
          if (candidates == null || candidates.isEmpty()) {
              // no module can handle this type
              return null;
          }
          for (Iterator<MaryModule> it = candidates.iterator(); it.hasNext(); ) {
              MaryModule candidate = it.next();
              MaryDataType outputType = candidate.outputType();
              // Ignore candidates that would bring us to a data type that we
              // have already seen (i.e., that would lead to a loop):
              if (!seenTypes.contains(outputType)) {
                  seenTypes.add(outputType);
                  Log.d(Mary.LOG,
                          "Module "
                                  + candidate.name()
                                  + " converts "
                                  + sourceType.name()
                                  + " into "
                                  + outputType
                                  + " (locale " + locale + ", voice " + voice + ")");
                  // recursive call:
                  LinkedList<MaryModule> path =
                          modulesRequiredForProcessing(
                                  outputType,
                                  targetType,
                                  locale,
                                  voice,
                                  seenTypes);
                  if (path != null) {
                      // success, found a path of which candidate is the first
                      // step
                      path.addFirst(candidate);
                      return path;
                  } // else, try next candidate
                  seenTypes.removeLast();
              }
          }
          // We get here only if none of the candidates lead to a valid path
          return null; // failure
      }
  
      /**
       * Lookup a list of modules for the given combination of type, locale and voice.
       * A given lookup will return a list of modules accepting the datatype as input,
       * ordered by specificity so that the most specific modules come first.
       * For each output type produced by modules, there will be only one module
       * producing that type, namely the most specific one found.
       * Specificity is ordered as follows:
       * 1. most specific is the full combination of datatype, locale and voice;
       * 2. the combination of datatype, language-only locale and voice (i.e., for requested locale "en-US" will find modules with locale "en");
       * 3. the combination of datatype and locale, ignoring voice;
       * 4. the combination of datatype and language-only locale, ignoring voice;
       * 5. least specific is the datatype, ignoring locale and voice.
       *
       * @param type   the type of input data
       * @param locale the locale
       * @param voice  the voice to use.
       * @return a list of mary modules accepting the datatype and suitable for
       * the given locale and voice, ordered by their specificity,
       * or an empty list if there is no suitable module.
       * @throws IllegalStateException if called when registration is not yet complete.
       */
      @SuppressWarnings("unchecked")
      private static List<MaryModule> get(MaryDataType type, Locale locale, Voice voice)
              throws IllegalStateException {
          if (!registrationComplete)
              throw new IllegalStateException("Cannot inquire about modules while registration is ongoing");
          LinkedHashMap<MaryDataType, MaryModule> results = new LinkedHashMap<MaryDataType, MaryModule>();
          // First, get all results:
          List<List<MaryModule>> listOfLists = new ArrayList<List<MaryModule>>();
          listOfLists.add((List<MaryModule>) mkm.get(type, locale, voice));
          Locale langOnly = locale != null ? new Locale(locale.getLanguage()) : null;
          boolean haveCountry = langOnly == null ? false : !langOnly.equals(locale);
          if (haveCountry) {
              listOfLists.add((List<MaryModule>) mkm.get(type, langOnly, voice));
          }
          listOfLists.add((List<MaryModule>) mkm.get(type, locale, null));
          if (haveCountry) {
              listOfLists.add((List<MaryModule>) mkm.get(type, langOnly, null));
          }
          listOfLists.add((List<MaryModule>) mkm.get(type, null, null));
          // Now, for each mary output type, keep only the most specific module,
          // and return the list ordered by the specificity of modules (most specific first):
          for (List<MaryModule> list : listOfLists) {
              if (list != null) {
                  for (MaryModule m : list) {
                      if (!results.containsKey(m.outputType())) results.put(m.outputType(), m);
                  }
              }
          }
          List<MaryModule> returnList = new ArrayList<MaryModule>();
          for (MaryDataType t : results.keySet()) {
              returnList.add(results.get(t));
          }
          return returnList;
      }
  
  
  }
  
  