import glob
import os
import re
import subprocess
import time

import dbus
import logging

import hwtest.ioctl


class ComponentStatus:
    def __init__(self, category, model, path, status, value=None):
        self.category = category
        self.model = model
        self.path = path
        self.status = status
        self.value = value

    def __repr__(self):
        if self.status:
            if self.value is not None:
                return '<ComponentStatus {} {} at {} is working ({})>'.format(self.category, self.model, self.path,
                                                                              self.value)
            else:
                return '<ComponentStatus {} {} at {} is working>'.format(self.category, self.model, self.path)
        else:
            return '<ComponentStatus {} {} at {} is BROKEN>'.format(self.category, self.model, self.path)


class Component:
    def test(self):
        pass

    def interactive(self):
        return False

    def read_sysfs(self, path):
        with open(path) as handle:
            return handle.read().strip()

    def read_sysfs_int(self, path):
        with open(path) as handle:
            return int(handle.read().strip())

    def read_sysfs_float(self, path):
        with open(path) as handle:
            return float(handle.read().strip())

    def guess_sysfs_name(self, device):
        if os.path.isfile(os.path.join(device, 'name')):
            with open(os.path.join(device, 'name')) as handle:
                return handle.read().strip()

        if os.path.islink(device):
            sys_device = os.readlink(device)
            driver = sys_device.split('/')[-2]
            return driver
        return "Unknown"


class Framebuffer(Component):
    def test(self):
        logging.debug('** FRAMEBUFFER COMPONENT **')
        if not os.path.isdir('/sys/class/graphics'):
            return

        for device in glob.glob('/sys/class/graphics/fb*'):
            if 'fbcon' in device:
                continue

            with open(os.path.join(device, 'name')) as handle:
                model = handle.read().strip()

            try:
                value = self.read_sysfs(os.path.join(device, 'mode'))
                status = True
            except:
                value = None
                status = False

            yield ComponentStatus('framebuffer', model, device, status, value)


class Drm(Component):
    def test(self):
        logging.debug('** DRM COMPONENT **')
        if not os.path.isdir('/sys/class/drm'):
            return

        for device in glob.glob('/sys/class/drm/card*-*'):
            model = "-"

            try:
                statusval = self.read_sysfs(os.path.join(device, 'status'))
                modes = self.read_sysfs(os.path.join(device, 'modes'))
                if statusval == "connected":
                    value = modes
                else:
                    value = "Not connected"
                status = True
            except:
                value = None
                status = False

            yield ComponentStatus('drm', model, device, status, value)


