use std::io::Write;

use anyhow::bail;
use vek::Vec3;

use crate::client::BlockFace;
use crate::protocol::{Decode, Encode};

/// Represents an absolute block position in world space.
#[derive(Clone, Copy, Default, PartialEq, Eq, Hash, Debug)]
pub struct BlockPos {
    pub x: i32,
    pub y: i32,
    pub z: i32,
}

impl BlockPos {
    /// Constructs a new block position.
    pub const fn new(x: i32, y: i32, z: i32) -> Self {
        Self { x, y, z }
    }

    /// Returns the block position a point is contained within.
    pub fn at(pos: impl Into<Vec3<f64>>) -> Self {
        pos.into().floor().as_::<i32>().into()
    }

    /// Get a new [`BlockPos`] that is adjacent to this position in `dir`
    /// direction.
    ///
    /// ```rust
    /// use valence::block::BlockPos;
    /// use valence::client::BlockFace;
    ///
    /// let pos = BlockPos::new(0, 0, 0);
    /// let adj = pos.get_in_direction(BlockFace::South);
    /// assert_eq!(adj, BlockPos::new(0, 0, 1));
    /// ```
    pub fn get_in_direction(self, dir: BlockFace) -> BlockPos {
        match dir {
            BlockFace::Bottom => BlockPos::new(self.x, self.y - 1, self.z),
            BlockFace::Top => BlockPos::new(self.x, self.y + 1, self.z),
            BlockFace::North => BlockPos::new(self.x, self.y, self.z - 1),
            BlockFace::South => BlockPos::new(self.x, self.y, self.z + 1),
            BlockFace::West => BlockPos::new(self.x - 1, self.y, self.z),
            BlockFace::East => BlockPos::new(self.x + 1, self.y, self.z),
        }
    }
}

impl Encode for BlockPos {
    fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> {
        match (self.x, self.y, self.z) {
            (-0x2000000..=0x1ffffff, -0x800..=0x7ff, -0x2000000..=0x1ffffff) => {
                let (x, y, z) = (self.x as u64, self.y as u64, self.z as u64);
                (x << 38 | z << 38 >> 26 | y & 0xfff).encode(w)
            }
            _ => bail!("out of range: {self:?}"),
        }
    }

    fn encoded_len(&self) -> usize {
        8
    }
}

impl Decode for BlockPos {
    fn decode(r: &mut &[u8]) -> anyhow::Result<Self> {
        // Use arithmetic right shift to determine sign.
        let val = i64::decode(r)?;
        let x = val >> 38;
        let z = val << 26 >> 38;
        let y = val << 52 >> 52;
        Ok(Self {
            x: x as i32,
            y: y as i32,
            z: z as i32,
        })
    }
}

impl From<(i32, i32, i32)> for BlockPos {
    fn from((x, y, z): (i32, i32, i32)) -> Self {
        BlockPos::new(x, y, z)
    }
}

impl From<BlockPos> for (i32, i32, i32) {
    fn from(pos: BlockPos) -> Self {
        (pos.x, pos.y, pos.z)
    }
}

impl From<[i32; 3]> for BlockPos {
    fn from([x, y, z]: [i32; 3]) -> Self {
        BlockPos::new(x, y, z)
    }
}

impl From<BlockPos> for [i32; 3] {
    fn from(pos: BlockPos) -> Self {
        [pos.x, pos.y, pos.z]
    }
}

impl From<Vec3<i32>> for BlockPos {
    fn from(pos: Vec3<i32>) -> Self {
        Self::new(pos.x, pos.y, pos.z)
    }
}

impl From<BlockPos> for Vec3<i32> {
    fn from(pos: BlockPos) -> Self {
        Vec3::new(pos.x, pos.y, pos.z)
    }
}

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

    #[test]
    fn position() {
        let xzs = [
            (-33554432, true),
            (-33554433, false),
            (33554431, true),
            (33554432, false),
            (0, true),
            (1, true),
            (-1, true),
        ];
        let ys = [
            (-2048, true),
            (-2049, false),
            (2047, true),
            (2048, false),
            (0, true),
            (1, true),
            (-1, true),
        ];

        let mut buf = [0; 8];

        for (x, x_valid) in xzs {
            for (y, y_valid) in ys {
                for (z, z_valid) in xzs {
                    let pos = BlockPos::new(x, y, z);
                    if x_valid && y_valid && z_valid {
                        pos.encode(&mut &mut buf[..]).unwrap();
                        assert_eq!(BlockPos::decode(&mut &buf[..]).unwrap(), pos);
                    } else {
                        assert!(pos.encode(&mut &mut buf[..]).is_err());
                    }
                }
            }
        }
    }
}