# Copyright (c) 2026 동일비전(Dongil Vision Korea). All Rights Reserved.
"""
╔══════════════════════════════════════════════════════════════════════════════╗
║  ALASKA v2.3 - SmRingBuffer                                                 ║
║  Lock-free Ring Buffer for Fast IPC                                          ║
╠══════════════════════════════════════════════════════════════════════════════╣
║  Version : 2.3.2                                                             ║
║  Date    : 2026-02-28                                                        ║
╠══════════════════════════════════════════════════════════════════════════════╣
║  Summary:                                                                    ║
║    Single Producer Single Consumer (SPSC) 패턴 lock-free 링 버퍼            ║
║    Layout: [write_idx: int64] [read_idx: int64] [data: uint8[N]]            ║
║  Changes (v2.3.1):                                                           ║
║    - stored_size 제거 → write_idx/read_idx만으로 empty/full 판별             ║
║    - numpy 블록 복사로 성능 개선 (바이트 루프 제거)                            ║
║    - read 시 length 범위 검증 추가                                            ║
║    - double-close 보호                                                       ║
║  Changes (v2.3.2):                                                           ║
║    - write/read에서 _used() 인라인 → 공유메모리 이중읽기 제거                ║
║    - read non-wrap 경로 2x복사→1x복사 (.copy() 제거)                         ║
║    - length prefix np.uint32 view 디코딩으로 int.from_bytes 제거             ║
║    - length 검증 off-by-one 버그 수정 (sz-4 → sz-5)                         ║
╚══════════════════════════════════════════════════════════════════════════════╝
"""

from multiprocessing.shared_memory import SharedMemory
from typing import Optional
import os
import warnings
import numpy as np

# SPSC 위반 런타임 감지 (ALASKA_SPSC_DEBUG=1 으로 활성화)
_SPSC_DEBUG = os.environ.get('ALASKA_SPSC_DEBUG', '') == '1'
_WARNED_SPSC = set()