class Camera(Component):
    def test(self):
        logging.debug('** CAMERA COMPONENT **')
        if not os.path.isdir('/sys/class/video4linux'):
            return

        for device in glob.glob('/dev/media*'):
            logging.debug('Processing {}'.format(device))
            for sensor in self.get_devices(device):
                yield self.test_camera(sensor[0], device, sensor[1], sensor[2])

    def get_devices(self, media_node):
        media_command = ['media-ctl', '-p', '-d', media_node]
        logging.debug('Running {}'.format(' '.join(media_command)))
        try:
            raw = subprocess.run(media_command, capture_output=True, universal_newlines=True)
        except Exception as e:
            logging.debug(e)
            return
        raw = raw.stdout

        re_entities = re.compile(
            r"entity (\d+): ([^\(]+?) \([^\)]+\)\n\s+type (.+?) subtype (.+?) flags[^\n]+\n(.+?)\n\n",
            re.MULTILINE | re.DOTALL)
        re_device = re.compile(r"device node name (/dev/[a-z0-9-]+)")

        v4l_nodes = set()
        sensors = []
        for entity in re_entities.findall(raw):
            name = entity[1]
            e_type = entity[2]
            e_subtype = entity[3]
            rest = entity[4]

            logging.debug('Entity {} is {} subtype {}'.format(name, e_type, e_subtype))

            if e_subtype == 'Sensor':
                sensors.append((name, re_device.findall(rest)[0]))
            elif e_type == 'Node' and e_subtype == 'V4L':
                v4l_nodes.add(re_device.findall(rest)[0])

        if len(sensors) == 0:
            logging.debug('Skipping {} because no Sensor type devices were found in the topology'.format(media_node))

        for sensor in sensors:
            logging.debug('Found camera {} on {}'.format(sensor[0], sensor[1]))
            yield (sensor[0], sensor[1], v4l_nodes)

    def test_camera(self, model, media_node, camera_node, v4l_nodes):
        resolutions = [
            '320x240',
            '640x480',
            '1024x768',
            '1280x720',
            '1280x960',
            '1536x1180',
            '1600x1200',
            '1920x1080',
            '2048x1536',
            '2240x1680',
            '2560x1920',
            '2592x1944'
        ]
        v4l_node = list(v4l_nodes)[0]
        working = []
        anything_works = False
        for res in resolutions:
            logging.debug('Trying resolution {} on {}'.format(res, v4l_node))
            try:
                # Todo: Find out how generic fmt:UYVY8_2X8 is
                media_command = ['media-ctl', '-d', media_node, '--set-v4l2',
                                 '"{}":0[fmt:UYVY8_2X8/{}]'.format(model, res)]
                subprocess.run(media_command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=5)

                capture_command = ['ffmpeg', '-s', res, '-f', 'video4linux2', '-i', v4l_node, '-vframes',
                                   '1', '-y', '/tmp/{}.jpg'.format(res)]
                subprocess.run(capture_command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=5)

                if os.path.getsize('/tmp/{}.jpg'.format(res)) > 0:
                    working.append(res)
                    anything_works = True
            except Exception as e:
                logging.debug(e)
            finally:
                if os.path.isfile('/tmp/{}.jpg'.format(res)):
                    os.unlink('/tmp/{}.jpg'.format(res))

        return ComponentStatus('camera', model, camera_node, anything_works, ",".join(working))


class Magnetometer(Component):
    def test(self):
        logging.debug('** MAGNETOMETER COMPONENT **')
        if not os.path.isdir('/sys/bus/iio'):
            return

        for device in glob.glob('/sys/bus/iio/devices/iio:device*'):
            if os.path.isfile(os.path.join(device, 'in_magn_x_raw')):
                model = self.guess_sysfs_name(device)

                try:
                    x_raw = self.read_sysfs_int(os.path.join(device, 'in_magn_x_raw'))
                    y_raw = self.read_sysfs_int(os.path.join(device, 'in_magn_y_raw'))
                    z_raw = self.read_sysfs_int(os.path.join(device, 'in_magn_z_raw'))

                    if os.path.isfile(os.path.join(device, 'in_magn_scale')):
                        scale = self.read_sysfs_float(os.path.join(device, 'in_magn_scale'))
                        x_scale = scale
                        y_scale = scale
                        z_scale = scale
                    else:
                        x_scale = self.read_sysfs_float(os.path.join(device, 'in_magn_x_scale'))
                        y_scale = self.read_sysfs_float(os.path.join(device, 'in_magn_y_scale'))
                        z_scale = self.read_sysfs_float(os.path.join(device, 'in_magn_z_scale'))

                    x = x_raw * x_scale
                    y = y_raw * y_scale
                    z = z_raw * z_scale
                    status = x + y + z != 0
                    value = "{}, {}, {} gauss".format(round(x, 2), round(y, 2), round(z, 2))
                except Exception as e:
                    logging.debug(e)
                    value = None
                    status = False

                yield ComponentStatus('magnetometer', model, device, status, value)


