package yi.util.config;

import java.util.*;
import java.io.*;
import java.lang.reflect.*;

import yi.util.log.*;

/**
 * <p>
 * Configuration is basically a properties list.  It is implemented as a
 * singleton.  An object of classA can retrieve its configuration through the
 * key of "classA.PROPERTY".  For example, BIN_NUMBER of ColorHistogram can be
 * retrieved by Configuration.getParameter("yi.ColorHistogram.BIN_NUMBER");
 * </p>
 *
 * <p>
 * User can use command line argument to overwrite the parameter definitions in
 * configuration file.  The argument format to define the parameter is
 * </p>
 *
 * <pre>
 * -Dparameter_name=parameter_vaule
 * </pre>
 *
 * <p>
 * Command line arguments can even be used to overwrite the default
 * configuration file name.  Use
 * </p>
 *
 * <pre>
 * -DCONFIG_FILE=configuration_file_name
 * </pre>
 *
 * <p>
 * A typical usage of this class
 * </p>
 *
 * <pre>
 * public static void main(String[] args) {
 *   Configuration.init(args);
 *   String v = Configuration.getParameter("yi.ColorHistogram.BIN_NUMBER");
 * }
 * </pre>
 *
 * <p>
 * Another common usage of this class is to use configParameters.  This method
 * takes a Configurable object as parameter (which defines a getParameterNames()).
 * Configuration will try config every parameters returned by getParameterNames().
 * </p>
 *
 */
public class Configuration implements Configurable{

  private final static String DEFINED_FLAG = "true";

  /**
   * Control the output message inside this module.  The messages get more from
   * 0 to 5.  0 means no message; while 5 means all messages.  Messages in level
   * 0 are critical (errors caught).  In level 1, are ignorable errors.  In
   * level 3, are import running messages.  In level 5, are general running
   * messages.  Always set the level default to 1 when possible.
   */
  public  int LOG_LEVEL = 1;

  /**
   * Configuration file to use.
   */
  private String m_configFile = "config.properties";

  /**
   * All the configurations for every parameters
   */
  private Properties m_configs = null;

  /**
   * Singleton of Configuration.
   */
  private static Configuration m_configuration = null;

  public String[] getParameterNames() {
    return new String[] {
      "LOG_LEVEL"
    };
  }

  /**
   * Initialize Configuration singleton with command line arguments
   * @param args command line arguments
   * @return initialized Configuration instance
   */
  private static Configuration getSingleton(String[] args) {
    if (m_configuration != null) return m_configuration;
    m_configuration = new Configuration(args);
    m_configuration.configParameters(m_configuration);
    return m_configuration;
  }

  /**
   * Initialize Configuration singleton with command line arguments
   * @param args command line arguments
   * @return initialized Configuration instance
   */
  public static Configuration init(String[] args) {
    return getSingleton(args);
  }

  /**
   * <p>
   * Init Configuration with command line arguments.  The important argument for
   * Configuration is Configuration.CONFIG_FILE.  You can use this argument to
   * overwrite the default configuration file name
   * </p>
   *
   * <p>
   * The parameter definitions from command line will overwrite those from
   * config file.
   * </p>
   *
   * <p>
   * To specify a parameter definition from command line, simple use
   * </p>
   *
   * <pre>
   * -Dparameter_name=parameter_value
   * </pre>
   *
   * <p>
   * eg.
   * </p>
   * <pre>
   * -DCONFIG_FILE=yi.config
   * </pre>
   *
   * @param args the command line argument array
   */
  private Configuration(String[] args) {
    // init the parameter list (implemented by Properties)
    m_configs = new Properties();

    // read CONFIG_FILE definition from command line first
    String CONFIG_FILE_FLAG = "-DCONFIG_FILE=";
    if (args != null) {
      for (int i=0; i<args.length; i++) {
        if (args[i].startsWith(CONFIG_FILE_FLAG)) {
          m_configFile = args[i].substring(CONFIG_FILE_FLAG.length());
          break;
        }
      }
    }

    // now we know the name of the configuration file
    if (LOG_LEVEL >=3) {
      System.out.println();
      System.out.println("Configuration file used: " + m_configFile);
    }
    m_configs.setProperty("CONFIG_FILE", m_configFile);

    // read configuration file
    loadParameters();

    // reparse command line to overwrite the parameter definitions
    if (args != null) {
      for (int i=0; i<args.length; i++) {
        if (args[i].startsWith("-D")) {
          String config = args[i].substring(2);
          int equal = config.indexOf('=');
          if (equal == -1) {  // '=' not found
            m_configs.setProperty(config, DEFINED_FLAG);
            if (LOG_LEVEL >= 3) {
              System.out.println();
              System.out.println("Parameter set: " + config + " = true");
            }
          } else {
            String k = config.substring(0, equal);
            String v = config.substring(equal + 1);
            if (k.length()>0 && v.length()>0) {
              m_configs.setProperty(k, v);
              if (LOG_LEVEL >= 3) {
                System.out.println();
                System.out.println("Parameter set: " + k + " = " + v);
              }
            }
            else {
              System.err.println("Can't parse argument: " + args[i]);
            }
          }
        }
      }
    }

    // print out the parameters
    String verbose_level_str = m_configs.getProperty("LOG_LEVEL");
    if (verbose_level_str != null) {
      try {
        LOG_LEVEL = Integer.parseInt(verbose_level_str);
      }
      catch (NumberFormatException e) {
        e.printStackTrace();
      }
    }
    if (LOG_LEVEL >= 5 ) {
      System.out.println();
      System.out.println("Parameter Configuration List");
      m_configs.list(System.out);
    }
  }

