use crate::entity::EntityBase;
use pumpkin_data::BlockDirection;
use pumpkin_data::BlockStateId;
use pumpkin_data::block_properties::Axis;
use pumpkin_data::block_properties::BlockProperties;
use pumpkin_data::block_properties::DoorHinge;
use pumpkin_data::block_properties::DoubleBlockHalf;
use pumpkin_data::block_properties::HorizontalFacing;
use pumpkin_data::sound::Sound;
use pumpkin_data::sound::SoundCategory;
use pumpkin_data::tag::Taggable;
use pumpkin_data::{Block, tag};
use pumpkin_macros::pumpkin_block_from_tag;
use pumpkin_util::math::position::BlockPos;
use pumpkin_world::world::BlockAccessor;
use pumpkin_world::world::BlockFlags;
use std::sync::Arc;

use crate::block::BlockBehaviour;
use crate::block::BlockFuture;
use crate::block::BrokenArgs;
use crate::block::CanPlaceAtArgs;
use crate::block::GetStateForNeighborUpdateArgs;
use crate::block::NormalUseArgs;
use crate::block::OnNeighborUpdateArgs;
use crate::block::OnPlaceArgs;
use crate::block::OnStateReplacedArgs;
use crate::block::PlacedArgs;
use crate::block::blocks::redstone::block_receives_redstone_power;
use crate::block::registry::BlockActionResult;
use crate::entity::player::Player;
use pumpkin_protocol::java::server::play::SUseItemOn;

use crate::world::World;
use pumpkin_util::GameMode;

type DoorProperties = pumpkin_data::block_properties::OakDoorLikeProperties;

async fn toggle_door(player: &Player, world: &Arc<World>, block_pos: &BlockPos) {
    let (block, block_state) = world.get_block_and_state_id(block_pos);
    let mut door_props = DoorProperties::from_state_id(block_state, block);
    door_props.open = !door_props.open;

    let other_half = match door_props.half {
        DoubleBlockHalf::Upper => BlockDirection::Down,
        DoubleBlockHalf::Lower => BlockDirection::Up,
    };
    let other_pos = block_pos.offset(other_half.to_offset());

    let (other_block, other_state_id) = world.get_block_and_state_id(&other_pos);
    let mut other_door_props = DoorProperties::from_state_id(other_state_id, other_block);
    other_door_props.open = door_props.open;

    world.play_block_sound_expect(
        player,
        get_sound(block, door_props.open),
        SoundCategory::Blocks,
        *block_pos,
    );

    world
        .set_block_state(
            block_pos,
            door_props.to_state_id(block),
            BlockFlags::NOTIFY_LISTENERS,
        )
        .await;
    world
        .set_block_state(
            &other_pos,
            other_door_props.to_state_id(other_block),
            BlockFlags::NOTIFY_LISTENERS,
        )
        .await;
}

fn can_open_door(block: &Block) -> bool {
    if block == &Block::IRON_DOOR {
        return false;
    }

    true
}

// Todo: The sounds should be from BlockSetType
fn get_sound(block: &Block, open: bool) -> Sound {
    if open {
        if block.has_tag(&tag::Block::MINECRAFT_WOODEN_DOORS) {
            Sound::BlockWoodenDoorOpen
        } else if block == &Block::IRON_DOOR {
            Sound::BlockIronDoorOpen
        } else {
            Sound::BlockCopperDoorOpen
        }
    } else if block.has_tag(&tag::Block::MINECRAFT_WOODEN_DOORS) {
        Sound::BlockWoodenDoorClose
    } else if block == &Block::IRON_DOOR {
        Sound::BlockIronDoorClose
    } else {
        Sound::BlockCopperDoorClose
    }
}

