use serde::{Deserialize, Serialize};
use ts_rs::TS;

use super::block::BlockId;
use crate::model::document::Document;

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
#[ts(export)]
pub struct Position {
    #[serde(rename = "id")]
    pub id: BlockId,
    pub offset: usize,
}

impl Position {
    pub fn new(block_id: impl Into<BlockId>, offset: usize) -> Self {
        Self {
            id: block_id.into(),
            offset,
        }
    }
}

/// A (row, col) coordinate within a table's logical grid.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
#[ts(export)]
pub struct CellCoord {
    pub row: usize,
    pub col: usize,
}

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

/// A rectangular selection of table cells.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
#[ts(export)]
pub struct CellSelection {
    pub table_id: BlockId,
    /// The cell where the selection started.
    pub anchor_cell: CellCoord,
    /// The cell where the selection ended.
    pub focus_cell: CellCoord,
}

impl CellSelection {
    pub fn new(
        table_id: impl Into<BlockId>,
        anchor_cell: CellCoord,
        focus_cell: CellCoord,
    ) -> Self {
        Self {
            table_id: table_id.into(),
            anchor_cell,
            focus_cell,
        }
    }

    /// Single-cell selection.
    pub fn single(table_id: impl Into<BlockId>, row: usize, col: usize) -> Self {
        let coord = CellCoord::new(row, col);
        Self {
            table_id: table_id.into(),
            anchor_cell: coord.clone(),
            focus_cell: coord,
        }
    }

    /// Normalize to (top-left, bottom-right) regardless of selection direction.
    pub fn normalized(&self) -> (CellCoord, CellCoord) {
        let min_row = self.anchor_cell.row.min(self.focus_cell.row);
        let max_row = self.anchor_cell.row.max(self.focus_cell.row);
        let min_col = self.anchor_cell.col.min(self.focus_cell.col);
        let max_col = self.anchor_cell.col.max(self.focus_cell.col);
        (
            CellCoord::new(min_row, min_col),
            CellCoord::new(max_row, max_col),
        )
    }

    /// Check if a given (row, col) is within the selected range.
    pub fn contains(&self, row: usize, col: usize) -> bool {
        let (tl, br) = self.normalized();
        row >= tl.row && row <= br.row && col >= tl.col && col <= br.col
    }

    /// Returns true if exactly one cell is selected.
    pub fn is_single_cell(&self) -> bool {
        self.anchor_cell == self.focus_cell
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, TS)]
#[ts(export)]
pub struct Selection {
    pub anchor: Position,
    pub focus: Position,
    /// Table cell range selection. `None` means normal text selection.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub cell_selection: Option<CellSelection>,
}

impl Selection {
    pub fn collapsed(block_id: impl Into<BlockId>, offset: usize) -> Self {
        let pos = Position::new(block_id, offset);
        Self {
            anchor: pos.clone(),
            focus: pos,
            cell_selection: None,
        }
    }

    pub fn new(anchor: Position, focus: Position) -> Self {
        Self {
            anchor,
            focus,
            cell_selection: None,
        }
    }

    /// Create a selection with a table cell range.
    pub fn with_cell_selection(anchor: Position, focus: Position, cell_sel: CellSelection) -> Self {
        Self {
            anchor,
            focus,
            cell_selection: Some(cell_sel),
        }
    }

    /// Returns true if this is a table cell range selection.
    pub fn is_cell_selection(&self) -> bool {
        self.cell_selection.is_some()
    }

    pub fn is_collapsed(&self) -> bool {
        self.anchor == self.focus
    }

    /// Returns the block ID if both anchor and focus are in the same block.
    pub fn single_block(&self) -> Option<&BlockId> {
        if self.anchor.id == self.focus.id {
            Some(&self.anchor.id)
        } else {
            None
        }
    }

    /// Returns (start, end) positions in document order within a single block.
    /// For cross-block selections, returns None.
    pub fn ordered_offsets(&self) -> Option<(usize, usize)> {
        if self.anchor.id != self.focus.id {
            return None;
        }
        let start = self.anchor.offset.min(self.focus.offset);
        let end = self.anchor.offset.max(self.focus.offset);
        Some((start, end))
    }

    /// Returns (start, end) positions normalized to document order.
    pub fn normalized(&self, doc: &Document) -> (Position, Position) {
        if self.anchor.id == self.focus.id {
            if self.anchor.offset <= self.focus.offset {
                (self.anchor.clone(), self.focus.clone())
            } else {
                (self.focus.clone(), self.anchor.clone())
            }
        } else {
            let anchor_idx = doc.block_root_index(&self.anchor.id);
            let focus_idx = doc.block_root_index(&self.focus.id);
            match (anchor_idx, focus_idx) {
                (Some(a), Some(f)) if a <= f => (self.anchor.clone(), self.focus.clone()),
                _ => (self.focus.clone(), self.anchor.clone()),
            }
        }
    }
}

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

    #[test]
    fn collapsed_selection() {
        let sel = Selection::collapsed("b1", 5);
        assert!(sel.is_collapsed());
        assert_eq!(sel.single_block(), Some(&BlockId::new("b1")));
    }

    #[test]
    fn range_selection_same_block() {
        let sel = Selection::new(Position::new("b1", 2), Position::new("b1", 8));
        assert!(!sel.is_collapsed());
        assert_eq!(sel.ordered_offsets(), Some((2, 8)));
    }

    #[test]
    fn range_selection_cross_block() {
        let sel = Selection::new(Position::new("b1", 2), Position::new("b2", 3));
        assert!(sel.single_block().is_none());
        assert!(sel.ordered_offsets().is_none());
    }
}
