# AUTOGENERATED! DO NOT EDIT! File to edit: ../nbs/api/cameras/cameras.ipynb.

# %% auto 0
__all__ = ['WebCamera', 'switched_camera', 'FlirCameraBase', 'FlirCamera', 'SharedFlirCamera', 'LucidCameraBase', 'LucidCamera', 'SharedLucidCamera', 'XimeaCameraBase', 'XimeaCamera', 'SharedXimeaCamera']

# %% ../nbs/api/cameras/cameras.ipynb 5
# monkey patching class methods using @patch
from fastcore.foundation import *
from fastcore.foundation import patch

# bring forth **kwargs from an inherited class for documentation
from fastcore.meta import delegates

# external
import numpy as np
import ctypes
import matplotlib.pyplot as plt
import warnings
from tqdm import tqdm
from functools import partial

# internal
from .capture import OpenHSI
from .shared import SharedOpenHSI

# %% ../nbs/api/cameras/cameras.ipynb 7
@delegates()
class WebCamera(OpenHSI):
    """Interface for webcam to test OpenHSI functionality"""
    def __init__(self, mode:str = None, **kwargs):
        """Initialise Webcam"""
        super().__init__(**kwargs)
        
        import cv2
        
        # Check if the webcam is opened correctly
        self.vid = cv2.VideoCapture(0)
        if not self.vid.isOpened():
            raise IOError("Cannot open webcam")
        
        self.rgb2gray = partial(cv2.cvtColor, code=cv2.COLOR_RGB2GRAY)
        self.resize   = partial(cv2.resize, dsize=tuple(np.flip(self.settings["resolution"])), interpolation=cv2.INTER_AREA)
        self.close    = cv2.destroyAllWindows
        
    def start_cam(self):
        pass
    
    def stop_cam(self):
        self.vid.release()
        self.close()
    
    def get_img(self) -> np.ndarray:
        ret, frame = self.vid.read()
        frame = self.rgb2gray(frame)
        frame = self.resize(frame)
        return frame
    
    def get_temp(self) -> float:
        return 20.0

# %% ../nbs/api/cameras/cameras.ipynb 12
def switched_camera(
    cam_class:str      = None, # Camera Class Name from openhsi.cameras
    n_lines:int        = 128, # how many along-track pixels
    processing_lvl:int = 0, # desired processing done in real time
    json_path:str      = "/media/pi/fastssd/cals/flir_settings.json", # path to settings file
    pkl_path:str       = "/media/pi/fastssd/cals/flir_calibration.pkl", # path to calibration file
    preconfig_meta:str = "/media/pi/fastssd/cals/preconfig_metadata.json", # path to metadata file
    ssd_dir:str        = "/media/pi/fastssd", # path to SSD
    toggle_interface   = None, # toggle_interface that controls collection
):
    """If `toggle_interface.status` is True, collect with the camera until switched is False."""
        
    cam = cam_class(n_lines = n_lines, processing_lvl = processing_lvl, 
                     json_path = json_path, 
                     pkl_path  = pkl_path)
    cam.start_cam()
    while toggle_interface.status == True: # collect while go button is on.
        cam.collect()
        if 'p' in locals():
            p.join()  # wait for the last process to finish so we don't modify the data when it's being saved
            pass
        p = cam.save(ssd_dir, preconfig_meta_path=preconfig_meta)
        
    cam.stop_cam()