class Accelerometer(Component):
    def test(self):
        logging.debug('** ACCELEROMETER COMPONENT **')
        if not os.path.isdir('/sys/bus/iio'):
            return

        for device in glob.glob('/sys/bus/iio/devices/iio:device*'):
            if os.path.isfile(os.path.join(device, 'in_accel_x_raw')):
                model = self.guess_sysfs_name(device)

                try:
                    x_raw = self.read_sysfs_int(os.path.join(device, 'in_accel_x_raw'))
                    y_raw = self.read_sysfs_int(os.path.join(device, 'in_accel_y_raw'))
                    z_raw = self.read_sysfs_int(os.path.join(device, 'in_accel_z_raw'))
                    scale = self.read_sysfs_float(os.path.join(device, 'in_accel_scale'))
                    x = x_raw * scale
                    y = y_raw * scale
                    z = z_raw * scale
                    status = x + y + z != 0
                    value = "{}, {}, {} g".format(round(x, 2), round(y, 2), round(z, 2))
                except Exception as e:
                    logging.debug(e)
                    value = None
                    status = False

                yield ComponentStatus('accelerometer', model, device, status, value)


class Gyroscope(Component):
    def test(self):
        logging.debug('** GYROSCOPE COMPONENT **')
        if not os.path.isdir('/sys/bus/iio'):
            return

        for device in glob.glob('/sys/bus/iio/devices/iio:device*'):
            if os.path.isfile(os.path.join(device, 'in_anglvel_x_raw')):
                model = self.guess_sysfs_name(device)

                try:
                    x_raw = self.read_sysfs_int(os.path.join(device, 'in_anglvel_x_raw'))
                    y_raw = self.read_sysfs_int(os.path.join(device, 'in_anglvel_y_raw'))
                    z_raw = self.read_sysfs_int(os.path.join(device, 'in_anglvel_z_raw'))
                    scale = self.read_sysfs_float(os.path.join(device, 'in_anglvel_scale'))
                    x = x_raw * scale
                    y = y_raw * scale
                    z = z_raw * scale
                    status = x + y + z != 0
                    value = "{}, {}, {} rad/s".format(round(x, 2), round(y, 2), round(z, 2))
                except Exception as e:
                    logging.debug(e)
                    value = None
                    status = False

                yield ComponentStatus('gyroscope', model, device, status, value)


class Temperature(Component):
    def test(self):
        logging.debug('** TEMPERATURE COMPONENT **')
        if not os.path.isdir('/sys/bus/iio'):
            return

        for device in glob.glob('/sys/bus/iio/devices/iio:device*'):
            if os.path.isfile(os.path.join(device, 'in_temp_raw')) or os.path.isfile(
                    os.path.join(device, 'in_temp_input')):
                model = self.guess_sysfs_name(device)

                try:
                    if os.path.isfile(os.path.join(device, 'in_temp_input')):
                        value = self.read_sysfs_float(os.path.join(device, 'in_temp_input'))
                    elif os.path.isfile(os.path.join(device, 'in_temp_raw')):
                        raw = self.read_sysfs_int(os.path.join(device, 'in_temp_raw'))
                        offset = self.read_sysfs_int(os.path.join(device, 'in_temp_offset'))
                        scale = self.read_sysfs_float(os.path.join(device, 'in_temp_scale'))
                        value = (raw + offset) * scale

                    # Hack, if the sensor reports in microdegree C, then convert to degree C
                    if value > 1000:
                        value = value / 1000

                    if value != 0:
                        value = "{} deg C".format(round(value, 1))
                        status = True
                    else:
                        status = False
                except Exception as e:
                    logging.debug(e)
                    value = None
                    status = False

                yield ComponentStatus('temperature', model, device, status, value)


class Proximity(Component):
    def test(self):
        logging.debug('** PROXIMITY COMPONENT **')
        if not os.path.isdir('/sys/bus/iio'):
            return

        for device in glob.glob('/sys/bus/iio/devices/iio:device*'):
            if os.path.isfile(os.path.join(device, 'in_proximity_raw')):
                model = self.guess_sysfs_name(device)

                try:
                    raw = self.read_sysfs_int(os.path.join(device, 'in_proximity_raw'))
                    scale = self.read_sysfs_float(os.path.join(device, 'in_proximity_scale'))
                    value = raw * scale
                    if value != 0:
                        value = "{}".format(value)
                        status = True
                    else:
                        status = False
                except Exception as e:
                    logging.debug(e)
                    value = None
                    status = False

                yield ComponentStatus('proximity', model, device, status, value)


