/*******************************************************************************
 * Copyright (c) 2017 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
 *
 * Contributors:
 *   Patrick Tasse - Initial API and implementation
 *******************************************************************************/

package org.eclipse.tracecompass.internal.tmf.ui.markers;

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

import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.graphics.RGBA;
import org.eclipse.tracecompass.internal.tmf.core.markers.IMarkerConstants;
import org.eclipse.tracecompass.internal.tmf.core.markers.Marker;
import org.eclipse.tracecompass.internal.tmf.core.markers.Marker.PeriodicMarker;
import org.eclipse.tracecompass.internal.tmf.core.markers.MarkerSegment;
import org.eclipse.tracecompass.internal.tmf.core.markers.MarkerSet;
import org.eclipse.tracecompass.internal.tmf.core.markers.SubMarker;
import org.eclipse.tracecompass.internal.tmf.core.markers.SubMarker.SplitMarker;
import org.eclipse.tracecompass.internal.tmf.core.markers.SubMarker.WeightedMarker;
import org.eclipse.tracecompass.tmf.core.signal.TmfMarkerEventSourceUpdatedSignal;
import org.eclipse.tracecompass.tmf.core.signal.TmfSignalHandler;
import org.eclipse.tracecompass.tmf.core.signal.TmfSignalManager;
import org.eclipse.tracecompass.tmf.core.trace.AbstractTmfTraceAdapterFactory.IDisposableAdapter;
import org.eclipse.tracecompass.tmf.core.trace.ICyclesConverter;
import org.eclipse.tracecompass.tmf.core.trace.ITmfTrace;
import org.eclipse.tracecompass.tmf.ui.colors.ColorUtils;
import org.eclipse.tracecompass.tmf.ui.markers.IMarkerReferenceProvider;
import org.eclipse.tracecompass.tmf.ui.markers.PeriodicMarkerEventSource;
import org.eclipse.tracecompass.tmf.ui.markers.PeriodicMarkerEventSource.Reference;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.model.IMarkerEvent;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.model.IMarkerEventSource;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.model.MarkerEvent;

import com.google.common.collect.Lists;
import com.google.common.collect.RangeSet;

/**
 * Configurable marker event source.
 */
public class ConfigurableMarkerEventSource implements IMarkerEventSource, IDisposableAdapter {

    private static final long NANO_PER_MILLI = 1000000L;
    private static final long NANO_PER_MICRO = 1000L;
    private static final int MIN_PERIOD = 5; // in units of resolution intervals
    private static final int ALPHA = 10;
    private static final String COLOR_REGEX = "#[A-Fa-f0-9]{6}"; //$NON-NLS-1$

    private List<IConfigurableMarkerEventSource> fMarkerEventSources;
    private Map<Marker, RGBA> fColors = new HashMap<>();
    private final ITmfTrace fTrace;

    /**
     * Constructor
     *
     * @param trace
     *            the trace
     */
    public ConfigurableMarkerEventSource(ITmfTrace trace) {
        fMarkerEventSources = new ArrayList<>();
        fTrace = trace;
        TmfSignalManager.register(this);
    }

    @Override
    public void dispose() {
        TmfSignalManager.deregister(this);
    }

    /**
     * Configure the marker source from the specified marker set
     *
     * @param markerSet
     *            the marker set, or null to clear the configuration
     */
    public void configure(MarkerSet markerSet) {
        fMarkerEventSources.clear();
        if (markerSet != null) {
            for (Marker marker : markerSet.getMarkers()) {
                configure(marker);
            }
        }
    }

    private void configure(Marker marker) {
        if (marker instanceof PeriodicMarker) {
            PeriodicMarker periodicMarker = (PeriodicMarker) marker;
            String referenceId = periodicMarker.getReferenceId();
            Reference baseReference = null;
            if (fTrace instanceof IAdaptable && !referenceId.isEmpty()) {
                @Nullable IMarkerReferenceProvider adapter = ((IAdaptable) fTrace).getAdapter(IMarkerReferenceProvider.class);
                if (adapter != null) {
                    baseReference = adapter.getReference(referenceId);
                }
            }
            if (baseReference == null) {
                baseReference = Reference.ZERO;
            }
            long rollover = periodicMarker.getRange().hasUpperBound() ? (periodicMarker.getRange().upperEndpoint() - periodicMarker.getRange().lowerEndpoint() + 1) : 0;
            RGBA evenColor = getColor(periodicMarker);
            RGBA oddColor = getOddColor(evenColor);
            double period = convertToNanos(periodicMarker.getPeriod(), periodicMarker.getUnit());
            Reference reference = new Reference(baseReference.getTime() + Math.round(convertToNanos(periodicMarker.getOffset(), periodicMarker.getUnit())), baseReference.getIndex());
            ConfigurablePeriodicMarkerEventSource markerEventSource = new ConfigurablePeriodicMarkerEventSource(marker, checkNotNull(periodicMarker.getName()), reference, period, rollover, evenColor, oddColor, false, periodicMarker.getRange().lowerEndpoint(), checkNotNull(periodicMarker.getLabel()), periodicMarker.getIndexRange());
            fMarkerEventSources.add(markerEventSource);
        }
    }

