use std::cell::RefCell;

use serde_json::Value;
use unicode_names2 as unicode_names;

use super::IconPickerTab;
use super::nerd_fonts;

#[derive(Clone, Debug)]
pub struct IconEntry {
    pub icon: String,
    pub name: String,
    pub name_lower: String,
}

pub struct IconSection {
    pub title: String,
    pub entries: Vec<IconEntry>,
}

/// A filtered view over a catalog section. Entries borrow from the catalog;
/// title borrows from the section string.
pub struct SectionView<'a> {
    pub title: &'a str,
    pub entries: Vec<&'a IconEntry>,
}

struct CachedUnicodeQuery {
    query: String,
    sections: Vec<IconSection>,
}

pub struct IconCatalogData {
    all_sections: Vec<IconSection>,
    emoji_sections: Vec<IconSection>,
    kaomoji_sections: Vec<IconSection>,
    unicode_browse_sections: Vec<IconSection>,
    nerd_sections: Vec<IconSection>,
    unicode_query_cache: RefCell<Vec<CachedUnicodeQuery>>,
}

const UNICODE_SEARCH_LIMIT: usize = 200;

/// Number of recent unicode queries kept in the LRU. Covers typing across a
/// search word plus a few backspaces without rescanning the codepoint range.
const UNICODE_QUERY_CACHE_CAP: usize = 16;

impl IconCatalogData {
    pub fn load() -> Self {
        let emoji_sections = vec![
            IconSection {
                title: "Common Emoji".to_string(),
                entries: build_emoji_common(),
            },
            IconSection {
                title: "All Emoji".to_string(),
                entries: build_emoji_all(),
            },
        ];

        let kaomoji_sections = vec![IconSection {
            title: "Kaomoji".to_string(),
            entries: build_kaomoji(),
        }];

        let unicode_browse_sections = {
            let mut sections = vec![IconSection {
                title: "Common Unicode".to_string(),
                entries: build_unicode_common(),
            }];
            sections.extend(load_unicode_ranges());
            sections
        };

        let nerd_all_raw = nerd_fonts::load();
        let (nerd_common, nerd_all) = build_nerd_sections(&nerd_all_raw);
        let nerd_sections = vec![
            IconSection {
                title: "Common Nerd Font".to_string(),
                entries: nerd_common,
            },
            IconSection {
                title: "All Nerd Font".to_string(),
                entries: nerd_all,
            },
        ];

        let all_sections = build_all_sections(
            &emoji_sections,
            &kaomoji_sections,
            &unicode_browse_sections,
            &nerd_sections,
        );

        Self {
            all_sections,
            emoji_sections,
            kaomoji_sections,
            unicode_browse_sections,
            nerd_sections,
            unicode_query_cache: RefCell::new(Vec::with_capacity(UNICODE_QUERY_CACHE_CAP)),
        }
    }

    pub fn with_filtered<R>(
        &self,
        tab: IconPickerTab,
        query: &str,
        f: impl FnOnce(&[SectionView<'_>]) -> R,
    ) -> R {
        match tab {
            IconPickerTab::All => {
                let sections = filter_sections(&self.all_sections, query);
                f(&sections)
            }
            IconPickerTab::Emoji => {
                let sections = filter_sections(&self.emoji_sections, query);
                f(&sections)
            }
            IconPickerTab::Kaomoji => {
                let sections = filter_sections(&self.kaomoji_sections, query);
                f(&sections)
            }
            IconPickerTab::Unicode => {
                let query = query.trim();
                if query.is_empty() {
                    let sections = filter_sections(&self.unicode_browse_sections, "");
                    return f(&sections);
                }

                {
                    let mut cache = self.unicode_query_cache.borrow_mut();
                    if let Some(pos) = cache.iter().position(|c| c.query == query) {
                        if pos != 0 {
                            let hit = cache.remove(pos);
                            cache.insert(0, hit);
                        }
                    } else {
                        let sections = build_unicode_search_sections(query);
                        cache.insert(
                            0,
                            CachedUnicodeQuery {
                                query: query.to_string(),
                                sections,
                            },
                        );
                        if cache.len() > UNICODE_QUERY_CACHE_CAP {
                            cache.pop();
                        }
                    }
                }

                let cache = self.unicode_query_cache.borrow();
                let cached = cache.first().expect("unicode query cache missing");
                let sections = filter_sections(&cached.sections, "");
                f(&sections)
            }
            IconPickerTab::NerdFont => {
                let sections = filter_sections(&self.nerd_sections, query);
                f(&sections)
            }
        }
    }
}

/// Concatenates every catalog into one combined set for the "All" tab,
/// prefixing each section title with its category so headers stay
/// distinguishable (e.g. "Emoji · Common Emoji").
fn build_all_sections(
    emoji_sections: &[IconSection],
    kaomoji_sections: &[IconSection],
    unicode_browse_sections: &[IconSection],
    nerd_sections: &[IconSection],
) -> Vec<IconSection> {
    let mut sections = Vec::new();
    sections.extend(prefixed_sections("Emoji", emoji_sections));
    sections.extend(prefixed_sections("Kaomoji", kaomoji_sections));
    sections.extend(prefixed_sections("Unicode", unicode_browse_sections));
    sections.extend(prefixed_sections("Nerd Font", nerd_sections));
    sections
}

fn prefixed_sections(category: &str, sections: &[IconSection]) -> Vec<IconSection> {
    sections
        .iter()
        .map(|section| {
            let title = if section.title.eq_ignore_ascii_case(category) {
                section.title.clone()
            } else {
                format!("{category} · {}", section.title)
            };
            IconSection {
                title,
                entries: section.entries.clone(),
            }
        })
        .collect()
}

fn filter_sections<'a>(sections: &'a [IconSection], query: &str) -> Vec<SectionView<'a>> {
    let query_lower = query.to_lowercase();
    sections
        .iter()
        .filter_map(|section| {
            let entries: Vec<&IconEntry> = if query_lower.is_empty() {
                section.entries.iter().collect()
            } else {
                section
                    .entries
                    .iter()
                    .filter(|entry| fuzzy_match(&entry.name_lower, &query_lower))
                    .collect()
            };
            if entries.is_empty() {
                None
            } else {
                Some(SectionView {
                    title: &section.title,
                    entries,
                })
            }
        })
        .collect()
}

