"""IMI Camera Control Class (Neptune API)"""

import ctypes
import logging
import multiprocessing
import time
from dataclasses import dataclass
from threading import Thread
from typing import Optional

import cv2
import numpy as np

from py_alaska import rmi_task, rmi_signal, rmi_run


@dataclass
class FrameInfo:
    """Frame metadata for camera callback"""
    sm_index: int       # SmBlock index
    rx_sequence: int    # Frame sequence number from camera
    rx_timestamp: float # Frame receive timestamp
    rx_count: int       # Total received frame count
    rx_drop: int        # Total dropped frame count

try:
    # Import as package
    from ...sm_block import SmBlock
    from ...gconfig import gconfig
    from .Neptune_API import *
except ImportError:
    # Direct execution
    import sys
    from pathlib import Path
    sys.path.insert(0, str(Path(__file__).resolve().parents[3]))
    from py_alaska.sm_block import SmBlock
    from py_alaska.gconfig import gconfig
    from py_alaska.drives.imi.Neptune_API import *

log = logging.getLogger(__name__)


class CamProperty:
    """카메라 하드웨어 property descriptor

    - getter: is_opened=True면 하드웨어에서 읽기, 아니면 캐시값 반환
    - setter: is_opened=True면 즉시 적용, 아니면 _lazy_updates에 저장
    """
    def __init__(self, get_func: str = None, set_func: str = None, default=None):
        self.get_func = get_func
        self.set_func = set_func
        self.default = default
        self.attr_name = None

    def __set_name__(self, owner, name):
        self.attr_name = name
        self.private_name = f'_{name}'

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        # is_opened가 아니면 캐시값 반환 (하드웨어 호출 안함)
        if not getattr(obj, 'is_opened', False):
            return getattr(obj, self.private_name, self.default)
        if self.get_func:
            try:
                value = getattr(obj, self.get_func)()
                if value is not None:
                    return value  # 하드웨어 값 반환 (캐시 갱신 안함)
            except Exception as e:
                log.error(f"[CamProperty] {self.attr_name} get failed: {e}")
        return getattr(obj, self.private_name, self.default)

    def __set__(self, obj, value):
        setattr(obj, self.private_name, value)
        if getattr(obj, 'is_opened', False):
            if self.set_func:
                try:
                    getattr(obj, self.set_func)(value)
                except Exception as e:
                    log.error(f"[CamProperty] {self.attr_name} set failed: {e}")
        else:
            if not hasattr(obj, '_lazy_updates'):
                obj._lazy_updates = {}
            obj._lazy_updates[self.attr_name] = value


