from gpiozero import PWMOutputDevice, SourceMixin, CompositeDevice


class BetterServo(SourceMixin, CompositeDevice):
    """
    Copy of GPIOZero servo, but with control over pulse width and active_high
    """
    def __init__(
            self, pin=None, initial_value=0.0,
            min_pulse_width=1/1000, max_pulse_width=2/1000,
            frame_width=20/1000, pin_factory=None, active_high=True):
        if min_pulse_width >= max_pulse_width:
            raise ValueError('min_pulse_width must be less than max_pulse_width')
        if max_pulse_width >= frame_width:
            raise ValueError('max_pulse_width must be less than frame_width')
        self._frame_width = frame_width
        self._min_dc = min_pulse_width / frame_width
        self._dc_range = (max_pulse_width - min_pulse_width) / frame_width
        self._min_value = -1
        self._value_range = 2
        super(BetterServo, self).__init__(
            pwm_device=PWMOutputDevice(
                pin, frequency=int(1 / frame_width), pin_factory=pin_factory,
                active_high=False
            ),
            pin_factory=pin_factory
        )
        self.pwm_device.active_high=active_high
        try:
            self.value = initial_value
        except:
            self.close()
            raise

    @property
    def frame_width(self):
        """
        The time between control pulses, measured in seconds.
        """
        return self._frame_width

    @property
    def min_pulse_width(self):
        """
        The control pulse width corresponding to the servo's minimum position,
        measured in seconds.
        """
        return self._min_dc * self.frame_width

    @property
    def max_pulse_width(self):
        """
        The control pulse width corresponding to the servo's maximum position,
        measured in seconds.
        """
        return (self._dc_range * self.frame_width) + self.min_pulse_width

    @property
    def pulse_width(self):
        """
        Returns the current pulse width controlling the servo.
        """
        if self.pwm_device.frequency is None:
            return None
        else:
            return self.pwm_device.state * self.frame_width

    @pulse_width.setter
    def pulse_width(self, value):
        if value is None:
            self.pwm_device.frequency = None
        elif self.min_pulse_width <= value <= self.max_pulse_width:
            self.pwm_device.frequency = int(1 / self.frame_width)
            self.pwm_device.value = (value / self.frame_width)
        else:
            raise OutputDeviceBadValue("Servo pulse_width must be between min and max supplied during construction, or None")

    def min(self):
        """
        Set the servo to its minimum position.
        """
        self.value = -1

    def mid(self):
        """
        Set the servo to its mid-point position.
        """
        self.value = 0

    def max(self):
        """
        Set the servo to its maximum position.
        """
        self.value = 1

    def detach(self):
        """
        Temporarily disable control of the servo. This is equivalent to
        setting :attr:`value` to ``None``.
        """
        self.value = None

    def _get_value(self):
        if self.pwm_device.frequency is None:
            return None
        else:
            return (
                ((self.pwm_device.state - self._min_dc) / self._dc_range) *
                self._value_range + self._min_value)

    @property
    def value(self):
        """
        Represents the position of the servo as a value between -1 (the minimum
        position) and +1 (the maximum position). This can also be the special
        value ``None`` indicating that the servo is currently "uncontrolled",
        i.e. that no control signal is being sent. Typically this means the
        servo's position remains unchanged, but that it can be moved by hand.
        """
        result = self._get_value()
        if result is None:
            return result
        else:
            # NOTE: This round() only exists to ensure we don't confuse people
            # by returning 2.220446049250313e-16 as the default initial value
            # instead of 0. The reason _get_value and _set_value are split
            # out is for descendents that require the un-rounded values for
            # accuracy
            return round(result, 14)

    @value.setter
    def value(self, value):
        if value is None:
            self.pwm_device.frequency = None
        elif -1 <= value <= 1:
            self.pwm_device.frequency = int(1 / self.frame_width)
            self.pwm_device.value = (
                self._min_dc + self._dc_range *
                ((value - self._min_value) / self._value_range)
                )
        else:
            raise OutputDeviceBadValue(
                "Servo value must be between -1 and 1, or None")

    @property
    def is_active(self):
        return self.value is not None
