/*
 * FindBugs - Find bugs in Java programs
 * Copyright (C) 2004, University of Maryland
 *
 * This library 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; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package edu.umd.cs.findbugs;

import java.io.File;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;

import edu.umd.cs.findbugs.ba.AnalysisContext;
import edu.umd.cs.findbugs.classfile.IAnalysisEngineRegistrar;
import edu.umd.cs.findbugs.cloud.CloudPlugin;
import edu.umd.cs.findbugs.plan.DetectorOrderingConstraint;
import edu.umd.cs.findbugs.util.DualKeyHashMap;

/**
 * A FindBugs plugin. A plugin contains executable Detector classes, as well as
 * meta information describing those detectors (such as human-readable detector
 * and bug descriptions).
 *
 * @see PluginLoader
 * @author David Hovemeyer
 */
public class Plugin {


    private static final String USE_FINDBUGS_VERSION = "USE_FINDBUGS_VERSION";
    private final String pluginId;

    private final String version;
    private Date releaseDate;

    private String provider;

    private URI website;
    private @CheckForNull URI updateUrl;

    private String shortDescription;
    private String detailedDescription;

    private final ArrayList<DetectorFactory> detectorFactoryList;
    private final Map<String, FindBugsMain> mainPlugins;

    private final LinkedHashSet<BugPattern> bugPatterns;

    private final LinkedHashSet<BugCode> bugCodeList;

    private final LinkedHashMap<String, BugCategory> bugCategories;
    private final LinkedHashSet<CloudPlugin> cloudList;

    private final HashMap<String,String> myGlobalOptions = new HashMap<String, String>();

    private final DualKeyHashMap<Class<?>, String, ComponentPlugin<?>> componentPlugins;

    private BugRanker bugRanker;

    // Ordering constraints
    private final ArrayList<DetectorOrderingConstraint> interPassConstraintList;

    private final ArrayList<DetectorOrderingConstraint> intraPassConstraintList;

    // Optional: engine registrar class
    private Class<? extends IAnalysisEngineRegistrar> engineRegistrarClass;

    // PluginLoader that loaded this plugin
    private final PluginLoader pluginLoader;

    private final boolean enabledByDefault;

    private final boolean cannotDisable;

    static Map<URI, Plugin> allPlugins = new LinkedHashMap<URI, Plugin>();

    enum EnabledState { PLUGIN_DEFAULT, ENABLED, DISABLED};

    private EnabledState enabled;


    /**
     * Constructor. Creates an empty plugin object.
     *
     * @param pluginId
     *            the plugin's unique identifier
     * @param version TODO
     * @param enabled TODO
     * @param cannotDisable TODO
     */
    public Plugin(String pluginId, String version, Date releaseDate, @Nonnull PluginLoader pluginLoader, boolean enabled, boolean cannotDisable) {
        this.pluginId = pluginId;
        if (version == null) {
            version = "";
        } else if (version.equals(USE_FINDBUGS_VERSION)) {
            version = Version.COMPUTED_RELEASE;
            releaseDate = Version.getReleaseDate();
        }
        assert enabled || !cannotDisable;
        cloudList = new LinkedHashSet<CloudPlugin>();
        componentPlugins = new DualKeyHashMap<Class<?>, String, ComponentPlugin<?>> ();
        this.version = version;
        this.releaseDate = releaseDate;
        this.detectorFactoryList = new ArrayList<DetectorFactory>();
        this.bugPatterns = new LinkedHashSet<BugPattern>();
        this.bugCodeList = new LinkedHashSet<BugCode>();
        this.bugCategories = new LinkedHashMap<String,BugCategory>();
        this.interPassConstraintList = new ArrayList<DetectorOrderingConstraint>();
        this.intraPassConstraintList = new ArrayList<DetectorOrderingConstraint>();
        this.mainPlugins = new HashMap<String, FindBugsMain>();
        this.pluginLoader = pluginLoader;
        this.enabledByDefault = enabled;
        this.cannotDisable = cannotDisable;
        this.enabled = EnabledState.PLUGIN_DEFAULT;
    }

