/*
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   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 General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/*
 *    PluginManager.java
 *    Copyright (C) 2011-2012 University of Waikato, Hamilton, New Zealand
 *
 */

package weka.classifiers.evaluation;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeMap;

/**
 * Class that manages a global map of plugins. The knowledge flow uses this to
 * manage plugins other than step components and perspectives. Is general
 * purpose, so can be used by other Weka components. Provides static methods for
 * registering and instantiating plugins.
 * 
 * @author Mark Hall (mhall{[at]}pentaho{[dot]}com)
 * @version $Revision: 9312 $
 */
public class PluginManager {

  /**
   * Global map that is keyed by plugin base class/interface type. The inner Map
   * then stores individual plugin instances of the interface type, keyed by
   * plugin name/short title with values the actual fully qualified class name
   */
  protected static Map<String, Map<String, String>> PLUGINS = new HashMap<String, Map<String, String>>();

  /**
   * Set of concrete fully qualified class names or abstract/interface base
   * types to "disable". Entries in this list wont ever be returned by any of
   * the getPlugin() methods. Registering an abstract/interface base name will
   * disable all concrete implementations of that type
   */
  protected static Set<String> DISABLED = new HashSet<String>();

  /**
   * Add the supplied list of fully qualified class names to the disabled list
   * 
   * @param classnames a list of class names to add
   */
  public static synchronized void addToDisabledList(List<String> classnames) {
    for (String s : classnames) {
      addToDisabledList(s);
    }
  }

  /**
   * Add the supplied fully qualified class name to the list of disabled plugins
   * 
   * @param classname the fully qualified name of a class to add
   */
  public static synchronized void addToDisabledList(String classname) {
    DISABLED.add(classname);
  }

  /**
   * Remove the supplied list of fully qualified class names to the disabled
   * list
   * 
   * @param classnames a list of class names to remove
   */
  public static synchronized void removeFromDisabledList(List<String> classnames) {
    for (String s : classnames) {
      removeFromDisabledList(s);
    }
  }

  /**
   * Remove the supplied fully qualified class name from the list of disabled
   * plugins
   * 
   * @param classname the fully qualified name of a class to remove
   */
  public static synchronized void removeFromDisabledList(String classname) {
    DISABLED.remove(classname);
  }

  /**
   * Returns true if the supplied fully qualified class name is in the disabled
   * list
   * 
   * @param classname the name of the class to check
   * @return true if the supplied class name is in the disabled list
   */
  public static boolean isInDisabledList(String classname) {
    return DISABLED.contains(classname);
  }

  /**
   * Add all key value pairs from the supplied property file
   * 
   * @param propsFile the properties file to add
   * @throws Exception if a problem occurs
   */
  public static synchronized void addFromProperties(File propsFile)
      throws Exception {
    BufferedInputStream bi = new BufferedInputStream(new FileInputStream(
        propsFile));
    addFromProperties(bi);
  }

  /**
   * Add all key value pairs from the supplied properties stream
   * 
   * @param propsStream an input stream to a properties file
   * @throws Exception if a problem occurs
   */
  public static synchronized void addFromProperties(InputStream propsStream)
      throws Exception {
    Properties expProps = new Properties();

    expProps.load(propsStream);
    propsStream.close();
    propsStream = null;

    addFromProperties(expProps);
  }

  /**
   * Add all key value pairs from the supplied properties object
   * 
   * @param props a Properties object
   * @throws Exception if a problem occurs
   */
  public static synchronized void addFromProperties(Properties props)
      throws Exception {
    Set keys = props.keySet();
    Iterator keysI = keys.iterator();
    while (keysI.hasNext()) {
      String baseType = (String) keysI.next();
      String implementations = props.getProperty(baseType);
      if (implementations != null && implementations.length() > 0) {
        String[] parts = implementations.split(",");
        for (String impl : parts) {
          PluginManager.addPlugin(baseType, impl.trim(), impl.trim());
        }
      }
    }
  }

  /**
   * Get a set of names of plugins that implement the supplied interface.
   * 
   * @param interfaceName the fully qualified name of the interface to list
   *          plugins for
   * 
   * @return a set of names of plugins
   */
  public static Set<String> getPluginNamesOfType(String interfaceName) {
    if (PLUGINS.get(interfaceName) != null) {
      Set<String> match = PLUGINS.get(interfaceName).keySet();
      Set<String> result = new HashSet<String>();
      for (String s : match) {
        String impl = PLUGINS.get(interfaceName).get(s);
        if (!DISABLED.contains(impl)) {
          result.add(s);
        }
      }
      // return PLUGINS.get(interfaceName).keySet();
      return result;
    }

    return null;
  }

  /**
   * Add a plugin.
   * 
   * @param interfaceName the fully qualified interface name that the plugin
   *          implements
   * 
   * @param name the name/short description of the plugin
   * @param concreteType the fully qualified class name of the actual concrete
   *          implementation
   */
  public static void addPlugin(String interfaceName, String name,
      String concreteType) {
    if (PLUGINS.get(interfaceName) == null) {
      Map<String, String> pluginsOfInterfaceType = new TreeMap<String, String>();
      pluginsOfInterfaceType.put(name, concreteType);
      PLUGINS.put(interfaceName, pluginsOfInterfaceType);
    } else {
      PLUGINS.get(interfaceName).put(name, concreteType);
    }
  }

  /**
   * Remove plugins of a specific type.
   * 
   * @param interfaceName the fully qualified interface name that the plugins to
   *          be remove implement
   * @param names a list of named plugins to remove
   */
  public static void removePlugins(String interfaceName, List<String> names) {
    for (String name : names) {
      removePlugin(interfaceName, name);
    }
  }

  /**
   * Remove a plugin.
   * 
   * @param interfaceName the fully qualified interface name that the plugin
   *          implements
   * 
   * @param name the name/short description of the plugin
   */
  public static void removePlugin(String interfaceName, String name) {
    if (PLUGINS.get(interfaceName) != null) {
      PLUGINS.get(interfaceName).remove(name);
    }
  }

  /**
   * Get an instance of a concrete implementation of a plugin type
   * 
   * @param interfaceType the fully qualified interface name of the plugin type
   * @param name the name/short description of the plugin to get
   * @return the concrete plugin or null if the plugin is disabled
   * @throws Exception if the plugin can't be found or instantiated
   */
  public static Object getPluginInstance(String interfaceType, String name)
      throws Exception {
    if (PLUGINS.get(interfaceType) == null
        || PLUGINS.get(interfaceType).size() == 0) {
      throw new Exception("No plugins of interface type: " + interfaceType
          + " available!!");
    }

    Map<String, String> pluginsOfInterfaceType = PLUGINS.get(interfaceType);
    if (pluginsOfInterfaceType.get(name) == null) {
      throw new Exception("Can't find named plugin '" + name + "' of type '"
          + interfaceType + "'!");
    }

    String concreteImpl = pluginsOfInterfaceType.get(name);
    Object plugin = null;
    if (!DISABLED.contains(concreteImpl)) {
      plugin = Class.forName(concreteImpl).newInstance();
    }

    return plugin;
  }
}
