// This file is part of ICU4X. For terms of use, please see the file
// called LICENSE at the top level of the ICU4X source tree
// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).

//! This module contains most of the actual algorithms for case mapping.
//!
//! Primarily, it implements methods on `CaseMap`, which contains the data model.

use crate::greek_to_me::{
    self, GreekCombiningCharacterSequenceDiacritics, GreekDiacritics, GreekPrecomposedLetterData,
    GreekVowel,
};
use crate::provider::data::{DotType, MappingKind};
use crate::provider::exception_helpers::ExceptionSlot;
use crate::provider::{CaseMap, CaseMapUnfold};
use crate::set::ClosureSink;
use crate::titlecase::TrailingCase;
use core::fmt;
use icu_locale_core::LanguageIdentifier;
use writeable::Writeable;

const ACUTE: char = '\u{301}';

// Used to control the behavior of CaseMapper::fold.
// Currently only used to decide whether to use Turkic (T) mappings for dotted/dotless i.
#[derive(Copy, Clone, Default)]
pub(crate) struct FoldOptions {
    exclude_special_i: bool,
}

impl FoldOptions {
    pub fn with_turkic_mappings() -> Self {
        Self {
            exclude_special_i: true,
        }
    }
}

/// Helper type that wraps a writeable in a prefix string
pub(crate) struct StringAndWriteable<'a, W> {
    pub string: &'a str,
    pub writeable: W,
}

impl<Wr: Writeable> Writeable for StringAndWriteable<'_, Wr> {
    fn write_to<W: fmt::Write + ?Sized>(&self, sink: &mut W) -> fmt::Result {
        sink.write_str(self.string)?;
        self.writeable.write_to(sink)
    }
    fn writeable_length_hint(&self) -> writeable::LengthHint {
        writeable::LengthHint::exact(self.string.len()) + self.writeable.writeable_length_hint()
    }
}

pub(crate) struct FullCaseWriteable<'a, 'data, const IS_TITLE_CONTEXT: bool> {
    data: &'data CaseMap<'data>,
    src: &'a str,
    locale: CaseMapLocale,
    mapping: MappingKind,
    titlecase_tail_casing: TrailingCase,
}

impl<'a, const IS_TITLE_CONTEXT: bool> Writeable for FullCaseWriteable<'a, '_, IS_TITLE_CONTEXT> {
    fn write_to<W: fmt::Write + ?Sized>(&self, sink: &mut W) -> fmt::Result {
        let src = self.src;
        let mut mapping = self.mapping;
        let mut iter = src.char_indices();
        // Number of characters to titlecase at the beginning
        // for the dutch IJ case.
        //
        // This cannot be handled in full_helper: this affects the trailing case logic. All other special casing
        // situations are able to cope by only titlecasing the first character, Dutch is the only place where
        // the first two base characters get uppercased.
        let mut dutch_titlecase_count = if IS_TITLE_CONTEXT && self.locale == CaseMapLocale::Dutch {
            dutch_ij_pair_at_beginning_count(src, self.data)
        } else {
            None
        };
        for (i, c) in &mut iter {
            let context = ContextIterator::new(&src[..i], &src[i..]);
            self.data
                .full_helper::<IS_TITLE_CONTEXT, W>(c, context, self.locale, mapping, sink)?;
            if IS_TITLE_CONTEXT {
                // Check if we're uppercasing a dutch IJ
                if let Some(count) = dutch_titlecase_count {
                    // If we are, we want to wait `count` characters
                    // before we switch to lowercasing (or TrailingCase::Unchanged)
                    if count > 1 {
                        // We still have code points to process
                        dutch_titlecase_count = Some(count - 1);
                        // Continue the loop to skip the mode switching code below
                        continue;
                    } else {
                        // We would have been down to zero. Time to continue the loop as normal.
                        dutch_titlecase_count = None;
                    }
                }

                // If titlecasing, switch the mode to lowercasing/TrailingCase::Unchanged
                if self.titlecase_tail_casing == TrailingCase::Lower {
                    mapping = MappingKind::Lower;
                } else {
                    break;
                }
            }
        }
        // Write the rest of the string
        if IS_TITLE_CONTEXT && self.titlecase_tail_casing == TrailingCase::Unchanged {
            sink.write_str(iter.as_str())?;
        }
        Ok(())
    }
    fn writeable_length_hint(&self) -> writeable::LengthHint {
        writeable::LengthHint::at_least(self.src.len())
    }
    fn write_to_string(&self) -> alloc::borrow::Cow<'a, str> {
        writeable::to_string_or_borrow(self, self.src.as_bytes())
    }
}

