use std::collections::BTreeSet;
use std::io::ErrorKind;
use std::path::{Path, PathBuf};

mod edit;
mod read;
mod replacement_store;
mod search;
pub(crate) mod walk;

use crate::config::Config;
use crate::core::file_view::FileView;
use crate::core::page_cache::CacheStats;
use crate::core::piece_table::{CellId, Piece, PieceSource, PieceTable};
use crate::core::save;
use crate::error::{HxError, HxResult};

use replacement_store::ReplacementStore;

const SEARCH_CHUNK: usize = 64 * 1024;

/// What the renderer sees at a given display offset.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ByteSlot {
    /// A visible byte with the given value (after applying replacements).
    Present(u8),
    /// A tombstone-deleted byte — still occupies a display slot but is
    /// rendered as "XX" and skipped on save.
    Deleted,
    /// Past the end of the document.
    Empty,
}

/// Backing document model for the editor.
///
/// Composes three layers:
///
/// 1. **PieceTable** — tracks insertions and real deletions (insert-mode
///    backspace).  These immediately shift subsequent display offsets.
/// 2. **Tombstones** (`BTreeSet<CellId>`) — normal/visual-mode deletions.
///    The cell still occupies its display slot (shown as `Deleted`) but is
///    skipped on save.
/// 3. **Replacements** (`ReplacementStore`) — in-place byte edits
///    (edit-mode nibble changes and large range overlays).  The replacement
///    value overrides the base byte from the file or add-buffer.
///
/// All external interfaces use *display offsets* derived from the piece table.
#[derive(Debug)]
pub struct Document {
    path: PathBuf,
    readonly: bool,
    page_size: usize,
    cache_pages: usize,
    original_len: u64,
    fixed_size: bool,
    view: FileView,
    pieces: PieceTable,
    tombstones: BTreeSet<CellId>,
    replacements: ReplacementStore,
}

impl Document {
    /// Open a document from disk with the given configuration.
    pub fn open(path: &Path, config: &Config) -> HxResult<Self> {
        let (view, readonly) =
            match FileView::open(path, config.readonly, config.page_size, config.cache_pages) {
                Ok(view) => (view, config.readonly),
                Err(err)
                    if !config.readonly
                        && matches!(
                            &err,
                            HxError::OpenPath { source, .. }
                                if source.kind() == ErrorKind::PermissionDenied
                        ) =>
                {
                    (
                        FileView::open(path, true, config.page_size, config.cache_pages)?,
                        true,
                    )
                }
                Err(err) => return Err(err),
            };
        let original_len = view.len();
        Ok(Self {
            path: path.to_path_buf(),
            readonly,
            page_size: config.page_size,
            cache_pages: config.cache_pages,
            original_len,
            fixed_size: false,
            view,
            pieces: PieceTable::new(original_len),
            tombstones: BTreeSet::new(),
            replacements: ReplacementStore::default(),
        })
    }

    pub fn from_memory_bytes(path: PathBuf, bytes: Vec<u8>, config: &Config) -> Self {
        let original_len = bytes.len() as u64;
        Self {
            path: path.clone(),
            readonly: false,
            page_size: config.page_size,
            cache_pages: config.cache_pages,
            original_len,
            fixed_size: true,
            view: FileView::from_bytes(path, bytes, config.page_size, config.cache_pages),
            pieces: PieceTable::new(original_len),
            tombstones: BTreeSet::new(),
            replacements: ReplacementStore::default(),
        }
    }

    pub fn path(&self) -> &Path {
        &self.path
    }

    pub fn original_len(&self) -> u64 {
        self.original_len
    }

    /// Total display length including tombstoned slots.
    pub fn len(&self) -> u64 {
        self.pieces.len()
    }

    pub fn is_empty(&self) -> bool {
        self.pieces.len() == 0
    }

    /// Number of bytes that would be written on save (display len minus tombstones).
    pub fn visible_len(&self) -> u64 {
        self.pieces
            .len()
            .saturating_sub(self.tombstones.len() as u64)
    }

