use crate::{
    graphics::Rect,
    gutter::{self, Gutter},
    Document, DocumentId, ViewId,
};
use helix_core::{
    pos_at_visual_coords, visual_coords_at_pos, Position, RopeSlice, Selection, Transaction,
};

use std::fmt;

type Jump = (DocumentId, Selection);

#[derive(Debug, Clone)]
pub struct JumpList {
    jumps: Vec<Jump>,
    current: usize,
}

impl JumpList {
    pub fn new(initial: Jump) -> Self {
        Self {
            jumps: vec![initial],
            current: 0,
        }
    }

    pub fn push(&mut self, jump: Jump) {
        self.jumps.truncate(self.current);
        // don't push duplicates
        if self.jumps.last() != Some(&jump) {
            self.jumps.push(jump);
            self.current = self.jumps.len();
        }
    }

    pub fn forward(&mut self, count: usize) -> Option<&Jump> {
        if self.current + count < self.jumps.len() {
            self.current += count;
            self.jumps.get(self.current)
        } else {
            None
        }
    }

    // Taking view and doc to prevent unnecessary cloning when jump is not required.
    pub fn backward(&mut self, view_id: ViewId, doc: &mut Document, count: usize) -> Option<&Jump> {
        if let Some(current) = self.current.checked_sub(count) {
            if self.current == self.jumps.len() {
                let jump = (doc.id(), doc.selection(view_id).clone());
                self.push(jump);
            }
            self.current = current;
            self.jumps.get(self.current)
        } else {
            None
        }
    }

    pub fn remove(&mut self, doc_id: &DocumentId) {
        self.jumps.retain(|(other_id, _)| other_id != doc_id);
    }

    pub fn get(&self) -> &[Jump] {
        &self.jumps
    }

    /// Applies a [`Transaction`] of changes to the jumplist.
    /// This is necessary to ensure that changes to documents do not leave jump-list
    /// selections pointing to parts of the text which no longer exist.
    fn apply(&mut self, transaction: &Transaction, doc: &Document) {
        let text = doc.text().slice(..);

        for (doc_id, selection) in &mut self.jumps {
            if doc.id() == *doc_id {
                *selection = selection
                    .clone()
                    .map(transaction.changes())
                    .ensure_invariants(text);
            }
        }
    }
}

#[derive(Clone)]
pub struct View {
    pub id: ViewId,
    pub offset: Position,
    pub area: Rect,
    pub doc: DocumentId,
    pub jumps: JumpList,
    // documents accessed from this view from the oldest one to last viewed one
    pub docs_access_history: Vec<DocumentId>,
    /// the last modified files before the current one
    /// ordered from most frequent to least frequent
    // uses two docs because we want to be able to swap between the
    // two last modified docs which we need to manually keep track of
    pub last_modified_docs: [Option<DocumentId>; 2],
    /// used to store previous selections of tree-sitter objects
    pub object_selections: Vec<Selection>,
    /// Gutter (constructor) and width of gutter, used to calculate
    /// `gutter_offset`
    gutters: Vec<(Gutter, usize)>,
    /// cached total width of gutter
    gutter_offset: u16,
}

impl fmt::Debug for View {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("View")
            .field("id", &self.id)
            .field("area", &self.area)
            .field("doc", &self.doc)
            .finish()
    }
}

impl View {
    pub fn new(doc: DocumentId, gutter_types: Vec<crate::editor::GutterType>) -> Self {
        let mut gutters: Vec<(Gutter, usize)> = vec![];
        let mut gutter_offset = 0;
        use crate::editor::GutterType;
        for gutter_type in &gutter_types {
            let width = match gutter_type {
                GutterType::Diagnostics => 1,
                GutterType::LineNumbers => 5,
                GutterType::Spacer => 1,
            };
            gutter_offset += width;
            gutters.push((
                match gutter_type {
                    GutterType::Diagnostics => gutter::diagnostics_or_breakpoints,
                    GutterType::LineNumbers => gutter::line_numbers,
                    GutterType::Spacer => gutter::padding,
                },
                width as usize,
            ));
        }
        if !gutter_types.is_empty() {
            gutter_offset += 1;
        }
        Self {
            id: ViewId::default(),
            doc,
            offset: Position::new(0, 0),
            area: Rect::default(), // will get calculated upon inserting into tree
            jumps: JumpList::new((doc, Selection::point(0))), // TODO: use actual sel
            docs_access_history: Vec::new(),
            last_modified_docs: [None, None],
            object_selections: Vec::new(),
            gutters,
            gutter_offset,
        }
    }

