"""
Multi-process page buffering system for high-performance ereader navigation.

This module provides intelligent page caching with background rendering using
multiprocessing to achieve sub-second page navigation performance.
"""

from __future__ import annotations
from typing import Dict, Optional, List, Tuple, Any
from collections import OrderedDict
from concurrent.futures import ProcessPoolExecutor, Future
import threading
import pickle

from .ereader_layout import RenderingPosition, BidirectionalLayouter, FontFamilyOverride
from pyWebLayout.concrete.page import Page
from pyWebLayout.abstract.block import Block
from pyWebLayout.style.page_style import PageStyle
from pyWebLayout.style.fonts import BundledFont


def _render_page_worker(args: Tuple[List[Block],
                                    PageStyle,
                                    RenderingPosition,
                                    float,
                                    bool,
                                    Optional[BundledFont]]) -> Tuple[RenderingPosition,
                                                    bytes,
                                                    RenderingPosition]:
    """
    Worker function for multiprocess page rendering.

    Args:
        args: Tuple of (blocks, page_style, position, font_scale, is_backward, font_family)

    Returns:
        Tuple of (original_position, pickled_page, next_position)
    """
    blocks, page_style, position, font_scale, is_backward, font_family = args

    # Create font family override if specified
    font_family_override = FontFamilyOverride(font_family) if font_family else None

    layouter = BidirectionalLayouter(blocks, page_style, font_family_override=font_family_override)

    if is_backward:
        page, next_pos = layouter.render_page_backward(position, font_scale)
    else:
        page, next_pos = layouter.render_page_forward(position, font_scale)

    # Serialize the page for inter-process communication
    pickled_page = pickle.dumps(page)

    return position, pickled_page, next_pos


