use crate::network::*;
use crate::packet::*;
use basin2_lib::result::*;
use bytes::BytesMut;
use basin2_data::{ RecipeSerializer, SimpleCookingSerializer };

struct RecipeSerializerNetworked();

impl RecipeSerializerNetworked {
    fn name(serializer: &RecipeSerializer) -> &'static str {
        use RecipeSerializer::*;

        match serializer {
            CraftingShaped { .. } => "crafting_shaped",
            CraftingShapeless { .. } => "crafting_shapeless",
            CraftingSpecialArmordye => "crafting_special_armordye",
            CraftingSpecialBookcloning => "crafting_special_bookcloning",
            CraftingSpecialMapcloning => "crafting_special_mapcloning",
            CraftingSpecialMapextending => "crafting_special_mapextending",
            CraftingSpecialFireworkRocket => "crafting_special_firework_rocket",
            CraftingSpecialFireworkStar => "crafting_special_firework_star",
            CraftingSpecialFireworkStarFade => "crafting_special_firework_star_fade",
            CraftingSpecialTippedarrow => "crafting_special_tippedarrow",
            CraftingSpecialBannerduplicate => "crafting_special_bannerduplicate",
            CraftingSpecialShielddecoration => "crafting_special_shielddecoration",
            CraftingSpecialShulkerboxcoloring => "crafting_special_shulkerboxcoloring",
            CraftingSpecialSuspiciousstew => "crafting_special_suspiciousstew",
            CraftingSpecialRepairitem => "crafting_special_repairitem",
            Smelting(_) => "smelting",
            Blasting(_) => "blasting",
            Smoking(_) => "smoking",
            CampfireCooking(_) => "campfire_cooking",
            Stonecutting { .. } => "stonecutting",
        }
    }

    fn parse_ingredient(buf: &mut BytesMut) -> Result<Vec<ItemStack>> {
        let count = buf.get_mc_var_int()?;
        let mut items: Vec<ItemStack> = vec![];
        for _ in 0..count {
            items.push(buf.get_mc_item_stack()?);
        }
        Ok(items)
    }

    fn write_ingredient(ingredient: Vec<ItemStack>, buf: &mut BytesMut) {
        buf.set_mc_var_int(ingredient.len() as i32);
        for item in ingredient {
            buf.set_mc_item_stack(item);
        }
    }

    fn parse_shaped(buf: &mut BytesMut) -> Result<RecipeSerializer> {
        use RecipeSerializer::*;

        let width = buf.get_mc_var_int()?;
        let height = buf.get_mc_var_int()?;
        let group = buf.get_mc_string(32767)?;
        let mut recipeItems: Vec<Vec<Vec<ItemStack>>> = vec![];
        for _ in 0..height {
            let mut row: Vec<Vec<ItemStack>> = vec![];
            for _ in 0..width {
                row.push(RecipeSerializerNetworked::parse_ingredient(buf)?);
            }
            recipeItems.push(row);
        }
        let result = buf.get_mc_item_stack()?;
        Ok(CraftingShaped {
            width,
            height,
            group: Some(group),
            recipeItems,
            result,
        })
    }

    fn parse_shapeless(buf: &mut BytesMut) -> Result<RecipeSerializer> {
        use RecipeSerializer::*;

        let group = buf.get_mc_string(32767)?;
        let count = buf.get_mc_var_int()?;
        let mut ingredients: Vec<Vec<ItemStack>> = vec![];
        for _ in 0..count {
            ingredients.push(RecipeSerializerNetworked::parse_ingredient(buf)?);
        }
        let result = buf.get_mc_item_stack()?;
        Ok(CraftingShapeless {
            group: Some(group),
            ingredients,
            result,
        })
    }

    fn parse_cooking(buf: &mut BytesMut) -> Result<SimpleCookingSerializer> {
        let group = buf.get_mc_string(32767)?;
        let ingredient = RecipeSerializerNetworked::parse_ingredient(buf)?;
        let result = buf.get_mc_item_stack()?;
        let experience = buf.get_mc_f32()?;
        let cookingTime = buf.get_mc_var_int()?;
        Ok(SimpleCookingSerializer {
            group: Some(group),
            ingredient,
            result,
            experience,
            cookingTime,
        })
    }

    fn from(value: String, buf: &mut BytesMut) -> Result<RecipeSerializer> {
        use RecipeSerializer::*;

        Ok(match &*value {
            "crafting_shaped" => RecipeSerializerNetworked::parse_shaped(buf)?,
            "crafting_shapeless" => RecipeSerializerNetworked::parse_shapeless(buf)?,
            "crafting_special_armordye" => CraftingSpecialArmordye,
            "crafting_special_bookcloning" => CraftingSpecialBookcloning,
            "crafting_special_mapcloning" => CraftingSpecialMapcloning,
            "crafting_special_mapextending" => CraftingSpecialMapextending,
            "crafting_special_firework_rocket" => CraftingSpecialFireworkRocket,
            "crafting_special_firework_star" => CraftingSpecialFireworkStar,
            "crafting_special_firework_star_fade" => CraftingSpecialFireworkStarFade,
            "crafting_special_tippedarrow" => CraftingSpecialTippedarrow,
            "crafting_special_bannerduplicate" => CraftingSpecialBannerduplicate,
            "crafting_special_shielddecoration" => CraftingSpecialShielddecoration,
            "crafting_special_shulkerboxcoloring" => CraftingSpecialShulkerboxcoloring,
            "crafting_special_suspiciousstew" => CraftingSpecialSuspiciousstew,
            "crafting_special_repairitem" => CraftingSpecialRepairitem,
            "smelting" => Smelting(RecipeSerializerNetworked::parse_cooking(buf)?),
            "blasting" => Blasting(RecipeSerializerNetworked::parse_cooking(buf)?),
            "smoking" => Smoking(RecipeSerializerNetworked::parse_cooking(buf)?),
            "campfire_cooking" => CampfireCooking(RecipeSerializerNetworked::parse_cooking(buf)?),
            "stonecutting" => Stonecutting {
                group: Some(buf.get_mc_string(32767)?),
                ingredient: RecipeSerializerNetworked::parse_ingredient(buf)?,
                result: buf.get_mc_item_stack()?,
            },
            _ => return Err(Box::new(IoError::from(ErrorKind::InvalidData))),
        })
    }
}

