import ctypes
import fcntl
import math
import os
import select
import threading
import time
import unittest


class sysfs_gpio():
    """ base class for controlling gpios with the sysfs-gpio interface """

    def __init__(self, *args, name_schema='fpga-{}-led-{}'):
        self.base = '/sys/class/gpio'
        self.name = name_schema.format(*args)

    def configure(self, direction):
        with open(os.path.join(self.base, self.name, 'direction'), 'w') as f:
            f.write(direction)

    def get(self):
        with open(os.path.join(self.base, self.name, 'value')) as f:
            return bool(int(f.read().strip()))

    def set(self, value):
        with open(os.path.join(self.base, self.name, 'value'), 'w') as f:
            f.write(str(value))


class sysfs_gpio_in(sysfs_gpio):
    """ control input-gpios with the sysfs-gpio interface """

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.configure('in')

    def enable_ir(self, edge):
        with open(os.path.join(self.base, self.name, 'edge'), 'w') as f:
            f.write(edge)

    def disable_ir(self):
        with open(os.path.join(self.base, self.name, 'edge'), 'w') as f:
            f.write('none')

    def check_ir(self):
        path = os.path.join(self.base, self.name, 'value')

        with select.epoll() as p:
            fd = os.open(path, os.O_RDONLY)
            try:
                os.read(fd, 1)
                os.lseek(fd, 0, os.SEEK_END)
                p.register(fd, select.EPOLLET)
                p.poll()
            finally:
                os.close(fd)


class sysfs_gpio_out(sysfs_gpio):
    """ control output-gpios with the sysfs-gpio interface """

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.configure('out')


class sysfs_led():
    """ control leds with the sysfs-led interface,
        Warning: can interfere with the sysfs-gpio interface """

    def __init__(self, *args, name_schema='de01:{1}:{0}'):
        self.base = '/sys/class/leds'

        self.name = name_schema.format(*args)
        self.max_brightness = self.get_max_brightness()

    def get_max_brightness(self):
        with open(os.path.join(self.base, self.name, 'max_brightness')) as f:
            return int(f.read().strip())

    def set_brightness(self, value):
        if value > self.max_brightness:
            print('value too high, setting maximum brightness instead ({})'.format(value))
            value = self.max_brightness

        with open(os.path.join(self.base, self.name, 'brightness'), 'w') as f:
            f.write(str(int(value)))

    def set(self, value):
        brightness = self.max_brightness if value == 1 else 0

        self.set_brightness(brightness)


class status_led():
    """ test leds either with the sysfs or gpio interface """

    def __init__(self, name, driver=sysfs_gpio_out):
        self.name = name
        self.gpio_leds = {
            'green': driver(name, 'green'),
            'red': driver(name, 'red'),
        }
        self.colors = {
            'red': [self.gpio_leds['red']],
            'green': [self.gpio_leds['green']],
            'yellow': [self.gpio_leds['red'], self.gpio_leds['green']],
            'off': [],
        }

    def test_colors(self):
        for color, gpio_leds in self.colors.items():
            #reset
            for gpio_led in self.gpio_leds.values():
                gpio_led.set(0)

            for gpio_led in gpio_leds:
                gpio_led.set(1)

            print('is led: {}, value: {} - ok? (y/n)'.format(
                self.name, color))#, gpio_led.__class__.__name__))

            if input() != 'y':
                return False

        return True


class watchdog():
    """ control watchdog with the watchdog-sysfs interface,
        some functionality can only be accessed over ioctls """

    def __init__(self, name):
        self.fd = -1
        self.base = '/sys/class/watchdog'
        self.name = name

    def disarm(self):
        if self.fd == -1:
            raise Exception('already disarmed')

        os.close(self.fd)
        self.fd = -1

    def arm(self):
        if self.fd != -1:
            raise Exception('already armed')

        self.fd = os.open(os.path.join('/dev', self.name), os.O_WRONLY)

    def get_state(self):
        with open(os.path.join(self.base, self.name, 'state')) as f:
            return f.read().strip()

    def get_timeout(self):
        with open(os.path.join(self.base, self.name, 'timeout')) as f:
            return int(f.read().strip())

    def set_timeout(self, timeout):
        was_deactivated = True if self.fd == -1 else False

        if was_deactivated == True:
            self.arm()

        # c function that returns the address of the ioctl
        lib = ctypes.CDLL('./get_ioctl.so')
        WDIOC_SETTIMEOUT = lib.get_ioctl_settimeout()
        fcntl.ioctl(self.fd, WDIOC_SETTIMEOUT, ctypes.c_int(timeout), True)

        if was_deactivated == True:
            self.disarm()

    def get_timeleft(self):
        with open(os.path.join(self.base, self.name, 'timeleft')) as f:
            return int(f.read().strip())


