use jni::{jni_sig, jni_str, Env, JValue};
use jni::objects::JObject;
use dropbear_engine::model::{Animation, AnimationChannel, AnimationInterpolation, ChannelValues, Material, Mesh, ModelVertex, Node, NodeTransform, Skin};
use eucalyptus_core::scripting::native::DropbearNativeError;
use eucalyptus_core::scripting::result::DropbearNativeResult;
use crate::math::{NQuaternion, NVector2, NVector3, NVector4};
use crate::ToJObject;

#[repr(C)]
#[derive(Clone, Debug)]
pub struct NModelVertex {
    pub position: NVector3,
    pub normal: NVector3,
    pub tangent: NVector4,
    pub tex_coords0: NVector2,
    pub tex_coords1: NVector2,
    pub colour0: NVector4,
    pub joints0: Vec<i32>,
    pub weights0: NVector4,
}

impl ToJObject for NModelVertex {
    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
        let class = env
            .load_class(jni_str!("com.dropbear.asset.model.ModelVertex"))
            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;

        let position = self.position.to_jobject(env)?;
        let normal = self.normal.to_jobject(env)?;
        let tangent = self.tangent.to_jobject(env)?;
        let tex_coords0 = self.tex_coords0.to_jobject(env)?;
        let tex_coords1 = self.tex_coords1.to_jobject(env)?;
        let colour0 = self.colour0.to_jobject(env)?;
        let joints0 = self.joints0.as_slice().to_jobject(env)?;
        let weights0 = self.weights0.to_jobject(env)?;

        let args = [
            JValue::Object(&position),
            JValue::Object(&normal),
            JValue::Object(&tangent),
            JValue::Object(&tex_coords0),
            JValue::Object(&tex_coords1),
            JValue::Object(&colour0),
            JValue::Object(&joints0),
            JValue::Object(&weights0),
        ];

        let obj = env
            .new_object(
                &class,
                jni_sig!((com.dropbear.math.Vector3f, com.dropbear.math.Vector3f, com.dropbear.math.Vector4f, com.dropbear.math.Vector2f, com.dropbear.math.Vector2f, com.dropbear.math.Vector4f, [int], com.dropbear.math.Vector4f) -> ()),
                &args,
            )
            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;

        Ok(obj)
    }
}

impl Into<NModelVertex> for ModelVertex {
    fn into(self) -> NModelVertex {
        NModelVertex {
            position: NVector3::from(self.position),
            normal: NVector3::from(self.normal),
            tangent: NVector4::from(self.tangent),
            tex_coords0: NVector2::from(self.tex_coords0),
            tex_coords1: NVector2::from(self.tex_coords1),
            colour0: NVector4::from(self.colour0),
            joints0: self.joints0.iter().map(|v| *v as i32).collect(),
            weights0: NVector4::from(self.weights0),
        }
    }
}


#[repr(C)]
#[derive(Clone, Debug)]
pub struct NMesh {
    pub name: String,
    pub num_elements: i32,
    pub material_index: i32,
    pub vertices: Vec<NModelVertex>,
    pub indices: Vec<u32>,
}

impl ToJObject for NMesh {
    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
        let class = env
            .load_class(jni_str!("com.dropbear.asset.model.Mesh"))
            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;

        let name = env
            .new_string(&self.name)
            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
        let vertices = self.vertices.to_jobject(env)?;

        let args = [
            JValue::Object(&name),
            JValue::Int(self.num_elements),
            JValue::Int(self.material_index),
            JValue::Object(&vertices),
        ];

        let obj = env
            .new_object(
                &class,
                jni_sig!((java.lang.String, int, int, java.util.List) -> ()),
                &args,
            )
            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;

        Ok(obj)
    }
}

impl From<&Mesh> for NMesh {
    fn from(mesh: &Mesh) -> NMesh {
        NMesh {
            name: mesh.name.clone(),
            num_elements: mesh.num_elements as i32,
            material_index: mesh.material as i32,
            vertices: mesh.vertex_buffer.data().iter().map(|v| (*v).into()).collect(),
            indices: mesh.index_buffer.data().to_vec(),
        }
    }
}