    /// True when any edits (inserts, deletions, replacements) have been made
    /// since the last save.
    pub fn is_dirty(&self) -> bool {
        !self.pieces.is_identity() || !self.tombstones.is_empty() || !self.replacements.is_empty()
    }

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

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

    pub fn io_stats(&self) -> CacheStats {
        self.view.cache_stats()
    }

    /// Save the document to disk. Walks the piece list in bulk, skipping
    /// tombstones and applying replacements. After saving, resets all edit
    /// state and reloads the file.
    pub fn save(&mut self, path: Option<PathBuf>) -> HxResult<(PathBuf, save::SaveProfile)> {
        if self.fixed_size {
            return Err(HxError::FixedSizeViolation);
        }
        let target = path.unwrap_or_else(|| self.path.clone());
        if self.readonly && target == self.path {
            return Err(HxError::ReadOnly);
        }

        let profile = save::save_rewrite(self, &target)?;

        self.path = target.clone();
        self.view
            .reload(&target, self.readonly, self.page_size, self.cache_pages)?;
        self.original_len = self.view.len();
        self.fixed_size = false;
        self.pieces = PieceTable::new(self.original_len);
        self.tombstones.clear();
        self.replacements.clear();
        Ok((target, profile))
    }

    /// Validate and return a display offset for `:goto`.
    pub fn goto(&self, offset: u64) -> HxResult<u64> {
        if self.is_empty() {
            return Ok(0);
        }
        if offset >= self.len() {
            return Err(HxError::OffsetOutOfRange);
        }
        Ok(offset)
    }

    /// Preferred chunk size for piece-walking readers. Keeping reads within the
    /// configured page-cache footprint avoids excessive churn for small cache
    /// configurations, even though the cache itself can now assemble larger
    /// ranges safely.
    pub(crate) fn max_contiguous_read_len(&self) -> usize {
        self.page_size
            .saturating_mul(self.cache_pages)
            .max(self.page_size)
            .max(1)
    }

    /// Cheap snapshot of the piece list (small structs, no data).
    pub fn pieces_snapshot(&self) -> Vec<Piece> {
        self.pieces.pieces().to_vec()
    }

    /// Collect `CellId`s for a contiguous display range, using the piece
    /// table's batched walk. Skips tombstone filtering — callers are
    /// expected to check `is_tombstone` on each returned id.
    pub fn cell_ids_range(&self, offset: u64, len: u64) -> Vec<CellId> {
        self.pieces.cell_ids_range(offset, len)
    }

    /// Check whether a cell is tombstoned (O(log n) BTreeSet lookup).
    pub fn is_tombstone(&self, id: CellId) -> bool {
        self.tombstones.contains(&id)
    }

    /// True when any tombstones exist.
    pub fn has_tombstones(&self) -> bool {
        !self.tombstones.is_empty()
    }

    /// True when any replacements exist.
    pub fn has_replacements(&self) -> bool {
        !self.replacements.is_empty()
    }

    /// Check if any tombstone falls within a CellId range (inclusive).
    pub fn has_tombstone_in_range(&self, lo: CellId, hi: CellId) -> bool {
        use std::ops::Bound;
        self.tombstones
            .range((Bound::Included(lo), Bound::Included(hi)))
            .next()
            .is_some()
    }

    /// Check if any replacement falls within a CellId range (inclusive).
    pub fn has_replacement_in_range(&self, lo: CellId, hi: CellId) -> bool {
        self.replacements.has_in_range(lo, hi)
    }

