# =============================================================================
# Kynsight — Circular Buffer Kit (Improved Readability)
#
# Author: Kerem Yollu (improved with Claude)
# License: MIT
# Company: Kynsight
# Created: 2025-10-24
#
# WHAT IS A CIRCULAR BUFFER?
# ===========================
# Think of it like a tape recorder with limited tape that loops back:
#
#   Write Position (wraps around)
#          ↓
#   [D][E][F][A][B][C]  ← Buffer (capacity = 6 bytes)
#    ↑           ↑
#   oldest     newest
#
# When full and you write more → oldest data gets overwritten (drop-oldest)
#
# KEY FEATURE: Absolute Write Offset (w_abs)
# ==========================================
# Unlike typical circular buffers that only track position (0-5 above),
# we ALSO track total bytes ever written:
#
#   w_abs = 0:  []                          (empty)
#   w_abs = 3:  [A][B][C][ ][ ][ ]         (3 bytes written)
#   w_abs = 6:  [A][B][C][D][E][F]         (6 bytes, full)
#   w_abs = 9:  [G][H][I][D][E][F]         (9 bytes total, overwrote A,B,C)
#                ↑ newest    ↑ oldest
#
# WHY? To copy data spans unambiguously:
#   - "Give me bytes from w_abs=5 to w_abs=9" → [F][G][H][I]
#   - Works even if buffer wrapped around multiple times
#
# C EQUIVALENT:
# =============
# In C, you'd use:
#   - uint8_t* buffer (malloc'd array)
#   - size_t write_pos = w_abs % capacity (for indexing)
#   - pthread_mutex_t lock (for thread safety)
#
# In Python:
#   - bytearray (like uint8_t* but managed memory)
#   - % operator works same as C
#   - threading.Lock (like pthread_mutex_t)
#
# =============================================================================

from __future__ import annotations
from dataclasses import dataclass, field
from enum import Enum
from typing import Tuple, Optional
import threading


# =============================================================================
# STATUS CODES (like errno in C)
# =============================================================================

class Status(Enum):
    """
    Return codes for all operations.
    
    C equivalent:
        enum Status { OK=0, BAD_CONFIG=1, NOT_READY=2, OVERFLOW=3 };
    
    Python advantage: Enum names are more readable than raw ints
    """
    OK = 0           # Operation succeeded
    BAD_CONFIG = 1   # Invalid parameter (e.g., size <= 0)
    NOT_READY = 2    # Buffer not initialized
    OVERFLOW = 3     # Data larger than capacity (was truncated)


# =============================================================================
# BUFFER HANDLE (like a struct in C)
# =============================================================================

@dataclass
class CircularBufferHandle:
    """
    Opaque handle for the circular buffer state.
    
    C equivalent:
        typedef struct {
            size_t capacity;
            uint8_t* buffer;
            size_t write_absolute;
            int overflow_count;
            pthread_mutex_t lock;
        } CircularBuffer;
    
    Python @dataclass: Automatically generates __init__, __repr__, etc.
    Like a C struct but with methods and automatic constructor.
    
    Fields explained:
    -----------------
    capacity: 
        Total size in bytes (like malloc(capacity) in C)
        
    buffer: 
        Actual storage (bytearray = Python's managed uint8_t array)
        
    write_absolute: 
        Total bytes written since creation (monotonic, never wraps)
        To get position in buffer: write_absolute % capacity
        
    overflow_count: 
        How many times we wrote more data than capacity at once
        (these writes get truncated to keep newest data)
        
    lock: 
        For thread safety (like pthread_mutex_t in C)
        'with h.lock:' in Python = pthread_mutex_lock/unlock in C
    """
    
    capacity: int
    buffer: bytearray = field(default_factory=bytearray)
    write_absolute: int = 0
    overflow_count: int = 0
    lock: threading.Lock = field(default_factory=threading.Lock)


# =============================================================================
# INITIALIZATION / CLEANUP
# =============================================================================

def cb_init(capacity_bytes: int) -> Tuple[Status, Optional[CircularBufferHandle]]:
    """
    Create and initialize a circular buffer.
    
    C equivalent:
        CircularBuffer* cb_init(size_t capacity) {
            if (capacity <= 0) return NULL;
            CircularBuffer* cb = malloc(sizeof(CircularBuffer));
            cb->buffer = malloc(capacity);
            cb->capacity = capacity;
            cb->write_absolute = 0;
            cb->overflow_count = 0;
            pthread_mutex_init(&cb->lock, NULL);
            return cb;
        }
    
    Args:
        capacity_bytes: Size of buffer in bytes (must be > 0)
    
    Returns:
        (Status.OK, handle)         if successful
        (Status.BAD_CONFIG, None)   if capacity_bytes <= 0
    
    Example:
        status, buffer = cb_init(1024)  # 1KB buffer
        if status == Status.OK:
            # use buffer...
    """
    # Validation
    if capacity_bytes <= 0:
        return (Status.BAD_CONFIG, None)
    
    # Create handle (like malloc + init in C)
    handle = CircularBufferHandle(
        capacity=capacity_bytes,
        buffer=bytearray(capacity_bytes)  # Pre-allocated, zero-filled
    )
    
    return (Status.OK, handle)