#[repr(C)]
#[derive(Clone, Debug)]
pub struct NMaterial {
    pub name: String,
    pub diffuse_texture: u64,
    pub normal_texture: Option<u64>,
    pub emissive_texture: Option<u64>,
    pub metallic_roughness_texture: Option<u64>,
    pub occlusion_texture: Option<u64>,
    pub tint: NVector4,
    pub emissive_factor: NVector3,
    pub metallic_factor: f32,
    pub roughness_factor: f32,
    pub alpha_cutoff: Option<f32>,
    pub occlusion_strength: f32,
    pub normal_scale: f32,
    pub uv_tiling: NVector2,
}

impl ToJObject for NMaterial {
    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
        let class = env
            .load_class(jni_str!("com.dropbear.asset.model.Material"))
            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;

        let name = env
            .new_string(&self.name)
            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
        let diffuse_texture = new_texture(env, self.diffuse_texture)?;
        let normal_texture = match self.normal_texture {
            Some(id) => new_texture(env, id)?,
            None => JObject::null(),
        };
        let emissive_texture = match self.emissive_texture {
            Some(id) => new_texture(env, id)?,
            None => JObject::null(),
        };
        let metallic_roughness_texture = match self.metallic_roughness_texture {
            Some(id) => new_texture(env, id)?,
            None => JObject::null(),
        };
        let occlusion_texture = match self.occlusion_texture {
            Some(id) => new_texture(env, id)?,
            None => JObject::null(),
        };
        let tint = self.tint.to_jobject(env)?;
        let emissive_factor = self.emissive_factor.to_jobject(env)?;
        let uv_tiling = self.uv_tiling.to_jobject(env)?;
        let alpha_cutoff = self.alpha_cutoff.to_jobject(env)?;

        let args = [
            JValue::Object(&name),
            JValue::Object(&diffuse_texture),
            JValue::Object(&normal_texture),
            JValue::Object(&tint),
            JValue::Object(&emissive_factor),
            JValue::Double(self.metallic_factor as f64),
            JValue::Double(self.roughness_factor as f64),
            JValue::Object(&alpha_cutoff),
            JValue::Double(self.occlusion_strength as f64),
            JValue::Double(self.normal_scale as f64),
            JValue::Object(&uv_tiling),
            JValue::Object(&emissive_texture),
            JValue::Object(&metallic_roughness_texture),
            JValue::Object(&occlusion_texture),
        ];

        let obj = env
            .new_object(
                &class,
                jni_sig!((
                    java.lang.String,
                    com.dropbear.asset.Texture,
                    com.dropbear.asset.Texture,
                    com.dropbear.math.Vector4d,
                    com.dropbear.math.Vector3d,
                    double, double,
                    java.lang.Double,
                    double, double,
                    com.dropbear.math.Vector2d,
                    com.dropbear.asset.Texture,
                    com.dropbear.asset.Texture,
                    com.dropbear.asset.Texture
                ) -> ()),
                &args,
            )
            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;

        Ok(obj)
    }
}

impl Into<NMaterial> for Material {
    fn into(self) -> NMaterial {
        NMaterial {
            name: self.name.clone(),
            diffuse_texture: self.diffuse_texture.id,
            normal_texture: self.normal_texture.and_then(|v| Some(v.id)),
            tint: NVector4::from(self.base_colour),
            emissive_factor: NVector3::from(self.emissive_factor),
            metallic_factor: self.metallic_factor,
            roughness_factor: self.roughness_factor,
            alpha_cutoff: self.alpha_cutoff,
            occlusion_strength: self.occlusion_strength,
            normal_scale: self.normal_scale,
            uv_tiling: NVector2::from(self.uv_tiling),
            emissive_texture: self.emissive_texture.and_then(|v| Some(v.id)),
            metallic_roughness_texture: self.metallic_roughness_texture.and_then(|v| Some(v.id)),
            occlusion_texture: self.occlusion_texture.and_then(|v| Some(v.id)),
        }
    }
}