#[expect(clippy::pedantic)]
#[inline]
async fn get_hinge(
    world: &World,
    pos: &BlockPos,
    use_item: &SUseItemOn,
    facing: HorizontalFacing,
) -> DoorHinge {
    let top_pos = pos.up();
    let left_dir = facing.rotate_counter_clockwise();
    let left_pos = pos.offset(left_dir.to_offset());
    let (left_block, left_state) = world.get_block_and_state(&left_pos);
    let top_facing = top_pos.offset(facing.to_offset());
    let top_state = world.get_block_state(&top_facing);
    let right_dir = facing.rotate_clockwise();
    let right_pos = pos.offset(right_dir.to_offset());
    let (right_block, right_state) = world.get_block_and_state(&right_pos);
    let top_right = top_pos.offset(facing.to_offset());
    let top_right_state = world.get_block_state(&top_right);

    let has_left_door = world
        .get_block(&left_pos)
        .has_tag(&tag::Block::MINECRAFT_DOORS)
        && DoorProperties::from_state_id(left_state.id, left_block).half == DoubleBlockHalf::Lower;

    let has_right_door = world
        .get_block(&right_pos)
        .has_tag(&tag::Block::MINECRAFT_DOORS)
        && DoorProperties::from_state_id(right_state.id, right_block).half
            == DoubleBlockHalf::Lower;

    let score = -(left_state.is_full_cube() as i32) - (top_state.is_full_cube() as i32)
        + right_state.is_full_cube() as i32
        + top_right_state.is_full_cube() as i32;

    if (!has_left_door || has_right_door) && score <= 0 {
        if (!has_right_door || has_left_door) && score >= 0 {
            let offset = facing.to_offset();
            let hit = use_item.cursor_pos;
            if (offset.x >= 0 || hit.z > 0.5)
                && (offset.x <= 0 || hit.z < 0.5)
                && (offset.z >= 0 || hit.x < 0.5)
                && (offset.z <= 0 || hit.x > 0.5)
            {
                DoorHinge::Left
            } else {
                DoorHinge::Right
            }
        } else {
            DoorHinge::Left
        }
    } else {
        DoorHinge::Right
    }
}

#[pumpkin_block_from_tag("minecraft:doors")]
pub struct DoorBlock;

impl BlockBehaviour for DoorBlock {
    fn on_place<'a>(&'a self, args: OnPlaceArgs<'a>) -> BlockFuture<'a, BlockStateId> {
        Box::pin(async move {
            let powered = block_receives_redstone_power(args.world, args.position).await
                || block_receives_redstone_power(args.world, &args.position.up()).await;

            let direction = args.player.get_entity().get_horizontal_facing();
            let hinge = get_hinge(args.world, args.position, args.use_item_on, direction).await;

            let mut door_props = DoorProperties::default(args.block);
            door_props.half = DoubleBlockHalf::Lower;
            door_props.facing = direction;
            door_props.hinge = hinge;
            door_props.powered = powered;
            door_props.open = powered;

            door_props.to_state_id(args.block)
        })
    }

    fn can_place_at(&self, args: CanPlaceAtArgs<'_>) -> bool {
        has_support(args.block_accessor, args.position)
            && args
                .block_accessor
                .get_block_state(&args.position.up())
                .replaceable()
    }

    fn placed<'a>(&'a self, args: PlacedArgs<'a>) -> BlockFuture<'a, ()> {
        Box::pin(async move {
            let mut door_props = DoorProperties::from_state_id(args.state_id, args.block);
            door_props.half = DoubleBlockHalf::Upper;

            args.world
                .set_block_state(
                    &args.position.offset(BlockDirection::Up.to_offset()),
                    door_props.to_state_id(args.block),
                    BlockFlags::NOTIFY_ALL | BlockFlags::SKIP_BLOCK_ADDED_CALLBACK,
                )
                .await;
        })
    }

    fn normal_use<'a>(&'a self, args: NormalUseArgs<'a>) -> BlockFuture<'a, BlockActionResult> {
        Box::pin(async move {
            if !can_open_door(args.block) {
                return BlockActionResult::Pass;
            }

            toggle_door(args.player, args.world, args.position).await;

            BlockActionResult::Success
        })
    }

    fn broken<'a>(&'a self, args: BrokenArgs<'a>) -> BlockFuture<'a, ()> {
        Box::pin(async move {
            let door_props = DoorProperties::from_state_id(args.state.id, args.block);
            let other_half_pos = match door_props.half {
                DoubleBlockHalf::Upper => args.position.down(),
                DoubleBlockHalf::Lower => args.position.up(),
            };

            let neighbor_state_id = args.world.get_block_state_id(&other_half_pos);
            if neighbor_state_id.to_block_id() != args.block.id {
                args.world.update_neighbors(&other_half_pos, None).await;
                return; // Neighbor is already gone or is a different block
            }

            let is_creative = args.player.gamemode.load() == GameMode::Creative;
            let flags = if door_props.half == DoubleBlockHalf::Upper && !is_creative {
                BlockFlags::NOTIFY_ALL
            } else {
                BlockFlags::SKIP_DROPS | BlockFlags::NOTIFY_ALL
            };

            args.world
                .break_block(&other_half_pos, Some(args.player.clone()), flags)
                .await;
        })
    }

