use cmp::Ordering;

use {
    crate::TextSize,
    std::{
        cmp,
        convert::TryFrom,
        fmt,
        ops::{Add, AddAssign, Bound, Index, IndexMut, Range, RangeBounds, Sub, SubAssign},
    },
};

/// A range in text, represented as a pair of [`TextSize`][struct@TextSize].
///
/// It is a logic error for `start` to be greater than `end`.
#[derive(Default, Copy, Clone, Eq, PartialEq, Hash)]
#[repr(C)]
pub struct TextRange {
    // Invariant: start <= end
    start: TextSize,
    end: TextSize,
}

impl fmt::Debug for TextRange {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}..{}", self.start().raw, self.end().raw)
    }
}

impl PartialOrd for TextRange {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for TextRange {
    fn cmp(&self, other: &Self) -> Ordering {
        match self.start.cmp(&other.start) {
            Ordering::Less => Ordering::Less,
            Ordering::Greater => Ordering::Greater,
            Ordering::Equal => self.end.cmp(&other.end),
        }
    }
}

impl TextRange {
    /// Creates a new `TextRange` with the given `start` and `end` (`start..end`).
    ///
    /// # Panics
    ///
    /// Panics if `end < start`.
    ///
    /// # Examples
    ///
    /// ```rust
    /// # use biome_text_size::*;
    /// let start = TextSize::from(5);
    /// let end = TextSize::from(10);
    /// let range = TextRange::new(start, end);
    ///
    /// assert_eq!(range.start(), start);
    /// assert_eq!(range.end(), end);
    /// assert_eq!(range.len(), end - start);
    /// ```
    #[inline]
    pub const fn new(start: TextSize, end: TextSize) -> Self {
        assert!(start.raw <= end.raw);
        Self { start, end }
    }

    /// Create a new `TextRange` with the given `offset` and `len` (`offset..offset + len`).
    ///
    /// # Examples
    ///
    /// ```rust
    /// # use biome_text_size::*;
    /// let text = "0123456789";
    ///
    /// let offset = TextSize::from(2);
    /// let length = TextSize::from(5);
    /// let range = TextRange::at(offset, length);
    ///
    /// assert_eq!(range, TextRange::new(offset, offset + length));
    /// assert_eq!(&text[range], "23456")
    /// ```
    #[inline]
    pub const fn at(offset: TextSize, len: TextSize) -> Self {
        Self {
            start: offset,
            end: TextSize {
                raw: offset.raw + len.raw,
            },
        }
    }

    /// Create a zero-length range at the specified offset (`offset..offset`).
    ///
    /// # Examples
    ///
    /// ```rust
    /// # use biome_text_size::*;
    /// let point: TextSize;
    /// # point = TextSize::from(3);
    /// let range = TextRange::empty(point);
    /// assert!(range.is_empty());
    /// assert_eq!(range, TextRange::new(point, point));
    /// ```
    #[inline]
    pub const fn empty(offset: TextSize) -> Self {
        Self {
            start: offset,
            end: offset,
        }
    }

    /// Create a range up to the given end (`..end`).
    ///
    /// # Examples
    ///
    /// ```rust
    /// # use biome_text_size::*;
    /// let point: TextSize;
    /// # point = TextSize::from(12);
    /// let range = TextRange::up_to(point);
    ///
    /// assert_eq!(range.len(), point);
    /// assert_eq!(range, TextRange::new(0.into(), point));
    /// assert_eq!(range, TextRange::at(0.into(), point));
    /// ```
    #[inline]
    pub const fn up_to(end: TextSize) -> Self {
        Self {
            start: TextSize { raw: 0 },
            end,
        }
    }
}

/// Identity methods.
impl TextRange {
    /// The start point of this range.
    #[inline]
    pub const fn start(self) -> TextSize {
        self.start
    }

    /// The end point of this range.
    #[inline]
    pub const fn end(self) -> TextSize {
        self.end
    }

    /// The size of this range.
    #[inline]
    pub const fn len(self) -> TextSize {
        // HACK for const fn: math on primitives only
        TextSize {
            raw: self.end().raw - self.start().raw,
        }
    }