    pub fn add_to_history(&mut self, id: DocumentId) {
        if let Some(pos) = self.docs_access_history.iter().position(|&doc| doc == id) {
            self.docs_access_history.remove(pos);
        }
        self.docs_access_history.push(id);
    }

    pub fn inner_area(&self) -> Rect {
        // TODO add abilty to not use cached offset for runtime configurable gutter
        self.area.clip_left(self.gutter_offset).clip_bottom(1) // -1 for statusline
    }

    pub fn gutters(&self) -> &[(Gutter, usize)] {
        &self.gutters
    }

    //
    pub fn offset_coords_to_in_view(
        &self,
        doc: &Document,
        scrolloff: usize,
    ) -> Option<(usize, usize)> {
        let cursor = doc
            .selection(self.id)
            .primary()
            .cursor(doc.text().slice(..));

        let Position { col, row: line } =
            visual_coords_at_pos(doc.text().slice(..), cursor, doc.tab_width());

        let inner_area = self.inner_area();
        let last_line = (self.offset.row + inner_area.height as usize).saturating_sub(1);

        // - 1 so we have at least one gap in the middle.
        // a height of 6 with padding of 3 on each side will keep shifting the view back and forth
        // as we type
        let scrolloff = scrolloff.min(inner_area.height.saturating_sub(1) as usize / 2);

        let last_col = self.offset.col + inner_area.width.saturating_sub(1) as usize;

        let row = if line > last_line.saturating_sub(scrolloff) {
            // scroll down
            self.offset.row + line - (last_line.saturating_sub(scrolloff))
        } else if line < self.offset.row + scrolloff {
            // scroll up
            line.saturating_sub(scrolloff)
        } else {
            self.offset.row
        };

        let col = if col > last_col.saturating_sub(scrolloff) {
            // scroll right
            self.offset.col + col - (last_col.saturating_sub(scrolloff))
        } else if col < self.offset.col + scrolloff {
            // scroll left
            col.saturating_sub(scrolloff)
        } else {
            self.offset.col
        };
        if row == self.offset.row && col == self.offset.col {
            None
        } else {
            Some((row, col))
        }
    }

    pub fn ensure_cursor_in_view(&mut self, doc: &Document, scrolloff: usize) {
        if let Some((row, col)) = self.offset_coords_to_in_view(doc, scrolloff) {
            self.offset.row = row;
            self.offset.col = col;
        }
    }

    pub fn is_cursor_in_view(&mut self, doc: &Document, scrolloff: usize) -> bool {
        self.offset_coords_to_in_view(doc, scrolloff).is_none()
    }

    /// Calculates the last visible line on screen
    #[inline]
    pub fn last_line(&self, doc: &Document) -> usize {
        let height = self.inner_area().height;
        std::cmp::min(
            // Saturating subs to make it inclusive zero indexing.
            (self.offset.row + height as usize).saturating_sub(1),
            doc.text().len_lines().saturating_sub(1),
        )
    }

