class BeamCentrePresenter(object):
    class ConcreteBeamCentreListener(BeamCentre.BeamCentreListener):
        def __init__(self, presenter):
            self._presenter = presenter

        def on_run_clicked(self):
            self._presenter.on_run_clicked()

    def __init__(self, parent_presenter, beam_centre_model=None):
        self._view = None
        self._parent_presenter = parent_presenter
        self._logger = Logger("SANS")
        self._beam_centre_model = BeamCentreModel(
        ) if not beam_centre_model else beam_centre_model
        self._worker = BeamCentreAsync(parent_presenter=self)

    def set_view(self, view):
        if view:
            self._view = view

            # Set up run listener
            listener = BeamCentrePresenter.ConcreteBeamCentreListener(self)
            self._view.add_listener(listener)

            # Set the default gui
            self._view.set_options(self._beam_centre_model)

            # Connect view signals
            self.connect_signals()

    def connect_signals(self):
        self._view.r_min_line_edit.textChanged.connect(
            self._validate_radius_values)
        self._view.r_max_line_edit.textChanged.connect(
            self._validate_radius_values)

    def on_update_instrument(self, instrument):
        self._view.on_update_instrument(instrument)

    def on_update_rows(self):
        self._beam_centre_model.reset_inst_defaults(
            self._parent_presenter.instrument)
        self.update_centre_positions()

    def on_update_centre_values(self, new_vals: Dict):
        self._beam_centre_model.update_centre_positions(new_vals)

    def on_processing_finished_centre_finder(self):
        # Enable button
        self._view.set_run_button_to_normal()
        # Update Centre Positions in model and GUI
        self._view.rear_pos_1 = self._round(self._beam_centre_model.rear_pos_1)
        self._view.rear_pos_2 = self._round(self._beam_centre_model.rear_pos_2)
        self._view.front_pos_1 = self._round(
            self._beam_centre_model.front_pos_1)
        self._view.front_pos_2 = self._round(
            self._beam_centre_model.front_pos_2)

    def on_processing_error_centre_finder(self, error):
        self._logger.warning(
            "There has been an error. See more: {}".format(error))
        self._view.set_run_button_to_normal()

    def on_processing_finished(self):
        # Signal from run tab presenter
        self._view.set_run_button_to_normal()

    def on_processing_error(self, error):
        # Signal from run tab presenter
        self._view.set_run_button_to_normal()

    def on_run_clicked(self):
        UsageService.registerFeatureUsage(
            FeatureType.Feature, ["ISIS SANS", "Beam Centre Finder - Run"],
            False)
        # Get the state information for the first row.
        state = self._parent_presenter.get_state_for_row(0)

        if not state:
            self._logger.information(
                "You can only calculate the beam centre if a user file has been loaded and there"
                "valid sample scatter entry has been provided in the selected row."
            )
            return

        # Disable the button
        self._view.set_run_button_to_processing()
        self._update_beam_model_from_view()

        # Run the task
        state_copy = copy.copy(state)
        self._worker.find_beam_centre(
            state_copy, self._beam_centre_model.pack_beam_centre_settings())

    def _update_beam_model_from_view(self):
        self._beam_centre_model.r_min = self._view.r_min
        self._beam_centre_model.r_max = self._view.r_max
        self._beam_centre_model.max_iterations = self._view.max_iterations
        self._beam_centre_model.tolerance = self._view.tolerance
        self._beam_centre_model.left_right = self._view.left_right
        self._beam_centre_model.verbose = self._view.verbose
        self._beam_centre_model.COM = self._view.COM
        self._beam_centre_model.up_down = self._view.up_down
        self._beam_centre_model.rear_pos_1 = self._view.rear_pos_1
        self._beam_centre_model.rear_pos_2 = self._view.rear_pos_2
        self._beam_centre_model.front_pos_1 = self._view.front_pos_1
        self._beam_centre_model.front_pos_2 = self._view.front_pos_2
        self._beam_centre_model.q_min = self._view.q_min
        self._beam_centre_model.q_max = self._view.q_max
        self._beam_centre_model.component = self._view.component
        self._beam_centre_model.update_front = self._view.update_front
        self._beam_centre_model.update_rear = self._view.update_rear

    def copy_centre_positions(self, state_model):
        """
        Copies rear / front positions from an external model
        """
        rear_pos_1 = getattr(state_model, 'rear_pos_1')
        rear_pos_2 = getattr(state_model, 'rear_pos_2')

        self._beam_centre_model.rear_pos_1 = rear_pos_1
        self._beam_centre_model.rear_pos_2 = rear_pos_2

        self._beam_centre_model.front_pos_1 = \
            getattr(state_model, 'front_pos_1') if getattr(state_model, 'front_pos_1') else rear_pos_1
        self._beam_centre_model.front_pos_2 = \
            getattr(state_model, 'front_pos_2') if getattr(state_model, 'front_pos_2') else rear_pos_2

    def update_centre_positions(self):
        rear_pos_1 = self._beam_centre_model.rear_pos_1
        rear_pos_2 = self._beam_centre_model.rear_pos_2

        front_pos_1 = self._beam_centre_model.front_pos_1 if self._beam_centre_model.front_pos_1 == '' else rear_pos_1
        front_pos_2 = self._beam_centre_model.front_pos_2 if self._beam_centre_model.front_pos_2 == '' else rear_pos_2

        self._view.rear_pos_1 = self._round(rear_pos_1)
        self._view.rear_pos_2 = self._round(rear_pos_2)

        self._view.front_pos_1 = self._round(front_pos_1)
        self._view.front_pos_2 = self._round(front_pos_2)

    def update_front_selected(self):
        self._beam_centre_model.update_front = True
        self._beam_centre_model.update_rear = False

        # front is selected, so ensure update front is enabled and checked
        self._view.enable_update_front(True)
        # Disable and deselect update rear
        self._view.enable_update_rear(False)

    def update_rear_selected(self):
        self._beam_centre_model.update_front = False
        self._beam_centre_model.update_rear = True

        # rear is selected, so ensure update rear is enabled and checked
        self._view.enable_update_rear(True)
        # Disable and deselect update front
        self._view.enable_update_front(False)

    def update_all_selected(self):
        self._beam_centre_model.update_front = True
        self._beam_centre_model.update_rear = True

        self._view.enable_update_front(True)
        self._view.enable_update_rear(True)

    def set_on_state_model(self, attribute_name, state_model):
        attribute = getattr(self._view, attribute_name)
        if attribute or isinstance(attribute, bool):
            setattr(state_model, attribute_name, attribute)

    def set_on_view(self, attribute_name, state_model):
        attribute = getattr(state_model, attribute_name)
        # We need to be careful here. We don't want to set empty strings, or None, but we want to set boolean values.
        if attribute or isinstance(attribute, bool):
            setattr(self._view, attribute_name, attribute)

    def _round(self, val):
        DECIMAL_PLACES_CENTRE_POS = 3
        try:
            val = float(val)
        except ValueError:
            return val
        return round(val, DECIMAL_PLACES_CENTRE_POS)

    def _validate_radius_values(self):
        min_value = getattr(self._view, "r_min_line_edit").text()
        max_value = getattr(self._view, "r_max_line_edit").text()

        try:
            min_value = float(min_value)
            max_value = float(max_value)
        except ValueError:
            # one of the values is empty
            pass
        else:
            if min_value == max_value == 0:
                self._view.run_button.setEnabled(False)
                return

            if min_value >= max_value:
                if self._view.run_button.isEnabled():
                    # Only post to logger once per disabling
                    self._logger.notice(
                        "Minimum radius is larger than maximum radius. "
                        "Cannot find beam centre with current settings.")
                    self._view.run_button.setEnabled(False)
            else:
                self._view.run_button.setEnabled(True)
