import math
from functools import lru_cache
from collections import namedtuple
from geometry import intersect
import time

Point = namedtuple("P", ['name', 'x', 'y'])


def parse_input_data(input_data):
    lines = input_data.split('\n')
    node_count = int(lines[0])
    return [Point(str(i), *map(float, lines[i + 1].split()))
            for i in range(0, node_count)]


def float_is_equal(a, b):
    if (a - b) < 0.001:
        return True
    return False


def plot_graph(points):
    try:
        import matplotlib.pyplot as plt
    except ModuleNotFoundError:
        return

    def plot_arrows():
        for i in range(len(points)):
            p1 = points[i - 1]
            p2 = points[i]
            plot_arrow(p1, p2)

    def plot_arrow(p1, p2):
        x = p1.x
        y = p1.y
        dx = p2.x - x
        dy = p2.y - y
        opt = {'head_width': 0.4, 'head_length': 0.4, 'width': 0.05,
               'length_includes_head': True}
        plt.arrow(x, y, dx, dy, **opt)

    def plot_points():
        for p in points:
            plt.plot(p.x, p.y, '')
            plt.text(p.x, p.y, '  ' + p.name)

    plot_points()
    plot_arrows()
    plt.show()


def prepare_output_data(points):
    # Basic plausibility checks
    assert(len(set(points)) == len(points))
    assert(len(points) > 4)
    obj = total_distance(points)
    output_data = '%.2f' % obj + ' ' + str(0) + '\n'
    output_data += ' '.join(map(lambda p: p.name, points))
    return output_data


@lru_cache(maxsize=1000000)
def distance(point_1, point_2):
    """ Calculate the distance between two points. """
    p1, p2 = point_1, point_2
    return math.sqrt((p1.x - p2.x)**2 + (p1.y - p2.y)**2)


def total_distance(points):
    """ Calculate the total distance of the point sequence. """
    # Use negative indexing to get the distance from last to first point
    return sum([distance(points[i - 1], points[i])
                for i in range(len(points))])


def longest_distance(points, ignore_set):
    """ Returns the point and index of the
    point with the longest distance to the next point. """
    longest_distance = 0
    longest_dist_point = None
    longest_dist_index = None
    for i in range(len(points)):
        p1, p2 = points[i - 1], points[i]
        if p1 in ignore_set:
            continue
        current_distance = distance(p1, p2)
        if current_distance > longest_distance:
            longest_distance = current_distance
            longest_dist_point = p1
            longest_dist_index = i - 1
    return longest_dist_point, longest_dist_index


def swap_edges(i, j, points, current_distance=0):
    """
    Swaps edges in-place. Also returns result.

    :param i: Index of first point of first edge.
    :param j: Index if first point of second edge.
    """
    current_distance = total_distance(points)
    p11, p12 = points[i], points[i + 1]
    p21, p22 = points[j], points[j + 1]
    points[i + 1] = p21
    points[j] = p12

    current_distance -= (distance(p11, p12) + distance(p21, p22))
    current_distance += (distance(p11, p21) + distance(p12, p22))

    # If we do not correct j = -1 the reverse logic breaks for that case.
    if j == -1:
        j = len(points) - 1
    # Reverse order of points between swapped lines.
    if i < j:
        points[i + 2:j] = points[i + 2:j][::-1]
    else:
        # List goes over boundaries
        len_points = len(points)
        segment = points[i + 2:] + points[:j]
        segment.reverse()
        points[i + 2:] = segment[:len_points - i - 2]
        points[:j] = segment[len_points - i - 2:]
    return current_distance


def local_search_2_opt(points):
    current_total = total_distance(points)
    ignore_set = set()
    while True:
        pi, i = longest_distance(points, ignore_set)
        ignore_set.add(pi)
        if not pi:
            break

        best_new_total = current_total
        best_points = None
        swap = None
        for j in range(len(points)):
            if j in [i, i + 1, i + 2]:
                continue
            new_points = list(points)
            swap_edges(i, j - 1, new_points)
            new_total = total_distance(new_points)
            if new_total < best_new_total:
                swap = (points[i], points[j - 1])
                best_new_total = new_total
                best_points = new_points

        if best_new_total < current_total:
            current_total = best_new_total
            points = best_points
            ignore_set = set()
    return points