impl<'data> CaseMap<'data> {
    fn simple_helper(&self, c: char, kind: MappingKind) -> char {
        let data = self.lookup_data(c);
        if !data.has_exception() {
            if data.is_relevant_to(kind) {
                let folded = c as i32 + data.delta() as i32;
                // GIGO: delta should be valid
                char::from_u32(folded as u32).unwrap_or(c)
            } else {
                c
            }
        } else {
            let idx = data.exception_index();
            let exception = self.exceptions.get(idx);
            if data.is_relevant_to(kind)
                && let Some(simple) = exception.get_simple_case_slot_for(c)
            {
                return simple;
            }
            exception.slot_char_for_kind(kind).unwrap_or(c)
        }
    }

    // Returns the lowercase mapping of the given `char`.
    #[inline]
    pub(crate) fn simple_lower(&self, c: char) -> char {
        self.simple_helper(c, MappingKind::Lower)
    }

    // Returns the uppercase mapping of the given `char`.
    #[inline]
    pub(crate) fn simple_upper(&self, c: char) -> char {
        self.simple_helper(c, MappingKind::Upper)
    }

    // Returns the titlecase mapping of the given `char`.
    #[inline]
    pub(crate) fn simple_title(&self, c: char) -> char {
        self.simple_helper(c, MappingKind::Title)
    }

    // Return the simple case folding mapping of the given char.
    #[inline]
    pub(crate) fn simple_fold(&self, c: char, options: FoldOptions) -> char {
        let data = self.lookup_data(c);
        if !data.has_exception() {
            if data.is_upper_or_title() {
                let folded = c as i32 + data.delta() as i32;
                // GIGO: delta should be valid
                char::from_u32(folded as u32).unwrap_or(c)
            } else {
                c
            }
        } else {
            // TODO: if we move conditional fold and no_simple_case_folding into
            // simple_helper, this function can just call simple_helper.
            let idx = data.exception_index();
            let exception = self.exceptions.get(idx);
            if exception.bits.has_conditional_fold() {
                self.simple_fold_special_case(c, options)
            } else if exception.bits.no_simple_case_folding() {
                c
            } else if data.is_upper_or_title() && exception.has_slot(ExceptionSlot::Delta) {
                // unwrap_or case should never happen but best to avoid panics
                exception.get_simple_case_slot_for(c).unwrap_or('\0')
            } else if let Some(slot_char) = exception.slot_char_for_kind(MappingKind::Fold) {
                slot_char
            } else {
                c
            }
        }
    }

    fn dot_type(&self, c: char) -> DotType {
        let data = self.lookup_data(c);
        if !data.has_exception() {
            data.dot_type()
        } else {
            let idx = data.exception_index();
            self.exceptions.get(idx).bits.dot_type()
        }
    }

    // Returns true if this code point is is case-sensitive.
    // This is not currently exposed.
    #[allow(dead_code)]
    fn is_case_sensitive(&self, c: char) -> bool {
        let data = self.lookup_data(c);
        if !data.has_exception() {
            data.is_sensitive()
        } else {
            let idx = data.exception_index();
            self.exceptions.get(idx).bits.is_sensitive()
        }
    }

    /// Returns whether the character is cased
    pub(crate) fn is_cased(&self, c: char) -> bool {
        self.lookup_data(c).case_type().is_some()
    }

