/*******************************************************************************
 * Copyright (c) 2011, 2016 Ericsson, É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:
 *   Patrick Tasse - Initial API and implementation
 *   Matthew Khouzam - Added import functionalities
 *   Geneviève Bastien - Added support for experiment types
 *   Bernd Hufmann - Updated custom trace type ID handling
 *******************************************************************************/

package org.eclipse.tracecompass.tmf.core.project.model;

import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeSet;

import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.osgi.util.NLS;
import org.eclipse.tracecompass.internal.tmf.core.Activator;
import org.eclipse.tracecompass.internal.tmf.core.project.model.Messages;
import org.eclipse.tracecompass.tmf.core.TmfCommonConstants;
import org.eclipse.tracecompass.tmf.core.parsers.custom.CustomTxtTrace;
import org.eclipse.tracecompass.tmf.core.parsers.custom.CustomTxtTraceDefinition;
import org.eclipse.tracecompass.tmf.core.parsers.custom.CustomXmlTrace;
import org.eclipse.tracecompass.tmf.core.parsers.custom.CustomXmlTraceDefinition;
import org.eclipse.tracecompass.tmf.core.signal.TmfSignalManager;
import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace;
import org.eclipse.tracecompass.tmf.core.util.Pair;

/**
 * Utility class for accessing TMF trace type extensions from the platform's
 * extensions registry.
 *
 * @author Patrick Tasse
 * @author Matthew Khouzam
 */
public final class TmfTraceType {

    // ------------------------------------------------------------------
    // Constants
    // ------------------------------------------------------------------

    /** Extension point ID */
    public static final String TMF_TRACE_TYPE_ID = "org.eclipse.linuxtools.tmf.core.tracetype"; //$NON-NLS-1$

    /** Extension point element 'Category' */
    public static final String CATEGORY_ELEM = "category"; //$NON-NLS-1$

    /** Extension point element 'Type' */
    public static final String TYPE_ELEM = "type"; //$NON-NLS-1$

    /** Extension point element 'Experiment' */
    public static final String EXPERIMENT_ELEM = "experiment"; //$NON-NLS-1$

    /** Extension point attribute 'ID' */
    public static final String ID_ATTR = "id"; //$NON-NLS-1$

    /** Extension point attribute 'name' */
    public static final String NAME_ATTR = "name"; //$NON-NLS-1$

    /** Extension point attribute 'category' */
    public static final String CATEGORY_ATTR = "category"; //$NON-NLS-1$

    /** Extension point attribute 'trace_type' */
    public static final String TRACE_TYPE_ATTR = "trace_type"; //$NON-NLS-1$

    /** Extension point attribute 'event_type' */
    public static final String EVENT_TYPE_ATTR = "event_type"; //$NON-NLS-1$

    /** Extension point attribute 'experiment_type' */
    public static final String EXPERIMENT_TYPE_ATTR = "experiment_type"; //$NON-NLS-1$

    /** Extension point attribute 'isDirectory' */
    public static final String IS_DIR_ATTR = "isDirectory"; //$NON-NLS-1$

    /** Default experiment type */
    public static final String DEFAULT_EXPERIMENT_TYPE = "org.eclipse.linuxtools.tmf.core.experiment.generic"; //$NON-NLS-1$

    // The mapping of available trace type IDs to their corresponding
    // configuration element
    private static final Map<String, IConfigurationElement> TRACE_TYPE_ATTRIBUTES = new HashMap<>();
    private static final Map<String, IConfigurationElement> TRACE_CATEGORIES = new HashMap<>();
    private static final Map<String, @NonNull TraceTypeHelper> TRACE_TYPES = new LinkedHashMap<>();

    static {
        populateCategoriesAndTraceTypes();
        populateCustomTraceTypes();
        enableTraceTypes();
    }

    /**
     * Enum to say whether a type applies to a trace or experiment
     *
     * @author Geneviève Bastien
     */
    public enum TraceElementType {
        /** Trace type applies to trace */
        TRACE,
        /** Trace type applies to experiment */
        EXPERIMENT,
    }

    // ------------------------------------------------------------------
    // Constructor
    // ------------------------------------------------------------------

    private TmfTraceType() {
    }

    // ------------------------------------------------------------------
    // Operations
    // ------------------------------------------------------------------

