//! Path extraction from PDF content streams.
//!
//! This module provides the `PathExtractor` type for extracting vector graphics
//! (paths) from PDF content streams.
//!
//! # PDF Path Operations
//!
//! PDF paths are constructed using a sequence of operators:
//! - `m` (MoveTo): Begin a new subpath
//! - `l` (LineTo): Add a line segment
//! - `c`, `v`, `y` (CurveTo variants): Add Bezier curve segments
//! - `re` (Rectangle): Add a rectangle as a complete subpath
//! - `h` (ClosePath): Close the current subpath
//!
//! Paths are then painted using:
//! - `S` (Stroke): Stroke the path
//! - `f`, `F`, `f*` (Fill): Fill the path
//! - `B`, `B*`, `b`, `b*`: Fill and stroke
//! - `n` (EndPath): End path without painting (used with clipping)
//!
//! # Example
//!
//! ```ignore
//! use pdf_oxide::extractors::paths::PathExtractor;
//!
//! let mut extractor = PathExtractor::new();
//!
//! // Process path construction operators
//! extractor.move_to(100.0, 100.0);
//! extractor.line_to(200.0, 100.0);
//! extractor.line_to(200.0, 200.0);
//! extractor.close_path();
//!
//! // Finalize with a painting operator
//! extractor.stroke();
//!
//! // Get extracted paths
//! let paths = extractor.finish();
//! ```

use crate::content::graphics_state::{GraphicsState, Matrix};
use crate::elements::{LineCap, LineJoin, PathContent, PathOperation};
use crate::geometry::{Point, Rect};
use crate::layout::Color;

/// Fill rule for path filling operations.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FillRule {
    /// Non-zero winding number rule (f, F operators)
    NonZero,
    /// Even-odd rule (f* operator)
    EvenOdd,
}

/// Path extractor for accumulating path operations from content streams.
///
/// This struct maintains state during content stream processing and accumulates
/// path operations. When a painting operator is encountered, the current path
/// is finalized and added to the extracted paths list.
///
/// # XObject Support (Issue #40)
///
/// The extractor can recursively process Form XObjects by maintaining a reference
/// to page resources and the document. When encountering a `Do` operator with a
/// Form XObject name, it will:
/// 1. Resolve the XObject from resources
/// 2. Check if it's a Form (not Image)
/// 3. Apply coordinate transformations
/// 4. Recursively extract paths from the XObject stream
///
/// XObjects are tracked to prevent infinite loops from circular references.
#[derive(Debug)]
pub struct PathExtractor {
    /// Accumulated complete paths
    paths: Vec<PathContent>,
    /// Current path being constructed
    current_operations: Vec<PathOperation>,
    /// Current point (set by MoveTo, LineTo, CurveTo, etc.)
    current_point: Option<Point>,
    /// Start point of current subpath (for ClosePath)
    subpath_start: Option<Point>,
    /// Current graphics state snapshot (for colors, line style)
    current_stroke_color: Option<Color>,
    current_fill_color: Option<Color>,
    current_line_width: f32,
    current_line_cap: LineCap,
    current_line_join: LineJoin,
    /// Current transformation matrix for coordinate transformation
    ctm: Matrix,
    /// Page resources for XObject resolution (Issue #40)
    resources: Option<crate::object::Object>,
    /// Stack of XObjects being processed to detect cycles (Issue #40)
    /// Uses a call stack approach instead of global "processed" set to allow
    /// the same XObject to be extracted at different transformations
    xobject_processing_stack: Vec<crate::object::ObjectRef>,
    /// Maximum XObject nesting depth (prevent stack overflow)
    max_xobject_depth: usize,
}

impl PathExtractor {
    /// Create a new path extractor.
    pub fn new() -> Self {
        Self {
            paths: Vec::new(),
            current_operations: Vec::new(),
            current_point: None,
            subpath_start: None,
            current_stroke_color: Some(Color::black()),
            current_fill_color: None,
            current_line_width: 1.0,
            current_line_cap: LineCap::Butt,
            current_line_join: LineJoin::Miter,
            ctm: Matrix::identity(),
            resources: None,
            xobject_processing_stack: Vec::new(),
            max_xobject_depth: 100, // Prevent infinite recursion
        }
    }

