use crate::{PbrtContext, either::Either, tokenizer::Tokenizer};
use imagemap::{ImageMapEncoding, ImageMapWrap, SpectrumImageMapTexture};
use miette::{Result, miette};
use ray_tracing_core::{
    color::Color,
    prelude::{Float, Rng},
};
use scale::SpectrumScaleTexture2d;
use std::sync::Arc;

mod checkerboard;
mod imagemap;
mod scale;

pub trait PbrtTexture: std::fmt::Debug + Send + Sync {
    fn get_dimension(&self) -> TextureDimension;
    fn get_2d_float_texture(self: Arc<Self>) -> Result<Arc<dyn Pbrt2dFloatTexture>>;
    fn get_2d_spectrum_texture(self: Arc<Self>) -> Result<Arc<dyn Pbrt2dSpectrumTexture>>;
}

pub trait Pbrt2dFloatTexture: std::fmt::Debug + Sync + Send {
    fn get(&self, u: Float, v: Float) -> Float;
}

pub trait Pbrt2dSpectrumTexture: std::fmt::Debug + Sync + Send {
    fn get(&self, u: Float, v: Float) -> Color;
}

#[derive(Debug)]
pub enum TextureDimension {
    D2,
    D3,
}

impl TextureDimension {
    fn new(x: String) -> Result<Self> {
        match x.as_str() {
            "2" => Ok(TextureDimension::D2),
            "3" => Ok(TextureDimension::D3),
            _ => Err(miette!("Error parsing Texture dimension")),
        }
    }
}

#[derive(Debug)]
enum TextureMapping {
    UV,
    Spherical,
}

impl TextureMapping {
    fn new(x: String) -> Result<Self> {
        match x.as_str() {
            "uv" => Ok(TextureMapping::UV),
            "spherical" => Ok(TextureMapping::Spherical),
            _ => Err(miette!("Error")),
        }
    }
}

pub fn parse_spectrum(input: &mut Tokenizer) -> Result<Color> {
    let _ = input;
    todo!()
}

pub fn parse_rgb(input: &mut Tokenizer) -> Result<Color> {
    let t = input.parse_list()?;
    Ok(Color::new(t[0], t[1], t[2]))
}

fn parse_float_texture<R: Rng>(
    input: &mut Tokenizer,
    context: &PbrtContext<R>,
) -> Result<Arc<dyn PbrtTexture>> {
    let texture_class = input.next_string_value()?;

    match texture_class.as_str() {
        "checkerboard" => {
            let t = parse_dict!(input =>
                mapping, TextureMapping, TextureMapping::UV;
                uscale, Float, 1.0;
                vscale, Float, 1.0;
                udelta, Float, 0.0;
                vdelta, Float, 0.0;
                dimension, TextureDimension, TextureDimension::D2;
                tex1, Either<Float, String>, Either::A(1.0);
                tex2, Either<Float, String>, Either::A(0.0)
                =>
                mapping, "string mapping", TextureMapping::new(input.next_string_value()?)?;
                uscale, "float uscale", input.parse_parameter()?;
                vscale, "float vscale", input.parse_parameter()?;
                udelta, "float udelta", input.parse_parameter()?;
                vdelta, "float vdelta", input.parse_parameter()?;
                dimension, "integer dimension", TextureDimension::new(input.parse_parameter()?)?;
                tex1, "float tex1", Either::A(input.parse_parameter()?);
                tex1, "texture tex1", Either::B(input.next_string_value()?);
                tex2, "float tex2", Either::A(input.parse_parameter()?);
                tex2, "texture tex2", Either::B(input.next_string_value()?)

            );

            let mapping = UVMapping::new(t.mapping, t.uscale, t.vscale, t.udelta, t.vdelta);

            match t.dimension {
                TextureDimension::D2 => Ok(Arc::new(checkerboard::FloatCheckerboardTexture2d {
                    mapping,
                    tex: [
                        t.tex1
                            .map_b(|name| {
                                Arc::clone(
                                    context
                                        .get_texture(&name)
                                        .ok_or_else(|| miette!("unable to find texture"))?,
                                )
                                .get_2d_float_texture()
                            })
                            .transpose_b()?,
                        t.tex2
                            .map_b(|name| {
                                Arc::clone(
                                    context
                                        .get_texture(&name)
                                        .ok_or_else(|| miette!("unable to find texture"))?,
                                )
                                .get_2d_float_texture()
                            })
                            .transpose_b()?,
                    ],
                })),
                TextureDimension::D3 => {
                    todo!()
                }
            }
        }

        _ => Err(miette!(
            labels = vec![input.last_span_labeled(Some("here"))],
            "unknown error {texture_class}"
        )
        .with_source_code(input.get_src())),
    }
}

