//! Rendering system for Lightningbeam documents
//!
//! Renders documents to Vello scenes for GPU-accelerated display.
//!
//! This module supports two rendering modes:
//! 1. **Legacy mode**: All layers rendered to a single Scene (simple, fast)
//! 2. **Compositing mode**: Each layer rendered to its own Scene for HDR compositing
//!
//! The compositing mode enables proper per-layer opacity, blend modes, and effects.

use crate::animation::TransformProperty;
use crate::clip::{ClipInstance, ImageAsset};
use crate::document::Document;
use crate::gpu::BlendMode;
use crate::layer::{AnyLayer, LayerTrait, VectorLayer};
use kurbo::{Affine, Shape};
use std::collections::HashMap;
use std::sync::Arc;
use uuid::Uuid;
use vello::kurbo::Rect;
use vello::peniko::{Blob, Fill, ImageAlphaType, ImageBrush, ImageData, ImageFormat};
use vello::Scene;

/// Cache for decoded image data to avoid re-decoding every frame
pub struct ImageCache {
    cache: HashMap<Uuid, Arc<ImageBrush>>,
}

impl ImageCache {
    /// Create a new empty image cache
    pub fn new() -> Self {
        Self {
            cache: HashMap::new(),
        }
    }

    /// Get or decode an image, caching the result
    pub fn get_or_decode(&mut self, asset: &ImageAsset) -> Option<Arc<ImageBrush>> {
        if let Some(cached) = self.cache.get(&asset.id) {
            return Some(Arc::clone(cached));
        }

        // Decode and cache
        let image = decode_image_asset(asset)?;
        let arc_image = Arc::new(image);
        self.cache.insert(asset.id, Arc::clone(&arc_image));
        Some(arc_image)
    }

    /// Clear cache entry when an image asset is deleted or modified
    pub fn invalidate(&mut self, id: &Uuid) {
        self.cache.remove(id);
    }

    /// Clear all cached images
    pub fn clear(&mut self) {
        self.cache.clear();
    }
}

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

/// Decode an image asset to peniko ImageBrush
fn decode_image_asset(asset: &ImageAsset) -> Option<ImageBrush> {
    // Get the raw file data
    let data = asset.data.as_ref()?;

    // Decode using the image crate
    let img = image::load_from_memory(data).ok()?;
    let rgba = img.to_rgba8();

    // Create peniko ImageData then ImageBrush
    let image_data = ImageData {
        data: Blob::from(rgba.into_raw()),
        format: ImageFormat::Rgba8,
        width: asset.width,
        height: asset.height,
        alpha_type: ImageAlphaType::Alpha,
    };
    Some(ImageBrush::new(image_data))
}

// ============================================================================
// Per-Layer Rendering for HDR Compositing Pipeline
// ============================================================================

/// Type of rendered layer for compositor handling
#[derive(Clone, Debug)]
pub enum RenderedLayerType {
    /// Regular content layer (vector, video) - composite its scene
    Content,
    /// Effect layer - apply effects to current composite state
    Effect {
        /// Active effect instances at the current time
        effect_instances: Vec<ClipInstance>,
    },
}

/// Metadata for a rendered layer, used for compositing
pub struct RenderedLayer {
    /// The layer's unique identifier
    pub layer_id: Uuid,
    /// The Vello scene containing the layer's rendered content
    pub scene: Scene,
    /// Layer opacity (0.0 to 1.0)
    pub opacity: f32,
    /// Blend mode for compositing
    pub blend_mode: BlendMode,
    /// Whether this layer has any visible content
    pub has_content: bool,
    /// Type of layer for compositor (content vs effect)
    pub layer_type: RenderedLayerType,
}

impl RenderedLayer {
    /// Create a new rendered layer with default settings
    pub fn new(layer_id: Uuid) -> Self {
        Self {
            layer_id,
            scene: Scene::new(),
            opacity: 1.0,
            blend_mode: BlendMode::Normal,
            has_content: false,
            layer_type: RenderedLayerType::Content,
        }
    }

    /// Create with specific opacity and blend mode
    pub fn with_settings(layer_id: Uuid, opacity: f32, blend_mode: BlendMode) -> Self {
        Self {
            layer_id,
            scene: Scene::new(),
            opacity,
            blend_mode,
            has_content: false,
            layer_type: RenderedLayerType::Content,
        }
    }

    /// Create an effect layer with active effect instances
    pub fn effect_layer(layer_id: Uuid, opacity: f32, effect_instances: Vec<ClipInstance>) -> Self {
        let has_content = !effect_instances.is_empty();
        Self {
            layer_id,
            scene: Scene::new(),
            opacity,
            blend_mode: BlendMode::Normal,
            has_content,
            layer_type: RenderedLayerType::Effect { effect_instances },
        }
    }
}

/// Result of rendering a document for compositing
pub struct CompositeRenderResult {
    /// Background scene (rendered separately for potential optimization)
    pub background: Scene,
    /// Rendered layers in bottom-to-top order
    pub layers: Vec<RenderedLayer>,
    /// Document dimensions
    pub width: f64,
    pub height: f64,
}

