import aiohttp
from aiohttp import web
import asyncio
import logging
import threading
import time

import config


class HTTPMJEPServer(threading.Thread):

    BOUNDRY = '3DPrinterOSMJPEGFrame'
    RESP_HEADER = { 'Content-Type': 'multipart/x-mixed-replace; boundary=--' + BOUNDRY }
    WRITER_HEADER = { 'Content-Type': 'image/jpeg' }
    DEFAULT_STREAM_NUMBER = 0 
    DEFAULT_ACTION = 'stream' 
    SHUTDOWN_TIMEOUT = 2
    SNAPSHOT_VIEW_TIMEOUT = 10
    FRAME_RATE_LIMITING_SLEEP = 1/60
    DEBUG = config.get_settings()['logging'].get('debug')
    SELF_GET_FRAMES = bool(config.get_settings()['camera'].get('async'))

    def __init__(self, camera):
        self.logger = logging.getLogger(self.__class__.__name__)
        self.stop_flag = False
        self.camera = camera
        self.frames_storage = {}
        self.watched_streams = {}
        self.timestamps = {}
        settings = config.get_settings()['camera']['http_output']
        if settings['only_localhost']:
            addr = '127.0.0.1'
        else:
            addr = '0.0.0.0'
        self.lock = threading.RLock()
        try:
            self.loop = asyncio.get_running_loop()
        except RuntimeError:
            self.loop = asyncio.new_event_loop()
        self.app = web.Application()
        asyncio.run_coroutine_threadsafe(self.view_timeout(), self.loop)
        asyncio.run_coroutine_threadsafe(self.init_server(addr, settings['port']), self.loop)
        threading.Thread.__init__(self, name=__class__.__name__, daemon=True)

    def is_viewed(self, cap_id):
        return bool(self.watched_streams.get(cap_id))

    def run(self):
        self.logger.info("Camera server start")
        try:
            self.loop.run_forever()
        except Exception as e:
            self.logger.exception(e)
        self.logger.info("Camera server finish")

    def stop(self):
        shutdown_coroutine = asyncio.run_coroutine_threadsafe(self.shutdown_server(), self.loop)
        start_time = time.monotonic()
        while time.monotonic() < start_time + self.SHUTDOWN_TIMEOUT + 0.01:
            try:
                if shutdown_coroutine.done():
                    break
            except asyncio.CancelledError:
                pass
            time.sleep(self.SHUTDOWN_TIMEOUT/100)
        self.logger.info("Sending loop stop")
        self.loop.call_soon_threadsafe(self.loop.stop)
        while self.loop.is_running():
            time.sleep(self.SHUTDOWN_TIMEOUT/100)
        self.loop.close()
        self.flush_storages()
        self.logger.info('Camera server thread joining...')
        self.join(6)
        self.logger.info('Camera server thread stopped')

    def put_frame(self, cap_id, frame):
        with self.lock:
            if not self.stop_flag:
                self.frames_storage[cap_id] = frame

    def flush_storages(self):
        with self.lock:
            self.frames_storage.clear()
            self.watched_streams.clear()
    
    def next_frame(self, cap_id):
        with self.camera.lock:
            keys = list(self.camera.captures.keys())
        for cap_id in keys:
            capture = self.camera.captures.get(cap_id)
            if capture:
                frame = capture.get_frame()
                if frame:
                    return frame

    async def stream_handler(self, request, cap_id=None):
        if cap_id is None:
            cap_id = self.parse_cap_id(request)
        self.logger.info(f"Starting stream {cap_id}")
        with self.lock:
            self.watched_streams[cap_id] = True
        self.timestamps[cap_id] = time.monotonic()
        response = web.StreamResponse(headers=self.RESP_HEADER)
        # self.logger.debug('Frame request created a response')
        try:
            await response.prepare(request)
            while not self.stop_flag:
                self.timestamps[cap_id] = time.monotonic()
                # self.logger.debug('Frame request enters its loop')
                if self.SELF_GET_FRAMES:
                    with self.camera.lock:
                        if cap_id not in self.camera.captures:
                            break
                    frame = self.next_frame(cap_id)
                else:
                    with self.lock:
                        frame = self.frames_storage.get(cap_id)
                if frame:
                    with aiohttp.MultipartWriter('image/jpeg', boundary=self.BOUNDRY) as writer:
                        self.logger.debug(f'Frame request appending a frame of {len(frame)}')
                        writer.append(frame, self.WRITER_HEADER)
                        # self.logger.debug('Frame request writing a response')
                        await writer.write(response, close_boundary=False) #pyright: ignore
                await asyncio.sleep(self.FRAME_RATE_LIMITING_SLEEP)
        except ConnectionResetError:
            self.logger.warning(f"Steam connection closed {cap_id}")
        except Exception as e:
            self.logger.exception(f'Exception in frame handler {cap_id} {e}')
        finally:
            self.logger.info(f"Stopping stream {cap_id}")
            try:
                await response.write_eof(b'\r\n')
            except:
                pass
            with self.lock:
                self.watched_streams[cap_id] = False
        return response

    def parse_action(self, request):
        query_dict = request.query
        action_string = query_dict.get('action')
        cap_id = None
        if action_string:
            try:
                if "_" in action_string:
                    action, number_string = action_string.split('_')
                    number = int(number_string)
                else:
                    action = action_string
                    number = self.DEFAULT_STREAM_NUMBER
            except (ValueError, AttributeError, TypeError) as e:
                self.logger.warning('Error parsing args: ' + str(e) + str(action_string))
            else:
                try:
                    with self.lock:
                        cap_id = list(self.frames_storage.keys())[number]
                except IndexError:
                    cap_id = None
                return action, cap_id
        return None, None

    def parse_cap_id(self, request):
        query_dict = request.query
        try:
            cap_id = query_dict.get('cap_id')
        except:
            cap_id = None
        if not cap_id:
            try:
                number = request.match_info['number']
            except Exception as e:
                number = query_dict.get('stream', query_dict.get('snapshot'))
            if number is not None:
                try:
                    number = int(number)
                except ValueError:
                    pass
            if number is None:
                number = self.DEFAULT_STREAM_NUMBER
            try:
                with self.lock:
                    cap_id = list(self.frames_storage.keys())[number]
            except IndexError:
                pass
            except Exception as e :
                self.logger.exception(str(e))
        if self.DEBUG:
            self.logger.info("Request path" + request.path)
        self.logger.info("Requested cap_id: " + str(cap_id))
        return cap_id

    async def snapshot_handler(self, request, cap_id=None):
        if cap_id is None:
            cap_id = self.parse_cap_id(request)
        with self.lock:
            self.timestamps[cap_id] = time.monotonic()
            if cap_id in self.frames_storage.keys():
                self.watched_streams[cap_id] = True
        if self.SELF_GET_FRAMES:
            frame = self.next_frame(cap_id)
        else:
            with self.lock:
                frame = self.frames_storage.get(cap_id)
        if not frame:
            frame = b""
        return web.Response(body=frame, content_type='image/jpeg')

    async def view_timeout(self):
        while not self.stop_flag:
            now = time.monotonic()
            with self.lock:
                for cap_id in self.timestamps:
                    if now - self.timestamps.get(cap_id, 0.0) > self.SNAPSHOT_VIEW_TIMEOUT:
                        self.watched_streams[cap_id] = False
            await asyncio.sleep(1)

    async def index(self, request):
        action, cap_id = self.parse_action(request)
        if action:
            if action == 'stream':
                return await self.stream_handler(request, cap_id)
            elif action == 'snapshot':
                return await self.snapshot_handler(request, cap_id)
        else:
            src_string = '<div style="display: flex; flex-wrap: wrap; gap: 10px">'
            with self.lock:
                for idx, cap_id in enumerate(self.frames_storage.keys()):
                    cap_name = "Lost"
                    cap = self.camera.captures.get(cap_id)
                    if cap:
                        cap_name = cap.get_name()
                    src_string += f'<div style="text-align: center"><img src="/stream_{idx}" title="Cam: {cap_name}/{cap_id}"><br>{cap_id}</div>\n'
            src_string += '</div>'
            self.logger.debug(f'Index: img src={src_string}')
            if src_string:
                status = 200
            else:
                status = 404
            return web.Response(text=src_string, status=status, content_type='text/html')

    async def init_server(self, addr, port):
        self.app.router.add_route('*', "/", self.index)
        self.app.router.add_route('*', r"/stream", self.stream_handler)
        self.app.router.add_route('*', r"/snapshot", self.snapshot_handler)
        self.app.router.add_route('*', r"/stream_{number:\d*}", self.stream_handler)
        self.app.router.add_route('*', r"/snapshot_{number:\d*}", self.snapshot_handler)
        if self.DEBUG:
            self.runner = web.AppRunner(self.app, handle_signals=False)
        else:
            self.runner = web.AppRunner(self.app, handle_signals=False, access_log=None)
        await self.runner.setup()
        self.http_site = web.TCPSite(self.runner, addr, port, shutdown_timeout=self.SHUTDOWN_TIMEOUT)
        await self.http_site.start()

    async def shutdown_server(self):
        self.stop_flag = True
        self.logger.info("Shutdown coroutine started")
        await self.runner.shutdown()
        try:
            await self.http_site.stop()
            self.http_site = None
        except AttributeError:
            pass
        await self.runner.cleanup()
        await asyncio.sleep(0)
        for task in asyncio.all_tasks():
            task.cancel()
            try:
                await task
            except asyncio.CancelledError:
                pass
        await asyncio.sleep(0)
        self.logger.info("Shutdown coroutine finished")