# %% ../nbs/api/cameras/flir.ipynb 6
@delegates()
class FlirCameraBase():
    """Interface for FLIR camera"""
    
    def __init__(self, **kwargs):
        """Initialise FLIR camera"""
        super().__init__(**kwargs)
        
        from simple_pyspin import Camera
        
        self.flircam = Camera()
        self.flircam.init()
        self.flircam.GainAuto = 'Off'
        self.flircam.Gain = 0
        self.flircam.AcquisitionFrameRateAuto = 'Off'
        self.flircam.AcquisitionFrameRateEnabled = True
        self.flircam.AcquisitionFrameRate = int( min(1_000/(self.settings["exposure_ms"]+1),120) )
    
        self.flircam.ExposureAuto = 'Off'
        self.flircam.ExposureTime = self.settings["exposure_ms"]*1e3 # convert to us
        self.flircam.GammaEnabled = False
        
        self.flircam.Width = self.flircam.SensorWidth if self.settings["win_resolution"][1] == 0 else self.settings["win_resolution"][1]
        self.flircam.Height = self.flircam.SensorHeight if self.settings["win_resolution"][0] == 0 else self.settings["win_resolution"][0]
        self.flircam.OffsetY, self.flircam.OffsetX = self.settings["win_offset"]

    
    def start_cam(self):
        self.flircam.start()
    
    def stop_cam(self):
        self.flircam.stop()
        
    def __close__(self):
        self.flircam.close()
    
    def get_img(self) -> np.ndarray:
        return self.flircam.get_array()
    
    def get_temp(self) -> float:
        return self.flircam.DeviceTemperature
    
    def set_exposure(self, exposure_ms:float):
        """sets the FLIR camera exposure time to `exposure_ms`."""
        self.settings["exposure_ms"] = exposure_ms
        
        self.flircam.AcquisitionFrameRateAuto = 'Off'
        self.flircam.AcquisitionFrameRateEnabled = True
        self.flircam.AcquisitionFrameRate = int( min(1_000/(self.settings["exposure_ms"]+1),120) )
        self.flircam.ExposureAuto = 'Off'
        self.flircam.ExposureTime = self.settings["exposure_ms"]*1e3 # convert to us
        
@delegates()
class FlirCamera(OpenHSI, FlirCameraBase):
    pass

# %% ../nbs/api/cameras/flir.ipynb 9
@delegates()
class SharedFlirCamera(FlirCameraBase, SharedOpenHSI):
    pass

