/**********************************************************************
 * Copyright (c) 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.tmf.core.histogram;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicLong;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.tracecompass.internal.tmf.core.model.AbstractTmfTraceDataProvider;
import org.eclipse.tracecompass.internal.tmf.core.model.TmfXyResponseFactory;
import org.eclipse.tracecompass.internal.tmf.core.model.filters.FetchParametersUtils;
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.model.CommonStatusMessage;
import org.eclipse.tracecompass.tmf.core.model.YModel;
import org.eclipse.tracecompass.tmf.core.model.filters.SelectionTimeQueryFilter;
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.ITmfTreeXYDataProvider;
import org.eclipse.tracecompass.tmf.core.model.xy.ITmfXyModel;
import org.eclipse.tracecompass.tmf.core.model.xy.IYModel;
import org.eclipse.tracecompass.tmf.core.response.ITmfResponse;
import org.eclipse.tracecompass.tmf.core.response.TmfModelResponse;
import org.eclipse.tracecompass.tmf.core.statistics.ITmfStatistics;
import org.eclipse.tracecompass.tmf.core.statistics.TmfStateStatistics.Attributes;
import org.eclipse.tracecompass.tmf.core.statistics.TmfStatisticsEventTypesModule;
import org.eclipse.tracecompass.tmf.core.statistics.TmfStatisticsModule;
import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableList.Builder;
import com.google.common.collect.Lists;

/**
 * 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 the new Histogram viewer
 *
 * @author Yonni Chen
 * @since 4.0
 */
public class HistogramDataProvider extends AbstractTmfTraceDataProvider implements ITmfTreeXYDataProvider<TmfTreeDataModel> {

    /**
     * Extension point ID.
     */
    public static final String ID = "org.eclipse.tracecompass.internal.tmf.core.histogram.HistogramDataProvider"; //$NON-NLS-1$
    static final String TITLE = Objects.requireNonNull(Messages.HistogramDataProvider_Title);
    private static final AtomicLong TRACE_IDS = new AtomicLong();

    private final TmfStatisticsModule fModule;
    private @Nullable TmfModelResponse<TmfTreeModel<TmfTreeDataModel>> fCached = null;
    private final long fTraceId = TRACE_IDS.getAndIncrement();
    private final long fTotalId = TRACE_IDS.getAndIncrement();
    private final long fLostId = TRACE_IDS.getAndIncrement();

    /**
     * Constructor
     *
     * @param trace
     *            A trace on which we are interested to fetch a model
     * @param module
     *            the {@link TmfStatisticsModule} for the trace.
     */
    public HistogramDataProvider(ITmfTrace trace, TmfStatisticsModule module) {
        super(trace);
        fModule = module;
    }

    @Override
    public TmfModelResponse<TmfTreeModel<TmfTreeDataModel>> fetchTree(Map<String, Object> fetchParameters, @Nullable IProgressMonitor monitor) {
        if (fCached != null) {
            return fCached;
        }
        fModule.waitForInitialization();
        Builder<TmfTreeDataModel> builder = ImmutableList.builder();
        builder.add(new TmfTreeDataModel(fTraceId, -1, Collections.singletonList(getTrace().getName())));
        builder.add(new TmfTreeDataModel(fTotalId, fTraceId, Collections.singletonList(Objects.requireNonNull(Messages.HistogramDataProvider_Total))));
        ITmfStateSystem eventsSs = Objects.requireNonNull(fModule.getStateSystem(TmfStatisticsEventTypesModule.ID));
        if (eventsSs.optQuarkAbsolute(Attributes.LOST_EVENTS) != ITmfStateSystem.INVALID_ATTRIBUTE) {
            builder.add(new TmfTreeDataModel(fLostId, fTraceId, Collections.singletonList(Objects.requireNonNull(Messages.HistogramDataProvider_Lost))));
        }
        if (eventsSs.waitUntilBuilt(0)) {
            TmfModelResponse<TmfTreeModel<TmfTreeDataModel>> response = new TmfModelResponse<>(new TmfTreeModel<>(Collections.emptyList(), builder.build()), ITmfResponse.Status.COMPLETED, CommonStatusMessage.COMPLETED);
            fCached = response;
            return response;
        }
        return new TmfModelResponse<>(new TmfTreeModel<>(Collections.emptyList(), builder.build()), ITmfResponse.Status.RUNNING, CommonStatusMessage.RUNNING);
    }

