use std::collections::HashMap;

use petgraph::graph::Graph;
pub use petgraph::graph::NodeIndex;
use serde::{Deserialize, Serialize};

use crate::ecs::bounding_volume::components::BoundingVolume;
use crate::ecs::camera::{Camera, OrthographicCamera, PerspectiveCamera, Projection};
use crate::ecs::graphics::resources::Atmosphere;
use crate::ecs::light::{Light, LightType};
use crate::ecs::lines::components::Line;
use crate::ecs::material::components::{AlphaMode, Material};
use crate::ecs::prefab::Prefab;
use crate::ecs::script::components::Script;
use crate::ecs::transform::LocalTransform;

pub const ENGINE_VERSION: &str = env!("CARGO_PKG_VERSION");

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Map {
    pub engine_version: String,
    pub name: String,
    pub atmosphere: Atmosphere,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub hdr_skybox: Option<MapHdrSkybox>,
    pub prefabs: HashMap<String, MapPrefab>,
    pub graph: Graph<MapNode, ()>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum MapHdrSkybox {
    Embedded { data: Vec<u8> },
    Reference { path: String },
}

impl Map {
    pub fn new(name: impl Into<String>) -> Self {
        Self {
            engine_version: ENGINE_VERSION.to_string(),
            name: name.into(),
            atmosphere: Atmosphere::default(),
            hdr_skybox: None,
            prefabs: HashMap::new(),
            graph: Graph::new(),
        }
    }

    pub fn add_prefab(&mut self, name: impl Into<String>, prefab: MapPrefab) {
        self.prefabs.insert(name.into(), prefab);
    }

    pub fn add_root_node(&mut self, node: MapNode) -> NodeIndex {
        self.graph.add_node(node)
    }

    pub fn add_child_node(&mut self, parent: NodeIndex, node: MapNode) -> NodeIndex {
        let child_index = self.graph.add_node(node);
        self.graph.add_edge(parent, child_index, ());
        child_index
    }

    pub fn remove_node(&mut self, index: NodeIndex) -> Option<MapNode> {
        self.graph.remove_node(index)
    }

    pub fn root_nodes(&self) -> Vec<NodeIndex> {
        self.graph
            .node_indices()
            .filter(|&index| {
                self.graph
                    .neighbors_directed(index, petgraph::Direction::Incoming)
                    .next()
                    .is_none()
            })
            .collect()
    }

    pub fn is_valid(&self) -> bool {
        !self.graph.node_count() == 0 || !self.root_nodes().is_empty()
    }

    pub fn children(&self, node: NodeIndex) -> Vec<NodeIndex> {
        self.graph
            .neighbors_directed(node, petgraph::Direction::Outgoing)
            .collect()
    }
}

use crate::ecs::mesh::components::Mesh;
use std::collections::HashMap as StdHashMap;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum MapPrefab {
    Embedded {
        prefab: Prefab,
        #[serde(default)]
        meshes: StdHashMap<String, Mesh>,
        #[serde(default)]
        textures: StdHashMap<String, EmbeddedTexture>,
    },
    Reference {
        path: String,
    },
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmbeddedTexture {
    pub rgba_data: Vec<u8>,
    pub width: u32,
    pub height: u32,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum MapNode {
    Entity {
        name: Option<String>,
        transform: LocalTransform,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        script: Option<Script>,
    },
    Light(MapLight),
    Camera(MapCamera),
    Mesh {
        mesh_name: String,
        #[serde(default)]
        material: Option<MapMaterial>,
        #[serde(default = "default_casts_shadow")]
        casts_shadow: bool,
        #[serde(default)]
        bounding_volume: Option<BoundingVolume>,
    },
    InstancedMesh {
        mesh_name: String,
        instances: Vec<MeshInstance>,
        #[serde(default)]
        material: Option<MapMaterial>,
        #[serde(default = "default_casts_shadow")]
        casts_shadow: bool,
        #[serde(default)]
        bounding_volume: Option<BoundingVolume>,
    },
    Lines(Vec<Line>),
    PrefabInstance {
        prefab_name: String,
    },
    Custom {
        type_name: String,
        data: serde_json::Value,
    },
}

fn default_casts_shadow() -> bool {
    true
}

impl MapNode {
    pub fn entity() -> Self {
        Self::Entity {
            name: None,
            transform: LocalTransform::default(),
            script: None,
        }
    }

    pub fn entity_named(name: impl Into<String>) -> Self {
        Self::Entity {
            name: Some(name.into()),
            transform: LocalTransform::default(),
            script: None,
        }
    }

    pub fn entity_with_transform(transform: LocalTransform) -> Self {
        Self::Entity {
            name: None,
            transform,
            script: None,
        }
    }

    pub fn entity_full(name: Option<String>, transform: LocalTransform) -> Self {
        Self::Entity {
            name,
            transform,
            script: None,
        }
    }

    pub fn entity_with_script(
        name: Option<String>,
        transform: LocalTransform,
        script: Script,
    ) -> Self {
        Self::Entity {
            name,
            transform,
            script: Some(script),
        }
    }

    pub fn light(light: MapLight) -> Self {
        Self::Light(light)
    }

    pub fn camera(camera: MapCamera) -> Self {
        Self::Camera(camera)
    }

    pub fn mesh(mesh_name: impl Into<String>) -> Self {
        Self::Mesh {
            mesh_name: mesh_name.into(),
            material: None,
            casts_shadow: true,
            bounding_volume: None,
        }
    }

    pub fn mesh_with_material(mesh_name: impl Into<String>, material: MapMaterial) -> Self {
        Self::Mesh {
            mesh_name: mesh_name.into(),
            material: Some(material),
            casts_shadow: true,
            bounding_volume: None,
        }
    }

    pub fn mesh_full(
        mesh_name: impl Into<String>,
        material: Option<MapMaterial>,
        casts_shadow: bool,
        bounding_volume: Option<BoundingVolume>,
    ) -> Self {
        Self::Mesh {
            mesh_name: mesh_name.into(),
            material,
            casts_shadow,
            bounding_volume,
        }
    }

    pub fn instanced_mesh(mesh_name: impl Into<String>, instances: Vec<MeshInstance>) -> Self {
        Self::InstancedMesh {
            mesh_name: mesh_name.into(),
            instances,
            material: None,
            casts_shadow: true,
            bounding_volume: None,
        }
    }

    pub fn instanced_mesh_with_material(
        mesh_name: impl Into<String>,
        instances: Vec<MeshInstance>,
        material: MapMaterial,
    ) -> Self {
        Self::InstancedMesh {
            mesh_name: mesh_name.into(),
            instances,
            material: Some(material),
            casts_shadow: true,
            bounding_volume: None,
        }
    }

    pub fn instanced_mesh_full(
        mesh_name: impl Into<String>,
        instances: Vec<MeshInstance>,
        material: Option<MapMaterial>,
        casts_shadow: bool,
        bounding_volume: Option<BoundingVolume>,
    ) -> Self {
        Self::InstancedMesh {
            mesh_name: mesh_name.into(),
            instances,
            material,
            casts_shadow,
            bounding_volume,
        }
    }

    pub fn lines(lines: Vec<Line>) -> Self {
        Self::Lines(lines)
    }

    pub fn prefab(prefab_name: impl Into<String>) -> Self {
        Self::PrefabInstance {
            prefab_name: prefab_name.into(),
        }
    }

    pub fn custom(type_name: impl Into<String>, data: serde_json::Value) -> Self {
        Self::Custom {
            type_name: type_name.into(),
            data,
        }
    }

    pub fn is_entity(&self) -> bool {
        matches!(self, MapNode::Entity { .. })
    }

    pub fn is_component(&self) -> bool {
        !self.is_entity()
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MeshInstance {
    pub translation: [f32; 3],
    pub rotation: [f32; 4],
    pub scale: [f32; 3],
    pub color: Option<[f32; 4]>,
}

impl MeshInstance {
    pub fn new(translation: [f32; 3]) -> Self {
        Self {
            translation,
            rotation: [0.0, 0.0, 0.0, 1.0],
            scale: [1.0, 1.0, 1.0],
            color: None,
        }
    }

    pub fn with_rotation(mut self, rotation: [f32; 4]) -> Self {
        self.rotation = rotation;
        self
    }

    pub fn with_scale(mut self, scale: [f32; 3]) -> Self {
        self.scale = scale;
        self
    }

    pub fn with_uniform_scale(mut self, scale: f32) -> Self {
        self.scale = [scale, scale, scale];
        self
    }

    pub fn with_color(mut self, color: [f32; 4]) -> Self {
        self.color = Some(color);
        self
    }

    pub fn to_local_transform(&self) -> LocalTransform {
        LocalTransform {
            translation: nalgebra_glm::Vec3::new(
                self.translation[0],
                self.translation[1],
                self.translation[2],
            ),
            rotation: nalgebra_glm::Quat::new(
                self.rotation[3],
                self.rotation[0],
                self.rotation[1],
                self.rotation[2],
            ),
            scale: nalgebra_glm::Vec3::new(self.scale[0], self.scale[1], self.scale[2]),
        }
    }
}

impl From<LocalTransform> for MeshInstance {
    fn from(transform: LocalTransform) -> Self {
        Self {
            translation: [
                transform.translation.x,
                transform.translation.y,
                transform.translation.z,
            ],
            rotation: [
                transform.rotation.i,
                transform.rotation.j,
                transform.rotation.k,
                transform.rotation.w,
            ],
            scale: [transform.scale.x, transform.scale.y, transform.scale.z],
            color: None,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum MapLight {
    Directional {
        color: [f32; 3],
        intensity: f32,
        cast_shadows: bool,
        shadow_bias: f32,
    },
    Point {
        color: [f32; 3],
        intensity: f32,
        range: f32,
        cast_shadows: bool,
        shadow_bias: f32,
    },
    Spot {
        color: [f32; 3],
        intensity: f32,
        range: f32,
        inner_cone_angle: f32,
        outer_cone_angle: f32,
        cast_shadows: bool,
        shadow_bias: f32,
    },
}

impl MapLight {
    pub fn directional(color: [f32; 3], intensity: f32) -> Self {
        Self::Directional {
            color,
            intensity,
            cast_shadows: true,
            shadow_bias: 0.005,
        }
    }

    pub fn point(color: [f32; 3], intensity: f32, range: f32) -> Self {
        Self::Point {
            color,
            intensity,
            range,
            cast_shadows: false,
            shadow_bias: 0.0,
        }
    }

    pub fn spot(
        color: [f32; 3],
        intensity: f32,
        range: f32,
        inner_cone_angle: f32,
        outer_cone_angle: f32,
    ) -> Self {
        Self::Spot {
            color,
            intensity,
            range,
            inner_cone_angle,
            outer_cone_angle,
            cast_shadows: false,
            shadow_bias: 0.0,
        }
    }

    pub fn to_light(&self) -> Light {
        match self {
            MapLight::Directional {
                color,
                intensity,
                cast_shadows,
                shadow_bias,
            } => Light {
                light_type: LightType::Directional,
                color: nalgebra_glm::Vec3::new(color[0], color[1], color[2]),
                intensity: *intensity,
                range: 0.0,
                inner_cone_angle: 0.0,
                outer_cone_angle: 0.0,
                cast_shadows: *cast_shadows,
                shadow_bias: *shadow_bias,
            },
            MapLight::Point {
                color,
                intensity,
                range,
                cast_shadows,
                shadow_bias,
            } => Light {
                light_type: LightType::Point,
                color: nalgebra_glm::Vec3::new(color[0], color[1], color[2]),
                intensity: *intensity,
                range: *range,
                inner_cone_angle: 0.0,
                outer_cone_angle: 0.0,
                cast_shadows: *cast_shadows,
                shadow_bias: *shadow_bias,
            },
            MapLight::Spot {
                color,
                intensity,
                range,
                inner_cone_angle,
                outer_cone_angle,
                cast_shadows,
                shadow_bias,
            } => Light {
                light_type: LightType::Spot,
                color: nalgebra_glm::Vec3::new(color[0], color[1], color[2]),
                intensity: *intensity,
                range: *range,
                inner_cone_angle: *inner_cone_angle,
                outer_cone_angle: *outer_cone_angle,
                cast_shadows: *cast_shadows,
                shadow_bias: *shadow_bias,
            },
        }
    }
}

impl From<&Light> for MapLight {
    fn from(light: &Light) -> Self {
        let color = [light.color.x, light.color.y, light.color.z];
        match light.light_type {
            LightType::Directional => MapLight::Directional {
                color,
                intensity: light.intensity,
                cast_shadows: light.cast_shadows,
                shadow_bias: light.shadow_bias,
            },
            LightType::Point => MapLight::Point {
                color,
                intensity: light.intensity,
                range: light.range,
                cast_shadows: light.cast_shadows,
                shadow_bias: light.shadow_bias,
            },
            LightType::Spot => MapLight::Spot {
                color,
                intensity: light.intensity,
                range: light.range,
                inner_cone_angle: light.inner_cone_angle,
                outer_cone_angle: light.outer_cone_angle,
                cast_shadows: light.cast_shadows,
                shadow_bias: light.shadow_bias,
            },
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum MapCamera {
    Perspective {
        aspect_ratio: Option<f32>,
        y_fov_rad: f32,
        z_far: Option<f32>,
        z_near: f32,
    },
    Orthographic {
        x_mag: f32,
        y_mag: f32,
        z_far: f32,
        z_near: f32,
    },
}

impl MapCamera {
    pub fn perspective(y_fov_rad: f32, z_near: f32) -> Self {
        Self::Perspective {
            aspect_ratio: None,
            y_fov_rad,
            z_far: None,
            z_near,
        }
    }

    pub fn orthographic(x_mag: f32, y_mag: f32, z_near: f32, z_far: f32) -> Self {
        Self::Orthographic {
            x_mag,
            y_mag,
            z_far,
            z_near,
        }
    }

    pub fn to_camera(&self) -> Camera {
        match self {
            MapCamera::Perspective {
                aspect_ratio,
                y_fov_rad,
                z_far,
                z_near,
            } => Camera {
                projection: Projection::Perspective(PerspectiveCamera {
                    aspect_ratio: *aspect_ratio,
                    y_fov_rad: *y_fov_rad,
                    z_far: *z_far,
                    z_near: *z_near,
                }),
                smoothing: None,
            },
            MapCamera::Orthographic {
                x_mag,
                y_mag,
                z_far,
                z_near,
            } => Camera {
                projection: Projection::Orthographic(OrthographicCamera {
                    x_mag: *x_mag,
                    y_mag: *y_mag,
                    z_far: *z_far,
                    z_near: *z_near,
                }),
                smoothing: None,
            },
        }
    }
}

impl From<&Camera> for MapCamera {
    fn from(camera: &Camera) -> Self {
        match &camera.projection {
            Projection::Perspective(perspective) => MapCamera::Perspective {
                aspect_ratio: perspective.aspect_ratio,
                y_fov_rad: perspective.y_fov_rad,
                z_far: perspective.z_far,
                z_near: perspective.z_near,
            },
            Projection::Orthographic(orthographic) => MapCamera::Orthographic {
                x_mag: orthographic.x_mag,
                y_mag: orthographic.y_mag,
                z_far: orthographic.z_far,
                z_near: orthographic.z_near,
            },
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MapMaterial {
    pub base_color: [f32; 4],
    pub emissive_factor: [f32; 3],
    #[serde(default)]
    pub alpha_mode: MapAlphaMode,
    #[serde(default = "default_alpha_cutoff")]
    pub alpha_cutoff: f32,
    pub base_texture: Option<String>,
    pub emissive_texture: Option<String>,
    #[serde(default = "default_roughness")]
    pub roughness: f32,
    #[serde(default)]
    pub metallic: f32,
    #[serde(default)]
    pub unlit: bool,
}

fn default_alpha_cutoff() -> f32 {
    0.5
}

fn default_roughness() -> f32 {
    0.5
}

#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq)]
pub enum MapAlphaMode {
    #[default]
    Opaque,
    Mask,
    Blend,
}

impl From<AlphaMode> for MapAlphaMode {
    fn from(mode: AlphaMode) -> Self {
        match mode {
            AlphaMode::Opaque => MapAlphaMode::Opaque,
            AlphaMode::Mask => MapAlphaMode::Mask,
            AlphaMode::Blend => MapAlphaMode::Blend,
        }
    }
}

impl From<MapAlphaMode> for AlphaMode {
    fn from(mode: MapAlphaMode) -> Self {
        match mode {
            MapAlphaMode::Opaque => AlphaMode::Opaque,
            MapAlphaMode::Mask => AlphaMode::Mask,
            MapAlphaMode::Blend => AlphaMode::Blend,
        }
    }
}

impl Default for MapMaterial {
    fn default() -> Self {
        Self {
            base_color: [1.0, 1.0, 1.0, 1.0],
            emissive_factor: [0.0, 0.0, 0.0],
            alpha_mode: MapAlphaMode::Opaque,
            alpha_cutoff: 0.5,
            base_texture: None,
            emissive_texture: None,
            roughness: 0.5,
            metallic: 0.0,
            unlit: false,
        }
    }
}

impl MapMaterial {
    pub fn to_material(&self) -> Material {
        Material {
            base_color: self.base_color,
            emissive_factor: self.emissive_factor,
            alpha_mode: self.alpha_mode.into(),
            alpha_cutoff: self.alpha_cutoff,
            base_texture: self.base_texture.clone(),
            emissive_texture: self.emissive_texture.clone(),
            roughness: self.roughness,
            metallic: self.metallic,
            unlit: self.unlit,
        }
    }
}

impl From<&Material> for MapMaterial {
    fn from(material: &Material) -> Self {
        Self {
            base_color: material.base_color,
            emissive_factor: material.emissive_factor,
            alpha_mode: material.alpha_mode.into(),
            alpha_cutoff: material.alpha_cutoff,
            base_texture: material.base_texture.clone(),
            emissive_texture: material.emissive_texture.clone(),
            roughness: material.roughness,
            metallic: material.metallic,
            unlit: material.unlit,
        }
    }
}