    #[inline(always)]
    // IS_TITLE_CONTEXT must be true if kind is MappingKind::Title
    // The kind may be a different kind with IS_TITLE_CONTEXT still true because
    // titlecasing a segment involves switching to lowercase later
    fn full_helper<const IS_TITLE_CONTEXT: bool, W: fmt::Write + ?Sized>(
        &self,
        c: char,
        context: ContextIterator,
        locale: CaseMapLocale,
        kind: MappingKind,
        sink: &mut W,
    ) -> fmt::Result {
        // If using a title mapping IS_TITLE_CONTEXT must be true
        debug_assert!(kind != MappingKind::Title || IS_TITLE_CONTEXT);
        // In a title context, kind MUST be Title or Lower
        debug_assert!(
            !IS_TITLE_CONTEXT || kind == MappingKind::Title || kind == MappingKind::Lower
        );

        // ICU4C's non-standard extension for Greek uppercasing:
        // https://icu.unicode.org/design/case/greek-upper.
        // Effectively removes Greek accents from Greek vowels during uppercasing,
        // whilst attempting to preserve additional marks like the dialytika (diæresis)
        // and ypogegrammeni (combining small iota).
        if !IS_TITLE_CONTEXT && locale == CaseMapLocale::Greek && kind == MappingKind::Upper {
            // Remove all combining diacritics on a Greek letter.
            // Ypogegrammeni is not an accent mark and is handled by regular casemapping (it turns into
            // a capital iota).
            // The dialytika is removed here, but it may be added again when the base letter is being processed.
            if greek_to_me::is_greek_diacritic_except_ypogegrammeni(c)
                && context.preceded_by_greek_letter()
            {
                return Ok(());
            }
            let data = greek_to_me::get_data(c);
            // Check if the character is a Greek vowel
            match data {
                Some(GreekPrecomposedLetterData::Vowel(vowel, mut precomposed_diacritics)) => {
                    // Get the diacritics on the character itself, and add any further combining diacritics
                    // from the context.
                    let mut diacritics = context.add_greek_diacritics(precomposed_diacritics);
                    // If the previous vowel had an accent (which would be removed) but no dialytika,
                    // and this is an iota or upsilon, add a dialytika since it is necessary to disambiguate
                    // the now-unaccented adjacent vowels from a digraph/diphthong.
                    // Use a precomposed dialytika if the accent was precomposed, and a combining dialytika
                    // if the accent was combining, so as to map NFD to NFD and NFC to NFC.
                    if !diacritics.dialytika
                        && (vowel == GreekVowel::Ι || vowel == GreekVowel::Υ)
                        && let Some(preceding_vowel) = context.preceding_greek_vowel_diacritics()
                        && !preceding_vowel.combining.dialytika
                        && !preceding_vowel.precomposed.dialytika
                    {
                        if preceding_vowel.combining.accented {
                            diacritics.dialytika = true;
                        } else {
                            precomposed_diacritics.dialytika = preceding_vowel.precomposed.accented;
                        }
                    }
                    // Write the base of the uppercased combining character sequence.
                    // In most branches this is [`upper_base`], i.e., the uppercase letter with all accents removed.
                    // In some branches the base has a precomposed diacritic.
                    // In the case of the Greek disjunctive "or", a combining tonos may also be written.
                    match vowel {
                        GreekVowel::Η => {
                            // The letter η (eta) is allowed to retain a tonos when it is form a single-letter word to distinguish
                            // the feminine definite article ἡ (monotonic η) from the disjunctive "or" ἤ (monotonic ή).
                            //
                            // A lone η with an accent other than the oxia/tonos is not expected,
                            // so there is no need to special-case the oxia/tonos.
                            // The ancient ᾖ (exist.PRS.SUBJ.3s) has a iota subscript as well as the circumflex,
                            // so it would not be given an oxia/tonos under this rule, and the subjunctive is formed with a particle
                            // (e.g. να είναι) since Byzantine times anyway.
                            if diacritics.accented
                                && !context.followed_by_cased_letter(self)
                                && !context.preceded_by_cased_letter(self)
                                && !diacritics.ypogegrammeni
                            {
                                if precomposed_diacritics.accented {
                                    sink.write_char('Ή')?;
                                } else {
                                    sink.write_char('Η')?;
                                    sink.write_char(greek_to_me::TONOS)?;
                                }
                            } else {
                                sink.write_char('Η')?;
                            }
                        }
                        GreekVowel::Ι => sink.write_char(if precomposed_diacritics.dialytika {
                            diacritics.dialytika = false;
                            'Ϊ'
                        } else {
                            vowel.into()
                        })?,
                        GreekVowel::Υ => sink.write_char(if precomposed_diacritics.dialytika {
                            diacritics.dialytika = false;
                            'Ϋ'
                        } else {
                            vowel.into()
                        })?,
                        _ => sink.write_char(vowel.into())?,
                    };
                    if diacritics.dialytika {
                        sink.write_char(greek_to_me::DIALYTIKA)?;
                    }
                    if precomposed_diacritics.ypogegrammeni {
                        sink.write_char('Ι')?;
                    }

                    return Ok(());
                }
                // Rho might have breathing marks, we handle it specially
                // to remove them
                Some(GreekPrecomposedLetterData::Consonant(true)) => {
                    sink.write_char(greek_to_me::CAPITAL_RHO)?;
                    return Ok(());
                }
                _ => (),
            }
        }

        let data = self.lookup_data(c);
        if !data.has_exception() {
            if data.is_relevant_to(kind) {
                let mapped = c as i32 + data.delta() as i32;
                // GIGO: delta should be valid
                let mapped = char::from_u32(mapped as u32).unwrap_or(c);
                sink.write_char(mapped)
            } else {
                sink.write_char(c)
            }
        } else {
            let idx = data.exception_index();
            let exception = self.exceptions.get(idx);
            if exception.bits.has_conditional_special()
                && let Some(special) = match kind {
                    MappingKind::Lower => {
                        self.full_lower_special_case::<IS_TITLE_CONTEXT>(c, context, locale)
                    }
                    MappingKind::Fold => self.full_fold_special_case(c, context, locale),
                    MappingKind::Upper | MappingKind::Title => self
                        .full_upper_or_title_special_case::<IS_TITLE_CONTEXT>(c, context, locale),
                }
            {
                return special.write_to(sink);
            }
            if let Some(mapped_string) = exception.get_fullmappings_slot_for_kind(kind)
                && !mapped_string.is_empty()
            {
                return sink.write_str(mapped_string);
            }

            if kind == MappingKind::Fold && exception.bits.no_simple_case_folding() {
                return sink.write_char(c);
            }

            if data.is_relevant_to(kind)
                && let Some(simple) = exception.get_simple_case_slot_for(c)
            {
                return sink.write_char(simple);
            }

            if let Some(slot_char) = exception.slot_char_for_kind(kind) {
                sink.write_char(slot_char)
            } else {
                sink.write_char(c)
            }
        }
    }

