/*******************************************************************************
 * Copyright (c) 2014, 2019 École Polytechnique de Montréal and others.
 *
 * 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:
 *   Florian Wininger - Initial API and implementation
 *   Geneviève Bastien - Review of the initial implementation
 *******************************************************************************/

package org.eclipse.tracecompass.internal.tmf.analysis.xml.ui.views.timegraph;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.widgets.Display;
import org.eclipse.tracecompass.internal.tmf.analysis.xml.core.output.DataDrivenOutputEntryModel;
import org.eclipse.tracecompass.tmf.analysis.xml.core.module.TmfXmlStrings;
import org.eclipse.tracecompass.tmf.analysis.xml.core.module.TmfXmlUtils;
import org.eclipse.tracecompass.tmf.core.model.tree.ITmfTreeDataModel;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.StateItem;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.TimeGraphPresentationProvider;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.model.ITimeEvent;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.model.NullTimeEvent;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.model.TimeEvent;
import org.eclipse.tracecompass.tmf.ui.widgets.timegraph.model.TimeGraphEntry;
import org.w3c.dom.Element;

import com.google.common.primitives.Ints;

/**
 * Presentation provider for the XML view, based on the generic TMF presentation
 * provider.
 *
 * TODO: This should support colors/states defined for each entry element in the
 * XML element.
 *
 * @author Florian Wininger
 */
public class XmlPresentationProvider extends TimeGraphPresentationProvider {

    private static final long[] COLOR_SEED = { 0x0000ff, 0xff0000, 0x00ff00,
            0xff00ff, 0x00ffff, 0xffff00, 0x000000, 0xf07300
    };

    private static final int COLOR_MASK = 0xffffff;

    private List<StateItem> stateValues = new ArrayList<>();
    /*
     * Maps the value of an event with the corresponding index in the
     * stateValues list
     */
    private Map<Integer, Integer> stateIndex = new HashMap<>();
    private StateItem[] stateTable = new StateItem[0];

    private final String fId;

    /**
     * Presentation provider constructor
     *
     * @param id
     *               the id of the generating analysis. This is needed since each
     *               xml view has the same presentation provider, so this can
     *               differentiate them.
     */
    public XmlPresentationProvider(String id) {
        fId = id;
    }

    @Override
    public int getStateTableIndex(ITimeEvent event) {
        if (event instanceof NullTimeEvent) {
            return INVISIBLE;
        }
        if (event instanceof TimeEvent && ((TimeEvent) event).hasValue()) {
            TimeEvent tcEvent = (TimeEvent) event;

            TimeGraphEntry entry = (TimeGraphEntry) event.getEntry();
            int value = tcEvent.getValue();

            ITmfTreeDataModel model = entry.getEntryModel();
            if (model instanceof DataDrivenOutputEntryModel) {
                // Draw state only if state is already known
                Integer index = stateIndex.get(value);
                if (index != null) {
                    return index;
                }
            }
        }
        return TRANSPARENT;
    }

    @Override
    public StateItem[] getStateTable() {
        return stateTable;
    }

    @Override
    public String getEventName(ITimeEvent event) {
        if (event instanceof TimeEvent && ((TimeEvent) event).hasValue()) {
            TimeEvent tcEvent = (TimeEvent) event;

            DataDrivenOutputEntryModel model = (DataDrivenOutputEntryModel) ((TimeGraphEntry) event.getEntry()).getEntryModel();
            int value = tcEvent.getValue();

            if (model.getDisplayQuark() >= 0) {
                Integer index = stateIndex.get(value);
                if (index != null) {
                    return stateValues.get(index.intValue()).getStateString();
                }
            }
            return null;
        }
        return Messages.XmlPresentationProvider_MultipleStates;
    }

    @Override
    public Map<String, String> getEventHoverToolTipInfo(ITimeEvent event, long hoverTime) {
        /*
         * TODO: Add the XML elements to support adding extra information in the
         * tooltips and implement this
         */
        return Collections.emptyMap();
    }

