//! Layout engine for computing visible lines and soft wrapping.
//!
//! This module handles:
//! - Viewport computation from buffer snapshots
//! - Soft wrapping (word wrap) with configurable width
//! - Line-level layout caching for performance
//! - Cursor position mapping across wrapped lines

use std::collections::BTreeMap;
use std::ops::Range;

use crate::editor::buffer::{BufferSnapshot, Version};
use crate::editor::position::Position;

#[derive(Debug, Clone)]
pub struct LineLayout {
    pub logical_line: usize,
    pub visual_rows: Vec<VisualRow>,
    pub version: Version,
}

#[derive(Debug, Clone, PartialEq)]
pub struct VisualRow {
    pub byte_range: Range<usize>,
    pub char_range: Range<usize>,
    pub width: usize,
}

impl VisualRow {
    pub fn new(byte_start: usize, byte_end: usize, char_start: usize, char_end: usize) -> Self {
        Self {
            byte_range: byte_start..byte_end,
            char_range: char_start..char_end,
            width: char_end - char_start,
        }
    }
}

#[derive(Debug, Clone)]
pub struct ViewportLayout {
    pub lines: Vec<ViewportLine>,
    pub cursor_screen_pos: ScreenPosition,
    pub total_visual_rows: usize,
    pub version: Version,
}

#[derive(Debug, Clone)]
pub struct ViewportLine {
    pub logical_line: usize,
    pub visual_rows: Vec<VisualRow>,
    pub is_current_line: bool,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ScreenPosition {
    pub row: usize,
    pub col: usize,
}

impl ScreenPosition {
    pub fn new(row: usize, col: usize) -> Self {
        Self { row, col }
    }
}

#[derive(Debug, Default)]
pub struct LayoutCache {
    entries: BTreeMap<(Version, usize), LineLayout>,
    max_entries: usize,
}

impl LayoutCache {
    pub fn new() -> Self {
        Self {
            entries: BTreeMap::new(),
            max_entries: 1000,
        }
    }

    pub fn get(&self, version: Version, line: usize) -> Option<&LineLayout> {
        self.entries.get(&(version, line))
    }

    pub fn insert(&mut self, version: Version, line: usize, layout: LineLayout) {
        if self.entries.len() >= self.max_entries
            && let Some(first_key) = self.entries.keys().next().copied() {
                self.entries.remove(&first_key);
            }
        self.entries.insert((version, line), layout);
    }

    pub fn invalidate_for_version(&mut self, version: Version) {
        self.entries.retain(|(v, _), _| *v != version);
    }

    pub fn clear(&mut self) {
        self.entries.clear();
    }
}

pub struct LayoutEngine {
    cache: LayoutCache,
    wrap_width: usize,
    wrap_enabled: bool,
}

impl Default for LayoutEngine {
    fn default() -> Self {
        Self::new()
    }
}

impl LayoutEngine {
    pub fn new() -> Self {
        Self {
            cache: LayoutCache::new(),
            wrap_width: 80,
            wrap_enabled: true,
        }
    }

    pub fn with_wrap_width(mut self, width: usize) -> Self {
        self.wrap_width = width;
        self
    }

    pub fn with_wrap_enabled(mut self, enabled: bool) -> Self {
        self.wrap_enabled = enabled;
        self
    }

    pub fn set_wrap_width(&mut self, width: usize) {
        if self.wrap_width != width {
            self.wrap_width = width;
            self.cache.clear();
        }
    }

    pub fn set_wrap_enabled(&mut self, enabled: bool) {
        if self.wrap_enabled != enabled {
            self.wrap_enabled = enabled;
            self.cache.clear();
        }
    }

    pub fn wrap_width(&self) -> usize {
        self.wrap_width
    }

    pub fn wrap_enabled(&self) -> bool {
        self.wrap_enabled
    }

    pub fn invalidate_cache(&mut self, version: Version) {
        self.cache.invalidate_for_version(version);
    }

    pub fn compute_line_layout(
        &mut self,
        line: &str,
        logical_line: usize,
        version: Version,
    ) -> LineLayout {
        if let Some(cached) = self.cache.get(version, logical_line) {
            return cached.clone();
        }

        let visual_rows = if self.wrap_enabled && self.wrap_width > 0 {
            self.compute_wrap_segments(line)
        } else {
            vec![VisualRow::new(0, line.len(), 0, line.chars().count())]
        };

        let layout = LineLayout {
            logical_line,
            visual_rows,
            version,
        };

        self.cache.insert(version, logical_line, layout.clone());
        layout
    }

    fn compute_wrap_segments(&self, line: &str) -> Vec<VisualRow> {
        let mut rows = Vec::new();
        let chars: Vec<char> = line.chars().collect();
        let total_chars = chars.len();

        if total_chars == 0 {
            rows.push(VisualRow::new(0, 0, 0, 0));
            return rows;
        }

        let mut char_pos = 0;
        let mut byte_offset = 0;

        while char_pos < total_chars {
            let remaining = total_chars - char_pos;
            let segment_width = remaining.min(self.wrap_width);

            let segment_end = char_pos + segment_width;
            let mut segment_byte_end = byte_offset;

            for (i, ch) in chars[char_pos..].iter().enumerate() {
                if i >= segment_width {
                    break;
                }
                segment_byte_end += ch.len_utf8();
            }

            rows.push(VisualRow::new(byte_offset, segment_byte_end, char_pos, segment_end));

            char_pos = segment_end;
            byte_offset = segment_byte_end;
        }

        if rows.is_empty() {
            rows.push(VisualRow::new(0, 0, 0, 0));
        }

        rows
    }