#[derive(PartialEq, Clone, Debug)]
pub struct UpdateRecipesPacket {
    pub recipes: Vec<(String, RecipeSerializer)>,
}

impl CodablePacket for UpdateRecipesPacket {
    fn encode(self, buf: &mut BytesMut) {
        use RecipeSerializer::*;

        buf.set_mc_var_int(self.recipes.len() as i32);
        for (id, recipe) in self.recipes {
            buf.set_mc_string(RecipeSerializerNetworked::name(&recipe).to_string());
            buf.set_mc_string(id);
            match recipe {
                CraftingShaped {
                    width,
                    height,
                    group,
                    recipeItems,
                    result,
                } => {
                    buf.set_mc_var_int(width);
                    buf.set_mc_var_int(height);
                    buf.set_mc_string(group.unwrap_or("".to_string()));
                    for ingredient in recipeItems {
                        for item in ingredient {
                            RecipeSerializerNetworked::write_ingredient(item, buf);
                        }
                    }
                    buf.set_mc_item_stack(result);
                }
                CraftingShapeless {
                    group,
                    ingredients,
                    result,
                } => {
                    buf.set_mc_string(group.unwrap_or("".to_string()));
                    buf.set_mc_var_int(ingredients.len() as i32);
                    for ingredient in ingredients {
                        RecipeSerializerNetworked::write_ingredient(ingredient, buf);
                    }
                    buf.set_mc_item_stack(result);
                }
                Smelting(cooking)
                | Blasting(cooking)
                | Smoking(cooking)
                | CampfireCooking(cooking) => {
                    buf.set_mc_string(cooking.group.unwrap_or("".to_string()));
                    RecipeSerializerNetworked::write_ingredient(cooking.ingredient, buf);
                    buf.set_mc_item_stack(cooking.result);
                    buf.set_mc_f32(cooking.experience);
                    buf.set_mc_var_int(cooking.cookingTime);
                }
                Stonecutting {
                    group,
                    ingredient,
                    result,
                } => {
                    buf.set_mc_string(group.unwrap_or("".to_string()));
                    RecipeSerializerNetworked::write_ingredient(ingredient, buf);
                    buf.set_mc_item_stack(result);
                }
                _ => (),
            }
        }
    }

