use heck::{ToShoutySnakeCase, ToUpperCamelCase};
use proc_macro2::{Span, TokenStream};
use pumpkin_nbt::deserializer::{NbtReadHelper, NbtReadHelperBedrock};
use pumpkin_util::math::{experience::Experience, vector3::Vector3};
use quote::{ToTokens, format_ident, quote};
use serde::Deserialize;
use std::{
    collections::{BTreeMap, HashSet},
    fs,
    io::{Cursor, Read, Seek, SeekFrom},
    panic,
};
use syn::{Ident, LitInt, LitStr};

use crate::{
    bitsets::{Bitset, gen_u16_bitset},
    loot::LootTableStruct,
};

/// Converts a sparse index-value list into a dense array, filling gaps with `None` tokens.
///
/// # Arguments
/// – `array` – pairs of `(index, value)` where `index` is the position in the output array.
fn fill_array<T: Clone + ToTokens>(array: Vec<(u16, T)>) -> Vec<TokenStream> {
    let max_index = array.iter().map(|(index, _)| index).max().unwrap();
    let mut raw_id_from_state_id_ordered = vec![quote! { None }; (max_index + 1) as usize];

    for (state_id, id_lit) in array {
        raw_id_from_state_id_ordered[state_id as usize] = quote! { #id_lit };
    }

    raw_id_from_state_id_ordered
}

/// Converts a sparse `(block_ident, state_index, state_id)` list into a dense `TokenStream` array.
///
/// # Arguments
/// – `array` – triples of `(block_ident, state_index, state_id)` mapping each state ID to the block constant and local index.
fn fill_state_array(array: Vec<(Ident, usize, u16)>) -> Vec<TokenStream> {
    let max_index = array.iter().map(|(_, _, index)| index).max().unwrap();
    let mut ret = vec![quote! { Missed State }; (max_index + 1) as usize];

    for (block, index, state_id) in array {
        let index_lit = LitInt::new(&index.to_string(), Span::call_site());
        ret[state_id as usize] = quote! { &Block::#block.states[#index_lit] };
    }

    ret
}

/// Converts a block registry name into the SCREAMING_SNAKE_CASE constant identifier.
///
/// # Arguments
/// – `block` – the lowercase registry name (e.g., `"stone_slab"`).
fn const_block_name_from_block_name(block: &str) -> String {
    block.to_shouty_snake_case()
}

/// Derives the UpperCamelCase struct name for a block-property group from a derived base name.
///
/// # Arguments
/// – `name` – a base name such as `"oak_slab_like"` which is converted to e.g., `"OakSlabLikeProperties"`.
fn property_group_name_from_derived_name(name: &str) -> String {
    format!("{name}_properties").to_upper_camel_case()
}

/// Discriminates between the two runtime representations of a block property.
enum PropertyType {
    /// The property is a simple boolean (`true`/`false`).
    Bool,
    /// The property is an integer with an inclusive range.
    Int { min: u8, max: u8 },
    /// The property is an enum with a generated Rust type identified by `name`.
    Enum { name: String },
}

/// Maps a single block-property field to its Rust type representation.
struct PropertyVariantMapping {
    /// The serialized property name as it appears in JSON (e.g., `"facing"`).
    original_name: String,
    /// The Rust type used to represent this property in the generated code.
    property_type: PropertyType,
}

/// Accumulated data for a group of blocks that share the same set of block properties.
struct PropertyCollectionData {
    /// The ordered list of property-to-type mappings that form the generated struct fields.
    variant_mappings: Vec<PropertyVariantMapping>,
    /// All blocks (by name and numeric ID) that belong to this property group.
    blocks: Vec<(String, u16)>,
}

impl PropertyCollectionData {
    /// Registers an additional block that shares this property group.
    ///
    /// # Arguments
    /// – `block_name` – the registry name of the block.
    /// – `block_id` – the numeric block ID.
    pub fn add_block(&mut self, block_name: String, block_id: u16) {
        self.blocks.push((block_name, block_id));
    }

    /// Creates a new `PropertyCollectionData` from an ordered list of property mappings with no blocks yet registered.
    ///
    /// # Arguments
    /// – `variant_mappings` – the property-to-type mappings for this group.
    pub const fn from_mappings(variant_mappings: Vec<PropertyVariantMapping>) -> Self {
        Self {
            variant_mappings,
            blocks: Vec::new(),
        }
    }

    /// Derives the base name for this property group from the first registered block.
    pub fn derive_name(&self) -> String {
        format!("{}_like", self.blocks[0].0)
    }
}

/// Deserialized representation of a single block property enum, used to emit the Rust enum definition.
#[derive(Deserialize, Clone, Debug)]
pub struct PropertyStruct {
    /// The UpperCamelCase name used for the generated Rust enum (e.g., `"Facing"`).
    pub name: String,
    /// The ordered list of variant value strings (e.g., `["north", "south", ...]`).
    pub values: Vec<String>,
}

impl ToTokens for PropertyStruct {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        if self.values[0] == "true" && self.values[1] == "false" {
            return;
        }

        let name = Ident::new(&self.name, Span::call_site());
        let count = self.values.len();
        let variant_count = count as u16;

        let is_number_values = !self.values.is_empty()
            && self.values.iter().all(|v| v.starts_with('L'))
            && self.values.iter().any(|v| v == "L1");

        let mut variants = Vec::with_capacity(count);
        let mut literals = Vec::with_capacity(count);
        let mut indices = Vec::with_capacity(count);

        for (i, raw_value) in self.values.iter().enumerate() {
            let ident = Ident::new(&raw_value.to_upper_camel_case(), Span::call_site());

            let literal_str = if is_number_values {
                raw_value.strip_prefix('L').unwrap_or(raw_value)
            } else {
                raw_value.as_str()
            };

            variants.push(ident);
            literals.push(literal_str);
            indices.push(i as u16);
        }
        tokens.extend(quote! {
            #[derive(Clone, Copy, Debug, Eq, PartialEq)]
            pub enum #name {
                #(#variants),*
            }

            impl EnumVariants for #name {
                fn variant_count() -> u16 {
                    #variant_count
                }

                fn to_index(&self) -> u16 {
                    match self {
                        #(Self::#variants => #indices),*
                    }
                }

                fn from_index(index: u16) -> Self {
                    match index {
                        #(#indices => Self::#variants,)*
                        _ => panic!("Invalid index: {index}"),
                    }
                }

                fn to_value(&self) -> &'static str {
                    match self {
                        #(Self::#variants => #literals),*
                    }
                }

                fn from_value(value: &str) -> Self {
                    match value {
                        #(#literals => Self::#variants),*,
                        _ => panic!("Invalid value: {value}"),
                    }
                }
            }
        });
    }
}

