class Simulator(object):

    def __init__(self, params, scene, initial_pos):
        self.scene = scene
        self.params = params
        self.initial_pos = initial_pos
        self.status = "none"

        self.camera = CameraModel(scene, initial_pos,
            simulate_backlash=self.params.backlash,
            simulate_noise=self.params.noise)

        if params.perfect_classification is None:
            self.perfect_classification = None
        else:
            self.perfect_classification = \
                params.perfect_classification[scene.filename]

    def _do_local_search(self, direction, rev_direction):
        """Perform a local search (incremental hillclimbing in a 
        given direction). The hillclimbing has a tolerance of two steps.
        i.e., Up to two steps that don't increase the focus value can be taken
        before we stop climbing."""
        while not self.camera.will_hit_edge(direction):
            prev_fmeasure = self.camera.last_fmeasure()
            self.camera.move_fine(direction)

            if self.camera.last_fmeasure() < prev_fmeasure:
                if (two_step_tolerance and 
                    not self.camera.will_hit_edge(direction)):
                    # We've seen a decrease. Consider moving one extra step.
                    prev_fmeasure = self.camera.last_fmeasure()
                    self.camera.move_fine(direction)

                    if (self.camera.last_fmeasure() < prev_fmeasure):
                        # Seen a decrease again, backtrack and stop.
                        self.camera.move_fine(rev_direction, 2)
                        break
                else:
                    # Backtrack and stop.
                    self.camera.move_fine(rev_direction)
                    break

    def _go_to_max(self):
        """Return to the location of the largest focus value seen so far and
        perform a local search to find the exact location of the peak."""
        current_pos = self.camera.last_position()
        maximum_pos = max(self.camera.visited_positions,
            key=(lambda pos : self.camera.get_fvalue(pos)))

        if maximum_pos < current_pos:
            direction = Direction("left")
        elif maximum_pos > current_pos:
            direction = Direction("right")
        elif current_pos < self.camera.visited_positions[-2]:
            direction = Direction("left")
        else:
            direction = Direction("right")
        rev_direction = direction.reverse()

        # Take as many coarse steps as needed to go back to the maximum
        # without going over it.
        distance = abs(current_pos - maximum_pos)
        coarse_steps = distance / 8

        self.camera.move_coarse(direction, coarse_steps)

        # Keep going in fine steps to see if we can find a higher position.
        start_pos = self.camera.last_position()
        self._do_local_search(direction, rev_direction)

        # If we didn't move further, we might want to look in the other
        # direction too.
        if start_pos == self.camera.last_position():
            self._do_local_search(rev_direction, direction)

        self.status = "foundmax"

    def _get_first_direction(self):
        """Direction in which we should start sweeping initially."""
        first, second, third = self.camera.get_fvalues(
            self.camera.visited_positions[-3:])
        norm_lens_pos = float(self.initial_pos) / (self.scene.step_count - 1)

        evaluator = featuresfirststep.firststep_feature_evaluator(
            first, second, third, norm_lens_pos)
        return Direction(evaluatetree.evaluate_tree(
            self.params.left_right_tree, evaluator))

    def _sweep(self, direction):
        """Sweep the lens in one direction and return a
        tuple (success state, number of steps taken) along the way.
        """
        initial_position = self.camera.last_position()
        sweep_fvalues = [ self.camera.last_fmeasure() ]

        while not self.camera.will_hit_edge(direction):
            # Move the lens forward.
            self.camera.move_coarse(direction)
            sweep_fvalues.append(self.camera.last_fmeasure())

            # Take at least two steps before we allow turning back.
            if len(sweep_fvalues) < 3:
                continue
       
            if self.perfect_classification is None:
                # Obtain the ML classification at the new lens position.
                evaluator = featuresturn.action_feature_evaluator(
                    sweep_fvalues, self.scene.step_count)
                classification = evaluatetree.evaluate_tree(
                    self.params.action_tree, evaluator)
            else:
                key = featuresturn.make_key(str(direction), initial_position, 
                                            self.camera.last_position())
                classification = self.perfect_classification[key]

            if classification != "continue":
                assert (classification == "turn_peak" or
                        classification == "backtrack")
                return classification, len(sweep_fvalues) - 1

        # We've reached an edge, but the decision tree still does not want
        # to turn back, so what do we do now?
        # After thinking a lot about it, I think the best thing to do is to
        # introduce a condition manually. It's a bit ad-hoc, but we really need
        # to be able to handle this case robustly, as there are lot of cases
        # (i.e., landscape shots) where peaks will be at the edge.
        min_val = min(self.camera.get_fvalues(self.camera.visited_positions))
        max_val = max(self.camera.get_fvalues(self.camera.visited_positions))
        if float(min_val) / max_val > 0.8:
            return "backtrack", len(sweep_fvalues) - 1
        else:
            return "turn_peak", len(sweep_fvalues) - 1


    def _backtrack(self, previous_direction, step_count):
        """From the current lens position, go back to the lens position we
        were at before and look on the other side."""

        new_direction = previous_direction.reverse()

        # Go back to where we started.
        self.camera.move_coarse(new_direction, step_count)

        # Sweep again the other way.
        result, step_count = self._sweep(new_direction)

        if result == "turn_peak":
            self._go_to_max()
        elif result == "backtrack":
            # If we need to backtrack a second time, we failed.
            self.status = "failed"
        else:
            assert False

    def evaluate(self):
        """For every scene and every lens position, run a simulation and
        store the statistics."""

        # Take the first two steps, as to get three focus measures with which
        # to decide which direction to sweep.
        self.camera.move_fine(Direction("right"), 2)

        # Decide initial direction in which to look.
        direction = self._get_first_direction()
            
        # Search in that direction.
        result, step_count = self._sweep(direction)

        if result == "turn_peak":
            self._go_to_max()
        elif result == "backtrack":
            self._backtrack(direction, step_count)
        else:
            assert False

    def is_true_positive(self):
        """Whether a peak was found and the peak is close to a real peak."""
        return (self.status == "foundmax" and 
                self.scene.distance_to_closest_peak(
                    self.camera.last_position()) <= 1)

    def is_false_positive(self):
        """Whether a peak was found and the peak not close to a real peak."""
        return (self.status == "foundmax" and 
                self.scene.distance_to_closest_peak(
                    self.camera.last_position()) > 1)

    def is_true_negative(self):
        """Whether we failed to find a peak and we didn't come 
        close to a real peak."""
        return (self.status == "failed" and 
                all(self.scene.distance_to_closest_peak(pos) > 1
                    for pos in self.camera.visited_positions))

    def is_false_negative(self):
        """Whether we failed to find a peak but we did come 
        close to a real peak."""
        return (self.status == "failed" and 
                any(self.scene.distance_to_closest_peak(pos) <= 1
                    for pos in self.camera.visited_positions))

    def get_evaluation(self):
        """Return whether a simulation for this scene starting at the given
        lens position gave a true/false positive/negative.
        """
        if self.is_true_positive():
            return "true positive"
        if self.is_false_positive():
            return "false positive"
        if self.is_true_negative():
            return "true negative"
        if self.is_false_negative():
            return "false negative"