use std::collections::HashMap;

use freecs::Entity;
use serde::{Deserialize, Deserializer, Serialize, Serializer};

use super::asset_uuid::AssetUuid;
use super::components::Scene;
use crate::ecs::world::World;

/// Field-type tag the editor uses to pick an inspector widget per
/// registered typed component. Keep additive: appending new variants
/// is fine, renaming or reordering breaks already-registered
/// descriptors.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum FieldKind {
    Bool,
    I32,
    U32,
    F32,
    String,
    Vec3,
    Enum { variants: Vec<String> },
    Color,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FieldDescriptor {
    pub name: String,
    pub kind: FieldKind,
    #[serde(default)]
    pub display_name: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComponentDescriptor {
    pub name: String,
    #[serde(default)]
    pub display_name: Option<String>,
    pub fields: Vec<FieldDescriptor>,
    #[serde(default)]
    pub persistence: Persistence,
}

/// Engine-side registry of typed-component schemas. Apps register
/// descriptors at startup via `register_component_descriptor`; the
/// editor reads them to render inspector UI for entities whose
/// `SceneEntity::components::game_components` contain matching
/// `TypedComponent` entries.
#[derive(Debug, Clone, Default)]
pub struct ComponentTypeRegistry {
    pub descriptors: HashMap<String, ComponentDescriptor>,
}

pub fn register_component_descriptor(world: &mut World, descriptor: ComponentDescriptor) {
    world
        .resources
        .component_types
        .descriptors
        .insert(descriptor.name.clone(), descriptor);
}

pub fn component_descriptor<'a>(world: &'a World, name: &str) -> Option<&'a ComponentDescriptor> {
    world.resources.component_types.descriptors.get(name)
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum Persistence {
    Authored,
    Runtime,
    #[default]
    Both,
}

/// JSON payload that round-trips through both human-readable
/// (`serde_json`) and binary (`bincode`) scene formats. Bincode can't
/// drive `serde_json::Value::deserialize` because it relies on
/// `deserialize_any`, which non-self-describing formats reject. In
/// binary mode we encode the value as a JSON-formatted string and
/// re-parse on load; in human-readable mode we serialize the value
/// directly so the scene file stays diff-friendly.
#[derive(Debug, Clone, PartialEq)]
pub struct TypedPayload(pub serde_json::Value);

impl Default for TypedPayload {
    fn default() -> Self {
        Self(serde_json::Value::Null)
    }
}

impl Serialize for TypedPayload {
    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
        if serializer.is_human_readable() {
            self.0.serialize(serializer)
        } else {
            let encoded = serde_json::to_string(&self.0).map_err(serde::ser::Error::custom)?;
            encoded.serialize(serializer)
        }
    }
}

impl<'de> Deserialize<'de> for TypedPayload {
    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
        if deserializer.is_human_readable() {
            serde_json::Value::deserialize(deserializer).map(TypedPayload)
        } else {
            let encoded = String::deserialize(deserializer)?;
            serde_json::from_str(&encoded)
                .map(TypedPayload)
                .map_err(serde::de::Error::custom)
        }
    }
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TypedComponent {
    pub name: String,
    pub data: TypedPayload,
    #[serde(default)]
    pub persistence: Persistence,
    /// Field-level deltas that override individual values inside
    /// `data` after deserialization. `Persistence::Both` components
    /// keep their authored payload in `data` and layer runtime
    /// changes here, so a save file can carry "the door is now open"
    /// without re-emitting the rest of the authored door state.
    /// Reuses the same `FieldOverride` shape as
    /// `PrefabOverrides::component_overrides`.
    #[serde(default)]
    pub field_overrides: Vec<crate::ecs::prefab::components::FieldOverride>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TypedResource {
    pub name: String,
    pub data: TypedPayload,
    #[serde(default)]
    pub persistence: Persistence,
}

/// Returns a fresh `serde_json::Value` obtained by overlaying the
/// `FieldOverride`s onto `base`. Object fields named by an override
/// are replaced; other fields fall through unchanged. Non-object
/// base values pass through untouched. Use this on the receiving
/// side of a `Persistence::Both` typed component to reconstruct the
/// final payload from authored data plus runtime deltas.
pub fn apply_field_overrides(
    base: &serde_json::Value,
    overrides: &[crate::ecs::prefab::components::FieldOverride],
) -> serde_json::Value {
    let mut merged = base.clone();
    if let serde_json::Value::Object(map) = &mut merged {
        for entry in overrides {
            map.insert(entry.field_name.clone(), entry.value.0.clone());
        }
    }
    merged
}

/// Game-side hook invoked at scene save. Implementors iterate their
/// world(s), match entities to the engine entities listed in
/// `entity_uuids`, and append typed payloads to the matching
/// `SceneEntity::components::game_components` and/or
/// `Scene::game_resources`.
pub trait SceneSerializer {
    fn populate_scene(
        &self,
        world: &World,
        entity_uuids: &HashMap<Entity, AssetUuid>,
        scene: &mut Scene,
    );
}

/// Game-side hook invoked at scene spawn. Implementors walk
/// `scene.entities`, dispatch on each `TypedComponent::name`, and
/// write the decoded payload into whichever world the game uses.
/// `uuid_to_entity` maps scene-file uuids onto the freshly-spawned
/// engine entities so handlers can find their counterparts.
pub trait SceneDeserializer {
    fn populate_world(
        &mut self,
        world: &mut World,
        uuid_to_entity: &HashMap<AssetUuid, Entity>,
        scene: &Scene,
    );
}
