
package com.uwetrottmann.shopr.algorithm.model;

import com.uwetrottmann.shopr.algorithm.model.Attributes.AttributeValue;

import org.jgrapht.Graphs;
import org.jgrapht.UndirectedGraph;
import org.jgrapht.graph.DefaultEdge;
import org.jgrapht.graph.SimpleGraph;

import java.util.Arrays;
import java.util.List;

public class Color extends GenericAttribute {

    private static UndirectedGraph<Color.Value, DefaultEdge> sSimilarValues;

    static {
        sSimilarValues = new SimpleGraph<Color.Value, DefaultEdge>(DefaultEdge.class);

        Value[] values = Value.values();
        for (Value value : values) {
            sSimilarValues.addVertex(value);
        }

        /**
         * Stores similar price values in an undirected graph. This is rather
         * subjective, e.g. white is similar to grey. Red is similar to pink,
         * etc.
         */
        sSimilarValues.addEdge(Value.BLUE, Value.PURPLE);
        sSimilarValues.addEdge(Value.BLUE, Value.TURQUOISE);
        sSimilarValues.addEdge(Value.RED, Value.PURPLE);
        sSimilarValues.addEdge(Value.RED, Value.PINK);
        sSimilarValues.addEdge(Value.YELLOW, Value.ORANGE);
        sSimilarValues.addEdge(Value.BLACK, Value.GREY);
        sSimilarValues.addEdge(Value.WHITE, Value.BEIGE);
        sSimilarValues.addEdge(Value.WHITE, Value.GREY);
        sSimilarValues.addEdge(Value.COLORED, Value.MIXED);
        sSimilarValues.addEdge(Value.BROWN, Value.BEIGE);
    }

    public static final String ID = "color";

    public enum Value implements AttributeValue {
        BLUE("Blue"),
        RED("Red"),
        PINK("Pink"), // rosa zu Deutsch
        PURPLE("Purple"),
        YELLOW("Yellow"),
        BROWN("Brown"),
        COLORED("Colored"),
        MIXED("Mixed"),
        GREY("Grey"),
        GREEN("Green"),
        ORANGE("Orange"),
        BLACK("Black"),
        TURQUOISE("Turquoise"),
        WHITE("White"),
        BEIGE("Beige");

        String mDescriptor;

        Value(String name) {
            mDescriptor = name;
        }

        @Override
        public String descriptor() {
            return mDescriptor;
        }

        @Override
        public int index() {
            return ordinal();
        }
    }

    public Color() {
        int numValues = Value.values().length;
        mValueWeights = new double[numValues];
        Arrays.fill(mValueWeights, 1.0 / numValues);
    }

    public Color(Value value) {
        setWeights(value);
    }

    /**
     * Tries to match the given string with a {@link Color.Value}.
     */
    public Color(String value) {
        if ("Blau".equals(value)) {
            setWeights(Color.Value.BLUE);
        }
        else if ("Braun".equals(value)) {
            setWeights(Color.Value.BROWN);
        }
        else if ("Bunt".equals(value)) {
            setWeights(Color.Value.COLORED);
        }
        else if ("Gelb".equals(value)) {
            setWeights(Color.Value.YELLOW);
        }
        else if ("Gemischt".equals(value)) {
            setWeights(Color.Value.MIXED);
        }
        else if ("Grau".equals(value)) {
            setWeights(Color.Value.GREY);
        }
        else if ("Grün".equals(value)) {
            setWeights(Color.Value.GREEN);
        }
        else if ("Orange".equals(value)) {
            setWeights(Color.Value.ORANGE);
        }
        else if ("Rosa".equals(value)) {
            setWeights(Color.Value.PINK);
        }
        else if ("Rot".equals(value)) {
            setWeights(Color.Value.RED);
        }
        else if ("Schwarz".equals(value)) {
            setWeights(Color.Value.BLACK);
        }
        else if ("Türkis".equals(value)) {
            setWeights(Color.Value.TURQUOISE);
        }
        else if ("Violett".equals(value)) {
            setWeights(Color.Value.PURPLE);
        }
        else if ("Weiß".equals(value)) {
            setWeights(Color.Value.WHITE);
        }
        else if ("Beige".equals(value)) {
            setWeights(Color.Value.BEIGE);
        }
    }

    private void setWeights(Value value) {
        mValueWeights = new double[Value.values().length];
        Arrays.fill(mValueWeights, 0.0);
        mValueWeights[value.ordinal()] = 1.0;
        currentValue(value);
    }

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

    @Override
    public AttributeValue[] getValueSymbols() {
        return Value.values();
    }

    @Override
    public void likeValue(int valueIndex, double[] weights) {
        Value[] values = Value.values();
        Value likedColor = values[valueIndex];
        List<Value> similarColors = Graphs.neighborListOf(sSimilarValues, likedColor);

        if (similarColors.isEmpty()) {
            // treat like a regular like
            super.likeValue(valueIndex, weights);
        } else {
            // do not touch similar colors weights

            // calculate average weight
            double weightIncrease = 1.0 / weights.length;

            /*
             * If the value was 0.0 (disliked) increase double so it will
             * definitely have highest weight.
             */
            if (weights[valueIndex] == 0.0) {
                weightIncrease *= 2;
            }

            // add weight to liked value
            weights[valueIndex] += weightIncrease;

            // if liked value weight exceeds 1.0, sum exceeds 1.0
            if (weights[valueIndex] > 1.0) {
                // all other weights have to be 0.0
                Arrays.fill(weights, 0.0);
                weights[valueIndex] = 1.0;
                return;
            }

            /*
             * Subtract added weight evenly from other colors, BUT only from
             * non-similar colors and those with non-zero weights.
             */
            // get number of non-zero weights
            int count = 0;
            for (int i = 0; i < weights.length; i++) {
                if (weights[i] != 0 && !hasValueWithSameIndex(similarColors, i)) {
                    count++;
                }
            }
            // calculate share for each value
            double redistributed = weightIncrease / (count - 1);
            for (int i = 0; i < weights.length; i++) {
                if (i != valueIndex && !hasValueWithSameIndex(similarColors, i)) {
                    weights[i] -= redistributed;
                    // floor at 0.0
                    if (weights[i] < 0) {
                        weights[i] = 0.0;
                    }
                }
            }

            // sum can not exceed 1.0
            double sum = 0;
            for (int i = 0; i < weights.length; i++) {
                sum += weights[i];
            }
            if (sum > 1.0) {
                // distribute remaining weight evenly over all similar values
                double redistribute = (1 - weights[valueIndex]) / similarColors.size();
                for (int i = 0; i < weights.length; i++) {
                    if (i == valueIndex) {
                        // weight for liked color already set
                        continue;
                    }
                    if (hasValueWithSameIndex(similarColors, i)) {
                        weights[i] = redistribute;
                    } else {
                        weights[i] = 0.0;
                    }
                }
            }
        }
    }

    /**
     * Checks whether one of the values has the given index.
     */
    private boolean hasValueWithSameIndex(List<Value> values, int index) {
        for (Value value : values) {
            if (value.index() == index) {
                return true;
            }
        }
        return false;
    }
}

@@ -4856,16 +4856,93 @@
 eights%0A%0A
+            // FIXME weights%5BvalueIndex%5D could be smaller than another value%0A
         
@@ -6944,24 +6944,102 @@
 ors.size();%0A
+                // FIXME: redistribute can be bigger than weights%5BvalueIndex%5D%0A
             

Add fixmes for issues.