from math import ceil, floor

from helix.calculators.subarray_helper import extract_subarray

from helix.constants.global_constants import minimum_racking_capacity
from helix.constants.panel_type import PanelType
from helix.constants.file_validation_error import FileValidationMessage,FileValidationException
from helix.models.subarray import Subarray



class SeismicCalculator(object):
    def __init__(self, values, graph_repository):
        self.values = values
        self.system_type = values.system_type()
        self.system_constants = values.module_system_constants()
        self.anchor_type = values.anchor_type()
        self.graph_repository = graph_repository

    def assign_seismic_anchors(self, subarray, panels):
        panel_data_for_subarray = extract_subarray(panels, subarray.subarray_number)
        self.assign_anchors_to_subarray(panel_data_for_subarray, subarray)
        return panels

    def assign_anchors_to_subarray(self, panels, subarray):
        sds = self.values.spectral_response()
        wind_anchors = sum([panel.wind_anchors for panel in panels])
        for panel in panels:
            if panel.seismic_anchors is None:
                panel.seismic_anchors = 0
        required_anchors = subarray.required_seismic_anchors
        if required_anchors == 0 and (wind_anchors == 0 or sds < 1):
            return panels

        graph = self.graph_repository.subarray_graph(subarray.subarray_number)
        if len(graph.nodes) == 0:
            return panels

        more_anchors_needed = True
        perimeter_covered = sds < 1.0
        anchor_threshold = 0
        was_rung_empty = False
        while more_anchors_needed:
            rung = graph.pop_rung()
            interval = int(self.seismic_anchor_interval())
            nodes_since_last_anchor = interval
            if len(rung) == 0:
                if was_rung_empty:
                    # detected an infinite loop
                    # something is wrong with the input file
                    # probably panels overlapping
                    raise FileValidationException(FileValidationMessage.PanelsTooClose.value)
                graph.reset()
                anchor_threshold += 1
                was_rung_empty = True
                continue
            while more_anchors_needed and interval >= 0:
                for node in rung:
                    nodes_since_last_anchor += 1
                    if node.wind_anchor + node.seismic_anchor > anchor_threshold:
                        nodes_since_last_anchor = 0

                    if nodes_since_last_anchor > interval:
                        node.assign_seismic_anchor()
                        required_anchors -= 1
                        nodes_since_last_anchor = 0

                    more_anchors_needed = (not perimeter_covered) or required_anchors > 0
                    if not more_anchors_needed:
                        break

                perimeter_covered = True
                if interval <= 1:
                    interval -= 1
                else:
                    interval /= 2

        for idx, node in enumerate(graph.nodes):
            panels[idx].seismic_anchors = node.seismic_anchor

        return panels

    def compute_provided_lateral_capacity(self, subarray_number, panels):
        subarray_panels = extract_subarray(panels, subarray_number)

        anchors = {PanelType.Corner: 0, PanelType.NorthSouth: 0, PanelType.EastWest: 0, PanelType.Middle: 0}

        for panel in subarray_panels:
            if panel.seismic_anchors is not None:
                anchors[panel.panel_type] += panel.seismic_anchors

        return self.anchors_shear_capacity(anchors[PanelType.Corner], anchors[PanelType.NorthSouth],
                                           anchors[PanelType.EastWest], anchors[PanelType.Middle])

    def seismic_anchors_for_subarray(self, F_p, subarray_weight, spectral_response, friction_coefficient,
                                     seismic_anchors, corner_anchors,
                                     north_south_anchors, east_west_anchors, middle_anchors, shear_capacity):
        demand = self.seismic_demand_for_subarray(F_p, subarray_weight, spectral_response, friction_coefficient,
                                                  seismic_anchors, corner_anchors, north_south_anchors,
                                                  east_west_anchors, middle_anchors)
        return ceil(demand / shear_capacity)

    def anchors_shear_capacity(self, corner_anchors, north_south_anchors, east_west_anchors, middle_anchors):

        anchor_shear_capacity = self.anchor_type.shear_capacity()
        panel_racking_capacity = self.system_constants.racking_capacity

        corner_capacity = corner_anchors * min(anchor_shear_capacity, panel_racking_capacity(PanelType.Corner))
        north_south_capacity = north_south_anchors * min(anchor_shear_capacity, panel_racking_capacity(PanelType.NorthSouth))
        east_west_capacity = east_west_anchors * min(anchor_shear_capacity, panel_racking_capacity(PanelType.EastWest))
        middle_capacity = middle_anchors * min(anchor_shear_capacity, panel_racking_capacity(PanelType.Middle))

        return corner_capacity + north_south_capacity + east_west_capacity + middle_capacity

    def seismic_demand_for_subarray(self, F_p, subarray_weight, spectral_response, friction_coefficient,
                                     seismic_anchors, corner_anchors,
                                     north_south_anchors, east_west_anchors, middle_anchors):
        if (corner_anchors + north_south_anchors + east_west_anchors + middle_anchors == 0) and seismic_anchors == 0:
            return 0

        existing_shear_resistance = self.anchors_shear_capacity(corner_anchors, north_south_anchors, east_west_anchors,
                                                                middle_anchors)

        shear_force = 0.7 * F_p * subarray_weight - (
            (0.6 - 0.14 * spectral_response) * 0.7 * friction_coefficient * subarray_weight) - existing_shear_resistance

        return max(shear_force, 0)

    def required_force_seismic_anchors(self, subarray_number, panels):

        demand = self.required_force_seismic_demand(subarray_number, panels)

        system_shear_capacity = min(self.anchor_type.shear_capacity(),
                                    minimum_racking_capacity)

        return ceil(demand / system_shear_capacity)

    def required_force_seismic_demand(self, subarray_number, panels):
        subarray_panels = extract_subarray(panels, subarray_number)

        importance_factor = self.values.importance_factor()
        spectral_response = self.values.spectral_response()
        F_p = 1.2 * spectral_response / (1.5 / importance_factor)

        # number of wind anchors by panel type
        anchors = {PanelType.Corner: 0,
                   PanelType.NorthSouth: 0,
                   PanelType.EastWest: 0,
                   PanelType.Middle: 0}

        # total weight
        subarray_weight = 0

        # total number of seismic anchors
        seismic_anchors = 0

        for panel in subarray_panels:
            if panel.seismic_anchors is not None:
                seismic_anchors += panel.seismic_anchors

            # it could be calculated before the loop to avoid redundant calculations
            effective_area = self.system_constants.surface_area / self.system_constants.ground_coverage_ratio
            weight = panel.pressure * effective_area

            anchors[panel.panel_type] += panel.wind_anchors
            subarray_weight += weight

        force_required_demand = self.seismic_demand_for_subarray(F_p, subarray_weight, spectral_response,
                                                                   self.system_constants.friction_coefficient,
                                                                   seismic_anchors,
                                                                   anchors[PanelType.Corner],
                                                                   anchors[PanelType.NorthSouth],
                                                                   anchors[PanelType.EastWest],
                                                                   anchors[PanelType.Middle])
        return force_required_demand

    def required_geometric_seismic_anchors(self, subarray_number, panels):
        if panels[0].coordinate is None or self.values.spectral_response() < 1:
            return 0
        panel_data_for_subarray = extract_subarray(panels, subarray_number)
        subarray = Subarray(required_seismic_anchors=0, subarray_number=subarray_number)
        anchors_for_subarray = self.assign_anchors_to_subarray(panel_data_for_subarray, subarray)
        return sum(panel.seismic_anchors for panel in anchors_for_subarray if panel.seismic_anchors)

    def seismic_anchor_interval(self):
        sds = self.values.spectral_response()
        importance_factor = self.values.importance_factor()
        interval_constant, interval_multiplier = self.system_constants.seismic_anchor_interval_constants
        denom = (22.96 * importance_factor * sds - interval_constant + interval_multiplier * sds)
        if denom <= 0:
            return 15
        return floor(minimum_racking_capacity / denom)