  private void loadParameters() {
    try {
      InputStream in = new BufferedInputStream(new FileInputStream(m_configFile));
      m_configs.load(in);
      in.close();
    } catch (IOException e) {
      System.err.println("Can't load configurations from " + m_configFile);
    }
  }

  /**
   * Store the configurations to a file
   * @param filename the name of the file to store the configurations
   */
  public static void storeParameters(String filename) {
    Configuration c = getSingleton(null);
    try {
      OutputStream out = new BufferedOutputStream(new FileOutputStream(filename));
      c.m_configs.store(out, "configuration file");
      out.close();
    } catch (IOException e) {
      System.err.println("Can't store configurations");
      e.printStackTrace();
    }
  }

  /**
   * Get the value of the specified parameter.
   * @param key the name of the parameter.
   * @return the String value of the parameter.
   */
  public static String getParameter(String key) {
    return getSingleton(null).m_configs.getProperty(key);
  }

  /**
   * Get the value of the specified parameter.
   * @param key the name of the parameter.
   * @param default_value
   * @return the String value of the parameter.
   */
  public static String getParameter(String key, String default_value) {
    return getSingleton(null).m_configs.getProperty(key, default_value);
  }

  /**
   * Set parameter
   * @param key key
   * @param value value
   */
  public static void setParameter(String key, String value) {
    getSingleton(null).m_configs.setProperty(key, value);
  }

  /**
   * Get the value of a boolean type parameter
   * @param key the name of the parameter
   * @param defaultValue default value of the parameter
   * @return the boolean value of the parameter
   */
  public static boolean getBooleanParameter(String key, boolean defaultValue) {
    Configuration c = Configuration.getSingleton(null);
    String v;
    if ((v=c.getParameter(key))== null) return defaultValue;
    else {
      return v.compareToIgnoreCase("true")==0 || v.compareTo("1")==0;
    }
  }

  /**
   * Get the value of a int type parameter.
   * @param key The name of the parameter.
   * @param defaultValue Default value of the parameter.
   * @return The int value of the parameter.
   */
  public static int getIntParameter(String key, int defaultValue) {
    Configuration c = Configuration.getSingleton(null);
    String v;
    if ((v=c.getParameter(key))== null) return defaultValue;
    else {
      return Integer.parseInt(v);
    }
  }

  /**
   * Get the value of a double type parameter.
   * @param key The name of the parameter.
   * @param defaultValue Default value of the parameter.
   * @return The double value of the parameter.
   */
  public static double getDoubleParameter(String key, double defaultValue) {
    Configuration c = Configuration.getSingleton(null);
    String v;
    if ((v=c.getParameter(key))== null) return defaultValue;
    else {
      return Double.parseDouble(v);
    }
  }

  /**
   * Get the value of a string type parameter.
   * @param key The name of the parameter.
   * @param defaultValue Default value of the parameter.
   * @return The string value of the parameter.
   */
  public static String getStringParameter(String key, String defaultValue) {
    Configuration c = Configuration.getSingleton(null);
    String v;
    if ((v=c.getParameter(key))== null) return defaultValue;
    else return v;
  }