    @Override
    public String toString() {
        return this.getClass().getSimpleName() + ":" + pluginId;
    }

    /**
     * Return whether or not the Plugin is enabled.
     *
     * @return true if the Plugin is enabled, false if not
     */
    public boolean isEnabledByDefault() {
        return enabledByDefault;
    }

    /**
     * Set plugin provider.
     *
     * @param provider
     *            the plugin provider
     */
    public void setProvider(String provider) {
        this.provider = provider;
    }

    /**
     * Get the plugin provider.
     *
     * @return the provider, or null if the provider was not specified
     */
    public @CheckForNull String getProvider() {
        return provider;
    }


    public void setUpdateUrl(String url) throws URISyntaxException {
        this.updateUrl = new URI(url);
    }

    public @CheckForNull URI getUpdateUrl() {
        return updateUrl;
    }

    public void setMyGlobalOption(String key, String value) {
        myGlobalOptions.put(key, value);
    }

    Map<String,String> getMyGlobalOptions() {
        return Collections.unmodifiableMap(myGlobalOptions);
    }
    /**
     * Set plugin website.
     *
     * @param website
     *            the plugin website
     * @throws URISyntaxException
     */
    public void setWebsite(String website) throws URISyntaxException {
        this.website = new URI(website);
    }

    /**
     * Get the plugin website.
     *
     * @return the website, or null if the was not specified
     */
    public  @CheckForNull String getWebsite() {
        if (website == null)
            return null;
        return website.toASCIIString();
    }

    public  @CheckForNull URI getWebsiteURI() {
        return website;
    }

    public String getVersion() {
        return version;
    }

    public Date getReleaseDate() {
        return releaseDate;
    }

    public void setShortDescription(String shortDescription) {
        this.shortDescription = shortDescription;
    }

    public String getShortDescription() {
        return shortDescription;
    }

    public String getDetailedDescription() {
        return detailedDescription;
    }

    public void setDetailedDescription(String detailedDescription) {
        this.detailedDescription = detailedDescription;
    }

    /**
     * Add a DetectorFactory for a Detector implemented by the Plugin.
     *
     * @param factory
     *            the DetectorFactory
     */
    public void addDetectorFactory(DetectorFactory factory) {
        detectorFactoryList.add(factory);
    }

    public void addCloudPlugin(CloudPlugin cloudPlugin) {
        cloudList.add(cloudPlugin);
    }

    /**
     * Add a BugPattern reported by the Plugin.
     *
     * @param bugPattern
     */
    public void addBugPattern(BugPattern bugPattern) {
        bugPatterns.add(bugPattern);
    }

    /**
     * Add a BugCode reported by the Plugin.
     *
     * @param bugCode
     */
    public void addBugCode(BugCode bugCode) {
        bugCodeList.add(bugCode);
    }

    /**
     * Add a BugCategory reported by the Plugin.
     *
     * @param bugCode
     */
    public void addBugCategory(BugCategory bugCategory) {
        BugCategory old = bugCategories.get(bugCategory.getCategory());
        if (old != null)
            throw new IllegalArgumentException("Category already exists");
        bugCategories.put(bugCategory.getCategory(), bugCategory);
    }


    public BugCategory addOrCreateBugCategory(String id) {
        BugCategory c = bugCategories.get(id);
        if (c != null)
            return c;
        c = new BugCategory(id);
        bugCategories.put(id, c);
        return c;
    }
    /**
     * Add an inter-pass Detector ordering constraint.
     *
     * @param constraint
     *            the inter-pass Detector ordering constraint
     */
    public void addInterPassOrderingConstraint(DetectorOrderingConstraint constraint) {
        interPassConstraintList.add(constraint);
    }

