/*******************************************************************************
 * Copyright (c) 2013, 2014 École Polytechnique de Montréal
 *
 * All rights reserved. This program and the accompanying materials are
 * made available under the terms of the Eclipse Public License 2.0 which
 * accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *   Geneviève Bastien - Initial API and implementation
 *******************************************************************************/

package org.eclipse.tracecompass.tmf.core.analysis;

import static org.eclipse.tracecompass.common.core.NonNullUtils.checkNotNull;

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.tracecompass.internal.tmf.core.Activator;
import org.eclipse.tracecompass.internal.tmf.core.analysis.TmfAnalysisModuleSources;
import org.eclipse.tracecompass.internal.tmf.core.analysis.TmfAnalysisParameterProviders;
import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace;

import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.Multimap;

/**
 * Manages the available analysis helpers from different sources and their
 * parameter providers.
 *
 * @author Geneviève Bastien
 */
@NonNullByDefault
public class TmfAnalysisManager {

    private static final Multimap<String, IAnalysisModuleHelper> fAnalysisModules = HashMultimap.create();
    private static final Map<String, List<Class<? extends IAnalysisParameterProvider>>> fParameterProviders = new HashMap<>();
    private static final Map<Class<? extends IAnalysisParameterProvider>, IAnalysisParameterProvider> fParamProviderInstances = new HashMap<>();
    private static final List<IAnalysisModuleSource> fSources = new ArrayList<>();
    private static final List<ITmfNewAnalysisModuleListener> fListeners = new ArrayList<>();

    /**
     * Constructor, not to be used
     */
    private TmfAnalysisManager() {

    }

    /**
     * Disposes the analysis manager
     *
     * @since 2.3
     */
    public static void dispose() {
        TmfAnalysisParameterProviders.dispose();
        synchronized (fParameterProviders) {
            fParamProviderInstances.values().forEach(provider -> provider.dispose());
        }
    }

    /**
     * Registers a new source of modules
     *
     * @param source
     *            A {@link IAnalysisModuleSource} instance
     */
    public static synchronized void registerModuleSource(IAnalysisModuleSource source) {
        fSources.add(source);
        refreshModules();
    }

    /**
     * Initializes sources and new module listeners from the extension point
     */
    public static synchronized void initialize() {
        fSources.clear();
        fListeners.clear();
        initializeModuleSources();
        initializeNewModuleListeners();
    }

    /**
     * Cleans the module sources list and initialize it from the extension point
     */
    private static synchronized void initializeModuleSources() {
        for (IAnalysisModuleSource source : TmfAnalysisModuleSources.getSources()) {
            fSources.add(source);
        }
    }

    /**
     * Cleans the new module listeners list and initialize it from the extension
     * point
     */
    private static synchronized void initializeNewModuleListeners() {
        for (ITmfNewAnalysisModuleListener output : TmfAnalysisModuleOutputs.getOutputListeners()) {
            fListeners.add(output);
        }
    }

    /**
     * Add a new module listener to the list of listeners
     *
     * @param listener
     *            The new module listener
     */
    public static synchronized void addNewModuleListener(ITmfNewAnalysisModuleListener listener) {
        fListeners.add(listener);
    }

    /**
     * Gets all available analysis module helpers
     *
     * This map is read-only
     *
     * @return The map of available {@link IAnalysisModuleHelper}
     * @since 1.0
     */
    public static synchronized Multimap<String, IAnalysisModuleHelper> getAnalysisModules() {
        if (fAnalysisModules.isEmpty()) {
            for (IAnalysisModuleSource source : fSources) {
                for (IAnalysisModuleHelper helper : source.getAnalysisModules()) {
                    fAnalysisModules.put(helper.getId(), helper);
                }
            }
        }
        return checkNotNull(ImmutableMultimap.copyOf(fAnalysisModules));
    }