  /**
   * Get the configuration of a list of parameters.  The parameters to be
   * configured and their default values are listed in p.
   * @param parameters The parameters to be configured.
   */
  public static void configParameters(Properties parameters) {
    Configuration c = getSingleton(null);
    Enumeration e = parameters.keys();
    while (e.hasMoreElements()) {
      String k = (String)e.nextElement();
      String v = c.m_configs.getProperty(k);
      if (v!=null) parameters.setProperty(k, v);
    }
    return;
  }

  /**
   * <p>
   * Config the parameters for a Configuable object.  A configuable object provides
   * a function returning the name list of its parameters.  This function will
   * use that name list to search the configuation list and set the value of the
   * parameters when possible.
   * </p>
   *
   * @param obj The module to be configued.
   */
  public static void configParameters(Configurable obj) {
    Configuration config = getSingleton(null);
    Class obj_class = obj.getClass();

    // go through every parameter configuration item in Configuation
    Enumeration e = config.m_configs.keys();
    while (e.hasMoreElements()) {
      String k = (String)e.nextElement(); // eg yi.iu.ui.color.ColorHistogram.BIN_NUM
      int ind = k.lastIndexOf('.');
      String pname = k.substring(ind+1);  // eg BIN_NUM (parameter name)
      String cname = (ind == -1)? "java.lang.Object" : k.substring(0, ind);
                                          // eg yi.iu.ui.color.ColorHistogram
      try {
        Class c = Class.forName(cname);     // the class the configuration for
        if (c.isInstance(obj)) {
          // configure it
          if (config.isConfigurableParameter(obj, pname)) {
            config.configParameter(obj, pname, config.getParameter(k));
          }
        }
      } catch (ClassNotFoundException ex) {
        System.err.println("Can't find such a class defined for config: " + k);
      }
    }
  }

  /**
   * Whether a parameter is configurable for an object.
   * @param obj The object to be looked at.
   * @param pname The parameter name.
   * @return True if it is a configurable parameter of the object
   */
  private boolean isConfigurableParameter(Configurable obj, String pname) {
    String[] configurable_params = obj.getParameterNames();
    for (int i=0; i<configurable_params.length; i++) {
      if (pname.compareTo(configurable_params[i])==0) {
        return true;
      }
    }
    return false;
  }

  /**
   * Configure a parameter of an object
   * @param obj The object to be configured.
   * @param pname Parameter name.
   * @param pvalue Value to be assigned.
   */
  private void configParameter(Configurable obj, String pname, String pvalue) {
    Class c = obj.getClass();
    try {
      Field f = c.getField(pname);
      Class type = f.getType();
      String type_name = type.getName();
      if (type_name.compareTo("int") == 0) {
        int old_value = f.getInt(obj);
        f.setInt(obj, Integer.parseInt(pvalue));
        int new_value = f.getInt(obj);
        LogHandler.log(this, LOG_LEVEL, 3,
          "Parameter set: " +
          c.getName() + "." + pname + " = " + new_value + " (" + old_value + ")");
      }
      else if (type_name.compareTo("double")==0) {
        double old_value = f.getDouble(obj);
        f.setDouble(obj, Double.parseDouble(pvalue));
        double new_value = f.getDouble(obj);
        LogHandler.log(this, LOG_LEVEL, 3,
          "Parameter set: " +
          c.getName() + "." + pname + " = " + new_value + " (" + old_value + ")");
      }
      else if (type_name.compareTo("boolean")==0) {
        boolean old_value = f.getBoolean(obj);
        boolean bvalue = Boolean.valueOf(pvalue).booleanValue();
        f.setBoolean(obj, bvalue);
        boolean new_value = f.getBoolean(obj);
        LogHandler.log(this, LOG_LEVEL, 3,
          "Parameter set: " +
          c.getName() + "." + pname + " = " + new_value + " (" + old_value + ")");
      }
      else {
        System.err.println("This parameter type is not supported yet: " +
          c.getName() + "." + pname);
      }
    }
    catch (NoSuchFieldException e) {
      e.printStackTrace();
    }
    catch (SecurityException e) {
      e.printStackTrace();
    }
    catch (IllegalAccessException e) {
      e.printStackTrace();
    }
  }


  public static String[] combineParameterNames(String[] strs1, String[] strs2) {
    String[] combined = new String[strs1.length + strs2.length];
    for (int i=0; i<strs1.length; i++) {
      combined[i] = strs1[i];
    }
    for (int i=0; i<strs2.length; i++) {
      combined[i+strs1.length] = strs2[i];
    }
    return combined;
  }
}