/**********************************************************************
 * Copyright (c) 2017, 2018 Ericsson
 *
 * 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
 **********************************************************************/

package org.eclipse.tracecompass.internal.analysis.counters.core;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.NavigableSet;
import java.util.Objects;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.tracecompass.analysis.counters.core.CounterAnalysis;
import org.eclipse.tracecompass.internal.tmf.core.model.filters.FetchParametersUtils;
import org.eclipse.tracecompass.internal.tmf.core.model.xy.AbstractTreeCommonXDataProvider;
import org.eclipse.tracecompass.statesystem.core.ITmfStateSystem;
import org.eclipse.tracecompass.statesystem.core.exceptions.StateSystemDisposedException;
import org.eclipse.tracecompass.statesystem.core.interval.ITmfStateInterval;
import org.eclipse.tracecompass.tmf.core.dataprovider.DataProviderParameterUtils;
import org.eclipse.tracecompass.tmf.core.model.YModel;
import org.eclipse.tracecompass.tmf.core.model.filters.SelectedCounterQueryFilter;
import org.eclipse.tracecompass.tmf.core.model.tree.TmfTreeDataModel;
import org.eclipse.tracecompass.tmf.core.model.tree.TmfTreeModel;
import org.eclipse.tracecompass.tmf.core.model.xy.IYModel;
import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace;
import org.eclipse.tracecompass.tmf.core.trace.experiment.TmfExperiment;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
import com.google.common.collect.TreeMultimap;

/**
 * This data provider will return a XY model (model is wrapped in a response)
 * based on a query filter. The model is used afterwards by any viewer to draw
 * charts. Model returned is for Counters views
 *
 * @author Mikael Ferland
 * @author Yonni Chen
 * @since 1.1
 */
public class CounterDataProvider extends AbstractTreeCommonXDataProvider<CounterAnalysis, TmfTreeDataModel> {

    /**
     * This data provider's extension point ID
     */
    public static final String ID = "org.eclipse.tracecompass.analysis.counters.core.CounterDataProvider"; //$NON-NLS-1$

    /**
     * Cumulative key to extract isCumulative from parameters map
     */
    public static final String CUMULATIVE_COUNTER_KEY = "isCumulative"; //$NON-NLS-1$

    /**
     * Chart's title
     */
    private static final String TITLE = Objects.requireNonNull(Messages.CounterDataProvider_ChartTitle);

    /**
     * Create an instance of {@link CounterDataProvider}. Returns a null instance if
     * the analysis module is not found.
     *
     * @param trace
     *            A trace on which we are interested to fetch a model
     * @param module
     *            A CounterAnalysis instance
     * @return A {@link CounterDataProvider} instance. If analysis module is not
     *         found, it returns null
     */
    public static @Nullable CounterDataProvider create(ITmfTrace trace, @Nullable CounterAnalysis module) {
        if (trace instanceof TmfExperiment) {
            throw new UnsupportedOperationException("This data providers does not support experiment"); //$NON-NLS-1$
        }

        if (module != null) {
            module.schedule();
            return new CounterDataProvider(trace, module);
        }
        return null;
    }

    /**
     * Constructor
     */
    private CounterDataProvider(ITmfTrace trace, CounterAnalysis analysis) {
        super(trace, analysis);
    }

    /**
     * @since 1.2
     */
    @Override
    protected TmfTreeModel<TmfTreeDataModel> getTree(ITmfStateSystem ss, Map<String, Object> parameters, @Nullable IProgressMonitor monitor) {
        List<TmfTreeDataModel> entries = new ArrayList<>();
        long rootId = getId(ITmfStateSystem.ROOT_ATTRIBUTE);
        entries.add(new TmfTreeDataModel(rootId, -1, Collections.singletonList(getTrace().getName())));

        addTreeViewerBranch(ss, rootId, Collections.singletonList(CounterAnalysis.GROUPED_COUNTER_ASPECTS_ATTRIB), entries);
        addTreeViewerBranch(ss, rootId, Collections.singletonList(CounterAnalysis.UNGROUPED_COUNTER_ASPECTS_ATTRIB), entries);

        return new TmfTreeModel<>(Collections.emptyList(), entries);
    }

    private void addTreeViewerBranch(ITmfStateSystem ss, long parentId, List<String> branchName, List<TmfTreeDataModel> entries) {
        int quark = ss.optQuarkAbsolute(branchName.get(0));
        if (quark != ITmfStateSystem.INVALID_ATTRIBUTE && !ss.getSubAttributes(quark, false).isEmpty()) {
            long id = getId(quark);
            TmfTreeDataModel branch = new TmfTreeDataModel(id, parentId, branchName);
            entries.add(branch);
            addTreeViewerEntries(ss, id, quark, entries);
        }
    }

    /**
     * Recursively add all child entries of a parent branch from the state system.
     */
    private void addTreeViewerEntries(ITmfStateSystem ss, long parentId, int quark, List<TmfTreeDataModel> entries) {
        for (int childQuark : ss.getSubAttributes(quark, false)) {
            long id = getId(childQuark);
            TmfTreeDataModel childBranch = new TmfTreeDataModel(id, parentId, Collections.singletonList(ss.getAttributeName(childQuark)));
            entries.add(childBranch);
            addTreeViewerEntries(ss, id, childQuark, entries);
        }
    }

    @Deprecated
    @Override
    protected @Nullable Map<String, IYModel> getYModels(ITmfStateSystem ss, Map<String, Object> fetchParameters, @Nullable IProgressMonitor monitor) throws StateSystemDisposedException {
        return Maps.uniqueIndex(getYSeriesModels(ss, fetchParameters, monitor), IYModel::getName);
    }

