use heck::ToPascalCase;
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use serde_json::Value;
use std::fs;

/// Reads `placed_feature.json` and emits the complete `PlacedFeature` enum `TokenStream`.
pub fn build_enum() -> TokenStream {
    let json_content = fs::read_to_string("../../assets/placed_feature.json")
        .expect("Failed to read placed_feature.json");
    let json: Value =
        serde_json::from_str(&json_content).expect("Failed to parse placed_feature.json");

    let mut from_name_arms = Vec::new();
    let mut to_name_arms = Vec::new();
    let mut all_names: Vec<String> = Vec::new();

    let variants: Vec<TokenStream> = json
        .as_object()
        .unwrap()
        .iter()
        .map(|(name, _)| {
            let variant_name = format_ident!("{}", name.to_pascal_case());
            from_name_arms.push(quote! {
                #name => Some(Self::#variant_name),
            });
            to_name_arms.push(quote! {
                Self::#variant_name => #name,
            });
            all_names.push(format!("minecraft:{name}"));
            quote! {
                #variant_name,
            }
        })
        .collect();

    let all_names_tokens: Vec<TokenStream> =
        all_names.iter().map(|name| quote! { #name }).collect();

    quote! {
        #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
        pub enum PlacedFeature {
            #(#variants)*
        }

        impl PlacedFeature {
            pub fn from_name(name: &str) -> Option<Self> {
                let name = name.strip_prefix("minecraft:").unwrap_or(name);
                match name {
                    #(#from_name_arms)*
                    _ => None,
                }
            }

            pub const fn to_name(&self) -> &'static str {
                match self {
                    #(#to_name_arms)*
                }
            }

            #[must_use]
            pub const fn all_names() -> &'static [&'static str] {
                &[#(#all_names_tokens),*]
            }
        }
    }
}

/// Reads `placed_feature.json` and emits the complete `build_placed_features()` function `TokenStream`.
pub fn build() -> TokenStream {
    let json_content = fs::read_to_string("../../assets/placed_feature.json")
        .expect("Failed to read placed_feature.json");
    let json: Value =
        serde_json::from_str(&json_content).expect("Failed to parse placed_feature.json");

    let entries: Vec<TokenStream> = json
        .as_object()
        .unwrap()
        .iter()
        .map(|(name, value)| {
            let variant_name = format_ident!("{}", name.to_pascal_case());
            let pf = value_to_placed_feature(value);
            quote! {
                map.insert(pumpkin_data::placed_feature::PlacedFeature::#variant_name, #pf);
            }
        })
        .collect();

    quote! {
        #[allow(clippy::all, unused_imports, dead_code, clippy::too_many_lines)]
        fn build_placed_features() -> std::collections::HashMap<pumpkin_data::placed_feature::PlacedFeature, PlacedFeature> {
            use crate::generation::block_predicate::{
                AllOfBlockPredicate, AnyOfBlockPredicate, BlockPredicate,
                HasSturdyFacePredicate, InsideWorldBoundsBlockPredicate,
                MatchingBlockTagPredicate, MatchingBlocksBlockPredicate, MatchingBlocksWrapper,
                MatchingFluidsBlockPredicate, NotBlockPredicate, OffsetBlocksBlockPredicate,
                ReplaceableBlockPredicate, SolidBlockPredicate, WouldSurviveBlockPredicate,
            };
            use crate::generation::height_provider::{
                HeightProvider, TrapezoidHeightProvider, UniformHeightProvider,
                VeryBiasedToBottomHeightProvider,
            };
            use pumpkin_util::y_offset::{AboveBottom, Absolute, BelowTop, YOffset};
            use pumpkin_util::math::int_provider::{
                BiasedToBottomIntProvider, ClampedIntProvider, TrapezoidIntProvider, ClampedNormalIntProvider,
                ConstantIntProvider, IntProvider, NormalIntProvider, UniformIntProvider,
                WeightedEntry, WeightedListIntProvider,
            };
            use crate::block::BlockStateCodec;
            use pumpkin_data::{Block, BlockDirection};
            use pumpkin_util::math::vector3::Vector3;
            use pumpkin_util::HeightMap;
            let mut map = std::collections::HashMap::new();
            #(#entries)*
            map
        }
    }
}

/// Converts a single placed-feature JSON value into a `PlacedFeature` token stream.
///
/// # Arguments
/// – `v` – the JSON object for the entry, expected to contain `"feature"` and `"placement"` fields.
fn value_to_placed_feature(v: &Value) -> TokenStream {
    let feature = value_to_feature(&v["feature"]);
    let placement_arr = v["placement"]
        .as_array()
        .map(|a| a.as_slice())
        .unwrap_or(&[]);
    let placement: Vec<TokenStream> = placement_arr
        .iter()
        .map(value_to_placement_modifier)
        .collect();
    quote! {
        PlacedFeature {
            feature: #feature,
            placement: vec![#(#placement),*],
        }
    }
}

/// Converts a JSON `"feature"` value into a `Feature` token stream.
///
/// # Arguments
/// - `v` – Either a string (named reference) or object (inline configured feature).
///
/// # Returns
/// `Feature::Named(…)` for string values or `Feature::Inlined(…)` for object values.
fn value_to_feature(v: &Value) -> TokenStream {
    match v {
        Value::String(s) => {
            let name = s.strip_prefix("minecraft:").unwrap_or(s);
            let variant_name = format_ident!("{}", name.to_pascal_case());
            quote! { Feature::Named(pumpkin_data::configured_feature::ConfiguredFeature::#variant_name) }
        }
        Value::Object(_) => {
            let cf = value_to_inline_configured_feature(v);
            quote! { Feature::Inlined(Box::new(#cf)) }
        }
        _ => panic!("Wrong feature value"),
    }
}

/// Converts a single placement-modifier JSON object into its corresponding `PlacementModifier` token stream.
///
/// # Arguments
/// - `v` – JSON object with a `"type"` discriminant and modifier-specific fields.
///
/// # Returns
/// A `PlacementModifier` variant token stream, or `compile_error!` for unknown types.
fn value_to_placement_modifier(v: &Value) -> TokenStream {
    let type_str = v["type"].as_str().unwrap_or("");
    match type_str {
        "minecraft:biome" => quote! { PlacementModifier::Biome(BiomePlacementModifier) },
        "minecraft:in_square" => quote! { PlacementModifier::InSquare(SquarePlacementModifier) },
        "minecraft:fixed_placement" => {
            let positions = v["positions"]
                .as_array()
                .map(|arr| {
                    arr.iter()
                        .map(|p| {
                            let coords = p.as_array().unwrap();
                            let x = coords[0].as_i64().unwrap_or(0) as i32;
                            let y = coords[1].as_i64().unwrap_or(0) as i32;
                            let z = coords[2].as_i64().unwrap_or(0) as i32;
                            quote! { BlockPos::new(#x, #y, #z) }
                        })
                        .collect::<Vec<_>>()
                })
                .unwrap_or_default();
            quote! { PlacementModifier::FixedPlacement(vec![#(#positions),*]) }
        }
        "minecraft:heightmap" => {
            let heightmap = value_to_height_map(v["heightmap"].as_str().unwrap());
            quote! {
                PlacementModifier::Heightmap(HeightmapPlacementModifier {
                    heightmap: #heightmap,
                })
            }
        }
        "minecraft:height_range" => {
            let height = value_to_height_provider(&v["height"]);
            quote! {
                PlacementModifier::HeightRange(HeightRangePlacementModifier {
                    height: #height,
                })
            }
        }
        "minecraft:count" => {
            let count = value_to_int_provider(&v["count"]);
            quote! {
                PlacementModifier::Count(CountPlacementModifier {
                    count: #count,
                })
            }
        }
        "minecraft:count_on_every_layer" => {
            let count = value_to_int_provider(&v["count"]);
            quote! {
                PlacementModifier::CountOnEveryLayer(CountOnEveryLayerPlacementModifier {
                    count: #count,
                })
            }
        }
        "minecraft:rarity_filter" => {
            let chance = v["chance"].as_u64().unwrap_or(1) as u32;
            quote! {
                PlacementModifier::RarityFilter(RarityFilterPlacementModifier {
                    chance: #chance,
                })
            }
        }
        "minecraft:block_predicate_filter" => {
            let predicate = value_to_block_predicate(&v["predicate"]);
            quote! {
                PlacementModifier::BlockPredicateFilter(BlockFilterPlacementModifier {
                    predicate: #predicate,
                })
            }
        }
        "minecraft:surface_relative_threshold_filter" => {
            let heightmap =
                value_to_height_map(v["heightmap"].as_str().unwrap_or("MOTION_BLOCKING"));
            let min_inc = value_to_option_i32(&v["min_inclusive"]);
            let max_inc = value_to_option_i32(&v["max_inclusive"]);
            quote! {
                PlacementModifier::SurfaceRelativeThresholdFilter(SurfaceThresholdFilterPlacementModifier {
                    heightmap: #heightmap,
                    min_inclusive: #min_inc,
                    max_inclusive: #max_inc,
                })
            }
        }
        "minecraft:surface_water_depth_filter" => {
            let depth = v["max_water_depth"].as_i64().unwrap_or(0) as i32;
            quote! {
                PlacementModifier::SurfaceWaterDepthFilter(SurfaceWaterDepthFilterPlacementModifier {
                    max_water_depth: #depth,
                })
            }
        }
        "minecraft:noise_based_count" => {
            let ratio = v["noise_to_count_ratio"].as_i64().unwrap_or(0) as i32;
            let factor = v["noise_factor"].as_f64().unwrap_or(1.0);
            let offset = v["noise_offset"].as_f64().unwrap_or(0.0);
            quote! {
                PlacementModifier::NoiseBasedCount(NoiseBasedCountPlacementModifier {
                    to_count_ratio: #ratio,
                    factor: #factor,
                    offset: #offset,
                })
            }
        }
        "minecraft:noise_threshold_count" => {
            let level = v["noise_level"].as_f64().unwrap_or(0.0);
            let below = v["below_noise"].as_i64().unwrap_or(0) as i32;
            let above = v["above_noise"].as_i64().unwrap_or(0) as i32;
            quote! {
                PlacementModifier::NoiseThresholdCount(NoiseThresholdCountPlacementModifier {
                    noise_level: #level,
                    below_noise: #below,
                    above_noise: #above,
                })
            }
        }
        "minecraft:environment_scan" => {
            let dir = value_to_block_direction(v["direction_of_search"].as_str().unwrap_or("down"));
            let target = value_to_block_predicate(&v["target_condition"]);
            let allowed = if v["allowed_search_condition"].is_null()
                || v["allowed_search_condition"].is_object()
            {
                if v["allowed_search_condition"].is_object() {
                    let p = value_to_block_predicate(&v["allowed_search_condition"]);
                    quote! { Some(#p) }
                } else {
                    quote! { None }
                }
            } else {
                quote! { None }
            };
            let steps = v["max_steps"].as_i64().unwrap_or(1) as i32;
            quote! {
                PlacementModifier::EnvironmentScan(EnvironmentScanPlacementModifier {
                    direction_of_search: #dir,
                    target_condition: #target,
                    allowed_search_condition: #allowed,
                    max_steps: #steps,
                })
            }
        }
        "minecraft:random_offset" => {
            let xz = value_to_int_provider(&v["xz_spread"]);
            let y = value_to_int_provider(&v["y_spread"]);
            quote! {
                PlacementModifier::RandomOffset(RandomOffsetPlacementModifier {
                    xz_spread: #xz,
                    y_spread: #y,
                })
            }
        }
        other => {
            let msg = format!("unknown placement modifier: {other}");
            quote! { compile_error!(#msg) }
        }
    }
}

/// Converts a block-predicate JSON object into its corresponding `BlockPredicate` token stream.
///
/// # Arguments
/// - `v` – JSON object with a `"type"` discriminant and predicate-specific fields.
///
/// # Returns
/// A `BlockPredicate` variant token stream, or `compile_error!` for unknown types.
pub fn value_to_block_predicate(v: &Value) -> TokenStream {
    // Handle bare string values: "#minecraft:some_tag" is a block-tag predicate,
    // "true" (or any non-# string) is AlwaysTrue.
    if let Some(s) = v.as_str() {
        if let Some(tag) = s.strip_prefix('#') {
            let tag_ident = quote::format_ident!("{}", tag.to_uppercase().replace([':', '-'], "_"));
            return quote! {
                BlockPredicate::MatchingBlockTag(MatchingBlockTagPredicate {
                    offset: OffsetBlocksBlockPredicate { offset: None },
                    tag: pumpkin_data::tag::Block::#tag_ident,
                })
            };
        }
        return quote! { BlockPredicate::AlwaysTrue };
    }

    let type_str = v["type"].as_str().unwrap_or("");
    match type_str {
        "minecraft:true" | "" => quote! { BlockPredicate::AlwaysTrue },
        "minecraft:matching_blocks" => {
            let offset = value_to_offset_predicate(&v["offset"]);
            let blocks = value_to_matching_blocks_wrapper(&v["blocks"]);
            quote! {
                BlockPredicate::MatchingBlocks(MatchingBlocksBlockPredicate {
                    offset: #offset,
                    blocks: #blocks,
                })
            }
        }
        "minecraft:matching_block_tag" => {
            let offset = value_to_offset_predicate(&v["offset"]);
            let tag = v["tag"].as_str().unwrap_or("");
            let tag_ident = quote::format_ident!("{}", tag.to_uppercase().replace([':', '-'], "_"));
            quote! {
                BlockPredicate::MatchingBlockTag(MatchingBlockTagPredicate {
                    offset: #offset,
                    tag: pumpkin_data::tag::Block::#tag_ident,
                })
            }
        }
        "minecraft:matching_fluids" => {
            let offset = value_to_offset_predicate(&v["offset"]);
            let fluids = value_to_matching_blocks_wrapper(&v["fluids"]);
            quote! {
                BlockPredicate::MatchingFluids(MatchingFluidsBlockPredicate {
                    offset: #offset,
                    fluids: #fluids,
                })
            }
        }
        "minecraft:has_sturdy_face" => {
            let offset = value_to_offset_predicate(&v["offset"]);
            let dir = value_to_block_direction(v["direction"].as_str().unwrap_or("down"));
            quote! {
                BlockPredicate::HasSturdyFace(HasSturdyFacePredicate {
                    offset: #offset,
                    direction: #dir,
                })
            }
        }
        "minecraft:solid" => {
            let offset = value_to_offset_predicate(&v["offset"]);
            quote! {
                BlockPredicate::Solid(SolidBlockPredicate { offset: #offset })
            }
        }
        "minecraft:replaceable" => {
            let offset = value_to_offset_predicate(&v["offset"]);
            quote! {
                BlockPredicate::Replaceable(ReplaceableBlockPredicate { offset: #offset })
            }
        }
        "minecraft:would_survive" => {
            let offset = value_to_offset_predicate(&v["offset"]);
            let state = value_to_block_state_codec(&v["state"]);
            quote! {
                BlockPredicate::WouldSurvive(WouldSurviveBlockPredicate {
                    offset: #offset,
                    state: #state,
                })
            }
        }
        "minecraft:inside_world_bounds" => {
            let offset_v = &v["offset"];
            let offset = if offset_v.is_array() {
                let x = offset_v[0].as_i64().unwrap_or(0) as i32;
                let y = offset_v[1].as_i64().unwrap_or(0) as i32;
                let z = offset_v[2].as_i64().unwrap_or(0) as i32;
                quote! { Vector3::new(#x, #y, #z) }
            } else {
                quote! { Vector3::new(0, 0, 0) }
            };
            quote! {
                BlockPredicate::InsideWorldBounds(InsideWorldBoundsBlockPredicate {
                    offset: #offset,
                })
            }
        }
        "minecraft:any_of" => {
            let predicates: Vec<TokenStream> = v["predicates"]
                .as_array()
                .map(|a| a.iter().map(value_to_block_predicate).collect())
                .unwrap_or_default();
            quote! {
                BlockPredicate::AnyOf(AnyOfBlockPredicate {
                    predicates: vec![#(#predicates),*],
                })
            }
        }
        "minecraft:all_of" => {
            let predicates: Vec<TokenStream> = v["predicates"]
                .as_array()
                .map(|a| a.iter().map(value_to_block_predicate).collect())
                .unwrap_or_default();
            quote! {
                BlockPredicate::AllOf(AllOfBlockPredicate {
                    predicates: vec![#(#predicates),*],
                })
            }
        }
        "minecraft:not" => {
            let inner = value_to_block_predicate(&v["predicate"]);
            quote! {
                BlockPredicate::Not(NotBlockPredicate {
                    predicate: Box::new(#inner),
                })
            }
        }
        other => {
            let msg = format!("unknown block predicate: {other}");
            quote! { compile_error!(#msg) }
        }
    }
}

/// Converts a height-provider JSON object into its corresponding `HeightProvider` token stream.
///
/// # Arguments
/// - `v` – JSON object with a `"type"` discriminant and provider-specific fields.
///
/// # Returns
/// A `HeightProvider` variant token stream, or `compile_error!` for unknown types.
pub fn value_to_height_provider(v: &Value) -> TokenStream {
    let type_str = v["type"].as_str().unwrap_or("");
    match type_str {
        "minecraft:uniform" => {
            let min = value_to_y_offset(&v["min_inclusive"]);
            let max = value_to_y_offset(&v["max_inclusive"]);
            quote! {
                HeightProvider::Uniform(UniformHeightProvider {
                    min_inclusive: #min,
                    max_inclusive: #max,
                })
            }
        }
        "minecraft:trapezoid" => {
            let min = value_to_y_offset(&v["min_inclusive"]);
            let max = value_to_y_offset(&v["max_inclusive"]);
            let plateau = value_to_option_i32(&v["plateau"]);
            quote! {
                HeightProvider::Trapezoid(TrapezoidHeightProvider {
                    min_inclusive: #min,
                    max_inclusive: #max,
                    plateau: #plateau,
                })
            }
        }
        "minecraft:very_biased_to_bottom" => {
            let min = value_to_y_offset(&v["min_inclusive"]);
            let max = value_to_y_offset(&v["max_inclusive"]);
            let inner = if v["inner"].is_null() || !v["inner"].is_number() {
                quote! { None }
            } else {
                let n = v["inner"].as_u64().unwrap_or(1) as u32;
                quote! { std::num::NonZeroU32::new(#n) }
            };
            quote! {
                HeightProvider::VeryBiasedToBottom(VeryBiasedToBottomHeightProvider {
                    min_inclusive: #min,
                    max_inclusive: #max,
                    inner: #inner,
                })
            }
        }
        "minecraft:constant" => {
            // constant height provider wraps a y_offset
            let y = value_to_y_offset(&v["value"]);
            // Map to uniform with same min/max
            quote! {
                HeightProvider::Uniform(UniformHeightProvider {
                    min_inclusive: #y,
                    max_inclusive: #y,
                })
            }
        }
        other => {
            let msg = format!("unknown height provider: {other}");
            quote! { compile_error!(#msg) }
        }
    }
}

/// Converts a Y-offset JSON object into its corresponding `YOffset` token stream.
///
/// # Arguments
/// - `v` – JSON object with one of the keys `"absolute"`, `"above_bottom"`, or `"below_top"`.
///
/// # Returns
/// A `YOffset` variant token stream; defaults to `YOffset::Absolute(0)` when unrecognized.
pub fn value_to_y_offset(v: &Value) -> TokenStream {
    if let Some(abs) = v.get("absolute") {
        let val = abs.as_i64().unwrap_or(0) as i16;
        quote! { YOffset::Absolute(Absolute { absolute: #val }) }
    } else if let Some(ab) = v.get("above_bottom") {
        let val = ab.as_i64().unwrap_or(0) as i8;
        quote! { YOffset::AboveBottom(AboveBottom { above_bottom: #val }) }
    } else if let Some(bt) = v.get("below_top") {
        let val = bt.as_i64().unwrap_or(0) as i8;
        quote! { YOffset::BelowTop(BelowTop { below_top: #val }) }
    } else {
        quote! { YOffset::Absolute(Absolute { absolute: 0 }) }
    }
}

/// Converts an integer-provider JSON value into its corresponding `IntProvider` token stream.
///
/// # Arguments
/// - `v` – Either a bare JSON number (constant) or an object with a `"type"` discriminant.
///
/// # Returns
/// An `IntProvider` variant token stream; defaults to `IntProvider::Constant(0)` when unrecognized.
pub fn value_to_int_provider(v: &Value) -> TokenStream {
    match v {
        Value::Null => {
            quote! { IntProvider::Constant(0) }
        }
        Value::Number(n) => {
            let val = n.as_i64().unwrap_or(0) as i32;
            quote! { IntProvider::Constant(#val) }
        }
        Value::Object(_) => {
            let type_str = v["type"].as_str().unwrap_or("");
            match type_str {
                "minecraft:constant" => {
                    let val = v["value"].as_i64().unwrap_or(0) as i32;
                    quote! { IntProvider::Object(NormalIntProvider::Constant(ConstantIntProvider { value: #val })) }
                }
                "minecraft:uniform" => {
                    let min = v["min_inclusive"].as_i64().unwrap_or(0) as i32;
                    let max = v["max_inclusive"].as_i64().unwrap_or(0) as i32;
                    quote! { IntProvider::Object(NormalIntProvider::Uniform(UniformIntProvider { min_inclusive: #min, max_inclusive: #max })) }
                }
                "minecraft:biased_to_bottom" => {
                    let min = v["min_inclusive"].as_i64().unwrap_or(0) as i32;
                    let max = v["max_inclusive"].as_i64().unwrap_or(0) as i32;
                    quote! { IntProvider::Object(NormalIntProvider::BiasedToBottom(BiasedToBottomIntProvider { min_inclusive: #min, max_inclusive: #max })) }
                }
                "minecraft:clamped" => {
                    let min = v["min_inclusive"].as_i64().unwrap_or(0) as i32;
                    let max = v["max_inclusive"].as_i64().unwrap_or(0) as i32;
                    let src = value_to_int_provider(&v["source"]);
                    quote! { IntProvider::Object(NormalIntProvider::Clamped(ClampedIntProvider { source: Box::new(#src), min_inclusive: #min, max_inclusive: #max })) }
                }
                "minecraft:trapezoid" => {
                    let min = v["min"].as_i64().unwrap_or(0) as i32;
                    let max = v["max"].as_i64().unwrap_or(0) as i32;
                    let plateau = v["plateau"].as_i64().unwrap_or(0) as i32;
                    quote! { IntProvider::Object(NormalIntProvider::Trapezoid(TrapezoidIntProvider { min_inclusive: #min, max_inclusive: #max, plateau: #plateau })) }
                }
                "minecraft:clamped_normal" => {
                    let mean = v["mean"].as_f64().unwrap_or(0.0) as f32;
                    let dev = v["deviation"].as_f64().unwrap_or(1.0) as f32;
                    let min = v["min_inclusive"].as_i64().unwrap_or(0) as i32;
                    let max = v["max_inclusive"].as_i64().unwrap_or(0) as i32;
                    quote! { IntProvider::Object(NormalIntProvider::ClampedNormal(ClampedNormalIntProvider { mean: #mean, deviation: #dev, min_inclusive: #min, max_inclusive: #max })) }
                }
                "minecraft:weighted_list" => {
                    let entries: Vec<TokenStream> = v["distribution"]
                        .as_array()
                        .map(|arr| {
                            arr.iter()
                                .map(|e| {
                                    let data = value_to_int_provider(&e["data"]);
                                    let weight = e["weight"].as_i64().unwrap_or(1) as i32;
                                    quote! { WeightedEntry { data: #data, weight: #weight } }
                                })
                                .collect()
                        })
                        .unwrap_or_default();
                    quote! { IntProvider::Object(NormalIntProvider::WeightedList(WeightedListIntProvider { distribution: vec![#(#entries),*] })) }
                }
                _ => {
                    panic!(
                        "Unknown Int Provider, Seems like Mojang added a new one: {:?}",
                        v
                    )
                }
            }
        }
        _ => panic!(
            "Unknown Int Provider, Seems like Mojang added a new one: {:?}",
            v
        ),
    }
}

/// Converts a SCREAMING_SNAKE_CASE heightmap name into its `HeightMap` token stream.
///
/// # Arguments
/// - `s` – Heightmap name string (e.g. `"MOTION_BLOCKING"`).
///
/// # Returns
/// A `HeightMap` variant token stream; defaults to `HeightMap::MotionBlocking` when unrecognized.
pub fn value_to_height_map(s: &str) -> TokenStream {
    match s {
        "WORLD_SURFACE_WG" => quote! { HeightMap::WorldSurfaceWg },
        "WORLD_SURFACE" => quote! { HeightMap::WorldSurface },
        "OCEAN_FLOOR_WG" => quote! { HeightMap::OceanFloorWg },
        "OCEAN_FLOOR" => quote! { HeightMap::OceanFloor },
        "MOTION_BLOCKING" => quote! { HeightMap::MotionBlocking },
        "MOTION_BLOCKING_NO_LEAVES" => quote! { HeightMap::MotionBlockingNoLeaves },
        _ => panic!("Unknown Height map"),
    }
}

/// Converts a lowercase direction string into its `BlockDirection` token stream.
///
/// # Arguments
/// - `s` – Direction name (e.g. `"down"`, `"up"`, `"north"`, etc.).
///
/// # Returns
/// A `BlockDirection` variant token stream; defaults to `BlockDirection::Down` when unrecognized.
pub fn value_to_block_direction(s: &str) -> TokenStream {
    match s.to_lowercase().as_str() {
        "down" => quote! { BlockDirection::Down },
        "up" => quote! { BlockDirection::Up },
        "north" => quote! { BlockDirection::North },
        "south" => quote! { BlockDirection::South },
        "west" => quote! { BlockDirection::West },
        "east" => quote! { BlockDirection::East },
        _ => panic!("Unknown Block direction"),
    }
}

/// Converts an optional offset JSON value into an `OffsetBlocksBlockPredicate` token stream.
///
/// # Arguments
/// - `v` – Either `null`/empty object (no offset) or a 3-element integer array `[x, y, z]`.
///
/// # Returns
/// An `OffsetBlocksBlockPredicate` with `offset: None` or `offset: Some(Vector3::new(x, y, z))`.
fn value_to_offset_predicate(v: &Value) -> TokenStream {
    if v.is_null() || v.is_object() && v.as_object().is_none_or(|o| o.is_empty()) {
        quote! { OffsetBlocksBlockPredicate { offset: None } }
    } else if v.is_array() {
        let x = v[0].as_i64().unwrap_or(0) as i32;
        let y = v[1].as_i64().unwrap_or(0) as i32;
        let z = v[2].as_i64().unwrap_or(0) as i32;
        quote! { OffsetBlocksBlockPredicate { offset: Some(Vector3::new(#x, #y, #z)) } }
    } else {
        quote! { OffsetBlocksBlockPredicate { offset: None } }
    }
}

/// Converts a blocks/fluids JSON value into a `MatchingBlocksWrapper` token stream.
///
/// # Arguments
/// - `v` – Either a single string (one block/fluid name) or an array of strings.
///
/// # Returns
/// `MatchingBlocksWrapper::Single(…)` or `MatchingBlocksWrapper::Multiple(…)`.
fn value_to_matching_blocks_wrapper(v: &Value) -> TokenStream {
    match v {
        Value::String(s) => {
            quote! { MatchingBlocksWrapper::Single(#s.to_string()) }
        }
        Value::Array(arr) => {
            let items: Vec<TokenStream> = arr
                .iter()
                .filter_map(|s| s.as_str().map(|s| quote! { #s.to_string() }))
                .collect();
            quote! { MatchingBlocksWrapper::Multiple(vec![#(#items),*]) }
        }
        _ => quote! { MatchingBlocksWrapper::Single(String::new()) },
    }
}

/// Converts a block-state JSON object into a `BlockStateCodec` token stream.
///
/// # Arguments
/// - `v` – JSON object with a `"Name"` field and optional `"Properties"` map.
///
/// # Returns
/// A `BlockStateCodec` token stream referencing the corresponding `Block` constant and optional property map.
pub fn value_to_block_state_codec(v: &Value) -> TokenStream {
    let name = v["Name"].as_str().unwrap_or("minecraft:air");
    let name_stripped = name.strip_prefix("minecraft:").unwrap_or(name);
    let block_ident =
        quote::format_ident!("{}", name_stripped.to_uppercase().replace([':', '-'], "_"));
    if let Some(props) = v["Properties"].as_object() {
        let keys: Vec<&str> = props.keys().map(|k| k.as_str()).collect();
        let vals: Vec<&str> = props.values().filter_map(|v| v.as_str()).collect();
        quote! {
            {
                let mut props = std::collections::HashMap::new();
                #(props.insert(#keys.to_string(), #vals.to_string());)*
                BlockStateCodec {
                    name: &pumpkin_data::Block::#block_ident,
                    properties: Some(props),
                }
            }
        }
    } else {
        quote! {
            BlockStateCodec {
                name: &pumpkin_data::Block::#block_ident,
                properties: None,
            }
        }
    }
}

pub fn value_to_block_state(v: &Value) -> TokenStream {
    let name = v["Name"].as_str().unwrap_or("minecraft:air");
    let name_stripped = name.strip_prefix("minecraft:").unwrap_or(name);
    let block_ident =
        quote::format_ident!("{}", name_stripped.to_uppercase().replace([':', '-'], "_"));
    if let Some(props) = v["Properties"].as_object() {
        let keys: Vec<&str> = props.keys().map(|k| k.as_str()).collect();
        let vals: Vec<&str> = props.values().filter_map(|v| v.as_str()).collect();
        quote! {
            {
                let mut props = std::collections::HashMap::new();
                #(props.insert(#keys.to_string(), #vals.to_string());)*
                BlockStateCodec {
                    name: &pumpkin_data::Block::#block_ident,
                    properties: Some(props),
                }.get_state()
            }
        }
    } else {
        quote! {
            pumpkin_data::Block::#block_ident.default_state
        }
    }
}

/// Converts a nullable JSON integer into an `Option<i32>` token stream.
///
/// # Arguments
/// - `v` – A JSON number or `null`.
///
/// # Returns
/// `Some(n)` if `v` is a valid integer, or `None` otherwise.
fn value_to_option_i32(v: &Value) -> TokenStream {
    if v.is_null() {
        quote! { None }
    } else if let Some(n) = v.as_i64() {
        let val = n as i32;
        quote! { Some(#val) }
    } else {
        quote! { None }
    }
}

/// Stub for configured feature inline in placed feature
fn value_to_inline_configured_feature(_v: &Value) -> TokenStream {
    quote! { crate::generation::feature::configured_features::ConfiguredFeature::NoOp }
}
