// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.plugins.graphview.core.visualisation;

import java.awt.Color;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import org.openstreetmap.josm.plugins.graphview.core.graph.GraphEdge;
import org.openstreetmap.josm.plugins.graphview.core.graph.GraphNode;
import org.openstreetmap.josm.plugins.graphview.core.property.GraphEdgeSegments;
import org.openstreetmap.josm.plugins.graphview.core.property.RoadPropertyType;
import org.openstreetmap.josm.plugins.graphview.core.transition.Segment;

/**
 * scheme using values of a property to determine a color.
 * Nodes are given an average color of all incoming and outgoing segments.
 */
public class FloatPropertyColorScheme implements ColorScheme {

    private final Class<? extends RoadPropertyType<Float>> propertyClass;
    private final Map<Float, Color> colorMap;
    private final Color defaultColor;

    /**
     * @param propertyClass  property type to get values for; != null
     * @param colorMap       map from some values to colors.
     *                       Colors for all other values are interpolated.
     *                       This map will be copied and not used directly later. != null
     * @param defaultColor   color that is used when the property is not available; != null
     */
    public FloatPropertyColorScheme(Class<? extends RoadPropertyType<Float>> propertyClass,
            Map<Float, Color> colorMap, Color defaultColor) {
        assert propertyClass != null && colorMap != null && defaultColor != null;

        this.propertyClass = propertyClass;
        this.colorMap = new HashMap<>(colorMap);
        this.defaultColor = defaultColor;
    }

    @Override
    public Color getSegmentColor(Segment segment) {
        assert segment != null;

        Float propertyValue = null;
        Collection<RoadPropertyType<?>> availableProperties = segment.getAvailableProperties();
        for (RoadPropertyType<?> property : availableProperties) {
            if (propertyClass.isInstance(property)) {
                @SuppressWarnings("unchecked") //has been checked using isInstance
                RoadPropertyType<Float> floatProperty = (RoadPropertyType<Float>) property;
                propertyValue = segment.getPropertyValue(floatProperty);
                break;
            }
        }

        if (propertyValue != null) {
            return getColorForValue(propertyValue);
        } else {
            return defaultColor;
        }
    }

    @Override
    public Color getNodeColor(GraphNode node) {

        List<Color> segmentColors = new ArrayList<>();

        for (GraphEdge edge : node.getInboundEdges()) {
            List<Segment> edgeSegments = edge.getPropertyValue(GraphEdgeSegments.PROPERTY);
            if (edgeSegments.size() > 0) {
                Segment firstSegment = edgeSegments.get(0);
                segmentColors.add(getSegmentColor(firstSegment));
            }
        }
        for (GraphEdge edge : node.getOutboundEdges()) {
            List<Segment> edgeSegments = edge.getPropertyValue(GraphEdgeSegments.PROPERTY);
            if (edgeSegments.size() > 0) {
                Segment lastSegment = edgeSegments.get(edgeSegments.size()-1);
                segmentColors.add(getSegmentColor(lastSegment));
            }
        }

        if (segmentColors.size() > 0) {
            return averageColor(segmentColors);
        } else {
            return Color.WHITE;
        }

    }

    /**
     * returns the color for a value
     * @param value  value to get color for; != null
     * @return       color; != null
     */
    protected Color getColorForValue(Float value) {
        assert value != null;

        if (colorMap.containsKey(value)) {

            return colorMap.get(value);

        } else {

            LinkedList<Float> valuesWithDefinedColor = new LinkedList<>(colorMap.keySet());
            Collections.sort(valuesWithDefinedColor);

            if (value <= valuesWithDefinedColor.getFirst()) {

                return colorMap.get(valuesWithDefinedColor.getFirst());

            } else if (value >= valuesWithDefinedColor.getLast()) {

                return colorMap.get(valuesWithDefinedColor.getLast());

            } else {

                /* interpolate */

                Float lowerValue = valuesWithDefinedColor.getFirst();
                Float higherValue = null;

                for (Float v : valuesWithDefinedColor) {
                    if (v >= value) {
                        higherValue = v;
                        break;
                    }
                    lowerValue = v;
                }

                assert lowerValue != null && higherValue != null;

                Color lowerColor = colorMap.get(lowerValue);
                Color higherColor = colorMap.get(higherValue);

                float weightHigherColor = (value - lowerValue) / (higherValue - lowerValue);

                return weightedAverageColor(lowerColor, higherColor, weightHigherColor);

            }

        }

    }

    /**
     * returns an average of all colors that have been passed as parameter
     *
     * @param colors  colors to calculate average from; not empty or null
     * @return        average color; != null
     */
    private static Color averageColor(List<Color> colors) {
        assert colors != null && colors.size() > 0;

        float weightPerColor = 1.0f / colors.size();

        Color average = new Color(0, 0, 0);

        for (Color color : colors) {
            average = new Color(
                    Math.min(Math.round(average.getRed() + weightPerColor*color.getRed()), 255),
                    Math.min(Math.round(average.getGreen() + weightPerColor*color.getGreen()), 255),
                    Math.min(Math.round(average.getBlue() + weightPerColor*color.getBlue()), 255));
        }

        return average;
    }

    /**
     * returns a weighted average of two colors
     *
     * @param color1        first color for the average; != null
     * @param color2        second color for the average; != null
     * @param weightColor2  weight of color2; must be in [0..1]
     * @return              average color; != null
     */
    private static Color weightedAverageColor(Color color1, Color color2, float weightColor2) {
        assert color1 != null && color2 != null;
        assert 0 <= weightColor2 && weightColor2 <= 1;

        return new Color(
                Math.round((1 - weightColor2) * color1.getRed() + weightColor2 * color2.getRed()),
                Math.round((1 - weightColor2) * color1.getGreen() + weightColor2 * color2.getGreen()),
                Math.round((1 - weightColor2) * color1.getBlue() + weightColor2 * color2.getBlue()));
    }

}