    /// Set the page resources for XObject resolution (Issue #40).
    pub fn set_resources(&mut self, resources: crate::object::Object) {
        self.resources = Some(resources);
    }

    /// Get the page resources if available.
    pub(crate) fn get_resources(&self) -> Option<&crate::object::Object> {
        self.resources.as_ref()
    }

    /// Check if an XObject is already in the processing stack (cycle detection)
    /// and if we haven't exceeded maximum nesting depth (Issue #40).
    pub(crate) fn can_process_xobject(&self, xobject_ref: crate::object::ObjectRef) -> bool {
        // Check if already in processing stack (would cause infinite recursion)
        if self.xobject_processing_stack.contains(&xobject_ref) {
            return false;
        }
        // Check if we've exceeded maximum nesting depth
        if self.xobject_processing_stack.len() >= self.max_xobject_depth {
            return false;
        }
        true
    }

    /// Push an XObject onto the processing stack (called before processing).
    pub(crate) fn push_xobject(&mut self, xobject_ref: crate::object::ObjectRef) {
        self.xobject_processing_stack.push(xobject_ref);
    }

    /// Pop an XObject from the processing stack (called after processing).
    pub(crate) fn pop_xobject(&mut self) {
        self.xobject_processing_stack.pop();
    }

    /// Update the current transformation matrix.
    pub fn set_ctm(&mut self, ctm: Matrix) {
        self.ctm = ctm;
    }

    /// Update graphics state from a GraphicsState snapshot.
    pub fn update_from_state(&mut self, state: &GraphicsState) {
        self.ctm = state.ctm;
        self.current_line_width = state.line_width;

        // Convert line cap (0=butt, 1=round, 2=square)
        self.current_line_cap = match state.line_cap {
            1 => LineCap::Round,
            2 => LineCap::Square,
            _ => LineCap::Butt,
        };

        // Convert line join (0=miter, 1=round, 2=bevel)
        self.current_line_join = match state.line_join {
            1 => LineJoin::Round,
            2 => LineJoin::Bevel,
            _ => LineJoin::Miter,
        };

        // Convert stroke color from RGB
        let (r, g, b) = state.stroke_color_rgb;
        self.current_stroke_color = Some(Color::new(r, g, b));

        // Convert fill color from RGB
        let (r, g, b) = state.fill_color_rgb;
        self.current_fill_color = Some(Color::new(r, g, b));
    }

    /// Set stroke color.
    pub fn set_stroke_color(&mut self, color: Color) {
        self.current_stroke_color = Some(color);
    }

    /// Set fill color.
    pub fn set_fill_color(&mut self, color: Color) {
        self.current_fill_color = Some(color);
    }

    /// Set line width.
    pub fn set_line_width(&mut self, width: f32) {
        self.current_line_width = width;
    }

    /// Set line cap style.
    pub fn set_line_cap(&mut self, cap: LineCap) {
        self.current_line_cap = cap;
    }

    /// Set line join style.
    pub fn set_line_join(&mut self, join: LineJoin) {
        self.current_line_join = join;
    }

    // === Path Construction Operators ===

    /// Move to a point (m operator).
    ///
    /// Begins a new subpath at the specified point.
    pub fn move_to(&mut self, x: f32, y: f32) {
        let point = self.transform_point(x, y);
        self.current_operations
            .push(PathOperation::MoveTo(point.x, point.y));
        self.current_point = Some(point);
        self.subpath_start = Some(point);
    }

    /// Line to a point (l operator).
    ///
    /// Adds a line segment from the current point to the specified point.
    pub fn line_to(&mut self, x: f32, y: f32) {
        let point = self.transform_point(x, y);
        self.current_operations
            .push(PathOperation::LineTo(point.x, point.y));
        self.current_point = Some(point);
    }