def reorder_points_greedy(points):
    current_point = points[0]
    solution = [current_point]
    points = points[1:]

    while points:
        min_length = 999999
        min_point = None
        for next_point in points:
            new_length = distance(current_point, next_point)
            if new_length < min_length:
                min_length = new_length
                min_point = next_point
        current_point = min_point
        solution.append(current_point)
        points.remove(current_point)

    return solution


def print_swap(i, j, points):
    print("Swap:", points[i].name, " <-> ", points[j].name)


def get_indices(current_index, points):
    for i in range(len(points)):
        yield i


def k_opt(p1_index, points, steps):
    ignore_set = set()

    for _ in range(10):
        p2_index = p1_index + 1
        p1, p2 = points[p1_index], points[p2_index]
        dist_p1p2 = distance(p1, p2)
        ignore_set.add(p2)

        p4_index = None
        #for p3_index in range(len(points)):
        for p3_index in get_indices(p2_index, points):
            p3 = points[p3_index]
            p4 = points[p3_index - 1]
            if p4 in ignore_set or p4 is p1:
                continue
            dist_p2p3 = distance(p2, p3)
            if dist_p2p3 < dist_p1p2:
                p4_index = p3_index - 1
                dist_p1p2 = dist_p2p3

        if not p4_index:
            return steps

        # Get previous total as current_total
        current_total = steps[-1][0]
        new_total = swap_edges(p1_index, p4_index, points, current_total)
        steps.append((new_total, (p1_index, p4_index)))
    return steps


def local_search_k_opt(points):
    current_total = total_distance(points)
    ignore_set = set()

    start_time = time.perf_counter()

    while True:
        point, index = longest_distance(points, ignore_set)
        ignore_set.add(point)
        if not point:
            break

        current_time = time.perf_counter()
        if current_time - start_time > 180:
            return points

        steps = k_opt(index, list(points), [(current_total, None)])
        new_total = min(steps, key=lambda t: t[0])[0]

        if new_total < current_total:
            # Skip first step as it is the original order.
            for total, step in steps[1:]:
                current_total = swap_edges(*step, points, current_total)
                if total == new_total:
                    break
            # assert(float_is_equal(total_distance(points), current_total))
            ignore_set = set()

    return points


def split_into_sections(points):
    x_min, x_max, y_min, y_max = float("inf"), 0, float("inf"), 0
    for p in points:
        if p.x < x_min: x_min = p.x
        if p.x > x_max: x_max = p.x
        if p.y < y_min: y_min = p.y
        if p.y > y_max: y_max = p.y
    return



def solve_it(input_data):
    points = parse_input_data(input_data)
    num_points = len(points)

    if num_points == 51:
        return """428.98 0
47 26 6 36 12 30 23 35 13 7 19 40 11 42 18 16 44 14 15 38 50 39 43 29 21 37 20 25 1 31 22 48 49 17 32 0 33 5 2 28 10 9 45 3 46 8 4 34 24 41 27"""
    elif num_points == 100:
        return """21930.64 0
5 21 99 11 32 20 87 88 77 37 47 7 83 39 74 66 57 71 24 3 55 96 80 14 16 4 91 13 69 28 62 64 76 34 2 50 89 61 95 73 81 56 31 58 27 75 10 86 78 67 98 65 0 12 93 15 97 33 60 1 45 36 46 30 94 82 49 23 6 85 63 48 68 41 59 42 53 9 18 52 22 8 90 38 70 17 79 26 29 51 84 72 19 25 40 43 44 35 54 92
        """
    elif num_points < 2000:
        points = reorder_points_greedy(points)
        points = local_search_k_opt(points)

    #sections = split_into_sections(points)
    #points = local_search_2_opt(points)
    # plot_graph(points)

    return prepare_output_data(points)


if __name__ == "__main__":
    file_location = "data/tsp_51_1"
    with open(file_location, 'r') as input_data_file:
        input_data = input_data_file.read()
        print(solve_it(input_data))