class Illuminance(Component):
    def test(self):
        logging.debug('** ILLUMINANCE COMPONENT **')
        if not os.path.isdir('/sys/bus/iio'):
            return

        for device in glob.glob('/sys/bus/iio/devices/iio:device*'):
            if os.path.isfile(os.path.join(device, 'in_illuminance_raw')):
                model = self.guess_sysfs_name(device)

                try:
                    raw = self.read_sysfs_int(os.path.join(device, 'in_illuminance_raw'))
                    scale = self.read_sysfs_float(os.path.join(device, 'in_illuminance_scale'))
                    value = raw * scale
                    if value != 0:
                        value = "{} lux".format(round(value))
                        status = True
                    else:
                        status = False
                except Exception as e:
                    logging.debug(e)
                    value = None
                    status = False

                yield ComponentStatus('illuminance', model, device, status, value)
            if os.path.isfile(os.path.join(device, 'in_illuminance0_input')):
                model = self.guess_sysfs_name(device)

                try:
                    value = self.read_sysfs_int(os.path.join(device, 'in_illuminance0_input'))
                    if value != 0:
                        value = "{} lux".format(round(value))
                        status = True
                    else:
                        status = False
                except:
                    value = None
                    status = False

                yield ComponentStatus('illuminance', model, device, status, value)


class Pressure(Component):
    def test(self):
        logging.debug('** PRESSURE COMPONENT **')
        if not os.path.isdir('/sys/bus/iio'):
            return

        for device in glob.glob('/sys/bus/iio/devices/iio:device*'):
            if os.path.isfile(os.path.join(device, 'in_pressure_input')):
                model = self.guess_sysfs_name(device)

                try:
                    pA = self.read_sysfs_float(os.path.join(device, 'in_pressure_input'))
                    hPa = pA * 10
                    if pA != 0:
                        value = "{} hPa".format(round(hPa, 2))
                        status = True
                    else:
                        value = None
                        status = False
                except Exception as e:
                    logging.debug(e)
                    value = None
                    status = False

                yield ComponentStatus('pressure', model, device, status, value)


class Input(Component):
    def __init__(self):
        self.events = {}
        self.fake_inputs = []

    def test(self):
        logging.debug('** INPUT COMPONENT **')
        for device in glob.glob('/dev/input/event*'):
            name = hwtest.ioctl.EVIOCGNAME(device)
            value = None

            self.evtest(device, noninteractive=True)
            if device in self.fake_inputs:
                continue

            if device in self.events:
                value = self.events[device]['type']
                state = len(self.events[device]['broken']) == 0
                if not state:
                    value = ', '.join(self.events[device]['broken'])
                yield ComponentStatus('input', name, device, state, value)
            else:
                yield ComponentStatus('input', name, device, True, value)

    def interactive(self):
        for device in glob.glob('/dev/input/event*'):
            self.events[device] = self.evtest(device)

    def evtest(self, device, noninteractive=False):
        result = {
            'type': 'Unknown',
            'types': [],
            'broken': []
        }
        test_left = []
        try:
            process = subprocess.Popen(['evtest', '--grab', device], stdout=subprocess.PIPE, universal_newlines=True)
            for line in iter(lambda: process.stdout.readline(), ''):
                if 'Event code' in line:
                    line = line.strip()
                    _, event_type = line.split('(', maxsplit=1)
                    event_type = event_type.replace(')', '')
                    result['types'].append(event_type)
                if 'interrupt to exit' in line:
                    if len(result['types']) > 26:
                        result['type'] = 'Full keyboard'
                        test_left.append('KEY_P')
                    elif 'ABS_X' in result['types'] or 'ABS_MT_SLOT' in result['types']:
                        result['type'] = 'Touchscreen'
                        test_left.append('ABS_X')
                    elif 'FF_RUMBLE' in result['types']:
                        self.fake_inputs.append(device)
                        process.kill()
                    else:
                        keys = []
                        for key in result['types']:
                            test_left.append(key)
                            if 'KEY_' in key:
                                keys.append(key.replace('KEY_', '').capitalize())

                        if 'Volumedown' in keys and 'Volumeup' in keys:
                            keys.remove('Volumedown')
                            keys.remove('Volumeup')
                            keys.append('Volume keys')
                        result['type'] = ', '.join(keys)

                    if noninteractive:
                        process.kill()
                        return None

                    print("Testing input device: {}".format(result['type']))
                    print("Generate these events: {}".format(', '.join(test_left)))
                if 'Event: time' in line:
                    for left in test_left:
                        if '({})'.format(left) in line:
                            test_left.remove(left)
                            if len(test_left) == 0:
                                process.kill()
                            else:
                                print("Generate these events: {}".format(', '.join(test_left)))
        except KeyboardInterrupt:
            if len(test_left) > 0:
                result['broken'] = test_left
        return result


