import json
from typing import List
from networks.geometry.Polyline import Polyline

from networks.geometry.Point3D import Point3D
from networks.geometry.Point2D import Point2D
from networks.geometry.Segment2D import Segment2D
from networks.geometry.Segment3D import Segment3D
from networks.geometry.Circle import Circle
from utils.Enums import LINE_THICKNESS_MODE
from gdpc import Block, Editor, geometry
from scipy.ndimage import gaussian_filter1d
import numpy as np
import random

from PIL import Image


class Road:
    def __init__(self, coordinates: List[Point3D], width: int):
        self.coordinates = self._remove_collinear_points(coordinates)
        self.output_block = []
        # with open(road_configuration) as f:
        #     self.road_configuration = json.load(f)
        #     self.width = self.road_configuration["width"]
        self.width = width
        self.polyline_height = None
        self.polyline_total_line_output = None
        self.segment_total_line_output = None
        self.index_factor = 0

        if len(self._remove_collinear_points(self.coordinates)) >= 4:
            self.polyline = Polyline(Point3D.to_2d(coordinates, 'y'))
            self.polyline_total_line_output = [
                [] for _ in range(len(self.polyline.total_line_output))]

            self._projection_polyline()

        if len(self.coordinates) == 2:
            self.segment_total_line_output = Segment2D(
                Point3D.to_2d([self.coordinates[0]], 'y')[0], Point3D.to_2d([self.coordinates[1]], 'y')[0]).segment_thick(self.width, LINE_THICKNESS_MODE.MIDDLE)
            self._projection_segment()
            self.place()

    @staticmethod
    def _remove_collinear_points(points):
        output_points = [points[0]]

        for i in range(1, len(points) - 1):
            if isinstance(points[0], Point3D):
                if not Point2D.collinear(
                        Point3D.to_2d([points[i-1]], 'y')[0], Point3D.to_2d([points[i]], 'y')[0], Point3D.to_2d([points[i+1]], 'y')[0]):
                    output_points.append(points[i])
            else:
                if not Point2D.collinear(points[i-1], points[i], points[i+1]):
                    output_points.append(points[i])

        output_points.append(points[-1])
        return output_points

    def _surface(self):
        for i in range(1, len(self.polyline.segments)):
            # Segments
            if len(self.polyline.segments[i].segment()) > 2:
                last_valid_index = i
                self.polyline.segments[i].segment_thick(
                    self.width-1, LINE_THICKNESS_MODE.MIDDLE)
                for k in range(len(self.polyline.segments[i].points_thick_by_line)):
                    for m in range(len(self.polyline.segments[i].points_thick_by_line[k])):
                        kk = k % self.width
                        if kk in [round((self.width-1)/2)]:
                            if m % 4 == 0:
                                block = random.choices(
                                    ["stone", "andesite"],
                                    weights=[3, 1],
                                    k=1,
                                )[0]
                            else:
                                block = random.choices(
                                    ["yellow_concrete", "yellow_concrete_powder"],
                                    weights=[3, 1],
                                    k=1,
                                )[0]
                        elif kk in [self.width-2, 0]:
                            block = random.choices(
                                ["white_concrete", "white_concrete_powder"],
                                weights=[3, 1],
                                k=1,
                            )[0]
                        else:
                            block = random.choices(
                                ["stone", "andesite"],
                                weights=[3, 1],
                                k=1,
                            )[0]
                        nearest = self.polyline.segments[i].points_thick_by_line[k][m].nearest(
                            Point3D.to_2d(self.polyline_total_line_output, removed_axis='y'), True)
                        self.output_block.append(
                            (Point3D.insert_3d([self.polyline.segments[i].points_thick_by_line[k][m]], 'y', [self.polyline_total_line_output[nearest[0]].y])[0].coordinates, Block(block)))
                        self.output_block.append(
                            (Point3D.insert_3d([self.polyline.segments[i].points_thick_by_line[k][m]], 'y', [self.polyline_total_line_output[nearest[0]].y-1])[0].coordinates, Block(random.choices(
                                ["stone", "andesite"],
                                weights=[3, 1],
                                k=1,
                            )[0])))

                    for m in range(len(self.polyline.segments[i].gaps[k])):
                        block = random.choices(
                            ["stone", "andesite"],
                            weights=[3, 1],
                            k=1,
                        )[0]
                        nearest = self.polyline.segments[i].gaps[k][m].nearest(
                            Point3D.to_2d(self.polyline_total_line_output, removed_axis='y'), True)
                        self.output_block.append(
                            (Point3D.insert_3d([self.polyline.segments[i].gaps[k][m]], 'y', [self.polyline_total_line_output[nearest[0]].y])[0].coordinates, Block(block)))
                        self.output_block.append(
                            (Point3D.insert_3d([self.polyline.segments[i].gaps[k][m]], 'y', [self.polyline_total_line_output[nearest[0]].y-1])[0].coordinates, Block(random.choices(
                                ["stone", "andesite"],
                                weights=[3, 1],
                                k=1,
                            )[0])))

            # Circle
            if i != len(self.polyline.segments)-1:
                circle, gaps = Circle(self.polyline.centers[i]).circle_thick_by_line(int(
                    (self.polyline.radii[i]-self.width/2))+1, int((self.polyline.radii[i]+self.width/2))+1)

                # Better to do here than drawing circle arc inside big triangle!
                double_point_a = Point2D.from_arrays(Point2D.to_arrays(self.polyline.acrs_intersections[i][0]) + 50 * (Point2D.to_arrays(
                    self.polyline.acrs_intersections[i][0]) - Point2D.to_arrays(self.polyline.centers[i])))
                double_point_b = Point2D.from_arrays(Point2D.to_arrays(self.polyline.acrs_intersections[i][2]) + 50 * (Point2D.to_arrays(
                    self.polyline.acrs_intersections[i][2]) - Point2D.to_arrays(self.polyline.centers[i])))

                circle_list = [[] for _ in range(len(circle))]
                for j in range(len(circle)):
                    for k in range(len(circle[j])):
                        if circle[j][k].is_in_triangle(double_point_a, self.polyline.centers[i], double_point_b):
                            circle_list[j].append(circle[j][k])

                # for j in range(len(gaps)):
                #     for k in range(len(gaps[j])):
                #         if gaps[j][k].is_in_triangle(double_point_a, self.polyline.centers[i], double_point_b):
                #             circle_list[j].append(gaps[j][k])

                middle_lane_index = round(len(circle_list)/2)
                middle_line_length = len(circle_list[middle_lane_index])
                circle_list[middle_lane_index] = circle_list[middle_lane_index][0].optimized_path(
                    circle_list[middle_lane_index])
                for k in range(len(circle_list[middle_lane_index])):
                    nearest = circle_list[middle_lane_index][k].nearest(
                        Point3D.to_2d(self.polyline_total_line_output, removed_axis='y'), True)
                    circle_list[middle_lane_index][k] = Point3D.insert_3d([circle_list[middle_lane_index][k]], 'y', [
                        self.polyline_total_line_output[nearest[0]].y])[0]

                for j in range(len(circle_list)):
                    if j != middle_lane_index and len(circle_list[j]) > 0:
                        circle_list[j] = circle_list[j][0].optimized_path(
                            circle_list[j])
                        if len(circle_list[j]) != 1:
                            factor = (middle_line_length-1) / \
                                (len(circle_list[j])-1)
                        else:
                            factor = 1

                        for k in range(len(circle_list[j])):
                            circle_list[j][k] = Point3D.insert_3d([circle_list[j][k]], 'y', [
                                circle_list[middle_lane_index][round(factor * k)].y])[0]

                    # Complete with gaps
                    if j < len(gaps):
                        for k in range(len(gaps[j])):
                            if gaps[j][k].is_in_triangle(double_point_a, self.polyline.centers[i], double_point_b):
                                circle_list[j].append(
                                    Point3D.insert_3d([gaps[j][k]], 'y', [
                                        circle_list[j][gaps[j][k].nearest(Point3D.to_2d(circle_list[j], 'y'), True)[0]].y])[0])

                    for k in range(len(circle_list[j])):
                        kk = j % self.width
                        if kk in [round(self.width/2)]:
                            if k % 4 == 0:
                                block = random.choices(
                                    ["stone", "andesite"],
                                    weights=[3, 1],
                                    k=1,
                                )[0]
                            else:
                                block = random.choices(
                                    ["yellow_concrete", "yellow_concrete_powder"],
                                    weights=[3, 1],
                                    k=1,
                                )[0]
                        elif kk in [self.width-1, 0]:
                            block = random.choices(
                                ["white_concrete", "white_concrete_powder"],
                                weights=[3, 1],
                                k=1,
                            )[0]
                        else:
                            block = random.choices(
                                ["stone", "andesite"],
                                weights=[3, 1],
                                k=1,
                            )[0]
                        self.output_block.append(
                            (circle_list[j][k].coordinates, Block(block)))
                        self.output_block.append(
                            ((circle_list[j][k].coordinates[0], circle_list[j][k].coordinates[1]-1, circle_list[j][k].coordinates[2]), Block(random.choices(
                                ["stone", "andesite"],
                                weights=[3, 1],
                                k=1,
                            )[0])))

    def _projection_gaussian(self):
        nearest_points_to_reference = []
        for i in range(len(self.coordinates)):
            # Index is used to space accordingly
            index, point = Point3D.to_2d([self.coordinates[i]], 'y')[0].nearest(
                self.polyline.total_line_output, return_index=True)
            nearest_points_to_reference.append(
                Point2D(index, self.coordinates[i].y))

        linear_y_interpolation = []
        for i in range(len(nearest_points_to_reference)-1):
            linear_y_interpolation.extend(Segment2D(
                nearest_points_to_reference[i], nearest_points_to_reference[i+1]).segment())

        linear_y_interpolation = np.array(
            Point2D.to_arrays(linear_y_interpolation))

        # Extract x and y coordinates
        x = linear_y_interpolation[:, 0]
        y = linear_y_interpolation[:, 1]

        y_smooth = gaussian_filter1d(y, sigma=5)

        self.index_factor = len(y_smooth)/len(self.polyline.total_line_output)

        for i in range(len(self.polyline.total_line_output)):
            self.polyline_total_line_output[i] = Point3D(
                self.polyline.total_line_output[i].x, y[round(i*self.index_factor)], self.polyline.total_line_output[i].y)

        self._surface()
        self.place()

    def _projection_polyline(self):
        nearest_points_to_reference = []
        for i in range(len(self.coordinates)):
            # nearest_points_to_reference.append(Point3D.insert_3d([Point3D.to_2d([self.coordinates[i]], 'y')[0].nearest(
            #     self.polyline.total_line_output, return_index=True)], 'y', [self.coordinates[i].y])[0])
            index, point = Point3D.to_2d([self.coordinates[i]], 'y')[0].nearest(
                self.polyline.total_line_output, return_index=True)
            nearest_points_to_reference.append(
                Point2D(index, self.coordinates[i].y))

        if len(self._remove_collinear_points(nearest_points_to_reference)) >= 4:
            self.polyline_height = Polyline(nearest_points_to_reference)

            self.index_factor = len(
                self.polyline_height.total_line_output)/len(self.polyline.total_line_output)

            for i in range(len(self.polyline.total_line_output)):
                self.polyline_total_line_output[i] = Point3D(
                    self.polyline.total_line_output[i].x, self.polyline_height.total_line_output[round(i*self.index_factor)].y, self.polyline.total_line_output[i].y)

            self._surface()
            self.place()
        # self.polyline_total_line_output = self.polyline_total_line_output[0].optimized_path(
        #     self.polyline_total_line_output)

    def _projection_segment(self):
        s = Segment3D(
            self.coordinates[0], self.coordinates[1])

        reference = s.segment()

        for i in range(len(self.segment_total_line_output)):
            self.output_block.append(((
                self.segment_total_line_output[i].x, reference[self.segment_total_line_output[i].nearest(Point3D.to_2d(reference, 'y'), True)[0]].y, self.segment_total_line_output[i].y), Block("black_concrete")))

    def place(self):
        editor = Editor(buffering=True)
        for i in range(len(self.output_block)):
            editor.placeBlock(self.output_block[i][0],
                              self.output_block[i][1])