fn parse_spectrum_texture<R: Rng>(
    input: &mut Tokenizer,
    context: &PbrtContext<R>,
) -> Result<Arc<dyn PbrtTexture>> {
    let texture_class = input.next_string_value()?;
    match texture_class.as_str() {
        "checkerboard" => {
            let t = parse_dict!(input =>
                mapping, TextureMapping, TextureMapping::UV;
                uscale, Float, 1.0;
                vscale, Float, 1.0;
                udelta, Float, 0.0;
                vdelta, Float, 0.0;
                dimension, TextureDimension, TextureDimension::D2;
                tex1, Either<Color, String>, Either::A(Color::white());
                tex2, Either<Color, String>, Either::A(Color::black())
                =>
                mapping, "string mapping", TextureMapping::new(input.next_string_value()?)?;
                uscale, "float uscale", input.parse_parameter()?;
                vscale, "float vscale", input.parse_parameter()?;
                udelta, "float udelta", input.parse_parameter()?;
                vdelta, "float vdelta", input.parse_parameter()?;
                dimension, "integer dimension", TextureDimension::new(input.parse_parameter()?)?;
                tex1, "rgb tex1", Either::A(parse_rgb(input)?);
                tex1, "spectrum tex1", Either::A(parse_spectrum(input)?);
                tex1, "texture tex1", Either::B(input.next_string_value()?);
                tex2, "rgb tex2", Either::A(parse_rgb(input)?);
                tex2, "spectrum tex2", Either::A(parse_spectrum(input)?);
                tex2, "texture tex2", Either::B(input.next_string_value()?)

            );

            let mapping = UVMapping::new(t.mapping, t.uscale, t.vscale, t.udelta, t.vdelta);

            match t.dimension {
                TextureDimension::D2 => Ok(Arc::new(checkerboard::SpectrumCheckerboardTexture2d {
                    mapping,
                    tex: [
                        match t.tex1 {
                            Either::A(v) => Either::A(v),
                            Either::B(name) => Either::B(
                                Arc::clone(
                                    context
                                        .get_texture(&name)
                                        .ok_or_else(|| miette!("unable to find texture"))?,
                                )
                                .get_2d_spectrum_texture()?,
                            ),
                        },
                        match t.tex2 {
                            Either::A(v) => Either::A(v),
                            Either::B(name) => Either::B(
                                Arc::clone(
                                    context
                                        .get_texture(&name)
                                        .ok_or_else(|| miette!("unable to find texture"))?,
                                )
                                .get_2d_spectrum_texture()?,
                            ),
                        },
                    ],
                })),
                TextureDimension::D3 => {
                    todo!()
                }
            }
        }
        "imagemap" => {
            let t = parse_dict!(input =>
                mapping, TextureMapping, TextureMapping::UV;
                uscale, Float, 1.0;
                vscale, Float, 1.0;
                udelta, Float, 0.0;
                vdelta, Float, 0.0;
                filename, String, String::from("");
                wrap, ImageMapWrap, ImageMapWrap::Repeat;
                encoding, ImageMapEncoding, ImageMapEncoding::SRGB;
                scale, Float, 1.0
                =>
                mapping, "string mapping", TextureMapping::new(input.next_string_value()?)?;
                uscale, "float uscale", input.parse_parameter()?;
                vscale, "float vscale", input.parse_parameter()?;
                udelta, "float udelta", input.parse_parameter()?;
                vdelta, "float vdelta", input.parse_parameter()?;
                filename, "string filename", input.next_string_value()?;
                wrap, "string wrap", ImageMapWrap::new(input.next_string_value()?)?;
                encoding, "string encoding", ImageMapEncoding::new(input.next_string_value()?)?;
                scale, "float scale", input.parse_parameter()?


            );

            let mapping = UVMapping::new(t.mapping, t.uscale, t.vscale, t.udelta, t.vdelta);

            let path = dbg!(input.get_base_path()).join(t.filename);

            Ok(Arc::new(SpectrumImageMapTexture::new(
                mapping,
                t.scale,
                t.wrap,
                t.encoding,
                false,
                dbg!(path),
            )?))
        }
        "scale" => {
            let t = parse_dict!(input =>
                tex, Either<Color, String>, Either::A(Color::white());
                scale, Either<Float, String>, Either::A(1.0)
                =>
                tex, "rgb tex", Either::A(parse_rgb(input)?);
                tex, "spectrum tex", Either::A(parse_spectrum(input)?);
                tex, "texture tex", Either::B(input.next_string_value()?);
                scale, "float scale", Either::A(input.parse_parameter()?);
                scale, "texture scale", Either::B(input.next_string_value()?)
            );

            Ok(Arc::new(SpectrumScaleTexture2d {
                tex: match t.tex {
                    Either::A(v) => Either::A(v),
                    Either::B(t) => Either::B(
                        Arc::clone(
                            context
                                .get_texture(&t)
                                .ok_or_else(|| miette!("Unable to find texture scale"))?,
                        )
                        .get_2d_spectrum_texture()?,
                    ),
                },
                scale: match t.scale {
                    Either::A(v) => Either::A(v),
                    Either::B(t) => Either::B(
                        Arc::clone(
                            context
                                .get_texture(&t)
                                .ok_or_else(|| miette!("Unable to find texture scale"))?,
                        )
                        .get_2d_float_texture()?,
                    ),
                },
            }))
        }
        _ => Err(miette!("unknown error {texture_class}")),
    }
}