    /**
     * Get an iterable view of the existing trace type helpers.
     *
     * @return The currently registered trace type helpers
     */
    public static Iterable<@NonNull TraceTypeHelper> getTraceTypeHelpers() {
        return TRACE_TYPES.values();
    }

    /**
     * Returns a list of trace type labels "category : name", ...
     *
     * Returns only trace types, not experiment types
     *
     * @return a list of trace type labels
     */
    public static String[] getAvailableTraceTypes() {
        return getAvailableTraceTypes(null);
    }

    /**
     * Returns a list of trace type labels "category : name", ... sorted by
     * given comparator.
     *
     * Returns only trace types, not experiment types
     *
     * @param comparator
     *            Comparator class (type String) or null for alphabetical order.
     * @return a list of trace type labels sorted according to the given
     *         comparator
     */
    public static String[] getAvailableTraceTypes(Comparator<String> comparator) {

        // Generate the list of Category:TraceType to populate the ComboBox
        List<String> traceTypes = new ArrayList<>();

        for (TraceTypeHelper tt : getTraceTypeHelpers()) {
            if (tt.isEnabled() && !tt.isExperimentType()) {
                traceTypes.add(tt.getLabel());
            }
        }

        if (comparator == null) {
            Collections.sort(traceTypes);
        } else {
            Collections.sort(traceTypes, comparator);
        }

        // Format result
        return traceTypes.toArray(new String[traceTypes.size()]);
    }

    private static void populateCustomTraceTypes() {
        // add the custom trace types
        for (CustomTxtTraceDefinition def : CustomTxtTraceDefinition.loadAll()) {
            CustomTxtTrace trace = new CustomTxtTrace(def);
            String traceTypeId = trace.getTraceTypeId();
            TraceTypeHelper tt = new TraceTypeHelper(traceTypeId, def.categoryName, def.definitionName, trace, false, TraceElementType.TRACE);
            TRACE_TYPES.put(traceTypeId, tt);
            // Deregister trace as signal handler because it is only used for
            // validation
            TmfSignalManager.deregister(trace);
        }
        for (CustomXmlTraceDefinition def : CustomXmlTraceDefinition.loadAll()) {
            CustomXmlTrace trace = new CustomXmlTrace(def);
            String traceTypeId = trace.getTraceTypeId();
            TraceTypeHelper tt = new TraceTypeHelper(traceTypeId, def.categoryName, def.definitionName, trace, false, TraceElementType.TRACE);
            TRACE_TYPES.put(traceTypeId, tt);
            // Deregister trace as signal handler because it is only used for
            // validation
            TmfSignalManager.deregister(trace);
        }
    }

    /**
     * Add or replace a custom trace type
     *
     * @param traceClass
     *            The custom trace class, either {@link CustomTxtTrace} or
     *            {@link CustomXmlTrace}
     * @param category
     *            The custom parser category
     * @param definitionName
     *            The custom parser definition name to add or replace
     */
    public static void addCustomTraceType(Class<? extends ITmfTrace> traceClass, String category, String definitionName) {
        String traceTypeId = null;
        ITmfTrace trace = null;

        if (traceClass.equals(CustomTxtTrace.class)) {
            CustomTxtTraceDefinition def = CustomTxtTraceDefinition.load(category, definitionName);
            if (def != null) {
                trace = new CustomTxtTrace(def);
                traceTypeId = trace.getTraceTypeId();
            }
        } else if (traceClass.equals(CustomXmlTrace.class)) {
            CustomXmlTraceDefinition def = CustomXmlTraceDefinition.load(category, definitionName);
            if (def != null) {
                trace = new CustomXmlTrace(def);
                traceTypeId = trace.getTraceTypeId();
            }
        }

        if (traceTypeId != null && trace != null) {
            TraceTypeHelper helper = TRACE_TYPES.get(traceTypeId);
            if (helper != null) {
                helper.getTrace().dispose();
            }
            TraceTypeHelper tt = new TraceTypeHelper(traceTypeId, category, definitionName, trace, false, TraceElementType.TRACE);
            TRACE_TYPES.put(traceTypeId, tt);
            // Deregister trace as signal handler because it is only used for
            // validation
            TmfSignalManager.deregister(trace);
        }
    }