class TestFunctionLed(unittest.TestCase):
    """ test all colors of function led """

    def setUp(self):
        self.led = status_led('function')

    def test_colors(self):
        self.assertTrue(self.led.test_colors(), 'function led does not work correctly')


class TestStatusLed(unittest.TestCase):
    """ test all colors of status led """

    def setUp(self):
        self.led = status_led('status', driver=sysfs_led)

    def test_colors(self):
        self.assertTrue(self.led.test_colors(), 'status led does not work correctly')


class TestLightLed(unittest.TestCase):
    """ light-led on/off check """

    def setUp(self):
        self.led = sysfs_gpio_out('light-led', name_schema='{}')

    def test_bright(self):
        self.led.set('1')

        print('is the light-led on (y/n)?')
        self.assertEqual(input(), 'y', 'light-led does not work')

    def test_off(self):
        self.led.set('0')

        print('is the light-led off (y/n)?')
        self.assertEqual(input(), 'y', 'light-led does not work')


class TestLedLightLed(unittest.TestCase):
    """ light-led dim check (led-sysfs interface) """

    def setUp(self):
        self.led = sysfs_led('de01:blue:light', name_schema='{}')

    def test_dim(self):
        off = 0
        full = self.led.get_max_brightness()
        period = 4
        tick_period = 0.01
        runtime = 6
        cur_t = 0

        while cur_t < runtime:
            val = (full - off) / 2 * math.sin(2 * math.pi * cur_t / period) + (full - off) / 2
            self.led.set_brightness(val)
            cur_t += tick_period
            time.sleep(tick_period)

        self.led.set_brightness(0)

        print('has the light led been dimmed (y/n)?')
        self.assertEqual(input(), 'y', 'light led failed to dim')


class TestConnectionButton(unittest.TestCase):
    """ status, polling and interrupt test of connection button """

    def setUp(self):
        self.button = sysfs_gpio_in('function-button', name_schema='{}')

    def test_state(self):
        self.assertFalse(self.button.get(), 'wrong default button state')

    def test_polling(self):
        print('function-button test: press in less than 3s to continue')

        def poll_thread(interval):
            value = self.button.get()
            while self.button.get() == value:
                time.sleep(interval)

        t = threading.Thread(target=poll_thread, args=(0.1,))
        t.start()
        t.join(3.0)

        self.assertFalse(t.is_alive(), 'timeout - button has not been pressed')

    def test_interrupt(self):
        print('function-button test: press in less than 3s to continue')

        self.button.enable_ir('both')

        def wait_thread():
            self.button.check_ir()

        t = threading.Thread(target=wait_thread)
        t.start()
        t.join(3.0)

        self.button.disable_ir()
        self.assertFalse(t.is_alive(), 'timeout - button has not been pressed')


class TestWatchdog(unittest.TestCase):
    """ check/set timeout, check timeleft, state and arm/disarm """

    def setUp(self):
        self.wdt = watchdog('watchdog0')

    def test_get_timeout(self):
        self.assertEqual(self.wdt.get_timeout(), 60, 'incorrect default timeout')

    def test_get_state(self):
        self.assertEqual(self.wdt.get_state(), 'inactive', 'must be inactive on start')

    def test_arm(self):
        self.wdt.arm()
        self.assertEqual(self.wdt.get_state(), 'active', 'failed to arm')
        self.wdt.disarm()

    def test_set_timeout(self):
        cur_timeout = self.wdt.get_timeout()
        self.wdt.set_timeout(cur_timeout + 20)
        self.assertEqual(self.wdt.get_timeout(), cur_timeout + 20,
            'failed to set timeout (failure on arming may also be the cause)')

    def test_timeleft(self):
        cur_timeout = self.wdt.get_timeout()
        self.wdt.arm()
        time.sleep(1.5)
        self.wdt.disarm()

        self.assertEqual(self.wdt.get_timeleft(), cur_timeout - 1,
            'timeleft is invalid (failure on arming may also be the cause)')

    def test_disarm(self):
        self.wdt.arm()
        time.sleep(1.5)
        cur_timeleft = self.wdt.get_timeleft()
        self.wdt.disarm()
        time.sleep(1.5)
        self.assertEqual(self.wdt.get_timeleft(), cur_timeleft,
            'failed to disarm the watchdog')

    @unittest.skip('would reset the machine')
    def test_reset(self):
        print('reset in 3s')
        self.wdt.set_timeout(3)
        self.wdt.arm()
        time.sleep(3.5)
        print('failure') #better: logging
        

if __name__ == '__main__':
    print('start of de01 functional test')
    unittest.main()
