from hashindex import Index
from math import hypot
import anneal
import random

def sort_paths_greedy(paths, reversable=True):
    first = max(paths, key=lambda x: x[0][1])
    paths.remove(first)
    result = [first]
    points = []
    for path in paths:
        x1, y1 = path[0]
        x2, y2 = path[-1]
        points.append((x1, y1, path, False))
        if reversable:
            points.append((x2, y2, path, True))
    index = Index(points)
    while index.size:
        x, y, path, reverse = index.search(result[-1][-1])
        x1, y1 = path[0]
        x2, y2 = path[-1]
        index.remove((x1, y1, path, False))
        if reversable:
            index.remove((x2, y2, path, True))
        if reverse:
            result.append(list(reversed(path)))
        else:
            result.append(path)
    return result

def sort_paths(paths, iterations=100000, reversable=True):
    '''
    This function re-orders a set of 2D paths (polylines) to minimize the
    distance required to visit each path. This is useful for 2D plotting to
    reduce wasted movements where the instrument is not drawing.

    If allowed, the algorithm will also reverse some paths if doing so reduces
    the total distance.

    The code uses simulated annealing as its optimization algorithm. The number
    of iterations can be increased to improve the chances of finding a perfect
    solution. However, a perfect solution isn't necessarily required - we just
    want to find something good enough.

    With randomly generated paths, the algorithm can quickly find a solution
    that reduces the extra distance to ~25 percent of its original value.
    '''
    state = Model(list(paths), reversable)
    max_temp = anneal.get_max_temp(state, 10000)
    min_temp = max_temp / 1000.0
    state = anneal.anneal(state, max_temp, min_temp, iterations)
    for path, reverse in zip(state.paths, state.reverse):
        if reverse:
            path.reverse()
    return state.paths

def sort_points(points, iterations=100000):
    '''
    Like sort_paths, but operates on individual points instead.
    This is basically a traveling salesman optimization.
    '''
    paths = [[x] for x in points]
    paths = sort_paths(paths, iterations, False)
    points = [x[0] for x in paths]
    return points

class Model(object):
    def __init__(self, paths, reversable=True, reverse=None, distances=None, total_distance=None):
        self.paths = paths
        self.reversable = reversable
        self.reverse = reverse or [False] * len(self.paths)
        if distances:
            self.total_distance = total_distance or 0
            self.distances = distances
        else:
            self.total_distance = 0
            self.distances = [0] * (len(paths) - 1)
            self.add_distances(range(len(self.distances)))
    def subtract_distances(self, indexes):
        n = len(self.distances)
        for i in indexes:
            if i >= 0 and i < n:
                self.total_distance -= self.distances[i]
    def add_distances(self, indexes):
        n = len(self.distances)
        for i in indexes:
            if i < 0 or i >= n:
                continue
            j = i + 1
            if self.reverse[i]:
                x1, y1 = self.paths[i][0]
            else:
                x1, y1 = self.paths[i][-1]
            if self.reverse[j]:
                x2, y2 = self.paths[j][-1]
            else:
                x2, y2 = self.paths[j][0]
            self.distances[i] = hypot(x2 - x1, y2 - y1)
            self.total_distance += self.distances[i]
    def energy(self):
        # return the total extra distance for this ordering
        return self.total_distance
    def do_move(self):
        if self.reversable and random.random() < 0.25:
            # mutate by reversing a random path
            n = len(self.paths) - 1
            i = random.randint(0, n)
            indexes = [i - 1, i]
            self.subtract_distances(indexes)
            self.reverse[i] = not self.reverse[i]
            self.add_distances(indexes)
            return (1, i, 0)
        else:
            # mutate by swapping two random paths
            n = len(self.paths) - 1
            i = random.randint(0, n)
            j = random.randint(0, n)
            indexes = set([i - 1, i, j - 1, j])
            self.subtract_distances(indexes)
            self.paths[i], self.paths[j] = self.paths[j], self.paths[i]
            self.add_distances(indexes)
            return (0, i, j)
    def undo_move(self, undo):
        # undo the previous mutation
        mode, i, j = undo
        if mode == 0:
            indexes = set([i - 1, i, j - 1, j])
            self.subtract_distances(indexes)
            self.paths[i], self.paths[j] = self.paths[j], self.paths[i]
            self.add_distances(indexes)
        else:
            indexes = [i - 1, i]
            self.subtract_distances(indexes)
            self.reverse[i] = not self.reverse[i]
            self.add_distances(indexes)
    def copy(self):
        # make a copy of the model
        return Model(
            list(self.paths), self.reversable, list(self.reverse),
            list(self.distances), self.total_distance)

def test(n_paths, n_iterations, seed=None):
    random.seed(seed)
    paths = []
    for _ in range(n_paths):
        x1 = random.random()
        y1 = random.random()
        x2 = random.random()
        y2 = random.random()
        path = [(x1, y1), (x2, y2)]
        paths.append(path)
    before = Model(paths).energy()
    if n_iterations:
        paths = sort_paths(paths, n_iterations)
    else:
        paths = sort_paths_greedy(paths)
    after = Model(paths).energy()
    pct = 100.0 * after / before
    return pct

if __name__ == '__main__':
    # test the module
    for n_paths in [10, 100, 1000, 10000]:
        for n_iterations in [None, 10, 100, 1000, 10000, 100000, 1000000]:
            pct = test(n_paths, n_iterations, 123)
            print n_paths, n_iterations, pct