    private double convertToNanos(double number, String unit) {
        if (unit.equalsIgnoreCase(IMarkerConstants.MS)) {
            return number * NANO_PER_MILLI;
        } else if (unit.equalsIgnoreCase(IMarkerConstants.US)) {
            return number * NANO_PER_MICRO;
        } else if (unit.equalsIgnoreCase(IMarkerConstants.NS)) {
            return number;
        } else if (unit.equalsIgnoreCase(IMarkerConstants.CYCLES) &&
                fTrace instanceof IAdaptable) {
            ICyclesConverter adapter = ((IAdaptable) fTrace).getAdapter(ICyclesConverter.class);
            if (adapter != null) {
                return adapter.cyclesToNanos((long) number);
            }
        }
        return number;
    }

    private @NonNull RGBA getColor(Marker marker) {
        RGBA color = fColors.get(marker);
        if (color == null) {
            color = parseColor(marker.getColor());
            fColors.put(marker, color);
        }
        return color;
    }

    private static @NonNull RGBA getOddColor(RGBA color) {
        return new RGBA(color.rgb.red, color.rgb.green, color.rgb.blue, 0);
    }

    private static @NonNull RGBA parseColor(String color) {
        RGB rgb = null;
        if (color.matches(COLOR_REGEX)) {
            rgb = ColorUtils.fromHexColor(color);
        } else {
            rgb = ColorUtils.fromX11Color(color);
            if (rgb == null) {
                rgb = new RGB(0, 0, 0);
            }
        }
        return new RGBA(rgb.red, rgb.green, rgb.blue, ALPHA);
    }

    @Override
    public List<String> getMarkerCategories() {
        Set<String> categories = new LinkedHashSet<>();
        for (IConfigurableMarkerEventSource source : fMarkerEventSources) {
            categories.addAll(source.getMarkerCategories());
            getSubMarkerCategories(categories, source.getSubMarkers());
        }
        return checkNotNull(Lists.newArrayList(categories));
    }

    private void getSubMarkerCategories(Set<String> categories, List<SubMarker> subMarkers) {
        for (SubMarker subMarker : subMarkers) {
            categories.add(subMarker.getName());
            getSubMarkerCategories(categories, subMarker.getSubMarkers());
            if (subMarker instanceof WeightedMarker) {
                for (MarkerSegment segment : ((WeightedMarker) subMarker).getSegments()) {
                    getSubMarkerCategories(categories, segment.getSubMarkers());
                }
            }
        }
    }

    @Override
    public List<IMarkerEvent> getMarkerList(String category, long startTime, long endTime, long resolution, IProgressMonitor monitor) {
        return getMarkerList(startTime, endTime, resolution, monitor).stream()
                .filter((marker) -> marker.getCategory().equals(category))
                .collect(Collectors.toList());
    }

    @Override
    public List<IMarkerEvent> getMarkerList(long startTime, long endTime, long resolution, IProgressMonitor monitor) {
        @NonNull List<@NonNull IMarkerEvent> markerList = new ArrayList<>();
        for (IConfigurableMarkerEventSource source : fMarkerEventSources) {
            long minDuration = resolution * MIN_PERIOD;
            if (source.getMaxDuration() > minDuration) {
                @NonNull List<@NonNull IMarkerEvent> list = source.getMarkerList(startTime, endTime, resolution, monitor);
                for (IMarkerEvent markerEvent : list) {
                    for (SubMarker subMarker : source.getSubMarkers()) {
                        getSubMarkerList(subMarker, markerEvent, markerList, startTime, endTime, minDuration);
                    }
                    markerList.add(markerEvent);
                }
            }
        }
        markerList.sort(Comparator.comparingLong(marker -> marker.getTime()));
        return markerList;
    }

    private void getSubMarkerList(SubMarker subMarker, IMarkerEvent markerEvent, @NonNull List<@NonNull IMarkerEvent> markerList, long startTime, long endTime, long minDuration) {
        if (subMarker instanceof SplitMarker) {
            getSubMarkerList((SplitMarker) subMarker, markerEvent, markerList, startTime, endTime, minDuration);
        } else if (subMarker instanceof WeightedMarker) {
            getSubMarkerList((WeightedMarker) subMarker, markerEvent, markerList, startTime, endTime, minDuration);
        }
    }