def cb_deinit(handle: CircularBufferHandle) -> Status:
    """
    Clean up buffer resources.
    
    C equivalent:
        void cb_deinit(CircularBuffer* cb) {
            free(cb->buffer);
            pthread_mutex_destroy(&cb->lock);
            free(cb);
        }
    
    Python note: Not strictly needed (garbage collector handles cleanup),
    but included for API consistency and explicit resource management.
    
    Args:
        handle: Buffer to clean up
    
    Returns:
        Status.OK always
    """
    # In Python, just returning is enough (GC will clean up)
    # But we could explicitly clear if needed:
    # handle.buffer = bytearray()
    return Status.OK


def cb_reset(handle: CircularBufferHandle) -> Status:
    """
    Reset buffer to empty state (zero bytes written).
    
    Like flushing or clearing the buffer.
    
    C equivalent:
        void cb_reset(CircularBuffer* cb) {
            pthread_mutex_lock(&cb->lock);
            memset(cb->buffer, 0, cb->capacity);
            cb->write_absolute = 0;
            cb->overflow_count = 0;
            pthread_mutex_unlock(&cb->lock);
        }
    
    Effects:
        - Zeros all bytes in buffer
        - Resets write_absolute to 0
        - Resets overflow_count to 0
    
    Thread-safe: Uses internal lock
    
    Args:
        handle: Buffer to reset
    
    Returns:
        Status.OK always
    """
    with handle.lock:  # ← Python's 'with' = pthread_mutex_lock/unlock in C
        # Zero out the buffer
        handle.buffer[:] = b"\x00" * handle.capacity
        
        # Reset counters
        handle.write_absolute = 0
        handle.overflow_count = 0
    
    return Status.OK


# =============================================================================
# WRITE OPERATION
# =============================================================================

def cb_write(handle: CircularBufferHandle, data: bytes) -> Tuple[Status, int]:
    """
    Write bytes to the buffer (drop-oldest on overflow).
    
    BEHAVIOR:
    ---------
    If data fits: Write normally
    If data > capacity: Keep only the NEWEST 'capacity' bytes
    
    Example with capacity=4:
        Initial:  [ ][ ][ ][ ]
        Write ABC:  [A][B][C][ ]  → w_abs=3
        Write DE:   [A][B][C][D]  → w_abs=4 (full)
        Write FG:   [F][G][C][D]  → w_abs=6 (overwrote A,B)
                     ↑ newest  ↑ oldest
    
    C equivalent:
        int cb_write(CircularBuffer* cb, const uint8_t* data, size_t len) {
            pthread_mutex_lock(&cb->lock);
            
            // Handle overflow
            if (len > cb->capacity) {
                data = data + (len - cb->capacity);  // keep last N bytes
                len = cb->capacity;
                cb->overflow_count++;
            }
            
            // Calculate positions
            size_t pos = cb->write_absolute % cb->capacity;
            
            // Write (might wrap around)
            if (pos + len <= cb->capacity) {
                memcpy(&cb->buffer[pos], data, len);
            } else {
                size_t first_part = cb->capacity - pos;
                memcpy(&cb->buffer[pos], data, first_part);
                memcpy(&cb->buffer[0], data + first_part, len - first_part);
            }
            
            cb->write_absolute += len;
            pthread_mutex_unlock(&cb->lock);
            return len;
        }
    
    Args:
        handle: Buffer to write to
        data: Bytes to write
    
    Returns:
        (Status.OK, bytes_written)
        where bytes_written = min(len(data), capacity)
    
    Thread-safe: Yes
    """
    # Edge case: empty write
    if not data:
        return (Status.OK, 0)
    
    with handle.lock:
        bytes_to_write = len(data)
        
        # OVERFLOW HANDLING: If data > capacity, keep only newest bytes
        if bytes_to_write > handle.capacity:
            # Keep last 'capacity' bytes only
            data = data[-handle.capacity:]  # Python slice (negative index from end)
            bytes_to_write = handle.capacity
            handle.overflow_count += 1
        
        # CALCULATE WRITE POSITION
        # Like: size_t pos = write_absolute % capacity in C
        position = handle.write_absolute % handle.capacity
        end_position = position + bytes_to_write
        
        # WRITE DATA (handle wrap-around)
        if end_position <= handle.capacity:
            # Case 1: Data fits without wrapping
            # [___ABC___] → write at position 3
            handle.buffer[position:end_position] = data
        else:
            # Case 2: Data wraps around to start
            # [___ABC] → write "DEFGH" becomes [GH_DEF]
            #                                    ↑wrap here
            
            # Split into two parts
            first_part_size = handle.capacity - position  # bytes until end
            
            # Write first part (to end of buffer)
            handle.buffer[position:] = data[:first_part_size]
            
            # Write second part (wrapped to start)
            handle.buffer[:end_position - handle.capacity] = data[first_part_size:]
        
        # UPDATE ABSOLUTE POSITION (always increases, never wraps)
        handle.write_absolute += bytes_to_write
        
        return (Status.OK, bytes_to_write)