class PageBuffer:
    """
    Intelligent page caching system with LRU eviction and background rendering.
    Maintains separate forward and backward buffers for optimal navigation performance.
    """

    def __init__(self, buffer_size: int = 5, max_workers: int = 4):
        """
        Initialize the page buffer.

        Args:
            buffer_size: Number of pages to cache in each direction
            max_workers: Maximum number of worker processes for background rendering
        """
        self.buffer_size = buffer_size
        self.max_workers = max_workers

        # LRU caches for forward and backward pages
        self.forward_buffer: OrderedDict[RenderingPosition, Page] = OrderedDict()
        self.backward_buffer: OrderedDict[RenderingPosition, Page] = OrderedDict()

        # Position tracking for next/previous positions
        self.position_map: Dict[RenderingPosition,
                                RenderingPosition] = {}  # current -> next
        self.reverse_position_map: Dict[RenderingPosition,
                                        RenderingPosition] = {}  # current -> previous

        # Background rendering
        self.executor: Optional[ProcessPoolExecutor] = None
        self.pending_renders: Dict[RenderingPosition, Future] = {}
        self.render_lock = threading.Lock()

        # Document state
        self.blocks: Optional[List[Block]] = None
        self.page_style: Optional[PageStyle] = None
        self.current_font_scale: float = 1.0
        self.current_font_family: Optional[BundledFont] = None

    def initialize(
            self,
            blocks: List[Block],
            page_style: PageStyle,
            font_scale: float = 1.0,
            font_family: Optional[BundledFont] = None):
        """
        Initialize the buffer with document blocks and page style.

        Args:
            blocks: Document blocks to render
            page_style: Page styling configuration
            font_scale: Current font scaling factor
            font_family: Optional font family override
        """
        self.blocks = blocks
        self.page_style = page_style
        self.current_font_scale = font_scale
        self.current_font_family = font_family

        # Start the process pool
        if self.executor is None:
            self.executor = ProcessPoolExecutor(max_workers=self.max_workers)

    def get_page(self, position: RenderingPosition) -> Optional[Page]:
        """
        Get a cached page if available.

        Args:
            position: Position to get page for

        Returns:
            Cached page or None if not available
        """
        # Check forward buffer first
        if position in self.forward_buffer:
            # Move to end (most recently used)
            page = self.forward_buffer.pop(position)
            self.forward_buffer[position] = page
            return page

        # Check backward buffer
        if position in self.backward_buffer:
            # Move to end (most recently used)
            page = self.backward_buffer.pop(position)
            self.backward_buffer[position] = page
            return page

        return None

    def cache_page(
            self,
            position: RenderingPosition,
            page: Page,
            next_position: Optional[RenderingPosition] = None,
            is_backward: bool = False):
        """
        Cache a rendered page with LRU eviction.

        Args:
            position: Position of the page
            page: Rendered page to cache
            next_position: Position of the next page (for forward navigation)
            is_backward: Whether this is a backward-rendered page
        """
        target_buffer = self.backward_buffer if is_backward else self.forward_buffer

        # Add to cache
        target_buffer[position] = page

        # Track position relationships
        if next_position:
            if is_backward:
                self.reverse_position_map[next_position] = position
            else:
                self.position_map[position] = next_position

        # Evict oldest if buffer is full
        if len(target_buffer) > self.buffer_size:
            oldest_pos, _ = target_buffer.popitem(last=False)
            # Clean up position maps
            self.position_map.pop(oldest_pos, None)
            self.reverse_position_map.pop(oldest_pos, None)

    def start_background_rendering(
            self,
            current_position: RenderingPosition,
            direction: str = 'forward'):
        """
        Start background rendering of upcoming pages.

        Args:
            current_position: Current reading position
            direction: 'forward', 'backward', or 'both'
        """
        if not self.blocks or not self.page_style or not self.executor:
            return

        with self.render_lock:
            if direction in ['forward', 'both']:
                self._queue_forward_renders(current_position)

            if direction in ['backward', 'both']:
                self._queue_backward_renders(current_position)

    def _queue_forward_renders(self, start_position: RenderingPosition):
        """Queue forward page renders starting from the given position"""
        current_pos = start_position

        for i in range(self.buffer_size):
            # Skip if already cached or being rendered
            if current_pos in self.forward_buffer or current_pos in self.pending_renders:
                # Try to get next position from cache
                current_pos = self.position_map.get(current_pos)
                if not current_pos:
                    break
                continue

            # Queue render job
            args = (
                self.blocks,
                self.page_style,
                current_pos,
                self.current_font_scale,
                False,
                self.current_font_family)
            future = self.executor.submit(_render_page_worker, args)
            self.pending_renders[current_pos] = future

            # We don't know the next position yet, so we'll update it when the render
            # completes
            break

    def _queue_backward_renders(self, start_position: RenderingPosition):
        """Queue backward page renders ending at the given position"""
        current_pos = start_position

        for i in range(self.buffer_size):
            # Skip if already cached or being rendered
            if current_pos in self.backward_buffer or current_pos in self.pending_renders:
                # Try to get previous position from cache
                current_pos = self.reverse_position_map.get(current_pos)
                if not current_pos:
                    break
                continue

            # Queue render job
            args = (
                self.blocks,
                self.page_style,
                current_pos,
                self.current_font_scale,
                True,
                self.current_font_family)
            future = self.executor.submit(_render_page_worker, args)
            self.pending_renders[current_pos] = future

            # We don't know the previous position yet, so we'll update it when the
            # render completes
            break

    def check_completed_renders(self):
        """Check for completed background renders and cache the results"""
        if not self.pending_renders:
            return

        completed = []

        with self.render_lock:
            for position, future in self.pending_renders.items():
                if future.done():
                    try:
                        original_pos, pickled_page, next_pos = future.result()

                        # Deserialize the page
                        page = pickle.loads(pickled_page)

                        # Cache the page
                        self.cache_page(original_pos, page, next_pos, is_backward=False)

                        completed.append(position)

                    except Exception as e:
                        print(f"Background render failed for position {position}: {e}")
                        completed.append(position)

            # Remove completed renders
            for pos in completed:
                self.pending_renders.pop(pos, None)

    def invalidate_all(self):
        """Clear all cached pages and cancel pending renders"""
        with self.render_lock:
            # Cancel pending renders
            for future in self.pending_renders.values():
                future.cancel()
            self.pending_renders.clear()

            # Clear caches
            self.forward_buffer.clear()
            self.backward_buffer.clear()
            self.position_map.clear()
            self.reverse_position_map.clear()

    def set_font_scale(self, font_scale: float):
        """
        Update font scale and invalidate cache.

        Args:
            font_scale: New font scaling factor
        """
        if font_scale != self.current_font_scale:
            self.current_font_scale = font_scale
            self.invalidate_all()

    def set_font_family(self, font_family: Optional[BundledFont]):
        """
        Update font family and invalidate cache.

        Args:
            font_family: New font family (None = use original fonts)
        """
        if font_family != self.current_font_family:
            self.current_font_family = font_family
            self.invalidate_all()

    def get_cache_stats(self) -> Dict[str, Any]:
        """Get cache statistics for debugging/monitoring"""
        return {
            'forward_buffer_size': len(self.forward_buffer),
            'backward_buffer_size': len(self.backward_buffer),
            'pending_renders': len(self.pending_renders),
            'position_mappings': len(self.position_map),
            'reverse_position_mappings': len(self.reverse_position_map),
            'current_font_scale': self.current_font_scale,
            'current_font_family': self.current_font_family.value if self.current_font_family else None
        }

    def shutdown(self):
        """Shutdown the page buffer and clean up resources"""
        if self.executor:
            # Cancel pending renders
            with self.render_lock:
                for future in self.pending_renders.values():
                    future.cancel()

            # Shutdown executor
            self.executor.shutdown(wait=True)
            self.executor = None

        # Clear all caches
        self.invalidate_all()

    def __del__(self):
        """Cleanup on destruction"""
        self.shutdown()


