# This plugin lets you remotely query and view a
# number of different image sources in Sideband,
# including remote or local webcams, video sources
# or images stored in a filesystem.
#
# This plugin requires the "pillow" pip package.
#
# For HTTP and local file sources, no extras are
# required, but for fetching images from connected
# video sources, you need "opencv-python" from pip.

import io
import os
import RNS
import time
import queue
import requests
import threading
import importlib
from PIL import Image as PilImage

if importlib.util.find_spec("cv2") != None:
    import cv2

# Add view sources to the plugin
def register_view_sources():
    ViewCommandPlugin.add_source("xkcd", HttpSource("https://imgs.xkcd.com/comics/tsp_vs_tbsp.png"))
    ViewCommandPlugin.add_source("camera", CameraSource(camera_index=0))
    ViewCommandPlugin.add_source("rocks", FileSource("~/Downloads/rocks.jpg"))
    ViewCommandPlugin.add_source("osaka", StreamSource("http://honjin1.miemasu.net/nphMotionJpeg?Resolution=640x480&Quality=Standard"))
    ViewCommandPlugin.add_source("factory", StreamSource("http://takemotopiano.aa1.netvolante.jp:8190/nphMotionJpeg?Resolution=640x480&Quality=Standard&Framerate=1"))

quality_presets = {
    "lora": {"max": 160, "quality": 18},
    "low": {"max": 256, "quality": 25},
    "default": {"max": 320, "quality": 33},
    "medium": {"max": 480, "quality": 50},
    "high": {"max": 960, "quality": 65},
    "hd": {"max": 1920, "quality": 75},
    "4k": {"max": 3840, "quality": 65},
}

if not "default" in quality_presets:
    raise ValueError("No default quality preset defined, please define one and reload the plugin")

class ViewSource():
    DEFAULT_STALE_TIME = 3.14159

    def __init__(self):
        self.source_data = None
        self.last_update = 0
        self.stale_time = ViewSource.DEFAULT_STALE_TIME

    def is_stale(self):
        return time.time() > self.last_update + self.stale_time

    def update(self):
        raise NotImplementedError()

    def scaled_image(self, max_dimension, quality):
        with PilImage.open(io.BytesIO(self.source_data)) as im:
            im.thumbnail((max_dimension, max_dimension))
            buf = io.BytesIO()
            im.save(buf, format="webp", quality=quality)
            return buf.getvalue()

    def get_image_field(self, preset="default"):
        if not preset in quality_presets:
            preset = "default"

        try:
            if self.is_stale():
                self.update()
            
            if self.source_data != None:
                max_dimension = quality_presets[preset]["max"]
                quality = quality_presets[preset]["quality"]
                return ["webp", self.scaled_image(max_dimension, quality)]

        except Exception as e:
            RNS.log(f"Could not create image field for {self}. The contained exception was: {e}", RNS.LOG_ERROR)
            RNS.trace_exception(e)

        return None

class HttpSource(ViewSource):
    def __init__(self, url):
        self.url = url
        super().__init__()

    def update(self):
        image_request = requests.get(self.url, stream=True)
        if image_request.status_code == 200:
            self.source_data = image_request.content
            self.last_update = time.time()
        else:
            self.source_data = None

class CameraSource(ViewSource):
    def __init__(self, camera_index=0, camera_width=1280, camera_height=720):
        self.camera_index  = camera_index
        self.camera_width  = camera_width
        self.camera_height = camera_height
        self.camera_ready  = False
        self.frame_queue   = queue.Queue()
        super().__init__()

        self.start_reading()

    def start_reading(self):
        self.camera = cv2.VideoCapture(self.camera_index)
        self.camera.set(cv2.CAP_PROP_FRAME_WIDTH, self.camera_width)
        self.camera.set(cv2.CAP_PROP_FRAME_HEIGHT, self.camera_height)
        threading.Thread(target=self.read_frames, daemon=True).start()

    def read_frames(self):
        try:
            while True:
                ret, frame = self.camera.read()
                self.camera_ready = True
                if not ret:
                    self.camera_ready = False
                    break
                
                if not self.frame_queue.empty():
                    try:
                        self.frame_queue.get_nowait()
                    except queue.Empty:
                        pass
                
                self.frame_queue.put(frame)

        except Exception as e:
            RNS.log("An error occurred while reading frames from the camera: "+str(e), RNS.LOG_ERROR)

        self.release_camera()

    def update(self):
        if not self.camera:
            self.start_reading()
            while not self.camera_ready:
              time.sleep(0.2)
        
        retval, frame = self.camera.read()
      
        if not retval:
            self.source_data = None
        else:
            retval, buffer = cv2.imencode(".png", frame)
            self.source_data = io.BytesIO(buffer).getvalue()
            self.last_update = time.time()

    def release_camera(self):
        try:
            self.camera.release()
        except:
            pass  
        
        self.camera = None
        self.camera_ready = False