    /// Map a display offset to the corresponding logical byte offset.
    ///
    /// Tombstoned display slots have no logical byte and return `None`.
    /// Without tombstones this is the identity mapping.
    pub fn logical_offset_for_display_offset(&self, display_offset: u64) -> Option<u64> {
        if display_offset >= self.len() {
            return None;
        }
        let id = self.cell_id_at(display_offset)?;
        if self.is_tombstone(id) {
            return None;
        }
        if !self.has_tombstones() {
            return Some(display_offset);
        }

        let mut display_cursor = 0_u64;
        let mut logical_cursor = 0_u64;
        for piece in self.pieces.pieces() {
            let piece_end = display_cursor + piece.len;
            if piece_end <= display_offset {
                logical_cursor += piece
                    .len
                    .saturating_sub(self.tombstone_count_in_piece(*piece));
                display_cursor = piece_end;
                continue;
            }

            let local_len = display_offset - display_cursor;
            logical_cursor +=
                local_len.saturating_sub(self.tombstone_count_in_piece_prefix(*piece, local_len));
            return Some(logical_cursor);
        }
        None
    }

    /// Map a logical byte offset back to a display offset.
    pub fn display_offset_for_logical_offset(&self, logical_offset: u64) -> Option<u64> {
        if logical_offset >= self.visible_len() {
            return None;
        }
        if !self.has_tombstones() {
            return (logical_offset < self.len()).then_some(logical_offset);
        }

        let mut display_cursor = 0_u64;
        let mut logical_cursor = 0_u64;
        for piece in self.pieces.pieces() {
            let visible_in_piece = piece
                .len
                .saturating_sub(self.tombstone_count_in_piece(*piece));
            if logical_cursor + visible_in_piece <= logical_offset {
                logical_cursor += visible_in_piece;
                display_cursor += piece.len;
                continue;
            }

            let mut display_local = logical_offset - logical_cursor;
            for tombstone_source_offset in self.tombstone_source_offsets_in_piece(*piece) {
                let tombstone_local = tombstone_source_offset.saturating_sub(piece.start);
                if tombstone_local <= display_local {
                    display_local += 1;
                } else {
                    break;
                }
            }
            return (display_local < piece.len).then_some(display_cursor + display_local);
        }
        None
    }

    /// Return the replacement value for a cell, if any. `base` must be the
    /// unmodified byte for that cell; range overlays such as XOR derive their
    /// visible byte from it.
    pub fn replacement_for(&self, id: CellId, base: u8) -> Option<u8> {
        self.replacements.get(id, base)
    }

    /// Return contiguous replacement runs keyed by original/display offset.
    ///
    /// Fixed-size memory documents are replacement-only, so their piece table
    /// remains the identity mapping and every replacement is an original cell.
    /// This helper gives the memory commit path compact write ranges without
    /// exposing the replacement map itself.
    pub fn replacement_spans(&mut self) -> HxResult<Vec<(u64, Vec<u8>)>> {
        let mut spans: Vec<(u64, Vec<u8>)> = Vec::new();
        for (id, value) in self.replacements.sparse_values() {
            if self.replacements.has_range_at(id) {
                continue;
            }
            let CellId::Original(offset) = id else {
                continue;
            };
            if let Some((start, bytes)) = spans.last_mut() {
                if *start + bytes.len() as u64 == offset {
                    bytes.push(value);
                    continue;
                }
            }
            spans.push((offset, vec![value]));
        }
        for range in self.replacements.range_snapshots() {
            if range.source != PieceSource::Original {
                continue;
            }
            let mut offset = range.source_offset;
            let end = offset + range.len;
            while offset < end {
                let batch = (end - offset).min(64 * 1024) as usize;
                let raw = self.raw_range(offset, batch)?;
                if raw.is_empty() {
                    break;
                }
                let mut span_start = offset;
                let mut bytes = Vec::new();
                for (idx, &base) in raw.iter().enumerate() {
                    let id = CellId::Original(offset + idx as u64);
                    match self.replacements.get(id, base) {
                        Some(value) => bytes.push(value),
                        None => {
                            push_span(&mut spans, span_start, std::mem::take(&mut bytes));
                            span_start = offset + idx as u64 + 1;
                        }
                    }
                }
                push_span(&mut spans, span_start, bytes);
                offset += raw.len() as u64;
            }
        }
        spans.sort_by_key(|(offset, _)| *offset);
        Ok(merge_spans(spans))
    }

