import os
import subprocess
import tempfile
import time
from subprocess import Popen, PIPE, CREATE_NO_WINDOW
import unittest
import polling2
from steamvr.paths import LH_CONSOLE_PATH
from steamvr.steamvr import load_steamvr_json, save_steamvr_json, get_serial_number
from functools import partial
from typing import Callable
from threading import Thread
import logging

logger = logging.getLogger(__name__)

class LighthouseDeviceNotFound(IOError): pass


# Todo consolidate to LighthouseConsoleUnbuffered
class LighthouseConsole:
    def __read_till_end(self):
        lines = []
        for line in iter(self.p.stdout.readline, b''):
            if self.p.stdout.peek() == b'lh> ':
                break
            if self.serial:
                logger.getChild(self.serial).info(line)
            else:
                logger.info(line)
            if b'No connected lighthouse device found.' in line:
                self.close()
                raise LighthouseDeviceNotFound("No Lighthouse devices connected")
            lines.append(line)
        return lines

    def __init__(self):
        self.p = None
        self.version = None
        self.serial = None

    @staticmethod
    def create():
        lhc = LighthouseConsole()
        lhc.open()
        return lhc

    def open(self):
        self.p = Popen([LH_CONSOLE_PATH], stdout=PIPE, stdin=PIPE, creationflags=CREATE_NO_WINDOW)
        self.version = self.p.stdout.readline().split()[-1]
        #due to failing version check on some systems, we'll just ignore it for now
        #assert self.version == b'07640364', f'Lighthouse Version {self.version} differs from expected 07640364'
        self.__read_till_end()

    def close(self):
        assert self.p is not None
        self.p.stdin.close()
        self.p.stdout.close()
        self.p.kill()
        self.p.wait(timeout=10)

    def __del__(self):
        if self.p is not None:
            self.close()

    def list_devices(self):
        assert self.p is not None
        ds = []
        self.p.stdin.write("serial\r\n".encode())
        self.p.stdin.flush()
        line = self.p.stdout.readline().decode()  # above counter can be wrong for some reason
        print(line)
        assert 'Attached lighthouse receiver devices:' in line
        line = self.p.stdout.readline().decode()
        while '\t' in line or '        ' in line:
            s = line.split()[-1]
            ds.append(s)
            line = self.p.stdout.readline().decode()
        return ds

    def list_hardware(self):
        assert self.p is not None
        hw = []
        self.p.stdout.flush()
        self.p.stdin.write("deviceinfo\r\n".encode())
        self.p.stdin.flush()
        # vvv arbitrary cmd to add extra line. without, the last line is not read
        self.p.stdin.write("userdata\r\n".encode())
        self.p.stdin.flush()
        lines = self.__read_till_end()
        print(lines) #debug
        for line in lines:
            lineStr = line.decode()
            # print(lineStr) #debug
            if 'DEVICEINFO' in lineStr:
                serial = lineStr.split()[1].split('=')[1]
                device_class = lineStr.split()[2].split('=')[1]
                hw.append({'serial': serial, 'device_class': device_class})
        return hw
        # return test #debug

    def select_device(self, serial):
        self.serial = serial
        self.p.stdin.write(("serial " + serial + "\r\n").encode())
        self.p.stdin.flush()
        # vvv arbitrary cmd to add extra line. without, the last line is not read
        self.p.stdin.write("userdata\r\n".encode())
        self.p.stdin.flush()
        self.__read_till_end()

    def reboot_device(self):
        self.p.stdin.write(("reboot\r\n").encode())
        self.p.stdin.flush()
        # vvv arbitrary cmd to add extra line. without, the last line is not read
        self.p.stdin.write("userdata\r\n".encode()) #debug
        self.p.stdin.flush() #debug
        self.__read_till_end()

    def shutdown_device(self):
        self.p.stdin.write(("poweroff\r\n").encode())
        self.p.stdin.flush()
        # vvv arbitrary cmd to add extra line. without, the last line is not read
        self.p.stdin.write("userdata\r\n".encode()) #debug
        self.p.stdin.flush() #debug
        self.__read_till_end()

    def device_haptics(self, freq: int = 43, ms: int = 500, strength: int = 100):
        self.p.stdin.write((f"haptic2 {freq} {ms} {strength}\r\n").encode())
        self.p.stdin.flush()

    def pair(self):
        self.p.stdin.write(("pair\r\n").encode())
        self.p.stdin.flush()
        # vvv arbitrary cmd to add extra line. without, the last line is not read
        self.p.stdin.write("userdata\r\n".encode()) #debug
        self.p.stdin.flush() #debug
        self.__read_till_end()        
        
    def download_config_to_file(self, path: str):
        assert '.json' in path
        self.p.stdin.write(("downloadconfig " + path + "\r\n").encode())
        self.p.stdin.flush()
        polling2.poll(partial(os.path.exists, path), step=0.5, timeout=10)
        self.__read_till_end()

    def upload_config_file(self, json_path: str):
        self.p.stdin.write(("uploadconfig " + json_path + "\r\n").encode())
        self.p.stdin.flush()
        self.__read_till_end()

    def upload_config(self, config: dict):
        with tempfile.TemporaryDirectory() as tmp_dir:
            tmp_file = tmp_dir + r"\config.json"
            save_steamvr_json(config, tmp_file)
            self.upload_config_file(tmp_file)
            time.sleep(1)

    def download_config(self) -> dict:
        with tempfile.TemporaryDirectory() as tmp_dir:
            tmp_file = tmp_dir + r"\config.json"
            self.download_config_to_file(tmp_file)
            config = load_steamvr_json(tmp_file)
            return config


class TestLighthouseConsole(unittest.TestCase):
    def setUp(self) -> None:
        self.console = LighthouseConsole()
        self.console.open()

    def test_init(self):
        self.assertIn('Version', str(self.console.version))

    def test_download_config_to_file(self):
        devs = self.console.list_devices()
        c = self.console.download_config()
        serial = get_serial_number(c)
        self.assertIn(serial, devs)

    def test_saving_config(self):
        c = self.console.download_config()
        with tempfile.TemporaryDirectory() as tmp:
            save_steamvr_json(c, tmp + "test.json")
            c2 = load_steamvr_json(tmp + "test.json")
            self.assertEqual(c, c2)