    /**
     * Loads the states from a {@link TmfXmlStrings#TIME_GRAPH_VIEW} XML
     * element
     *
     * @param viewElement
     *            The XML view element
     * @return A map of string values loaded and their corresponding numerical value
     */
    public synchronized Map<String, Integer> loadNewStates(@NonNull Element viewElement) {
        Map<String, Integer> map = new HashMap<>();
        stateValues.clear();
        stateIndex.clear();
        List<Element> states = TmfXmlUtils.getChildElements(viewElement, TmfXmlStrings.DEFINED_VALUE);

        for (Element state : states) {
            String valueStr = state.getAttribute(TmfXmlStrings.VALUE);
            Integer value = Ints.tryParse(valueStr);
            String name = state.getAttribute(TmfXmlStrings.NAME);
            if (value == null) {
                // find a numerical value for this one
                int innerVal = 10000;
                while (stateIndex.get(innerVal) != null) {
                    innerVal++;
                }
                value = innerVal;
                //  FIXME: We will use the value as the name, as this is how the colors work, when value is a string, the name attribute will be ignored.
                name = valueStr;
                map.put(name, value);
            }
            String color = state.getAttribute(TmfXmlStrings.COLOR);
            addOrUpdateState(value, name, color);
        }
        stateTable = stateValues.toArray(new StateItem[stateValues.size()]);
        Display.getDefault().asyncExec(this::fireColorSettingsChanged);
        return map;
    }

    /**
     * Add a new state in the time graph view. This allow to define at runtime
     * new states that cannot be known at the conception of this analysis.
     *
     * @param name
     *            The string associated with the state
     * @return the value for this state
     */
    public synchronized int addState(String name) {
        // Find a value for this name, start at 10000
        int value = 10000;
        while (stateIndex.get(value) != null) {
            value++;
        }
        addOrUpdateState(value, name, ""); //$NON-NLS-1$
        Display.getDefault().asyncExec(this::fireColorSettingsChanged);
        return value;
    }

    private synchronized void addOrUpdateState(int value, String name, String color) {
        // FIXME Allow this case
        if (value < 0) {
            return;
        }

        final RGB colorRGB = (color.startsWith(TmfXmlStrings.COLOR_PREFIX)) ? parseColor(color) : calcColor(name);

        StateItem item = new StateItem(colorRGB, name);

        Integer index = stateIndex.get(value);
        if (index == null) {
            /* Add the new state value */
            stateIndex.put(value, stateValues.size());
            stateValues.add(item);
        } else {
            /* Override a previous state value */
            stateValues.set(index, item);
        }
        stateTable = stateValues.toArray(new StateItem[stateValues.size()]);
    }

    private static RGB parseColor(String color) {
        RGB colorRGB;
        Integer hex = Integer.parseInt(color.substring(1), 16);
        int hex1 = hex.intValue() % 256;
        int hex2 = (hex.intValue() / 256) % 256;
        int hex3 = (hex.intValue() / (256 * 256)) % 256;
        colorRGB = new RGB(hex3, hex2, hex1);
        return colorRGB;
    }

    /*
     * This method will always return the same color for a same name, no matter
     * the value, so that different traces with the same XML analysis will
     * display identically states with the same name.
     */
    private static RGB calcColor(String name) {
        long hash = name.hashCode(); // hashcodes can be Integer.MIN_VALUE.
        long base = COLOR_SEED[(int) (Math.abs(hash) % COLOR_SEED.length)];
        int x = (int) ((hash & COLOR_MASK) ^ base);
        final int r = (x >> 16) & 0xff;
        final int g = (x >> 8) & 0xff;
        final int b = x & 0xff;
        return new RGB(r, g, b);
    }

    /**
     * Return whether an integer value has a corresponding index in the
     * available states
     *
     * @param status
     *            The numerical status of the event
     * @return <code>true</code> if the numerical value is an existing value in
     *         the available states
     */
    public boolean hasIndex(int status) {
        return stateIndex.containsKey(status);
    }

    @Override
    public String getPreferenceKey() {
        return fId + '.' + super.getPreferenceKey();
    }
}
