"""[explorerhat]

API library for Explorer HAT and Explorer HAT Pro, Raspberry Pi add-on boards"""

import atexit
import signal
import time
from sys import version_info

try:
    from smbus import SMBus
except ImportError:
    if version_info[0] < 3:
        raise ImportError("This library requires python-smbus\nInstall with: sudo apt-get install python-smbus")
    elif version_info[0] == 3:
        raise ImportError("This library requires python3-smbus\nInstall with: sudo apt-get install python3-smbus")

try:
    import RPi.GPIO as GPIO
except ImportError:
    raise ImportError("This library requires the RPi.GPIO module\nInstall with: sudo pip install RPi.GPIO")

try:
    from cap1xxx import Cap1208
except ImportError:
    raise ImportError("This library requires the cap1xxx module\nInstall with: sudo pip install cap1xxx")

from .pins import ObjectCollection, AsyncWorker, StoppableThread


__version__ = '0.4.2'

_verbose = False
_gpio_is_setup = False
_analog_is_setup = False
_captouch_is_setup = False

explorer_pro = False
explorer_phat = False
has_captouch = False
has_analog = False

# Assume A+, B+ and no funny business

# Onboard LEDs above 1, 2, 3, 4
LED1 = 4
LED2 = 17
LED3 = 27
LED4 = 5

# Outputs via ULN2003A
OUT1 = 6
OUT2 = 12
OUT3 = 13
OUT4 = 16

# 5v Tolerant Inputs
IN1 = 23
IN2 = 22
IN3 = 24
IN4 = 25

# Motor, via DRV8833PWP Dual H-Bridge
M1B = 19
M1F = 20
M2B = 21
M2F = 26

# Number of times to update
# pulsing LEDs per second
PULSE_FPS = 50
PULSE_FREQUENCY = 1000

DEBOUNCE_TIME = 20

CAP_PRODUCT_ID = 107


def help(topic=None):
    return _help[topic]

def set_verbose(value):
    global _verbose
    _verbose = value

def explorerhat_exit():
    if _verbose: print("\nExplorer HAT exiting cleanly, please wait...")

    if _verbose: print("Stopping flashy things...")
    output.stop()
    input.stop()
    light.stop()
    light.stop_pulse()

    if _verbose: print("Stopping user tasks...")
    async_stop_all()

    if _verbose: print("Cleaning up...")
    GPIO.cleanup()

    if _verbose: print("Goodbye!")

def setup():
    setup_gpio()
    setup_captouch()
    setup_analog()

def setup_gpio(pin=None, mode=None, initial=0):
    global _gpio_is_setup

    if not _gpio_is_setup:
        _gpio_is_setup = True
        GPIO.setmode(GPIO.BCM)
        GPIO.setwarnings(False)
        atexit.register(explorerhat_exit)

    if pin is not None and mode is not None:
        if mode == GPIO.OUT:
            GPIO.setup(pin, mode, initial=initial)
        else:
            GPIO.setup(pin, mode)

def setup_captouch():
    global _captouch_is_setup, has_captouch, _cap1208

    if _captouch_is_setup:
        return has_captouch

    _captouch_is_setup = True

    try:
        _cap1208 = Cap1208()
        has_captouch = True
    except IOError:
        has_captouch = False

    return has_captouch

def setup_analog():
    global _analog_is_setup, adc_available, read_se_adc, has_analog

    if _analog_is_setup:
        return has_analog

    _analog_is_setup = True

    from .ads1015 import read_se_adc, adc_available 

    if adc_available:
        has_analog = True
    else:
        has_analog = False

    return has_analog

def is_explorer_pro():
    setup_analog()
    setup_captouch()
    return has_captouch and has_analog

def is_explorer_basic():
    setup_analog()
    setup_captouch()
    return has_captouch and not has_analog

def is_explorer_phat():
    setup_analog()
    setup_captouch()
    return has_analog and not has_captouch