// todo: cant this just be represented as a `com.dropbear.math.Transform`?
#[repr(C)]
#[derive(Clone, Debug)]
pub struct NNodeTransform {
    pub translation: NVector3,
    pub rotation: NQuaternion,
    pub scale: NVector3,
}

impl ToJObject for NNodeTransform {
    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
        let class = env
            .load_class(jni_str!("com.dropbear.asset.model.NodeTransform"))
            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;

        let translation = self.translation.to_jobject(env)?;
        let rotation = self.rotation.to_jobject(env)?;
        let scale = self.scale.to_jobject(env)?;

        let args = [
            JValue::Object(&translation),
            JValue::Object(&rotation),
            JValue::Object(&scale),
        ];

        let obj = env
            .new_object(
                &class,
                jni_sig!("(Lcom/dropbear/math/Vector3d;Lcom/dropbear/math/Quaterniond;Lcom/dropbear/math/Vector3d;)V"),
                &args,
            )
            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;

        Ok(obj)
    }
}

impl Into<NNodeTransform> for NodeTransform {
    fn into(self) -> NNodeTransform {
        NNodeTransform {
            translation: NVector3::from(self.translation),
            rotation: NQuaternion::from(self.rotation),
            scale: NVector3::from(self.scale),
        }
    }
}


#[repr(C)]
#[derive(Clone, Debug)]
pub struct NNode {
    pub name: String,
    pub parent: Option<i32>,
    pub children: Vec<i32>,
    pub transform: NNodeTransform,
}

impl ToJObject for NNode {
    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
        let class = env
            .load_class(jni_str!("com.dropbear.asset.model.Node"))
            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;

        let name = env
            .new_string(&self.name)
            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
        let parent = self.parent.to_jobject(env)?;
        let children = self.children.as_slice().to_jobject(env)?;
        let transform = self.transform.to_jobject(env)?;

        let args = [
            JValue::Object(&name),
            JValue::Object(&parent),
            JValue::Object(&children),
            JValue::Object(&transform),
        ];

        let obj = env
            .new_object(
                &class,
                jni_sig!("(Ljava/lang/String;Ljava/lang/Integer;Ljava/util/List;Lcom/dropbear/asset/model/NodeTransform;)V"),
                &args,
            )
            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;

        Ok(obj)
    }
}

impl Into<NNode> for Node {
    fn into(self) -> NNode {
        NNode {
            name: self.name.clone(),
            parent: self.parent.map(|v| v as i32),
            children: self.children.iter().map(|v| *v as i32).collect(),
            transform: self.transform.into(),
        }
    }
}

#[repr(C)]
#[derive(Clone, Debug)]
pub struct NSkin {
    pub name: String,
    pub joints: Vec<i32>,
    pub inverse_bind_matrices: Vec<Vec<f64>>,
    pub skeleton_root: Option<i32>,
}

impl ToJObject for NSkin {
    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
        let class = env
            .load_class(jni_str!("com.dropbear.asset.model.Skin"))
            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;

        let name = env
            .new_string(&self.name)
            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
        let joints = self.joints.as_slice().to_jobject(env)?;
        let inverse_bind_matrices = self.inverse_bind_matrices.as_slice().to_jobject(env)?;
        let skeleton_root = self.skeleton_root.to_jobject(env)?;

        let args = [
            JValue::Object(&name),
            JValue::Object(&joints),
            JValue::Object(&inverse_bind_matrices),
            JValue::Object(&skeleton_root),
        ];

        let obj = env
            .new_object(
                &class,
                jni_sig!(
                    "(Ljava/lang/String;Ljava/util/List;Ljava/util/List;Ljava/lang/Integer;)V"
                ),
                &args,
            )
            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;

        Ok(obj)
    }
}