# =============================================================================
# READ OPERATION (SPAN COPY)
# =============================================================================

def cb_copy_span(
    handle: CircularBufferHandle,
    start_absolute: int,
    end_absolute: int
) -> Tuple[Status, bytes]:
    """
    Copy a span of bytes [start_absolute, end_absolute) from the buffer.
    
    KEY CONCEPT: Uses absolute offsets, not ring positions!
    
    Example with capacity=6, w_abs=10:
        Buffer state: [G][H][I][J][D][E][F]
                       ↑ newest (w_abs=10)  ↑ oldest (w_abs=4)
        
        Request: copy_span(5, 8)  → want bytes at w_abs [5,6,7]
        Result: [F][G][H]
    
    If requested span > capacity:
        Returns the newest 'capacity' bytes
        (because older data was already overwritten)
    
    C equivalent:
        int cb_copy_span(CircularBuffer* cb, size_t start, size_t end,
                         uint8_t* out_buffer) {
            pthread_mutex_lock(&cb->lock);
            
            size_t len = end - start;
            if (len > cb->capacity) {
                // Can't retrieve more than capacity
                start = cb->write_absolute - cb->capacity;
                end = cb->write_absolute;
                len = cb->capacity;
            }
            
            for (size_t i = 0; i < len; i++) {
                size_t pos = (start + i) % cb->capacity;
                out_buffer[i] = cb->buffer[pos];
            }
            
            pthread_mutex_unlock(&cb->lock);
            return len;
        }
    
    Args:
        handle: Buffer to read from
        start_absolute: Start offset (absolute, not position in ring)
        end_absolute: End offset (exclusive)
    
    Returns:
        (Status.OK, bytes_data)
    
    Thread-safe: Yes
    """
    # Edge case: empty span
    if end_absolute <= start_absolute:
        return (Status.OK, b"")
    
    span_length = end_absolute - start_absolute
    
    # OVERFLOW PROTECTION: Can't retrieve more than capacity
    if span_length > handle.capacity:
        # Adjust to newest available window
        with handle.lock:
            start_absolute = handle.write_absolute - handle.capacity
            end_absolute = handle.write_absolute
            span_length = handle.capacity
    
    # ALLOCATE OUTPUT BUFFER
    output = bytearray(span_length)
    
    # COPY BYTES ONE BY ONE (handling wrap-around automatically)
    with handle.lock:
        for i in range(span_length):
            # Calculate position in circular buffer
            # Like: size_t pos = (start + i) % capacity in C
            position = (start_absolute + i) % handle.capacity
            output[i] = handle.buffer[position]
    
    return (Status.OK, bytes(output))


# =============================================================================
# QUERY FUNCTIONS (read-only, thread-safe)
# =============================================================================

def cb_overflows(handle: CircularBufferHandle) -> int:
    """
    Get number of overflow events.
    
    An overflow happens when a single write is larger than capacity.
    
    C equivalent:
        int cb_overflows(CircularBuffer* cb) {
            pthread_mutex_lock(&cb->lock);
            int count = cb->overflow_count;
            pthread_mutex_unlock(&cb->lock);
            return count;
        }
    
    Returns:
        Number of times data was truncated
    """
    with handle.lock:
        return handle.overflow_count


def cb_w_abs(handle: CircularBufferHandle) -> int:
    """
    Get total bytes written since creation.
    
    This is the absolute write offset (monotonic counter).
    
    C equivalent:
        size_t cb_w_abs(CircularBuffer* cb) {
            pthread_mutex_lock(&cb->lock);
            size_t w = cb->write_absolute;
            pthread_mutex_unlock(&cb->lock);
            return w;
        }
    
    Returns:
        Total bytes written (never decreases, never wraps)
    """
    with handle.lock:
        return handle.write_absolute