/// Code-generation wrapper that emits the full `BlockProperties` impl for one property group.
struct BlockPropertyStruct {
    /// The property group data used to generate the struct and its trait implementation.
    data: PropertyCollectionData,
}

impl ToTokens for BlockPropertyStruct {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        let struct_name = property_group_name_from_derived_name(&self.data.derive_name());
        let name = Ident::new(&struct_name, Span::call_site());

        // Strukturfelder generieren
        let fields = self.data.variant_mappings.iter().map(|entry| {
            let key = Ident::new_raw(&entry.original_name, Span::call_site());
            match &entry.property_type {
                PropertyType::Bool => quote! { pub #key: bool },
                PropertyType::Int { .. } => quote! { pub #key: u8 },
                PropertyType::Enum { name } => {
                    let value = Ident::new(name, Span::call_site());
                    quote! { pub #key: #value }
                }
            }
        });

        let block_ids = self
            .data
            .blocks
            .iter()
            .map(|(_, id)| *id)
            .collect::<Vec<_>>();

        let to_index_logic = self.data.variant_mappings.iter().rev().map(|entry| {
            let field = Ident::new_raw(&entry.original_name, Span::call_site());
            match &entry.property_type {
                PropertyType::Bool => quote! { (!self.#field as u16, 2) },
                PropertyType::Int { min, max } => {
                    let count = (max - min + 1) as u16;

                    if *min > 0 {
                        quote! { ((self.#field - #min) as u16, #count) }
                    } else {
                        quote! { (self.#field as u16, #count) }
                    }
                }
                PropertyType::Enum { name } => {
                    let ty = Ident::new(name, Span::call_site());
                    quote! { (self.#field.to_index(), #ty::variant_count()) }
                }
            }
        });

        let from_index_body = self
            .data
            .variant_mappings
            .iter()
            .rev()
            .map(|entry| {
                let field_name = Ident::new_raw(&entry.original_name, Span::call_site());
                match &entry.property_type {
                    PropertyType::Bool => quote! {
                        #field_name: {
                            let value = index % 2;
                            index /= 2;
                            value == 0
                        }
                    },
                    PropertyType::Int { min, max } => {
                        let count = (max - min + 1) as u16;
                        let val = if *min > 0 {
                            quote! { value + #min }
                        } else {
                            quote! {value}
                        };
                        quote! {
                            #field_name: {
                                let value = (index % #count) as u8;
                                index /= #count;
                                #val
                            }
                        }
                    }
                    PropertyType::Enum { name } => {
                        let enum_ident = Ident::new(name, Span::call_site());
                        quote! {
                            #field_name: {
                                let value = index % #enum_ident::variant_count();
                                index /= #enum_ident::variant_count();
                                #enum_ident::from_index(value)
                            }
                        }
                    }
                }
            })
            .collect::<Vec<_>>();

        let to_props_entries = self.data.variant_mappings.iter().map(|entry| {
            let key_str = &entry.original_name;
            let field = Ident::new_raw(&entry.original_name, Span::call_site());
            match &entry.property_type {
                PropertyType::Bool => quote! {
                    (#key_str, if self.#field { "true" } else { "false" })
                },
                PropertyType::Int { min, max } => {
                    let mut arms = Vec::new();
                    for i in *min..=*max {
                        let i_str = i.to_string();
                        arms.push(quote! { #i => #i_str });
                    }
                    quote! {
                        (#key_str, match self.#field {
                            #(#arms,)*
                            _ => unreachable!()
                        })
                    }
                }
                PropertyType::Enum { .. } => quote! {
                    (#key_str, self.#field.to_value())
                },
            }
        });

        let from_props_keys = self
            .data
            .variant_mappings
            .iter()
            .map(|entry| &entry.original_name);
        let from_props_values = self.data.variant_mappings.iter().map(|entry| {
            let field_name = Ident::new_raw(&entry.original_name, Span::call_site());
            match &entry.property_type {
                PropertyType::Bool => quote! {
                    block_props.#field_name = matches!(*value, "true")
                },
                PropertyType::Int { min, max } => {
                    let mut arms = Vec::new();
                    for i in *min..=*max {
                        let i_str = i.to_string();
                        arms.push(quote! { #i_str => #i });
                    }
                    quote! {
                        block_props.#field_name = match *value {
                            #(#arms,)*
                            _ => #min,
                        }
                    }
                }
                PropertyType::Enum { name } => {
                    let enum_ident = Ident::new(name, Span::call_site());
                    quote! {
                        block_props.#field_name = #enum_ident::from_value(value)
                    }
                }
            }
        });

        let from_props_loop_body = if self.data.variant_mappings.len() > 1 {
            quote! {
                match *key {
                    #(#from_props_keys => #from_props_values),*,
                    _ => {}, //
                }
            }
        } else {
            let key = from_props_keys.into_iter().next();
            let val = from_props_values.into_iter().next();
            quote! { if *key == #key { #val } }
        };

        tokens.extend(quote! {
            #[derive(Clone, Copy, Eq, PartialEq)]
            pub struct #name {
                #(#fields),*
            }

            impl BlockProperties for #name {
               fn to_index(&self) -> u16 {
                    let (index, _) = [#(#to_index_logic),*]
                        .iter()
                        .fold((0, 1), |(curr, mul), &(val, count)| (curr + val * mul, mul * count));
                    index
                }

                #[allow(unused_assignments)]
                fn from_index(mut index: u16) -> Self {
                    Self {
                        #(#from_index_body),*
                    }
                }

                #[inline]
                #[allow(clippy::manual_range_patterns)]
                fn handles_block_id(block_id: BlockId) -> bool where Self: Sized {
                    matches!(block_id.as_u16(), #(#block_ids)|*)
                }

                fn to_state_id(&self, block: &Block) -> BlockStateId {
                    if !Self::handles_block_id(block.id) {
                        panic!("{} is not a valid block for {}", block.name, #struct_name);
                    }
                    block.states[self.to_index() as usize].id
                }

                fn from_state_id(id: BlockStateId, block: &Block) -> Self {
                    debug_assert!(
                        Self::handles_block_id(block.id),
                        "{} is not a valid block for {}", block.name, #struct_name
                    );

                    let min_id = block.states[0].id.as_u16();
                    let max_id = block.states.last().map(|s| s.id.as_u16()).unwrap_or(min_id);

                    if (min_id..=max_id).contains(&id.as_u16()) {
                        Self::from_index(id.as_u16() - min_id)
                    } else {
                        #[cfg(debug_assertions)]
                        panic!("State ID {} does not exist for {}", id, block.name);

                        #[cfg(not(debug_assertions))]
                        Self::from_index(0)
                    }
                }

                fn default(block: &Block) -> Self {
                    if !Self::handles_block_id(block.id) {
                        panic!("{} is not a valid block for {}", block.name, #struct_name);
                    }
                    Self::from_state_id(block.default_state.id, block)
                }

                fn to_props(&self) -> Vec<(&'static str, &'static str)> {
                   vec![ #(#to_props_entries),* ]
                }

                #[allow(clippy::manual_range_patterns)]
                fn from_props(props: &[(&str, &str)], block: &Block) -> Self {
                    #[cfg(debug_assertions)]
                    if !matches!(block.id.as_u16(), #(#block_ids)|*) {
                        panic!("{} is not a valid block for {}", block.name, #struct_name);
                    }
                    let mut block_props = Self::default(block);
                    for (key, value) in props {
                        #from_props_loop_body
                    }
                    block_props
                }
            }
        });
    }
}

/// Deserialized flammability data for a block.
#[derive(Deserialize)]
pub struct FlammableStruct {
    /// Chance (0–300) that fire spreads to neighbouring blocks.
    pub spread_chance: u8,
    /// Chance (0–300) that the block itself burns away.
    pub burn_chance: u8,
}

impl ToTokens for FlammableStruct {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        let spread_chance = &self.spread_chance;
        let burn_chance = &self.burn_chance;

        tokens.extend(quote! {
            Flammable {
                spread_chance: #spread_chance,
                burn_chance: #burn_chance,
            }
        });
    }
}

/// Axis-aligned bounding box used to describe block collision and outline shapes.
#[derive(Deserialize, Clone, Copy)]
pub struct BoundingBox {
    /// The minimum corner of the bounding box.
    pub min: Vector3<f64>,
    /// The maximum corner of the bounding box.
    pub max: Vector3<f64>,
}

impl ToTokens for BoundingBox {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        let min_x = &self.min.x;
        let min_y = &self.min.y;
        let min_z = &self.min.z;

        let max_x = &self.max.x;
        let max_y = &self.max.y;
        let max_z = &self.max.z;

        tokens.extend(quote! {
            BoundingBox {
                min: Vector3::new(#min_x, #min_y, #min_z),
                max: Vector3::new(#max_x, #max_y, #max_z),
            }
        });
    }
}

#[derive(Deserialize, Copy, Clone, PartialEq, Eq)]
pub struct BlockStateId(pub u16);

impl ToTokens for BlockStateId {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        let inner = self.0;
        tokens.extend(quote! { BlockStateId::new(#inner).unwrap() });
    }
}

/// Deserialized representation of a single block state as stored in `blocks.json`.
#[derive(Deserialize, Clone)]
pub struct BlockState {
    /// Globally unique numeric ID for this block state.
    pub id: BlockStateId,
    /// Bitfield encoding boolean state properties (air, random ticks, etc.).
    pub state_flags: u16,
    /// Bitfield encoding which sides of the block are solid.
    pub side_flags: u8,
    /// Name of the note-block instrument for this state. TODO: make this an enum
    pub instrument: String,
    /// Light level emitted by this state (0–15).
    pub luminance: u8,
    /// How pistons interact with this block state.
    pub piston_behavior: PistonBehavior,
    /// Mining hardness of this block state.
    pub hardness: f32,
    /// Indices into the global shapes array for collision shape segments.
    pub collision_shapes: Vec<u16>,
    /// Indices into the global shapes array for outline (selection) shape segments.
    pub outline_shapes: Vec<u16>,
    /// Opacity value for light propagation, if non-full.
    pub opacity: Option<u8>,
    /// Associated block-entity type ID, if any.
    pub block_entity_type: Option<u16>,
}

/// Describes how a piston interacts with a block.
#[derive(Deserialize, Clone, Debug)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum PistonBehavior {
    /// The block can be pushed and pulled normally.
    Normal,
    /// The block is destroyed when pushed.
    Destroy,
    /// The block prevents piston movement.
    Block,
    /// The piston ignores the block entirely.
    Ignore,
    /// The block can only be pushed, not pulled.
    PushOnly,
}

impl PistonBehavior {
    fn to_tokens(&self) -> TokenStream {
        match self {
            Self::Normal => quote! { PistonBehavior::Normal },
            Self::Destroy => quote! { PistonBehavior::Destroy },
            Self::Block => quote! { PistonBehavior::Block },
            Self::Ignore => quote! { PistonBehavior::Ignore },
            Self::PushOnly => quote! { PistonBehavior::PushOnly },
        }
    }
}

impl BlockState {
    /// Bit flag indicating this state is an air block.
    const IS_AIR: u16 = 1 << 0;

    const IS_LIQUID: u16 = 1 << 5;

    /// Bit flag indicating this state receives random tick events.
    const HAS_RANDOM_TICKS: u16 = 1 << 9;

    /// Returns `true` if this state receives random tick events.
    const fn has_random_ticks(&self) -> bool {
        self.state_flags & Self::HAS_RANDOM_TICKS != 0
    }

    /// Returns `true` if this state is an air variant.
    pub const fn is_air(&self) -> bool {
        self.state_flags & Self::IS_AIR != 0
    }

    pub const fn is_liquid(&self) -> bool {
        self.state_flags & Self::IS_LIQUID != 0
    }

    /// Emits the `BlockState { … }` struct literal token stream for code generation.
    fn to_tokens(&self) -> TokenStream {
        let mut tokens = TokenStream::new();
        let id = self.id;
        let state_flags = LitInt::new(&self.state_flags.to_string(), Span::call_site());
        let side_flags = LitInt::new(&self.side_flags.to_string(), Span::call_site());
        let instrument = format_ident!("{}", self.instrument.to_upper_camel_case());
        let luminance = LitInt::new(&self.luminance.to_string(), Span::call_site());
        let hardness = self.hardness;
        let opacity = if let Some(opacity) = self.opacity {
            let opacity = LitInt::new(&opacity.to_string(), Span::call_site());
            quote! { #opacity }
        } else {
            quote! { 0 }
        };
        let block_entity_type = if let Some(block_entity_type) = self.block_entity_type {
            let block_entity_type = LitInt::new(&block_entity_type.to_string(), Span::call_site());
            quote! { #block_entity_type }
        } else {
            quote! { u16::MAX }
        };

        let collision_shapes = self
            .collision_shapes
            .iter()
            .map(|shape_id| LitInt::new(&shape_id.to_string(), Span::call_site()));
        let outline_shapes = self
            .outline_shapes
            .iter()
            .map(|shape_id| LitInt::new(&shape_id.to_string(), Span::call_site()));
        let piston_behavior = &self.piston_behavior.to_tokens();

        tokens.extend(quote! {
            BlockState {
                id: #id,
                state_flags: #state_flags,
                side_flags: #side_flags,
                instrument: NoteblockInstrument::#instrument,
                luminance: #luminance,
                piston_behavior: #piston_behavior,
                hardness: #hardness,
                collision_shapes: &[#(#collision_shapes),*],
                outline_shapes: &[#(#outline_shapes),*],
                opacity: #opacity,
                block_entity_type: #block_entity_type,
            }
        });
        tokens
    }
}

#[derive(Deserialize, Copy, Clone)]
pub struct BlockId(pub u16);

impl ToTokens for BlockId {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        let inner = self.0;
        tokens.extend(quote! { BlockId::new(#inner).unwrap() });
    }
}

/// Deserialized representation of a Minecraft block as stored in `blocks.json`.
#[derive(Deserialize)]
pub struct Block {
    /// Numeric block ID used in the protocol.
    pub id: BlockId,
    /// Registry name without the `minecraft:` namespace prefix.
    pub name: String,
    /// Translation key for the block's display name.
    // pub translation_key: String,
    /// Mining hardness; affects how long the block takes to break.
    pub hardness: f32,
    /// Blast resistance against explosions.
    pub blast_resistance: f32,
    pub map_color: u8,
    /// Numeric ID of the corresponding item, if any.
    pub item_id: u16,
    /// Flammability data, present only if the block can catch fire.
    pub flammable: Option<FlammableStruct>,
    /// Loot table used when the block is broken, if any.
    pub loot_table: Option<LootTableStruct>,
    /// Friction applied to entities walking on this block.
    pub slipperiness: f32,
    /// Horizontal velocity multiplier for entities inside this block.
    pub velocity_multiplier: f32,
    /// Jump velocity multiplier applied when jumping from this block.
    pub jump_velocity_multiplier: f32,
    /// Hash keys referencing the properties defined for this block.
    pub properties: Vec<i32>,
    /// State ID of the default (canonical) block state.
    pub default_state_id: BlockStateId,
    /// All possible states for this block in state-ID order.
    pub states: Vec<BlockState>,
    /// Experience points dropped when the block is mined, if any.
    pub experience: Option<Experience>,
}

impl ToTokens for Block {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        let id = self.id;
        let name = LitStr::new(&self.name, Span::call_site());
        //let translation_key = LitStr::new(&self.translation_key, Span::call_site());
        let hardness = &self.hardness;
        let blast_resistance = &self.blast_resistance;
        let map_color = &self.map_color;

        let item_id = LitInt::new(&self.item_id.to_string(), Span::call_site());
        let slipperiness = &self.slipperiness;
        let velocity_multiplier = &self.velocity_multiplier;
        let jump_velocity_multiplier = &self.jump_velocity_multiplier;
        let experience = if let Some(exp) = &self.experience {
            let exp_tokens = exp.to_token_stream();
            quote! { Some(#exp_tokens) }
        } else {
            quote! { None }
        };
        // Generate state tokens
        let states = self.states.iter().map(BlockState::to_tokens);
        let loot_table = if let Some(table) = &self.loot_table {
            let table_tokens = table.to_token_stream();
            quote! { Some(#table_tokens) }
        } else {
            quote! { None }
        };

        let default_state_ref: &BlockState = self
            .states
            .iter()
            .find(|state| state.id == self.default_state_id)
            .unwrap();
        let mut default_state = default_state_ref.clone();
        default_state.id = default_state_ref.id;
        let default_state = default_state.to_tokens();
        let flammable = if let Some(flammable) = &self.flammable {
            let flammable_tokens = flammable.to_token_stream();
            quote! { Some(#flammable_tokens) }
        } else {
            quote! { None }
        };
        tokens.extend(quote! {
            Block {
                id: #id,
                name: #name,
                hardness: #hardness,
                blast_resistance: #blast_resistance,
                map_color: #map_color,
                slipperiness: #slipperiness,
                velocity_multiplier: #velocity_multiplier,
                jump_velocity_multiplier: #jump_velocity_multiplier,
                item_id: #item_id,
                default_state: &#default_state,
                states: &[#(#states),*],
                flammable: #flammable,
                loot_table: #loot_table,
                experience: #experience,
            }
        });
    }
}

/// The underlying data type of generated block property as classified in `properties.json`.
#[derive(Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(tag = "type")]
pub enum GeneratedPropertyType {
    /// A simple `true`/`false` boolean property.
    #[serde(rename = "boolean")]
    Boolean,
    /// An integer property with an inclusive range.
    #[serde(rename = "int")]
    Int {
        /// Minimum value (inclusive).
        min: u8,
        /// Maximum value (inclusive).
        max: u8,
    },
    /// A named-variant enum property.
    #[serde(rename = "enum")]
    Enum {
        /// The ordered list of variant strings.
        values: Vec<String>,
    },
}

/// A single block property entry as deserialized from `properties.json`.
#[derive(Deserialize, Clone)]
pub struct GeneratedProperty {
    /// A stable integer hash used to identify this property across blocks.
    hash_key: i32,
    /// The name used for the generated Rust enum type.
    enum_name: String,
    /// The property name as it appears in block state JSON.
    serialized_name: String,
    /// The kind and possible values of this property.
    #[serde(rename = "type")]
    #[serde(flatten)]
    property_type: GeneratedPropertyType,
}

impl GeneratedProperty {
    /// Converts this deserialized property into the intermediate `Property` used during code generation.
    fn to_property(&self) -> Property {
        let enum_name = match &self.property_type {
            GeneratedPropertyType::Boolean => "boolean".to_string(),
            GeneratedPropertyType::Int { min, max } => format!("integer_{min}_to_{max}"),
            GeneratedPropertyType::Enum { .. } => self.enum_name.clone(),
        };

        let values = match &self.property_type {
            GeneratedPropertyType::Boolean => {
                vec!["true".to_string(), "false".to_string()]
            }
            GeneratedPropertyType::Int { min, max } => {
                let mut values = Vec::new();
                for i in *min..=*max {
                    values.push(format!("L{i}"));
                }
                values
            }
            GeneratedPropertyType::Enum { values } => values.clone(),
        };

        Property {
            enum_name,
            serialized_name: self.serialized_name.clone(),
            values,
        }
    }
}

/// Intermediate representation of a block property used while building the property group map.
#[derive(Clone)]
struct Property {
    /// The Rust enum type name for this property.
    enum_name: String,
    /// The JSON key name for this property.
    serialized_name: String,
    /// All possible string values this property can take.
    values: Vec<String>,
}

/// Top-level container for all block assets loaded from `blocks.json`.
#[derive(Deserialize)]
pub struct BlockAssets {
    /// All blocks with their states and properties.
    pub blocks: Vec<Block>,
    /// All unique bounding boxes referenced by block states.
    pub shapes: Vec<BoundingBox>,
    /// Registry names of all block entity types.
    pub block_entity_types: Vec<String>,
}

/// Reads all block assets and generates the complete block registry `TokenStream`.
pub fn build() -> TokenStream {
    let be_blocks_data = fs::read("../../assets/bedrock/block_states.nbt").unwrap();
    let mut be_blocks_cursor = Cursor::new(be_blocks_data);
    let be_blocks = get_be_data_from_nbt(&mut be_blocks_cursor);

    let geyser_blocks_data = fs::read("../../assets/bedrock/blocks.nbt").unwrap();
    let geyser_nbt_compound =
        pumpkin_nbt::nbt_compress::read_gzip_compound_tag(Cursor::new(geyser_blocks_data)).unwrap();
    let bedrock_mappings_list = geyser_nbt_compound
        .get_list("bedrock_mappings")
        .expect("Missing bedrock_mappings list in Geyser blocks.nbt");

    let blocks_assets: BlockAssets =
        serde_json::from_str(&fs::read_to_string("../../assets/blocks.json").unwrap())
            .expect("Failed to parse blocks.json");

    let generated_properties: Vec<GeneratedProperty> =
        serde_json::from_str(&fs::read_to_string("../../assets/properties.json").unwrap())
            .expect("Failed to parse properties.json");

    let generated_prop_map: BTreeMap<i32, &GeneratedProperty> = generated_properties
        .iter()
        .map(|p| (p.hash_key, p))
        .collect();

    let mut random_tick_states = Vec::new();
    let mut air_states = Vec::new();
    let mut liquid_states = Vec::new();

    let mut constants_list = Vec::new();
    let mut block_id_constants = Vec::new();
    let mut block_from_name_entries = Vec::new();
    let mut block_from_item_id_arms = Vec::new();
    let mut block_state_to_bedrock = Vec::new();

    let mut raw_id_from_state_id_array = Vec::new();
    let mut type_from_raw_id_array = Vec::new();
    let mut state_from_state_id_array = Vec::<(Ident, usize, u16)>::new();

    let mut property_enums: BTreeMap<String, PropertyStruct> = BTreeMap::new();
    let mut block_properties: Vec<BlockPropertyStruct> = Vec::new();
    let mut property_collection_map: BTreeMap<Vec<i32>, PropertyCollectionData> = BTreeMap::new();
    let mut existing_item_ids: HashSet<u16> = HashSet::new();

    for block in blocks_assets.blocks {
        let mut property_collection = HashSet::new();
        let mut property_mapping = Vec::new();

        for property_hash in &block.properties {
            let generated_property = generated_prop_map
                .get(property_hash)
                .expect("Property hash not found in generated_properties");

            property_collection.insert(generated_property.hash_key);

            let property = generated_property.to_property();
            let renamed_property = property.enum_name.to_upper_camel_case();

            let property_type = match &generated_property.property_type {
                GeneratedPropertyType::Boolean => PropertyType::Bool,
                GeneratedPropertyType::Int { min, max } => PropertyType::Int {
                    min: *min,
                    max: *max,
                },
                GeneratedPropertyType::Enum { .. } => PropertyType::Enum {
                    name: renamed_property.clone(),
                },
            };

            if let PropertyType::Enum { name } = &property_type {
                property_enums
                    .entry(name.clone())
                    .or_insert_with(|| PropertyStruct {
                        name: name.clone(),
                        values: property.values.clone(),
                    });
            }

            property_mapping.push(PropertyVariantMapping {
                original_name: property.serialized_name,
                property_type,
            });
        }

        let mut multiplier = 1;
        let mut property_descriptors = Vec::new();
        for hash in block.properties.iter().rev() {
            let gen_prop = generated_prop_map.get(hash).unwrap();
            let variant_count = match &gen_prop.property_type {
                GeneratedPropertyType::Boolean => 2,
                GeneratedPropertyType::Int { min, max } => (max - min + 1) as u16,
                GeneratedPropertyType::Enum { values } => values.len() as u16,
            };
            property_descriptors.push(quote! {
                PropertyDescriptor {
                    hash_key: #hash,
                    multiplier: #multiplier,
                    variant_count: #variant_count,
                }
            });
            multiplier *= variant_count;
        }

        let const_ident = format_ident!("{}", const_block_name_from_block_name(&block.name));
        let name_str = &block.name;
        let item_id = block.item_id;
        let block_id = block.id;

        // let mut block_with_descriptors = block.clone();
        // block_with_descriptors.property_descriptors = property_descriptors;

        constants_list.push(quote! {
            pub const #const_ident: Self = #block;
        });

        block_id_constants.push(quote! {
            pub const #const_ident: Self = #block_id;
        });

        type_from_raw_id_array.push((block.id.0, quote! { &Block::#const_ident }));

        block_from_name_entries.push(quote! {
            #name_str => Block::#const_ident,
        });

        for (i, state) in block.states.iter().enumerate() {
            if state.has_random_ticks() {
                let state_id = LitInt::new(&state.id.0.to_string(), Span::call_site());
                random_tick_states.push(state_id);
            }
            if state.is_air() {
                let state_id = LitInt::new(&state.id.0.to_string(), Span::call_site());
                air_states.push(state_id);
            }
            if state.is_liquid() {
                let state_id = LitInt::new(&state.id.0.to_string(), Span::call_site());
                liquid_states.push(state_id);
            }

            let mut matched_be_id = 1;

            if let Some(geyser_entry) = bedrock_mappings_list.get(state.id.0 as usize) {
                let (bedrock_name, bedrock_props) = parse_geyser_entry(geyser_entry, &block.name);

                if let Some(be_variants) = be_blocks.get(&bedrock_name) {
                    matched_be_id = be_variants
                        .iter()
                        .find(|(_, be_props)| {
                            bedrock_props
                                .iter()
                                .all(|(k, v)| be_props.get(k).is_some_and(|be_v| be_v == v))
                        })
                        .map_or_else(
                            || be_variants.first().map_or(1, |(id, _)| *id),
                            |(id, _)| *id,
                        );
                } else if let Some(be_variants) = be_blocks.get(&block.name) {
                    matched_be_id = be_variants.first().map_or(1, |(id, _)| *id);
                }
            } else if let Some(be_variants) = be_blocks.get(&block.name) {
                matched_be_id = be_variants.first().map_or(1, |(id, _)| *id);
            }

            block_state_to_bedrock.push((state.id.0, matched_be_id));
            raw_id_from_state_id_array.push((state.id.0, quote! { BlockId::#const_ident }));
            state_from_state_id_array.push((const_ident.clone(), i, state.id.0));
        }

        if !property_collection.is_empty() {
            let mut property_collection_vec: Vec<i32> = property_collection.into_iter().collect();
            property_collection_vec.sort_unstable();

            property_collection_map
                .entry(property_collection_vec)
                .or_insert_with(|| PropertyCollectionData::from_mappings(property_mapping))
                .add_block(block.name.clone(), block.id.0);
        }

        if existing_item_ids.insert(item_id) {
            block_from_item_id_arms.push(quote! {
                #item_id => Some(&Self::#const_ident),
            });
        }
    }

    let mut block_properties_from_state_and_block_id_arms = Vec::new();
    let mut block_properties_from_props_and_name_arms = Vec::new();

    for property_group in property_collection_map.into_values() {
        let property_name = Ident::new(
            &property_group_name_from_derived_name(&property_group.derive_name()),
            Span::call_site(),
        );

        for (block_name, id) in &property_group.blocks {
            let const_block_name = Ident::new(
                &const_block_name_from_block_name(block_name),
                Span::call_site(),
            );
            let id_lit = LitInt::new(&id.to_string(), Span::call_site());

            block_properties_from_state_and_block_id_arms.push(quote! {
                #id_lit => Box::new(#property_name::from_state_id(state_id, &Block::#const_block_name)),
            });

            block_properties_from_props_and_name_arms.push(quote! {
                #id_lit => Box::new(#property_name::from_props(props, &Block::#const_block_name)),
            });
        }

        block_properties.push(BlockPropertyStruct {
            data: property_group,
        });
    }

    let shapes = blocks_assets.shapes.iter().map(ToTokens::to_token_stream);

    let air_state_ids = quote! { #(#air_states)|* };
    let liquid_state_ids = quote! { #(#liquid_states)|* };

    let block_props = block_properties.iter().map(ToTokens::to_token_stream);
    let properties = property_enums.values().map(ToTokens::to_token_stream);

    let block_entity_types = blocks_assets
        .block_entity_types
        .iter()
        .map(|entity_type| LitStr::new(entity_type, Span::call_site()));

    let raw_id_from_state_id_ordered = fill_array(raw_id_from_state_id_array);
    let max_state_id = raw_id_from_state_id_ordered.len();
    let raw_id_from_state_id = quote! { #(#raw_id_from_state_id_ordered),* };

    let max_index = block_state_to_bedrock
        .iter()
        .map(|(idx, _)| *idx)
        .max()
        .unwrap_or(0);
    let mut state_to_bedrock_tokens = vec![quote! { 1 }; (max_index + 1) as usize];
    for (state_id, id_lit) in block_state_to_bedrock {
        let lit = LitInt::new(&id_lit.to_string(), Span::call_site());
        state_to_bedrock_tokens[state_id as usize] = quote! { #lit };
    }
    let block_state_to_bedrock_t = quote! { #(#state_to_bedrock_tokens),* };

    let type_from_raw_id_vec = fill_array(type_from_raw_id_array);
    let max_type_id = type_from_raw_id_vec.len();
    let type_from_raw_id_items = quote! { #(#type_from_raw_id_vec),* };

    let state_from_state_id_vec = fill_state_array(state_from_state_id_array);
    let max_state_id_2 = state_from_state_id_vec.len();
    let state_from_state_id = quote! { #(#state_from_state_id_vec),* };

    assert_eq!(max_state_id, max_state_id_2);

    let Bitset {
        items,
        mod_ident,
        contains_ident,
    } = &gen_u16_bitset(
        "RANDOM_TICKS",
        &random_tick_states
            .iter()
            .map(|it| it.base10_parse().unwrap())
            .collect::<Vec<u16>>(),
    );

    quote! {
        #[allow(clippy::wildcard_imports, clippy::enum_glob_use, clippy::too_many_lines, clippy::match_same_arms)]
        use pumpkin_util::math::boundingbox::BoundingBox;

        use crate::{BlockState, BlockStateId, Block, BlockId, blocks::Flammable};
        use crate::block_state::PistonBehavior;
        use pumpkin_util::math::int_provider::{UniformIntProvider, IntProvider, NormalIntProvider};
        use pumpkin_util::loot_table::*;
        use pumpkin_util::math::experience::Experience;
        use pumpkin_util::math::vector3::Vector3;
        use std::collections::BTreeMap;

        #items

        #[derive(Clone, Copy, Debug)]
        pub struct BlockProperty {
            pub name: &'static str,
            pub values: &'static [&'static str],
        }

        pub trait BlockProperties where Self: 'static {
            fn to_index(&self) -> u16;
            fn from_index(index: u16) -> Self where Self: Sized;
            fn handles_block_id(id: BlockId) -> bool where Self: Sized;
            fn to_state_id(&self, block: &Block) -> BlockStateId;
            fn from_state_id(id: BlockStateId, block: &Block) -> Self where Self: Sized;
            fn default(block: &Block) -> Self where Self: Sized;
            fn to_props(&self) -> Vec<(&'static str, &'static str)>;
            fn from_props(props: &[(&str, &str)], block: &Block) -> Self where Self: Sized;
        }

        pub trait EnumVariants {
            fn variant_count() -> u16;
            fn to_index(&self) -> u16;
            fn from_index(index: u16) -> Self;
            fn to_value(&self) -> &'static str;
            fn from_value(value: &str) -> Self;
        }

        pub const COLLISION_SHAPES: &[BoundingBox] = &[
            #(#shapes),*
        ];

        pub const BLOCK_ENTITY_TYPES: &[&str] = &[
            #(#block_entity_types),*
        ];

        #[inline(always)]
        #[must_use]
        pub const fn is_air(id: BlockStateId) -> bool {
            matches!(id.as_u16(), #air_state_ids)
        }

         #[inline(always)]
         #[must_use]
        pub const fn is_liquid(id: BlockStateId) -> bool {
            matches!(id.as_u16(), #liquid_state_ids)
        }

        #[inline(always)]
        #[must_use]
        pub fn has_random_ticks(id: BlockStateId) -> bool {
            #mod_ident::#contains_ident(id.as_u16())
        }

        #[must_use]
        pub const fn blocks_movement(block_state: &BlockState, id: BlockId) -> bool {
            block_state.is_solid() && !matches!(id, BlockId::COBWEB | BlockId::BAMBOO_SAPLING)
        }

        impl BlockState {
            const STATE_ID_TO_BEDROCK: &[u16] = &[
                #block_state_to_bedrock_t
            ];

            /// Get a [`BlockState`] from a [`BlockStateId`].
            /// If you need access to the block use `BlockState::from_id_with_block` instead.
            #[inline]
            #[must_use]
            pub const fn from_id(id: BlockStateId) -> &'static Self {
                // Safety: We always check this condition when creating a BlockStateId.
                // the u16 field is private and immutable. BlockStateId::STATE_COUNT is a const u16.
                // If the condition held once, it will always hold.
                unsafe { std::hint::assert_unchecked(id.as_u16() < BlockStateId::STATE_COUNT) }
                // This hint guarantees that bound checks can be optimized away in release builds.
                // Due to debug_assertions forcing -Zub_checks=yes (rust-lang/rust#123499)
                // bound checks-panics are replaced with ub-checks on profile.dev
                // https://doc.rust-lang.org/nightly/unstable-book/compiler-flags/ub-checks.html

                mappings::STATE_FROM_STATE_ID[id.as_u16() as usize]
            }

            #[doc = r" Get a block state from a state id and the corresponding block."]
            #[inline]
            #[must_use]
            pub const fn from_id_with_block(id: BlockStateId) -> (&'static Block, &'static Self) {
                let block = Block::from_state_id(id);
                let state = Self::from_id(id);
                (block, state)
            }

            #[must_use]
            pub const fn to_be_network_id(id: BlockStateId) -> u16 {
                // Safety: We always check this condition when creating a BlockStateId.
                // the u16 field is private and immutable. BlockStateId::STATE_COUNT is a const u16.
                // If the condition held once, it will always hold.
                unsafe { std::hint::assert_unchecked(id.as_u16() < BlockStateId::STATE_COUNT) }
                // This hint guarantees that bound checks can be optimized away in release builds.
                // Due to debug_assertions forcing -Zub_checks=yes (rust-lang/rust#123499)
                // bound checks-panics are replaced with ub-checks on profile.dev
                // https://doc.rust-lang.org/nightly/unstable-book/compiler-flags/ub-checks.html

                Self::STATE_ID_TO_BEDROCK[id.as_u16() as usize]
            }
        }

        impl BlockStateId {
            pub const AIR: Self = Block::AIR.default_state.id;

            pub(crate) const STATE_COUNT: u16 = mappings::STATE_FROM_STATE_ID.len() as u16;
        }

        mod mappings {
            use crate::{Block, BlockId, BlockState, BlockStateId};
            use phf;

            pub(super) static BLOCK_FROM_NAME_MAP: phf::Map<&'static str, Block> = phf::phf_map!{
                #(#block_from_name_entries)*
            };

            pub(super) static BLOCK_ID_FROM_STATE_ID: [BlockId; #max_state_id] = [
                #raw_id_from_state_id
            ];

            pub(super) static TYPE_FROM_RAW_ID: [&Block; #max_type_id] = [
                #type_from_raw_id_items
            ];

            pub(super) static STATE_FROM_STATE_ID: [&BlockState; #max_state_id] = [
                #state_from_state_id
            ];
        }


        impl Block {
            #(#constants_list)*

            #[doc = r" Try to parse a block from a resource location string."]
            #[inline]
            #[must_use]
            pub fn from_registry_key(name: &str) -> Option<&'static Self> {
                mappings::BLOCK_FROM_NAME_MAP.get(name)
            }

            #[doc = r" Try to get a block from a namespace prefixed name."]
            #[must_use]
            pub fn from_name(name: &str) -> Option<&'static Self> {
                let key = name.strip_prefix("minecraft:").unwrap_or(name);
                mappings::BLOCK_FROM_NAME_MAP.get(key)
            }

            /// Get a [`Block`] from a [`BlockId`]
            #[inline]
            #[must_use]
            pub const fn from_id(id: BlockId) -> &'static Self {
                // Safety: We always check this condition when creating a BlockId.
                // the u16 field is private and immutable. BlockId::BLOCK_COUNT is a const u16.
                // If the condition held once, it will always hold.
                unsafe { std::hint::assert_unchecked(id.as_u16() < BlockId::BLOCK_COUNT) }
                // This hint guarantees that bound checks can be optimized away in release builds.
                // Due to debug_assertions forcing -Zub_checks=yes (rust-lang/rust#123499)
                // bound checks-panics are replaced with ub-checks on profile.dev
                // https://doc.rust-lang.org/nightly/unstable-book/compiler-flags/ub-checks.html

                mappings::TYPE_FROM_RAW_ID[id.as_u16() as usize]
            }

            /// Get a [`Block`] from a state id
            #[inline]
            #[must_use]
            pub const fn from_state_id(id: BlockStateId) -> &'static Self {
                Self::from_id(BlockId::from_state_id(id))
            }

            #[doc = r" Try to parse a block from an item id."]
            #[must_use]
            pub const fn from_item_id(id: u16) -> Option<&'static Self> {
                #[allow(unreachable_patterns)]
                match id {
                    #(#block_from_item_id_arms)*
                    _ => None
                }
            }

            #[track_caller]
            #[doc = r" Get the properties of the block."]
            pub fn properties(&self, state_id: BlockStateId) -> Option<Box<dyn BlockProperties>> {
                Some(match self.id.as_u16() {
                    #(#block_properties_from_state_and_block_id_arms)*
                    _ => return None,
                })
            }

            #[track_caller]
            #[doc = r" Get the properties of the block."]
            pub fn from_properties(&self, props: &[(&str, &str)]) -> Box<dyn BlockProperties> {
                match self.id.as_u16() {
                    #(#block_properties_from_props_and_name_arms)*
                    _ => panic!("Invalid props")
                }
            }
        }

        impl BlockId {
            #(#block_id_constants)*

            pub(crate) const BLOCK_COUNT: u16 = mappings::TYPE_FROM_RAW_ID.len() as u16;

            /// Get a [`BlockId`] from a [`BlockStateId`]
            #[inline]
            #[must_use]
            pub const fn from_state_id(id: BlockStateId) -> BlockId {
                // Safety: We always check this condition when creating a BlockStateId.
                // the u16 field is private and immutable. BlockStateId::STATE_COUNT is a const u16.
                // If the condition held once, it will always hold.
                unsafe { std::hint::assert_unchecked(id.as_u16() < BlockStateId::STATE_COUNT) }
                // This hint guarantees that bound checks can be optimized away in release builds.
                // Due to debug_assertions forcing -Zub_checks=yes (rust-lang/rust#123499)
                // bound checks-panics are replaced with ub-checks on profile.dev
                // https://doc.rust-lang.org/nightly/unstable-book/compiler-flags/ub-checks.html

                mappings::BLOCK_ID_FROM_STATE_ID[id.as_u16() as usize]
            }
        }

        #(#properties)*

        #(#block_props)*

        impl Facing {
            #[must_use]
            pub const fn opposite(&self) -> Self {
                match self {
                    Self::North => Self::South,
                    Self::South => Self::North,
                    Self::East => Self::West,
                    Self::West => Self::East,
                    Self::Up => Self::Down,
                    Self::Down => Self::Up,
                }
            }
        }

        impl HorizontalFacing {
            #[must_use]
            pub fn all() -> [Self; 4] {
                [
                    Self::North,
                    Self::South,
                    Self::West,
                    Self::East,
                ]
            }

            #[must_use]
            pub fn to_offset(&self) -> Vector3<i32> {
                match self {
                    Self::North => (0, 0, -1),
                    Self::South => (0, 0, 1),
                    Self::West => (-1, 0, 0),
                    Self::East => (1, 0, 0),
                }
                .into()
            }
            #[must_use]
            pub const fn to_axis(&self) -> HorizontalAxis {
                match self {
                    Self::North | Self::South => HorizontalAxis::Z,
                    Self::West | Self::East => HorizontalAxis::X,
                }
            }
             #[must_use]
            pub const fn to_facing(&self) -> HorizontalFacing {
                match self {
                    Self::North => HorizontalFacing::North,
                    Self::South => HorizontalFacing::South,
                    Self::West => HorizontalFacing::West,
                    Self::East => HorizontalFacing::East,
                }
            }
            #[must_use]
            pub const fn opposite(&self) -> Self {
                match self {
                    Self::North => Self::South,
                    Self::South => Self::North,
                    Self::West => Self::East,
                    Self::East => Self::West,
                }
            }

            #[must_use]
            pub const fn rotate_clockwise(&self) -> Self {
                match self {
                    Self::North => Self::East,
                    Self::South => Self::West,
                    Self::West => Self::North,
                    Self::East => Self::South,
                }
            }

            #[must_use]
            pub const fn rotate_counter_clockwise(&self) -> Self {
                match self {
                    Self::North => Self::West,
                    Self::South => Self::East,
                    Self::West => Self::South,
                    Self::East => Self::North,
                }
            }
        }

        impl RailShape {
            #[must_use]
            pub const fn is_ascending(&self) -> bool {
                matches!(self, Self::AscendingEast | Self::AscendingWest | Self::AscendingNorth | Self::AscendingSouth)
            }
        }

        impl RailShapeStraight {
            #[must_use]
            pub const fn is_ascending(&self) -> bool {
                matches!(self, Self::AscendingEast | Self::AscendingWest | Self::AscendingNorth | Self::AscendingSouth)
            }
        }
    }
}

/// Parses the Bedrock Edition block palette NBT file into a map of block names to their state variants.
///
/// # Arguments
/// – `reader` – a readable byte source positioned at the start of the NBT data.
#[expect(clippy::type_complexity)]
fn get_be_data_from_nbt<R: Read + Seek>(
    reader: &mut R,
) -> BTreeMap<String, Vec<(u32, BTreeMap<String, String>)>> {
    let mut block_data: BTreeMap<String, Vec<(u32, BTreeMap<String, String>)>> = BTreeMap::new();
    let mut current_id = 0;

    let data_start = reader.stream_position().unwrap();
    let data_end = reader.seek(SeekFrom::End(0)).unwrap();
    let _ = reader.seek(SeekFrom::Start(data_start));

    let nbt_reader =
        &mut NbtReadHelperBedrock::new(pumpkin_nbt::deserializer::NbtStreamReader(&mut *reader));

    loop {
        if nbt_reader.reader().0.stream_position().unwrap() >= data_end {
            break;
        }

        let nbt = pumpkin_nbt::Nbt::read(nbt_reader).unwrap();

        let block_name = {
            let raw_name = nbt.get_string("name").unwrap();
            raw_name
                .strip_prefix("minecraft:")
                .unwrap_or(raw_name)
                .to_string()
        };

        let properties = nbt
            .get_compound("states")
            .unwrap()
            .clone()
            .into_iter()
            .map(|(key, val)| {
                let unpacked = match val {
                    pumpkin_nbt::tag::NbtTag::Byte(v) => {
                        if v == 1 {
                            "true".into()
                        } else {
                            "false".into()
                        }
                    }
                    pumpkin_nbt::tag::NbtTag::Int(v) => v.to_string(),
                    pumpkin_nbt::tag::NbtTag::String(v) => v.into(),
                    _ => {
                        panic!("Unexpected type for {}. Value: {val:?}", key);
                    }
                };

                (key.into(), unpacked)
            })
            .collect::<BTreeMap<_, _>>();

        if !block_name.is_empty() {
            block_data
                .entry(block_name)
                .or_default()
                .push((current_id, properties));
        }

        current_id += 1;
    }

    block_data
}

fn parse_geyser_entry(
    entry: &pumpkin_nbt::tag::NbtTag,
    java_block_name: &str,
) -> (String, BTreeMap<String, String>) {
    let compound = match entry {
        pumpkin_nbt::tag::NbtTag::Compound(c) => c,
        _ => panic!("Expected NbtCompound in Geyser bedrock_mappings list"),
    };

    let raw_identifier = compound
        .get_string("bedrock_identifier")
        .filter(|s| !s.is_empty())
        .unwrap_or(java_block_name);

    let bedrock_identifier = raw_identifier
        .strip_prefix("minecraft:")
        .unwrap_or(raw_identifier)
        .to_string();

    let mut properties = BTreeMap::new();
    if let Some(state_comp) = compound.get_compound("state") {
        for (key, val) in state_comp.clone() {
            let unpacked = match val {
                pumpkin_nbt::tag::NbtTag::Byte(v) => {
                    if v == 1 {
                        "true".to_string()
                    } else {
                        "false".to_string()
                    }
                }
                pumpkin_nbt::tag::NbtTag::Int(v) => v.to_string(),
                pumpkin_nbt::tag::NbtTag::String(v) => v.to_string(),
                _ => {
                    panic!(
                        "Unexpected NBT type in Geyser state tag for {}. Value: {:?}",
                        key, val
                    );
                }
            };
            properties.insert(key.to_string(), unpacked);
        }
    }

    (bedrock_identifier, properties)
}