    /// Cubic Bezier curve (c operator).
    ///
    /// Adds a cubic Bezier curve from the current point to (x3, y3),
    /// using (x1, y1) and (x2, y2) as control points.
    pub fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x3: f32, y3: f32) {
        let p1 = self.transform_point(x1, y1);
        let p2 = self.transform_point(x2, y2);
        let p3 = self.transform_point(x3, y3);
        self.current_operations
            .push(PathOperation::CurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y));
        self.current_point = Some(p3);
    }

    /// Cubic Bezier curve with first control point = current point (v operator).
    ///
    /// Adds a cubic Bezier curve from the current point to (x3, y3),
    /// using the current point as the first control point and (x2, y2) as the second.
    pub fn curve_to_v(&mut self, x2: f32, y2: f32, x3: f32, y3: f32) {
        let p1 = self.current_point.unwrap_or(Point::new(0.0, 0.0));
        let p2 = self.transform_point(x2, y2);
        let p3 = self.transform_point(x3, y3);
        self.current_operations
            .push(PathOperation::CurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y));
        self.current_point = Some(p3);
    }

    /// Cubic Bezier curve with second control point = end point (y operator).
    ///
    /// Adds a cubic Bezier curve from the current point to (x3, y3),
    /// using (x1, y1) as the first control point and (x3, y3) as the second.
    pub fn curve_to_y(&mut self, x1: f32, y1: f32, x3: f32, y3: f32) {
        let p1 = self.transform_point(x1, y1);
        let p3 = self.transform_point(x3, y3);
        // Second control point equals end point
        self.current_operations
            .push(PathOperation::CurveTo(p1.x, p1.y, p3.x, p3.y, p3.x, p3.y));
        self.current_point = Some(p3);
    }

    /// Rectangle (re operator).
    ///
    /// Adds a complete rectangle subpath.
    pub fn rectangle(&mut self, x: f32, y: f32, width: f32, height: f32) {
        // Rectangle needs to transform all corners properly
        let p1 = self.transform_point(x, y);
        let p2 = self.transform_point(x + width, y + height);

        // Calculate transformed width and height
        let transformed_width = p2.x - p1.x;
        let transformed_height = p2.y - p1.y;

        self.current_operations.push(PathOperation::Rectangle(
            p1.x,
            p1.y,
            transformed_width,
            transformed_height,
        ));

        // Rectangle implicitly closes the subpath
        self.current_point = Some(p1);
        self.subpath_start = Some(p1);
    }

    /// Close the current subpath (h operator).
    ///
    /// Adds a line segment from the current point to the start of the subpath.
    pub fn close_path(&mut self) {
        self.current_operations.push(PathOperation::ClosePath);
        // Reset current point to subpath start
        if let Some(start) = self.subpath_start {
            self.current_point = Some(start);
        }
    }

    // === Path Painting Operators ===

    /// Stroke the path (S operator).
    pub fn stroke(&mut self) {
        self.finalize_path(true, false, FillRule::NonZero);
    }

    /// Close and stroke the path (s operator).
    pub fn close_and_stroke(&mut self) {
        self.close_path();
        self.stroke();
    }

    /// Fill the path (f or F operator).
    pub fn fill(&mut self, rule: FillRule) {
        self.finalize_path(false, true, rule);
    }

    /// Fill and stroke the path (B operator).
    pub fn fill_and_stroke(&mut self, rule: FillRule) {
        self.finalize_path(true, true, rule);
    }

    /// Close, fill and stroke the path (b operator).
    pub fn close_fill_and_stroke(&mut self, rule: FillRule) {
        self.close_path();
        self.fill_and_stroke(rule);
    }

    /// End path without painting (n operator).
    ///
    /// Used primarily with clipping paths.
    pub fn end_path(&mut self) {
        // Clear current path without creating a PathContent
        self.current_operations.clear();
        self.current_point = None;
        self.subpath_start = None;
    }

    // === Clipping Operators ===

    /// Set clipping path using non-zero winding rule (W operator).
    pub fn clip_non_zero(&mut self) {
        // Clipping doesn't paint the path, just sets the clipping region
        // The path is still available for subsequent painting
        // We don't extract clipping paths as separate content
    }

    /// Set clipping path using even-odd rule (W* operator).
    pub fn clip_even_odd(&mut self) {
        // Same as clip_non_zero - clipping doesn't create visible content
    }

    // === Helper Methods ===

    /// Transform a point using the current CTM.
    fn transform_point(&self, x: f32, y: f32) -> Point {
        self.ctm.transform_point(x, y)
    }

    /// Finalize the current path and add it to the extracted paths.
    fn finalize_path(&mut self, stroke: bool, fill: bool, _rule: FillRule) {
        if self.current_operations.is_empty() {
            return;
        }

        // Compute bounding box
        let bbox = Self::compute_bbox(&self.current_operations);

        // Create PathContent
        let mut path = PathContent::new(bbox);
        path.operations = std::mem::take(&mut self.current_operations);

        // Set stroke properties
        if stroke {
            path.stroke_color = self.current_stroke_color;
            path.stroke_width = self.current_line_width;
            path.line_cap = self.current_line_cap;
            path.line_join = self.current_line_join;
        } else {
            path.stroke_color = None;
        }

        // Set fill properties
        if fill {
            path.fill_color = self.current_fill_color;
        } else {
            path.fill_color = None;
        }

        self.paths.push(path);

        // Reset state
        self.current_point = None;
        self.subpath_start = None;
    }

    /// Compute bounding box from path operations.
    fn compute_bbox(operations: &[PathOperation]) -> Rect {
        let mut min_x = f32::MAX;
        let mut min_y = f32::MAX;
        let mut max_x = f32::MIN;
        let mut max_y = f32::MIN;

        for op in operations {
            match op {
                PathOperation::MoveTo(x, y) | PathOperation::LineTo(x, y) => {
                    min_x = min_x.min(*x);
                    min_y = min_y.min(*y);
                    max_x = max_x.max(*x);
                    max_y = max_y.max(*y);
                },
                PathOperation::CurveTo(x1, y1, x2, y2, x3, y3) => {
                    for (x, y) in [(*x1, *y1), (*x2, *y2), (*x3, *y3)] {
                        min_x = min_x.min(x);
                        min_y = min_y.min(y);
                        max_x = max_x.max(x);
                        max_y = max_y.max(y);
                    }
                },
                PathOperation::Rectangle(x, y, w, h) => {
                    min_x = min_x.min(*x);
                    min_y = min_y.min(*y);
                    max_x = max_x.max(*x + *w);
                    max_y = max_y.max(*y + *h);
                },
                PathOperation::ClosePath => {},
            }
        }

        if min_x == f32::MAX {
            Rect::new(0.0, 0.0, 0.0, 0.0)
        } else {
            Rect::new(min_x, min_y, max_x - min_x, max_y - min_y)
        }
    }

    /// Finish extraction and return all extracted paths.
    pub fn finish(self) -> Vec<PathContent> {
        self.paths
    }

    /// Get the number of paths extracted so far.
    pub fn path_count(&self) -> usize {
        self.paths.len()
    }

    /// Check if there's a path currently being constructed.
    pub fn has_current_path(&self) -> bool {
        !self.current_operations.is_empty()
    }
}