/// Returns true if `name` matches `query` — exact substring first, then
/// word-level Levenshtein so that small typos still find the right icon.
fn fuzzy_match(name: &str, query: &str) -> bool {
    if name.contains(query) {
        return true;
    }

    let query_words: Vec<&str> = query.split_whitespace().collect();
    if query_words.is_empty() {
        return true;
    }

    let name_words: Vec<&str> = name.split_whitespace().collect();
    query_words
        .iter()
        .all(|qw| name_words.iter().any(|nw| word_match(qw, nw)))
}

/// A query word matches a name word if it is a substring of it, or the
/// Levenshtein edit distance is within a length-based tolerance.
fn word_match(query_word: &str, name_word: &str) -> bool {
    if name_word.contains(query_word) {
        return true;
    }

    let max_dist: usize = match query_word.len() {
        0..=2 => 0,
        3..=5 => 1,
        _ => 2,
    };

    if query_word.len().abs_diff(name_word.len()) > max_dist {
        return false;
    }

    levenshtein(query_word, name_word) <= max_dist
}

/// Single-row DP Levenshtein distance between two strings.
fn levenshtein(a: &str, b: &str) -> usize {
    let a: Vec<char> = a.chars().collect();
    let b: Vec<char> = b.chars().collect();
    let (m, n) = (a.len(), b.len());
    let mut row: Vec<usize> = (0..=n).collect();

    for i in 1..=m {
        let mut prev = row[0];
        row[0] = i;
        for j in 1..=n {
            let old = row[j];
            row[j] = if a[i - 1] == b[j - 1] {
                prev
            } else {
                1 + prev.min(row[j]).min(row[j - 1])
            };
            prev = old;
        }
    }
    row[n]
}

fn make_entry(icon: String, name: String) -> IconEntry {
    IconEntry {
        icon,
        name_lower: name.to_lowercase(),
        name,
    }
}

fn build_emoji_common() -> Vec<IconEntry> {
    let raw = include_str!("../../data/emoji.json");
    let val: Value = serde_json::from_str(raw).expect("invalid emoji.json");
    let common = val["common"]
        .as_array()
        .expect("emoji.json: missing 'common'");
    common
        .iter()
        .filter_map(|v| v.as_str())
        .filter_map(|s| {
            let emoji = emojis::get(s)?;
            Some(make_entry(
                emoji.as_str().to_string(),
                emoji.name().to_string(),
            ))
        })
        .collect()
}

fn build_emoji_all() -> Vec<IconEntry> {
    emojis::iter()
        .map(|emoji| make_entry(emoji.as_str().to_string(), emoji.name().to_string()))
        .collect()
}

fn build_kaomoji() -> Vec<IconEntry> {
    let raw = include_str!("../../data/kaomoji.json");
    let entries: Vec<Value> = serde_json::from_str(raw).expect("invalid kaomoji.json");
    entries
        .iter()
        .filter_map(|v| {
            let icon = v.get("icon")?.as_str()?;
            let name = v.get("name")?.as_str()?;
            Some(make_entry(icon.to_string(), name.to_string()))
        })
        .collect()
}

fn build_unicode_common() -> Vec<IconEntry> {
    let raw = include_str!("../../data/unicode.json");
    let val: Value = serde_json::from_str(raw).expect("invalid unicode.json");
    let common = val["common"]
        .as_array()
        .expect("unicode.json: missing 'common'");
    common
        .iter()
        .filter_map(|v| v.as_str())
        .filter_map(|s| s.chars().next())
        .filter_map(make_named_unicode_entry)
        .collect()
}

