use pumpkin_data::{item_stack::ItemStack, recipes::CookingRecipe};

use crate::block::entities::{BlockEntity, PropertyDelegate};
pub use pumpkin_world::block::entities::ExperienceContainer;
use pumpkin_world::inventory::{Clearable, Inventory};

/// Trait for extracting smelting experience from cooking block entities.
pub trait CookingBlockEntityBase:
    Sync + Send + Inventory + PropertyDelegate + BlockEntity + Clearable
{
    fn get_cooking_time_spent(&self) -> u16;
    fn get_cooking_total_time(&self) -> u16;
    fn get_lit_time_remaining(&self) -> u16;
    fn get_lit_total_time(&self) -> u16;

    /// Track that a recipe was used (for XP calculation on extraction)
    /// Uses the result item ID as the recipe identifier
    fn add_recipe_used(&self, recipe: &CookingRecipe);
    /// Extract and reset accumulated experience, returning the total as an integer
    /// Calculates XP from tracked recipes and clears the `recipes_used` map
    fn extract_experience_from_recipes(&self) -> i32;

    fn get_input_item(&self) -> impl std::future::Future<Output = ItemStack>;
    fn get_fuel_item(&self) -> impl std::future::Future<Output = ItemStack>;
    fn get_output_item(&self) -> impl std::future::Future<Output = ItemStack>;

    fn set_cooking_time_spent(&self, spent_time: u16);
    fn set_cooking_total_time(&self, total_time: u16);
    fn set_lit_time_remaining(&self, remaining_time: u16);
    fn set_lit_total_time(&self, total_time: u16);

    fn is_burning(&self) -> bool;
    fn can_accept_recipe_output(
        &self,
        recipe: Option<&CookingRecipe>,
        max_count: u8,
    ) -> impl Future<Output = bool>;
    fn craft_recipe(&self, recipe: Option<&CookingRecipe>) -> impl Future<Output = bool>;
}