    /**
     * Add an intra-pass Detector ordering constraint.
     *
     * @param constraint
     *            the intra-pass Detector ordering constraint
     */
    public void addIntraPassOrderingConstraint(DetectorOrderingConstraint constraint) {
        intraPassConstraintList.add(constraint);
    }

    /**
     * Look up a DetectorFactory by short name.
     *
     * @param shortName
     *            the short name
     * @return the DetectorFactory
     */
    public DetectorFactory getFactoryByShortName(final String shortName) {
        return findFirstMatchingFactory(new FactoryChooser() {
            public boolean choose(DetectorFactory factory) {
                return factory.getShortName().equals(shortName);
            }
        });
    }

    /**
     * Look up a DetectorFactory by full name.
     *
     * @param fullName
     *            the full name
     * @return the DetectorFactory
     */
    public DetectorFactory getFactoryByFullName(final String fullName) {
        return findFirstMatchingFactory(new FactoryChooser() {
            public boolean choose(DetectorFactory factory) {
                return factory.getFullName().equals(fullName);
            }
        });
    }

    /**
     * Get Iterator over DetectorFactory objects in the Plugin.
     *
     * @return Iterator over DetectorFactory objects
     */
    public Collection<DetectorFactory> getDetectorFactories() {
        return detectorFactoryList;
    }

    /**
     * Get the set of BugPatterns
     *
     */
    public Set<BugPattern> getBugPatterns() {
        return bugPatterns;
    }

    /**
     * Get Iterator over BugCode objects in the Plugin.
     *
     * @return Iterator over BugCode objects
     */
    public Set<BugCode> getBugCodes() {
        return bugCodeList;
    }
    /**
     * Get Iterator over BugCategories objects in the Plugin.
     *
     * @return Iterator over BugCategory objects
     */
    public Collection<BugCategory> getBugCategories() {
        return bugCategories.values();
    }

    /**
     * @param id may be null
     * @return return bug category with given id, may return null if the bug category is unknown
     */
    @CheckForNull
    public BugCategory getBugCategory(String id) {
        return bugCategories.get(id);
    }

    public Set<CloudPlugin> getCloudPlugins() {
        return cloudList;
    }
    /**
     * Return an Iterator over the inter-pass Detector ordering constraints.
     */
    public Iterator<DetectorOrderingConstraint> interPassConstraintIterator() {
        return interPassConstraintList.iterator();
    }

    /**
     * Return an Iterator over the intra-pass Detector ordering constraints.
     */
    public Iterator<DetectorOrderingConstraint> intraPassConstraintIterator() {
        return intraPassConstraintList.iterator();
    }

    /**
     * @return Returns the pluginId.
     */
    public String getPluginId() {
        return pluginId;
    }
    /**
     * @return Returns the short pluginId.
     */
    public String getShortPluginId() {
        int i = pluginId.lastIndexOf('.');
        return pluginId.substring(i+1);
    }
    /**
     * Set the analysis engine registrar class that, when instantiated, can be
     * used to register the plugin's analysis engines with the analysis cache.
     *
     * @param engineRegistrarClass
     *            The engine registrar class to set.
     */
    public void setEngineRegistrarClass(Class<? extends IAnalysisEngineRegistrar> engineRegistrarClass) {
        this.engineRegistrarClass = engineRegistrarClass;
    }

    /**
     * Get the analysis engine registrar class that, when instantiated, can be
     * used to register the plugin's analysis engines with the analysis cache.
     *
     * @return Returns the engine registrar class.
     */
    public Class<? extends IAnalysisEngineRegistrar> getEngineRegistrarClass() {
        return engineRegistrarClass;
    }

    /**
     * @return Returns the pluginLoader.
     */
    public PluginLoader getPluginLoader() {
        return pluginLoader;
    }

    private interface FactoryChooser {
        public boolean choose(DetectorFactory factory);
    }

    private @CheckForNull
    DetectorFactory findFirstMatchingFactory(FactoryChooser chooser) {
        for (DetectorFactory factory : getDetectorFactories()) {
            if (chooser.choose(factory))
                return factory;
        }
        return null;
    }

