use std::collections::HashMap;
use std::fs::File;
use std::io::{BufReader, BufWriter};
use std::path::Path;

use freecs::Entity;

use crate::ecs::name::components::Name;
use crate::ecs::transform::components::Parent;
use crate::ecs::world::{
    NAME, PARENT, RENDER_LAYER, SPRITE, SPRITE_ANIMATOR, SPRITE_PARTICLE_EMITTER, TILEMAP, TWEEN,
    VISIBILITY, World,
};

use super::components::{Scene2D, SceneEntity2D};

#[derive(Debug, thiserror::Error)]
pub enum Scene2DError {
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),

    #[error("JSON error: {0}")]
    Json(#[from] serde_json::Error),

    #[error("Cyclic parent reference detected")]
    CyclicReference,

    #[error("Parent not found: scene entity {0} references parent {1}")]
    ParentNotFound(u32, u32),
}

pub fn save_scene_2d(scene: &Scene2D, path: &Path) -> Result<(), Scene2DError> {
    let file = File::create(path)?;
    let writer = BufWriter::new(file);
    serde_json::to_writer_pretty(writer, scene)?;
    Ok(())
}

pub fn load_scene_2d(path: &Path) -> Result<Scene2D, Scene2DError> {
    let file = File::open(path)?;
    let reader = BufReader::new(file);
    let scene: Scene2D = serde_json::from_reader(reader)?;
    Ok(scene)
}

pub fn spawn_scene_2d(
    world: &mut World,
    scene: &Scene2D,
) -> Result<HashMap<u32, Entity>, Scene2DError> {
    let spawn_order = compute_spawn_order_2d(&scene.entities)?;
    let mut scene_id_to_entity: HashMap<u32, Entity> = HashMap::new();

    for scene_entity_id in spawn_order {
        let scene_entity = scene
            .find_entity(scene_entity_id)
            .expect("spawn order references valid entity id");

        let parent_entity = scene_entity
            .parent_id
            .map(|parent_id| {
                scene_id_to_entity
                    .get(&parent_id)
                    .copied()
                    .ok_or(Scene2DError::ParentNotFound(scene_entity.id, parent_id))
            })
            .transpose()?;

        let mut component_mask = 0u64;

        if scene_entity.sprite.is_some() {
            component_mask |= SPRITE;
        }
        if scene_entity.tilemap.is_some() {
            component_mask |= TILEMAP;
        }
        if scene_entity.sprite_animator.is_some() {
            component_mask |= SPRITE_ANIMATOR;
        }
        if scene_entity.tween.is_some() {
            component_mask |= TWEEN;
        }
        if scene_entity.sprite_particle_emitter.is_some() {
            component_mask |= SPRITE_PARTICLE_EMITTER;
        }
        if scene_entity.visibility.is_some() {
            component_mask |= VISIBILITY;
        }
        if scene_entity.render_layer.is_some() {
            component_mask |= RENDER_LAYER;
        }
        if scene_entity.name.is_some() {
            component_mask |= NAME;
        }
        if parent_entity.is_some() {
            component_mask |= PARENT;
        }

        let entity = world.spawn_entities(component_mask, 1)[0];

        if let Some(sprite) = &scene_entity.sprite {
            world.set_sprite(entity, sprite.clone());
        }
        if let Some(tilemap) = &scene_entity.tilemap {
            world.set_tilemap(entity, tilemap.clone());
        }
        if let Some(sprite_animator) = &scene_entity.sprite_animator {
            world.set_sprite_animator(entity, sprite_animator.clone());
        }
        if let Some(tween) = &scene_entity.tween {
            world.set_tween(entity, tween.clone());
        }
        if let Some(emitter) = &scene_entity.sprite_particle_emitter {
            world.set_sprite_particle_emitter(entity, emitter.clone());
        }
        if let Some(visibility) = &scene_entity.visibility {
            world.set_visibility(entity, visibility.clone());
        }
        if let Some(render_layer) = &scene_entity.render_layer {
            world.set_render_layer(entity, *render_layer);
        }
        if let Some(name) = &scene_entity.name {
            world.set_name(entity, Name(name.clone()));
            world.resources.entity_names.insert(name.clone(), entity);
        }
        if let Some(parent) = parent_entity {
            world.set_parent(entity, Parent(Some(parent)));
            world
                .resources
                .children_cache
                .entry(parent)
                .or_default()
                .push(entity);
        }

        scene_id_to_entity.insert(scene_entity.id, entity);
    }

    Ok(scene_id_to_entity)
}