/// Render a document for the HDR compositing pipeline
///
/// Unlike `render_document_with_transform`, this function renders each visible
/// layer to its own Scene, enabling proper per-layer opacity, blend modes,
/// and effects in the GPU compositor.
///
/// Layers are returned in bottom-to-top order for compositing.
pub fn render_document_for_compositing(
    document: &Document,
    base_transform: Affine,
    image_cache: &mut ImageCache,
    video_manager: &std::sync::Arc<std::sync::Mutex<crate::video::VideoManager>>,
    skip_instance_id: Option<uuid::Uuid>,
) -> CompositeRenderResult {
    let time = document.current_time;

    // Render background to its own scene
    let mut background = Scene::new();
    render_background(document, &mut background, base_transform);

    // Check if any layers are soloed
    let any_soloed = document.visible_layers().any(|layer| layer.soloed());

    // Collect layers to render
    let layers_to_render: Vec<_> = document
        .visible_layers()
        .filter(|layer| {
            if any_soloed {
                layer.soloed()
            } else {
                true
            }
        })
        .collect();

    // Render each layer to its own scene
    let mut rendered_layers = Vec::with_capacity(layers_to_render.len());

    for layer in layers_to_render {
        let rendered = render_layer_isolated(
            document,
            time,
            layer,
            base_transform,
            image_cache,
            video_manager,
            skip_instance_id,
        );
        rendered_layers.push(rendered);
    }

    CompositeRenderResult {
        background,
        layers: rendered_layers,
        width: document.width,
        height: document.height,
    }
}

/// Render a single layer to its own isolated Scene
///
/// The layer is rendered with full opacity in its scene; the actual opacity
/// will be applied during compositing. This enables proper alpha blending
/// for nested clips and complex layer hierarchies.
pub fn render_layer_isolated(
    document: &Document,
    time: f64,
    layer: &AnyLayer,
    base_transform: Affine,
    image_cache: &mut ImageCache,
    video_manager: &std::sync::Arc<std::sync::Mutex<crate::video::VideoManager>>,
    skip_instance_id: Option<uuid::Uuid>,
) -> RenderedLayer {
    let layer_id = layer.id();
    let opacity = layer.opacity() as f32;

    // TODO: When we add blend mode support to layers, read it here
    let blend_mode = BlendMode::Normal;

    let mut rendered = RenderedLayer::with_settings(layer_id, opacity, blend_mode);

    // Render layer content with full opacity (1.0) - opacity applied during compositing
    match layer {
        AnyLayer::Vector(vector_layer) => {
            render_vector_layer_to_scene(
                document,
                time,
                vector_layer,
                &mut rendered.scene,
                base_transform,
                1.0, // Full opacity - layer opacity handled in compositing
                image_cache,
                video_manager,
                skip_instance_id,
            );
            rendered.has_content = !vector_layer.shape_instances.is_empty()
                || !vector_layer.clip_instances.is_empty();
        }
        AnyLayer::Audio(_) => {
            // Audio layers don't render visually
            rendered.has_content = false;
        }
        AnyLayer::Video(video_layer) => {
            let mut video_mgr = video_manager.lock().unwrap();
            render_video_layer_to_scene(
                document,
                time,
                video_layer,
                &mut rendered.scene,
                base_transform,
                1.0, // Full opacity - layer opacity handled in compositing
                &mut video_mgr,
            );
            rendered.has_content = !video_layer.clip_instances.is_empty();
        }
        AnyLayer::Effect(effect_layer) => {
            // Effect layers are processed during compositing, not rendered to scene
            // Return early with a dedicated effect layer type
            let active_effects: Vec<ClipInstance> = effect_layer
                .active_clip_instances_at(time)
                .into_iter()
                .cloned()
                .collect();
            return RenderedLayer::effect_layer(layer_id, opacity, active_effects);
        }
    }

    rendered
}

/// Render a vector layer to an isolated scene (for compositing pipeline)
fn render_vector_layer_to_scene(
    document: &Document,
    time: f64,
    layer: &VectorLayer,
    scene: &mut Scene,
    base_transform: Affine,
    parent_opacity: f64,
    image_cache: &mut ImageCache,
    video_manager: &std::sync::Arc<std::sync::Mutex<crate::video::VideoManager>>,
    skip_instance_id: Option<uuid::Uuid>,
) {
    // Render using the existing function but to this isolated scene
    render_vector_layer(
        document,
        time,
        layer,
        scene,
        base_transform,
        parent_opacity,
        image_cache,
        video_manager,
        skip_instance_id,
    );
}

/// Render a video layer to an isolated scene (for compositing pipeline)
fn render_video_layer_to_scene(
    document: &Document,
    time: f64,
    layer: &crate::layer::VideoLayer,
    scene: &mut Scene,
    base_transform: Affine,
    parent_opacity: f64,
    video_manager: &mut crate::video::VideoManager,
) {
    // Render using the existing function but to this isolated scene
    render_video_layer(
        document,
        time,
        layer,
        scene,
        base_transform,
        parent_opacity,
        video_manager,
    );
}