    @Override
    public @NonNull TmfModelResponse<ITmfXyModel> fetchXY(Map<String, Object> fetchParameters, @Nullable IProgressMonitor monitor) {
        fModule.waitForInitialization();
        SelectionTimeQueryFilter filter = FetchParametersUtils.createSelectionTimeQuery(fetchParameters);
        long[] xValues = new long[0];
        if (filter == null) {
            return TmfXyResponseFactory.create(TITLE, xValues, Collections.emptyList(), true);
        }
        xValues = filter.getTimesRequested();

        Collection<Long> selected = filter.getSelectedItems();
        int n = xValues.length;
        ImmutableList.Builder<IYModel> builder = ImmutableList.builder();

        final ITmfStatistics stats = Objects.requireNonNull(fModule.getStatistics());
        if (selected.contains(fTotalId)) {
            List<Long> values = stats.histogramQuery(filter.getTimesRequested());

            double[] y = new double[n];
            Arrays.setAll(y, values::get);
            String totalName = getTrace().getName() + '/' + Messages.HistogramDataProvider_Total;
            builder.add(new YModel(fTotalId, totalName, y));
        }

        ITmfStateSystem eventsSs = fModule.getStateSystem(TmfStatisticsEventTypesModule.ID);
        if (selected.contains(fLostId) && eventsSs != null) {
            try {
                YModel series = getLostEvents(eventsSs, xValues);
                builder.add(series);
            } catch (StateSystemDisposedException e) {
                return TmfXyResponseFactory.createFailedResponse(CommonStatusMessage.STATE_SYSTEM_FAILED);
            }
        }
        boolean completed = eventsSs != null ? eventsSs.waitUntilBuilt(0) || eventsSs.getCurrentEndTime() >= filter.getEnd() : false;

        return TmfXyResponseFactory.create(TITLE, xValues, builder.build(), completed);
    }

    private YModel getLostEvents(ITmfStateSystem ss, long[] times) throws StateSystemDisposedException {
        int leEndQuark = ss.optQuarkAbsolute(Attributes.LOST_EVENTS);
        int leCountQuark = ss.optQuarkAbsolute(Attributes.EVENT_TYPES, "Lost event"); //$NON-NLS-1$
        long step = (times[times.length - 1] - times[0]) / times.length;
        long lastTime = times[times.length - 1];
        long firstTime = times[0];
        double[] leY = new double[times.length];

        long t = firstTime;
        if (ss.getStartTime() <= t) {
            List<ITmfStateInterval> sortedEndIntervals = Lists.newArrayList(ss.query2D(Collections.singleton(leEndQuark), firstTime, lastTime));
            sortedEndIntervals.sort(Comparator.comparing(ITmfStateInterval::getStartTime));
            List<ITmfStateInterval> sortedCountIntervals = Lists.newArrayList(ss.query2D(Collections.singleton(leCountQuark), firstTime, lastTime));
            sortedCountIntervals.sort(Comparator.comparing(ITmfStateInterval::getStartTime));

            Iterator<ITmfStateInterval> endTimeIter = sortedEndIntervals.iterator();
            List<LostEventInterval> lostEventIntervals = new ArrayList<>();
            ITmfStateInterval endTimeInterval = endTimeIter.next();
            for (ITmfStateInterval lostCountInterval : sortedCountIntervals) {
                while (!endTimeInterval.intersects(lostCountInterval.getStartTime())) {
                    if (!endTimeIter.hasNext()) {
                        throw new IllegalStateException();
                    }
                    endTimeInterval = endTimeIter.next();
                }
                Object endTime = endTimeInterval.getValue();
                Object lostCount = lostCountInterval.getValue();
                if (endTime instanceof Number && lostCount instanceof Number) {
                    lostEventIntervals.add(new LostEventInterval(endTimeInterval.getStartTime(), ((Number) endTime).longValue(), ((Number) lostCount).longValue()));
                }
            }

            for (int i = 0; i < times.length - 2; i++) {
                int intervalIndex;
                boolean intersect = false;
                for (intervalIndex = 0; intervalIndex < lostEventIntervals.size(); intervalIndex++) {
                    if (lostEventIntervals.get(intervalIndex).intersects(times[i], times[i + 1])) {
                        intersect = true;
                        break;
                    }
                }

                if (intersect) {
                    LostEventInterval lostEventInterval = lostEventIntervals.get(intervalIndex);
                    long prevCount = 0;
                    if (intervalIndex - 1 >= 0) {
                        prevCount = lostEventIntervals.get(intervalIndex - 1).getLostEventCount();
                    }

                    long lostEventCount = lostEventInterval.getLostEventCount();
                    double yValue = step * (double) (lostEventCount - prevCount) / (lostEventInterval.getEndTime() - lostEventInterval.getStartTime());
                    if (yValue > lostEventCount) {
                        yValue = (double) lostEventCount - prevCount;
                    }
                    leY[i] = yValue;
                }
            }
        }
        String lostName = getTrace().getName() + '/' + Messages.HistogramDataProvider_Lost;
        return new YModel(fLostId, lostName, leY);
    }

    private class LostEventInterval {
        private final long fStartTime;
        private final long fEndTime;
        private final long fLostEventCount;

        public LostEventInterval(long startTime, long endTime, long lostEventCount) {
            fStartTime = startTime;
            fEndTime = endTime;
            fLostEventCount = lostEventCount;
        }

        public long getEndTime() {
            return fEndTime;
        }

        public long getStartTime() {
            return fStartTime;
        }

        public long getLostEventCount() {
            return fLostEventCount;
        }

        public boolean intersects(long start, long end) {
            return start <= fEndTime && end >= fStartTime;
        }
    }

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