//! In-memory text buffer.
//!
//! Storage: one `String` per logical line, without trailing newlines.
//! Cursor columns are byte offsets at grapheme-cluster boundaries; display
//! columns are computed on demand via `unicode-width`.

use std::collections::VecDeque;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};

use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;

use crate::highlight::Language;

/// Line-ending convention used when saving.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LineEnding {
    /// `\n`.
    Unix,
    /// `\r\n`.
    Dos,
}

impl LineEnding {
    /// Short identifier for the status bar.
    pub fn as_str(self) -> &'static str {
        match self {
            Self::Unix => "LF",
            Self::Dos => "CRLF",
        }
    }
}

/// A position inside a buffer.  `col` is a **byte** offset within the line.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct Pos {
    /// Zero-based line index.
    pub row: usize,
    /// Byte offset within the line at a grapheme-cluster boundary.
    pub col: usize,
}

/// One reversible edit operation.
#[derive(Debug, Clone)]
pub enum Op {
    /// `text` was inserted at `at` — undo by deleting the same range.
    Insert {
        /// Where the text was inserted.
        at: Pos,
        /// What was inserted.
        text: String,
    },
    /// `text` was deleted starting at `at` — undo by re-inserting it.
    Delete {
        /// Where the text was deleted from.
        at: Pos,
        /// What was deleted.
        text: String,
    },
    /// A line was split at `at` — undo by joining.
    Split {
        /// Where the split happened.
        at: Pos,
    },
    /// Lines `at.row` and `at.row+1` were joined at column `at.col` — undo by splitting.
    Join {
        /// Where the join happened.
        at: Pos,
    },
}

/// One file / tab.
pub struct Buffer {
    /// Text content, one entry per line.
    pub lines: Vec<String>,
    /// Backing file path, if any.
    pub path: Option<PathBuf>,
    /// Has the buffer been edited since the last save?
    pub modified: bool,
    /// Cursor position.
    pub cursor: Pos,
    /// Goal column (display column) preserved across vertical movement.
    pub goal_col: usize,
    /// Index of the first visible line in the viewport.
    pub viewport_top: usize,
    /// Horizontal scroll offset (display columns).
    pub viewport_left: usize,
    /// Line-ending convention.
    pub line_ending: LineEnding,
    /// Encoding label (always `"UTF-8"` here).
    pub encoding: String,
    /// Detected language for syntax highlighting.
    pub language: Language,
    /// Per-line dirty flags, mirrored against `lines`.
    pub line_dirty: Vec<bool>,
    /// True if the viewport scrolled and the content area needs a full repaint.
    pub viewport_dirty: bool,
    undo: VecDeque<Op>,
    redo: Vec<Op>,
    max_undo: usize,
}

impl Buffer {
    /// Build an empty, untitled buffer.
    pub fn empty() -> Self {
        Self {
            lines: vec![String::new()],
            path: None,
            modified: false,
            cursor: Pos::default(),
            goal_col: 0,
            viewport_top: 0,
            viewport_left: 0,
            line_ending: LineEnding::Unix,
            encoding: "UTF-8".to_string(),
            language: Language::PlainText,
            line_dirty: vec![true],
            viewport_dirty: true,
            undo: VecDeque::with_capacity(64),
            redo: Vec::with_capacity(64),
            max_undo: 64,
        }
    }

    /// Load a file from disk (UTF-8 lossy; line endings auto-detected).
    pub fn open(path: &Path) -> io::Result<Self> {
        let bytes = fs::read(path)?;
        let content = String::from_utf8_lossy(&bytes).into_owned();
        let line_ending = if content.contains("\r\n") {
            LineEnding::Dos
        } else {
            LineEnding::Unix
        };
        let mut lines: Vec<String> = content
            .split('\n')
            .map(|l| l.trim_end_matches('\r').to_string())
            .collect();
        if lines.is_empty() {
            lines.push(String::new());
        }
        let language = Language::from_path(path);
        let n = lines.len();
        Ok(Self {
            lines,
            path: Some(path.to_path_buf()),
            modified: false,
            cursor: Pos::default(),
            goal_col: 0,
            viewport_top: 0,
            viewport_left: 0,
            line_ending,
            encoding: "UTF-8".to_string(),
            language,
            line_dirty: vec![true; n],
            viewport_dirty: true,
            undo: VecDeque::with_capacity(64),
            redo: Vec::with_capacity(64),
            max_undo: 64,
        })
    }