class Modem(Component):
    def __init__(self):
        self.bus = None

    def test(self):
        logging.debug('** MODEM COMPONENT **')
        self.bus = dbus.SystemBus()
        try:
            manager = dbus.Interface(self.bus.get_object('org.ofono', '/'), 'org.ofono.Manager')
        except dbus.exceptions.DBusException:
            logging.debug('Could not aquire org.ofono.Manager on dbus')
            return

        modems = manager.GetModems()

        for path, properties in modems:
            modem = dbus.Interface(self.bus.get_object('org.ofono', path), 'org.ofono.Modem')

            model = path[1:]
            sysfspath = ''
            online = False
            powered = False

            self.set_property_wait(modem, "Powered", dbus.Boolean(1))
            self.set_property_wait(modem, "Online", dbus.Boolean(1))

            if 'SystemPath' in properties:
                sysfspath = properties['SystemPath']

            if 'Powered' in properties:
                powered = properties['Powered'] == 1

            if 'Online' in properties:
                online = properties['Online'] == 1

            registration = self.get_registration(path)
            registered = registration is not None

            status = 'Unpowered'
            if powered:
                status = 'Powered'
            if online:
                status = 'Unregistered'
            if registered:
                status = 'Registered to {}'.format(registration)

            yield ComponentStatus('modem', model, sysfspath, online and powered and registered, status)

    def set_property_wait(self, interface, property, value, timeout=10):
        logging.debug('Setting dbus property {} to {}'.format(property, value))
        interface.SetProperty(property, value, timeout=120)
        for i in range(0, timeout):
            state = interface.GetProperties()
            if state[property] == value:
                return
            logging.debug('- Sleeping again')
            time.sleep(1)

    def get_registration(self, path):
        try:
            interface = dbus.Interface(self.bus.get_object('org.ofono', path), 'org.ofono.NetworkRegistration')
            properties = interface.GetProperties()
            return properties['Name']
        except Exception as e:
            return None


