//! This file only contains fallback implementations of functions which have been found to be missing
//! or broken by the configuration scripts.
//!
//! Many of these functions are more or less broken and incomplete.

use fish_widecharwidth::{WcLookupTable, WcWidth};
use fish_widestring::prelude::*;
use std::cmp;
use std::sync::{
    LazyLock,
    atomic::{AtomicUsize, Ordering},
};

/// Width of ambiguous East Asian characters and, as of TR11, all private-use characters.
/// 1 is the typical default, but we accept any non-negative override via `$fish_ambiguous_width`.
pub static FISH_AMBIGUOUS_WIDTH: AtomicUsize = AtomicUsize::new(1);

/// Width of emoji characters.
///
/// This must be configurable because the value changed between Unicode 8 and Unicode 9, `wcwidth()`
/// is emoji-unaware, and terminal emulators do different things.
///
/// See issues like [#4539](https://github.com/fish-shell/fish-shell/issues/4539) and <https://github.com/neovim/neovim/issues/4976> for how painful this is.
///
/// Valid values are 1, and 2. 1 is the typical emoji width used in Unicode 8 while some newer
/// terminals use a width of 2 since Unicode 9.
// For some reason, this is declared here and exposed here, but is set in `env_dispatch`.
pub static FISH_EMOJI_WIDTH: AtomicUsize = AtomicUsize::new(2);

static WC_LOOKUP_TABLE: LazyLock<WcLookupTable> = LazyLock::new(WcLookupTable::new);

pub fn fish_wcwidth(c: char) -> Option<usize> {
    // Check for VS16 which selects emoji presentation. This "promotes" a character like U+2764
    // (width 1) to an emoji (probably width 2). So treat it as width 1 so the sums work. See #2652.
    // VS15 selects text presentation.
    let variation_selector_16 = '\u{FE0F}';
    let variation_selector_15 = '\u{FE0E}';
    if c == variation_selector_16 {
        return Some(1);
    } else if c == variation_selector_15 {
        return Some(0);
    }

    // Check for Emoji_Modifier property. Only the Fitzpatrick modifiers have this, in range
    // 1F3FB..1F3FF. This is a hack because such an emoji appearing on its own would be drawn as
    // width 2, but that's unlikely to be useful. See #8275.
    if ('\u{1F3FB}'..='\u{1F3FF}').contains(&c) {
        return Some(0);
    }

    let width = WC_LOOKUP_TABLE.classify(c);
    Some(match width {
        WcWidth::NonPrint => return None,
        WcWidth::NonCharacter | WcWidth::Combining | WcWidth::Unassigned => 0,
        WcWidth::Ambiguous | WcWidth::PrivateUse => {
            // TR11: "All private-use characters are by default classified as Ambiguous".
            FISH_AMBIGUOUS_WIDTH.load(Ordering::Relaxed)
        }
        WcWidth::One => 1,
        WcWidth::Two => 2,
        WcWidth::WidenedIn9 => FISH_EMOJI_WIDTH.load(Ordering::Relaxed),
    })
}

pub fn fish_wcswidth(s: &wstr) -> Option<usize> {
    fish_wcswidth_canonicalizing(s, std::convert::identity)
}

pub fn fish_wcswidth_canonicalizing(s: &wstr, canonicalize: fn(char) -> char) -> Option<usize> {
    let chars = s.chars().map(canonicalize);
    // ascii fast path
    if chars.clone().all(|c| c.is_ascii() && !c.is_ascii_control()) {
        return Some(s.len());
    }

    let mut result = 0;
    for c in chars {
        result += fish_wcwidth(c)?;
    }
    Some(result)
}

pub fn wcscasecmp(lhs: &wstr, rhs: &wstr) -> cmp::Ordering {
    wcscasecmp_fuzzy(lhs, rhs, std::convert::identity)
}

/// Compare two wide strings in a case-insensitive fashion
pub fn wcscasecmp_fuzzy(
    lhs: &wstr,
    rhs: &wstr,
    extra_canonicalization: fn(char) -> char,
) -> cmp::Ordering {
    lowercase(lhs.chars())
        .map(extra_canonicalization)
        .cmp(lowercase(rhs.chars()).map(extra_canonicalization))
}

pub fn lowercase(chars: impl Iterator<Item = char>) -> impl Iterator<Item = char> {
    lowercase_impl(chars, |c| c.to_lowercase())
}
pub fn lowercase_rev(chars: impl DoubleEndedIterator<Item = char>) -> impl Iterator<Item = char> {
    lowercase_impl(chars.rev(), |c| c.to_lowercase().rev())
}
fn lowercase_impl<ToLowercase: Iterator<Item = char>>(
    chars: impl Iterator<Item = char>,
    to_lowercase: fn(char) -> ToLowercase,
) -> impl Iterator<Item = char> {
    /// This struct streams the underlying lowercase chars of a string without allocating.
    struct ToLowerBuffer<Chars: Iterator<Item = char>, ToLowercase: Iterator<Item = char>> {
        to_lowercase: fn(char) -> ToLowercase,
        current: ToLowercase,
        chars: Chars,
    }

    impl<Chars: Iterator<Item = char>, ToLowercase: Iterator<Item = char>> Iterator
        for ToLowerBuffer<Chars, ToLowercase>
    {
        type Item = char;

        fn next(&mut self) -> Option<Self::Item> {
            if let Some(c) = self.current.next() {
                return Some(c);
            }

            self.current = (self.to_lowercase)(self.chars.next()?);
            self.current.next()
        }
    }

    impl<Chars: Iterator<Item = char>, ToLowercase: Iterator<Item = char>>
        ToLowerBuffer<Chars, ToLowercase>
    {
        pub fn new(mut chars: Chars, to_lowercase: fn(char) -> ToLowercase) -> Self {
            Self {
                to_lowercase,
                current: chars.next().map_or_else(
                    || {
                        let mut empty = to_lowercase('a');
                        let _ = empty.next();
                        debug_assert!(empty.next().is_none());
                        empty
                    },
                    to_lowercase,
                ),
                chars,
            }
        }
    }
    ToLowerBuffer::new(chars, to_lowercase)
}

#[cfg(test)]
mod tests {
    use super::wcscasecmp;
    use fish_widestring::prelude::*;
    use std::cmp::Ordering;

    #[test]
    fn test_wcscasecmp() {
        // Comparison with empty
        assert_eq!(wcscasecmp(L!("a"), L!("")), Ordering::Greater);
        assert_eq!(wcscasecmp(L!(""), L!("a")), Ordering::Less);
        assert_eq!(wcscasecmp(L!(""), L!("")), Ordering::Equal);

        // Basic comparison
        assert_eq!(wcscasecmp(L!("A"), L!("a")), Ordering::Equal);
        assert_eq!(wcscasecmp(L!("B"), L!("a")), Ordering::Greater);
        assert_eq!(wcscasecmp(L!("A"), L!("B")), Ordering::Less);

        // Multi-byte comparison
        assert_eq!(wcscasecmp(L!("İ"), L!("i\u{307}")), Ordering::Equal);
        assert_eq!(wcscasecmp(L!("ia"), L!("İa")), Ordering::Less);
    }
}