    // These constants are used for hardcoded locale-specific foldings.
    const I_DOT: &'static str = "\u{69}\u{307}";
    const J_DOT: &'static str = "\u{6a}\u{307}";
    const I_OGONEK_DOT: &'static str = "\u{12f}\u{307}";
    const I_DOT_GRAVE: &'static str = "\u{69}\u{307}\u{300}";
    const I_DOT_ACUTE: &'static str = "\u{69}\u{307}\u{301}";
    const I_DOT_TILDE: &'static str = "\u{69}\u{307}\u{303}";

    // Special case folding mappings, hardcoded.
    // This handles the special Turkic mappings for uppercase I and dotted uppercase I
    // For non-Turkic languages, this mapping is normally not used.
    // For Turkic languages (tr, az), this mapping can be used instead of the normal mapping for these characters.
    fn simple_fold_special_case(&self, c: char, options: FoldOptions) -> char {
        debug_assert!(c == '\u{49}' || c == '\u{130}');
        let is_turkic = options.exclude_special_i;
        match (c, is_turkic) {
            // Turkic mappings
            ('\u{49}', true) => '\u{131}', // 0049; T; 0131; # LATIN CAPITAL LETTER I
            ('\u{130}', true) => '\u{69}', /* 0130; T; 0069; # LATIN CAPITAL LETTER I WITH DOT ABOVE */

            // Default mappings
            ('\u{49}', false) => '\u{69}', // 0049; C; 0069; # LATIN CAPITAL LETTER I

            // There is no simple case folding for U+130.
            (c, _) => c,
        }
    }

    fn full_lower_special_case<const IS_TITLE_CONTEXT: bool>(
        &self,
        c: char,
        context: ContextIterator,
        locale: CaseMapLocale,
    ) -> Option<FullMappingResult<'_>> {
        if locale == CaseMapLocale::Lithuanian {
            // Lithuanian retains the dot in a lowercase i when followed by accents.
            // Introduce an explicit dot above when lowercasing capital I's and J's
            // whenever there are more accents above (of the accents used in
            // Lithuanian: grave, acute, and tilde above).

            // Check for accents above I, J, and I-with-ogonek.
            if c == 'I' && context.followed_by_more_above(self) {
                return Some(FullMappingResult::String(Self::I_DOT));
            } else if c == 'J' && context.followed_by_more_above(self) {
                return Some(FullMappingResult::String(Self::J_DOT));
            } else if c == '\u{12e}' && context.followed_by_more_above(self) {
                return Some(FullMappingResult::String(Self::I_OGONEK_DOT));
            }

            // These characters are precomposed with accents above, so we don't
            // have to look at the context.
            if c == '\u{cc}' {
                return Some(FullMappingResult::String(Self::I_DOT_GRAVE));
            } else if c == '\u{cd}' {
                return Some(FullMappingResult::String(Self::I_DOT_ACUTE));
            } else if c == '\u{128}' {
                return Some(FullMappingResult::String(Self::I_DOT_TILDE));
            }
        }