    /**
     * Gets all analysis module helpers that apply to a given trace type. For
     * each analysis ID, only one helper will be returned if more than one
     * applies.
     *
     * This map is read-only
     *
     * TODO: This method is only used to populate the project view in the UI. It
     * should be deprecated eventually, after some UI rework, so that the trace
     * type does not drive whether the analysis module applies or not to a
     * trace, but rather the content of the trace or experiment (once it is
     * opened)
     *
     * @param traceclass
     *            The trace class to get modules for
     * @return The map of available {@link IAnalysisModuleHelper}
     */
    public static Map<String, IAnalysisModuleHelper> getAnalysisModules(Class<? extends ITmfTrace> traceclass) {
        Multimap<String, IAnalysisModuleHelper> allModules = getAnalysisModules();
        Map<String, IAnalysisModuleHelper> map = new HashMap<>();
        for (IAnalysisModuleHelper module : allModules.values()) {
            if (module.appliesToTraceType(traceclass)) {
                map.put(module.getId(), module);
            }
        }
        return ImmutableMap.copyOf(map);
    }

    /**
     * Register a new parameter provider for an analysis
     *
     * @param analysisId
     *            The id of the analysis
     * @param paramProvider
     *            The class of the parameter provider
     */
    public static void registerParameterProvider(String analysisId, Class<? extends IAnalysisParameterProvider> paramProvider) {
        synchronized (fParameterProviders) {
            if (!fParameterProviders.containsKey(analysisId)) {
                fParameterProviders.put(analysisId, new ArrayList<Class<? extends IAnalysisParameterProvider>>());
            }
            /* We checked via containsKey() above, get() should not return null */
            checkNotNull(fParameterProviders.get(analysisId)).add(paramProvider);
        }
    }

    /**
     * Get the parameter providers that apply to the requested trace
     *
     * @param module
     *            Analysis module
     * @param trace
     *            The trace
     * @return The set of parameter providers that apply to a trace for this module
     * @since 2.0
     */
    public static Set<IAnalysisParameterProvider> getParameterProvidersForModule(IAnalysisModule module, ITmfTrace trace) {
        /* First, get the parameter providers from the extension point */
        Set<IAnalysisParameterProvider> providerSet = TmfAnalysisParameterProviders.getParameterProvidersFor(module.getId());
        /* Then add any new parameter provider coming from other sources */
        synchronized (fParameterProviders) {
            if (!fParameterProviders.containsKey(module.getId())) {
                return providerSet;
            }
            /* We checked  via containsKey, get() should not return null */
            List<Class<? extends IAnalysisParameterProvider>> parameterProviders = checkNotNull(fParameterProviders.get(module.getId()));
            for (Class<? extends IAnalysisParameterProvider> providerClass : parameterProviders) {
                try {
                    @Nullable IAnalysisParameterProvider provider = fParamProviderInstances.get(providerClass);
                    if (provider == null) {
                        provider = providerClass.getConstructor().newInstance();
                        fParamProviderInstances.put(providerClass, provider);
                    }
                    if (provider.appliesToTrace(trace)) {
                        providerSet.add(provider);
                    }
                } catch (IllegalArgumentException | SecurityException | InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
                    Activator.logError(Messages.TmfAnalysisManager_ErrorParameterProvider, e);
                }
            }
        }
        return Collections.unmodifiableSet(providerSet);
    }

    /**
     * Clear the list of modules so that next time, it is computed again from
     * sources
     */
    public static synchronized void refreshModules() {
        fAnalysisModules.clear();
    }

    /**
     * This method should be called when new analysis modules have been created
     * by module helpers to that the {@link ITmfNewAnalysisModuleListener} can
     * be executed on the module instance.
     *
     * @param module
     *            The newly created analysis module
     */
    public static synchronized void analysisModuleCreated(IAnalysisModule module) {
        for (ITmfNewAnalysisModuleListener listener : fListeners) {
            listener.moduleCreated(module);
        }
    }

}