// ============================================================================
// Legacy Single-Scene Rendering (kept for backwards compatibility)
// ============================================================================

/// Render a document to a Vello scene
pub fn render_document(
    document: &Document,
    scene: &mut Scene,
    image_cache: &mut ImageCache,
    video_manager: &std::sync::Arc<std::sync::Mutex<crate::video::VideoManager>>,
) {
    render_document_with_transform(document, scene, Affine::IDENTITY, image_cache, video_manager, None);
}

/// Render a document to a Vello scene with a base transform
/// The base transform is composed with all object transforms (useful for camera zoom/pan)
pub fn render_document_with_transform(
    document: &Document,
    scene: &mut Scene,
    base_transform: Affine,
    image_cache: &mut ImageCache,
    video_manager: &std::sync::Arc<std::sync::Mutex<crate::video::VideoManager>>,
    skip_instance_id: Option<uuid::Uuid>,
) {
    // 1. Draw background
    render_background(document, scene, base_transform);

    // 2. Recursively render the root graphics object at current time
    let time = document.current_time;
    render_graphics_object(document, time, scene, base_transform, image_cache, video_manager, skip_instance_id);
}

/// Draw the document background
fn render_background(document: &Document, scene: &mut Scene, base_transform: Affine) {
    let background_rect = Rect::new(0.0, 0.0, document.width, document.height);

    // Convert our ShapeColor to vello's peniko Color
    let background_color = document.background_color.to_peniko();

    scene.fill(
        Fill::NonZero,
        base_transform,
        background_color,
        None,
        &background_rect,
    );
}

/// Recursively render the root graphics object and its children
fn render_graphics_object(
    document: &Document,
    time: f64,
    scene: &mut Scene,
    base_transform: Affine,
    image_cache: &mut ImageCache,
    video_manager: &std::sync::Arc<std::sync::Mutex<crate::video::VideoManager>>,
    skip_instance_id: Option<uuid::Uuid>,
) {
    // Check if any layers are soloed
    let any_soloed = document.visible_layers().any(|layer| layer.soloed());

    // Render layers based on solo state
    // If any layer is soloed, only render soloed layers
    // Otherwise, render all visible layers
    // Start with full opacity (1.0)
    for layer in document.visible_layers() {
        if any_soloed {
            // Only render soloed layers when solo is active
            if layer.soloed() {
                render_layer(document, time, layer, scene, base_transform, 1.0, image_cache, video_manager, skip_instance_id);
            }
        } else {
            // Render all visible layers when no solo is active
            render_layer(document, time, layer, scene, base_transform, 1.0, image_cache, video_manager, skip_instance_id);
        }
    }
}

/// Render a single layer
fn render_layer(
    document: &Document,
    time: f64,
    layer: &AnyLayer,
    scene: &mut Scene,
    base_transform: Affine,
    parent_opacity: f64,
    image_cache: &mut ImageCache,
    video_manager: &std::sync::Arc<std::sync::Mutex<crate::video::VideoManager>>,
    skip_instance_id: Option<uuid::Uuid>,
) {
    match layer {
        AnyLayer::Vector(vector_layer) => {
            render_vector_layer(document, time, vector_layer, scene, base_transform, parent_opacity, image_cache, video_manager, skip_instance_id)
        }
        AnyLayer::Audio(_) => {
            // Audio layers don't render visually
        }
        AnyLayer::Video(video_layer) => {
            let mut video_mgr = video_manager.lock().unwrap();
            render_video_layer(document, time, video_layer, scene, base_transform, parent_opacity, &mut video_mgr);
        }
        AnyLayer::Effect(_) => {
            // Effect layers are processed during GPU compositing, not rendered to scene
        }
    }
}

