# Copyright (c) 2026 동일비전(Dongil Vision Korea). All Rights Reserved.
import threading
import time
from typing import Any, Callable, Optional, Type, Union

class DeviceProperty:
    """Python Descriptor for Device Property Management.
    
    Handles validation, type conversion, caching, debouncing, 
    HW writing (setter), HW reading (getter), and Signal notification.
    """

    def __init__(
        self,
        dtype: Optional[Type] = None,
        default: Any = None,
        validator: Optional[Union[Callable, str]] = None,
        setter: Optional[Union[Callable, str]] = None,
        getter: Optional[Union[Callable, str]] = None,
        opstate: Optional[Callable] = None,
        order: int = 0,
        readonly: bool = False,
        debounce: float = 0,
        per_sync: bool = False,
        notify_mode: str = "immediate"  # "immediate" or "on_write"
    ):
        self.name = ""  # Set by __set_name__
        self.dtype = dtype
        self.default = default
        self.validator_ref = validator
        self.setter_ref = setter
        self.getter_ref = getter
        self.opstate = opstate
        self.order = order
        self.readonly = readonly
        self.debounce = debounce
        self.per_sync = per_sync
        self.notify_mode = notify_mode

    def __set_name__(self, owner, name):
        self.name = name

    def _get_callback(self, instance, ref):
        if ref is None:
            return None
        if callable(ref):
            return ref
        if isinstance(ref, str):
            method = getattr(instance, ref, None)
            if method is None:
                raise AttributeError(f"[{instance.__class__.__name__}] Callback '{ref}' not found for property '{self.name}'")
            return method
        return None

    def __get__(self, instance, owner):
        if instance is None:
            return self
        
        # 1. Getter exists? HW Read
        getter = self._get_callback(instance, self.getter_ref)
        if getter:
            try:
                # Wrap in resync session if requested (read_all does this in batch)
                # Here we just call it directly for simple attribute access
                val = getter()
                instance._cache[self.name] = val
                return val
            except Exception as e:
                # Log error and return cached value
                print(f"[DeviceProperty] Error reading {self.name}: {e}")
        
        return instance._cache.get(self.name, self.default)

    def __set__(self, instance, value):
        if self.readonly:
            raise AttributeError(f"Property '{self.name}' is read-only")

        # 1. Type conversion
        if self.dtype is not None:
            try:
                value = self.dtype(value)
            except (ValueError, TypeError) as e:
                raise TypeError(f"Invalid type for '{self.name}': expected {self.dtype.__name__}, got {type(value).__name__} ({e})")

        # 2. Validation
        validator = self._get_callback(instance, self.validator_ref)
        if validator:
            value = validator(value)

        # 3. Change Detection
        old_value = instance._cache.get(self.name, self.default)
        if old_value == value and self.name in instance._cache:
            return

        # 4. Update Cache
        instance._cache[self.name] = value

        # 5. Signal Notification (Immediate)
        if self.notify_mode == "immediate":
            self._emit_signal(instance, old_value, value)

        # 6. HW Writing (Setter)
        if self.debounce > 0:
            self._set_delayed(instance, value)
        else:
            self._apply_hw(instance, value)

        # 7. Resync Trigger
        # This is called regardless of whether this property was applied to HW,
        # because this change might satisfy opstate for other properties.
        if hasattr(instance, "_resync_dependents"):
            instance._resync_dependents()

    def _emit_signal(self, instance, old, new):
        if hasattr(instance, "signal") and hasattr(instance, "name"):
            # Format: self.signal.{task_name}.emit({"name": "prop", "old": x, "new": y})
            task_signal = getattr(instance.signal, instance.name, None)
            if task_signal:
                task_signal.emit({"name": self.name, "old": old, "new": new})

    def _set_delayed(self, instance, value):
        # Cancel existing timer
        if self.name in instance._delay_timers:
            instance._delay_timers[self.name].cancel()
        
        # Start new timer
        timer = threading.Timer(self.debounce, instance._flush_delayed, args=[self.name])
        instance._delay_timers[self.name] = timer
        timer.start()

    def _apply_hw(self, instance, value):
        setter = self._get_callback(instance, self.setter_ref)
        if not setter:
            return

        # Check opstate
        if self.opstate and not self.opstate(instance):
            return

        # Apply HW with session wrapping
        open_fn = getattr(instance, "_resync_open", None)
        close_fn = getattr(instance, "_resync_close", None)

        try:
            if self.per_sync and open_fn:
                open_fn()
            
            try:
                setter(value)
                # Success: emit signal if on_write
                if self.notify_mode == "on_write":
                    # We don't have the exact old_value here easily if debounced, 
                    # but for on_write, the fact it changed is enough.
                    self._emit_signal(instance, None, value)
            finally:
                if self.per_sync and close_fn:
                    close_fn()
        except Exception as e:
            # Rollback cache on error? 
            # Designing decision: Design doc says rollback cache on single set error
            # but for resync we might just log. Here is __set__.
            # instance._cache[self.name] = old_value # We'd need to pass old_value here
            print(f"[DeviceProperty] HW Write Error for {self.name}: {e}")
            raise