#[macro_export]
macro_rules! impl_cooking_block_entity_base {
    ($struct_name:ty) => {
        impl CookingBlockEntityBase for $struct_name {
            fn get_cooking_time_spent(&self) -> u16 {
                self.cooking_time_spent.load(Ordering::Relaxed)
            }

            fn get_cooking_total_time(&self) -> u16 {
                self.cooking_total_time.load(Ordering::Relaxed)
            }

            fn get_lit_time_remaining(&self) -> u16 {
                self.lit_time_remaining.load(Ordering::Relaxed)
            }

            fn get_lit_total_time(&self) -> u16 {
                self.lit_total_time.load(Ordering::Relaxed)
            }

            fn get_input_item(&self) -> impl std::future::Future<Output = ItemStack> {
                async move {
                    let items = self.items.read().await;
                    items[0].clone()
                }
            }

            fn get_fuel_item(&self) -> impl std::future::Future<Output = ItemStack> {
                async move {
                    let items = self.items.read().await;
                    items[1].clone()
                }
            }

            fn get_output_item(&self) -> impl std::future::Future<Output = ItemStack> {
                async move {
                    let items = self.items.read().await;
                    items[2].clone()
                }
            }

            fn set_cooking_time_spent(&self, spent_time: u16) {
                self.cooking_time_spent.store(spent_time, Ordering::Relaxed);
            }

            fn set_cooking_total_time(&self, total_time: u16) {
                self.cooking_total_time.store(total_time, Ordering::Relaxed);
            }

            fn set_lit_time_remaining(&self, remaining_time: u16) {
                self.lit_time_remaining
                    .store(remaining_time, Ordering::Relaxed);
            }

            fn set_lit_total_time(&self, total_time: u16) {
                self.lit_total_time.store(total_time, Ordering::Relaxed);
            }

            fn is_burning(&self) -> bool {
                self.get_lit_time_remaining() > 0
            }

            fn add_recipe_used(&self, recipe: &pumpkin_data::recipes::CookingRecipe) {
                // Track recipe usage by recipe ID for XP calculation
                let recipe_id = recipe.recipe_id.to_string();
                let mut recipes = self
                    .recipes_used
                    .lock()
                    .unwrap_or_else(std::sync::PoisonError::into_inner);
                *recipes.entry(recipe_id).or_insert(0) += 1;
            }

            fn extract_experience_from_recipes(&self) -> i32 {
                // Calculate total XP from tracked recipes and clear the map (vanilla behavior)
                let mut recipes = self
                    .recipes_used
                    .lock()
                    .unwrap_or_else(std::sync::PoisonError::into_inner);
                let mut total_xp: f32 = 0.0;
                for (recipe_id, count) in recipes.iter() {
                    // Look up the recipe's XP value
                    if let Some(xp) = pumpkin_data::recipes::get_recipe_experience(recipe_id) {
                        total_xp += xp * (*count as f32);
                    }
                }
                recipes.clear();
                total_xp.floor() as i32
            }

            async fn can_accept_recipe_output(
                &self,
                recipe: Option<&pumpkin_data::recipes::CookingRecipe>,
                max_count: u8,
            ) -> bool {
                let Some(recipe) = recipe else { return false };
                let items = self.items.read().await;

                let is_top_items_empty = items[0].is_empty();
                let side_item_stack = &items[2];

                if side_item_stack.is_empty() {
                    return !is_top_items_empty;
                }

                if let Some(recipe_output_item) = pumpkin_data::item::Item::from_registry_key(
                    recipe
                        .result
                        .id
                        .strip_prefix("minecraft:")
                        .expect("Recipe ID should have minecraft: prefix"),
                ) && !is_top_items_empty
                    && recipe_output_item.id == side_item_stack.item.id
                    && side_item_stack.item_count < max_count
                    && side_item_stack.item_count < side_item_stack.get_max_stack_size()
                {
                    return true;
                }
                false
            }
            async fn craft_recipe(
                &self,
                recipe: Option<&pumpkin_data::recipes::CookingRecipe>,
            ) -> bool {
                let can_accept_output = self
                    .can_accept_recipe_output(recipe, self.get_max_count_per_stack())
                    .await;
                if let Some(recipe) = recipe {
                    if can_accept_output {
                        let mut items = self.items.write().await;
                        let Some(output_item) = pumpkin_data::item::Item::from_registry_key(
                            recipe
                                .result
                                .id
                                .strip_prefix("minecraft:")
                                .expect("Recipe ID should have minecraft: prefix"),
                        ) else {
                            return false;
                        };
                        let output_item_stack = ItemStack::new(recipe.result.count, output_item);

                        if items[2].are_equal(ItemStack::EMPTY) {
                            items[2] = output_item_stack;
                        } else if items[2].are_items_and_components_equal(&output_item_stack) {
                            items[2].increment(1);
                        }

                        // Track recipe usage for XP calculation (vanilla RecipesUsed format)
                        self.add_recipe_used(recipe);
                    }

                    let mut items = self.items.write().await;
                    if items[0].item.id == pumpkin_data::item::Item::WET_SPONGE.id
                        && !items[1].is_empty()
                        && items[1].item.id == pumpkin_data::item::Item::BUCKET.id
                    {
                        items[1] = ItemStack::new(1, &pumpkin_data::item::Item::WATER_BUCKET);
                    }

                    items[0].decrement(1);
                    return true;
                }

                false
            }
        }
    };
}

#[macro_export]
macro_rules! impl_property_delegate_for_cooking {
    ($struct_name:ty) => {
        impl $crate::block::entities::PropertyDelegate for $struct_name {
            fn get_property(&self, index: i32) -> i32 {
                match index {
                    0 => self.get_lit_time_remaining() as i32,
                    1 => self.get_lit_total_time() as i32,
                    2 => self.get_cooking_time_spent() as i32,
                    3 => self.get_cooking_total_time() as i32,
                    _ => 0,
                }
            }

            fn set_property(&self, index: i32, value: i32) {
                let value = value as u16;
                match index {
                    0 => self.set_lit_time_remaining(value),
                    1 => self.set_lit_total_time(value),
                    2 => self.set_cooking_time_spent(value),
                    3 => self.set_cooking_total_time(value),
                    _ => {}
                }
            }

            fn get_properties_size(&self) -> i32 {
                4
            }
        }
    };
}

#[macro_export]
macro_rules! impl_clearable_for_cooking {
    ($struct_name:ty) => {
        impl pumpkin_world::inventory::Clearable for $struct_name {
            fn clear(&self) -> std::pin::Pin<Box<dyn Future<Output = ()> + Send + '_>> {
                Box::pin(async move {
                    let mut items = self.items.write().await;
                    items.fill_with(|| ItemStack::EMPTY.clone());
                    self.mark_dirty();
                })
            }
        }
    };
}