    /// Translates a document position to an absolute position in the terminal.
    /// Returns a (line, col) position if the position is visible on screen.
    // TODO: Could return width as well for the character width at cursor.
    pub fn screen_coords_at_pos(
        &self,
        doc: &Document,
        text: RopeSlice,
        pos: usize,
    ) -> Option<Position> {
        let line = text.char_to_line(pos);

        if line < self.offset.row || line > self.last_line(doc) {
            // Line is not visible on screen
            return None;
        }

        let tab_width = doc.tab_width();
        // TODO: visual_coords_at_pos also does char_to_line which we ignore, can we reuse the call?
        let Position { col, .. } = visual_coords_at_pos(text, pos, tab_width);

        // It is possible for underflow to occur if the buffer length is larger than the terminal width.
        let row = line.saturating_sub(self.offset.row);
        let col = col.saturating_sub(self.offset.col);

        Some(Position::new(row, col))
    }

    pub fn text_pos_at_screen_coords(
        &self,
        text: &RopeSlice,
        row: u16,
        column: u16,
        tab_width: usize,
    ) -> Option<usize> {
        let inner = self.inner_area();
        // 1 for status
        if row < inner.top() || row >= inner.bottom() {
            return None;
        }

        if column < inner.left() || column > inner.right() {
            return None;
        }

        let text_row = (row - inner.y) as usize + self.offset.row;
        if text_row > text.len_lines() - 1 {
            return Some(text.len_chars());
        }

        let text_col = (column - inner.x) as usize + self.offset.col;

        Some(pos_at_visual_coords(
            *text,
            Position {
                row: text_row,
                col: text_col,
            },
            tab_width,
        ))
    }

    /// Translates a screen position to position in the text document.
    /// Returns a usize typed position in bounds of the text if found in this view, None if out of view.
    pub fn pos_at_screen_coords(&self, doc: &Document, row: u16, column: u16) -> Option<usize> {
        self.text_pos_at_screen_coords(&doc.text().slice(..), row, column, doc.tab_width())
    }

    /// Translates screen coordinates into coordinates on the gutter of the view.
    /// Returns a tuple of usize typed line and column numbers starting with 0.
    /// Returns None if coordinates are not on the gutter.
    pub fn gutter_coords_at_screen_coords(&self, row: u16, column: u16) -> Option<Position> {
        // 1 for status
        if row < self.area.top() || row >= self.area.bottom() {
            return None;
        }

        if column < self.area.left() || column > self.area.right() {
            return None;
        }

        Some(Position::new(
            (row - self.area.top()) as usize,
            (column - self.area.left()) as usize,
        ))
    }

    pub fn remove_document(&mut self, doc_id: &DocumentId) {
        self.jumps.remove(doc_id);
        self.docs_access_history.retain(|doc| doc != doc_id);
    }

    // pub fn traverse<F>(&self, text: RopeSlice, start: usize, end: usize, fun: F)
    // where
    //     F: Fn(usize, usize),
    // {
    //     let start = self.screen_coords_at_pos(text, start);
    //     let end = self.screen_coords_at_pos(text, end);

    //     match (start, end) {
    //         // fully on screen
    //         (Some(start), Some(end)) => {
    //             // we want to calculate ends of lines for each char..
    //         }
    //         // from start to end of screen
    //         (Some(start), None) => {}
    //         // from start of screen to end
    //         (None, Some(end)) => {}
    //         // not on screen
    //         (None, None) => return,
    //     }
    // }