    /// Check if this range is empty.
    #[inline]
    pub const fn is_empty(self) -> bool {
        // HACK for const fn: math on primitives only
        self.start().raw == self.end().raw
    }
}

/// Manipulation methods.
impl TextRange {
    /// Check if this range contains an offset.
    ///
    /// The end index is considered excluded.
    ///
    /// # Examples
    ///
    /// ```rust
    /// # use biome_text_size::*;
    /// let (start, end): (TextSize, TextSize);
    /// # start = 10.into(); end = 20.into();
    /// let range = TextRange::new(start, end);
    /// assert!(range.contains(start));
    /// assert!(!range.contains(end));
    /// ```
    #[inline]
    pub fn contains(self, offset: TextSize) -> bool {
        self.start() <= offset && offset < self.end()
    }

    /// Check if this range contains an offset.
    ///
    /// The end index is considered included.
    ///
    /// # Examples
    ///
    /// ```rust
    /// # use biome_text_size::*;
    /// let (start, end): (TextSize, TextSize);
    /// # start = 10.into(); end = 20.into();
    /// let range = TextRange::new(start, end);
    /// assert!(range.contains_inclusive(start));
    /// assert!(range.contains_inclusive(end));
    /// ```
    #[inline]
    pub fn contains_inclusive(self, offset: TextSize) -> bool {
        self.start() <= offset && offset <= self.end()
    }

    /// Check if this range completely contains another range.
    ///
    /// # Examples
    ///
    /// ```rust
    /// # use biome_text_size::*;
    /// let larger = TextRange::new(0.into(), 20.into());
    /// let smaller = TextRange::new(5.into(), 15.into());
    /// assert!(larger.contains_range(smaller));
    /// assert!(!smaller.contains_range(larger));
    ///
    /// // a range always contains itself
    /// assert!(larger.contains_range(larger));
    /// assert!(smaller.contains_range(smaller));
    /// ```
    #[inline]
    pub fn contains_range(self, other: Self) -> bool {
        self.start() <= other.start() && other.end() <= self.end()
    }

    /// The range covered by both ranges, if it exists.
    /// If the ranges touch but do not overlap, the output range is empty.
    ///
    /// # Examples
    ///
    /// ```rust
    /// # use biome_text_size::*;
    /// assert_eq!(
    ///     TextRange::intersect(
    ///         TextRange::new(0.into(), 10.into()),
    ///         TextRange::new(5.into(), 15.into()),
    ///     ),
    ///     Some(TextRange::new(5.into(), 10.into())),
    /// );
    /// ```
    #[inline]
    pub fn intersect(self, other: Self) -> Option<Self> {
        let start = cmp::max(self.start(), other.start());
        let end = cmp::min(self.end(), other.end());
        if end < start {
            return None;
        }
        Some(Self::new(start, end))
    }

    /// Extends the range to cover `other` as well.
    ///
    /// # Examples
    ///
    /// ```rust
    /// # use biome_text_size::*;
    /// assert_eq!(
    ///     TextRange::cover(
    ///         TextRange::new(0.into(), 5.into()),
    ///         TextRange::new(15.into(), 20.into()),
    ///     ),
    ///     TextRange::new(0.into(), 20.into()),
    /// );
    /// ```
    #[inline]
    pub fn cover(self, other: Self) -> Self {
        let start = cmp::min(self.start(), other.start());
        let end = cmp::max(self.end(), other.end());
        Self::new(start, end)
    }

    /// Extends the range to cover `other` offsets as well.
    ///
    /// # Examples
    ///
    /// ```rust
    /// # use biome_text_size::*;
    /// assert_eq!(
    ///     TextRange::empty(0.into()).cover_offset(20.into()),
    ///     TextRange::new(0.into(), 20.into()),
    /// )
    /// ```
    #[inline]
    pub fn cover_offset(self, offset: TextSize) -> Self {
        self.cover(Self::empty(offset))
    }