# %% ../nbs/api/cameras/lucidvision.ipynb 6
@delegates()
class LucidCameraBase():
    """Core functionality for Lucid Vision Lab cameras
        
        Any keyword-value pair arguments must match the those avaliable in settings file. LucidCamera expects the ones listed below:

        - `binxy`: number of pixels to bin in (x,y) direction
        - `win_resolution`: size of area on detector to readout (width, height)
        - `win_offset`: offsets (x,y) from edge of detector for a selective 
        - `exposure_ms`: is the camera exposure time to use
        - `pixel_format`: format of pixels readout sensor, ie Mono8, Mono10, Mono10p, Mono10Packed, Mono12, Mono12p, Mono12Packed, Mono16
        - `mac_addr`: str = "1c:0f:af:01:7b:a0",
    """
    def __init__(self,**kwargs):
        """Initialise Camera"""
        # https://thinklucid.com/downloads-hub/
        super().__init__(**kwargs)
        
        from arena_api.system import system as arsys
        
        self.arsys = arsys  # make avalaible for later access just in case.
        arsys.destroy_device() # reset an existing connections.
        
        try:
            self.arsys.device_infos
            #self.device = arsys.create_device(device_infos=[{"mac": mac_addr}])[0]
            self.device = arsys.create_device()[0]
        except DeviceNotFoundError as exc:
            raise RuntimeError("DeviceNotFoundError: Please connect a lucid vision camera and run again.") from exc
            
        # allow api to optimise stream
        tl_stream_nodemap = self.device.tl_stream_nodemap
        tl_stream_nodemap["StreamAutoNegotiatePacketSize"].value = True
        tl_stream_nodemap["StreamPacketResendEnable"].value = True

        # init access to device settings
        self.deviceSettings = self.device.nodemap.get_node([
                "AcquisitionFrameRate",
                "AcquisitionFrameRateEnable",
                "AcquisitionMode",
                "AcquisitionStart",
                "AcquisitionStop",
                "BinningHorizontal",
                "BinningVertical",
                "DevicePower",
                "DeviceTemperature",
                "DeviceUpTime",
                "DeviceUserID",
                "ExposureAuto",
                "ExposureTime",
                "Gain",
                "GammaEnable",
                "Height",
                "OffsetX",
                "OffsetY",
                "PixelFormat",
                "ReverseX",
                "ReverseY",
                "Width",
                "GevMACAddress",
                "DeviceSerialNumber"
            ]
        )
        
        # set pixel settings
        self.deviceSettings["BinningHorizontal"].value = self.settings["binxy"][0] # binning is symetric on this sensor, no need to set vertical
        self.deviceSettings["PixelFormat"].value = self.settings["pixel_format"]
        
        # always reset to no window.
        self.deviceSettings["OffsetY"].value = 0
        self.deviceSettings["OffsetX"].value = 0
        self.deviceSettings["Height"].value = self.deviceSettings["Height"].max
        self.deviceSettings["Width"].value = self.deviceSettings["Width"].max
        
        # print("Setting window to: height {}, offset y {}, width {}, offsetx {}".format(self.settings["win_resolution"][0],
        #                                                                     self.settings["win_offset"][0],
        #                                                                     self.settings["win_resolution"][1],
        #                                                                     self.settings["win_offset"][1])
        #      )
        
        # set window up.
        self.deviceSettings["Height"].value = self.settings["win_resolution"][0] if self.settings["win_resolution"][0] > 0 else self.deviceSettings["Height"].max
        self.deviceSettings["Width"].value = self.settings["win_resolution"][1] if self.settings["win_resolution"][1] > 0 else self.deviceSettings["Width"].max
    
        self.deviceSettings["OffsetY"].value = self.settings["win_offset"][0] if self.settings["win_offset"][0] > 0 else self.deviceSettings["OffsetY"].max
        self.deviceSettings["OffsetX"].value = self.settings["win_offset"][1] if self.settings["win_offset"][1] > 0 else self.deviceSettings["OffsetX"].max
        
        # set exposure realted props
        self.deviceSettings["ExposureAuto"].value = "Off" # always off as we need to match exposure to calibration data
        self.set_exposure(self.settings["exposure_ms"])
        
        self.set_gain(0) # default to 0 as we need to match to calibration data
        
        self.rows, self.cols = (
            self.deviceSettings["Height"].value,
            self.deviceSettings["Width"].value,
        )
        
        self.settings['camera_id'] = self.deviceSettings["DeviceUserID"].value

    def __exit__(self, *args, **kwargs):
        self.device.stop_stream()
        self.arsys.destroy_device()

    def start_cam(self):
        self.device.start_stream(1)

    def stop_cam(self):
        self.device.stop_stream()
        
    def set_exposure(self,exposure_ms:float):
        
        if exposure_ms < self.deviceSettings["ExposureTime"].min/1000.0:
            exposure_us=self.deviceSettings["ExposureTime"].min
        else:
            exposure_us = exposure_ms*1000.0
            
        nominal_framerate = 1_000_000.0/exposure_us*0.98
        
        # print("nominal_framerate {}, exposure_us {}".format(nominal_framerate,exposure_us))
        
        if  nominal_framerate < self.deviceSettings['AcquisitionFrameRate'].max:
            self.deviceSettings["AcquisitionFrameRateEnable"].value=True
            self.deviceSettings['AcquisitionFrameRate'].value = nominal_framerate
        else:
            self.deviceSettings["AcquisitionFrameRateEnable"].value=False
            
        self.deviceSettings["ExposureTime"].value = exposure_us # requires time in us float
        self.settings["exposure_ms"] = self.deviceSettings["ExposureTime"].value/1000.00  # exposure time rounds, so storing actual value

    def set_gain(self,gain_val:float):
        self.deviceSettings["Gain"].value = gain_val * 1. # make float always
        
    def get_img(self) -> np.ndarray:
        image_buffer = self.device.get_buffer()
        if image_buffer.bits_per_pixel == 8:
            nparray_reshaped = np.ctypeslib.as_array(
                image_buffer.pdata, (image_buffer.height, image_buffer.width)
            ).copy()
        
        elif image_buffer.bits_per_pixel == 12 or image_buffer.bits_per_pixel == 10:
            split=np.ctypeslib.as_array(image_buffer.pdata,(image_buffer.buffer_size,1)).astype(np.uint16)
            fst_uint12 = (split[0::3] << 4) + (split[1::3] >> 4)
            snd_uint12 = (split[2::3] << 4) + (np.bitwise_and(15, split[1::3]))
            nparray_reshaped = np.reshape(np.concatenate((fst_uint12[:, None], snd_uint12[:, None]), axis=1), 
                                          (image_buffer.height, image_buffer.width))

        elif image_buffer.bits_per_pixel == 16:        
            pdata_as16 = ctypes.cast(image_buffer.pdata, ctypes.POINTER(ctypes.c_ushort))
            nparray_reshaped = np.ctypeslib.as_array(
                pdata_as16, (image_buffer.height, image_buffer.width)
            ).copy()
        
        #nparray_reshaped=np.ctypeslib.as_array(image_buffer,(1,image_buffer.buffer_size))
        self.device.requeue_buffer(image_buffer)
        return nparray_reshaped

    def get_temp(self) -> float:
        return self.deviceSettings["DeviceTemperature"].value
        
    def get_mac(self)-> str:
        return ':'.join(['{}{}'.format(a, b)
                         for a, b
                         in zip(*[iter('{:012x}'.format(cam.deviceSettings['GevMACAddress'].value))]*2)])
    