class Pulse(StoppableThread):
    """Basic thread wrapper class for delta-timed LED pulsing

    Pulses an LED in perfect wall-clock time
    Small delay by 1.0/FPS to prevent unnecessary workload"""
    def __init__(self, pin, time_on, time_off, transition_on, transition_off):
        StoppableThread.__init__(self)

        self._paused = False
        self.pin = pin
        self.time_on = time_on
        self.time_off = time_off
        self.transition_on = transition_on
        self.transition_off = transition_off

        self.fps = PULSE_FPS

        # Total time of transition
        self.time_start = time.time()

    def start(self):
        self.pin.frequency(PULSE_FREQUENCY)

        if self._paused:
            self.time_start = time.time()
            self._paused = False
            return

        self.time_start = time.time()
        StoppableThread.start(self)

    def pause(self):
        self._paused = True

    def run(self):
        # This loop runs at the specified "FPS" uses time.time()
        while not self.stop_event.is_set():
            if not self._paused:
                current_time = time.time() - self.time_start
                delta = current_time % (self.transition_on+self.time_on+self.transition_off+self.time_off)

                time_off = self.transition_on + self.time_on + self.transition_off
                time_on = self.transition_on + self.time_on

                if delta <= self.transition_on:
                    # Transition On Phase
                    self.pin.duty_cycle(round((100.0 / self.transition_on) * delta))

                elif time_on < delta <= time_off:
                    # Transition Off Phase
                    current_delta = delta - self.transition_on - self.time_on
                    self.pin.duty_cycle(round(100.0 - ((100.0 / self.transition_off) * current_delta)))

                elif delta > self.transition_on < delta <= time_on:
                    self.pin.duty_cycle(100)

                elif delta > time_off:
                    self.pin.duty_cycle(0)

            time.sleep(1.0/self.fps)

        self.pin.duty_cycle(0)


class Pin(object):
    """ExplorerHAT class representing a GPIO Pin

    Pin contains methods that apply to both inputs and outputs"""
    type = 'Pin'

    def __init__(self, pin, mode=GPIO.IN):
        self.pin = pin
        self.mode = mode
        self.last = GPIO.LOW
        self.handle_change = False
        self.handle_high = False
        self.handle_low = False
        self._is_gpio_setup = False

    # Return a tidy list of  all "public" methods
    def __call__(self):
        return filter(lambda x: x[0] != '_', dir(self))

    def _setup_gpio(self):
        if self._is_gpio_setup:
            return True

        self._is_gpio_setup = True
        setup_gpio(self.pin, self.mode)

    def has_changed(self):
        if self.read() != self.last:
            self.last = self.read()
            return True
        return False

    def is_off(self):
        return self.read() == 0

    def is_on(self):
        return self.read() == 1

    def read(self):
        self._setup_gpio()
        return GPIO.input(self.pin)

    def stop(self):
        return True

    def __enter__(self):
        return self

    def __exit__(self, type, value, traceback):
        pass

    def __del__(self):
        pass

    is_high = is_on
    is_low = is_off
    get = read