    pub fn replacement_dirty_bytes(&self) -> usize {
        self.replacements.dirty_bytes()
    }

    /// Clear all replacement overlays after an external fixed-size commit.
    pub fn clear_replacements(&mut self) {
        self.replacements.clear();
    }

    /// Borrow a slice of the add-buffer.
    pub fn add_slice(&self, start: u64, len: u64) -> &[u8] {
        self.pieces.add_buffer_slice(start, len)
    }

    fn tombstone_count_in_piece(&self, piece: Piece) -> u64 {
        self.tombstone_count_in_piece_prefix(piece, piece.len)
    }

    fn tombstone_count_in_piece_prefix(&self, piece: Piece, len: u64) -> u64 {
        if len == 0 || !self.has_tombstones() {
            return 0;
        }
        let clamped = len.min(piece.len);
        let lo = CellId::from_source(piece.source, piece.start);
        let hi = CellId::from_source(piece.source, piece.start + clamped - 1);
        use std::ops::Bound;
        self.tombstones
            .range((Bound::Included(lo), Bound::Included(hi)))
            .count() as u64
    }

    fn tombstone_source_offsets_in_piece(&self, piece: Piece) -> Vec<u64> {
        if piece.len == 0 || !self.has_tombstones() {
            return Vec::new();
        }
        let lo = CellId::from_source(piece.source, piece.start);
        let hi = CellId::from_source(piece.source, piece.start + piece.len - 1);
        use std::ops::Bound;
        self.tombstones
            .range((Bound::Included(lo), Bound::Included(hi)))
            .filter_map(|id| match (piece.source, *id) {
                (PieceSource::Original, CellId::Original(offset))
                | (PieceSource::Add, CellId::Add(offset)) => Some(offset),
                _ => None,
            })
            .collect()
    }
}

impl Document {
    /// Resolve a display offset to (CellId, current display byte) for editing.
    fn display_byte_for_edit(&mut self, offset: u64) -> HxResult<(CellId, u8)> {
        let id = self.cell_id_at(offset).ok_or(HxError::OffsetOutOfRange)?;
        if self.tombstones.contains(&id) {
            return Err(HxError::OffsetOutOfRange);
        }
        Ok((id, self.display_byte_for_id(id)?))
    }

    /// Get the display value for a cell: replacement if present, else base byte.
    fn display_byte_for_id(&mut self, id: CellId) -> HxResult<u8> {
        let base = self.base_byte(id)?;
        if let Some(value) = self.replacements.get(id, base) {
            return Ok(value);
        }
        Ok(base)
    }

    /// Set the display value for a cell. If the new value equals the base
    /// byte, the replacement entry is removed.
    fn set_display_byte_by_id(&mut self, id: CellId, value: u8) -> HxResult<()> {
        let base = self.base_byte(id)?;
        if value == base {
            self.replacements.clear_cell(id);
        } else {
            self.replacements.set_cell(id, value);
        }
        Ok(())
    }

    /// Read the base (unmodified) byte for a cell from the file or add-buffer.
    fn base_byte(&mut self, id: CellId) -> HxResult<u8> {
        match id {
            CellId::Original(offset) => self.raw_byte(offset),
            CellId::Add(offset) => self
                .pieces
                .add_byte(offset)
                .ok_or(HxError::OffsetOutOfRange),
        }
    }

    /// Read a single byte from the original file via the page cache.
    fn raw_byte(&mut self, offset: u64) -> HxResult<u8> {
        let raw = self.view.read_range(offset, 1)?;
        raw.first().copied().ok_or(HxError::OffsetOutOfRange)
    }

    /// Read a chunk of bytes from the given source (Original file or Add buffer).
    fn read_chunk(&mut self, source: PieceSource, offset: u64, len: usize) -> HxResult<Vec<u8>> {
        match source {
            PieceSource::Original => self.raw_range(offset, len),
            PieceSource::Add => Ok(self.add_slice(offset, len as u64).to_vec()),
        }
    }
}