    /// Add an offset to this range.
    ///
    /// Note that this is not appropriate for changing where a `TextRange` is
    /// within some string; rather, it is for changing the reference anchor
    /// that the `TextRange` is measured against.
    ///
    /// The unchecked version (`Add::add`) will _always_ panic on overflow,
    /// in contrast to primitive integers, which check in debug mode only.
    #[inline]
    pub fn checked_add(self, offset: TextSize) -> Option<Self> {
        Some(Self {
            start: self.start.checked_add(offset)?,
            end: self.end.checked_add(offset)?,
        })
    }

    /// Subtract an offset from this range.
    ///
    /// Note that this is not appropriate for changing where a `TextRange` is
    /// within some string; rather, it is for changing the reference anchor
    /// that the `TextRange` is measured against.
    ///
    /// The unchecked version (`Sub::sub`) will _always_ panic on overflow,
    /// in contrast to primitive integers, which check in debug mode only.
    #[inline]
    pub fn checked_sub(self, offset: TextSize) -> Option<Self> {
        Some(Self {
            start: self.start.checked_sub(offset)?,
            end: self.end.checked_sub(offset)?,
        })
    }

    /// Relative order of the two ranges (overlapping ranges are considered
    /// equal).
    ///
    ///
    /// This is useful when, for example, binary searching an array of disjoint
    /// ranges.
    ///
    /// # Examples
    ///
    /// ```
    /// # use biome_text_size::*;
    /// # use std::cmp::Ordering;
    ///
    /// let a = TextRange::new(0.into(), 3.into());
    /// let b = TextRange::new(4.into(), 5.into());
    /// assert_eq!(a.ordering(b), Ordering::Less);
    ///
    /// let a = TextRange::new(0.into(), 3.into());
    /// let b = TextRange::new(3.into(), 5.into());
    /// assert_eq!(a.ordering(b), Ordering::Less);
    ///
    /// let a = TextRange::new(0.into(), 3.into());
    /// let b = TextRange::new(2.into(), 5.into());
    /// assert_eq!(a.ordering(b), Ordering::Equal);
    ///
    /// let a = TextRange::new(0.into(), 3.into());
    /// let b = TextRange::new(2.into(), 2.into());
    /// assert_eq!(a.ordering(b), Ordering::Equal);
    ///
    /// let a = TextRange::new(2.into(), 3.into());
    /// let b = TextRange::new(2.into(), 2.into());
    /// assert_eq!(a.ordering(b), Ordering::Greater);
    /// ```
    #[inline]
    pub fn ordering(self, other: Self) -> Ordering {
        if self.end() <= other.start() {
            Ordering::Less
        } else if other.end() <= self.start() {
            Ordering::Greater
        } else {
            Ordering::Equal
        }
    }

    /// Subtracts an offset from the start position.
    ///
    ///
    /// ## Panics
    /// If `start - amount` is less than zero.
    ///
    /// ## Examples
    ///
    /// ```
    /// use biome_text_size::{TextRange, TextSize};
    ///
    /// let range = TextRange::new(TextSize::from(5), TextSize::from(10));
    /// assert_eq!(range.sub_start(TextSize::from(2)), TextRange::new(TextSize::from(3), TextSize::from(10)));
    /// ```
    #[inline]
    pub fn sub_start(&self, amount: TextSize) -> Self {
        Self::new(self.start() - amount, self.end())
    }

    /// Adds an offset to the start position.
    ///
    /// ## Panics
    /// If `start + amount > end`
    ///
    /// ## Examples
    ///
    /// ```
    /// use biome_text_size::{TextRange, TextSize};
    ///
    /// let range = TextRange::new(TextSize::from(5), TextSize::from(10));
    /// assert_eq!(range.add_start(TextSize::from(3)), TextRange::new(TextSize::from(8), TextSize::from(10)));
    /// ```
    #[inline]
    pub fn add_start(&self, amount: TextSize) -> Self {
        Self::new(self.start() + amount, self.end())
    }