        if locale == CaseMapLocale::Turkish {
            if c == '\u{130}' {
                // I and i-dotless; I-dot and i are case pairs in Turkish and Azeri
                return Some(FullMappingResult::CodePoint('i'));
            } else if c == '\u{307}' && context.preceded_by_capital_i::<IS_TITLE_CONTEXT>(self) {
                // When lowercasing, remove dot_above in the sequence I + dot_above,
                // which will turn into i. This matches the behaviour of the
                // canonically equivalent I-dot_above.
                //
                // In a titlecase context, we do not want to apply this behavior to cases where the I
                // was at the beginning of the string, as that I and its marks should be handled by the
                // uppercasing rules (which ignore it, see below)

                return Some(FullMappingResult::Remove);
            } else if c == 'I' && !context.followed_by_dot_above(self) {
                // When lowercasing, unless an I is before a dot_above, it turns
                // into a dotless i.
                return Some(FullMappingResult::CodePoint('\u{131}'));
            }
        }

        if c == '\u{130}' {
            // Preserve canonical equivalence for I with dot. Turkic is handled above.
            return Some(FullMappingResult::String(Self::I_DOT));
        }

        if c == '\u{3a3}'
            && context.preceded_by_cased_letter(self)
            && !context.followed_by_cased_letter(self)
        {
            // Greek capital sigman maps depending on surrounding cased letters.
            return Some(FullMappingResult::CodePoint('\u{3c2}'));
        }

        // No relevant special case mapping. Use a normal mapping.
        None
    }

    fn full_upper_or_title_special_case<const IS_TITLE_CONTEXT: bool>(
        &self,
        c: char,
        context: ContextIterator,
        locale: CaseMapLocale,
    ) -> Option<FullMappingResult<'_>> {
        if locale == CaseMapLocale::Turkish && c == 'i' {
            // In Turkic languages, i turns into a dotted capital I.
            return Some(FullMappingResult::CodePoint('İ'));
        }
        if locale == CaseMapLocale::Lithuanian
            && c == '\u{307}'
            && context.preceded_by_soft_dotted(self)
        {
            // Lithuanian retains the dot in a lowercase i when followed by accents.
            // Remove dot_above after i with upper or titlecase.
            return Some(FullMappingResult::Remove);
        }
        // ICU4C's non-standard extension for Armenian ligature ech-yiwn.
        if c == 'և' {
            return match (locale, IS_TITLE_CONTEXT) {
                (CaseMapLocale::Armenian, false) => Some(FullMappingResult::String("ԵՎ")),
                (CaseMapLocale::Armenian, true) => Some(FullMappingResult::String("Եվ")),
                (_, false) => Some(FullMappingResult::String("ԵՒ")),
                (_, true) => Some(FullMappingResult::String("Եւ")),
            };
        }
        None
    }

    fn full_fold_special_case(
        &self,
        c: char,
        _context: ContextIterator,
        locale: CaseMapLocale,
    ) -> Option<FullMappingResult<'_>> {
        let is_turkic = locale == CaseMapLocale::Turkish;
        match (c, is_turkic) {
            // Turkic mappings
            ('\u{49}', true) => Some(FullMappingResult::CodePoint('\u{131}')),
            ('\u{130}', true) => Some(FullMappingResult::CodePoint('\u{69}')),

            // Default mappings
            ('\u{49}', false) => Some(FullMappingResult::CodePoint('\u{69}')),
            ('\u{130}', false) => Some(FullMappingResult::String(Self::I_DOT)),
            (_, _) => None,
        }
    }
    /// `IS_TITLE_CONTEXT` is true iff the mapping is [`MappingKind::Title`], primarily exists
    /// to avoid perf impacts on other more common modes of operation
    ///
    /// `titlecase_tail_casing` is only read in `IS_TITLE_CONTEXT`
    pub(crate) fn full_helper_writeable<'a: 'data, const IS_TITLE_CONTEXT: bool>(
        &'data self,
        src: &'a str,
        locale: CaseMapLocale,
        mapping: MappingKind,
        titlecase_tail_casing: TrailingCase,
    ) -> FullCaseWriteable<'a, 'data, IS_TITLE_CONTEXT> {
        // Ensure that they are either both true or both false
        debug_assert!(IS_TITLE_CONTEXT == (mapping == MappingKind::Title));

        FullCaseWriteable::<IS_TITLE_CONTEXT> {
            data: self,
            src,
            locale,
            mapping,
            titlecase_tail_casing,
        }
    }

    /// Adds all simple case mappings and the full case folding for `c` to `set`.
    /// Also adds special case closure mappings.
    /// The character itself is not added.
    /// For example, the mappings
    /// - for s include long s
    /// - for sharp s include ss
    /// - for k include the Kelvin sign
    pub(crate) fn add_case_closure_to<S: ClosureSink>(&self, c: char, set: &mut S) {
        // Hardcode the case closure of i and its relatives and ignore the
        // data file data for these characters.
        // The Turkic dotless i and dotted I with their case mapping conditions
        // and case folding option make the related characters behave specially.
        // This code matches their closure behavior to their case folding behavior.
        match c {
            // Regular i and I are in one equivalence class.
            '\u{49}' => {
                set.add_char('\u{69}');
                return;
            }
            '\u{69}' => {
                set.add_char('\u{49}');
                return;
            }

            // Dotted I is in a class with <0069 0307> (for canonical equivalence with <0049 0307>)
            '\u{130}' => {
                set.add_string(Self::I_DOT);
                return;
            }

            // Dotless i is in a class by itself
            '\u{131}' => {
                return;
            }

            _ => {}
        }

        let data = self.lookup_data(c);
        if !data.has_exception() {
            if data.case_type().is_some() {
                let delta = data.delta() as i32;
                if delta != 0 {
                    // Add the one simple case mapping, no matter what type it is.
                    let codepoint = c as i32 + delta;
                    // GIGO: delta should be valid
                    let mapped = char::from_u32(codepoint as u32).unwrap_or(c);
                    set.add_char(mapped);
                }
            }
            return;
        }

        // c has exceptions, so there may be multiple simple and/or full case mappings.
        let idx = data.exception_index();
        let exception = self.exceptions.get(idx);

        // Add all simple case mappings.
        for slot in [
            ExceptionSlot::Lower,
            ExceptionSlot::Fold,
            ExceptionSlot::Upper,
            ExceptionSlot::Title,
        ] {
            if let Some(simple) = exception.get_char_slot(slot) {
                set.add_char(simple);
            }
        }
        if let Some(simple) = exception.get_simple_case_slot_for(c) {
            set.add_char(simple);
        }

        exception.add_full_and_closure_mappings(set);
    }

    /// Maps the string to single code points and adds the associated case closure
    /// mappings.
    ///
    /// (see docs on [`CaseMap::add_string_case_closure_to`])
    pub(crate) fn add_string_case_closure_to<S: ClosureSink>(
        &self,
        s: &str,
        set: &mut S,
        unfold_data: &CaseMapUnfold,
    ) -> bool {
        if s.chars().count() <= 1 {
            // The string is too short to find any match.
            return false;
        }
        match unfold_data.get(s) {
            Some(closure_string) => {
                for c in closure_string.chars() {
                    set.add_char(c);
                    self.add_case_closure_to(c, set);
                }
                true
            }
            None => false,
        }
    }
}