def cb_capacity(handle: CircularBufferHandle) -> int:
    """
    Get buffer capacity in bytes.
    
    C equivalent:
        size_t cb_capacity(CircularBuffer* cb) {
            return cb->capacity;  // No lock needed (const)
        }
    
    Returns:
        Total capacity (fixed at creation)
    """
    # No lock needed - capacity never changes
    return handle.capacity


def cb_fill_bytes(handle: CircularBufferHandle) -> int:
    """
    Get current fill level in bytes.
    
    Definition:
        fill = min(write_absolute, capacity)
    
    Why min? Once we've written 'capacity' bytes, buffer is considered
    full (even though we keep writing, overwriting old data).
    
    Example with capacity=100:
        w_abs=50   → fill=50  (half full)
        w_abs=100  → fill=100 (full)
        w_abs=200  → fill=100 (still "full", old data overwritten)
    
    C equivalent:
        size_t cb_fill_bytes(CircularBuffer* cb) {
            pthread_mutex_lock(&cb->lock);
            size_t fill = (cb->write_absolute < cb->capacity) 
                          ? cb->write_absolute 
                          : cb->capacity;
            pthread_mutex_unlock(&cb->lock);
            return fill;
        }
    
    Returns:
        Bytes currently in buffer (0 to capacity)
    """
    with handle.lock:
        # Ternary operator in Python: value_if_true if condition else value_if_false
        # C equivalent: write_absolute < capacity ? write_absolute : capacity
        return handle.write_absolute if handle.write_absolute < handle.capacity else handle.capacity


def cb_fill_pct(handle: CircularBufferHandle) -> int:
    """
    Get fill level as percentage (0-100).
    
    Useful for progress bars or monitoring.
    
    C equivalent:
        int cb_fill_pct(CircularBuffer* cb) {
            size_t fill = cb_fill_bytes(cb);
            size_t cap = cb->capacity > 0 ? cb->capacity : 1;
            return (fill * 100) / cap;  // Integer division
        }
    
    Returns:
        Integer percentage 0-100 (no decimal, no '%' sign)
    
    Example:
        capacity=1000, fill=750 → returns 75
    """
    fill = cb_fill_bytes(handle)
    cap = handle.capacity if handle.capacity > 0 else 1  # Avoid division by zero
    
    # Integer division (// in Python = / in C for integers)
    return (fill * 100) // cap


# =============================================================================
# HELPER: Visual representation (for debugging)
# =============================================================================

def cb_debug_info(handle: CircularBufferHandle) -> str:
    """
    Get human-readable debug information.
    
    Not in original, but useful for learning/debugging!
    
    Returns:
        Multi-line string with buffer state
    """
    with handle.lock:
        info = f"""
Circular Buffer State:
----------------------
Capacity:      {handle.capacity} bytes
Written total: {handle.write_absolute} bytes
Fill level:    {cb_fill_bytes(handle)} bytes ({cb_fill_pct(handle)}%)
Overflows:     {handle.overflow_count}
Position:      {handle.write_absolute % handle.capacity}
"""
    return info


# =============================================================================
# TEACHING NOTES
# =============================================================================

"""
KEY PYTHON vs C DIFFERENCES:
=============================

1. Memory Management:
   C:      malloc/free, manual cleanup
   Python: Automatic garbage collection

2. Locking:
   C:      pthread_mutex_lock(&lock); ... pthread_mutex_unlock(&lock);
   Python: with lock: ...  (automatic unlock even if exception)

3. Arrays:
   C:      uint8_t* buffer (pointer arithmetic)
   Python: bytearray (looks like list but is byte-optimized)

4. Slicing:
   C:      memcpy(dst, src, len)
   Python: dst[:] = src  (copies entire array)
           dst[2:5] = src[2:5]  (copies slice)

5. Modulo:
   C:      pos = offset % capacity;
   Python: pos = offset % capacity  (same!)

6. Enums:
   C:      enum Status { OK=0, ERROR=1 };
   Python: class Status(Enum): OK=0; ERROR=1

7. Structs:
   C:      typedef struct { int x; } MyStruct;
   Python: @dataclass class MyStruct: x: int

WHEN TO USE THIS BUFFER:
=========================
✅ Streaming data (serial, network)
✅ Multi-threaded producer/consumer
✅ Fixed memory budget
✅ Don't care about old data
✅ Need thread-safe operations

❌ Need to keep ALL data (use list/deque instead)
❌ Single-threaded (simpler structures work)
❌ Need to read from middle frequently (use deque)
"""
