import math
from statistics import mean

from helix.calculators.subarray_helper import get_subarray_sizes_and_rows
from helix.constants.dxf_validation import INVALID_DUAL_TILT_DESIGN
from helix.constants.panel_type import PanelType
from helix.constants.subarray import SUBARRAY_SIZE_BIG
from helix.constants.system_type import SystemType
from helix.helpers.polygon_helper import *
from helix.models.coordinate import Coordinate
from helix.models.dxf.dxf_error import DXFError
from helix.models.dxf.graph_direction import GraphDirection
from helix.models.dxf.graph_node import GraphNode
from helix.models.dxf.graph_node_store import GraphNodeStore
from helix.models.dxf.polygon import Polygon
from helix.models.panel import Panel
from helix.validators.dxf_layer_validator import DXFLayerValidator


class DXFHelper(object):
    def __init__(self):
        self.aurora_detector = DXFLayerValidator()

    def is_new_aurora_format(self):
        """ Determine if it's the new dxf format

            return
                boolean

        """

        return self.aurora_detector.is_new_aurora_format()

    @staticmethod
    def generate_panels(modules, translated_modules):
        """Joins the modules and creates

        Parameters:
            modules (list): List of obtained modules
            translated_modules (list): A list containing the shifted
            positions of the modules
        Returns:
            list

        """

        panels = []
        for idx, module in enumerate(modules):
            x_center = mean([x for x, _ in module.points])
            y_center = mean([y for _, y in module.points])

            rotation = module.determine_orientation()

            x_translated_center = mean([x for x, _ in translated_modules[idx].points])
            y_translated_center = mean([y for _, y in translated_modules[idx].points])

            panels.append(Panel(id=idx + 1, coordinate=Coordinate(
                x_translated_center, y_translated_center, rotation), original_coordinate=Coordinate(x_center, y_center, rotation)))
        return panels

    @staticmethod
    def should_consolidate_modules(modules, system_type, module_constants):
        """Determine if the modules should be consolidated into panel pairs.
        Some dual tilt files came with separate modules, this method determines
        if we should consolidate them.

        Arguments:
            modules (list) List of helix.models.dxf.Polygon
            system_type (object) Type of system selected single tilt, dual tilt
            module_constants (object)
                helix.constants.module_type_constants.dual_tilt_96_cell_constants.DualTilt96CellConstants

        Returns:
            boolean

        """

        if system_type != SystemType.dualTilt:
            return False
        for module in modules:
            points = module.sorted_points()
            p1 = points[0]
            other_points = sorted(points[1:], key=lambda x: (x[0] - p1[0]) ** 2 + (x[1] - p1[1]) ** 2)
            p2 = other_points[1]
            if (math.hypot(p2[0] - p1[0], p2[1] - p1[1]) - min(module_constants.panel_spacing)) < 1e-3:
                return True
        return False

    @staticmethod
    def consolidate_dual_tilt_modules(original_modules, system_type,
                                      pair_spacing):
        """Consolidate the dual tilt modules. This method is called
        with a certain type of dual tilt files that had the modules
        separated

        Parameters:
            original_modules (list) Contains the list of modules from the dxf
                file
            system_type (object) Type of system selected single tilt, dual tilt
            pair_spacing (object) Contains the possible pair spacing that a
            dual tilt system will have

        """

        if system_type == SystemType.dualTilt:
            modules = []
            while len(original_modules) > 0:
                current_module = original_modules.pop(0)
                modules.append(current_module)
                found_pair = False
                for idx, potential_pair in enumerate(original_modules):
                    if current_module.shares_module_on_long_edge(potential_pair,
                                                                 pair_spacing):
                        current_module.consolidate_with(potential_pair,
                                                        pair_spacing)
                        original_modules.pop(idx)
                        found_pair = True
                        break
                if not found_pair:
                    raise DXFError(INVALID_DUAL_TILT_DESIGN)
        else:
            return original_modules
        return modules

    def build_polygons(self, dxf_entities):
        """Obtain two types of polygon objects,
        that represent modules and buildings.
        It's important to understand that the
        polygons are scaled using inches.

        Arguments:
            dxf_entities (list) This list contains
                dxfgrabber.entities

        """

        building_lines = []
        module_lines = []

        for entity in dxf_entities:
            self.aurora_detector.add_layer(entity.layer)
            if entity.layer == 'Modules':  # is a module
                module_lines.append(entity)
            elif entity.layer == 'Roofs' or entity.layer == 'Buildings':  # is a building/roof line
                building_lines.append(entity)

        self.aurora_detector.determine_file()
        inches_per_feet = 12

        buildings = [p.scale(inches_per_feet, inches_per_feet) for p in DXFHelper.generate_polygons(building_lines)]
        modules = [p.scale(inches_per_feet, inches_per_feet) for p in DXFHelper.generate_polygons(module_lines)]
        return buildings, modules

    @staticmethod
    def generate_polygons(lines):
        polygons = []
        for line in lines:
            if len(polygons) == 0 or not polygons[-1].continues_with_line(line):
                polygons.append(Polygon(line))
            elif line.end in polygons[-1].points:
                continue
            else:
                polygons[-1].points.append(line.end)
        return polygons

    @staticmethod
    def translate_towards_origin(buildings, modules):
        """Obtains the minimum value of x and y points and shifts
        all the modules and buildings.

        Arguments:
            buildings (list): List of buildings
            modules (list): List of modules
        Returns:
            tuple

        """

        min_x = float('inf')  # positive infinity
        min_y = float('inf')
        for polygon in buildings + modules:
            min_x = min(min_x, min(x for x, _ in polygon.points))
            min_y = min(min_y, min(y for _, y in polygon.points))

        translated_buildings = [Polygon(points=[(x - min_x, y - min_y) for x, y in polygon.points]) for polygon in buildings]
        translated_modules = [Polygon(points=[(x - min_x, y - min_y) for x, y in polygon.points]) for polygon in modules]

        return translated_buildings, translated_modules

    @staticmethod
    def get_polygons_counterclockwise(polygons):
        """Checks if polygon's points are in counterclockwise order, if not reverse the order.

        Arguments:
            polygons (list): List of polygons
        Returns:
            list of polygons with points in counterclockwise order
        """

        output_polygons = []

        for polygon in polygons:
            points = polygon.points

            # checking if building's points are in clockwise order
            cumulative = 0

            for i in range(len(points)):
                current = points[i - 1]
                next = points[i]
                cumulative += (next[0] - current[0]) * (next[1] + current[1])

            clockwise = cumulative > 0

            # change to counter-clockwise if necessary
            # further code assumes we have counter-clockwise points for the building
            if clockwise:
                points = list(reversed(points))

            output_polygons.append(Polygon(points=[(x, y) for x, y in points]))

        return output_polygons


    @staticmethod
    def build_node_graph(panels, spacing):
        nodes = []
        node_store = GraphNodeStore()
        for panel in panels:
            x_spacing = spacing[0]
            y_spacing = spacing[1]
            node = GraphNode(panel, x_spacing, y_spacing)
            nodes.append(node)

            node_store.add_node(node)

        # Optimization: avoid creating thousands of replicated objects
        graph_directions_cache = {}
        for x in (0, 1, -1):
            for y in (0, 1, -1):
                if x == y == 0:
                    continue
                graph_directions_cache[(x, y)] = GraphDirection((x, y))

        for node in nodes:
            if len(node.neighboring_nodes()) == 8:
                continue

            rotation = math.radians(node.coordinate.rotation)
            # Optimization: Avoid replicate calculations thousands of times
            x_spacing_cos_rotation = (node.x_spacing * math.cos(rotation))
            y_spacing_sin_rotation = (node.y_spacing * math.sin(rotation))
            x_spacing_sin_rotation = (node.x_spacing * math.sin(rotation))
            y_spacing_cos_rotation = (node.y_spacing * math.cos(rotation))

            for x in (0, 1, -1):
                for y in (0, 1, -1):
                    if x == y == 0:
                        continue
                    x_spacing = (x * x_spacing_cos_rotation) - (y * y_spacing_sin_rotation)
                    y_spacing = (x * x_spacing_sin_rotation) + (y * y_spacing_cos_rotation)

                    coordinate = Coordinate(node.coordinate.x + x_spacing, node.coordinate.y + y_spacing, node.coordinate.rotation)
                    if coordinate.x < 0 or coordinate.y < 0:
                        continue

                    # Optimization for `direction = GraphDirection((x, y))`
                    direction = graph_directions_cache[(x, y)]
                    if node.has_existing_neighbor(direction):
                        continue

                    # FIXME: This is the bottleneck of the loop
                    # Calling this ~10000 times needs ~20 seconds
                    neighbor = node_store.find_coordinate(coordinate)
                    if neighbor:
                        node.add_neighbor(neighbor, direction)
            if len(node.neighboring_nodes()) == 0:
                raise DXFError("Error - invalid module spacing. Please check to make sure the correct system type and panel spacing are present")

        return nodes

    @staticmethod
    def detect_subarrays(nodes, panels):
        subarray_number = 0

        def walk_graph_and_assign_subarray(node, subarray_number):
            if node.panel.subarray is not None:
                return
            node.panel.subarray = subarray_number
            for neighbor in node.neighboring_nodes():
                walk_graph_and_assign_subarray(neighbor, subarray_number)

        for node in nodes:
            if node.panel.subarray is None:
                subarray_number += 1
                try:
                    walk_graph_and_assign_subarray(node, subarray_number)
                except RecursionError:
                    raise DXFError(SUBARRAY_SIZE_BIG)

        panels.sort(key=lambda p: p.subarray)
        subarray_list = get_subarray_sizes_and_rows(panels)
        subarrays = {}
        for subarray in subarray_list:
            subarray_number = subarray.subarray_number
            if not subarrays.get(subarray.subarray_number):
                subarrays[subarray_number] = subarray
            else:
                subarrays[subarray_number].size += subarray.size
        return list(subarrays.values())

    @staticmethod
    def detect_panel_types(nodes):
        for node in nodes:
            ordinal_neighbors = node.ordinal_neighbors()
            ordinal_neighbors_count = len(ordinal_neighbors)
            if ordinal_neighbors_count == 4:
                node.panel.panel_type = PanelType.Middle
            elif ordinal_neighbors_count <= 2:
                node.panel.panel_type = PanelType.Corner
            elif ordinal_neighbors.get(GraphDirection.North) is None or ordinal_neighbors.get(GraphDirection.South) is None:
                node.panel.panel_type = PanelType.NorthSouth
            elif ordinal_neighbors.get(GraphDirection.East) is None or ordinal_neighbors.get(GraphDirection.West) is None:
                node.panel.panel_type = PanelType.EastWest
            else:
                raise DXFError("Invalid Array")
        pass

    @staticmethod
    def detect_wind_zones(panels, buildings, modules, l_b, system_type):
        if system_type == SystemType.dualTilt:
            DXFHelper.__detect_dual_tilt_wind_zones__(panels, buildings, modules, l_b)
        else:
            DXFHelper.__detect_single_tilt_wind_zones__(panels, buildings, modules, l_b)

    @staticmethod
    def __detect_dual_tilt_wind_zones__(panels, buildings, modules, l_b):
        sorted_panels = sorted(panels, key=lambda p: p.id)
        wind_zone_a = DXFHelper.__generate_wind_zone_a_dual_tilt__(buildings, l_b)
        fuzzy_wind_zone_a = DXFHelper.__generate_wind_zone_a_fuzzy_dual_tilt__(buildings, l_b)
        wind_zone_b = DXFHelper.__generate_wind_zone_b_dual_tilt__(buildings, l_b)
        fuzzy_wind_zone_b = DXFHelper.__generate_wind_zone_b_fuzzy_dual_tilt__(buildings, l_b)
        wind_zone_c = DXFHelper.__generate_wind_zone_c_dual_tilt__(buildings, l_b)
        fuzzy_wind_zone_c = DXFHelper.__generate_wind_zone_c_fuzzy_dual_tilt__(buildings, l_b)
        wind_zone_d = DXFHelper.__generate_wind_zone_d_dual_tilt__(buildings, l_b)
        fuzzy_wind_zone_d = DXFHelper.__generate_wind_zone_d_fuzzy_dual_tilt__(buildings, l_b)
        for idx, panel in enumerate(sorted_panels):
            module = modules[idx]
            if DXFHelper.__is_panel_in_wind_zone__(module, wind_zone_a):
                panel.wind_zone = 0
            elif DXFHelper.__is_panel_in_wind_zone__(module, fuzzy_wind_zone_a):
                panel.wind_zone = 0
                panel.fuzzy_wind_zone = True
            elif DXFHelper.__is_panel_in_wind_zone__(module, wind_zone_b):
                panel.wind_zone = 1
            elif DXFHelper.__is_panel_in_wind_zone__(module, fuzzy_wind_zone_b):
                panel.wind_zone = 1
                panel.fuzzy_wind_zone = True
            elif DXFHelper.__is_panel_in_wind_zone__(module, wind_zone_c):
                panel.wind_zone = 2
            elif DXFHelper.__is_panel_in_wind_zone__(module, fuzzy_wind_zone_c):
                panel.wind_zone = 2
                panel.fuzzy_wind_zone = True
            elif DXFHelper.__is_panel_in_wind_zone__(module, wind_zone_d):
                panel.wind_zone = 3
            elif DXFHelper.__is_panel_in_wind_zone__(module, fuzzy_wind_zone_d):
                panel.wind_zone = 3
                panel.fuzzy_wind_zone = True
            else:  # wind zone E
                panel.wind_zone = 4

    @staticmethod
    def __detect_single_tilt_wind_zones__(panels, buildings, modules, l_b):
        sorted_panels = sorted(panels, key=lambda p: p.id)
        panel_orientation = panels[0].coordinate.rotation
        wind_zone_a = DXFHelper.__generate_wind_zone_a_single_tilt__(buildings, l_b, panel_orientation)
        fuzzy_wind_zone_a = DXFHelper.__generate_wind_zone_a_fuzzy_single_tilt__(buildings, l_b)
        wind_zone_b = DXFHelper.__generate_wind_zone_b_single_tilt__(buildings, l_b, panel_orientation)
        fuzzy_wind_zone_b = DXFHelper.__generate_wind_zone_b_fuzzy_single_tilt__(buildings, l_b)
        wind_zone_c = DXFHelper.__generate_wind_zone_c_single_tilt__(buildings, l_b, panel_orientation)
        fuzzy_wind_zone_c = DXFHelper.__generate_wind_zone_c_fuzzy_single_tilt__(buildings, l_b)
        wind_zone_d = DXFHelper.__generate_wind_zone_d_single_tilt__(buildings, l_b, panel_orientation)
        fuzzy_wind_zone_d = DXFHelper.__generate_wind_zone_d_fuzzy_single_tilt__(buildings, l_b)
        wind_zone_i = DXFHelper.__generate_wind_zone_i_single_tilt__(buildings, l_b, panel_orientation)
        wind_zone_h = DXFHelper.__generate_wind_zone_h_single_tilt__(buildings, l_b, panel_orientation)
        wind_zone_j = DXFHelper.__generate_wind_zone_j_single_tilt__(buildings, l_b, panel_orientation)
        wind_zone_e = DXFHelper.__generate_wind_zone_e_single_tilt__(buildings, l_b, panel_orientation)
        wind_zone_f = DXFHelper.__generate_wind_zone_f_single_tilt__(buildings, l_b, panel_orientation)
        wind_zone_g = DXFHelper.__generate_wind_zone_g_single_tilt__(buildings, l_b, panel_orientation)
        for idx, panel in enumerate(sorted_panels):
            module = modules[idx]
            if DXFHelper.__is_panel_in_wind_zone__(module, wind_zone_a):
                panel.wind_zone = 0
            elif DXFHelper.__is_panel_in_wind_zone__(module, wind_zone_i):
                panel.wind_zone = 8
            elif DXFHelper.__is_panel_in_wind_zone__(module, fuzzy_wind_zone_a):
                panel.wind_zone = 0
                panel.fuzzy_wind_zone = True
            elif DXFHelper.__is_panel_in_wind_zone__(module, wind_zone_j):
                panel.wind_zone = 9
            elif DXFHelper.__is_panel_in_wind_zone__(module, wind_zone_b):
                panel.wind_zone = 1
            elif DXFHelper.__is_panel_in_wind_zone__(module, fuzzy_wind_zone_b):
                panel.wind_zone = 1
                panel.fuzzy_wind_zone = True
            elif DXFHelper.__is_panel_in_wind_zone__(module, wind_zone_h):
                panel.wind_zone = 7
            elif DXFHelper.__is_panel_in_wind_zone__(module, wind_zone_c):
                panel.wind_zone = 2
            elif DXFHelper.__is_panel_in_wind_zone__(module, fuzzy_wind_zone_c):
                panel.wind_zone = 2
                panel.fuzzy_wind_zone = True
            elif DXFHelper.__is_panel_in_wind_zone__(module, wind_zone_e):
                panel.wind_zone = 4
            elif DXFHelper.__is_panel_in_wind_zone__(module, wind_zone_f):
                panel.wind_zone = 5
            elif DXFHelper.__is_panel_in_wind_zone__(module, wind_zone_d):
                panel.wind_zone = 3
            elif DXFHelper.__is_panel_in_wind_zone__(module, fuzzy_wind_zone_d):
                panel.wind_zone = 3
                panel.fuzzy_wind_zone = True
            elif DXFHelper.__is_panel_in_wind_zone__(module, wind_zone_g):
                panel.wind_zone = 6
            else:
                panel.wind_zone = 10

    @staticmethod
    def __is_panel_in_wind_zone__(module, wind_zone):
        for x, y in module.points:
            for wind_subzone in wind_zone:
                if point_inside_polygon(x, y, wind_subzone):
                    return True
        return False

    @staticmethod
    def __compute_segment_direction(p1, p2):
        """
        Computes direction of a segment. Points taken from building outline are assumed to be in counterclockwise order.

        :param p1: first point
        :param p2: second point
        :return: tuple representing orientation ('north', 'south', 'east', 'west') and variation in degrees from ideally
        directed segment
        """
        segment_angle = (math.degrees(math.atan2(p1[1] - p2[1], p1[0] - p2[0])) + 360) % 360

        if segment_angle >= 315 or segment_angle < 45:
            return 'north', segment_angle if segment_angle < 45 else 360 - segment_angle
        elif 45 <= segment_angle < 135:
            return 'west', abs(90 - segment_angle)
        elif 135 <= segment_angle < 225:
            return 'south', abs(180 - segment_angle)
        else:
            return 'east', abs(270 - segment_angle)

    @staticmethod
    def compute_corner_directions(vertex, prev, next, angle_correction):
        """
        Determines if point is located in north/east corner

        :param vertex: point located in the corner
        :param prev: point previous to vertex, assuming counterclockwise order
        :param next: point next to vertex, assuming counterclockwise order
        :param angle_correction: points are rotated by this angle first, in degrees
        :return: (is_north, is_east) tuple
        """

        rotated = DXFHelper.__rotate_points([vertex, prev, next], math.radians(angle_correction))

        vertex_rotated, prev_rotated, next_rotated = rotated[0], rotated[1], rotated[2]

        prev_segment_orientation, prev_dev = DXFHelper.__compute_segment_direction(prev_rotated, vertex_rotated)
        next_segment_orientation, next_dev = DXFHelper.__compute_segment_direction(vertex_rotated, next_rotated)

        is_north = prev_segment_orientation == 'north' or next_segment_orientation == 'north'

        dirs = (prev_segment_orientation, next_segment_orientation)

        if 'north' in dirs and 'south' not in dirs:
            is_north = True
        elif 'south' in dirs and 'north' not in dirs:
            is_north = False
        elif dirs == ('north', 'south'):
            is_north = prev_dev < next_dev
        elif dirs == ('south', 'north'):
            is_north = next_dev < prev_dev
        elif dirs == ('east', 'west'):
            is_north = True
        elif dirs == ('west', 'east'):
            is_north = False
        elif dirs == ('west', 'west') or dirs == ('east', 'east'):
            is_north = prev_rotated[0] > next_rotated[0]

        if 'east' in dirs and 'west' not in dirs:
            is_east = True
        elif 'west' in dirs and 'east' not in dirs:
            is_east = False
        elif dirs == ('east', 'west'):
            is_east = prev_dev < next_dev
        elif dirs == ('west', 'east'):
            is_east = next_dev < prev_dev
        elif dirs == ('north', 'south'):
            is_east = False
        elif dirs == ('south', 'north'):
            is_east = True
        elif dirs == ('north', 'north') or dirs == ('south', 'south'):
            is_east = prev_rotated[1] < next_rotated[1]

        return is_north, is_east

    @staticmethod
    def __rotate_points(points, angle):
        """
        :param points: points to be rotated as list of tuples
        :param angle: angle in radians
        :return: list of rotated points
        """
        sin = math.sin(angle)
        cos = math.cos(angle)

        return [(x * cos - y * sin, x * sin + y * cos) for x, y in points]


    @staticmethod
    def __rotate_point(point, angle):
        return DXFHelper.__rotate_points([point], angle)[0]


    @staticmethod
    def __generate_wind_zone__(buildings, scaling_factor, points_callback, panel_orientation):
        """
        Important: polygons representing buildings are expected to have points in counterclockwise order
        """

        wind_zones = []
        for building in buildings:
            for idx, vertex in enumerate(building.points):
                prev = building.points[(idx - 1) % len(building.points)]
                next = building.points[(idx + 1) % len(building.points)]

                a_sq = (vertex[0] - prev[0]) ** 2 + (vertex[1] - prev[1]) ** 2
                b_sq = (vertex[0] - next[0]) ** 2 + (vertex[1] - next[1]) ** 2
                c_sq = (next[0] - prev[0]) ** 2 + (next[1] - prev[1]) ** 2

                distance_to_prev = math.sqrt(a_sq)
                distance_to_next = math.sqrt(b_sq)
                angle = math.acos((c_sq - a_sq - b_sq) / (-2 * distance_to_prev * distance_to_next))

                # angles between x-axis and line created by current and next/previous vertex
                angle_to_next = math.atan2(next[1] - vertex[1], next[0] - vertex[0])
                angle_to_prev = math.atan2(prev[1] - vertex[1], prev[0] - vertex[0])

                is_north, is_east = DXFHelper.compute_corner_directions(vertex, prev, next, -panel_orientation)

                relative_angle = ((angle_to_next - angle_to_prev + 2 * math.pi) % (2 * math.pi)) - math.pi

                if abs(angle_to_next - math.radians(panel_orientation)) > abs(angle_to_prev - math.radians(panel_orientation)):
                    max_distance = distance_to_next
                else:
                    max_distance = distance_to_prev

                orientation = angle_to_next

                def generate_point(x, y):
                    d_x, d_y = DXFHelper.__rotate_point((x, y), orientation)
                    return vertex[0] + d_x, vertex[1] + d_y

                if angle <= math.radians(135) and relative_angle >= 0:
                    points = points_callback(angle, is_north, is_east, max_distance)
                    if points:
                        wind_zones.append([generate_point(scaling_factor * x, scaling_factor * y) for x, y in points])
        return wind_zones

    @staticmethod
    def __generate_dual_tilt_wind_zone_with_transforms__(buildings, scaling_factor, points):
        def callback(angle, *_):
            if angle < math.radians(80):
                return []
            rotation = angle - math.radians(90)

            def inner_transform(x, y, perform_rotation, rotation_value=rotation):
                if perform_rotation:
                    return DXFHelper.__rotate_point((x, y), rotation_value)
                else:
                    return x, y

            transformed_points = []
            for x, y, apply_rotation, rotation_scale in points:
                if rotation_scale is not None:
                    rotation_value = rotation_scale * rotation
                else:
                    rotation_value = rotation
                transformed_points.append(inner_transform(x, y, apply_rotation, rotation_value))

            return transformed_points

        return DXFHelper.__generate_wind_zone__(buildings, scaling_factor, callback, 0)

    @staticmethod
    def __generate_wind_zone_a_dual_tilt__(buildings, scaling_factor):
        wind_zone_points = [
            (0, 0, False, None),
            (2, 0, False, None),
            (2, 1, False, None),
            (1, 1, True, 0.5),
            (1, 2, True, None),
            (0, 2, True, None)
        ]

        return DXFHelper.__generate_dual_tilt_wind_zone_with_transforms__(buildings, scaling_factor, wind_zone_points)

    @staticmethod
    def __generate_wind_zone_b_dual_tilt__(buildings, scaling_factor):
        wind_zone_points = [
            (1, 1, True, 0.5),
            (2, 1, False, None),
            (2, 0, False, None),
            (2, 0, False, None),
            (4, 0, False, None),
            (4, 2, False, None),
            (2, 2, True, 0.5),
            (2, 4, True, None),
            (0, 4, True, None),
            (0, 2, True, None),
            (1, 2, True, None),
        ]
        return DXFHelper.__generate_dual_tilt_wind_zone_with_transforms__(buildings, scaling_factor, wind_zone_points)

    @staticmethod
    def __generate_wind_zone_c_dual_tilt__(buildings, scaling_factor):
        wind_zone_points = [
            (2, 2, True, 0.5),
            (4, 2, False, None),
            (4, 0, False, None),
            (6, 0, False, None),
            (6, 3, False, None),
            (3, 3, True, 0.5),
            (3, 6, True, None),
            (0, 6, True, None),
            (0, 4, True, None),
            (2, 4, True, None),
        ]
        return DXFHelper.__generate_dual_tilt_wind_zone_with_transforms__(buildings, scaling_factor, wind_zone_points)

    @staticmethod
    def __generate_wind_zone_d_dual_tilt__(buildings, scaling_factor):
        wind_zone_points = [
            (3, 3, True, 0.5),
            (6, 3, False, None),
            (6, 0, False, None),
            (8, 0, False, None),
            (8, 4, False, None),
            (4, 8, True, None),
            (0, 8, True, None),
            (0, 6, True, None),
            (3, 6, True, None),
        ]
        return DXFHelper.__generate_dual_tilt_wind_zone_with_transforms__(buildings, scaling_factor, wind_zone_points)

    @staticmethod
    def __generate_fuzzy_wind_zone_dual_tilt__(buildings, scaling_factor, inner_l_b, outer_l_b):
        def callback(angle, *_):
            if angle >= math.radians(80):
                return []
            number_points_on_inner_arc = int(math.ceil(inner_l_b * scaling_factor / 10))
            number_points_on_outer_arc = int(math.ceil(outer_l_b * scaling_factor / 10))

            inner_points_length = (inner_l_b, number_points_on_inner_arc)
            outer_points_length = (outer_l_b, number_points_on_outer_arc)

            points = []
            if inner_l_b == 0:
                points.append((0, 0))
            for idx, (l_b_length, number_points) in enumerate((inner_points_length, outer_points_length)):
                if number_points == 0:
                    continue
                for step in range(number_points + 1):
                    sub_angle = (angle / number_points) * step
                    x = l_b_length * math.cos(sub_angle)
                    y = l_b_length * math.sin(sub_angle)
                    if idx == 0:
                        points.append((x, y))
                    else:
                        points.insert(0, (x, y))
            return points

        return DXFHelper.__generate_wind_zone__(buildings, scaling_factor, callback, 0)

    @staticmethod
    def __generate_wind_zone_a_fuzzy_dual_tilt__(buildings, scaling_factor):
        return DXFHelper.__generate_fuzzy_wind_zone_dual_tilt__(buildings, scaling_factor, 0, math.sqrt(5))

    @staticmethod
    def __generate_wind_zone_b_fuzzy_dual_tilt__(buildings, scaling_factor):
        return DXFHelper.__generate_fuzzy_wind_zone_dual_tilt__(buildings, scaling_factor, math.sqrt(5), math.sqrt(20))

    @staticmethod
    def __generate_wind_zone_c_fuzzy_dual_tilt__(buildings, scaling_factor):
        return DXFHelper.__generate_fuzzy_wind_zone_dual_tilt__(buildings, scaling_factor, math.sqrt(20), math.sqrt(45))

    @staticmethod
    def __generate_wind_zone_d_fuzzy_dual_tilt__(buildings, scaling_factor):
        return DXFHelper.__generate_fuzzy_wind_zone_dual_tilt__(buildings, scaling_factor, math.sqrt(45), math.sqrt(80))

    # Single Tilt

    @staticmethod
    def __generate_single_tilt_wind_zone_with_transforms__(buildings, scaling_factor, northern_zone, points, panel_orientation):
        def callback(angle, is_north, is_east, max_distance):
            if is_north != northern_zone:
                return []
            if angle < math.radians(80):
                return []
            rotation = angle - math.radians(90)

            def inner_transform(x, y, perform_rotation, rotation_value=rotation):
                if y == -1:
                    y = max_distance / scaling_factor
                if not is_east:
                    perform_rotation = not perform_rotation

                    # swap x,y coordinates - reflection about line x = y
                    new_x = y
                    new_y = x
                    x = new_x
                    y = new_y
                if perform_rotation:
                    return DXFHelper.__rotate_point((x, y), rotation_value)
                else:
                    return x, y

            transformed_points = []
            for x, y, apply_rotation, rotation_scale in points:
                if rotation_scale is not None:
                    rotation_value = rotation_scale * rotation
                else:
                    rotation_value = rotation
                transformed_points.append(inner_transform(x, y, apply_rotation, rotation_value))

            return transformed_points

        return DXFHelper.__generate_wind_zone__(buildings, scaling_factor, callback, panel_orientation)

    @staticmethod
    def __generate_wind_zone_a_single_tilt__(buildings, scaling_factor, panel_orientation):
        wind_zone_points = [
            (0, 0, False, None),
            (0, 2, True, None),
            (2, 2, True, 0.5),
        ]
        return DXFHelper.__generate_single_tilt_wind_zone_with_transforms__(buildings, scaling_factor, True,
                                                                            wind_zone_points, panel_orientation)

    @staticmethod
    def __generate_wind_zone_b_single_tilt__(buildings, scaling_factor, panel_orientation):
        first_wind_zone_points = [
            (0, 0, False, None),
            (2, 0, False, None),
            (2, 2, True, 0.5),
        ]
        first_wind_zone_polygons = DXFHelper.__generate_single_tilt_wind_zone_with_transforms__(buildings,
                                                                                                scaling_factor, True,
                                                                                                first_wind_zone_points,
                                                                                                panel_orientation)

        second_wind_zone_points = [
            (0, 2, True, None),
            (0, 4, True, None),
            (1, 4, True, None),
            (2, 3, True, None),
            (2, 2, True, 0.5),
        ]
        second_wind_zone_polygons = DXFHelper.__generate_single_tilt_wind_zone_with_transforms__(buildings,
                                                                                                 scaling_factor, True,
                                                                                                 second_wind_zone_points,
                                                                                                 panel_orientation)
        return first_wind_zone_polygons + second_wind_zone_polygons

    @staticmethod
    def __generate_wind_zone_c_single_tilt__(buildings, scaling_factor, panel_orientation):
        wind_zone_points = [
            (2, 0, False, None),
            (4, 0, False, None),
            (4, 4, True, 0.5),
            (2, 6, True, None),
            (0, 6, True, None),
            (0, 4, True, None),
            (1, 4, True, None),
            (2, 3, True, None),
        ]
        return DXFHelper.__generate_single_tilt_wind_zone_with_transforms__(buildings, scaling_factor, True,
                                                                            wind_zone_points, panel_orientation)

    @staticmethod
    def __generate_wind_zone_d_single_tilt__(buildings, scaling_factor, panel_orientation):
        wind_zone_points = [
            (4, 0, False, None),
            (6, 0, False, None),
            (6, 6, True, 0.5),
            (4, 8, True, None),
            (0, 8, True, None),
            (0, 6, True, None),
            (2, 6, True, None),
            (2, 6, True, None),
            (4, 4, True, 0.5),
        ]
        return DXFHelper.__generate_single_tilt_wind_zone_with_transforms__(buildings, scaling_factor, True,
                                                                            wind_zone_points, panel_orientation)

    @staticmethod
    def __generate_wind_zone_i_single_tilt__(buildings, scaling_factor, panel_orientation):
        wind_zone_points = [
            (0, 0, False, None),
            (0, 2, True, None),
            (1.5, 2, True, 2.0 / 3.0),
            (1, 0, False, None),
        ]
        return DXFHelper.__generate_single_tilt_wind_zone_with_transforms__(buildings, scaling_factor, False,
                                                                            wind_zone_points, panel_orientation)

    @staticmethod
    def __generate_wind_zone_h_single_tilt__(buildings, scaling_factor, panel_orientation):
        wind_zone_points = [
            (0, 2, True, None),
            (0, 4, True, None),
            (2, 4, True, 2.0 / 3.0),
            (1.5, 2, True, 2.0 / 3.0),
        ]
        return DXFHelper.__generate_single_tilt_wind_zone_with_transforms__(buildings, scaling_factor, False,
                                                                            wind_zone_points, panel_orientation)

    @staticmethod
    def __generate_wind_zone_j_single_tilt__(buildings, scaling_factor, panel_orientation):
        wind_zone_points = [
            (1, 0, False, None),
            (3, 0, False, None),
            (3, 2, True, 2.0 / 3.0),
            (1.5, 2, True, 2.0 / 3.0),
        ]
        return DXFHelper.__generate_single_tilt_wind_zone_with_transforms__(buildings, scaling_factor, False,
                                                                            wind_zone_points, panel_orientation)

    @staticmethod
    def __generate_wind_zone_e_single_tilt__(buildings, scaling_factor, panel_orientation):
        wind_zone_points = [
            (0, 4, True, None),
            (0, 7, True, None),
            (3, 7, True, 2.0 / 3.0),
            (2, 4, True, 2.0 / 3.0),
        ]
        return DXFHelper.__generate_single_tilt_wind_zone_with_transforms__(buildings, scaling_factor, False,
                                                                            wind_zone_points, panel_orientation)

    @staticmethod
    def __generate_wind_zone_f_single_tilt__(buildings, scaling_factor, panel_orientation):
        wind_zone_points = [
            (3, 0, False, None),
            (8, 0, False, None),
            (8, 2, False, None),
            (3, 7, True, 2.0 / 3.0),
            (2, 4, True, 2.0 / 3.0),
            (1.5, 2, True, 2.0 / 3.0),
            (3, 2, True, 2.0 / 3.0),
        ]
        return DXFHelper.__generate_single_tilt_wind_zone_with_transforms__(buildings, scaling_factor, False,
                                                                            wind_zone_points, panel_orientation)

    @staticmethod
    def __generate_wind_zone_g_single_tilt__(buildings, scaling_factor, panel_orientation):
        wind_zone_points = [
            (0, 7, True, None),
            (0, -1, True, None),
            (4, -1, True, None),
            (4, 6, True, 2.0 / 3.0),
            (3, 7, True, 2.0 / 3.0),
        ]
        return DXFHelper.__generate_single_tilt_wind_zone_with_transforms__(buildings, scaling_factor, False,
                                                                            wind_zone_points, panel_orientation)

    @staticmethod
    def __generate_fuzzy_wind_zone_single_tilt__(buildings, scaling_factor, inner_l_b, outer_l_b):
        def callback(angle, *_):
            if angle >= math.radians(80):
                return []
            number_points_on_inner_arc = int(math.ceil(inner_l_b * scaling_factor / 10))
            number_points_on_outer_arc = int(math.ceil(outer_l_b * scaling_factor / 10))

            inner_points_length = (inner_l_b, number_points_on_inner_arc)
            outer_points_length = (outer_l_b, number_points_on_outer_arc)

            points = []
            if inner_l_b == 0:
                points.append((0, 0))
            for idx, (l_b_length, number_points) in enumerate((inner_points_length, outer_points_length)):
                if number_points == 0:
                    continue
                start = 0
                end = number_points
                for step in range(start, end + 1):
                    sub_angle = (angle / number_points) * step
                    x = l_b_length * math.cos(sub_angle)
                    y = l_b_length * math.sin(sub_angle)
                    if idx == 0:
                        points.append((x, y))
                    else:
                        points.insert(0, (x, y))
            return points

        return DXFHelper.__generate_wind_zone__(buildings, scaling_factor, callback, 0)

    @staticmethod
    def __generate_wind_zone_a_fuzzy_single_tilt__(buildings, scaling_factor):
        return DXFHelper.__generate_fuzzy_wind_zone_single_tilt__(buildings, scaling_factor, 0, math.sqrt(8))

    @staticmethod
    def __generate_wind_zone_b_fuzzy_single_tilt__(buildings, scaling_factor):
        return DXFHelper.__generate_fuzzy_wind_zone_single_tilt__(buildings, scaling_factor, math.sqrt(8), math.sqrt(20))

    @staticmethod
    def __generate_wind_zone_c_fuzzy_single_tilt__(buildings, scaling_factor):
        return DXFHelper.__generate_fuzzy_wind_zone_single_tilt__(buildings, scaling_factor, math.sqrt(20), math.sqrt(44))

    @staticmethod
    def __generate_wind_zone_d_fuzzy_single_tilt__(buildings, scaling_factor):
        return DXFHelper.__generate_fuzzy_wind_zone_single_tilt__(buildings, scaling_factor, math.sqrt(44),
                                                                  math.sqrt(80))

    @staticmethod
    def l_b_polygons(buildings, scaling_factor, system_type, panel_orientation):
        if system_type == SystemType.dualTilt:
            return DXFHelper.__dual_tilt_wind_zone_polygons__(buildings, scaling_factor)
        else:
            return DXFHelper.__single_tilt_wind_zone_polygons__(buildings, scaling_factor, panel_orientation)

    @staticmethod
    def __dual_tilt_wind_zone_polygons__(buildings, scaling_factor):
        wind_zone_a = DXFHelper.__generate_wind_zone_a_dual_tilt__(buildings, scaling_factor)
        wind_zone_a += DXFHelper.__generate_wind_zone_a_fuzzy_dual_tilt__(buildings, scaling_factor)
        polygons = []
        for wind_zone in wind_zone_a:
            polygon = Polygon(points=wind_zone)
            polygon.color = "red"
            polygons.append(polygon)
        wind_zone_b = DXFHelper.__generate_wind_zone_b_dual_tilt__(buildings, scaling_factor)
        wind_zone_b += DXFHelper.__generate_wind_zone_b_fuzzy_dual_tilt__(buildings, scaling_factor)
        for wind_zone in wind_zone_b:
            polygon = Polygon(points=wind_zone)
            polygon.color = "green"
            polygons.append(polygon)
        wind_zone_c = DXFHelper.__generate_wind_zone_c_dual_tilt__(buildings, scaling_factor)
        wind_zone_c += DXFHelper.__generate_wind_zone_c_fuzzy_dual_tilt__(buildings, scaling_factor)
        for wind_zone in wind_zone_c:
            polygon = Polygon(points=wind_zone)
            polygon.color = "blue"
            polygons.append(polygon)
        wind_zone_d = DXFHelper.__generate_wind_zone_d_dual_tilt__(buildings, scaling_factor)
        wind_zone_d += DXFHelper.__generate_wind_zone_d_fuzzy_dual_tilt__(buildings, scaling_factor)
        for wind_zone in wind_zone_d:
            polygon = Polygon(points=wind_zone)
            polygon.color = "black"
            polygons.append(polygon)
        return reversed(polygons)

    @staticmethod
    def __single_tilt_wind_zone_polygons__(buildings, scaling_factor, panel_orientation):
        wind_zone_a = DXFHelper.__generate_wind_zone_a_single_tilt__(buildings, scaling_factor, panel_orientation)
        wind_zone_a += DXFHelper.__generate_wind_zone_a_fuzzy_single_tilt__(buildings, scaling_factor)
        polygons = []
        for wind_zone in wind_zone_a:
            polygon = Polygon(points=wind_zone)
            polygon.color = "red"
            polygons.append(polygon)
        wind_zone_i = DXFHelper.__generate_wind_zone_i_single_tilt__(buildings, scaling_factor, panel_orientation)
        for wind_zone in wind_zone_i:
            polygon = Polygon(points=wind_zone)
            polygon.color = "orange"
            polygons.append(polygon)
        wind_zone_j = DXFHelper.__generate_wind_zone_j_single_tilt__(buildings, scaling_factor, panel_orientation)
        for wind_zone in wind_zone_j:
            polygon = Polygon(points=wind_zone)
            polygon.color = "forestgreen"
            polygons.append(polygon)
        wind_zone_b = DXFHelper.__generate_wind_zone_b_single_tilt__(buildings, scaling_factor, panel_orientation)
        wind_zone_b += DXFHelper.__generate_wind_zone_b_fuzzy_single_tilt__(buildings, scaling_factor)
        for wind_zone in wind_zone_b:
            polygon = Polygon(points=wind_zone)
            polygon.color = "green"
            polygons.append(polygon)
        wind_zone_h = DXFHelper.__generate_wind_zone_h_single_tilt__(buildings, scaling_factor, panel_orientation)
        for wind_zone in wind_zone_h:
            polygon = Polygon(points=wind_zone)
            polygon.color = "yellow"
            polygons.append(polygon)
        wind_zone_c = DXFHelper.__generate_wind_zone_c_single_tilt__(buildings, scaling_factor, panel_orientation)
        wind_zone_c += DXFHelper.__generate_wind_zone_c_fuzzy_single_tilt__(buildings, scaling_factor)
        for wind_zone in wind_zone_c:
            polygon = Polygon(points=wind_zone)
            polygon.color = "blue"
            polygons.append(polygon)
        wind_zone_e = DXFHelper.__generate_wind_zone_e_single_tilt__(buildings, scaling_factor, panel_orientation)
        for wind_zone in wind_zone_e:
            polygon = Polygon(points=wind_zone)
            polygon.color = "purple"
            polygons.append(polygon)
        wind_zone_f = DXFHelper.__generate_wind_zone_f_single_tilt__(buildings, scaling_factor, panel_orientation)
        for wind_zone in wind_zone_f:
            polygon = Polygon(points=wind_zone)
            polygon.color = "grey"
            polygons.append(polygon)
        wind_zone_d = DXFHelper.__generate_wind_zone_d_single_tilt__(buildings, scaling_factor, panel_orientation)
        wind_zone_d += DXFHelper.__generate_wind_zone_d_fuzzy_single_tilt__(buildings, scaling_factor)
        for wind_zone in wind_zone_d:
            polygon = Polygon(points=wind_zone)
            polygon.color = "black"
            polygons.append(polygon)
        wind_zone_g = DXFHelper.__generate_wind_zone_g_single_tilt__(buildings, scaling_factor, panel_orientation)
        for wind_zone in wind_zone_g:
            polygon = Polygon(points=wind_zone)
            polygon.color = "hotpink"
            polygons.append(polygon)
        return reversed(polygons)