#[macro_export]
macro_rules! impl_experience_container_for_cooking {
    ($struct_name:ty) => {
        impl $crate::block::entities::furnace_like_block_entity::ExperienceContainer
            for $struct_name
        {
            fn extract_experience(&self) -> i32 {
                // Delegate to the CookingBlockEntityBase method
                self.extract_experience_from_recipes()
            }
        }
    };
}

#[macro_export]
macro_rules! impl_inventory_for_cooking {
    ($struct_name:ty) => {
        impl pumpkin_world::inventory::Inventory for $struct_name {
            fn size(&self) -> usize {
                Self::INVENTORY_SIZE
            }

            fn is_empty(&self) -> pumpkin_world::inventory::InventoryFuture<'_, bool> {
                Box::pin(async move {
                    let items = self.items.read().await;
                    items.iter().all(|s| s.is_empty())
                })
            }

            fn get_stack(
                &self,
                slot: usize,
            ) -> pumpkin_world::inventory::InventoryFuture<'_, ItemStack> {
                Box::pin(async move {
                    let items = self.items.read().await;
                    items[slot].clone()
                })
            }

            fn remove_stack(
                &self,
                slot: usize,
            ) -> pumpkin_world::inventory::InventoryFuture<'_, ItemStack> {
                Box::pin(async move {
                    let mut items = self.items.write().await;
                    let removed = std::mem::replace(&mut items[slot], ItemStack::EMPTY.clone());
                    self.mark_dirty();
                    removed
                })
            }

            fn remove_stack_specific(
                &self,
                slot: usize,
                amount: u8,
            ) -> pumpkin_world::inventory::InventoryFuture<'_, ItemStack> {
                Box::pin(async move {
                    let mut items = self.items.write().await;
                    let res = if !items[slot].is_empty() && amount > 0 {
                        items[slot].split(amount)
                    } else {
                        ItemStack::EMPTY.clone()
                    };
                    self.mark_dirty();
                    res
                })
            }

            fn set_stack(
                &self,
                slot: usize,
                stack: ItemStack,
            ) -> pumpkin_world::inventory::InventoryFuture<'_, ()> {
                Box::pin(async move {
                    let mut items = self.items.write().await;
                    let is_same_item = !stack.is_empty()
                        && ItemStack::are_items_and_components_equal(&items[slot], &stack);

                    items[slot] = stack.clone();

                    if slot == 0 && !is_same_item {
                        if let Some(recipe) =
                            pumpkin_data::recipes::get_cooking_recipe_with_ingredient(
                                stack.item,
                                CookingRecipeKind::Smelting,
                            )
                        {
                            self.set_cooking_total_time(recipe.cookingtime as u16);
                        } else {
                            self.set_cooking_total_time(0);
                        }
                        self.set_cooking_time_spent(0);
                    }

                    // Always consider the inventory changed when setting a stack
                    self.mark_dirty();
                })
            }

            fn mark_dirty(&self) {
                self.dirty.store(true, Ordering::Relaxed);
            }

            fn as_any(&self) -> &dyn std::any::Any {
                self
            }
        }
    };
}

