#!/usr/bin/env python3

"""
Experiment with the algorithms from:
http://www.alanzucconi.com/2015/09/30/colour-sorting/

Against Rubiks cube RGB values
I skipped hilbert, that one was more trouble than it was worth
"""

from rubikscolorresolver import k_means_colors_dictionary
from sklearn.cluster import KMeans
from copy import deepcopy
from scipy.spatial import distance
import numpy as np
import colorsys
import json
import logging
import math
import sys


def convert_key_strings_to_int(data):
    """
    Convert keys that are strings of integers to integers
    """
    result = {}
    for (key, value) in data.items():

        if isinstance(value, list):
            value = tuple(value)

        if key.isdigit():
            result[int(key)] = value
        else:
            result[key] = value
    return result


def write_header(fh):
    fh.write(
        """<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
div.clear {
    clear: both;
}

div.colors span {
    width: 40px;
    height: 40px;
    white-space-collapsing: discard;
    display: inline-block;
}
</style>
<title>Python Color Sorter</title>
</head>
<body>
"""
    )


def write_colors(fh, algorithm, colors):
    # squares_per_side = int(len(colors) / 6)
    fh.write("<h2>%s</h2>\n" % algorithm)
    fh.write("<div class='clear colors'>\n")
    for (index, (red, green, blue)) in enumerate(colors):

        if red is None and green is None and blue is None:
            fh.write("<br>")
            continue

        # to use python coloursys convertion we have to rescale to range 0-1
        (H, S, V) = colorsys.rgb_to_hsv(
            float(red / 255), float(green / 255), float(blue / 255)
        )

        # rescale H to 360 degrees and S, V to percent of 100%
        H = int(H * 360)
        S = int(S * 100)
        V = int(V * 100)
        # log.info("%3d: RGB (%3d, %3d, %3d) -> HSV (%3d, %3d, %3d)" % (index+1, red, green, blue, H, S, V))
        fh.write(
            "<span style='background-color:#%02x%02x%02x' title='RGB (%s, %s, %s), HSV (%s, %s, %s)'>&nbsp;</span>\n"
            % (red, green, blue, red, green, blue, H, S, V)
        )
        # if (index+1) % squares_per_side == 0:
        #    log.info('')

    fh.write("</div>\n")
    log.info("\n\n")


def write_footer(fh):
    fh.write("</body>\n")
    fh.write("</html>\n")


def lum(r, g, b):
    return math.sqrt(0.241 * r + 0.691 * g + 0.068 * b)


def step(r, g, b, repetitions=1):
    lum = math.sqrt(0.241 * r + 0.691 * g + 0.068 * b)

    h, s, v = colorsys.rgb_to_hsv(r, g, b)

    h2 = int(h * repetitions)
    # lum2 = int(lum * repetitions)
    v2 = int(v * repetitions)

    return (h2, lum, v2)


def NN(A, start):
    """
    http://stackoverflow.com/questions/17493494/nearest-neighbour-algorithm

    NEAREST NEIGHBOUR ALGORITHM
    ---------------------------
    The algorithm takes two arguments. The first one is an array, with elements
    being lists/column-vectors from the given complete incidensmatrix. The second
    argument is an integer which represents the startingnode where 1 is the
    smallest. The program will only make sense, if the triangle inequality is satisfied.
    Furthermore, diagonal elements needs to be inf. The pseudocode is listed below:


    1. - stand on an arbitrary vertex as current vertex.
    2. - find out the shortest edge connecting current vertex and an unvisited vertex V.
    3. - set current vertex to V.
    4. - mark V as visited.
    5. - if all the vertices in domain are visited, then terminate.
    6. - Go to step 2.

    The sequence of the visited vertices is the output of the algorithm

    Remark - infinity is entered as np.inf
    """

    start = start - 1  # To compensate for the python index starting at 0.
    n = len(A)
    path = [start]
    costList = []
    tmp = deepcopy(start)
    B = deepcopy(A)

    # This block eliminates the startingnode, by setting it equal to inf.
    for h in range(n):
        B[h][start] = np.inf

    for i in range(n):

        # This block appends the visited nodes to the path, and appends
        # the cost of the path.
        for j in range(n):
            if B[tmp][j] == min(B[tmp]):
                costList.append(B[tmp][j])
                path.append(j)
                tmp = j
                break

        # This block sets the current node to inf, so it can't be visited again.
        for k in range(n):
            B[k][tmp] = np.inf

    # The last term adds the weight of the edge connecting the start - and endnote.
    # cost = sum([i for i in costList if i < np.inf]) + A[path[len(path) - 2]][start]

    # The last element needs to be popped, because it is equal to inf.
    path.pop(n)

    # Because we want to return to start, we append this node as the last element.
    path.insert(n, start)

    # Prints the path with original indicies.
    path = [i + 1 for i in path]

    # print "The path is: ", path
    # print "The cost is: ", cost
    return path