// An internal representation of locale. Non-Root values of this
// enumeration imply that hard-coded special cases exist for this
// language.
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub enum CaseMapLocale {
    Root,
    Turkish,
    Lithuanian,
    Greek,
    Dutch,
    Armenian,
}

impl CaseMapLocale {
    pub const fn from_langid(langid: &LanguageIdentifier) -> Self {
        use icu_locale_core::subtags::{Language, language};
        const TR: Language = language!("tr");
        const AZ: Language = language!("az");
        const LT: Language = language!("lt");
        const EL: Language = language!("el");
        const NL: Language = language!("nl");
        const HY: Language = language!("hy");
        match langid.language {
            TR | AZ => Self::Turkish,
            LT => Self::Lithuanian,
            EL => Self::Greek,
            NL => Self::Dutch,
            HY => Self::Armenian,
            _ => Self::Root,
        }
    }
}

pub enum FullMappingResult<'a> {
    Remove,
    CodePoint(char),
    String(&'a str),
}

impl FullMappingResult<'_> {
    #[allow(dead_code)]
    fn add_to_set<S: ClosureSink>(&self, set: &mut S) {
        match *self {
            FullMappingResult::CodePoint(c) => set.add_char(c),
            FullMappingResult::String(s) => set.add_string(s),
            FullMappingResult::Remove => {}
        }
    }
}

impl Writeable for FullMappingResult<'_> {
    fn write_to<W: fmt::Write + ?Sized>(&self, sink: &mut W) -> fmt::Result {
        match *self {
            FullMappingResult::CodePoint(c) => sink.write_char(c),
            FullMappingResult::String(s) => sink.write_str(s),
            FullMappingResult::Remove => Ok(()),
        }
    }
}

pub(crate) struct ContextIterator<'a> {
    before: &'a str,
    after: &'a str,
}

impl<'a> ContextIterator<'a> {
    // Returns a context iterator with the characters before
    // and after the character at a given index, given the preceding
    // string and the succeeding string including the character itself
    pub fn new(before: &'a str, char_and_after: &'a str) -> Self {
        let mut char_and_after = char_and_after.chars();
        char_and_after.next(); // skip the character itself
        let after = char_and_after.as_str();
        Self { before, after }
    }