class Motor(object):
    type = 'Motor'

    def __init__(self, pin_fw, pin_bw):
        self._invert = False
        self.pin_fw = pin_fw
        self.pin_bw = pin_bw
        self._speed = 0
        self._gpio_is_setup = False

    def _setup_gpio(self):
        if self._gpio_is_setup:
            return

        self._gpio_is_setup = True
        setup_gpio(self.pin_fw, GPIO.OUT, initial=GPIO.LOW)
        setup_gpio(self.pin_bw, GPIO.OUT, initial=GPIO.LOW)

        self.pwm_fw = GPIO.PWM(self.pin_fw, 100)
        self.pwm_fw.start(0)

        self.pwm_bw = GPIO.PWM(self.pin_bw, 100)
        self.pwm_bw.start(0)

    def invert(self):
        self._invert = not self._invert
        self._speed = -self._speed
        self.speed(self._speed)
        return self._invert

    def forwards(self, speed=100):
        if speed > 100 or speed < 0:
            raise ValueError("Speed must be between 0 and 100")
        if self._invert:
            self.speed(-speed)
        else:
            self.speed(speed)

    def backwards(self, speed=100):
        if speed > 100 or speed < 0:
            raise ValueError("Speed must be between 0 and 100")
        if self._invert:
            self.speed(speed)
        else:
            self.speed(-speed)

    def speed(self, speed=100):
        self._setup_gpio()

        if speed > 100 or speed < -100:
            raise ValueError("Speed must be between -100 and 100")

        self._speed = speed
        if speed > 0:
            self.pwm_bw.ChangeDutyCycle(0)
            self.pwm_fw.ChangeDutyCycle(speed)
        if speed < 0:
            self.pwm_fw.ChangeDutyCycle(0)
            self.pwm_bw.ChangeDutyCycle(abs(speed))
        if speed == 0:
            self.pwm_fw.ChangeDutyCycle(0)
            self.pwm_bw.ChangeDutyCycle(0)

        return speed

    def stop(self):
        self.speed(0)

    forward = forwards
    backward = backwards
    reverse = invert


class Input(Pin):
    """ExplorerHAT class representing a GPIO Input

    Input contains methods that apply only to inputs"""

    type = 'Input'

    def __init__(self, pin):
        self.handle_pressed = None
        self.handle_released = None
        self.handle_changed = None
        self.has_callback = False

        super(Input, self).__init__(pin, GPIO.IN)

    def on_high(self, callback, bouncetime=DEBOUNCE_TIME):
        self.handle_pressed = callback
        self._setup_callback(bouncetime)
        return True

    def _setup_callback(self, bouncetime):
        if self.has_callback:
            return False

        def handle_callback(pin):
            if self.read() == 1 and callable(self.handle_pressed):
                self.handle_pressed(self)
            elif self.read() == 0 and callable(self.handle_released):
                self.handle_released(self)
            if callable(self.handle_changed):
                self.handle_changed(self)

        self._setup_gpio()
        GPIO.add_event_detect(self.pin, GPIO.BOTH, callback=handle_callback, bouncetime=bouncetime)
        self.has_callback = True
        return True

    def on_low(self, callback, bouncetime=DEBOUNCE_TIME):
        self.handle_released = callback
        self._setup_callback(bouncetime)
        return True

    def on_changed(self, callback, bouncetime=DEBOUNCE_TIME):
        self.handle_changed = callback
        self._setup_callback(bouncetime)
        return True

    def clear_events(self):
        if self._is_gpio_setup():
            GPIO.remove_event_detect(self.pin)
        self.has_callback = False

    # Alias handlers
    changed = on_changed
    pressed = on_high
    released = on_low


