from collections import namedtuple, OrderedDict
from math import ceil, floor

from helix.constants.panel_type import PanelType
from helix.constants.system_type import SystemType
from helix.models.panel import Panel, PanelWarnings

Result = namedtuple('Result', ['ballast_count', 'link_tray_count', 'cross_tray_count', 'system_weight', 'needs_anchor'])


class BallastCalculator(object):
    def __init__(self, user_values):
        self.values = user_values
        self.system_type = user_values.system_type()
        self.anchor_type = user_values.anchor_type()
        self.system_constants = self.system_type.system_constants()
        self.module_constants = user_values.module_system_constants()

    def ballast_and_trays_matrix(self, c_p_matrix, q_z, panels, ballast_block_weight=None):
        if not ballast_block_weight:
            ballast_block_weight = self.values.ballast_block_weight()
        ballast_store = self.calculate_ballast_store(c_p_matrix, q_z, ballast_block_weight)

        for idx, panel in enumerate(panels):
            stored_panel = ballast_store[panel.panel_type][panel.wind_zone][panel.fuzzy_wind_zone]
            panels[idx] = stored_panel.merge(panel)
        return panels

    def update_ballast(self, c_p_matrix, q_z, panels):
        ballast_block_weight = self.values.ballast_block_weight()
        ballast_store = self.calculate_ballast_store(c_p_matrix, q_z, ballast_block_weight)

        seismic_ballast_store = {}

        for panel in panels:
            seismic_anchors = panel.seismic_anchors if panel.seismic_anchors else 0
            if seismic_anchors != 0:
                key = hash(panel.wind_zone) + hash(panel.panel_type) + seismic_anchors + hash(panel.fuzzy_wind_zone)  # hack
                stored_panel = seismic_ballast_store.get(key)
                if stored_panel:
                    panel.ballast = stored_panel.ballast
                    panel.link_tray = stored_panel.link_tray
                    panel.cross_tray = stored_panel.cross_tray
                    panel.pressure = stored_panel.pressure
                else:
                    anchors = panel.wind_anchors + seismic_anchors
                    c_p = c_p_matrix[panel.wind_zone, panel.panel_type.index()] * (1.15 if panel.fuzzy_wind_zone else 1)
                    force = self.uplift(c_p, q_z) - anchors * self.anchor_type.uplift_capacity()
                    ballast_and_tray_count = self.ballast_and_tray_count(force, panel.panel_type, ballast_block_weight, anchors)
                    pressure = self.calculate_pressure_on_roof(ballast_and_tray_count.ballast_count, ballast_block_weight, ballast_and_tray_count.system_weight)

                    panel.ballast = ballast_and_tray_count.ballast_count
                    panel.link_tray = ballast_and_tray_count.link_tray_count
                    panel.cross_tray = ballast_and_tray_count.cross_tray_count
                    panel.pressure = pressure

                    seismic_ballast_store[key] = panel
            else:
                stored_panel = ballast_store[panel.panel_type][panel.wind_zone][panel.fuzzy_wind_zone]

                panel.ballast = stored_panel.ballast
                panel.link_tray = stored_panel.link_tray
                panel.cross_tray = stored_panel.cross_tray
                panel.pressure = stored_panel.pressure
        return panels

    def calculate_ballast_store(self, cp_matrix, qz, ballast_block_weight):
        max_psf = self.values.max_system_pressure()
        store = {}
        for panel_type in PanelType.all():
            sub_store = {}
            for wind_zone, _ in enumerate(self.values.system_type().system_constants().wind_zones):
                sub_store[wind_zone] = {}
                for use_fuzzy in (True, False):
                    sub_store[wind_zone][use_fuzzy] = self.ballast_tray_and_anchor_count(wind_zone=wind_zone,
                                                                                         panel_type=panel_type,
                                                                                         ballast_block_weight=ballast_block_weight,
                                                                                         max_system_pressure=max_psf,
                                                                                         c_p_matrix=cp_matrix,
                                                                                         q_z=qz,
                                                                                         use_fuzzy=use_fuzzy)
            store[panel_type] = sub_store

        return store

    def summary_table(self, c_p_matrix, q_z):
        wind_zones = self.system_constants.wind_zones
        ballast_block_weight = self.values.ballast_block_weight()
        max_system_pressure = self.values.max_system_pressure()

        table = OrderedDict()
        for panel_type in PanelType.all():
            ballast_counts = []
            anchor_counts = []
            pressures = []
            warnings = []
            for wind_zone_index, _ in enumerate(wind_zones):
                ballast_tray_anchor_panels = self.ballast_tray_and_anchor_count(wind_zone_index, panel_type,
                                                                              ballast_block_weight, max_system_pressure,
                                                                              c_p_matrix, q_z)
                anchor_count = ballast_tray_anchor_panels.wind_anchors
                ballast_count = ballast_tray_anchor_panels.ballast
                pressure = ballast_tray_anchor_panels.pressure
                warning = ballast_tray_anchor_panels.warnings

                pressure_as_string = "{0:.2f}".format(pressure)
                # Because pressure is stored as a floating point number, it is possible, because floats
                # for pressure to be something like 5.02999999999999. Which is clearly meant to be 5.03.
                # This represents that as a string, which doesn't have that issue.

                anchor_counts.append(anchor_count)
                ballast_counts.append(ballast_count)
                pressures.append(pressure_as_string)
                warnings.append(warning)

            table[panel_type] = {
                'ballast blocks': ballast_counts,
                'anchors': anchor_counts,
                'pressure': pressures,
                'warnings': warnings
            }
        return table

    def ballast_tray_and_anchor_count(self, wind_zone, panel_type, ballast_block_weight, max_system_pressure,
                                      c_p_matrix, q_z, use_fuzzy=False):
        fuzzy_factor = 1.15 if use_fuzzy else 1
        c_p = c_p_matrix[wind_zone, panel_type.index()] * fuzzy_factor
        uplift_force = self.uplift(c_p, q_z)

        warnings = []
        keep_trying = True
        anchor_count = 0
        pressure = 0.
        tries = 0
        ballast_and_tray_count = None
        while keep_trying:
            remainder_force = uplift_force - anchor_count * self.anchor_type.uplift_capacity()
            ballast_and_tray_count = self.ballast_and_tray_count(remainder_force, panel_type, ballast_block_weight, anchor_count)
            pressure = self.calculate_pressure_on_roof(ballast_and_tray_count.ballast_count, ballast_block_weight, ballast_and_tray_count.system_weight)
            keep_trying = (ballast_and_tray_count.needs_anchor or pressure > max_system_pressure) and ballast_and_tray_count.ballast_count > 0
            if keep_trying:
                anchor_count = self.calculate_anchors(panel_type, uplift_force) + tries
            tries += 1
            keep_trying &= tries < 100

        if uplift_force / self.module_constants.surface_area >= self.module_constants.max_psf:
            warnings.append(PanelWarnings.MaxPsf)

        return Panel(wind_zone=wind_zone,
                     panel_type=panel_type,
                     ballast=ballast_and_tray_count.ballast_count,
                     link_tray=self.interpret_tray_count(ballast_and_tray_count.link_tray_count, panel_type),
                     cross_tray=ballast_and_tray_count.cross_tray_count,
                     wind_anchors=anchor_count,
                     pressure=pressure,
                     fuzzy_wind_zone=use_fuzzy,
                     warnings=warnings)

    def ballast_and_tray_count(self, force_to_resist, panel_type, ballast_block_weight, anchor_count):
        system_weight = self.module_constants.base_weight(panel_type, 0)
        ballast_count = self.calculate_ballast(force_to_resist, system_weight, ballast_block_weight)

        link_tray_count = 0
        cross_tray_count = 0
        needs_anchor = False

        keep_trying = True
        tries = 0

        while keep_trying and tries < 3:
            tries += 1
            if ballast_count:
                new_link_tray_count, _ = self.calculate_trays(ballast_count + 2 * anchor_count,
                                                          self.module_constants.link_tray_thresholds(panel_type))

                # Recalculate weight given new link trays; recalculate ballast given new weight
                system_weight = self.module_constants.base_weight(panel_type, new_link_tray_count + cross_tray_count)
                ballast_count = self.calculate_ballast(force_to_resist, system_weight, ballast_block_weight)

                new_cross_tray_count, needs_anchor = self.calculate_trays(ballast_count + 2 * anchor_count,
                                                                      self.module_constants.cross_tray_thresholds(
                                                                          panel_type))
                system_weight = self.module_constants.base_weight(panel_type, new_cross_tray_count + new_link_tray_count)
                ballast_count = self.calculate_ballast(force_to_resist, system_weight, ballast_block_weight)

                if link_tray_count == new_link_tray_count and cross_tray_count == new_cross_tray_count:
                    keep_trying = False
                link_tray_count = new_link_tray_count
                cross_tray_count = new_cross_tray_count
            else:
                keep_trying = False

        return Result(ballast_count, link_tray_count=link_tray_count, cross_tray_count=cross_tray_count,
                      system_weight=system_weight, needs_anchor=needs_anchor)

    def uplift(self, c_p, q_z):
        return q_z * self.module_constants.surface_area * c_p

    def calculate_ballast(self, uplift, non_ballast_weight, ballast_block_weight):
        if non_ballast_weight > uplift:
            return 0

        return ceil((uplift - non_ballast_weight) / ballast_block_weight)

    def calculate_trays(self, ballast_count, thresholds):
        for idx, threshold in enumerate(thresholds):
            if ballast_count <= threshold:
                return idx, False
        return len(thresholds) - 1, True

    def calculate_pressure_on_roof(self, ballast_count, ballast_block_weight, non_ballast_weight):
        effective_area = self.module_constants.surface_area / self.module_constants.ground_coverage_ratio
        return (ballast_count * ballast_block_weight + non_ballast_weight) / effective_area

    def interpret_tray_count(self, link_tray_count, panel_type):
        if self.system_type == SystemType.singleTilt:
            if panel_type == PanelType.EastWest:
                return 2
            elif panel_type == PanelType.Corner or panel_type == PanelType.NorthSouth:
                return 0
        return link_tray_count or 0

    def calculate_anchors(self, panel_type, uplift):
        base_system_weight = self.module_constants.base_weight(panel_type, 0)
        anchor_capacity = self.anchor_type.uplift_capacity()
        return max(floor((uplift - base_system_weight) / anchor_capacity), 1)

    def show_presented_link_trays(self, panels):
        for panel in panels:
            panel.presented_link_tray = self.present_link_tray(panel.link_tray, panel.panel_type)
        return panels

    def present_link_tray(self, link_tray_count, panel_type):
        if self.system_type == SystemType.singleTilt:
            link_tray_representation = {
                PanelType.Corner: 0,
                PanelType.NorthSouth: 0,
                PanelType.EastWest: 2,
                PanelType.Middle: min(1, int(link_tray_count)),
            }[panel_type]
        else:
            link_tray_representation = {
                PanelType.Corner: 2,
                PanelType.NorthSouth: 2,
                PanelType.EastWest: 1,
                PanelType.Middle: min(1, int(link_tray_count)),
            }[panel_type]
        return link_tray_representation
