import copy
from helix.Repositories.graph_repository import GraphRepository
from helix.calculators.ballast_calculator import BallastCalculator
from helix.calculators.bom_calculator import BomCalculator
from helix.calculators.coordinates_calculator import CoordinatesCalculator
from helix.calculators.pressure_coefficient_calculator import PressureCoefficientCalculator
from helix.calculators.seismic_calculator import SeismicCalculator
from helix.calculators.subarray_helper import get_subarray_sizes_and_rows, extract_subarray
from helix.calculators.summary_values_calculator import SummaryValuesCalculator
from helix.calculators.wind_pressure_calculator import WindPressureCalculator


class Calculator(object):
    def __init__(self, user_values, calculate_panel_data=True):
        self.values = user_values
        self._q_z = None
        self._c_p_matrix = None
        self._L_B = None
        self._K_z = None
        self.subarrays = None
        self.buildings = self.values.buildings_polygons()
        self.buildings_for_drawing = []

        self.panels = self.values.csv()
        if calculate_panel_data and self.panels is not None:
            for idx, panel in enumerate(self.panels):
                panel.id = idx + 1
            self.panels.sort(key=lambda x: x.subarray)
            self.subarrays = get_subarray_sizes_and_rows(self.panels)

            self.__compute_ballast()
            _,_,self.buildings_for_drawing = self.__transform_coordinates()
            self.graph_repository = GraphRepository(self.panels, self.subarrays, self.values.system_type())
            if self.values.user_override_seismic_anchors():
                user_provided_panels = self.values.get_user_provided_seismic_anchors()
                for user_panel in user_provided_panels:
                    panel = [panel for panel in self.panels if panel.id == user_panel.id][0]
                    panel.seismic_anchors = user_panel.seismic_anchors
                self.__compute_seismic_anchors(self.panels)  # Update subarrays to include required seismic anchors
                self.__update_ballast(self.panels)
            else:
                 # Update subarrays *and panels* to include required seismic anchors
                self.panels = self.__compute_seismic_anchors(self.panels)

    def k_z(self):
        if self._K_z is None:
            self._K_z = WindPressureCalculator(self.values).K_z()
        return self._K_z

    def L_B(self):
        if self._L_B is None:
            self._L_B = PressureCoefficientCalculator(self.values).L_B()
        return self._L_B

    def summary_table(self):
        return BallastCalculator(self.values).summary_table(self.__c_p_matrix(), self.q_z())

    def minimum_array_sizes(self):
        return PressureCoefficientCalculator(self.values).minimum_array_size(self.L_B())

    # Used in the array summary page - is the table of weight, psf, anchors, ballast, etc. for the entire system
    def summary_values(self):
        seismic_anchors = self.subarray_summary()
        ballast_calculator = BallastCalculator(self.values)
        seismic_interval = SeismicCalculator(self.values, self.graph_repository).seismic_anchor_interval()

        return SummaryValuesCalculator(self.values).summary_values(self.panels, seismic_anchors, self.__c_p_matrix(),
                                                                   self.q_z(), seismic_interval, ballast_calculator)

    def documentation_summary_values(self):
        seismic_anchors = self.subarray_summary()
        ballast_calculator = BallastCalculator(self.values)
        seismic_interval = SeismicCalculator(self.values, self.graph_repository).seismic_anchor_interval()

        return SummaryValuesCalculator(self.values).documentation_summary_values(self.panels, seismic_anchors, self.__c_p_matrix(),
                                                                   self.q_z(), seismic_interval, ballast_calculator)

    # used in the array visualization - is parsed into json and displayed using the fancy canvas
    def get_computed_csv_columns(self):
        return BallastCalculator(self.values).show_presented_link_trays(self.panels)

    def compute_bom(self):
        required_seismic_anchors = self.subarray_summary()
        return BomCalculator(self.values, self.panels, required_seismic_anchors, self.graph_repository).compute_bom()

    def documentation_bom(self):
        required_seismic_anchors = self.subarray_summary()
        return BomCalculator(self.values, self.panels, required_seismic_anchors, self.graph_repository).documentation_bom()

    # used in the array summary page - is part of the fancy scrolling table of summing up each subarray
    def subarray_summary(self):
        summary_values_calculator = SummaryValuesCalculator(self.values)
        for subarray in self.subarrays:
            panels_for_subarray = extract_subarray(self.panels, subarray.subarray_number)
            weight, _ = summary_values_calculator.system_weight_and_pressure(panels_for_subarray)
            subarray.weight = weight

        return self.subarrays

    def q_z(self):
        if self._q_z is None:
            self._q_z = WindPressureCalculator(self.values).q_z(self.k_z())
        return self._q_z

    def __c_p_matrix(self):
        if self._c_p_matrix is None:
            self._c_p_matrix = PressureCoefficientCalculator(self.values).c_p_matrix(self.L_B())
        return self._c_p_matrix

    def __compute_seismic_anchors(self, panels):
        panels = copy.deepcopy(panels)
        seismic_calculator = SeismicCalculator(self.values, self.graph_repository)
        for subarray in self.subarrays:
            if subarray.required_seismic_anchors is None:
                subarray.required_seismic_anchors = 0
            panels = self.__seismic_anchors_for_subarray(panels, subarray, seismic_calculator)
        return panels

    def __seismic_anchors_for_subarray(self, panels, subarray, seismic_calculator):
        # do first estimation to obtain upper bound
        required_seismic = seismic_calculator.required_force_seismic_anchors(subarray.subarray_number, panels)

        test_value = required_seismic
        tried_acceptable_values = []

        def assign_seismic_anchors(count):
            subarray.required_seismic_anchors = count
            seismic_calculator.assign_seismic_anchors(subarray, panels)
            self.__update_ballast(panels)

            assigned = sum([panel.seismic_anchors for panel in panels if panel.seismic_anchors is not None])

            return assigned

        step = max(1, test_value // 2)

        while True:
            seismic_anchors_assigned = assign_seismic_anchors(test_value)

            provided_force = seismic_calculator.compute_provided_lateral_capacity(subarray.subarray_number, panels)
            required_seismic_force = seismic_calculator.required_force_seismic_demand(subarray.subarray_number, panels)

            if seismic_anchors_assigned == 0:
                # anchors were not assigned propably because self.graph_repository.subarray_graph(subarray.subarray_number) is empty
                # which may be because of test construction
                return panels

            if provided_force < required_seismic_force:
                test_value += step
            else:
                if (test_value in tried_acceptable_values) or (required_seismic_force == 0):
                    return panels

                tried_acceptable_values.append(test_value)
                test_value = max(0, test_value - step)

            step = max(1, step // 2)

    def __transform_coordinates(self):
        return CoordinatesCalculator(self.values).transform_coordinates(self.panels, self.subarrays, self.buildings)

    def __compute_ballast(self):
        ballast_calculator = BallastCalculator(self.values)
        changed_panels = ballast_calculator.ballast_and_trays_matrix(self.__c_p_matrix(), self.q_z(), self.panels)
        self.panels = [panel.merge(changed_panels[idx]) for idx, panel in enumerate(self.panels)]

    def __update_ballast(self, panels):
        BallastCalculator(self.values).update_ballast(self.__c_p_matrix(), self.q_z(), panels)