    fn decode(buf: &mut BytesMut) -> Result<Self>
    where
        Self: Sized,
    {
        let count = buf.get_mc_var_int()?;
        let mut recipes: Vec<(String, RecipeSerializer)> = vec![];
        for _ in 0..count {
            let serializer = buf.get_mc_string(32767)?;
            let id = buf.get_mc_string(32767)?;
            let serializer = RecipeSerializerNetworked::from(serializer, buf)?;
            recipes.push((id, serializer));
        }
        return Ok(UpdateRecipesPacket { recipes });
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::packet::test::*;

    #[test]
    fn test_cycle_shaped() -> Result<()> {
        cycle(UpdateRecipesPacket {
            recipes: vec![
                (
                    "shaped recipe".to_string(),
                    RecipeSerializer::CraftingShaped {
                        width: 2,
                        height: 2,
                        group: Some("group".to_string()),
                        recipeItems: vec![
                            vec![vec![ItemStack::empty()], vec![ItemStack::empty()]],
                            vec![vec![ItemStack::empty()], vec![ItemStack::empty()]],
                        ],
                        result: ItemStack::empty(),
                    },
                ),
            ],
        })
    }

    #[test]
    fn test_cycle_shapeless() -> Result<()> {
        cycle(UpdateRecipesPacket {
            recipes: vec![
                (
                    "shapeless recipe".to_string(),
                    RecipeSerializer::CraftingShapeless {
                        group: Some("group".to_string()),
                        ingredients: vec![
                            vec![ItemStack::empty()],
                            vec![ItemStack::empty()],
                            vec![ItemStack::empty()],
                            vec![ItemStack::empty()],
                        ],
                        result: ItemStack::empty(),
                    },
                ),
            ],
        })
    }

    #[test]
    fn test_cycle_simple() -> Result<()> {
        cycle(UpdateRecipesPacket {
            recipes: vec![
                (
                    "simple recipe".to_string(),
                    RecipeSerializer::CraftingSpecialArmordye,
                ),
            ],
        })
    }

    #[test]
    fn test_cycle_smelting() -> Result<()> {
        cycle(UpdateRecipesPacket {
            recipes: vec![
                (
                    "smelting recipe".to_string(),
                    RecipeSerializer::Smelting(SimpleCookingSerializer {
                        group: Some("group".to_string()),
                        ingredient: vec![ItemStack::empty(), ItemStack::empty()],
                        result: ItemStack::empty(),
                        experience: 120.0,
                        cookingTime: 300,
                    }),
                ),
            ],
        })
    }

    #[test]
    fn test_cycle_stonecutting() -> Result<()> {
        cycle(UpdateRecipesPacket {
            recipes: vec![
                (
                    "stonecutting recipe".to_string(),
                    RecipeSerializer::Stonecutting {
                        group: Some("group".to_string()),
                        ingredient: vec![ItemStack::empty(), ItemStack::empty()],
                        result: ItemStack::empty(),
                    },
                ),
            ],
        })
    }
}