pub fn world_to_scene_2d(world: &World, name: Option<&str>) -> Scene2D {
    let mut scene = Scene2D::new(name);
    let mut entity_to_id: HashMap<Entity, u32> = HashMap::new();
    let mut next_id: u32 = 0;

    let sprite_entities: Vec<Entity> = world.query_entities(SPRITE).collect();
    let tilemap_entities: Vec<Entity> = world.query_entities(TILEMAP).collect();
    let emitter_entities: Vec<Entity> = world.query_entities(SPRITE_PARTICLE_EMITTER).collect();

    let mut all_entities: Vec<Entity> = Vec::new();
    for entity in sprite_entities
        .iter()
        .chain(tilemap_entities.iter())
        .chain(emitter_entities.iter())
    {
        if !entity_to_id.contains_key(entity) {
            entity_to_id.insert(*entity, next_id);
            all_entities.push(*entity);
            next_id += 1;
        }
    }

    for entity in &all_entities {
        let entity = *entity;
        let id = entity_to_id[&entity];

        let parent_id = world
            .get_parent(entity)
            .and_then(|parent| parent.0)
            .and_then(|parent_entity| entity_to_id.get(&parent_entity).copied());

        let name = world.get_name(entity).map(|name| name.0.clone());

        let sprite = world.get_sprite(entity).cloned();
        let tilemap = world.get_tilemap(entity).cloned();
        let sprite_animator = world.get_sprite_animator(entity).cloned();
        let tween = world.get_tween(entity).cloned();
        let sprite_particle_emitter = world.get_sprite_particle_emitter(entity).cloned();
        let visibility = world.get_visibility(entity).cloned();
        let render_layer = world.get_render_layer(entity).copied();

        let scene_entity = SceneEntity2D {
            id,
            parent_id,
            name,
            sprite,
            tilemap,
            sprite_animator,
            tween,
            sprite_particle_emitter,
            visibility,
            render_layer,
        };

        scene.add_entity(scene_entity);
    }

    scene
}

fn compute_spawn_order_2d(entities: &[SceneEntity2D]) -> Result<Vec<u32>, Scene2DError> {
    let id_set: std::collections::HashSet<u32> = entities.iter().map(|entity| entity.id).collect();

    let mut result: Vec<u32> = Vec::with_capacity(entities.len());
    let mut visited: std::collections::HashSet<u32> = std::collections::HashSet::new();
    let mut in_stack: std::collections::HashSet<u32> = std::collections::HashSet::new();

    let entity_map: HashMap<u32, &SceneEntity2D> =
        entities.iter().map(|entity| (entity.id, entity)).collect();

    fn visit(
        id: u32,
        entity_map: &HashMap<u32, &SceneEntity2D>,
        id_set: &std::collections::HashSet<u32>,
        visited: &mut std::collections::HashSet<u32>,
        in_stack: &mut std::collections::HashSet<u32>,
        result: &mut Vec<u32>,
    ) -> Result<(), Scene2DError> {
        if visited.contains(&id) {
            return Ok(());
        }

        if in_stack.contains(&id) {
            return Err(Scene2DError::CyclicReference);
        }

        in_stack.insert(id);

        if let Some(entity) = entity_map.get(&id)
            && let Some(parent_id) = entity.parent_id
            && id_set.contains(&parent_id)
        {
            visit(parent_id, entity_map, id_set, visited, in_stack, result)?;
        }

        in_stack.remove(&id);
        visited.insert(id);
        result.push(id);

        Ok(())
    }

    for entity in entities {
        visit(
            entity.id,
            &entity_map,
            &id_set,
            &mut visited,
            &mut in_stack,
            &mut result,
        )?;
    }

    Ok(result)
}