impl Default for PathExtractor {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_path_extractor_new() {
        let extractor = PathExtractor::new();
        assert_eq!(extractor.path_count(), 0);
        assert!(!extractor.has_current_path());
    }

    #[test]
    fn test_simple_line_stroke() {
        let mut extractor = PathExtractor::new();

        extractor.move_to(10.0, 10.0);
        extractor.line_to(100.0, 10.0);
        extractor.stroke();

        let paths = extractor.finish();
        assert_eq!(paths.len(), 1);
        assert_eq!(paths[0].operations.len(), 2);
        assert!(paths[0].has_stroke());
        assert!(!paths[0].has_fill());
    }

    #[test]
    fn test_rectangle_fill() {
        let mut extractor = PathExtractor::new();
        extractor.set_fill_color(Color::new(1.0, 0.0, 0.0));

        extractor.rectangle(50.0, 50.0, 100.0, 80.0);
        extractor.fill(FillRule::NonZero);

        let paths = extractor.finish();
        assert_eq!(paths.len(), 1);
        assert_eq!(paths[0].operations.len(), 1);
        assert!(!paths[0].has_stroke());
        assert!(paths[0].has_fill());

        // Check bbox
        assert_eq!(paths[0].bbox.x, 50.0);
        assert_eq!(paths[0].bbox.y, 50.0);
        assert_eq!(paths[0].bbox.width, 100.0);
        assert_eq!(paths[0].bbox.height, 80.0);
    }