    pub fn compute_viewport(
        &mut self,
        snapshot: &BufferSnapshot,
        cursor: Position,
        scroll: usize,
        height: usize,
        width: usize,
    ) -> ViewportLayout {
        let version = snapshot.version;

        if self.wrap_width != width && self.wrap_enabled {
            self.wrap_width = width;
            self.cache.clear();
        }

        let mut visible_lines = Vec::new();
        let mut visual_row = 0;
        let mut cursor_screen_pos = ScreenPosition::new(0, 0);

        let lines = snapshot.lines();
        let total_lines = lines.len();
        let mut buffer_line = scroll;

        while visual_row < height && buffer_line < total_lines {
            let line_text = &lines[buffer_line];
            let layout = self.compute_line_layout(line_text, buffer_line, version);
            let is_current_line = buffer_line == cursor.line;

            if is_current_line && cursor_screen_pos.row == 0 {
                cursor_screen_pos = self.map_cursor_to_screen(
                    cursor,
                    &layout,
                    visual_row,
                );
            }

            for row in &layout.visual_rows {
                if visual_row >= height {
                    break;
                }
                visible_lines.push(ViewportLine {
                    logical_line: buffer_line,
                    visual_rows: vec![row.clone()],
                    is_current_line,
                });
                visual_row += 1;
            }

            buffer_line += 1;
        }

        ViewportLayout {
            lines: visible_lines,
            cursor_screen_pos,
            total_visual_rows: visual_row,
            version,
        }
    }

    fn map_cursor_to_screen(
        &self,
        cursor: Position,
        layout: &LineLayout,
        start_visual_row: usize,
    ) -> ScreenPosition {
        if layout.visual_rows.is_empty() {
            return ScreenPosition::new(start_visual_row, 0);
        }

        for (row_idx, visual_row) in layout.visual_rows.iter().enumerate() {
            if cursor.column >= visual_row.char_range.start
                && cursor.column < visual_row.char_range.end
            {
                return ScreenPosition::new(
                    start_visual_row + row_idx,
                    cursor.column - visual_row.char_range.start,
                );
            }

            if cursor.column >= visual_row.char_range.end && row_idx == layout.visual_rows.len() - 1 {
                return ScreenPosition::new(
                    start_visual_row + row_idx,
                    visual_row.width,
                );
            }
        }

        let last_row = layout.visual_rows.last();
        if let Some(row) = last_row {
            if cursor.column >= row.char_range.end {
                ScreenPosition::new(start_visual_row + layout.visual_rows.len() - 1, row.width)
            } else {
                ScreenPosition::new(start_visual_row, 0)
            }
        } else {
            ScreenPosition::new(start_visual_row, 0)
        }
    }

    pub fn compute_cursor_screen_position(
        &mut self,
        lines: &[String],
        cursor: Position,
        scroll: usize,
        height: usize,
        version: Version,
    ) -> ScreenPosition {
        let mut visual_row = 0;
        let mut buffer_line = scroll;

        while visual_row < height && buffer_line <= cursor.line {
            let line_text = lines.get(buffer_line).map(|s| s.as_str()).unwrap_or("");
            let layout = self.compute_line_layout(line_text, buffer_line, version);

            for (row_idx, row) in layout.visual_rows.iter().enumerate() {
                if visual_row + row_idx >= height {
                    return ScreenPosition::new(visual_row + row_idx, 0);
                }

                if buffer_line == cursor.line {
                    if cursor.column >= row.char_range.start
                        && cursor.column < row.char_range.end
                    {
                        return ScreenPosition::new(visual_row + row_idx, cursor.column - row.char_range.start);
                    }
                    if row_idx == layout.visual_rows.len() - 1
                        && cursor.column >= row.char_range.end
                    {
                        return ScreenPosition::new(visual_row + row_idx, row.width);
                    }
                }

                if buffer_line < cursor.line {
                    visual_row += 1;
                }
            }

            if buffer_line < cursor.line {
                visual_row += layout.visual_rows.len();
            }

            buffer_line += 1;
        }

        ScreenPosition::new(visual_row, cursor.column)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn wrap_segments_basic() {
        let engine = LayoutEngine::new().with_wrap_width(10);
        let layout = engine.compute_wrap_segments("hello world foo bar");

        assert!(layout.len() > 1);
        let total_chars: usize = layout.iter().map(|r| r.width).sum();
        assert_eq!(total_chars, 19);
    }

    #[test]
    fn wrap_segments_short_line() {
        let engine = LayoutEngine::new().with_wrap_width(80);
        let layout = engine.compute_wrap_segments("hello");

        assert_eq!(layout.len(), 1);
        assert_eq!(layout[0].width, 5);
    }

    #[test]
    fn wrap_segments_empty_line() {
        let engine = LayoutEngine::new().with_wrap_width(80);
        let layout = engine.compute_wrap_segments("");

        assert_eq!(layout.len(), 1);
        assert_eq!(layout[0].width, 0);
    }

    #[test]
    fn cache_operations() {
        let mut cache = LayoutCache::new();
        let version = Version::new();

        let layout = LineLayout {
            logical_line: 0,
            visual_rows: vec![VisualRow::new(0, 5, 0, 5)],
            version,
        };

        cache.insert(version, 0, layout.clone());
        assert!(cache.get(version, 0).is_some());

        cache.invalidate_for_version(version);
        assert!(cache.get(version, 0).is_none());
    }
}