class SmRingBuffer:
    """고속 공유 메모리 링 버퍼 (Lock-free SPSC)

    Single Producer Single Consumer (SPSC) 전용 — 1개 프로세스만 write,
    1개 프로세스만 read 호출 가능합니다.

    주의: 다중 writer 시 53% 데이터 손상 발생 (RC-04 시험 결과).
    다중 producer 필요 시 SmQueue(lock=mp.Lock()) 사용하세요.
    ALASKA_SPSC_DEBUG=1 환경변수로 런타임 위반 감지 가능.

    Layout: [write_idx: int64] [read_idx: int64] [data: uint8[N]]
    - 1 슬롯 예약으로 full/empty 구분 (write_idx == read_idx → empty)
    - stored_size 카운터 없음 → 경쟁 조건 근본 해결

    Usage:
        # Producer (1개 프로세스만)
        buf = SmRingBuffer("my_buffer", size=1024, create=True)
        buf.write(b"hello")

        # Consumer (1개 프로세스만)
        buf = SmRingBuffer("my_buffer", size=1024, create=False)
        data = buf.read()  # b"hello"
    """
    __slots__ = ('_name', '_shm', '_size', '_is_new', '_write_idx', '_read_idx',
                 '_data', '_closed', '_writer_pid', '_reader_pid')

    _HEADER_SIZE = 16  # 2 * int64 (stored_size 제거)

    def __init__(self, name: str, size: int = 4096, create: bool = False):
        """SmRingBuffer 초기화.

        Args:
            name: 공유 메모리 이름
            size: 버퍼 크기 (bytes)
            create: True=생성, False=연결
        """
        self._name = name
        self._size = size
        self._is_new = create
        self._closed = False
        self._writer_pid = 0
        self._reader_pid = 0

        total_size = self._HEADER_SIZE + size

        if create:
            self._shm = SharedMemory(name=name, create=True, size=total_size)
            self._setup_buffers()
            self._write_idx[0] = 0
            self._read_idx[0] = 0
        else:
            self._shm = SharedMemory(name=name, create=False)
            self._setup_buffers()

    def _setup_buffers(self):
        buf = self._shm.buf
        self._write_idx = np.ndarray((1,), dtype=np.int64, buffer=buf[0:8])
        self._read_idx = np.ndarray((1,), dtype=np.int64, buffer=buf[8:16])
        self._data = np.ndarray((self._size,), dtype=np.uint8,
                                buffer=buf[self._HEADER_SIZE:self._HEADER_SIZE + self._size])

    def _used(self) -> int:
        """사용 중인 바이트 수 (write_idx - read_idx 기반)"""
        w = int(self._write_idx[0])
        r = int(self._read_idx[0])
        return (w - r) % self._size

    def write(self, data: bytes) -> bool:
        """데이터 쓰기 (SPSC에서 lock-free)

        Returns:
            True=성공, False=버퍼 부족
        """
        if _SPSC_DEBUG:
            pid = os.getpid()
            if self._writer_pid == 0:
                self._writer_pid = pid
            elif self._writer_pid != pid and self._name not in _WARNED_SPSC:
                _WARNED_SPSC.add(self._name)
                warnings.warn(
                    f"SmRingBuffer('{self._name}') SPSC violation: "
                    f"writer PID {self._writer_pid} -> {pid} (RC-04)",
                    stacklevel=2
                )
        length = len(data)
        total = length + 4  # 4 bytes length prefix
        sz = self._size
        capacity = sz - 1  # 1 슬롯 예약 (full/empty 구분)
        if total > capacity:
            return False

        # P-1: _used() 인라인 — write_idx 1회만 읽기
        write_pos = int(self._write_idx[0])
        used = (write_pos - int(self._read_idx[0])) % sz
        if total > capacity - used:
            return False  # 버퍼 부족

        # P-3: length prefix를 numpy uint32 view로 직접 기록
        len_arr = np.array([length], dtype=np.uint32).view(np.uint8)
        end = write_pos + 4
        if end <= sz:
            self._data[write_pos:end] = len_arr
        else:
            split = sz - write_pos
            self._data[write_pos:sz] = len_arr[:split]
            self._data[0:4 - split] = len_arr[split:]

        # Data — numpy 블록 복사
        data_arr = np.frombuffer(data, dtype=np.uint8)
        start = (write_pos + 4) % sz
        end = start + length
        if end <= sz:
            self._data[start:end] = data_arr
        else:
            split = sz - start
            self._data[start:sz] = data_arr[:split]
            self._data[0:length - split] = data_arr[split:]

        # Write index 업데이트 (data 쓰기 완료 후)
        self._write_idx[0] = (write_pos + total) % sz
        return True

    def read(self) -> Optional[bytes]:
        """데이터 읽기 (SPSC에서 lock-free)

        Returns:
            데이터 또는 None (버퍼 비어있음)
        """
        if _SPSC_DEBUG:
            pid = os.getpid()
            if self._reader_pid == 0:
                self._reader_pid = pid
            elif self._reader_pid != pid and self._name not in _WARNED_SPSC:
                _WARNED_SPSC.add(self._name)
                warnings.warn(
                    f"SmRingBuffer('{self._name}') SPSC violation: "
                    f"reader PID {self._reader_pid} -> {pid}",
                    stacklevel=2
                )
        # P-1: _used() 인라인 — read_idx 1회만 읽기
        read_pos = int(self._read_idx[0])
        sz = self._size
        used = (int(self._write_idx[0]) - read_pos) % sz
        if used == 0:
            return None

        # P-2b: Length prefix를 np.uint32 view로 직접 디코딩
        end = read_pos + 4
        if end <= sz:
            length = int(self._data[read_pos:end].view(np.uint32)[0])
        else:
            split = sz - read_pos
            len_arr = np.empty(4, dtype=np.uint8)
            len_arr[:split] = self._data[read_pos:sz]
            len_arr[split:] = self._data[0:4 - split]
            length = int(len_arr.view(np.uint32)[0])

        # BUG-1 수정: length 범위 검증 (sz-4 → sz-5, 1슬롯 예약 고려)
        if length < 0 or length > sz - 5:
            self._read_idx[0] = self._write_idx[0]  # 복구: 버퍼 비움
            return None

        # P-2: Data 읽기 — non-wrap 경로에서 .copy() 제거 (tobytes()가 복사함)
        start = (read_pos + 4) % sz
        end = start + length
        if end <= sz:
            result = self._data[start:end].tobytes()
        else:
            split = sz - start
            data_arr = np.empty(length, dtype=np.uint8)
            data_arr[:split] = self._data[start:sz]
            data_arr[split:] = self._data[0:length - split]
            result = data_arr.tobytes()

        # Read index 업데이트
        self._read_idx[0] = (read_pos + 4 + length) % sz
        return result

    def available(self) -> int:
        """읽을 수 있는 대략적 바이트 수 (메시지 수 아님)"""
        return self._used()

    def close(self):
        if self._closed:
            return
        self._closed = True
        self._shm.close()
        if self._is_new:
            try:
                self._shm.unlink()
            except FileNotFoundError:
                pass
            except PermissionError:
                import warnings; warnings.warn(f"SHM unlink failed (in use): {self._shm.name}", ResourceWarning, stacklevel=2)

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

    @property
    def size(self) -> int:
        return self._size

    def reset(self):
        """읽기/쓰기 인덱스 초기화 (재시작 시 사용)."""
        self._write_idx[0] = 0
        self._read_idx[0] = 0

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

    def __setstate__(self, state):
        """Pickle 역직렬화 (자식 프로세스에서 SharedMemory 재연결)."""
        self._name = state['name']
        self._size = state['size']
        self._is_new = False
        self._closed = False
        self._writer_pid = 0
        self._reader_pid = 0
        self._shm = SharedMemory(name=self._name, create=False)
        self._setup_buffers()

    def __repr__(self):
        return f"SmRingBuffer({self._name}, size={self._size}, used={self._used()})"