/// Render a clip instance (recursive rendering for nested compositions)
fn render_clip_instance(
    document: &Document,
    time: f64,
    clip_instance: &crate::clip::ClipInstance,
    parent_opacity: f64,
    scene: &mut Scene,
    base_transform: Affine,
    animation_data: &crate::animation::AnimationData,
    image_cache: &mut ImageCache,
    video_manager: &std::sync::Arc<std::sync::Mutex<crate::video::VideoManager>>,
) {
    // Try to find the clip in the document's clip libraries
    // For now, only handle VectorClips (VideoClip and AudioClip rendering not yet implemented)
    let Some(vector_clip) = document.vector_clips.get(&clip_instance.clip_id) else {
        return; // Clip not found or not a vector clip
    };

    // Remap timeline time to clip's internal time
    let Some(clip_time) = clip_instance.remap_time(time, vector_clip.duration) else {
        return; // Clip instance not active at this time
    };

    // Evaluate animated transform properties
    let transform = &clip_instance.transform;
    let x = animation_data.eval(
        &crate::animation::AnimationTarget::Object {
            id: clip_instance.id,
            property: TransformProperty::X,
        },
        time,
        transform.x,
    );
    let y = animation_data.eval(
        &crate::animation::AnimationTarget::Object {
            id: clip_instance.id,
            property: TransformProperty::Y,
        },
        time,
        transform.y,
    );
    let rotation = animation_data.eval(
        &crate::animation::AnimationTarget::Object {
            id: clip_instance.id,
            property: TransformProperty::Rotation,
        },
        time,
        transform.rotation,
    );
    let scale_x = animation_data.eval(
        &crate::animation::AnimationTarget::Object {
            id: clip_instance.id,
            property: TransformProperty::ScaleX,
        },
        time,
        transform.scale_x,
    );
    let scale_y = animation_data.eval(
        &crate::animation::AnimationTarget::Object {
            id: clip_instance.id,
            property: TransformProperty::ScaleY,
        },
        time,
        transform.scale_y,
    );
    let skew_x = animation_data.eval(
        &crate::animation::AnimationTarget::Object {
            id: clip_instance.id,
            property: TransformProperty::SkewX,
        },
        time,
        transform.skew_x,
    );
    let skew_y = animation_data.eval(
        &crate::animation::AnimationTarget::Object {
            id: clip_instance.id,
            property: TransformProperty::SkewY,
        },
        time,
        transform.skew_y,
    );

    // Build transform matrix (similar to shape instances)
    // For clip instances, we don't have a path to calculate center from,
    // so we use the clip's center point (width/2, height/2)
    let center_x = vector_clip.width / 2.0;
    let center_y = vector_clip.height / 2.0;

    // Build skew transforms (applied around clip center)
    let skew_transform = if skew_x != 0.0 || skew_y != 0.0 {
        let skew_x_affine = if skew_x != 0.0 {
            let tan_skew = skew_x.to_radians().tan();
            Affine::new([1.0, 0.0, tan_skew, 1.0, 0.0, 0.0])
        } else {
            Affine::IDENTITY
        };

        let skew_y_affine = if skew_y != 0.0 {
            let tan_skew = skew_y.to_radians().tan();
            Affine::new([1.0, tan_skew, 0.0, 1.0, 0.0, 0.0])
        } else {
            Affine::IDENTITY
        };

        // Skew around center: translate to origin, skew, translate back
        Affine::translate((center_x, center_y))
            * skew_x_affine
            * skew_y_affine
            * Affine::translate((-center_x, -center_y))
    } else {
        Affine::IDENTITY
    };

    let clip_transform = Affine::translate((x, y))
        * Affine::rotate(rotation.to_radians())
        * Affine::scale_non_uniform(scale_x, scale_y)
        * skew_transform;
    let instance_transform = base_transform * clip_transform;

    // Evaluate animated opacity
    let opacity = animation_data.eval(
        &crate::animation::AnimationTarget::Object {
            id: clip_instance.id,
            property: TransformProperty::Opacity,
        },
        time,
        clip_instance.opacity,
    );

    // Cascade opacity: parent_opacity × animated opacity
    let clip_opacity = parent_opacity * opacity;

    // Recursively render all root layers in the clip at the remapped time
    for layer_node in vector_clip.layers.iter() {
        // Skip invisible layers for performance
        if !layer_node.data.visible() {
            continue;
        }
        render_layer(document, clip_time, &layer_node.data, scene, instance_transform, clip_opacity, image_cache, video_manager, None);
    }
}

