//! Markdown related logic

use crate::{
	dictionary,
	utils::{self, AlphabeticalKey, ErrorCandidate},
};

use itertools::Itertools;
use pulldown_cmark::{Event, Options, Parser, TextMergeWithOffset};
use std::{collections::BTreeMap, range::Range};

pub(crate) fn get_error_candidates<'text>(
	dict: &dictionary::Dictionary,
	md_file: &'text str,
) -> impl Iterator<Item = ErrorCandidate<'text>> {
	let utils::UnicodeData { ref segmenter } = utils::UNICODE_DATA;
	let text_offsets = md_texts(md_file);

	let mut mistakes = BTreeMap::<AlphabeticalKey, Vec<Range<usize>>>::new();

	for offset in text_offsets {
		let start = offset.start;
		let words = segmenter
			.as_borrowed()
			.segment_str(&md_file[offset])
			.tuple_windows()
			.filter_map(|(s, e)| {
				let range = (s + start..e + start).into();
				let text: &str = &md_file[range];

				Some((text, range)).filter(|s| !s.0.trim().is_empty())
			});

		for (word, r) in words {
			let Some(_) = word.chars().find(|c| c.is_alphabetic()) else {
				continue;
			};

			if !dict.check(word) {
				mistakes.entry(AlphabeticalKey(word)).or_default().push(r);
			}
		}
	}

	mistakes.into_iter()
		.map(move |(AlphabeticalKey(word), ranges)| ErrorCandidate::new(word, ranges))
}

fn md_texts(md_file: &str) -> impl Iterator<Item = Range<usize>> {
	// mirror Zola's options
	let mut opts = Options::empty();
	opts.insert(Options::ENABLE_TABLES);
	opts.insert(Options::ENABLE_FOOTNOTES);
	opts.insert(Options::ENABLE_STRIKETHROUGH);
	opts.insert(Options::ENABLE_TASKLISTS);
	opts.insert(Options::ENABLE_HEADING_ATTRIBUTES);
	opts.insert(Options::ENABLE_GFM);

	let parser = Parser::new_ext(md_file, opts).into_offset_iter();
	let parser = TextMergeWithOffset::new(parser);

	parser.scan(true, |include, (e, o)| {
		match e {
			Event::Start(pulldown_cmark::Tag::CodeBlock(_)) => *include = false,
			Event::End(pulldown_cmark::TagEnd::CodeBlock) => *include = true,
			_ => {}
		}

		Some((*include, e, o.into()))
	})
	.filter_map(|(include, e, o)| (include && matches!(e, Event::Text(_))).then_some(o))
	// .take(40)
}