class BeamCentreModelTest(unittest.TestCase):
    def setUp(self):
        self.result = {'pos1': 300, 'pos2': -300}
        self.centre_finder_instance = mock.MagicMock(return_value=self.result)
        self.SANSCentreFinder = mock.MagicMock(
            return_value=self.centre_finder_instance)
        self.beam_centre_model = BeamCentreModel()

    def test_that_model_initialises_with_correct_values(self):
        self.assertEqual(self.beam_centre_model.max_iterations, 10)
        self.assertEqual(self.beam_centre_model.r_min, 60)
        self.assertEqual(self.beam_centre_model.r_max, 280)
        self.assertEqual(self.beam_centre_model.left_right, True)
        self.assertEqual(self.beam_centre_model.up_down, True)
        self.assertEqual(self.beam_centre_model.tolerance, 0.0001251)
        self.assertEqual(self.beam_centre_model.rear_pos_1, '')
        self.assertEqual(self.beam_centre_model.rear_pos_2, '')
        self.assertEqual(self.beam_centre_model.front_pos_2, '')
        self.assertEqual(self.beam_centre_model.front_pos_1, '')
        self.assertEqual(self.beam_centre_model.COM, False)
        self.assertEqual(self.beam_centre_model.verbose, False)
        self.assertEqual(self.beam_centre_model.q_min, 0.01)
        self.assertEqual(self.beam_centre_model.q_max, 0.1)
        self.assertEqual(self.beam_centre_model.component, DetectorType.LAB)
        self.assertTrue(self.beam_centre_model.update_rear)
        self.assertTrue(self.beam_centre_model.update_front)

    def test_all_other_hardcoded_inst_values_taken(self):
        for inst in {
                SANSInstrument.NO_INSTRUMENT, SANSInstrument.SANS2D,
                SANSInstrument.ZOOM
        }:
            self.beam_centre_model.reset_inst_defaults(instrument=inst)
            self.assertEqual(60, self.beam_centre_model.r_min)
            self.assertEqual(280, self.beam_centre_model.r_max)

    def test_update_centre_positions_front_mode(self):
        expected_vals = {"pos1": 101.123, "pos2": 202.234}
        rear_vals_before = (self.beam_centre_model.rear_pos_1,
                            self.beam_centre_model.rear_pos_2)
        self.beam_centre_model.component = DetectorType.HAB  # Where HAB == front
        self.beam_centre_model.update_centre_positions(expected_vals)

        # mm -> m scaling
        self.assertEqual(expected_vals["pos1"] * 1000,
                         self.beam_centre_model.front_pos_1)
        self.assertEqual(expected_vals["pos2"] * 1000,
                         self.beam_centre_model.front_pos_2)

        self.assertEqual(rear_vals_before[0],
                         self.beam_centre_model.rear_pos_1)
        self.assertEqual(rear_vals_before[1],
                         self.beam_centre_model.rear_pos_2)

    def test_update_centre_positions_rear_mode(self):
        expected_vals = {"pos1": 303.345, "pos2": 404.456}
        front_vals_before = (self.beam_centre_model.front_pos_1,
                             self.beam_centre_model.front_pos_2)
        self.beam_centre_model.component = DetectorType.LAB  # Where LAB == rear
        self.beam_centre_model.update_centre_positions(expected_vals)

        # mm -> m scaling
        self.assertEqual(expected_vals["pos1"] * 1000,
                         self.beam_centre_model.rear_pos_1)
        self.assertEqual(expected_vals["pos2"] * 1000,
                         self.beam_centre_model.rear_pos_2)

        self.assertEqual(front_vals_before[0],
                         self.beam_centre_model.front_pos_1)
        self.assertEqual(front_vals_before[1],
                         self.beam_centre_model.front_pos_2)

    def test_loq_values_updated(self):
        self.beam_centre_model.reset_inst_defaults(SANSInstrument.LOQ)
        self.assertEqual(96, self.beam_centre_model.r_min)
        self.assertEqual(216, self.beam_centre_model.r_max)

    def test_that_can_update_model_values(self):
        self.beam_centre_model.r_max = 1.0
        self.assertEqual(self.beam_centre_model.r_max, 1.0)

    def test_beam_centre_scales_to_mills(self):
        self.assertIsNot(SANSInstrument.LARMOR,
                         self.beam_centre_model.instrument)

        value_in_mm = 1200
        self.beam_centre_model.rear_pos_1 = value_in_mm
        self.beam_centre_model.front_pos_2 = value_in_mm * 2

        self.assertEqual(value_in_mm, self.beam_centre_model.rear_pos_1)
        self.assertEqual((value_in_mm * 2), self.beam_centre_model.front_pos_2)
        # Should internally be in m
        self.assertEqual((value_in_mm / 1000),
                         self.beam_centre_model._rear_pos_1)
        self.assertEqual((value_in_mm * 2 / 1000),
                         self.beam_centre_model._front_pos_2)

    def test_beam_centre_does_not_scale(self):
        self.beam_centre_model.instrument = SANSInstrument.LARMOR

        value_in_m = 1.2
        self.beam_centre_model.rear_pos_1 = value_in_m
        self.beam_centre_model.front_pos_1 = value_in_m * 2
        self.assertEqual(value_in_m, self.beam_centre_model.rear_pos_1)
        self.assertEqual(value_in_m, self.beam_centre_model._rear_pos_1)

        self.assertEqual((value_in_m * 2), self.beam_centre_model.front_pos_1)
        self.assertEqual((value_in_m * 2), self.beam_centre_model._front_pos_1)

    def test_instrument_is_set(self):
        for inst in SANSInstrument:
            self.beam_centre_model.reset_inst_defaults(inst)
            self.assertEqual(inst, self.beam_centre_model.instrument)

    def test_scaling_does_not_affect_non_rear_front_values(self):
        value_in_mm = 100  # 0.1 m

        for inst in [SANSInstrument.LARMOR, SANSInstrument.LOQ]:
            self.beam_centre_model.instrument = inst
            self.beam_centre_model.tolerance = value_in_mm
            self.assertEqual((value_in_mm / 1000),
                             self.beam_centre_model._tolerance)
            self.assertEqual(self.beam_centre_model.tolerance, value_in_mm)

    def test_scaling_can_handle_non_float_types(self):
        self.beam_centre_model.instrument = SANSInstrument.NO_INSTRUMENT

        # When in doubt it should just forward the value as is
        self.beam_centre_model.rear_pos_1 = 'a'
        self.assertEqual(self.beam_centre_model.rear_pos_1, 'a')

    def test_scaling_ignores_zero_vals(self):
        self.beam_centre_model.lab_pos_1 = 0.0
        self.beam_centre_model.lab_pos_2 = 0.0
        self.beam_centre_model.hab_pos_1 = 0.0
        self.beam_centre_model.hab_pos_2 = 0.0

        self.assertEqual(0.0, self.beam_centre_model.lab_pos_1)
        self.assertEqual(0.0, self.beam_centre_model.lab_pos_2)
        self.assertEqual(0.0, self.beam_centre_model.hab_pos_1)
        self.assertEqual(0.0, self.beam_centre_model.hab_pos_2)