class StreamSource(ViewSource):
    DEFAULT_IDLE_TIMEOUT = 10

    def __init__(self, url=None):
        self.url          = url
        self.stream_ready = False
        self.frame_queue  = queue.Queue()
        self.stream       = None
        self.started      = 0
        self.idle_timeout = StreamSource.DEFAULT_IDLE_TIMEOUT
        super().__init__()

        self.start_reading()

    def start_reading(self):
        self.stream  = cv2.VideoCapture(self.url)
        self.started = time.time()
        threading.Thread(target=self.read_frames, daemon=True).start()

    def read_frames(self):
        try:
            while max(self.last_update, self.started)+self.idle_timeout > time.time():
                ret, frame = self.stream.read()
                if not ret:
                    self.stream_ready = False
                else:
                    self.stream_ready = True            
                    if not self.frame_queue.empty():
                        if self.frame_queue.qsize() > 1:
                            try:
                                self.frame_queue.get_nowait()
                            except queue.Empty:
                                pass
                    
                    self.frame_queue.put(frame)

            RNS.log(str(self)+" idled", RNS.LOG_DEBUG)

        except Exception as e:
            RNS.log("An error occurred while reading frames from the stream: "+str(e), RNS.LOG_ERROR)

        self.release_stream()

    def update(self):
        if not self.stream:
            self.start_reading()
            while not self.stream_ready:
              time.sleep(0.2)
              if self.stream == None:
                self.source_data = None
                return

        frame = self.frame_queue.get()
        retval, buffer = cv2.imencode(".png", frame)
        self.source_data = io.BytesIO(buffer).getvalue()
        self.last_update = time.time()

    def release_stream(self):
        try:
            self.stream.release()
        except:
            pass  
        
        self.stream = None
        self.stream_ready = False

class FileSource(ViewSource):
    def __init__(self, path):
        self.path = os.path.expanduser(path)
        super().__init__()

    def update(self):
        try:
            with open(self.path, "rb") as image_file:
                self.source_data = image_file.read()

        except Exception as e:
            RNS.log("Could not read image at \"{self.path}\": "+str(e), RNS.LOG_ERROR)
            self.source_data = None

class ViewCommandPlugin(SidebandCommandPlugin):
    command_name = "view"
    sources      = {}

    stamptimefmt = "%Y-%m-%d %H:%M:%S"

    def start(self):
        RNS.log("View command plugin starting...")
        super().start()

    def stop(self):
        super().stop()

    @staticmethod
    def add_source(name, source):
        ViewCommandPlugin.sources[name] = source

    def message_response(self, message, destination):
        self.get_sideband().send_message(
            message,
            destination,
            False,                # Don't use propagation by default, try direct first
            skip_fields = True,   # Don't include any additional fields automatically
            no_display = True,    # Don't display this message in the message stream
        )

    def image_response(self, message, image_field, destination):
        self.get_sideband().send_message(
            message,
            destination,
            False,                # Don't use propagation by default, try direct first
            skip_fields = True,   # Don't include any additional fields automatically
            no_display = True,    # Don't display this message in the message stream
            image = image_field,  # Add the scaled and compressed image
        )

    def timestamp_str(self, time_s):
        timestamp = time.localtime(time_s)
        return time.strftime(self.stamptimefmt, timestamp)

    def handle_command(self, arguments, lxm):
        requestor = lxm.source_hash

        if len(arguments) == 0:
            self.message_response("No view source was specified", requestor)
            return

        if arguments[0] == "--list" or arguments[0] == "-l":
            if len(self.sources) == 0:
                response = "No sources available on this system"
            else:
                response = "Available Sources:\n"
                for source in self.sources:
                    response += "\n - "+str(source)

            self.message_response(response, requestor)
            return

        try:
            source = arguments[0]
            if len(arguments) > 1:
                quality_preset = arguments[1]
            else:
                quality_preset = "default"

            if not source in self.sources:
                self.message_response("The specified view source does not exist on this system", requestor)
            
            else:
                image_field     = self.sources[source].get_image_field(quality_preset)
                image_timestamp = self.timestamp_str(self.sources[source].last_update)
                message         = f"Source [b]{source}[/b] at [b]{image_timestamp}[/b]"

                if image_field != None:
                    self.image_response(message, image_field, requestor)
                else:
                    self.message_response("The image source could not be retrieved or prepared", requestor)

        except Exception as e:
            self.message_response(f"An error occurred:\n\n{e}", requestor)

register_view_sources()

# Finally, tell Sideband what class in this
# file is the actual plugin class.
plugin_class = ViewCommandPlugin