    fn add_greek_diacritics(&self, mut diacritics: GreekDiacritics) -> GreekDiacritics {
        diacritics.consume_greek_diacritics(self.after);
        diacritics
    }

    fn preceded_by_greek_letter(&self) -> bool {
        greek_to_me::preceded_by_greek_letter(self.before)
    }

    fn preceding_greek_vowel_diacritics(
        &self,
    ) -> Option<GreekCombiningCharacterSequenceDiacritics> {
        greek_to_me::preceding_greek_vowel_diacritics(self.before)
    }

    fn preceded_by_soft_dotted(&self, mapping: &CaseMap) -> bool {
        for c in self.before.chars().rev() {
            match mapping.dot_type(c) {
                DotType::SoftDotted => return true,
                DotType::OtherAccent => continue,
                _ => return false,
            }
        }
        false
    }
    /// Checks if the preceding character is a capital I, allowing for non-Above combining characters in between.
    ///
    /// If `I_MUST_NOT_START_STRING` is true, additionally will require that the capital I does not start the string
    fn preceded_by_capital_i<const I_MUST_NOT_START_STRING: bool>(
        &self,
        mapping: &CaseMap,
    ) -> bool {
        let mut iter = self.before.chars().rev();
        while let Some(c) = iter.next() {
            if c == 'I' {
                if I_MUST_NOT_START_STRING {
                    return iter.next().is_some();
                } else {
                    return true;
                }
            }
            if mapping.dot_type(c) != DotType::OtherAccent {
                break;
            }
        }
        false
    }
    fn preceded_by_cased_letter(&self, mapping: &CaseMap) -> bool {
        for c in self.before.chars().rev() {
            let data = mapping.lookup_data(c);
            if !data.is_ignorable() {
                return data.case_type().is_some();
            }
        }
        false
    }
    fn followed_by_cased_letter(&self, mapping: &CaseMap) -> bool {
        for c in self.after.chars() {
            let data = mapping.lookup_data(c);
            if !data.is_ignorable() {
                return data.case_type().is_some();
            }
        }
        false
    }
    fn followed_by_more_above(&self, mapping: &CaseMap) -> bool {
        for c in self.after.chars() {
            match mapping.dot_type(c) {
                DotType::Above => return true,
                DotType::OtherAccent => continue,
                _ => return false,
            }
        }
        false
    }
    fn followed_by_dot_above(&self, mapping: &CaseMap) -> bool {
        for c in self.after.chars() {
            if c == '\u{307}' {
                return true;
            }
            if mapping.dot_type(c) != DotType::OtherAccent {
                return false;
            }
        }
        false
    }
}

/// Data on an i, I, í, or Í at the beginning of a string
#[derive(PartialEq, Eq, Debug, Clone)]
struct DutchIData<'a> {
    /// The rest of the string after this i
    rest: &'a str,
    has_acute: bool,
    // Number of code points consumed.
    char_count: usize,
}