class BufferedPageRenderer:
    """
    High-level interface for buffered page rendering with automatic background caching.
    """

    def __init__(self,
                 blocks: List[Block],
                 page_style: PageStyle,
                 buffer_size: int = 5,
                 page_size: Tuple[int,
                                  int] = (800,
                                          600),
                 font_family: Optional[BundledFont] = None):
        """
        Initialize the buffered renderer.

        Args:
            blocks: Document blocks to render
            page_style: Page styling configuration
            buffer_size: Number of pages to cache in each direction
            page_size: Page size (width, height) in pixels
            font_family: Optional font family override
        """
        # Create font family override if specified
        font_family_override = FontFamilyOverride(font_family) if font_family else None

        self.layouter = BidirectionalLayouter(blocks, page_style, page_size, font_family_override=font_family_override)
        self.buffer = PageBuffer(buffer_size)
        self.buffer.initialize(blocks, page_style, font_family=font_family)
        self.page_size = page_size
        self.blocks = blocks
        self.page_style = page_style

        self.current_position = RenderingPosition()
        self.font_scale = 1.0
        self.font_family = font_family

    def render_page(self, position: RenderingPosition,
                    font_scale: float = 1.0) -> Tuple[Page, RenderingPosition]:
        """
        Render a page with intelligent caching.

        Args:
            position: Position to render from
            font_scale: Font scaling factor

        Returns:
            Tuple of (rendered_page, next_position)
        """
        # Update font scale if changed
        if font_scale != self.font_scale:
            self.font_scale = font_scale
            self.buffer.set_font_scale(font_scale)

        # Check cache first
        cached_page = self.buffer.get_page(position)
        if cached_page:
            # Get next position from position map
            next_pos = self.buffer.position_map.get(position)

            # Only use cache if we have the forward position mapping
            # Otherwise, we need to compute it
            if next_pos is not None:
                # Start background rendering for upcoming pages
                self.buffer.start_background_rendering(position, 'forward')

                return cached_page, next_pos

            # Cache hit for the page, but we don't have the forward position
            # Fall through to compute it below

        # Render the page directly
        page, next_pos = self.layouter.render_page_forward(position, font_scale)

        # Cache the result
        self.buffer.cache_page(position, page, next_pos)

        # Start background rendering
        self.buffer.start_background_rendering(position, 'both')

        # Check for completed background renders
        self.buffer.check_completed_renders()

        return page, next_pos

    def render_page_backward(self,
                             end_position: RenderingPosition,
                             font_scale: float = 1.0) -> Tuple[Page,
                                                               RenderingPosition]:
        """
        Render a page ending at the given position with intelligent caching.

        Args:
            end_position: Position where page should end
            font_scale: Font scaling factor

        Returns:
            Tuple of (rendered_page, start_position)
        """
        # Update font scale if changed
        if font_scale != self.font_scale:
            self.font_scale = font_scale
            self.buffer.set_font_scale(font_scale)

        # Check cache first
        cached_page = self.buffer.get_page(end_position)
        if cached_page:
            # Get previous position from reverse position map
            prev_pos = self.buffer.reverse_position_map.get(end_position)

            # Only use cache if we have the reverse position mapping
            # Otherwise, we need to compute it
            if prev_pos is not None:
                # Start background rendering for previous pages
                self.buffer.start_background_rendering(end_position, 'backward')

                return cached_page, prev_pos

            # Cache hit for the page, but we don't have the reverse position
            # Fall through to compute it below

        # Render the page directly
        page, start_pos = self.layouter.render_page_backward(end_position, font_scale)

        # Cache the result
        self.buffer.cache_page(start_pos, page, end_position, is_backward=True)

        # Start background rendering
        self.buffer.start_background_rendering(end_position, 'both')

        # Check for completed background renders
        self.buffer.check_completed_renders()

        return page, start_pos

    def set_font_family(self, font_family: Optional[BundledFont]):
        """
        Change the font family and invalidate cache.

        Args:
            font_family: New font family (None = use original fonts)
        """
        if font_family != self.font_family:
            self.font_family = font_family

            # Update buffer
            self.buffer.set_font_family(font_family)

            # Recreate layouter with new font family override
            font_family_override = FontFamilyOverride(font_family) if font_family else None
            self.layouter = BidirectionalLayouter(
                self.blocks,
                self.page_style,
                self.page_size,
                font_family_override=font_family_override
            )

    def get_font_family(self) -> Optional[BundledFont]:
        """Get the current font family override"""
        return self.font_family

    def get_cache_stats(self) -> Dict[str, Any]:
        """Get cache statistics"""
        return self.buffer.get_cache_stats()

    def shutdown(self):
        """Shutdown the renderer and clean up resources"""
        self.buffer.shutdown()