/// Render a video layer with all its clip instances
fn render_video_layer(
    document: &Document,
    time: f64,
    layer: &crate::layer::VideoLayer,
    scene: &mut Scene,
    base_transform: Affine,
    parent_opacity: f64,
    video_manager: &mut crate::video::VideoManager,
) {
    use crate::animation::TransformProperty;

    // Cascade opacity: parent_opacity × layer.opacity
    let layer_opacity = parent_opacity * layer.layer.opacity;

    // Render each video clip instance
    for clip_instance in &layer.clip_instances {
        // Get the video clip from the document
        let Some(video_clip) = document.video_clips.get(&clip_instance.clip_id) else {
            continue; // Clip not found
        };

        // Remap timeline time to clip's internal time
        let Some(clip_time) = clip_instance.remap_time(time, video_clip.duration) else {
            continue; // Clip instance not active at this time
        };

        // Get video frame from VideoManager
        let Some(frame) = video_manager.get_frame(&clip_instance.clip_id, clip_time) else {
            continue; // Frame not available
        };

        // Evaluate animated transform properties
        let transform = &clip_instance.transform;
        let x = layer.layer.animation_data.eval(
            &crate::animation::AnimationTarget::Object {
                id: clip_instance.id,
                property: TransformProperty::X,
            },
            time,
            transform.x,
        );
        let y = layer.layer.animation_data.eval(
            &crate::animation::AnimationTarget::Object {
                id: clip_instance.id,
                property: TransformProperty::Y,
            },
            time,
            transform.y,
        );
        let rotation = layer.layer.animation_data.eval(
            &crate::animation::AnimationTarget::Object {
                id: clip_instance.id,
                property: TransformProperty::Rotation,
            },
            time,
            transform.rotation,
        );
        let scale_x = layer.layer.animation_data.eval(
            &crate::animation::AnimationTarget::Object {
                id: clip_instance.id,
                property: TransformProperty::ScaleX,
            },
            time,
            transform.scale_x,
        );
        let scale_y = layer.layer.animation_data.eval(
            &crate::animation::AnimationTarget::Object {
                id: clip_instance.id,
                property: TransformProperty::ScaleY,
            },
            time,
            transform.scale_y,
        );
        let skew_x = layer.layer.animation_data.eval(
            &crate::animation::AnimationTarget::Object {
                id: clip_instance.id,
                property: TransformProperty::SkewX,
            },
            time,
            transform.skew_x,
        );
        let skew_y = layer.layer.animation_data.eval(
            &crate::animation::AnimationTarget::Object {
                id: clip_instance.id,
                property: TransformProperty::SkewY,
            },
            time,
            transform.skew_y,
        );

        // Build skew transform (applied around center)
        let center_x = video_clip.width / 2.0;
        let center_y = video_clip.height / 2.0;

        let skew_transform = if skew_x != 0.0 || skew_y != 0.0 {
            let skew_x_affine = if skew_x != 0.0 {
                let tan_skew = skew_x.to_radians().tan();
                Affine::new([1.0, 0.0, tan_skew, 1.0, 0.0, 0.0])
            } else {
                Affine::IDENTITY
            };

            let skew_y_affine = if skew_y != 0.0 {
                let tan_skew = skew_y.to_radians().tan();
                Affine::new([1.0, tan_skew, 0.0, 1.0, 0.0, 0.0])
            } else {
                Affine::IDENTITY
            };

            // Skew around center
            Affine::translate((center_x, center_y))
                * skew_x_affine
                * skew_y_affine
                * Affine::translate((-center_x, -center_y))
        } else {
            Affine::IDENTITY
        };

        let clip_transform = Affine::translate((x, y))
            * Affine::rotate(rotation.to_radians())
            * Affine::scale_non_uniform(scale_x, scale_y)
            * skew_transform;
        let instance_transform = base_transform * clip_transform;

        // Evaluate animated opacity
        let opacity = layer.layer.animation_data.eval(
            &crate::animation::AnimationTarget::Object {
                id: clip_instance.id,
                property: TransformProperty::Opacity,
            },
            time,
            clip_instance.opacity,
        );

        // Cascade opacity: layer_opacity × animated opacity
        let final_opacity = (layer_opacity * opacity) as f32;

        // Create peniko ImageBrush from video frame data (zero-copy via Arc clone)
        // Coerce Arc<Vec<u8>> to Arc<dyn AsRef<[u8]> + Send + Sync>
        let blob_data: Arc<dyn AsRef<[u8]> + Send + Sync> = frame.rgba_data.clone();
        let image_data = ImageData {
            data: Blob::new(blob_data),
            format: ImageFormat::Rgba8,
            width: frame.width,
            height: frame.height,
            alpha_type: ImageAlphaType::Alpha,
        };
        let image = ImageBrush::new(image_data);

        // Apply opacity
        let image_with_alpha = image.with_alpha(final_opacity);

        // Create rectangle path for the video frame
        let video_rect = Rect::new(0.0, 0.0, video_clip.width, video_clip.height);

        // Render video frame as image fill
        scene.fill(
            Fill::NonZero,
            instance_transform,
            &image_with_alpha,
            None,
            &video_rect,
        );
    }
}