/// Is there an i at the beginning of the string which may be relevant
/// for Dutch titlecasing?
fn dutch_i_at_beginning(s: &'_ str) -> Option<DutchIData<'_>> {
    let mut chars = s.chars();
    match chars.next() {
        Some('i') | Some('I') => {
            let rest = chars.as_str();
            match chars.next() {
                Some(ACUTE) => {
                    // We have consumed an i and an acute accent.
                    // So chars.as_str() will have the rest of the string
                    Some(DutchIData {
                        rest: chars.as_str(),
                        has_acute: true,
                        char_count: 2,
                    })
                }
                _ => {
                    // We have consumed an i and a non-acute accent character.
                    // So `rest`, from before our `.next()` call, will have the rest of the string
                    Some(DutchIData {
                        rest,
                        has_acute: false,
                        char_count: 1,
                    })
                }
            }
        }
        // We have consumed an i and an acute accent.
        // So chars.as_str() will have the rest of the string
        Some('í') | Some('Í') => Some(DutchIData {
            rest: chars.as_str(),
            has_acute: true,
            char_count: 1,
        }),
        _ => None,
    }
}

/// This checks for a Dutch-relevant IJ pair at the beginning of a string.
/// This is an I followed by a J, with any casing, and no accents other than acute.
/// Acute accents must be on both or neither.
///
/// This returns the number of characters (codepoint-wise, not code unit-wise)
/// in the IJ pair, not including any combining characters on the J.
///
/// In dutch titlecasing mode, the first N characters should be uppercased:
/// `ijabc` should titlecase to `IJabc`.
fn dutch_ij_pair_at_beginning_count(s: &str, mapping: &CaseMap) -> Option<usize> {
    let i_at_beginning = dutch_i_at_beginning(s)?;

    let mut chars = i_at_beginning.rest.chars();

    match chars.next() {
        Some('j' | 'J') => (),
        _ => return None,
    }

    let mut j_has_acute = false;
    for c in chars {
        if c == ACUTE {
            j_has_acute = true;
            continue;
        }
        // We are supposed to check that `j` has no other combining marks aside
        // from potentially an acute accent. Once we hit the first non-combining mark
        // we are done.
        //
        // ICU4C checks for `gc=Mn` to determine if something is a combining mark,
        // however this requires extra data (and is the *only* point in the casemapping algorithm
        // where there is a direct dependency on properties data not mediated by the casemapping data trie).
        //
        // Instead, we can check for ccc via dot_type, the same way the rest of the algorithm does.
        //
        // See https://unicode-org.atlassian.net/browse/ICU-22429
        match mapping.dot_type(c) {
            // Not a combining character; ccc = 0
            DotType::NoDot | DotType::SoftDotted => break,
            // found combining character, bail
            _ => return None,
        }
    }

    // either both should have an acute accent, or none. this is an XNOR operation
    if !(j_has_acute ^ i_at_beginning.has_acute) {
        // There were char_count characters in the i, and 1 more j character.
        // The accent won't be cased.
        Some(i_at_beginning.char_count + 1)
    } else {
        None
    }
}

#[test]
fn test_dutch_i_at_beginning() {
    #[allow(
        clippy::unnecessary_wraps,
        reason = "this is a convenience function for testing"
    )]
    fn id(rest: &str, has_acute: bool, char_count: usize) -> Option<DutchIData<'_>> {
        Some(DutchIData {
            rest,
            has_acute,
            char_count,
        })
    }

    // Should remove the dutch I (capital or lowercase, possibly accented I)
    // and return the rest of the string
    assert_eq!(dutch_i_at_beginning("iX"), id("X", false, 1));
    assert_eq!(dutch_i_at_beginning("íX"), id("X", true, 1));
    assert_eq!(dutch_i_at_beginning("i\u{301}X"), id("X", true, 2));
    assert_eq!(dutch_i_at_beginning("IX"), id("X", false, 1));
    assert_eq!(dutch_i_at_beginning("ÍX"), id("X", true, 1));
    assert_eq!(dutch_i_at_beginning("ÍX"), id("X", true, 1));
    assert_eq!(dutch_i_at_beginning("I\u{301}X"), id("X", true, 2));

    // Shouldn't get confused about other accent marks: ONLY acute accents, and only one of them.
    assert_eq!(dutch_i_at_beginning("í\u{301}X"), id("\u{301}X", true, 1));
    assert_eq!(dutch_i_at_beginning("i\u{302}X"), id("\u{302}X", false, 1));
    // This is an acute accent that comes *after* but that's fine, other parts of the algorithm
    // will reject that.
    assert_eq!(
        dutch_i_at_beginning("i\u{302}\u{301}X"),
        id("\u{302}\u{301}X", false, 1)
    );

    assert_eq!(dutch_i_at_beginning("ï\u{301}X"), None);
}

#[test]
fn test_dutch_ij_at_beginning() {
    let data = crate::CaseMapperBorrowed::new().data;

    assert_eq!(dutch_ij_pair_at_beginning_count("ijabcd", data), Some(2));
    assert_eq!(dutch_ij_pair_at_beginning_count("iJabcd", data), Some(2));
    assert_eq!(dutch_ij_pair_at_beginning_count("IJabcd", data), Some(2));
    assert_eq!(dutch_ij_pair_at_beginning_count("Ijabcd", data), Some(2));
    assert_eq!(
        dutch_ij_pair_at_beginning_count("íj\u{301}abcd", data),
        Some(2)
    );
    assert_eq!(
        dutch_ij_pair_at_beginning_count("ÍJ\u{301}abcd", data),
        Some(2)
    );
    assert_eq!(
        dutch_ij_pair_at_beginning_count("i\u{301}J\u{301}abcd", data),
        Some(3)
    );
    assert_eq!(
        dutch_ij_pair_at_beginning_count("i\u{301}Jabcd", data),
        None
    );
    assert_eq!(dutch_ij_pair_at_beginning_count("íJabcd", data), None);
    assert_eq!(dutch_ij_pair_at_beginning_count("abcdijk", data), None);
}
