import socket
import struct
import threading
import time
import typing as t
from time import sleep

import numpy as np
from adbutils import AdbError, Network

from module.base.decorator import cached_property
from module.base.timer import Timer
from module.device.connection import Connection
from module.device.method.scrcpy.control import ControlSender
from module.device.method.scrcpy.options import ScrcpyOptions
from module.device.method.utils import AdbConnection, recv_all
from module.exception import RequestHumanTakeover
from module.logger import logger


class ScrcpyError(Exception):
    pass


class ScrcpyCore(Connection):
    """
    Scrcpy: https://github.com/Genymobile/scrcpy
    Module from https://github.com/leng-yue/py-scrcpy-client
    """

    _scrcpy_last_frame: t.Optional[np.ndarray] = None
    _scrcpy_last_frame_time: float = 0.

    _scrcpy_alive = False
    _scrcpy_server_stream: t.Optional[AdbConnection] = None
    _scrcpy_video_socket: t.Optional[socket.socket] = None
    _scrcpy_control_socket: t.Optional[socket.socket] = None
    _scrcpy_control_socket_lock = threading.Lock()

    _scrcpy_stream_loop_thread = None
    _scrcpy_resolution: t.Tuple[int, int] = (1280, 720)

    @cached_property
    def _scrcpy_control(self) -> ControlSender:
        return ControlSender(self)

    def scrcpy_init(self):
        self._scrcpy_server_stop()

        logger.hr('Scrcpy init')
        logger.info(f'pushing {self.config.SCRCPY_FILEPATH_LOCAL}')
        self.adb_push(self.config.SCRCPY_FILEPATH_LOCAL, self.config.SCRCPY_FILEPATH_REMOTE)

        self._scrcpy_alive = False
        self.scrcpy_ensure_running()

    def scrcpy_ensure_running(self):
        if not self._scrcpy_alive:
            with self._scrcpy_control_socket_lock:
                self._scrcpy_server_start()

    def _scrcpy_server_start(self):
        """
        Connect to scrcpy server, there will be two sockets, video and control socket.

        Raises:
            ScrcpyError:
        """
        logger.hr('Scrcpy server start')
        commands = ScrcpyOptions.command_v120(jar_path=self.config.SCRCPY_FILEPATH_REMOTE)
        self._scrcpy_server_stream: AdbConnection = self.adb.shell(
            commands,
            stream=True,
        )

        logger.info('Create server stream')
        ret = self._scrcpy_server_stream.read(10)
        # b'Aborted \r\n'
        # Probably because file not exists
        if b'Aborted' in ret:
            raise ScrcpyError('Aborted')
        if ret == b'[server] E':
            # [server] ERROR: ...
            ret += recv_all(self._scrcpy_server_stream)
            logger.error(ret)
            # java.lang.IllegalArgumentException: The server version (1.25) does not match the client (...)
            if b'does not match the client' in ret:
                raise ScrcpyError('Server version does not match the client')
            else:
                raise ScrcpyError('Unknown scrcpy error')
        else:
            # [server] INFO: Device: ...
            ret += self._scrcpy_receive_from_server_stream()
            logger.info(ret)
            pass

        logger.info('Create video socket')
        timeout = Timer(3).start()
        while 1:
            if timeout.reached():
                raise ScrcpyError('Connect scrcpy-server timeout')

            try:
                self._scrcpy_video_socket = self.adb.create_connection(
                    Network.LOCAL_ABSTRACT, "scrcpy"
                )
                break
            except AdbError:
                sleep(0.1)
        dummy_byte = self._scrcpy_video_socket.recv(1)
        if not len(dummy_byte) or dummy_byte != b"\x00":
            raise ScrcpyError('Did not receive Dummy Byte from video stream')

        logger.info('Create control socket')
        self._scrcpy_control_socket = self.adb.create_connection(
            Network.LOCAL_ABSTRACT, "scrcpy"
        )

        logger.info('Fetch device info')
        device_name = self._scrcpy_video_socket.recv(64).decode("utf-8").rstrip("\x00")
        if len(device_name):
            logger.attr('Scrcpy Device', device_name)
        else:
            raise ScrcpyError('Did not receive Device Name')
        ret = self._scrcpy_video_socket.recv(4)
        self._scrcpy_resolution = struct.unpack(">HH", ret)
        logger.attr('Scrcpy Resolution', self._scrcpy_resolution)

        self._scrcpy_video_socket.setblocking(False)
        self._scrcpy_alive = True

        logger.info('Start video stream loop thread')
        self._scrcpy_stream_loop_thread = threading.Thread(
            target=self._scrcpy_stream_loop, daemon=True
        )
        self._scrcpy_stream_loop_thread.start()
        while 1:
            if self._scrcpy_stream_loop_thread is not None and self._scrcpy_stream_loop_thread.is_alive():
                break
            self.sleep(0.001)

        logger.info('Scrcpy server is up')

    def _scrcpy_server_stop(self):
        """
        Stop listening (both threaded and blocked)
        """
        logger.hr('Scrcpy server stop')
        # err = self._scrcpy_receive_from_server_stream()
        # if err:
        #     logger.error(err)

        self._scrcpy_alive = False
        if self._scrcpy_server_stream is not None:
            try:
                self._scrcpy_server_stream.close()
            except Exception:
                pass

        if self._scrcpy_control_socket is not None:
            try:
                self._scrcpy_control_socket.close()
            except Exception:
                pass

        if self._scrcpy_video_socket is not None:
            try:
                self._scrcpy_video_socket.close()
            except Exception:
                pass

        logger.info('Scrcpy server stopped')

    def _scrcpy_receive_from_server_stream(self):
        if self._scrcpy_server_stream is not None:
            try:
                return self._scrcpy_server_stream.conn.recv(4096)
            except Exception:
                pass

    def _scrcpy_stream_loop(self) -> None:
        """
        Core loop for video parsing
        """
        try:
            from av.codec import CodecContext
            from av.error import InvalidDataError
        except ImportError as e:
            logger.error(e)
            logger.error('You must have `av` installed to use scrcpy screenshot, please update dependencies')
            raise RequestHumanTakeover

        codec = CodecContext.create("h264", "r")
        while self._scrcpy_alive:
            try:
                raw_h264 = self._scrcpy_video_socket.recv(0x10000)
                if raw_h264 == b"":
                    raise ScrcpyError("Video stream is disconnected")
                packets = codec.parse(raw_h264)
                for packet in packets:
                    frames = codec.decode(packet)
                    for frame in frames:
                        # logger.info('frame received')
                        frame = frame.to_ndarray(format="rgb24")
                        self._scrcpy_last_frame = frame
                        self._scrcpy_last_frame_time = time.time()
                        self._scrcpy_resolution = (frame.shape[1], frame.shape[0])
            except (BlockingIOError, InvalidDataError):
                # only return nonempty frames, may block cv2 render thread
                time.sleep(0.001)
            except (ConnectionError, OSError) as e:  # Socket Closed
                if self._scrcpy_alive:
                    logger.error(f'_scrcpy_stream_loop_thread: {repr(e)}')
                    raise

        raise ScrcpyError('_scrcpy_stream_loop stopped')