/// Render a vector layer with all its clip instances and shape instances
fn render_vector_layer(
    document: &Document,
    time: f64,
    layer: &VectorLayer,
    scene: &mut Scene,
    base_transform: Affine,
    parent_opacity: f64,
    image_cache: &mut ImageCache,
    video_manager: &std::sync::Arc<std::sync::Mutex<crate::video::VideoManager>>,
    skip_instance_id: Option<uuid::Uuid>,
) {
    // Cascade opacity: parent_opacity × layer.opacity
    let layer_opacity = parent_opacity * layer.layer.opacity;

    // Render clip instances first (they appear under shape instances)
    for clip_instance in &layer.clip_instances {
        render_clip_instance(document, time, clip_instance, layer_opacity, scene, base_transform, &layer.layer.animation_data, image_cache, video_manager);
    }

    // Render each shape instance in the layer
    for shape_instance in &layer.shape_instances {
        // Skip this instance if it's being edited
        if Some(shape_instance.id) == skip_instance_id {
            continue;
        }

        // Get the shape for this instance
        let Some(shape) = layer.get_shape(&shape_instance.shape_id) else {
            continue;
        };

        // Evaluate animated properties
        let transform = &shape_instance.transform;
        let x = layer
            .layer
            .animation_data
            .eval(
                &crate::animation::AnimationTarget::Object {
                    id: shape_instance.id,
                    property: TransformProperty::X,
                },
                time,
                transform.x,
            );
        let y = layer
            .layer
            .animation_data
            .eval(
                &crate::animation::AnimationTarget::Object {
                    id: shape_instance.id,
                    property: TransformProperty::Y,
                },
                time,
                transform.y,
            );
        let rotation = layer
            .layer
            .animation_data
            .eval(
                &crate::animation::AnimationTarget::Object {
                    id: shape_instance.id,
                    property: TransformProperty::Rotation,
                },
                time,
                transform.rotation,
            );
        let scale_x = layer
            .layer
            .animation_data
            .eval(
                &crate::animation::AnimationTarget::Object {
                    id: shape_instance.id,
                    property: TransformProperty::ScaleX,
                },
                time,
                transform.scale_x,
            );
        let scale_y = layer
            .layer
            .animation_data
            .eval(
                &crate::animation::AnimationTarget::Object {
                    id: shape_instance.id,
                    property: TransformProperty::ScaleY,
                },
                time,
                transform.scale_y,
            );
        let skew_x = layer
            .layer
            .animation_data
            .eval(
                &crate::animation::AnimationTarget::Object {
                    id: shape_instance.id,
                    property: TransformProperty::SkewX,
                },
                time,
                transform.skew_x,
            );
        let skew_y = layer
            .layer
            .animation_data
            .eval(
                &crate::animation::AnimationTarget::Object {
                    id: shape_instance.id,
                    property: TransformProperty::SkewY,
                },
                time,
                transform.skew_y,
            );
        let opacity = layer
            .layer
            .animation_data
            .eval(
                &crate::animation::AnimationTarget::Object {
                    id: shape_instance.id,
                    property: TransformProperty::Opacity,
                },
                time,
                shape_instance.opacity,
            );

        // Check if shape has morphing animation
        let shape_index = layer
            .layer
            .animation_data
            .eval(
                &crate::animation::AnimationTarget::Shape {
                    id: shape.id,
                    property: crate::animation::ShapeProperty::ShapeIndex,
                },
                time,
                0.0,
            );

        // Get the morphed path
        let path = shape.get_morphed_path(shape_index);

        // Build transform matrix (compose with base transform for camera)
        // Get shape center for skewing around center
        let shape_bbox = shape.path().bounding_box();
        let center_x = (shape_bbox.x0 + shape_bbox.x1) / 2.0;
        let center_y = (shape_bbox.y0 + shape_bbox.y1) / 2.0;

        // Build skew transforms (applied around shape center)
        let skew_transform = if skew_x != 0.0 || skew_y != 0.0 {
            let skew_x_affine = if skew_x != 0.0 {
                let tan_skew = skew_x.to_radians().tan();
                Affine::new([1.0, 0.0, tan_skew, 1.0, 0.0, 0.0])
            } else {
                Affine::IDENTITY
            };

            let skew_y_affine = if skew_y != 0.0 {
                let tan_skew = skew_y.to_radians().tan();
                Affine::new([1.0, tan_skew, 0.0, 1.0, 0.0, 0.0])
            } else {
                Affine::IDENTITY
            };

            // Skew around center: translate to origin, skew, translate back
            Affine::translate((center_x, center_y))
                * skew_x_affine
                * skew_y_affine
                * Affine::translate((-center_x, -center_y))
        } else {
            Affine::IDENTITY
        };

        let object_transform = Affine::translate((x, y))
            * Affine::rotate(rotation.to_radians())
            * Affine::scale_non_uniform(scale_x, scale_y)
            * skew_transform;
        let affine = base_transform * object_transform;

        // Calculate final opacity (cascaded from parent → layer → shape instance)
        // layer_opacity already includes parent_opacity from render_vector_layer
        let final_opacity = (layer_opacity * opacity) as f32;

        // Determine fill rule
        let fill_rule = match shape.fill_rule {
            crate::shape::FillRule::NonZero => Fill::NonZero,
            crate::shape::FillRule::EvenOdd => Fill::EvenOdd,
        };

        // Render fill - prefer image fill over color fill
        let mut filled = false;

        // Check for image fill first
        if let Some(image_asset_id) = shape.image_fill {
            if let Some(image_asset) = document.get_image_asset(&image_asset_id) {
                if let Some(image) = image_cache.get_or_decode(image_asset) {
                    // Apply opacity to image (clone is cheap - Image uses Arc<Blob> internally)
                    let image_with_alpha = (*image).clone().with_alpha(final_opacity);

                    // The image is rendered as a fill for the shape path
                    // Since the shape path is a rectangle matching the image dimensions,
                    // the image should fill the shape perfectly
                    scene.fill(fill_rule, affine, &image_with_alpha, None, &path);
                    filled = true;
                }
            }
        }

        // Fall back to color fill if no image fill (or image failed to load)
        if !filled {
            if let Some(fill_color) = &shape.fill_color {
                // Apply opacity to color
                let alpha = ((fill_color.a as f32 / 255.0) * final_opacity * 255.0) as u8;
                let adjusted_color = crate::shape::ShapeColor::rgba(
                    fill_color.r,
                    fill_color.g,
                    fill_color.b,
                    alpha,
                );

                scene.fill(
                    fill_rule,
                    affine,
                    adjusted_color.to_peniko(),
                    None,
                    &path,
                );
            }
        }

        // Render stroke if present
        if let (Some(stroke_color), Some(stroke_style)) = (&shape.stroke_color, &shape.stroke_style)
        {
            // Apply opacity to color
            let alpha = ((stroke_color.a as f32 / 255.0) * final_opacity * 255.0) as u8;
            let adjusted_color = crate::shape::ShapeColor::rgba(
                stroke_color.r,
                stroke_color.g,
                stroke_color.b,
                alpha,
            );

            scene.stroke(
                &stroke_style.to_stroke(),
                affine,
                adjusted_color.to_peniko(),
                None,
                &path,
            );
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::document::Document;
    use crate::layer::{AnyLayer, LayerTrait, VectorLayer};
    use crate::object::ShapeInstance;
    use crate::shape::{Shape, ShapeColor};
    use kurbo::{Circle, Shape as KurboShape};

    #[test]
    fn test_render_empty_document() {
        let doc = Document::new("Test");
        let mut scene = Scene::new();
        let mut image_cache = ImageCache::new();

        render_document(&doc, &mut scene, &mut image_cache);
        // Should render background without errors
    }

    #[test]
    fn test_render_document_with_shape() {
        let mut doc = Document::new("Test");

        // Create a simple circle shape
        let circle = Circle::new((100.0, 100.0), 50.0);
        let path = circle.to_path(0.1);
        let shape = Shape::new(path).with_fill(ShapeColor::rgb(255, 0, 0));

        // Create a shape instance for the shape
        let shape_instance = ShapeInstance::new(shape.id);

        // Create a vector layer
        let mut vector_layer = VectorLayer::new("Layer 1");
        vector_layer.add_shape(shape);
        vector_layer.add_object(shape_instance);

        // Add to document
        doc.root.add_child(AnyLayer::Vector(vector_layer));

        // Render
        let mut scene = Scene::new();
        let mut image_cache = ImageCache::new();
        render_document(&doc, &mut scene, &mut image_cache);
        // Should render without errors
    }

    // === Solo Rendering Tests ===

    /// Helper to check if any layer is soloed in document
    fn has_soloed_layer(doc: &Document) -> bool {
        doc.visible_layers().any(|layer| layer.soloed())
    }

    /// Helper to count visible layers for rendering (respecting solo)
    fn count_layers_to_render(doc: &Document) -> usize {
        let any_soloed = has_soloed_layer(doc);
        doc.visible_layers()
            .filter(|layer| {
                if any_soloed {
                    layer.soloed()
                } else {
                    true
                }
            })
            .count()
    }

    #[test]
    fn test_no_solo_all_layers_render() {
        let mut doc = Document::new("Test");

        // Add two visible layers, neither soloed
        let layer1 = VectorLayer::new("Layer 1");
        let layer2 = VectorLayer::new("Layer 2");

        doc.root.add_child(AnyLayer::Vector(layer1));
        doc.root.add_child(AnyLayer::Vector(layer2));

        // Both should be rendered
        assert_eq!(has_soloed_layer(&doc), false);
        assert_eq!(count_layers_to_render(&doc), 2);

        // Render should work without errors
        let mut scene = Scene::new();
        let mut image_cache = ImageCache::new();
        render_document(&doc, &mut scene, &mut image_cache);
    }

    #[test]
    fn test_one_layer_soloed() {
        let mut doc = Document::new("Test");

        // Add two layers
        let mut layer1 = VectorLayer::new("Layer 1");
        let layer2 = VectorLayer::new("Layer 2");

        // Solo layer 1
        layer1.layer.soloed = true;

        doc.root.add_child(AnyLayer::Vector(layer1));
        doc.root.add_child(AnyLayer::Vector(layer2));

        // Only soloed layer should be rendered
        assert_eq!(has_soloed_layer(&doc), true);
        assert_eq!(count_layers_to_render(&doc), 1);

        // Verify the soloed layer is the one that would render
        let any_soloed = has_soloed_layer(&doc);
        let soloed_count: usize = doc.visible_layers()
            .filter(|l| any_soloed && l.soloed())
            .count();
        assert_eq!(soloed_count, 1);

        // Render should work
        let mut scene = Scene::new();
        let mut image_cache = ImageCache::new();
        render_document(&doc, &mut scene, &mut image_cache);
    }

    #[test]
    fn test_multiple_layers_soloed() {
        let mut doc = Document::new("Test");

        // Add three layers
        let mut layer1 = VectorLayer::new("Layer 1");
        let mut layer2 = VectorLayer::new("Layer 2");
        let layer3 = VectorLayer::new("Layer 3");

        // Solo layers 1 and 2
        layer1.layer.soloed = true;
        layer2.layer.soloed = true;

        doc.root.add_child(AnyLayer::Vector(layer1));
        doc.root.add_child(AnyLayer::Vector(layer2));
        doc.root.add_child(AnyLayer::Vector(layer3));

        // Only soloed layers (1 and 2) should render
        assert_eq!(has_soloed_layer(&doc), true);
        assert_eq!(count_layers_to_render(&doc), 2);

        // Render
        let mut scene = Scene::new();
        let mut image_cache = ImageCache::new();
        render_document(&doc, &mut scene, &mut image_cache);
    }

    #[test]
    fn test_hidden_layer_not_rendered() {
        let mut doc = Document::new("Test");

        let layer1 = VectorLayer::new("Layer 1");
        let mut layer2 = VectorLayer::new("Layer 2");

        // Hide layer 2
        layer2.layer.visible = false;

        doc.root.add_child(AnyLayer::Vector(layer1));
        doc.root.add_child(AnyLayer::Vector(layer2));

        // Only visible layer (1) should be considered
        assert_eq!(doc.visible_layers().count(), 1);

        // Render
        let mut scene = Scene::new();
        let mut image_cache = ImageCache::new();
        render_document(&doc, &mut scene, &mut image_cache);
    }

    #[test]
    fn test_hidden_but_soloed_layer() {
        // A hidden layer that is soloed shouldn't render
        // because visible_layers() filters out hidden layers first
        let mut doc = Document::new("Test");

        let layer1 = VectorLayer::new("Layer 1");
        let mut layer2 = VectorLayer::new("Layer 2");

        // Layer 2: soloed but hidden
        layer2.layer.soloed = true;
        layer2.layer.visible = false;

        doc.root.add_child(AnyLayer::Vector(layer1));
        doc.root.add_child(AnyLayer::Vector(layer2));

        // visible_layers only returns layer 1 (layer 2 is hidden)
        // Since layer 1 isn't soloed and no visible layers are soloed,
        // all visible layers render
        let any_soloed = has_soloed_layer(&doc);
        assert_eq!(any_soloed, false); // No *visible* layer is soloed

        // Both visible layers render (only 1 is visible)
        assert_eq!(count_layers_to_render(&doc), 1);

        // Render
        let mut scene = Scene::new();
        let mut image_cache = ImageCache::new();
        render_document(&doc, &mut scene, &mut image_cache);
    }

    #[test]
    fn test_solo_with_layer_opacity() {
        let mut doc = Document::new("Test");

        // Create layers with different opacities
        let mut layer1 = VectorLayer::new("Layer 1");
        let mut layer2 = VectorLayer::new("Layer 2");

        layer1.layer.opacity = 0.5;
        layer1.layer.soloed = true;

        layer2.layer.opacity = 0.8;

        // Add circle shapes for visible rendering
        let circle = Circle::new((50.0, 50.0), 20.0);
        let path = circle.to_path(0.1);
        let shape = Shape::new(path).with_fill(ShapeColor::rgb(255, 0, 0));
        let shape_instance = ShapeInstance::new(shape.id);
        layer1.add_shape(shape.clone());
        layer1.add_object(shape_instance);

        let shape2 = Shape::new(circle.to_path(0.1)).with_fill(ShapeColor::rgb(0, 255, 0));
        let shape_instance2 = ShapeInstance::new(shape2.id);
        layer2.add_shape(shape2);
        layer2.add_object(shape_instance2);

        doc.root.add_child(AnyLayer::Vector(layer1));
        doc.root.add_child(AnyLayer::Vector(layer2));

        // Only layer 1 (soloed with 0.5 opacity) should render
        assert_eq!(has_soloed_layer(&doc), true);
        assert_eq!(count_layers_to_render(&doc), 1);

        // Render
        let mut scene = Scene::new();
        let mut image_cache = ImageCache::new();
        render_document(&doc, &mut scene, &mut image_cache);
    }

    #[test]
    fn test_unsolo_returns_to_normal() {
        let mut doc = Document::new("Test");

        let mut layer1 = VectorLayer::new("Layer 1");
        let mut layer2 = VectorLayer::new("Layer 2");

        // First, solo layer 1
        layer1.layer.soloed = true;

        let id1 = doc.root.add_child(AnyLayer::Vector(layer1));
        let id2 = doc.root.add_child(AnyLayer::Vector(layer2));

        // Only 1 layer renders when soloed
        assert_eq!(count_layers_to_render(&doc), 1);

        // Now unsolo layer 1
        if let Some(layer) = doc.root.get_child_mut(&id1) {
            layer.set_soloed(false);
        }

        // Now both should render again
        assert_eq!(has_soloed_layer(&doc), false);
        assert_eq!(count_layers_to_render(&doc), 2);
    }
}