fn push_span(spans: &mut Vec<(u64, Vec<u8>)>, offset: u64, bytes: Vec<u8>) {
    if bytes.is_empty() {
        return;
    }
    if let Some((start, existing)) = spans.last_mut() {
        if *start + existing.len() as u64 == offset {
            existing.extend_from_slice(&bytes);
            return;
        }
    }
    spans.push((offset, bytes));
}

fn merge_spans(spans: Vec<(u64, Vec<u8>)>) -> Vec<(u64, Vec<u8>)> {
    let mut merged: Vec<(u64, Vec<u8>)> = Vec::new();
    for (offset, bytes) in spans {
        push_span(&mut merged, offset, bytes);
    }
    merged
}

#[cfg(test)]
mod tests {
    use std::path::PathBuf;

    use crate::config::Config;
    use crate::core::document::{ByteSlot, Document};
    use crate::core::piece_table::CellId;
    use crate::error::HxError;
    use crate::mode::NibblePhase;

    #[test]
    fn fixed_size_memory_document_allows_replacement_only() {
        let config = Config::default();
        let mut doc = Document::from_memory_bytes(
            PathBuf::from("memory://pid/0x1000-0x1004"),
            vec![0x12, 0x34, 0x56, 0x78],
            &config,
        );

        assert!(doc.is_fixed_size());
        assert_eq!(doc.len(), 4);
        assert_eq!(doc.visible_len(), 4);
        assert_eq!(doc.original_len(), 4);

        let id = doc.replace_nibble(1, NibblePhase::Low, 0xf).unwrap();
        assert_eq!(doc.byte_at(1).unwrap(), ByteSlot::Present(0x3f));
        doc.replace_display_byte_by_id(id, 0x34).unwrap();
        assert_eq!(doc.byte_at(1).unwrap(), ByteSlot::Present(0x34));
        assert_eq!(doc.len(), 4);
        assert_eq!(doc.visible_len(), 4);
    }

    #[test]
    fn fixed_size_memory_document_rejects_length_changing_ops() {
        let config = Config::default();
        let mut doc = Document::from_memory_bytes(
            PathBuf::from("memory://pid/0x1000-0x1004"),
            vec![0x12, 0x34, 0x56, 0x78],
            &config,
        );

        assert!(matches!(
            doc.replace_nibble(doc.len(), NibblePhase::High, 0xa),
            Err(HxError::FixedSizeViolation)
        ));
        assert!(matches!(
            doc.set_byte(doc.len(), 0xff),
            Err(HxError::FixedSizeViolation)
        ));
        assert!(matches!(
            doc.insert_byte(1, 0xff),
            Err(HxError::FixedSizeViolation)
        ));
        assert!(matches!(
            doc.mark_tombstone(1),
            Err(HxError::FixedSizeViolation)
        ));
        assert!(matches!(
            doc.delete_range_real(1, 1),
            Err(HxError::FixedSizeViolation)
        ));
        assert!(matches!(
            doc.restore_real_delete(1, &[CellId::Original(1)]),
            Err(HxError::FixedSizeViolation)
        ));
        assert!(matches!(doc.save(None), Err(HxError::FixedSizeViolation)));
        assert_eq!(doc.len(), 4);
        assert_eq!(doc.visible_len(), 4);
    }