    private void getSubMarkerList(SplitMarker splitMarker, IMarkerEvent markerEvent, @NonNull List<@NonNull IMarkerEvent> markerList, long startTime, long endTime, long minDuration) {
        if (markerEvent.getTime() > endTime || markerEvent.getTime() + markerEvent.getDuration() < startTime) {
            return;
        }
        long lower = splitMarker.getRange().lowerEndpoint();
        long upper = splitMarker.getRange().upperEndpoint();
        long segments = upper - lower + 1;
        long start = markerEvent.getTime();
        for (int i = 0; i < segments; i++) {
            long end = markerEvent.getTime() + Math.round((double) (i + 1) / segments * markerEvent.getDuration());
            long duration = end - start;
            long labelIndex = lower + i;
            if (end >= startTime && duration > minDuration && splitMarker.getIndexRange().contains(labelIndex)) {
                RGBA color = (labelIndex & 1) == 0 ? getColor(splitMarker) : getOddColor(getColor(splitMarker));
                IMarkerEvent subMarkerEvent = new MarkerEvent(null, start, end - start, splitMarker.getName(), color, String.format(splitMarker.getLabel(), labelIndex), false);
                for (SubMarker subMarker : splitMarker.getSubMarkers()) {
                    getSubMarkerList(subMarker, subMarkerEvent, markerList, startTime, endTime, minDuration);
                }
                markerList.add(subMarkerEvent);
            }
            if (start >= endTime) {
                break;
            }
            start = end;
        }
    }

    private void getSubMarkerList(WeightedMarker weightedMarker, IMarkerEvent markerEvent, @NonNull List<@NonNull IMarkerEvent> markerList, long startTime, long endTime, long minDuration) {
        if (markerEvent.getTime() > endTime || markerEvent.getTime() + markerEvent.getDuration() < startTime) {
            return;
        }
        long start = markerEvent.getTime();
        long length = 0;
        for (int i = 0; i < weightedMarker.getSegments().size(); i++) {
            MarkerSegment segment = weightedMarker.getSegments().get(i);
            length += segment.getLength();
            long end = markerEvent.getTime() + Math.round((length / (double) weightedMarker.getTotalLength()) * markerEvent.getDuration());
            long duration = end - start;
            if (end >= startTime && duration > minDuration && !segment.getColor().isEmpty()) {
                RGBA color = getColor(segment);
                IMarkerEvent subMarkerEvent = new MarkerEvent(null, start, end - start, weightedMarker.getName(), color, String.format(segment.getLabel(), i), false);
                for (SubMarker subMarker : segment.getSubMarkers()) {
                    getSubMarkerList(subMarker, subMarkerEvent, markerList, startTime, endTime, minDuration);
                }
                for (SubMarker subMarker : weightedMarker.getSubMarkers()) {
                    getSubMarkerList(subMarker, subMarkerEvent, markerList, startTime, endTime, minDuration);
                }
                markerList.add(subMarkerEvent);
            }
            if (start >= endTime) {
                break;
            }
            start = end;
        }
    }

    private static interface IConfigurableMarkerEventSource extends IMarkerEventSource {
        double getMaxDuration();

        public List<SubMarker> getSubMarkers();
    }

    private class ConfigurablePeriodicMarkerEventSource extends PeriodicMarkerEventSource implements IConfigurableMarkerEventSource {

        private final Marker fMarker;
        private final long fStartIndex;
        private final String fLabel;
        private final RangeSet<Long> fIndexRange;
        private final double fMaxDuration;

        public ConfigurablePeriodicMarkerEventSource(Marker marker, @NonNull String category, @NonNull Reference reference, double period, long rollover, @NonNull RGBA evenColor, @NonNull RGBA oddColor, boolean foreground, long startIndex, @NonNull String label, RangeSet<Long> indexRange) {
            super(category, reference, period, rollover, evenColor, oddColor, foreground);
            fMarker = marker;
            fStartIndex = startIndex;
            fLabel = label;
            fIndexRange = indexRange;
            fMaxDuration = period;
        }

        @Override
        public @NonNull String getMarkerLabel(long index) {
            return checkNotNull(String.format(fLabel, fStartIndex + index));
        }

        @Override
        public boolean isApplicable(long index) {
            if (fIndexRange != null) {
                return fIndexRange.contains(fStartIndex + index);
            }
            return true;
        }

        @Override
        public double getMaxDuration() {
            return fMaxDuration;
        }

        @Override
        public List<SubMarker> getSubMarkers() {
            return fMarker.getSubMarkers();
        }
    }

    /**
     * A marker event source has been updated
     *
     * @param signal
     *            the signal
     */
    @TmfSignalHandler
    public void markerEventSourceUpdated(final TmfMarkerEventSourceUpdatedSignal signal) {
        configure(MarkerUtils.getDefaultMarkerSet());
    }
}
