// External includes.

// Standard includes.
use std::fmt;
use std::ops::{Add, Mul, Neg, Sub};

// Internal includes.
use super::{
    CardinalDirection, CardinalRotation, Coord, HasPosition, IsPosition, ProvidesPosition,
};

/// A position on a cartesian coordinate system.
///
/// The x and y components of `Position` can be each be an integer negative, zero, or positive value. In the cartesian system used in `dungen_minion`, and most roguelikes, (x: 0, y: 0) defines the top-left of the coordinate system.
#[derive(AddAssign, Copy, Clone, Debug, Eq, Hash, PartialEq, SubAssign)]
pub struct Position {
    x: Coord,
    y: Coord,
}

impl Position {
    /// Creates a new `Position` from horizontal x and vertical y components.
    ///
    /// ```
    /// # use dungen_minion_geometry::*;
    /// // 5 to the right, 3 up.
    /// let pos: Position = Position::new(5, -3);
    /// assert!(pos.x() == 5);
    /// assert!(pos.y() == -3);
    /// ```
    #[must_use]
    pub fn new(x: Coord, y: Coord) -> Self {
        Self { x, y }
    }

    /// Returns a `Position` of (0, 0).
    ///
    /// ```
    /// # use dungen_minion_geometry::Position;
    /// assert!(Position::new(0, 0) == Position::zero());
    /// ```
    #[must_use]
    pub fn zero() -> Self {
        Self { x: 0, y: 0 }
    }

    /// Represents a constant with x = 0 and y = -1.
    ///
    /// ```
    /// # use dungen_minion_geometry::Position;
    /// assert!(Position::new(0, -1) == Position::NORTH);
    /// ```
    pub const NORTH: Position = Self { x: 0, y: -1 };

    /// Represents a constant with x = 1 and y = 0.
    ///
    /// ```
    /// # use dungen_minion_geometry::Position;
    /// assert!(Position::new(1, 0) == Position::EAST);
    /// ```
    pub const EAST: Position = Self { x: 1, y: 0 };

    /// Represents a constant with x = 0 and y = 1.
    ///
    /// ```
    /// # use dungen_minion_geometry::Position;
    /// assert!(Position::new(0, 1) == Position::SOUTH);
    /// ```
    pub const SOUTH: Position = Self { x: 0, y: 1 };

    /// Represents a constant with x = -1 and y = 0.
    ///
    /// ```
    /// # use dungen_minion_geometry::Position;
    /// assert!(Position::new(-1, 0) == Position::WEST);
    /// ```
    pub const WEST: Position = Self { x: -1, y: 0 };
}

impl Add for Position {
    type Output = Self;

    fn add(self, other: Self) -> Self {
        Self {
            x: self.x + other.x,
            y: self.y + other.y,
        }
    }
}

impl fmt::Display for Position {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "( x: {}, y: {} )", self.x(), self.y())
    }
}

impl From<CardinalDirection> for Position {
    /// Converts a `Position` from an `CardinalDirection`.
    ///
    /// ```
    /// # use dungen_minion_geometry::*;
    /// assert!(Position::from(CardinalDirection::North) == Position::new(0, -1));
    /// assert!(Position::from(CardinalDirection::East) == Position::new(1, 0));
    /// assert!(Position::from(CardinalDirection::South) == Position::new(0, 1));
    /// assert!(Position::from(CardinalDirection::West) == Position::new(-1, 0));
    /// ```
    fn from(ordinal_direction: CardinalDirection) -> Self {
        match ordinal_direction {
            CardinalDirection::North => Position::new(0, -1),
            CardinalDirection::East => Position::new(1, 0),
            CardinalDirection::South => Position::new(0, 1),
            CardinalDirection::West => Position::new(-1, 0),
        }
    }
}

impl HasPosition for Position {
    fn position(&self) -> &Position {
        self
    }

    fn position_mut(&mut self) -> &mut Position {
        self
    }
}

impl IsPosition for Position {
    fn x(&self) -> Coord {
        self.x
    }

    fn x_mut(&mut self) -> &mut Coord {
        &mut self.x
    }

    fn y(&self) -> Coord {
        self.y
    }

    fn y_mut(&mut self) -> &mut Coord {
        &mut self.y
    }
}

impl Mul<CardinalRotation> for Position {
    type Output = Self;

    /// Returns a copy of `self` after an [`CardinalRotation`](enum.CardinalRotation.html).
    ///
    /// ```
    /// # use dungen_minion_geometry::*;
    /// let north_raw: Position = Position::new(0, 1);
    /// let east_raw: Position = Position::new(1, 0);
    /// let south_raw: Position = Position::new(0, -1);
    /// let west_raw: Position = Position::new(-1, 0);
    ///
    /// let north_from_north_raw: Position = north_raw * CardinalRotation::None;
    /// let east_from_north_raw: Position = north_raw * CardinalRotation::Right90;
    /// let south_from_north_raw: Position = north_raw * CardinalRotation::Full180;
    /// let west_from_north_raw: Position = north_raw * CardinalRotation::Left90;
    ///
    /// assert!(north_from_north_raw == north_raw);
    /// assert!(east_from_north_raw == east_raw);
    /// assert!(south_from_north_raw == south_raw);
    /// assert!(west_from_north_raw == west_raw);
    /// ```
    fn mul(self, rotation: CardinalRotation) -> Self::Output {
        match rotation {
            CardinalRotation::None => self,
            CardinalRotation::Right90 => Self::new(self.y(), -self.x()),
            CardinalRotation::Full180 => Self::new(-self.x(), -self.y()),
            CardinalRotation::Left90 => Self::new(-self.y(), self.x()),
        }
    }
}

impl Neg for Position {
    type Output = Self;

    fn neg(self) -> Self::Output {
        Self::new(-self.x(), -self.y())
    }
}

impl ProvidesPosition for Position {
    fn provide_position(&self) -> Position {
        *self
    }
}

impl Sub for Position {
    type Output = Self;

    fn sub(self, other: Self) -> Self {
        Self {
            x: self.x - other.x,
            y: self.y - other.y,
        }
    }
}