class Output(Pin):
    """ExplorerHAT class representing a GPIO Output

    Output contains methods that apply only to outputs.
    It also contains methods for pulsing, blinking LEDs or other attached devices"""
    type = 'Output'

    def __init__(self, pin):
        super(Output, self).__init__(pin, GPIO.OUT)

        self.pulser = Pulse(self, 0, 0, 0, 0)
        self.blinking = False
        self.pulsing = False
        self.fading = False
        self.fader = None
        self._value = 0
        self.gpio_pwm = None

    def _setup_gpio(self):
        if self._is_gpio_setup:
            return True

        setup_gpio(self.pin, self.mode)
        self.gpio_pwm = GPIO.PWM(self.pin, PULSE_FREQUENCY)
        self.gpio_pwm.start(0)

    def __del__(self):
        if self.gpio_pwm is not None:
            self.gpio_pwm.stop()
        Pin.__del__(self)

    def fade(self, start, end, duration):
        """Fades an LED to a specific brightness over a specific time in seconds

        @param self Object pointer.
        @param start Starting brightness %
        @param end Ending brightness %
        @param duration Time duration ( in seconds ) of the fade"""
        self.stop()
        time_start = time.time()
        self.pwm(PULSE_FREQUENCY, start)

        def _fade():
            self.fading = True

            if time.time() - time_start >= duration:
                self.duty_cycle(end)
                self.fading = False
                return False

            current = (time.time() - time_start) / duration
            brightness = start + (float(end-start) * current)
            self.duty_cycle(round(brightness))
            time.sleep(1.0 / PULSE_FPS)

        self.fader = AsyncWorker(_fade)
        self.fader.start()
        return True

    def blink(self, on=1, off=-1):
        """Blinks an LED by working out the correct PWM frequency/duty cycle

        @param self Object pointer.
        @param on Time the LED should stay at 100%/on
        @param off Time the LED should stay at 0%/off"""

        self.stop()

        if off == -1:
            off = on

        off = float(off)
        on = float(on)

        total = off + on

        duty_cycle = 100.0 * (on/total)

        # Use pure PWM blinking, because threads are ugly
        self.frequency(1.0/total)
        self.duty_cycle(duty_cycle)
        self.blinking = True

        return True

    def pulse(self, transition_on=None, transition_off=None, time_on=None, time_off=None):
        """Pulses an LED

        @param self Object pointer.
        @param transition_on Time the transition from 0% to 100% brightness should take
        @param transition_off Time the trantition from 100% to 0% brightness should take
        @param time_on Time the LED should stay at 100% brightness
        @param time_off Time the LED should stay at 0% brightness"""

        self.stop()

        # This needs a thread to handle the fade in and out

        # Attempt to cascade parameters
        # pulse() = pulse(0.5,0.5,0.5,0.5)
        # pulse(0.5,1.0) = pulse(0.5,1.0,0.5,0.5)
        # pulse(0.5,1.0,1.0) = pulse(0.5,1.0,1.0,1.0)
        # pulse(0.5,1.0,1.0,0.5) = -

        if transition_on is None:
            transition_on = 0.5
        if transition_off is None:
            transition_off = transition_on
        if time_on is None:
            time_on = transition_on
        if time_off is None:
            time_off = transition_on

        # pulse(x,y,0,0) is basically just a regular blink
        # only fire up a thread if we really need it
        if transition_on == 0 and transition_off == 0:
            self.blink(time_on, time_off)
            self.blinking = True
        else:
            self.pulser.time_on = time_on
            self.pulser.time_off = time_off
            self.pulser.transition_on = transition_on
            self.pulser.transition_off = transition_off
            self.pulser.start()
            self.pulsing = True

        return True

    def pwm(self, freq, duty_cycle=50):
        self.gpio_pwm.ChangeDutyCycle(duty_cycle)
        self.gpio_pwm.ChangeFrequency(freq)
        return True

    def frequency(self, freq):
        self.gpio_pwm.ChangeFrequency(freq)
        return True

    def duty_cycle(self, duty_cycle):
        self.gpio_pwm.ChangeDutyCycle(duty_cycle)
        return True

    def stop(self):
        """Spops all animation"""
        self._setup_gpio()

        if self.fading:
            self.fader.stop()
            self.fading = False

        if self.pulsing:
            self.pulsing = False
            self.pulser.pause()

        if self.blinking:
            self.blinking = False

        if self._value:
            self.duty_cycle(100)
        else:
            self.duty_cycle(0)

        return True

    def stop_pulse(self):
        """Stops the pulsing thread

        @param self Object pointer."""
        self.pulsing = False
        self.pulser.stop()
        self.pulser = Pulse(self, 0, 0, 0, 0)

    def brightness(self, value):
        if not 0 <= value <= 100:
            raise ValueError("Brightness must be between 0 and 100")

        self.frequency(PULSE_FREQUENCY)
        self.duty_cycle(value)

    def write(self, value):
        if value is not True and value is not False and value is not 1 and value is not 0:
            raise ValueError("You must write a value of 1/True or 0/False")

        self.stop()
        self._value = value

        self.frequency(PULSE_FREQUENCY)

        if self._value:
            self.duty_cycle(100)
        else:
            self.duty_cycle(0)

        return True

    def on(self):
        """Turns an Output on
        @param self Object pointer."""
        self.write(1)
        return True

    def off(self):
        """Turns an Output off
        @param self Object pointer."""
        self.write(0)
        return True

    high = on
    low = off

    def toggle(self):
        self.stop()

        if self.read():
            self.write(0)
        else:
            self.write(1)

        return True


