use std::{
    collections::HashMap,
    io::Write,
    time::{SystemTime, UNIX_EPOCH},
};

use anyhow::Context;
use flate2::Compression;
use flate2::write::GzEncoder;
use lib::{BlockPalette, StructureExporter, VoxelType};
use serde::Serialize;

#[derive(Serialize)]
#[serde(rename_all = "PascalCase")]
pub struct Litematic {
    pub metadata: Metadata,
    pub minecraft_data_version: i32,
    pub version: i32,
    pub regions: std::collections::HashMap<String, Region>,
}

#[derive(Serialize)]
#[serde(rename_all = "PascalCase")]
pub struct Metadata {
    pub author: String,
    pub description: String,
    pub enclosing_size: Vector3,
    pub name: String,
    pub region_count: i32,
    pub time_created: i64,
    pub time_modified: i64,
    pub total_blocks: i32,
    pub total_volume: i32,
}

#[derive(Serialize)]
#[serde(rename_all = "PascalCase")]
pub struct Region {
    pub block_state_palette: Vec<BlockState>,
    pub block_states: fastnbt::LongArray,
    pub position: Vector3,
    pub size: Vector3,
}

#[derive(Serialize)]
#[serde(rename_all = "PascalCase")]
pub struct BlockState {
    pub name: String,
    // Optional properties like facing=north would go here,
}

#[derive(Serialize, Clone, Copy)]
pub struct Vector3 {
    pub x: i32,
    pub y: i32,
    pub z: i32,
}

pub struct LitematicaExporter {
    palette_map: HashMap<VoxelType, String>,
}

impl LitematicaExporter {
    pub fn new(palette: &BlockPalette) -> Self {
        let mut palette_map = HashMap::new();
        palette_map.insert(VoxelType::Body, palette.resolve(&VoxelType::Body));
        palette_map.insert(VoxelType::Outline, palette.resolve(&VoxelType::Outline));
        palette_map.insert(VoxelType::Shadow, palette.resolve(&VoxelType::Shadow));
        Self { palette_map }
    }

    fn pack_bits(indicies: &[usize], bits_per_entry: usize) -> Vec<i64> {
        let entries_per_long = 64 / bits_per_entry;
        let mut packed = Vec::new();
        let mut current_long: i64 = 0;
        let mut current_idx = 0;

        for &index in indicies {
            current_long |= (index as i64) << (current_idx * bits_per_entry);
            current_idx += 1;

            if current_idx >= entries_per_long {
                packed.push(current_long as i64);
                current_long = 0;
                current_idx = 0;
            }
        }

        if current_idx > 0 {
            packed.push(current_long as i64);
        }

        packed
    }
}

impl StructureExporter for LitematicaExporter {
    fn export(&self, grid: &lib::VoxelGrid) -> anyhow::Result<Vec<u8>> {
        let mut palette_list = vec![BlockState {
            name: "minecraft:air".to_string(),
        }];
        let mut type_to_index = HashMap::new();

        for (voxel_type, block_id) in &self.palette_map {
            type_to_index.insert(*voxel_type, palette_list.len());
            palette_list.push(BlockState {
                name: block_id.clone(),
            });
        }

        let bits_per_entry =
            std::cmp::max(2, (palette_list.len() as f64 - 1.0).log2().ceil() as usize);

        let mut indices = Vec::with_capacity((grid.width * grid.height * grid.depth) as usize);
        let mut total_blocks = 0;

        for y in 0..grid.height {
            for z in 0..grid.depth {
                for x in 0..grid.width {
                    if let Some(voxel) = grid.get(x, y, z) {
                        indices.push(*type_to_index.get(&voxel).unwrap());
                        total_blocks += 1;
                    } else {
                        indices.push(0); // Index 0 is Air
                    }
                }
            }
        }

        let packed_states = Self::pack_bits(&indices, bits_per_entry);

        let timestamp = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_millis() as i64;
        let dimensions = Vector3 {
            x: grid.width as i32,
            y: grid.height as i32,
            z: grid.depth as i32,
        };

        let mut regions = HashMap::new();
        regions.insert(
            "TextRegion".to_string(),
            Region {
                block_state_palette: palette_list,
                block_states: fastnbt::LongArray::new(packed_states),
                position: Vector3 { x: 0, y: 0, z: 0 },
                size: dimensions,
            },
        );

        let litematic = Litematic {
            minecraft_data_version: 3465, // 1.20+ Data Version
            version: 6,                   // Litematica format version
            metadata: Metadata {
                author: "Minecraft Text Builder".to_string(),
                description: "Generated 3D Text".to_string(),
                name: "Text_Structure".to_string(),
                enclosing_size: dimensions,
                region_count: 1,
                total_blocks,
                total_volume: (grid.width * grid.height * grid.depth) as i32,
                time_created: timestamp,
                time_modified: timestamp,
            },
            regions,
        };

        let mut encoder = GzEncoder::new(Vec::new(), Compression::default());

        let nbt_bytes = fastnbt::to_bytes(&litematic).context("NBT serialization failed")?;

        encoder.write_all(&nbt_bytes)?;

        Ok(encoder.finish()?)
    }

    fn file_extension(&self) -> &'static str {
        "litematic"
    }
}