def travelling_salesman(colors):
    colors_length = len(colors)

    # Distance matrix
    A = np.zeros([colors_length, colors_length])
    for x in range(0, colors_length - 1):
        for y in range(0, colors_length - 1):
            A[x, y] = distance.euclidean(colors[x], colors[y])

    # Nearest neighbour algorithm
    path = NN(A, 0)

    # Final array
    colors_nn = []
    for i in path:
        colors_nn.append(colors[i])

    return colors_nn


if __name__ == "__main__":

    # logging.basicConfig(filename='rubiks.log',
    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s %(filename)12s %(levelname)8s: %(message)s",
    )
    log = logging.getLogger(__name__)

    filename = sys.argv[1]

    with open(filename, "r") as fh:
        data = convert_key_strings_to_int(json.load(fh))
        # pprint(data)

    colors = sorted(list(data.values()))

    with open("foo.html", "w") as fh:
        write_header(fh)
        # for algorithm in ('none', 'rgb', 'hsv', 'hls', 'luminosity', 'step', 'travelling-salesman'):
        # for algorithm in ('none', 'hsv', 'step', 'kmeans'):
        # for algorithm in ('none', 'hsv', 'kmeans'):
        for algorithm in ("kmeans", "kmeans2"):

            if algorithm == "none":
                tmp_colors = colors

            elif algorithm == "rgb":
                tmp_colors = sorted(colors)

            elif algorithm == "hsv":
                tmp_colors = deepcopy(colors)
                tmp_colors.sort(key=lambda rgb: colorsys.rgb_to_hsv(*rgb))

            elif algorithm == "hls":
                tmp_colors = deepcopy(colors)
                tmp_colors.sort(key=lambda rgb: colorsys.rgb_to_hls(*rgb))

            elif algorithm == "luminosity":
                tmp_colors = deepcopy(colors)
                tmp_colors.sort(key=lambda rgb: lum(*rgb))

            elif algorithm == "step":
                tmp_colors = deepcopy(colors)
                tmp_colors.sort(key=lambda rgb: step(*rgb, 6))

            elif algorithm == "travelling-salesman":
                tmp_colors = travelling_salesman(deepcopy(colors))

            elif algorithm == "kmeans":
                # http://www.pyimagesearch.com/2014/05/26/opencv-python-k-means-color-clustering/
                clt = KMeans(n_clusters=6)
                clt.fit(deepcopy(colors))

                tmp_colors = []
                for target_cluster in range(6):
                    for (index, cluster) in enumerate(clt.labels_):
                        if cluster == target_cluster:
                            tmp_colors.append(colors[index])
                    tmp_colors.append((None, None, None))

            elif algorithm == "kmeans2":
                tmp_colors = []
                # for rgb_list in k_means_colors_dictionary(deepcopy(data), (13, 38, 63, 88, 113, 138)):
                for rgb_list in k_means_colors_dictionary(
                    deepcopy(data), (31, 42, 73, 108, 139, 186)
                ):
                    for rgb in rgb_list:
                        tmp_colors.append(rgb)
                    tmp_colors.append((None, None, None))

            else:
                log.warning("Implement %s" % algorithm)
                continue

            write_colors(fh, algorithm, tmp_colors)

        write_footer(fh)