fn load_unicode_ranges() -> Vec<IconSection> {
    let raw = include_str!("../../data/unicode.json");
    let val: Value = serde_json::from_str(raw).expect("invalid unicode.json");
    let ranges = val["ranges"]
        .as_array()
        .expect("unicode.json: missing 'ranges'");
    ranges
        .iter()
        .filter_map(|v| {
            let title = v.get("title")?.as_str()?.to_string();
            let start = parse_hex(v.get("start")?.as_str()?)?;
            let end = parse_hex(v.get("end")?.as_str()?)?;
            Some(build_unicode_range(title, start..=end))
        })
        .collect()
}

fn parse_hex(s: &str) -> Option<u32> {
    let hex = s
        .strip_prefix("0x")
        .or_else(|| s.strip_prefix("0X"))
        .unwrap_or(s);
    u32::from_str_radix(hex, 16).ok()
}

fn build_unicode_range(title: String, range: std::ops::RangeInclusive<u32>) -> IconSection {
    let entries = range
        .filter_map(char::from_u32)
        .filter_map(make_named_unicode_entry)
        .collect();
    IconSection { title, entries }
}

fn build_unicode_search_sections(query: &str) -> Vec<IconSection> {
    let mut sections = Vec::new();
    let mut seen = std::collections::HashSet::new();

    if let Some(ch) = resolve_unicode_query(query)
        && let Some(entry) = make_unicode_entry(ch, true)
    {
        seen.insert(ch);
        sections.push(IconSection {
            title: "Exact Match".to_string(),
            entries: vec![entry],
        });
    }

    if should_scan_unicode_names(query) {
        let matches = scan_unicode_matches(query, &seen, UNICODE_SEARCH_LIMIT);
        if !matches.is_empty() {
            sections.push(IconSection {
                title: "Unicode Matches".to_string(),
                entries: matches,
            });
        }
    }

    sections
}

fn resolve_unicode_query(query: &str) -> Option<char> {
    let query = query.trim();
    if query.is_empty() {
        return None;
    }

    if query.chars().count() == 1 {
        let ch = query.chars().next()?;
        if !ch.is_control() {
            return Some(ch);
        }
    }

    parse_codepoint_query(query).or_else(|| unicode_names::character(query))
}

fn should_scan_unicode_names(query: &str) -> bool {
    let query = query.trim();
    if query.is_empty() {
        return false;
    }
    if query.chars().count() == 1 {
        return false;
    }
    true
}

fn scan_unicode_matches(
    query: &str,
    seen: &std::collections::HashSet<char>,
    limit: usize,
) -> Vec<IconEntry> {
    let query_lower = query.trim().to_lowercase();
    let mut matches = Vec::new();

    for codepoint in 0..=0x10FFFF {
        let Some(ch) = char::from_u32(codepoint) else {
            continue;
        };
        if seen.contains(&ch) || ch.is_control() {
            continue;
        }
        let Some(name) = unicode_names::name(ch) else {
            continue;
        };
        let display = format!("{name} · U+{codepoint:04X}");
        if display.to_lowercase().contains(&query_lower) {
            matches.push(make_entry(ch.to_string(), display));
            if matches.len() >= limit {
                break;
            }
        }
    }

    matches
}

fn parse_codepoint_query(query: &str) -> Option<char> {
    let trimmed = query.trim();
    let hex = trimmed
        .strip_prefix("U+")
        .or_else(|| trimmed.strip_prefix("u+"))
        .or_else(|| trimmed.strip_prefix("0x"))
        .or_else(|| trimmed.strip_prefix("0X"))
        .unwrap_or(trimmed);

    if hex.is_empty() || !hex.chars().all(|ch| ch.is_ascii_hexdigit()) {
        return None;
    }

    u32::from_str_radix(hex, 16).ok().and_then(char::from_u32)
}

fn make_named_unicode_entry(ch: char) -> Option<IconEntry> {
    make_unicode_entry(ch, false)
}

fn make_unicode_entry(ch: char, allow_unnamed: bool) -> Option<IconEntry> {
    if ch.is_control() {
        return None;
    }

    let label = match unicode_names::name(ch) {
        Some(name) => format!("{name} · U+{:04X}", ch as u32),
        None if allow_unnamed => format!("U+{:04X}", ch as u32),
        None => return None,
    };
    Some(make_entry(ch.to_string(), label))
}

fn build_nerd_sections(all: &[nerd_fonts::NerdFontGlyph]) -> (Vec<IconEntry>, Vec<IconEntry>) {
    let raw = include_str!("../../data/common_nerd.json");
    let common_names: Vec<String> = serde_json::from_str(raw).expect("invalid common_nerd.json");

    let common = common_names
        .iter()
        .filter_map(|name| {
            all.iter()
                .find(|glyph| &glyph.name == name)
                .map(|glyph| make_entry(glyph.icon.clone(), glyph.name.clone()))
        })
        .collect();

    let all_entries = all
        .iter()
        .map(|glyph| make_entry(glyph.icon.clone(), glyph.name.clone()))
        .collect();

    (common, all_entries)
}
