# Copyright (c) 2026 동일비전(Dongil Vision Korea). All Rights Reserved.
"""
╔══════════════════════════════════════════════════════════════════════════════╗
║  ALASKA v2.3 - SmQueue                                                       ║
║  SharedMemory IPC Queue (Drop-in replacement for multiprocessing.Queue)      ║
╠══════════════════════════════════════════════════════════════════════════════╣
║  Version : 1.1.0                                                             ║
║  Date    : 2026-02-28                                                        ║
╠══════════════════════════════════════════════════════════════════════════════╣
║  Summary:                                                                    ║
║    multiprocessing.Queue의 Pipe 전송을 SmRingBuffer(SharedMemory)로 대체     ║
║    Semaphore 시그널링으로 CPU-efficient 블로킹 유지                          ║
║    크로스프로세스 왕복 Queue 대비 ~11x 빠름                                  ║
║  Layout:                                                                     ║
║    Transport: SmRingBuffer (SharedMemory memcpy)                             ║
║    Signaling: multiprocessing.Semaphore (OS-level, CPU 0%)                   ║
║    Serialization: pickle (protocol 5)                                        ║
║  Changes (v1.0.1):                                                           ║
║    - FileExistsError: attach→reset 방식 (Windows 호환)                       ║
║    - empty(), qsize(), get_nowait() 호환 메서드 추가                         ║
║  Changes (v1.1.0):                                                           ║
║    - Hybrid Polling: user-space spin → Semaphore fallback                    ║
║    - spin 파라미터: 0=Semaphore only, N=spin N회, -1=pure spin (no OS)       ║
╚══════════════════════════════════════════════════════════════════════════════╝
"""

import pickle
import time
import queue as _queue
from contextlib import nullcontext

from .sm_ring_buffer import SmRingBuffer

# Spin-wait 기본값: 64회 반복 ≈ 30~60μs (Python loop ~0.5~1μs/iter)
_DEFAULT_SPIN = 64

# 무한 대기 방지: timeout=None 시 적용되는 최대 대기 시간 (초)
_DEFAULT_MAX_WAIT = 5.0

# put() write 재시도 상한 (500회 × sleep(0) ≈ 50ms)
_MAX_WRITE_RETRY = 500

# M-06: 적응형 스핀 파라미터
_ADAPT_PERIOD = 100   # 조정 주기 (get 호출 N회마다)
_SPIN_MAX = 256       # 스핀 상한