#[macro_export]
macro_rules! impl_block_entity_for_cooking {
    ($struct_name:ty,$recipe_kind:expr) => {
        impl $crate::block::entities::BlockEntity for $struct_name {
            #[expect(clippy::too_many_lines)]
            fn tick<'a>(
                &'a self,
                world: &'a Arc<$crate::world::World>,
            ) -> std::pin::Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
                Box::pin(async move {
                    let is_burning = self.is_burning();
                    let mut is_dirty = false;
                    if self.is_burning() {
                        self.lit_time_remaining.fetch_sub(1, Ordering::Relaxed);
                    }

                    let items_guard = self.items.read().await;
                    let top_item = items_guard[0].clone();
                    let bottom_item = items_guard[1].clone();
                    drop(items_guard);

                    let is_top_items_empty = top_item.is_empty();

                    let furnace_recipe = pumpkin_data::recipes::get_cooking_recipe_with_ingredient(
                        top_item.item,
                        $recipe_kind,
                    );

                    let can_accept_output = self
                        .can_accept_recipe_output(furnace_recipe, self.get_max_count_per_stack())
                        .await;

                    let bottom_items_is_empty = bottom_item.is_empty();
                    if self.is_burning() || !bottom_items_is_empty && !is_top_items_empty {
                        if !self.is_burning() && can_accept_output {
                            let base_fuel_ticks =
                                pumpkin_data::fuels::get_item_burn_ticks(bottom_item.item.id)
                                    .unwrap_or(0);

                            let adjusted_fuel_ticks = if matches!(
                                $recipe_kind,
                                CookingRecipeKind::Blasting | CookingRecipeKind::Smoking
                            ) {
                                base_fuel_ticks / 2
                            } else {
                                base_fuel_ticks
                            };

                            self.set_lit_time_remaining(adjusted_fuel_ticks);
                            self.set_lit_total_time(adjusted_fuel_ticks);

                            if self.is_burning() {
                                is_dirty = true;
                                let mut items_guard = self.items.write().await;
                                if !items_guard[1].is_empty() {
                                    items_guard[1].decrement(1);
                                    if let Some(remainder_id) =
                                        pumpkin_data::recipe_remainder::get_recipe_remainder_id(
                                            items_guard[1].item.id,
                                        )
                                        && items_guard[1].is_empty()
                                        && let Some(remainder_item) =
                                            pumpkin_data::item::Item::from_id(remainder_id)
                                    {
                                        items_guard[1] = ItemStack::new(1, remainder_item);
                                    }
                                }
                            }
                        }

                        if self.is_burning() && can_accept_output {
                            self.cooking_time_spent.fetch_add(1, Ordering::Relaxed);

                            if self.get_cooking_time_spent() == self.get_cooking_total_time() {
                                self.set_cooking_time_spent(0);
                                if let Some(cooking_recipe) = furnace_recipe {
                                    let cooking_total_time = cooking_recipe.cookingtime;
                                    self.set_cooking_total_time(cooking_total_time as u16);

                                    self.craft_recipe(Some(cooking_recipe)).await;
                                    is_dirty = true;
                                }
                            }
                        } else {
                            self.set_cooking_time_spent(0);
                        }
                    } else if !self.is_burning() && self.get_cooking_time_spent() > 0 {
                        let _ = self.cooking_time_spent.try_update(
                            Ordering::Acquire,
                            Ordering::Acquire,
                            |v| {
                                Some(
                                    v.saturating_sub(2)
                                        .min(self.cooking_total_time.load(Ordering::Acquire)),
                                )
                            },
                        );
                    }

                    if is_burning != self.is_burning() {
                        is_dirty = true;
                        let world = world.clone();

                        let (furnace_block, furnace_block_state) =
                            world.get_block_and_state(&self.position);
                        let mut props =
                            pumpkin_data::block_properties::FurnaceLikeProperties::from_state_id(
                                furnace_block_state.id,
                                furnace_block,
                            );

                        if self.is_burning() {
                            props.lit = true;
                            world
                                .set_block_state(
                                    &self.position,
                                    props.to_state_id(furnace_block),
                                    $crate::world::BlockFlags::NOTIFY_ALL,
                                )
                                .await;
                        } else {
                            props.lit = false;
                            world
                                .set_block_state(
                                    &self.position,
                                    props.to_state_id(furnace_block),
                                    $crate::world::BlockFlags::NOTIFY_ALL,
                                )
                                .await;
                        }
                    }

                    if is_dirty {
                        self.mark_dirty();
                    }
                })
            }

            fn resource_location(&self) -> &'static str {
                Self::ID
            }

            fn get_position(&self) -> BlockPos {
                self.position
            }

            fn from_nbt(nbt: &pumpkin_nbt::compound::NbtCompound, position: BlockPos) -> Self
            where
                Self: Sized,
            {
                let cooking_total_time = AtomicU16::new(
                    nbt.get_short("cooking_total_time")
                        .map_or(0, |cooking_total_time| cooking_total_time as u16),
                );
                let cooking_time_spent = AtomicU16::new(
                    nbt.get_short("cooking_time_spent")
                        .map_or(0, |cooking_time_spent| cooking_time_spent as u16),
                );
                let lit_total_time = AtomicU16::new(
                    nbt.get_short("lit_total_time")
                        .map_or(0, |lit_total_time| lit_total_time as u16),
                );
                let lit_time_remaining = AtomicU16::new(
                    nbt.get_short("lit_time_remaining")
                        .map_or(0, |lit_time_remaining| lit_time_remaining as u16),
                );
                // Load RecipesUsed from NBT (vanilla format: map of recipe ID -> craft count)
                let mut recipes_used_map = HashMap::new();
                if let Some(recipes_compound) = nbt.get_compound("RecipesUsed") {
                    for (recipe_id, tag) in &recipes_compound.child_tags {
                        if let pumpkin_nbt::tag::NbtTag::Int(count) = tag {
                            recipes_used_map.insert(recipe_id.to_string(), *count as u32);
                        }
                    }
                }

                let mut furnace = Self {
                    position,
                    dirty: AtomicBool::new(false),
                    items: tokio::sync::RwLock::new(from_fn(|_| ItemStack::EMPTY.clone())),
                    cooking_total_time,
                    cooking_time_spent,
                    lit_total_time,
                    lit_time_remaining,
                    recipes_used: std::sync::Mutex::new(recipes_used_map),
                };
                pumpkin_world::inventory::sync_read_items_from_nbt(nbt, furnace.items.get_mut());

                furnace
            }

            fn write_nbt<'a>(
                &'a self,
                nbt: &'a mut pumpkin_nbt::compound::NbtCompound,
            ) -> std::pin::Pin<Box<dyn Future<Output = ()> + Send + 'a>> {
                Box::pin(async move {
                    nbt.put_short("cooking_total_time", self.get_cooking_total_time() as i16);
                    nbt.put_short("cooking_time_spent", self.get_cooking_time_spent() as i16);
                    nbt.put_short("lit_total_time", self.get_lit_total_time() as i16);
                    nbt.put_short("lit_time_remaining", self.get_lit_time_remaining() as i16);

                    // Save RecipesUsed in vanilla format (map of recipe ID -> craft count)
                    // Scope the mutex guard so it's dropped before the await
                    {
                        let recipes = self
                            .recipes_used
                            .lock()
                            .unwrap_or_else(std::sync::PoisonError::into_inner);
                        if !recipes.is_empty() {
                            let mut recipes_compound = pumpkin_nbt::compound::NbtCompound::new();
                            for (recipe_id, count) in recipes.iter() {
                                recipes_compound.put(
                                    recipe_id.as_str(),
                                    pumpkin_nbt::tag::NbtTag::Int(*count as i32),
                                );
                            }
                            nbt.put(
                                "RecipesUsed",
                                pumpkin_nbt::tag::NbtTag::Compound(recipes_compound),
                            );
                        }
                    }

                    self.write_inventory_nbt(nbt, true).await;
                })
            }

            fn get_inventory(
                self: Arc<Self>,
            ) -> Option<Arc<dyn pumpkin_world::inventory::Inventory>> {
                Some(self)
            }

            fn is_dirty(&self) -> bool {
                self.dirty.load(Ordering::Relaxed)
            }

            fn chunk_data_nbt(&self) -> Option<pumpkin_nbt::compound::NbtCompound> {
                let mut nbt = pumpkin_nbt::compound::NbtCompound::new();
                nbt.put_short("cooking_total_time", self.get_cooking_total_time() as i16);
                nbt.put_short("cooking_time_spent", self.get_cooking_time_spent() as i16);
                nbt.put_short("lit_total_time", self.get_lit_total_time() as i16);
                nbt.put_short("lit_time_remaining", self.get_lit_time_remaining() as i16);

                if let Ok(recipes) = self.recipes_used.lock() {
                    if !recipes.is_empty() {
                        let mut recipes_compound = pumpkin_nbt::compound::NbtCompound::new();
                        for (recipe_id, count) in recipes.iter() {
                            recipes_compound.put(
                                recipe_id.as_str(),
                                pumpkin_nbt::tag::NbtTag::Int(*count as i32),
                            );
                        }
                        nbt.put(
                            "RecipesUsed",
                            pumpkin_nbt::tag::NbtTag::Compound(recipes_compound),
                        );
                    }
                }

                if let Ok(guard) = self.items.try_read() {
                    pumpkin_world::inventory::sync_write_items_to_nbt(&*guard, &mut nbt);
                }
                Some(nbt)
            }

            fn as_any(&self) -> &dyn std::any::Any {
                self
            }

            fn to_property_delegate(
                self: Arc<Self>,
            ) -> Option<Arc<dyn $crate::block::entities::PropertyDelegate>> {
                Some(self as Arc<dyn $crate::block::entities::PropertyDelegate>)
            }

            fn to_experience_container(
                self: Arc<Self>,
            ) -> Option<
                Arc<dyn $crate::block::entities::furnace_like_block_entity::ExperienceContainer>,
            > {
                Some(
                    self as Arc<
                        dyn $crate::block::entities::furnace_like_block_entity::ExperienceContainer,
                    >,
                )
            }
        }
    };
}