impl Into<NSkin> for Skin {
    fn into(self) -> NSkin {
        let inverse_bind_matrices = self
            .inverse_bind_matrices
            .iter()
            .map(|matrix| matrix.to_cols_array().iter().map(|v| *v as f64).collect())
            .collect();

        NSkin {
            name: self.name.clone(),
            joints: self.joints.iter().map(|v| *v as i32).collect(),
            inverse_bind_matrices,
            skeleton_root: self.skeleton_root.map(|v| v as i32),
        }
    }
}

#[repr(C)]
#[derive(Clone, Debug)]
pub struct NAnimation {
    pub name: String,
    pub channels: Vec<NAnimationChannel>,
    pub duration: f32,
}

impl ToJObject for NAnimation {
    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
        let class = env
            .load_class(jni_str!("com.dropbear.asset.model.Animation"))
            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;

        let name = env
            .new_string(&self.name)
            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
        let channels = self.channels.to_jobject(env)?;

        let args = [
            JValue::Object(&name),
            JValue::Object(&channels),
            JValue::Double(self.duration as f64),
        ];

        let obj = env
            .new_object(
                &class,
                jni_sig!("(Ljava/lang/String;Ljava/util/List;D)V"),
                &args,
            )
            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;

        Ok(obj)
    }
}

impl Into<NAnimation> for Animation {
    fn into(self) -> NAnimation {
        NAnimation {
            name: self.name.clone(),
            channels: self
                .channels
                .into_iter()
                .map(|v| v.into())
                .collect(),
            duration: self.duration,
        }
    }
}

#[repr(C)]
#[derive(Clone, Debug)]
pub struct NAnimationChannel {
    pub target_node: i32,
    pub times: Vec<f64>,
    pub values: NChannelValues,
    pub interpolation: NAnimationInterpolation,
}

impl ToJObject for NAnimationChannel {
    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
        let class = env
            .load_class(jni_str!("com.dropbear.asset.model.AnimationChannel"))
            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;

        let times = self.times.as_slice().to_jobject(env)?;
        let values = self.values.to_jobject(env)?;
        let interpolation = self.interpolation.to_jobject(env)?;

        let args = [
            JValue::Int(self.target_node),
            JValue::Object(&times),
            JValue::Object(&values),
            JValue::Object(&interpolation),
        ];

        let obj = env
            .new_object(
                &class,
                jni_sig!("(I[DLcom/dropbear/asset/model/ChannelValues;Lcom/dropbear/asset/model/AnimationInterpolation;)V"),
                &args,
            )
            .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;

        Ok(obj)
    }
}

impl Into<NAnimationChannel> for AnimationChannel {
    fn into(self) -> NAnimationChannel {
        NAnimationChannel {
            target_node: self.target_node as i32,
            times: self.times.iter().map(|v| *v as f64).collect(),
            values: self.values.into(),
            interpolation: self.interpolation.into(),
        }
    }
}


#[repr(C)]
#[derive(Clone, Debug)]
#[dropbear_macro::repr_c_enum]
pub enum NAnimationInterpolation {
    Linear,
    Step,
    CubicSpline,
}

impl ToJObject for NAnimationInterpolation {
    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
        let class = env
            .load_class(jni_str!("com.dropbear.asset.model.AnimationInterpolation"))
            .map_err(|_| DropbearNativeError::JNIClassNotFound)?;

        let field_name = match self {
            NAnimationInterpolation::Linear => "LINEAR",
            NAnimationInterpolation::Step => "STEP",
            NAnimationInterpolation::CubicSpline => "CUBIC_SPLINE",
        };

        let value = env
            .get_static_field(
                class,
                jni::strings::JNIString::from(field_name),
                jni_sig!("Lcom/dropbear/asset/model/AnimationInterpolation;"),
            )
            .map_err(|_| DropbearNativeError::JNIFailedToGetField)?
            .l()
            .map_err(|_| DropbearNativeError::JNIUnwrapFailed)?;

        Ok(value)
    }
}

