from __future__ import (
    unicode_literals,
    absolute_import,
    print_function,
    division,
    )
str = type('')

import warnings
from RPi import GPIO

from . import LocalPin
from .data import pi_info
from ..exc import (
    PinInvalidFunction,
    PinSetInput,
    PinFixedPull,
    PinInvalidPull,
    PinInvalidState,
    PinInvalidBounce,
    PinPWMFixedValue,
    PinNonPhysical,
    PinNoPins,
    )


class RPiGPIOPin(LocalPin):
    """
    Uses the `RPi.GPIO`_ library to interface to the Pi's GPIO pins. This is
    the default pin implementation if the RPi.GPIO library is installed.
    Supports all features including PWM (via software).

    Because this is the default pin implementation you can use it simply by
    specifying an integer number for the pin in most operations, e.g.::

        from gpiozero import LED

        led = LED(12)

    However, you can also construct RPi.GPIO pins manually if you wish::

        from gpiozero.pins.rpigpio import RPiGPIOPin
        from gpiozero import LED

        led = LED(RPiGPIOPin(12))

    .. _RPi.GPIO: https://pypi.python.org/pypi/RPi.GPIO
    """

    _PINS = {}

    GPIO_FUNCTIONS = {
        'input':   GPIO.IN,
        'output':  GPIO.OUT,
        'i2c':     GPIO.I2C,
        'spi':     GPIO.SPI,
        'pwm':     GPIO.HARD_PWM,
        'serial':  GPIO.SERIAL,
        'unknown': GPIO.UNKNOWN,
        }

    GPIO_PULL_UPS = {
        'up':       GPIO.PUD_UP,
        'down':     GPIO.PUD_DOWN,
        'floating': GPIO.PUD_OFF,
        }

    GPIO_EDGES = {
        'both':    GPIO.BOTH,
        'rising':  GPIO.RISING,
        'falling': GPIO.FALLING,
        }

    GPIO_FUNCTION_NAMES = {v: k for (k, v) in GPIO_FUNCTIONS.items()}
    GPIO_PULL_UP_NAMES = {v: k for (k, v) in GPIO_PULL_UPS.items()}
    GPIO_EDGES_NAMES = {v: k for (k, v) in GPIO_EDGES.items()}

    PI_INFO = None

    def __new__(cls, number):
        if not cls._PINS:
            GPIO.setmode(GPIO.BCM)
            GPIO.setwarnings(False)
        if cls.PI_INFO is None:
            cls.PI_INFO = pi_info()
        try:
            return cls._PINS[number]
        except KeyError:
            self = super(RPiGPIOPin, cls).__new__(cls)
            try:
                cls.PI_INFO.physical_pin('GPIO%d' % number)
            except PinNoPins:
                warnings.warn(
                    PinNonPhysical(
                        'no physical pins exist for GPIO%d' % number))
            self._number = number
            self._pull = 'up' if cls.PI_INFO.pulled_up('GPIO%d' % number) else 'floating'
            self._pwm = None
            self._frequency = None
            self._duty_cycle = None
            self._bounce = -666
            self._when_changed = None
            self._edges = GPIO.BOTH
            GPIO.setup(self._number, GPIO.IN, self.GPIO_PULL_UPS[self._pull])
            cls._PINS[number] = self
            return self

    def __repr__(self):
        return "GPIO%d" % self._number

    @property
    def number(self):
        return self._number

    def close(self):
        self.frequency = None
        self.when_changed = None
        GPIO.cleanup(self._number)

    def output_with_state(self, state):
        self._pull = 'floating'
        GPIO.setup(self._number, GPIO.OUT, initial=state)

    def input_with_pull(self, pull):
        if pull != 'up' and self.PI_INFO.pulled_up('GPIO%d' % self._number):
            raise PinFixedPull('%r has a physical pull-up resistor' % self)
        try:
            GPIO.setup(self._number, GPIO.IN, self.GPIO_PULL_UPS[pull])
            self._pull = pull
        except KeyError:
            raise PinInvalidPull('invalid pull "%s" for pin %r' % (pull, self))

    def _get_function(self):
        return self.GPIO_FUNCTION_NAMES[GPIO.gpio_function(self._number)]

    def _set_function(self, value):
        if value != 'input':
            self._pull = 'floating'
        if value in ('input', 'output') and value in self.GPIO_FUNCTIONS:
            GPIO.setup(self._number, self.GPIO_FUNCTIONS[value], self.GPIO_PULL_UPS[self._pull])
        else:
            raise PinInvalidFunction('invalid function "%s" for pin %r' % (value, self))

    def _get_state(self):
        if self._pwm:
            return self._duty_cycle
        else:
            return GPIO.input(self._number)

    def _set_state(self, value):
        if self._pwm:
            try:
                self._pwm.ChangeDutyCycle(value * 100)
            except ValueError:
                raise PinInvalidState('invalid state "%s" for pin %r' % (value, self))
            self._duty_cycle = value
        else:
            try:
                GPIO.output(self._number, value)
            except ValueError:
                raise PinInvalidState('invalid state "%s" for pin %r' % (value, self))
            except RuntimeError:
                raise PinSetInput('cannot set state of pin %r' % self)

    def _get_pull(self):
        return self._pull

    def _set_pull(self, value):
        if self.function != 'input':
            raise PinFixedPull('cannot set pull on non-input pin %r' % self)
        if value != 'up' and self.PI_INFO.pulled_up('GPIO%d' % self._number):
            raise PinFixedPull('%r has a physical pull-up resistor' % self)
        try:
            GPIO.setup(self._number, GPIO.IN, self.GPIO_PULL_UPS[value])
            self._pull = value
        except KeyError:
            raise PinInvalidPull('invalid pull "%s" for pin %r' % (value, self))

    def _get_frequency(self):
        return self._frequency

    def _set_frequency(self, value):
        if self._frequency is None and value is not None:
            try:
                self._pwm = GPIO.PWM(self._number, value)
            except RuntimeError:
                raise PinPWMFixedValue('cannot start PWM on pin %r' % self)
            self._pwm.start(0)
            self._duty_cycle = 0
            self._frequency = value
        elif self._frequency is not None and value is not None:
            self._pwm.ChangeFrequency(value)
            self._frequency = value
        elif self._frequency is not None and value is None:
            self._pwm.stop()
            self._pwm = None
            self._duty_cycle = None
            self._frequency = None

    def _get_bounce(self):
        return None if self._bounce == -666 else (self._bounce / 1000)

    def _set_bounce(self, value):
        if value is not None and value < 0:
            raise PinInvalidBounce('bounce must be 0 or greater')
        f = self.when_changed
        self.when_changed = None
        try:
            self._bounce = -666 if value is None else int(value * 1000)
        finally:
            self.when_changed = f

    def _get_edges(self):
        return self.GPIO_EDGES_NAMES[self._edges]

    def _set_edges(self, value):
        f = self.when_changed
        self.when_changed = None
        try:
            self._edges = self.GPIO_EDGES[value]
        finally:
            self.when_changed = f

    def _get_when_changed(self):
        return self._when_changed

    def _set_when_changed(self, value):
        if self._when_changed is None and value is not None:
            self._when_changed = value
            GPIO.add_event_detect(
                self._number, self._edges,
                callback=lambda channel: self._when_changed(),
                bouncetime=self._bounce)
        elif self._when_changed is not None and value is None:
            GPIO.remove_event_detect(self._number)
            self._when_changed = None
        else:
            self._when_changed = value