class SmQueue:
    """SharedMemory 기반 IPC Queue — multiprocessing.Queue 드롭인 대체

    SmRingBuffer로 데이터 전송, Semaphore로 시그널링.
    Queue 대비 크로스프로세스 왕복 ~11x 빠름.

    Hybrid Polling:
        spin > 0: user-space에서 spin회 폴링 후 Semaphore fallback
        spin = 0: Semaphore only (CPU 절약, 지연 ~0.16ms)
        spin = -1: pure spin (Semaphore 제거, CPU 1코어 점유, 지연 <0.01ms)

    Usage:
        # 일반 (Semaphore + spin)
        q = SmQueue("my_q", create=True, lock=mp.Lock(), sem=mp.Semaphore(0))

        # 고속 (pure spin, 실시간 태스크용)
        q = SmQueue("my_q", create=True, lock=mp.Lock(), sem=mp.Semaphore(0), spin=-1)
    """
    __slots__ = ('_name', '_size', '_lock', '_sem', '_rb', '_spin',
                 '_spin_hits', '_spin_misses')

    def __init__(self, name: str, size: int = 65536, create: bool = False,
                 lock=None, sem=None, spin: int = _DEFAULT_SPIN):
        """SmQueue 초기화.

        Args:
            name: 공유 메모리 이름 (시스템 전역 고유)
            size: 링 버퍼 크기 (bytes, 기본 64KB)
            create: True=새 공유 메모리 생성, False=기존에 연결
            lock: multiprocessing.Lock (다중 생산자용)
            sem: multiprocessing.Semaphore (블로킹 시그널링용)
            spin: Hybrid polling 반복 횟수 (0=off, -1=pure spin)
        """
        self._name = name
        self._size = size
        self._lock = lock
        self._sem = sem
        self._spin = spin
        self._spin_hits = 0    # M-06: 적응형 스핀 카운터
        self._spin_misses = 0
        if create:
            try:
                self._rb = SmRingBuffer(name, size=size, create=True)
            except FileExistsError:
                # 이전 실행의 잔여 SharedMemory → 기존에 연결 후 리셋
                self._rb = SmRingBuffer(name, size=size, create=False)
                self._rb._is_new = True  # creator 역할 인계 (close 시 unlink)
                self._rb.reset()
        else:
            self._rb = SmRingBuffer(name, size=size, create=False)

    def put(self, obj, timeout=None):
        """객체를 큐에 넣기 (pickle 직렬화 + SharedMemory 전송).

        Args:
            obj: 직렬화 가능 객체
            timeout: 최대 대기 시간(초). None=_MAX_WRITE_RETRY 횟수 제한.

        Raises:
            queue.Full: 버퍼 부족으로 쓰기 실패
        """
        data = pickle.dumps(obj, protocol=pickle.HIGHEST_PROTOCOL)
        _write = self._rb.write
        ctx = self._lock if self._lock else nullcontext()
        with ctx:
            if timeout is not None:
                deadline = time.perf_counter() + timeout
                while not _write(data):
                    if time.perf_counter() >= deadline:
                        raise _queue.Full()
                    time.sleep(0)
            else:
                for _ in range(_MAX_WRITE_RETRY):
                    if _write(data):
                        break
                    time.sleep(0)
                else:
                    raise _queue.Full()
        self._sem.release()  # 소비자에게 시그널

    def put_nowait(self, obj):
        """비블로킹 put — 버퍼 부족 시 queue.Full 발생."""
        data = pickle.dumps(obj, protocol=pickle.HIGHEST_PROTOCOL)
        ctx = self._lock if self._lock else nullcontext()
        with ctx:
            if not self._rb.write(data):
                raise _queue.Full()
        self._sem.release()

    def get(self, timeout=None):
        """Hybrid Polling: user-space spin → Semaphore fallback.

        Phase 1 (spin > 0): SmRingBuffer를 직접 폴링 (커널 전환 없음)
        Phase 2: Semaphore.acquire (OS-level 블로킹)

        Args:
            timeout: 대기 시간 (초). None=_DEFAULT_MAX_WAIT 적용.

        Raises:
            queue.Empty: 타임아웃 초과
        """
        rb = self._rb
        sem = self._sem
        spin = self._spin
        # 무한 대기 방지: timeout=None → 기본 최대 대기 시간 적용
        effective_timeout = timeout if timeout is not None else _DEFAULT_MAX_WAIT

        # ── Phase 1: User-space spin-wait (커널 전환 0회) ──
        if spin != 0:
            _used = rb._used
            if spin > 0:
                # Hybrid: spin N회 후 Semaphore fallback
                for _ in range(spin):
                    if _used() > 0:
                        if not sem.acquire(block=False):
                            continue
                        data = rb.read()
                        if data is not None:
                            self._spin_hits += 1
                            self._adapt_spin()  # M-06
                            return pickle.loads(data)
                        sem.release()
                        raise _queue.Empty()
                # spin 소진 → Phase 2로 fallthrough
                self._spin_misses += 1
                self._adapt_spin()  # M-06
            else:
                # Pure spin (spin=-1): Semaphore 완전 제거
                deadline = time.perf_counter() + effective_timeout
                while time.perf_counter() < deadline:
                    if _used() > 0:
                        if not sem.acquire(block=False):
                            continue
                        data = rb.read()
                        if data is not None:
                            return pickle.loads(data)
                        sem.release()
                        raise _queue.Empty()
                raise _queue.Empty()

        # ── Phase 2: OS-level Semaphore blocking ──
        if not sem.acquire(timeout=effective_timeout):
            raise _queue.Empty()
        data = rb.read()
        if data is not None:
            return pickle.loads(data)
        sem.release()
        raise _queue.Empty()

    def _adapt_spin(self):
        """M-06: 적응형 스핀 — hit_rate 기반 스핀 카운트 조정."""
        total = self._spin_hits + self._spin_misses
        if total < _ADAPT_PERIOD:
            return
        hit_rate = self._spin_hits / total
        cur = self._spin
        if cur <= 0:
            return  # pure spin(-1) 또는 sem-only(0) → 적응 불필요
        if hit_rate > 0.5:
            self._spin = min(cur * 2, _SPIN_MAX)   # burst → 스핀 증가
        elif hit_rate < 0.1:
            self._spin = max(cur // 2, 0)           # idle → 스핀 감소
        self._spin_hits = 0
        self._spin_misses = 0

    def get_nowait(self):
        """비블로킹 get — 데이터 없으면 queue.Empty 발생."""
        return self.get(timeout=0)

    def empty(self):
        """큐가 비어있는지 확인 (근사값)."""
        return self._rb._used() == 0

    def qsize(self):
        """큐 내 대략적 바이트 수 (메시지 개수 아님)."""
        return self._rb._used()

    def close(self):
        """공유 메모리 연결 해제 + unlink."""
        self._rb.close()

    def __getstate__(self):
        """Pickle 직렬화 (프로세스 간 전달용)."""
        return {
            'name': self._name,
            'size': self._size,
            'lock': self._lock,
            'sem': self._sem,
            'spin': self._spin,
        }

    def __setstate__(self, state):
        """Pickle 역직렬화 (자식 프로세스에서 SharedMemory 재연결)."""
        self._name = state['name']
        self._size = state['size']
        self._lock = state['lock']
        self._sem = state['sem']
        self._spin = state.get('spin', _DEFAULT_SPIN)
        self._spin_hits = 0   # M-06: 적응형 카운터 초기화
        self._spin_misses = 0
        self._rb = SmRingBuffer(self._name, self._size, create=False)

    @property
    def name(self) -> str:
        return self._name

    def __repr__(self):
        return f"SmQueue({self._name}, size={self._size}, spin={self._spin})"
