import datetime
import asyncio
import functools
import signal

import OPi.GPIO as GPIO

import util
import Constants
from MonitorTask import MonitorTask
from EncoderTask import EncoderTask
from DisplayTask import Display, create_black_screen


class ApplicationModel(object):
    def __init__(self, encoder, a, b, sw):
        self.encoder = encoder
        self.A = a
        self.B = b
        self.SW = sw
        self.state = '00'
        self.direction = None
        self.last_switch_time = datetime.datetime.now()
        GPIO.setwarnings(False)
        GPIO.setmode(GPIO.BOARD)
        GPIO.setup(a, GPIO.IN)
        GPIO.setup(b, GPIO.IN)
        GPIO.setup(sw, GPIO.IN)
        GPIO.add_event_detect(a, GPIO.BOTH, callback=self.__pos_update)
        GPIO.add_event_detect(b, GPIO.BOTH, callback=self.__pos_update)
        GPIO.add_event_detect(sw, GPIO.FALLING, callback=self.__press_update)

    def __pos_update(self, channel):
        if not MonitorTask.is_encoder_blocked:
            p1 = GPIO.input(self.A)
            p2 = GPIO.input(self.B)
            newState = "{}{}".format(p1, p2)

            if self.state == "11": # Resting position
                if newState == "10": # Turned right 1
                    self.direction = "R"
                elif newState == "01": # Turned left 1
                    self.direction = "L"

            elif self.state == "10": # R1 or L3 position
                if newState == "00": # Turned right 1
                    self.direction = "R"
                elif newState == "11": # Turned left 1
                    if self.direction == "L":
                        self.encoder.on_scroll(-1)

            elif self.state == "01": # R3 or L1
                if newState == "00": # Turned left 1
                    self.direction = "L"
                elif newState == "11": # Turned right 1
                    if self.direction == "R":
                        self.encoder.on_scroll(1)

            else: # self.state == "11"
                if newState == "10": # Turned left 1
                    self.direction = "L"
                elif newState == "01": # Turned right 1
                    self.direction = "R"
                elif newState == "11": # Skipped an intermediate 01 or 10 state, but if we know direction then a turn is complete
                    if self.direction == "L":
                        self.encoder.on_scroll(-1)
                    elif self.direction == "R":
                        self.encoder.on_scroll(1)
                else:
                    self.state = '00'
                    self.direction = None
                
            self.state = newState


    def __press_update(self, channel):
        # true if pressed (0 voltage level due to switch being active-low input)
        if not MonitorTask.is_encoder_blocked:
            self.encoder.on_click()


def main():
    util.update_board_revision()

    enc = EncoderTask()
    display = Display()
    monitor = MonitorTask()
    model = ApplicationModel(enc,
                             Constants.IPC_BOARD_GPIO_ENCA,
                             Constants.IPC_BOARD_GPIO_ENCB,
                             Constants.IPC_BOARD_GPIO_ENCSW)

    loop = asyncio.get_event_loop()
    # Register signal handler for SIGINT (Ctrl+C) and SIGTERM ('kill' command).
    # Needs to be done via event loop method in order to be able to interact with event loop itself (e.g. to stop it).
    loop.add_signal_handler(signal.SIGINT, functools.partial(signal_handler, loop=loop))
    loop.add_signal_handler(signal.SIGTERM, functools.partial(signal_handler, loop=loop))

    loop.run_forever()


def signal_handler(loop):
    print("Cleaning up...")
    EncoderTask.cancel_timers = True  # Prevent all EncoderTask timers from respawning
    create_black_screen()
    loop.stop()
    try:
        GPIO.cleanup()
    except RuntimeError:
        pass
    finally:
        GPIO.cleanup()


if __name__ == "__main__":
    main()
