use crate::core::texture::{FloatTexture, SpectrumTexture};
use crate::Arena;
use shared::core::texture::{GPUFloatTexture, GPUSpectrumTexture};
use shared::textures::*;
use shared::Ptr;
use std::sync::Arc;

pub trait Upload {
    type Target;
    fn upload(self, arena: &Arena) -> Self::Target;
}

fn convert_float(tex: &FloatTexture, arena: &Arena) -> GPUFloatTexture {
    match tex {
        FloatTexture::Constant(t) => GPUFloatTexture::Constant(*t),
        FloatTexture::Bilerp(t) => GPUFloatTexture::Bilerp(*t),
        FloatTexture::Checkerboard(t) => GPUFloatTexture::Checkerboard(*t),
        FloatTexture::Dots(t) => GPUFloatTexture::Dots(*t),
        FloatTexture::FBm(t) => GPUFloatTexture::FBm(*t),
        FloatTexture::Windy(t) => GPUFloatTexture::Windy(*t),
        FloatTexture::Wrinkled(t) => GPUFloatTexture::Wrinkled(*t),

        FloatTexture::Scaled(t) => {
            let tex = arena.alloc(convert_float(&t.tex, arena));
            let scale = arena.alloc(convert_float(&t.scale, arena));
            GPUFloatTexture::Scaled(GPUFloatScaledTexture { tex, scale })
        }

        FloatTexture::Mix(t) => {
            let tex1 = arena.alloc(convert_float(&t.tex1, arena));
            let tex2 = arena.alloc(convert_float(&t.tex2, arena));
            let amount = arena.alloc(convert_float(&t.amount, arena));
            GPUFloatTexture::Mix(GPUFloatMixTexture { tex1, tex2, amount })
        }

        FloatTexture::DirectionMix(t) => {
            let tex1 = arena.alloc(convert_float(&t.tex1, arena));
            let tex2 = arena.alloc(convert_float(&t.tex2, arena));
            GPUFloatTexture::DirectionMix(GPUFloatDirectionMixTexture {
                tex1,
                tex2,
                dir: t.dir,
            })
        }

        FloatTexture::Image(t) => {
            let tex_obj = arena.get_texture_object(&t.base.mipmap);
            GPUFloatTexture::Image(GPUFloatImageTexture {
                mapping: t.base.mapping,
                tex_obj,
                scale: t.base.scale,
                invert: t.base.invert,
            })
        }
    }
}

fn convert_spectrum(tex: &SpectrumTexture, arena: &Arena) -> GPUSpectrumTexture {
    match tex {
        SpectrumTexture::Constant(t) => GPUSpectrumTexture::Constant(*t),
        SpectrumTexture::Bilerp(t) => GPUSpectrumTexture::Bilerp(*t),
        SpectrumTexture::Checkerboard(t) => GPUSpectrumTexture::Checkerboard(*t),
        SpectrumTexture::Dots(t) => GPUSpectrumTexture::Dots(*t),
        SpectrumTexture::Marble(t) => GPUSpectrumTexture::Marble(*t),

        SpectrumTexture::Scaled(t) => {
            let tex = arena.alloc(convert_spectrum(&t.tex, arena));
            let scale = arena.alloc(convert_float(&t.scale, arena));
            GPUSpectrumTexture::Scaled(GPUSpectrumScaledTexture { tex, scale })
        }

        SpectrumTexture::Mix(t) => {
            let tex1 = arena.alloc(convert_spectrum(&t.tex1, arena));
            let tex2 = arena.alloc(convert_spectrum(&t.tex2, arena));
            let amount = arena.alloc(convert_float(&t.amount, arena));
            GPUSpectrumTexture::Mix(GPUSpectrumMixTexture { tex1, tex2, amount })
        }

        SpectrumTexture::DirectionMix(t) => {
            let tex1 = arena.alloc(convert_spectrum(&t.tex1, arena));
            let tex2 = arena.alloc(convert_spectrum(&t.tex2, arena));
            GPUSpectrumTexture::DirectionMix(GPUSpectrumDirectionMixTexture {
                tex1,
                tex2,
                dir: t.dir,
            })
        }

        SpectrumTexture::Image(t) => {
            let tex_obj = arena.get_texture_object(&t.base.mipmap);
            GPUSpectrumTexture::Image(GPUSpectrumImageTexture {
                mapping: t.base.mapping,
                tex_obj,
                scale: t.base.scale,
                invert: t.base.invert,
                is_single_channel: t.base.mipmap.is_single_channel(),
                spectrum_type: t.spectrum_type,

                color_space: arena.alloc(
                    t.base
                        .mipmap
                        .color_space
                        .clone()
                        .unwrap_or_else(crate::spectra::default_colorspace),
                ),
            })
        }
    }
}

impl Upload for Arc<FloatTexture> {
    type Target = Ptr<GPUFloatTexture>;
    fn upload(self, arena: &Arena) -> Self::Target {
        arena.alloc(convert_float(&self, arena))
    }
}

impl Upload for Arc<SpectrumTexture> {
    type Target = Ptr<GPUSpectrumTexture>;
    fn upload(self, arena: &Arena) -> Self::Target {
        arena.alloc(convert_spectrum(&self, arena))
    }
}

impl Upload for &FloatTexture {
    type Target = Ptr<GPUFloatTexture>;
    fn upload(self, arena: &Arena) -> Self::Target {
        arena.alloc(convert_float(&self, arena))
    }
}

impl Upload for &SpectrumTexture {
    type Target = Ptr<GPUSpectrumTexture>;
    fn upload(self, arena: &Arena) -> Self::Target {
        arena.alloc(convert_spectrum(&self, arena))
    }
}
impl Upload for Option<Arc<FloatTexture>> {
    type Target = Ptr<GPUFloatTexture>;
    fn upload(self, arena: &Arena) -> Self::Target {
        arena.alloc_opt(self.map(|v| convert_float(&v, arena)))
    }
}

impl Upload for Option<Arc<SpectrumTexture>> {
    type Target = Ptr<GPUSpectrumTexture>;
    fn upload(self, arena: &Arena) -> Self::Target {
        arena.alloc_opt(self.map(|v| convert_spectrum(&v, arena)))
    }
}

pub trait ArenaUpload {
    fn upload<T: Upload>(&self, value: T) -> T::Target;
}

impl ArenaUpload for Arena {
    fn upload<T: Upload>(&self, value: T) -> T::Target {
        value.upload(self)
    }
}