    /**
     * Remove a custom trace type
     *
     * @param traceClass
     *            The custom trace class, either {@link CustomTxtTrace} or
     *            {@link CustomXmlTrace}
     * @param category
     *            The custom parser category
     * @param definitionName
     *            The custom parser definition name to add or replace
     */
    public static void removeCustomTraceType(Class<? extends ITmfTrace> traceClass, String category, String definitionName) {
        String traceTypeId = null;
        if (traceClass.equals(CustomTxtTrace.class)) {
            traceTypeId = CustomTxtTrace.buildTraceTypeId(category, definitionName);
        } else if (traceClass.equals(CustomXmlTrace.class)) {
            traceTypeId = CustomXmlTrace.buildTraceTypeId(category, definitionName);
        }
        if (traceTypeId != null) {
            TraceTypeHelper helper = TRACE_TYPES.remove(traceTypeId);
            if (helper != null) {
                helper.getTrace().dispose();
            }
        }
    }

    /**
     * Gets a trace type for a given canonical id
     *
     * @param id
     *            the ID of the trace
     * @return the return type
     */
    public static TraceTypeHelper getTraceType(String id) {
        return TRACE_TYPES.get(id);
    }

    private static void enableTraceTypes() {
        List<String> preferences = TraceTypePreferences.getPreferenceValue();
        TRACE_TYPES.values().forEach(helper -> {
            if (!helper.isExperimentType()) {
                helper.setEnabled(!preferences.contains(helper.getTraceTypeId()));
            }
        });
    }

    private static void populateCategoriesAndTraceTypes() {
        if (TRACE_TYPES.isEmpty()) {
            // Populate the Categories and Trace Types
            IConfigurationElement[] config = Platform.getExtensionRegistry().getConfigurationElementsFor(TmfTraceType.TMF_TRACE_TYPE_ID);
            for (IConfigurationElement ce : config) {
                String elementName = ce.getName();
                if (elementName.equals(TmfTraceType.TYPE_ELEM)) {
                    String traceTypeId = ce.getAttribute(TmfTraceType.ID_ATTR);
                    TRACE_TYPE_ATTRIBUTES.put(traceTypeId, ce);
                } else if (elementName.equals(TmfTraceType.CATEGORY_ELEM)) {
                    String categoryId = ce.getAttribute(TmfTraceType.ID_ATTR);
                    TRACE_CATEGORIES.put(categoryId, ce);
                } else if (elementName.equals(TmfTraceType.EXPERIMENT_ELEM)) {
                    String experimentTypeId = ce.getAttribute(TmfTraceType.ID_ATTR);
                    TRACE_TYPE_ATTRIBUTES.put(experimentTypeId, ce);
                }
            }
            // create the trace types
            for (Entry<String, IConfigurationElement> entry : TRACE_TYPE_ATTRIBUTES.entrySet()) {
                IConfigurationElement ce = entry.getValue();
                final String category = getCategory(ce);
                final String attribute = ce.getAttribute(TmfTraceType.NAME_ATTR);
                ITmfTrace trace = null;
                TraceElementType elementType = TraceElementType.TRACE;
                try {
                    if (ce.getName().equals(TmfTraceType.TYPE_ELEM)) {
                        trace = (ITmfTrace) ce.createExecutableExtension(TmfTraceType.TRACE_TYPE_ATTR);
                    } else if (ce.getName().equals(TmfTraceType.EXPERIMENT_ELEM)) {
                        trace = (ITmfTrace) ce.createExecutableExtension(TmfTraceType.EXPERIMENT_TYPE_ATTR);
                        elementType = TraceElementType.EXPERIMENT;
                    }
                    if (trace == null) {
                        break;
                    }
                    // Deregister trace as signal handler because it is only
                    // used for validation
                    TmfSignalManager.deregister(trace);

                    final String dirString = ce.getAttribute(TmfTraceType.IS_DIR_ATTR);
                    boolean isDir = Boolean.parseBoolean(dirString);

                    final String typeId = entry.getKey();
                    TraceTypeHelper tt = new TraceTypeHelper(typeId, category, attribute, trace, isDir, elementType);
                    TRACE_TYPES.put(typeId, tt);
                } catch (CoreException e) {
                    Activator.logError("Unexpected error during populating trace types", e); //$NON-NLS-1$
                }

            }
        }
    }