@rmi_task(name="imi_cam_driver", mode="process", restart=True)
class imi_cam_driver:
    """IMI Camera Control Class"""

    # ═══════════════════════════════════════════════════════════════════════════
    # Class Variables & Descriptors
    # ═══════════════════════════════════════════════════════════════════════════
    DriverInit = False
    HEARTBEAT_TIME = 500

    fps = CamProperty(get_func='_hw_get_fps', set_func='_hw_set_fps', default=12.0)
    exposure = CamProperty(get_func='_hw_get_exposure', set_func='_hw_set_exposure', default=15000)
    trigger_mode = CamProperty(get_func='_hw_get_trigger', set_func='_hw_set_trigger', default=False)

    # ═══════════════════════════════════════════════════════════════════════════
    # Initialization
    # ═══════════════════════════════════════════════════════════════════════════
    def __init__(self,runtime):
        self.runtime = runtime
        self.is_opened = False
        self._lazy_updates = {}
        self.rx_cmdq = None  # Lazy init in task_loop (pickle 호환)
        self.is_opened = False
        self.rx_drop = 0
        self.rx_count = 0
        self.handle = None  # Lazy init in task_loop (pickle 호환)
        # 캐시 초기값 (CamProperty default와 일치)
        self._fps = 12.0
        self._exposure = 15000
        self._trigger_mode = False
        self.trigger_source = "software"  

    # ═══════════════════════════════════════════════════════════════════════════
    # Lifecycle (open / reopen / close)
    # ═══════════════════════════════════════════════════════════════════════════
    def open(self) -> bool:
        log.info(f"[ImiCamera] Open called mac_address={self.mac_address}*********************")
               # Driver initialization
        if not imi_cam_driver.DriverInit:
            if ntcInit() != ENeptuneError.NEPTUNE_ERR_Success.value:
                log.error("[ImiCamera] Driver initialization failed")
                return False
            imi_cam_driver.DriverInit = True
            log.info("[ImiCamera] Driver initialized")
            
        if self.is_opened:
            log.debug("[ImiCamera] Already connected")
            return True
        self.put_cmd("connected")  # invoke reopen
        return True
    
    def discovery(self): 
        """연결된 카메라 MAC 주소 리스트 반환"""
        return imi_cam_driver.get_mac_list()

    def reopen(self):
        print(f"[ImiCamera] try reopen {self.mac_address} ")
        if self.is_opened:
            log.debug("[ImiCamera] Already connected")
            return True
        
        # Clean up existing handle
        if self.handle:
            ntcClose(self.handle)
        self.handle = ctypes.c_void_p(0)

        # mac_address가 미지정이면 카메라 검색
        if self.mac_address is None:
            mac_list = imi_cam_driver.get_mac_list()
            if not mac_list:
                log.error("[ImiCamera] No available camera found")
                return False
            # mac_list 내에서 첫 번째 카메라 선택
            self.mac_address = mac_list[0]
            log.info(f"\n[ImiCamera] Auto-selected camera: {self.mac_address}")

        # mac_address가 지정된 상태에서 연결 시도
        err = ntcOpen(bytes(self.mac_address, encoding='utf-8'), ctypes.byref(self.handle))
        if err != ENeptuneError.NEPTUNE_ERR_Success.value:
            log.error(f"[ImiCamera] ntcOpen failed: {self.mac_address} (err={err})")
            return False
        self.is_opened = True

        if ntcSetHeartbeatTime(self.handle, self.HEARTBEAT_TIME) != ENeptuneError.NEPTUNE_ERR_Success.value:
            log.error("[ImiCamera] ntcSetHeartbeatTime failed")
            return False

        # Callback setup
        self.frameCallBack = ctypes.CFUNCTYPE(None, NEPTUNE_IMAGE, ctypes.c_void_p)(self.RecvFrameCallBack)
        ntcSetFrameCallback(self.handle, self.frameCallBack, self.handle)
        self.callback_drop_func = ctypes.CFUNCTYPE(None, ctypes.c_void_p)(self.RecvFrameDropCallBack)
        ntcSetFrameDropCallback(self.handle, self.callback_drop_func, self.handle)
        self.callback_device_check_func = ctypes.CFUNCTYPE(None, ctypes.c_int, ctypes.c_void_p)(self.RecvDeviceCheckCallBack)
        ntcSetDeviceCheckCallback(self.callback_device_check_func, self.handle)

        self.func_unplug = ctypes.CFUNCTYPE(None, ctypes.c_void_p)(self.RecvUnPlugCallBack)
        ntcSetUnplugCallback(self.handle, self.func_unplug, self.handle)
        self.func_timeout = ctypes.CFUNCTYPE(None, ctypes.c_void_p)(self.RecvTimeoutCallBack)
        ntcSetRecvTimeoutCallback(self.handle, self.func_timeout, self.handle)

        for i, state in enumerate([False, True]):
            ntcSetAcquisition(self.handle, ENeptuneBoolean.NEPTUNE_BOOL_TRUE.value if state else ENeptuneBoolean.NEPTUNE_BOOL_FALSE.value)
        self._lazy_reset()

        self.is_opened = True
        log.info("[ImiCamera] Connected")
        self._emit_connected()

    def close(self) -> None:
        log.info(f"[ImiCamera] Close called, connected={self.is_opened}")
        if not self.is_opened:
            return
        try:
            self.put_cmd("close")
            ntcClose(self.handle)
            log.info("[ImiCamera] Closed")
            self.put_cmd("disconnected")
        except Exception as e:
            log.exception(f"[ImiCamera] Error during close: {e}")
            self.put_cmd("disconnected")

    # ═══════════════════════════════════════════════════════════════════════════
    # Signal Emit (카메라 상태 알림)
    # ═══════════════════════════════════════════════════════════════════════════
    def _emit_connected(self):
        """카메라 연결 Signal 발행"""
        if hasattr(self, 'runtime') and self.runtime and hasattr(self.runtime, 'signal') and self.runtime.signal:
            data = {
                "source": self.runtime.name,
                "mode": "real",
                "fps": self._fps
            }
            print(f"[{self.runtime.name}] EMIT camera.connected: {data}")
            self.runtime.signal.emit("camera.connected", data=data)
        else:
            print(f"[ImiCamera] _emit_connected: no signal client available")

    def _emit_disconnected(self, reason: str = "stopped"):
        """카메라 연결 해제 Signal 발행"""
        if hasattr(self, 'runtime') and self.runtime and hasattr(self.runtime, 'signal') and self.runtime.signal:
            data = {
                "source": self.runtime.name,
                "reason": reason,
                "captured": self.rx_count,
                "dropped": self.rx_drop
            }
            print(f"[{self.runtime.name}] EMIT camera.disconnected: {data}")
            self.runtime.signal.emit("camera.disconnected", data=data)
        else:
            print(f"[ImiCamera] _emit_disconnected: no signal client available")

    # ═══════════════════════════════════════════════════════════════════════════
    # Task Interface (task_loop / command queue)
    # ═══════════════════════════════════════════════════════════════════════════
    def put_cmd(self, cmd):
        if self.rx_cmdq is not None:
            self.rx_cmdq.put(cmd)
            
    @rmi_run()
    def run(self, runtime):
        log = runtime.log
        # Lazy 초기화 (pickle 호환을 위해 여기서 생성)
        self.rx_cmdq = multiprocessing.Queue()
        self.handle = ctypes.c_void_p(0)
        self.open()
        last_reconnect_time = 0.0
        RECONNECT_INTERVAL = 3.0

        while not runtime.should_stop():
            if not self.rx_cmdq.empty():
                cmd = self.rx_cmdq.get()
                log.debug(f"[ImiCamera] Signal received: {cmd}")
                if cmd == "close":
                    self._emit_disconnected("close")
                    self.is_opened = False
                    break
                if cmd == "connected":
                    log.info("[ImiCamera] Reopen command received")
                    self.reopen()
                if cmd == "disconnected":
                    self._emit_disconnected("disconnected")
                    log.info("[ImiCamera] Disconnected command received")
                    self.is_opened = False
            # 자동 재연결: is_opened가 아니면 3초마다 reopen 시도
            if not self.is_opened:
                now = time.time()
                if now - last_reconnect_time >= RECONNECT_INTERVAL:
                    log.info("\n[ImiCamera] Auto-reconnect attempt...***************")
                    self.reopen()
                    last_reconnect_time = now
            time.sleep(0.001)

        self._emit_disconnected("end_of_run")
        log.info(f"[ImiCamera] Thread stopped, reason=end_of_run")

    # ═══════════════════════════════════════════════════════════════════════════
    # Hardware Callbacks
    # ═══════════════════════════════════════════════════════════════════════════
    def RecvDeviceCheckCallBack(self, state: int, pContext: Optional[ctypes.c_void_p] = None) -> None:
        log.info(f"[ImiCamera] Device {'added' if state == 0 else 'removed'}")
        if state == 0:
            self.put_cmd("connected") # call reoped with acquisition restart
        else:
            self.put_cmd("disconnected")
        return None

    def RecvUnPlugCallBack(self, _: Optional[ctypes.c_void_p] = None) -> None:
        """USB 분리 콜백"""
        log.warning("[ImiCamera] Device unplugged")
        self.put_cmd("disconnected")

    def RecvTimeoutCallBack(self, _: Optional[ctypes.c_void_p] = None) -> None:
        """프레임 수신 타임아웃 콜백"""
        log.warning("[ImiCamera] Frame timeout")

    def RecvFrameCallBack(self, pImage: NEPTUNE_IMAGE, pContext: Optional[ctypes.c_void_p] = None) -> Optional[np.ndarray]:
        self.rx_count += 1

        if self.smblock is None:
            log.warning("[ImiCamera] SmBlock not initialized, frame dropped")
            return None

        # Zero-copy: SmBlock 버퍼에 직접 쓰기
        index = self.smblock.alloc()
        if index == -1:
            self.rx_drop += 1
            return None

        try:
            # 카메라 버퍼 → numpy view (복사 없음)
            buffer_type = ctypes.c_uint8 * pImage.uiSize
            buffer_ptr = ctypes.cast(pImage.pData, ctypes.POINTER(buffer_type))
            rx_frame = np.frombuffer(buffer_ptr.contents, dtype=np.uint8)

            # SmBlock 버퍼 참조 획득
            dst_buffer = self.smblock.get_buffer(index)
            sm_channels = self.smblock.shape[2] if len(self.smblock.shape) > 2 else 1

            if sm_channels == 1:
                # Grayscale 출력
                if pImage.uiBitDepth != 8:
                    dst_buffer[:] = rx_frame.reshape(pImage.uiHeight, pImage.uiWidth)
                else:
                    cv2.cvtColor(rx_frame.reshape(pImage.uiHeight, pImage.uiWidth, -1),
                                 cv2.COLOR_BGR2GRAY, dst=dst_buffer)
            else:
                # BGR 출력 - Bayer 디코딩을 SmBlock에 직접 수행
                if pImage.uiBitDepth != 8:
                    cv2.cvtColor(rx_frame.reshape(pImage.uiHeight, pImage.uiWidth),
                                 cv2.COLOR_GRAY2BGR, dst=dst_buffer)
                else:
                    cv2.cvtColor(rx_frame.reshape(pImage.uiHeight, pImage.uiWidth, -1),
                                 cv2.COLOR_BayerGB2BGR, dst=dst_buffer)

            # Create FrameInfo and call callback
            frame_info = FrameInfo(
                sm_index=index,
                rx_sequence=self.rx_count,
                rx_timestamp=time.time(),
                rx_count=self.rx_count,
                rx_drop=self.rx_drop
            )

            if hasattr(self, 'nextTask') and self.nextTask is not None:
                self.nextTask.on_image(frame_info)
            else:
                print(" no next task defined ")
                self.smblock.mfree(index)  # nextTask 없으면 블록 해제
        except Exception as e:
            self.smblock.mfree(index)  # 예외 발생 시 블록 해제
            log.exception(f"[ImiCamera] Frame processing error: {e}")

    def RecvFrameDropCallBack(self, pContext: Optional[ctypes.c_void_p] = None) -> None:
        self.rx_drop += 1

    # ═══════════════════════════════════════════════════════════════════════════
    # Hardware I/O (CamProperty descriptors용)
    # ═══════════════════════════════════════════════════════════════════════════
    
    def one_shot(self):
        """소프트웨어 트리거 1회 발생"""
        if not self.is_opened :
            log.warning("[ImiCamera] oneShot_trigger called but not connected")
            return
        
        if self.trigger_source != "software":
            log.warning("[ImiCamera] oneShot_trigger called but trigger_source is not 'software'")
            return
        
        err = ntcRunSWTrigger(self.handle)
        if err != ENeptuneError.NEPTUNE_ERR_Success.value:
            log.error(f"[ImiCamera] ntcRunSWTrigger failed (err={err})")
            
        # if ntcOneShot(self.handle) != ENeptuneError.NEPTUNE_ERR_Success.value:
        #     log.error("[ImiCamera] ntcOneShot failed")
            
    def _hw_get_fps(self) -> float:
        emFPS = ctypes.c_int32()
        dbFPS = ctypes.c_double()
        err = ntcGetFrameRate(self.handle, ctypes.byref(emFPS), ctypes.byref(dbFPS))
        if err == 0:
            log.debug(f"[ImiCamera] Current FPS: {dbFPS.value}")
            return dbFPS.value
        log.error(f"[ImiCamera] ntcGetFrameRate failed: err={err}")
        return None  # 에러 시 None 반환 → 캐시값 유지

    def _hw_set_fps(self, value: float):
        ntcSetAcquisition(self.handle, ENeptuneBoolean.NEPTUNE_BOOL_FALSE.value)
        emFPS = ctypes.c_int32(ENeptuneFrameRate.FPS_VALUE.value)
        dbFPS = ctypes.c_double(value)
        err = ntcSetFrameRate(self.handle, emFPS.value, dbFPS.value)
        if err != 0:
            log.error(f"[ImiCamera] ntcSetFrameRate failed: fps={value}, err={err}")
        ntcSetAcquisition(self.handle, ENeptuneBoolean.NEPTUNE_BOOL_TRUE.value)

    def _hw_get_exposure(self) -> int:
        pui = ctypes.c_uint32()
        err = ntcGetExposureTime(self.handle, ctypes.byref(pui))
        if err != 0:
            log.error(f"[ImiCamera] ntcGetExposureTime failed: err={err}")
            return None  # 에러 시 None 반환 → 캐시값 유지
        return pui.value

    def _hw_set_exposure(self, value: int):
        pui = ctypes.c_uint32(value)
        if ntcSetExposureTime(self.handle, pui) != 0:
            log.error("[ImiCamera] ntcSetExposureTime failed")

    def _hw_get_trigger(self) -> bool:
        neptune_trigger = NEPTUNE_TRIGGER()
        err = ntcGetTrigger(self.handle, ctypes.pointer(neptune_trigger))
        if err != 0:
            log.error(f"[ImiCamera] ntcGetTrigger failed: err={err}")
            return None  # 에러 시 None 반환 → 캐시값 유지
        return neptune_trigger.OnOff == ENeptuneBoolean.NEPTUNE_BOOL_TRUE.value
        

    def _hw_set_trigger(self, enabled: bool):
        st_trigger = NEPTUNE_TRIGGER()
        if self.trigger_source == "hardware":
            st_trigger.Source = ENeptuneTriggerSource.NEPTUNE_TRIGGER_SOURCE_LINE1.value
        else:
            st_trigger.Source = ENeptuneTriggerSource.NEPTUNE_TRIGGER_SOURCE_SW.value
            print( "********************** SOFTWARE TRIGGER MODE SET **********************" )
        st_trigger.Mode = ENeptuneTriggerMode.NEPTUNE_TRIGGER_MODE_0.value
        st_trigger.Polarity = ENeptunePolarity.NEPTUNE_POLARITY_FALLINGEDGE.value
        st_trigger.OnOff = ENeptuneBoolean.NEPTUNE_BOOL_TRUE.value if enabled else ENeptuneBoolean.NEPTUNE_BOOL_FALSE.value

        # trigger on: 고속 FPS 설정, trigger off: 캐시된 사용자 FPS 유지
        if enabled:
            self._hw_set_fps(300.0)
        else:
            self._hw_set_fps(self._fps)

        if ntcSetTrigger(self.handle, st_trigger) != ENeptuneError.NEPTUNE_ERR_Success.value:
            log.error("[ImiCamera] ntcSetTrigger failed")

    # ═══════════════════════════════════════════════════════════════════════════
    # Internal Helpers
    # ═══════════════════════════════════════════════════════════════════════════
    def clear_counters(self):
        self.rx_count = 0
        self.rx_drop = 0

    def _lazy_apply(self):
        """pending된 설정만 적용"""
        for attr, value in self._lazy_updates.items():
            log.info(f"[ImiCamera] Lazy applying {attr}={value}")
            setattr(self, attr, value)
        self._lazy_updates.clear()

    def _lazy_reset(self):
        """하드웨어 리셋 후 모든 캐시된 값을 재설정 (최적화: 한번만 stop/start)"""
        if not self.is_opened:
            log.warning("[ImiCamera] _lazy_reset called but not opened")
            return

        # 일시스톱
        ntcSetAcquisition(self.handle, ENeptuneBoolean.NEPTUNE_BOOL_FALSE.value)

        # FPS 설정
        emFPS = ctypes.c_int32(ENeptuneFrameRate.FPS_VALUE.value)
        dbFPS = ctypes.c_double(self._fps)
        err = ntcSetFrameRate(self.handle, emFPS.value, dbFPS.value)
        if err != 0:
            log.error(f"[ImiCamera] ntcSetFrameRate failed: fps={self._fps}, err={err}")

        # Exposure 설정
        pui = ctypes.c_uint32(self._exposure)
        if ntcSetExposureTime(self.handle, pui) != 0:
            log.error("[ImiCamera] ntcSetExposureTime failed")

        # Trigger 설정
        st_trigger = NEPTUNE_TRIGGER()
        if self.trigger_source == "hardware":
            st_trigger.Source = ENeptuneTriggerSource.NEPTUNE_TRIGGER_SOURCE_LINE1.value
        else:
            st_trigger.Source = ENeptuneTriggerSource.NEPTUNE_TRIGGER_SOURCE_SW.value
        st_trigger.Mode = ENeptuneTriggerMode.NEPTUNE_TRIGGER_MODE_0.value
        st_trigger.Polarity = ENeptunePolarity.NEPTUNE_POLARITY_FALLINGEDGE.value
        st_trigger.OnOff = ENeptuneBoolean.NEPTUNE_BOOL_TRUE.value if self._trigger_mode else ENeptuneBoolean.NEPTUNE_BOOL_FALSE.value
        if ntcSetTrigger(self.handle, st_trigger) != ENeptuneError.NEPTUNE_ERR_Success.value:
            log.error("[ImiCamera] ntcSetTrigger failed")

        # 한번만 start
        ntcSetAcquisition(self.handle, ENeptuneBoolean.NEPTUNE_BOOL_TRUE.value)
        log.info(f"[ImiCamera] Settings applied: fps={self._fps}, exposure={self._exposure}, trigger={self._trigger_mode}")

    # ═══════════════════════════════════════════════════════════════════════════
    # Static Utilities
    # ═══════════════════════════════════════════════════════════════════════════
    @staticmethod
    def get_mac_list() -> list[str]:
        """Return list of connected camera MAC addresses"""
        numberOfCamera = ctypes.c_uint32(0)
        err = ntcGetCameraCount(ctypes.pointer(numberOfCamera))
        if err != ENeptuneError.NEPTUNE_ERR_Success.value or numberOfCamera.value == 0:
            log.error(f"[get_mac_list] Camera not found / Check admin rights, cable err={err} count={numberOfCamera.value}")
            return []

        cam_count = numberOfCamera.value
        info = (NEPTUNE_CAM_INFO * cam_count)()
        err = ntcGetCameraInfo(info, cam_count)

        if err != ENeptuneError.NEPTUNE_ERR_Success.value:
            log.error(f"[get_mac_list] ntcGetCameraInfo failed: err={err}")
            return []

        mac_list = [info[i].strMAC.decode('utf-8') for i in range(cam_count)]
        log.info(f"[get_mac_list] Found {cam_count} camera(s): {mac_list}")
        return mac_list


def main() -> int:
    """Test main function"""
    logging.basicConfig(
        level=logging.INFO,
        format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
    )

    camera = imi_cam_driver()
    try:
        if not camera.open():
            log.error("Camera connection failed")
            return 1
        # camera.set_trigger_mode(True)
        time.sleep(20)
        camera.close()
        log.info(f"Total frames received: {camera.rx_count}")
        return 0

    except KeyboardInterrupt:
        log.info("User interrupted (Ctrl+C)")
        return 0
    except Exception as e:
        log.error(f"Exception occurred: {e}")
        import traceback
        traceback.print_exc()
        return 1
    finally:
        camera.close()
        log.info("Program terminated")


if __name__ == "__main__":
    exit(main())
