use std::ops::Range;

use ratatui::text::{Line, Span};

use crate::tui::palette::{search_inactive_match_bg, search_match_bg};
use crate::tui::text::{char_count, push_styled_span, slice_chars};

use super::RenderContext;

pub(in crate::viewer) fn apply_search_highlight(
    line: Line<'static>,
    query: Option<&str>,
    context: RenderContext,
    active_range: Option<Range<usize>>,
) -> Line<'static> {
    let Some(query) = query else {
        return line;
    };
    if query.is_empty() {
        return line;
    }

    let visual_text = line
        .spans
        .iter()
        .map(|span| span.content.as_ref())
        .collect::<String>();
    let start_char = search_start_char(context);
    let ranges = search_match_ranges(&visual_text, query, start_char);
    if ranges.is_empty() {
        return line;
    }

    Line {
        style: line.style,
        alignment: line.alignment,
        spans: apply_search_ranges_to_spans(&line.spans, &ranges, active_range),
    }
}

fn search_start_char(context: RenderContext) -> usize {
    context.gutter.content_start()
}

pub(in crate::viewer) fn search_match_ranges(
    text: &str,
    query: &str,
    start_char: usize,
) -> Vec<Range<usize>> {
    if query.is_empty() {
        return Vec::new();
    }

    let total_chars = char_count(text);
    if start_char >= total_chars {
        return Vec::new();
    }

    let search_text = slice_chars(text, start_char, total_chars);
    let query_len = char_count(query);
    search_text
        .match_indices(query)
        .map(|(byte_index, _)| {
            let start = start_char + char_count(&search_text[..byte_index]);
            start..start + query_len
        })
        .collect()
}

pub(in crate::viewer) fn apply_search_ranges_to_spans(
    spans: &[Span<'static>],
    ranges: &[Range<usize>],
    active_range: Option<Range<usize>>,
) -> Vec<Span<'static>> {
    let mut highlighted = Vec::new();
    let mut cursor = 0;

    for span in spans {
        let text = span.content.as_ref();
        let len = char_count(text);
        let span_start = cursor;
        let span_end = cursor + len;
        cursor = span_end;

        let split_points = search_split_points(span_start, span_end, ranges, active_range.as_ref());
        for window in split_points.windows(2) {
            let start = window[0];
            let end = window[1];
            if start == end {
                continue;
            }

            let mut style = span.style;
            if active_range
                .as_ref()
                .is_some_and(|range| range_contains(start, end, range))
            {
                style = style.bg(search_match_bg());
            } else if range_is_highlighted(start, end, ranges) {
                style = style.bg(search_inactive_match_bg());
            }
            push_styled_span(
                &mut highlighted,
                slice_chars(text, start - span_start, end - span_start),
                style,
            );
        }
    }

    highlighted
}

pub(in crate::viewer) fn search_split_points(
    span_start: usize,
    span_end: usize,
    ranges: &[Range<usize>],
    active_range: Option<&Range<usize>>,
) -> Vec<usize> {
    let mut points = vec![span_start, span_end];
    for range in ranges {
        let start = range.start.max(span_start).min(span_end);
        let end = range.end.max(span_start).min(span_end);
        if start < end {
            points.push(start);
            points.push(end);
        }
    }
    if let Some(range) = active_range {
        let start = range.start.max(span_start).min(span_end);
        let end = range.end.max(span_start).min(span_end);
        if start < end {
            points.push(start);
            points.push(end);
        }
    }
    points.sort_unstable();
    points.dedup();
    points
}

pub(in crate::viewer) fn range_is_highlighted(
    start: usize,
    end: usize,
    ranges: &[Range<usize>],
) -> bool {
    ranges.iter().any(|range| range_contains(start, end, range))
}

fn range_contains(start: usize, end: usize, range: &Range<usize>) -> bool {
    start >= range.start && end <= range.end
}