class Light(Output):
    """ExplorerHAT class representing an onboard LED"""

    type = 'Light'

    def __init__(self, pin):
        super(Light, self).__init__(pin)


class AnalogInput(object):
    type = 'Analog Input'

    def __init__(self, channel):
        self.channel = channel
        self._sensitivity = 0.1
        self._t_watch = None
        self.last_value = None
        self._handler = None

    def read(self):
        if not setup_analog():
            raise RuntimeError("Analog is unavailable, check your pHAT/HAT and/or connections!")
        return read_se_adc(self.channel)

    def sensitivity(self, sensitivity):
        self._sensitivity = sensitivity

    def changed(self, handler, sensitivity=None):
        self._handler = handler
        if sensitivity is not None:
            self._sensitivity = sensitivity
        if self._t_watch is None:
            self._t_watch = AsyncWorker(self._watch)
            self._t_watch.start()

    def _watch(self):
        value = self.read()
        if self.last_value is not None and abs(value-self.last_value) > self._sensitivity:
            if callable(self._handler):
                self._handler(self, value)
        self.last_value = value
        time.sleep(0.01)


class CapTouchSettings(object):
    type = 'Cap Touch Settings'

    @staticmethod
    def enable_multitouch(en=True):
        _cap1208.enable_multitouch(en)


class CapTouchInput(object):
    type = 'Cap Touch Input'

    def __init__(self, channel, alias):
        self.alias = alias
        self._pressed = False
        self._held = False
        self.channel = channel
        self.handlers = {'press': None, 'release': None, 'held': None}
        self._captouch_is_setup = False

    def _setup_captouch(self):
        if self._captouch_is_setup:
            return has_captouch

        self._captouch_is_setup = True

        if setup_captouch():
            for event in ['press', 'release', 'held']:
                _cap1208.on(channel=self.channel, event=event, handler=self._handle_state)

        return has_captouch

    def _handle_state(self, channel, event):
        if channel == self.channel:
            if event == 'press':
                self._pressed = True
            elif event == 'held':
                self._held = True
            elif event in ['release', 'none']:
                self._pressed = False
                self._held = False
            if callable(self.handlers[event]):
                self.handlers[event](self.alias, event)

    def is_pressed(self):
        if not self._setup_captouch():
            raise RuntimeError("Touch is unavailable, check your pHAT/HAT and/or connections!")

        return self._pressed

    def is_held(self):
        if not self._setup_captouch():
            raise RuntimeError("Touch is unavailable, check your pHAT/HAT and/or connections!")

        return self._held

    def pressed(self, handler):
        if not self._setup_captouch():
            raise RuntimeError("Touch is unavailable, check your pHAT/HAT and/or connections!")

        self.handlers['press'] = handler

    def released(self, handler):
        if not self._setup_captouch():
            raise RuntimeError("Touch is unavailable, check your pHAT/HAT and/or connections!")

        self.handlers['release'] = handler

    def held(self, handler):
        if not self._setup_captouch():
            raise RuntimeError("Touch is unavailable, check your pHAT/HAT and/or connections!")

        self.handlers['held'] = handler

running = False
workers = {}


def async_start(name, function):
    global workers
    workers[name] = AsyncWorker(function)
    workers[name].start()
    return True

def async_stop(name):
    global workers
    workers[name].stop()
    return True

def async_stop_all():
    global workers
    for worker in workers:
        print("Stopping user task: " + worker)
        workers[worker].stop()
    return True

