/* SPDX-FileCopyrightText: © 2024-2025 Decompollaborate */
/* SPDX-License-Identifier: MIT */

use core::{fmt, ops};

#[cfg(feature = "pyo3")]
use pyo3::prelude::*;

use crate::vram::VramOffset;

/// A VRAM (Virtual RAM) address.
///
/// A Vram address could be modified by a [`VramOffset`] instance. This can
/// be done by either the [`add_offset`] function or by using the `+` operator.
///
/// It is also possible to calculate the difference between two Vram addresses,
/// which will return a [`VramOffset`] instance. This can be done with the
/// [`sub_vram`] function or by using the `-` operator.
///
/// To get the raw inner value use the [`inner`] function.
///
/// [`VramOffset`]: crate::vram::VramOffset
/// [`add_offset`]: Vram::add_offset
/// [`sub_vram`]: Vram::sub_vram
/// [`inner`]: Vram::inner
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "pyo3", pyclass(module = "rabbitizer", eq, from_py_object))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Vram {
    inner: u32,
}

impl Vram {
    /// Constructs a `Vram` from a given value.
    #[must_use]
    pub const fn new(value: u32) -> Self {
        Self { inner: value }
    }

    /// Returns the internal vram value.
    #[must_use]
    pub const fn inner(&self) -> u32 {
        self.inner
    }

    /// Adds an offset to this Vram, generating a new Vram value.
    ///
    /// # Examples
    ///
    /// ```
    /// use rabbitizer::vram::{VramOffset, Vram};
    ///
    /// let offset = VramOffset::new(0x8);
    /// let vram = Vram::new(0x80000100);
    ///
    /// assert_eq!(vram.add_offset(&offset), Vram::new(0x80000108));
    /// ```
    #[must_use]
    pub const fn add_offset(&self, rhs: &VramOffset) -> Self {
        Self::new(self.inner.wrapping_add_signed(rhs.inner()))
    }

    /// Subtracts a Vram to this Vram.
    ///
    /// # Examples
    ///
    /// ```
    /// use rabbitizer::vram::{VramOffset, Vram};
    ///
    /// let vram_a = Vram::new(0x80000100);
    /// let vram_b = Vram::new(0x80000140);
    ///
    /// assert_eq!(vram_a.sub_vram(&vram_b), VramOffset::new(-0x40));
    /// ```
    #[must_use]
    pub const fn sub_vram(&self, rhs: &Self) -> VramOffset {
        VramOffset::new((self.inner as i32).wrapping_sub_unsigned(rhs.inner()))
    }

    /// Aligns down the Vram to the given power-of-two `alignment`.
    ///
    /// If the `alignment` parameter is not a power-of-two then it will be rounded down.
    ///
    /// # Panics
    ///
    /// This function will panic if `alignment` is zero.
    ///
    /// # Examples
    ///
    /// ```
    /// use rabbitizer::vram::Vram;
    ///
    /// let vram = Vram::new(0x800000A4);
    ///
    /// assert_eq!(vram.align_down(8), Vram::new(0x800000A0));
    /// ```
    #[must_use = "this returns the result of the operation, without modifying the original"]
    pub const fn align_down(&self, alignment: u8) -> Self {
        let shift = alignment.ilog2();

        // Strip the lower bits by shifting.
        Self::new((self.inner >> shift) << shift)
    }
}

impl ops::Add<VramOffset> for Vram {
    type Output = Self;

    fn add(self, rhs: VramOffset) -> Self::Output {
        self.add_offset(&rhs)
    }
}

impl ops::Add<&VramOffset> for Vram {
    type Output = Self;

    fn add(self, rhs: &VramOffset) -> Self::Output {
        self.add_offset(rhs)
    }
}

impl ops::AddAssign<VramOffset> for Vram {
    fn add_assign(&mut self, rhs: VramOffset) {
        *self = self.add_offset(&rhs)
    }
}

impl ops::AddAssign<&VramOffset> for Vram {
    fn add_assign(&mut self, rhs: &VramOffset) {
        *self = self.add_offset(rhs)
    }
}

impl ops::Sub<Self> for Vram {
    type Output = VramOffset;

    fn sub(self, rhs: Self) -> Self::Output {
        self.sub_vram(&rhs)
    }
}

impl ops::Sub<&Self> for Vram {
    type Output = VramOffset;

    fn sub(self, rhs: &Self) -> Self::Output {
        self.sub_vram(rhs)
    }
}

impl fmt::Debug for Vram {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "Vram {{ 0x{:08X} }}", self.inner)
    }
}

impl fmt::Display for Vram {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{:08X}", self.inner)
    }
}

#[cfg(feature = "pyo3")]
pub(crate) mod python_bindings {
    use super::*;

    #[pymethods]
    impl Vram {
        #[new]
        #[must_use]
        pub const fn py_new(value: u32) -> Self {
            Self::new(value)
        }

        #[pyo3(name = "inner")]
        #[must_use]
        pub const fn py_inner(&self) -> u32 {
            self.inner()
        }
    }
}