    /// Applies a [`Transaction`] to the view.
    /// Instead of calling this function directly, use [crate::apply_transaction]
    /// which applies a transaction to the [`Document`] and view together.
    pub fn apply(&mut self, transaction: &Transaction, doc: &Document) -> bool {
        self.jumps.apply(transaction, doc);
        true
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use helix_core::Rope;
    const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
    const OFFSET_WITHOUT_LINE_NUMBERS: u16 = 2; // 1 diagnostic + 1 gutter
                                                // const OFFSET: u16 = GUTTERS.iter().map(|(_, width)| *width as u16).sum();
    use crate::editor::GutterType;

    #[test]
    fn test_text_pos_at_screen_coords() {
        let mut view = View::new(
            DocumentId::default(),
            vec![GutterType::Diagnostics, GutterType::LineNumbers],
        );
        view.area = Rect::new(40, 40, 40, 40);
        let rope = Rope::from_str("abc\n\tdef");
        let text = rope.slice(..);

        assert_eq!(view.text_pos_at_screen_coords(&text, 40, 2, 4), None);

        assert_eq!(view.text_pos_at_screen_coords(&text, 40, 41, 4), None);

        assert_eq!(view.text_pos_at_screen_coords(&text, 0, 2, 4), None);

        assert_eq!(view.text_pos_at_screen_coords(&text, 0, 49, 4), None);

        assert_eq!(view.text_pos_at_screen_coords(&text, 0, 41, 4), None);

        assert_eq!(view.text_pos_at_screen_coords(&text, 40, 81, 4), None);

        assert_eq!(view.text_pos_at_screen_coords(&text, 78, 41, 4), None);

        assert_eq!(
            view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 3, 4),
            Some(3)
        );

        assert_eq!(view.text_pos_at_screen_coords(&text, 40, 80, 4), Some(3));

        assert_eq!(
            view.text_pos_at_screen_coords(&text, 41, 40 + OFFSET + 1, 4),
            Some(4)
        );

        assert_eq!(
            view.text_pos_at_screen_coords(&text, 41, 40 + OFFSET + 4, 4),
            Some(5)
        );

        assert_eq!(
            view.text_pos_at_screen_coords(&text, 41, 40 + OFFSET + 7, 4),
            Some(8)
        );

        assert_eq!(view.text_pos_at_screen_coords(&text, 41, 80, 4), Some(8));
    }

    #[test]
    fn test_text_pos_at_screen_coords_without_line_numbers_gutter() {
        let mut view = View::new(DocumentId::default(), vec![GutterType::Diagnostics]);
        view.area = Rect::new(40, 40, 40, 40);
        let rope = Rope::from_str("abc\n\tdef");
        let text = rope.slice(..);
        assert_eq!(
            view.text_pos_at_screen_coords(&text, 41, 40 + OFFSET_WITHOUT_LINE_NUMBERS + 1, 4),
            Some(4)
        );
    }

    #[test]
    fn test_text_pos_at_screen_coords_without_any_gutters() {
        let mut view = View::new(DocumentId::default(), vec![]);
        view.area = Rect::new(40, 40, 40, 40);
        let rope = Rope::from_str("abc\n\tdef");
        let text = rope.slice(..);
        assert_eq!(
            view.text_pos_at_screen_coords(&text, 41, 40 + 1, 4),
            Some(4)
        );
    }

    #[test]
    fn test_text_pos_at_screen_coords_cjk() {
        let mut view = View::new(
            DocumentId::default(),
            vec![GutterType::Diagnostics, GutterType::LineNumbers],
        );
        view.area = Rect::new(40, 40, 40, 40);
        let rope = Rope::from_str("Hi! こんにちは皆さん");
        let text = rope.slice(..);

        assert_eq!(
            view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET, 4),
            Some(0)
        );

        assert_eq!(
            view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 4, 4),
            Some(4)
        );
        assert_eq!(
            view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 5, 4),
            Some(4)
        );

        assert_eq!(
            view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 6, 4),
            Some(5)
        );

        assert_eq!(
            view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 7, 4),
            Some(5)
        );

        assert_eq!(
            view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 8, 4),
            Some(6)
        );
    }

    #[test]
    fn test_text_pos_at_screen_coords_graphemes() {
        let mut view = View::new(
            DocumentId::default(),
            vec![GutterType::Diagnostics, GutterType::LineNumbers],
        );
        view.area = Rect::new(40, 40, 40, 40);
        let rope = Rope::from_str("Hèl̀l̀ò world!");
        let text = rope.slice(..);

        assert_eq!(
            view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET, 4),
            Some(0)
        );

        assert_eq!(
            view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 1, 4),
            Some(1)
        );

        assert_eq!(
            view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 2, 4),
            Some(3)
        );

        assert_eq!(
            view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 3, 4),
            Some(5)
        );

        assert_eq!(
            view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 4, 4),
            Some(7)
        );
    }
}
