pub(crate) mod anchors;
pub(crate) mod changes;

use std::collections::HashMap;

use crate::model::{Block, SpanChangeKind, SpanTrackedChange, TrackedChange, TrackedChangeKind};
use crate::TrackChangesMode;

pub(crate) fn apply_text_spans(
    blocks: &mut [Block],
    tracked_changes: &[TrackedChange],
    parser_raw_changes: &[TrackedChange],
    mode: &TrackChangesMode,
    include_text_spans: bool,
) {
    if !include_text_spans {
        for block in blocks {
            block.text_spans = None;
        }
        return;
    }

    let span_change_map = build_span_change_map(tracked_changes, parser_raw_changes, mode);

    for block in blocks {
        if let Some(spans) = block.text_spans.as_mut() {
            for span in spans.iter_mut() {
                span.tracked_changes = span
                    .tracked_changes
                    .iter()
                    .filter_map(|change| span_change_map.get(change.id.as_str()).cloned())
                    .collect();
            }

            spans.retain(|span| !span.text.is_empty());
        }
    }
}

fn build_span_change_map(
    tracked_changes: &[TrackedChange],
    parser_raw_changes: &[TrackedChange],
    mode: &TrackChangesMode,
) -> HashMap<String, SpanTrackedChange> {
    let mut map = HashMap::new();

    for change in tracked_changes {
        match &change.kind {
            TrackedChangeKind::Replacement { .. }
                if matches!(mode, TrackChangesMode::Paired | TrackChangesMode::Both) =>
            {
                if let Some(raw_change_id) = find_matching_replacement_insert_id(change, parser_raw_changes) {
                    map.insert(
                        raw_change_id,
                        SpanTrackedChange {
                            id: change.id.clone(),
                            kind: SpanChangeKind::Replacement,
                        },
                    );
                }
            }
            TrackedChangeKind::Insert { .. } => {
                map.insert(
                    change.id.clone(),
                    SpanTrackedChange {
                        id: change.id.clone(),
                        kind: SpanChangeKind::Insert,
                    },
                );
            }
            TrackedChangeKind::MoveTo { .. } => {
                map.insert(
                    change.id.clone(),
                    SpanTrackedChange {
                        id: change.id.clone(),
                        kind: SpanChangeKind::MoveTo,
                    },
                );
            }
            TrackedChangeKind::Format { .. } => {
                map.insert(
                    change.id.clone(),
                    SpanTrackedChange {
                        id: change.id.clone(),
                        kind: SpanChangeKind::Format,
                    },
                );
            }
            _ => {}
        }
    }

    map
}

fn find_matching_replacement_insert_id(
    replacement: &TrackedChange,
    parser_raw_changes: &[TrackedChange],
) -> Option<String> {
    let TrackedChangeKind::Replacement { old_text, new_text } = &replacement.kind else {
        return None;
    };

    for pair in parser_raw_changes.windows(2) {
        let first = &pair[0];
        let second = &pair[1];

        let matches_delete_insert = matches!(
            (&first.kind, &second.kind),
            (TrackedChangeKind::Delete { text }, TrackedChangeKind::Insert { text: new })
                if text == old_text && new == new_text
        );
        let matches_insert_delete = matches!(
            (&first.kind, &second.kind),
            (TrackedChangeKind::Insert { text: new }, TrackedChangeKind::Delete { text })
                if text == old_text && new == new_text
        );

        let matches_metadata = first.author == replacement.author
            && second.author == replacement.author
            && first.date == replacement.date
            && second.date == replacement.date
            && first.block_ids == replacement.block_ids
            && second.block_ids == replacement.block_ids;

        if matches_metadata && (matches_delete_insert || matches_insert_delete) {
            return pair.iter().find_map(|change| match change.kind {
                TrackedChangeKind::Insert { .. } => Some(change.id.clone()),
                _ => None,
            });
        }
    }

    None
}