    /**
     * @param ranker
     */
    public void setBugRanker(BugRanker ranker) {
        this.bugRanker = ranker;
    }

    public BugRanker getBugRanker() {
        return bugRanker;
    }

    <T> void addFindBugsMain(Class<?> mainClass, String cmd, String description, String kind, boolean analysis) 
            throws SecurityException, NoSuchMethodException {
        FindBugsMain main = new FindBugsMain(mainClass, cmd, description, kind, analysis);
        mainPlugins.put(cmd, main);
    }

    public @CheckForNull FindBugsMain getFindBugsMain(String cmd) {
        return mainPlugins.get(cmd);

    }

    public Collection<FindBugsMain> getAllFindBugsMain() {
        return mainPlugins.values();

    }

    <T> void addComponentPlugin(Class<T> componentKind, ComponentPlugin<T> plugin) {
        Class<? extends T> componentClass = plugin.getComponentClass();
        if (componentClass != null && !componentKind.isAssignableFrom(componentClass))
                throw new IllegalArgumentException();
        componentPlugins.put(componentKind, plugin.getId(), plugin);
    }

    @SuppressWarnings("unchecked")
    public <T> Iterable<ComponentPlugin<T>> getComponentPlugins(Class<T> componentClass) {
        @SuppressWarnings("rawtypes")
        Collection values = componentPlugins.get(componentClass).values();
        return values;
    }

    @SuppressWarnings("unchecked")
    public <T>  ComponentPlugin<T> getComponentPlugin(Class<T> componentClass, String name) {
        return (ComponentPlugin<T>) componentPlugins.get(componentClass, name);
    }

    public static synchronized @CheckForNull Plugin getByPluginId(String name) {
        if(name == null) {
            return null;
        }
        for(Plugin plugin : allPlugins.values()) {
            // the second part is questionable, as this may lead to id collisions
            if (name.equals(plugin.getPluginId()) /*|| name.equals(plugin.getShortPluginId())*/)
                return plugin;
        }
        return null;
    }
    
    public static synchronized void removePlugin(URI uri) {
       allPlugins.remove(uri);
    }

    /**
     * @return a copy of the internal plugins collection
     */
    public static synchronized Collection<Plugin> getAllPlugins() {
        return new ArrayList<Plugin>(allPlugins.values());
    }

    public static synchronized Collection<String> getAllPluginIds() {
       ArrayList<String> result = new ArrayList<String>();
       for(Plugin p : allPlugins.values())
           result.add(p.getPluginId());
       return result;
    }
    /**
     * @return a copy of the internal plugins collection
     */
    public static synchronized Map<URI, Plugin> getAllPluginsMap() {
        return new LinkedHashMap<URI, Plugin>(allPlugins);
    }

    public static synchronized Set<URI> getAllPluginsURIs() {
        Collection<Plugin> plugins = getAllPlugins();
        Set<URI> uris = new HashSet<URI>();
        for (Plugin plugin : plugins) {

            try {
                URI uri = plugin.getPluginLoader().getURL().toURI();
                if(uri != null) {
                    uris.add(uri);
                }
            } catch (URISyntaxException e) {
                AnalysisContext.logError("Unable to get URI", e);
            }

        }
        return uris;
    }

    /**
     * @return may return null
     */
    @CheckForNull
    static synchronized Plugin getPlugin(URI uri) {
        return allPlugins.get(uri);
    }

    /**
     * @return may return null
     */
    @CheckForNull
    static synchronized Plugin putPlugin(URI uri, Plugin plugin) {
        return allPlugins.put(uri, plugin);
    }

    public boolean isCorePlugin() {
        return pluginLoader.isCorePlugin();
    }

    public boolean cannotDisable() {
        return cannotDisable;
    }