impl Into<NAnimationInterpolation> for AnimationInterpolation {
    fn into(self) -> NAnimationInterpolation {
        match self {
            AnimationInterpolation::Linear => NAnimationInterpolation::Linear,
            AnimationInterpolation::Step => NAnimationInterpolation::Step,
            AnimationInterpolation::CubicSpline => NAnimationInterpolation::CubicSpline,
        }
    }
}

#[repr(C)]
#[derive(Clone, Debug)]
#[dropbear_macro::repr_c_enum]
pub enum NChannelValues {
    Translations { values: Vec<NVector3> },
    Rotations { values: Vec<NQuaternion> },
    Scales { values: Vec<NVector3> },
    MorphWeights { values: Vec<Vec<f64>> },
}

impl ToJObject for NChannelValues {
    fn to_jobject<'a>(&self, env: &mut Env<'a>) -> DropbearNativeResult<JObject<'a>> {
        match self {
            NChannelValues::Translations { values } => {
                let class = env
                    .load_class(jni_str!(
                        "com.dropbear.asset.model.ChannelValues$Translations"
                    ))
                    .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
                let list = values.to_jobject(env)?;
                let obj = env
                    .new_object(
                        &class,
                        jni_sig!("(Ljava/util/List;)V"),
                        &[JValue::Object(&list)],
                    )
                    .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
                Ok(obj)
            }
            NChannelValues::Rotations { values } => {
                let class = env
                    .load_class(jni_str!("com.dropbear.asset.model.ChannelValues$Rotations"))
                    .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
                let list = values.to_jobject(env)?;
                let obj = env
                    .new_object(
                        &class,
                        jni_sig!("(Ljava/util/List;)V"),
                        &[JValue::Object(&list)],
                    )
                    .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
                Ok(obj)
            }
            NChannelValues::Scales { values } => {
                let class = env
                    .load_class(jni_str!("com.dropbear.asset.model.ChannelValues$Scales"))
                    .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
                let list = values.to_jobject(env)?;
                let obj = env
                    .new_object(
                        &class,
                        jni_sig!("(Ljava/util/List;)V"),
                        &[JValue::Object(&list)],
                    )
                    .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
                Ok(obj)
            }
            NChannelValues::MorphWeights { values } => {
                let class = env
                    .load_class(jni_str!(
                        "com.dropbear.asset.model.ChannelValues$MorphWeights"
                    ))
                    .map_err(|_| DropbearNativeError::JNIClassNotFound)?;
                let list = values.to_jobject(env)?;
                let obj = env
                    .new_object(
                        &class,
                        jni_sig!("(Ljava/util/List;)V"),
                        &[JValue::Object(&list)],
                    )
                    .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)?;
                Ok(obj)
            }
        }
    }
}


impl Into<NChannelValues> for ChannelValues {
    fn into(self) -> NChannelValues {
        match self {
            ChannelValues::Translations(list) => NChannelValues::Translations {
                values: list.iter().map(|v| NVector3::from(*v)).collect(),
            },
            ChannelValues::Rotations(list) => NChannelValues::Rotations {
                values: list.iter().map(|v| NQuaternion::from(*v)).collect(),
            },
            ChannelValues::Scales(list) => NChannelValues::Scales {
                values: list.iter().map(|v| NVector3::from(*v)).collect(),
            },
            ChannelValues::MorphWeights(items) => NChannelValues::MorphWeights {
                values: items
                    .iter()
                    .map(|weights| weights.iter().map(|v| *v as f64).collect())
                    .collect(),
            },
        }
    }
}

fn new_texture<'a>(env: &mut Env<'a>, texture_id: u64) -> DropbearNativeResult<JObject<'a>> {
    let class = env
        .load_class(jni_str!("com.dropbear.asset.Texture"))
        .map_err(|_| DropbearNativeError::JNIClassNotFound)?;

    env.new_object(&class, jni_sig!("(J)V"), &[JValue::Long(texture_id as i64)])
        .map_err(|_| DropbearNativeError::JNIFailedToCreateObject)
}