import aiohttp
from aiohttp import web
import asyncio
import collections
import cv2
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' }
    SHUTDOWN_TIMEOUT = 2
    FRAME_RATE_LIMITING_SLEEP = 1/60

    DEBUG = True

    def __init__(self):
        self.logger = logging.getLogger(__name__.__class__.__name__)
        self.stop_flag = False
        self.frame_storage_lock = threading.RLock()
        self.stored_frames = collections.OrderedDict()
        self.frame_getting_functions = collections.OrderedDict()
        self.watched_streams = collections.OrderedDict()
        self.requested_snaps = collections.OrderedDict()
        self.default_stream_number = 1
        settings = config.get_settings()['camera']['http_output']
        if settings['only_localhost']:
            addr = '127.0.0.1'
        else:
            addr = '0.0.0.0'
        #  if settings['https']:
        #      self.logger.warning("HTTPS is not implemented yet")
        self.loop = asyncio.new_event_loop()
        self.app = web.Application()
        asyncio.run_coroutine_threadsafe(self.init_server(addr, settings['port']), self.loop)
        threading.Thread.__init__(self, name=__class__.__name__, daemon=True)

    def run(self):
        self.logger.info("Starting " + __class__.__name__)
        self.loop.run_forever()
        self.logger.info("Exit " + __class__.__name__)

    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()
        with self.frame_storage_lock:
            for key in self.stored_frames.keys():
                self.stored_frames[key] = None
            self.stored_frames = collections.OrderedDict()
            for key in self.frame_getting_functions.keys():
                self.frame_getting_functions[key] = None
            self.frame_getting_functions = collections.OrderedDict()

    def put_frame(self, frame, camera_id):
        with self.frame_storage_lock:
            self.stored_frames[camera_id] = frame

    def set_frame_getting_functions(self, func, number):
        with self.frame_storage_lock:
            self.frame_getting_functions[number] = func

    def flush_storages(self):
        with self.frame_storage_lock:
            self.stored_frames.clear()
            self.frame_getting_functions.clear()
    
    def next_frame(self, stream_number):
        with self.frame_storage_lock:
            if stream_number in self.stored_frames.keys():
                # TODO add an ability to get frame by order number instead of key
                # keys = list(self.stored_frames.keys())
                # try:
                #    key = keys[stream_number]
                # except IndexError:
                #     self.logger.warning(f'No such stream: {stream_number}. Available keys: {keys}')
                #     #TODO handle invalid number properly with a message to a user
                # else:
                frame = self.stored_frames[stream_number]
                if frame:
                    #del self.stored_frames[stream_number]
                    return frame
            if stream_number in self.frame_getting_functions.keys():
                return self.frame_getting_functions[stream_number]()

    async def stream_handler(self, request):
        # TODO add capture disconnection processing
        number = self.parse_number(request)
        self.logger.debug(f'Frame request {number}')
        self.watched_streams[number] = True
        response = web.StreamResponse(headers=self.RESP_HEADER)
        # self.logger.debug('Frame request created a response')
        try:
            await response.prepare(request)
            prev_frame = None
            while not self.stop_flag:
                # self.logger.debug('Frame request enters its loop')
                frame = self.next_frame(number)
                if frame and prev_frame != 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)
                # else:
                #     await asyncio.sleep(self.FRAME_RATE_LIMITING_SLEEP)
                    prev_frame = frame
                await asyncio.sleep(self.FRAME_RATE_LIMITING_SLEEP)
            await response.write_eof()
        except Exception as e:
            try:
                await response.write_eof()
            except:
                pass
            self.logger.error(f'Exception in frame handler {e}')
        self.watched_streams[number] = False
        return response

    def parse_action(self, request):
        query_dict = request.query
        action_string = query_dict.get('action')
        stream_number = None
        if action_string:
            try:
                action, stream_number_string = action_string.split('_')
            except ValueError:
                pass
            else:
                try:
                    stream_number = int(stream_number_string)
                except:
                    self.logger.warning(f'Unparsable stream number {stream_number_string}')
            if action == 'snapshot':
                return 'snapshot', stream_number
            if action == 'stream':
                return 'stream', stream_number
        return None, None

    def parse_number(self, request):
        try:
            stream_number = int(request.match_info['number'])
        except:
            stream_number = self.default_stream_number
        return stream_number

    async def snapshot_handler(self, request):
        stream_number = self.parse_number(request)
        return web.Response(body=self.next_frame(stream_number), content_type='image/jpeg')

    async def index(self, request):
        action, stream_number = self.parse_action(request)
        if not action:
            action = 'stream'
        if stream_number == None:
            stream_number = self.default_stream_number
        src_string = f"/{action}_{stream_number}"
        self.logger.debug(f'Index: img src={src_string}')
        return web.Response(text=f'<img src="%s">' % src_string, 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_{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.logger.info("Shutdown coroutine started")
        await self.runner.shutdown()
        try:
            await self.http_site.stop()
            self.http_site = None
        except AttributeError:
            pass
        #  try:
        #      await self.https_site.stop()
        #      self.https_site = None
        #  except AttributeError:
        #      pass
        await self.runner.cleanup()
        self.logger.info("Shutdown coroutine finished")


if __name__ == '__main__':

    with open('/tmp/1.jpg', 'rb') as f:
        frame = f.read()

    def get_frame():
        return frame

    logging.basicConfig(level=logging.DEBUG)
    logger = logging.getLogger(__name__)
    logger.setLevel(logging.DEBUG)
    logger.info('Creating server')
    server = HTTPMJEPServer()
    #server.set_frame_getting_functions(get_frame, 1)
    run_flag = True
    try:
        logger.info('Running server')
        server.start()
        while run_flag:
            server.put_frame(frame, 1)
            time.sleep(1/120)
    except KeyboardInterrupt:
        run_flag = False
        if server:
            logger.info('Calling server close')
            server.stop()
            logger.info('Joining server thread')
            server.join(10)
