use crate::metrics::{TextMetrics, TextPoint};

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct TextRange {
    pub start: usize,
    pub end: usize,
}

impl TextRange {
    pub const fn new(start: usize, end: usize) -> Self {
        Self { start, end }
    }

    pub const fn len(self) -> usize {
        self.end - self.start
    }

    pub const fn is_empty(self) -> bool {
        self.start == self.end
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct RopeEdit {
    pub range: TextRange,
    pub insert: String,
}

impl RopeEdit {
    pub fn insert(offset: usize, text: impl Into<String>) -> Self {
        Self {
            range: TextRange::new(offset, offset),
            insert: text.into(),
        }
    }

    pub fn delete(range: TextRange) -> Self {
        Self {
            range,
            insert: String::new(),
        }
    }

    pub fn replace(range: TextRange, text: impl Into<String>) -> Self {
        Self {
            range,
            insert: text.into(),
        }
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct EditDelta {
    pub range: TextRange,
    pub removed_text: String,
    pub inserted_text: String,
    pub removed: TextMetrics,
    pub inserted: TextMetrics,
    pub start: TextPoint,
    pub old_end: TextPoint,
    pub new_end: TextPoint,
}

impl EditDelta {
    pub fn net_byte_delta(&self) -> isize {
        self.inserted.bytes as isize - self.removed.bytes as isize
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum AnchorBias {
    Left,
    Right,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Anchor {
    pub byte: usize,
    pub bias: AnchorBias,
}

impl Anchor {
    pub const fn new(byte: usize, bias: AnchorBias) -> Self {
        Self { byte, bias }
    }

    pub fn map_through(&mut self, delta: &EditDelta) {
        if self.byte < delta.range.start {
            return;
        }

        if self.byte > delta.range.end {
            self.byte = self.byte.saturating_add_signed(delta.net_byte_delta());
            return;
        }

        self.byte = match self.bias {
            AnchorBias::Left => delta.range.start,
            AnchorBias::Right => delta.range.start + delta.inserted.bytes,
        };
    }
}

#[cfg(test)]
mod tests {
    use super::{Anchor, AnchorBias, EditDelta, RopeEdit, TextRange};
    use crate::{TextMetrics, TextPoint};

    #[test]
    fn constructors_build_expected_edits() {
        assert_eq!(RopeEdit::insert(2, "x").range, TextRange::new(2, 2));
        assert_eq!(RopeEdit::delete(TextRange::new(2, 4)).insert, "");
        assert_eq!(RopeEdit::replace(TextRange::new(1, 3), "zz").insert, "zz");
    }

    #[test]
    fn text_range_helpers_report_size() {
        let range = TextRange::new(3, 7);
        assert_eq!(range.len(), 4);
        assert!(!range.is_empty());
        assert!(TextRange::new(5, 5).is_empty());
    }

    #[test]
    fn anchors_map_before_inside_and_after_edits() {
        let delta = EditDelta {
            range: TextRange::new(4, 7),
            removed_text: "abc".into(),
            inserted_text: "x".into(),
            removed: TextMetrics::from_text("abc"),
            inserted: TextMetrics::from_text("x"),
            start: TextPoint::origin().advance("1234"),
            old_end: TextPoint::origin().advance("1234abc"),
            new_end: TextPoint::origin().advance("1234x"),
        };

        let mut before = Anchor::new(2, AnchorBias::Left);
        let mut inside_left = Anchor::new(5, AnchorBias::Left);
        let mut inside_right = Anchor::new(5, AnchorBias::Right);
        let mut after = Anchor::new(10, AnchorBias::Left);

        before.map_through(&delta);
        inside_left.map_through(&delta);
        inside_right.map_through(&delta);
        after.map_through(&delta);

        assert_eq!(before.byte, 2);
        assert_eq!(inside_left.byte, 4);
        assert_eq!(inside_right.byte, 5);
        assert_eq!(after.byte, 8);
    }
}