    public boolean isGloballyEnabled() {
        if (isCorePlugin())
            return true;
        switch (enabled) {
        case ENABLED:
            return true;
        case DISABLED:
            return false;
        case PLUGIN_DEFAULT:
            return isEnabledByDefault();
        default:
            throw new IllegalStateException("Unknown state : " + enabled);
        }
    }

    /**
     * @return
     */
    public void setGloballyEnabled(boolean enabled) {
        if (isCorePlugin()) {
            if (!enabled)
                throw new IllegalArgumentException("Can't disable core plugin");
            return;
        }
        if (cannotDisable) {
            if (enabled) return;
            throw new IllegalArgumentException("Cannot disable " + pluginId);
        }
        EnabledState oldState = this.enabled;

        if (enabled) {
            if (isEnabledByDefault())
                this.enabled = EnabledState.PLUGIN_DEFAULT;
            else
                this.enabled = EnabledState.ENABLED;
        } else {
            if (isEnabledByDefault())
                this.enabled = EnabledState.DISABLED;
            else
                this.enabled = EnabledState.PLUGIN_DEFAULT;
        }
        if(oldState != this.enabled) {
            // TODO update detector factory collection?
        }
    }

    public boolean isInitialPlugin() {
        return getPluginLoader().initialPlugin;
    }

    public URL getResource(String name) {
        return getPluginLoader().getResource(name);
    }

    public ClassLoader getClassLoader() {
        return getPluginLoader().getClassLoader();
    }
    
    public @CheckForNull Plugin getParentPlugin() {
        if (getPluginLoader().hasParent()) 
            return Plugin.getByPluginId(getPluginLoader().parentId);
        return null;
    }

    /**
     * Loads the given plugin and enables it for the given project.
     */
    public static Plugin loadCustomPlugin(File f, @CheckForNull Project project)
            throws PluginException {
        URL urlString;
        try {
            urlString = f.toURI().toURL();
        } catch (MalformedURLException e) {
            throw new IllegalArgumentException(e);
        }
        return loadCustomPlugin(urlString, project);
    }

    /**
     * Loads the given plugin and enables it for the given project.
     */
    public static Plugin loadCustomPlugin(URL urlString, @CheckForNull Project project) throws PluginException {
        Plugin plugin = addCustomPlugin(urlString);
        if (project != null) {
            project.setPluginStatusTrinary(plugin.getPluginId(), true);
        }
        return plugin;
    }

    public static @CheckForNull Plugin addCustomPlugin(URL u) throws PluginException {
        return addCustomPlugin(u, PluginLoader.class.getClassLoader());
    }
    public static @CheckForNull  Plugin addCustomPlugin(URI u) throws PluginException {
        return addCustomPlugin(u, PluginLoader.class.getClassLoader());
    }
    public static @CheckForNull Plugin addCustomPlugin(URL u, ClassLoader parent) throws PluginException {
        PluginLoader pluginLoader = PluginLoader.getPluginLoader(u, parent, false, true);
        Plugin plugin = pluginLoader.loadPlugin();
        if (plugin != null)
            DetectorFactoryCollection.instance().loadPlugin(plugin);

        return plugin;
    }

    public static @CheckForNull Plugin addCustomPlugin(URI u, ClassLoader parent) throws PluginException {
        URL url;
        try {
            url = u.toURL();
        } catch (MalformedURLException e) {
            throw new PluginException("Unable to convert uri to url:" + u, e);
        }
        return addCustomPlugin(url, parent);
    }
    public static synchronized void removeCustomPlugin(Plugin plugin) {
        Set<Entry<URI, Plugin>> entrySet = Plugin.allPlugins.entrySet();
        for (Entry<URI, Plugin> entry : entrySet) {
            if(entry.getValue() == plugin) {
                Plugin.allPlugins.remove(entry.getKey());
                PluginLoader.loadedPluginIds.remove(plugin.getPluginId());
                break;
            }
        }
        DetectorFactoryCollection.instance().unLoadPlugin(plugin);
    }

}