    private static String getCategory(IConfigurationElement ce) {
        final String categoryId = ce.getAttribute(TmfTraceType.CATEGORY_ATTR);
        if (categoryId != null) {
            IConfigurationElement category = TRACE_CATEGORIES.get(categoryId);
            if (category != null && !category.getName().equals("")) { //$NON-NLS-1$
                return category.getAttribute(TmfTraceType.NAME_ATTR);
            }
        }
        return ""; //$NON-NLS-1$
    }

    /**
     * Get a configuration element for a given name
     *
     * @param traceType
     *            the name canonical
     * @return the configuration element, can be null
     */
    public static IConfigurationElement getTraceAttributes(String traceType) {
        return TRACE_TYPE_ATTRIBUTES.get(traceType);
    }

    /**
     * Find the id of a trace type by its label "category : name"
     *
     * @param label
     *            the trace type label
     * @return the trace type id
     */
    public static String getTraceTypeId(String label) {
        for (Entry<String, TraceTypeHelper> entry : TRACE_TYPES.entrySet()) {
            if (entry.getValue().getLabel().equals(label)) {
                return entry.getKey();
            }
        }
        return null;
    }

    /**
     * Checks if a trace is a valid directory trace
     *
     * @param path
     *            the file name (and path)
     * @return <code>true</code> if the trace is a valid directory trace else
     *         <code>false</code>
     */
    public static boolean isDirectoryTrace(String path) {
        final Iterable<TraceTypeHelper> traceTypeHelpers = getTraceTypeHelpers();
        for (TraceTypeHelper traceTypeHelper : traceTypeHelpers) {
            if (traceTypeHelper.isDirectoryTraceType() &&
                    (traceTypeHelper.validate(path).getSeverity() != IStatus.ERROR)) {
                return true;
            }
        }
        return false;
    }

    /**
     * @param traceType
     *            the trace type
     * @return <code>true</code> if it is a directory trace type,
     *         <code>false</code> otherwise
     */
    public static boolean isDirectoryTraceType(String traceType) {
        if (traceType != null) {
            TraceTypeHelper traceTypeHelper = getTraceType(traceType);
            if (traceTypeHelper != null) {
                return traceTypeHelper.isDirectoryTraceType();
            }
            return false;
        }
        throw new IllegalArgumentException("Invalid trace type string: " + traceType); //$NON-NLS-1$
    }

    /**
     * Get the trace type id for a resource
     *
     * @param resource
     *            the resource
     * @return the trace type id or null if it is not set
     * @throws CoreException
     *             if the trace type id cannot be accessed
     */
    public static String getTraceTypeId(IResource resource) throws CoreException {
        String traceTypeId = resource.getPersistentProperties().get(TmfCommonConstants.TRACETYPE);
        return buildCompatibilityTraceTypeId(traceTypeId);
    }

    /**
     * This methods builds a trace type ID from a given ID taking into
     * consideration any format changes that were done for the IDs of custom
     * text or XML traces. For example, such format change took place when
     * moving to Trace Compass. Trace type IDs that are part of the plug-in
     * extension for trace types won't be changed.
     *
     * This method is useful for IDs that were persisted in the workspace before
     * the format changes (e.g. in the persistent properties of a trace
     * resource).
     *
     * It ensures backwards compatibility of the workspace for custom text and
     * XML traces.
     *
     * @param traceTypeId
     *            the legacy trace type ID
     * @return the trace type ID in Trace Compass format
     */
    public static String buildCompatibilityTraceTypeId(String traceTypeId) {
        // Fix custom trace type id with old class name or without category name
        // for backward compatibility
        if (traceTypeId != null) {
            String newTraceType = CustomTxtTrace.buildCompatibilityTraceTypeId(traceTypeId);
            if (newTraceType.equals(traceTypeId)) {
                newTraceType = CustomXmlTrace.buildCompatibilityTraceTypeId(traceTypeId);
            }
            return newTraceType;
        }
        return traceTypeId;
    }