    /// Subtracts an offset from the end position.
    ///
    ///
    /// ## Panics
    /// If `end - amount < 0` or `end - amount < start`
    ///
    /// ## Examples
    ///
    /// ```
    /// use biome_text_size::{TextRange, TextSize};
    ///
    /// let range = TextRange::new(TextSize::from(5), TextSize::from(10));
    /// assert_eq!(range.sub_end(TextSize::from(2)), TextRange::new(TextSize::from(5), TextSize::from(8)));
    /// ```
    #[inline]
    pub fn sub_end(&self, amount: TextSize) -> Self {
        Self::new(self.start(), self.end() - amount)
    }

    /// Adds an offset to the end position.
    ///
    ///
    /// ## Panics
    /// If `end + amount > u32::MAX`
    ///
    /// ## Examples
    ///
    /// ```
    /// use biome_text_size::{TextRange, TextSize};
    ///
    /// let range = TextRange::new(TextSize::from(5), TextSize::from(10));
    /// assert_eq!(range.add_end(TextSize::from(2)), TextRange::new(TextSize::from(5), TextSize::from(12)));
    /// ```
    #[inline]
    pub fn add_end(&self, amount: TextSize) -> Self {
        Self::new(self.start(), self.end() + amount)
    }
}

impl Index<TextRange> for str {
    type Output = Self;
    #[inline]
    fn index(&self, index: TextRange) -> &Self {
        &self[Range::<usize>::from(index)]
    }
}

impl Index<TextRange> for String {
    type Output = str;
    #[inline]
    fn index(&self, index: TextRange) -> &str {
        &self[Range::<usize>::from(index)]
    }
}

impl IndexMut<TextRange> for str {
    #[inline]
    fn index_mut(&mut self, index: TextRange) -> &mut Self {
        &mut self[Range::<usize>::from(index)]
    }
}

impl IndexMut<TextRange> for String {
    #[inline]
    fn index_mut(&mut self, index: TextRange) -> &mut str {
        &mut self[Range::<usize>::from(index)]
    }
}

impl RangeBounds<TextSize> for TextRange {
    fn start_bound(&self) -> Bound<&TextSize> {
        Bound::Included(&self.start)
    }

    fn end_bound(&self) -> Bound<&TextSize> {
        Bound::Excluded(&self.end)
    }
}

impl<T> From<TextRange> for Range<T>
where
    T: From<TextSize>,
{
    #[inline]
    fn from(r: TextRange) -> Self {
        r.start().into()..r.end().into()
    }
}

macro_rules! ops {
    (impl $Op:ident for TextRange by fn $f:ident = $op:tt) => {
        impl $Op<&TextSize> for TextRange {
            type Output = TextRange;
            #[inline]
            fn $f(self, other: &TextSize) -> TextRange {
                self $op *other
            }
        }
        impl<T> $Op<T> for &TextRange
        where
            TextRange: $Op<T, Output=TextRange>,
        {
            type Output = TextRange;
            #[inline]
            fn $f(self, other: T) -> TextRange {
                *self $op other
            }
        }
    };
}

impl Add<TextSize> for TextRange {
    type Output = Self;
    #[inline]
    fn add(self, offset: TextSize) -> Self {
        self.checked_add(offset)
            .expect("TextRange +offset overflowed")
    }
}

impl Sub<TextSize> for TextRange {
    type Output = Self;
    #[inline]
    fn sub(self, offset: TextSize) -> Self {
        self.checked_sub(offset)
            .expect("TextRange -offset overflowed")
    }
}

ops!(impl Add for TextRange by fn add = +);
ops!(impl Sub for TextRange by fn sub = -);

impl<A> AddAssign<A> for TextRange
where
    Self: Add<A, Output = Self>,
{
    #[inline]
    fn add_assign(&mut self, rhs: A) {
        *self = *self + rhs
    }
}

impl<S> SubAssign<S> for TextRange
where
    Self: Sub<S, Output = Self>,
{
    #[inline]
    fn sub_assign(&mut self, rhs: S) {
        *self = *self - rhs
    }
}

impl TryFrom<(usize, usize)> for TextRange {
    type Error = std::num::TryFromIntError;
    #[inline]
    fn try_from((start, end): (usize, usize)) -> Result<Self, Self::Error> {
        Ok(Self::new(
            TextSize::try_from(start)?,
            TextSize::try_from(end)?,
        ))
    }
}