    /**
     * @since 1.2
     */
    @Override
    protected @Nullable Collection<IYModel> getYSeriesModels(ITmfStateSystem ss, Map<String, Object> fetchParameters,
            @Nullable IProgressMonitor monitor) throws StateSystemDisposedException {
        // Check if the parameters contain timeRequested and selectedItems
        // isCumulative will be compute later
        if (FetchParametersUtils.createSelectionTimeQuery(fetchParameters) != null) {
            return internalFetch(ss, fetchParameters, monitor);
        }
        return Collections.emptyList();
    }

    private @Nullable Collection<IYModel> internalFetch(ITmfStateSystem ss, Map<String, Object> fetchParameters,
            @Nullable IProgressMonitor monitor) throws StateSystemDisposedException {
        SelectedCounterQueryFilter filter = createCounterQuery(fetchParameters);
        if (filter == null) {
            return null;
        }
        long stateSystemEndTime = ss.getCurrentEndTime();
        Collection<Long> times = extractRequestedTimes(ss, filter, stateSystemEndTime);

        Map<Long, Integer> entries = Maps.filterValues(getSelectedEntries(filter), q -> ss.getSubAttributes(q, false).isEmpty());

        TreeMultimap<Integer, ITmfStateInterval> countersIntervals = TreeMultimap.create(Comparator.naturalOrder(),
                Comparator.comparingLong(ITmfStateInterval::getStartTime));

        Iterable<@NonNull ITmfStateInterval> query2d = ss.query2D(entries.values(), times);
        for (ITmfStateInterval interval : query2d) {
            if (monitor != null && monitor.isCanceled()) {
                return null;
            }
            countersIntervals.put(interval.getAttribute(), interval);
        }

        ImmutableList.Builder<IYModel> ySeries = ImmutableList.builder();
        for (Entry<Long, Integer> entry : entries.entrySet()) {
            if (monitor != null && monitor.isCanceled()) {
                return null;
            }
            int quark = entry.getValue();
            double[] yValues = buildYValues(countersIntervals.get(quark), filter);
            String seriesName = getTrace().getName() + '/' + ss.getFullAttributePath(quark);
            ySeries.add(new YModel(entry.getKey(), seriesName, yValues));
        }

        return ySeries.build();
    }

    private static @Nullable SelectedCounterQueryFilter createCounterQuery(Map<String, Object> parameters) {
        List<Long> timeRequested = DataProviderParameterUtils.extractTimeRequested(parameters);
        List<Long> selectedItems = DataProviderParameterUtils.extractSelectedItems(parameters);

        if (timeRequested == null || selectedItems == null) {
            return null;
        }

        Boolean isCumulativeParameter = DataProviderParameterUtils.extractBoolean(parameters, CUMULATIVE_COUNTER_KEY);
        // If the cumulative parameter is not present in the parameters use
        // "false" as default value
        boolean isCumulative = isCumulativeParameter != null && isCumulativeParameter;
        return new SelectedCounterQueryFilter(timeRequested, selectedItems, isCumulative);
    }

    /**
     * Extracts an array of times that will be used for a 2D query. It extracts the
     * times based on the state system bounds, the requested time of the query
     * filter and the current end time
     *
     * @param ss
     *            The state system
     * @param filter
     *            The query filter
     * @param currentEndTime
     *            We want to make sure that current end time is consistent
     *            throughout the query even if the analysis progresses
     * @return A collection of time
     */
    private static Collection<Long> extractRequestedTimes(ITmfStateSystem ss, SelectedCounterQueryFilter filter, long currentEndTime) {
        Collection<Long> times = getTimes(filter, ss.getStartTime(), currentEndTime);

        long[] xValues = filter.getTimesRequested();
        long queryStart = filter.getStart();
        long stateSystemStartTime = ss.getStartTime();

        /* We only need the previous time for differential mode */
        if (!filter.isCumulative() && xValues.length > 1) {
            /*
             * For differential mode, we need to get the time before query start. To do so,
             * we subtract to query start the delta between xValues[1] and query start
             */
            long prevTime = Long.max(stateSystemStartTime, 2 * queryStart - xValues[1]);
            if (prevTime <= currentEndTime) {
                times.add(prevTime);
            }
        }

        return times;
    }

    private static double[] buildYValues(NavigableSet<ITmfStateInterval> countersIntervals, SelectedCounterQueryFilter filter) {

        long[] times = filter.getTimesRequested();
        boolean isCumulative = filter.isCumulative();

        double[] yValues = new double[times.length];
        long prevValue = 0L;
        if (!countersIntervals.isEmpty()) {
            Object value = countersIntervals.first().getValue();
            if (value instanceof Number) {
                prevValue = ((Number) value).longValue();
            }
        }
        int to = 0;

        for (ITmfStateInterval interval : countersIntervals) {
            int from = Arrays.binarySearch(times, interval.getStartTime());
            from = (from >= 0) ? from : -1 - from;
            Number value = (Number) interval.getValue();
            long l = value != null ? value.longValue() : 0l;
            if (isCumulative) {
                /* Fill in all the time stamps that the interval overlaps */
                to = Arrays.binarySearch(times, interval.getEndTime());
                to = (to >= 0) ? to + 1 : -1 - to;
                Arrays.fill(yValues, from, to, l);
            } else {
                yValues[from] = (l - prevValue);
            }
            prevValue = l;
        }

        /* Fill the time stamps after the state system, if any. */
        if (isCumulative) {
            Arrays.fill(yValues, to, yValues.length, prevValue);
        }

        return yValues;
    }

    @Override
    public String getTitle() {
        return TITLE;
    }

    @Override
    public String getId() {
        return ID;
    }

    /**
     * @since 1.2
     */
    @Override
    protected boolean isCacheable() {
        return true;
    }

}