def set_timeout(function, seconds):
    def fn_timeout():
        time.sleep(seconds)
        function()
        return False
    timeout = AsyncWorker(fn_timeout)
    timeout.start()
    return True

def pause():
    signal.pause()

def loop(callback):
    global running
    running = True
    while running:
        callback()

def stop():
    global running
    running = False
    return True


settings = ObjectCollection()
settings._add(touch=CapTouchSettings())

light = ObjectCollection()
light._add(blue=Light(LED1))
light._add(yellow=Light(LED2))
light._add(red=Light(LED3))
light._add(green=Light(LED4))
light._alias(amber='yellow')

output = ObjectCollection()
output._add(one=Output(OUT1))
output._add(two=Output(OUT2))
output._add(three=Output(OUT3))
output._add(four=Output(OUT4))

input = ObjectCollection()
input._add(one=Input(IN1))
input._add(two=Input(IN2))
input._add(three=Input(IN3))
input._add(four=Input(IN4))

touch = ObjectCollection()
touch._add(one=CapTouchInput(4, 1))
touch._add(two=CapTouchInput(5, 2))
touch._add(three=CapTouchInput(6, 3))
touch._add(four=CapTouchInput(7, 4))
touch._add(five=CapTouchInput(0, 5))
touch._add(six=CapTouchInput(1, 6))
touch._add(seven=CapTouchInput(2, 7))
touch._add(eight=CapTouchInput(3, 8))

motor = ObjectCollection()
motor._add(one=Motor(M1F, M1B))
motor._add(two=Motor(M2F, M2B))

analog = ObjectCollection()
analog._add(one=AnalogInput(3))
analog._add(two=AnalogInput(2))
analog._add(three=AnalogInput(1))
analog._add(four=AnalogInput(0))

_help = {
    'index': '''Call with "explorerhat.help(topic)" for help with:

    * touch
    * input
    * output
    * light
    * analog
    * motor

Explorer HAT uses simple named collections of things to get you
started writing Python to control and sense the world around you.

In the same way as you called help, try calling the name of a
collection of things.

    explorerhat.touch
    ...
    explorerhat.light

You can then call methods on either entire collections, like so:

    explorerhat.light.on()

Or just one thing, like so:

    explorerhat.light.red.on()
''',
    'touch':  '''Touch Inputs

Explorer HAT includes 8 touch inputs which act just like buttons.

The 8 touch pads are named "one" to "eight" and can be called like so:

    explorerhat.touch.one
    explorerhat.touch.two
    ...
    explorerhat.touch.eight
''',
    'input':  '''Inputs

Explorer HAT includes 4 buffered, 5v tolerant inputs.

The 4 inputs are named "one" to "four" and can be called like so:

    explorerhat.input.one
    ...
    explorerhat.input.four
''',
    'output': '''Outputs

Explorer HAT includes 4 5v tolerant outputs.

Beware, these are driven through a Darlington Array ( ULN2003A )
and will *pull down to ground* rather than supply 5v.

The 4 outputs are named "one" to "four" and can be called like so:

    explorerhat.output.one
    ...
    explorerhat.output.four
''',
    'light':  '''Lights

Explorer HAT includs 4 LEDs; Yellow, Blue, Red and Green

You can call them like so:

    explorerhat.light.yellow
    ...
    explorerhat.light.green

''',
    'analog': '''Analog Inputs

Explorer HAT inclues 4, 5v tolerant analogue inputs.

The 4 analog inputs are named "one" to "four" and can be called like so:

    explorerhat.analog.one
    ...
    explorerhat.analog.four
''',
    'motor':  '''Motor Driver

Explorer HAT includes a motor driver, capable of driving two motors.

The two motors are named "one" and "two" and can be called like so:

    explorerhat.motor.one
    explorerhat.motor.two
''',
}


def help(topic='index'):
    if topic.lower() in _help.keys():
        print("HELP{}\n\n{}\n{}".format('-'*66, _help[topic.lower()], '-'*70))
    else:
        print(_help['index'])
    return None