    /**
     * This method figures out the trace type of a given trace.
     *
     * @param path
     *            The path of trace to import (file or directory for directory traces)
     * @param traceTypeHint
     *            the ID of a trace (like "o.e.l.specifictrace" )
     * @return a list of {@link TraceTypeHelper} sorted by confidence (highest first)
     *
     * @throws TmfTraceImportException
     *             if there are errors in the trace file or no trace type found
     *             for a directory trace
     * @since 2.0
     */
    public static @NonNull List<TraceTypeHelper> selectTraceType(String path, String traceTypeHint) throws TmfTraceImportException {

        Comparator<Pair<Integer, TraceTypeHelper>> comparator = (o1, o2) -> {
            int res = o2.getFirst().compareTo(o1.getFirst()); // invert so that highest confidence is first
            if (res == 0) {
                res = o1.getSecond().getName().compareTo(o2.getSecond().getName());
            }
            return res;
        };

        TreeSet<Pair<Integer, TraceTypeHelper>> validCandidates = new TreeSet<>(comparator);
        final Iterable<TraceTypeHelper> traceTypeHelpers = TmfTraceType.getTraceTypeHelpers();
        for (TraceTypeHelper traceTypeHelper : traceTypeHelpers) {
            if (!traceTypeHelper.isEnabled() || traceTypeHelper.isExperimentType()) {
                continue;
            }
            int confidence = traceTypeHelper.validateWithConfidence(path);
            if (confidence >= 0) {
                if (traceTypeHelper.getTraceTypeId().equals(traceTypeHint)) {
                    // if the trace type hint is valid, return it immediately
                    return Collections.singletonList(traceTypeHelper);
                }
                // insert in the tree map, ordered by confidence (highest confidence first) then name
                Pair<Integer, TraceTypeHelper> element = new Pair<>(confidence, traceTypeHelper);
                validCandidates.add(element);
            }
        }

        List<TraceTypeHelper> returned = new ArrayList<>();
        if (validCandidates.isEmpty()) {
            File traceFile = new File(path);
            if (traceFile.isFile()) {
                return returned;
            }
            final String errorMsg = NLS.bind(Messages.TmfOpenTraceHelper_NoTraceTypeMatch, path);
            throw new TmfTraceImportException(errorMsg);
        }

        if (validCandidates.size() != 1) {
            List<Pair<Integer, TraceTypeHelper>> reducedCandidates = reduce(validCandidates);
            if (reducedCandidates.isEmpty()) {
                throw new TmfTraceImportException("Error reducing trace type candidates"); //$NON-NLS-1$
            } else if (reducedCandidates.size() == 1) {
                // Don't select the trace type if it has the lowest confidence
                if (reducedCandidates.get(0).getFirst() > 0) {
                    returned.add(reducedCandidates.get(0).getSecond());
                }
            } else {
                for (Pair<Integer, TraceTypeHelper> candidatePair : reducedCandidates) {
                    // Don't select the trace type if it has the lowest confidence
                    if (candidatePair.getFirst() > 0) {
                        returned.add(candidatePair.getSecond());
                    }
                }
            }
        } else {
            // Don't select the trace type if it has the lowest confidence
            if (validCandidates.first().getFirst() > 0) {
                returned.add(validCandidates.first().getSecond());
            }
        }
        return returned;
    }

    private static List<Pair<Integer, TraceTypeHelper>> reduce(Collection<Pair<Integer, TraceTypeHelper>> candidates) {
        List<Pair<Integer, TraceTypeHelper>> retVal = new ArrayList<>();

        // get all the tracetypes that are unique in that stage
        for (Pair<Integer, TraceTypeHelper> candidatePair : candidates) {
            TraceTypeHelper candidate = candidatePair.getSecond();
            if (isUnique(candidate, candidates)) {
                retVal.add(candidatePair);
            }
        }
        return retVal;
    }

    /*
     * Only return the leaves of the trace types. Ignore custom trace types.
     */
    private static boolean isUnique(TraceTypeHelper trace, Collection<Pair<Integer, TraceTypeHelper>> set) {
        if (trace.getTraceClass().equals(CustomTxtTrace.class) ||
                trace.getTraceClass().equals(CustomXmlTrace.class)) {
            return true;
        }
        // check if the trace type is the leaf. we make an instance of the trace
        // type and if it is only an instance of itself, it is a leaf
        final ITmfTrace tmfTrace = trace.getTrace();
        int count = -1;
        for (Pair<Integer, TraceTypeHelper> child : set) {
            final ITmfTrace traceCandidate = child.getSecond().getTrace();
            if (tmfTrace.getClass().isInstance(traceCandidate)) {
                count++;
            }
        }
        return count == 0;
    }

}