    #[test]
    fn test_closed_path() {
        let mut extractor = PathExtractor::new();
        // Set fill color for fill_and_stroke to work
        extractor.set_fill_color(Color::new(0.5, 0.5, 0.5));

        extractor.move_to(0.0, 0.0);
        extractor.line_to(100.0, 0.0);
        extractor.line_to(100.0, 100.0);
        extractor.line_to(0.0, 100.0);
        extractor.close_path();
        extractor.fill_and_stroke(FillRule::NonZero);

        let paths = extractor.finish();
        assert_eq!(paths.len(), 1);
        assert_eq!(paths[0].operations.len(), 5); // MoveTo, 3x LineTo, ClosePath
        assert!(paths[0].has_stroke());
        assert!(paths[0].has_fill());
    }

    #[test]
    fn test_bezier_curve() {
        let mut extractor = PathExtractor::new();

        extractor.move_to(0.0, 0.0);
        extractor.curve_to(25.0, 100.0, 75.0, 100.0, 100.0, 0.0);
        extractor.stroke();

        let paths = extractor.finish();
        assert_eq!(paths.len(), 1);
        assert_eq!(paths[0].operations.len(), 2);
        assert!(matches!(paths[0].operations[1], PathOperation::CurveTo(_, _, _, _, _, _)));
    }

    #[test]
    fn test_multiple_paths() {
        let mut extractor = PathExtractor::new();

        // First path: horizontal line
        extractor.move_to(0.0, 0.0);
        extractor.line_to(100.0, 0.0);
        extractor.stroke();

        // Second path: vertical line
        extractor.move_to(50.0, 0.0);
        extractor.line_to(50.0, 100.0);
        extractor.stroke();

        let paths = extractor.finish();
        assert_eq!(paths.len(), 2);
    }

    #[test]
    fn test_end_path_clears_operations() {
        let mut extractor = PathExtractor::new();

        extractor.move_to(0.0, 0.0);
        extractor.line_to(100.0, 100.0);
        extractor.end_path(); // Should discard the path

        let paths = extractor.finish();
        assert_eq!(paths.len(), 0);
    }

    #[test]
    fn test_line_style_properties() {
        let mut extractor = PathExtractor::new();
        extractor.set_line_width(3.0);
        extractor.set_line_cap(LineCap::Round);
        extractor.set_line_join(LineJoin::Bevel);
        extractor.set_stroke_color(Color::new(0.0, 0.0, 1.0));

        extractor.move_to(0.0, 0.0);
        extractor.line_to(100.0, 100.0);
        extractor.stroke();

        let paths = extractor.finish();
        assert_eq!(paths.len(), 1);
        assert_eq!(paths[0].stroke_width, 3.0);
        assert_eq!(paths[0].line_cap, LineCap::Round);
        assert_eq!(paths[0].line_join, LineJoin::Bevel);
    }

    #[test]
    fn test_ctm_transformation() {
        let mut extractor = PathExtractor::new();

        // Set a translation matrix (move everything by 50, 50)
        extractor.set_ctm(Matrix::translation(50.0, 50.0));

        extractor.move_to(0.0, 0.0);
        extractor.line_to(100.0, 0.0);
        extractor.stroke();

        let paths = extractor.finish();
        assert_eq!(paths.len(), 1);

        // Check that the first operation was transformed
        if let PathOperation::MoveTo(x, y) = paths[0].operations[0] {
            assert_eq!(x, 50.0);
            assert_eq!(y, 50.0);
        } else {
            panic!("Expected MoveTo operation");
        }
    }