    fn on_neighbor_update<'a>(&'a self, args: OnNeighborUpdateArgs<'a>) -> BlockFuture<'a, ()> {
        Box::pin(async move {
            let block_state = args.world.get_block_state(args.position);
            let mut door_props = DoorProperties::from_state_id(block_state.id, args.block);

            let other_half = match door_props.half {
                DoubleBlockHalf::Upper => BlockDirection::Down,
                DoubleBlockHalf::Lower => BlockDirection::Up,
            };
            let other_pos = args.position.offset(other_half.to_offset());
            let (other_block, other_state_id) = args.world.get_block_and_state_id(&other_pos);

            if other_block.id != args.block.id {
                return;
            }

            let powered = block_receives_redstone_power(args.world, args.position).await
                || block_receives_redstone_power(args.world, &other_pos).await;

            if args.block.id == other_block.id && powered != door_props.powered {
                let mut other_door_props =
                    DoorProperties::from_state_id(other_state_id, other_block);
                door_props.powered = !door_props.powered;
                other_door_props.powered = door_props.powered;

                if powered != door_props.open {
                    door_props.open = door_props.powered;
                    other_door_props.open = other_door_props.powered;

                    args.world.play_block_sound(
                        get_sound(args.block, powered),
                        SoundCategory::Blocks,
                        *args.position,
                    );
                }

                args.world
                    .set_block_state(
                        args.position,
                        door_props.to_state_id(args.block),
                        BlockFlags::NOTIFY_ALL,
                    )
                    .await;
                args.world
                    .set_block_state(
                        &other_pos,
                        other_door_props.to_state_id(other_block),
                        BlockFlags::NOTIFY_ALL,
                    )
                    .await;
            }
        })
    }

    fn get_state_for_neighbor_update<'a>(
        &'a self,
        args: GetStateForNeighborUpdateArgs<'a>,
    ) -> BlockFuture<'a, BlockStateId> {
        Box::pin(async move {
            let lv = DoorProperties::from_state_id(args.state_id, args.block).half;
            if args.direction.to_axis() != Axis::Y
                || (lv == DoubleBlockHalf::Lower) != (args.direction == BlockDirection::Up)
            {
                if lv == DoubleBlockHalf::Lower
                    && args.direction == BlockDirection::Down
                    && !has_support(args.world, args.position)
                {
                    return BlockStateId::AIR;
                }
            } else if Block::from_state_id(args.neighbor_state_id).id == args.block.id
                && DoorProperties::from_state_id(args.neighbor_state_id, args.block).half != lv
            {
                let mut new_state =
                    DoorProperties::from_state_id(args.neighbor_state_id, args.block);
                new_state.half = lv;
                return new_state.to_state_id(args.block);
            } else {
                return BlockStateId::AIR;
            }
            args.state_id
        })
    }

    fn on_state_replaced<'a>(&'a self, args: OnStateReplacedArgs<'a>) -> BlockFuture<'a, ()> {
        Box::pin(async move {
            if args.moved {
                return;
            }

            let new_state_id = args.world.get_block_state_id(args.position);
            let new_block = Block::from_state_id(new_state_id);
            if new_block == &Block::AIR {
                return;
            }

            let door_props = DoorProperties::from_state_id(args.old_state_id, args.block);
            let other_half_pos = match door_props.half {
                DoubleBlockHalf::Upper => args.position.down(),
                DoubleBlockHalf::Lower => args.position.up(),
            };

            args.world
                .break_block(
                    &other_half_pos,
                    None,
                    BlockFlags::SKIP_DROPS | BlockFlags::NOTIFY_ALL,
                )
                .await;
        })
    }
}

fn has_support(world: &dyn BlockAccessor, block_pos: &BlockPos) -> bool {
    world
        .get_block_state(&block_pos.down())
        .is_side_solid(BlockDirection::Up)
}