class Audio(Component):
    def test(self):
        logging.debug('** AUDIO COMPONENT **')
        pcm_command = ['aplay', '-L']
        logging.debug('Finding cards with aplay -L')
        pcms_raw = subprocess.run(pcm_command, capture_output=True, universal_newlines=True)
        pcms = []
        working_playback = []
        working_capture = []
        for line in pcms_raw.stdout.splitlines():
            if not line.startswith(' ') and ':' in line:
                pcms.append(line.strip())
        logging.debug('Found PCM devices: {}'.format(pcms))
        if 'pulse' in pcms_raw.stdout:
            pcms.append('pulse')
            logging.debug('Adding pulse')

        if len(pcms) == 0:
            logging.debug('No PCM devices found, skipping test')
            return

        for pcm_play in pcms:
            for pcm_capture in pcms:
                logging.debug('Audio loop test between {} and {}'.format(pcm_play, pcm_capture))
                command = ['alsabat', '-P', pcm_play, '-C', pcm_capture, '-k', '4', '-n', '0.3s']
                bat_raw = subprocess.run(command, capture_output=True, universal_newlines=True)
                logging.debug('Return code: {}'.format(bat_raw.returncode))
                logging.debug('Output: {}'.format(bat_raw.stdout))
                logging.debug('Error: {}'.format(bat_raw.stderr))
                if 'Peak detected at target frequency' in bat_raw.stdout:
                    if pcm_play not in working_playback:
                        working_playback.append(pcm_play)
                        yield ComponentStatus('speaker', pcm_play, '', True)

                    if pcm_capture not in working_capture:
                        working_capture.append(pcm_capture)
                        yield ComponentStatus('mic', pcm_capture, '', True)


class Led(Component):
    def __init__(self):
        self.leds = []

    def interactive(self):
        for device in glob.glob('/sys/class/leds/*'):
            self.check_led(device)

    def test(self):
        logging.debug('** LED COMPONENT **')
        return self.leds

    def check_led(self, device):
        name = device.split('/')[-1]
        model = '-'

        if os.path.isfile(os.path.join(device, 'device/name')):
            model = self.read_sysfs(os.path.join(device, 'device/name'))
        elif os.path.isfile(os.path.join(device, 'device/modalias')):
            model = self.read_sysfs(os.path.join(device, 'device/modalias'))

        if model == "-":
            model = name
        else:
            model += ' ' + name

        print("Testing led '{}'".format(name))

        while True:
            state = 'on' if self.is_on(device) else 'off'
            try:
                response = input("{} is {} [Toggle,Skip,Broken,Works] ".format(name, state))
            except:
                response = 's'
            if response == '':
                response = 't'
            response = response.lower()[0]

            if response == 't':
                try:
                    self.toggle_led(device)
                except:
                    print("Couldn't toggle led, reporting as broken")
                    self.leds.append(ComponentStatus('led', model, device, False))
                    return
            elif response == 's':
                return
            elif response == 'b':
                self.leds.append(ComponentStatus('led', model, device, False))
                return
            elif response == 'w':
                self.leds.append(ComponentStatus('led', model, device, True))
                return

    def is_on(self, device):
        brightness = self.read_sysfs_float(os.path.join(device, 'brightness'))
        max_brightness = self.read_sysfs_float(os.path.join(device, 'max_brightness'))
        return brightness > (max_brightness / 2.0)

    def toggle_led(self, device):
        if self.is_on(device):
            value = 0
        else:
            value = self.read_sysfs_float(os.path.join(device, 'max_brightness'))

        with open(os.path.join(device, 'brightness'), 'w', encoding='ascii') as handle:
            handle.write("{}".format(int(value)))


class Vibrator(Component):
    def __init__(self):
        self.vib = []

    def interactive(self):
        for device in glob.glob('/dev/input/event*'):
            if self.is_vibrator(device):
                name = hwtest.ioctl.EVIOCGNAME(device)
                print("Testing vibrator '{}'".format(name))

    def test(self):
        logging.debug('** VIBRATOR COMPONENT **')
        for device in glob.glob('/dev/input/event*'):
            if self.is_vibrator(device):
                name = hwtest.ioctl.EVIOCGNAME(device)
                yield ComponentStatus('vibrator', name, device, True)

    def is_vibrator(self, device):
        command = ['fftest', device]
        p = subprocess.Popen(command, universal_newlines=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE)
        stdout, stderr = p.communicate('-1\n')

        for line in stdout.splitlines():
            if 'Uploading effect' in line:
                if 'Function not implemented' not in line:
                    return True

        return False