    /// Save the buffer at its current path.  Errors if no path is set.
    pub fn save(&mut self) -> io::Result<()> {
        let path = self
            .path
            .clone()
            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "no path"))?;
        self.save_as(&path)
    }

    /// Save the buffer at `path`, then make `path` the buffer's path.
    pub fn save_as(&mut self, path: &Path) -> io::Result<()> {
        let sep = match self.line_ending {
            LineEnding::Unix => "\n",
            LineEnding::Dos => "\r\n",
        };
        let cap: usize = self.lines.iter().map(|l| l.len() + sep.len()).sum();
        let mut buf = String::with_capacity(cap);
        for (i, line) in self.lines.iter().enumerate() {
            if i > 0 {
                buf.push_str(sep);
            }
            buf.push_str(line);
        }
        fs::write(path, buf.as_bytes())?;
        self.path = Some(path.to_path_buf());
        self.modified = false;
        self.language = Language::from_path(path);
        Ok(())
    }

    /// Display filename for the status / tab bar.
    pub fn filename(&self) -> &str {
        self.path
            .as_ref()
            .and_then(|p| p.file_name())
            .and_then(|n| n.to_str())
            .unwrap_or("[no name]")
    }

    fn resize_dirty(&mut self) {
        if self.line_dirty.len() != self.lines.len() {
            self.line_dirty.resize(self.lines.len(), true);
        }
    }

    fn mark(&mut self, row: usize) {
        self.resize_dirty();
        if let Some(d) = self.line_dirty.get_mut(row) {
            *d = true;
        }
    }

    fn mark_from(&mut self, row: usize) {
        self.resize_dirty();
        self.viewport_dirty = true;
        for d in self.line_dirty.iter_mut().skip(row) {
            *d = true;
        }
    }

    /// Mark every line dirty (forces a full repaint).
    pub fn mark_all_dirty(&mut self) {
        self.resize_dirty();
        for d in &mut self.line_dirty {
            *d = true;
        }
        self.viewport_dirty = true;
    }

    /// Move cursor left one grapheme cluster.
    pub fn move_left(&mut self) {
        if self.cursor.col == 0 {
            if self.cursor.row > 0 {
                self.cursor.row -= 1;
                self.cursor.col = self.lines[self.cursor.row].len();
            }
        } else {
            let line = &self.lines[self.cursor.row];
            let mut prev = 0;
            for (i, _) in line.grapheme_indices(true) {
                if i >= self.cursor.col {
                    break;
                }
                prev = i;
            }
            self.cursor.col = prev;
        }
        self.goal_col = self.cursor_display_col();
    }

    /// Move cursor right one grapheme cluster.
    pub fn move_right(&mut self) {
        let line_len = self.lines[self.cursor.row].len();
        if self.cursor.col >= line_len {
            if self.cursor.row + 1 < self.lines.len() {
                self.cursor.row += 1;
                self.cursor.col = 0;
            }
        } else {
            let line = &self.lines[self.cursor.row];
            let mut next = line_len;
            for (i, _) in line.grapheme_indices(true) {
                if i > self.cursor.col {
                    next = i;
                    break;
                }
            }
            self.cursor.col = next;
        }
        self.goal_col = self.cursor_display_col();
    }

    /// Move cursor up one line, trying to preserve `goal_col`.
    pub fn move_up(&mut self) {
        if self.cursor.row == 0 {
            self.cursor.col = 0;
            return;
        }
        self.cursor.row -= 1;
        self.cursor.col = self.col_for_display(self.cursor.row, self.goal_col);
    }

    /// Move cursor down one line, trying to preserve `goal_col`.
    pub fn move_down(&mut self) {
        if self.cursor.row + 1 >= self.lines.len() {
            self.cursor.col = self.lines[self.cursor.row].len();
            return;
        }
        self.cursor.row += 1;
        self.cursor.col = self.col_for_display(self.cursor.row, self.goal_col);
    }

    /// Cursor → start of line.
    pub fn move_home(&mut self) {
        self.cursor.col = 0;
        self.goal_col = 0;
    }

    /// Cursor → end of line.
    pub fn move_end(&mut self) {
        self.cursor.col = self.lines[self.cursor.row].len();
        self.goal_col = self.cursor_display_col();
    }

    /// Move cursor to the next word boundary.
    pub fn move_word_right(&mut self) {
        let n = self.lines.len();
        loop {
            let line = &self.lines[self.cursor.row];
            if self.cursor.col >= line.len() {
                if self.cursor.row + 1 >= n {
                    break;
                }
                self.cursor.row += 1;
                self.cursor.col = 0;
                continue;
            }
            let rest = &line[self.cursor.col..];
            let mut skipped = rest.len();
            for (i, c) in rest.char_indices() {
                if !c.is_whitespace() {
                    skipped = i;
                    break;
                }
            }
            let mut end = rest.len();
            for (i, c) in rest[skipped..].char_indices() {
                if c.is_whitespace() {
                    end = skipped + i;
                    break;
                }
            }
            self.cursor.col += end;
            break;
        }
        self.goal_col = self.cursor_display_col();
    }

    /// Move cursor to the previous word boundary.
    pub fn move_word_left(&mut self) {
        loop {
            if self.cursor.col == 0 {
                if self.cursor.row == 0 {
                    break;
                }
                self.cursor.row -= 1;
                self.cursor.col = self.lines[self.cursor.row].len();
                continue;
            }
            let line = &self.lines[self.cursor.row];
            let prefix = &line[..self.cursor.col];
            let mut chars: Vec<(usize, char)> = prefix.char_indices().collect();
            while let Some(&(_, c)) = chars.last() {
                if c.is_whitespace() {
                    chars.pop();
                } else {
                    break;
                }
            }
            while let Some(&(_, c)) = chars.last() {
                if !c.is_whitespace() {
                    chars.pop();
                } else {
                    break;
                }
            }
            self.cursor.col = chars
                .last()
                .map(|&(i, c)| i + c.len_utf8())
                .unwrap_or(0);
            break;
        }
        self.goal_col = self.cursor_display_col();
    }

    /// Page up by `n` lines.
    pub fn page_up(&mut self, n: usize) {
        self.cursor.row = self.cursor.row.saturating_sub(n);
        self.cursor.col = self.col_for_display(self.cursor.row, self.goal_col);
    }

    /// Page down by `n` lines.
    pub fn page_down(&mut self, n: usize) {
        let last = self.lines.len().saturating_sub(1);
        self.cursor.row = (self.cursor.row + n).min(last);
        self.cursor.col = self.col_for_display(self.cursor.row, self.goal_col);
    }

    /// Insert a single character at the cursor.
    pub fn insert_char(&mut self, c: char) {
        let mut s = [0u8; 4];
        let st: &str = c.encode_utf8(&mut s);
        let owned = st.to_string();
        self.insert_str(&owned);
    }

    /// Insert a string at the cursor.  The string must not contain newlines.
    pub fn insert_str(&mut self, text: &str) {
        let at = self.cursor;
        self.lines[at.row].insert_str(at.col, text);
        self.cursor.col += text.len();
        self.push_undo(Op::Insert { at, text: text.to_string() });
        self.modified = true;
        self.mark(at.row);
        self.goal_col = self.cursor_display_col();
    }

    /// Backspace: delete the grapheme to the left of the cursor.
    pub fn backspace(&mut self) {
        if self.cursor.col == 0 {
            if self.cursor.row == 0 {
                return;
            }
            let prev_len = self.lines[self.cursor.row - 1].len();
            let current = self.lines.remove(self.cursor.row);
            self.line_dirty.remove(self.cursor.row);
            self.lines[self.cursor.row - 1].push_str(&current);
            self.cursor.row -= 1;
            self.cursor.col = prev_len;
            self.push_undo(Op::Join {
                at: Pos { row: self.cursor.row, col: prev_len },
            });
            self.modified = true;
            self.mark_from(self.cursor.row);
        } else {
            let line = &self.lines[self.cursor.row];
            let mut prev = 0;
            for (i, _) in line.grapheme_indices(true) {
                if i >= self.cursor.col {
                    break;
                }
                prev = i;
            }
            let deleted = self.lines[self.cursor.row][prev..self.cursor.col].to_string();
            self.lines[self.cursor.row].replace_range(prev..self.cursor.col, "");
            self.cursor.col = prev;
            self.push_undo(Op::Delete {
                at: Pos { row: self.cursor.row, col: prev },
                text: deleted,
            });
            self.modified = true;
            self.mark(self.cursor.row);
        }
        self.goal_col = self.cursor_display_col();
    }

    /// Forward-delete: delete the grapheme at the cursor.
    pub fn delete(&mut self) {
        let line_len = self.lines[self.cursor.row].len();
        if self.cursor.col >= line_len {
            if self.cursor.row + 1 >= self.lines.len() {
                return;
            }
            let next = self.lines.remove(self.cursor.row + 1);
            self.line_dirty.remove(self.cursor.row + 1);
            self.lines[self.cursor.row].push_str(&next);
            self.push_undo(Op::Join { at: self.cursor });
            self.modified = true;
            self.mark_from(self.cursor.row);
        } else {
            let line = &self.lines[self.cursor.row];
            let mut next = line_len;
            for (i, _) in line.grapheme_indices(true) {
                if i > self.cursor.col {
                    next = i;
                    break;
                }
            }
            let deleted = line[self.cursor.col..next].to_string();
            self.lines[self.cursor.row].replace_range(self.cursor.col..next, "");
            self.push_undo(Op::Delete {
                at: self.cursor,
                text: deleted,
            });
            self.modified = true;
            self.mark(self.cursor.row);
        }
    }

    /// Insert a line break at the cursor (Enter).
    pub fn insert_newline(&mut self) {
        let at = self.cursor;
        let rest = self.lines[at.row].split_off(at.col);
        self.lines.insert(at.row + 1, rest);
        self.line_dirty.insert(at.row + 1, true);
        self.cursor.row += 1;
        self.cursor.col = 0;
        self.push_undo(Op::Split { at });
        self.modified = true;
        self.mark_from(at.row);
        self.goal_col = 0;
    }

    fn push_undo(&mut self, op: Op) {
        if self.undo.len() == self.max_undo {
            self.undo.pop_front();
        }
        self.undo.push_back(op);
        self.redo.clear();
    }

    /// Undo the most recent edit.  Returns `true` if anything happened.
    pub fn undo(&mut self) -> bool {
        let op = match self.undo.pop_back() {
            Some(o) => o,
            None => return false,
        };
        let inverse = self.apply_inverse(&op);
        self.redo.push(inverse);
        self.modified = true;
        true
    }

    /// Redo the most recently undone edit.  Returns `true` if anything happened.
    pub fn redo(&mut self) -> bool {
        let op = match self.redo.pop() {
            Some(o) => o,
            None => return false,
        };
        let inverse = self.apply_inverse(&op);
        if self.undo.len() == self.max_undo {
            self.undo.pop_front();
        }
        self.undo.push_back(inverse);
        self.modified = true;
        true
    }

    fn apply_inverse(&mut self, op: &Op) -> Op {
        match op {
            Op::Insert { at, text } => {
                let end = at.col + text.len();
                self.lines[at.row].replace_range(at.col..end, "");
                self.cursor = *at;
                self.mark(at.row);
                Op::Delete { at: *at, text: text.clone() }
            }
            Op::Delete { at, text } => {
                self.lines[at.row].insert_str(at.col, text);
                self.cursor = Pos { row: at.row, col: at.col + text.len() };
                self.mark(at.row);
                Op::Insert { at: *at, text: text.clone() }
            }
            Op::Split { at } => {
                if at.row + 1 < self.lines.len() {
                    let next = self.lines.remove(at.row + 1);
                    self.line_dirty.remove(at.row + 1);
                    self.lines[at.row].push_str(&next);
                }
                self.cursor = *at;
                self.mark_from(at.row);
                Op::Join { at: *at }
            }
            Op::Join { at } => {
                let rest = self.lines[at.row].split_off(at.col);
                self.lines.insert(at.row + 1, rest);
                self.line_dirty.insert(at.row + 1, true);
                self.cursor = *at;
                self.mark_from(at.row);
                Op::Split { at: *at }
            }
        }
    }

    /// Current cursor position expressed as a display column.
    pub fn cursor_display_col(&self) -> usize {
        let line = &self.lines[self.cursor.row];
        let end = self.cursor.col.min(line.len());
        UnicodeWidthStr::width(&line[..end])
    }

    fn col_for_display(&self, row: usize, target: usize) -> usize {
        let line = &self.lines[row];
        let mut width = 0usize;
        let mut last = 0usize;
        for (i, g) in line.grapheme_indices(true) {
            let w = UnicodeWidthStr::width(g);
            if width + w > target {
                return i;
            }
            width += w;
            last = i + g.len();
        }
        last
    }

    /// Scroll the viewport so the cursor is visible.  Returns `true` if the
    /// viewport moved.
    pub fn ensure_cursor_visible(&mut self, rows: u16, cols: u16) -> bool {
        let rows = rows.max(1) as usize;
        let cols = cols.max(1) as usize;
        let mut scrolled = false;
        if self.cursor.row < self.viewport_top {
            self.viewport_top = self.cursor.row;
            scrolled = true;
        } else if self.cursor.row >= self.viewport_top + rows {
            self.viewport_top = self.cursor.row + 1 - rows;
            scrolled = true;
        }
        let dcol = self.cursor_display_col();
        if dcol < self.viewport_left {
            self.viewport_left = dcol;
            scrolled = true;
        } else if dcol >= self.viewport_left + cols {
            self.viewport_left = dcol + 1 - cols;
            scrolled = true;
        }
        if scrolled {
            self.viewport_dirty = true;
            self.mark_all_dirty();
        }
        scrolled
    }

    /// Find the next occurrence of `query` after `from` (exclusive of `from`),
    /// wrapping around the buffer.  Returns `None` only for empty `query`
    /// or no matches.
    pub fn find_next(&self, query: &str, from: Pos) -> Option<Pos> {
        if query.is_empty() || self.lines.is_empty() {
            return None;
        }
        let n = self.lines.len();
        for offset in 0..=n {
            let row = (from.row + offset) % n;
            let line = &self.lines[row];
            let start = if offset == 0 {
                (from.col + 1).min(line.len())
            } else {
                0
            };
            if let Some(found) = line[start..].find(query) {
                return Some(Pos { row, col: start + found });
            }
        }
        None
    }

    /// Collect every match of `query` on `row` (used for highlighting).
    pub fn matches_on(&self, row: usize, query: &str) -> Vec<(usize, usize)> {
        if query.is_empty() || row >= self.lines.len() {
            return Vec::new();
        }
        let line = &self.lines[row];
        let mut out = Vec::new();
        let mut start = 0;
        while let Some(p) = line[start..].find(query) {
            let abs = start + p;
            out.push((abs, abs + query.len()));
            start = abs + query.len().max(1);
        }
        out
    }
}