pub fn parse_texture<R: Rng>(
    input: &mut Tokenizer,
    context: &PbrtContext<R>,
) -> Result<(String, Arc<dyn PbrtTexture>)> {
    let texture_name: String = input.next_string_value()?;
    let texture_type: String = input.next_string_value()?;

    match texture_type.as_str() {
        "spectrum" => parse_spectrum_texture(input, context).map(|t| (texture_name, t)),
        "float" => parse_float_texture(input, context).map(|t| (texture_name, t)),
        _ => Err(miette!("Texture type has to be spectrum or float")),
    }
}

#[derive(Debug)]
struct UVMapping {
    uscale: Float,
    vscale: Float,
    udelta: Float,
    vdelta: Float,
    mapping: TextureMapping,
}

impl std::default::Default for UVMapping {
    fn default() -> Self {
        Self {
            uscale: 1.0,
            vscale: 1.0,
            udelta: 0.0,
            vdelta: 0.0,
            mapping: TextureMapping::UV,
        }
    }
}

impl UVMapping {
    fn new(
        mapping: TextureMapping,
        uscale: Float,
        vscale: Float,
        udelta: Float,
        vdelta: Float,
    ) -> Self {
        Self {
            uscale,
            vscale,
            udelta,
            vdelta,
            mapping,
        }
    }
    fn map(&self, u: Float, v: Float) -> (Float, Float) {
        match self.mapping {
            TextureMapping::UV => (u * self.uscale + self.udelta, v * self.vscale + self.vdelta),
            TextureMapping::Spherical => todo!(),
        }
    }
}
