use crate::core::shape::CreateShape;
use crate::core::texture::FloatTexture;
use crate::{Arena, FileLoc, ParameterDictionary};
use anyhow::{anyhow, Result};
use log::warn;
use shared::core::geometry::{Normal3f, Point3f};
use shared::core::shape::Shape;
use shared::shapes::{CurveCommon, CurveShape, CurveType};
use shared::utils::math::lerp;
use shared::utils::splines::{
    cubic_bspline_to_bezier, elevate_quadratic_bezier_to_cubic, quadratic_bspline_to_bezier,
};
use shared::Float;
use shared::{Ptr, Transform};
use std::collections::HashMap;
use std::sync::Arc;

pub fn create_curve(
    render_from_object: Transform,
    object_from_render: Transform,
    reverse_orientation: bool,
    seg_cp_bezier: &[Point3f],
    w0: Float,
    w1: Float,
    curve_type: CurveType,
    seg_normals: &[Normal3f],
    split_depth: usize,
    arena: &Arena,
) -> Vec<Ptr<Shape>> {
    let curve_common = CurveCommon::new(
        seg_cp_bezier,
        w0,
        w1,
        curve_type,
        seg_normals,
        render_from_object,
        object_from_render,
        reverse_orientation,
    );
    let n_segments = 1 << split_depth;
    let mut segments: Vec<Ptr<Shape>> = Vec::with_capacity(n_segments);

    for i in 0..n_segments {
        let u_min = i as Float / n_segments as Float;
        let u_max = (i + 1) as Float / n_segments as Float;

        let curve = CurveShape {
            common: curve_common.clone(),
            u_min,
            u_max,
        };

        segments.push(arena.alloc(Shape::Curve(curve)));
    }
    segments
}

impl CreateShape for CurveShape {
    fn create(
        render_from_object: Transform,
        object_from_render: Transform,
        reverse_orientation: bool,
        parameters: ParameterDictionary,
        _float_textures: &HashMap<String, Arc<FloatTexture>>,
        _loc: FileLoc,
        arena: &Arena,
    ) -> Result<Vec<Ptr<Shape>>> {
        let width = parameters.get_one_float("width", 1.0)?;
        let width0 = parameters.get_one_float("width0", width)?;
        let width1 = parameters.get_one_float("width1", width)?;

        let degree = parameters.get_one_int("degree", 3)?;
        if degree != 2 && degree != 3 {
            return Err(anyhow!(
                "Invalid degree {}: only degree 2 and 3 curves are supported.",
                degree
            ));
        }

        let basis = parameters.get_one_string("basis", "bezier")?;
        if basis != "bezier" && basis != "bspline" {
            return Err(anyhow!(
                "Invalid basis \"{}\": only \"bezier\" and \"bspline\" are supported.",
                basis
            ));
        }

        let cp = parameters.get_point3f_array("P")?;
        let n_segments;

        if basis == "bezier" {
            if cp.len() <= degree as usize
                || ((cp.len() - 1 - degree as usize) % degree as usize) != 0
            {
                return Err(anyhow!(
                    "Invalid number of control points {}: for the degree {} Bezier basis {} + n * {} are required.",
                    cp.len(),
                    degree,
                    degree + 1,
                    degree
                ));
            }
            n_segments = (cp.len() - 1) / degree as usize;
        } else {
            if cp.len() < (degree + 1) as usize {
                return Err(anyhow!(
                    "Invalid number of control points {}: for the degree {} b-spline basis, must have >= {}.",
                    cp.len(),
                    degree,
                    degree + 1
                ));
            }
            n_segments = cp.len() - degree as usize;
        }

        let curve_type_str = parameters.get_one_string("type", "flat")?;
        let curve_type = match curve_type_str.as_str() {
            "flat" => CurveType::Flat,
            "ribbon" => CurveType::Ribbon,
            "cylinder" => CurveType::Cylinder,
            _ => {
                return Err(anyhow!("Unknown curve type \"{}\".", curve_type_str));
            }
        };

        let mut n = parameters.get_normal3f_array("N")?;
        if !n.is_empty() {
            if curve_type != CurveType::Ribbon {
                warn!("Curve normals are only used with \"ribbon\" type curves. Discarding.");
                n.clear();
            } else if n.len() != n_segments + 1 {
                return Err(anyhow!(
                    "Invalid number of normals {}: must provide {} normals for ribbon curves with {} segments.",
                    n.len(),
                    n_segments + 1,
                    n_segments
                ));
            }
        } else if curve_type == CurveType::Ribbon {
            return Err(anyhow!(
                "Must provide normals \"N\" at curve endpoints with ribbon curves."
            ));
        }

        let use_gpu = false; // Replace with actual config check
        let split_depth = if use_gpu {
            0
        } else {
            parameters.get_one_int("splitdepth", 3)?
        };

        let mut curves: Vec<Ptr<Shape>> = Vec::new();
        let mut cp_offset = 0;

        for seg in 0..n_segments {
            let seg_cp_bezier: [Point3f; 4];

            if basis == "bezier" {
                if degree == 2 {
                    let slice = &cp[cp_offset..cp_offset + 3];
                    seg_cp_bezier = elevate_quadratic_bezier_to_cubic(slice);
                    cp_offset += 2; // Advance by degree
                } else {
                    let slice = &cp[cp_offset..cp_offset + 4];
                    seg_cp_bezier = [slice[0], slice[1], slice[2], slice[3]];
                    cp_offset += 3; // Advance by degree
                }
            } else {
                if degree == 2 {
                    let slice = &cp[cp_offset..cp_offset + 3];
                    let bez_cp = quadratic_bspline_to_bezier(slice);
                    seg_cp_bezier = elevate_quadratic_bezier_to_cubic(&bez_cp);
                } else {
                    let slice = &cp[cp_offset..cp_offset + 4];
                    seg_cp_bezier = cubic_bspline_to_bezier(slice);
                }
                cp_offset += 1; // Advance by 1 for B-Splines
            }

            let w0 = lerp(seg as Float / n_segments as Float, width0, width1);
            let w1 = lerp((seg + 1) as Float / n_segments as Float, width0, width1);

            let seg_normals = if !n.is_empty() {
                Some(&n[seg..seg + 2])
            } else {
                None
            };

            let new_curves = create_curve(
                render_from_object,
                object_from_render,
                reverse_orientation,
                &seg_cp_bezier,
                w0,
                w1,
                curve_type,
                seg_normals.expect("Could not determine normals to curve segments"),
                split_depth.try_into().unwrap(),
                arena,
            );

            curves.extend(new_curves);
        }
        Ok(curves)
    }
}