    #[test]
    fn apply_replacement_spans_roundtrips_with_replacement_spans() {
        let config = Config::default();
        let mut doc = Document::from_memory_bytes(
            PathBuf::from("memory://pid/0x1000-0x1004"),
            vec![0x12, 0x34, 0x56, 0x78],
            &config,
        );

        let spans = vec![(0u64, vec![0xaa, 0xbb]), (3u64, vec![0xcc])];
        doc.apply_replacement_spans(&spans).unwrap();
        assert_eq!(doc.byte_at(0).unwrap(), ByteSlot::Present(0xaa));
        assert_eq!(doc.byte_at(1).unwrap(), ByteSlot::Present(0xbb));
        assert_eq!(doc.byte_at(3).unwrap(), ByteSlot::Present(0xcc));
        assert_eq!(doc.replacement_spans().unwrap(), spans);

        // Out-of-bounds spans are rejected without changing length.
        assert!(matches!(
            doc.apply_replacement_spans(&[(4, vec![0x00])]),
            Err(HxError::OffsetOutOfRange)
        ));
        assert_eq!(doc.len(), 4);
    }

    #[test]
    fn pattern_overlay_supports_sparse_clears_and_span_export() {
        let config = Config::default();
        let mut doc = Document::from_memory_bytes(
            PathBuf::from("memory://pid/0x1000-0x1006"),
            vec![0, 1, 2, 3, 4, 5],
            &config,
        );

        let stats = doc
            .overwrite_run_pattern_overlay(1, 4, &[0xaa, 0xbb])
            .unwrap();
        assert_eq!(stats.visited, 4);
        assert_eq!(stats.changed, 4);
        assert_eq!(
            doc.logical_bytes(0, 5).unwrap(),
            vec![0, 0xaa, 0xbb, 0xaa, 0xbb, 5]
        );
        assert_eq!(doc.replacement_dirty_bytes(), 4);

        doc.replace_display_byte(2, 2).unwrap();
        assert_eq!(doc.byte_at(2).unwrap(), ByteSlot::Present(2));
        assert_eq!(doc.replacement_dirty_bytes(), 3);
        assert_eq!(
            doc.replacement_spans().unwrap(),
            vec![(1, vec![0xaa]), (3, vec![0xaa, 0xbb])]
        );

        doc.clear_replacements_in_display_range(3, 1).unwrap();
        assert_eq!(
            doc.logical_bytes(0, 5).unwrap(),
            vec![0, 0xaa, 2, 3, 0xbb, 5]
        );
        assert_eq!(doc.replacement_dirty_bytes(), 2);
        assert_eq!(
            doc.replacement_spans().unwrap(),
            vec![(1, vec![0xaa]), (4, vec![0xbb])]
        );
    }

    #[test]
    fn pattern_overlay_treats_full_run_as_changed_even_when_bytes_match() {
        let config = Config::default();
        let mut doc = Document::from_memory_bytes(
            PathBuf::from("memory://pid/0x1000-0x1004"),
            vec![0xaa, 0xbb, 0xaa, 0xbb],
            &config,
        );

        let stats = doc
            .overwrite_run_pattern_overlay(0, 4, &[0xaa, 0xbb])
            .unwrap();

        assert_eq!(stats.visited, 4);
        assert_eq!(stats.changed, 4);
        assert!(doc.is_dirty());
        assert_eq!(doc.replacement_dirty_bytes(), 4);
        assert_eq!(
            doc.replacement_spans().unwrap(),
            vec![(0, vec![0xaa, 0xbb, 0xaa, 0xbb])]
        );
    }

    #[test]
    fn xor_overlay_applies_without_expanding_replacements() {
        let config = Config::default();
        let mut doc = Document::from_memory_bytes(
            PathBuf::from("memory://pid/0x1000-0x1004"),
            vec![0x00, 0xff, 0xaa, 0x55],
            &config,
        );

        let stats = doc.xor_visible_range_overlay(0, 3, 0xff).unwrap();

        assert_eq!(stats.visited, 4);
        assert_eq!(stats.changed, 4);
        assert_eq!(
            doc.logical_bytes(0, 3).unwrap(),
            vec![0xff, 0x00, 0x55, 0xaa]
        );
        assert_eq!(doc.replacement_dirty_bytes(), 4);
        assert_eq!(
            doc.replacement_spans().unwrap(),
            vec![(0, vec![0xff, 0x00, 0x55, 0xaa])]
        );
    }
}