@delegates()
class LucidCamera(LucidCameraBase, OpenHSI):
    pass
        

# %% ../nbs/api/cameras/lucidvision.ipynb 9
@delegates()
class SharedLucidCamera(LucidCameraBase, SharedOpenHSI):
    pass

# %% ../nbs/api/cameras/ximea.ipynb 5
@delegates()
class XimeaCameraBase():
        
    """Core functionality for Ximea cameras"""
    # https://www.ximea.com/support/wiki/apis/Python
    def __init__(self, exposure_ms:float = 10, serial_num:str = None, **kwargs):
        """Initialise Camera"""
        
        super().__init__(**kwargs)
                    
        from ximea import xiapi
        self.xiapi=xiapi # make avalaible for later access just in case.
        
        self.xicam = self.xiapi.Camera()
        
        self.xicam.open_device_by_SN(serial_num) if serial_num else self.xicam.open_device()

        print(f'Connected to device {self.xicam.get_device_sn()}')
        
        self.xicam.enable_horizontal_flip()

        self.xicam.set_binning_vertical(self.settings["binxy"][0])
        self.xicam.set_binning_vertical_mode("XI_BIN_MODE_SUM")

        
        # set window up.
        self.xicam.set_height(self.settings["win_resolution"][0] if self.settings["win_resolution"][0] > 0 else self.xicam.get_height_maximum())
        self.xicam.set_width(self.settings["win_resolution"][1] if self.settings["win_resolution"][1] > 0 else self.xicam.get_width_maximum())
    
        self.xicam.set_offsetY(self.settings["win_offset"][0] if self.settings["win_offset"][0] > 0 else self.xicam.get_offsetY_maximum())
        self.xicam.set_offsetX(self.settings["win_offset"][1] if self.settings["win_offset"][1] > 0 else self.xicam.get_offsetX_maximum())
        

        self.set_exposure(self.settings["exposure_ms"])
        
        self.xicam.set_gain_direct(0.0)

        self.xicam.set_imgdataformat(self.settings["pixel_format"])
        if self.settings["pixel_format"] == "XI_RAW16":
            self.xicam.set_output_bit_depth("XI_BPP_12")
            self.xicam.enable_output_bit_packing()
            
        self.xicam.disable_aeag()
        
        self.rows, self.cols = self.xicam.get_height(), self.xicam.get_width()
        self.img = xiapi.Image()
        
        
    def __exit__(self, *args, **kwargs):
        self.xicam.stop_acquisition()
        self.xicam.close_device()
        
    def set_exposure(self,exposure_ms:float):
            self.xicam.set_exposure_direct(1000*exposure_ms)
            self.settings["exposure_ms"] = self.xicam.get_exposure()/1000  # exposure time rounds, so storing actual value

    def start_cam(self):
        self.xicam.start_acquisition()
    
    def stop_cam(self):
        self.xicam.stop_acquisition()
    
    def get_img(self) -> np.ndarray:
        self.xicam.get_image(self.img)
        return self.img.get_image_data_numpy()
    
    def get_temp(self) -> float:
        return self.xicam.get_temp()

@delegates()
class XimeaCamera(XimeaCameraBase, OpenHSI):
    pass

# %% ../nbs/api/cameras/ximea.ipynb 9
@delegates()
class SharedXimeaCamera(XimeaCameraBase, SharedOpenHSI):
    pass