    #[test]
    fn test_bbox_calculation() {
        let mut extractor = PathExtractor::new();

        // Create a path with known bounds
        extractor.move_to(10.0, 20.0);
        extractor.line_to(110.0, 20.0);
        extractor.line_to(110.0, 120.0);
        extractor.line_to(10.0, 120.0);
        extractor.close_path();
        extractor.stroke();

        let paths = extractor.finish();
        assert_eq!(paths.len(), 1);

        let bbox = &paths[0].bbox;
        assert_eq!(bbox.x, 10.0);
        assert_eq!(bbox.y, 20.0);
        assert_eq!(bbox.width, 100.0);
        assert_eq!(bbox.height, 100.0);
    }

    #[test]
    fn test_curve_to_v() {
        let mut extractor = PathExtractor::new();

        extractor.move_to(0.0, 0.0);
        extractor.curve_to_v(50.0, 100.0, 100.0, 0.0);
        extractor.stroke();

        let paths = extractor.finish();
        assert_eq!(paths.len(), 1);

        // The v operator uses current point as first control point
        if let PathOperation::CurveTo(x1, y1, _, _, _, _) = paths[0].operations[1] {
            assert_eq!(x1, 0.0);
            assert_eq!(y1, 0.0);
        }
    }

    #[test]
    fn test_curve_to_y() {
        let mut extractor = PathExtractor::new();

        extractor.move_to(0.0, 0.0);
        extractor.curve_to_y(50.0, 100.0, 100.0, 0.0);
        extractor.stroke();

        let paths = extractor.finish();
        assert_eq!(paths.len(), 1);

        // The y operator uses end point as second control point
        if let PathOperation::CurveTo(_, _, x2, y2, x3, y3) = paths[0].operations[1] {
            assert_eq!(x2, x3);
            assert_eq!(y2, y3);
        }
    }

    #[test]
    fn test_fill_even_odd() {
        let mut extractor = PathExtractor::new();
        extractor.set_fill_color(Color::new(0.0, 1.0, 0.0));

        extractor.rectangle(0.0, 0.0, 100.0, 100.0);
        extractor.fill(FillRule::EvenOdd);

        let paths = extractor.finish();
        assert_eq!(paths.len(), 1);
        assert!(paths[0].has_fill());
    }

    #[test]
    fn test_close_and_stroke() {
        let mut extractor = PathExtractor::new();

        extractor.move_to(0.0, 0.0);
        extractor.line_to(100.0, 0.0);
        extractor.line_to(50.0, 100.0);
        extractor.close_and_stroke(); // Should add ClosePath and stroke

        let paths = extractor.finish();
        assert_eq!(paths.len(), 1);
        // Operations: MoveTo, LineTo, LineTo, ClosePath
        assert_eq!(paths[0].operations.len(), 4);
        assert!(matches!(paths[0].operations[3], PathOperation::ClosePath));
    }

    #[test]
    fn test_update_from_state() {
        let mut extractor = PathExtractor::new();

        let mut state = GraphicsState::new();
        state.line_width = 5.0;
        state.line_cap = 1; // Round
        state.line_join = 2; // Bevel
        state.stroke_color_rgb = (1.0, 0.0, 0.0);
        state.fill_color_rgb = (0.0, 1.0, 0.0);

        extractor.update_from_state(&state);

        extractor.rectangle(0.0, 0.0, 100.0, 100.0);
        extractor.fill_and_stroke(FillRule::NonZero);

        let paths = extractor.finish();
        assert_eq!(paths.len(), 1);
        assert_eq!(paths[0].stroke_width, 5.0);
        assert_eq!(paths[0].line_cap, LineCap::Round);
        assert_eq!(paths[0].line_join, LineJoin::Bevel);
    }
}
