// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

//! Bindings to the ICU library.

use std::cmp::Ordering;
use std::ffi::{CStr, c_char};
use std::mem;
use std::mem::MaybeUninit;
use std::ops::Range;
use std::ptr::{null, null_mut};

use crate::arena::{Arena, ArenaString, scratch_arena};
use crate::buffer::TextBuffer;
use crate::unicode::Utf8Chars;
use crate::{apperr, arena_format, sys};

#[derive(Clone, Copy)]
pub struct Encoding {
    pub label: &'static str,
    pub canonical: &'static str,
}

pub struct Encodings {
    pub preferred: &'static [Encoding],
    pub all: &'static [Encoding],
}

static mut ENCODINGS: Encodings = Encodings { preferred: &[], all: &[] };

/// Returns a list of encodings ICU supports.
pub fn get_available_encodings() -> &'static Encodings {
    // OnceCell for people that want to put it into a static.
    #[allow(static_mut_refs)]
    unsafe {
        if ENCODINGS.all.is_empty() {
            let scratch = scratch_arena(None);
            let mut preferred = Vec::new_in(&*scratch);
            let mut alternative = Vec::new_in(&*scratch);

            // These encodings are always available.
            preferred.push(Encoding { label: "UTF-8", canonical: "UTF-8" });
            preferred.push(Encoding { label: "UTF-8 BOM", canonical: "UTF-8 BOM" });

            if let Ok(f) = init_if_needed() {
                let mut n = 0;
                loop {
                    let name = (f.ucnv_getAvailableName)(n);
                    if name.is_null() {
                        break;
                    }

                    n += 1;

                    let name = CStr::from_ptr(name).to_str().unwrap_unchecked();
                    // We have already pushed UTF-8 above and can skip it.
                    // There is no need to filter UTF-8 BOM here,
                    // since ICU does not distinguish it from UTF-8.
                    if name.is_empty() || name == "UTF-8" {
                        continue;
                    }

                    let mut status = icu_ffi::U_ZERO_ERROR;
                    let mime = (f.ucnv_getStandardName)(
                        name.as_ptr(),
                        c"MIME".as_ptr() as *const _,
                        &mut status,
                    );
                    if !mime.is_null() && status.is_success() {
                        let mime = CStr::from_ptr(mime).to_str().unwrap_unchecked();
                        preferred.push(Encoding { label: mime, canonical: name });
                    } else {
                        alternative.push(Encoding { label: name, canonical: name });
                    }
                }
            }

            let preferred_len = preferred.len();

            // Combine the preferred and alternative encodings into a single list.
            let mut all = Vec::with_capacity(preferred.len() + alternative.len());
            all.extend(preferred);
            all.extend(alternative);

            let all = all.leak();
            ENCODINGS.preferred = &all[..preferred_len];
            ENCODINGS.all = &all[..];
        }

        &ENCODINGS
    }
}

/// Formats the given ICU error code into a human-readable string.
pub fn apperr_format(f: &mut std::fmt::Formatter<'_>, code: u32) -> std::fmt::Result {
    fn format(code: u32) -> &'static str {
        let Ok(f) = init_if_needed() else {
            return "";
        };

        let status = icu_ffi::UErrorCode::new(code);
        let ptr = unsafe { (f.u_errorName)(status) };
        if ptr.is_null() {
            return "";
        }

        let str = unsafe { CStr::from_ptr(ptr) };
        str.to_str().unwrap_or("")
    }

    let msg = format(code);
    if !msg.is_empty() {
        write!(f, "ICU Error: {msg}")
    } else {
        write!(f, "ICU Error: {code:#08x}")
    }
}

/// Converts between two encodings using ICU.
pub struct Converter<'pivot> {
    source: *mut icu_ffi::UConverter,
    target: *mut icu_ffi::UConverter,
    pivot_buffer: &'pivot mut [MaybeUninit<u16>],
    pivot_source: *mut u16,
    pivot_target: *mut u16,
    reset: bool,
}

impl Drop for Converter<'_> {
    fn drop(&mut self) {
        let f = assume_loaded();
        unsafe { (f.ucnv_close)(self.source) };
        unsafe { (f.ucnv_close)(self.target) };
    }
}

impl<'pivot> Converter<'pivot> {
    /// Constructs a new `Converter` instance.
    ///
    /// # Parameters
    ///
    /// * `pivot_buffer`: A buffer used to cache partial conversions.
    ///   Don't make it too small.
    /// * `source_encoding`: The source encoding name (e.g., "UTF-8").
    /// * `target_encoding`: The target encoding name (e.g., "UTF-16").
    pub fn new(
        pivot_buffer: &'pivot mut [MaybeUninit<u16>],
        source_encoding: &str,
        target_encoding: &str,
    ) -> apperr::Result<Self> {
        let f = init_if_needed()?;

        let arena = scratch_arena(None);
        let source_encoding = Self::append_nul(&arena, source_encoding);
        let target_encoding = Self::append_nul(&arena, target_encoding);

        let mut status = icu_ffi::U_ZERO_ERROR;
        let source = unsafe { (f.ucnv_open)(source_encoding.as_ptr(), &mut status) };
        let target = unsafe { (f.ucnv_open)(target_encoding.as_ptr(), &mut status) };
        if status.is_failure() {
            if !source.is_null() {
                unsafe { (f.ucnv_close)(source) };
            }
            if !target.is_null() {
                unsafe { (f.ucnv_close)(target) };
            }
            return Err(status.as_error());
        }

        let pivot_source = pivot_buffer.as_mut_ptr() as *mut u16;
        let pivot_target = unsafe { pivot_source.add(pivot_buffer.len()) };

        Ok(Self { source, target, pivot_buffer, pivot_source, pivot_target, reset: true })
    }

    fn append_nul<'a>(arena: &'a Arena, input: &str) -> ArenaString<'a> {
        arena_format!(arena, "{}\0", input)
    }

    /// Performs one step of the encoding conversion.
    ///
    /// # Parameters
    ///
    /// * `input`: The input buffer to convert from.
    ///   It should be in the `source_encoding` that was previously specified.
    /// * `output`: The output buffer to convert to.
    ///   It should be in the `target_encoding` that was previously specified.
    ///
    /// # Returns
    ///
    /// A tuple containing:
    /// 1. The number of bytes read from the input buffer.
    /// 2. The number of bytes written to the output buffer.
    pub fn convert(
        &mut self,
        input: &[u8],
        output: &mut [MaybeUninit<u8>],
    ) -> apperr::Result<(usize, usize)> {
        let f = assume_loaded();

        let input_beg = input.as_ptr();
        let input_end = unsafe { input_beg.add(input.len()) };
        let mut input_ptr = input_beg;

        let output_beg = output.as_mut_ptr() as *mut u8;
        let output_end = unsafe { output_beg.add(output.len()) };
        let mut output_ptr = output_beg;

        let pivot_beg = self.pivot_buffer.as_mut_ptr() as *mut u16;
        let pivot_end = unsafe { pivot_beg.add(self.pivot_buffer.len()) };

        let flush = input.is_empty();
        let mut status = icu_ffi::U_ZERO_ERROR;

        unsafe {
            (f.ucnv_convertEx)(
                /* target_cnv   */ self.target,
                /* source_cnv   */ self.source,
                /* target       */ &mut output_ptr,
                /* target_limit */ output_end,
                /* source       */ &mut input_ptr,
                /* source_limit */ input_end,
                /* pivot_start  */ pivot_beg,
                /* pivot_source */ &mut self.pivot_source,
                /* pivot_target */ &mut self.pivot_target,
                /* pivot_limit  */ pivot_end,
                /* reset        */ self.reset,
                /* flush        */ flush,
                /* status       */ &mut status,
            );
        }

        self.reset = false;
        if status.is_failure() && status != icu_ffi::U_BUFFER_OVERFLOW_ERROR {
            return Err(status.as_error());
        }

        let input_advance = unsafe { input_ptr.offset_from(input_beg) as usize };
        let output_advance = unsafe { output_ptr.offset_from(output_beg) as usize };
        Ok((input_advance, output_advance))
    }
}

// In benchmarking, I found that the performance does not really change much by changing this value.
// I picked 64 because it seemed like a reasonable lower bound.
const CACHE_SIZE: usize = 64;

/// Caches a chunk of TextBuffer contents (UTF-8) in UTF-16 format.
#[repr(C)]
struct Cache {
    /// The translated text. Contains [`Cache::utf16_len`]-many valid items.
    utf16: [u16; CACHE_SIZE],
    /// For each character in [`Cache::utf16`] this stores the offset in the [`TextBuffer`],
    /// relative to the start offset stored in `native_beg`.
    /// This has the same length as [`Cache::utf16`].
    utf16_to_utf8_offsets: [u16; CACHE_SIZE],
    /// `utf8_to_utf16_offsets[native_offset - native_beg]` will tell you which character in
    /// [`Cache::utf16`] maps to the given `native_offset` in the underlying [`TextBuffer`].
    /// Contains `native_end - native_beg`-many valid items.
    utf8_to_utf16_offsets: [u16; CACHE_SIZE],

    /// The number of valid items in [`Cache::utf16`].
    utf16_len: usize,
    /// Offset of the first non-ASCII character.
    /// Less than or equal to [`Cache::utf16_len`].
    native_indexing_limit: usize,

    /// The range of UTF-8 text in the [`TextBuffer`] that this chunk covers.
    utf8_range: Range<usize>,
}

#[repr(C)]
struct DoubleCache {
    cache: [Cache; 2],
    /// You can consider this a 1 bit index into `cache`.
    mru: bool,
}

/// A wrapper around ICU's `UText` struct.
///
/// In our case its only purpose is to adapt a [`TextBuffer`] for ICU.
///
/// # Safety
///
/// Warning! No lifetime tracking is done here.
/// I initially did it properly with a PhantomData marker for the TextBuffer
/// lifetime, but it was a pain so now I don't. Not a big deal in our case.
pub struct Text(&'static mut icu_ffi::UText);

impl Drop for Text {
    fn drop(&mut self) {
        let f = assume_loaded();
        unsafe { (f.utext_close)(self.0) };
    }
}

impl Text {
    /// Constructs an ICU `UText` instance from a [`TextBuffer`].
    ///
    /// # Safety
    ///
    /// The caller must ensure that the given [`TextBuffer`]
    /// outlives the returned `Text` instance.
    pub unsafe fn new(tb: &TextBuffer) -> apperr::Result<Self> {
        let f = init_if_needed()?;

        let mut status = icu_ffi::U_ZERO_ERROR;
        let ptr =
            unsafe { (f.utext_setup)(null_mut(), size_of::<DoubleCache>() as i32, &mut status) };
        if status.is_failure() {
            return Err(status.as_error());
        }

        const FUNCS: icu_ffi::UTextFuncs = icu_ffi::UTextFuncs {
            table_size: size_of::<icu_ffi::UTextFuncs>() as i32,
            reserved1: 0,
            reserved2: 0,
            reserved3: 0,
            clone: Some(utext_clone),
            native_length: Some(utext_native_length),
            access: Some(utext_access),
            extract: None,
            replace: None,
            copy: None,
            map_offset_to_native: Some(utext_map_offset_to_native),
            map_native_index_to_utf16: Some(utext_map_native_index_to_utf16),
            close: None,
            spare1: None,
            spare2: None,
            spare3: None,
        };

        let ut = unsafe { &mut *ptr };
        ut.p_funcs = &FUNCS;
        ut.context = tb as *const TextBuffer as *mut _;
        ut.a = -1;

        Ok(Self(ut))
    }
}

fn text_buffer_from_utext<'a>(ut: &icu_ffi::UText) -> &'a TextBuffer {
    unsafe { &*(ut.context as *const TextBuffer) }
}

fn double_cache_from_utext<'a>(ut: &icu_ffi::UText) -> &'a mut DoubleCache {
    unsafe { &mut *(ut.p_extra as *mut DoubleCache) }
}

extern "C" fn utext_clone(
    dest: *mut icu_ffi::UText,
    src: &icu_ffi::UText,
    deep: bool,
    status: &mut icu_ffi::UErrorCode,
) -> *mut icu_ffi::UText {
    if status.is_failure() {
        return null_mut();
    }

    if deep {
        *status = icu_ffi::U_UNSUPPORTED_ERROR;
        return null_mut();
    }

    let f = assume_loaded();
    let ut_ptr = unsafe { (f.utext_setup)(dest, size_of::<DoubleCache>() as i32, status) };
    if status.is_failure() {
        return null_mut();
    }

    // TODO: I'm somewhat unsure whether we have to preserve the `chunk_offset`.
    // We can't blindly copy chunk contents and the `Cache` in `ut.p_extra`,
    // because they may contain dirty contents (different `TextBuffer` generation).
    unsafe {
        let ut = &mut *ut_ptr;
        ut.p_funcs = src.p_funcs;
        ut.context = src.context;
        ut.a = -1;
    }

    ut_ptr
}

extern "C" fn utext_native_length(ut: &mut icu_ffi::UText) -> i64 {
    let tb = text_buffer_from_utext(ut);
    tb.text_length() as i64
}

extern "C" fn utext_access(ut: &mut icu_ffi::UText, native_index: i64, forward: bool) -> bool {
    if let Some(cache) = utext_access_impl(ut, native_index, forward) {
        let native_off = native_index as usize - cache.utf8_range.start;
        ut.chunk_contents = cache.utf16.as_ptr();
        ut.chunk_length = cache.utf16_len as i32;
        ut.chunk_offset = cache.utf8_to_utf16_offsets[native_off] as i32;
        ut.chunk_native_start = cache.utf8_range.start as i64;
        ut.chunk_native_limit = cache.utf8_range.end as i64;
        ut.native_indexing_limit = cache.native_indexing_limit as i32;
        true
    } else {
        false
    }
}

fn utext_access_impl<'a>(
    ut: &mut icu_ffi::UText,
    native_index: i64,
    forward: bool,
) -> Option<&'a mut Cache> {
    let tb = text_buffer_from_utext(ut);
    let mut index_contained = native_index;

    if !forward {
        index_contained -= 1;
    }
    if index_contained < 0 || index_contained as usize >= tb.text_length() {
        return None;
    }

    let index_contained = index_contained as usize;
    let native_index = native_index as usize;
    let double_cache = double_cache_from_utext(ut);
    let dirty = ut.a != tb.generation() as i64;

    if dirty {
        // The text buffer contents have changed.
        // Invalidate both caches so that future calls don't mistakenly use them
        // when they enter the for loop in the else branch below (`dirty == false`).
        double_cache.cache[0].utf16_len = 0;
        double_cache.cache[1].utf16_len = 0;
        double_cache.cache[0].utf8_range = 0..0;
        double_cache.cache[1].utf8_range = 0..0;
        ut.a = tb.generation() as i64;
    } else {
        // Check if one of the caches already contains the requested range.
        for (i, cache) in double_cache.cache.iter_mut().enumerate() {
            if cache.utf8_range.contains(&index_contained) {
                double_cache.mru = i != 0;
                return Some(cache);
            }
        }
    }

    // Turn the least recently used cache into the most recently used one.
    let double_cache = double_cache_from_utext(ut);
    double_cache.mru = !double_cache.mru;
    let cache = &mut double_cache.cache[double_cache.mru as usize];

    // In order to safely fit any UTF-8 character into our cache,
    // we must assume the worst case of a 4-byte long encoding.
    const UTF16_LEN_LIMIT: usize = CACHE_SIZE - 4;
    let utf8_len_limit;
    let native_start;

    if forward {
        utf8_len_limit = (tb.text_length() - native_index).min(UTF16_LEN_LIMIT);
        native_start = native_index;
    } else {
        // The worst case ratio for UTF-8 to UTF-16 is 1:1, when the text is ASCII.
        // This allows us to safely subtract the UTF-16 buffer size
        // and assume that whatever we read as UTF-8 will fit.
        // TODO: Test what happens if you have lots of invalid UTF-8 text blow up to U+FFFD.
        utf8_len_limit = native_index.min(UTF16_LEN_LIMIT);

        // Since simply subtracting an offset may end up in the middle of a codepoint sequence,
        // we must align the offset to the next codepoint boundary.
        // Here we skip trail bytes until we find a lead.
        let mut beg = native_index - utf8_len_limit;
        let chunk = tb.read_forward(beg);
        for &c in chunk {
            if c & 0b1100_0000 != 0b1000_0000 {
                break;
            }
            beg += 1;
        }

        native_start = beg;
    }

    // Translate the given range from UTF-8 to UTF-16.
    // NOTE: This code makes the assumption that the `native_index` is always
    // at UTF-8 codepoint boundaries which technically isn't guaranteed.
    let mut utf16_len = 0;
    let mut utf8_len = 0;
    let mut ascii_len = 0;
    'outer: loop {
        let initial_utf8_len = utf8_len;
        let chunk = tb.read_forward(native_start + utf8_len);
        if chunk.is_empty() {
            break;
        }

        let mut it = Utf8Chars::new(chunk, 0);

        // If we've only seen ASCII so far we can fast-pass the UTF-16 translation,
        // because we can just widen from u8 -> u16.
        if utf16_len == ascii_len {
            let haystack = &chunk[..chunk.len().min(utf8_len_limit - ascii_len)];

            // When it comes to performance, and the search space is small (which it is here),
            // it's always a good idea to keep the loops small and tight...
            let len = haystack.iter().position(|&c| c >= 0x80).unwrap_or(haystack.len());

            // ...In this case it allows the compiler to vectorize this loop and double
            // the performance. Luckily, llvm doesn't unroll the loop, which is great,
            // because `len` will always be a relatively small number.
            for &c in &chunk[..len] {
                unsafe {
                    *cache.utf16.get_unchecked_mut(ascii_len) = c as u16;
                    *cache.utf16_to_utf8_offsets.get_unchecked_mut(ascii_len) = ascii_len as u16;
                    *cache.utf8_to_utf16_offsets.get_unchecked_mut(ascii_len) = ascii_len as u16;
                }
                ascii_len += 1;
            }

            utf16_len += len;
            utf8_len += len;
            it.seek(len);
            if ascii_len >= UTF16_LEN_LIMIT {
                break;
            }
        }

        loop {
            let Some(c) = it.next() else {
                break;
            };

            // Thanks to our `if utf16_len >= UTF16_LEN_LIMIT` check,
            // we can safely assume that this will fit.
            unsafe {
                let utf8_len_beg = utf8_len;
                let utf8_len_end = initial_utf8_len + it.offset();

                while utf8_len < utf8_len_end {
                    *cache.utf8_to_utf16_offsets.get_unchecked_mut(utf8_len) = utf16_len as u16;
                    utf8_len += 1;
                }

                if c <= '\u{FFFF}' {
                    *cache.utf16.get_unchecked_mut(utf16_len) = c as u16;
                    *cache.utf16_to_utf8_offsets.get_unchecked_mut(utf16_len) = utf8_len_beg as u16;
                    utf16_len += 1;
                } else {
                    let c = c as u32 - 0x10000;
                    let b = utf8_len_beg as u16;
                    *cache.utf16.get_unchecked_mut(utf16_len) = (c >> 10) as u16 | 0xD800;
                    *cache.utf16.get_unchecked_mut(utf16_len + 1) = (c & 0x3FF) as u16 | 0xDC00;
                    *cache.utf16_to_utf8_offsets.get_unchecked_mut(utf16_len) = b;
                    *cache.utf16_to_utf8_offsets.get_unchecked_mut(utf16_len + 1) = b;
                    utf16_len += 2;
                }
            }

            if utf16_len >= UTF16_LEN_LIMIT || utf8_len >= utf8_len_limit {
                break 'outer;
            }
        }
    }

    // Allow for looking up past-the-end indices via
    // `utext_map_offset_to_native` and `utext_map_native_index_to_utf16`.
    cache.utf16_to_utf8_offsets[utf16_len] = utf8_len as u16;
    cache.utf8_to_utf16_offsets[utf8_len] = utf16_len as u16;

    let native_limit = native_start + utf8_len;
    cache.utf16_len = utf16_len;
    // If parts of the UTF-8 chunk are ASCII, we can tell ICU that it doesn't need to call
    // utext_map_offset_to_native. For some reason, uregex calls that function *a lot*,
    // literally half the CPU time is spent on it.
    cache.native_indexing_limit = ascii_len;
    cache.utf8_range = native_start..native_limit;
    Some(cache)
}

extern "C" fn utext_map_offset_to_native(ut: &icu_ffi::UText) -> i64 {
    debug_assert!((0..=ut.chunk_length).contains(&ut.chunk_offset));

    let double_cache = double_cache_from_utext(ut);
    let cache = &double_cache.cache[double_cache.mru as usize];
    let off_rel = cache.utf16_to_utf8_offsets[ut.chunk_offset as usize];
    let off_abs = cache.utf8_range.start + off_rel as usize;
    off_abs as i64
}

extern "C" fn utext_map_native_index_to_utf16(ut: &icu_ffi::UText, native_index: i64) -> i32 {
    debug_assert!((ut.chunk_native_start..=ut.chunk_native_limit).contains(&native_index));

    let double_cache = double_cache_from_utext(ut);
    let cache = &double_cache.cache[double_cache.mru as usize];
    let off_rel = cache.utf8_to_utf16_offsets[(native_index - ut.chunk_native_start) as usize];
    off_rel as i32
}

/// A wrapper around ICU's `URegularExpression` struct.
///
/// # Safety
///
/// Warning! No lifetime tracking is done here.
pub struct Regex(&'static mut icu_ffi::URegularExpression);

impl Drop for Regex {
    fn drop(&mut self) {
        let f = assume_loaded();
        unsafe { (f.uregex_close)(self.0) };
    }
}

impl Regex {
    /// Enable case-insensitive matching.
    pub const CASE_INSENSITIVE: i32 = icu_ffi::UREGEX_CASE_INSENSITIVE;

    /// If set, ^ and $ match the start and end of each line.
    /// Otherwise, they match the start and end of the entire string.
    pub const MULTILINE: i32 = icu_ffi::UREGEX_MULTILINE;

    /// Treat the given pattern as a literal string.
    pub const LITERAL: i32 = icu_ffi::UREGEX_LITERAL;

    /// Constructs a regex, plain and simple. Read `uregex_open` docs.
    ///
    /// # Safety
    ///
    /// The caller must ensure that the given `Text` outlives the returned `Regex` instance.
    pub unsafe fn new(pattern: &str, flags: i32, text: &Text) -> apperr::Result<Self> {
        let f = init_if_needed()?;
        unsafe {
            let scratch = scratch_arena(None);
            let mut utf16 = Vec::new_in(&*scratch);
            let mut status = icu_ffi::U_ZERO_ERROR;

            utf16.extend(pattern.encode_utf16());

            let ptr = (f.uregex_open)(
                utf16.as_ptr(),
                utf16.len() as i32,
                icu_ffi::UREGEX_MULTILINE | icu_ffi::UREGEX_ERROR_ON_UNKNOWN_ESCAPES | flags,
                None,
                &mut status,
            );
            // ICU describes the time unit as being dependent on CPU performance
            // and "typically [in] the order of milliseconds", but this claim seems
            // highly outdated. On my CPU from 2021, a limit of 4096 equals roughly 600ms.
            (f.uregex_setTimeLimit)(ptr, 4096, &mut status);
            (f.uregex_setUText)(ptr, text.0 as *const _ as *mut _, &mut status);
            if status.is_failure() {
                return Err(status.as_error());
            }

            Ok(Self(&mut *ptr))
        }
    }

    /// Updates the regex pattern with the given text.
    /// If the text contents have changed, you can pass the same text as you used
    /// initially and it'll trigger ICU to reload the text and invalidate its caches.
    ///
    /// # Safety
    ///
    /// The caller must ensure that the given `Text` outlives the `Regex` instance.
    pub unsafe fn set_text(&mut self, text: &mut Text, offset: usize) {
        // Get `utext_access_impl` to detect the `TextBuffer::generation` change,
        // and refresh its contents. This ensures that ICU doesn't reuse
        // stale `UText::chunk_contents`, as it has no way tell that it's stale.
        utext_access(text.0, offset as i64, true);

        let f = assume_loaded();
        let mut status = icu_ffi::U_ZERO_ERROR;
        unsafe { (f.uregex_setUText)(self.0, text.0 as *const _ as *mut _, &mut status) };
        // `uregex_setUText` resets the regex to the start of the text.
        // Because of this, we must also call `uregex_reset64`.
        unsafe { (f.uregex_reset64)(self.0, offset as i64, &mut status) };
    }

    /// Sets the regex to the absolute offset in the underlying text.
    pub fn reset(&mut self, offset: usize) {
        let f = assume_loaded();
        let mut status = icu_ffi::U_ZERO_ERROR;
        unsafe { (f.uregex_reset64)(self.0, offset as i64, &mut status) };
    }

    /// Gets captured group count.
    pub fn group_count(&mut self) -> i32 {
        let f = assume_loaded();

        let mut status = icu_ffi::U_ZERO_ERROR;
        let count = unsafe { (f.uregex_groupCount)(self.0, &mut status) };
        if status.is_failure() { 0 } else { count }
    }

    /// Gets the text range of a captured group by index.
    pub fn group(&mut self, group: i32) -> Option<Range<usize>> {
        let f = assume_loaded();

        let mut status = icu_ffi::U_ZERO_ERROR;
        let start = unsafe { (f.uregex_start64)(self.0, group, &mut status) };
        let end = unsafe { (f.uregex_end64)(self.0, group, &mut status) };
        if status.is_failure() {
            None
        } else {
            let start = start.max(0);
            let end = end.max(start);
            Some(start as usize..end as usize)
        }
    }
}

impl Iterator for Regex {
    type Item = Range<usize>;

    fn next(&mut self) -> Option<Self::Item> {
        let f = assume_loaded();

        let mut status = icu_ffi::U_ZERO_ERROR;
        let ok = unsafe { (f.uregex_findNext)(self.0, &mut status) };
        if !ok {
            return None;
        }

        self.group(0)
    }
}

static mut ROOT_COLLATOR: Option<*mut icu_ffi::UCollator> = None;

/// Compares two UTF-8 strings for sorting using ICU's collation algorithm.
pub fn compare_strings(a: &[u8], b: &[u8]) -> Ordering {
    #[cold]
    fn init() {
        unsafe {
            let mut coll = null_mut();

            if let Ok(f) = init_if_needed() {
                let mut status = icu_ffi::U_ZERO_ERROR;
                coll = (f.ucol_open)(c"".as_ptr(), &mut status);
            }

            ROOT_COLLATOR = Some(coll);
        }
    }

    // OnceCell for people that want to put it into a static.
    #[allow(static_mut_refs)]
    let coll = unsafe {
        if ROOT_COLLATOR.is_none() {
            init();
        }
        ROOT_COLLATOR.unwrap_unchecked()
    };

    if coll.is_null() {
        compare_strings_ascii(a, b)
    } else {
        let f = assume_loaded();
        let mut status = icu_ffi::U_ZERO_ERROR;
        let res = unsafe {
            (f.ucol_strcollUTF8)(
                coll,
                a.as_ptr(),
                a.len() as i32,
                b.as_ptr(),
                b.len() as i32,
                &mut status,
            )
        };

        match res {
            icu_ffi::UCollationResult::UCOL_EQUAL => Ordering::Equal,
            icu_ffi::UCollationResult::UCOL_GREATER => Ordering::Greater,
            icu_ffi::UCollationResult::UCOL_LESS => Ordering::Less,
        }
    }
}

/// Unicode collation via `ucol_strcollUTF8`, now for ASCII!
fn compare_strings_ascii(a: &[u8], b: &[u8]) -> Ordering {
    let mut iter = a.iter().zip(b.iter());

    // Low weight: Find the first character which differs.
    //
    // Remember that result in case all remaining characters are
    // case-insensitive equal, because then we use that as a fallback.

    // Fallback: The shorter string wins.
    a.len().cmp(&b.len())
}

static mut ROOT_CASEMAP: Option<*mut icu_ffi::UCaseMap> = None;

/// Converts the given UTF-8 string to lower case.
///
/// Case folding differs from lower case in that the output is primarily useful
/// to machines for comparisons. It's like applying Unicode normalization.
pub fn fold_case<'a>(arena: &'a Arena, input: &str) -> ArenaString<'a> {
    // OnceCell for people that want to put it into a static.
    #[allow(static_mut_refs)]
    let casemap = unsafe {
        if ROOT_CASEMAP.is_none() {
            ROOT_CASEMAP = Some(if let Ok(f) = init_if_needed() {
                let mut status = icu_ffi::U_ZERO_ERROR;
                (f.ucasemap_open)(null(), 0, &mut status)
            } else {
                null_mut()
            })
        }
        ROOT_CASEMAP.unwrap_unchecked()
    };

    if !casemap.is_null() {
        let f = assume_loaded();
        let mut status = icu_ffi::U_ZERO_ERROR;
        let mut output = Vec::new_in(arena);
        let mut output_len;

        // First, guess the output length:
        // TODO: What's a good heuristic here?
        {
            output.reserve_exact(input.len() + 16);
            let output = output.spare_capacity_mut();
            output_len = unsafe {
                (f.ucasemap_utf8FoldCase)(
                    casemap,
                    output.as_mut_ptr() as *mut _,
                    output.len() as i32,
                    input.as_ptr() as *const _,
                    input.len() as i32,
                    &mut status,
                )
            };
        }

        // If that failed to fit, retry with the correct length.
        if status == icu_ffi::U_BUFFER_OVERFLOW_ERROR && output_len > 0 {
            output.reserve_exact(output_len as usize);
            let output = output.spare_capacity_mut();
            output_len = unsafe {
                (f.ucasemap_utf8FoldCase)(
                    casemap,
                    output.as_mut_ptr() as *mut _,
                    output.len() as i32,
                    input.as_ptr() as *const _,
                    input.len() as i32,
                    &mut status,
                )
            };
        }

        if status.is_success() && output_len > 0 {
            unsafe {
                output.set_len(output_len as usize);
            }
            return unsafe { ArenaString::from_utf8_unchecked(output) };
        }
    }

    let mut result = ArenaString::from_str(arena, input);
    for b in unsafe { result.as_bytes_mut() } {
        b.make_ascii_lowercase();
    }
    result
}

// NOTE:
// To keep this neat, fields are ordered by prefix (= `ucol_` before `uregex_`),
// followed by functions in this order:
// * Static methods (e.g. `ucnv_getAvailableName`)
// * Constructors (e.g. `ucnv_open`)
// * Destructors (e.g. `ucnv_close`)
// * Methods, grouped by relationship
//   (e.g. `uregex_start64` and `uregex_end64` are near each other)
//
// WARNING:
// The order of the fields MUST match the order of strings in the following two arrays.
#[allow(non_snake_case)]
#[repr(C)]
struct LibraryFunctions {
    // LIBICUUC_PROC_NAMES
    u_errorName: icu_ffi::u_errorName,
    ucasemap_open: icu_ffi::ucasemap_open,
    ucasemap_utf8FoldCase: icu_ffi::ucasemap_utf8FoldCase,
    ucnv_getAvailableName: icu_ffi::ucnv_getAvailableName,
    ucnv_getStandardName: icu_ffi::ucnv_getStandardName,
    ucnv_open: icu_ffi::ucnv_open,
    ucnv_close: icu_ffi::ucnv_close,
    ucnv_convertEx: icu_ffi::ucnv_convertEx,
    utext_setup: icu_ffi::utext_setup,
    utext_close: icu_ffi::utext_close,

    // LIBICUI18N_PROC_NAMES
    ucol_open: icu_ffi::ucol_open,
    ucol_strcollUTF8: icu_ffi::ucol_strcollUTF8,
    uregex_open: icu_ffi::uregex_open,
    uregex_close: icu_ffi::uregex_close,
    uregex_setTimeLimit: icu_ffi::uregex_setTimeLimit,
    uregex_setUText: icu_ffi::uregex_setUText,
    uregex_reset64: icu_ffi::uregex_reset64,
    uregex_findNext: icu_ffi::uregex_findNext,
    uregex_groupCount: icu_ffi::uregex_groupCount,
    uregex_start64: icu_ffi::uregex_start64,
    uregex_end64: icu_ffi::uregex_end64,
}

macro_rules! proc_name {
    ($s:literal) => {
        concat!(env!("EDIT_CFG_ICU_EXPORT_PREFIX"), $s, env!("EDIT_CFG_ICU_EXPORT_SUFFIX"), "\0")
            .as_ptr() as *const c_char
    };
}

// Found in libicuuc.so on UNIX, icuuc.dll/icu.dll on Windows.
const LIBICUUC_PROC_NAMES: [*const c_char; 10] = [
    proc_name!("u_errorName"),
    proc_name!("ucasemap_open"),
    proc_name!("ucasemap_utf8FoldCase"),
    proc_name!("ucnv_getAvailableName"),
    proc_name!("ucnv_getStandardName"),
    proc_name!("ucnv_open"),
    proc_name!("ucnv_close"),
    proc_name!("ucnv_convertEx"),
    proc_name!("utext_setup"),
    proc_name!("utext_close"),
];

// Found in libicui18n.so on UNIX, icuin.dll/icu.dll on Windows.
const LIBICUI18N_PROC_NAMES: [*const c_char; 11] = [
    proc_name!("ucol_open"),
    proc_name!("ucol_strcollUTF8"),
    proc_name!("uregex_open"),
    proc_name!("uregex_close"),
    proc_name!("uregex_setTimeLimit"),
    proc_name!("uregex_setUText"),
    proc_name!("uregex_reset64"),
    proc_name!("uregex_findNext"),
    proc_name!("uregex_groupCount"),
    proc_name!("uregex_start64"),
    proc_name!("uregex_end64"),
];

enum LibraryFunctionsState {
    Uninitialized,
    Failed,
    Loaded(LibraryFunctions),
}

static mut LIBRARY_FUNCTIONS: LibraryFunctionsState = LibraryFunctionsState::Uninitialized;

pub fn init() -> apperr::Result<()> {
    init_if_needed()?;
    Ok(())
}

#[allow(static_mut_refs)]
fn init_if_needed() -> apperr::Result<&'static LibraryFunctions> {
    #[cold]
    fn load() {
        unsafe {
            LIBRARY_FUNCTIONS = LibraryFunctionsState::Failed;

            let Ok(icu) = sys::load_icu() else {
                return;
            };

            type TransparentFunction = unsafe extern "C" fn() -> *const ();

            // OH NO I'M DOING A BAD THING
            //
            // If this assertion hits, you either forgot to update `LIBRARY_PROC_NAMES`
            // or you're on a platform where `dlsym` behaves different from classic UNIX and Windows.
            //
            // This code assumes that we can treat the `LibraryFunctions` struct containing various different function
            // pointers as an array of `TransparentFunction` pointers. In C, this works on any platform that supports
            // POSIX `dlsym` or equivalent, but I suspect Rust is once again being extra about it. In any case, that's
            // still better than loading every function one by one, just to blow up our binary size for no reason.
            const _: () = assert!(
                mem::size_of::<LibraryFunctions>()
                    == mem::size_of::<TransparentFunction>()
                        * (LIBICUUC_PROC_NAMES.len() + LIBICUI18N_PROC_NAMES.len())
            );

            let mut funcs = MaybeUninit::<LibraryFunctions>::uninit();
            let mut ptr = funcs.as_mut_ptr() as *mut TransparentFunction;

            #[cfg(edit_icu_renaming_auto_detect)]
            let scratch_outer = scratch_arena(None);
            #[cfg(edit_icu_renaming_auto_detect)]
            let suffix = sys::icu_detect_renaming_suffix(&scratch_outer, icu.libicuuc);

            for (handle, names) in [
                (icu.libicuuc, &LIBICUUC_PROC_NAMES[..]),
                (icu.libicui18n, &LIBICUI18N_PROC_NAMES[..]),
            ] {
                for &name in names {
                    #[cfg(edit_icu_renaming_auto_detect)]
                    let scratch = scratch_arena(Some(&scratch_outer));
                    #[cfg(edit_icu_renaming_auto_detect)]
                    let name = sys::icu_add_renaming_suffix(&scratch, name, &suffix);

                    let Ok(func) = sys::get_proc_address(handle, name) else {
                        debug_assert!(
                            false,
                            "Failed to load ICU function: {:?}",
                            CStr::from_ptr(name)
                        );
                        return;
                    };

                    ptr.write(func);
                    ptr = ptr.add(1);
                }
            }

            LIBRARY_FUNCTIONS = LibraryFunctionsState::Loaded(funcs.assume_init());
        }
    }

    unsafe {
        if matches!(&LIBRARY_FUNCTIONS, LibraryFunctionsState::Uninitialized) {
            load();
        }
    }

    match unsafe { &LIBRARY_FUNCTIONS } {
        LibraryFunctionsState::Loaded(f) => Ok(f),
        _ => Err(apperr::APP_ICU_MISSING),
    }
}

#[allow(static_mut_refs)]
fn assume_loaded() -> &'static LibraryFunctions {
    match unsafe { &LIBRARY_FUNCTIONS } {
        LibraryFunctionsState::Loaded(f) => f,
        _ => unreachable!(),
    }
}

mod icu_ffi {
    #![allow(dead_code, non_camel_case_types)]

    use std::ffi::{c_char, c_int, c_void};

    use crate::apperr;

    #[derive(Copy, Clone, Eq, PartialEq)]
    #[repr(transparent)]
    pub struct UErrorCode(c_int);

    impl UErrorCode {
        pub const fn new(code: u32) -> Self {
            Self(code as c_int)
        }

        pub fn is_success(&self) -> bool {
            self.0 <= 0
        }

        pub fn is_failure(&self) -> bool {
            self.0 > 0
        }

        pub fn as_error(&self) -> apperr::Error {
            debug_assert!(self.0 > 0);
            apperr::Error::new_icu(self.0 as u32)
        }
    }

    pub const U_ZERO_ERROR: UErrorCode = UErrorCode(0);
    pub const U_BUFFER_OVERFLOW_ERROR: UErrorCode = UErrorCode(15);
    pub const U_UNSUPPORTED_ERROR: UErrorCode = UErrorCode(16);

    pub type u_errorName = unsafe extern "C" fn(code: UErrorCode) -> *const c_char;

    pub struct UConverter;

    pub type ucnv_getAvailableName = unsafe extern "C" fn(n: i32) -> *const c_char;

    pub type ucnv_getStandardName = unsafe extern "C" fn(
        name: *const u8,
        standard: *const u8,
        status: &mut UErrorCode,
    ) -> *const c_char;

    pub type ucnv_open =
        unsafe extern "C" fn(converter_name: *const u8, status: &mut UErrorCode) -> *mut UConverter;

    pub type ucnv_close = unsafe extern "C" fn(converter: *mut UConverter);

    pub type ucnv_convertEx = unsafe extern "C" fn(
        target_cnv: *mut UConverter,
        source_cnv: *mut UConverter,
        target: *mut *mut u8,
        target_limit: *const u8,
        source: *mut *const u8,
        source_limit: *const u8,
        pivot_start: *mut u16,
        pivot_source: *mut *mut u16,
        pivot_target: *mut *mut u16,
        pivot_limit: *const u16,
        reset: bool,
        flush: bool,
        status: &mut UErrorCode,
    );

    pub struct UCaseMap;

    pub type ucasemap_open = unsafe extern "C" fn(
        locale: *const c_char,
        options: u32,
        status: &mut UErrorCode,
    ) -> *mut UCaseMap;

    pub type ucasemap_utf8FoldCase = unsafe extern "C" fn(
        csm: *const UCaseMap,
        dest: *mut c_char,
        dest_capacity: i32,
        src: *const c_char,
        src_length: i32,
        status: &mut UErrorCode,
    ) -> i32;

    #[repr(C)]
    pub enum UCollationResult {
        UCOL_EQUAL = 0,
        UCOL_GREATER = 1,
        UCOL_LESS = -1,
    }

    #[repr(C)]
    pub struct UCollator;

    pub type ucol_open =
        unsafe extern "C" fn(loc: *const c_char, status: &mut UErrorCode) -> *mut UCollator;

    pub type ucol_strcollUTF8 = unsafe extern "C" fn(
        coll: *mut UCollator,
        source: *const u8,
        source_length: i32,
        target: *const u8,
        target_length: i32,
        status: &mut UErrorCode,
    ) -> UCollationResult;

    // UText callback functions
    pub type UTextClone = unsafe extern "C" fn(
        dest: *mut UText,
        src: &UText,
        deep: bool,
        status: &mut UErrorCode,
    ) -> *mut UText;
    pub type UTextNativeLength = unsafe extern "C" fn(ut: &mut UText) -> i64;
    pub type UTextAccess =
        unsafe extern "C" fn(ut: &mut UText, native_index: i64, forward: bool) -> bool;
    pub type UTextExtract = unsafe extern "C" fn(
        ut: &mut UText,
        native_start: i64,
        native_limit: i64,
        dest: *mut u16,
        dest_capacity: i32,
        status: &mut UErrorCode,
    ) -> i32;
    pub type UTextReplace = unsafe extern "C" fn(
        ut: &mut UText,
        native_start: i64,
        native_limit: i64,
        replacement_text: *const u16,
        replacement_length: i32,
        status: &mut UErrorCode,
    ) -> i32;
    pub type UTextCopy = unsafe extern "C" fn(
        ut: &mut UText,
        native_start: i64,
        native_limit: i64,
        native_dest: i64,
        move_text: bool,
        status: &mut UErrorCode,
    );
    pub type UTextMapOffsetToNative = unsafe extern "C" fn(ut: &UText) -> i64;
    pub type UTextMapNativeIndexToUTF16 =
        unsafe extern "C" fn(ut: &UText, native_index: i64) -> i32;
    pub type UTextClose = unsafe extern "C" fn(ut: &mut UText);

    #[repr(C)]
    pub struct UTextFuncs {
        pub table_size: i32,
        pub reserved1: i32,
        pub reserved2: i32,
        pub reserved3: i32,
        pub clone: Option<UTextClone>,
        pub native_length: Option<UTextNativeLength>,
        pub access: Option<UTextAccess>,
        pub extract: Option<UTextExtract>,
        pub replace: Option<UTextReplace>,
        pub copy: Option<UTextCopy>,
        pub map_offset_to_native: Option<UTextMapOffsetToNative>,
        pub map_native_index_to_utf16: Option<UTextMapNativeIndexToUTF16>,
        pub close: Option<UTextClose>,
        pub spare1: Option<UTextClose>,
        pub spare2: Option<UTextClose>,
        pub spare3: Option<UTextClose>,
    }

    #[repr(C)]
    pub struct UText {
        pub magic: u32,
        pub flags: i32,
        pub provider_properties: i32,
        pub size_of_struct: i32,
        pub chunk_native_limit: i64,
        pub extra_size: i32,
        pub native_indexing_limit: i32,
        pub chunk_native_start: i64,
        pub chunk_offset: i32,
        pub chunk_length: i32,
        pub chunk_contents: *const u16,
        pub p_funcs: &'static UTextFuncs,
        pub p_extra: *mut c_void,
        pub context: *mut c_void,
        pub p: *mut c_void,
        pub q: *mut c_void,
        pub r: *mut c_void,
        pub priv_p: *mut c_void,
        pub a: i64,
        pub b: i32,
        pub c: i32,
        pub priv_a: i64,
        pub priv_b: i32,
        pub priv_c: i32,
    }

    pub const UTEXT_MAGIC: u32 = 0x345ad82c;
    pub const UTEXT_PROVIDER_LENGTH_IS_EXPENSIVE: i32 = 1;
    pub const UTEXT_PROVIDER_STABLE_CHUNKS: i32 = 2;
    pub const UTEXT_PROVIDER_WRITABLE: i32 = 3;
    pub const UTEXT_PROVIDER_HAS_META_DATA: i32 = 4;
    pub const UTEXT_PROVIDER_OWNS_TEXT: i32 = 5;

    pub type utext_setup = unsafe extern "C" fn(
        ut: *mut UText,
        extra_space: i32,
        status: &mut UErrorCode,
    ) -> *mut UText;
    pub type utext_close = unsafe extern "C" fn(ut: *mut UText) -> *mut UText;

    #[repr(C)]
    pub struct UParseError {
        pub line: i32,
        pub offset: i32,
        pub pre_context: [u16; 16],
        pub post_context: [u16; 16],
    }

    #[repr(C)]
    pub struct URegularExpression;

    pub const UREGEX_UNIX_LINES: i32 = 1;
    pub const UREGEX_CASE_INSENSITIVE: i32 = 2;
    pub const UREGEX_COMMENTS: i32 = 4;
    pub const UREGEX_MULTILINE: i32 = 8;
    pub const UREGEX_LITERAL: i32 = 16;
    pub const UREGEX_DOTALL: i32 = 32;
    pub const UREGEX_UWORD: i32 = 256;
    pub const UREGEX_ERROR_ON_UNKNOWN_ESCAPES: i32 = 512;

    pub type uregex_open = unsafe extern "C" fn(
        pattern: *const u16,
        pattern_length: i32,
        flags: i32,
        pe: Option<&mut UParseError>,
        status: &mut UErrorCode,
    ) -> *mut URegularExpression;
    pub type uregex_close = unsafe extern "C" fn(regexp: *mut URegularExpression);
    pub type uregex_setTimeLimit =
        unsafe extern "C" fn(regexp: *mut URegularExpression, limit: i32, status: &mut UErrorCode);
    pub type uregex_setUText = unsafe extern "C" fn(
        regexp: *mut URegularExpression,
        text: *mut UText,
        status: &mut UErrorCode,
    );
    pub type uregex_reset64 =
        unsafe extern "C" fn(regexp: *mut URegularExpression, index: i64, status: &mut UErrorCode);
    pub type uregex_findNext =
        unsafe extern "C" fn(regexp: *mut URegularExpression, status: &mut UErrorCode) -> bool;
    pub type uregex_groupCount =
        unsafe extern "C" fn(regexp: *mut URegularExpression, status: &mut UErrorCode) -> i32;
    pub type uregex_start64 = unsafe extern "C" fn(
        regexp: *mut URegularExpression,
        group_num: i32,
        status: &mut UErrorCode,
    ) -> i64;
    pub type uregex_end64 = unsafe extern "C" fn(
        regexp: *mut URegularExpression,
        group_num: i32,
        status: &mut UErrorCode,
    ) -> i64;
}

#[cfg(test)]
mod tests {
    use super::*;

    #[ignore]
    #[test]
    fn init() {
        assert!(init_if_needed().is_ok());
    }

    #[test]
    fn test_compare_strings_ascii() {
        // Empty strings
        assert_eq!(compare_strings_ascii(b"", b""), Ordering::Equal);
        // Equal strings
        assert_eq!(compare_strings_ascii(b"hello", b"hello"), Ordering::Equal);
        // Different lengths
        assert_eq!(compare_strings_ascii(b"abc", b"abcd"), Ordering::Less);
        assert_eq!(compare_strings_ascii(b"abcd", b"abc"), Ordering::Greater);
        // Same chars, different cases - 1st char wins
        assert_eq!(compare_strings_ascii(b"AbC", b"aBc"), Ordering::Less);
        // Different chars, different cases - 2nd char wins, because it differs
        assert_eq!(compare_strings_ascii(b"hallo", b"Hello"), Ordering::Less);
        assert_eq!(compare_strings_ascii(b"Hello", b"hallo"), Ordering::Greater);
    }
}

while let Some((&a, &b)) = iter.next() {
        if a != b {
            let mut order = a.cmp(&b);
            let la = a.to_ascii_lowercase();
            let lb = b.to_ascii_lowercase();
            if la == lb {
                for (a, b) in iter {
                    let la = a.to_ascii_lowercase();
                    let lb = b.to_ascii_lowercase();
                    if la != lb {
                        order = la.cmp(&lb);
                        break;
                    }
                }
            }
            return order;
        }
    }

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

//! Unix-specific platform code.
//!
//! Read the `windows` module for reference.
//! TODO: This reminds me that the sys API should probably be a trait.

use std::ffi::{CStr, c_char, c_int, c_void};
use std::fs::File;
use std::mem::{self, ManuallyDrop, MaybeUninit};
use std::os::fd::{AsRawFd as _, FromRawFd as _};
use std::path::Path;
use std::ptr::{self, NonNull, null_mut};
use std::{thread, time};

use crate::arena::{Arena, ArenaString, scratch_arena};
use crate::helpers::*;
use crate::{apperr, arena_format};

#[cfg(target_os = "netbsd")]
const fn desired_mprotect(flags: c_int) -> c_int {
    // NetBSD allows an mmap(2) caller to specify what protection flags they
    // will use later via mprotect. It does not allow a caller to move from
    // PROT_NONE to PROT_READ | PROT_WRITE.
    //
    // see PROT_MPROTECT in man 2 mmap
    flags << 3
}

#[cfg(not(target_os = "netbsd"))]
const fn desired_mprotect(_: c_int) -> c_int {
    libc::PROT_NONE
}

struct State {
    stdin: libc::c_int,
    stdin_flags: libc::c_int,
    stdout: libc::c_int,
    stdout_initial_termios: Option<libc::termios>,
    inject_resize: bool,
    // Buffer for incomplete UTF-8 sequences (max 4 bytes needed)
    utf8_buf: [u8; 4],
    utf8_len: usize,
}

static mut STATE: State = State {
    stdin: libc::STDIN_FILENO,
    stdin_flags: 0,
    stdout: libc::STDOUT_FILENO,
    stdout_initial_termios: None,
    inject_resize: false,
    utf8_buf: [0; 4],
    utf8_len: 0,
};

extern "C" fn sigwinch_handler(_: libc::c_int) {
    unsafe {
        STATE.inject_resize = true;
    }
}

pub fn init() -> Deinit {
    Deinit
}

pub fn switch_modes() -> apperr::Result<()> {
    unsafe {
        // Reopen stdin if it's redirected (= piped input).
        if libc::isatty(STATE.stdin) == 0 {
            STATE.stdin = check_int_return(libc::open(c"/dev/tty".as_ptr(), libc::O_RDONLY))?;
        }

        // Store the stdin flags so we can more easily toggle `O_NONBLOCK` later on.
        STATE.stdin_flags = check_int_return(libc::fcntl(STATE.stdin, libc::F_GETFL))?;

        // Set STATE.inject_resize to true whenever we get a SIGWINCH.
        let mut sigwinch_action: libc::sigaction = mem::zeroed();
        sigwinch_action.sa_sigaction = sigwinch_handler as libc::sighandler_t;
        check_int_return(libc::sigaction(libc::SIGWINCH, &sigwinch_action, null_mut()))?;

        // Get the original terminal modes so we can disable raw mode on exit.
        let mut termios = MaybeUninit::<libc::termios>::uninit();
        check_int_return(libc::tcgetattr(STATE.stdout, termios.as_mut_ptr()))?;
        let mut termios = termios.assume_init();
        STATE.stdout_initial_termios = Some(termios);

        termios.c_iflag &= !(
            // When neither IGNBRK...
            libc::IGNBRK
            // ...nor BRKINT are set, a BREAK reads as a null byte ('\0'), ...
            | libc::BRKINT
            // ...except when PARMRK is set, in which case it reads as the sequence \377 \0 \0.
            | libc::PARMRK
            // Disable input parity checking.
            | libc::INPCK
            // Disable stripping of eighth bit.
            | libc::ISTRIP
            // Disable mapping of NL to CR on input.
            | libc::INLCR
            // Disable ignoring CR on input.
            | libc::IGNCR
            // Disable mapping of CR to NL on input.
            | libc::ICRNL
            // Disable software flow control.
            | libc::IXON
        );
        // Disable output processing.
        termios.c_oflag &= !libc::OPOST;
        termios.c_cflag &= !(
            // Reset character size mask.
            libc::CSIZE
            // Disable parity generation.
            | libc::PARENB
        );
        // Set character size back to 8 bits.
        termios.c_cflag |= libc::CS8;
        termios.c_lflag &= !(
            // Disable signal generation (SIGINT, SIGTSTP, SIGQUIT).
            libc::ISIG
            // Disable canonical mode (line buffering).
            | libc::ICANON
            // Disable echoing of input characters.
            | libc::ECHO
            // Disable echoing of NL.
            | libc::ECHONL
            // Disable extended input processing (e.g. Ctrl-V).
            | libc::IEXTEN
        );

        // Set the terminal to raw mode.
        termios.c_lflag &= !(libc::ICANON | libc::ECHO);
        check_int_return(libc::tcsetattr(STATE.stdout, libc::TCSANOW, &termios))?;

        Ok(())
    }
}

pub struct Deinit;

impl Drop for Deinit {
    fn drop(&mut self) {
        unsafe {
            #[allow(static_mut_refs)]
            if let Some(termios) = STATE.stdout_initial_termios.take() {
                // Restore the original terminal modes.
                libc::tcsetattr(STATE.stdout, libc::TCSANOW, &termios);
            }
        }
    }
}

pub fn inject_window_size_into_stdin() {
    unsafe {
        STATE.inject_resize = true;
    }
}

fn get_window_size() -> (u16, u16) {
    let mut winsz: libc::winsize = unsafe { mem::zeroed() };

    for attempt in 1.. {
        let ret = unsafe { libc::ioctl(STATE.stdout, libc::TIOCGWINSZ, &raw mut winsz) };
        if ret == -1 || (winsz.ws_col != 0 && winsz.ws_row != 0) {
            break;
        }

        if attempt == 10 {
            winsz.ws_col = 80;
            winsz.ws_row = 24;
            break;
        }

        // Some terminals are bad emulators and don't report TIOCGWINSZ immediately.
        thread::sleep(time::Duration::from_millis(10 * attempt));
    }

    (winsz.ws_col, winsz.ws_row)
}

/// Reads from stdin.
///
/// Returns `None` if there was an error reading from stdin.
/// Returns `Some("")` if the given timeout was reached.
/// Otherwise, it returns the read, non-empty string.
pub fn read_stdin(arena: &Arena, mut timeout: time::Duration) -> Option<ArenaString<'_>> {
    unsafe {
        if STATE.inject_resize {
            timeout = time::Duration::ZERO;
        }

        let read_poll = timeout != time::Duration::MAX;
        let mut buf = Vec::new_in(arena);

        // We don't know if the input is valid UTF8, so we first use a Vec and then
        // later turn it into UTF8 using `from_utf8_lossy_owned`.
        // It is important that we allocate the buffer with an explicit capacity,
        // because we later use `spare_capacity_mut` to access it.
        buf.reserve(4 * KIBI);

        // We got some leftover broken UTF8 from a previous read? Prepend it.
        if STATE.utf8_len != 0 {
            buf.extend_from_slice(&STATE.utf8_buf[..STATE.utf8_len]);
            STATE.utf8_len = 0;
        }

        loop {
            if timeout != time::Duration::MAX {
                let beg = time::Instant::now();

                let mut pollfd = libc::pollfd { fd: STATE.stdin, events: libc::POLLIN, revents: 0 };
                let ret;
                #[cfg(target_os = "linux")]
                {
                    let ts = libc::timespec {
                        tv_sec: timeout.as_secs() as libc::time_t,
                        tv_nsec: timeout.subsec_nanos() as libc::c_long,
                    };
                    ret = libc::ppoll(&mut pollfd, 1, &ts, ptr::null());
                }
                #[cfg(not(target_os = "linux"))]
                {
                    ret = libc::poll(&mut pollfd, 1, timeout.as_millis() as libc::c_int);
                }
                if ret < 0 {
                    return None; // Error? Let's assume it's an EOF.
                }
                if ret == 0 {
                    break; // Timeout? We can stop reading.
                }

                timeout = timeout.saturating_sub(beg.elapsed());
            };

            // If we're asked for a non-blocking read we need
            // to manipulate `O_NONBLOCK` and vice versa.
            set_tty_nonblocking(read_poll);

            // Read from stdin.
            let spare = buf.spare_capacity_mut();
            let ret = libc::read(STATE.stdin, spare.as_mut_ptr() as *mut _, spare.len());
            if ret > 0 {
                buf.set_len(buf.len() + ret as usize);
                break;
            }
            if ret == 0 {
                return None; // EOF
            }
            if ret < 0 {
                match errno() {
                    libc::EINTR if STATE.inject_resize => break,
                    libc::EAGAIN if timeout == time::Duration::ZERO => break,
                    libc::EINTR | libc::EAGAIN => {}
                    _ => return None,
                }
            }
        }

        if !buf.is_empty() {
            // We only need to check the last 3 bytes for UTF-8 continuation bytes,
            // because we should be able to assume that any 4 byte sequence is complete.
            let lim = buf.len().saturating_sub(3);
            let mut off = buf.len() - 1;

            // Find the start of the last potentially incomplete UTF-8 sequence.
            while off > lim && buf[off] & 0b1100_0000 == 0b1000_0000 {
                off -= 1;
            }

            let seq_len = match buf[off] {
                b if b & 0b1000_0000 == 0 => 1,
                b if b & 0b1110_0000 == 0b1100_0000 => 2,
                b if b & 0b1111_0000 == 0b1110_0000 => 3,
                b if b & 0b1111_1000 == 0b1111_0000 => 4,
                // If the lead byte we found isn't actually one, we don't cache it.
                // `from_utf8_lossy_owned` will replace it with U+FFFD.
                _ => 0,
            };

            // Cache incomplete sequence if any.
            if off + seq_len > buf.len() {
                STATE.utf8_len = buf.len() - off;
                STATE.utf8_buf[..STATE.utf8_len].copy_from_slice(&buf[off..]);
                buf.truncate(off);
            }
        }

        let mut result = ArenaString::from_utf8_lossy_owned(buf);

        // We received a SIGWINCH? Add a fake window size sequence for our input parser.
        // I prepend it so that on startup, the TUI system gets first initialized with a size.
        if STATE.inject_resize {
            STATE.inject_resize = false;
            let (w, h) = get_window_size();
            if w > 0 && h > 0 {
                let scratch = scratch_arena(Some(arena));
                let seq = arena_format!(&scratch, "\x1b[8;{h};{w}t");
                result.replace_range(0..0, &seq);
            }
        }

        result.shrink_to_fit();
        Some(result)
    }
}

pub fn write_stdout(text: &str) {
    if text.is_empty() {
        return;
    }

    // If we don't set the TTY to blocking mode,
    // the write will potentially fail with EAGAIN.
    set_tty_nonblocking(false);

    let buf = text.as_bytes();
    let mut written = 0;

    while written < buf.len() {
        let w = &buf[written..];
        let w = &buf[..w.len().min(GIBI)];
        let n = unsafe { libc::write(STATE.stdout, w.as_ptr() as *const _, w.len()) };

        if n >= 0 {
            written += n as usize;
            continue;
        }

        let err = errno();
        if err != libc::EINTR {
            return;
        }
    }
}

/// Sets/Resets `O_NONBLOCK` on the TTY handle.
///
/// Note that setting this flag applies to both stdin and stdout, because the
/// TTY is a bidirectional device and both handles refer to the same thing.
fn set_tty_nonblocking(nonblock: bool) {
    unsafe {
        let is_nonblock = (STATE.stdin_flags & libc::O_NONBLOCK) != 0;
        if is_nonblock != nonblock {
            STATE.stdin_flags ^= libc::O_NONBLOCK;
            let _ = libc::fcntl(STATE.stdin, libc::F_SETFL, STATE.stdin_flags);
        }
    }
}

pub fn open_stdin_if_redirected() -> Option<File> {
    unsafe {
        // Did we reopen stdin during `init()`?
        if STATE.stdin != libc::STDIN_FILENO {
            Some(File::from_raw_fd(libc::STDIN_FILENO))
        } else {
            None
        }
    }
}

#[derive(Clone, PartialEq, Eq)]
pub struct FileId {
    st_dev: libc::dev_t,
    st_ino: libc::ino_t,
}

/// Returns a unique identifier for the given file by handle or path.
pub fn file_id(file: Option<&File>, path: &Path) -> apperr::Result<FileId> {
    let file = match file {
        Some(f) => f,
        None => &File::open(path)?,
    };

    unsafe {
        let mut stat = MaybeUninit::<libc::stat>::uninit();
        check_int_return(libc::fstat(file.as_raw_fd(), stat.as_mut_ptr()))?;
        let stat = stat.assume_init();
        Ok(FileId { st_dev: stat.st_dev, st_ino: stat.st_ino })
    }
}

/// Reserves a virtual memory region of the given size.
/// To commit the memory, use `virtual_commit`.
/// To release the memory, use `virtual_release`.
///
/// # Safety
///
/// This function is unsafe because it uses raw pointers.
/// Don't forget to release the memory when you're done with it or you'll leak it.
pub unsafe fn virtual_reserve(size: usize) -> apperr::Result<NonNull<u8>> {
    unsafe {
        let ptr = libc::mmap(
            null_mut(),
            size,
            desired_mprotect(libc::PROT_READ | libc::PROT_WRITE),
            libc::MAP_PRIVATE | libc::MAP_ANONYMOUS,
            -1,
            0,
        );
        if ptr.is_null() || ptr::eq(ptr, libc::MAP_FAILED) {
            Err(errno_to_apperr(libc::ENOMEM))
        } else {
            Ok(NonNull::new_unchecked(ptr as *mut u8))
        }
    }
}

/// Releases a virtual memory region of the given size.
///
/// # Safety
///
/// This function is unsafe because it uses raw pointers.
/// Make sure to only pass pointers acquired from `virtual_reserve`.
pub unsafe fn virtual_release(base: NonNull<u8>, size: usize) {
    unsafe {
        libc::munmap(base.cast().as_ptr(), size);
    }
}

/// Commits a virtual memory region of the given size.
///
/// # Safety
///
/// This function is unsafe because it uses raw pointers.
/// Make sure to only pass pointers acquired from `virtual_reserve`
/// and to pass a size less than or equal to the size passed to `virtual_reserve`.
pub unsafe fn virtual_commit(base: NonNull<u8>, size: usize) -> apperr::Result<()> {
    unsafe {
        let status = libc::mprotect(base.cast().as_ptr(), size, libc::PROT_READ | libc::PROT_WRITE);
        if status != 0 { Err(errno_to_apperr(libc::ENOMEM)) } else { Ok(()) }
    }
}

unsafe fn load_library(name: *const c_char) -> apperr::Result<NonNull<c_void>> {
    unsafe {
        NonNull::new(libc::dlopen(name, libc::RTLD_LAZY))
            .ok_or_else(|| errno_to_apperr(libc::ENOENT))
    }
}

/// Loads a function from a dynamic library.
///
/// # Safety
///
/// This function is highly unsafe as it requires you to know the exact type
/// of the function you're loading. No type checks whatsoever are performed.
//
// It'd be nice to constrain T to std::marker::FnPtr, but that's unstable.
pub unsafe fn get_proc_address<T>(
    handle: NonNull<c_void>,
    name: *const c_char,
) -> apperr::Result<T> {
    unsafe {
        let sym = libc::dlsym(handle.as_ptr(), name);
        if sym.is_null() {
            Err(errno_to_apperr(libc::ENOENT))
        } else {
            Ok(mem::transmute_copy(&sym))
        }
    }
}

pub struct LibIcu {
    pub libicuuc: NonNull<c_void>,
    pub libicui18n: NonNull<c_void>,
}

pub fn load_icu() -> apperr::Result<LibIcu> {
    const fn const_str_eq(a: &str, b: &str) -> bool {
        let a = a.as_bytes();
        let b = b.as_bytes();
        let mut i = 0;

        loop {
            if i >= a.len() || i >= b.len() {
                return a.len() == b.len();
            }
            if a[i] != b[i] {
                return false;
            }
            i += 1;
        }
    }

    const LIBICUUC: &str = concat!(env!("EDIT_CFG_ICUUC_SONAME"), "\0");
    const LIBICUI18N: &str = concat!(env!("EDIT_CFG_ICUI18N_SONAME"), "\0");

    if const { const_str_eq(LIBICUUC, LIBICUI18N) } {
        let icu = unsafe { load_library(LIBICUUC.as_ptr() as *const _)? };
        Ok(LibIcu { libicuuc: icu, libicui18n: icu })
    } else {
        let libicuuc = unsafe { load_library(LIBICUUC.as_ptr() as *const _)? };
        let libicui18n = unsafe { load_library(LIBICUI18N.as_ptr() as *const _)? };
        Ok(LibIcu { libicuuc, libicui18n })
    }
}
/// ICU, by default, adds the major version as a suffix to each exported symbol.
/// They also recommend to disable this for system-level installations (`runConfigureICU Linux --disable-renaming`),
/// but I found that many (most?) Linux distributions don't do this for some reason.
/// This function returns the suffix, if any.
#[cfg(edit_icu_renaming_auto_detect)]
pub fn icu_detect_renaming_suffix(arena: &Arena, handle: NonNull<c_void>) -> ArenaString<'_> {
    unsafe {
        type T = *const c_void;

        let mut res = ArenaString::new_in(arena);

        // Check if the ICU library is using unversioned symbols.
        // Return an empty suffix in that case.
        if get_proc_address::<T>(handle, c"u_errorName".as_ptr()).is_ok() {
            return res;
        }

        // In the versions (63-76) and distributions (Arch/Debian) I tested,
        // this symbol seems to be always present. This allows us to call `dladdr`.
        // It's the `UCaseMap::~UCaseMap()` destructor which for some reason isn't
        // in a namespace. Thank you ICU maintainers for this oversight.
        let proc = match get_proc_address::<T>(handle, c"_ZN8UCaseMapD1Ev".as_ptr()) {
            Ok(proc) => proc,
            Err(_) => return res,
        };

        // `dladdr` is specific to GNU's libc unfortunately.
        let mut info: libc::Dl_info = mem::zeroed();
        let ret = libc::dladdr(proc, &mut info);
        if ret == 0 {
            return res;
        }

        // The library path is in `info.dli_fname`.
        let path = match CStr::from_ptr(info.dli_fname).to_str() {
            Ok(name) => name,
            Err(_) => return res,
        };

        let path = match std::fs::read_link(path) {
            Ok(path) => path,
            Err(_) => path.into(),
        };

        // I'm going to assume it's something like "libicuuc.so.76.1".
        let path = path.into_os_string();
        let path = path.to_string_lossy();
        let suffix_start = match path.rfind(".so.") {
            Some(pos) => pos + 4,
            None => return res,
        };
        let version = &path[suffix_start..];
        let version_end = version.find('.').unwrap_or(version.len());
        let version = &version[..version_end];

        res.push('_');
        res.push_str(version);
        res
    }
}

#[cfg(edit_icu_renaming_auto_detect)]
#[allow(clippy::not_unsafe_ptr_arg_deref)]
pub fn icu_add_renaming_suffix<'a, 'b, 'r>(
    arena: &'a Arena,
    name: *const c_char,
    suffix: &str,
) -> *const c_char
where
    'a: 'r,
    'b: 'r,
{
    if suffix.is_empty() {
        name
    } else {
        // SAFETY: In this particular case we know that the string
        // is valid UTF-8, because it comes from icu.rs.
        let name = unsafe { CStr::from_ptr(name) };
        let name = unsafe { name.to_str().unwrap_unchecked() };

        let mut res = ManuallyDrop::new(ArenaString::new_in(arena));
        res.reserve(name.len() + suffix.len() + 1);
        res.push_str(name);
        res.push_str(suffix);
        res.push('\0');
        res.as_ptr() as *const c_char
    }
}

pub fn preferred_languages(arena: &Arena) -> Vec<ArenaString<'_>, &Arena> {
    let mut locales = Vec::new_in(arena);

    for key in ["LANGUAGE", "LC_ALL", "LANG"] {
        if let Ok(val) = std::env::var(key)
            && !val.is_empty()
        {
            locales.extend(val.split(':').filter(|s| !s.is_empty()).map(|s| {
                // Replace all underscores with dashes,
                // because the localization code expects pt-br, not pt_BR.
                let mut res = Vec::new_in(arena);
                res.extend(s.as_bytes().iter().map(|&b| if b == b'_' { b'-' } else { b }));
                unsafe { ArenaString::from_utf8_unchecked(res) }
            }));
            break;
        }
    }

    locales
}

#[inline]
fn errno() -> i32 {
    // Under `-O -Copt-level=s` the 1.87 compiler fails to fully inline and
    // remove the raw_os_error() call. This leaves us with the drop() call.
    // ManuallyDrop fixes that and results in a direct `std::sys::os::errno` call.
    ManuallyDrop::new(std::io::Error::last_os_error()).raw_os_error().unwrap_or(0)
}

#[inline]
pub(crate) fn io_error_to_apperr(err: std::io::Error) -> apperr::Error {
    errno_to_apperr(err.raw_os_error().unwrap_or(0))
}

pub fn apperr_format(f: &mut std::fmt::Formatter<'_>, code: u32) -> std::fmt::Result {
    write!(f, "Error {code}")?;

    unsafe {
        let ptr = libc::strerror(code as i32);
        if !ptr.is_null() {
            let msg = CStr::from_ptr(ptr).to_string_lossy();
            write!(f, ": {msg}")?;
        }
    }

    Ok(())
}

pub fn apperr_is_not_found(err: apperr::Error) -> bool {
    err == errno_to_apperr(libc::ENOENT)
}

const fn errno_to_apperr(no: c_int) -> apperr::Error {
    apperr::Error::new_sys(if no < 0 { 0 } else { no as u32 })
}

fn check_int_return(ret: libc::c_int) -> apperr::Result<libc::c_int> {
    if ret < 0 { Err(errno_to_apperr(errno())) } else { Ok(ret) }
}

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

//! A text buffer for a text editor.
//!
//! Implements a Unicode-aware, layout-aware text buffer for terminals.
//! It's based on a gap buffer. It has no line cache and instead relies
//! on the performance of the ucd module for fast text navigation.
//!
//! ---
//!
//! If the project ever outgrows a basic gap buffer (e.g. to add time travel)
//! an ideal, alternative architecture would be a piece table with immutable trees.
//! The tree nodes can be allocated on the same arena allocator as the added chunks,
//! making lifetime management fairly easy. The algorithm is described here:
//! * <https://cdacamar.github.io/data%20structures/algorithms/benchmarking/text%20editors/c++/editor-data-structures/>
//! * <https://github.com/cdacamar/fredbuf>
//!
//! The downside is that text navigation & search takes a performance hit due to small chunks.
//! The solution to the former is to keep line caches, which further complicates the architecture.
//! There's no solution for the latter. However, there's a chance that the performance will still be sufficient.

mod gap_buffer;
mod navigation;

use std::borrow::Cow;
use std::cell::UnsafeCell;
use std::collections::LinkedList;
use std::fmt::Write as _;
use std::fs::File;
use std::io::{Read as _, Write as _};
use std::mem::{self, MaybeUninit};
use std::ops::Range;
use std::rc::Rc;
use std::str;

pub use gap_buffer::GapBuffer;

use crate::arena::{Arena, ArenaString, scratch_arena};
use crate::cell::SemiRefCell;
use crate::clipboard::Clipboard;
use crate::document::{ReadableDocument, WriteableDocument};
use crate::framebuffer::{Framebuffer, IndexedColor};
use crate::helpers::*;
use crate::oklab::oklab_blend;
use crate::simd::memchr2;
use crate::unicode::{self, Cursor, MeasurementConfig, Utf8Chars};
use crate::{apperr, icu, simd};

/// The margin template is used for line numbers.
/// The max. line number we should ever expect is probably 64-bit,
/// and so this template fits 19 digits, followed by " │ ".
const MARGIN_TEMPLATE: &str = "                    │ ";
/// Just a bunch of whitespace you can use for turning tabs into spaces.
/// Happens to reuse MARGIN_TEMPLATE, because it has sufficient whitespace.
const TAB_WHITESPACE: &str = MARGIN_TEMPLATE;
const VISUAL_SPACE: &str = "･";
const VISUAL_SPACE_PREFIX_ADD: usize = '･'.len_utf8() - 1;
const VISUAL_TAB: &str = "￫       ";
const VISUAL_TAB_PREFIX_ADD: usize = '￫'.len_utf8() - 1;

/// Stores statistics about the whole document.
#[derive(Copy, Clone)]
pub struct TextBufferStatistics {
    logical_lines: CoordType,
    visual_lines: CoordType,
}

/// Stores the active text selection anchors.
///
/// The two points are not sorted. Instead, `beg` refers to where the selection
/// started being made and `end` refers to the currently being updated position.
#[derive(Copy, Clone)]
struct TextBufferSelection {
    beg: Point,
    end: Point,
}

/// In order to group actions into a single undo step,
/// we need to know the type of action that was performed.
/// This stores the action type.
#[derive(Copy, Clone, Eq, PartialEq)]
enum HistoryType {
    Other,
    Write,
    Delete,
}

/// An undo/redo entry.
struct HistoryEntry {
    /// [`TextBuffer::cursor`] position before the change was made.
    cursor_before: Point,
    /// [`TextBuffer::selection`] before the change was made.
    selection_before: Option<TextBufferSelection>,
    /// [`TextBuffer::stats`] before the change was made.
    stats_before: TextBufferStatistics,
    /// [`GapBuffer::generation`] before the change was made.
    ///
    /// **NOTE:** Entries with the same generation are grouped together.
    generation_before: u32,
    /// Logical cursor position where the change took place.
    /// The position is at the start of the changed range.
    cursor: Point,
    /// Text that was deleted from the buffer.
    deleted: Vec<u8>,
    /// Text that was added to the buffer.
    added: Vec<u8>,
}

/// Caches an ICU search operation.
struct ActiveSearch {
    /// The search pattern.
    pattern: String,
    /// The search options.
    options: SearchOptions,
    /// The ICU `UText` object.
    text: icu::Text,
    /// The ICU `URegularExpression` object.
    regex: icu::Regex,
    /// [`GapBuffer::generation`] when the search was created.
    /// This is used to detect if we need to refresh the
    /// [`ActiveSearch::regex`] object.
    buffer_generation: u32,
    /// [`TextBuffer::selection_generation`] when the search was
    /// created. When the user manually selects text, we need to
    /// refresh the [`ActiveSearch::pattern`] with it.
    selection_generation: u32,
    /// Stores the text buffer offset in between searches.
    next_search_offset: usize,
    /// If we know there were no hits, we can skip searching.
    no_matches: bool,
}

/// Options for a search operation.
#[derive(Default, Clone, Copy, Eq, PartialEq)]
pub struct SearchOptions {
    /// If true, the search is case-sensitive.
    pub match_case: bool,
    /// If true, the search matches whole words.
    pub whole_word: bool,
    /// If true, the search uses regex.
    pub use_regex: bool,
}

enum RegexReplacement<'a> {
    Group(i32),
    Text(Vec<u8, &'a Arena>),
}

/// Caches the start and length of the active edit line for a single edit.
/// This helps us avoid having to remeasure the buffer after an edit.
struct ActiveEditLineInfo {
    /// Points to the start of the currently being edited line.
    safe_start: Cursor,
    /// Number of visual rows of the line that starts
    /// at [`ActiveEditLineInfo::safe_start`].
    line_height_in_rows: CoordType,
    /// Byte distance from the start of the line at
    /// [`ActiveEditLineInfo::safe_start`] to the next line.
    distance_next_line_start: usize,
}

/// Undo/redo grouping works by recording a set of "overrides",
/// which are then applied in [`TextBuffer::edit_begin()`].
/// This allows us to create a group of edits that all share a
/// common `generation_before` and can be undone/redone together.
/// This struct stores those overrides.
struct ActiveEditGroupInfo {
    /// [`TextBuffer::cursor`] position before the change was made.
    cursor_before: Point,
    /// [`TextBuffer::selection`] before the change was made.
    selection_before: Option<TextBufferSelection>,
    /// [`TextBuffer::stats`] before the change was made.
    stats_before: TextBufferStatistics,
    /// [`GapBuffer::generation`] before the change was made.
    ///
    /// **NOTE:** Entries with the same generation are grouped together.
    generation_before: u32,
}

/// Char- or word-wise navigation? Your choice.
pub enum CursorMovement {
    Grapheme,
    Word,
}

/// See [`TextBuffer::move_selected_lines`].
pub enum MoveLineDirection {
    Up,
    Down,
}

/// The result of a call to [`TextBuffer::render()`].
pub struct RenderResult {
    /// The maximum visual X position we encountered during rendering.
    pub visual_pos_x_max: CoordType,
}

/// A [`TextBuffer`] with inner mutability.
pub type TextBufferCell = SemiRefCell<TextBuffer>;

/// A [`TextBuffer`] inside an [`Rc`].
///
/// We need this because the TUI system needs to borrow
/// the given text buffer(s) until after the layout process.
pub type RcTextBuffer = Rc<TextBufferCell>;

/// A text buffer for a text editor.
pub struct TextBuffer {
    buffer: GapBuffer,

    undo_stack: LinkedList<SemiRefCell<HistoryEntry>>,
    redo_stack: LinkedList<SemiRefCell<HistoryEntry>>,
    last_history_type: HistoryType,
    last_save_generation: u32,

    active_edit_group: Option<ActiveEditGroupInfo>,
    active_edit_line_info: Option<ActiveEditLineInfo>,
    active_edit_depth: i32,
    active_edit_off: usize,

    stats: TextBufferStatistics,
    cursor: Cursor,
    // When scrolling significant amounts of text away from the cursor,
    // rendering will naturally slow down proportionally to the distance.
    // To avoid this, we cache the cursor position for rendering.
    // Must be cleared on every edit or reflow.
    cursor_for_rendering: Option<Cursor>,
    selection: Option<TextBufferSelection>,
    selection_generation: u32,
    search: Option<UnsafeCell<ActiveSearch>>,

    width: CoordType,
    margin_width: CoordType,
    margin_enabled: bool,
    word_wrap_column: CoordType,
    word_wrap_enabled: bool,
    tab_size: CoordType,
    indent_with_tabs: bool,
    line_highlight_enabled: bool,
    ruler: CoordType,
    encoding: &'static str,
    newlines_are_crlf: bool,
    insert_final_newline: bool,
    overtype: bool,

    wants_cursor_visibility: bool,
}

impl TextBuffer {
    /// Creates a new text buffer inside an [`Rc`].
    /// See [`TextBuffer::new()`].
    pub fn new_rc(small: bool) -> apperr::Result<RcTextBuffer> {
        let buffer = Self::new(small)?;
        Ok(Rc::new(SemiRefCell::new(buffer)))
    }

    /// Creates a new text buffer. With `small` you can control
    /// if the buffer is optimized for <1MiB contents.
    pub fn new(small: bool) -> apperr::Result<Self> {
        Ok(Self {
            buffer: GapBuffer::new(small)?,

            undo_stack: LinkedList::new(),
            redo_stack: LinkedList::new(),
            last_history_type: HistoryType::Other,
            last_save_generation: 0,

            active_edit_group: None,
            active_edit_line_info: None,
            active_edit_depth: 0,
            active_edit_off: 0,

            stats: TextBufferStatistics { logical_lines: 1, visual_lines: 1 },
            cursor: Default::default(),
            cursor_for_rendering: None,
            selection: None,
            selection_generation: 0,
            search: None,

            width: 0,
            margin_width: 0,
            margin_enabled: false,
            word_wrap_column: 0,
            word_wrap_enabled: false,
            tab_size: 4,
            indent_with_tabs: false,
            line_highlight_enabled: false,
            ruler: 0,
            encoding: "UTF-8",
            newlines_are_crlf: cfg!(windows), // Windows users want CRLF
            insert_final_newline: false,
            overtype: false,

            wants_cursor_visibility: false,
        })
    }

    /// Length of the document in bytes.
    pub fn text_length(&self) -> usize {
        self.buffer.len()
    }

    /// Number of logical lines in the document,
    /// that is, lines separated by newlines.
    pub fn logical_line_count(&self) -> CoordType {
        self.stats.logical_lines
    }

    /// Number of visual lines in the document,
    /// that is, the number of lines after layout.
    pub fn visual_line_count(&self) -> CoordType {
        self.stats.visual_lines
    }

    /// Does the buffer need to be saved?
    pub fn is_dirty(&self) -> bool {
        self.last_save_generation != self.buffer.generation()
    }

    /// The buffer generation changes on every edit.
    /// With this you can check if it has changed since
    /// the last time you called this function.
    pub fn generation(&self) -> u32 {
        self.buffer.generation()
    }

    /// Force the buffer to be dirty.
    pub fn mark_as_dirty(&mut self) {
        self.last_save_generation = self.buffer.generation().wrapping_sub(1);
    }

    fn mark_as_clean(&mut self) {
        self.last_save_generation = self.buffer.generation();
    }

    /// The encoding used during reading/writing. "UTF-8" is the default.
    pub fn encoding(&self) -> &'static str {
        self.encoding
    }

    /// Set the encoding used during reading/writing.
    pub fn set_encoding(&mut self, encoding: &'static str) {
        if self.encoding != encoding {
            self.encoding = encoding;
            self.mark_as_dirty();
        }
    }

    /// The newline type used in the document. LF or CRLF.
    pub fn is_crlf(&self) -> bool {
        self.newlines_are_crlf
    }

    /// Changes the newline type without normalizing the document.
    pub fn set_crlf(&mut self, crlf: bool) {
        self.newlines_are_crlf = crlf;
    }

    /// Changes the newline type used in the document.
    ///
    /// NOTE: Cannot be undone.
    pub fn normalize_newlines(&mut self, crlf: bool) {
        let newline: &[u8] = if crlf { b"\r\n" } else { b"\n" };
        let mut off = 0;

        let mut cursor_offset = self.cursor.offset;
        let mut cursor_for_rendering_offset =
            self.cursor_for_rendering.map_or(cursor_offset, |c| c.offset);

        #[cfg(debug_assertions)]
        let mut adjusted_newlines = 0;

        'outer: loop {
            // Seek to the offset of the next line start.
            loop {
                let chunk = self.read_forward(off);
                if chunk.is_empty() {
                    break 'outer;
                }

                let (delta, line) = simd::lines_fwd(chunk, 0, 0, 1);
                off += delta;
                if line == 1 {
                    break;
                }
            }

            // Get the preceding newline.
            let chunk = self.read_backward(off);
            let chunk_newline_len = if chunk.ends_with(b"\r\n") { 2 } else { 1 };
            let chunk_newline = &chunk[chunk.len() - chunk_newline_len..];

            if chunk_newline != newline {
                // If this newline is still before our cursor position, then it still has an effect on its offset.
                // Any newline adjustments past that cursor position are irrelevant.
                let delta = newline.len() as isize - chunk_newline_len as isize;
                if off <= cursor_offset {
                    cursor_offset = cursor_offset.saturating_add_signed(delta);
                    #[cfg(debug_assertions)]
                    {
                        adjusted_newlines += 1;
                    }
                }
                if off <= cursor_for_rendering_offset {
                    cursor_for_rendering_offset =
                        cursor_for_rendering_offset.saturating_add_signed(delta);
                }

                // Replace the newline.
                off -= chunk_newline_len;
                self.buffer.replace(off..off + chunk_newline_len, newline);
                off += newline.len();
            }
        }

        // If this fails, the cursor offset calculation above is wrong.
        #[cfg(debug_assertions)]
        debug_assert_eq!(adjusted_newlines, self.cursor.logical_pos.y);

        self.cursor.offset = cursor_offset;
        if let Some(cursor) = &mut self.cursor_for_rendering {
            cursor.offset = cursor_for_rendering_offset;
        }

        self.newlines_are_crlf = crlf;
    }

    /// If enabled, automatically insert a final newline
    /// when typing at the end of the file.
    pub fn set_insert_final_newline(&mut self, enabled: bool) {
        self.insert_final_newline = enabled;
    }

    /// Whether to insert or overtype text when writing.
    pub fn is_overtype(&self) -> bool {
        self.overtype
    }

    /// Set the overtype mode.
    pub fn set_overtype(&mut self, overtype: bool) {
        self.overtype = overtype;
    }

    /// Gets the logical cursor position, that is,
    /// the position in lines and graphemes per line.
    pub fn cursor_logical_pos(&self) -> Point {
        self.cursor.logical_pos
    }

    /// Gets the visual cursor position, that is,
    /// the position in laid out rows and columns.
    pub fn cursor_visual_pos(&self) -> Point {
        self.cursor.visual_pos
    }

    /// Gets the width of the left margin.
    pub fn margin_width(&self) -> CoordType {
        self.margin_width
    }

    /// Is the left margin enabled?
    pub fn set_margin_enabled(&mut self, enabled: bool) -> bool {
        if self.margin_enabled == enabled {
            false
        } else {
            self.margin_enabled = enabled;
            self.reflow();
            true
        }
    }

    /// Gets the width of the text contents for layout.
    pub fn text_width(&self) -> CoordType {
        self.width - self.margin_width
    }

    /// Ask the TUI system to scroll the buffer and make the cursor visible.
    ///
    /// TODO: This function shows that [`TextBuffer`] is poorly abstracted
    /// away from the TUI system. The only reason this exists is so that
    /// if someone outside the TUI code enables word-wrap, the TUI code
    /// recognizes this and scrolls the cursor into view. But outside of this
    /// scrolling, views, etc., are all UI concerns = this should not be here.
    pub fn make_cursor_visible(&mut self) {
        self.wants_cursor_visibility = true;
    }

    /// For the TUI code to retrieve a prior [`TextBuffer::make_cursor_visible()`] request.
    pub fn take_cursor_visibility_request(&mut self) -> bool {
        mem::take(&mut self.wants_cursor_visibility)
    }

    /// Is word-wrap enabled?
    ///
    /// Technically, this is a misnomer, because it's line-wrapping.
    pub fn is_word_wrap_enabled(&self) -> bool {
        self.word_wrap_enabled
    }

    /// Enable or disable word-wrap.
    ///
    /// NOTE: It's expected that the tui code calls `set_width()` sometime after this.
    /// This will then trigger the actual recalculation of the cursor position.
    pub fn set_word_wrap(&mut self, enabled: bool) {
        if self.word_wrap_enabled != enabled {
            self.word_wrap_enabled = enabled;
            self.width = 0; // Force a reflow.
            self.make_cursor_visible();
        }
    }

    /// Set the width available for layout.
    ///
    /// Ideally this would be a pure UI concern, but the text buffer needs this
    /// so that it can abstract away  visual cursor movement such as "go a line up".
    /// What would that even mean if it didn't know how wide a line is?
    pub fn set_width(&mut self, width: CoordType) -> bool {
        if width <= 0 || width == self.width {
            false
        } else {
            self.width = width;
            self.reflow();
            true
        }
    }

    /// Set the tab width. Could be anything, but is expected to be 1-8.
    pub fn tab_size(&self) -> CoordType {
        self.tab_size
    }

    /// Set the tab size. Clamped to 1-8.
    pub fn set_tab_size(&mut self, width: CoordType) -> bool {
        let width = width.clamp(1, 8);
        if width == self.tab_size {
            false
        } else {
            self.tab_size = width;
            self.reflow();
            true
        }
    }

    /// Calculates the amount of spaces a tab key press would insert at the given column.
    /// This also equals the visual width of an actual tab character.
    ///
    /// This exists because Rust doesn't have range constraints yet, and without
    /// them assembly blows up in size by 7x. It's a recurring issue with Rust.
    #[inline]
    fn tab_size_eval(&self, column: CoordType) -> CoordType {
        // SAFETY: `set_tab_size` clamps `self.tab_size` to 1-8.
        unsafe { std::hint::assert_unchecked(self.tab_size >= 1 && self.tab_size <= 8) };
        self.tab_size - (column % self.tab_size)
    }

    /// If the cursor is at an indentation of `column`, this returns
    /// the column to which a backspace key press would delete to.
    #[inline]
    fn tab_size_prev_column(&self, column: CoordType) -> CoordType {
        // SAFETY: `set_tab_size` clamps `self.tab_size` to 1-8.
        unsafe { std::hint::assert_unchecked(self.tab_size >= 1 && self.tab_size <= 8) };
        (column - 1).max(0) / self.tab_size * self.tab_size
    }

    /// Returns whether tabs are used for indentation.
    pub fn indent_with_tabs(&self) -> bool {
        self.indent_with_tabs
    }

    /// Sets whether tabs or spaces are used for indentation.
    pub fn set_indent_with_tabs(&mut self, indent_with_tabs: bool) {
        self.indent_with_tabs = indent_with_tabs;
    }

    /// Sets whether the line the cursor is on should be highlighted.
    pub fn set_line_highlight_enabled(&mut self, enabled: bool) {
        self.line_highlight_enabled = enabled;
    }

    /// Sets a ruler column, e.g. 80.
    pub fn set_ruler(&mut self, column: CoordType) {
        self.ruler = column;
    }

    pub fn reflow(&mut self) {
        self.reflow_internal(true);
    }

    fn recalc_after_content_changed(&mut self) {
        self.reflow_internal(false);
    }

    fn reflow_internal(&mut self, force: bool) {
        let word_wrap_column_before = self.word_wrap_column;

        {
            // +1 onto logical_lines, because line numbers are 1-based.
            // +1 onto log10, because we want the digit width and not the actual log10.
            // +3 onto log10, because we append " | " to the line numbers to form the margin.
            self.margin_width = if self.margin_enabled {
                self.stats.logical_lines.ilog10() as CoordType + 4
            } else {
                0
            };

            let text_width = self.text_width();
            // 2 columns are required, because otherwise wide glyphs wouldn't ever fit.
            self.word_wrap_column =
                if self.word_wrap_enabled && text_width >= 2 { text_width } else { 0 };
        }

        self.cursor_for_rendering = None;

        if force || self.word_wrap_column != word_wrap_column_before {
            // Recalculate the cursor position.
            self.cursor = self.cursor_move_to_logical_internal(
                if self.word_wrap_column > 0 {
                    Default::default()
                } else {
                    self.goto_line_start(self.cursor, self.cursor.logical_pos.y)
                },
                self.cursor.logical_pos,
            );

            // Recalculate the line statistics.
            if self.word_wrap_column > 0 {
                let end = self.cursor_move_to_logical_internal(self.cursor, Point::MAX);
                self.stats.visual_lines = end.visual_pos.y + 1;
            } else {
                self.stats.visual_lines = self.stats.logical_lines;
            }
        }
    }

    /// Replaces the entire buffer contents with the given `text`.
    /// Assumes that the line count doesn't change.
    pub fn copy_from_str(&mut self, text: &dyn ReadableDocument) {
        if self.buffer.copy_from(text) {
            self.recalc_after_content_swap();
            self.cursor_move_to_logical(Point { x: CoordType::MAX, y: 0 });

            let delete = self.buffer.len() - self.cursor.offset;
            if delete != 0 {
                self.buffer.allocate_gap(self.cursor.offset, 0, delete);
            }
        }
    }

    fn recalc_after_content_swap(&mut self) {
        // If the buffer was changed, nothing we previously saved can be relied upon.
        self.undo_stack.clear();
        self.redo_stack.clear();
        self.last_history_type = HistoryType::Other;
        self.cursor = Default::default();
        self.set_selection(None);
        self.mark_as_clean();
        self.reflow();
    }

    /// Copies the contents of the buffer into a string.
    pub fn save_as_string(&mut self, dst: &mut dyn WriteableDocument) {
        self.buffer.copy_into(dst);
        self.mark_as_clean();
    }

    /// Reads a file from disk into the text buffer, detecting encoding and BOM.
    pub fn read_file(
        &mut self,
        file: &mut File,
        encoding: Option<&'static str>,
    ) -> apperr::Result<()> {
        let scratch = scratch_arena(None);
        let mut buf = scratch.alloc_uninit().transpose();
        let mut first_chunk_len = 0;
        let mut read = 0;

        // Read enough bytes to detect the BOM.
        while first_chunk_len < BOM_MAX_LEN {
            read = file_read_uninit(file, &mut buf[first_chunk_len..])?;
            if read == 0 {
                break;
            }
            first_chunk_len += read;
        }

        if let Some(encoding) = encoding {
            self.encoding = encoding;
        } else {
            let bom = detect_bom(unsafe { buf[..first_chunk_len].assume_init_ref() });
            self.encoding = bom.unwrap_or("UTF-8");
        }

        // TODO: Since reading the file can fail, we should ensure that we also reset the cursor here.
        // I don't do it, so that `recalc_after_content_swap()` works.
        self.buffer.clear();

        let done = read == 0;
        if self.encoding == "UTF-8" {
            self.read_file_as_utf8(file, &mut buf, first_chunk_len, done)?;
        } else {
            self.read_file_with_icu(file, &mut buf, first_chunk_len, done)?;
        }

        // Figure out
        // * the logical line count
        // * the newline type (LF or CRLF)
        // * the indentation type (tabs or spaces)
        // * whether there's a final newline
        {
            let chunk = self.read_forward(0);
            let mut offset = 0;
            let mut lines = 0;
            // Number of lines ending in CRLF.
            let mut crlf_count = 0;
            // Number of lines starting with a tab.
            let mut tab_indentations = 0;
            // Number of lines starting with a space.
            let mut space_indentations = 0;
            // Histogram of the indentation depth of lines starting with between 2 and 8 spaces.
            // In other words, `space_indentation_sizes[0]` is the number of lines starting with 2 spaces.
            let mut space_indentation_sizes = [0; 7];

            loop {
                // Check if the line starts with a tab.
                if offset < chunk.len() && chunk[offset] == b'\t' {
                    tab_indentations += 1;
                } else {
                    // Otherwise, check how many spaces the line starts with. Searching for >8 spaces
                    // allows us to reject lines that have more than 1 level of indentation.
                    let space_indentation =
                        chunk[offset..].iter().take(9).take_while(|&&c| c == b' ').count();

                    // We'll also reject lines starting with 1 space, because that's too fickle as a heuristic.
                    if (2..=8).contains(&space_indentation) {
                        space_indentations += 1;

                        // If we encounter an indentation depth of 6, it may either be a 6-space indentation,
                        // two 3-space indentation or 3 2-space indentations. To make this work, we increment
                        // all 3 possible histogram slots.
                        //   2 -> 2
                        //   3 -> 3
                        //   4 -> 4 2
                        //   5 -> 5
                        //   6 -> 6 3 2
                        //   7 -> 7
                        //   8 -> 8 4 2
                        space_indentation_sizes[space_indentation - 2] += 1;
                        if space_indentation & 4 != 0 {
                            space_indentation_sizes[0] += 1;
                        }
                        if space_indentation == 6 || space_indentation == 8 {
                            space_indentation_sizes[space_indentation / 2 - 2] += 1;
                        }
                    }
                }

                (offset, lines) = simd::lines_fwd(chunk, offset, lines, lines + 1);

                // Check if the preceding line ended in CRLF.
                if offset >= 2 && &chunk[offset - 2..offset] == b"\r\n" {
                    crlf_count += 1;
                }

                // We'll limit our heuristics to the first 1000 lines.
                // That should hopefully be enough in practice.
                if offset >= chunk.len() || lines >= 1000 {
                    break;
                }
            }

            // We'll assume CRLF if more than half of the lines end in CRLF.
            let newlines_are_crlf = crlf_count >= lines / 2;

            // We'll assume tabs if there are more lines starting with tabs than with spaces.
            let indent_with_tabs = tab_indentations > space_indentations;
            let tab_size = if indent_with_tabs {
                // Tabs will get a visual size of 4 spaces by default.
                4
            } else {
                // Otherwise, we'll assume the most common indentation depth.
                // If there are conflicting indentation depths, we'll prefer the maximum, because in the loop
                // above we incremented the histogram slot for 2-spaces when encountering 4-spaces and so on.
                let mut max = 1;
                let mut tab_size = 4;
                for (i, &count) in space_indentation_sizes.iter().enumerate() {
                    if count >= max {
                        max = count;
                        tab_size = i as CoordType + 2;
                    }
                }
                tab_size
            };

            // If the file has more than 1000 lines, figure out how many are remaining.
            if offset < chunk.len() {
                (_, lines) = simd::lines_fwd(chunk, offset, lines, CoordType::MAX);
            }

            let final_newline = chunk.ends_with(b"\n");

            // Add 1, because the last line doesn't end in a newline (it ends in the literal end).
            self.stats.logical_lines = lines + 1;
            self.stats.visual_lines = self.stats.logical_lines;
            self.newlines_are_crlf = newlines_are_crlf;
            self.insert_final_newline = final_newline;
            self.indent_with_tabs = indent_with_tabs;
            self.tab_size = tab_size;
        }

        self.recalc_after_content_swap();
        Ok(())
    }

    fn read_file_as_utf8(
        &mut self,
        file: &mut File,
        buf: &mut [MaybeUninit<u8>; 4 * KIBI],
        first_chunk_len: usize,
        done: bool,
    ) -> apperr::Result<()> {
        {
            let mut first_chunk = unsafe { buf[..first_chunk_len].assume_init_ref() };
            if first_chunk.starts_with(b"\xEF\xBB\xBF") {
                first_chunk = &first_chunk[3..];
                self.encoding = "UTF-8 BOM";
            }

            self.buffer.replace(0..0, first_chunk);
        }

        if done {
            return Ok(());
        }

        // If we don't have file metadata, the input may be a pipe or a socket.
        // Every read will have the same size until we hit the end.
        let mut chunk_size = 128 * KIBI;
        let mut extra_chunk_size = 128 * KIBI;

        if let Ok(m) = file.metadata() {
            // Usually the next read of size `chunk_size` will read the entire file,
            // but if the size has changed for some reason, then `extra_chunk_size`
            // should be large enough to read the rest of the file.
            // 4KiB is not too large and not too slow.
            let len = m.len() as usize;
            chunk_size = len.saturating_sub(first_chunk_len);
            extra_chunk_size = 4 * KIBI;
        }

        loop {
            let gap = self.buffer.allocate_gap(self.text_length(), chunk_size, 0);
            if gap.is_empty() {
                break;
            }

            let read = file.read(gap)?;
            if read == 0 {
                break;
            }

            self.buffer.commit_gap(read);
            chunk_size = extra_chunk_size;
        }

        Ok(())
    }

    fn read_file_with_icu(
        &mut self,
        file: &mut File,
        buf: &mut [MaybeUninit<u8>; 4 * KIBI],
        first_chunk_len: usize,
        mut done: bool,
    ) -> apperr::Result<()> {
        let scratch = scratch_arena(None);
        let pivot_buffer = scratch.alloc_uninit_slice(4 * KIBI);
        let mut c = icu::Converter::new(pivot_buffer, self.encoding, "UTF-8")?;
        let mut first_chunk = unsafe { buf[..first_chunk_len].assume_init_ref() };

        while !first_chunk.is_empty() {
            let off = self.text_length();
            let gap = self.buffer.allocate_gap(off, 8 * KIBI, 0);
            let (input_advance, mut output_advance) =
                c.convert(first_chunk, slice_as_uninit_mut(gap))?;

            // Remove the BOM from the file, if this is the first chunk.
            // Our caller ensures to only call us once the BOM has been identified,
            // which means that if there's a BOM it must be wholly contained in this chunk.
            if off == 0 {
                let written = &mut gap[..output_advance];
                if written.starts_with(b"\xEF\xBB\xBF") {
                    written.copy_within(3.., 0);
                    output_advance -= 3;
                }
            }

            self.buffer.commit_gap(output_advance);
            first_chunk = &first_chunk[input_advance..];
        }

        let mut buf_len = 0;

        loop {
            if !done {
                let read = file_read_uninit(file, &mut buf[buf_len..])?;
                buf_len += read;
                done = read == 0;
            }

            let gap = self.buffer.allocate_gap(self.text_length(), 8 * KIBI, 0);
            if gap.is_empty() {
                break;
            }

            let read = unsafe { buf[..buf_len].assume_init_ref() };
            let (input_advance, output_advance) = c.convert(read, slice_as_uninit_mut(gap))?;

            self.buffer.commit_gap(output_advance);

            let flush = done && buf_len == 0;
            buf_len -= input_advance;
            buf.copy_within(input_advance.., 0);

            if flush {
                break;
            }
        }

        Ok(())
    }

    /// Writes the text buffer contents to a file, handling BOM and encoding.
    pub fn write_file(&mut self, file: &mut File) -> apperr::Result<()> {
        let mut offset = 0;

        if self.encoding.starts_with("UTF-8") {
            if self.encoding == "UTF-8 BOM" {
                file.write_all(b"\xEF\xBB\xBF")?;
            }
            loop {
                let chunk = self.read_forward(offset);
                if chunk.is_empty() {
                    break;
                }
                file.write_all(chunk)?;
                offset += chunk.len();
            }
        } else {
            self.write_file_with_icu(file)?;
        }

        self.mark_as_clean();
        Ok(())
    }

    fn write_file_with_icu(&mut self, file: &mut File) -> apperr::Result<()> {
        let scratch = scratch_arena(None);
        let pivot_buffer = scratch.alloc_uninit_slice(4 * KIBI);
        let buf = scratch.alloc_uninit_slice(4 * KIBI);
        let mut c = icu::Converter::new(pivot_buffer, "UTF-8", self.encoding)?;
        let mut offset = 0;

        // Write the BOM for the encodings we know need it.
        if self.encoding.starts_with("UTF-16")
            || self.encoding.starts_with("UTF-32")
            || self.encoding == "GB18030"
        {
            let (_, output_advance) = c.convert(b"\xEF\xBB\xBF", buf)?;
            let chunk = unsafe { buf[..output_advance].assume_init_ref() };
            file.write_all(chunk)?;
        }

        loop {
            let chunk = self.read_forward(offset);
            let (input_advance, output_advance) = c.convert(chunk, buf)?;
            let chunk = unsafe { buf[..output_advance].assume_init_ref() };

            file.write_all(chunk)?;
            offset += input_advance;

            if chunk.is_empty() {
                break;
            }
        }

        Ok(())
    }

    /// Returns the current selection.
    pub fn has_selection(&self) -> bool {
        self.selection.is_some()
    }

    fn set_selection(&mut self, selection: Option<TextBufferSelection>) -> u32 {
        self.selection = selection.filter(|s| s.beg != s.end);
        self.selection_generation = self.selection_generation.wrapping_add(1);
        self.selection_generation
    }

    /// Moves the cursor by `offset` and updates the selection to contain it.
    pub fn selection_update_offset(&mut self, offset: usize) {
        self.set_cursor_for_selection(self.cursor_move_to_offset_internal(self.cursor, offset));
    }

    /// Moves the cursor to `visual_pos` and updates the selection to contain it.
    pub fn selection_update_visual(&mut self, visual_pos: Point) {
        self.set_cursor_for_selection(self.cursor_move_to_visual_internal(self.cursor, visual_pos));
    }

    /// Moves the cursor to `logical_pos` and updates the selection to contain it.
    pub fn selection_update_logical(&mut self, logical_pos: Point) {
        self.set_cursor_for_selection(
            self.cursor_move_to_logical_internal(self.cursor, logical_pos),
        );
    }

    /// Moves the cursor by `delta` and updates the selection to contain it.
    pub fn selection_update_delta(&mut self, granularity: CursorMovement, delta: CoordType) {
        self.set_cursor_for_selection(self.cursor_move_delta_internal(
            self.cursor,
            granularity,
            delta,
        ));
    }

    /// Select the current word.
    pub fn select_word(&mut self) {
        let Range { start, end } = navigation::word_select(&self.buffer, self.cursor.offset);
        let beg = self.cursor_move_to_offset_internal(self.cursor, start);
        let end = self.cursor_move_to_offset_internal(beg, end);
        unsafe { self.set_cursor(end) };
        self.set_selection(Some(TextBufferSelection {
            beg: beg.logical_pos,
            end: end.logical_pos,
        }));
    }

    /// Select the current line.
    pub fn select_line(&mut self) {
        let beg = self.cursor_move_to_logical_internal(
            self.cursor,
            Point { x: 0, y: self.cursor.logical_pos.y },
        );
        let end = self
            .cursor_move_to_logical_internal(beg, Point { x: 0, y: self.cursor.logical_pos.y + 1 });
        unsafe { self.set_cursor(end) };
        self.set_selection(Some(TextBufferSelection {
            beg: beg.logical_pos,
            end: end.logical_pos,
        }));
    }

    /// Select the entire document.
    pub fn select_all(&mut self) {
        let beg = Default::default();
        let end = self.cursor_move_to_logical_internal(beg, Point::MAX);
        unsafe { self.set_cursor(end) };
        self.set_selection(Some(TextBufferSelection {
            beg: beg.logical_pos,
            end: end.logical_pos,
        }));
    }

    /// Starts a new selection, if there's none already.
    pub fn start_selection(&mut self) {
        if self.selection.is_none() {
            self.set_selection(Some(TextBufferSelection {
                beg: self.cursor.logical_pos,
                end: self.cursor.logical_pos,
            }));
        }
    }

    /// Destroy the current selection.
    pub fn clear_selection(&mut self) -> bool {
        let had_selection = self.selection.is_some();
        self.set_selection(None);
        had_selection
    }

    /// Find the next occurrence of the given `pattern` and select it.
    pub fn find_and_select(&mut self, pattern: &str, options: SearchOptions) -> apperr::Result<()> {
        if let Some(search) = &mut self.search {
            let search = search.get_mut();
            // When the search input changes we must reset the search.
            if search.pattern != pattern || search.options != options {
                self.search = None;
            }

            // When transitioning from some search to no search, we must clear the selection.
            if pattern.is_empty()
                && let Some(TextBufferSelection { beg, .. }) = self.selection
            {
                self.cursor_move_to_logical(beg);
            }
        }

        if pattern.is_empty() {
            return Ok(());
        }

        let search = match &self.search {
            Some(search) => unsafe { &mut *search.get() },
            None => {
                let search = self.find_construct_search(pattern, options)?;
                self.search = Some(UnsafeCell::new(search));
                unsafe { &mut *self.search.as_ref().unwrap().get() }
            }
        };

        // If we previously searched through the entire document and found 0 matches,
        // then we can avoid searching again.
        if search.no_matches {
            return Ok(());
        }

        // If the user moved the cursor since the last search, but the needle remained the same,
        // we still need to move the start of the search to the new cursor position.
        let next_search_offset = match self.selection {
            Some(TextBufferSelection { beg, end }) => {
                if self.selection_generation == search.selection_generation {
                    search.next_search_offset
                } else {
                    self.cursor_move_to_logical_internal(self.cursor, beg.min(end)).offset
                }
            }
            _ => self.cursor.offset,
        };

        self.find_select_next(search, next_search_offset, true);
        Ok(())
    }

    /// Find the next occurrence of the given `pattern` and replace it with `replacement`.
    pub fn find_and_replace(
        &mut self,
        pattern: &str,
        options: SearchOptions,
        replacement: &[u8],
    ) -> apperr::Result<()> {
        // Editors traditionally replace the previous search hit, not the next possible one.
        if let (Some(search), Some(..)) = (&self.search, &self.selection) {
            let search = unsafe { &mut *search.get() };
            if search.selection_generation == self.selection_generation {
                let scratch = scratch_arena(None);
                let parsed_replacements =
                    Self::find_parse_replacement(&scratch, &mut *search, replacement);
                let replacement =
                    self.find_fill_replacement(&mut *search, replacement, &parsed_replacements);
                self.write(&replacement, self.cursor, true);
            }
        }

        self.find_and_select(pattern, options)
    }

    /// Find all occurrences of the given `pattern` and replace them with `replacement`.
    pub fn find_and_replace_all(
        &mut self,
        pattern: &str,
        options: SearchOptions,
        replacement: &[u8],
    ) -> apperr::Result<()> {
        let scratch = scratch_arena(None);
        let mut search = self.find_construct_search(pattern, options)?;
        let mut offset = 0;
        let parsed_replacements = Self::find_parse_replacement(&scratch, &mut search, replacement);

        loop {
            self.find_select_next(&mut search, offset, false);
            if !self.has_selection() {
                break;
            }

            let replacement =
                self.find_fill_replacement(&mut search, replacement, &parsed_replacements);
            self.write(&replacement, self.cursor, true);
            offset = self.cursor.offset;
        }

        Ok(())
    }

    fn find_construct_search(
        &self,
        pattern: &str,
        options: SearchOptions,
    ) -> apperr::Result<ActiveSearch> {
        if pattern.is_empty() {
            return Err(apperr::Error::Icu(1)); // U_ILLEGAL_ARGUMENT_ERROR
        }

        let sanitized_pattern = if options.whole_word && options.use_regex {
            Cow::Owned(format!(r"\b(?:{pattern})\b"))
        } else if options.whole_word {
            let mut p = String::with_capacity(pattern.len() + 16);
            p.push_str(r"\b");

            // Escape regex special characters.
            let b = unsafe { p.as_mut_vec() };
            for &byte in pattern.as_bytes() {
                match byte {
                    b'*' | b'?' | b'+' | b'[' | b'(' | b')' | b'{' | b'}' | b'^' | b'$' | b'|'
                    | b'\\' | b'.' => {
                        b.push(b'\\');
                        b.push(byte);
                    }
                    _ => b.push(byte),
                }
            }

            p.push_str(r"\b");
            Cow::Owned(p)
        } else {
            Cow::Borrowed(pattern)
        };

        let mut flags = icu::Regex::MULTILINE;
        if !options.match_case {
            flags |= icu::Regex::CASE_INSENSITIVE;
        }
        if !options.use_regex && !options.whole_word {
            flags |= icu::Regex::LITERAL;
        }

        // Move the start of the search to the start of the selection,
        // or otherwise to the current cursor position.

        let text = unsafe { icu::Text::new(self)? };
        let regex = unsafe { icu::Regex::new(&sanitized_pattern, flags, &text)? };

        Ok(ActiveSearch {
            pattern: pattern.to_string(),
            options,
            text,
            regex,
            buffer_generation: self.buffer.generation(),
            selection_generation: 0,
            next_search_offset: 0,
            no_matches: false,
        })
    }

    fn find_select_next(&mut self, search: &mut ActiveSearch, offset: usize, wrap: bool) {
        if search.buffer_generation != self.buffer.generation() {
            unsafe { search.regex.set_text(&mut search.text, offset) };
            search.buffer_generation = self.buffer.generation();
            search.next_search_offset = offset;
        } else if search.next_search_offset != offset {
            search.next_search_offset = offset;
            search.regex.reset(offset);
        }

        let mut hit = search.regex.next();

        // If we hit the end of the buffer, and we know that there's something to find,
        // start the search again from the beginning (= wrap around).
        if wrap && hit.is_none() && search.next_search_offset != 0 {
            search.next_search_offset = 0;
            search.regex.reset(0);
            hit = search.regex.next();
        }

        search.selection_generation = if let Some(range) = hit {
            // Now the search offset is no more at the start of the buffer.
            search.next_search_offset = range.end;

            let beg = self.cursor_move_to_offset_internal(self.cursor, range.start);
            let end = self.cursor_move_to_offset_internal(beg, range.end);

            unsafe { self.set_cursor(end) };
            self.make_cursor_visible();

            self.set_selection(Some(TextBufferSelection {
                beg: beg.logical_pos,
                end: end.logical_pos,
            }))
        } else {
            // Avoid searching through the entire document again if we know there's nothing to find.
            search.no_matches = true;
            self.set_selection(None)
        };
    }

    fn find_parse_replacement<'a>(
        arena: &'a Arena,
        search: &mut ActiveSearch,
        replacement: &[u8],
    ) -> Vec<RegexReplacement<'a>, &'a Arena> {
        let mut res = Vec::new_in(arena);

        if !search.options.use_regex {
            return res;
        }

        let group_count = search.regex.group_count();
        let mut text = Vec::new_in(arena);
        let mut text_beg = 0;

        loop {
            let mut off = memchr2(b'$', b'\\', replacement, text_beg);

            // Push the raw, unescaped text, if any.
            if text_beg < off {
                text.extend_from_slice(&replacement[text_beg..off]);
            }

            // Unescape any escaped characters.
            while off < replacement.len() && replacement[off] == b'\\' {
                off += 2;

                // If this backslash is the last character (e.g. because
                // `replacement` is just 1 byte long, holding just b"\\"),
                // we can't unescape it. In that case, we map it to `b'\\'` here.
                // This results in us appending a literal backslash to the text.
                let ch = replacement.get(off - 1).map_or(b'\\', |&c| c);

                // Unescape and append the character.
                text.push(match ch {
                    b'n' => b'\n',
                    b'r' => b'\r',
                    b't' => b'\t',
                    ch => ch,
                });
            }

            // Parse out a group number, if any.
            let mut group = -1;
            if off < replacement.len() && replacement[off] == b'$' {
                let mut beg = off;
                let mut end = off + 1;
                let mut acc = 0i32;
                let mut acc_bad = true;

                if end < replacement.len() {
                    let ch = replacement[end];

                    if ch == b'$' {
                        // Translate "$$" to "$".
                        beg += 1;
                        end += 1;
                    } else if ch.is_ascii_digit() {
                        // Parse "$1234" into 1234i32.
                        // If the number is larger than the group count,
                        // we flag `acc_bad` which causes us to treat it as text.
                        acc_bad = false;
                        while {
                            acc =
                                acc.wrapping_mul(10).wrapping_add((replacement[end] - b'0') as i32);
                            acc_bad |= acc > group_count;
                            end += 1;
                            end < replacement.len() && replacement[end].is_ascii_digit()
                        } {}
                    }
                }

                if !acc_bad {
                    group = acc;
                } else {
                    text.extend_from_slice(&replacement[beg..end]);
                }

                off = end;
            }

            if !text.is_empty() {
                res.push(RegexReplacement::Text(text));
                text = Vec::new_in(arena);
            }
            if group >= 0 {
                res.push(RegexReplacement::Group(group));
            }

            text_beg = off;
            if text_beg >= replacement.len() {
                break;
            }
        }

        res
    }

    fn find_fill_replacement<'a>(
        &self,
        search: &mut ActiveSearch,
        replacement: &'a [u8],
        parsed_replacements: &[RegexReplacement],
    ) -> Cow<'a, [u8]> {
        if !search.options.use_regex {
            Cow::Borrowed(replacement)
        } else {
            let mut res = Vec::new();

            for replacement in parsed_replacements {
                match replacement {
                    RegexReplacement::Text(text) => res.extend_from_slice(text),
                    RegexReplacement::Group(group) => {
                        if let Some(range) = search.regex.group(*group) {
                            self.buffer.extract_raw(range, &mut res, usize::MAX);
                        }
                    }
                }
            }

            Cow::Owned(res)
        }
    }

    fn measurement_config(&self) -> MeasurementConfig<'_> {
        MeasurementConfig::new(&self.buffer)
            .with_word_wrap_column(self.word_wrap_column)
            .with_tab_size(self.tab_size)
    }

    fn goto_line_start(&self, cursor: Cursor, y: CoordType) -> Cursor {
        let mut result = cursor;
        let mut seek_to_line_start = true;

        if y > result.logical_pos.y {
            while y > result.logical_pos.y {
                let chunk = self.read_forward(result.offset);
                if chunk.is_empty() {
                    break;
                }

                let (delta, line) = simd::lines_fwd(chunk, 0, result.logical_pos.y, y);
                result.offset += delta;
                result.logical_pos.y = line;
            }

            // If we're at the end of the buffer, we could either be there because the last
            // character in the buffer is genuinely a newline, or because the buffer ends in a
            // line of text without trailing newline. The only way to make sure is to seek
            // backwards to the line start again. But otherwise we can skip that.
            seek_to_line_start =
                result.offset == self.text_length() && result.offset != cursor.offset;
        }

        if seek_to_line_start {
            loop {
                let chunk = self.read_backward(result.offset);
                if chunk.is_empty() {
                    break;
                }

                let (delta, line) = simd::lines_bwd(chunk, chunk.len(), result.logical_pos.y, y);
                result.offset -= chunk.len() - delta;
                result.logical_pos.y = line;
                if delta > 0 {
                    break;
                }
            }
        }

        if result.offset == cursor.offset {
            return result;
        }

        result.logical_pos.x = 0;
        result.visual_pos.x = 0;
        result.visual_pos.y = result.logical_pos.y;
        result.column = 0;
        result.wrap_opp = false;

        if self.word_wrap_column > 0 {
            let upward = result.offset < cursor.offset;
            let (top, bottom) = if upward { (result, cursor) } else { (cursor, result) };

            let mut bottom_remeasured =
                self.measurement_config().with_cursor(top).goto_logical(bottom.logical_pos);

            // The second problem is that visual positions can be ambiguous. A single logical position
            // can map to two visual positions: One at the end of the preceding line in front of
            // a word wrap, and another at the start of the next line after the same word wrap.
            //
            // This, however, only applies if we go upwards, because only then `bottom ≅ cursor`,
            // and thus only then this `bottom` is ambiguous. Otherwise, `bottom ≅ result`
            // and `result` is at a line start which is never ambiguous.
            if upward {
                let a = bottom_remeasured.visual_pos.x;
                let b = bottom.visual_pos.x;
                bottom_remeasured.visual_pos.y = bottom_remeasured.visual_pos.y
                    + (a != 0 && b == 0) as CoordType
                    - (a == 0 && b != 0) as CoordType;
            }

            let mut delta = bottom_remeasured.visual_pos.y - top.visual_pos.y;
            if upward {
                delta = -delta;
            }

            result.visual_pos.y = cursor.visual_pos.y + delta;
        }

        result
    }

    fn cursor_move_to_offset_internal(&self, mut cursor: Cursor, offset: usize) -> Cursor {
        if offset == cursor.offset {
            return cursor;
        }

        // goto_line_start() is fast for seeking across lines _if_ line wrapping is disabled.
        // For backward seeking we have to use it either way, so we're covered there.
        // This implements the forward seeking portion, if it's approx. worth doing so.
        if self.word_wrap_column <= 0 && offset.saturating_sub(cursor.offset) > 1024 {
            // Replacing this with a more optimal, direct memchr() loop appears
            // to improve performance only marginally by another 2% or so.
            // Still, it's kind of "meh" looking at how poorly this is implemented...
            loop {
                let next = self.goto_line_start(cursor, cursor.logical_pos.y + 1);
                // Stop when we either ran past the target offset,
                // or when we hit the end of the buffer and `goto_line_start` backtracked to the line start.
                if next.offset > offset || next.offset <= cursor.offset {
                    break;
                }
                cursor = next;
            }
        }

        while offset < cursor.offset {
            cursor = self.goto_line_start(cursor, cursor.logical_pos.y - 1);
        }

        self.measurement_config().with_cursor(cursor).goto_offset(offset)
    }

    fn cursor_move_to_logical_internal(&self, mut cursor: Cursor, pos: Point) -> Cursor {
        let pos = Point { x: pos.x.max(0), y: pos.y.max(0) };

        if pos == cursor.logical_pos {
            return cursor;
        }

        // goto_line_start() is the fastest way for seeking across lines. As such we always
        // use it if the requested `.y` position is different. We still need to use it if the
        // `.x` position is smaller, but only because `goto_logical()` cannot seek backwards.
        if pos.y != cursor.logical_pos.y || pos.x < cursor.logical_pos.x {
            cursor = self.goto_line_start(cursor, pos.y);
        }

        self.measurement_config().with_cursor(cursor).goto_logical(pos)
    }

    fn cursor_move_to_visual_internal(&self, mut cursor: Cursor, pos: Point) -> Cursor {
        let pos = Point { x: pos.x.max(0), y: pos.y.max(0) };

        if pos == cursor.visual_pos {
            return cursor;
        }

        if self.word_wrap_column <= 0 {
            // Identical to the fast-pass in `cursor_move_to_logical_internal()`.
            if pos.y != cursor.visual_pos.y || pos.x < cursor.visual_pos.x {
                cursor = self.goto_line_start(cursor, pos.y);
            }
        } else {
            // `goto_visual()` can only seek forward, so we need to seek backward here if needed.
            // NOTE that this intentionally doesn't use the `Eq` trait of `Point`, because if
            // `pos.y == cursor.visual_pos.y` we don't need to go to `cursor.logical_pos.y - 1`.
            while pos.y < cursor.visual_pos.y {
                cursor = self.goto_line_start(cursor, cursor.logical_pos.y - 1);
            }
            if pos.y == cursor.visual_pos.y && pos.x < cursor.visual_pos.x {
                cursor = self.goto_line_start(cursor, cursor.logical_pos.y);
            }
        }

        self.measurement_config().with_cursor(cursor).goto_visual(pos)
    }

    fn cursor_move_delta_internal(
        &self,
        mut cursor: Cursor,
        granularity: CursorMovement,
        mut delta: CoordType,
    ) -> Cursor {
        if delta == 0 {
            return cursor;
        }

        let sign = if delta > 0 { 1 } else { -1 };

        match granularity {
            CursorMovement::Grapheme => {
                let start_x = if delta > 0 { 0 } else { CoordType::MAX };

                loop {
                    let target_x = cursor.logical_pos.x + delta;

                    cursor = self.cursor_move_to_logical_internal(
                        cursor,
                        Point { x: target_x, y: cursor.logical_pos.y },
                    );

                    // We can stop if we ran out of remaining delta
                    // (or perhaps ran past the goal; in either case the sign would've changed),
                    // or if we hit the beginning or end of the buffer.
                    delta = target_x - cursor.logical_pos.x;
                    if delta.signum() != sign
                        || (delta < 0 && cursor.offset == 0)
                        || (delta > 0 && cursor.offset >= self.text_length())
                    {
                        break;
                    }

                    cursor = self.cursor_move_to_logical_internal(
                        cursor,
                        Point { x: start_x, y: cursor.logical_pos.y + sign },
                    );

                    // We crossed a newline which counts for 1 grapheme cluster.
                    // So, we also need to run the same check again.
                    delta -= sign;
                    if delta.signum() != sign
                        || cursor.offset == 0
                        || cursor.offset >= self.text_length()
                    {
                        break;
                    }
                }
            }
            CursorMovement::Word => {
                let doc = &self.buffer as &dyn ReadableDocument;
                let mut offset = self.cursor.offset;

                while delta != 0 {
                    if delta < 0 {
                        offset = navigation::word_backward(doc, offset);
                    } else {
                        offset = navigation::word_forward(doc, offset);
                    }
                    delta -= sign;
                }

                cursor = self.cursor_move_to_offset_internal(cursor, offset);
            }
        }

        cursor
    }

    /// Moves the cursor to the given offset.
    pub fn cursor_move_to_offset(&mut self, offset: usize) {
        unsafe { self.set_cursor(self.cursor_move_to_offset_internal(self.cursor, offset)) }
    }

    /// Moves the cursor to the given logical position.
    pub fn cursor_move_to_logical(&mut self, pos: Point) {
        unsafe { self.set_cursor(self.cursor_move_to_logical_internal(self.cursor, pos)) }
    }

    /// Moves the cursor to the given visual position.
    pub fn cursor_move_to_visual(&mut self, pos: Point) {
        unsafe { self.set_cursor(self.cursor_move_to_visual_internal(self.cursor, pos)) }
    }

    /// Moves the cursor by the given delta.
    pub fn cursor_move_delta(&mut self, granularity: CursorMovement, delta: CoordType) {
        unsafe { self.set_cursor(self.cursor_move_delta_internal(self.cursor, granularity, delta)) }
    }

    /// Sets the cursor to the given position, and clears the selection.
    ///
    /// # Safety
    ///
    /// This function performs no checks that the cursor is valid. "Valid" in this case means
    /// that the TextBuffer has not been modified since you received the cursor from this class.
    pub unsafe fn set_cursor(&mut self, cursor: Cursor) {
        self.set_cursor_internal(cursor);
        self.last_history_type = HistoryType::Other;
        self.set_selection(None);
    }

    fn set_cursor_for_selection(&mut self, cursor: Cursor) {
        let beg = match self.selection {
            Some(TextBufferSelection { beg, .. }) => beg,
            None => self.cursor.logical_pos,
        };

        self.set_cursor_internal(cursor);
        self.last_history_type = HistoryType::Other;

        let end = self.cursor.logical_pos;
        self.set_selection(if beg == end { None } else { Some(TextBufferSelection { beg, end }) });
    }

    fn set_cursor_internal(&mut self, cursor: Cursor) {
        debug_assert!(
            cursor.offset <= self.text_length()
                && cursor.logical_pos.x >= 0
                && cursor.logical_pos.y >= 0
                && cursor.logical_pos.y <= self.stats.logical_lines
                && cursor.visual_pos.x >= 0
                && (self.word_wrap_column <= 0 || cursor.visual_pos.x <= self.word_wrap_column)
                && cursor.visual_pos.y >= 0
                && cursor.visual_pos.y <= self.stats.visual_lines
        );
        self.cursor = cursor;
    }

    /// Extracts a rectangular region of the text buffer and writes it to the framebuffer.
    /// The `destination` rect is framebuffer coordinates. The extracted region within this
    /// text buffer has the given `origin` and the same size as the `destination` rect.
    pub fn render(
        &mut self,
        origin: Point,
        destination: Rect,
        focused: bool,
        fb: &mut Framebuffer,
    ) -> Option<RenderResult> {
        if destination.is_empty() {
            return None;
        }

        let scratch = scratch_arena(None);
        let width = destination.width();
        let height = destination.height();
        let line_number_width = self.margin_width.max(3) as usize - 3;
        let text_width = width - self.margin_width;
        let mut visualizer_buf = [0xE2, 0x90, 0x80]; // U+2400 in UTF8
        let mut line = ArenaString::new_in(&scratch);
        let mut visual_pos_x_max = 0;

        // Pick the cursor closer to the `origin.y`.
        let mut cursor = {
            let a = self.cursor;
            let b = self.cursor_for_rendering.unwrap_or_default();
            let da = (a.visual_pos.y - origin.y).abs();
            let db = (b.visual_pos.y - origin.y).abs();
            if da < db { a } else { b }
        };

        let [selection_beg, selection_end] = match self.selection {
            None => [Point::MIN, Point::MIN],
            Some(TextBufferSelection { beg, end }) => minmax(beg, end),
        };

        line.reserve(width as usize * 2);

        for y in 0..height {
            line.clear();

            let visual_line = origin.y + y;
            let mut cursor_beg =
                self.cursor_move_to_visual_internal(cursor, Point { x: origin.x, y: visual_line });
            let cursor_end = self.cursor_move_to_visual_internal(
                cursor_beg,
                Point { x: origin.x + text_width, y: visual_line },
            );

            // Accelerate the next render pass by remembering where we started off.
            if y == 0 {
                self.cursor_for_rendering = Some(cursor_beg);
            }

            if line_number_width != 0 {
                if visual_line >= self.stats.visual_lines {
                    // Past the end of the buffer? Place "    | " in the margin.
                    // Since we know that we won't see line numbers greater than i64::MAX (9223372036854775807)
                    // any time soon, we can use a static string as the template (`MARGIN`) and slice it,
                    // because `line_number_width` can't possibly be larger than 19.
                    let off = 19 - line_number_width;
                    unsafe { std::hint::assert_unchecked(off < MARGIN_TEMPLATE.len()) };
                    line.push_str(&MARGIN_TEMPLATE[off..]);
                } else if self.word_wrap_column <= 0 || cursor_beg.logical_pos.x == 0 {
                    // Regular line? Place "123 | " in the margin.
                    _ = write!(line, "{:1$} │ ", cursor_beg.logical_pos.y + 1, line_number_width);
                } else {
                    // Wrapped line? Place " ... | " in the margin.
                    let number_width = (cursor_beg.logical_pos.y + 1).ilog10() as usize + 1;
                    _ = write!(
                        line,
                        "{0:1$}{0:∙<2$} │ ",
                        "",
                        line_number_width - number_width,
                        number_width
                    );
                    // Blending in the background color will "dim" the indicator dots.
                    let left = destination.left;
                    let top = destination.top + y;
                    fb.blend_fg(
                        Rect {
                            left,
                            top,
                            right: left + line_number_width as CoordType,
                            bottom: top + 1,
                        },
                        fb.indexed_alpha(IndexedColor::Background, 1, 2),
                    );
                }
            }

            let mut selection_off = 0..0;

            // Figure out the selection range on this line, if any.
            if cursor_beg.visual_pos.y == visual_line
                && selection_beg <= cursor_end.logical_pos
                && selection_end >= cursor_beg.logical_pos
            {
                let mut cursor = cursor_beg;

                // By default, we assume the entire line is selected.
                let mut selection_pos_beg = 0;
                let mut selection_pos_end = COORD_TYPE_SAFE_MAX;
                selection_off.start = cursor_beg.offset;
                selection_off.end = cursor_end.offset;

                // The start of the selection is within this line. We need to update selection_beg.
                if selection_beg <= cursor_end.logical_pos
                    && selection_beg >= cursor_beg.logical_pos
                {
                    cursor = self.cursor_move_to_logical_internal(cursor, selection_beg);
                    selection_off.start = cursor.offset;
                    selection_pos_beg = cursor.visual_pos.x;
                }

                // The end of the selection is within this line. We need to update selection_end.
                if selection_end <= cursor_end.logical_pos
                    && selection_end >= cursor_beg.logical_pos
                {
                    cursor = self.cursor_move_to_logical_internal(cursor, selection_end);
                    selection_off.end = cursor.offset;
                    selection_pos_end = cursor.visual_pos.x;
                }

                let left = destination.left + self.margin_width - origin.x;
                let top = destination.top + y;
                let rect = Rect {
                    left: left + selection_pos_beg.max(origin.x),
                    top,
                    right: left + selection_pos_end.min(origin.x + text_width),
                    bottom: top + 1,
                };

                let mut bg = oklab_blend(
                    fb.indexed(IndexedColor::Foreground),
                    fb.indexed_alpha(IndexedColor::BrightBlue, 1, 2),
                );
                if !focused {
                    bg = oklab_blend(bg, fb.indexed_alpha(IndexedColor::Background, 1, 2))
                };
                let fg = fb.contrasted(bg);
                fb.blend_bg(rect, bg);
                fb.blend_fg(rect, fg);
            }

            // Nothing to do if the entire line is empty.
            if cursor_beg.offset != cursor_end.offset {
                // If we couldn't reach the left edge, we may have stopped short due to a wide glyph.
                // In that case we'll try to find the next character and then compute by how many
                // columns it overlaps the left edge (can be anything between 1 and 7).
                if cursor_beg.visual_pos.x < origin.x {
                    let cursor_next = self.cursor_move_to_logical_internal(
                        cursor_beg,
                        Point { x: cursor_beg.logical_pos.x + 1, y: cursor_beg.logical_pos.y },
                    );

                    if cursor_next.visual_pos.x > origin.x {
                        let overlap = cursor_next.visual_pos.x - origin.x;
                        debug_assert!((1..=7).contains(&overlap));
                        line.push_str(&TAB_WHITESPACE[..overlap as usize]);
                        cursor_beg = cursor_next;
                    }
                }

                let mut global_off = cursor_beg.offset;
                let mut cursor_line = cursor_beg;

                while global_off < cursor_end.offset {
                    let chunk = self.read_forward(global_off);
                    let chunk = &chunk[..chunk.len().min(cursor_end.offset - global_off)];
                    let mut it = Utf8Chars::new(chunk, 0);

                    // TODO: Looping char-by-char is bad for performance.
                    // >25% of the total rendering time is spent here.
                    loop {
                        let chunk_off = it.offset();
                        let global_off = global_off + chunk_off;
                        let Some(ch) = it.next() else {
                            break;
                        };

                        if ch == ' ' || ch == '\t' {
                            let is_tab = ch == '\t';
                            let visualize = selection_off.contains(&global_off);
                            let mut whitespace = TAB_WHITESPACE;
                            let mut prefix_add = 0;

                            if is_tab || visualize {
                                // We need the character's visual position in order to either compute the tab size,
                                // or set the foreground color of the visualizer, respectively.
                                // TODO: Doing this char-by-char is of course also bad for performance.
                                cursor_line =
                                    self.cursor_move_to_offset_internal(cursor_line, global_off);
                            }

                            let tab_size =
                                if is_tab { self.tab_size_eval(cursor_line.column) } else { 1 };

                            if visualize {
                                // If the whitespace is part of the selection,
                                // we replace " " with "･" and "\t" with "￫".
                                (whitespace, prefix_add) = if is_tab {
                                    (VISUAL_TAB, VISUAL_TAB_PREFIX_ADD)
                                } else {
                                    (VISUAL_SPACE, VISUAL_SPACE_PREFIX_ADD)
                                };

                                // Make the visualized characters slightly gray.
                                let visualizer_rect = {
                                    let left = destination.left
                                        + self.margin_width
                                        + cursor_line.visual_pos.x
                                        - origin.x;
                                    let top = destination.top + cursor_line.visual_pos.y - origin.y;
                                    Rect { left, top, right: left + 1, bottom: top + 1 }
                                };
                                fb.blend_fg(
                                    visualizer_rect,
                                    fb.indexed_alpha(IndexedColor::Foreground, 1, 2),
                                );
                            }

                            line.push_str(&whitespace[..prefix_add + tab_size as usize]);
                        } else if ch <= '\x1f' || ('\u{7f}'..='\u{9f}').contains(&ch) {
                            // Append a Unicode representation of the C0 or C1 control character.
                            visualizer_buf[2] = if ch <= '\x1f' {
                                0x80 | ch as u8 // U+2400..=U+241F
                            } else if ch == '\x7f' {
                                0xA1 // U+2421
                            } else {
                                0xA6 // U+2426, because there are no pictures for C1 control characters.
                            };

                            // Our manually constructed UTF8 is never going to be invalid. Trust.
                            line.push_str(unsafe { str::from_utf8_unchecked(&visualizer_buf) });

                            // Highlight the control character yellow.
                            cursor_line =
                                self.cursor_move_to_offset_internal(cursor_line, global_off);
                            let visualizer_rect = {
                                let left =
                                    destination.left + self.margin_width + cursor_line.visual_pos.x
                                        - origin.x;
                                let top = destination.top + cursor_line.visual_pos.y - origin.y;
                                Rect { left, top, right: left + 1, bottom: top + 1 }
                            };
                            let bg = fb.indexed(IndexedColor::Yellow);
                            let fg = fb.contrasted(bg);
                            fb.blend_bg(visualizer_rect, bg);
                            fb.blend_fg(visualizer_rect, fg);
                        } else {
                            line.push(ch);
                        }
                    }

                    global_off += chunk.len();
                }

                visual_pos_x_max = visual_pos_x_max.max(cursor_end.visual_pos.x);
            }

            fb.replace_text(destination.top + y, destination.left, destination.right, &line);

            cursor = cursor_end;
        }

        // Colorize the margin that we wrote above.
        if self.margin_width > 0 {
            let margin = Rect {
                left: destination.left,
                top: destination.top,
                right: destination.left + self.margin_width,
                bottom: destination.bottom,
            };
            fb.blend_fg(margin, 0x7f3f3f3f);
        }

        if self.ruler > 0 {
            let left = destination.left + self.margin_width + (self.ruler - origin.x).max(0);
            let right = destination.right;
            if left < right {
                fb.blend_bg(
                    Rect { left, top: destination.top, right, bottom: destination.bottom },
                    fb.indexed_alpha(IndexedColor::BrightRed, 1, 4),
                );
            }
        }

        if focused {
            let mut x = self.cursor.visual_pos.x;
            let mut y = self.cursor.visual_pos.y;

            if self.word_wrap_column > 0 && x >= self.word_wrap_column {
                // The line the cursor is on wraps exactly on the word wrap column which
                // means the cursor is invisible. We need to move it to the next line.
                x = 0;
                y += 1;
            }

            // Move the cursor into screen space.
            x += destination.left - origin.x + self.margin_width;
            y += destination.top - origin.y;

            let cursor = Point { x, y };
            let text = Rect {
                left: destination.left + self.margin_width,
                top: destination.top,
                right: destination.right,
                bottom: destination.bottom,
            };

            if text.contains(cursor) {
                fb.set_cursor(cursor, self.overtype);

                if self.line_highlight_enabled && selection_beg >= selection_end {
                    fb.blend_bg(
                        Rect {
                            left: destination.left,
                            top: cursor.y,
                            right: destination.right,
                            bottom: cursor.y + 1,
                        },
                        0x50282828,
                    );
                }
            }
        }

        Some(RenderResult { visual_pos_x_max })
    }

    pub fn cut(&mut self, clipboard: &mut Clipboard) {
        self.cut_copy(clipboard, true);
    }

    pub fn copy(&mut self, clipboard: &mut Clipboard) {
        self.cut_copy(clipboard, false);
    }

    fn cut_copy(&mut self, clipboard: &mut Clipboard, cut: bool) {
        let line_copy = !self.has_selection();
        let selection = self.extract_selection(cut);
        clipboard.write(selection);
        clipboard.write_was_line_copy(line_copy);
    }

    pub fn paste(&mut self, clipboard: &Clipboard) {
        let data = clipboard.read();
        if data.is_empty() {
            return;
        }

        let pos = self.cursor_logical_pos();
        let at = if clipboard.is_line_copy() {
            self.goto_line_start(self.cursor, pos.y)
        } else {
            self.cursor
        };

        self.write(data, at, true);

        if clipboard.is_line_copy() {
            self.cursor_move_to_logical(Point { x: pos.x, y: pos.y + 1 });
        }
    }

    /// Inserts the user input `text` at the current cursor position.
    /// Replaces tabs with whitespace if needed, etc.
    pub fn write_canon(&mut self, text: &[u8]) {
        self.write(text, self.cursor, false);
    }

    /// Inserts `text` as-is at the current cursor position.
    /// The only transformation applied is that newlines are normalized.
    pub fn write_raw(&mut self, text: &[u8]) {
        self.write(text, self.cursor, true);
    }

    fn write(&mut self, text: &[u8], at: Cursor, raw: bool) {
        let history_type = if raw { HistoryType::Other } else { HistoryType::Write };
        let mut edit_begun = false;

        // If we have an active selection, writing an empty `text`
        // will still delete the selection. As such, we check this first.
        if let Some((beg, end)) = self.selection_range_internal(false) {
            self.edit_begin(history_type, beg);
            self.edit_delete(end);
            self.set_selection(None);
            edit_begun = true;
        }

        // If the text is empty the remaining code won't do anything,
        // allowing us to exit early.
        if text.is_empty() {
            // ...we still need to end any active edit session though.
            if edit_begun {
                self.edit_end();
            }
            return;
        }

        if !edit_begun {
            self.edit_begin(history_type, at);
        }

        let mut offset = 0;
        let scratch = scratch_arena(None);
        let mut newline_buffer = ArenaString::new_in(&scratch);

        loop {
            // Can't use `unicode::newlines_forward` because bracketed paste uses CR instead of LF/CRLF.
            let offset_next = memchr2(b'\r', b'\n', text, offset);
            let line = &text[offset..offset_next];
            let column_before = self.cursor.logical_pos.x;

            // Write the contents of the line into the buffer.
            let mut line_off = 0;
            while line_off < line.len() {
                // Split the line into chunks of non-tabs and tabs.
                let mut plain = line;
                if !raw && !self.indent_with_tabs {
                    let end = memchr2(b'\t', b'\t', line, line_off);
                    plain = &line[line_off..end];
                }

                // Non-tabs are written as-is, because the outer loop already handles newline translation.
                self.edit_write(plain);
                line_off += plain.len();

                // Now replace tabs with spaces.
                while line_off < line.len() && line[line_off] == b'\t' {
                    let spaces = self.tab_size_eval(self.cursor.column);
                    let spaces = &TAB_WHITESPACE.as_bytes()[..spaces as usize];
                    self.edit_write(spaces);
                    line_off += 1;
                }
            }

            if !raw && self.overtype {
                let delete = self.cursor.logical_pos.x - column_before;
                let end = self.cursor_move_to_logical_internal(
                    self.cursor,
                    Point { x: self.cursor.logical_pos.x + delete, y: self.cursor.logical_pos.y },
                );
                self.edit_delete(end);
            }

            offset += line.len();
            if offset >= text.len() {
                break;
            }

            // First, write the newline.
            newline_buffer.clear();
            newline_buffer.push_str(if self.newlines_are_crlf { "\r\n" } else { "\n" });

            if !raw {
                // We'll give the next line the same indentation as the previous one.
                // This block figures out how much that is. We can't reuse that value,
                // because "  a\n  a\n" should give the 3rd line a total indentation of 4.
                // Assuming your terminal has bracketed paste, this won't be a concern though.
                // (If it doesn't, use a different terminal.)
                let line_beg = self.goto_line_start(self.cursor, self.cursor.logical_pos.y);
                let limit = self.cursor.offset;
                let mut off = line_beg.offset;
                let mut newline_indentation = 0;

                'outer: while off < limit {
                    let chunk = self.read_forward(off);
                    let chunk = &chunk[..chunk.len().min(limit - off)];

                    for &c in chunk {
                        if c == b' ' {
                            newline_indentation += 1;
                        } else if c == b'\t' {
                            newline_indentation += self.tab_size_eval(newline_indentation);
                        } else {
                            break 'outer;
                        }
                    }

                    off += chunk.len();
                }

                // If tabs are enabled, add as many tabs as we can.
                if self.indent_with_tabs {
                    let tab_count = newline_indentation / self.tab_size;
                    newline_buffer.push_repeat('\t', tab_count as usize);
                    newline_indentation -= tab_count * self.tab_size;
                }

                // If tabs are disabled, or if the indentation wasn't a multiple of the tab size,
                // add spaces to make up the difference.
                newline_buffer.push_repeat(' ', newline_indentation as usize);
            }

            self.edit_write(newline_buffer.as_bytes());

            // Skip one CR/LF/CRLF.
            if offset >= text.len() {
                break;
            }
            if text[offset] == b'\r' {
                offset += 1;
            }
            if offset >= text.len() {
                break;
            }
            if text[offset] == b'\n' {
                offset += 1;
            }
            if offset >= text.len() {
                break;
            }
        }

        // POSIX mandates that all valid lines end in a newline.
        // This isn't all that common on Windows and so we have
        // `self.final_newline` to control this.
        //
        // In order to not annoy people with this, we only add a
        // newline if you just edited the very end of the buffer.
        if self.insert_final_newline
            && self.cursor.offset > 0
            && self.cursor.offset == self.text_length()
            && self.cursor.logical_pos.x > 0
        {
            let cursor = self.cursor;
            self.edit_write(if self.newlines_are_crlf { b"\r\n" } else { b"\n" });
            self.set_cursor_internal(cursor);
        }

        self.edit_end();
    }

    /// Deletes 1 grapheme cluster from the buffer.
    /// `cursor_movements` is expected to be -1 for backspace and 1 for delete.
    /// If there's a current selection, it will be deleted and `cursor_movements` ignored.
    /// The selection is cleared after the call.
    /// Deletes characters from the buffer based on a delta from the cursor.
    pub fn delete(&mut self, granularity: CursorMovement, delta: CoordType) {
        if delta == 0 {
            return;
        }

        let mut beg;
        let mut end;

        if let Some(r) = self.selection_range_internal(false) {
            (beg, end) = r;
        } else {
            if (delta < 0 && self.cursor.offset == 0)
                || (delta > 0 && self.cursor.offset >= self.text_length())
            {
                // Nothing to delete.
                return;
            }

            beg = self.cursor;
            end = self.cursor_move_delta_internal(beg, granularity, delta);
            if beg.offset == end.offset {
                return;
            }
            if beg.offset > end.offset {
                mem::swap(&mut beg, &mut end);
            }
        }

        self.edit_begin(HistoryType::Delete, beg);
        self.edit_delete(end);
        self.edit_end();

        self.set_selection(None);
    }

    /// Returns the logical position of the first character on this line.
    /// Return `.x == 0` if there are no non-whitespace characters.
    pub fn indent_end_logical_pos(&self) -> Point {
        let cursor = self.goto_line_start(self.cursor, self.cursor.logical_pos.y);
        let (chars, _) = self.measure_indent_internal(cursor.offset, CoordType::MAX);
        Point { x: chars, y: cursor.logical_pos.y }
    }

    /// Indents/unindents the current selection or line.
    pub fn indent_change(&mut self, direction: CoordType) {
        let selection = self.selection;
        let mut selection_beg = self.cursor.logical_pos;
        let mut selection_end = selection_beg;

        if let Some(TextBufferSelection { beg, end }) = &selection {
            selection_beg = *beg;
            selection_end = *end;
        }

        if direction >= 0 && self.selection.is_none_or(|sel| sel.beg.y == sel.end.y) {
            self.write_canon(b"\t");
            return;
        }

        self.edit_begin_grouping();

        for y in selection_beg.y.min(selection_end.y)..=selection_beg.y.max(selection_end.y) {
            self.cursor_move_to_logical(Point { x: 0, y });

            let line_start_offset = self.cursor.offset;
            let (curr_chars, curr_columns) =
                self.measure_indent_internal(line_start_offset, CoordType::MAX);

            self.cursor_move_to_logical(Point { x: curr_chars, y: self.cursor.logical_pos.y });

            let delta;

            if direction < 0 {
                // Unindent the line. If there's no indentation, skip.
                if curr_columns <= 0 {
                    continue;
                }

                let (prev_chars, _) = self.measure_indent_internal(
                    line_start_offset,
                    self.tab_size_prev_column(curr_columns),
                );

                delta = prev_chars - curr_chars;
                self.delete(CursorMovement::Grapheme, delta);
            } else {
                // Indent the line. `self.cursor` is already at the level of indentation.
                delta = self.tab_size_eval(curr_columns);
                self.write_canon(b"\t");
            }

            // As the lines get unindented, the selection should shift with them.
            if y == selection_beg.y {
                selection_beg.x += delta;
            }
            if y == selection_end.y {
                selection_end.x += delta;
            }
        }
        self.edit_end_grouping();

        // Move the cursor to the new end of the selection.
        self.set_cursor_internal(self.cursor_move_to_logical_internal(self.cursor, selection_end));

        // NOTE: If the selection was previously `None`,
        // it should continue to be `None` after this.
        self.set_selection(
            selection.map(|_| TextBufferSelection { beg: selection_beg, end: selection_end }),
        );
    }

    fn measure_indent_internal(
        &self,
        mut offset: usize,
        max_columns: CoordType,
    ) -> (CoordType, CoordType) {
        let mut chars = 0;
        let mut columns = 0;

        'outer: loop {
            let chunk = self.read_forward(offset);
            if chunk.is_empty() {
                break;
            }

            for &c in chunk {
                let next = match c {
                    b' ' => columns + 1,
                    b'\t' => columns + self.tab_size_eval(columns),
                    _ => break 'outer,
                };
                if next > max_columns {
                    break 'outer;
                }
                chars += 1;
                columns = next;
            }

            offset += chunk.len();

            // No need to do another round if we
            // already got the exact right amount.
            if columns >= max_columns {
                break;
            }
        }

        (chars, columns)
    }

    /// Displaces the current, cursor or the selection, line(s) in the given direction.
    pub fn move_selected_lines(&mut self, direction: MoveLineDirection) {
        let selection = self.selection;
        let cursor = self.cursor;

        // If there's no selection, we move the line the cursor is on instead.
        let [beg, end] = match self.selection {
            Some(s) => minmax(s.beg.y, s.end.y),
            None => [cursor.logical_pos.y, cursor.logical_pos.y],
        };

        // Check if this would be a no-op.
        if match direction {
            MoveLineDirection::Up => beg <= 0,
            MoveLineDirection::Down => end >= self.stats.logical_lines - 1,
        } {
            return;
        }

        let delta = match direction {
            MoveLineDirection::Up => -1,
            MoveLineDirection::Down => 1,
        };
        let (cut, paste) = match direction {
            MoveLineDirection::Up => (beg - 1, end),
            MoveLineDirection::Down => (end + 1, beg),
        };

        self.edit_begin_grouping();
        {
            // Let's say this is `MoveLineDirection::Up`.
            // In that case, we'll cut (remove) the line above the selection here...
            self.cursor_move_to_logical(Point { x: 0, y: cut });
            let line = self.extract_selection(true);

            // ...and paste it below the selection. This will then
            // appear to the user as if the selection was moved up.
            self.cursor_move_to_logical(Point { x: 0, y: paste });
            self.edit_begin(HistoryType::Write, self.cursor);
            // The `extract_selection` call can return an empty `Vec`),
            // if the `cut` line was at the end of the file. Since we want to
            // paste the line somewhere it needs a trailing newline at the minimum.
            //
            // Similarly, if the `paste` line is at the end of the file
            // and there's no trailing newline, we'll have failed to reach
            // that end in which case `logical_pos.y != past`.
            if line.is_empty() || self.cursor.logical_pos.y != paste {
                self.write_canon(b"\n");
            }
            if !line.is_empty() {
                self.write_raw(&line);
            }
            self.edit_end();
        }
        self.edit_end_grouping();

        // Shift the cursor and selection together with the moved lines.
        self.cursor_move_to_logical(Point {
            x: cursor.logical_pos.x,
            y: cursor.logical_pos.y + delta,
        });
        self.set_selection(selection.map(|mut s| {
            s.beg.y += delta;
            s.end.y += delta;
            s
        }));
    }

    /// Extracts the contents of the current selection.
    /// May optionally delete it, if requested. This is meant to be used for Ctrl+X.
    fn extract_selection(&mut self, delete: bool) -> Vec<u8> {
        let line_copy = !self.has_selection();
        let Some((beg, end)) = self.selection_range_internal(true) else {
            return Vec::new();
        };

        let mut out = Vec::new();
        self.buffer.extract_raw(beg.offset..end.offset, &mut out, 0);

        if delete && !out.is_empty() {
            self.edit_begin(HistoryType::Delete, beg);
            self.edit_delete(end);
            self.edit_end();
            self.set_selection(None);
        }

        // Line copies (= Ctrl+C when there's no selection) always end with a newline.
        if line_copy && !out.ends_with(b"\n") {
            out.replace_range(out.len().., if self.newlines_are_crlf { b"\r\n" } else { b"\n" });
        }

        out
    }

    /// Extracts the contents of the current selection the user made.
    /// This differs from [`TextBuffer::extract_selection()`] in that
    /// it does nothing if the selection was made by searching.
    pub fn extract_user_selection(&mut self, delete: bool) -> Option<Vec<u8>> {
        if !self.has_selection() {
            return None;
        }

        if let Some(search) = &self.search {
            let search = unsafe { &*search.get() };
            if search.selection_generation == self.selection_generation {
                return None;
            }
        }

        Some(self.extract_selection(delete))
    }

    /// Returns the current selection anchors, or `None` if there
    /// is no selection. The returned logical positions are sorted.
    pub fn selection_range(&self) -> Option<(Cursor, Cursor)> {
        self.selection_range_internal(false)
    }

    /// Returns the current selection anchors.
    ///
    /// If there's no selection and `line_fallback` is `true`,
    /// the start/end of the current line are returned.
    /// This is meant to be used for Ctrl+C / Ctrl+X.
    fn selection_range_internal(&self, line_fallback: bool) -> Option<(Cursor, Cursor)> {
        let [beg, end] = match self.selection {
            None if !line_fallback => return None,
            None => [
                Point { x: 0, y: self.cursor.logical_pos.y },
                Point { x: 0, y: self.cursor.logical_pos.y + 1 },
            ],
            Some(TextBufferSelection { beg, end }) => minmax(beg, end),
        };

        let beg = self.cursor_move_to_logical_internal(self.cursor, beg);
        let end = self.cursor_move_to_logical_internal(beg, end);

        if beg.offset < end.offset { Some((beg, end)) } else { None }
    }

    fn edit_begin_grouping(&mut self) {
        self.active_edit_group = Some(ActiveEditGroupInfo {
            cursor_before: self.cursor.logical_pos,
            selection_before: self.selection,
            stats_before: self.stats,
            generation_before: self.buffer.generation(),
        });
    }

    fn edit_end_grouping(&mut self) {
        self.active_edit_group = None;
    }

    /// Starts a new edit operation.
    /// This is used for tracking the undo/redo history.
    fn edit_begin(&mut self, history_type: HistoryType, cursor: Cursor) {
        self.active_edit_depth += 1;
        if self.active_edit_depth > 1 {
            return;
        }

        let cursor_before = self.cursor;
        self.set_cursor_internal(cursor);

        // If both the last and this are a Write/Delete operation, we skip allocating a new undo history item.
        if cursor_before.offset != cursor.offset
            || history_type != self.last_history_type
            || !matches!(history_type, HistoryType::Write | HistoryType::Delete)
        {
            self.redo_stack.clear();
            while self.undo_stack.len() > 1000 {
                self.undo_stack.pop_front();
            }

            self.last_history_type = history_type;
            self.undo_stack.push_back(SemiRefCell::new(HistoryEntry {
                cursor_before: cursor_before.logical_pos,
                selection_before: self.selection,
                stats_before: self.stats,
                generation_before: self.buffer.generation(),
                cursor: cursor.logical_pos,
                deleted: Vec::new(),
                added: Vec::new(),
            }));

            if let Some(info) = &self.active_edit_group
                && let Some(entry) = self.undo_stack.back()
            {
                let mut entry = entry.borrow_mut();
                entry.cursor_before = info.cursor_before;
                entry.selection_before = info.selection_before;
                entry.stats_before = info.stats_before;
                entry.generation_before = info.generation_before;
            }
        }

        self.active_edit_off = cursor.offset;

        // If word-wrap is enabled, the visual layout of all logical lines affected by the write
        // may have changed. This includes even text before the insertion point up to the line
        // start, because this write may have joined with a word before the initial cursor.
        // See other uses of `word_wrap_cursor_next_line` in this function.
        if self.word_wrap_column > 0 {
            let safe_start = self.goto_line_start(cursor, cursor.logical_pos.y);
            let next_line = self.cursor_move_to_logical_internal(
                cursor,
                Point { x: 0, y: cursor.logical_pos.y + 1 },
            );
            self.active_edit_line_info = Some(ActiveEditLineInfo {
                safe_start,
                line_height_in_rows: next_line.visual_pos.y - safe_start.visual_pos.y,
                distance_next_line_start: next_line.offset - cursor.offset,
            });
        }
    }

    /// Writes `text` into the buffer at the current cursor position.
    /// It records the change in the undo stack.
    fn edit_write(&mut self, text: &[u8]) {
        let logical_y_before = self.cursor.logical_pos.y;

        // Copy the written portion into the undo entry.
        {
            let mut undo = self.undo_stack.back_mut().unwrap().borrow_mut();
            undo.added.extend_from_slice(text);
        }

        // Write!
        self.buffer.replace(self.active_edit_off..self.active_edit_off, text);

        // Move self.cursor to the end of the newly written text. Can't use `self.set_cursor_internal`,
        // because we're still in the progress of recalculating the line stats.
        self.active_edit_off += text.len();
        self.cursor = self.cursor_move_to_offset_internal(self.cursor, self.active_edit_off);
        self.stats.logical_lines += self.cursor.logical_pos.y - logical_y_before;
    }

    /// Deletes the text between the current cursor position and `to`.
    /// It records the change in the undo stack.
    fn edit_delete(&mut self, to: Cursor) {
        debug_assert!(to.offset >= self.active_edit_off);

        let logical_y_before = self.cursor.logical_pos.y;
        let off = self.active_edit_off;
        let mut out_off = usize::MAX;

        let mut undo = self.undo_stack.back_mut().unwrap().borrow_mut();

        // If this is a continued backspace operation,
        // we need to prepend the deleted portion to the undo entry.
        if self.cursor.logical_pos < undo.cursor {
            out_off = 0;
            undo.cursor = self.cursor.logical_pos;
        }

        // Copy the deleted portion into the undo entry.
        let deleted = &mut undo.deleted;
        self.buffer.extract_raw(off..to.offset, deleted, out_off);

        // Delete the portion from the buffer by enlarging the gap.
        let count = to.offset - off;
        self.buffer.allocate_gap(off, 0, count);

        self.stats.logical_lines += logical_y_before - to.logical_pos.y;
    }

    /// Finalizes the current edit operation
    /// and recalculates the line statistics.
    fn edit_end(&mut self) {
        self.active_edit_depth -= 1;
        debug_assert!(self.active_edit_depth >= 0);
        if self.active_edit_depth > 0 {
            return;
        }

        #[cfg(debug_assertions)]
        {
            let entry = self.undo_stack.back_mut().unwrap().borrow_mut();
            debug_assert!(!entry.deleted.is_empty() || !entry.added.is_empty());
        }

        if let Some(info) = self.active_edit_line_info.take() {
            let deleted_count = self.undo_stack.back_mut().unwrap().borrow_mut().deleted.len();
            let target = self.cursor.logical_pos;

            // From our safe position we can measure the actual visual position of the cursor.
            self.set_cursor_internal(self.cursor_move_to_logical_internal(info.safe_start, target));

            // If content is added at the insertion position, that's not a problem:
            // We can just remeasure the height of this one line and calculate the delta.
            // `deleted_count` is 0 in this case.
            //
            // The problem is when content is deleted, because it may affect lines
            // beyond the end of the `next_line`. In that case we have to measure
            // the entire buffer contents until the end to compute `self.stats.visual_lines`.
            if deleted_count < info.distance_next_line_start {
                // Now we can measure how many more visual rows this logical line spans.
                let next_line = self
                    .cursor_move_to_logical_internal(self.cursor, Point { x: 0, y: target.y + 1 });
                let lines_before = info.line_height_in_rows;
                let lines_after = next_line.visual_pos.y - info.safe_start.visual_pos.y;
                self.stats.visual_lines += lines_after - lines_before;
            } else {
                let end = self.cursor_move_to_logical_internal(self.cursor, Point::MAX);
                self.stats.visual_lines = end.visual_pos.y + 1;
            }
        } else {
            // If word-wrap is disabled the visual line count always matches the logical one.
            self.stats.visual_lines = self.stats.logical_lines;
        }

        self.recalc_after_content_changed();
    }

    /// Undo the last edit operation.
    pub fn undo(&mut self) {
        self.undo_redo(true);
    }

    /// Redo the last undo operation.
    pub fn redo(&mut self) {
        self.undo_redo(false);
    }

    fn undo_redo(&mut self, undo: bool) {
        let buffer_generation = self.buffer.generation();
        let mut entry_buffer_generation = None;

        loop {
            // Transfer the last entry from the undo stack to the redo stack or vice versa.
            {
                let (from, to) = if undo {
                    (&mut self.undo_stack, &mut self.redo_stack)
                } else {
                    (&mut self.redo_stack, &mut self.undo_stack)
                };

                if let Some(g) = entry_buffer_generation
                    && from.back().is_none_or(|c| c.borrow().generation_before != g)
                {
                    break;
                }

                let Some(list) = from.cursor_back_mut().remove_current_as_list() else {
                    break;
                };

                to.cursor_back_mut().splice_after(list);
            }

            let change = {
                let to = if undo { &self.redo_stack } else { &self.undo_stack };
                to.back().unwrap()
            };

            // Remember the buffer generation of the change so we can stop popping undos/redos.
            // Also, move to the point where the modification took place.
            let cursor = {
                let change = change.borrow();
                entry_buffer_generation = Some(change.generation_before);
                self.cursor_move_to_logical_internal(self.cursor, change.cursor)
            };

            let safe_cursor = if self.word_wrap_column > 0 {
                // If word-wrap is enabled, we need to move the cursor to the beginning of the line.
                // This is because the undo/redo operation may have changed the visual position of the cursor.
                self.goto_line_start(cursor, cursor.logical_pos.y)
            } else {
                cursor
            };

            {
                let mut change = change.borrow_mut();
                let change = &mut *change;

                // Undo: Whatever was deleted is now added and vice versa.
                mem::swap(&mut change.deleted, &mut change.added);

                // Delete the inserted portion.
                self.buffer.allocate_gap(cursor.offset, 0, change.deleted.len());

                // Reinsert the deleted portion.
                {
                    let added = &change.added[..];
                    let mut beg = 0;
                    let mut offset = cursor.offset;

                    while beg < added.len() {
                        let (end, line) = simd::lines_fwd(added, beg, 0, 1);
                        let has_newline = line != 0;
                        let link = &added[beg..end];
                        let line = unicode::strip_newline(link);
                        let mut written;

                        {
                            let gap = self.buffer.allocate_gap(offset, line.len() + 2, 0);
                            written = slice_copy_safe(gap, line);

                            if has_newline {
                                if self.newlines_are_crlf && written < gap.len() {
                                    gap[written] = b'\r';
                                    written += 1;
                                }
                                if written < gap.len() {
                                    gap[written] = b'\n';
                                    written += 1;
                                }
                            }

                            self.buffer.commit_gap(written);
                        }

                        beg = end;
                        offset += written;
                    }
                }

                // Restore the previous line statistics.
                mem::swap(&mut self.stats, &mut change.stats_before);

                // Restore the previous selection.
                mem::swap(&mut self.selection, &mut change.selection_before);

                // Pretend as if the buffer was never modified.
                self.buffer.set_generation(change.generation_before);
                change.generation_before = buffer_generation;

                // Restore the previous cursor.
                let cursor_before =
                    self.cursor_move_to_logical_internal(safe_cursor, change.cursor_before);
                change.cursor_before = self.cursor.logical_pos;
                // Can't use `set_cursor_internal` here, because we haven't updated the line stats yet.
                self.cursor = cursor_before;

                if self.undo_stack.is_empty() {
                    self.last_history_type = HistoryType::Other;
                }
            }
        }

        if entry_buffer_generation.is_some() {
            self.recalc_after_content_changed();
        }
    }

    /// For interfacing with ICU.
    pub(crate) fn read_backward(&self, off: usize) -> &[u8] {
        self.buffer.read_backward(off)
    }

    /// For interfacing with ICU.
    pub fn read_forward(&self, off: usize) -> &[u8] {
        self.buffer.read_forward(off)
    }
}

pub enum Bom {
    None,
    UTF8,
    UTF16LE,
    UTF16BE,
    UTF32LE,
    UTF32BE,
    GB18030,
}

const BOM_MAX_LEN: usize = 4;

fn detect_bom(bytes: &[u8]) -> Option<&'static str> {
    if bytes.len() >= 4 {
        if bytes.starts_with(b"\xFF\xFE\x00\x00") {
            return Some("UTF-32LE");
        }
        if bytes.starts_with(b"\x00\x00\xFE\xFF") {
            return Some("UTF-32BE");
        }
        if bytes.starts_with(b"\x84\x31\x95\x33") {
            return Some("GB18030");
        }
    }
    if bytes.len() >= 3 && bytes.starts_with(b"\xEF\xBB\xBF") {
        return Some("UTF-8");
    }
    if bytes.len() >= 2 {
        if bytes.starts_with(b"\xFF\xFE") {
            return Some("UTF-16LE");
        }
        if bytes.starts_with(b"\xFE\xFF") {
            return Some("UTF-16BE");
        }
    }
    None
}

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use std::ffi::{OsString, c_char, c_void};
use std::fmt::Write as _;
use std::fs::{self, File};
use std::mem::MaybeUninit;
use std::os::windows::io::{AsRawHandle as _, FromRawHandle};
use std::path::{Path, PathBuf};
use std::ptr::{self, NonNull, null, null_mut};
use std::{mem, time};

use windows_sys::Win32::Storage::FileSystem;
use windows_sys::Win32::System::Diagnostics::Debug;
use windows_sys::Win32::System::{Console, IO, LibraryLoader, Memory, Threading};
use windows_sys::Win32::{Foundation, Globalization};
use windows_sys::w;

use crate::apperr;
use crate::arena::{Arena, ArenaString, scratch_arena};
use crate::helpers::*;

macro_rules! w_env {
    ($s:literal) => {{
        const INPUT: &[u8] = env!($s).as_bytes();
        const OUTPUT_LEN: usize = windows_sys::core::utf16_len(INPUT) + 1;
        const OUTPUT: &[u16; OUTPUT_LEN] = {
            let mut buffer = [0; OUTPUT_LEN];
            let mut input_pos = 0;
            let mut output_pos = 0;
            while let Some((mut code_point, new_pos)) =
                windows_sys::core::decode_utf8_char(INPUT, input_pos)
            {
                input_pos = new_pos;
                if code_point <= 0xffff {
                    buffer[output_pos] = code_point as u16;
                    output_pos += 1;
                } else {
                    code_point -= 0x10000;
                    buffer[output_pos] = 0xd800 + (code_point >> 10) as u16;
                    output_pos += 1;
                    buffer[output_pos] = 0xdc00 + (code_point & 0x3ff) as u16;
                    output_pos += 1;
                }
            }
            &{ buffer }
        };
        OUTPUT.as_ptr()
    }};
}

type ReadConsoleInputExW = unsafe extern "system" fn(
    h_console_input: Foundation::HANDLE,
    lp_buffer: *mut Console::INPUT_RECORD,
    n_length: u32,
    lp_number_of_events_read: *mut u32,
    w_flags: u16,
) -> Foundation::BOOL;

unsafe extern "system" fn read_console_input_ex_placeholder(
    _: Foundation::HANDLE,
    _: *mut Console::INPUT_RECORD,
    _: u32,
    _: *mut u32,
    _: u16,
) -> Foundation::BOOL {
    panic!();
}

const CONSOLE_READ_NOWAIT: u16 = 0x0002;

const INVALID_CONSOLE_MODE: u32 = u32::MAX;

struct State {
    read_console_input_ex: ReadConsoleInputExW,
    stdin: Foundation::HANDLE,
    stdout: Foundation::HANDLE,
    stdin_cp_old: u32,
    stdout_cp_old: u32,
    stdin_mode_old: u32,
    stdout_mode_old: u32,
    leading_surrogate: u16,
    inject_resize: bool,
    wants_exit: bool,
}

static mut STATE: State = State {
    read_console_input_ex: read_console_input_ex_placeholder,
    stdin: null_mut(),
    stdout: null_mut(),
    stdin_cp_old: 0,
    stdout_cp_old: 0,
    stdin_mode_old: INVALID_CONSOLE_MODE,
    stdout_mode_old: INVALID_CONSOLE_MODE,
    leading_surrogate: 0,
    inject_resize: false,
    wants_exit: false,
};

extern "system" fn console_ctrl_handler(_ctrl_type: u32) -> Foundation::BOOL {
    unsafe {
        STATE.wants_exit = true;
        IO::CancelIoEx(STATE.stdin, null());
    }
    1
}

/// Initializes the platform-specific state.
pub fn init() -> Deinit {
    unsafe {
        // Get the stdin and stdout handles first, so that if this function fails,
        // we at least got something to use for `write_stdout`.
        STATE.stdin = Console::GetStdHandle(Console::STD_INPUT_HANDLE);
        STATE.stdout = Console::GetStdHandle(Console::STD_OUTPUT_HANDLE);

        Deinit
    }
}

/// Switches the terminal into raw mode, etc.
pub fn switch_modes() -> apperr::Result<()> {
    unsafe {
        // `kernel32.dll` doesn't exist on OneCore variants of Windows.
        // NOTE: `kernelbase.dll` is NOT a stable API to rely on. In our case it's the best option though.
        //
        // This is written as two nested `match` statements so that we can return the error from the first
        // `load_read_func` call if it fails. The kernel32.dll lookup may contain some valid information,
        // while the kernelbase.dll lookup may not, since it's not a stable API.
        unsafe fn load_read_func(module: *const u16) -> apperr::Result<ReadConsoleInputExW> {
            unsafe {
                get_module(module)
                    .and_then(|m| get_proc_address(m, c"ReadConsoleInputExW".as_ptr()))
            }
        }
        STATE.read_console_input_ex = match load_read_func(w!("kernel32.dll")) {
            Ok(func) => func,
            Err(err) => match load_read_func(w!("kernelbase.dll")) {
                Ok(func) => func,
                Err(_) => return Err(err),
            },
        };

        // Reopen stdin if it's redirected (= piped input).
        if ptr::eq(STATE.stdin, Foundation::INVALID_HANDLE_VALUE)
            || !matches!(FileSystem::GetFileType(STATE.stdin), FileSystem::FILE_TYPE_CHAR)
        {
            STATE.stdin = FileSystem::CreateFileW(
                w!("CONIN$"),
                Foundation::GENERIC_READ | Foundation::GENERIC_WRITE,
                FileSystem::FILE_SHARE_READ | FileSystem::FILE_SHARE_WRITE,
                null_mut(),
                FileSystem::OPEN_EXISTING,
                0,
                null_mut(),
            );
        }
        if ptr::eq(STATE.stdin, Foundation::INVALID_HANDLE_VALUE)
            || ptr::eq(STATE.stdout, Foundation::INVALID_HANDLE_VALUE)
        {
            return Err(get_last_error());
        }

        check_bool_return(Console::SetConsoleCtrlHandler(Some(console_ctrl_handler), 1))?;

        STATE.stdin_cp_old = Console::GetConsoleCP();
        STATE.stdout_cp_old = Console::GetConsoleOutputCP();
        check_bool_return(Console::GetConsoleMode(STATE.stdin, &raw mut STATE.stdin_mode_old))?;
        check_bool_return(Console::GetConsoleMode(STATE.stdout, &raw mut STATE.stdout_mode_old))?;

        check_bool_return(Console::SetConsoleCP(Globalization::CP_UTF8))?;
        check_bool_return(Console::SetConsoleOutputCP(Globalization::CP_UTF8))?;
        check_bool_return(Console::SetConsoleMode(
            STATE.stdin,
            Console::ENABLE_WINDOW_INPUT
                | Console::ENABLE_EXTENDED_FLAGS
                | Console::ENABLE_VIRTUAL_TERMINAL_INPUT,
        ))?;
        check_bool_return(Console::SetConsoleMode(
            STATE.stdout,
            Console::ENABLE_PROCESSED_OUTPUT
                | Console::ENABLE_WRAP_AT_EOL_OUTPUT
                | Console::ENABLE_VIRTUAL_TERMINAL_PROCESSING
                | Console::DISABLE_NEWLINE_AUTO_RETURN,
        ))?;

        Ok(())
    }
}

pub struct Deinit;

impl Drop for Deinit {
    fn drop(&mut self) {
        unsafe {
            if STATE.stdin_cp_old != 0 {
                Console::SetConsoleCP(STATE.stdin_cp_old);
                STATE.stdin_cp_old = 0;
            }
            if STATE.stdout_cp_old != 0 {
                Console::SetConsoleOutputCP(STATE.stdout_cp_old);
                STATE.stdout_cp_old = 0;
            }
            if STATE.stdin_mode_old != INVALID_CONSOLE_MODE {
                Console::SetConsoleMode(STATE.stdin, STATE.stdin_mode_old);
                STATE.stdin_mode_old = INVALID_CONSOLE_MODE;
            }
            if STATE.stdout_mode_old != INVALID_CONSOLE_MODE {
                Console::SetConsoleMode(STATE.stdout, STATE.stdout_mode_old);
                STATE.stdout_mode_old = INVALID_CONSOLE_MODE;
            }
        }
    }
}

/// During startup we need to get the window size from the terminal.
/// Because I didn't want to type a bunch of code, this function tells
/// [`read_stdin`] to inject a fake sequence, which gets picked up by
/// the input parser and provided to the TUI code.
pub fn inject_window_size_into_stdin() {
    unsafe {
        STATE.inject_resize = true;
    }
}

fn get_console_size() -> Option<Size> {
    unsafe {
        let mut info: Console::CONSOLE_SCREEN_BUFFER_INFOEX = mem::zeroed();
        info.cbSize = mem::size_of::<Console::CONSOLE_SCREEN_BUFFER_INFOEX>() as u32;
        if Console::GetConsoleScreenBufferInfoEx(STATE.stdout, &mut info) == 0 {
            return None;
        }

        let w = (info.srWindow.Right - info.srWindow.Left + 1).max(1) as CoordType;
        let h = (info.srWindow.Bottom - info.srWindow.Top + 1).max(1) as CoordType;
        Some(Size { width: w, height: h })
    }
}

/// Reads from stdin.
///
/// # Returns
///
/// * `None` if there was an error reading from stdin.
/// * `Some("")` if the given timeout was reached.
/// * Otherwise, it returns the read, non-empty string.
pub fn read_stdin(arena: &Arena, mut timeout: time::Duration) -> Option<ArenaString<'_>> {
    let scratch = scratch_arena(Some(arena));

    // On startup we're asked to inject a window size so that the UI system can layout the elements.
    // --> Inject a fake sequence for our input parser.
    let mut resize_event = None;
    if unsafe { STATE.inject_resize } {
        unsafe { STATE.inject_resize = false };
        timeout = time::Duration::ZERO;
        resize_event = get_console_size();
    }

    let read_poll = timeout != time::Duration::MAX; // there is a timeout -> don't block in read()
    let input_buf = scratch.alloc_uninit_slice(4 * KIBI);
    let mut input_buf_cap = input_buf.len();
    let utf16_buf = scratch.alloc_uninit_slice(4 * KIBI);
    let mut utf16_buf_len = 0;

    // If there was a leftover leading surrogate from the last read, we prepend it to the buffer.
    if unsafe { STATE.leading_surrogate } != 0 {
        utf16_buf[0] = MaybeUninit::new(unsafe { STATE.leading_surrogate });
        utf16_buf_len = 1;
        input_buf_cap -= 1;
        unsafe { STATE.leading_surrogate = 0 };
    }

    // Read until there's either a timeout or we have something to process.
    loop {
        if timeout != time::Duration::MAX {
            let beg = time::Instant::now();

            match unsafe { Threading::WaitForSingleObject(STATE.stdin, timeout.as_millis() as u32) }
            {
                // Ready to read? Continue with reading below.
                Foundation::WAIT_OBJECT_0 => {}
                // Timeout? Skip reading entirely.
                Foundation::WAIT_TIMEOUT => break,
                // Error? Tell the caller stdin is broken.
                _ => return None,
            }

            timeout = timeout.saturating_sub(beg.elapsed());
        }

        // Read from stdin.
        let input = unsafe {
            // If we had a `inject_resize`, we don't want to block indefinitely for other pending input on startup,
            // but are still interested in any other pending input that may be waiting for us.
            let flags = if read_poll { CONSOLE_READ_NOWAIT } else { 0 };
            let mut read = 0;
            let ok = (STATE.read_console_input_ex)(
                STATE.stdin,
                input_buf[0].as_mut_ptr(),
                input_buf_cap as u32,
                &mut read,
                flags,
            );
            if ok == 0 || STATE.wants_exit {
                return None;
            }
            input_buf[..read as usize].assume_init_ref()
        };

        // Convert Win32 input records into UTF16.
        for inp in input {
            match inp.EventType as u32 {
                Console::KEY_EVENT => {
                    let event = unsafe { &inp.Event.KeyEvent };
                    let ch = unsafe { event.uChar.UnicodeChar };
                    if event.bKeyDown != 0 && ch != 0 {
                        utf16_buf[utf16_buf_len] = MaybeUninit::new(ch);
                        utf16_buf_len += 1;
                    }
                }
                Console::WINDOW_BUFFER_SIZE_EVENT => {
                    let event = unsafe { &inp.Event.WindowBufferSizeEvent };
                    let w = event.dwSize.X as CoordType;
                    let h = event.dwSize.Y as CoordType;
                    // Windows is prone to sending broken/useless `WINDOW_BUFFER_SIZE_EVENT`s.
                    // E.g. starting conhost will emit 3 in a row. Skip rendering in that case.
                    if w > 0 && h > 0 {
                        resize_event = Some(Size { width: w, height: h });
                    }
                }
                _ => {}
            }
        }

        if resize_event.is_some() || utf16_buf_len != 0 {
            break;
        }
    }

    const RESIZE_EVENT_FMT_MAX_LEN: usize = 16; // "\x1b[8;65535;65535t"
    let resize_event_len = if resize_event.is_some() { RESIZE_EVENT_FMT_MAX_LEN } else { 0 };
    // +1 to account for a potential `STATE.leading_surrogate`.
    let utf8_max_len = (utf16_buf_len + 1) * 3;
    let mut text = ArenaString::new_in(arena);
    text.reserve(utf8_max_len + resize_event_len);

    // Now prepend our previously extracted resize event.
    if let Some(resize_event) = resize_event {
        // If I read xterm's documentation correctly, CSI 18 t reports the window size in characters.
        // CSI 8 ; height ; width t is the response. Of course, we didn't send the request,
        // but we can use this fake response to trigger the editor to resize itself.
        _ = write!(text, "\x1b[8;{};{}t", resize_event.height, resize_event.width);
    }

    // If the input ends with a lone lead surrogate, we need to remember it for the next read.
    if utf16_buf_len > 0 {
        unsafe {
            let last_char = utf16_buf[utf16_buf_len - 1].assume_init();
            if (0xD800..0xDC00).contains(&last_char) {
                STATE.leading_surrogate = last_char;
                utf16_buf_len -= 1;
            }
        }
    }

    // Convert the remaining input to UTF8, the sane encoding.
    if utf16_buf_len > 0 {
        unsafe {
            let vec = text.as_mut_vec();
            let spare = vec.spare_capacity_mut();

            let len = Globalization::WideCharToMultiByte(
                Globalization::CP_UTF8,
                0,
                utf16_buf[0].as_ptr(),
                utf16_buf_len as i32,
                spare.as_mut_ptr() as *mut _,
                spare.len() as i32,
                null(),
                null_mut(),
            );

            if len > 0 {
                vec.set_len(vec.len() + len as usize);
            }
        }
    }

    text.shrink_to_fit();
    Some(text)
}

/// Writes a string to stdout.
///
/// Use this instead of `print!` or `println!` to avoid
/// the overhead of Rust's stdio handling. Don't need that.
pub fn write_stdout(text: &str) {
    unsafe {
        let mut offset = 0;

        while offset < text.len() {
            let ptr = text.as_ptr().add(offset);
            let write = (text.len() - offset).min(GIBI) as u32;
            let mut written = 0;
            let ok = FileSystem::WriteFile(STATE.stdout, ptr, write, &mut written, null_mut());
            offset += written as usize;
            if ok == 0 || written == 0 {
                break;
            }
        }
    }
}

/// Check if the stdin handle is redirected to a file, etc.
///
/// # Returns
///
/// * `Some(file)` if stdin is redirected.
/// * Otherwise, `None`.
pub fn open_stdin_if_redirected() -> Option<File> {
    unsafe {
        let handle = Console::GetStdHandle(Console::STD_INPUT_HANDLE);
        // Did we reopen stdin during `init()`?
        if !std::ptr::eq(STATE.stdin, handle) { Some(File::from_raw_handle(handle)) } else { None }
    }
}

pub fn drives() -> impl Iterator<Item = char> {
    unsafe {
        let mut mask = FileSystem::GetLogicalDrives();
        std::iter::from_fn(move || {
            let bit = mask.trailing_zeros();
            if bit >= 26 {
                None
            } else {
                mask &= !(1 << bit);
                Some((b'A' + bit as u8) as char)
            }
        })
    }
}

/// A unique identifier for a file.
pub enum FileId {
    Id(FileSystem::FILE_ID_INFO),
    Path(PathBuf),
}

impl PartialEq for FileId {
    fn eq(&self, other: &Self) -> bool {
        match (self, other) {
            (Self::Id(left), Self::Id(right)) => {
                // Lowers to an efficient word-wise comparison.
                const SIZE: usize = std::mem::size_of::<FileSystem::FILE_ID_INFO>();
                let a: &[u8; SIZE] = unsafe { mem::transmute(left) };
                let b: &[u8; SIZE] = unsafe { mem::transmute(right) };
                a == b
            }
            (Self::Path(left), Self::Path(right)) => left == right,
            _ => false,
        }
    }
}

impl Eq for FileId {}

/// Returns a unique identifier for the given file by handle or path.
pub fn file_id(file: Option<&File>, path: &Path) -> apperr::Result<FileId> {
    let file = match file {
        Some(f) => f,
        None => &File::open(path)?,
    };

    file_id_from_handle(file).or_else(|_| Ok(FileId::Path(std::fs::canonicalize(path)?)))
}

fn file_id_from_handle(file: &File) -> apperr::Result<FileId> {
    unsafe {
        let mut info = MaybeUninit::<FileSystem::FILE_ID_INFO>::uninit();
        check_bool_return(FileSystem::GetFileInformationByHandleEx(
            file.as_raw_handle(),
            FileSystem::FileIdInfo,
            info.as_mut_ptr() as *mut _,
            mem::size_of::<FileSystem::FILE_ID_INFO>() as u32,
        ))?;
        Ok(FileId::Id(info.assume_init()))
    }
}

/// Canonicalizes the given path.
///
/// This differs from [`fs::canonicalize`] in that it strips the `\\?\` UNC
/// prefix on Windows. This is because it's confusing/ugly when displaying it.
pub fn canonicalize(path: &Path) -> std::io::Result<PathBuf> {
    let mut path = fs::canonicalize(path)?;
    let path = path.as_mut_os_string();
    let mut path = mem::take(path).into_encoded_bytes();

    if path.len() > 6 && &path[0..4] == br"\\?\" && path[4].is_ascii_uppercase() && path[5] == b':'
    {
        path.drain(0..4);
    }

    let path = unsafe { OsString::from_encoded_bytes_unchecked(path) };
    let path = PathBuf::from(path);
    Ok(path)
}

/// Reserves a virtual memory region of the given size.
/// To commit the memory, use [`virtual_commit`].
/// To release the memory, use [`virtual_release`].
///
/// # Safety
///
/// This function is unsafe because it uses raw pointers.
/// Don't forget to release the memory when you're done with it or you'll leak it.
pub unsafe fn virtual_reserve(size: usize) -> apperr::Result<NonNull<u8>> {
    unsafe {
        #[allow(unused_assignments, unused_mut)]
        let mut base = null_mut();

        // In debug builds, we use fixed addresses to aid in debugging.
        // Makes it possible to immediately tell which address space a pointer belongs to.
        #[cfg(all(debug_assertions, not(target_pointer_width = "32")))]
        {
            static mut S_BASE_GEN: usize = 0x0000100000000000; // 16 TiB
            S_BASE_GEN += 0x0000001000000000; // 64 GiB
            base = S_BASE_GEN as *mut _;
        }

        check_ptr_return(Memory::VirtualAlloc(
            base,
            size,
            Memory::MEM_RESERVE,
            Memory::PAGE_READWRITE,
        ) as *mut u8)
    }
}

/// Releases a virtual memory region of the given size.
///
/// # Safety
///
/// This function is unsafe because it uses raw pointers.
/// Make sure to only pass pointers acquired from [`virtual_reserve`].
pub unsafe fn virtual_release(base: NonNull<u8>, _size: usize) {
    unsafe {
        // NOTE: `VirtualFree` fails if the pointer isn't
        // a valid base address or if the size isn't zero.
        Memory::VirtualFree(base.as_ptr() as *mut _, 0, Memory::MEM_RELEASE);
    }
}

/// Commits a virtual memory region of the given size.
///
/// # Safety
///
/// This function is unsafe because it uses raw pointers.
/// Make sure to only pass pointers acquired from [`virtual_reserve`]
/// and to pass a size less than or equal to the size passed to [`virtual_reserve`].
pub unsafe fn virtual_commit(base: NonNull<u8>, size: usize) -> apperr::Result<()> {
    unsafe {
        check_ptr_return(Memory::VirtualAlloc(
            base.as_ptr() as *mut _,
            size,
            Memory::MEM_COMMIT,
            Memory::PAGE_READWRITE,
        ))
        .map(|_| ())
    }
}

unsafe fn get_module(name: *const u16) -> apperr::Result<NonNull<c_void>> {
    unsafe { check_ptr_return(LibraryLoader::GetModuleHandleW(name)) }
}

unsafe fn load_library(name: *const u16) -> apperr::Result<NonNull<c_void>> {
    unsafe {
        check_ptr_return(LibraryLoader::LoadLibraryExW(
            name,
            null_mut(),
            LibraryLoader::LOAD_LIBRARY_SEARCH_SYSTEM32,
        ))
    }
}

/// Loads a function from a dynamic library.
///
/// # Safety
///
/// This function is highly unsafe as it requires you to know the exact type
/// of the function you're loading. No type checks whatsoever are performed.
//
// It'd be nice to constrain T to std::marker::FnPtr, but that's unstable.
pub unsafe fn get_proc_address<T>(
    handle: NonNull<c_void>,
    name: *const c_char,
) -> apperr::Result<T> {
    unsafe {
        let ptr = LibraryLoader::GetProcAddress(handle.as_ptr(), name as *const u8);
        if let Some(ptr) = ptr { Ok(mem::transmute_copy(&ptr)) } else { Err(get_last_error()) }
    }
}

pub struct LibIcu {
    pub libicuuc: NonNull<c_void>,
    pub libicui18n: NonNull<c_void>,
}

pub fn load_icu() -> apperr::Result<LibIcu> {
    const fn const_ptr_u16_eq(a: *const u16, b: *const u16) -> bool {
        unsafe {
            let mut a = a;
            let mut b = b;
            loop {
                if *a != *b {
                    return false;
                }
                if *a == 0 {
                    return true;
                }
                a = a.add(1);
                b = b.add(1);
            }
        }
    }

    const LIBICUUC: *const u16 = w_env!("EDIT_CFG_ICUUC_SONAME");
    const LIBICUI18N: *const u16 = w_env!("EDIT_CFG_ICUI18N_SONAME");

    if const { const_ptr_u16_eq(LIBICUUC, LIBICUI18N) } {
        let icu = unsafe { load_library(LIBICUUC)? };
        Ok(LibIcu { libicuuc: icu, libicui18n: icu })
    } else {
        let libicuuc = unsafe { load_library(LIBICUUC)? };
        let libicui18n = unsafe { load_library(LIBICUI18N)? };
        Ok(LibIcu { libicuuc, libicui18n })
    }
}

/// Returns a list of preferred languages for the current user.
pub fn preferred_languages(arena: &Arena) -> Vec<ArenaString<'_>, &Arena> {
    // If the GetUserPreferredUILanguages() don't fit into 512 characters,
    // honestly, just give up. How many languages do you realistically need?
    const LEN: usize = 512;

    let scratch = scratch_arena(Some(arena));
    let mut res = Vec::new_in(arena);

    // Get the list of preferred languages via `GetUserPreferredUILanguages`.
    let langs = unsafe {
        let buf = scratch.alloc_uninit_slice(LEN);
        let mut len = buf.len() as u32;
        let mut num = 0;

        let ok = Globalization::GetUserPreferredUILanguages(
            Globalization::MUI_LANGUAGE_NAME,
            &mut num,
            buf[0].as_mut_ptr(),
            &mut len,
        );

        if ok == 0 || num == 0 {
            len = 0;
        }

        // Drop the terminating double-null character.
        len = len.saturating_sub(1);

        buf[..len as usize].assume_init_ref()
    };

    // Convert UTF16 to UTF8.
    let langs = wide_to_utf8(&scratch, langs);

    // Split the null-delimited string into individual chunks
    // and copy them into the given arena.
    res.extend(
        langs
            .split_terminator('\0')
            .filter(|s| !s.is_empty())
            .map(|s| ArenaString::from_str(arena, s)),
    );
    res
}

fn wide_to_utf8<'a>(arena: &'a Arena, wide: &[u16]) -> ArenaString<'a> {
    let mut res = ArenaString::new_in(arena);
    res.reserve(wide.len() * 3);

    let len = unsafe {
        Globalization::WideCharToMultiByte(
            Globalization::CP_UTF8,
            0,
            wide.as_ptr(),
            wide.len() as i32,
            res.as_mut_ptr() as *mut _,
            res.capacity() as i32,
            null(),
            null_mut(),
        )
    };
    if len > 0 {
        unsafe { res.as_mut_vec().set_len(len as usize) };
    }

    res.shrink_to_fit();
    res
}

#[cold]
fn get_last_error() -> apperr::Error {
    unsafe { gle_to_apperr(Foundation::GetLastError()) }
}

#[inline]
const fn gle_to_apperr(gle: u32) -> apperr::Error {
    apperr::Error::new_sys(if gle == 0 { 0x8000FFFF } else { 0x80070000 | gle })
}

#[inline]
pub(crate) fn io_error_to_apperr(err: std::io::Error) -> apperr::Error {
    gle_to_apperr(err.raw_os_error().unwrap_or(0) as u32)
}

/// Formats a platform error code into a human-readable string.
pub fn apperr_format(f: &mut std::fmt::Formatter<'_>, code: u32) -> std::fmt::Result {
    unsafe {
        let mut ptr: *mut u8 = null_mut();
        let len = Debug::FormatMessageA(
            Debug::FORMAT_MESSAGE_ALLOCATE_BUFFER
                | Debug::FORMAT_MESSAGE_FROM_SYSTEM
                | Debug::FORMAT_MESSAGE_IGNORE_INSERTS,
            null(),
            code,
            0,
            &mut ptr as *mut *mut _ as *mut _,
            0,
            null_mut(),
        );

        write!(f, "Error {code:#08x}")?;

        if len > 0 {
            let msg = str_from_raw_parts(ptr, len as usize);
            let msg = msg.trim_ascii();
            let msg = msg.replace(['\r', '\n'], " ");
            write!(f, ": {msg}")?;
            Foundation::LocalFree(ptr as *mut _);
        }

        Ok(())
    }
}

/// Checks if the given error is a "file not found" error.
pub fn apperr_is_not_found(err: apperr::Error) -> bool {
    err == gle_to_apperr(Foundation::ERROR_FILE_NOT_FOUND)
}

fn check_bool_return(ret: Foundation::BOOL) -> apperr::Result<()> {
    if ret == 0 { Err(get_last_error()) } else { Ok(()) }
}

fn check_ptr_return<T>(ret: *mut T) -> apperr::Result<NonNull<T>> {
    NonNull::new(ret).ok_or_else(get_last_error)
}

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

//! An immediate mode UI framework for terminals.
//!
//! # Why immediate mode?
//!
//! This uses an "immediate mode" design, similar to [ImGui](https://github.com/ocornut/imgui).
//! The reason for this is that I expect the UI needs for any terminal application to be
//! fairly minimal, and for that purpose an immediate mode design is much simpler to use.
//!
//! So what's "immediate mode"? The primary alternative is called "retained mode".
//! The difference is that when you create a button in this framework in one frame,
//! and you stop telling this framework in the next frame, the button will vanish.
//! When you use a regular retained mode UI framework, you create the button once,
//! set up callbacks for when it is clicked, and then stop worrying about it.
//!
//! The downside of immediate mode is that your UI code _may_ become cluttered.
//! The upside however is that you cannot leak UI elements, you don't need to
//! worry about lifetimes nor callbacks, and that simple UIs are simple to write.
//!
//! More importantly though, the primary reason for this is that the
//! lack of callbacks means we can use this design across a plain C ABI,
//! which we'll need once plugins come into play. GTK's `g_signal_connect`
//! shows that the alternative can be rather cumbersome.
//!
//! # Design overview
//!
//! While this file is fairly lengthy, the overall algorithm is simple.
//! On the first frame ever:
//! * Prepare an empty `arena_next`.
//! * Parse the incoming [`input::Input`] which should be a resize event.
//! * Create a new [`Context`] instance and give it the caller.
//! * Now the caller will draw their UI with the [`Context`] by calling the
//!   various [`Context`] UI methods, such as [`Context::block_begin()`] and
//!   [`Context::block_end()`]. These two are the basis which all other UI
//!   elements are built upon by the way. Each UI element that is created gets
//!   allocated onto `arena_next` and inserted into the UI tree.
//!   That tree works exactly like the DOM tree in HTML: Each node in the tree
//!   has a parent, children, and siblings. The tree layout at the end is then
//!   a direct mirror of the code "layout" that created it.
//! * Once the caller is done and drops the [`Context`], it'll secretly call
//!   `report_context_completion`. This causes a number of things:
//!   * The DOM tree that was built is stored in `prev_tree`.
//!   * A hashmap of all nodes is built and stored in `prev_node_map`.
//!   * `arena_next` is swapped with `arena_prev`.
//!   * Each UI node is measured and laid out.
//! * Now the caller is expected to repeat this process with a [`None`]
//!   input event until [`Tui::needs_settling()`] returns false.
//!   This is necessary, because when [`Context::button()`] returns `true`
//!   in one frame, it may change the state in the caller's code
//!   and require another frame to be drawn.
//! * Finally a call to [`Tui::render()`] will render the UI tree into the
//!   framebuffer and return VT output.
//!
//! On every subsequent frame the process is similar, but one crucial element
//! of any immediate mode UI framework is added:
//! Now when the caller draws their UI, the various [`Context`] UI elements
//! have access to `prev_node_map` and the previously built UI tree.
//! This allows the UI framework to reuse the previously computed layout for
//! hit tests, caching scroll offsets, and so on.
//!
//! In the end it looks very similar:
//! * Prepare an empty `arena_next`.
//! * Parse the incoming [`input::Input`]...
//!   * **BUT** now we can hit-test mouse clicks onto the previously built
//!     UI tree. This way we can delegate focus on left mouse clicks.
//! * Create a new [`Context`] instance and give it the caller.
//! * The caller draws their UI with the [`Context`]...
//!   * **BUT** we can preserve the UI state across frames.
//! * Continue rendering until [`Tui::needs_settling()`] returns false.
//! * And the final call to [`Tui::render()`].
//!
//! # Classnames and node IDs
//!
//! So how do we find which node from the previous tree correlates to the
//! current node? Each node needs to be constructed with a "classname".
//! The classname is hashed with the parent node ID as the seed. This derived
//! hash is then used as the new child node ID. Under the assumption that the
//! collision likelihood of the hash function is low, this serves as true IDs.
//!
//! This has the nice added property that finding a node with the same ID
//! guarantees that all of the parent nodes must have equivalent IDs as well.
//! This turns "is the focus anywhere inside this subtree" into an O(1) check.
//!
//! The reason "classnames" are used is because I was hoping to add theming
//! in the future with a syntax similar to CSS (simplified, however).
//!
//! # Example
//!
//! ```
//! use edit::helpers::Size;
//! use edit::input::Input;
//! use edit::tui::*;
//! use edit::{arena, arena_format};
//!
//! struct State {
//!     counter: i32,
//! }
//!
//! fn main() {
//!     arena::init(128 * 1024 * 1024).unwrap();
//!
//!     // Create a `Tui` instance which holds state across frames.
//!     let mut tui = Tui::new().unwrap();
//!     let mut state = State { counter: 0 };
//!     let input = Input::Resize(Size { width: 80, height: 24 });
//!
//!     // Pass the input to the TUI.
//!     {
//!         let mut ctx = tui.create_context(Some(input));
//!         draw(&mut ctx, &mut state);
//!     }
//!
//!     // Continue until the layout has settled.
//!     while tui.needs_settling() {
//!         let mut ctx = tui.create_context(None);
//!         draw(&mut ctx, &mut state);
//!     }
//!
//!     // Render the output.
//!     let scratch = arena::scratch_arena(None);
//!     let output = tui.render(&*scratch);
//!     println!("{}", output);
//! }
//!
//! fn draw(ctx: &mut Context, state: &mut State) {
//!     ctx.table_begin("classname");
//!     {
//!         ctx.table_next_row();
//!
//!         // Thanks to the lack of callbacks, we can use a primitive
//!         // if condition here, as well as in any potential C code.
//!         if ctx.button("button", "Click me!", ButtonStyle::default()) {
//!             state.counter += 1;
//!         }
//!
//!         // Similarly, formatting and showing labels is straightforward.
//!         // It's impossible to forget updating the label this way.
//!         ctx.label("label", &arena_format!(ctx.arena(), "Counter: {}", state.counter));
//!     }
//!     ctx.table_end();
//! }
//! ```

use std::arch::breakpoint;
#[cfg(debug_assertions)]
use std::collections::HashSet;
use std::fmt::Write as _;
use std::{iter, mem, ptr, time};

use crate::arena::{Arena, ArenaString, scratch_arena};
use crate::buffer::{CursorMovement, MoveLineDirection, RcTextBuffer, TextBuffer, TextBufferCell};
use crate::cell::*;
use crate::clipboard::Clipboard;
use crate::document::WriteableDocument;
use crate::framebuffer::{Attributes, Framebuffer, INDEXED_COLORS_COUNT, IndexedColor};
use crate::hash::*;
use crate::helpers::*;
use crate::input::{InputKeyMod, kbmod, vk};
use crate::{apperr, arena_format, input, simd, unicode};

const ROOT_ID: u64 = 0x14057B7EF767814F; // Knuth's MMIX constant
const SHIFT_TAB: InputKey = vk::TAB.with_modifiers(kbmod::SHIFT);
const KBMOD_FOR_WORD_NAV: InputKeyMod =
    if cfg!(target_os = "macos") { kbmod::ALT } else { kbmod::CTRL };

type Input<'input> = input::Input<'input>;
type InputKey = input::InputKey;
type InputMouseState = input::InputMouseState;

/// Since [`TextBuffer`] creation and management is expensive,
/// we cache instances of them for reuse between frames.
/// This is used for [`Context::editline()`].
struct CachedTextBuffer {
    node_id: u64,
    editor: RcTextBuffer,
    seen: bool,
}

/// Since [`Context::editline()`] and [`Context::textarea()`]
/// do almost the same thing, this abstracts over the two.
enum TextBufferPayload<'a> {
    Editline(&'a mut dyn WriteableDocument),
    Textarea(RcTextBuffer),
}

/// In order for the TUI to show the correct Ctrl/Alt/Shift
/// translations, this struct lets you set them.
pub struct ModifierTranslations {
    pub ctrl: &'static str,
    pub alt: &'static str,
    pub shift: &'static str,
}

/// Controls to which node the floater is anchored.
#[derive(Default, Clone, Copy, PartialEq, Eq)]
pub enum Anchor {
    /// The floater is attached relative to the node created last.
    #[default]
    Last,
    /// The floater is attached relative to the current node (= parent of new nodes).
    Parent,
    /// The floater is attached relative to the root node (= usually the viewport).
    Root,
}

/// Controls the position of the floater. See [`Context::attr_float`].
#[derive(Default)]
pub struct FloatSpec {
    /// Controls to which node the floater is anchored.
    pub anchor: Anchor,
    // Specifies the origin of the container relative to the container size. [0, 1]
    pub gravity_x: f32,
    pub gravity_y: f32,
    // Specifies an offset from the origin in cells.
    pub offset_x: f32,
    pub offset_y: f32,
}

/// Informs you about the change that was made to the list selection.
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum ListSelection {
    /// The selection wasn't changed.
    Unchanged,
    /// The selection was changed to the current list item.
    Selected,
    /// The selection was changed to the current list item
    /// *and* the item was also activated (Enter or Double-click).
    Activated,
}

/// Controls the position of a node relative to its parent.
#[derive(Default)]
pub enum Position {
    /// The child is stretched to fill the parent.
    #[default]
    Stretch,
    /// The child is positioned at the left edge of the parent.
    Left,
    /// The child is positioned at the center of the parent.
    Center,
    /// The child is positioned at the right edge of the parent.
    Right,
}

/// Controls the text overflow behavior of a label
/// when the text doesn't fit the container.
#[derive(Default, Clone, Copy, PartialEq, Eq)]
pub enum Overflow {
    /// Text is simply cut off when it doesn't fit.
    #[default]
    Clip,
    /// An ellipsis is shown at the end of the text.
    TruncateHead,
    /// An ellipsis is shown in the middle of the text.
    TruncateMiddle,
    /// An ellipsis is shown at the beginning of the text.
    TruncateTail,
}

/// Controls the style with which a button label renders
#[derive(Clone, Copy)]
pub struct ButtonStyle {
    accelerator: Option<char>,
    checked: Option<bool>,
    bracketed: bool,
}

impl ButtonStyle {
    /// Draw an accelerator label: `[_E_xample button]` or `[Example button(X)]`
    ///
    /// Must provide an upper-case ASCII character.
    pub fn accelerator(self, char: char) -> Self {
        Self { accelerator: Some(char), ..self }
    }
    /// Draw a checkbox prefix: `[🗹 Example Button]`
    pub fn checked(self, checked: bool) -> Self {
        Self { checked: Some(checked), ..self }
    }
    /// Draw with or without brackets: `[Example Button]` or `Example Button`
    pub fn bracketed(self, bracketed: bool) -> Self {
        Self { bracketed, ..self }
    }
}

impl Default for ButtonStyle {
    fn default() -> Self {
        Self {
            accelerator: None,
            checked: None,
            bracketed: true, // Default style for most buttons. Brackets may be disabled e.g. for buttons in menus
        }
    }
}

/// There's two types of lifetimes the TUI code needs to manage:
/// * Across frames
/// * Per frame
///
/// [`Tui`] manages the first one. It's also the entrypoint for
/// everything else you may want to do.
pub struct Tui {
    /// Arena used for the previous frame.
    arena_prev: Arena,
    /// Arena used for the current frame.
    arena_next: Arena,
    /// The UI tree built in the previous frame.
    /// This refers to memory in `arena_prev`.
    prev_tree: Tree<'static>,
    /// A hashmap of all nodes built in the previous frame.
    /// This refers to memory in `arena_prev`.
    prev_node_map: NodeMap<'static>,
    /// The framebuffer used for rendering.
    framebuffer: Framebuffer,

    modifier_translations: ModifierTranslations,
    floater_default_bg: u32,
    floater_default_fg: u32,
    modal_default_bg: u32,
    modal_default_fg: u32,

    /// Last known terminal size.
    ///
    /// This lives here instead of [`Context`], because we need to
    /// track the state across frames and input events.
    /// This also applies to the remaining members in this block below.
    size: Size,
    /// Last known mouse position.
    mouse_position: Point,
    /// Between mouse down and up, the position where the mouse was pressed.
    /// Otherwise, this contains Point::MIN.
    mouse_down_position: Point,
    /// Node ID of the node that was clicked on.
    /// Used for tracking drag targets.
    left_mouse_down_target: u64,
    /// Timestamp of the last mouse up event.
    /// Used for tracking double/triple clicks.
    mouse_up_timestamp: std::time::Instant,
    /// The current mouse state.
    mouse_state: InputMouseState,
    /// Whether the mouse is currently being dragged.
    mouse_is_drag: bool,
    /// The number of clicks that have happened in a row.
    /// Gets reset when the mouse was released for a while.
    mouse_click_counter: CoordType,
    /// The path to the node that was clicked on.
    mouse_down_node_path: Vec<u64>,
    /// The position of the first click in a double/triple click series.
    first_click_position: Point,
    /// The node ID of the node that was first clicked on
    /// in a double/triple click series.
    first_click_target: u64,

    /// Path to the currently focused node.
    focused_node_path: Vec<u64>,
    /// Contains the last element in [`Tui::focused_node_path`].
    /// This way we can track if the focus changed, because then we
    /// need to scroll the node into view if it's within a scrollarea.
    focused_node_for_scrolling: u64,

    /// A list of cached text buffers used for [`Context::editline()`].
    cached_text_buffers: Vec<CachedTextBuffer>,

    /// The clipboard contents.
    clipboard: Clipboard,

    settling_have: i32,
    settling_want: i32,
    read_timeout: time::Duration,
}

impl Tui {
    /// Creates a new [`Tui`] instance for storing state across frames.
    pub fn new() -> apperr::Result<Self> {
        let arena_prev = Arena::new(128 * MEBI)?;
        let arena_next = Arena::new(128 * MEBI)?;
        // SAFETY: Since `prev_tree` refers to `arena_prev`/`arena_next`, from its POV the lifetime
        // is `'static`, requiring us to use `transmute` to circumvent the borrow checker.
        let prev_tree = Tree::new(unsafe { mem::transmute::<&Arena, &Arena>(&arena_next) });

        let mut tui = Self {
            arena_prev,
            arena_next,
            prev_tree,
            prev_node_map: Default::default(),
            framebuffer: Framebuffer::new(),

            modifier_translations: ModifierTranslations {
                ctrl: "Ctrl",
                alt: "Alt",
                shift: "Shift",
            },
            floater_default_bg: 0,
            floater_default_fg: 0,
            modal_default_bg: 0,
            modal_default_fg: 0,

            size: Size { width: 0, height: 0 },
            mouse_position: Point::MIN,
            mouse_down_position: Point::MIN,
            left_mouse_down_target: 0,
            mouse_up_timestamp: std::time::Instant::now(),
            mouse_state: InputMouseState::None,
            mouse_is_drag: false,
            mouse_click_counter: 0,
            mouse_down_node_path: Vec::with_capacity(16),
            first_click_position: Point::MIN,
            first_click_target: 0,

            focused_node_path: Vec::with_capacity(16),
            focused_node_for_scrolling: ROOT_ID,

            cached_text_buffers: Vec::with_capacity(16),

            clipboard: Default::default(),

            settling_have: 0,
            settling_want: 0,
            read_timeout: time::Duration::MAX,
        };
        Self::clean_node_path(&mut tui.mouse_down_node_path);
        Self::clean_node_path(&mut tui.focused_node_path);
        Ok(tui)
    }

    /// Sets up the framebuffer's color palette.
    pub fn setup_indexed_colors(&mut self, colors: [u32; INDEXED_COLORS_COUNT]) {
        self.framebuffer.set_indexed_colors(colors);
    }

    /// Set up translations for Ctrl/Alt/Shift modifiers.
    pub fn setup_modifier_translations(&mut self, translations: ModifierTranslations) {
        self.modifier_translations = translations;
    }

    /// Set the default background color for floaters (dropdowns, etc.).
    pub fn set_floater_default_bg(&mut self, color: u32) {
        self.floater_default_bg = color;
    }

    /// Set the default foreground color for floaters (dropdowns, etc.).
    pub fn set_floater_default_fg(&mut self, color: u32) {
        self.floater_default_fg = color;
    }

    /// Set the default background color for modals.
    pub fn set_modal_default_bg(&mut self, color: u32) {
        self.modal_default_bg = color;
    }

    /// Set the default foreground color for modals.
    pub fn set_modal_default_fg(&mut self, color: u32) {
        self.modal_default_fg = color;
    }

    /// If the TUI is currently running animations, etc.,
    /// this will return a timeout smaller than [`time::Duration::MAX`].
    pub fn read_timeout(&mut self) -> time::Duration {
        mem::replace(&mut self.read_timeout, time::Duration::MAX)
    }

    /// Returns the viewport size.
    pub fn size(&self) -> Size {
        // We don't use the size stored in the framebuffer, because until
        // `render()` is called, the framebuffer will use a stale size.
        self.size
    }

    /// Returns an indexed color from the framebuffer.
    #[inline]
    pub fn indexed(&self, index: IndexedColor) -> u32 {
        self.framebuffer.indexed(index)
    }

    /// Returns an indexed color from the framebuffer with the given alpha.
    /// See [`Framebuffer::indexed_alpha()`].
    #[inline]
    pub fn indexed_alpha(&self, index: IndexedColor, numerator: u32, denominator: u32) -> u32 {
        self.framebuffer.indexed_alpha(index, numerator, denominator)
    }

    /// Returns a color in contrast with the given color.
    /// See [`Framebuffer::contrasted()`].
    pub fn contrasted(&self, color: u32) -> u32 {
        self.framebuffer.contrasted(color)
    }

    /// Returns the clipboard.
    pub fn clipboard_ref(&self) -> &Clipboard {
        &self.clipboard
    }

    /// Returns the clipboard (mutable).
    pub fn clipboard_mut(&mut self) -> &mut Clipboard {
        &mut self.clipboard
    }

    /// Starts a new frame and returns a [`Context`] for it.
    pub fn create_context<'a, 'input>(
        &'a mut self,
        input: Option<Input<'input>>,
    ) -> Context<'a, 'input> {
        // SAFETY: Since we have a unique `&mut self`, nothing is holding onto `arena_prev`,
        // which will become `arena_next` and get reset. It's safe to reset and reuse its memory.
        mem::swap(&mut self.arena_prev, &mut self.arena_next);
        unsafe { self.arena_next.reset(0) };

        // In the input handler below we transformed a mouse up into a release event.
        // Now, a frame later, we must reset it back to none, to stop it from triggering things.
        // Same for Scroll events.
        if self.mouse_state > InputMouseState::Right {
            self.mouse_down_position = Point::MIN;
            self.mouse_down_node_path.clear();
            self.left_mouse_down_target = 0;
            self.mouse_state = InputMouseState::None;
            self.mouse_is_drag = false;
        }

        let now = std::time::Instant::now();
        let mut input_text = None;
        let mut input_keyboard = None;
        let mut input_mouse_modifiers = kbmod::NONE;
        let mut input_mouse_click = 0;
        let mut input_scroll_delta = Point { x: 0, y: 0 };
        // `input_consumed` should be `true` if we're in the settling phase which is indicated by
        // `self.needs_settling() == true`. However, there's a possibility for it being true from
        // a previous frame, and we do have fresh new input. In that case want `input_consumed`
        // to be false of course which is ensured by checking for `input.is_none()`.
        let input_consumed = self.needs_settling() && input.is_none();

        if self.scroll_to_focused() {
            self.needs_more_settling();
        }

        match input {
            None => {}
            Some(Input::Resize(resize)) => {
                assert!(resize.width > 0 && resize.height > 0);
                assert!(resize.width < 32768 && resize.height < 32768);
                self.size = resize;
            }
            Some(Input::Text(text)) => {
                input_text = Some(text);
                // TODO: the .len()==1 check causes us to ignore keyboard inputs that are faster than we process them.
                // For instance, imagine the user presses "A" twice and we happen to read it in a single chunk.
                // This causes us to ignore the keyboard input here. We need a way to inform the caller over
                // how much of the input text we actually processed in a single frame. Or perhaps we could use
                // the needs_settling logic?
                if text.len() == 1 {
                    let ch = text.as_bytes()[0];
                    input_keyboard = InputKey::from_ascii(ch as char)
                }
            }
            Some(Input::Paste(paste)) => {
                let clipboard = self.clipboard_mut();
                clipboard.write(paste);
                clipboard.mark_as_synchronized();
                input_keyboard = Some(kbmod::CTRL | vk::V);
            }
            Some(Input::Keyboard(keyboard)) => {
                input_keyboard = Some(keyboard);
            }
            Some(Input::Mouse(mouse)) => {
                let mut next_state = mouse.state;
                let next_position = mouse.position;
                let next_scroll = mouse.scroll;
                let mouse_down = self.mouse_state == InputMouseState::None
                    && next_state != InputMouseState::None;
                let mouse_up = self.mouse_state != InputMouseState::None
                    && next_state == InputMouseState::None;
                let is_drag = self.mouse_state == InputMouseState::Left
                    && next_state == InputMouseState::Left
                    && next_position != self.mouse_position;

                let mut hovered_node = None; // Needed for `mouse_down`
                let mut focused_node = None; // Needed for `mouse_down` and `is_click`
                if mouse_down || mouse_up {
                    // Roots (aka windows) are ordered in Z order, so we iterate
                    // them in reverse order, from topmost to bottommost.
                    for root in self.prev_tree.iterate_roots_rev() {
                        // Find the node that contains the cursor.
                        Tree::visit_all(root, root, true, |node| {
                            let n = node.borrow();
                            if !n.outer_clipped.contains(next_position) {
                                // Skip the entire sub-tree, because it doesn't contain the cursor.
                                return VisitControl::SkipChildren;
                            }
                            hovered_node = Some(node);
                            if n.attributes.focusable {
                                focused_node = Some(node);
                            }
                            VisitControl::Continue
                        });

                        // This root/window contains the cursor.
                        // We don't care about any lower roots.
                        if hovered_node.is_some() {
                            break;
                        }

                        // This root is modal and swallows all clicks,
                        // no matter whether the click was inside it or not.
                        if matches!(root.borrow().content, NodeContent::Modal(_)) {
                            break;
                        }
                    }
                }

                if mouse_down {
                    // Transition from no mouse input to some mouse input --> Record the mouse down position.
                    Self::build_node_path(hovered_node, &mut self.mouse_down_node_path);

                    // On left-mouse-down we change focus.
                    let mut target = 0;
                    if next_state == InputMouseState::Left {
                        target = focused_node.map_or(0, |n| n.borrow().id);
                        Self::build_node_path(focused_node, &mut self.focused_node_path);
                        self.needs_more_settling(); // See `needs_more_settling()`.
                    }

                    // Double-/Triple-/Etc.-clicks are triggered on mouse-down,
                    // unlike the first initial click, which is triggered on mouse-up.
                    if self.mouse_click_counter != 0 {
                        if self.first_click_target != target
                            || self.first_click_position != next_position
                            || (now - self.mouse_up_timestamp)
                                > std::time::Duration::from_millis(500)
                        {
                            // If the cursor moved / the focus changed in between, or if the user did a slow click,
                            // we reset the click counter. On mouse-up it'll transition to a regular click.
                            self.mouse_click_counter = 0;
                            self.first_click_position = Point::MIN;
                            self.first_click_target = 0;
                        } else {
                            self.mouse_click_counter += 1;
                            input_mouse_click = self.mouse_click_counter;
                        };
                    }

                    // Gets reset at the start of this function.
                    self.left_mouse_down_target = target;
                    self.mouse_down_position = next_position;
                } else if mouse_up {
                    // Transition from some mouse input to no mouse input --> The mouse button was released.
                    next_state = InputMouseState::Release;

                    let target = focused_node.map_or(0, |n| n.borrow().id);

                    if self.left_mouse_down_target == 0 || self.left_mouse_down_target != target {
                        // If `left_mouse_down_target == 0`, then it wasn't a left-click, in which case
                        // the target gets reset. Same, if the focus changed in between any clicks.
                        self.mouse_click_counter = 0;
                        self.first_click_position = Point::MIN;
                        self.first_click_target = 0;
                    } else if self.mouse_click_counter == 0 {
                        // No focus change, and no previous clicks? This is an initial, regular click.
                        self.mouse_click_counter = 1;
                        self.first_click_position = self.mouse_down_position;
                        self.first_click_target = target;
                        input_mouse_click = 1;
                    }

                    self.mouse_up_timestamp = now;
                } else if is_drag {
                    self.mouse_is_drag = true;
                }

                input_mouse_modifiers = mouse.modifiers;
                input_scroll_delta = next_scroll;
                self.mouse_position = next_position;
                self.mouse_state = next_state;
            }
        }

        if !input_consumed {
            // Every time there's input, we naturally need to re-render at least once.
            self.settling_have = 0;
            self.settling_want = 1;
        }

        // TODO: There should be a way to do this without unsafe.
        // Allocating from the arena borrows the arena, and so allocating the tree here borrows self.
        // This conflicts with us passing a mutable reference to `self` into the struct below.
        let tree = Tree::new(unsafe { mem::transmute::<&Arena, &Arena>(&self.arena_next) });

        Context {
            tui: self,

            input_text,
            input_keyboard,
            input_mouse_modifiers,
            input_mouse_click,
            input_scroll_delta,
            input_consumed,

            tree,
            last_modal: None,
            focused_node: None,
            next_block_id_mixin: 0,
            needs_settling: false,

            #[cfg(debug_assertions)]
            seen_ids: HashSet::new(),
        }
    }

    fn report_context_completion<'a>(&'a mut self, ctx: &mut Context<'a, '_>) {
        // If this hits, you forgot to block_end() somewhere. The best way to figure
        // out where is to do a binary search of commenting out code in main.rs.
        debug_assert!(
            ctx.tree.current_node.borrow().stack_parent.is_none(),
            "Dangling parent! Did you miss a block_end?"
        );

        // End the root node.
        ctx.block_end();

        // Ensure that focus doesn't escape the active modal.
        if let Some(node) = ctx.last_modal
            && !self.is_subtree_focused(&node.borrow())
        {
            ctx.steal_focus_for(node);
        }

        // If nodes have appeared or disappeared, we need to re-render.
        // Same, if the focus has changed (= changes the highlight color, etc.).
        let mut needs_settling = ctx.needs_settling;
        needs_settling |= self.prev_tree.checksum != ctx.tree.checksum;

        // Adopt the new tree and recalculate the node hashmap.
        //
        // SAFETY: The memory used by the tree is owned by the `self.arena_next` right now.
        // Stealing the tree here thus doesn't need to copy any memory unless someone resets the arena.
        // (The arena is reset in `reset()` above.)
        unsafe {
            self.prev_tree = mem::transmute_copy(&ctx.tree);
            self.prev_node_map = NodeMap::new(mem::transmute(&self.arena_next), &self.prev_tree);
        }

        let mut focus_path_pop_min = 0;
        // If the user pressed Escape, we move the focus to a parent node.
        if !ctx.input_consumed && ctx.consume_shortcut(vk::ESCAPE) {
            focus_path_pop_min = 1;
        }

        // Remove any unknown nodes from the focus path.
        // It's important that we do this after the tree has been swapped out,
        // so that pop_focusable_node() has access to the newest version of the tree.
        needs_settling |= self.pop_focusable_node(focus_path_pop_min);

        // `needs_more_settling()` depends on the current value
        // of `settling_have` and so we increment it first.
        self.settling_have += 1;

        if needs_settling {
            self.needs_more_settling();
        }

        // Remove cached text editors that are no longer in use.
        self.cached_text_buffers.retain(|c| c.seen);

        for root in Tree::iterate_siblings(Some(self.prev_tree.root_first)) {
            let mut root = root.borrow_mut();
            root.compute_intrinsic_size();
        }

        let viewport = self.size.as_rect();

        for root in Tree::iterate_siblings(Some(self.prev_tree.root_first)) {
            let mut root = root.borrow_mut();
            let root = &mut *root;

            if let Some(float) = &root.attributes.float {
                let mut x = 0;
                let mut y = 0;

                if let Some(node) = root.parent {
                    let node = node.borrow();
                    x = node.outer.left;
                    y = node.outer.top;
                }

                let size = root.intrinsic_to_outer();

                x += (float.offset_x - float.gravity_x * size.width as f32) as CoordType;
                y += (float.offset_y - float.gravity_y * size.height as f32) as CoordType;

                root.outer.left = x;
                root.outer.top = y;
                root.outer.right = x + size.width;
                root.outer.bottom = y + size.height;
                root.outer = root.outer.intersect(viewport);
            } else {
                root.outer = viewport;
            }

            root.inner = root.outer_to_inner(root.outer);
            root.outer_clipped = root.outer;
            root.inner_clipped = root.inner;

            let outer = root.outer;
            root.layout_children(outer);
        }
    }

    fn build_node_path(node: Option<&NodeCell>, path: &mut Vec<u64>) {
        path.clear();
        if let Some(mut node) = node {
            loop {
                let n = node.borrow();
                path.push(n.id);
                node = match n.parent {
                    Some(parent) => parent,
                    None => break,
                };
            }
            path.reverse();
        } else {
            path.push(ROOT_ID);
        }
    }

    fn clean_node_path(path: &mut Vec<u64>) {
        Self::build_node_path(None, path);
    }

    /// After you finished processing all input, continue redrawing your UI until this returns false.
    pub fn needs_settling(&mut self) -> bool {
        self.settling_have <= self.settling_want
    }

    fn needs_more_settling(&mut self) {
        // If the focus has changed, the new node may need to be re-rendered.
        // Same, every time we encounter a previously unknown node via `get_prev_node`,
        // because that means it likely failed to get crucial information such as the layout size.
        if cfg!(debug_assertions) && self.settling_have == 15 {
            breakpoint();
        }
        self.settling_want = (self.settling_have + 1).min(20);
    }

    /// Renders the last frame into the framebuffer and returns the VT output.
    pub fn render<'a>(&mut self, arena: &'a Arena) -> ArenaString<'a> {
        self.framebuffer.flip(self.size);
        for child in self.prev_tree.iterate_roots() {
            let mut child = child.borrow_mut();
            self.render_node(&mut child);
        }
        self.framebuffer.render(arena)
    }

    /// Recursively renders each node and its children.
    #[allow(clippy::only_used_in_recursion)]
    fn render_node(&mut self, node: &mut Node) {
        let outer_clipped = node.outer_clipped;
        if outer_clipped.is_empty() {
            return;
        }

        let scratch = scratch_arena(None);

        if node.attributes.bordered {
            // ┌────┐
            {
                let mut fill = ArenaString::new_in(&scratch);
                fill.push('┌');
                fill.push_repeat('─', (outer_clipped.right - outer_clipped.left - 2) as usize);
                fill.push('┐');
                self.framebuffer.replace_text(
                    outer_clipped.top,
                    outer_clipped.left,
                    outer_clipped.right,
                    &fill,
                );
            }

            // │    │
            {
                let mut fill = ArenaString::new_in(&scratch);
                fill.push('│');
                fill.push_repeat(' ', (outer_clipped.right - outer_clipped.left - 2) as usize);
                fill.push('│');

                for y in outer_clipped.top + 1..outer_clipped.bottom - 1 {
                    self.framebuffer.replace_text(
                        y,
                        outer_clipped.left,
                        outer_clipped.right,
                        &fill,
                    );
                }
            }

            // └────┘
            {
                let mut fill = ArenaString::new_in(&scratch);
                fill.push('└');
                fill.push_repeat('─', (outer_clipped.right - outer_clipped.left - 2) as usize);
                fill.push('┘');
                self.framebuffer.replace_text(
                    outer_clipped.bottom - 1,
                    outer_clipped.left,
                    outer_clipped.right,
                    &fill,
                );
            }
        }

        if node.attributes.float.is_some() {
            if !node.attributes.bordered {
                let mut fill = ArenaString::new_in(&scratch);
                fill.push_repeat(' ', (outer_clipped.right - outer_clipped.left) as usize);

                for y in outer_clipped.top..outer_clipped.bottom {
                    self.framebuffer.replace_text(
                        y,
                        outer_clipped.left,
                        outer_clipped.right,
                        &fill,
                    );
                }
            }

            self.framebuffer.replace_attr(outer_clipped, Attributes::All, Attributes::None);

            if matches!(node.content, NodeContent::Modal(_)) {
                let rect =
                    Rect { left: 0, top: 0, right: self.size.width, bottom: self.size.height };
                let dim = self.indexed_alpha(IndexedColor::Background, 1, 2);
                self.framebuffer.blend_bg(rect, dim);
                self.framebuffer.blend_fg(rect, dim);
            }
        }

        self.framebuffer.blend_bg(outer_clipped, node.attributes.bg);
        self.framebuffer.blend_fg(outer_clipped, node.attributes.fg);

        if node.attributes.reverse {
            self.framebuffer.reverse(outer_clipped);
        }

        let inner = node.inner;
        let inner_clipped = node.inner_clipped;
        if inner_clipped.is_empty() {
            return;
        }

        match &mut node.content {
            NodeContent::Modal(title) => {
                if !title.is_empty() {
                    self.framebuffer.replace_text(
                        node.outer.top,
                        node.outer.left + 2,
                        node.outer.right - 1,
                        title,
                    );
                }
            }
            NodeContent::Text(content) => self.render_styled_text(
                inner,
                node.intrinsic_size.width,
                &content.text,
                &content.chunks,
                content.overflow,
            ),
            NodeContent::Textarea(tc) => {
                let mut tb = tc.buffer.borrow_mut();
                let mut destination = Rect {
                    left: inner_clipped.left,
                    top: inner_clipped.top,
                    right: inner_clipped.right,
                    bottom: inner_clipped.bottom,
                };

                if !tc.single_line {
                    // Account for the scrollbar.
                    destination.right -= 1;
                }

                if let Some(res) =
                    tb.render(tc.scroll_offset, destination, tc.has_focus, &mut self.framebuffer)
                {
                    tc.scroll_offset_x_max = res.visual_pos_x_max;
                }

                if !tc.single_line {
                    // Render the scrollbar.
                    let track = Rect {
                        left: inner_clipped.right - 1,
                        top: inner_clipped.top,
                        right: inner_clipped.right,
                        bottom: inner_clipped.bottom,
                    };
                    tc.thumb_height = self.framebuffer.draw_scrollbar(
                        inner_clipped,
                        track,
                        tc.scroll_offset.y,
                        tb.visual_line_count() + inner.height() - 1,
                    );
                }
            }
            NodeContent::Scrollarea(sc) => {
                let content = node.children.first.unwrap().borrow();
                let track = Rect {
                    left: inner.right,
                    top: inner.top,
                    right: inner.right + 1,
                    bottom: inner.bottom,
                };
                sc.thumb_height = self.framebuffer.draw_scrollbar(
                    outer_clipped,
                    track,
                    sc.scroll_offset.y,
                    content.intrinsic_size.height,
                );
            }
            _ => {}
        }

        for child in Tree::iterate_siblings(node.children.first) {
            let mut child = child.borrow_mut();
            self.render_node(&mut child);
        }
    }

    fn render_styled_text(
        &mut self,
        target: Rect,
        actual_width: CoordType,
        text: &str,
        chunks: &[StyledTextChunk],
        overflow: Overflow,
    ) {
        let target_width = target.width();
        // The section of `text` that is skipped by the ellipsis.
        let mut skipped = 0..0;
        // The number of columns skipped by the ellipsis.
        let mut skipped_cols = 0;

        if overflow == Overflow::Clip || target_width >= actual_width {
            self.framebuffer.replace_text(target.top, target.left, target.right, text);
        } else {
            let bytes = text.as_bytes();
            let mut cfg = unicode::MeasurementConfig::new(&bytes);

            match overflow {
                Overflow::Clip => unreachable!(),
                Overflow::TruncateHead => {
                    let beg = cfg.goto_visual(Point { x: actual_width - target_width + 1, y: 0 });
                    skipped = 0..beg.offset;
                    skipped_cols = beg.visual_pos.x - 1;
                }
                Overflow::TruncateMiddle => {
                    let mid_beg_x = (target_width - 1) / 2;
                    let mid_end_x = actual_width - target_width / 2;
                    let beg = cfg.goto_visual(Point { x: mid_beg_x, y: 0 });
                    let end = cfg.goto_visual(Point { x: mid_end_x, y: 0 });
                    skipped = beg.offset..end.offset;
                    skipped_cols = end.visual_pos.x - beg.visual_pos.x - 1;
                }
                Overflow::TruncateTail => {
                    let end = cfg.goto_visual(Point { x: target_width - 1, y: 0 });
                    skipped_cols = actual_width - end.visual_pos.x - 1;
                    skipped = end.offset..text.len();
                }
            }

            let scratch = scratch_arena(None);

            let mut modified = ArenaString::new_in(&scratch);
            modified.reserve(text.len() + 3);
            modified.push_str(&text[..skipped.start]);
            modified.push('…');
            modified.push_str(&text[skipped.end..]);

            self.framebuffer.replace_text(target.top, target.left, target.right, &modified);
        }

        if !chunks.is_empty() {
            let bytes = text.as_bytes();
            let mut cfg = unicode::MeasurementConfig::new(&bytes).with_cursor(unicode::Cursor {
                visual_pos: Point { x: target.left, y: 0 },
                ..Default::default()
            });

            let mut iter = chunks.iter().peekable();

            while let Some(chunk) = iter.next() {
                let beg = chunk.offset;
                let end = iter.peek().map_or(text.len(), |c| c.offset);

                if beg >= skipped.start && end <= skipped.end {
                    // Chunk is fully inside the text skipped by the ellipsis.
                    // We don't need to render it at all.
                    continue;
                }

                if beg < skipped.start {
                    let beg = cfg.goto_offset(beg).visual_pos.x;
                    let end = cfg.goto_offset(end.min(skipped.start)).visual_pos.x;
                    let rect =
                        Rect { left: beg, top: target.top, right: end, bottom: target.bottom };
                    self.framebuffer.blend_fg(rect, chunk.fg);
                    self.framebuffer.replace_attr(rect, chunk.attr, chunk.attr);
                }

                if end > skipped.end {
                    let beg = cfg.goto_offset(beg.max(skipped.end)).visual_pos.x - skipped_cols;
                    let end = cfg.goto_offset(end).visual_pos.x - skipped_cols;
                    let rect =
                        Rect { left: beg, top: target.top, right: end, bottom: target.bottom };
                    self.framebuffer.blend_fg(rect, chunk.fg);
                    self.framebuffer.replace_attr(rect, chunk.attr, chunk.attr);
                }
            }
        }
    }

    /// Outputs a debug string of the layout and focus tree.
    pub fn debug_layout<'a>(&mut self, arena: &'a Arena) -> ArenaString<'a> {
        let mut result = ArenaString::new_in(arena);
        result.push_str("general:\r\n- focus_path:\r\n");

        for &id in &self.focused_node_path {
            _ = write!(result, "  - {id:016x}\r\n");
        }

        result.push_str("\r\ntree:\r\n");

        for root in self.prev_tree.iterate_roots() {
            Tree::visit_all(root, root, true, |node| {
                let node = node.borrow();
                let depth = node.depth;
                result.push_repeat(' ', depth * 2);
                _ = write!(result, "- id: {:016x}\r\n", node.id);

                result.push_repeat(' ', depth * 2);
                _ = write!(result, "  classname:    {}\r\n", node.classname);

                if depth == 0
                    && let Some(parent) = node.parent
                {
                    let parent = parent.borrow();
                    result.push_repeat(' ', depth * 2);
                    _ = write!(result, "  parent:       {:016x}\r\n", parent.id);
                }

                result.push_repeat(' ', depth * 2);
                _ = write!(
                    result,
                    "  intrinsic:    {{{}, {}}}\r\n",
                    node.intrinsic_size.width, node.intrinsic_size.height
                );

                result.push_repeat(' ', depth * 2);
                _ = write!(
                    result,
                    "  outer:        {{{}, {}, {}, {}}}\r\n",
                    node.outer.left, node.outer.top, node.outer.right, node.outer.bottom
                );

                result.push_repeat(' ', depth * 2);
                _ = write!(
                    result,
                    "  inner:        {{{}, {}, {}, {}}}\r\n",
                    node.inner.left, node.inner.top, node.inner.right, node.inner.bottom
                );

                if node.attributes.bordered {
                    result.push_repeat(' ', depth * 2);
                    result.push_str("  bordered:     true\r\n");
                }

                if node.attributes.bg != 0 {
                    result.push_repeat(' ', depth * 2);
                    _ = write!(result, "  bg:           #{:08x}\r\n", node.attributes.bg);
                }

                if node.attributes.fg != 0 {
                    result.push_repeat(' ', depth * 2);
                    _ = write!(result, "  fg:           #{:08x}\r\n", node.attributes.fg);
                }

                if self.is_node_focused(node.id) {
                    result.push_repeat(' ', depth * 2);
                    result.push_str("  focused:      true\r\n");
                }

                match &node.content {
                    NodeContent::Text(content) => {
                        result.push_repeat(' ', depth * 2);
                        _ = write!(result, "  text:         \"{}\"\r\n", &content.text);
                    }
                    NodeContent::Textarea(content) => {
                        let tb = content.buffer.borrow();
                        let tb = &*tb;
                        result.push_repeat(' ', depth * 2);
                        _ = write!(result, "  textarea:     {tb:p}\r\n");
                    }
                    NodeContent::Scrollarea(..) => {
                        result.push_repeat(' ', depth * 2);
                        result.push_str("  scrollable:   true\r\n");
                    }
                    _ => {}
                }

                VisitControl::Continue
            });
        }

        result
    }

    fn was_mouse_down_on_node(&self, id: u64) -> bool {
        self.mouse_down_node_path.last() == Some(&id)
    }

    fn was_mouse_down_on_subtree(&self, node: &Node) -> bool {
        self.mouse_down_node_path.get(node.depth) == Some(&node.id)
    }

    fn is_node_focused(&self, id: u64) -> bool {
        // We construct the focused_node_path always with at least 1 element (the root id).
        unsafe { *self.focused_node_path.last().unwrap_unchecked() == id }
    }

    fn is_subtree_focused(&self, node: &Node) -> bool {
        self.focused_node_path.get(node.depth) == Some(&node.id)
    }

    fn is_subtree_focused_alt(&self, id: u64, depth: usize) -> bool {
        self.focused_node_path.get(depth) == Some(&id)
    }

    fn pop_focusable_node(&mut self, pop_minimum: usize) -> bool {
        let last_before = self.focused_node_path.last().cloned().unwrap_or(0);

        // Remove `pop_minimum`-many nodes from the end of the focus path.
        let path = &self.focused_node_path[..];
        let path = &path[..path.len().saturating_sub(pop_minimum)];
        let mut len = 0;

        for (i, &id) in path.iter().enumerate() {
            // Truncate the path so that it only contains nodes that still exist.
            let Some(node) = self.prev_node_map.get(id) else {
                break;
            };

            let n = node.borrow();
            // If the caller requested upward movement, pop out of the current focus void, if any.
            // This is kind of janky, to be fair.
            if pop_minimum != 0 && n.attributes.focus_void {
                break;
            }

            // Skip over those that aren't focusable.
            if n.attributes.focusable {
                // At this point `n.depth == i` should be true,
                // but I kind of don't want to rely on that.
                len = i + 1;
            }
        }

        self.focused_node_path.truncate(len);

        // If it's empty now, push `ROOT_ID` because there must always be >=1 element.
        if self.focused_node_path.is_empty() {
            self.focused_node_path.push(ROOT_ID);
        }

        // Return true if the focus path changed.
        let last_after = self.focused_node_path.last().cloned().unwrap_or(0);
        last_before != last_after
    }

    // Scroll the focused node(s) into view inside scrollviews
    fn scroll_to_focused(&mut self) -> bool {
        let focused_id = self.focused_node_path.last().cloned().unwrap_or(0);
        if self.focused_node_for_scrolling == focused_id {
            return false;
        }

        let Some(node) = self.prev_node_map.get(focused_id) else {
            // Node not found because we're using the old layout tree.
            // Retry in the next rendering loop.
            return true;
        };

        let mut node = node.borrow_mut();
        let mut scroll_to = node.outer;

        while node.parent.is_some() && node.attributes.float.is_none() {
            let n = &mut *node;
            if let NodeContent::Scrollarea(sc) = &mut n.content {
                let off_y = sc.scroll_offset.y.max(0);
                let mut y = off_y;
                y = y.min(scroll_to.top - n.inner.top + off_y);
                y = y.max(scroll_to.bottom - n.inner.bottom + off_y);
                sc.scroll_offset.y = y;
                scroll_to = n.outer;
            }
            node = node.parent.unwrap().borrow_mut();
        }

        self.focused_node_for_scrolling = focused_id;
        true
    }
}

/// Context is a temporary object that is created for each frame.
/// Its primary purpose is to build a UI tree.
pub struct Context<'a, 'input> {
    tui: &'a mut Tui,

    /// Current text input, if any.
    input_text: Option<&'input str>,
    /// Current keyboard input, if any.
    input_keyboard: Option<InputKey>,
    input_mouse_modifiers: InputKeyMod,
    input_mouse_click: CoordType,
    /// By how much the mouse wheel was scrolled since the last frame.
    input_scroll_delta: Point,
    input_consumed: bool,

    tree: Tree<'a>,
    last_modal: Option<&'a NodeCell<'a>>,
    focused_node: Option<&'a NodeCell<'a>>,
    next_block_id_mixin: u64,
    needs_settling: bool,

    #[cfg(debug_assertions)]
    seen_ids: HashSet<u64>,
}

impl<'a> Drop for Context<'a, '_> {
    fn drop(&mut self) {
        let tui: &'a mut Tui = unsafe { mem::transmute(&mut *self.tui) };
        tui.report_context_completion(self);
    }
}

impl<'a> Context<'a, '_> {
    /// Get an arena for temporary allocations such as for [`arena_format`].
    pub fn arena(&self) -> &'a Arena {
        // TODO:
        // `Context` borrows `Tui` for lifetime 'a, so `self.tui` should be `&'a Tui`, right?
        // And if I do `&self.tui.arena` then that should be 'a too, right?
        // Searching for and failing to find a workaround for this was _very_ annoying.
        //
        // SAFETY: Both the returned reference and its allocations outlive &self.
        unsafe { mem::transmute::<&'_ Arena, &'a Arena>(&self.tui.arena_next) }
    }

    /// Returns the viewport size.
    pub fn size(&self) -> Size {
        self.tui.size()
    }

    /// Returns an indexed color from the framebuffer.
    #[inline]
    pub fn indexed(&self, index: IndexedColor) -> u32 {
        self.tui.framebuffer.indexed(index)
    }

    /// Returns an indexed color from the framebuffer with the given alpha.
    /// See [`Framebuffer::indexed_alpha()`].
    #[inline]
    pub fn indexed_alpha(&self, index: IndexedColor, numerator: u32, denominator: u32) -> u32 {
        self.tui.framebuffer.indexed_alpha(index, numerator, denominator)
    }

    /// Returns a color in contrast with the given color.
    /// See [`Framebuffer::contrasted()`].
    pub fn contrasted(&self, color: u32) -> u32 {
        self.tui.framebuffer.contrasted(color)
    }

    /// Returns the clipboard.
    pub fn clipboard_ref(&self) -> &Clipboard {
        &self.tui.clipboard
    }

    /// Returns the clipboard (mutable).
    pub fn clipboard_mut(&mut self) -> &mut Clipboard {
        &mut self.tui.clipboard
    }

    /// Tell the UI framework that your state changed and you need another layout pass.
    pub fn needs_rerender(&mut self) {
        // If this hits, the call stack is responsible is trying to deadlock you.
        debug_assert!(self.tui.settling_have < 15);
        self.needs_settling = true;
    }

    /// Begins a generic UI block (container) with a unique ID derived from the given `classname`.
    pub fn block_begin(&mut self, classname: &'static str) {
        let parent = self.tree.current_node;

        let mut id = hash_str(parent.borrow().id, classname);
        if self.next_block_id_mixin != 0 {
            id = hash(id, &self.next_block_id_mixin.to_ne_bytes());
            self.next_block_id_mixin = 0;
        }

        // If this hits, you have tried to create a block with the same ID as a previous one
        // somewhere up this call stack. Change the classname, or use next_block_id_mixin().
        // TODO: HashMap
        #[cfg(debug_assertions)]
        if !self.seen_ids.insert(id) {
            panic!("Duplicate node ID: {id:x}");
        }

        let node = Tree::alloc_node(self.arena());
        {
            let mut n = node.borrow_mut();
            n.id = id;
            n.classname = classname;
        }

        self.tree.push_child(node);
    }

    /// Ends the current UI block, returning to its parent container.
    pub fn block_end(&mut self) {
        self.tree.pop_stack();
        self.block_end_move_focus();
    }

    fn block_end_move_focus(&mut self) {
        // At this point, it's more like "focus_well?" instead of "focus_well!".
        let focus_well = self.tree.last_node;

        // Remember the focused node, if any, because once the code below runs,
        // we need it for the `Tree::visit_all` call.
        if self.is_focused() {
            self.focused_node = Some(focus_well);
        }

        // The mere fact that there's a `focused_node` indicates that we're the
        // first `block_end()` call that's a focus well and also contains the focus.
        let Some(focused) = self.focused_node else {
            return;
        };

        // Filter down to nodes that are focus wells and contain the focus. They're
        // basically the "tab container". We test for the node depth to ensure that
        // we don't accidentally pick a focus well next to or inside the focused node.
        {
            let n = focus_well.borrow();
            if !n.attributes.focus_well || n.depth > focused.borrow().depth {
                return;
            }
        }

        // Filter down to Tab/Shift+Tab inputs.
        if self.input_consumed {
            return;
        }
        let Some(input) = self.input_keyboard else {
            return;
        };
        if !matches!(input, SHIFT_TAB | vk::TAB) {
            return;
        }

        let forward = input == vk::TAB;
        let mut focused_start = focused;
        let mut focused_next = focused;

        // We may be in a focus void right now (= doesn't want to be tabbed into),
        // so first we must go up the tree until we're outside of it.
        loop {
            if ptr::eq(focused_start, focus_well) {
                // If we hit the root / focus well, we weren't in a focus void,
                // and can reset `focused_before` to the current focused node.
                focused_start = focused;
                break;
            }

            focused_start = focused_start.borrow().parent.unwrap();
            if focused_start.borrow().attributes.focus_void {
                break;
            }
        }

        Tree::visit_all(focus_well, focused_start, forward, |node| {
            let n = node.borrow();
            if n.attributes.focusable && !ptr::eq(node, focused_start) {
                focused_next = node;
                VisitControl::Stop
            } else if n.attributes.focus_void {
                VisitControl::SkipChildren
            } else {
                VisitControl::Continue
            }
        });

        if ptr::eq(focused_next, focused_start) {
            return;
        }

        Tui::build_node_path(Some(focused_next), &mut self.tui.focused_node_path);
        self.set_input_consumed();
        self.needs_rerender();
    }

    /// Mixes in an extra value to the next UI block's ID for uniqueness.
    /// Use this when you build a list of items with the same classname.
    pub fn next_block_id_mixin(&mut self, id: u64) {
        self.next_block_id_mixin = id;
    }

    fn attr_focusable(&mut self) {
        let mut last_node = self.tree.last_node.borrow_mut();
        last_node.attributes.focusable = true;
    }

    /// If this is the first time the current node is being drawn,
    /// it'll steal the active focus.
    pub fn focus_on_first_present(&mut self) {
        let steal = {
            let mut last_node = self.tree.last_node.borrow_mut();
            last_node.attributes.focusable = true;
            self.tui.prev_node_map.get(last_node.id).is_none()
        };
        if steal {
            self.steal_focus();
        }
    }

    /// Steals the focus unconditionally.
    pub fn steal_focus(&mut self) {
        self.steal_focus_for(self.tree.last_node);
    }

    fn steal_focus_for(&mut self, node: &NodeCell<'a>) {
        if !self.tui.is_node_focused(node.borrow().id) {
            Tui::build_node_path(Some(node), &mut self.tui.focused_node_path);
            self.needs_rerender();
        }
    }

    /// If the current node owns the focus, it'll be given to the parent.
    pub fn toss_focus_up(&mut self) {
        if self.tui.pop_focusable_node(1) {
            self.needs_rerender();
        }
    }

    /// If the parent node owns the focus, it'll be given to the current node.
    pub fn inherit_focus(&mut self) {
        let mut last_node = self.tree.last_node.borrow_mut();
        let Some(parent) = last_node.parent else {
            return;
        };

        last_node.attributes.focusable = true;

        // Mark the parent as focusable, so that if the user presses Escape,
        // and `block_end` bubbles the focus up the tree, it'll stop on our parent,
        // which will then focus us on the next iteration.
        let mut parent = parent.borrow_mut();
        parent.attributes.focusable = true;

        if self.tui.is_node_focused(parent.id) {
            self.needs_rerender();
            self.tui.focused_node_path.push(last_node.id);
        }
    }

    /// Causes keyboard focus to be unable to escape this node and its children.
    /// It's a "well" because if the focus is inside it, it can't escape.
    pub fn attr_focus_well(&mut self) {
        let mut last_node = self.tree.last_node.borrow_mut();
        last_node.attributes.focus_well = true;
    }

    /// Explicitly sets the intrinsic size of the current node.
    /// The intrinsic size is the size the node ideally wants to be.
    pub fn attr_intrinsic_size(&mut self, size: Size) {
        let mut last_node = self.tree.last_node.borrow_mut();
        last_node.intrinsic_size = size;
        last_node.intrinsic_size_set = true;
    }

    /// Turns the current node into a floating node,
    /// like a popup, modal or a tooltip.
    pub fn attr_float(&mut self, spec: FloatSpec) {
        let last_node = self.tree.last_node;
        let anchor = {
            let ln = last_node.borrow();
            match spec.anchor {
                Anchor::Last if ln.siblings.prev.is_some() => ln.siblings.prev,
                Anchor::Last | Anchor::Parent => ln.parent,
                // By not giving such floats a parent, they get the same origin as the original root node,
                // but they also gain their own "root id" in the tree. That way, their focus path is totally unique,
                // which means that we can easily check if a modal is open by calling `is_focused()` on the original root.
                Anchor::Root => None,
            }
        };

        self.tree.move_node_to_root(last_node, anchor);

        let mut ln = last_node.borrow_mut();
        ln.attributes.focus_well = true;
        ln.attributes.float = Some(FloatAttributes {
            gravity_x: spec.gravity_x.clamp(0.0, 1.0),
            gravity_y: spec.gravity_y.clamp(0.0, 1.0),
            offset_x: spec.offset_x,
            offset_y: spec.offset_y,
        });
        ln.attributes.bg = self.tui.floater_default_bg;
        ln.attributes.fg = self.tui.floater_default_fg;
    }

    /// Gives the current node a border.
    pub fn attr_border(&mut self) {
        let mut last_node = self.tree.last_node.borrow_mut();
        last_node.attributes.bordered = true;
    }

    /// Sets the current node's position inside the parent.
    pub fn attr_position(&mut self, align: Position) {
        let mut last_node = self.tree.last_node.borrow_mut();
        last_node.attributes.position = align;
    }

    /// Assigns padding to the current node.
    pub fn attr_padding(&mut self, padding: Rect) {
        let mut last_node = self.tree.last_node.borrow_mut();
        last_node.attributes.padding = Self::normalize_rect(padding);
    }

    fn normalize_rect(rect: Rect) -> Rect {
        Rect {
            left: rect.left.max(0),
            top: rect.top.max(0),
            right: rect.right.max(0),
            bottom: rect.bottom.max(0),
        }
    }

    /// Assigns a sRGB background color to the current node.
    pub fn attr_background_rgba(&mut self, bg: u32) {
        let mut last_node = self.tree.last_node.borrow_mut();
        last_node.attributes.bg = bg;
    }

    /// Assigns a sRGB foreground color to the current node.
    pub fn attr_foreground_rgba(&mut self, fg: u32) {
        let mut last_node = self.tree.last_node.borrow_mut();
        last_node.attributes.fg = fg;
    }

    /// Applies reverse-video to the current node:
    /// Background and foreground colors are swapped.
    pub fn attr_reverse(&mut self) {
        let mut last_node = self.tree.last_node.borrow_mut();
        last_node.attributes.reverse = true;
    }

    /// Checks if the current keyboard input matches the given shortcut,
    /// consumes it if it is and returns true in that case.
    pub fn consume_shortcut(&mut self, shortcut: InputKey) -> bool {
        if !self.input_consumed && self.input_keyboard == Some(shortcut) {
            self.set_input_consumed();
            true
        } else {
            false
        }
    }

    /// Returns current keyboard input, if any.
    /// Returns None if the input was already consumed.
    pub fn keyboard_input(&self) -> Option<InputKey> {
        if self.input_consumed { None } else { self.input_keyboard }
    }

    #[inline]
    pub fn set_input_consumed(&mut self) {
        debug_assert!(!self.input_consumed);
        self.set_input_consumed_unchecked();
    }

    #[inline]
    fn set_input_consumed_unchecked(&mut self) {
        self.input_consumed = true;
    }

    /// Returns whether the mouse was pressed down on the current node.
    pub fn was_mouse_down(&mut self) -> bool {
        let last_node = self.tree.last_node.borrow();
        self.tui.was_mouse_down_on_node(last_node.id)
    }

    /// Returns whether the mouse was pressed down on the current node's subtree.
    pub fn contains_mouse_down(&mut self) -> bool {
        let last_node = self.tree.last_node.borrow();
        self.tui.was_mouse_down_on_subtree(&last_node)
    }

    /// Returns whether the current node is focused.
    pub fn is_focused(&mut self) -> bool {
        let last_node = self.tree.last_node.borrow();
        self.tui.is_node_focused(last_node.id)
    }

    /// Returns whether the current node's subtree is focused.
    pub fn contains_focus(&mut self) -> bool {
        let last_node = self.tree.last_node.borrow();
        self.tui.is_subtree_focused(&last_node)
    }

    /// Begins a modal window. Call [`Context::modal_end()`].
    pub fn modal_begin(&mut self, classname: &'static str, title: &str) {
        self.block_begin(classname);
        self.attr_float(FloatSpec {
            anchor: Anchor::Root,
            gravity_x: 0.5,
            gravity_y: 0.5,
            offset_x: self.tui.size.width as f32 * 0.5,
            offset_y: self.tui.size.height as f32 * 0.5,
        });
        self.attr_border();
        self.attr_background_rgba(self.tui.modal_default_bg);
        self.attr_foreground_rgba(self.tui.modal_default_fg);
        self.attr_focus_well();
        self.focus_on_first_present();

        let mut last_node = self.tree.last_node.borrow_mut();
        let title = if title.is_empty() {
            ArenaString::new_in(self.arena())
        } else {
            arena_format!(self.arena(), " {} ", title)
        };
        last_node.content = NodeContent::Modal(title);
        self.last_modal = Some(self.tree.last_node);
    }

    /// Ends the current modal window block.
    /// Returns true if the user pressed Escape (a request to close).
    pub fn modal_end(&mut self) -> bool {
        self.block_end();

        // Consume the input unconditionally, so that the root (the "main window")
        // doesn't accidentally receive any input via `consume_shortcut()`.
        if self.contains_focus() {
            let exit = !self.input_consumed && self.input_keyboard == Some(vk::ESCAPE);
            self.set_input_consumed_unchecked();
            exit
        } else {
            false
        }
    }

    /// Begins a table block. Call [`Context::table_end()`].
    /// Tables are the primary way to create a grid layout,
    /// and to layout controls on a single row (= a table with 1 row).
    pub fn table_begin(&mut self, classname: &'static str) {
        self.block_begin(classname);

        let mut last_node = self.tree.last_node.borrow_mut();
        last_node.content = NodeContent::Table(TableContent {
            columns: Vec::new_in(self.arena()),
            cell_gap: Default::default(),
        });
    }

    /// Assigns widths to the columns of the current table.
    /// By default, the table will left-align all columns.
    pub fn table_set_columns(&mut self, columns: &[CoordType]) {
        let mut last_node = self.tree.last_node.borrow_mut();
        if let NodeContent::Table(spec) = &mut last_node.content {
            spec.columns.clear();
            spec.columns.extend_from_slice(columns);
        } else {
            debug_assert!(false);
        }
    }

    /// Assigns the gap between cells in the current table.
    pub fn table_set_cell_gap(&mut self, cell_gap: Size) {
        let mut last_node = self.tree.last_node.borrow_mut();
        if let NodeContent::Table(spec) = &mut last_node.content {
            spec.cell_gap = cell_gap;
        } else {
            debug_assert!(false);
        }
    }

    /// Starts the next row in the current table.
    pub fn table_next_row(&mut self) {
        {
            let current_node = self.tree.current_node.borrow();

            // If this is the first call to table_next_row() inside a new table, the
            // current_node will refer to the table. Otherwise, it'll refer to the current row.
            if !matches!(current_node.content, NodeContent::Table(_)) {
                let Some(parent) = current_node.parent else {
                    return;
                };

                let parent = parent.borrow();
                // Neither the current nor its parent nodes are a table?
                // You definitely called this outside of a table block.
                debug_assert!(matches!(parent.content, NodeContent::Table(_)));

                self.block_end();
                self.table_end_row();

                self.next_block_id_mixin(parent.child_count as u64);
            }
        }

        self.block_begin("row");
    }

    fn table_end_row(&mut self) {
        self.table_move_focus(vk::LEFT, vk::RIGHT);
    }

    /// Ends the current table block.
    pub fn table_end(&mut self) {
        let current_node = self.tree.current_node.borrow();

        // If this is the first call to table_next_row() inside a new table, the
        // current_node will refer to the table. Otherwise, it'll refer to the current row.
        if !matches!(current_node.content, NodeContent::Table(_)) {
            self.block_end();
            self.table_end_row();
        }

        self.block_end(); // table
        self.table_move_focus(vk::UP, vk::DOWN);
    }

    fn table_move_focus(&mut self, prev_key: InputKey, next_key: InputKey) {
        // Filter down to table rows that are focused.
        if !self.contains_focus() {
            return;
        }

        // Filter down to our prev/next inputs.
        if self.input_consumed {
            return;
        }
        let Some(input) = self.input_keyboard else {
            return;
        };
        if input != prev_key && input != next_key {
            return;
        }

        let container = self.tree.last_node;
        let Some(&focused_id) = self.tui.focused_node_path.get(container.borrow().depth + 1) else {
            return;
        };

        let mut prev_next = NodeSiblings { prev: None, next: None };
        let mut focused = None;

        // Iterate through the cells in the row / the rows in the table, looking for focused_id.
        // Take note of the previous and next focusable cells / rows around the focused one.
        for cell in Tree::iterate_siblings(container.borrow().children.first) {
            let n = cell.borrow();
            if n.id == focused_id {
                focused = Some(cell);
            } else if n.attributes.focusable {
                if focused.is_none() {
                    prev_next.prev = Some(cell);
                } else {
                    prev_next.next = Some(cell);
                    break;
                }
            }
        }

        if focused.is_none() {
            return;
        }

        let forward = input == next_key;
        let children_idx = if forward { NodeChildren::FIRST } else { NodeChildren::LAST };
        let siblings_idx = if forward { NodeSiblings::NEXT } else { NodeSiblings::PREV };
        let Some(focused_next) =
            prev_next.get(siblings_idx).or_else(|| container.borrow().children.get(children_idx))
        else {
            return;
        };

        Tui::build_node_path(Some(focused_next), &mut self.tui.focused_node_path);
        self.set_input_consumed();
        self.needs_rerender();
    }

    /// Creates a simple text label.
    pub fn label(&mut self, classname: &'static str, text: &str) {
        self.styled_label_begin(classname);
        self.styled_label_add_text(text);
        self.styled_label_end();
    }

    /// Creates a styled text label.
    ///
    /// # Example
    /// ```
    /// use edit::framebuffer::IndexedColor;
    /// use edit::tui::Context;
    ///
    /// fn draw(ctx: &mut Context) {
    ///     ctx.styled_label_begin("label");
    ///     // Shows "Hello" in the inherited foreground color.
    ///     ctx.styled_label_add_text("Hello");
    ///     // Shows ", World!" next to "Hello" in red.
    ///     ctx.styled_label_set_foreground(ctx.indexed(IndexedColor::Red));
    ///     ctx.styled_label_add_text(", World!");
    /// }
    /// ```
    pub fn styled_label_begin(&mut self, classname: &'static str) {
        self.block_begin(classname);
        self.tree.last_node.borrow_mut().content = NodeContent::Text(TextContent {
            text: ArenaString::new_in(self.arena()),
            chunks: Vec::with_capacity_in(4, self.arena()),
            overflow: Overflow::Clip,
        });
    }

    /// Changes the active pencil color of the current label.
    pub fn styled_label_set_foreground(&mut self, fg: u32) {
        let mut node = self.tree.last_node.borrow_mut();
        let NodeContent::Text(content) = &mut node.content else {
            unreachable!();
        };

        let last = content.chunks.last().unwrap_or(&INVALID_STYLED_TEXT_CHUNK);
        if last.offset != content.text.len() && last.fg != fg {
            content.chunks.push(StyledTextChunk {
                offset: content.text.len(),
                fg,
                attr: last.attr,
            });
        }
    }

    /// Changes the active pencil attributes of the current label.
    pub fn styled_label_set_attributes(&mut self, attr: Attributes) {
        let mut node = self.tree.last_node.borrow_mut();
        let NodeContent::Text(content) = &mut node.content else {
            unreachable!();
        };

        let last = content.chunks.last().unwrap_or(&INVALID_STYLED_TEXT_CHUNK);
        if last.offset != content.text.len() && last.attr != attr {
            content.chunks.push(StyledTextChunk { offset: content.text.len(), fg: last.fg, attr });
        }
    }

    /// Adds text to the current label.
    pub fn styled_label_add_text(&mut self, text: &str) {
        let mut node = self.tree.last_node.borrow_mut();
        let NodeContent::Text(content) = &mut node.content else {
            unreachable!();
        };

        content.text.push_str(text);
    }

    /// Ends the current label block.
    pub fn styled_label_end(&mut self) {
        {
            let mut last_node = self.tree.last_node.borrow_mut();
            let NodeContent::Text(content) = &last_node.content else {
                return;
            };

            let cursor = unicode::MeasurementConfig::new(&content.text.as_bytes())
                .goto_visual(Point { x: CoordType::MAX, y: 0 });
            last_node.intrinsic_size.width = cursor.visual_pos.x;
            last_node.intrinsic_size.height = 1;
            last_node.intrinsic_size_set = true;
        }

        self.block_end();
    }

    /// Sets the overflow behavior of the current label.
    pub fn attr_overflow(&mut self, overflow: Overflow) {
        let mut last_node = self.tree.last_node.borrow_mut();
        let NodeContent::Text(content) = &mut last_node.content else {
            return;
        };

        content.overflow = overflow;
    }

    /// Creates a button with the given text.
    /// Returns true if the button was activated.
    pub fn button(&mut self, classname: &'static str, text: &str, style: ButtonStyle) -> bool {
        self.button_label(classname, text, style);
        self.attr_focusable();
        if self.is_focused() {
            self.attr_reverse();
        }
        self.button_activated()
    }

    /// Creates a checkbox with the given text.
    /// Returns true if the checkbox was activated.
    pub fn checkbox(&mut self, classname: &'static str, text: &str, checked: &mut bool) -> bool {
        self.styled_label_begin(classname);
        self.attr_focusable();
        if self.is_focused() {
            self.attr_reverse();
        }
        self.styled_label_add_text(if *checked { "[🗹 " } else { "[☐ " });
        self.styled_label_add_text(text);
        self.styled_label_add_text("]");
        self.styled_label_end();

        let activated = self.button_activated();
        if activated {
            *checked = !*checked;
        }
        activated
    }

    fn button_activated(&mut self) -> bool {
        if !self.input_consumed
            && ((self.input_mouse_click != 0 && self.contains_mouse_down())
                || self.input_keyboard == Some(vk::RETURN)
                || self.input_keyboard == Some(vk::SPACE))
            && self.is_focused()
        {
            self.set_input_consumed();
            true
        } else {
            false
        }
    }

    /// Creates a text input field.
    /// Returns true if the text contents changed.
    pub fn editline(&mut self, classname: &'static str, text: &mut dyn WriteableDocument) -> bool {
        self.textarea_internal(classname, TextBufferPayload::Editline(text))
    }

    /// Creates a text area.
    pub fn textarea(&mut self, classname: &'static str, tb: RcTextBuffer) {
        self.textarea_internal(classname, TextBufferPayload::Textarea(tb));
    }

    fn textarea_internal(&mut self, classname: &'static str, payload: TextBufferPayload) -> bool {
        self.block_begin(classname);
        self.block_end();

        let mut node = self.tree.last_node.borrow_mut();
        let node = &mut *node;
        let single_line = match &payload {
            TextBufferPayload::Editline(_) => true,
            TextBufferPayload::Textarea(_) => false,
        };

        let buffer = {
            let buffers = &mut self.tui.cached_text_buffers;

            let cached = match buffers.iter_mut().find(|t| t.node_id == node.id) {
                Some(cached) => {
                    if let TextBufferPayload::Textarea(tb) = &payload {
                        cached.editor = tb.clone();
                    };
                    cached.seen = true;
                    cached
                }
                None => {
                    // If the node is not in the cache, we need to create a new one.
                    buffers.push(CachedTextBuffer {
                        node_id: node.id,
                        editor: match &payload {
                            TextBufferPayload::Editline(_) => TextBuffer::new_rc(true).unwrap(),
                            TextBufferPayload::Textarea(tb) => tb.clone(),
                        },
                        seen: true,
                    });
                    buffers.last_mut().unwrap()
                }
            };

            // SAFETY: *Assuming* that there are no duplicate node IDs in the tree that
            // would cause this cache slot to be overwritten, then this operation is safe.
            // The text buffer cache will keep the buffer alive for us long enough.
            unsafe { mem::transmute(&*cached.editor) }
        };

        node.content = NodeContent::Textarea(TextareaContent {
            buffer,
            scroll_offset: Default::default(),
            scroll_offset_y_drag_start: CoordType::MIN,
            scroll_offset_x_max: 0,
            thumb_height: 0,
            preferred_column: 0,
            single_line,
            has_focus: self.tui.is_node_focused(node.id),
        });

        let content = match node.content {
            NodeContent::Textarea(ref mut content) => content,
            _ => unreachable!(),
        };

        if let TextBufferPayload::Editline(text) = &payload {
            content.buffer.borrow_mut().copy_from_str(*text);
        }

        if let Some(node_prev) = self.tui.prev_node_map.get(node.id) {
            let node_prev = node_prev.borrow();
            if let NodeContent::Textarea(content_prev) = &node_prev.content {
                content.scroll_offset = content_prev.scroll_offset;
                content.scroll_offset_y_drag_start = content_prev.scroll_offset_y_drag_start;
                content.scroll_offset_x_max = content_prev.scroll_offset_x_max;
                content.thumb_height = content_prev.thumb_height;
                content.preferred_column = content_prev.preferred_column;

                let mut text_width = node_prev.inner.width();
                if !single_line {
                    // Subtract -1 to account for the scrollbar.
                    text_width -= 1;
                }

                let mut make_cursor_visible;
                {
                    let mut tb = content.buffer.borrow_mut();
                    make_cursor_visible = tb.take_cursor_visibility_request();
                    make_cursor_visible |= tb.set_width(text_width);
                }

                make_cursor_visible |= self.textarea_handle_input(content, &node_prev, single_line);

                if make_cursor_visible {
                    self.textarea_make_cursor_visible(content, &node_prev);
                }
            } else {
                debug_assert!(false);
            }
        }

        let dirty;
        {
            let mut tb = content.buffer.borrow_mut();
            dirty = tb.is_dirty();
            if dirty && let TextBufferPayload::Editline(text) = payload {
                tb.save_as_string(text);
            }
        }

        self.textarea_adjust_scroll_offset(content);

        if single_line {
            node.attributes.fg = self.indexed(IndexedColor::Foreground);
            node.attributes.bg = self.indexed(IndexedColor::Background);
            if !content.has_focus {
                node.attributes.fg = self.contrasted(node.attributes.bg);
                node.attributes.bg = self.indexed_alpha(IndexedColor::Background, 1, 2);
            }
        }

        node.attributes.focusable = true;
        node.intrinsic_size.height = content.buffer.borrow().visual_line_count();
        node.intrinsic_size_set = true;

        dirty
    }

    fn textarea_handle_input(
        &mut self,
        tc: &mut TextareaContent,
        node_prev: &Node,
        single_line: bool,
    ) -> bool {
        if self.input_consumed {
            return false;
        }

        let mut tb = tc.buffer.borrow_mut();
        let tb = &mut *tb;
        let mut make_cursor_visible = false;
        let mut change_preferred_column = false;

        if self.tui.mouse_state != InputMouseState::None
            && self.tui.was_mouse_down_on_node(node_prev.id)
        {
            // Scrolling works even if the node isn't focused.
            if self.tui.mouse_state == InputMouseState::Scroll {
                tc.scroll_offset.x += self.input_scroll_delta.x;
                tc.scroll_offset.y += self.input_scroll_delta.y;
                self.set_input_consumed();
            } else if self.tui.is_node_focused(node_prev.id) {
                let mouse = self.tui.mouse_position;
                let inner = node_prev.inner;
                let text_rect = Rect {
                    left: inner.left + tb.margin_width(),
                    top: inner.top,
                    right: inner.right - !single_line as CoordType,
                    bottom: inner.bottom,
                };
                let track_rect = Rect {
                    left: text_rect.right,
                    top: inner.top,
                    right: inner.right,
                    bottom: inner.bottom,
                };
                let pos = Point {
                    x: mouse.x - inner.left - tb.margin_width() + tc.scroll_offset.x,
                    y: mouse.y - inner.top + tc.scroll_offset.y,
                };

                if text_rect.contains(self.tui.mouse_down_position) {
                    if self.tui.mouse_is_drag {
                        tb.selection_update_visual(pos);
                        tc.preferred_column = tb.cursor_visual_pos().x;

                        let height = inner.height();

                        // If the editor is only 1 line tall we can't possibly scroll up or down.
                        if height >= 2 {
                            fn calc(min: CoordType, max: CoordType, mouse: CoordType) -> CoordType {
                                // Otherwise, the scroll zone is up to 3 lines at the top/bottom.
                                let zone_height = ((max - min) / 2).min(3);

                                // The .y positions where the scroll zones begin:
                                // Mouse coordinates above top and below bottom respectively.
                                let scroll_min = min + zone_height;
                                let scroll_max = max - zone_height - 1;

                                // Calculate the delta for scrolling up or down.
                                let delta_min = (mouse - scroll_min).clamp(-zone_height, 0);
                                let delta_max = (mouse - scroll_max).clamp(0, zone_height);

                                // If I didn't mess up my logic here, only one of the two values can possibly be !=0.
                                let idx = 3 + delta_min + delta_max;

                                const SPEEDS: [CoordType; 7] = [-9, -3, -1, 0, 1, 3, 9];
                                let idx = idx.clamp(0, SPEEDS.len() as CoordType) as usize;
                                SPEEDS[idx]
                            }

                            let delta_x = calc(text_rect.left, text_rect.right, mouse.x);
                            let delta_y = calc(text_rect.top, text_rect.bottom, mouse.y);

                            tc.scroll_offset.x += delta_x;
                            tc.scroll_offset.y += delta_y;

                            if delta_x != 0 || delta_y != 0 {
                                self.tui.read_timeout = time::Duration::from_millis(25);
                            }
                        }
                    } else {
                        match self.input_mouse_click {
                            5.. => {}
                            4 => tb.select_all(),
                            3 => tb.select_line(),
                            2 => tb.select_word(),
                            _ => match self.tui.mouse_state {
                                InputMouseState::Left => {
                                    if self.input_mouse_modifiers.contains(kbmod::SHIFT) {
                                        // TODO: Untested because Windows Terminal surprisingly doesn't support Shift+Click.
                                        tb.selection_update_visual(pos);
                                    } else {
                                        tb.cursor_move_to_visual(pos);
                                    }
                                    tc.preferred_column = tb.cursor_visual_pos().x;
                                    make_cursor_visible = true;
                                }
                                _ => return false,
                            },
                        }
                    }
                } else if track_rect.contains(self.tui.mouse_down_position) {
                    if self.tui.mouse_state == InputMouseState::Release {
                        tc.scroll_offset_y_drag_start = CoordType::MIN;
                    } else if self.tui.mouse_is_drag {
                        if tc.scroll_offset_y_drag_start == CoordType::MIN {
                            tc.scroll_offset_y_drag_start = tc.scroll_offset.y;
                        }

                        // The textarea supports 1 height worth of "scrolling beyond the end".
                        // `track_height` is the same as the viewport height.
                        let scrollable_height = tb.visual_line_count() - 1;

                        if scrollable_height > 0 {
                            let trackable = track_rect.height() - tc.thumb_height;
                            let delta_y = mouse.y - self.tui.mouse_down_position.y;
                            tc.scroll_offset.y = tc.scroll_offset_y_drag_start
                                + (delta_y as i64 * scrollable_height as i64 / trackable as i64)
                                    as CoordType;
                        }
                    }
                }

                self.set_input_consumed();
            }

            return make_cursor_visible;
        }

        if !tc.has_focus {
            return false;
        }

        let mut write: &[u8] = &[];

        if let Some(input) = &self.input_text {
            write = input.as_bytes();
        } else if let Some(input) = &self.input_keyboard {
            let key = input.key();
            let modifiers = input.modifiers();

            make_cursor_visible = true;

            match key {
                vk::BACK => {
                    let granularity = if modifiers == kbmod::CTRL {
                        CursorMovement::Word
                    } else {
                        CursorMovement::Grapheme
                    };
                    tb.delete(granularity, -1);
                }
                vk::TAB => {
                    if single_line {
                        // If this is just a simple input field, don't consume Tab (= early return).
                        return false;
                    }
                    tb.indent_change(if modifiers == kbmod::SHIFT { -1 } else { 1 });
                }
                vk::RETURN => {
                    if single_line {
                        // If this is just a simple input field, don't consume Enter (= early return).
                        return false;
                    }
                    write = b"\n";
                }
                vk::ESCAPE => {
                    // If there was a selection, clear it and show the cursor (= fallthrough).
                    if !tb.clear_selection() {
                        if single_line {
                            // If this is just a simple input field, don't consume the escape key
                            // (early return) and don't show the cursor (= return false).
                            return false;
                        }

                        // If this is a textarea, don't show the cursor if
                        // the escape key was pressed and nothing happened.
                        make_cursor_visible = false;
                    }
                }
                vk::PRIOR => {
                    let height = node_prev.inner.height() - 1;

                    // If the cursor was already on the first line,
                    // move it to the start of the buffer.
                    if tb.cursor_visual_pos().y == 0 {
                        tc.preferred_column = 0;
                    }

                    if modifiers == kbmod::SHIFT {
                        tb.selection_update_visual(Point {
                            x: tc.preferred_column,
                            y: tb.cursor_visual_pos().y - height,
                        });
                    } else {
                        tb.cursor_move_to_visual(Point {
                            x: tc.preferred_column,
                            y: tb.cursor_visual_pos().y - height,
                        });
                    }
                }
                vk::NEXT => {
                    let height = node_prev.inner.height() - 1;

                    // If the cursor was already on the last line,
                    // move it to the end of the buffer.
                    if tb.cursor_visual_pos().y >= tb.visual_line_count() - 1 {
                        tc.preferred_column = CoordType::MAX;
                    }

                    if modifiers == kbmod::SHIFT {
                        tb.selection_update_visual(Point {
                            x: tc.preferred_column,
                            y: tb.cursor_visual_pos().y + height,
                        });
                    } else {
                        tb.cursor_move_to_visual(Point {
                            x: tc.preferred_column,
                            y: tb.cursor_visual_pos().y + height,
                        });
                    }

                    if tc.preferred_column == CoordType::MAX {
                        tc.preferred_column = tb.cursor_visual_pos().x;
                    }
                }
                vk::END => {
                    let logical_before = tb.cursor_logical_pos();
                    let destination = if modifiers.contains(kbmod::CTRL) {
                        Point::MAX
                    } else {
                        Point { x: CoordType::MAX, y: tb.cursor_visual_pos().y }
                    };

                    if modifiers.contains(kbmod::SHIFT) {
                        tb.selection_update_visual(destination);
                    } else {
                        tb.cursor_move_to_visual(destination);
                    }

                    if !modifiers.contains(kbmod::CTRL) {
                        let logical_after = tb.cursor_logical_pos();

                        // If word-wrap is enabled and the user presses End the first time,
                        // it moves to the start of the visual line. The second time they
                        // press it, it moves to the start of the logical line.
                        if tb.is_word_wrap_enabled() && logical_after == logical_before {
                            if modifiers == kbmod::SHIFT {
                                tb.selection_update_logical(Point {
                                    x: CoordType::MAX,
                                    y: tb.cursor_logical_pos().y,
                                });
                            } else {
                                tb.cursor_move_to_logical(Point {
                                    x: CoordType::MAX,
                                    y: tb.cursor_logical_pos().y,
                                });
                            }
                        }
                    }
                }
                vk::HOME => {
                    let logical_before = tb.cursor_logical_pos();
                    let destination = if modifiers.contains(kbmod::CTRL) {
                        Default::default()
                    } else {
                        Point { x: 0, y: tb.cursor_visual_pos().y }
                    };

                    if modifiers.contains(kbmod::SHIFT) {
                        tb.selection_update_visual(destination);
                    } else {
                        tb.cursor_move_to_visual(destination);
                    }

                    if !modifiers.contains(kbmod::CTRL) {
                        let mut logical_after = tb.cursor_logical_pos();

                        // If word-wrap is enabled and the user presses Home the first time,
                        // it moves to the start of the visual line. The second time they
                        // press it, it moves to the start of the logical line.
                        if tb.is_word_wrap_enabled() && logical_after == logical_before {
                            if modifiers == kbmod::SHIFT {
                                tb.selection_update_logical(Point {
                                    x: 0,
                                    y: tb.cursor_logical_pos().y,
                                });
                            } else {
                                tb.cursor_move_to_logical(Point {
                                    x: 0,
                                    y: tb.cursor_logical_pos().y,
                                });
                            }
                            logical_after = tb.cursor_logical_pos();
                        }

                        // If the line has some indentation and the user pressed Home,
                        // the first time it'll stop at the indentation. The second time
                        // they press it, it'll move to the true start of the line.
                        //
                        // If the cursor is already at the start of the line,
                        // we move it back to the end of the indentation.
                        if logical_after.x == 0
                            && let indent_end = tb.indent_end_logical_pos()
                            && (logical_before > indent_end || logical_before.x == 0)
                        {
                            if modifiers == kbmod::SHIFT {
                                tb.selection_update_logical(indent_end);
                            } else {
                                tb.cursor_move_to_logical(indent_end);
                            }
                        }
                    }
                }
                vk::LEFT => {
                    let granularity = if modifiers.contains(KBMOD_FOR_WORD_NAV) {
                        CursorMovement::Word
                    } else {
                        CursorMovement::Grapheme
                    };
                    if modifiers.contains(kbmod::SHIFT) {
                        tb.selection_update_delta(granularity, -1);
                    } else if let Some((beg, _)) = tb.selection_range() {
                        unsafe { tb.set_cursor(beg) };
                    } else {
                        tb.cursor_move_delta(granularity, -1);
                    }
                }
                vk::UP => {
                    if single_line {
                        return false;
                    }
                    match modifiers {
                        kbmod::NONE => {
                            let mut x = tc.preferred_column;
                            let mut y = tb.cursor_visual_pos().y - 1;

                            // If there's a selection we put the cursor above it.
                            if let Some((beg, _)) = tb.selection_range() {
                                x = beg.visual_pos.x;
                                y = beg.visual_pos.y - 1;
                                tc.preferred_column = x;
                            }

                            // If the cursor was already on the first line,
                            // move it to the start of the buffer.
                            if y < 0 {
                                x = 0;
                                tc.preferred_column = 0;
                            }

                            tb.cursor_move_to_visual(Point { x, y });
                        }
                        kbmod::CTRL => {
                            tc.scroll_offset.y -= 1;
                            make_cursor_visible = false;
                        }
                        kbmod::SHIFT => {
                            // If the cursor was already on the first line,
                            // move it to the start of the buffer.
                            if tb.cursor_visual_pos().y == 0 {
                                tc.preferred_column = 0;
                            }

                            tb.selection_update_visual(Point {
                                x: tc.preferred_column,
                                y: tb.cursor_visual_pos().y - 1,
                            });
                        }
                        kbmod::ALT => tb.move_selected_lines(MoveLineDirection::Up),
                        kbmod::CTRL_ALT => {
                            // TODO: Add cursor above
                        }
                        _ => return false,
                    }
                }
                vk::RIGHT => {
                    let granularity = if modifiers.contains(KBMOD_FOR_WORD_NAV) {
                        CursorMovement::Word
                    } else {
                        CursorMovement::Grapheme
                    };
                    if modifiers.contains(kbmod::SHIFT) {
                        tb.selection_update_delta(granularity, 1);
                    } else if let Some((_, end)) = tb.selection_range() {
                        unsafe { tb.set_cursor(end) };
                    } else {
                        tb.cursor_move_delta(granularity, 1);
                    }
                }
                vk::DOWN => {
                    if single_line {
                        return false;
                    }
                    match modifiers {
                        kbmod::NONE => {
                            let mut x = tc.preferred_column;
                            let mut y = tb.cursor_visual_pos().y + 1;

                            // If there's a selection we put the cursor below it.
                            if let Some((_, end)) = tb.selection_range() {
                                x = end.visual_pos.x;
                                y = end.visual_pos.y + 1;
                                tc.preferred_column = x;
                            }

                            // If the cursor was already on the last line,
                            // move it to the end of the buffer.
                            if y >= tb.visual_line_count() {
                                x = CoordType::MAX;
                            }

                            tb.cursor_move_to_visual(Point { x, y });

                            // If we fell into the `if y >= tb.get_visual_line_count()` above, we wanted to
                            // update the `preferred_column` but didn't know yet what it was. Now we know!
                            if x == CoordType::MAX {
                                tc.preferred_column = tb.cursor_visual_pos().x;
                            }
                        }
                        kbmod::CTRL => {
                            tc.scroll_offset.y += 1;
                            make_cursor_visible = false;
                        }
                        kbmod::SHIFT => {
                            // If the cursor was already on the last line,
                            // move it to the end of the buffer.
                            if tb.cursor_visual_pos().y >= tb.visual_line_count() - 1 {
                                tc.preferred_column = CoordType::MAX;
                            }

                            tb.selection_update_visual(Point {
                                x: tc.preferred_column,
                                y: tb.cursor_visual_pos().y + 1,
                            });

                            if tc.preferred_column == CoordType::MAX {
                                tc.preferred_column = tb.cursor_visual_pos().x;
                            }
                        }
                        kbmod::ALT => tb.move_selected_lines(MoveLineDirection::Down),
                        kbmod::CTRL_ALT => {
                            // TODO: Add cursor above
                        }
                        _ => return false,
                    }
                }
                vk::INSERT => match modifiers {
                    kbmod::SHIFT => tb.paste(self.clipboard_ref()),
                    kbmod::CTRL => tb.copy(self.clipboard_mut()),
                    _ => tb.set_overtype(!tb.is_overtype()),
                },
                vk::DELETE => match modifiers {
                    kbmod::SHIFT => tb.cut(self.clipboard_mut()),
                    kbmod::CTRL => tb.delete(CursorMovement::Word, 1),
                    _ => tb.delete(CursorMovement::Grapheme, 1),
                },
                vk::A => match modifiers {
                    kbmod::CTRL => tb.select_all(),
                    _ => return false,
                },
                vk::B => match modifiers {
                    kbmod::ALT if cfg!(target_os = "macos") => {
                        // On macOS, terminals commonly emit the Emacs style
                        // Alt+B (ESC b) sequence for Alt+Left.
                        tb.cursor_move_delta(CursorMovement::Word, -1);
                    }
                    _ => return false,
                },
                vk::F => match modifiers {
                    kbmod::ALT if cfg!(target_os = "macos") => {
                        // On macOS, terminals commonly emit the Emacs style
                        // Alt+F (ESC f) sequence for Alt+Right.
                        tb.cursor_move_delta(CursorMovement::Word, 1);
                    }
                    _ => return false,
                },
                vk::H => match modifiers {
                    kbmod::CTRL => tb.delete(CursorMovement::Word, -1),
                    _ => return false,
                },
                vk::L => match modifiers {
                    kbmod::CTRL => tb.select_line(),
                    _ => return false,
                },
                vk::X => match modifiers {
                    kbmod::CTRL => tb.cut(self.clipboard_mut()),
                    _ => return false,
                },
                vk::C => match modifiers {
                    kbmod::CTRL => tb.copy(self.clipboard_mut()),
                    _ => return false,
                },
                vk::V => match modifiers {
                    kbmod::CTRL => tb.paste(self.clipboard_ref()),
                    _ => return false,
                },
                vk::Y => match modifiers {
                    kbmod::CTRL => tb.redo(),
                    _ => return false,
                },
                vk::Z => match modifiers {
                    kbmod::CTRL => tb.undo(),
                    kbmod::CTRL_SHIFT => tb.redo(),
                    kbmod::ALT => tb.set_word_wrap(!tb.is_word_wrap_enabled()),
                    _ => return false,
                },
                _ => return false,
            }

            change_preferred_column = !matches!(key, vk::PRIOR | vk::NEXT | vk::UP | vk::DOWN);
        } else {
            return false;
        }

        if single_line && !write.is_empty() {
            let (end, _) = simd::lines_fwd(write, 0, 0, 1);
            write = unicode::strip_newline(&write[..end]);
        }
        if !write.is_empty() {
            tb.write_canon(write);
            change_preferred_column = true;
            make_cursor_visible = true;
        }

        if change_preferred_column {
            tc.preferred_column = tb.cursor_visual_pos().x;
        }

        self.set_input_consumed();
        make_cursor_visible
    }

    fn textarea_make_cursor_visible(&self, tc: &mut TextareaContent, node_prev: &Node) {
        let tb = tc.buffer.borrow();
        let mut scroll_x = tc.scroll_offset.x;
        let mut scroll_y = tc.scroll_offset.y;

        let text_width = tb.text_width();
        let cursor_x = tb.cursor_visual_pos().x;
        scroll_x = scroll_x.min(cursor_x - 10);
        scroll_x = scroll_x.max(cursor_x - text_width + 10);

        let viewport_height = node_prev.inner.height();
        let cursor_y = tb.cursor_visual_pos().y;
        // Scroll up if the cursor is above the visible area.
        scroll_y = scroll_y.min(cursor_y);
        // Scroll down if the cursor is below the visible area.
        scroll_y = scroll_y.max(cursor_y - viewport_height + 1);

        tc.scroll_offset.x = scroll_x;
        tc.scroll_offset.y = scroll_y;
    }

    fn textarea_adjust_scroll_offset(&self, tc: &mut TextareaContent) {
        let tb = tc.buffer.borrow();
        let mut scroll_x = tc.scroll_offset.x;
        let mut scroll_y = tc.scroll_offset.y;

        scroll_x = scroll_x.min(tc.scroll_offset_x_max.max(tb.cursor_visual_pos().x) - 10);
        scroll_x = scroll_x.max(0);
        scroll_y = scroll_y.clamp(0, tb.visual_line_count() - 1);

        if tb.is_word_wrap_enabled() {
            scroll_x = 0;
        }

        tc.scroll_offset.x = scroll_x;
        tc.scroll_offset.y = scroll_y;
    }

    /// Creates a scrollable area.
    pub fn scrollarea_begin(&mut self, classname: &'static str, intrinsic_size: Size) {
        self.block_begin(classname);

        let container_node = self.tree.last_node;
        {
            let mut container = self.tree.last_node.borrow_mut();
            container.content = NodeContent::Scrollarea(ScrollareaContent {
                scroll_offset: Point::MIN,
                scroll_offset_y_drag_start: CoordType::MIN,
                thumb_height: 0,
            });

            if intrinsic_size.width > 0 || intrinsic_size.height > 0 {
                container.intrinsic_size.width = intrinsic_size.width.max(0);
                container.intrinsic_size.height = intrinsic_size.height.max(0);
                container.intrinsic_size_set = true;
            }
        }

        self.block_begin("content");
        self.inherit_focus();

        // Ensure that attribute modifications apply to the outer container.
        self.tree.last_node = container_node;
    }

    /// Scrolls the current scrollable area to the given position.
    pub fn scrollarea_scroll_to(&mut self, pos: Point) {
        let mut container = self.tree.last_node.borrow_mut();
        if let NodeContent::Scrollarea(sc) = &mut container.content {
            sc.scroll_offset = pos;
        } else {
            debug_assert!(false);
        }
    }

    /// Ends the current scrollarea block.
    pub fn scrollarea_end(&mut self) {
        self.block_end(); // content block
        self.block_end(); // outer container

        let mut container = self.tree.last_node.borrow_mut();
        let container_id = container.id;
        let container_depth = container.depth;
        let Some(prev_container) = self.tui.prev_node_map.get(container_id) else {
            return;
        };

        let prev_container = prev_container.borrow();
        let NodeContent::Scrollarea(sc) = &mut container.content else {
            unreachable!();
        };

        if sc.scroll_offset == Point::MIN
            && let NodeContent::Scrollarea(sc_prev) = &prev_container.content
        {
            *sc = sc_prev.clone();
        }

        if !self.input_consumed {
            if self.tui.mouse_state != InputMouseState::None {
                let container_rect = prev_container.inner;

                match self.tui.mouse_state {
                    InputMouseState::Left => {
                        if self.tui.mouse_is_drag {
                            // We don't need to look up the previous track node,
                            // since it has a fixed size based on the container size.
                            let track_rect = Rect {
                                left: container_rect.right,
                                top: container_rect.top,
                                right: container_rect.right + 1,
                                bottom: container_rect.bottom,
                            };
                            if track_rect.contains(self.tui.mouse_down_position) {
                                if sc.scroll_offset_y_drag_start == CoordType::MIN {
                                    sc.scroll_offset_y_drag_start = sc.scroll_offset.y;
                                }

                                let content = prev_container.children.first.unwrap().borrow();
                                let content_rect = content.inner;
                                let content_height = content_rect.height();
                                let track_height = track_rect.height();
                                let scrollable_height = content_height - track_height;

                                if scrollable_height > 0 {
                                    let trackable = track_height - sc.thumb_height;
                                    let delta_y =
                                        self.tui.mouse_position.y - self.tui.mouse_down_position.y;
                                    sc.scroll_offset.y = sc.scroll_offset_y_drag_start
                                        + (delta_y as i64 * scrollable_height as i64
                                            / trackable as i64)
                                            as CoordType;
                                }

                                self.set_input_consumed();
                            }
                        }
                    }
                    InputMouseState::Release => {
                        sc.scroll_offset_y_drag_start = CoordType::MIN;
                    }
                    InputMouseState::Scroll => {
                        if container_rect.contains(self.tui.mouse_position) {
                            sc.scroll_offset.x += self.input_scroll_delta.x;
                            sc.scroll_offset.y += self.input_scroll_delta.y;
                            self.set_input_consumed();
                        }
                    }
                    _ => {}
                }
            } else if self.tui.is_subtree_focused_alt(container_id, container_depth)
                && let Some(key) = self.input_keyboard
            {
                match key {
                    vk::PRIOR => sc.scroll_offset.y -= prev_container.inner_clipped.height(),
                    vk::NEXT => sc.scroll_offset.y += prev_container.inner_clipped.height(),
                    vk::END => sc.scroll_offset.y = CoordType::MAX,
                    vk::HOME => sc.scroll_offset.y = 0,
                    _ => return,
                }
                self.set_input_consumed();
            }
        }
    }

    /// Creates a list where exactly one item is selected.
    pub fn list_begin(&mut self, classname: &'static str) {
        self.block_begin(classname);
        self.attr_focusable();

        let mut last_node = self.tree.last_node.borrow_mut();
        let content = self
            .tui
            .prev_node_map
            .get(last_node.id)
            .and_then(|node| match &node.borrow().content {
                NodeContent::List(content) => {
                    Some(ListContent { selected: content.selected, selected_node: None })
                }
                _ => None,
            })
            .unwrap_or(ListContent { selected: 0, selected_node: None });

        last_node.attributes.focus_void = true;
        last_node.content = NodeContent::List(content);
    }

    /// Creates a list item with the given text.
    pub fn list_item(&mut self, select: bool, text: &str) -> ListSelection {
        self.styled_list_item_begin();
        self.styled_label_add_text(text);
        self.styled_list_item_end(select)
    }

    /// Creates a list item consisting of a styled label.
    /// See [`Context::styled_label_begin`].
    pub fn styled_list_item_begin(&mut self) {
        let list = self.tree.current_node;
        let idx = list.borrow().child_count;

        self.next_block_id_mixin(idx as u64);
        self.styled_label_begin("item");
        self.styled_label_add_text("  ");
        self.attr_focusable();
    }

    /// Ends the current styled list item.
    pub fn styled_list_item_end(&mut self, select: bool) -> ListSelection {
        self.styled_label_end();

        let list = self.tree.current_node;

        let selected_before;
        let selected_now;
        let focused;
        {
            let mut list = list.borrow_mut();
            let content = match &mut list.content {
                NodeContent::List(content) => content,
                _ => unreachable!(),
            };

            let item = self.tree.last_node.borrow();
            let item_id = item.id;
            selected_before = content.selected == item_id;
            focused = self.is_focused();

            // Inherit the default selection & Click changes selection
            selected_now = selected_before || (select && content.selected == 0) || focused;

            // Note down the selected node for keyboard navigation.
            if selected_now {
                content.selected_node = Some(self.tree.last_node);
                if !selected_before {
                    content.selected = item_id;
                    self.needs_rerender();
                }
            }
        }

        // Clicking an item activates it
        let clicked =
            !self.input_consumed && (self.input_mouse_click == 2 && self.was_mouse_down());
        // Pressing Enter on a selected item activates it as well
        let entered = focused
            && selected_before
            && !self.input_consumed
            && matches!(self.input_keyboard, Some(vk::RETURN));
        let activated = clicked || entered;
        if activated {
            self.set_input_consumed();
        }

        if selected_before && activated {
            ListSelection::Activated
        } else if selected_now && !selected_before {
            ListSelection::Selected
        } else {
            ListSelection::Unchanged
        }
    }

    /// [`Context::steal_focus`], but for a list view.
    ///
    /// This exists, because didn't want to figure out how to get
    /// [`Context::styled_list_item_end`] to recognize a regular,
    /// programmatic focus steal.
    pub fn list_item_steal_focus(&mut self) {
        self.steal_focus();

        match &mut self.tree.current_node.borrow_mut().content {
            NodeContent::List(content) => {
                content.selected = self.tree.last_node.borrow().id;
                content.selected_node = Some(self.tree.last_node);
            }
            _ => unreachable!(),
        }
    }

    /// Ends the current list block.
    pub fn list_end(&mut self) {
        self.block_end();

        let contains_focus;
        let selected_now;
        let mut selected_next;
        {
            let list = self.tree.last_node.borrow();

            contains_focus = self.tui.is_subtree_focused(&list);
            selected_now = match &list.content {
                NodeContent::List(content) => content.selected_node,
                _ => unreachable!(),
            };
            selected_next = match selected_now.or(list.children.first) {
                Some(node) => node,
                None => return,
            };
        }

        if contains_focus
            && !self.input_consumed
            && let Some(key) = self.input_keyboard
            && let Some(selected_now) = selected_now
        {
            let list = self.tree.last_node.borrow();

            if let Some(prev_container) = self.tui.prev_node_map.get(list.id) {
                let mut consumed = true;

                match key {
                    vk::PRIOR => {
                        selected_next = selected_now;
                        for _ in 0..prev_container.borrow().inner_clipped.height() - 1 {
                            let node = selected_next.borrow();
                            selected_next = match node.siblings.prev {
                                Some(node) => node,
                                None => break,
                            };
                        }
                    }
                    vk::NEXT => {
                        selected_next = selected_now;
                        for _ in 0..prev_container.borrow().inner_clipped.height() - 1 {
                            let node = selected_next.borrow();
                            selected_next = match node.siblings.next {
                                Some(node) => node,
                                None => break,
                            };
                        }
                    }
                    vk::END => {
                        selected_next = list.children.last.unwrap_or(selected_next);
                    }
                    vk::HOME => {
                        selected_next = list.children.first.unwrap_or(selected_next);
                    }
                    vk::UP => {
                        selected_next = selected_now
                            .borrow()
                            .siblings
                            .prev
                            .or(list.children.last)
                            .unwrap_or(selected_next);
                    }
                    vk::DOWN => {
                        selected_next = selected_now
                            .borrow()
                            .siblings
                            .next
                            .or(list.children.first)
                            .unwrap_or(selected_next);
                    }
                    _ => consumed = false,
                }

                if consumed {
                    self.set_input_consumed();
                }
            }
        }

        // Now that we know which item is selected we can mark it as such.
        if !opt_ptr_eq(selected_now, Some(selected_next))
            && let NodeContent::List(content) = &mut self.tree.last_node.borrow_mut().content
        {
            content.selected_node = Some(selected_next);
        }

        // Now that we know which item is selected we can mark it as such.
        if let NodeContent::Text(content) = &mut selected_next.borrow_mut().content {
            unsafe {
                content.text.as_bytes_mut()[0] = b'>';
            }
        }

        // If the list has focus, we also delegate focus to the selected item and colorize it.
        if contains_focus {
            {
                let mut node = selected_next.borrow_mut();
                node.attributes.bg = self.indexed(IndexedColor::Green);
                node.attributes.fg = self.contrasted(self.indexed(IndexedColor::Green));
            }
            self.steal_focus_for(selected_next);
        }
    }

    /// Creates a menubar, to be shown at the top of the screen.
    pub fn menubar_begin(&mut self) {
        self.table_begin("menubar");
        self.attr_focus_well();
        self.table_next_row();
    }

    /// Appends a menu to the current menubar.
    ///
    /// Returns true if the menu is open. Continue appending items to it in that case.
    pub fn menubar_menu_begin(&mut self, text: &str, accelerator: char) -> bool {
        let accelerator = if cfg!(target_os = "macos") { '\0' } else { accelerator };
        let mixin = self.tree.current_node.borrow().child_count as u64;
        self.next_block_id_mixin(mixin);

        self.button_label(
            "menu_button",
            text,
            ButtonStyle::default().accelerator(accelerator).bracketed(false),
        );
        self.attr_focusable();
        self.attr_padding(Rect::two(0, 1));

        let contains_focus = self.contains_focus();
        let keyboard_focus = accelerator != '\0'
            && !contains_focus
            && self.consume_shortcut(kbmod::ALT | InputKey::new(accelerator as u32));

        if contains_focus || keyboard_focus {
            self.attr_background_rgba(self.tui.floater_default_bg);
            self.attr_foreground_rgba(self.tui.floater_default_fg);

            if self.is_focused() {
                self.attr_background_rgba(self.indexed(IndexedColor::Green));
                self.attr_foreground_rgba(self.contrasted(self.indexed(IndexedColor::Green)));
            }

            self.next_block_id_mixin(mixin);
            self.table_begin("flyout");
            self.attr_float(FloatSpec {
                anchor: Anchor::Last,
                gravity_x: 0.0,
                gravity_y: 0.0,
                offset_x: 0.0,
                offset_y: 1.0,
            });
            self.attr_border();
            self.attr_focus_well();

            if keyboard_focus {
                self.steal_focus();
            }

            true
        } else {
            false
        }
    }

    /// Appends a button to the current menu.
    pub fn menubar_menu_button(
        &mut self,
        text: &str,
        accelerator: char,
        shortcut: InputKey,
    ) -> bool {
        self.menubar_menu_checkbox(text, accelerator, shortcut, false)
    }

    /// Appends a checkbox to the current menu.
    /// Returns true if the checkbox was activated.
    pub fn menubar_menu_checkbox(
        &mut self,
        text: &str,
        accelerator: char,
        shortcut: InputKey,
        checked: bool,
    ) -> bool {
        self.table_next_row();
        self.attr_focusable();

        // First menu item? Steal focus.
        if self.tree.current_node.borrow_mut().siblings.prev.is_none() {
            self.inherit_focus();
        }

        if self.is_focused() {
            self.attr_background_rgba(self.indexed(IndexedColor::Green));
            self.attr_foreground_rgba(self.contrasted(self.indexed(IndexedColor::Green)));
        }

        let clicked =
            self.button_activated() || self.consume_shortcut(InputKey::new(accelerator as u32));

        self.button_label(
            "menu_checkbox",
            text,
            ButtonStyle::default().bracketed(false).checked(checked).accelerator(accelerator),
        );
        self.menubar_shortcut(shortcut);

        if clicked {
            // TODO: This should reassign the previous focused path.
            self.needs_rerender();
            Tui::clean_node_path(&mut self.tui.focused_node_path);
        }

        clicked
    }

    /// Ends the current menu.
    pub fn menubar_menu_end(&mut self) {
        self.table_end();

        if !self.input_consumed
            && let Some(key) = self.input_keyboard
            && matches!(key, vk::ESCAPE | vk::UP | vk::DOWN)
        {
            if matches!(key, vk::UP | vk::DOWN) {
                // If the focus is on the menubar, and the user presses up/down,
                // focus the first/last item of the flyout respectively.
                let ln = self.tree.last_node.borrow();
                if self.tui.is_node_focused(ln.parent.map_or(0, |n| n.borrow().id)) {
                    let selected_next =
                        if key == vk::UP { ln.children.last } else { ln.children.first };
                    if let Some(selected_next) = selected_next {
                        self.steal_focus_for(selected_next);
                        self.set_input_consumed();
                    }
                }
            } else if self.contains_focus() {
                // Otherwise, if the menu is the focused one and the
                // user presses Escape, pass focus back to the menubar.
                self.tui.pop_focusable_node(1);
            }
        }
    }

    /// Ends the current menubar.
    pub fn menubar_end(&mut self) {
        self.table_end();
    }

    /// Renders a button label with an optional accelerator character
    /// May also renders a checkbox or square brackets for inline buttons
    fn button_label(&mut self, classname: &'static str, text: &str, style: ButtonStyle) {
        // Label prefix
        self.styled_label_begin(classname);
        if style.bracketed {
            self.styled_label_add_text("[");
        }
        if let Some(checked) = style.checked {
            self.styled_label_add_text(if checked { "🗹 " } else { "  " });
        }
        // Label text
        match style.accelerator {
            Some(accelerator) if accelerator.is_ascii_uppercase() => {
                // Complex case:
                // Locate the offset of the accelerator character in the label text
                let mut off = text.len();
                for (i, c) in text.bytes().enumerate() {
                    // Perfect match (uppercase character) --> stop
                    if c as char == accelerator {
                        off = i;
                        break;
                    }
                    // Inexact match (lowercase character) --> use first hit
                    if (c & !0x20) as char == accelerator && off == text.len() {
                        off = i;
                    }
                }

                if off < text.len() {
                    // Add an underline to the accelerator.
                    self.styled_label_add_text(&text[..off]);
                    self.styled_label_set_attributes(Attributes::Underlined);
                    self.styled_label_add_text(&text[off..off + 1]);
                    self.styled_label_set_attributes(Attributes::None);
                    self.styled_label_add_text(&text[off + 1..]);
                } else {
                    // Add the accelerator in parentheses and underline it.
                    let ch = accelerator as u8;
                    self.styled_label_add_text(text);
                    self.styled_label_add_text("(");
                    self.styled_label_set_attributes(Attributes::Underlined);
                    self.styled_label_add_text(unsafe { str_from_raw_parts(&ch, 1) });
                    self.styled_label_set_attributes(Attributes::None);
                    self.styled_label_add_text(")");
                }
            }
            _ => {
                // Simple case:
                // no accelerator character
                self.styled_label_add_text(text);
            }
        }
        // Label postfix
        if style.bracketed {
            self.styled_label_add_text("]");
        }
        self.styled_label_end();
    }

    fn menubar_shortcut(&mut self, shortcut: InputKey) {
        let shortcut_letter = shortcut.value() as u8 as char;
        if shortcut_letter.is_ascii_uppercase() {
            let mut shortcut_text = ArenaString::new_in(self.arena());
            if shortcut.modifiers_contains(kbmod::CTRL) {
                shortcut_text.push_str(self.tui.modifier_translations.ctrl);
                shortcut_text.push('+');
            }
            if shortcut.modifiers_contains(kbmod::ALT) {
                shortcut_text.push_str(self.tui.modifier_translations.alt);
                shortcut_text.push('+');
            }
            if shortcut.modifiers_contains(kbmod::SHIFT) {
                shortcut_text.push_str(self.tui.modifier_translations.shift);
                shortcut_text.push('+');
            }
            shortcut_text.push(shortcut_letter);

            self.label("shortcut", &shortcut_text);
        } else {
            self.block_begin("shortcut");
            self.block_end();
        }
        self.attr_padding(Rect { left: 2, top: 0, right: 2, bottom: 0 });
    }
}

/// See [`Tree::visit_all`].
#[derive(Clone, Copy)]
enum VisitControl {
    Continue,
    SkipChildren,
    Stop,
}

/// Stores the root of the "DOM" tree of the UI.
struct Tree<'a> {
    tail: &'a NodeCell<'a>,
    root_first: &'a NodeCell<'a>,
    root_last: &'a NodeCell<'a>,
    last_node: &'a NodeCell<'a>,
    current_node: &'a NodeCell<'a>,

    count: usize,
    checksum: u64,
}

impl<'a> Tree<'a> {
    /// Creates a new tree inside the given arena.
    /// A single root node is added for the main contents.
    fn new(arena: &'a Arena) -> Self {
        let root = Self::alloc_node(arena);
        {
            let mut r = root.borrow_mut();
            r.id = ROOT_ID;
            r.classname = "root";
            r.attributes.focusable = true;
            r.attributes.focus_well = true;
        }
        Self {
            tail: root,
            root_first: root,
            root_last: root,
            last_node: root,
            current_node: root,
            count: 1,
            checksum: ROOT_ID,
        }
    }

    fn alloc_node(arena: &'a Arena) -> &'a NodeCell<'a> {
        arena.alloc_uninit().write(Default::default())
    }

    /// Appends a child node to the current node.
    fn push_child(&mut self, node: &'a NodeCell<'a>) {
        let mut n = node.borrow_mut();
        n.parent = Some(self.current_node);
        n.stack_parent = Some(self.current_node);

        {
            let mut p = self.current_node.borrow_mut();
            n.siblings.prev = p.children.last;
            n.depth = p.depth + 1;

            if let Some(child_last) = p.children.last {
                let mut child_last = child_last.borrow_mut();
                child_last.siblings.next = Some(node);
            }
            if p.children.first.is_none() {
                p.children.first = Some(node);
            }
            p.children.last = Some(node);
            p.child_count += 1;
        }

        n.prev = Some(self.tail);
        {
            let mut tail = self.tail.borrow_mut();
            tail.next = Some(node);
        }
        self.tail = node;

        self.last_node = node;
        self.current_node = node;
        self.count += 1;
        // wymix is weak, but both checksum and node.id are proper random, so... it's not *that* bad.
        self.checksum = wymix(self.checksum, n.id);
    }

    /// Removes the current node from its parent and appends it as a new root.
    /// Used for [`Context::attr_float`].
    fn move_node_to_root(&mut self, node: &'a NodeCell<'a>, anchor: Option<&'a NodeCell<'a>>) {
        let mut n = node.borrow_mut();
        let Some(parent) = n.parent else {
            return;
        };

        if let Some(sibling_prev) = n.siblings.prev {
            let mut sibling_prev = sibling_prev.borrow_mut();
            sibling_prev.siblings.next = n.siblings.next;
        }
        if let Some(sibling_next) = n.siblings.next {
            let mut sibling_next = sibling_next.borrow_mut();
            sibling_next.siblings.prev = n.siblings.prev;
        }

        {
            let mut p = parent.borrow_mut();
            if opt_ptr_eq(p.children.first, Some(node)) {
                p.children.first = n.siblings.next;
            }
            if opt_ptr_eq(p.children.last, Some(node)) {
                p.children.last = n.siblings.prev;
            }
            p.child_count -= 1;
        }

        n.parent = anchor;
        n.depth = anchor.map_or(0, |n| n.borrow().depth + 1);
        n.siblings.prev = Some(self.root_last);
        n.siblings.next = None;

        self.root_last.borrow_mut().siblings.next = Some(node);
        self.root_last = node;
    }

    /// Completes the current node and moves focus to the parent.
    fn pop_stack(&mut self) {
        let current_node = self.current_node.borrow();
        if let Some(stack_parent) = current_node.stack_parent {
            self.last_node = self.current_node;
            self.current_node = stack_parent;
        }
    }

    fn iterate_siblings(
        mut node: Option<&'a NodeCell<'a>>,
    ) -> impl Iterator<Item = &'a NodeCell<'a>> + use<'a> {
        iter::from_fn(move || {
            let n = node?;
            node = n.borrow().siblings.next;
            Some(n)
        })
    }

    fn iterate_siblings_rev(
        mut node: Option<&'a NodeCell<'a>>,
    ) -> impl Iterator<Item = &'a NodeCell<'a>> + use<'a> {
        iter::from_fn(move || {
            let n = node?;
            node = n.borrow().siblings.prev;
            Some(n)
        })
    }

    fn iterate_roots(&self) -> impl Iterator<Item = &'a NodeCell<'a>> + use<'a> {
        Self::iterate_siblings(Some(self.root_first))
    }

    fn iterate_roots_rev(&self) -> impl Iterator<Item = &'a NodeCell<'a>> + use<'a> {
        Self::iterate_siblings_rev(Some(self.root_last))
    }

    /// Visits all nodes under and including `root` in depth order.
    /// Starts with node `start`.
    ///
    /// WARNING: Breaks in hilarious ways if `start` is not within `root`.
    fn visit_all<T: FnMut(&'a NodeCell<'a>) -> VisitControl>(
        root: &'a NodeCell<'a>,
        start: &'a NodeCell<'a>,
        forward: bool,
        mut cb: T,
    ) {
        let root_depth = root.borrow().depth;
        let mut node = start;
        let children_idx = if forward { NodeChildren::FIRST } else { NodeChildren::LAST };
        let siblings_idx = if forward { NodeSiblings::NEXT } else { NodeSiblings::PREV };

        while {
            'traverse: {
                match cb(node) {
                    VisitControl::Continue => {
                        // Depth first search: It has a child? Go there.
                        if let Some(child) = node.borrow().children.get(children_idx) {
                            node = child;
                            break 'traverse;
                        }
                    }
                    VisitControl::SkipChildren => {}
                    VisitControl::Stop => return,
                }

                loop {
                    // If we hit the root while going up, we restart the traversal at
                    // `root` going down again until we hit `start` again.
                    let n = node.borrow();
                    if n.depth <= root_depth {
                        break 'traverse;
                    }

                    // Go to the parent's next sibling. --> Next subtree.
                    if let Some(sibling) = n.siblings.get(siblings_idx) {
                        node = sibling;
                        break;
                    }

                    // Out of children? Go back to the parent.
                    node = n.parent.unwrap();
                }
            }

            // We're done once we wrapped around to the `start`.
            !ptr::eq(node, start)
        } {}
    }
}

/// A hashmap of node IDs to nodes.
///
/// This map uses a simple open addressing scheme with linear probing.
/// It's fast, simple, and sufficient for the small number of nodes we have.
struct NodeMap<'a> {
    slots: &'a [Option<&'a NodeCell<'a>>],
    shift: usize,
    mask: u64,
}

impl Default for NodeMap<'static> {
    fn default() -> Self {
        Self { slots: &[None, None], shift: 63, mask: 0 }
    }
}

impl<'a> NodeMap<'a> {
    /// Creates a new node map for the given tree.
    fn new(arena: &'a Arena, tree: &Tree<'a>) -> Self {
        // Since we aren't expected to have millions of nodes,
        // we allocate 4x the number of slots for a 25% fill factor.
        let width = (4 * tree.count + 1).ilog2().max(1) as usize;
        let slots = 1 << width;
        let shift = 64 - width;
        let mask = (slots - 1) as u64;

        let slots = arena.alloc_uninit_slice(slots).write_filled(None);
        let mut node = tree.root_first;

        loop {
            let n = node.borrow();
            let mut slot = n.id >> shift;

            loop {
                if slots[slot as usize].is_none() {
                    slots[slot as usize] = Some(node);
                    break;
                }
                slot = (slot + 1) & mask;
            }

            node = match n.next {
                Some(node) => node,
                None => break,
            };
        }

        Self { slots, shift, mask }
    }

    /// Gets a node by its ID.
    fn get(&mut self, id: u64) -> Option<&'a NodeCell<'a>> {
        let shift = self.shift;
        let mask = self.mask;
        let mut slot = id >> shift;

        loop {
            let node = self.slots[slot as usize]?;
            if node.borrow().id == id {
                return Some(node);
            }
            slot = (slot + 1) & mask;
        }
    }
}

struct FloatAttributes {
    // Specifies the origin of the container relative to the container size. [0, 1]
    gravity_x: f32,
    gravity_y: f32,
    // Specifies an offset from the origin in cells.
    offset_x: f32,
    offset_y: f32,
}

/// NOTE: Must not contain items that require drop().
#[derive(Default)]
struct NodeAttributes {
    float: Option<FloatAttributes>,
    position: Position,
    padding: Rect,
    bg: u32,
    fg: u32,
    reverse: bool,
    bordered: bool,
    focusable: bool,
    focus_well: bool, // Prevents focus from leaving via Tab
    focus_void: bool, // Prevents focus from entering via Tab
}

/// NOTE: Must not contain items that require drop().
struct ListContent<'a> {
    selected: u64,
    // Points to the Node that holds this ListContent instance, if any>.
    selected_node: Option<&'a NodeCell<'a>>,
}

/// NOTE: Must not contain items that require drop().
struct TableContent<'a> {
    columns: Vec<CoordType, &'a Arena>,
    cell_gap: Size,
}

/// NOTE: Must not contain items that require drop().
struct StyledTextChunk {
    offset: usize,
    fg: u32,
    attr: Attributes,
}

const INVALID_STYLED_TEXT_CHUNK: StyledTextChunk =
    StyledTextChunk { offset: usize::MAX, fg: 0, attr: Attributes::None };

/// NOTE: Must not contain items that require drop().
struct TextContent<'a> {
    text: ArenaString<'a>,
    chunks: Vec<StyledTextChunk, &'a Arena>,
    overflow: Overflow,
}

/// NOTE: Must not contain items that require drop().
struct TextareaContent<'a> {
    buffer: &'a TextBufferCell,

    // Carries over between frames.
    scroll_offset: Point,
    scroll_offset_y_drag_start: CoordType,
    scroll_offset_x_max: CoordType,
    thumb_height: CoordType,
    preferred_column: CoordType,

    single_line: bool,
    has_focus: bool,
}

/// NOTE: Must not contain items that require drop().
#[derive(Clone)]
struct ScrollareaContent {
    scroll_offset: Point,
    scroll_offset_y_drag_start: CoordType,
    thumb_height: CoordType,
}

/// NOTE: Must not contain items that require drop().
#[derive(Default)]
enum NodeContent<'a> {
    #[default]
    None,
    List(ListContent<'a>),
    Modal(ArenaString<'a>), // title
    Table(TableContent<'a>),
    Text(TextContent<'a>),
    Textarea(TextareaContent<'a>),
    Scrollarea(ScrollareaContent),
}

/// NOTE: Must not contain items that require drop().
#[derive(Default)]
struct NodeSiblings<'a> {
    prev: Option<&'a NodeCell<'a>>,
    next: Option<&'a NodeCell<'a>>,
}

impl<'a> NodeSiblings<'a> {
    const PREV: usize = 0;
    const NEXT: usize = 1;

    fn get(&self, off: usize) -> Option<&'a NodeCell<'a>> {
        match off & 1 {
            0 => self.prev,
            1 => self.next,
            _ => unreachable!(),
        }
    }
}

/// NOTE: Must not contain items that require drop().
#[derive(Default)]
struct NodeChildren<'a> {
    first: Option<&'a NodeCell<'a>>,
    last: Option<&'a NodeCell<'a>>,
}

impl<'a> NodeChildren<'a> {
    const FIRST: usize = 0;
    const LAST: usize = 1;

    fn get(&self, off: usize) -> Option<&'a NodeCell<'a>> {
        match off & 1 {
            0 => self.first,
            1 => self.last,
            _ => unreachable!(),
        }
    }
}

type NodeCell<'a> = SemiRefCell<Node<'a>>;

/// A node in the UI tree.
///
/// NOTE: Must not contain items that require drop().
#[derive(Default)]
struct Node<'a> {
    prev: Option<&'a NodeCell<'a>>,
    next: Option<&'a NodeCell<'a>>,
    stack_parent: Option<&'a NodeCell<'a>>,

    id: u64,
    classname: &'static str,
    parent: Option<&'a NodeCell<'a>>,
    depth: usize,
    siblings: NodeSiblings<'a>,
    children: NodeChildren<'a>,
    child_count: usize,

    attributes: NodeAttributes,
    content: NodeContent<'a>,

    intrinsic_size: Size,
    intrinsic_size_set: bool,
    outer: Rect,         // in screen-space, calculated during layout
    inner: Rect,         // in screen-space, calculated during layout
    outer_clipped: Rect, // in screen-space, calculated during layout, restricted to the viewport
    inner_clipped: Rect, // in screen-space, calculated during layout, restricted to the viewport
}

impl Node<'_> {
    /// Given an outer rectangle (including padding and borders) of this node,
    /// this returns the inner rectangle (excluding padding and borders).
    fn outer_to_inner(&self, mut outer: Rect) -> Rect {
        let l = self.attributes.bordered;
        let t = self.attributes.bordered;
        let r = self.attributes.bordered || matches!(self.content, NodeContent::Scrollarea(..));
        let b = self.attributes.bordered;

        outer.left += self.attributes.padding.left + l as CoordType;
        outer.top += self.attributes.padding.top + t as CoordType;
        outer.right -= self.attributes.padding.right + r as CoordType;
        outer.bottom -= self.attributes.padding.bottom + b as CoordType;
        outer
    }

    /// Given an intrinsic size (excluding padding and borders) of this node,
    /// this returns the outer size (including padding and borders).
    fn intrinsic_to_outer(&self) -> Size {
        let l = self.attributes.bordered;
        let t = self.attributes.bordered;
        let r = self.attributes.bordered || matches!(self.content, NodeContent::Scrollarea(..));
        let b = self.attributes.bordered;

        let mut size = self.intrinsic_size;
        size.width += self.attributes.padding.left
            + self.attributes.padding.right
            + l as CoordType
            + r as CoordType;
        size.height += self.attributes.padding.top
            + self.attributes.padding.bottom
            + t as CoordType
            + b as CoordType;
        size
    }

    /// Computes the intrinsic size of this node and its children.
    fn compute_intrinsic_size(&mut self) {
        match &mut self.content {
            NodeContent::Table(spec) => {
                // Calculate each row's height and the maximum width of each of its columns.
                for row in Tree::iterate_siblings(self.children.first) {
                    let mut row = row.borrow_mut();
                    let mut row_height = 0;

                    for (column, cell) in Tree::iterate_siblings(row.children.first).enumerate() {
                        let mut cell = cell.borrow_mut();
                        cell.compute_intrinsic_size();

                        let size = cell.intrinsic_to_outer();

                        // If the spec.columns[] value is positive, it's an absolute width.
                        // Otherwise, it's a fraction of the remaining space.
                        //
                        // TODO: The latter is computed incorrectly.
                        // Example: If the items are "a","b","c" then the intrinsic widths are [1,1,1].
                        // If the column spec is [0,-3,-1], then this code assigns an intrinsic row
                        // width of 3, but it should be 5 (1+1+3), because the spec says that the
                        // last column (flexible 1/1) must be 3 times as wide as the 2nd one (1/3rd).
                        // It's not a big deal yet, because such functionality isn't needed just yet.
                        if column >= spec.columns.len() {
                            spec.columns.push(0);
                        }
                        spec.columns[column] = spec.columns[column].max(size.width);

                        row_height = row_height.max(size.height);
                    }

                    row.intrinsic_size.height = row_height;
                }

                // Assuming each column has the width of the widest cell in that column,
                // calculate the total width of the table.
                let total_gap_width =
                    spec.cell_gap.width * spec.columns.len().saturating_sub(1) as CoordType;
                let total_inner_width = spec.columns.iter().sum::<CoordType>() + total_gap_width;
                let mut total_width = 0;
                let mut total_height = 0;

                // Assign the total width to each row.
                for row in Tree::iterate_siblings(self.children.first) {
                    let mut row = row.borrow_mut();
                    row.intrinsic_size.width = total_inner_width;
                    row.intrinsic_size_set = true;

                    let size = row.intrinsic_to_outer();
                    total_width = total_width.max(size.width);
                    total_height += size.height;
                }

                let total_gap_height =
                    spec.cell_gap.height * self.child_count.saturating_sub(1) as CoordType;
                total_height += total_gap_height;

                // Assign the total width/height to the table.
                if !self.intrinsic_size_set {
                    self.intrinsic_size.width = total_width;
                    self.intrinsic_size.height = total_height;
                    self.intrinsic_size_set = true;
                }
            }
            _ => {
                let mut max_width = 0;
                let mut total_height = 0;

                for child in Tree::iterate_siblings(self.children.first) {
                    let mut child = child.borrow_mut();
                    child.compute_intrinsic_size();

                    let size = child.intrinsic_to_outer();
                    max_width = max_width.max(size.width);
                    total_height += size.height;
                }

                if !self.intrinsic_size_set {
                    self.intrinsic_size.width = max_width;
                    self.intrinsic_size.height = total_height;
                    self.intrinsic_size_set = true;
                }
            }
        }
    }

    /// Lays out the children of this node.
    /// The clip rect restricts "rendering" to a certain area (the viewport).
    fn layout_children(&mut self, clip: Rect) {
        if self.children.first.is_none() || self.inner.is_empty() {
            return;
        }

        match &mut self.content {
            NodeContent::Table(spec) => {
                let width = self.inner.right - self.inner.left;
                let mut x = self.inner.left;
                let mut y = self.inner.top;

                for row in Tree::iterate_siblings(self.children.first) {
                    let mut row = row.borrow_mut();
                    let mut size = row.intrinsic_to_outer();
                    size.width = width;
                    row.outer.left = x;
                    row.outer.top = y;
                    row.outer.right = x + size.width;
                    row.outer.bottom = y + size.height;
                    row.outer = row.outer.intersect(self.inner);
                    row.inner = row.outer_to_inner(row.outer);
                    row.outer_clipped = row.outer.intersect(clip);
                    row.inner_clipped = row.inner.intersect(clip);

                    let mut row_height = 0;

                    for (column, cell) in Tree::iterate_siblings(row.children.first).enumerate() {
                        let mut cell = cell.borrow_mut();
                        let mut size = cell.intrinsic_to_outer();
                        size.width = spec.columns[column];
                        cell.outer.left = x;
                        cell.outer.top = y;
                        cell.outer.right = x + size.width;
                        cell.outer.bottom = y + size.height;
                        cell.outer = cell.outer.intersect(self.inner);
                        cell.inner = cell.outer_to_inner(cell.outer);
                        cell.outer_clipped = cell.outer.intersect(clip);
                        cell.inner_clipped = cell.inner.intersect(clip);

                        x += size.width + spec.cell_gap.width;
                        row_height = row_height.max(size.height);

                        cell.layout_children(clip);
                    }

                    x = self.inner.left;
                    y += row_height + spec.cell_gap.height;
                }
            }
            NodeContent::Scrollarea(sc) => {
                let mut content = self.children.first.unwrap().borrow_mut();

                // content available viewport size (-1 for the track)
                let sx = self.inner.right - self.inner.left;
                let sy = self.inner.bottom - self.inner.top;
                // actual content size
                let cx = sx;
                let cy = content.intrinsic_size.height.max(sy);
                // scroll offset
                let ox = 0;
                let oy = sc.scroll_offset.y.clamp(0, cy - sy);

                sc.scroll_offset.x = ox;
                sc.scroll_offset.y = oy;

                content.outer.left = self.inner.left - ox;
                content.outer.top = self.inner.top - oy;
                content.outer.right = content.outer.left + cx;
                content.outer.bottom = content.outer.top + cy;
                content.inner = content.outer_to_inner(content.outer);
                content.outer_clipped = content.outer.intersect(self.inner_clipped);
                content.inner_clipped = content.inner.intersect(self.inner_clipped);

                let clip = content.inner_clipped;
                content.layout_children(clip);
            }
            _ => {
                let width = self.inner.right - self.inner.left;
                let x = self.inner.left;
                let mut y = self.inner.top;

                for child in Tree::iterate_siblings(self.children.first) {
                    let mut child = child.borrow_mut();
                    let size = child.intrinsic_to_outer();
                    let remaining = (width - size.width).max(0);

                    child.outer.left = x + match child.attributes.position {
                        Position::Stretch | Position::Left => 0,
                        Position::Center => remaining / 2,
                        Position::Right => remaining,
                    };
                    child.outer.right = child.outer.left
                        + match child.attributes.position {
                            Position::Stretch => width,
                            _ => size.width,
                        };
                    child.outer.top = y;
                    child.outer.bottom = y + size.height;

                    child.outer = child.outer.intersect(self.inner);
                    child.inner = child.outer_to_inner(child.outer);
                    child.outer_clipped = child.outer.intersect(clip);
                    child.inner_clipped = child.inner.intersect(clip);

                    y += size.height;
                }

                for child in Tree::iterate_siblings(self.children.first) {
                    let mut child = child.borrow_mut();
                    child.layout_children(clip);
                }
            }
        }
    }
}

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

//! A shoddy framebuffer for terminal applications.

use std::cell::Cell;
use std::fmt::Write;
use std::ops::{BitOr, BitXor};
use std::ptr;
use std::slice::ChunksExact;

use crate::arena::{Arena, ArenaString};
use crate::helpers::{CoordType, Point, Rect, Size};
use crate::oklab::{oklab_blend, srgb_to_oklab};
use crate::simd::{MemsetSafe, memset};
use crate::unicode::MeasurementConfig;

// Same constants as used in the PCG family of RNGs.
#[cfg(target_pointer_width = "32")]
const HASH_MULTIPLIER: usize = 747796405; // https://doi.org/10.1090/S0025-5718-99-00996-5, Table 5
#[cfg(target_pointer_width = "64")]
const HASH_MULTIPLIER: usize = 6364136223846793005; // Knuth's MMIX multiplier

/// The size of our cache table. 1<<8 = 256.
const CACHE_TABLE_LOG2_SIZE: usize = 8;
const CACHE_TABLE_SIZE: usize = 1 << CACHE_TABLE_LOG2_SIZE;
/// To index into the cache table, we use `color * HASH_MULTIPLIER` as the hash.
/// Since the multiplication "shifts" the bits up, we don't just mask the lowest
/// 8 bits out, but rather shift 56 bits down to get the best bits from the top.
const CACHE_TABLE_SHIFT: usize = usize::BITS as usize - CACHE_TABLE_LOG2_SIZE;

/// Standard 16 VT & default foreground/background colors.
#[derive(Clone, Copy)]
pub enum IndexedColor {
    Black,
    Red,
    Green,
    Yellow,
    Blue,
    Magenta,
    Cyan,
    White,
    BrightBlack,
    BrightRed,
    BrightGreen,
    BrightYellow,
    BrightBlue,
    BrightMagenta,
    BrightCyan,
    BrightWhite,

    Background,
    Foreground,
}

/// Number of indices used by [`IndexedColor`].
pub const INDEXED_COLORS_COUNT: usize = 18;

/// Fallback theme. Matches Windows Terminal's Ottosson theme.
pub const DEFAULT_THEME: [u32; INDEXED_COLORS_COUNT] = [
    0xff000000, // Black
    0xff212cbe, // Red
    0xff3aae3f, // Green
    0xff4a9abe, // Yellow
    0xffbe4d20, // Blue
    0xffbe54bb, // Magenta
    0xffb2a700, // Cyan
    0xffbebebe, // White
    0xff808080, // BrightBlack
    0xff303eff, // BrightRed
    0xff51ea58, // BrightGreen
    0xff44c9ff, // BrightYellow
    0xffff6a2f, // BrightBlue
    0xffff74fc, // BrightMagenta
    0xfff0e100, // BrightCyan
    0xffffffff, // BrightWhite
    // --------
    0xff000000, // Background
    0xffbebebe, // Foreground
];

/// A shoddy framebuffer for terminal applications.
///
/// The idea is that you create a [`Framebuffer`], draw a bunch of text and
/// colors into it, and it takes care of figuring out what changed since the
/// last rendering and sending the differences as VT to the terminal.
///
/// This is an improvement over how many other terminal applications work,
/// as they fail to accurately track what changed. If you watch the output
/// of `vim` for instance, you'll notice that it redraws unrelated parts of
/// the screen all the time.
pub struct Framebuffer {
    /// Store the color palette.
    indexed_colors: [u32; INDEXED_COLORS_COUNT],
    /// Front and back buffers. Indexed by `frame_counter & 1`.
    buffers: [Buffer; 2],
    /// The current frame counter. Increments on every `flip` call.
    frame_counter: usize,
    /// The colors used for `contrast()`. It stores the default colors
    /// of the palette as [dark, light], unless the palette is recognized
    /// as a light them, in which case it swaps them.
    auto_colors: [u32; 2],
    /// A cache table for previously contrasted colors.
    /// See: <https://fgiesen.wordpress.com/2019/02/11/cache-tables/>
    contrast_colors: [Cell<(u32, u32)>; CACHE_TABLE_SIZE],
    background_fill: u32,
    foreground_fill: u32,
}

impl Framebuffer {
    /// Creates a new framebuffer.
    pub fn new() -> Self {
        Self {
            indexed_colors: DEFAULT_THEME,
            buffers: Default::default(),
            frame_counter: 0,
            auto_colors: [
                DEFAULT_THEME[IndexedColor::Black as usize],
                DEFAULT_THEME[IndexedColor::BrightWhite as usize],
            ],
            contrast_colors: [const { Cell::new((0, 0)) }; CACHE_TABLE_SIZE],
            background_fill: DEFAULT_THEME[IndexedColor::Background as usize],
            foreground_fill: DEFAULT_THEME[IndexedColor::Foreground as usize],
        }
    }

    /// Sets the base color palette.
    ///
    /// If you call this method, [`Framebuffer`] expects that you
    /// successfully detect the light/dark mode of the terminal.
    pub fn set_indexed_colors(&mut self, colors: [u32; INDEXED_COLORS_COUNT]) {
        self.indexed_colors = colors;
        self.background_fill = 0;
        self.foreground_fill = 0;

        self.auto_colors = [
            self.indexed_colors[IndexedColor::Black as usize],
            self.indexed_colors[IndexedColor::BrightWhite as usize],
        ];
        if !Self::is_dark(self.auto_colors[0]) {
            self.auto_colors.swap(0, 1);
        }
    }

    /// Begins a new frame with the given `size`.
    pub fn flip(&mut self, size: Size) {
        if size != self.buffers[0].bg_bitmap.size {
            for buffer in &mut self.buffers {
                buffer.text = LineBuffer::new(size);
                buffer.bg_bitmap = Bitmap::new(size);
                buffer.fg_bitmap = Bitmap::new(size);
                buffer.attributes = AttributeBuffer::new(size);
            }

            let front = &mut self.buffers[self.frame_counter & 1];
            // Trigger a full redraw. (Yes, it's a hack.)
            front.fg_bitmap.fill(1);
            // Trigger a cursor update as well, just to be sure.
            front.cursor = Cursor::new_invalid();
        }

        self.frame_counter = self.frame_counter.wrapping_add(1);

        let back = &mut self.buffers[self.frame_counter & 1];

        back.text.fill_whitespace();
        back.bg_bitmap.fill(self.background_fill);
        back.fg_bitmap.fill(self.foreground_fill);
        back.attributes.reset();
        back.cursor = Cursor::new_disabled();
    }

    /// Replaces text contents in a single line of the framebuffer.
    /// All coordinates are in viewport coordinates.
    /// Assumes that control characters have been replaced or escaped.
    pub fn replace_text(
        &mut self,
        y: CoordType,
        origin_x: CoordType,
        clip_right: CoordType,
        text: &str,
    ) {
        let back = &mut self.buffers[self.frame_counter & 1];
        back.text.replace_text(y, origin_x, clip_right, text)
    }

    /// Draws a scrollbar in the given `track` rectangle.
    ///
    /// Not entirely sure why I put it here instead of elsewhere.
    ///
    /// # Parameters
    ///
    /// * `clip_rect`: Clips the rendering to this rectangle.
    ///   This is relevant when you have scrollareas inside scrollareas.
    /// * `track`: The rectangle in which to draw the scrollbar.
    ///   In absolute viewport coordinates.
    /// * `content_offset`: The current offset of the scrollarea.
    /// * `content_height`: The height of the scrollarea content.
    pub fn draw_scrollbar(
        &mut self,
        clip_rect: Rect,
        track: Rect,
        content_offset: CoordType,
        content_height: CoordType,
    ) -> CoordType {
        let track_clipped = track.intersect(clip_rect);
        if track_clipped.is_empty() {
            return 0;
        }

        let viewport_height = track.height();
        // The content height is at least the viewport height.
        let content_height = content_height.max(viewport_height);

        // No need to draw a scrollbar if the content fits in the viewport.
        let content_offset_max = content_height - viewport_height;
        if content_offset_max == 0 {
            return 0;
        }

        // The content offset must be at least one viewport height from the bottom.
        // You don't want to scroll past the end after all...
        let content_offset = content_offset.clamp(0, content_offset_max);

        // In order to increase the visual resolution of the scrollbar,
        // we'll use 1/8th blocks to represent the thumb.
        // First, scale the offsets to get that 1/8th resolution.
        let viewport_height = viewport_height as i64 * 8;
        let content_offset_max = content_offset_max as i64 * 8;
        let content_offset = content_offset as i64 * 8;
        let content_height = content_height as i64 * 8;

        // The proportional thumb height (0-1) is the fraction of viewport and
        // content height. The taller the content, the smaller the thumb:
        // = viewport_height / content_height
        // We then scale that to the viewport height to get the height in 1/8th units.
        // = viewport_height * viewport_height / content_height
        // We add content_height/2 to round the integer division, which results in a numerator of:
        // = viewport_height * viewport_height + content_height / 2
        let numerator = viewport_height * viewport_height + content_height / 2;
        let thumb_height = numerator / content_height;
        // Ensure the thumb has a minimum size of 1 row.
        let thumb_height = thumb_height.max(8);

        // The proportional thumb top position (0-1) is:
        // = content_offset / content_offset_max
        // The maximum thumb top position is the viewport height minus the thumb height:
        // = viewport_height - thumb_height
        // To get the thumb top position in 1/8th units, we multiply both:
        // = (viewport_height - thumb_height) * content_offset / content_offset_max
        // We add content_offset_max/2 to round the integer division, which results in a numerator of:
        // = (viewport_height - thumb_height) * content_offset + content_offset_max / 2
        let numerator = (viewport_height - thumb_height) * content_offset + content_offset_max / 2;
        let thumb_top = numerator / content_offset_max;
        // The thumb bottom position is the thumb top position plus the thumb height.
        let thumb_bottom = thumb_top + thumb_height;

        // Shift to absolute coordinates.
        let thumb_top = thumb_top + track.top as i64 * 8;
        let thumb_bottom = thumb_bottom + track.top as i64 * 8;

        // Clamp to the visible area.
        let thumb_top = thumb_top.max(track_clipped.top as i64 * 8);
        let thumb_bottom = thumb_bottom.min(track_clipped.bottom as i64 * 8);

        // Calculate the height of the top/bottom cell of the thumb.
        let top_fract = (thumb_top % 8) as CoordType;
        let bottom_fract = (thumb_bottom % 8) as CoordType;

        // Shift to absolute coordinates.
        let thumb_top = ((thumb_top + 7) / 8) as CoordType;
        let thumb_bottom = (thumb_bottom / 8) as CoordType;

        self.blend_bg(track_clipped, self.indexed(IndexedColor::BrightBlack));
        self.blend_fg(track_clipped, self.indexed(IndexedColor::BrightWhite));

        // Draw the full blocks.
        for y in thumb_top..thumb_bottom {
            self.replace_text(y, track_clipped.left, track_clipped.right, "█");
        }

        // Draw the top/bottom cell of the thumb.
        // U+2581 to U+2588, 1/8th block to 8/8th block elements glyphs: ▁▂▃▄▅▆▇█
        // In UTF8: E2 96 81 to E2 96 88
        let mut fract_buf = [0xE2, 0x96, 0x88];
        if top_fract != 0 {
            fract_buf[2] = (0x88 - top_fract) as u8;
            self.replace_text(thumb_top - 1, track_clipped.left, track_clipped.right, unsafe {
                std::str::from_utf8_unchecked(&fract_buf)
            });
        }
        if bottom_fract != 0 {
            fract_buf[2] = (0x88 - bottom_fract) as u8;
            self.replace_text(thumb_bottom, track_clipped.left, track_clipped.right, unsafe {
                std::str::from_utf8_unchecked(&fract_buf)
            });
            let rect = Rect {
                left: track_clipped.left,
                top: thumb_bottom,
                right: track_clipped.right,
                bottom: thumb_bottom + 1,
            };
            self.blend_bg(rect, self.indexed(IndexedColor::BrightWhite));
            self.blend_fg(rect, self.indexed(IndexedColor::BrightBlack));
        }

        ((thumb_height + 4) / 8) as CoordType
    }

    #[inline]
    pub fn indexed(&self, index: IndexedColor) -> u32 {
        self.indexed_colors[index as usize]
    }

    /// Returns a color from the palette.
    ///
    /// To facilitate constant folding by the compiler,
    /// alpha is given as a fraction (`numerator` / `denominator`).
    #[inline]
    pub fn indexed_alpha(&self, index: IndexedColor, numerator: u32, denominator: u32) -> u32 {
        let c = self.indexed_colors[index as usize];
        let a = 255 * numerator / denominator;
        let r = (((c >> 16) & 0xFF) * numerator) / denominator;
        let g = (((c >> 8) & 0xFF) * numerator) / denominator;
        let b = ((c & 0xFF) * numerator) / denominator;
        a << 24 | r << 16 | g << 8 | b
    }

    /// Returns a color opposite to the brightness of the given `color`.
    pub fn contrasted(&self, color: u32) -> u32 {
        let idx = (color as usize).wrapping_mul(HASH_MULTIPLIER) >> CACHE_TABLE_SHIFT;
        let slot = self.contrast_colors[idx].get();
        if slot.0 == color { slot.1 } else { self.contrasted_slow(color) }
    }

    #[cold]
    fn contrasted_slow(&self, color: u32) -> u32 {
        let idx = (color as usize).wrapping_mul(HASH_MULTIPLIER) >> CACHE_TABLE_SHIFT;
        let contrast = self.auto_colors[Self::is_dark(color) as usize];
        self.contrast_colors[idx].set((color, contrast));
        contrast
    }

    fn is_dark(color: u32) -> bool {
        srgb_to_oklab(color).l < 0.5
    }

    /// Blends the given sRGB color onto the background bitmap.
    ///
    /// TODO: The current approach blends foreground/background independently,
    /// but ideally `blend_bg` with semi-transparent dark should also darken text below it.
    pub fn blend_bg(&mut self, target: Rect, bg: u32) {
        let back = &mut self.buffers[self.frame_counter & 1];
        back.bg_bitmap.blend(target, bg);
    }

    /// Blends the given sRGB color onto the foreground bitmap.
    ///
    /// TODO: The current approach blends foreground/background independently,
    /// but ideally `blend_fg` should blend with the background color below it.
    pub fn blend_fg(&mut self, target: Rect, fg: u32) {
        let back = &mut self.buffers[self.frame_counter & 1];
        back.fg_bitmap.blend(target, fg);
    }

    /// Reverses the foreground and background colors in the given rectangle.
    pub fn reverse(&mut self, target: Rect) {
        let back = &mut self.buffers[self.frame_counter & 1];

        let target = target.intersect(back.bg_bitmap.size.as_rect());
        if target.is_empty() {
            return;
        }

        let top = target.top as usize;
        let bottom = target.bottom as usize;
        let left = target.left as usize;
        let right = target.right as usize;
        let stride = back.bg_bitmap.size.width as usize;

        for y in top..bottom {
            let beg = y * stride + left;
            let end = y * stride + right;
            let bg = &mut back.bg_bitmap.data[beg..end];
            let fg = &mut back.fg_bitmap.data[beg..end];
            bg.swap_with_slice(fg);
        }
    }

    /// Replaces VT attributes in the given rectangle.
    pub fn replace_attr(&mut self, target: Rect, mask: Attributes, attr: Attributes) {
        let back = &mut self.buffers[self.frame_counter & 1];
        back.attributes.replace(target, mask, attr);
    }

    /// Sets the current visible cursor position and type.
    ///
    /// Call this when focus is inside an editable area and you want to show the cursor.
    pub fn set_cursor(&mut self, pos: Point, overtype: bool) {
        let back = &mut self.buffers[self.frame_counter & 1];
        back.cursor.pos = pos;
        back.cursor.overtype = overtype;
    }

    /// Renders the framebuffer contents accumulated since the
    /// last call to `flip()` and returns them serialized as VT.
    pub fn render<'a>(&mut self, arena: &'a Arena) -> ArenaString<'a> {
        let idx = self.frame_counter & 1;
        // Borrows the front/back buffers without letting Rust know that we have a reference to self.
        // SAFETY: Well this is certainly correct, but whether Rust and its strict rules likes it is another question.
        let (back, front) = unsafe {
            let ptr = self.buffers.as_mut_ptr();
            let back = &mut *ptr.add(idx);
            let front = &*ptr.add(1 - idx);
            (back, front)
        };

        let mut front_lines = front.text.lines.iter(); // hahaha
        let mut front_bgs = front.bg_bitmap.iter();
        let mut front_fgs = front.fg_bitmap.iter();
        let mut front_attrs = front.attributes.iter();

        let mut back_lines = back.text.lines.iter();
        let mut back_bgs = back.bg_bitmap.iter();
        let mut back_fgs = back.fg_bitmap.iter();
        let mut back_attrs = back.attributes.iter();

        let mut result = ArenaString::new_in(arena);
        let mut last_bg = u64::MAX;
        let mut last_fg = u64::MAX;
        let mut last_attr = Attributes::None;

        for y in 0..front.text.size.height {
            // SAFETY: The only thing that changes the size of these containers,
            // is the reset() method and it always resets front/back to the same size.
            let front_line = unsafe { front_lines.next().unwrap_unchecked() };
            let front_bg = unsafe { front_bgs.next().unwrap_unchecked() };
            let front_fg = unsafe { front_fgs.next().unwrap_unchecked() };
            let front_attr = unsafe { front_attrs.next().unwrap_unchecked() };

            let back_line = unsafe { back_lines.next().unwrap_unchecked() };
            let back_bg = unsafe { back_bgs.next().unwrap_unchecked() };
            let back_fg = unsafe { back_fgs.next().unwrap_unchecked() };
            let back_attr = unsafe { back_attrs.next().unwrap_unchecked() };

            // TODO: Ideally, we should properly diff the contents and so if
            // only parts of a line change, we should only update those parts.
            if front_line == back_line
                && front_bg == back_bg
                && front_fg == back_fg
                && front_attr == back_attr
            {
                continue;
            }

            let line_bytes = back_line.as_bytes();
            let mut cfg = MeasurementConfig::new(&line_bytes);
            let mut chunk_end = 0;

            if result.is_empty() {
                result.push_str("\x1b[m");
            }
            _ = write!(result, "\x1b[{};1H", y + 1);

            while {
                let bg = back_bg[chunk_end];
                let fg = back_fg[chunk_end];
                let attr = back_attr[chunk_end];

                // Chunk into runs of the same color.
                while {
                    chunk_end += 1;
                    chunk_end < back_bg.len()
                        && back_bg[chunk_end] == bg
                        && back_fg[chunk_end] == fg
                        && back_attr[chunk_end] == attr
                } {}

                if last_bg != bg as u64 {
                    last_bg = bg as u64;
                    self.format_color(&mut result, false, bg);
                }

                if last_fg != fg as u64 {
                    last_fg = fg as u64;
                    self.format_color(&mut result, true, fg);
                }

                if last_attr != attr {
                    let diff = last_attr ^ attr;
                    if diff.is(Attributes::Italic) {
                        if attr.is(Attributes::Italic) {
                            result.push_str("\x1b[3m");
                        } else {
                            result.push_str("\x1b[23m");
                        }
                    }
                    if diff.is(Attributes::Underlined) {
                        if attr.is(Attributes::Underlined) {
                            result.push_str("\x1b[4m");
                        } else {
                            result.push_str("\x1b[24m");
                        }
                    }
                    last_attr = attr;
                }

                let beg = cfg.cursor().offset;
                let end = cfg.goto_visual(Point { x: chunk_end as CoordType, y: 0 }).offset;
                result.push_str(&back_line[beg..end]);

                chunk_end < back_bg.len()
            } {}
        }

        // If the cursor has changed since the last frame we naturally need to update it,
        // but this also applies if the code above wrote to the screen,
        // as it uses CUP sequences to reposition the cursor for writing.
        if !result.is_empty() || back.cursor != front.cursor {
            if back.cursor.pos.x >= 0 && back.cursor.pos.y >= 0 {
                // CUP to the cursor position.
                // DECSCUSR to set the cursor style.
                // DECTCEM to show the cursor.
                _ = write!(
                    result,
                    "\x1b[{};{}H\x1b[{} q\x1b[?25h",
                    back.cursor.pos.y + 1,
                    back.cursor.pos.x + 1,
                    if back.cursor.overtype { 1 } else { 5 }
                );
            } else {
                // DECTCEM to hide the cursor.
                result.push_str("\x1b[?25l");
            }
        }

        result
    }

    fn format_color(&self, dst: &mut ArenaString, fg: bool, mut color: u32) {
        let typ = if fg { '3' } else { '4' };

        // Some terminals support transparent backgrounds which are used
        // if the default background color is active (CSI 49 m).
        //
        // If [`Framebuffer::set_indexed_colors`] was never called, we assume
        // that the terminal doesn't support transparency and initialize the
        // background bitmap with the `DEFAULT_THEME` default background color.
        // Otherwise, we assume that the terminal supports transparency
        // and initialize it with 0x00000000 (transparent).
        //
        // We also apply this to the foreground color, because it compresses
        // the output slightly and ensures that we keep "default foreground"
        // and "color that happens to be default foreground" separate.
        // (This also applies to the background color by the way.)
        if color == 0 {
            _ = write!(dst, "\x1b[{typ}9m");
            return;
        }

        if (color & 0xff000000) != 0xff000000 {
            let idx = if fg { IndexedColor::Foreground } else { IndexedColor::Background };
            let dst = self.indexed(idx);
            color = oklab_blend(dst, color);
        }

        let r = color & 0xff;
        let g = (color >> 8) & 0xff;
        let b = (color >> 16) & 0xff;
        _ = write!(dst, "\x1b[{typ}8;2;{r};{g};{b}m");
    }
}

#[derive(Default)]
struct Buffer {
    text: LineBuffer,
    bg_bitmap: Bitmap,
    fg_bitmap: Bitmap,
    attributes: AttributeBuffer,
    cursor: Cursor,
}

/// A buffer for the text contents of the framebuffer.
#[derive(Default)]
struct LineBuffer {
    lines: Vec<String>,
    size: Size,
}

impl LineBuffer {
    fn new(size: Size) -> Self {
        Self { lines: vec![String::new(); size.height as usize], size }
    }

    fn fill_whitespace(&mut self) {
        let width = self.size.width as usize;
        for l in &mut self.lines {
            l.clear();
            l.reserve(width + width / 2);

            let buf = unsafe { l.as_mut_vec() };
            // Compiles down to `memset()`.
            buf.extend(std::iter::repeat_n(b' ', width));
        }
    }

    /// Replaces text contents in a single line of the framebuffer.
    /// All coordinates are in viewport coordinates.
    /// Assumes that control characters have been replaced or escaped.
    fn replace_text(
        &mut self,
        y: CoordType,
        origin_x: CoordType,
        clip_right: CoordType,
        text: &str,
    ) {
        let Some(line) = self.lines.get_mut(y as usize) else {
            return;
        };

        let bytes = text.as_bytes();
        let clip_right = clip_right.clamp(0, self.size.width);
        let layout_width = clip_right - origin_x;

        // Can't insert text that can't fit or is empty.
        if layout_width <= 0 || bytes.is_empty() {
            return;
        }

        let mut cfg = MeasurementConfig::new(&bytes);

        // Check if the text intersects with the left edge of the framebuffer
        // and figure out the parts that are inside.
        let mut left = origin_x;
        if left < 0 {
            let mut cursor = cfg.goto_visual(Point { x: -left, y: 0 });

            if left + cursor.visual_pos.x < 0 && cursor.offset < text.len() {
                // `-left` must've intersected a wide glyph and since goto_visual stops _before_ reaching the target,
                // we stopped before the wide glyph and thus must step forward to the next glyph.
                cursor = cfg.goto_logical(Point { x: cursor.logical_pos.x + 1, y: 0 });
            }

            left += cursor.visual_pos.x;
        }

        // If the text still starts outside the framebuffer, we must've ran out of text above.
        // Otherwise, if it starts outside the right edge to begin with, we can't insert it anyway.
        if left < 0 || left >= clip_right {
            return;
        }

        // Measure the width of the new text (= `res_new.visual_target.x`).
        let beg_off = cfg.cursor().offset;
        let end = cfg.goto_visual(Point { x: layout_width, y: 0 });

        // Figure out at which byte offset the new text gets inserted.
        let right = left + end.visual_pos.x;
        let line_bytes = line.as_bytes();
        let mut cfg_old = MeasurementConfig::new(&line_bytes);
        let res_old_beg = cfg_old.goto_visual(Point { x: left, y: 0 });
        let mut res_old_end = cfg_old.goto_visual(Point { x: right, y: 0 });

        // Since the goto functions will always stop short of the target position,
        // we need to manually step beyond it if we intersect with a wide glyph.
        if res_old_end.visual_pos.x < right {
            res_old_end = cfg_old.goto_logical(Point { x: res_old_end.logical_pos.x + 1, y: 0 });
        }

        // If we intersect a wide glyph, we need to pad the new text with spaces.
        let src = &text[beg_off..end.offset];
        let overlap_beg = (left - res_old_beg.visual_pos.x).max(0) as usize;
        let overlap_end = (res_old_end.visual_pos.x - right).max(0) as usize;
        let total_add = src.len() + overlap_beg + overlap_end;
        let total_del = res_old_end.offset - res_old_beg.offset;

        // This is basically a hand-written version of `Vec::splice()`,
        // but for strings under the assumption that all inputs are valid.
        // It also takes care of `overlap_beg` and `overlap_end` by inserting spaces.
        unsafe {
            // SAFETY: Our ucd code only returns valid UTF-8 offsets.
            // If it didn't that'd be a priority -9000 bug for any text editor.
            // And apart from that, all inputs are &str (= UTF8).
            let dst = line.as_mut_vec();

            let dst_len = dst.len();
            let src_len = src.len();

            // Make room for the new elements. NOTE that this must be done before
            // we call as_mut_ptr, or else we risk accessing a stale pointer.
            // We only need to reserve as much as the string actually grows by.
            dst.reserve(total_add.saturating_sub(total_del));

            // Move the pointer to the start of the insert.
            let mut ptr = dst.as_mut_ptr().add(res_old_beg.offset);

            // Move the tail end of the string by `total_add - total_del`-many bytes.
            // This both effectively deletes the old text and makes room for the new text.
            if total_add != total_del {
                // Move the tail of the vector to make room for the new elements.
                ptr::copy(
                    ptr.add(total_del),
                    ptr.add(total_add),
                    dst_len - total_del - res_old_beg.offset,
                );
            }

            // Pad left.
            for _ in 0..overlap_beg {
                ptr.write(b' ');
                ptr = ptr.add(1);
            }

            // Copy the new elements into the vector.
            ptr::copy_nonoverlapping(src.as_ptr(), ptr, src_len);
            ptr = ptr.add(src_len);

            // Pad right.
            for _ in 0..overlap_end {
                ptr.write(b' ');
                ptr = ptr.add(1);
            }

            // Update the length of the vector.
            dst.set_len(dst_len - total_del + total_add);
        }
    }
}

/// An sRGB bitmap.
#[derive(Default)]
struct Bitmap {
    data: Vec<u32>,
    size: Size,
}

impl Bitmap {
    fn new(size: Size) -> Self {
        Self { data: vec![0; (size.width * size.height) as usize], size }
    }

    fn fill(&mut self, color: u32) {
        memset(&mut self.data, color);
    }

    /// Blends the given sRGB color onto the bitmap.
    ///
    /// This uses the `oklab` color space for blending so the
    /// resulting colors may look different from what you'd expect.
    fn blend(&mut self, target: Rect, color: u32) {
        if (color & 0xff000000) == 0x00000000 {
            return;
        }

        let target = target.intersect(self.size.as_rect());
        if target.is_empty() {
            return;
        }

        let top = target.top as usize;
        let bottom = target.bottom as usize;
        let left = target.left as usize;
        let right = target.right as usize;
        let stride = self.size.width as usize;

        for y in top..bottom {
            let beg = y * stride + left;
            let end = y * stride + right;
            let data = &mut self.data[beg..end];

            if (color & 0xff000000) == 0xff000000 {
                memset(data, color);
            } else {
                let end = data.len();
                let mut off = 0;

                while {
                    let c = data[off];

                    // Chunk into runs of the same color, so that we only call alpha_blend once per run.
                    let chunk_beg = off;
                    while {
                        off += 1;
                        off < end && data[off] == c
                    } {}
                    let chunk_end = off;

                    let c = oklab_blend(c, color);
                    memset(&mut data[chunk_beg..chunk_end], c);

                    off < end
                } {}
            }
        }
    }

    /// Iterates over each row in the bitmap.
    fn iter(&self) -> ChunksExact<'_, u32> {
        self.data.chunks_exact(self.size.width as usize)
    }
}

/// A bitfield for VT text attributes.
///
/// It being a bitfield allows for simple diffing.
#[repr(transparent)]
#[derive(Default, Clone, Copy, PartialEq, Eq)]
pub struct Attributes(u8);

#[allow(non_upper_case_globals)]
impl Attributes {
    pub const None: Self = Self(0);
    pub const Italic: Self = Self(0b1);
    pub const Underlined: Self = Self(0b10);
    pub const All: Self = Self(0b11);

    pub const fn is(self, attr: Self) -> bool {
        (self.0 & attr.0) == attr.0
    }
}

unsafe impl MemsetSafe for Attributes {}

impl BitOr for Attributes {
    type Output = Self;

    fn bitor(self, rhs: Self) -> Self::Output {
        Self(self.0 | rhs.0)
    }
}

impl BitXor for Attributes {
    type Output = Self;

    fn bitxor(self, rhs: Self) -> Self::Output {
        Self(self.0 ^ rhs.0)
    }
}

/// Stores VT attributes for the framebuffer.
#[derive(Default)]
struct AttributeBuffer {
    data: Vec<Attributes>,
    size: Size,
}

impl AttributeBuffer {
    fn new(size: Size) -> Self {
        Self { data: vec![Default::default(); (size.width * size.height) as usize], size }
    }

    fn reset(&mut self) {
        memset(&mut self.data, Default::default());
    }

    fn replace(&mut self, target: Rect, mask: Attributes, attr: Attributes) {
        let target = target.intersect(self.size.as_rect());
        if target.is_empty() {
            return;
        }

        let top = target.top as usize;
        let bottom = target.bottom as usize;
        let left = target.left as usize;
        let right = target.right as usize;
        let stride = self.size.width as usize;

        for y in top..bottom {
            let beg = y * stride + left;
            let end = y * stride + right;
            let dst = &mut self.data[beg..end];

            if mask == Attributes::All {
                memset(dst, attr);
            } else {
                for a in dst {
                    *a = Attributes(a.0 & !mask.0 | attr.0);
                }
            }
        }
    }

    /// Iterates over each row in the bitmap.
    fn iter(&self) -> ChunksExact<'_, Attributes> {
        self.data.chunks_exact(self.size.width as usize)
    }
}

/// Stores cursor position and type for the framebuffer.
#[derive(Default, PartialEq, Eq)]
struct Cursor {
    pos: Point,
    overtype: bool,
}

impl Cursor {
    const fn new_invalid() -> Self {
        Self { pos: Point::MIN, overtype: false }
    }

    const fn new_disabled() -> Self {
        Self { pos: Point { x: -1, y: -1 }, overtype: false }
    }
}

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

#![feature(allocator_api, linked_list_cursors, string_from_utf8_lossy_owned)]

mod documents;
mod draw_editor;
mod draw_filepicker;
mod draw_menubar;
mod draw_statusbar;
mod localization;
mod state;

use std::borrow::Cow;
#[cfg(feature = "debug-latency")]
use std::fmt::Write;
use std::path::{Path, PathBuf};
use std::time::Duration;
use std::{env, process};

use draw_editor::*;
use draw_filepicker::*;
use draw_menubar::*;
use draw_statusbar::*;
use edit::arena::{self, Arena, ArenaString, scratch_arena};
use edit::framebuffer::{self, IndexedColor};
use edit::helpers::{CoordType, KIBI, MEBI, MetricFormatter, Rect, Size};
use edit::input::{self, kbmod, vk};
use edit::oklab::oklab_blend;
use edit::tui::*;
use edit::vt::{self, Token};
use edit::{apperr, arena_format, base64, path, sys, unicode};
use localization::*;
use state::*;

#[cfg(target_pointer_width = "32")]
const SCRATCH_ARENA_CAPACITY: usize = 128 * MEBI;
#[cfg(target_pointer_width = "64")]
const SCRATCH_ARENA_CAPACITY: usize = 512 * MEBI;

fn main() -> process::ExitCode {
    if cfg!(debug_assertions) {
        let hook = std::panic::take_hook();
        std::panic::set_hook(Box::new(move |info| {
            drop(RestoreModes);
            drop(sys::Deinit);
            hook(info);
        }));
    }

    match run() {
        Ok(()) => process::ExitCode::SUCCESS,
        Err(err) => {
            sys::write_stdout(&format!("{}\n", FormatApperr::from(err)));
            process::ExitCode::FAILURE
        }
    }
}

fn run() -> apperr::Result<()> {
    // Init `sys` first, as everything else may depend on its functionality (IO, function pointers, etc.).
    let _sys_deinit = sys::init();
    // Next init `arena`, so that `scratch_arena` works. `loc` depends on it.
    arena::init(SCRATCH_ARENA_CAPACITY)?;
    // Init the `loc` module, so that error messages are localized.
    localization::init();

    let mut state = State::new()?;
    if handle_args(&mut state)? {
        return Ok(());
    }

    // This will reopen stdin if it's redirected (which may fail) and switch
    // the terminal to raw mode which prevents the user from pressing Ctrl+C.
    // `handle_args` may want to print a help message (must not fail),
    // and reads files (may hang; should be cancelable with Ctrl+C).
    // As such, we call this after `handle_args`.
    sys::switch_modes()?;

    let mut vt_parser = vt::Parser::new();
    let mut input_parser = input::Parser::new();
    let mut tui = Tui::new()?;

    let _restore = setup_terminal(&mut tui, &mut state, &mut vt_parser);

    state.menubar_color_bg = oklab_blend(
        tui.indexed(IndexedColor::Background),
        tui.indexed_alpha(IndexedColor::BrightBlue, 1, 2),
    );
    state.menubar_color_fg = tui.contrasted(state.menubar_color_bg);
    let floater_bg = oklab_blend(
        tui.indexed_alpha(IndexedColor::Background, 2, 3),
        tui.indexed_alpha(IndexedColor::Foreground, 1, 3),
    );
    let floater_fg = tui.contrasted(floater_bg);
    tui.setup_modifier_translations(ModifierTranslations {
        ctrl: loc(LocId::Ctrl),
        alt: loc(LocId::Alt),
        shift: loc(LocId::Shift),
    });
    tui.set_floater_default_bg(floater_bg);
    tui.set_floater_default_fg(floater_fg);
    tui.set_modal_default_bg(floater_bg);
    tui.set_modal_default_fg(floater_fg);

    sys::inject_window_size_into_stdin();

    #[cfg(feature = "debug-latency")]
    let mut last_latency_width = 0;

    loop {
        #[cfg(feature = "debug-latency")]
        let time_beg;
        #[cfg(feature = "debug-latency")]
        let mut passes;

        // Process a batch of input.
        {
            let scratch = scratch_arena(None);
            let read_timeout = vt_parser.read_timeout().min(tui.read_timeout());
            let Some(input) = sys::read_stdin(&scratch, read_timeout) else {
                break;
            };

            #[cfg(feature = "debug-latency")]
            {
                time_beg = std::time::Instant::now();
                passes = 0usize;
            }

            let vt_iter = vt_parser.parse(&input);
            let mut input_iter = input_parser.parse(vt_iter);

            while {
                let input = input_iter.next();
                let more = input.is_some();
                let mut ctx = tui.create_context(input);

                draw(&mut ctx, &mut state);

                #[cfg(feature = "debug-latency")]
                {
                    passes += 1;
                }

                more
            } {}
        }

        // Continue rendering until the layout has settled.
        // This can take >1 frame, if the input focus is tossed between different controls.
        while tui.needs_settling() {
            let mut ctx = tui.create_context(None);

            draw(&mut ctx, &mut state);

            #[cfg(feature = "debug-latency")]
            {
                passes += 1;
            }
        }

        if state.exit {
            break;
        }

        // Render the UI and write it to the terminal.
        {
            let scratch = scratch_arena(None);
            let mut output = tui.render(&scratch);

            write_terminal_title(&mut output, &mut state);

            if state.osc_clipboard_sync {
                write_osc_clipboard(&mut tui, &mut state, &mut output);
            }

            #[cfg(feature = "debug-latency")]
            {
                // Print the number of passes and latency in the top right corner.
                let time_end = std::time::Instant::now();
                let status = time_end - time_beg;

                let scratch_alt = scratch_arena(Some(&scratch));
                let status = arena_format!(
                    &scratch_alt,
                    "{}P {}B {:.3}μs",
                    passes,
                    output.len(),
                    status.as_nanos() as f64 / 1000.0
                );

                // "μs" is 3 bytes and 2 columns.
                let cols = status.len() as edit::helpers::CoordType - 3 + 2;

                // Since the status may shrink and grow, we may have to overwrite the previous one with whitespace.
                let padding = (last_latency_width - cols).max(0);

                // If the `output` is already very large,
                // Rust may double the size during the write below.
                // Let's avoid that by reserving the needed size in advance.
                output.reserve_exact(128);

                // To avoid moving the cursor, push and pop it onto the VT cursor stack.
                _ = write!(
                    output,
                    "\x1b7\x1b[0;41;97m\x1b[1;{0}H{1:2$}{3}\x1b8",
                    tui.size().width - cols - padding + 1,
                    "",
                    padding as usize,
                    status
                );

                last_latency_width = cols;
            }

            sys::write_stdout(&output);
        }
    }

    Ok(())
}

// Returns true if the application should exit early.
fn handle_args(state: &mut State) -> apperr::Result<bool> {
    let scratch = scratch_arena(None);
    let mut paths: Vec<PathBuf, &Arena> = Vec::new_in(&*scratch);
    let mut cwd = env::current_dir()?;

    // The best CLI argument parser in the world.
    for arg in env::args_os().skip(1) {
        if arg == "-h" || arg == "--help" || (cfg!(windows) && arg == "/?") {
            print_help();
            return Ok(true);
        } else if arg == "-v" || arg == "--version" {
            print_version();
            return Ok(true);
        } else if arg == "-" {
            paths.clear();
            break;
        }
        let p = cwd.join(Path::new(&arg));
        let p = path::normalize(&p);
        if !p.is_dir() {
            paths.push(p);
        }
    }

    for p in &paths {
        state.documents.add_file_path(p)?;
    }
    if let Some(parent) = paths.first().and_then(|p| p.parent()) {
        cwd = parent.to_path_buf();
    }

    if let Some(mut file) = sys::open_stdin_if_redirected() {
        let doc = state.documents.add_untitled()?;
        let mut tb = doc.buffer.borrow_mut();
        tb.read_file(&mut file, None)?;
        tb.mark_as_dirty();
    } else if paths.is_empty() {
        // No files were passed, and stdin is not redirected.
        state.documents.add_untitled()?;
    }

    state.file_picker_pending_dir = DisplayablePathBuf::from_path(cwd);
    Ok(false)
}

fn print_help() {
    sys::write_stdout(concat!(
        "Usage: edit [OPTIONS] [FILE[:LINE[:COLUMN]]]\n",
        "Options:\n",
        "    -h, --help       Print this help message\n",
        "    -v, --version    Print the version number\n",
        "\n",
        "Arguments:\n",
        "    FILE[:LINE[:COLUMN]]    The file to open, optionally with line and column (e.g., foo.txt:123:45)\n",
    ));
}

fn print_version() {
    sys::write_stdout(concat!("edit version ", env!("CARGO_PKG_VERSION"), "\n"));
}

fn draw(ctx: &mut Context, state: &mut State) {
    draw_menubar(ctx, state);
    draw_editor(ctx, state);
    draw_statusbar(ctx, state);

    if state.wants_close {
        draw_handle_wants_close(ctx, state);
    }
    if state.wants_exit {
        draw_handle_wants_exit(ctx, state);
    }
    if state.wants_goto {
        draw_goto_menu(ctx, state);
    }
    if state.wants_file_picker != StateFilePicker::None {
        draw_file_picker(ctx, state);
    }
    if state.wants_save {
        draw_handle_save(ctx, state);
    }
    if state.wants_encoding_change != StateEncodingChange::None {
        draw_dialog_encoding_change(ctx, state);
    }
    if state.wants_go_to_file {
        draw_go_to_file(ctx, state);
    }
    if state.wants_about {
        draw_dialog_about(ctx, state);
    }
    if ctx.clipboard_ref().wants_host_sync() {
        draw_handle_clipboard_change(ctx, state);
    }
    if state.error_log_count != 0 {
        draw_error_log(ctx, state);
    }

    if let Some(key) = ctx.keyboard_input() {
        // Shortcuts that are not handled as part of the textarea, etc.

        if key == kbmod::CTRL | vk::N {
            draw_add_untitled_document(ctx, state);
        } else if key == kbmod::CTRL | vk::O {
            state.wants_file_picker = StateFilePicker::Open;
        } else if key == kbmod::CTRL | vk::S {
            state.wants_save = true;
        } else if key == kbmod::CTRL_SHIFT | vk::S {
            state.wants_file_picker = StateFilePicker::SaveAs;
        } else if key == kbmod::CTRL | vk::W {
            state.wants_close = true;
        } else if key == kbmod::CTRL | vk::P {
            state.wants_go_to_file = true;
        } else if key == kbmod::CTRL | vk::Q {
            state.wants_exit = true;
        } else if key == kbmod::CTRL | vk::G {
            state.wants_goto = true;
        } else if key == kbmod::CTRL | vk::F && state.wants_search.kind != StateSearchKind::Disabled
        {
            state.wants_search.kind = StateSearchKind::Search;
            state.wants_search.focus = true;
        } else if key == kbmod::CTRL | vk::R && state.wants_search.kind != StateSearchKind::Disabled
        {
            state.wants_search.kind = StateSearchKind::Replace;
            state.wants_search.focus = true;
        } else if key == vk::F3 {
            search_execute(ctx, state, SearchAction::Search);
        } else {
            return;
        }

        // All of the above shortcuts happen to require a rerender.
        ctx.needs_rerender();
        ctx.set_input_consumed();
    }
}

fn draw_handle_wants_exit(_ctx: &mut Context, state: &mut State) {
    while let Some(doc) = state.documents.active() {
        if doc.buffer.borrow().is_dirty() {
            state.wants_close = true;
            return;
        }
        state.documents.remove_active();
    }

    if state.documents.len() == 0 {
        state.exit = true;
    }
}

fn write_terminal_title(output: &mut ArenaString, state: &mut State) {
    let (filename, dirty) = state
        .documents
        .active()
        .map_or(("", false), |d| (&d.filename, d.buffer.borrow().is_dirty()));

    if filename == state.osc_title_file_status.filename
        && dirty == state.osc_title_file_status.dirty
    {
        return;
    }

    output.push_str("\x1b]0;");
    if !filename.is_empty() {
        if dirty {
            output.push_str("● ");
        }
        output.push_str(&sanitize_control_chars(filename));
        output.push_str(" - ");
    }
    output.push_str("edit\x1b\\");

    state.osc_title_file_status.filename = filename.to_string();
    state.osc_title_file_status.dirty = dirty;
}

const LARGE_CLIPBOARD_THRESHOLD: usize = 128 * KIBI;

fn draw_handle_clipboard_change(ctx: &mut Context, state: &mut State) {
    let data_len = ctx.clipboard_ref().read().len();

    if state.osc_clipboard_always_send || data_len < LARGE_CLIPBOARD_THRESHOLD {
        ctx.clipboard_mut().mark_as_synchronized();
        state.osc_clipboard_sync = true;
        return;
    }

    let over_limit = data_len >= SCRATCH_ARENA_CAPACITY / 4;
    let mut done = None;

    ctx.modal_begin("warning", loc(LocId::WarningDialogTitle));
    {
        ctx.block_begin("description");
        ctx.attr_padding(Rect::three(1, 2, 1));

        if over_limit {
            ctx.label("line1", loc(LocId::LargeClipboardWarningLine1));
            ctx.attr_position(Position::Center);
            ctx.label("line2", loc(LocId::SuperLargeClipboardWarning));
            ctx.attr_position(Position::Center);
        } else {
            let label2 = {
                let template = loc(LocId::LargeClipboardWarningLine2);
                let size = arena_format!(ctx.arena(), "{}", MetricFormatter(data_len));

                let mut label =
                    ArenaString::with_capacity_in(template.len() + size.len(), ctx.arena());
                label.push_str(template);
                label.replace_once_in_place("{size}", &size);
                label
            };

            ctx.label("line1", loc(LocId::LargeClipboardWarningLine1));
            ctx.attr_position(Position::Center);
            ctx.label("line2", &label2);
            ctx.attr_position(Position::Center);
            ctx.label("line3", loc(LocId::LargeClipboardWarningLine3));
            ctx.attr_position(Position::Center);
        }
        ctx.block_end();

        ctx.table_begin("choices");
        ctx.inherit_focus();
        ctx.attr_padding(Rect::three(0, 2, 1));
        ctx.attr_position(Position::Center);
        ctx.table_set_cell_gap(Size { width: 2, height: 0 });
        {
            ctx.table_next_row();
            ctx.inherit_focus();

            if over_limit {
                if ctx.button("ok", loc(LocId::Ok), ButtonStyle::default()) {
                    done = Some(true);
                }
                ctx.inherit_focus();
            } else {
                if ctx.button("always", loc(LocId::Always), ButtonStyle::default()) {
                    state.osc_clipboard_always_send = true;
                    done = Some(true);
                }

                if ctx.button("yes", loc(LocId::Yes), ButtonStyle::default()) {
                    done = Some(true);
                }
                if data_len < 10 * LARGE_CLIPBOARD_THRESHOLD {
                    ctx.inherit_focus();
                }

                if ctx.button("no", loc(LocId::No), ButtonStyle::default()) {
                    done = Some(false);
                }
                if data_len >= 10 * LARGE_CLIPBOARD_THRESHOLD {
                    ctx.inherit_focus();
                }
            }
        }
        ctx.table_end();
    }
    if ctx.modal_end() {
        done = Some(false);
    }

    if let Some(sync) = done {
        state.osc_clipboard_sync = sync;
        ctx.clipboard_mut().mark_as_synchronized();
        ctx.needs_rerender();
    }
}

#[cold]
fn write_osc_clipboard(tui: &mut Tui, state: &mut State, output: &mut ArenaString) {
    let clipboard = tui.clipboard_mut();
    let data = clipboard.read();

    if !data.is_empty() {
        // Rust doubles the size of a string when it needs to grow it.
        // If `data` is *really* large, this may then double
        // the size of the `output` from e.g. 100MB to 200MB. Not good.
        // We can avoid that by reserving the needed size in advance.
        output.reserve_exact(base64::encode_len(data.len()) + 16);
        output.push_str("\x1b]52;c;");
        base64::encode(output, data);
        output.push_str("\x1b\\");
    }

    state.osc_clipboard_sync = false;
}

struct RestoreModes;

impl Drop for RestoreModes {
    fn drop(&mut self) {
        // Same as in the beginning but in the reverse order.
        // It also includes DECSCUSR 0 to reset the cursor style and DECTCEM to show the cursor.
        // We specifically don't reset mode 1036, because most applications expect it to be set nowadays.
        sys::write_stdout("\x1b[0 q\x1b[?25h\x1b]0;\x07\x1b[?1002;1006;2004l\x1b[?1049l");
    }
}

fn setup_terminal(tui: &mut Tui, state: &mut State, vt_parser: &mut vt::Parser) -> RestoreModes {
    sys::write_stdout(concat!(
        // 1049: Alternative Screen Buffer
        //   I put the ASB switch in the beginning, just in case the terminal performs
        //   some additional state tracking beyond the modes we enable/disable.
        // 1002: Cell Motion Mouse Tracking
        // 1006: SGR Mouse Mode
        // 2004: Bracketed Paste Mode
        // 1036: Xterm: "meta sends escape" (Alt keypresses should be encoded with ESC + char)
        "\x1b[?1049h\x1b[?1002;1006;2004h\x1b[?1036h",
        // OSC 4 color table requests for indices 0 through 15 (base colors).
        "\x1b]4;0;?;1;?;2;?;3;?;4;?;5;?;6;?;7;?\x07",
        "\x1b]4;8;?;9;?;10;?;11;?;12;?;13;?;14;?;15;?\x07",
        // OSC 10 and 11 queries for the current foreground and background colors.
        "\x1b]10;?\x07\x1b]11;?\x07",
        // Test whether ambiguous width characters are two columns wide.
        // We use "…", because it's the most common ambiguous width character we use,
        // and the old Windows conhost doesn't actually use wcwidth, it measures the
        // actual display width of the character and assigns it columns accordingly.
        // We detect it by writing the character and asking for the cursor position.
        "\r…\x1b[6n",
        // CSI c reports the terminal capabilities.
        // It also helps us to detect the end of the responses, because not all
        // terminals support the OSC queries, but all of them support CSI c.
        "\x1b[c",
    ));

    let mut done = false;
    let mut osc_buffer = String::new();
    let mut indexed_colors = framebuffer::DEFAULT_THEME;
    let mut color_responses = 0;
    let mut ambiguous_width = 1;

    while !done {
        let scratch = scratch_arena(None);

        // We explicitly set a high read timeout, because we're not
        // waiting for user keyboard input. If we encounter a lone ESC,
        // it's unlikely to be from a ESC keypress, but rather from a VT sequence.
        let Some(input) = sys::read_stdin(&scratch, Duration::from_secs(3)) else {
            break;
        };

        let mut vt_stream = vt_parser.parse(&input);
        while let Some(token) = vt_stream.next() {
            match token {
                Token::Csi(csi) => match csi.final_byte {
                    'c' => done = true,
                    // CPR (Cursor Position Report) response.
                    'R' => ambiguous_width = csi.params[1] as CoordType - 1,
                    _ => {}
                },
                Token::Osc { mut data, partial } => {
                    if partial {
                        osc_buffer.push_str(data);
                        continue;
                    }
                    if !osc_buffer.is_empty() {
                        osc_buffer.push_str(data);
                        data = &osc_buffer;
                    }

                    let mut splits = data.split_terminator(';');

                    let color = match splits.next().unwrap_or("") {
                        // The response is `4;<color>;rgb:<r>/<g>/<b>`.
                        "4" => match splits.next().unwrap_or("").parse::<usize>() {
                            Ok(val) if val < 16 => &mut indexed_colors[val],
                            _ => continue,
                        },
                        // The response is `10;rgb:<r>/<g>/<b>`.
                        "10" => &mut indexed_colors[IndexedColor::Foreground as usize],
                        // The response is `11;rgb:<r>/<g>/<b>`.
                        "11" => &mut indexed_colors[IndexedColor::Background as usize],
                        _ => continue,
                    };

                    let color_param = splits.next().unwrap_or("");
                    if !color_param.starts_with("rgb:") {
                        continue;
                    }

                    let mut iter = color_param[4..].split_terminator('/');
                    let rgb_parts = [(); 3].map(|_| iter.next().unwrap_or("0"));
                    let mut rgb = 0;

                    for part in rgb_parts {
                        if part.len() == 2 || part.len() == 4 {
                            let Ok(mut val) = usize::from_str_radix(part, 16) else {
                                continue;
                            };
                            if part.len() == 4 {
                                // Round from 16 bits to 8 bits.
                                val = (val * 0xff + 0x7fff) / 0xffff;
                            }
                            rgb = (rgb >> 8) | ((val as u32) << 16);
                        }
                    }

                    *color = rgb | 0xff000000;
                    color_responses += 1;
                    osc_buffer.clear();
                }
                _ => {}
            }
        }
    }

    if ambiguous_width == 2 {
        unicode::setup_ambiguous_width(2);
        state.documents.reflow_all();
    }

    if color_responses == indexed_colors.len() {
        tui.setup_indexed_colors(indexed_colors);
    }

    RestoreModes
}

/// Strips all C0 control characters from the string and replaces them with "_".
///
/// Jury is still out on whether this should also strip C1 control characters.
/// That requires parsing UTF8 codepoints, which is annoying.
fn sanitize_control_chars(text: &str) -> Cow<'_, str> {
    if let Some(off) = text.bytes().position(|b| (..0x20).contains(&b)) {
        let mut sanitized = text.to_string();
        // SAFETY: We only search for ASCII and replace it with ASCII.
        let vec = unsafe { sanitized.as_bytes_mut() };

        for i in &mut vec[off..] {
            *i = if (..0x20).contains(i) { b'_' } else { *i }
        }

        Cow::Owned(sanitized)
    } else {
        Cow::Borrowed(text)
    }
}

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use std::fmt;
use std::ops::{Bound, Deref, DerefMut, RangeBounds};

use super::Arena;
use crate::helpers::*;

/// A custom string type, because `std` lacks allocator support for [`String`].
///
/// To keep things simple, this one is hardcoded to [`Arena`].
#[derive(Clone)]
pub struct ArenaString<'a> {
    vec: Vec<u8, &'a Arena>,
}

impl<'a> ArenaString<'a> {
    /// Creates a new [`ArenaString`] in the given arena.
    #[must_use]
    pub const fn new_in(arena: &'a Arena) -> Self {
        Self { vec: Vec::new_in(arena) }
    }

    #[must_use]
    pub fn with_capacity_in(capacity: usize, arena: &'a Arena) -> Self {
        Self { vec: Vec::with_capacity_in(capacity, arena) }
    }

    /// Turns a [`str`] into an [`ArenaString`].
    #[must_use]
    pub fn from_str(arena: &'a Arena, s: &str) -> Self {
        let mut res = Self::new_in(arena);
        res.push_str(s);
        res
    }

    /// It says right here that you checked if `bytes` is valid UTF-8
    /// and you are sure it is. Presto! Here's an `ArenaString`!
    ///
    /// # Safety
    ///
    /// You fool! It says "unchecked" right there. Now the house is burning.
    #[inline]
    #[must_use]
    pub unsafe fn from_utf8_unchecked(bytes: Vec<u8, &'a Arena>) -> Self {
        Self { vec: bytes }
    }

    /// Checks whether `text` contains only valid UTF-8.
    /// If the entire string is valid, it returns `Ok(text)`.
    /// Otherwise, it returns `Err(ArenaString)` with all invalid sequences replaced with U+FFFD.
    pub fn from_utf8_lossy<'s>(arena: &'a Arena, text: &'s [u8]) -> Result<&'s str, Self> {
        let mut iter = text.utf8_chunks();
        let Some(mut chunk) = iter.next() else {
            return Ok("");
        };

        let valid = chunk.valid();
        if chunk.invalid().is_empty() {
            debug_assert_eq!(valid.len(), text.len());
            return Ok(unsafe { str::from_utf8_unchecked(text) });
        }

        const REPLACEMENT: &str = "\u{FFFD}";

        let mut res = Self::new_in(arena);
        res.reserve(text.len());

        loop {
            res.push_str(chunk.valid());
            if !chunk.invalid().is_empty() {
                res.push_str(REPLACEMENT);
            }
            chunk = match iter.next() {
                Some(chunk) => chunk,
                None => break,
            };
        }

        Err(res)
    }

    /// Turns a [`Vec<u8>`] into an [`ArenaString`], replacing invalid UTF-8 sequences with U+FFFD.
    #[must_use]
    pub fn from_utf8_lossy_owned(v: Vec<u8, &'a Arena>) -> Self {
        match Self::from_utf8_lossy(v.allocator(), &v) {
            Ok(..) => unsafe { Self::from_utf8_unchecked(v) },
            Err(s) => s,
        }
    }

    /// It's empty.
    pub fn is_empty(&self) -> bool {
        self.vec.is_empty()
    }

    /// It's lengthy.
    pub fn len(&self) -> usize {
        self.vec.len()
    }

    /// It's capacatity.
    pub fn capacity(&self) -> usize {
        self.vec.capacity()
    }

    /// It's a [`String`], now it's a [`str`]. Wow!
    pub fn as_str(&self) -> &str {
        unsafe { str::from_utf8_unchecked(self.vec.as_slice()) }
    }

    /// It's a [`String`], now it's a [`str`]. And it's mutable! WOW!
    pub fn as_mut_str(&mut self) -> &mut str {
        unsafe { str::from_utf8_unchecked_mut(self.vec.as_mut_slice()) }
    }

    /// Now it's bytes!
    pub fn as_bytes(&self) -> &[u8] {
        self.vec.as_slice()
    }

    /// Returns a mutable reference to the contents of this `String`.
    ///
    /// # Safety
    ///
    /// The underlying `&mut Vec` allows writing bytes which are not valid UTF-8.
    pub unsafe fn as_mut_vec(&mut self) -> &mut Vec<u8, &'a Arena> {
        &mut self.vec
    }

    /// Reserves *additional* memory. For you old folks out there (totally not me),
    /// this is different from C++'s `reserve` which reserves a total size.
    pub fn reserve(&mut self, additional: usize) {
        self.vec.reserve(additional)
    }

    /// Just like [`ArenaString::reserve`], but it doesn't overallocate.
    pub fn reserve_exact(&mut self, additional: usize) {
        self.vec.reserve_exact(additional)
    }

    /// Now it's small! Alarming!
    ///
    /// *Do not* call this unless this string is the last thing on the arena.
    /// Arenas are stacks, they can't deallocate what's in the middle.
    pub fn shrink_to_fit(&mut self) {
        self.vec.shrink_to_fit()
    }

    /// To no surprise, this clears the string.
    pub fn clear(&mut self) {
        self.vec.clear()
    }

    /// Append some text.
    pub fn push_str(&mut self, string: &str) {
        self.vec.extend_from_slice(string.as_bytes())
    }

    /// Append a single character.
    #[inline]
    pub fn push(&mut self, ch: char) {
        match ch.len_utf8() {
            1 => self.vec.push(ch as u8),
            _ => self.vec.extend_from_slice(ch.encode_utf8(&mut [0; 4]).as_bytes()),
        }
    }

    /// Same as `push(char)` but with a specified number of character copies.
    /// Shockingly absent from the standard library.
    pub fn push_repeat(&mut self, ch: char, total_copies: usize) {
        if total_copies == 0 {
            return;
        }

        let buf = unsafe { self.as_mut_vec() };

        if ch.is_ascii() {
            // Compiles down to `memset()`.
            buf.extend(std::iter::repeat_n(ch as u8, total_copies));
        } else {
            // Implements efficient string padding using quadratic duplication.
            let mut utf8_buf = [0; 4];
            let utf8 = ch.encode_utf8(&mut utf8_buf).as_bytes();
            let initial_len = buf.len();
            let added_len = utf8.len() * total_copies;
            let final_len = initial_len + added_len;

            buf.reserve(added_len);
            buf.extend_from_slice(utf8);

            while buf.len() != final_len {
                let end = (final_len - buf.len() + initial_len).min(buf.len());
                buf.extend_from_within(initial_len..end);
            }
        }
    }

    /// Replaces a range of characters with a new string.
    pub fn replace_range<R: RangeBounds<usize>>(&mut self, range: R, replace_with: &str) {
        match range.start_bound() {
            Bound::Included(&n) => assert!(self.is_char_boundary(n)),
            Bound::Excluded(&n) => assert!(self.is_char_boundary(n + 1)),
            Bound::Unbounded => {}
        };
        match range.end_bound() {
            Bound::Included(&n) => assert!(self.is_char_boundary(n + 1)),
            Bound::Excluded(&n) => assert!(self.is_char_boundary(n)),
            Bound::Unbounded => {}
        };
        unsafe { self.as_mut_vec() }.replace_range(range, replace_with.as_bytes());
    }

    /// Finds `old` in the string and replaces it with `new`.
    /// Only performs one replacement.
    pub fn replace_once_in_place(&mut self, old: &str, new: &str) {
        if let Some(beg) = self.find(old) {
            unsafe { self.as_mut_vec() }.replace_range(beg..beg + old.len(), new.as_bytes());
        }
    }
}

impl fmt::Debug for ArenaString<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        fmt::Debug::fmt(&**self, f)
    }
}

impl PartialEq<&str> for ArenaString<'_> {
    fn eq(&self, other: &&str) -> bool {
        self.as_str() == *other
    }
}

impl Deref for ArenaString<'_> {
    type Target = str;

    fn deref(&self) -> &Self::Target {
        self.as_str()
    }
}

impl DerefMut for ArenaString<'_> {
    fn deref_mut(&mut self) -> &mut Self::Target {
        self.as_mut_str()
    }
}

impl fmt::Display for ArenaString<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.as_str())
    }
}

impl fmt::Write for ArenaString<'_> {
    #[inline]
    fn write_str(&mut self, s: &str) -> fmt::Result {
        self.push_str(s);
        Ok(())
    }

    #[inline]
    fn write_char(&mut self, c: char) -> fmt::Result {
        self.push(c);
        Ok(())
    }
}

#[macro_export]
macro_rules! arena_format {
    ($arena:expr, $($arg:tt)*) => {{
        use std::fmt::Write as _;
        let mut output = $crate::arena::ArenaString::new_in($arena);
        output.write_fmt(format_args!($($arg)*)).unwrap();
        output
    }}
}

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use std::hint::cold_path;

use super::Utf8Chars;
use super::tables::*;
use crate::document::ReadableDocument;
use crate::helpers::{CoordType, Point};

// On one hand it's disgusting that I wrote this as a global variable, but on the
// other hand, this isn't a public library API, and it makes the code a lot cleaner,
// because we don't need to inject this once-per-process value everywhere.
static mut AMBIGUOUS_WIDTH: usize = 1;

/// Sets the width of "ambiguous" width characters as per "UAX #11: East Asian Width".
///
/// Defaults to 1.
pub fn setup_ambiguous_width(ambiguous_width: CoordType) {
    unsafe { AMBIGUOUS_WIDTH = ambiguous_width as usize };
}

#[inline]
fn ambiguous_width() -> usize {
    // SAFETY: This is a global variable that is set once per process.
    // It is never changed after that, so this is safe to call.
    unsafe { AMBIGUOUS_WIDTH }
}

/// Stores a position inside a [`ReadableDocument`].
///
/// The cursor tracks both the absolute byte-offset,
/// as well as the position in terminal-related coordinates.
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
pub struct Cursor {
    /// Offset in bytes within the buffer.
    pub offset: usize,
    /// Position in the buffer in lines (.y) and grapheme clusters (.x).
    ///
    /// Line wrapping has NO influence on this.
    pub logical_pos: Point,
    /// Position in the buffer in laid out rows (.y) and columns (.x).
    ///
    /// Line wrapping has an influence on this.
    pub visual_pos: Point,
    /// Horizontal position in visual columns.
    ///
    /// Line wrapping has NO influence on this and if word wrap is disabled,
    /// it's identical to `visual_pos.x`. This is useful for calculating tab widths.
    pub column: CoordType,
    /// When `measure_forward` hits the `word_wrap_column`, the question is:
    /// Was there a wrap opportunity on this line? Because if there wasn't,
    /// a hard-wrap is required; otherwise, the word that is being laid-out is
    /// moved to the next line. This boolean carries this state between calls.
    pub wrap_opp: bool,
}

/// Your entrypoint to navigating inside a [`ReadableDocument`].
#[derive(Clone)]
pub struct MeasurementConfig<'doc> {
    cursor: Cursor,
    tab_size: CoordType,
    word_wrap_column: CoordType,
    buffer: &'doc dyn ReadableDocument,
}

impl<'doc> MeasurementConfig<'doc> {
    /// Creates a new [`MeasurementConfig`] for the given document.
    pub fn new(buffer: &'doc dyn ReadableDocument) -> Self {
        Self { cursor: Default::default(), tab_size: 8, word_wrap_column: 0, buffer }
    }

    /// Sets the initial cursor to the given position.
    ///
    /// WARNING: While the code doesn't panic if the cursor is invalid,
    /// the results will obviously be complete garbage.
    pub fn with_cursor(mut self, cursor: Cursor) -> Self {
        self.cursor = cursor;
        self
    }

    /// Sets the tab size.
    ///
    /// Defaults to 8, because that's what a tab in terminals evaluates to.
    pub fn with_tab_size(mut self, tab_size: CoordType) -> Self {
        self.tab_size = tab_size.max(1);
        self
    }

    /// You want word wrap? Set it here!
    ///
    /// Defaults to 0, which means no word wrap.
    pub fn with_word_wrap_column(mut self, word_wrap_column: CoordType) -> Self {
        self.word_wrap_column = word_wrap_column;
        self
    }

    /// Navigates **forward** to the given absolute offset.
    ///
    /// # Returns
    ///
    /// The cursor position after the navigation.
    pub fn goto_offset(&mut self, offset: usize) -> Cursor {
        self.measure_forward(offset, Point::MAX, Point::MAX)
    }

    /// Navigates **forward** to the given logical position.
    ///
    /// Logical positions are in lines and grapheme clusters.
    ///
    /// # Returns
    ///
    /// The cursor position after the navigation.
    pub fn goto_logical(&mut self, logical_target: Point) -> Cursor {
        self.measure_forward(usize::MAX, logical_target, Point::MAX)
    }

    /// Navigates **forward** to the given visual position.
    ///
    /// Visual positions are in laid out rows and columns.
    ///
    /// # Returns
    ///
    /// The cursor position after the navigation.
    pub fn goto_visual(&mut self, visual_target: Point) -> Cursor {
        self.measure_forward(usize::MAX, Point::MAX, visual_target)
    }

    /// Returns the current cursor position.
    pub fn cursor(&self) -> Cursor {
        self.cursor
    }

    // NOTE that going to a visual target can result in ambiguous results,
    // where going to an identical logical target will yield a different result.
    //
    // Imagine if you have a `word_wrap_column` of 6 and there's "Hello World" on the line:
    // `goto_logical` will return a `visual_pos` of {0,1}, while `goto_visual` returns {6,0}.
    // This is because from a logical POV, if the wrap location equals the wrap column,
    // the wrap exists on both lines and it'll default to wrapping. `goto_visual` however will always
    // try to return a Y position that matches the requested position, so that Home/End works properly.
    fn measure_forward(
        &mut self,
        offset_target: usize,
        logical_target: Point,
        visual_target: Point,
    ) -> Cursor {
        if self.cursor.offset >= offset_target
            || self.cursor.logical_pos >= logical_target
            || self.cursor.visual_pos >= visual_target
        {
            return self.cursor;
        }

        let mut offset = self.cursor.offset;
        let mut logical_pos_x = self.cursor.logical_pos.x;
        let mut logical_pos_y = self.cursor.logical_pos.y;
        let mut visual_pos_x = self.cursor.visual_pos.x;
        let mut visual_pos_y = self.cursor.visual_pos.y;
        let mut column = self.cursor.column;

        let mut logical_target_x = Self::calc_target_x(logical_target, logical_pos_y);
        let mut visual_target_x = Self::calc_target_x(visual_target, visual_pos_y);

        // wrap_opp = Wrap Opportunity
        // These store the position and column of the last wrap opportunity. If `word_wrap_column` is
        // zero (word wrap disabled), all grapheme clusters are a wrap opportunity, because none are.
        let mut wrap_opp = self.cursor.wrap_opp;
        let mut wrap_opp_offset = offset;
        let mut wrap_opp_logical_pos_x = logical_pos_x;
        let mut wrap_opp_visual_pos_x = visual_pos_x;
        let mut wrap_opp_column = column;

        let mut chunk_iter = Utf8Chars::new(b"", 0);
        let mut chunk_range = offset..offset;
        let mut props_next_cluster = ucd_start_of_text_properties();

        loop {
            // Have we reached the target already? Stop.
            if offset >= offset_target
                || logical_pos_x >= logical_target_x
                || visual_pos_x >= visual_target_x
            {
                break;
            }

            let props_current_cluster = props_next_cluster;
            let mut props_last_char;
            let mut offset_next_cluster;
            let mut state = 0;
            let mut width = 0;

            // Since we want to measure the width of the current cluster,
            // by necessity we need to seek to the next cluster.
            // We'll then reuse the offset and properties of the next cluster in
            // the next iteration of the this (outer) loop (`props_next_cluster`).
            loop {
                if !chunk_iter.has_next() {
                    cold_path();
                    chunk_iter = Utf8Chars::new(self.buffer.read_forward(chunk_range.end), 0);
                    chunk_range = chunk_range.end..chunk_range.end + chunk_iter.len();
                }

                // Since this loop seeks ahead to the next cluster, and since `chunk_iter`
                // records the offset of the next character after the returned one, we need
                // to save the offset of the previous `chunk_iter` before calling `next()`.
                // Similar applies to the width.
                props_last_char = props_next_cluster;
                offset_next_cluster = chunk_range.start + chunk_iter.offset();
                width += ucd_grapheme_cluster_character_width(props_next_cluster, ambiguous_width())
                    as CoordType;

                // The `Document::read_forward` interface promises us that it will not split
                // grapheme clusters across chunks. Therefore, we can safely break here.
                let ch = match chunk_iter.next() {
                    Some(ch) => ch,
                    None => break,
                };

                // Get the properties of the next cluster.
                props_next_cluster = ucd_grapheme_cluster_lookup(ch);
                state = ucd_grapheme_cluster_joins(state, props_last_char, props_next_cluster);

                // Stop if the next character does not join.
                if ucd_grapheme_cluster_joins_done(state) {
                    break;
                }
            }

            if offset_next_cluster == offset {
                // No advance and the iterator is empty? End of text reached.
                if chunk_iter.is_empty() {
                    break;
                }
                // Ignore the first iteration when processing the start-of-text.
                continue;
            }

            // The max. width of a terminal cell is 2.
            width = width.min(2);

            // Tabs require special handling because they can have a variable width.
            if props_last_char == ucd_tab_properties() {
                // SAFETY: `self.tab_size` is clamped to >= 1 in `with_tab_size`.
                // This assert ensures that Rust doesn't insert panicking null checks.
                unsafe { std::hint::assert_unchecked(self.tab_size >= 1) };
                width = self.tab_size - (column % self.tab_size);
            }

            // Hard wrap: Both the logical and visual position advance by one line.
            if props_last_char == ucd_linefeed_properties() {
                cold_path();

                wrap_opp = false;

                // Don't cross the newline if the target is on this line but we haven't reached it.
                // E.g. if the callers asks for column 100 on a 10 column line,
                // we'll return with the cursor set to column 10.
                if logical_pos_y >= logical_target.y || visual_pos_y >= visual_target.y {
                    break;
                }

                offset = offset_next_cluster;
                logical_pos_x = 0;
                logical_pos_y += 1;
                visual_pos_x = 0;
                visual_pos_y += 1;
                column = 0;

                logical_target_x = Self::calc_target_x(logical_target, logical_pos_y);
                visual_target_x = Self::calc_target_x(visual_target, visual_pos_y);
                continue;
            }

            // Avoid advancing past the visual target, because `width` can be greater than 1.
            if visual_pos_x + width > visual_target_x {
                break;
            }

            // Since this code above may need to revert to a previous `wrap_opp_*`,
            // it must be done before advancing / checking for `ucd_line_break_joins`.
            if self.word_wrap_column > 0 && visual_pos_x + width > self.word_wrap_column {
                if !wrap_opp {
                    // Otherwise, the lack of a wrap opportunity means that a single word
                    // is wider than the word wrap column. We need to force-break the word.
                    // This is similar to the above, but "bar" gets written at column 0.
                    wrap_opp_offset = offset;
                    wrap_opp_logical_pos_x = logical_pos_x;
                    wrap_opp_visual_pos_x = visual_pos_x;
                    wrap_opp_column = column;
                    visual_pos_x = 0;
                } else {
                    // If we had a wrap opportunity on this line, we can move all
                    // characters since then to the next line without stopping this loop:
                    //   +---------+      +---------+      +---------+
                    //   |      foo|  ->  |         |  ->  |         |
                    //   |         |      |foo      |      |foobar   |
                    //   +---------+      +---------+      +---------+
                    // We don't actually move "foo", but rather just change where "bar" goes.
                    // Since this function doesn't copy text, the end result is the same.
                    visual_pos_x -= wrap_opp_visual_pos_x;
                }

                wrap_opp = false;
                visual_pos_y += 1;
                visual_target_x = Self::calc_target_x(visual_target, visual_pos_y);

                if visual_pos_x == visual_target_x {
                    break;
                }

                // Imagine the word is "hello" and on the "o" we notice it wraps.
                // If the target however was the "e", then we must revert back to "h" and search for it.
                if visual_pos_x > visual_target_x {
                    cold_path();

                    offset = wrap_opp_offset;
                    logical_pos_x = wrap_opp_logical_pos_x;
                    visual_pos_x = 0;
                    column = wrap_opp_column;

                    chunk_iter.seek(chunk_iter.len());
                    chunk_range = offset..offset;
                    props_next_cluster = ucd_start_of_text_properties();
                    continue;
                }
            }

            offset = offset_next_cluster;
            logical_pos_x += 1;
            visual_pos_x += width;
            column += width;

            if self.word_wrap_column > 0
                && !ucd_line_break_joins(props_current_cluster, props_next_cluster)
            {
                wrap_opp = true;
                wrap_opp_offset = offset;
                wrap_opp_logical_pos_x = logical_pos_x;
                wrap_opp_visual_pos_x = visual_pos_x;
                wrap_opp_column = column;
            }
        }

        // If we're here, we hit our target. Now the only question is:
        // Is the word we're currently on so wide that it will be wrapped further down the document?
        if self.word_wrap_column > 0 {
            if !wrap_opp {
                // If the current laid-out line had no wrap opportunities, it means we had an input
                // such as "fooooooooooooooooooooo" at a `word_wrap_column` of e.g. 10. The word
                // didn't fit and the lack of a `wrap_opp` indicates we must force a hard wrap.
                // Thankfully, if we reach this point, that was already done by the code above.
            } else if wrap_opp_logical_pos_x != logical_pos_x && visual_pos_y <= visual_target.y {
                // Imagine the string "foo bar" with a word wrap column of 6. If I ask for the cursor at
                // `logical_pos={5,0}`, then the code above exited while reaching the target.
                // At this point, this function doesn't know yet that after the "b" there's "ar"
                // which causes a word wrap, and causes the final visual position to be {1,1}.
                // This code thus seeks ahead and checks if the current word will wrap or not.
                // Of course we only need to do this if the cursor isn't on a wrap opportunity already.

                // The loop below should not modify the target we already found.
                let mut visual_pos_x_lookahead = visual_pos_x;

                loop {
                    let props_current_cluster = props_next_cluster;
                    let mut props_last_char;
                    let mut offset_next_cluster;
                    let mut state = 0;
                    let mut width = 0;

                    // Since we want to measure the width of the current cluster,
                    // by necessity we need to seek to the next cluster.
                    // We'll then reuse the offset and properties of the next cluster in
                    // the next iteration of the this (outer) loop (`props_next_cluster`).
                    loop {
                        if !chunk_iter.has_next() {
                            cold_path();
                            chunk_iter =
                                Utf8Chars::new(self.buffer.read_forward(chunk_range.end), 0);
                            chunk_range = chunk_range.end..chunk_range.end + chunk_iter.len();
                        }

                        // Since this loop seeks ahead to the next cluster, and since `chunk_iter`
                        // records the offset of the next character after the returned one, we need
                        // to save the offset of the previous `chunk_iter` before calling `next()`.
                        // Similar applies to the width.
                        props_last_char = props_next_cluster;
                        offset_next_cluster = chunk_range.start + chunk_iter.offset();
                        width += ucd_grapheme_cluster_character_width(
                            props_next_cluster,
                            ambiguous_width(),
                        ) as CoordType;

                        // The `Document::read_forward` interface promises us that it will not split
                        // grapheme clusters across chunks. Therefore, we can safely break here.
                        let ch = match chunk_iter.next() {
                            Some(ch) => ch,
                            None => break,
                        };

                        // Get the properties of the next cluster.
                        props_next_cluster = ucd_grapheme_cluster_lookup(ch);
                        state =
                            ucd_grapheme_cluster_joins(state, props_last_char, props_next_cluster);

                        // Stop if the next character does not join.
                        if ucd_grapheme_cluster_joins_done(state) {
                            break;
                        }
                    }

                    if offset_next_cluster == offset {
                        // No advance and the iterator is empty? End of text reached.
                        if chunk_iter.is_empty() {
                            break;
                        }
                        // Ignore the first iteration when processing the start-of-text.
                        continue;
                    }

                    // The max. width of a terminal cell is 2.
                    width = width.min(2);

                    // Tabs require special handling because they can have a variable width.
                    if props_last_char == ucd_tab_properties() {
                        // SAFETY: `self.tab_size` is clamped to >= 1 in `with_tab_size`.
                        // This assert ensures that Rust doesn't insert panicking null checks.
                        unsafe { std::hint::assert_unchecked(self.tab_size >= 1) };
                        width = self.tab_size - (column % self.tab_size);
                    }

                    // Hard wrap: Both the logical and visual position advance by one line.
                    if props_last_char == ucd_linefeed_properties() {
                        break;
                    }

                    visual_pos_x_lookahead += width;

                    if visual_pos_x_lookahead > self.word_wrap_column {
                        visual_pos_x -= wrap_opp_visual_pos_x;
                        visual_pos_y += 1;
                        break;
                    } else if !ucd_line_break_joins(props_current_cluster, props_next_cluster) {
                        break;
                    }
                }
            }

            if visual_pos_y > visual_target.y {
                // Imagine the string "foo bar" with a word wrap column of 6. If I ask for the cursor at
                // `visual_pos={100,0}`, the code above exited early after wrapping without reaching the target.
                // Since I asked for the last character on the first line, we must wrap back up the last wrap
                offset = wrap_opp_offset;
                logical_pos_x = wrap_opp_logical_pos_x;
                visual_pos_x = wrap_opp_visual_pos_x;
                visual_pos_y = visual_target.y;
                column = wrap_opp_column;
                wrap_opp = true;
            }
        }

        self.cursor.offset = offset;
        self.cursor.logical_pos = Point { x: logical_pos_x, y: logical_pos_y };
        self.cursor.visual_pos = Point { x: visual_pos_x, y: visual_pos_y };
        self.cursor.column = column;
        self.cursor.wrap_opp = wrap_opp;
        self.cursor
    }

    #[inline]
    fn calc_target_x(target: Point, pos_y: CoordType) -> CoordType {
        match pos_y.cmp(&target.y) {
            std::cmp::Ordering::Less => CoordType::MAX,
            std::cmp::Ordering::Equal => target.x,
            std::cmp::Ordering::Greater => 0,
        }
    }
}

/// Returns an offset past a newline.
///
/// If `offset` is right in front of a newline,
/// this will return the offset past said newline.
pub fn skip_newline(text: &[u8], mut offset: usize) -> usize {
    if offset >= text.len() {
        return offset;
    }
    if text[offset] == b'\r' {
        offset += 1;
    }
    if offset >= text.len() {
        return offset;
    }
    if text[offset] == b'\n' {
        offset += 1;
    }
    offset
}

/// Strips a trailing newline from the given text.
pub fn strip_newline(mut text: &[u8]) -> &[u8] {
    // Rust generates surprisingly tight assembly for this.
    if text.last() == Some(&b'\n') {
        text = &text[..text.len() - 1];
    }
    if text.last() == Some(&b'\r') {
        text = &text[..text.len() - 1];
    }
    text
}

#[cfg(test)]
mod test {
    use super::*;

    struct ChunkedDoc<'a>(&'a [&'a [u8]]);

    impl ReadableDocument for ChunkedDoc<'_> {
        fn read_forward(&self, mut off: usize) -> &[u8] {
            for chunk in self.0 {
                if off < chunk.len() {
                    return &chunk[off..];
                }
                off -= chunk.len();
            }
            &[]
        }

        fn read_backward(&self, mut off: usize) -> &[u8] {
            for chunk in self.0.iter().rev() {
                if off < chunk.len() {
                    return &chunk[..chunk.len() - off];
                }
                off -= chunk.len();
            }
            &[]
        }
    }

    #[test]
    fn test_measure_forward_newline_start() {
        let cursor =
            MeasurementConfig::new(&"foo\nbar".as_bytes()).goto_visual(Point { x: 0, y: 1 });
        assert_eq!(
            cursor,
            Cursor {
                offset: 4,
                logical_pos: Point { x: 0, y: 1 },
                visual_pos: Point { x: 0, y: 1 },
                column: 0,
                wrap_opp: false,
            }
        );
    }

    #[test]
    fn test_measure_forward_clipped_wide_char() {
        let cursor = MeasurementConfig::new(&"a😶🌫b".as_bytes()).goto_visual(Point { x: 2, y: 0 });
        assert_eq!(
            cursor,
            Cursor {
                offset: 1,
                logical_pos: Point { x: 1, y: 0 },
                visual_pos: Point { x: 1, y: 0 },
                column: 1,
                wrap_opp: false,
            }
        );
    }

    #[test]
    fn test_measure_forward_word_wrap() {
        //   |foo␣  |
        //   |bar␣  |
        //   |baz   |
        let text = "foo bar \nbaz".as_bytes();

        // Does hitting a logical target wrap the visual position along with the word?
        let mut cfg = MeasurementConfig::new(&text).with_word_wrap_column(6);
        let cursor = cfg.goto_logical(Point { x: 5, y: 0 });
        assert_eq!(
            cursor,
            Cursor {
                offset: 5,
                logical_pos: Point { x: 5, y: 0 },
                visual_pos: Point { x: 1, y: 1 },
                column: 5,
                wrap_opp: true,
            }
        );

        // Does hitting the visual target within a word reset the hit back to the end of the visual line?
        let mut cfg = MeasurementConfig::new(&text).with_word_wrap_column(6);
        let cursor = cfg.goto_visual(Point { x: CoordType::MAX, y: 0 });
        assert_eq!(
            cursor,
            Cursor {
                offset: 4,
                logical_pos: Point { x: 4, y: 0 },
                visual_pos: Point { x: 4, y: 0 },
                column: 4,
                wrap_opp: true,
            }
        );

        // Does hitting the same target but with a non-zero starting position result in the same outcome?
        let mut cfg = MeasurementConfig::new(&text).with_word_wrap_column(6).with_cursor(Cursor {
            offset: 1,
            logical_pos: Point { x: 1, y: 0 },
            visual_pos: Point { x: 1, y: 0 },
            column: 1,
            wrap_opp: false,
        });
        let cursor = cfg.goto_visual(Point { x: 5, y: 0 });
        assert_eq!(
            cursor,
            Cursor {
                offset: 4,
                logical_pos: Point { x: 4, y: 0 },
                visual_pos: Point { x: 4, y: 0 },
                column: 4,
                wrap_opp: true,
            }
        );

        let cursor = cfg.goto_visual(Point { x: 0, y: 1 });
        assert_eq!(
            cursor,
            Cursor {
                offset: 4,
                logical_pos: Point { x: 4, y: 0 },
                visual_pos: Point { x: 0, y: 1 },
                column: 4,
                wrap_opp: false,
            }
        );

        let cursor = cfg.goto_visual(Point { x: 5, y: 1 });
        assert_eq!(
            cursor,
            Cursor {
                offset: 8,
                logical_pos: Point { x: 8, y: 0 },
                visual_pos: Point { x: 4, y: 1 },
                column: 8,
                wrap_opp: false,
            }
        );

        let cursor = cfg.goto_visual(Point { x: 0, y: 2 });
        assert_eq!(
            cursor,
            Cursor {
                offset: 9,
                logical_pos: Point { x: 0, y: 1 },
                visual_pos: Point { x: 0, y: 2 },
                column: 0,
                wrap_opp: false,
            }
        );

        let cursor = cfg.goto_visual(Point { x: 5, y: 2 });
        assert_eq!(
            cursor,
            Cursor {
                offset: 12,
                logical_pos: Point { x: 3, y: 1 },
                visual_pos: Point { x: 3, y: 2 },
                column: 3,
                wrap_opp: false,
            }
        );
    }

    #[test]
    fn test_measure_forward_tabs() {
        let text = "a\tb\tc".as_bytes();
        let cursor =
            MeasurementConfig::new(&text).with_tab_size(4).goto_visual(Point { x: 4, y: 0 });
        assert_eq!(
            cursor,
            Cursor {
                offset: 2,
                logical_pos: Point { x: 2, y: 0 },
                visual_pos: Point { x: 4, y: 0 },
                column: 4,
                wrap_opp: false,
            }
        );
    }

    #[test]
    fn test_measure_forward_chunk_boundaries() {
        let chunks = [
            "Hello".as_bytes(),
            "\u{1F469}\u{1F3FB}".as_bytes(), // 8 bytes, 2 columns
            "World".as_bytes(),
        ];
        let doc = ChunkedDoc(&chunks);
        let cursor = MeasurementConfig::new(&doc).goto_visual(Point { x: 5 + 2 + 3, y: 0 });
        assert_eq!(cursor.offset, 5 + 8 + 3);
        assert_eq!(cursor.logical_pos, Point { x: 5 + 1 + 3, y: 0 });
    }

    #[test]
    fn test_exact_wrap() {
        //   |foo_   |
        //   |bar.   |
        //   |abc    |
        let chunks = ["foo ".as_bytes(), "bar".as_bytes(), ".\n".as_bytes(), "abc".as_bytes()];
        let doc = ChunkedDoc(&chunks);
        let mut cfg = MeasurementConfig::new(&doc).with_word_wrap_column(7);
        let max = CoordType::MAX;

        let end0 = cfg.goto_visual(Point { x: 7, y: 0 });
        assert_eq!(
            end0,
            Cursor {
                offset: 4,
                logical_pos: Point { x: 4, y: 0 },
                visual_pos: Point { x: 4, y: 0 },
                column: 4,
                wrap_opp: true,
            }
        );

        let beg1 = cfg.goto_visual(Point { x: 0, y: 1 });
        assert_eq!(
            beg1,
            Cursor {
                offset: 4,
                logical_pos: Point { x: 4, y: 0 },
                visual_pos: Point { x: 0, y: 1 },
                column: 4,
                wrap_opp: false,
            }
        );

        let end1 = cfg.goto_visual(Point { x: max, y: 1 });
        assert_eq!(
            end1,
            Cursor {
                offset: 8,
                logical_pos: Point { x: 8, y: 0 },
                visual_pos: Point { x: 4, y: 1 },
                column: 8,
                wrap_opp: false,
            }
        );

        let beg2 = cfg.goto_visual(Point { x: 0, y: 2 });
        assert_eq!(
            beg2,
            Cursor {
                offset: 9,
                logical_pos: Point { x: 0, y: 1 },
                visual_pos: Point { x: 0, y: 2 },
                column: 0,
                wrap_opp: false,
            }
        );

        let end2 = cfg.goto_visual(Point { x: max, y: 2 });
        assert_eq!(
            end2,
            Cursor {
                offset: 12,
                logical_pos: Point { x: 3, y: 1 },
                visual_pos: Point { x: 3, y: 2 },
                column: 3,
                wrap_opp: false,
            }
        );
    }

    #[test]
    fn test_force_wrap() {
        // |//_     |
        // |aaaaaaaa|
        // |aaaa    |
        let bytes = "// aaaaaaaaaaaa".as_bytes();
        let mut cfg = MeasurementConfig::new(&bytes).with_word_wrap_column(8);
        let max = CoordType::MAX;

        // At the end of "// " there should be a wrap.
        let end0 = cfg.goto_visual(Point { x: max, y: 0 });
        assert_eq!(
            end0,
            Cursor {
                offset: 3,
                logical_pos: Point { x: 3, y: 0 },
                visual_pos: Point { x: 3, y: 0 },
                column: 3,
                wrap_opp: true,
            }
        );

        // Test if the ambiguous visual position at the wrap location doesn't change the offset.
        let beg0 = cfg.goto_visual(Point { x: 0, y: 1 });
        assert_eq!(
            beg0,
            Cursor {
                offset: 3,
                logical_pos: Point { x: 3, y: 0 },
                visual_pos: Point { x: 0, y: 1 },
                column: 3,
                wrap_opp: false,
            }
        );

        // Test if navigating inside the wrapped line doesn't cause further wrapping.
        //
        // This step of the test is important, as it ensures that the following force-wrap works,
        // even if 1 of the 8 "a"s was already processed.
        let beg0_off1 = cfg.goto_logical(Point { x: 4, y: 0 });
        assert_eq!(
            beg0_off1,
            Cursor {
                offset: 4,
                logical_pos: Point { x: 4, y: 0 },
                visual_pos: Point { x: 1, y: 1 },
                column: 4,
                wrap_opp: false,
            }
        );

        // Test if the force-wrap applies at the end of the first 8 "a"s.
        let end1 = cfg.goto_visual(Point { x: max, y: 1 });
        assert_eq!(
            end1,
            Cursor {
                offset: 11,
                logical_pos: Point { x: 11, y: 0 },
                visual_pos: Point { x: 8, y: 1 },
                column: 11,
                wrap_opp: true,
            }
        );

        // Test if the remaining 4 "a"s are properly laid-out.
        let end2 = cfg.goto_visual(Point { x: max, y: 2 });
        assert_eq!(
            end2,
            Cursor {
                offset: 15,
                logical_pos: Point { x: 15, y: 0 },
                visual_pos: Point { x: 4, y: 2 },
                column: 15,
                wrap_opp: false,
            }
        );
    }

    #[test]
    fn test_force_wrap_wide() {
        // These Yijing Hexagram Symbols form no word wrap opportunities.
        let text = "䷀䷁䷂䷃䷄䷅䷆䷇䷈䷉";
        let expected = ["䷀䷁", "䷂䷃", "䷄䷅", "䷆䷇", "䷈䷉"];
        let bytes = text.as_bytes();
        let mut cfg = MeasurementConfig::new(&bytes).with_word_wrap_column(5);

        for (y, &expected) in expected.iter().enumerate() {
            let y = y as CoordType;
            // In order for `goto_visual()` to hit column 0 after a word wrap,
            // it MUST be able to go back by 1 grapheme, which is what this tests.
            let beg = cfg.goto_visual(Point { x: 0, y });
            let end = cfg.goto_visual(Point { x: 5, y });
            let actual = &text[beg.offset..end.offset];
            assert_eq!(actual, expected);
        }
    }

    // Similar to the `test_force_wrap` test, but here we vertically descend
    // down the document without ever touching the first or last column.
    // I found that this finds curious bugs at times.
    #[test]
    fn test_force_wrap_column() {
        // |//_     |
        // |aaaaaaaa|
        // |aaaa    |
        let bytes = "// aaaaaaaaaaaa".as_bytes();
        let mut cfg = MeasurementConfig::new(&bytes).with_word_wrap_column(8);

        // At the end of "// " there should be a wrap.
        let end0 = cfg.goto_visual(Point { x: CoordType::MAX, y: 0 });
        assert_eq!(
            end0,
            Cursor {
                offset: 3,
                logical_pos: Point { x: 3, y: 0 },
                visual_pos: Point { x: 3, y: 0 },
                column: 3,
                wrap_opp: true,
            }
        );

        let mid1 = cfg.goto_visual(Point { x: end0.visual_pos.x, y: 1 });
        assert_eq!(
            mid1,
            Cursor {
                offset: 6,
                logical_pos: Point { x: 6, y: 0 },
                visual_pos: Point { x: 3, y: 1 },
                column: 6,
                wrap_opp: false,
            }
        );

        let mid2 = cfg.goto_visual(Point { x: end0.visual_pos.x, y: 2 });
        assert_eq!(
            mid2,
            Cursor {
                offset: 14,
                logical_pos: Point { x: 14, y: 0 },
                visual_pos: Point { x: 3, y: 2 },
                column: 14,
                wrap_opp: false,
            }
        );
    }

    #[test]
    fn test_any_wrap() {
        // |//_-----|
        // |------- |
        let bytes = "// ------------".as_bytes();
        let mut cfg = MeasurementConfig::new(&bytes).with_word_wrap_column(8);
        let max = CoordType::MAX;

        let end0 = cfg.goto_visual(Point { x: max, y: 0 });
        assert_eq!(
            end0,
            Cursor {
                offset: 8,
                logical_pos: Point { x: 8, y: 0 },
                visual_pos: Point { x: 8, y: 0 },
                column: 8,
                wrap_opp: true,
            }
        );

        let end1 = cfg.goto_visual(Point { x: max, y: 1 });
        assert_eq!(
            end1,
            Cursor {
                offset: 15,
                logical_pos: Point { x: 15, y: 0 },
                visual_pos: Point { x: 7, y: 1 },
                column: 15,
                wrap_opp: true,
            }
        );
    }

    #[test]
    fn test_any_wrap_wide() {
        // These Japanese characters form word wrap opportunity between each character.
        let text = "零一二三四五六七八九";
        let expected = ["零一", "二三", "四五", "六七", "八九"];
        let bytes = text.as_bytes();
        let mut cfg = MeasurementConfig::new(&bytes).with_word_wrap_column(5);

        for (y, &expected) in expected.iter().enumerate() {
            let y = y as CoordType;
            // In order for `goto_visual()` to hit column 0 after a word wrap,
            // it MUST be able to go back by 1 grapheme, which is what this tests.
            let beg = cfg.goto_visual(Point { x: 0, y });
            let end = cfg.goto_visual(Point { x: 5, y });
            let actual = &text[beg.offset..end.offset];
            assert_eq!(actual, expected);
        }
    }

    #[test]
    fn test_wrap_tab() {
        // |foo_    | <- 1 space
        // |____b   | <- 1 tab, 1 space
        let text = "foo \t b";
        let bytes = text.as_bytes();
        let mut cfg = MeasurementConfig::new(&bytes).with_word_wrap_column(8).with_tab_size(4);
        let max = CoordType::MAX;

        let end0 = cfg.goto_visual(Point { x: max, y: 0 });
        assert_eq!(
            end0,
            Cursor {
                offset: 4,
                logical_pos: Point { x: 4, y: 0 },
                visual_pos: Point { x: 4, y: 0 },
                column: 4,
                wrap_opp: true,
            },
        );

        let beg1 = cfg.goto_visual(Point { x: 0, y: 1 });
        assert_eq!(
            beg1,
            Cursor {
                offset: 4,
                logical_pos: Point { x: 4, y: 0 },
                visual_pos: Point { x: 0, y: 1 },
                column: 4,
                wrap_opp: false,
            },
        );

        let end1 = cfg.goto_visual(Point { x: max, y: 1 });
        assert_eq!(
            end1,
            Cursor {
                offset: 7,
                logical_pos: Point { x: 7, y: 0 },
                visual_pos: Point { x: 6, y: 1 },
                column: 10,
                wrap_opp: true,
            },
        );
    }

    #[test]
    fn test_crlf() {
        let text = "a\r\nbcd\r\ne".as_bytes();
        let cursor = MeasurementConfig::new(&text).goto_visual(Point { x: CoordType::MAX, y: 1 });
        assert_eq!(
            cursor,
            Cursor {
                offset: 6,
                logical_pos: Point { x: 3, y: 1 },
                visual_pos: Point { x: 3, y: 1 },
                column: 3,
                wrap_opp: false,
            }
        );
    }

    #[test]
    fn test_wrapped_cursor_can_seek_backward() {
        let bytes = "hello world".as_bytes();
        let mut cfg = MeasurementConfig::new(&bytes).with_word_wrap_column(10);

        // When the word wrap at column 10 hits, the cursor will be at the end of the word "world" (between l and d).
        // This tests if the algorithm is capable of going back to the start of the word and find the actual target.
        let cursor = cfg.goto_visual(Point { x: 2, y: 1 });
        assert_eq!(
            cursor,
            Cursor {
                offset: 8,
                logical_pos: Point { x: 8, y: 0 },
                visual_pos: Point { x: 2, y: 1 },
                column: 8,
                wrap_opp: false,
            }
        );
    }

    #[test]
    fn test_strip_newline() {
        assert_eq!(strip_newline(b"hello\n"), b"hello");
        assert_eq!(strip_newline(b"hello\r\n"), b"hello");
        assert_eq!(strip_newline(b"hello"), b"hello");
    }
}

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

//! Random assortment of helpers I didn't know where to put.

use std::alloc::Allocator;
use std::cmp::Ordering;
use std::io::Read;
use std::mem::{self, MaybeUninit};
use std::ops::{Bound, Range, RangeBounds};
use std::{fmt, ptr, slice, str};

use crate::apperr;

pub const KILO: usize = 1000;
pub const MEGA: usize = 1000 * 1000;
pub const GIGA: usize = 1000 * 1000 * 1000;

pub const KIBI: usize = 1024;
pub const MEBI: usize = 1024 * 1024;
pub const GIBI: usize = 1024 * 1024 * 1024;

pub struct MetricFormatter<T>(pub T);

impl fmt::Display for MetricFormatter<usize> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let mut value = self.0;
        let mut suffix = "B";
        if value >= GIGA {
            value /= GIGA;
            suffix = "GB";
        } else if value >= MEGA {
            value /= MEGA;
            suffix = "MB";
        } else if value >= KILO {
            value /= KILO;
            suffix = "kB";
        }
        write!(f, "{value}{suffix}")
    }
}

/// A viewport coordinate type used throughout the application.
pub type CoordType = isize;

/// To avoid overflow issues because you're adding two [`CoordType::MAX`]
/// values together, you can use [`COORD_TYPE_SAFE_MAX`] instead.
///
/// It equates to half the bits contained in [`CoordType`], which
/// for instance is 32767 (0x7FFF) when [`CoordType`] is a [`i32`].
pub const COORD_TYPE_SAFE_MAX: CoordType = (1 << (CoordType::BITS / 2 - 1)) - 1;

/// A 2D point. Uses [`CoordType`].
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
pub struct Point {
    pub x: CoordType,
    pub y: CoordType,
}

impl Point {
    pub const MIN: Self = Self { x: CoordType::MIN, y: CoordType::MIN };
    pub const MAX: Self = Self { x: CoordType::MAX, y: CoordType::MAX };
}

impl PartialOrd<Self> for Point {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

impl Ord for Point {
    fn cmp(&self, other: &Self) -> Ordering {
        self.y.cmp(&other.y).then(self.x.cmp(&other.x))
    }
}

/// A 2D size. Uses [`CoordType`].
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
pub struct Size {
    pub width: CoordType,
    pub height: CoordType,
}

impl Size {
    pub fn as_rect(&self) -> Rect {
        Rect { left: 0, top: 0, right: self.width, bottom: self.height }
    }
}

/// A 2D rectangle. Uses [`CoordType`].
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
pub struct Rect {
    pub left: CoordType,
    pub top: CoordType,
    pub right: CoordType,
    pub bottom: CoordType,
}

impl Rect {
    /// Mimics CSS's `padding` property where `padding: a` is `a a a a`.
    pub fn one(value: CoordType) -> Self {
        Self { left: value, top: value, right: value, bottom: value }
    }

    /// Mimics CSS's `padding` property where `padding: a b` is `a b a b`,
    /// and `a` is top/bottom and `b` is left/right.
    pub fn two(top_bottom: CoordType, left_right: CoordType) -> Self {
        Self { left: left_right, top: top_bottom, right: left_right, bottom: top_bottom }
    }

    /// Mimics CSS's `padding` property where `padding: a b c` is `a b c b`,
    /// and `a` is top, `b` is left/right, and `c` is bottom.
    pub fn three(top: CoordType, left_right: CoordType, bottom: CoordType) -> Self {
        Self { left: left_right, top, right: left_right, bottom }
    }

    /// Is the rectangle empty?
    pub fn is_empty(&self) -> bool {
        self.left >= self.right || self.top >= self.bottom
    }

    /// Width of the rectangle.
    pub fn width(&self) -> CoordType {
        self.right - self.left
    }

    /// Height of the rectangle.
    pub fn height(&self) -> CoordType {
        self.bottom - self.top
    }

    /// Check if it contains a point.
    pub fn contains(&self, point: Point) -> bool {
        point.x >= self.left && point.x < self.right && point.y >= self.top && point.y < self.bottom
    }

    /// Intersect two rectangles.
    pub fn intersect(&self, rhs: Self) -> Self {
        let l = self.left.max(rhs.left);
        let t = self.top.max(rhs.top);
        let r = self.right.min(rhs.right);
        let b = self.bottom.min(rhs.bottom);

        // Ensure that the size is non-negative. This avoids bugs,
        // because some height/width is negative all of a sudden.
        let r = l.max(r);
        let b = t.max(b);

        Self { left: l, top: t, right: r, bottom: b }
    }
}

/// [`std::cmp::minmax`] is unstable, as per usual.
pub fn minmax<T>(v1: T, v2: T) -> [T; 2]
where
    T: Ord,
{
    if v2 < v1 { [v2, v1] } else { [v1, v2] }
}

#[inline(always)]
#[allow(clippy::ptr_eq)]
fn opt_ptr<T>(a: Option<&T>) -> *const T {
    unsafe { mem::transmute(a) }
}

/// Surprisingly, there's no way in Rust to do a `ptr::eq` on `Option<&T>`.
/// Uses `unsafe` so that the debug performance isn't too bad.
#[inline(always)]
#[allow(clippy::ptr_eq)]
pub fn opt_ptr_eq<T>(a: Option<&T>, b: Option<&T>) -> bool {
    opt_ptr(a) == opt_ptr(b)
}

/// Creates a `&str` from a pointer and a length.
/// Exists, because `std::str::from_raw_parts` is unstable, par for the course.
///
/// # Safety
///
/// The given data must be valid UTF-8.
/// The given data must outlive the returned reference.
#[inline]
#[must_use]
pub const unsafe fn str_from_raw_parts<'a>(ptr: *const u8, len: usize) -> &'a str {
    unsafe { str::from_utf8_unchecked(slice::from_raw_parts(ptr, len)) }
}

/// [`<[T]>::copy_from_slice`] panics if the two slices have different lengths.
/// This one just returns the copied amount.
pub fn slice_copy_safe<T: Copy>(dst: &mut [T], src: &[T]) -> usize {
    let len = src.len().min(dst.len());
    unsafe { ptr::copy_nonoverlapping(src.as_ptr(), dst.as_mut_ptr(), len) };
    len
}

/// [`Vec::splice`] results in really bad assembly.
/// This doesn't. Don't use [`Vec::splice`].
pub trait ReplaceRange<T: Copy> {
    fn replace_range<R: RangeBounds<usize>>(&mut self, range: R, src: &[T]);
}

impl<T: Copy, A: Allocator> ReplaceRange<T> for Vec<T, A> {
    fn replace_range<R: RangeBounds<usize>>(&mut self, range: R, src: &[T]) {
        let start = match range.start_bound() {
            Bound::Included(&start) => start,
            Bound::Excluded(start) => start + 1,
            Bound::Unbounded => 0,
        };
        let end = match range.end_bound() {
            Bound::Included(end) => end + 1,
            Bound::Excluded(&end) => end,
            Bound::Unbounded => usize::MAX,
        };
        vec_replace_impl(self, start..end, src);
    }
}

fn vec_replace_impl<T: Copy, A: Allocator>(dst: &mut Vec<T, A>, range: Range<usize>, src: &[T]) {
    unsafe {
        let dst_len = dst.len();
        let src_len = src.len();
        let off = range.start.min(dst_len);
        let del_len = range.end.saturating_sub(off).min(dst_len - off);

        if del_len == 0 && src_len == 0 {
            return; // nothing to do
        }

        let tail_len = dst_len - off - del_len;
        let new_len = dst_len - del_len + src_len;

        if src_len > del_len {
            dst.reserve(src_len - del_len);
        }

        // NOTE: drop_in_place() is not needed here, because T is constrained to Copy.

        // SAFETY: as_mut_ptr() must called after reserve() to ensure that the pointer is valid.
        let ptr = dst.as_mut_ptr().add(off);

        // Shift the tail.
        if tail_len > 0 && src_len != del_len {
            ptr::copy(ptr.add(del_len), ptr.add(src_len), tail_len);
        }

        // Copy in the replacement.
        ptr::copy_nonoverlapping(src.as_ptr(), ptr, src_len);
        dst.set_len(new_len);
    }
}

/// [`Read`] but with [`MaybeUninit<u8>`] buffers.
pub fn file_read_uninit<T: Read>(
    file: &mut T,
    buf: &mut [MaybeUninit<u8>],
) -> apperr::Result<usize> {
    unsafe {
        let buf_slice = slice::from_raw_parts_mut(buf.as_mut_ptr() as *mut u8, buf.len());
        let n = file.read(buf_slice)?;
        Ok(n)
    }
}

/// Turns a [`&[u8]`] into a [`&[MaybeUninit<T>]`].
#[inline(always)]
pub const fn slice_as_uninit_ref<T>(slice: &[T]) -> &[MaybeUninit<T>] {
    unsafe { slice::from_raw_parts(slice.as_ptr() as *const MaybeUninit<T>, slice.len()) }
}

/// Turns a [`&mut [T]`] into a [`&mut [MaybeUninit<T>]`].
#[inline(always)]
pub const fn slice_as_uninit_mut<T>(slice: &mut [T]) -> &mut [MaybeUninit<T>] {
    unsafe { slice::from_raw_parts_mut(slice.as_mut_ptr() as *mut MaybeUninit<T>, slice.len()) }
}

/// Helpers for ASCII string comparisons.
pub trait AsciiStringHelpers {
    /// Tests if a string starts with a given ASCII prefix.
    ///
    /// This function name really is a mouthful, but it's a combination
    /// of [`str::starts_with`] and [`str::eq_ignore_ascii_case`].
    fn starts_with_ignore_ascii_case(&self, prefix: &str) -> bool;
}

impl AsciiStringHelpers for str {
    fn starts_with_ignore_ascii_case(&self, prefix: &str) -> bool {
        // Casting to bytes first ensures we skip any UTF8 boundary checks.
        // Since the comparison is ASCII, we don't need to worry about that.
        let s = self.as_bytes();
        let p = prefix.as_bytes();
        p.len() <= s.len() && s[..p.len()].eq_ignore_ascii_case(p)
    }
}

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use std::{hint, iter};

/// An iterator over UTF-8 encoded characters.
///
/// This differs from [`std::str::Chars`] in that it works on unsanitized
/// byte slices and transparently replaces invalid UTF-8 sequences with U+FFFD.
///
/// This follows ICU's bitmask approach for `U8_NEXT_OR_FFFD` relatively
/// closely. This is important for compatibility, because it implements the
/// WHATWG recommendation for UTF8 error recovery. It's also helpful, because
/// the excellent folks at ICU have probably spent a lot of time optimizing it.
#[derive(Clone, Copy)]
pub struct Utf8Chars<'a> {
    source: &'a [u8],
    offset: usize,
}

impl<'a> Utf8Chars<'a> {
    /// Creates a new `Utf8Chars` iterator starting at the given `offset`.
    pub fn new(source: &'a [u8], offset: usize) -> Self {
        Self { source, offset }
    }

    /// Returns the byte slice this iterator was created with.
    pub fn source(&self) -> &'a [u8] {
        self.source
    }

    /// Checks if the source is empty.
    pub fn is_empty(&self) -> bool {
        self.source.is_empty()
    }

    /// Returns the length of the source.
    pub fn len(&self) -> usize {
        self.source.len()
    }

    /// Returns the current offset in the byte slice.
    ///
    /// This will be past the last returned character.
    pub fn offset(&self) -> usize {
        self.offset
    }

    /// Sets the offset to continue iterating from.
    pub fn seek(&mut self, offset: usize) {
        self.offset = offset;
    }

    /// Returns true if `next` will return another character.
    pub fn has_next(&self) -> bool {
        self.offset < self.source.len()
    }

    // I found that on mixed 50/50 English/Non-English text,
    // performance actually suffers when this gets inlined.
    #[cold]
    fn next_slow(&mut self, c: u8) -> char {
        if self.offset >= self.source.len() {
            return Self::fffd();
        }

        let mut cp = c as u32;

        if cp < 0xE0 {
            // UTF8-2 = %xC2-DF UTF8-tail

            if cp < 0xC2 {
                return Self::fffd();
            }

            // The lead byte is 110xxxxx
            // -> Strip off the 110 prefix
            cp &= !0xE0;
        } else if cp < 0xF0 {
            // UTF8-3 =
            //   %xE0    %xA0-BF   UTF8-tail
            //   %xE1-EC UTF8-tail UTF8-tail
            //   %xED    %x80-9F   UTF8-tail
            //   %xEE-EF UTF8-tail UTF8-tail

            // This is a pretty neat approach seen in ICU4C, because it's a 1:1 translation of the RFC.
            // I don't understand why others don't do the same thing. It's rather performant.
            const BITS_80_9F: u8 = 1 << 0b100; // 0x80-9F, aka 0b100xxxxx
            const BITS_A0_BF: u8 = 1 << 0b101; // 0xA0-BF, aka 0b101xxxxx
            const BITS_BOTH: u8 = BITS_80_9F | BITS_A0_BF;
            const LEAD_TRAIL1_BITS: [u8; 16] = [
                //             v-- lead byte
                BITS_A0_BF, // 0xE0
                BITS_BOTH,  // 0xE1
                BITS_BOTH,  // 0xE2
                BITS_BOTH,  // 0xE3
                BITS_BOTH,  // 0xE4
                BITS_BOTH,  // 0xE5
                BITS_BOTH,  // 0xE6
                BITS_BOTH,  // 0xE7
                BITS_BOTH,  // 0xE8
                BITS_BOTH,  // 0xE9
                BITS_BOTH,  // 0xEA
                BITS_BOTH,  // 0xEB
                BITS_BOTH,  // 0xEC
                BITS_80_9F, // 0xED
                BITS_BOTH,  // 0xEE
                BITS_BOTH,  // 0xEF
            ];

            // The lead byte is 1110xxxx
            // -> Strip off the 1110 prefix
            cp &= !0xF0;

            let t = self.source[self.offset] as u32;
            if LEAD_TRAIL1_BITS[cp as usize] & (1 << (t >> 5)) == 0 {
                return Self::fffd();
            }
            cp = (cp << 6) | (t & 0x3F);

            self.offset += 1;
            if self.offset >= self.source.len() {
                return Self::fffd();
            }
        } else {
            // UTF8-4 =
            //   %xF0    %x90-BF   UTF8-tail UTF8-tail
            //   %xF1-F3 UTF8-tail UTF8-tail UTF8-tail
            //   %xF4    %x80-8F   UTF8-tail UTF8-tail

            // This is similar to the above, but with the indices flipped:
            // The trail byte is the index and the lead byte mask is the value.
            // This is because the split at 0x90 requires more bits than fit into an u8.
            const TRAIL1_LEAD_BITS: [u8; 16] = [
                // --------- 0xF4 lead
                // |         ...
                // |   +---- 0xF0 lead
                // v   v
                0b_00000, //
                0b_00000, //
                0b_00000, //
                0b_00000, //
                0b_00000, //
                0b_00000, //
                0b_00000, // trail bytes:
                0b_00000, //
                0b_11110, // 0x80-8F -> 0x80-8F can be preceded by 0xF1-F4
                0b_01111, // 0x90-9F -v
                0b_01111, // 0xA0-AF -> 0x90-BF can be preceded by 0xF0-F3
                0b_01111, // 0xB0-BF -^
                0b_00000, //
                0b_00000, //
                0b_00000, //
                0b_00000, //
            ];

            // The lead byte *may* be 11110xxx, but could also be e.g. 11111xxx.
            // -> Only strip off the 1111 prefix
            cp &= !0xF0;

            // Now we can verify if it's actually <= 0xF4.
            // Curiously, this if condition does a lot of heavy lifting for
            // performance (+13%). I think it's just a coincidence though.
            if cp > 4 {
                return Self::fffd();
            }

            let t = self.source[self.offset] as u32;
            if TRAIL1_LEAD_BITS[(t >> 4) as usize] & (1 << cp) == 0 {
                return Self::fffd();
            }
            cp = (cp << 6) | (t & 0x3F);

            self.offset += 1;
            if self.offset >= self.source.len() {
                return Self::fffd();
            }

            // UTF8-tail = %x80-BF
            let t = (self.source[self.offset] as u32).wrapping_sub(0x80);
            if t > 0x3F {
                return Self::fffd();
            }
            cp = (cp << 6) | t;

            self.offset += 1;
            if self.offset >= self.source.len() {
                return Self::fffd();
            }
        }

        // SAFETY: All branches above check for `if self.offset >= self.source.len()`
        // one way or another. This is here because the compiler doesn't get it otherwise.
        unsafe { hint::assert_unchecked(self.offset < self.source.len()) };

        // UTF8-tail = %x80-BF
        let t = (self.source[self.offset] as u32).wrapping_sub(0x80);
        if t > 0x3F {
            return Self::fffd();
        }
        cp = (cp << 6) | t;

        self.offset += 1;

        // SAFETY: If `cp` wasn't a valid codepoint, we already returned U+FFFD above.
        unsafe { char::from_u32_unchecked(cp) }
    }

    // This simultaneously serves as a `cold_path` marker.
    // It improves performance by ~5% and reduces code size.
    #[cold]
    #[inline(always)]
    fn fffd() -> char {
        '\u{FFFD}'
    }
}

impl Iterator for Utf8Chars<'_> {
    type Item = char;

    #[inline]
    fn next(&mut self) -> Option<Self::Item> {
        if self.offset >= self.source.len() {
            return None;
        }

        let c = self.source[self.offset];
        self.offset += 1;

        // Fast-passing ASCII allows this function to be trivially inlined everywhere,
        // as the full decoder is a little too large for that.
        if (c & 0x80) == 0 {
            // UTF8-1 = %x00-7F
            Some(c as char)
        } else {
            // Weirdly enough, adding a hint here to assert that `next_slow`
            // only returns codepoints >= 0x80 makes `ucd` ~5% slower.
            Some(self.next_slow(c))
        }
    }

    #[inline]
    fn size_hint(&self) -> (usize, Option<usize>) {
        // Lower bound: All remaining bytes are 4-byte sequences.
        // Upper bound: All remaining bytes are ASCII.
        let remaining = self.source.len() - self.offset;
        (remaining / 4, Some(remaining))
    }
}

impl iter::FusedIterator for Utf8Chars<'_> {}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_broken_utf8() {
        let source = [b'a', 0xED, 0xA0, 0x80, b'b'];
        let mut chars = Utf8Chars::new(&source, 0);
        let mut offset = 0;
        for chunk in source.utf8_chunks() {
            for ch in chunk.valid().chars() {
                offset += ch.len_utf8();
                assert_eq!(chars.next(), Some(ch));
                assert_eq!(chars.offset(), offset);
            }
            if !chunk.invalid().is_empty() {
                offset += chunk.invalid().len();
                assert_eq!(chars.next(), Some('\u{FFFD}'));
                assert_eq!(chars.offset(), offset);
            }
        }
    }
}

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use std::ops::Range;
use std::ptr::{self, NonNull};
use std::slice;

use crate::document::{ReadableDocument, WriteableDocument};
use crate::helpers::*;
use crate::{apperr, sys};

#[cfg(target_pointer_width = "32")]
const LARGE_CAPACITY: usize = 128 * MEBI;
#[cfg(target_pointer_width = "64")]
const LARGE_CAPACITY: usize = 4 * GIBI;
const LARGE_ALLOC_CHUNK: usize = 64 * KIBI;
const LARGE_GAP_CHUNK: usize = 4 * KIBI;

const SMALL_CAPACITY: usize = 128 * KIBI;
const SMALL_ALLOC_CHUNK: usize = 256;
const SMALL_GAP_CHUNK: usize = 16;

// TODO: Instead of having a specialization for small buffers here,
// tui.rs could also just keep a MRU set of large buffers around.
enum BackingBuffer {
    VirtualMemory(NonNull<u8>, usize),
    Vec(Vec<u8>),
}

impl Drop for BackingBuffer {
    fn drop(&mut self) {
        unsafe {
            if let Self::VirtualMemory(ptr, reserve) = *self {
                sys::virtual_release(ptr, reserve);
            }
        }
    }
}

/// Most people know how `Vec<T>` works: It has some spare capacity at the end,
/// so that pushing into it doesn't reallocate every single time. A gap buffer
/// is the same thing, but the spare capacity can be anywhere in the buffer.
/// This variant is optimized for large buffers and uses virtual memory.
pub struct GapBuffer {
    /// Pointer to the buffer.
    text: NonNull<u8>,
    /// Maximum size of the buffer, including gap.
    reserve: usize,
    /// Size of the buffer, including gap.
    commit: usize,
    /// Length of the stored text, NOT including gap.
    text_length: usize,
    /// Gap offset.
    gap_off: usize,
    /// Gap length.
    gap_len: usize,
    /// Increments every time the buffer is modified.
    generation: u32,
    /// If `Vec(..)`, the buffer is optimized for small amounts of text
    /// and uses the standard heap. Otherwise, it uses virtual memory.
    buffer: BackingBuffer,
}

impl GapBuffer {
    pub fn new(small: bool) -> apperr::Result<Self> {
        let reserve;
        let buffer;
        let text;

        if small {
            reserve = SMALL_CAPACITY;
            text = NonNull::dangling();
            buffer = BackingBuffer::Vec(Vec::new());
        } else {
            reserve = LARGE_CAPACITY;
            text = unsafe { sys::virtual_reserve(reserve)? };
            buffer = BackingBuffer::VirtualMemory(text, reserve);
        }

        Ok(Self {
            text,
            reserve,
            commit: 0,
            text_length: 0,
            gap_off: 0,
            gap_len: 0,
            generation: 0,
            buffer,
        })
    }

    #[allow(clippy::len_without_is_empty)]
    pub fn len(&self) -> usize {
        self.text_length
    }

    pub fn generation(&self) -> u32 {
        self.generation
    }

    pub fn set_generation(&mut self, generation: u32) {
        self.generation = generation;
    }

    /// WARNING: The returned slice must not necessarily be the same length as `len` (due to OOM).
    pub fn allocate_gap(&mut self, off: usize, len: usize, delete: usize) -> &mut [u8] {
        // Sanitize parameters
        let off = off.min(self.text_length);
        let delete = delete.min(self.text_length - off);

        // Move the existing gap if it exists
        if off != self.gap_off {
            self.move_gap(off);
        }

        // Delete the text
        if delete > 0 {
            self.delete_text(delete);
        }

        // Enlarge the gap if needed
        if len > self.gap_len {
            self.enlarge_gap(len);
        }

        self.generation = self.generation.wrapping_add(1);
        unsafe { slice::from_raw_parts_mut(self.text.add(self.gap_off).as_ptr(), self.gap_len) }
    }

    fn move_gap(&mut self, off: usize) {
        if self.gap_len > 0 {
            //
            //                       v gap_off
            // left:  |ABCDEFGHIJKLMN   OPQRSTUVWXYZ|
            //        |ABCDEFGHI   JKLMNOPQRSTUVWXYZ|
            //                  ^ off
            //        move: JKLMN
            //
            //                       v gap_off
            // !left: |ABCDEFGHIJKLMN   OPQRSTUVWXYZ|
            //        |ABCDEFGHIJKLMNOPQRS   TUVWXYZ|
            //                            ^ off
            //        move: OPQRS
            //
            let left = off < self.gap_off;
            let move_src = if left { off } else { self.gap_off + self.gap_len };
            let move_dst = if left { off + self.gap_len } else { self.gap_off };
            let move_len = if left { self.gap_off - off } else { off - self.gap_off };

            unsafe { self.text.add(move_src).copy_to(self.text.add(move_dst), move_len) };

            if cfg!(debug_assertions) {
                // Fill the moved-out bytes with 0xCD to make debugging easier.
                unsafe { self.text.add(off).write_bytes(0xCD, self.gap_len) };
            }
        }

        self.gap_off = off;
    }

    fn delete_text(&mut self, delete: usize) {
        if cfg!(debug_assertions) {
            // Fill the deleted bytes with 0xCD to make debugging easier.
            unsafe { self.text.add(self.gap_off + self.gap_len).write_bytes(0xCD, delete) };
        }

        self.gap_len += delete;
        self.text_length -= delete;
    }

    fn enlarge_gap(&mut self, len: usize) {
        let gap_chunk;
        let alloc_chunk;

        if matches!(self.buffer, BackingBuffer::VirtualMemory(..)) {
            gap_chunk = LARGE_GAP_CHUNK;
            alloc_chunk = LARGE_ALLOC_CHUNK;
        } else {
            gap_chunk = SMALL_GAP_CHUNK;
            alloc_chunk = SMALL_ALLOC_CHUNK;
        }

        let gap_len_old = self.gap_len;
        let gap_len_new = (len + gap_chunk + gap_chunk - 1) & !(gap_chunk - 1);

        let bytes_old = self.commit;
        let bytes_new = self.text_length + gap_len_new;

        if bytes_new > bytes_old {
            let bytes_new = (bytes_new + alloc_chunk - 1) & !(alloc_chunk - 1);

            if bytes_new > self.reserve {
                return;
            }

            match &mut self.buffer {
                BackingBuffer::VirtualMemory(ptr, _) => unsafe {
                    if sys::virtual_commit(ptr.add(bytes_old), bytes_new - bytes_old).is_err() {
                        return;
                    }
                },
                BackingBuffer::Vec(v) => {
                    v.resize(bytes_new, 0);
                    self.text = unsafe { NonNull::new_unchecked(v.as_mut_ptr()) };
                }
            }

            self.commit = bytes_new;
        }

        let gap_beg = unsafe { self.text.add(self.gap_off) };
        unsafe {
            ptr::copy(
                gap_beg.add(gap_len_old).as_ptr(),
                gap_beg.add(gap_len_new).as_ptr(),
                self.text_length - self.gap_off,
            )
        };

        if cfg!(debug_assertions) {
            // Fill the moved-out bytes with 0xCD to make debugging easier.
            unsafe { gap_beg.add(gap_len_old).write_bytes(0xCD, gap_len_new - gap_len_old) };
        }

        self.gap_len = gap_len_new;
    }

    pub fn commit_gap(&mut self, len: usize) {
        assert!(len <= self.gap_len);
        self.text_length += len;
        self.gap_off += len;
        self.gap_len -= len;
    }

    pub fn replace(&mut self, range: Range<usize>, src: &[u8]) {
        let gap = self.allocate_gap(range.start, src.len(), range.end.saturating_sub(range.start));
        let len = slice_copy_safe(gap, src);
        self.commit_gap(len);
    }

    pub fn clear(&mut self) {
        self.gap_off = 0;
        self.gap_len += self.text_length;
        self.generation = self.generation.wrapping_add(1);
        self.text_length = 0;
    }

    pub fn extract_raw(&self, range: Range<usize>, out: &mut Vec<u8>, mut out_off: usize) {
        let end = range.end.min(self.text_length);
        let mut beg = range.start.min(end);
        out_off = out_off.min(out.len());

        if beg >= end {
            return;
        }

        out.reserve(end - beg);

        while beg < end {
            let chunk = self.read_forward(beg);
            let chunk = &chunk[..chunk.len().min(end - beg)];
            out.replace_range(out_off..out_off, chunk);
            beg += chunk.len();
            out_off += chunk.len();
        }
    }

    /// Replaces the entire buffer contents with the given `text`.
    /// The method is optimized for the case where the given `text` already matches
    /// the existing contents. Returns `true` if the buffer contents were changed.
    pub fn copy_from(&mut self, src: &dyn ReadableDocument) -> bool {
        let mut off = 0;

        // Find the position at which the contents change.
        loop {
            let dst_chunk = self.read_forward(off);
            let src_chunk = src.read_forward(off);

            let dst_len = dst_chunk.len();
            let src_len = src_chunk.len();
            let len = dst_len.min(src_len);
            let mismatch = dst_chunk[..len] != src_chunk[..len];

            if mismatch {
                break; // The contents differ.
            }
            if len == 0 {
                if dst_len == src_len {
                    return false; // Both done simultaneously. -> Done.
                }
                break; // One of the two is shorter.
            }

            off += len;
        }

        // Update the buffer starting at `off`.
        loop {
            let chunk = src.read_forward(off);
            self.replace(off..usize::MAX, chunk);
            off += chunk.len();

            // No more data to copy -> Done. By checking this _after_ the replace()
            // call, we ensure that the initial `off..usize::MAX` range is deleted.
            // This fixes going from some buffer contents to being empty.
            if chunk.is_empty() {
                return true;
            }
        }
    }

    /// Copies the contents of the buffer into a string.
    pub fn copy_into(&self, dst: &mut dyn WriteableDocument) {
        let mut beg = 0;
        let mut off = 0;

        while {
            let chunk = self.read_forward(off);

            // The first write will be 0..usize::MAX and effectively clear() the destination.
            // Every subsequent write will be usize::MAX..usize::MAX and thus effectively append().
            dst.replace(beg..usize::MAX, chunk);
            beg = usize::MAX;

            off += chunk.len();
            off < self.text_length
        } {}
    }
}

impl ReadableDocument for GapBuffer {
    fn read_forward(&self, off: usize) -> &[u8] {
        let off = off.min(self.text_length);
        let beg;
        let len;

        if off < self.gap_off {
            // Cursor is before the gap: We can read until the start of the gap.
            beg = off;
            len = self.gap_off - off;
        } else {
            // Cursor is after the gap: We can read until the end of the buffer.
            beg = off + self.gap_len;
            len = self.text_length - off;
        }

        unsafe { slice::from_raw_parts(self.text.add(beg).as_ptr(), len) }
    }

    fn read_backward(&self, off: usize) -> &[u8] {
        let off = off.min(self.text_length);
        let beg;
        let len;

        if off <= self.gap_off {
            // Cursor is before the gap: We can read until the beginning of the buffer.
            beg = 0;
            len = off;
        } else {
            // Cursor is after the gap: We can read until the end of the gap.
            beg = self.gap_off + self.gap_len;
            // The cursor_off doesn't account of the gap_len.
            // (This allows us to move the gap without recalculating the cursor position.)
            len = off - self.gap_off;
        }

        unsafe { slice::from_raw_parts(self.text.add(beg).as_ptr(), len) }
    }
}

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

//! Parses VT sequences into input events.
//!
//! In the future this allows us to take apart the application and
//! support input schemes that aren't VT, such as UEFI, or GUI.

use std::mem;

use crate::helpers::{CoordType, Point, Size};
use crate::vt;

/// Represents a key/modifier combination.
///
/// TODO: Is this a good idea? I did it to allow typing `kbmod::CTRL | vk::A`.
/// The reason it's an awkward u32 and not a struct is to hopefully make ABIs easier later.
/// Of course you could just translate on the ABI boundary, but my hope is that this
/// design lets me realize some restrictions early on that I can't foresee yet.
#[repr(transparent)]
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct InputKey(u32);

impl InputKey {
    pub(crate) const fn new(v: u32) -> Self {
        Self(v)
    }

    pub(crate) const fn from_ascii(ch: char) -> Option<Self> {
        if ch == ' ' || (ch >= '0' && ch <= '9') {
            Some(Self(ch as u32))
        } else if ch >= 'a' && ch <= 'z' {
            Some(Self(ch as u32 & !0x20)) // Shift a-z to A-Z
        } else if ch >= 'A' && ch <= 'Z' {
            Some(Self(kbmod::SHIFT.0 | ch as u32))
        } else {
            None
        }
    }

    pub(crate) const fn value(&self) -> u32 {
        self.0
    }

    pub(crate) const fn key(&self) -> Self {
        Self(self.0 & 0x00FFFFFF)
    }

    pub(crate) const fn modifiers(&self) -> InputKeyMod {
        InputKeyMod(self.0 & 0xFF000000)
    }

    pub(crate) const fn modifiers_contains(&self, modifier: InputKeyMod) -> bool {
        (self.0 & modifier.0) != 0
    }

    pub(crate) const fn with_modifiers(&self, modifiers: InputKeyMod) -> Self {
        Self(self.0 | modifiers.0)
    }
}

/// A keyboard modifier. Ctrl/Alt/Shift.
#[repr(transparent)]
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct InputKeyMod(u32);

impl InputKeyMod {
    const fn new(v: u32) -> Self {
        Self(v)
    }

    pub(crate) const fn contains(&self, modifier: Self) -> bool {
        (self.0 & modifier.0) != 0
    }
}

impl std::ops::BitOr<InputKeyMod> for InputKey {
    type Output = Self;

    fn bitor(self, rhs: InputKeyMod) -> Self {
        Self(self.0 | rhs.0)
    }
}

impl std::ops::BitOr<InputKey> for InputKeyMod {
    type Output = InputKey;

    fn bitor(self, rhs: InputKey) -> InputKey {
        InputKey(self.0 | rhs.0)
    }
}

impl std::ops::BitOrAssign for InputKeyMod {
    fn bitor_assign(&mut self, rhs: Self) {
        self.0 |= rhs.0;
    }
}

/// Keyboard keys.
///
/// The codes defined here match the VK_* constants on Windows.
/// It's a convenient way to handle keyboard input, even on other platforms.
pub mod vk {
    use super::InputKey;

    pub const NULL: InputKey = InputKey::new('\0' as u32);
    pub const BACK: InputKey = InputKey::new(0x08);
    pub const TAB: InputKey = InputKey::new('\t' as u32);
    pub const RETURN: InputKey = InputKey::new('\r' as u32);
    pub const ESCAPE: InputKey = InputKey::new(0x1B);
    pub const SPACE: InputKey = InputKey::new(' ' as u32);
    pub const PRIOR: InputKey = InputKey::new(0x21);
    pub const NEXT: InputKey = InputKey::new(0x22);

    pub const END: InputKey = InputKey::new(0x23);
    pub const HOME: InputKey = InputKey::new(0x24);

    pub const LEFT: InputKey = InputKey::new(0x25);
    pub const UP: InputKey = InputKey::new(0x26);
    pub const RIGHT: InputKey = InputKey::new(0x27);
    pub const DOWN: InputKey = InputKey::new(0x28);

    pub const INSERT: InputKey = InputKey::new(0x2D);
    pub const DELETE: InputKey = InputKey::new(0x2E);

    pub const N0: InputKey = InputKey::new('0' as u32);
    pub const N1: InputKey = InputKey::new('1' as u32);
    pub const N2: InputKey = InputKey::new('2' as u32);
    pub const N3: InputKey = InputKey::new('3' as u32);
    pub const N4: InputKey = InputKey::new('4' as u32);
    pub const N5: InputKey = InputKey::new('5' as u32);
    pub const N6: InputKey = InputKey::new('6' as u32);
    pub const N7: InputKey = InputKey::new('7' as u32);
    pub const N8: InputKey = InputKey::new('8' as u32);
    pub const N9: InputKey = InputKey::new('9' as u32);

    pub const A: InputKey = InputKey::new('A' as u32);
    pub const B: InputKey = InputKey::new('B' as u32);
    pub const C: InputKey = InputKey::new('C' as u32);
    pub const D: InputKey = InputKey::new('D' as u32);
    pub const E: InputKey = InputKey::new('E' as u32);
    pub const F: InputKey = InputKey::new('F' as u32);
    pub const G: InputKey = InputKey::new('G' as u32);
    pub const H: InputKey = InputKey::new('H' as u32);
    pub const I: InputKey = InputKey::new('I' as u32);
    pub const J: InputKey = InputKey::new('J' as u32);
    pub const K: InputKey = InputKey::new('K' as u32);
    pub const L: InputKey = InputKey::new('L' as u32);
    pub const M: InputKey = InputKey::new('M' as u32);
    pub const N: InputKey = InputKey::new('N' as u32);
    pub const O: InputKey = InputKey::new('O' as u32);
    pub const P: InputKey = InputKey::new('P' as u32);
    pub const Q: InputKey = InputKey::new('Q' as u32);
    pub const R: InputKey = InputKey::new('R' as u32);
    pub const S: InputKey = InputKey::new('S' as u32);
    pub const T: InputKey = InputKey::new('T' as u32);
    pub const U: InputKey = InputKey::new('U' as u32);
    pub const V: InputKey = InputKey::new('V' as u32);
    pub const W: InputKey = InputKey::new('W' as u32);
    pub const X: InputKey = InputKey::new('X' as u32);
    pub const Y: InputKey = InputKey::new('Y' as u32);
    pub const Z: InputKey = InputKey::new('Z' as u32);

    pub const NUMPAD0: InputKey = InputKey::new(0x60);
    pub const NUMPAD1: InputKey = InputKey::new(0x61);
    pub const NUMPAD2: InputKey = InputKey::new(0x62);
    pub const NUMPAD3: InputKey = InputKey::new(0x63);
    pub const NUMPAD4: InputKey = InputKey::new(0x64);
    pub const NUMPAD5: InputKey = InputKey::new(0x65);
    pub const NUMPAD6: InputKey = InputKey::new(0x66);
    pub const NUMPAD7: InputKey = InputKey::new(0x67);
    pub const NUMPAD8: InputKey = InputKey::new(0x68);
    pub const NUMPAD9: InputKey = InputKey::new(0x69);
    pub const MULTIPLY: InputKey = InputKey::new(0x6A);
    pub const ADD: InputKey = InputKey::new(0x6B);
    pub const SEPARATOR: InputKey = InputKey::new(0x6C);
    pub const SUBTRACT: InputKey = InputKey::new(0x6D);
    pub const DECIMAL: InputKey = InputKey::new(0x6E);
    pub const DIVIDE: InputKey = InputKey::new(0x6F);

    pub const F1: InputKey = InputKey::new(0x70);
    pub const F2: InputKey = InputKey::new(0x71);
    pub const F3: InputKey = InputKey::new(0x72);
    pub const F4: InputKey = InputKey::new(0x73);
    pub const F5: InputKey = InputKey::new(0x74);
    pub const F6: InputKey = InputKey::new(0x75);
    pub const F7: InputKey = InputKey::new(0x76);
    pub const F8: InputKey = InputKey::new(0x77);
    pub const F9: InputKey = InputKey::new(0x78);
    pub const F10: InputKey = InputKey::new(0x79);
    pub const F11: InputKey = InputKey::new(0x7A);
    pub const F12: InputKey = InputKey::new(0x7B);
    pub const F13: InputKey = InputKey::new(0x7C);
    pub const F14: InputKey = InputKey::new(0x7D);
    pub const F15: InputKey = InputKey::new(0x7E);
    pub const F16: InputKey = InputKey::new(0x7F);
    pub const F17: InputKey = InputKey::new(0x80);
    pub const F18: InputKey = InputKey::new(0x81);
    pub const F19: InputKey = InputKey::new(0x82);
    pub const F20: InputKey = InputKey::new(0x83);
    pub const F21: InputKey = InputKey::new(0x84);
    pub const F22: InputKey = InputKey::new(0x85);
    pub const F23: InputKey = InputKey::new(0x86);
    pub const F24: InputKey = InputKey::new(0x87);
}

/// Keyboard modifiers.
pub mod kbmod {
    use super::InputKeyMod;

    pub const NONE: InputKeyMod = InputKeyMod::new(0x00000000);
    pub const CTRL: InputKeyMod = InputKeyMod::new(0x01000000);
    pub const ALT: InputKeyMod = InputKeyMod::new(0x02000000);
    pub const SHIFT: InputKeyMod = InputKeyMod::new(0x04000000);

    pub const CTRL_ALT: InputKeyMod = InputKeyMod::new(0x03000000);
    pub const CTRL_SHIFT: InputKeyMod = InputKeyMod::new(0x05000000);
    pub const ALT_SHIFT: InputKeyMod = InputKeyMod::new(0x06000000);
    pub const CTRL_ALT_SHIFT: InputKeyMod = InputKeyMod::new(0x07000000);
}

/// Mouse input state. Up/Down, Left/Right, etc.
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
pub enum InputMouseState {
    #[default]
    None,

    // These 3 carry their state between frames.
    Left,
    Middle,
    Right,

    // These 2 get reset to None on the next frame.
    Release,
    Scroll,
}

/// Mouse input.
#[derive(Clone, Copy)]
pub struct InputMouse {
    /// The state of the mouse.Up/Down, Left/Right, etc.
    pub state: InputMouseState,
    /// Any keyboard modifiers that are held down.
    pub modifiers: InputKeyMod,
    /// Position of the mouse in the viewport.
    pub position: Point,
    /// Scroll delta.
    pub scroll: Point,
}

/// Primary result type of the parser.
pub enum Input<'input> {
    /// Window resize event.
    Resize(Size),
    /// Text input.
    /// Note that [`Input::Keyboard`] events can also be text.
    Text(&'input str),
    /// A clipboard paste.
    Paste(Vec<u8>),
    /// Keyboard input.
    Keyboard(InputKey),
    /// Mouse input.
    Mouse(InputMouse),
}

/// Parses VT sequences into input events.
pub struct Parser {
    bracketed_paste: bool,
    bracketed_paste_buf: Vec<u8>,
    x10_mouse_want: bool,
    x10_mouse_buf: [u8; 3],
    x10_mouse_len: usize,
}

impl Parser {
    /// Creates a new parser that turns VT sequences into input events.
    ///
    /// Keep the instance alive for the lifetime of the input stream.
    pub fn new() -> Self {
        Self {
            bracketed_paste: false,
            bracketed_paste_buf: Vec::new(),
            x10_mouse_want: false,
            x10_mouse_buf: [0; 3],
            x10_mouse_len: 0,
        }
    }

    /// Takes an [`vt::Stream`] and returns a [`Stream`]
    /// that turns VT sequences into input events.
    pub fn parse<'parser, 'vt, 'input>(
        &'parser mut self,
        stream: vt::Stream<'vt, 'input>,
    ) -> Stream<'parser, 'vt, 'input> {
        Stream { parser: self, stream }
    }
}

/// An iterator that parses VT sequences into input events.
pub struct Stream<'parser, 'vt, 'input> {
    parser: &'parser mut Parser,
    stream: vt::Stream<'vt, 'input>,
}

impl<'input> Iterator for Stream<'_, '_, 'input> {
    type Item = Input<'input>;

    fn next(&mut self) -> Option<Input<'input>> {
        loop {
            if self.parser.bracketed_paste {
                return self.handle_bracketed_paste();
            }

            if self.parser.x10_mouse_want {
                return self.parse_x10_mouse_coordinates();
            }

            const KEYPAD_LUT: [u8; 8] = [
                vk::UP.value() as u8,    // A
                vk::DOWN.value() as u8,  // B
                vk::RIGHT.value() as u8, // C
                vk::LEFT.value() as u8,  // D
                0,                       // E
                vk::END.value() as u8,   // F
                0,                       // G
                vk::HOME.value() as u8,  // H
            ];

            match self.stream.next()? {
                vt::Token::Text(text) => {
                    return Some(Input::Text(text));
                }
                vt::Token::Ctrl(ch) => match ch {
                    '\0' | '\t' | '\r' => return Some(Input::Keyboard(InputKey::new(ch as u32))),
                    '\n' => return Some(Input::Keyboard(kbmod::CTRL | vk::RETURN)),
                    ..='\x1a' => {
                        // Shift control code to A-Z
                        let key = ch as u32 | 0x40;
                        return Some(Input::Keyboard(kbmod::CTRL | InputKey::new(key)));
                    }
                    '\x7f' => return Some(Input::Keyboard(vk::BACK)),
                    _ => {}
                },
                vt::Token::Esc(ch) => {
                    match ch {
                        '\0' => return Some(Input::Keyboard(vk::ESCAPE)),
                        '\n' => return Some(Input::Keyboard(kbmod::CTRL_ALT | vk::RETURN)),
                        ' '..='~' => {
                            let ch = ch as u32;
                            let key = ch & !0x20; // Shift a-z to A-Z
                            let modifiers =
                                if (ch & 0x20) != 0 { kbmod::ALT } else { kbmod::ALT_SHIFT };
                            return Some(Input::Keyboard(modifiers | InputKey::new(key)));
                        }
                        _ => {}
                    }
                }
                vt::Token::SS3(ch) => match ch {
                    'A'..='H' => {
                        let vk = KEYPAD_LUT[ch as usize - 'A' as usize];
                        if vk != 0 {
                            return Some(Input::Keyboard(InputKey::new(vk as u32)));
                        }
                    }
                    'P'..='S' => {
                        let key = vk::F1.value() + ch as u32 - 'P' as u32;
                        return Some(Input::Keyboard(InputKey::new(key)));
                    }
                    _ => {}
                },
                vt::Token::Csi(csi) => {
                    match csi.final_byte {
                        'A'..='H' => {
                            let vk = KEYPAD_LUT[csi.final_byte as usize - 'A' as usize];
                            if vk != 0 {
                                return Some(Input::Keyboard(
                                    InputKey::new(vk as u32) | Self::parse_modifiers(csi),
                                ));
                            }
                        }
                        'Z' => return Some(Input::Keyboard(kbmod::SHIFT | vk::TAB)),
                        '~' => {
                            const LUT: [u8; 35] = [
                                0,
                                vk::HOME.value() as u8,   // 1
                                vk::INSERT.value() as u8, // 2
                                vk::DELETE.value() as u8, // 3
                                vk::END.value() as u8,    // 4
                                vk::PRIOR.value() as u8,  // 5
                                vk::NEXT.value() as u8,   // 6
                                0,
                                0,
                                0,
                                0,
                                0,
                                0,
                                0,
                                0,
                                vk::F5.value() as u8, // 15
                                0,
                                vk::F6.value() as u8,  // 17
                                vk::F7.value() as u8,  // 18
                                vk::F8.value() as u8,  // 19
                                vk::F9.value() as u8,  // 20
                                vk::F10.value() as u8, // 21
                                0,
                                vk::F11.value() as u8, // 23
                                vk::F12.value() as u8, // 24
                                vk::F13.value() as u8, // 25
                                vk::F14.value() as u8, // 26
                                0,
                                vk::F15.value() as u8, // 28
                                vk::F16.value() as u8, // 29
                                0,
                                vk::F17.value() as u8, // 31
                                vk::F18.value() as u8, // 32
                                vk::F19.value() as u8, // 33
                                vk::F20.value() as u8, // 34
                            ];
                            const LUT_LEN: u16 = LUT.len() as u16;

                            match csi.params[0] {
                                0..LUT_LEN => {
                                    let vk = LUT[csi.params[0] as usize];
                                    if vk != 0 {
                                        return Some(Input::Keyboard(
                                            InputKey::new(vk as u32) | Self::parse_modifiers(csi),
                                        ));
                                    }
                                }
                                200 => self.parser.bracketed_paste = true,
                                _ => {}
                            }
                        }
                        'm' | 'M' if csi.private_byte == '<' => {
                            let btn = csi.params[0];
                            let mut mouse = InputMouse {
                                state: InputMouseState::None,
                                modifiers: kbmod::NONE,
                                position: Default::default(),
                                scroll: Default::default(),
                            };

                            mouse.state = InputMouseState::None;
                            if (btn & 0x40) != 0 {
                                mouse.state = InputMouseState::Scroll;
                                mouse.scroll.y += if (btn & 0x01) != 0 { 3 } else { -3 };
                            } else if csi.final_byte == 'M' {
                                const STATES: [InputMouseState; 4] = [
                                    InputMouseState::Left,
                                    InputMouseState::Middle,
                                    InputMouseState::Right,
                                    InputMouseState::None,
                                ];
                                mouse.state = STATES[(btn as usize) & 0x03];
                            }

                            mouse.modifiers = kbmod::NONE;
                            mouse.modifiers |=
                                if (btn & 0x04) != 0 { kbmod::SHIFT } else { kbmod::NONE };
                            mouse.modifiers |=
                                if (btn & 0x08) != 0 { kbmod::ALT } else { kbmod::NONE };
                            mouse.modifiers |=
                                if (btn & 0x10f) != 0 { kbmod::CTRL } else { kbmod::NONE };

                            mouse.position.x = csi.params[1] as CoordType - 1;
                            mouse.position.y = csi.params[2] as CoordType - 1;
                            return Some(Input::Mouse(mouse));
                        }
                        'M' if csi.param_count == 0 => {
                            self.parser.x10_mouse_want = true;
                        }
                        't' if csi.params[0] == 8 => {
                            // Window Size
                            let width = (csi.params[2] as CoordType).clamp(1, 32767);
                            let height = (csi.params[1] as CoordType).clamp(1, 32767);
                            return Some(Input::Resize(Size { width, height }));
                        }
                        _ => {}
                    }
                }
                _ => {}
            }
        }
    }
}

impl<'input> Stream<'_, '_, 'input> {
    /// Once we encounter the start of a bracketed paste
    /// we seek to the end of the paste in this function.
    ///
    /// A bracketed paste is basically:
    /// ```text
    /// <ESC>[201~    lots of text    <ESC>[201~
    /// ```
    ///
    /// That in between text is then expected to be taken literally.
    /// It can be in between anything though, including other escape sequences.
    /// This is the reason why this is a separate method.
    #[cold]
    fn handle_bracketed_paste(&mut self) -> Option<Input<'input>> {
        let beg = self.stream.offset();
        let mut end = beg;

        while let Some(token) = self.stream.next() {
            if let vt::Token::Csi(csi) = token
                && csi.final_byte == '~'
                && csi.params[0] == 201
            {
                self.parser.bracketed_paste = false;
                break;
            }
            end = self.stream.offset();
        }

        if end != beg {
            self.parser
                .bracketed_paste_buf
                .extend_from_slice(&self.stream.input().as_bytes()[beg..end]);
        }

        if !self.parser.bracketed_paste {
            Some(Input::Paste(mem::take(&mut self.parser.bracketed_paste_buf)))
        } else {
            None
        }
    }

    /// Implements the X10 mouse protocol via `CSI M CbCxCy`.
    ///
    /// You want to send numeric mouse coordinates.
    /// You have CSI sequences with numeric parameters.
    /// So, of course you put the coordinates as shifted ASCII characters after
    /// the end of the sequence. Limited coordinate range and complicated parsing!
    /// This is so puzzling to me. The existence of this function makes me unhappy.
    #[cold]
    fn parse_x10_mouse_coordinates(&mut self) -> Option<Input<'input>> {
        self.parser.x10_mouse_len +=
            self.stream.read(&mut self.parser.x10_mouse_buf[self.parser.x10_mouse_len..]);
        if self.parser.x10_mouse_len < 3 {
            return None;
        }

        let button = self.parser.x10_mouse_buf[0] & 0b11;
        let modifier = self.parser.x10_mouse_buf[0] & 0b11100;
        let x = self.parser.x10_mouse_buf[1] as CoordType - 0x21;
        let y = self.parser.x10_mouse_buf[2] as CoordType - 0x21;
        let action = match button {
            0 => InputMouseState::Left,
            1 => InputMouseState::Middle,
            2 => InputMouseState::Right,
            _ => InputMouseState::None,
        };
        let modifiers = match modifier {
            4 => kbmod::SHIFT,
            8 => kbmod::ALT,
            16 => kbmod::CTRL,
            _ => kbmod::NONE,
        };

        self.parser.x10_mouse_want = false;
        self.parser.x10_mouse_len = 0;

        Some(Input::Mouse(InputMouse {
            state: action,
            modifiers,
            position: Point { x, y },
            scroll: Default::default(),
        }))
    }

    fn parse_modifiers(csi: &vt::Csi) -> InputKeyMod {
        let mut modifiers = kbmod::NONE;
        let p1 = csi.params[1].saturating_sub(1);
        if (p1 & 0x01) != 0 {
            modifiers |= kbmod::SHIFT;
        }
        if (p1 & 0x02) != 0 {
            modifiers |= kbmod::ALT;
        }
        if (p1 & 0x04) != 0 {
            modifiers |= kbmod::CTRL;
        }
        modifiers
    }
}

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

//! Our VT parser.

use std::time;

use crate::simd::memchr2;
use crate::unicode::Utf8Chars;

/// The parser produces these tokens.
pub enum Token<'parser, 'input> {
    /// A bunch of text. Doesn't contain any control characters.
    Text(&'input str),
    /// A single control character, like backspace or return.
    Ctrl(char),
    /// We encountered `ESC x` and this contains `x`.
    Esc(char),
    /// We encountered `ESC O x` and this contains `x`.
    SS3(char),
    /// A CSI sequence started with `ESC [`.
    ///
    /// They are the most common escape sequences. See [`Csi`].
    Csi(&'parser Csi),
    /// An OSC sequence started with `ESC ]`.
    ///
    /// The sequence may be split up into multiple tokens if the input
    /// is given in chunks. This is indicated by the `partial` field.
    Osc { data: &'input str, partial: bool },
    /// An DCS sequence started with `ESC P`.
    ///
    /// The sequence may be split up into multiple tokens if the input
    /// is given in chunks. This is indicated by the `partial` field.
    Dcs { data: &'input str, partial: bool },
}

/// Stores the state of the parser.
#[derive(Clone, Copy)]
enum State {
    Ground,
    Esc,
    Ss3,
    Csi,
    Osc,
    Dcs,
    OscEsc,
    DcsEsc,
}

/// A single CSI sequence, parsed for your convenience.
pub struct Csi {
    /// The parameters of the CSI sequence.
    pub params: [u16; 32],
    /// The number of parameters stored in [`Csi::params`].
    pub param_count: usize,
    /// The private byte, if any. `0` if none.
    ///
    /// The private byte is the first character right after the
    /// `ESC [` sequence. It is usually a `?` or `<`.
    pub private_byte: char,
    /// The final byte of the CSI sequence.
    ///
    /// This is the last character of the sequence, e.g. `m` or `H`.
    pub final_byte: char,
}

pub struct Parser {
    state: State,
    // Csi is not part of State, because it allows us
    // to more quickly erase and reuse the struct.
    csi: Csi,
}

impl Parser {
    pub fn new() -> Self {
        Self {
            state: State::Ground,
            csi: Csi { params: [0; 32], param_count: 0, private_byte: '\0', final_byte: '\0' },
        }
    }

    /// Suggests a timeout for the next call to `read()`.
    ///
    /// We need this because of the ambiguity of whether a trailing
    /// escape character in an input is starting another escape sequence or
    /// is just the result of the user literally pressing the Escape key.
    pub fn read_timeout(&mut self) -> std::time::Duration {
        match self.state {
            // 100ms is a upper ceiling for a responsive feel.
            // Realistically though, this could be much lower.
            //
            // However, there seems to be issues with OpenSSH on Windows.
            // See: https://github.com/PowerShell/Win32-OpenSSH/issues/2275
            State::Esc => time::Duration::from_millis(100),
            _ => time::Duration::MAX,
        }
    }

    /// Parses the given input into VT sequences.
    ///
    /// You should call this function even if your `read()`
    /// had a timeout (pass an empty string in that case).
    pub fn parse<'parser, 'input>(
        &'parser mut self,
        input: &'input str,
    ) -> Stream<'parser, 'input> {
        Stream { parser: self, input, off: 0 }
    }
}

/// An iterator that parses VT sequences into [`Token`]s.
///
/// Can't implement [`Iterator`], because this is a "lending iterator".
pub struct Stream<'parser, 'input> {
    parser: &'parser mut Parser,
    input: &'input str,
    off: usize,
}

impl<'input> Stream<'_, 'input> {
    /// Returns the input that is being parsed.
    pub fn input(&self) -> &'input str {
        self.input
    }

    /// Returns the current parser offset.
    pub fn offset(&self) -> usize {
        self.off
    }

    /// Reads and consumes raw bytes from the input.
    pub fn read(&mut self, dst: &mut [u8]) -> usize {
        let bytes = self.input.as_bytes();
        let off = self.off.min(bytes.len());
        let len = dst.len().min(bytes.len() - off);
        dst[..len].copy_from_slice(&bytes[off..off + len]);
        self.off += len;
        len
    }

    fn decode_next(&mut self) -> char {
        let mut iter = Utf8Chars::new(self.input.as_bytes(), self.off);
        let c = iter.next().unwrap_or('\0');
        self.off = iter.offset();
        c
    }

    /// Parses the next VT sequence from the previously given input.
    #[allow(
        clippy::should_implement_trait,
        reason = "can't implement Iterator because this is a lending iterator"
    )]
    pub fn next(&mut self) -> Option<Token<'_, 'input>> {
        let input = self.input;
        let bytes = input.as_bytes();

        // If the previous input ended with an escape character, `read_timeout()`
        // returned `Some(..)` timeout, and if the caller did everything correctly
        // and there was indeed a timeout, we should be called with an empty
        // input. In that case we'll return the escape as its own token.
        if input.is_empty() && matches!(self.parser.state, State::Esc) {
            self.parser.state = State::Ground;
            return Some(Token::Esc('\0'));
        }

        while self.off < bytes.len() {
            // TODO: The state machine can be roughly broken up into two parts:
            // * Wants to parse 1 `char` at a time: Ground, Esc, Ss3
            //   These could all be unified to a single call to `decode_next()`.
            // * Wants to bulk-process bytes: Csi, Osc, Dcs
            // We should do that so the UTF8 handling is a bit more "unified".
            match self.parser.state {
                State::Ground => match bytes[self.off] {
                    0x1b => {
                        self.parser.state = State::Esc;
                        self.off += 1;
                    }
                    c @ (0x00..0x20 | 0x7f) => {
                        self.off += 1;
                        return Some(Token::Ctrl(c as char));
                    }
                    _ => {
                        let beg = self.off;
                        while {
                            self.off += 1;
                            self.off < bytes.len()
                                && bytes[self.off] >= 0x20
                                && bytes[self.off] != 0x7f
                        } {}
                        return Some(Token::Text(&input[beg..self.off]));
                    }
                },
                State::Esc => match self.decode_next() {
                    '[' => {
                        self.parser.state = State::Csi;
                        self.parser.csi.private_byte = '\0';
                        self.parser.csi.final_byte = '\0';
                        while self.parser.csi.param_count > 0 {
                            self.parser.csi.param_count -= 1;
                            self.parser.csi.params[self.parser.csi.param_count] = 0;
                        }
                    }
                    ']' => {
                        self.parser.state = State::Osc;
                    }
                    'O' => {
                        self.parser.state = State::Ss3;
                    }
                    'P' => {
                        self.parser.state = State::Dcs;
                    }
                    c => {
                        self.parser.state = State::Ground;
                        return Some(Token::Esc(c));
                    }
                },
                State::Ss3 => {
                    self.parser.state = State::Ground;
                    return Some(Token::SS3(self.decode_next()));
                }
                State::Csi => {
                    loop {
                        // If we still have slots left, parse the parameter.
                        if self.parser.csi.param_count < self.parser.csi.params.len() {
                            let dst = &mut self.parser.csi.params[self.parser.csi.param_count];
                            while self.off < bytes.len() && bytes[self.off].is_ascii_digit() {
                                let add = bytes[self.off] as u32 - b'0' as u32;
                                let value = *dst as u32 * 10 + add;
                                *dst = value.min(u16::MAX as u32) as u16;
                                self.off += 1;
                            }
                        } else {
                            // ...otherwise, skip the parameters until we find the final byte.
                            while self.off < bytes.len() && bytes[self.off].is_ascii_digit() {
                                self.off += 1;
                            }
                        }

                        // Encountered the end of the input before finding the final byte.
                        if self.off >= bytes.len() {
                            return None;
                        }

                        let c = bytes[self.off];
                        self.off += 1;

                        match c {
                            0x40..=0x7e => {
                                self.parser.state = State::Ground;
                                self.parser.csi.final_byte = c as char;
                                if self.parser.csi.param_count != 0
                                    || self.parser.csi.params[0] != 0
                                {
                                    self.parser.csi.param_count += 1;
                                }
                                return Some(Token::Csi(&self.parser.csi));
                            }
                            b';' => self.parser.csi.param_count += 1,
                            b'<'..=b'?' => self.parser.csi.private_byte = c as char,
                            _ => {}
                        }
                    }
                }
                State::Osc | State::Dcs => {
                    let beg = self.off;
                    let mut data;
                    let mut partial;

                    loop {
                        // Find any indication for the end of the OSC/DCS sequence.
                        self.off = memchr2(b'\x07', b'\x1b', bytes, self.off);

                        data = &input[beg..self.off];
                        partial = self.off >= bytes.len();

                        // Encountered the end of the input before finding the terminator.
                        if partial {
                            break;
                        }

                        let c = bytes[self.off];
                        self.off += 1;

                        if c == 0x1b {
                            // It's only a string terminator if it's followed by \.
                            // We're at the end so we're saving the state and will continue next time.
                            if self.off >= bytes.len() {
                                self.parser.state = match self.parser.state {
                                    State::Osc => State::OscEsc,
                                    _ => State::DcsEsc,
                                };
                                partial = true;
                                break;
                            }

                            // False alarm: Not a string terminator.
                            if bytes[self.off] != b'\\' {
                                continue;
                            }

                            self.off += 1;
                        }

                        break;
                    }

                    let state = self.parser.state;
                    if !partial {
                        self.parser.state = State::Ground;
                    }
                    return match state {
                        State::Osc => Some(Token::Osc { data, partial }),
                        _ => Some(Token::Dcs { data, partial }),
                    };
                }
                State::OscEsc | State::DcsEsc => {
                    // We were processing an OSC/DCS sequence and the last byte was an escape character.
                    // It's only a string terminator if it's followed by \ (= "\x1b\\").
                    if bytes[self.off] == b'\\' {
                        // It was indeed a string terminator and we can now tell the caller about it.
                        let state = self.parser.state;

                        // Consume the terminator (one byte in the previous input and this byte).
                        self.parser.state = State::Ground;
                        self.off += 1;

                        return match state {
                            State::OscEsc => Some(Token::Osc { data: "", partial: false }),
                            _ => Some(Token::Dcs { data: "", partial: false }),
                        };
                    } else {
                        // False alarm: Not a string terminator.
                        // We'll return the escape character as a separate token.
                        // Processing will continue from the current state (`bytes[self.off]`).
                        self.parser.state = match self.parser.state {
                            State::OscEsc => State::Osc,
                            _ => State::Dcs,
                        };
                        return match self.parser.state {
                            State::Osc => Some(Token::Osc { data: "\x1b", partial: true }),
                            _ => Some(Token::Dcs { data: "\x1b", partial: true }),
                        };
                    }
                }
            }
        }

        None
    }
}

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use std::borrow::Cow;
use std::ffi::{OsStr, OsString};
use std::mem;
use std::path::{Path, PathBuf};

use edit::framebuffer::IndexedColor;
use edit::helpers::*;
use edit::tui::*;
use edit::{apperr, buffer, icu, sys};

use crate::documents::DocumentManager;
use crate::localization::*;

#[repr(transparent)]
pub struct FormatApperr(apperr::Error);

impl From<apperr::Error> for FormatApperr {
    fn from(err: apperr::Error) -> Self {
        Self(err)
    }
}

impl std::fmt::Display for FormatApperr {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self.0 {
            apperr::APP_ICU_MISSING => f.write_str(loc(LocId::ErrorIcuMissing)),
            apperr::Error::App(code) => write!(f, "Unknown app error code: {code}"),
            apperr::Error::Icu(code) => icu::apperr_format(f, code),
            apperr::Error::Sys(code) => sys::apperr_format(f, code),
        }
    }
}

pub struct DisplayablePathBuf {
    value: PathBuf,
    str: Cow<'static, str>,
}

impl DisplayablePathBuf {
    #[allow(dead_code, reason = "only used on Windows")]
    pub fn from_string(string: String) -> Self {
        let str = Cow::Borrowed(string.as_str());
        let str = unsafe { mem::transmute::<Cow<'_, str>, Cow<'_, str>>(str) };
        let value = PathBuf::from(string);
        Self { value, str }
    }

    pub fn from_path(value: PathBuf) -> Self {
        let str = value.to_string_lossy();
        let str = unsafe { mem::transmute::<Cow<'_, str>, Cow<'_, str>>(str) };
        Self { value, str }
    }

    pub fn as_path(&self) -> &Path {
        &self.value
    }

    pub fn as_str(&self) -> &str {
        &self.str
    }

    pub fn as_bytes(&self) -> &[u8] {
        self.value.as_os_str().as_encoded_bytes()
    }
}

impl Default for DisplayablePathBuf {
    fn default() -> Self {
        Self { value: Default::default(), str: Cow::Borrowed("") }
    }
}

impl Clone for DisplayablePathBuf {
    fn clone(&self) -> Self {
        Self::from_path(self.value.clone())
    }
}

impl From<OsString> for DisplayablePathBuf {
    fn from(s: OsString) -> Self {
        Self::from_path(PathBuf::from(s))
    }
}

impl<T: ?Sized + AsRef<OsStr>> From<&T> for DisplayablePathBuf {
    fn from(s: &T) -> Self {
        Self::from_path(PathBuf::from(s))
    }
}

pub struct StateSearch {
    pub kind: StateSearchKind,
    pub focus: bool,
}

#[derive(Clone, Copy, PartialEq, Eq)]
pub enum StateSearchKind {
    Hidden,
    Disabled,
    Search,
    Replace,
}

#[derive(Clone, Copy, PartialEq, Eq)]
pub enum StateFilePicker {
    None,
    Open,
    SaveAs,

    SaveAsShown, // Transitioned from SaveAs
}

#[derive(Clone, Copy, PartialEq, Eq)]
pub enum StateEncodingChange {
    None,
    Convert,
    Reopen,
}

#[derive(Default)]
pub struct OscTitleFileStatus {
    pub filename: String,
    pub dirty: bool,
}

pub struct State {
    pub menubar_color_bg: u32,
    pub menubar_color_fg: u32,

    pub documents: DocumentManager,

    // A ring buffer of the last 10 errors.
    pub error_log: [String; 10],
    pub error_log_index: usize,
    pub error_log_count: usize,

    pub wants_file_picker: StateFilePicker,
    pub file_picker_pending_dir: DisplayablePathBuf,
    pub file_picker_pending_dir_revision: u64, // Bumped every time `file_picker_pending_dir` changes.
    pub file_picker_pending_name: PathBuf,
    pub file_picker_entries: Option<[Vec<DisplayablePathBuf>; 3]>, // ["..", directories, files]
    pub file_picker_overwrite_warning: Option<PathBuf>,            // The path the warning is about.
    pub file_picker_autocomplete: Vec<DisplayablePathBuf>,

    pub wants_search: StateSearch,
    pub search_needle: String,
    pub search_replacement: String,
    pub search_options: buffer::SearchOptions,
    pub search_success: bool,

    pub wants_encoding_picker: bool,
    pub wants_encoding_change: StateEncodingChange,
    pub encoding_picker_needle: String,
    pub encoding_picker_results: Option<Vec<icu::Encoding>>,

    pub wants_save: bool,
    pub wants_statusbar_focus: bool,
    pub wants_indentation_picker: bool,
    pub wants_go_to_file: bool,
    pub wants_about: bool,
    pub wants_close: bool,
    pub wants_exit: bool,
    pub wants_goto: bool,
    pub goto_target: String,
    pub goto_invalid: bool,

    pub osc_title_file_status: OscTitleFileStatus,
    pub osc_clipboard_sync: bool,
    pub osc_clipboard_always_send: bool,
    pub exit: bool,
}

impl State {
    pub fn new() -> apperr::Result<Self> {
        Ok(Self {
            menubar_color_bg: 0,
            menubar_color_fg: 0,

            documents: Default::default(),

            error_log: [const { String::new() }; 10],
            error_log_index: 0,
            error_log_count: 0,

            wants_file_picker: StateFilePicker::None,
            file_picker_pending_dir: Default::default(),
            file_picker_pending_dir_revision: 0,
            file_picker_pending_name: Default::default(),
            file_picker_entries: None,
            file_picker_overwrite_warning: None,
            file_picker_autocomplete: Vec::new(),

            wants_search: StateSearch { kind: StateSearchKind::Hidden, focus: false },
            search_needle: Default::default(),
            search_replacement: Default::default(),
            search_options: Default::default(),
            search_success: true,

            wants_encoding_picker: false,
            encoding_picker_needle: Default::default(),
            encoding_picker_results: Default::default(),

            wants_save: false,
            wants_statusbar_focus: false,
            wants_encoding_change: StateEncodingChange::None,
            wants_indentation_picker: false,
            wants_go_to_file: false,
            wants_about: false,
            wants_close: false,
            wants_exit: false,
            wants_goto: false,
            goto_target: Default::default(),
            goto_invalid: false,

            osc_title_file_status: Default::default(),
            osc_clipboard_sync: false,
            osc_clipboard_always_send: false,
            exit: false,
        })
    }
}

pub fn draw_add_untitled_document(ctx: &mut Context, state: &mut State) {
    if let Err(err) = state.documents.add_untitled() {
        error_log_add(ctx, state, err);
    }
}

pub fn error_log_add(ctx: &mut Context, state: &mut State, err: apperr::Error) {
    let msg = format!("{}", FormatApperr::from(err));
    if !msg.is_empty() {
        state.error_log[state.error_log_index] = msg;
        state.error_log_index = (state.error_log_index + 1) % state.error_log.len();
        state.error_log_count = state.error_log.len().min(state.error_log_count + 1);
        ctx.needs_rerender();
    }
}

pub fn draw_error_log(ctx: &mut Context, state: &mut State) {
    ctx.modal_begin("error", loc(LocId::ErrorDialogTitle));
    ctx.attr_background_rgba(ctx.indexed(IndexedColor::Red));
    ctx.attr_foreground_rgba(ctx.indexed(IndexedColor::BrightWhite));
    {
        ctx.block_begin("content");
        ctx.attr_padding(Rect::three(0, 2, 1));
        {
            let off = state.error_log_index + state.error_log.len() - state.error_log_count;

            for i in 0..state.error_log_count {
                let idx = (off + i) % state.error_log.len();
                let msg = &state.error_log[idx][..];

                if !msg.is_empty() {
                    ctx.next_block_id_mixin(i as u64);
                    ctx.label("error", msg);
                    ctx.attr_overflow(Overflow::TruncateTail);
                }
            }
        }
        ctx.block_end();

        if ctx.button("ok", loc(LocId::Ok), ButtonStyle::default()) {
            state.error_log_count = 0;
        }
        ctx.attr_position(Position::Center);
        ctx.inherit_focus();
    }
    if ctx.modal_end() {
        state.error_log_count = 0;
    }
}

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use std::cmp::Ordering;
use std::fs;
use std::path::{Path, PathBuf};

use edit::arena::scratch_arena;
use edit::framebuffer::IndexedColor;
use edit::helpers::*;
use edit::input::{kbmod, vk};
use edit::tui::*;
use edit::{icu, path};

use crate::localization::*;
use crate::state::*;

pub fn draw_file_picker(ctx: &mut Context, state: &mut State) {
    // The save dialog is pre-filled with the current document filename.
    if state.wants_file_picker == StateFilePicker::SaveAs {
        state.wants_file_picker = StateFilePicker::SaveAsShown;

        if state.file_picker_pending_name.as_os_str().is_empty() {
            state.file_picker_pending_name =
                state.documents.active().map_or("Untitled.txt", |doc| doc.filename.as_str()).into();
        }
    }

    let width = (ctx.size().width - 20).max(10);
    let height = (ctx.size().height - 10).max(10);
    let mut doit = None;
    let mut done = false;

    ctx.modal_begin(
        "file-picker",
        if state.wants_file_picker == StateFilePicker::Open {
            loc(LocId::FileOpen)
        } else {
            loc(LocId::FileSaveAs)
        },
    );
    ctx.attr_intrinsic_size(Size { width, height });
    {
        let contains_focus = ctx.contains_focus();
        let mut activated = false;

        ctx.table_begin("path");
        ctx.table_set_columns(&[0, COORD_TYPE_SAFE_MAX]);
        ctx.table_set_cell_gap(Size { width: 1, height: 0 });
        ctx.attr_padding(Rect::two(1, 1));
        ctx.inherit_focus();
        {
            ctx.table_next_row();

            ctx.label("dir-label", loc(LocId::SaveAsDialogPathLabel));
            ctx.label("dir", state.file_picker_pending_dir.as_str());
            ctx.attr_overflow(Overflow::TruncateMiddle);

            ctx.table_next_row();
            ctx.inherit_focus();

            ctx.label("name-label", loc(LocId::SaveAsDialogNameLabel));

            let name_changed = ctx.editline("name", &mut state.file_picker_pending_name);
            ctx.inherit_focus();

            if ctx.contains_focus() {
                if name_changed && ctx.is_focused() {
                    update_autocomplete_suggestions(state);
                }
            } else if !state.file_picker_autocomplete.is_empty() {
                state.file_picker_autocomplete.clear();
            }

            if !state.file_picker_autocomplete.is_empty() {
                let bg = ctx.indexed_alpha(IndexedColor::Background, 3, 4);
                let fg = ctx.contrasted(bg);
                let focus_list_beg = ctx.is_focused() && ctx.consume_shortcut(vk::DOWN);
                let focus_list_end = ctx.is_focused() && ctx.consume_shortcut(vk::UP);
                let mut autocomplete_done = ctx.consume_shortcut(vk::ESCAPE);

                ctx.list_begin("suggestions");
                ctx.attr_float(FloatSpec {
                    anchor: Anchor::Last,
                    gravity_x: 0.0,
                    gravity_y: 0.0,
                    offset_x: 0.0,
                    offset_y: 1.0,
                });
                ctx.attr_border();
                ctx.attr_background_rgba(bg);
                ctx.attr_foreground_rgba(fg);
                {
                    for (idx, suggestion) in state.file_picker_autocomplete.iter().enumerate() {
                        let sel = ctx.list_item(false, suggestion.as_str());
                        if sel != ListSelection::Unchanged {
                            state.file_picker_pending_name = suggestion.as_path().into();
                        }
                        if sel == ListSelection::Activated {
                            autocomplete_done = true;
                        }

                        let is_first = idx == 0;
                        let is_last = idx == state.file_picker_autocomplete.len() - 1;
                        if (is_first && focus_list_beg) || (is_last && focus_list_end) {
                            ctx.list_item_steal_focus();
                        } else if ctx.is_focused()
                            && ((is_first && ctx.consume_shortcut(vk::UP))
                                || (is_last && ctx.consume_shortcut(vk::DOWN)))
                        {
                            ctx.toss_focus_up();
                        }
                    }
                }
                ctx.list_end();

                // If the user typed something, we want to put focus back into the editline.
                // TODO: The input should be processed by the editline and not simply get swallowed.
                if ctx.keyboard_input().is_some() {
                    ctx.set_input_consumed();
                    autocomplete_done = true;
                }

                if autocomplete_done {
                    state.file_picker_autocomplete.clear();
                }
            }

            if ctx.is_focused() && ctx.consume_shortcut(vk::RETURN) {
                activated = true;
            }
        }
        ctx.table_end();

        if state.file_picker_entries.is_none() {
            draw_dialog_saveas_refresh_files(state);
        }

        ctx.scrollarea_begin(
            "directory",
            Size {
                width: 0,
                // -1 for the label (top)
                // -1 for the label (bottom)
                // -1 for the editline (bottom)
                height: height - 3,
            },
        );
        ctx.attr_background_rgba(ctx.indexed_alpha(IndexedColor::Black, 1, 4));
        {
            ctx.next_block_id_mixin(state.file_picker_pending_dir_revision);
            ctx.list_begin("files");
            ctx.inherit_focus();

            for entries in state.file_picker_entries.as_ref().unwrap() {
                for entry in entries {
                    match ctx.list_item(false, entry.as_str()) {
                        ListSelection::Unchanged => {}
                        ListSelection::Selected => {
                            state.file_picker_pending_name = entry.as_path().into()
                        }
                        ListSelection::Activated => activated = true,
                    }
                    ctx.attr_overflow(Overflow::TruncateMiddle);
                }
            }

            ctx.list_end();
        }
        ctx.scrollarea_end();

        if contains_focus
            && (ctx.consume_shortcut(vk::BACK) || ctx.consume_shortcut(kbmod::ALT | vk::UP))
        {
            state.file_picker_pending_name = "..".into();
            activated = true;
        }

        if activated {
            doit = draw_file_picker_update_path(state);

            // Check if the file already exists and show an overwrite warning in that case.
            if state.wants_file_picker != StateFilePicker::Open
                && let Some(path) = doit.as_deref()
                && path.exists()
            {
                state.file_picker_overwrite_warning = doit.take();
            }
        }
    }
    if ctx.modal_end() {
        done = true;
    }

    if state.file_picker_overwrite_warning.is_some() {
        let mut save;

        ctx.modal_begin("overwrite", loc(LocId::FileOverwriteWarning));
        ctx.attr_background_rgba(ctx.indexed(IndexedColor::Red));
        ctx.attr_foreground_rgba(ctx.indexed(IndexedColor::BrightWhite));
        {
            let contains_focus = ctx.contains_focus();

            ctx.label("description", loc(LocId::FileOverwriteWarningDescription));
            ctx.attr_overflow(Overflow::TruncateTail);
            ctx.attr_padding(Rect::three(1, 2, 1));

            ctx.table_begin("choices");
            ctx.inherit_focus();
            ctx.attr_padding(Rect::three(0, 2, 1));
            ctx.attr_position(Position::Center);
            ctx.table_set_cell_gap(Size { width: 2, height: 0 });
            {
                ctx.table_next_row();
                ctx.inherit_focus();

                save = ctx.button("yes", loc(LocId::Yes), ButtonStyle::default());
                ctx.inherit_focus();

                if ctx.button("no", loc(LocId::No), ButtonStyle::default()) {
                    state.file_picker_overwrite_warning = None;
                }
            }
            ctx.table_end();

            if contains_focus {
                save |= ctx.consume_shortcut(vk::Y);
                if ctx.consume_shortcut(vk::N) {
                    state.file_picker_overwrite_warning = None;
                }
            }
        }
        if ctx.modal_end() {
            state.file_picker_overwrite_warning = None;
        }

        if save {
            doit = state.file_picker_overwrite_warning.take();
        }
    }

    if let Some(path) = doit {
        let res = if state.wants_file_picker == StateFilePicker::Open {
            state.documents.add_file_path(&path).map(|_| ())
        } else if let Some(doc) = state.documents.active_mut() {
            doc.save(Some(path))
        } else {
            Ok(())
        };
        match res {
            Ok(..) => {
                ctx.needs_rerender();
                done = true;
            }
            Err(err) => error_log_add(ctx, state, err),
        }
    }

    if done {
        state.wants_file_picker = StateFilePicker::None;
        state.file_picker_pending_name = Default::default();
        state.file_picker_entries = Default::default();
        state.file_picker_overwrite_warning = Default::default();
        state.file_picker_autocomplete = Default::default();
    }
}

// Returns Some(path) if the path refers to a file.
fn draw_file_picker_update_path(state: &mut State) -> Option<PathBuf> {
    let old_path = state.file_picker_pending_dir.as_path();
    let path = old_path.join(&state.file_picker_pending_name);
    let path = path::normalize(&path);

    let (dir, name) = if path.is_dir() {
        // If the current path is C:\ and the user selects "..", we want to
        // navigate to the drive picker. Since `path::normalize` will turn C:\.. into C:\,
        // we can detect this by checking if the length of the path didn't change.
        let dir = if cfg!(windows)
            && state.file_picker_pending_name == Path::new("..")
            // It's unnecessary to check the contents of the paths.
            && old_path.as_os_str().len() == path.as_os_str().len()
        {
            Path::new("")
        } else {
            path.as_path()
        };
        (dir, PathBuf::new())
    } else {
        let dir = path.parent().unwrap_or(&path);
        let name = path.file_name().map_or(Default::default(), |s| s.into());
        (dir, name)
    };
    if dir != state.file_picker_pending_dir.as_path() {
        state.file_picker_pending_dir = DisplayablePathBuf::from_path(dir.to_path_buf());
        state.file_picker_pending_dir_revision =
            state.file_picker_pending_dir_revision.wrapping_add(1);
        state.file_picker_entries = None;
    }

    state.file_picker_pending_name = name;
    if state.file_picker_pending_name.as_os_str().is_empty() { None } else { Some(path) }
}

fn draw_dialog_saveas_refresh_files(state: &mut State) {
    let dir = state.file_picker_pending_dir.as_path();
    // ["..", directories, files]
    let mut dirs_files = [Vec::new(), Vec::new(), Vec::new()];

    #[cfg(windows)]
    if dir.as_os_str().is_empty() {
        // If the path is empty, we are at the drive picker.
        // Add all drives as entries.
        for drive in edit::sys::drives() {
            dirs_files[1].push(DisplayablePathBuf::from_string(format!("{drive}:\\")));
        }

        state.file_picker_entries = Some(dirs_files);
        return;
    }

    if cfg!(windows) || dir.parent().is_some() {
        dirs_files[0].push(DisplayablePathBuf::from(".."));
    }

    if let Ok(iter) = fs::read_dir(dir) {
        for entry in iter.flatten() {
            if let Ok(metadata) = entry.metadata() {
                let mut name = entry.file_name();
                let dir = metadata.is_dir()
                    || (metadata.is_symlink()
                        && fs::metadata(entry.path()).is_ok_and(|m| m.is_dir()));
                let idx = if dir { 1 } else { 2 };

                if dir {
                    name.push("/");
                }

                dirs_files[idx].push(DisplayablePathBuf::from(name));
            }
        }
    }

    for entries in &mut dirs_files[1..] {
        entries.sort_by(|a, b| {
            let a = a.as_bytes();
            let b = b.as_bytes();

            let a_is_dir = a.last() == Some(&b'/');
            let b_is_dir = b.last() == Some(&b'/');

            match b_is_dir.cmp(&a_is_dir) {
                Ordering::Equal => icu::compare_strings(a, b),
                other => other,
            }
        });
    }

    state.file_picker_entries = Some(dirs_files);
}

#[inline(never)]
fn update_autocomplete_suggestions(state: &mut State) {
    state.file_picker_autocomplete.clear();

    if state.file_picker_pending_name.as_os_str().is_empty() {
        return;
    }

    let scratch = scratch_arena(None);
    let needle = state.file_picker_pending_name.as_os_str().as_encoded_bytes();
    let mut matches = Vec::new();

    // Using binary search below we'll quickly find the lower bound
    // of items that match the needle (= share a common prefix).
    //
    // The problem is finding the upper bound. Here I'm using a trick:
    // By appending U+10FFFF (the highest possible Unicode code point)
    // we create a needle that naturally yields an upper bound.
    let mut needle_upper_bound = Vec::with_capacity_in(needle.len() + 4, &*scratch);
    needle_upper_bound.extend_from_slice(needle);
    needle_upper_bound.extend_from_slice(b"\xf4\x8f\xbf\xbf");

    if let Some(dirs_files) = &state.file_picker_entries {
        'outer: for entries in &dirs_files[1..] {
            let lower = entries
                .binary_search_by(|entry| icu::compare_strings(entry.as_bytes(), needle))
                .unwrap_or_else(|i| i);

            for entry in &entries[lower..] {
                let haystack = entry.as_bytes();
                match icu::compare_strings(haystack, &needle_upper_bound) {
                    Ordering::Less => {
                        matches.push(entry.clone());
                        if matches.len() >= 5 {
                            break 'outer; // Limit to 5 suggestions
                        }
                    }
                    // We're looking for suggestions, not for matches.
                    Ordering::Equal => {}
                    // No more matches possible.
                    Ordering::Greater => break,
                }
            }
        }
    }

    state.file_picker_autocomplete = matches;
}

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use std::collections::LinkedList;
use std::ffi::OsStr;
use std::fs::File;
use std::path::{Path, PathBuf};

use edit::buffer::{RcTextBuffer, TextBuffer};
use edit::helpers::{CoordType, Point};
use edit::{apperr, path, sys};

use crate::state::DisplayablePathBuf;

pub struct Document {
    pub buffer: RcTextBuffer,
    pub path: Option<PathBuf>,
    pub dir: Option<DisplayablePathBuf>,
    pub filename: String,
    pub file_id: Option<sys::FileId>,
    pub new_file_counter: usize,
}

impl Document {
    pub fn save(&mut self, new_path: Option<PathBuf>) -> apperr::Result<()> {
        let path = new_path.as_deref().unwrap_or_else(|| self.path.as_ref().unwrap().as_path());
        let mut file = DocumentManager::open_for_writing(path)?;

        {
            let mut tb = self.buffer.borrow_mut();
            tb.write_file(&mut file)?;
        }

        if let Ok(id) = sys::file_id(None, path) {
            self.file_id = Some(id);
        }

        if let Some(path) = new_path {
            self.set_path(path);
        }

        Ok(())
    }

    pub fn reread(&mut self, encoding: Option<&'static str>) -> apperr::Result<()> {
        let path = self.path.as_ref().unwrap().as_path();
        let mut file = DocumentManager::open_for_reading(path)?;

        {
            let mut tb = self.buffer.borrow_mut();
            tb.read_file(&mut file, encoding)?;
        }

        if let Ok(id) = sys::file_id(None, path) {
            self.file_id = Some(id);
        }

        Ok(())
    }

    fn set_path(&mut self, path: PathBuf) {
        let filename = path.file_name().unwrap_or_default().to_string_lossy().into_owned();
        let dir = path.parent().map(ToOwned::to_owned).unwrap_or_default();
        self.filename = filename;
        self.dir = Some(DisplayablePathBuf::from_path(dir));
        self.path = Some(path);
        self.update_file_mode();
    }

    fn update_file_mode(&mut self) {
        let mut tb = self.buffer.borrow_mut();
        tb.set_ruler(if self.filename == "COMMIT_EDITMSG" { 72 } else { 0 });
    }
}

#[derive(Default)]
pub struct DocumentManager {
    list: LinkedList<Document>,
}

impl DocumentManager {
    #[inline]
    pub fn len(&self) -> usize {
        self.list.len()
    }

    #[inline]
    pub fn active(&self) -> Option<&Document> {
        self.list.front()
    }

    #[inline]
    pub fn active_mut(&mut self) -> Option<&mut Document> {
        self.list.front_mut()
    }

    #[inline]
    pub fn update_active<F: FnMut(&Document) -> bool>(&mut self, mut func: F) -> bool {
        let mut cursor = self.list.cursor_front_mut();
        while let Some(doc) = cursor.current() {
            if func(doc) {
                let list = cursor.remove_current_as_list().unwrap();
                self.list.cursor_front_mut().splice_before(list);
                return true;
            }
            cursor.move_next();
        }
        false
    }

    pub fn remove_active(&mut self) {
        self.list.pop_front();
    }

    pub fn add_untitled(&mut self) -> apperr::Result<&mut Document> {
        let buffer = Self::create_buffer()?;
        let mut doc = Document {
            buffer,
            path: None,
            dir: Default::default(),
            filename: Default::default(),
            file_id: None,
            new_file_counter: 0,
        };
        self.gen_untitled_name(&mut doc);

        self.list.push_front(doc);
        Ok(self.list.front_mut().unwrap())
    }

    pub fn gen_untitled_name(&self, doc: &mut Document) {
        let mut new_file_counter = 0;
        for doc in &self.list {
            new_file_counter = new_file_counter.max(doc.new_file_counter);
        }
        new_file_counter += 1;

        doc.filename = format!("Untitled-{new_file_counter}.txt");
        doc.new_file_counter = new_file_counter;
    }

    pub fn add_file_path(&mut self, path: &Path) -> apperr::Result<&mut Document> {
        let (path, goto) = Self::parse_filename_goto(path);
        let path = path::normalize(path);

        let mut file = match Self::open_for_reading(&path) {
            Ok(file) => Some(file),
            Err(err) if sys::apperr_is_not_found(err) => None,
            Err(err) => return Err(err),
        };

        let file_id = if file.is_some() { Some(sys::file_id(file.as_ref(), &path)?) } else { None };

        // Check if the file is already open.
        if file_id.is_some() && self.update_active(|doc| doc.file_id == file_id) {
            let doc = self.active_mut().unwrap();
            if let Some(goto) = goto {
                doc.buffer.borrow_mut().cursor_move_to_logical(goto);
            }
            return Ok(doc);
        }

        let buffer = Self::create_buffer()?;
        {
            if let Some(file) = &mut file {
                let mut tb = buffer.borrow_mut();
                tb.read_file(file, None)?;

                if let Some(goto) = goto
                    && goto != Default::default()
                {
                    tb.cursor_move_to_logical(goto);
                }
            }
        }

        let mut doc = Document {
            buffer,
            path: None,
            dir: None,
            filename: Default::default(),
            file_id,
            new_file_counter: 0,
        };
        doc.set_path(path);

        if let Some(active) = self.active()
            && active.path.is_none()
            && active.file_id.is_none()
            && !active.buffer.borrow().is_dirty()
        {
            // If the current document is a pristine Untitled document with no
            // name and no ID, replace it with the new document.
            self.remove_active();
        }

        self.list.push_front(doc);
        Ok(self.list.front_mut().unwrap())
    }

    pub fn reflow_all(&self) {
        for doc in &self.list {
            let mut tb = doc.buffer.borrow_mut();
            tb.reflow();
        }
    }

    pub fn open_for_reading(path: &Path) -> apperr::Result<File> {
        File::open(path).map_err(apperr::Error::from)
    }

    pub fn open_for_writing(path: &Path) -> apperr::Result<File> {
        File::create(path).map_err(apperr::Error::from)
    }

    fn create_buffer() -> apperr::Result<RcTextBuffer> {
        let buffer = TextBuffer::new_rc(false)?;
        {
            let mut tb = buffer.borrow_mut();
            tb.set_insert_final_newline(!cfg!(windows)); // As mandated by POSIX.
            tb.set_margin_enabled(true);
            tb.set_line_highlight_enabled(true);
        }
        Ok(buffer)
    }

    // Parse a filename in the form of "filename:line:char".
    // Returns the position of the first colon and the line/char coordinates.
    fn parse_filename_goto(path: &Path) -> (&Path, Option<Point>) {
        fn parse(s: &[u8]) -> Option<CoordType> {
            if s.is_empty() {
                return None;
            }

            let mut num: CoordType = 0;
            for &b in s {
                if !b.is_ascii_digit() {
                    return None;
                }
                let digit = (b - b'0') as CoordType;
                num = num.checked_mul(10)?.checked_add(digit)?;
            }
            Some(num)
        }

        fn find_colon_rev(bytes: &[u8], offset: usize) -> Option<usize> {
            (0..offset.min(bytes.len())).rev().find(|&i| bytes[i] == b':')
        }

        let bytes = path.as_os_str().as_encoded_bytes();
        let colend = match find_colon_rev(bytes, bytes.len()) {
            // Reject filenames that would result in an empty filename after stripping off the :line:char suffix.
            // For instance, a filename like ":123:456" will not be processed by this function.
            Some(colend) if colend > 0 => colend,
            _ => return (path, None),
        };

        let last = match parse(&bytes[colend + 1..]) {
            Some(last) => last,
            None => return (path, None),
        };
        let last = (last - 1).max(0);
        let mut len = colend;
        let mut goto = Point { x: 0, y: last };

        if let Some(colbeg) = find_colon_rev(bytes, colend) {
            // Same here: Don't allow empty filenames.
            if colbeg != 0
                && let Some(first) = parse(&bytes[colbeg + 1..colend])
            {
                let first = (first - 1).max(0);
                len = colbeg;
                goto = Point { x: last, y: first };
            }
        }

        // Strip off the :line:char suffix.
        let path = &bytes[..len];
        let path = unsafe { OsStr::from_encoded_bytes_unchecked(path) };
        let path = Path::new(path);
        (path, Some(goto))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse_last_numbers() {
        fn parse(s: &str) -> (&str, Option<Point>) {
            let (p, g) = DocumentManager::parse_filename_goto(Path::new(s));
            (p.to_str().unwrap(), g)
        }

        assert_eq!(parse("123"), ("123", None));
        assert_eq!(parse("abc"), ("abc", None));
        assert_eq!(parse(":123"), (":123", None));
        assert_eq!(parse("abc:123"), ("abc", Some(Point { x: 0, y: 122 })));
        assert_eq!(parse("45:123"), ("45", Some(Point { x: 0, y: 122 })));
        assert_eq!(parse(":45:123"), (":45", Some(Point { x: 0, y: 122 })));
        assert_eq!(parse("abc:45:123"), ("abc", Some(Point { x: 122, y: 44 })));
        assert_eq!(parse("abc:def:123"), ("abc:def", Some(Point { x: 0, y: 122 })));
        assert_eq!(parse("1:2:3"), ("1", Some(Point { x: 2, y: 1 })));
        assert_eq!(parse("::3"), (":", Some(Point { x: 0, y: 2 })));
        assert_eq!(parse("1::3"), ("1:", Some(Point { x: 0, y: 2 })));
        assert_eq!(parse(""), ("", None));
        assert_eq!(parse(":"), (":", None));
        assert_eq!(parse("::"), ("::", None));
        assert_eq!(parse("a:1"), ("a", Some(Point { x: 0, y: 0 })));
        assert_eq!(parse("1:a"), ("1:a", None));
        assert_eq!(parse("file.txt:10"), ("file.txt", Some(Point { x: 0, y: 9 })));
        assert_eq!(parse("file.txt:10:5"), ("file.txt", Some(Point { x: 4, y: 9 })));
    }
}

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

//! Fuzzy search algorithm based on the one used in VS Code (`/src/vs/base/common/fuzzyScorer.ts`).
//! Other algorithms exist, such as Sublime Text's, or the one used in `fzf`,
//! but I figured that this one is what lots of people may be familiar with.

use std::vec;

use crate::arena::{Arena, scratch_arena};
use crate::icu;

const NO_MATCH: i32 = 0;

pub fn score_fuzzy<'a>(
    arena: &'a Arena,
    haystack: &str,
    needle: &str,
    allow_non_contiguous_matches: bool,
) -> (i32, Vec<usize, &'a Arena>) {
    if haystack.is_empty() || needle.is_empty() {
        // return early if target or query are empty
        return (NO_MATCH, Vec::new_in(arena));
    }

    let scratch = scratch_arena(Some(arena));
    let target = map_chars(&scratch, haystack);
    let query = map_chars(&scratch, needle);

    if target.len() < query.len() {
        // impossible for query to be contained in target
        return (NO_MATCH, Vec::new_in(arena));
    }

    let target_lower = icu::fold_case(&scratch, haystack);
    let query_lower = icu::fold_case(&scratch, needle);
    let target_lower = map_chars(&scratch, &target_lower);
    let query_lower = map_chars(&scratch, &query_lower);

    let area = query.len() * target.len();
    let mut scores = vec::from_elem_in(0, area, &*scratch);
    let mut matches = vec::from_elem_in(0, area, &*scratch);

    //
    // Build Scorer Matrix:
    //
    // The matrix is composed of query q and target t. For each index we score
    // q[i] with t[i] and compare that with the previous score. If the score is
    // equal or larger, we keep the match. In addition to the score, we also keep
    // the length of the consecutive matches to use as boost for the score.
    //
    //      t   a   r   g   e   t
    //  q
    //  u
    //  e
    //  r
    //  y
    //
    for query_index in 0..query.len() {
        let query_index_offset = query_index * target.len();
        let query_index_previous_offset =
            if query_index > 0 { (query_index - 1) * target.len() } else { 0 };

        for target_index in 0..target.len() {
            let current_index = query_index_offset + target_index;
            let diag_index = if query_index > 0 && target_index > 0 {
                query_index_previous_offset + target_index - 1
            } else {
                0
            };
            let left_score = if target_index > 0 { scores[current_index - 1] } else { 0 };
            let diag_score =
                if query_index > 0 && target_index > 0 { scores[diag_index] } else { 0 };
            let matches_sequence_len =
                if query_index > 0 && target_index > 0 { matches[diag_index] } else { 0 };

            // If we are not matching on the first query character anymore, we only produce a
            // score if we had a score previously for the last query index (by looking at the diagScore).
            // This makes sure that the query always matches in sequence on the target. For example
            // given a target of "ede" and a query of "de", we would otherwise produce a wrong high score
            // for query[1] ("e") matching on target[0] ("e") because of the "beginning of word" boost.
            let score = if diag_score == 0 && query_index != 0 {
                0
            } else {
                compute_char_score(
                    query[query_index],
                    query_lower[query_index],
                    if target_index != 0 { Some(target[target_index - 1]) } else { None },
                    target[target_index],
                    target_lower[target_index],
                    matches_sequence_len,
                )
            };

            // We have a score and its equal or larger than the left score
            // Match: sequence continues growing from previous diag value
            // Score: increases by diag score value
            let is_valid_score = score != 0 && diag_score + score >= left_score;
            if is_valid_score
                && (
                    // We don't need to check if it's contiguous if we allow non-contiguous matches
                    allow_non_contiguous_matches ||
                        // We must be looking for a contiguous match.
                        // Looking at an index above 0 in the query means we must have already
                        // found out this is contiguous otherwise there wouldn't have been a score
                        query_index > 0 ||
                        // lastly check if the query is completely contiguous at this index in the target
                        target_lower[target_index..].starts_with(&query_lower)
                )
            {
                matches[current_index] = matches_sequence_len + 1;
                scores[current_index] = diag_score + score;
            } else {
                // We either have no score or the score is lower than the left score
                // Match: reset to 0
                // Score: pick up from left hand side
                matches[current_index] = NO_MATCH;
                scores[current_index] = left_score;
            }
        }
    }

    // Restore Positions (starting from bottom right of matrix)
    let mut positions = Vec::new_in(arena);

    if !query.is_empty() && !target.is_empty() {
        let mut query_index = query.len() - 1;
        let mut target_index = target.len() - 1;

        loop {
            let current_index = query_index * target.len() + target_index;
            if matches[current_index] == NO_MATCH {
                if target_index == 0 {
                    break;
                }
                target_index -= 1; // go left
            } else {
                positions.push(target_index);

                // go up and left
                if query_index == 0 || target_index == 0 {
                    break;
                }
                query_index -= 1;
                target_index -= 1;
            }
        }

        positions.reverse();
    }

    (scores[area - 1], positions)
}

fn compute_char_score(
    query: char,
    query_lower: char,
    target_prev: Option<char>,
    target_curr: char,
    target_curr_lower: char,
    matches_sequence_len: i32,
) -> i32 {
    let mut score = 0;

    if !consider_as_equal(query_lower, target_curr_lower) {
        return score; // no match of characters
    }

    // Character match bonus
    score += 1;

    // Consecutive match bonus
    if matches_sequence_len > 0 {
        score += matches_sequence_len * 5;
    }

    // Same case bonus
    if query == target_curr {
        score += 1;
    }

    if let Some(target_prev) = target_prev {
        // After separator bonus
        let separator_bonus = score_separator_at_pos(target_prev);
        if separator_bonus > 0 {
            score += separator_bonus;
        }
        // Inside word upper case bonus (camel case). We only give this bonus if we're not in a contiguous sequence.
        // For example:
        // NPE => NullPointerException = boost
        // HTTP => HTTP = not boost
        else if target_curr != target_curr_lower && matches_sequence_len == 0 {
            score += 2;
        }
    } else {
        // Start of word bonus
        score += 8;
    }

    score
}

fn consider_as_equal(a: char, b: char) -> bool {
    // Special case path separators: ignore platform differences
    a == b || (a == '/' && b == '\\') || (a == '\\' && b == '/')
}

fn score_separator_at_pos(ch: char) -> i32 {
    match ch {
        '/' | '\\' => 5,                               // prefer path separators...
        '_' | '-' | '.' | ' ' | '\'' | '"' | ':' => 4, // ...over other separators
        _ => 0,
    }
}

fn map_chars<'a>(arena: &'a Arena, s: &str) -> Vec<char, &'a Arena> {
    let mut chars = Vec::with_capacity_in(s.len(), arena);
    chars.extend(s.chars());
    chars.shrink_to_fit();
    chars
}

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

#![allow(clippy::mut_from_ref)]

use std::alloc::{AllocError, Allocator, Layout};
use std::cell::Cell;
use std::hint::cold_path;
use std::mem::MaybeUninit;
use std::ptr::{self, NonNull};
use std::{mem, slice};

use crate::helpers::*;
use crate::{apperr, sys};

const ALLOC_CHUNK_SIZE: usize = 64 * KIBI;

/// An arena allocator.
///
/// If you have never used an arena allocator before, think of it as
/// allocating objects on the stack, but the stack is *really* big.
/// Each time you allocate, memory gets pushed at the end of the stack,
/// each time you deallocate, memory gets popped from the end of the stack.
///
/// One reason you'd want to use this is obviously performance: It's very simple
/// and so it's also very fast, >10x faster than your system allocator.
///
/// However, modern allocators such as `mimalloc` are just as fast, so why not use them?
/// Because their performance comes at the cost of binary size and we can't have that.
///
/// The biggest benefit though is that it sometimes massively simplifies lifetime
/// and memory management. This can best be seen by this project's UI code, which
/// uses an arena to allocate a tree of UI nodes. This is infamously difficult
/// to do in Rust, but not so when you got an arena allocator:
/// All nodes have the same lifetime, so you can just use references.
///
/// <div class="warning">
///
/// **Do not** push objects into the arena that require destructors.
/// Destructors are not executed. Use a pool allocator for that.
///
/// </div>
pub struct Arena {
    base: NonNull<u8>,
    capacity: usize,
    commit: Cell<usize>,
    offset: Cell<usize>,

    /// See [`super::debug`], which uses this for borrow tracking.
    #[cfg(debug_assertions)]
    pub(super) borrows: Cell<usize>,
}

impl Arena {
    pub const fn empty() -> Self {
        Self {
            base: NonNull::dangling(),
            capacity: 0,
            commit: Cell::new(0),
            offset: Cell::new(0),

            #[cfg(debug_assertions)]
            borrows: Cell::new(0),
        }
    }

    pub fn new(capacity: usize) -> apperr::Result<Self> {
        let capacity = (capacity.max(1) + ALLOC_CHUNK_SIZE - 1) & !(ALLOC_CHUNK_SIZE - 1);
        let base = unsafe { sys::virtual_reserve(capacity)? };

        Ok(Self {
            base,
            capacity,
            commit: Cell::new(0),
            offset: Cell::new(0),

            #[cfg(debug_assertions)]
            borrows: Cell::new(0),
        })
    }

    pub fn is_empty(&self) -> bool {
        self.base == NonNull::dangling()
    }

    pub fn offset(&self) -> usize {
        self.offset.get()
    }

    /// "Deallocates" the memory in the arena down to the given offset.
    ///
    /// # Safety
    ///
    /// Obviously, this is GIGA UNSAFE. It runs no destructors and does not check
    /// whether the offset is valid. You better take care when using this function.
    pub unsafe fn reset(&self, to: usize) {
        // Fill the deallocated memory with 0xDD to aid debugging.
        if cfg!(debug_assertions) && self.offset.get() > to {
            let commit = self.commit.get();
            let len = (self.offset.get() + 128).min(commit) - to;
            unsafe { slice::from_raw_parts_mut(self.base.add(to).as_ptr(), len).fill(0xDD) };
        }

        self.offset.replace(to);
    }

    #[inline]
    pub(super) fn alloc_raw(
        &self,
        bytes: usize,
        alignment: usize,
    ) -> Result<NonNull<[u8]>, AllocError> {
        let commit = self.commit.get();
        let offset = self.offset.get();

        let beg = (offset + alignment - 1) & !(alignment - 1);
        let end = beg + bytes;

        if end > commit {
            return self.alloc_raw_bump(beg, end);
        }

        if cfg!(debug_assertions) {
            let ptr = unsafe { self.base.add(offset) };
            let len = (end + 128).min(self.commit.get()) - offset;
            unsafe { slice::from_raw_parts_mut(ptr.as_ptr(), len).fill(0xCD) };
        }

        self.offset.replace(end);
        Ok(unsafe { NonNull::slice_from_raw_parts(self.base.add(beg), bytes) })
    }

    // With the code in `alloc_raw_bump()` out of the way, `alloc_raw()` compiles down to some super tight assembly.
    #[cold]
    fn alloc_raw_bump(&self, beg: usize, end: usize) -> Result<NonNull<[u8]>, AllocError> {
        let offset = self.offset.get();
        let commit_old = self.commit.get();
        let commit_new = (end + ALLOC_CHUNK_SIZE - 1) & !(ALLOC_CHUNK_SIZE - 1);

        if commit_new > self.capacity
            || unsafe {
                sys::virtual_commit(self.base.add(commit_old), commit_new - commit_old).is_err()
            }
        {
            return Err(AllocError);
        }

        if cfg!(debug_assertions) {
            let ptr = unsafe { self.base.add(offset) };
            let len = (end + 128).min(self.commit.get()) - offset;
            unsafe { slice::from_raw_parts_mut(ptr.as_ptr(), len).fill(0xCD) };
        }

        self.commit.replace(commit_new);
        self.offset.replace(end);
        Ok(unsafe { NonNull::slice_from_raw_parts(self.base.add(beg), end - beg) })
    }

    #[allow(clippy::mut_from_ref)]
    pub fn alloc_uninit<T>(&self) -> &mut MaybeUninit<T> {
        let bytes = mem::size_of::<T>();
        let alignment = mem::align_of::<T>();
        let ptr = self.alloc_raw(bytes, alignment).unwrap();
        unsafe { ptr.cast().as_mut() }
    }

    #[allow(clippy::mut_from_ref)]
    pub fn alloc_uninit_slice<T>(&self, count: usize) -> &mut [MaybeUninit<T>] {
        let bytes = mem::size_of::<T>() * count;
        let alignment = mem::align_of::<T>();
        let ptr = self.alloc_raw(bytes, alignment).unwrap();
        unsafe { slice::from_raw_parts_mut(ptr.cast().as_ptr(), count) }
    }
}

impl Drop for Arena {
    fn drop(&mut self) {
        if !self.is_empty() {
            unsafe { sys::virtual_release(self.base, self.capacity) };
        }
    }
}

impl Default for Arena {
    fn default() -> Self {
        Self::empty()
    }
}

unsafe impl Allocator for Arena {
    fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
        self.alloc_raw(layout.size(), layout.align())
    }

    fn allocate_zeroed(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
        let p = self.alloc_raw(layout.size(), layout.align())?;
        unsafe { p.cast::<u8>().as_ptr().write_bytes(0, p.len()) }
        Ok(p)
    }

    // While it is possible to shrink the tail end of the arena, it is
    // not very useful given the existence of scoped scratch arenas.
    unsafe fn deallocate(&self, _: NonNull<u8>, _: Layout) {}

    unsafe fn grow(
        &self,
        ptr: NonNull<u8>,
        old_layout: Layout,
        new_layout: Layout,
    ) -> Result<NonNull<[u8]>, AllocError> {
        debug_assert!(new_layout.size() >= old_layout.size());
        debug_assert!(new_layout.align() <= old_layout.align());

        let new_ptr;

        // Growing the given area is possible if it is at the end of the arena.
        if unsafe { ptr.add(old_layout.size()) == self.base.add(self.offset.get()) } {
            new_ptr = ptr;
            let delta = new_layout.size() - old_layout.size();
            // Assuming that the given ptr/length area is at the end of the arena,
            // we can just push more memory to the end of the arena to grow it.
            self.alloc_raw(delta, 1)?;
        } else {
            cold_path();

            new_ptr = self.allocate(new_layout)?.cast();

            // SAFETY: It's weird to me that this doesn't assert new_layout.size() >= old_layout.size(),
            // but neither does the stdlib code at the time of writing.
            // So, assuming that is not needed, this code is safe since it just copies the old data over.
            unsafe {
                ptr::copy_nonoverlapping(ptr.as_ptr(), new_ptr.as_ptr(), old_layout.size());
                self.deallocate(ptr, old_layout);
            }
        }

        Ok(NonNull::slice_from_raw_parts(new_ptr, new_layout.size()))
    }

    unsafe fn grow_zeroed(
        &self,
        ptr: NonNull<u8>,
        old_layout: Layout,
        new_layout: Layout,
    ) -> Result<NonNull<[u8]>, AllocError> {
        unsafe {
            // SAFETY: Same as grow().
            let ptr = self.grow(ptr, old_layout, new_layout)?;

            // SAFETY: At this point, `ptr` must be valid for `new_layout.size()` bytes,
            // allowing us to safely zero out the delta since `old_layout.size()`.
            ptr.cast::<u8>()
                .add(old_layout.size())
                .write_bytes(0, new_layout.size() - old_layout.size());

            Ok(ptr)
        }
    }

    unsafe fn shrink(
        &self,
        ptr: NonNull<u8>,
        old_layout: Layout,
        new_layout: Layout,
    ) -> Result<NonNull<[u8]>, AllocError> {
        debug_assert!(new_layout.size() <= old_layout.size());
        debug_assert!(new_layout.align() <= old_layout.align());

        let mut len = old_layout.size();

        // Shrinking the given area is possible if it is at the end of the arena.
        if unsafe { ptr.add(len) == self.base.add(self.offset.get()) } {
            self.offset.set(self.offset.get() - len + new_layout.size());
            len = new_layout.size();
        } else {
            debug_assert!(
                false,
                "Did you call shrink_to_fit()? Only the last allocation can be shrunk!"
            );
        }

        Ok(NonNull::slice_from_raw_parts(ptr, len))
    }
}

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

//! `memchr`, but with two needles.

use std::ptr;

/// `memchr`, but with two needles.
///
/// Returns the index of the first occurrence of either needle in the
/// `haystack`. If no needle is found, `haystack.len()` is returned.
/// `offset` specifies the index to start searching from.
pub fn memchr2(needle1: u8, needle2: u8, haystack: &[u8], offset: usize) -> usize {
    unsafe {
        let beg = haystack.as_ptr();
        let end = beg.add(haystack.len());
        let it = beg.add(offset.min(haystack.len()));
        let it = memchr2_raw(needle1, needle2, it, end);
        it.offset_from_unsigned(beg)
    }
}

unsafe fn memchr2_raw(needle1: u8, needle2: u8, beg: *const u8, end: *const u8) -> *const u8 {
    #[cfg(any(target_arch = "x86", target_arch = "x86_64", target_arch = "loongarch64"))]
    return unsafe { MEMCHR2_DISPATCH(needle1, needle2, beg, end) };

    #[cfg(target_arch = "aarch64")]
    return unsafe { memchr2_neon(needle1, needle2, beg, end) };

    #[allow(unreachable_code)]
    return unsafe { memchr2_fallback(needle1, needle2, beg, end) };
}

unsafe fn memchr2_fallback(
    needle1: u8,
    needle2: u8,
    mut beg: *const u8,
    end: *const u8,
) -> *const u8 {
    unsafe {
        while !ptr::eq(beg, end) {
            let ch = *beg;
            if ch == needle1 || ch == needle2 {
                break;
            }
            beg = beg.add(1);
        }
        beg
    }
}

// In order to make `memchr2_raw` slim and fast, we use a function pointer that updates
// itself to the correct implementation on the first call. This reduces binary size.
// It would also reduce branches if we had >2 implementations (a jump still needs to be predicted).
// NOTE that this ONLY works if Control Flow Guard is disabled on Windows.
#[cfg(any(target_arch = "x86", target_arch = "x86_64", target_arch = "loongarch64"))]
static mut MEMCHR2_DISPATCH: unsafe fn(
    needle1: u8,
    needle2: u8,
    beg: *const u8,
    end: *const u8,
) -> *const u8 = memchr2_dispatch;

#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
unsafe fn memchr2_dispatch(needle1: u8, needle2: u8, beg: *const u8, end: *const u8) -> *const u8 {
    let func = if is_x86_feature_detected!("avx2") { memchr2_avx2 } else { memchr2_fallback };
    unsafe { MEMCHR2_DISPATCH = func };
    unsafe { func(needle1, needle2, beg, end) }
}

// FWIW, I found that adding support for AVX512 was not useful at the time,
// as it only marginally improved file load performance by <5%.
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
#[target_feature(enable = "avx2")]
unsafe fn memchr2_avx2(needle1: u8, needle2: u8, mut beg: *const u8, end: *const u8) -> *const u8 {
    unsafe {
        #[cfg(target_arch = "x86")]
        use std::arch::x86::*;
        #[cfg(target_arch = "x86_64")]
        use std::arch::x86_64::*;

        let n1 = _mm256_set1_epi8(needle1 as i8);
        let n2 = _mm256_set1_epi8(needle2 as i8);
        let mut remaining = end.offset_from_unsigned(beg);

        while remaining >= 32 {
            let v = _mm256_loadu_si256(beg as *const _);
            let a = _mm256_cmpeq_epi8(v, n1);
            let b = _mm256_cmpeq_epi8(v, n2);
            let c = _mm256_or_si256(a, b);
            let m = _mm256_movemask_epi8(c) as u32;

            if m != 0 {
                return beg.add(m.trailing_zeros() as usize);
            }

            beg = beg.add(32);
            remaining -= 32;
        }

        memchr2_fallback(needle1, needle2, beg, end)
    }
}

#[cfg(target_arch = "loongarch64")]
unsafe fn memchr2_dispatch(needle1: u8, needle2: u8, beg: *const u8, end: *const u8) -> *const u8 {
    use std::arch::is_loongarch_feature_detected;

    let func = if is_loongarch_feature_detected!("lasx") {
        memchr2_lasx
    } else if is_loongarch_feature_detected!("lsx") {
        memchr2_lsx
    } else {
        memchr2_fallback
    };
    unsafe { MEMCHR2_DISPATCH = func };
    unsafe { func(needle1, needle2, beg, end) }
}

#[cfg(target_arch = "loongarch64")]
#[target_feature(enable = "lasx")]
unsafe fn memchr2_lasx(needle1: u8, needle2: u8, mut beg: *const u8, end: *const u8) -> *const u8 {
    unsafe {
        use std::arch::loongarch64::*;
        use std::mem::transmute as T;

        let n1 = lasx_xvreplgr2vr_b(needle1 as i32);
        let n2 = lasx_xvreplgr2vr_b(needle2 as i32);

        let off = beg.align_offset(32);
        if off != 0 && off < end.offset_from_unsigned(beg) {
            beg = memchr2_lsx(needle1, needle2, beg, beg.add(off));
        }

        while end.offset_from_unsigned(beg) >= 32 {
            let v = lasx_xvld::<0>(beg as *const _);
            let a = lasx_xvseq_b(v, n1);
            let b = lasx_xvseq_b(v, n2);
            let c = lasx_xvor_v(T(a), T(b));
            let m = lasx_xvmskltz_b(T(c));
            let l = lasx_xvpickve2gr_wu::<0>(T(m));
            let h = lasx_xvpickve2gr_wu::<4>(T(m));
            let m = (h << 16) | l;

            if m != 0 {
                return beg.add(m.trailing_zeros() as usize);
            }

            beg = beg.add(32);
        }

        memchr2_fallback(needle1, needle2, beg, end)
    }
}

#[cfg(target_arch = "loongarch64")]
#[target_feature(enable = "lsx")]
unsafe fn memchr2_lsx(needle1: u8, needle2: u8, mut beg: *const u8, end: *const u8) -> *const u8 {
    unsafe {
        use std::arch::loongarch64::*;
        use std::mem::transmute as T;

        let n1 = lsx_vreplgr2vr_b(needle1 as i32);
        let n2 = lsx_vreplgr2vr_b(needle2 as i32);

        let off = beg.align_offset(16);
        if off != 0 && off < end.offset_from_unsigned(beg) {
            beg = memchr2_fallback(needle1, needle2, beg, beg.add(off));
        }

        while end.offset_from_unsigned(beg) >= 16 {
            let v = lsx_vld::<0>(beg as *const _);
            let a = lsx_vseq_b(v, n1);
            let b = lsx_vseq_b(v, n2);
            let c = lsx_vor_v(T(a), T(b));
            let m = lsx_vmskltz_b(T(c));
            let m = lsx_vpickve2gr_wu::<0>(T(m));

            if m != 0 {
                return beg.add(m.trailing_zeros() as usize);
            }

            beg = beg.add(16);
        }

        memchr2_fallback(needle1, needle2, beg, end)
    }
}

#[cfg(target_arch = "aarch64")]
unsafe fn memchr2_neon(needle1: u8, needle2: u8, mut beg: *const u8, end: *const u8) -> *const u8 {
    unsafe {
        use std::arch::aarch64::*;

        if end.offset_from_unsigned(beg) >= 16 {
            let n1 = vdupq_n_u8(needle1);
            let n2 = vdupq_n_u8(needle2);

            loop {
                let v = vld1q_u8(beg as *const _);
                let a = vceqq_u8(v, n1);
                let b = vceqq_u8(v, n2);
                let c = vorrq_u8(a, b);

                // https://community.arm.com/arm-community-blogs/b/servers-and-cloud-computing-blog/posts/porting-x86-vector-bitmask-optimizations-to-arm-neon
                let m = vreinterpretq_u16_u8(c);
                let m = vshrn_n_u16(m, 4);
                let m = vreinterpret_u64_u8(m);
                let m = vget_lane_u64(m, 0);

                if m != 0 {
                    return beg.add(m.trailing_zeros() as usize >> 2);
                }

                beg = beg.add(16);
                if end.offset_from_unsigned(beg) < 16 {
                    break;
                }
            }
        }

        memchr2_fallback(needle1, needle2, beg, end)
    }
}

#[cfg(test)]
mod tests {
    use std::slice;

    use super::*;
    use crate::sys;

    #[test]
    fn test_empty() {
        assert_eq!(memchr2(b'a', b'b', b"", 0), 0);
    }

    #[test]
    fn test_basic() {
        let haystack = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
        let haystack = &haystack[..43];

        assert_eq!(memchr2(b'a', b'z', haystack, 0), 0);
        assert_eq!(memchr2(b'p', b'q', haystack, 0), 15);
        assert_eq!(memchr2(b'Q', b'Z', haystack, 0), 42);
        assert_eq!(memchr2(b'0', b'9', haystack, 0), haystack.len());
    }

    // Test that it doesn't match before/after the start offset respectively.
    #[test]
    fn test_with_offset() {
        let haystack = b"abcdefghabcdefghabcdefghabcdefghabcdefgh";

        assert_eq!(memchr2(b'a', b'b', haystack, 0), 0);
        assert_eq!(memchr2(b'a', b'b', haystack, 1), 1);
        assert_eq!(memchr2(b'a', b'b', haystack, 2), 8);
        assert_eq!(memchr2(b'a', b'b', haystack, 9), 9);
        assert_eq!(memchr2(b'a', b'b', haystack, 16), 16);
        assert_eq!(memchr2(b'a', b'b', haystack, 41), 40);
    }

    // Test memory access safety at page boundaries.
    // The test is a success if it doesn't segfault.
    #[test]
    fn test_page_boundary() {
        let page = unsafe {
            const PAGE_SIZE: usize = 64 * 1024; // 64 KiB to cover many architectures.

            // 3 pages: uncommitted, committed, uncommitted
            let ptr = sys::virtual_reserve(PAGE_SIZE * 3).unwrap();
            sys::virtual_commit(ptr.add(PAGE_SIZE), PAGE_SIZE).unwrap();
            slice::from_raw_parts_mut(ptr.add(PAGE_SIZE).as_ptr(), PAGE_SIZE)
        };

        page.fill(b'a');

        // Test if it seeks beyond the page boundary.
        assert_eq!(memchr2(b'\0', b'\0', &page[page.len() - 40..], 0), 40);
        // Test if it seeks before the page boundary for the masked/partial load.
        assert_eq!(memchr2(b'\0', b'\0', &page[..10], 0), 10);
    }
}

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

//! `memchr` for arbitrary sizes (1/2/4/8 bytes).
//!
//! Clang calls the C `memset` function only for byte-sized types (or 0 fills).
//! We however need to fill other types as well. For that, clang generates
//! SIMD loops under higher optimization levels. With `-Os` however, it only
//! generates a trivial loop which is too slow for our needs.
//!
//! This implementation uses SWAR to only have a single implementation for all
//! 4 sizes: By duplicating smaller types into a larger `u64` register we can
//! treat all sizes as if they were `u64`. The only thing we need to take care
//! of is the tail end of the array, which needs to write 0-7 additional bytes.

use std::mem;

/// A marker trait for types that are safe to `memset`.
///
/// # Safety
///
/// Just like with C's `memset`, bad things happen
/// if you use this with non-trivial types.
pub unsafe trait MemsetSafe: Copy {}

unsafe impl MemsetSafe for u8 {}
unsafe impl MemsetSafe for u16 {}
unsafe impl MemsetSafe for u32 {}
unsafe impl MemsetSafe for u64 {}
unsafe impl MemsetSafe for usize {}

unsafe impl MemsetSafe for i8 {}
unsafe impl MemsetSafe for i16 {}
unsafe impl MemsetSafe for i32 {}
unsafe impl MemsetSafe for i64 {}
unsafe impl MemsetSafe for isize {}

/// Fills a slice with the given value.
#[inline]
pub fn memset<T: MemsetSafe>(dst: &mut [T], val: T) {
    unsafe {
        match mem::size_of::<T>() {
            1 => {
                // LLVM will compile this to a call to `memset`,
                // which hopefully should be better optimized than my code.
                let beg = dst.as_mut_ptr();
                let val = mem::transmute_copy::<_, u8>(&val);
                beg.write_bytes(val, dst.len());
            }
            2 => {
                let beg = dst.as_mut_ptr();
                let end = beg.add(dst.len());
                let val = mem::transmute_copy::<_, u16>(&val);
                memset_raw(beg as *mut u8, end as *mut u8, val as u64 * 0x0001000100010001);
            }
            4 => {
                let beg = dst.as_mut_ptr();
                let end = beg.add(dst.len());
                let val = mem::transmute_copy::<_, u32>(&val);
                memset_raw(beg as *mut u8, end as *mut u8, val as u64 * 0x0000000100000001);
            }
            8 => {
                let beg = dst.as_mut_ptr();
                let end = beg.add(dst.len());
                let val = mem::transmute_copy::<_, u64>(&val);
                memset_raw(beg as *mut u8, end as *mut u8, val);
            }
            _ => unreachable!(),
        }
    }
}

#[inline]
fn memset_raw(beg: *mut u8, end: *mut u8, val: u64) {
    #[cfg(any(target_arch = "x86", target_arch = "x86_64", target_arch = "loongarch64"))]
    return unsafe { MEMSET_DISPATCH(beg, end, val) };

    #[cfg(target_arch = "aarch64")]
    return unsafe { memset_neon(beg, end, val) };

    #[allow(unreachable_code)]
    return unsafe { memset_fallback(beg, end, val) };
}

#[inline(never)]
unsafe fn memset_fallback(mut beg: *mut u8, end: *mut u8, val: u64) {
    unsafe {
        let mut remaining = end.offset_from_unsigned(beg);

        while remaining >= 8 {
            (beg as *mut u64).write_unaligned(val);
            beg = beg.add(8);
            remaining -= 8;
        }

        if remaining >= 4 {
            // 4-7 bytes remaining
            (beg as *mut u32).write_unaligned(val as u32);
            (end.sub(4) as *mut u32).write_unaligned(val as u32);
        } else if remaining >= 2 {
            // 2-3 bytes remaining
            (beg as *mut u16).write_unaligned(val as u16);
            (end.sub(2) as *mut u16).write_unaligned(val as u16);
        } else if remaining >= 1 {
            // 1 byte remaining
            beg.write(val as u8);
        }
    }
}

#[cfg(any(target_arch = "x86", target_arch = "x86_64", target_arch = "loongarch64"))]
static mut MEMSET_DISPATCH: unsafe fn(beg: *mut u8, end: *mut u8, val: u64) = memset_dispatch;

#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
fn memset_dispatch(beg: *mut u8, end: *mut u8, val: u64) {
    let func = if is_x86_feature_detected!("avx2") { memset_avx2 } else { memset_sse2 };
    unsafe { MEMSET_DISPATCH = func };
    unsafe { func(beg, end, val) }
}

#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
#[target_feature(enable = "sse2")]
unsafe fn memset_sse2(mut beg: *mut u8, end: *mut u8, val: u64) {
    unsafe {
        #[cfg(target_arch = "x86")]
        use std::arch::x86::*;
        #[cfg(target_arch = "x86_64")]
        use std::arch::x86_64::*;

        let mut remaining = end.offset_from_unsigned(beg);

        if remaining >= 16 {
            let fill = _mm_set1_epi64x(val as i64);

            while remaining >= 32 {
                _mm_storeu_si128(beg as *mut _, fill);
                _mm_storeu_si128(beg.add(16) as *mut _, fill);

                beg = beg.add(32);
                remaining -= 32;
            }

            if remaining >= 16 {
                // 16-31 bytes remaining
                _mm_storeu_si128(beg as *mut _, fill);
                _mm_storeu_si128(end.sub(16) as *mut _, fill);
                return;
            }
        }

        if remaining >= 8 {
            // 8-15 bytes remaining
            (beg as *mut u64).write_unaligned(val);
            (end.sub(8) as *mut u64).write_unaligned(val);
        } else if remaining >= 4 {
            // 4-7 bytes remaining
            (beg as *mut u32).write_unaligned(val as u32);
            (end.sub(4) as *mut u32).write_unaligned(val as u32);
        } else if remaining >= 2 {
            // 2-3 bytes remaining
            (beg as *mut u16).write_unaligned(val as u16);
            (end.sub(2) as *mut u16).write_unaligned(val as u16);
        } else if remaining >= 1 {
            // 1 byte remaining
            beg.write(val as u8);
        }
    }
}

#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
#[target_feature(enable = "avx2")]
fn memset_avx2(mut beg: *mut u8, end: *mut u8, val: u64) {
    unsafe {
        #[cfg(target_arch = "x86")]
        use std::arch::x86::*;
        #[cfg(target_arch = "x86_64")]
        use std::arch::x86_64::*;
        use std::hint::black_box;

        let mut remaining = end.offset_from_unsigned(beg);

        if remaining >= 128 {
            let fill = _mm256_set1_epi64x(val as i64);

            loop {
                _mm256_storeu_si256(beg as *mut _, fill);
                _mm256_storeu_si256(beg.add(32) as *mut _, fill);
                _mm256_storeu_si256(beg.add(64) as *mut _, fill);
                _mm256_storeu_si256(beg.add(96) as *mut _, fill);

                beg = beg.add(128);
                remaining -= 128;
                if remaining < 128 {
                    break;
                }
            }
        }

        if remaining >= 16 {
            let fill = _mm_set1_epi64x(val as i64);

            loop {
                // LLVM is _very_ eager to unroll loops. In the absence of an unroll attribute, black_box does the job.
                // Note that this must not be applied to the intrinsic parameters, as they're otherwise misoptimized.
                #[allow(clippy::unit_arg)]
                black_box(_mm_storeu_si128(beg as *mut _, fill));

                beg = beg.add(16);
                remaining -= 16;
                if remaining < 16 {
                    break;
                }
            }
        }

        // `remaining` is between 0 and 15 at this point.
        // By overlapping the stores we can write all of them in at most 2 stores. This approach
        // can be seen in various libraries, such as wyhash which uses it for loading data in `wyr3`.
        if remaining >= 8 {
            // 8-15 bytes
            (beg as *mut u64).write_unaligned(val);
            (end.sub(8) as *mut u64).write_unaligned(val);
        } else if remaining >= 4 {
            // 4-7 bytes
            (beg as *mut u32).write_unaligned(val as u32);
            (end.sub(4) as *mut u32).write_unaligned(val as u32);
        } else if remaining >= 2 {
            // 2-3 bytes
            (beg as *mut u16).write_unaligned(val as u16);
            (end.sub(2) as *mut u16).write_unaligned(val as u16);
        } else if remaining >= 1 {
            // 1 byte
            beg.write(val as u8);
        }
    }
}

#[cfg(target_arch = "loongarch64")]
fn memset_dispatch(beg: *mut u8, end: *mut u8, val: u64) {
    use std::arch::is_loongarch_feature_detected;

    let func = if is_loongarch_feature_detected!("lasx") {
        memset_lasx
    } else if is_loongarch_feature_detected!("lsx") {
        memset_lsx
    } else {
        memset_fallback
    };
    unsafe { MEMSET_DISPATCH = func };
    unsafe { func(beg, end, val) }
}

#[cfg(target_arch = "loongarch64")]
#[target_feature(enable = "lasx")]
fn memset_lasx(mut beg: *mut u8, end: *mut u8, val: u64) {
    unsafe {
        use std::arch::loongarch64::*;
        use std::mem::transmute as T;

        let fill: v32i8 = T(lasx_xvreplgr2vr_d(val as i64));

        if end.offset_from_unsigned(beg) >= 32 {
            lasx_xvst::<0>(fill, beg as *mut _);
            let off = beg.align_offset(32);
            beg = beg.add(off);
        }

        if end.offset_from_unsigned(beg) >= 128 {
            loop {
                lasx_xvst::<0>(fill, beg as *mut _);
                lasx_xvst::<32>(fill, beg as *mut _);
                lasx_xvst::<64>(fill, beg as *mut _);
                lasx_xvst::<96>(fill, beg as *mut _);

                beg = beg.add(128);
                if end.offset_from_unsigned(beg) < 128 {
                    break;
                }
            }
        }

        if end.offset_from_unsigned(beg) >= 16 {
            let fill: v16i8 = T(lsx_vreplgr2vr_d(val as i64));

            loop {
                lsx_vst::<0>(fill, beg as *mut _);

                beg = beg.add(16);
                if end.offset_from_unsigned(beg) < 16 {
                    break;
                }
            }
        }

        if end.offset_from_unsigned(beg) >= 8 {
            // 8-15 bytes
            (beg as *mut u64).write_unaligned(val);
            (end.sub(8) as *mut u64).write_unaligned(val);
        } else if end.offset_from_unsigned(beg) >= 4 {
            // 4-7 bytes
            (beg as *mut u32).write_unaligned(val as u32);
            (end.sub(4) as *mut u32).write_unaligned(val as u32);
        } else if end.offset_from_unsigned(beg) >= 2 {
            // 2-3 bytes
            (beg as *mut u16).write_unaligned(val as u16);
            (end.sub(2) as *mut u16).write_unaligned(val as u16);
        } else if end.offset_from_unsigned(beg) >= 1 {
            // 1 byte
            beg.write(val as u8);
        }
    }
}

#[cfg(target_arch = "loongarch64")]
#[target_feature(enable = "lsx")]
unsafe fn memset_lsx(mut beg: *mut u8, end: *mut u8, val: u64) {
    unsafe {
        use std::arch::loongarch64::*;
        use std::mem::transmute as T;

        if end.offset_from_unsigned(beg) >= 16 {
            let fill: v16i8 = T(lsx_vreplgr2vr_d(val as i64));

            lsx_vst::<0>(fill, beg as *mut _);
            let off = beg.align_offset(16);
            beg = beg.add(off);

            while end.offset_from_unsigned(beg) >= 32 {
                lsx_vst::<0>(fill, beg as *mut _);
                lsx_vst::<16>(fill, beg as *mut _);

                beg = beg.add(32);
            }

            if end.offset_from_unsigned(beg) >= 16 {
                // 16-31 bytes remaining
                lsx_vst::<0>(fill, beg as *mut _);
                lsx_vst::<-16>(fill, end as *mut _);
                return;
            }
        }

        if end.offset_from_unsigned(beg) >= 8 {
            // 8-15 bytes remaining
            (beg as *mut u64).write_unaligned(val);
            (end.sub(8) as *mut u64).write_unaligned(val);
        } else if end.offset_from_unsigned(beg) >= 4 {
            // 4-7 bytes remaining
            (beg as *mut u32).write_unaligned(val as u32);
            (end.sub(4) as *mut u32).write_unaligned(val as u32);
        } else if end.offset_from_unsigned(beg) >= 2 {
            // 2-3 bytes remaining
            (beg as *mut u16).write_unaligned(val as u16);
            (end.sub(2) as *mut u16).write_unaligned(val as u16);
        } else if end.offset_from_unsigned(beg) >= 1 {
            // 1 byte remaining
            beg.write(val as u8);
        }
    }
}

#[cfg(target_arch = "aarch64")]
unsafe fn memset_neon(mut beg: *mut u8, end: *mut u8, val: u64) {
    unsafe {
        use std::arch::aarch64::*;
        let mut remaining = end.offset_from_unsigned(beg);

        if remaining >= 32 {
            let fill = vdupq_n_u64(val);

            loop {
                // Compiles to a single `stp` instruction.
                vst1q_u64(beg as *mut _, fill);
                vst1q_u64(beg.add(16) as *mut _, fill);

                beg = beg.add(32);
                remaining -= 32;
                if remaining < 32 {
                    break;
                }
            }
        }

        if remaining >= 16 {
            // 16-31 bytes remaining
            let fill = vdupq_n_u64(val);
            vst1q_u64(beg as *mut _, fill);
            vst1q_u64(end.sub(16) as *mut _, fill);
        } else if remaining >= 8 {
            // 8-15 bytes remaining
            (beg as *mut u64).write_unaligned(val);
            (end.sub(8) as *mut u64).write_unaligned(val);
        } else if remaining >= 4 {
            // 4-7 bytes remaining
            (beg as *mut u32).write_unaligned(val as u32);
            (end.sub(4) as *mut u32).write_unaligned(val as u32);
        } else if remaining >= 2 {
            // 2-3 bytes remaining
            (beg as *mut u16).write_unaligned(val as u16);
            (end.sub(2) as *mut u16).write_unaligned(val as u16);
        } else if remaining >= 1 {
            // 1 byte remaining
            beg.write(val as u8);
        }
    }
}

#[cfg(test)]
mod tests {
    use std::fmt;
    use std::ops::Not;

    use super::*;

    fn check_memset<T>(val: T, len: usize)
    where
        T: MemsetSafe + Not<Output = T> + PartialEq + fmt::Debug,
    {
        let mut buf = vec![!val; len];
        memset(&mut buf, val);
        assert!(buf.iter().all(|&x| x == val));
    }

    #[test]
    fn test_memset_empty() {
        check_memset(0u8, 0);
        check_memset(0u16, 0);
        check_memset(0u32, 0);
        check_memset(0u64, 0);
    }

    #[test]
    fn test_memset_single() {
        check_memset(0u8, 1);
        check_memset(0xFFu8, 1);
        check_memset(0xABu16, 1);
        check_memset(0x12345678u32, 1);
        check_memset(0xDEADBEEFu64, 1);
    }

    #[test]
    fn test_memset_small() {
        for &len in &[2, 3, 4, 5, 7, 8, 9] {
            check_memset(0xAAu8, len);
            check_memset(0xBEEFu16, len);
            check_memset(0xCAFEBABEu32, len);
            check_memset(0x1234567890ABCDEFu64, len);
        }
    }

    #[test]
    fn test_memset_large() {
        check_memset(0u8, 1000);
        check_memset(0xFFu8, 1024);
        check_memset(0xBEEFu16, 512);
        check_memset(0xCAFEBABEu32, 256);
        check_memset(0x1234567890ABCDEFu64, 128);
    }

    #[test]
    fn test_memset_various_values() {
        check_memset(0u8, 17);
        check_memset(0x7Fu8, 17);
        check_memset(0x8001u16, 17);
        check_memset(0xFFFFFFFFu32, 17);
        check_memset(0x8000000000000001u64, 17);
    }

    #[test]
    fn test_memset_signed_types() {
        check_memset(-1i8, 8);
        check_memset(-2i16, 8);
        check_memset(-3i32, 8);
        check_memset(-4i64, 8);
        check_memset(-5isize, 8);
    }

    #[test]
    fn test_memset_usize_isize() {
        check_memset(0usize, 4);
        check_memset(usize::MAX, 4);
        check_memset(0isize, 4);
        check_memset(isize::MIN, 4);
    }

    #[test]
    fn test_memset_alignment() {
        // Check that memset works for slices not aligned to 8 bytes
        let mut buf = [0u8; 15];
        for offset in 0..8 {
            let slice = &mut buf[offset..(offset + 7)];
            memset(slice, 0x5A);
            assert!(slice.iter().all(|&x| x == 0x5A));
        }
    }
}

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use std::ptr;

use crate::helpers::CoordType;

/// Starting from the `offset` in `haystack` with a current line index of
/// `line`, this seeks backwards to the `line_stop`-nth line and returns the
/// new offset and the line index at that point.
///
/// Note that this function differs from `lines_fwd` in that it
/// seeks backwards even if the `line` is already at `line_stop`.
/// This allows you to ensure (or test) whether `offset` is at a line start.
///
/// It returns an offset *past* a newline and thus at the start of a line.
pub fn lines_bwd(
    haystack: &[u8],
    offset: usize,
    line: CoordType,
    line_stop: CoordType,
) -> (usize, CoordType) {
    unsafe {
        let beg = haystack.as_ptr();
        let it = beg.add(offset.min(haystack.len()));
        let (it, line) = lines_bwd_raw(beg, it, line, line_stop);
        (it.offset_from_unsigned(beg), line)
    }
}

unsafe fn lines_bwd_raw(
    beg: *const u8,
    end: *const u8,
    line: CoordType,
    line_stop: CoordType,
) -> (*const u8, CoordType) {
    #[cfg(any(target_arch = "x86_64", target_arch = "loongarch64"))]
    return unsafe { LINES_BWD_DISPATCH(beg, end, line, line_stop) };

    #[cfg(target_arch = "aarch64")]
    return unsafe { lines_bwd_neon(beg, end, line, line_stop) };

    #[allow(unreachable_code)]
    return unsafe { lines_bwd_fallback(beg, end, line, line_stop) };
}

unsafe fn lines_bwd_fallback(
    beg: *const u8,
    mut end: *const u8,
    mut line: CoordType,
    line_stop: CoordType,
) -> (*const u8, CoordType) {
    unsafe {
        while !ptr::eq(end, beg) {
            let n = end.sub(1);
            if *n == b'\n' {
                if line <= line_stop {
                    break;
                }
                line -= 1;
            }
            end = n;
        }
        (end, line)
    }
}

#[cfg(any(target_arch = "x86_64", target_arch = "loongarch64"))]
static mut LINES_BWD_DISPATCH: unsafe fn(
    beg: *const u8,
    end: *const u8,
    line: CoordType,
    line_stop: CoordType,
) -> (*const u8, CoordType) = lines_bwd_dispatch;

#[cfg(target_arch = "x86_64")]
unsafe fn lines_bwd_dispatch(
    beg: *const u8,
    end: *const u8,
    line: CoordType,
    line_stop: CoordType,
) -> (*const u8, CoordType) {
    let func = if is_x86_feature_detected!("avx2") { lines_bwd_avx2 } else { lines_bwd_fallback };
    unsafe { LINES_BWD_DISPATCH = func };
    unsafe { func(beg, end, line, line_stop) }
}

#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2")]
unsafe fn lines_bwd_avx2(
    beg: *const u8,
    mut end: *const u8,
    mut line: CoordType,
    line_stop: CoordType,
) -> (*const u8, CoordType) {
    unsafe {
        use std::arch::x86_64::*;

        #[inline(always)]
        unsafe fn horizontal_sum_i64(v: __m256i) -> i64 {
            unsafe {
                let hi = _mm256_extracti128_si256::<1>(v);
                let lo = _mm256_castsi256_si128(v);
                let sum = _mm_add_epi64(lo, hi);
                let shuf = _mm_shuffle_epi32::<0b11_10_11_10>(sum);
                let sum = _mm_add_epi64(sum, shuf);
                _mm_cvtsi128_si64(sum)
            }
        }

        let lf = _mm256_set1_epi8(b'\n' as i8);
        let off = end.addr() & 31;
        if off != 0 && off < end.offset_from_unsigned(beg) {
            (end, line) = lines_bwd_fallback(end.sub(off), end, line, line_stop);
        }

        while end.offset_from_unsigned(beg) >= 128 {
            let chunk_start = end.sub(128);

            let v1 = _mm256_loadu_si256(chunk_start.add(0) as *const _);
            let v2 = _mm256_loadu_si256(chunk_start.add(32) as *const _);
            let v3 = _mm256_loadu_si256(chunk_start.add(64) as *const _);
            let v4 = _mm256_loadu_si256(chunk_start.add(96) as *const _);

            let mut sum = _mm256_setzero_si256();
            sum = _mm256_sub_epi8(sum, _mm256_cmpeq_epi8(v1, lf));
            sum = _mm256_sub_epi8(sum, _mm256_cmpeq_epi8(v2, lf));
            sum = _mm256_sub_epi8(sum, _mm256_cmpeq_epi8(v3, lf));
            sum = _mm256_sub_epi8(sum, _mm256_cmpeq_epi8(v4, lf));

            let sum = _mm256_sad_epu8(sum, _mm256_setzero_si256());
            let sum = horizontal_sum_i64(sum);

            let line_next = line - sum as CoordType;
            if line_next <= line_stop {
                break;
            }

            end = chunk_start;
            line = line_next;
        }

        while end.offset_from_unsigned(beg) >= 32 {
            let chunk_start = end.sub(32);
            let v = _mm256_loadu_si256(chunk_start as *const _);
            let c = _mm256_cmpeq_epi8(v, lf);

            let ones = _mm256_and_si256(c, _mm256_set1_epi8(0x01));
            let sum = _mm256_sad_epu8(ones, _mm256_setzero_si256());
            let sum = horizontal_sum_i64(sum);

            let line_next = line - sum as CoordType;
            if line_next <= line_stop {
                break;
            }

            end = chunk_start;
            line = line_next;
        }

        lines_bwd_fallback(beg, end, line, line_stop)
    }
}

#[cfg(target_arch = "loongarch64")]
unsafe fn lines_bwd_dispatch(
    beg: *const u8,
    end: *const u8,
    line: CoordType,
    line_stop: CoordType,
) -> (*const u8, CoordType) {
    use std::arch::is_loongarch_feature_detected;

    let func = if is_loongarch_feature_detected!("lasx") {
        lines_bwd_lasx
    } else if is_loongarch_feature_detected!("lsx") {
        lines_bwd_lsx
    } else {
        lines_bwd_fallback
    };
    unsafe { LINES_BWD_DISPATCH = func };
    unsafe { func(beg, end, line, line_stop) }
}

#[cfg(target_arch = "loongarch64")]
#[target_feature(enable = "lasx")]
unsafe fn lines_bwd_lasx(
    beg: *const u8,
    mut end: *const u8,
    mut line: CoordType,
    line_stop: CoordType,
) -> (*const u8, CoordType) {
    unsafe {
        use std::arch::loongarch64::*;
        use std::mem::transmute as T;

        #[inline(always)]
        unsafe fn horizontal_sum(sum: v32i8) -> u32 {
            unsafe {
                let sum = lasx_xvhaddw_h_b(sum, sum);
                let sum = lasx_xvhaddw_w_h(sum, sum);
                let sum = lasx_xvhaddw_d_w(sum, sum);
                let sum = lasx_xvhaddw_q_d(sum, sum);
                let tmp = lasx_xvpermi_q::<1>(T(sum), T(sum));
                let sum = lasx_xvadd_w(T(sum), T(tmp));
                lasx_xvpickve2gr_wu::<0>(sum)
            }
        }

        let lf = lasx_xvrepli_b(b'\n' as i32);
        let line_stop = line_stop.min(line);
        let off = end.addr() & 31;
        if off != 0 && off < end.offset_from_unsigned(beg) {
            (end, line) = lines_bwd_fallback(end.sub(off), end, line, line_stop);
        }

        while end.offset_from_unsigned(beg) >= 128 {
            let chunk_start = end.sub(128);

            let v1 = lasx_xvld::<0>(chunk_start as *const _);
            let v2 = lasx_xvld::<32>(chunk_start as *const _);
            let v3 = lasx_xvld::<64>(chunk_start as *const _);
            let v4 = lasx_xvld::<96>(chunk_start as *const _);

            let mut sum = lasx_xvrepli_b(0);
            sum = lasx_xvsub_b(sum, lasx_xvseq_b(v1, lf));
            sum = lasx_xvsub_b(sum, lasx_xvseq_b(v2, lf));
            sum = lasx_xvsub_b(sum, lasx_xvseq_b(v3, lf));
            sum = lasx_xvsub_b(sum, lasx_xvseq_b(v4, lf));
            let sum = horizontal_sum(sum);

            let line_next = line - sum as CoordType;
            if line_next <= line_stop {
                break;
            }

            end = chunk_start;
            line = line_next;
        }

        while end.offset_from_unsigned(beg) >= 32 {
            let chunk_start = end.sub(32);
            let v = lasx_xvld::<0>(chunk_start as *const _);
            let c = lasx_xvseq_b(v, lf);

            let ones = lasx_xvand_v(T(c), T(lasx_xvrepli_b(1)));
            let sum = horizontal_sum(T(ones));

            let line_next = line - sum as CoordType;
            if line_next <= line_stop {
                break;
            }

            end = chunk_start;
            line = line_next;
        }

        lines_bwd_fallback(beg, end, line, line_stop)
    }
}

#[cfg(target_arch = "loongarch64")]
#[target_feature(enable = "lsx")]
unsafe fn lines_bwd_lsx(
    beg: *const u8,
    mut end: *const u8,
    mut line: CoordType,
    line_stop: CoordType,
) -> (*const u8, CoordType) {
    unsafe {
        use std::arch::loongarch64::*;
        use std::mem::transmute as T;

        #[inline(always)]
        unsafe fn horizontal_sum(sum: v16i8) -> u32 {
            unsafe {
                let sum = lsx_vhaddw_h_b(sum, sum);
                let sum = lsx_vhaddw_w_h(sum, sum);
                let sum = lsx_vhaddw_d_w(sum, sum);
                let sum = lsx_vhaddw_q_d(sum, sum);
                lsx_vpickve2gr_wu::<0>(T(sum))
            }
        }

        let lf = lsx_vrepli_b(b'\n' as i32);
        let line_stop = line_stop.min(line);
        let off = end.addr() & 15;
        if off != 0 && off < end.offset_from_unsigned(beg) {
            (end, line) = lines_bwd_fallback(end.sub(off), end, line, line_stop);
        }

        while end.offset_from_unsigned(beg) >= 64 {
            let chunk_start = end.sub(64);

            let v1 = lsx_vld::<0>(chunk_start as *const _);
            let v2 = lsx_vld::<16>(chunk_start as *const _);
            let v3 = lsx_vld::<32>(chunk_start as *const _);
            let v4 = lsx_vld::<48>(chunk_start as *const _);

            let mut sum = lsx_vrepli_b(0);
            sum = lsx_vsub_b(sum, lsx_vseq_b(v1, lf));
            sum = lsx_vsub_b(sum, lsx_vseq_b(v2, lf));
            sum = lsx_vsub_b(sum, lsx_vseq_b(v3, lf));
            sum = lsx_vsub_b(sum, lsx_vseq_b(v4, lf));
            let sum = horizontal_sum(sum);

            let line_next = line - sum as CoordType;
            if line_next <= line_stop {
                break;
            }

            end = chunk_start;
            line = line_next;
        }

        while end.offset_from_unsigned(beg) >= 16 {
            let chunk_start = end.sub(16);
            let v = lsx_vld::<0>(chunk_start as *const _);
            let c = lsx_vseq_b(v, lf);

            let ones = lsx_vand_v(T(c), T(lsx_vrepli_b(1)));
            let sum = horizontal_sum(T(ones));

            let line_next = line - sum as CoordType;
            if line_next <= line_stop {
                break;
            }

            end = chunk_start;
            line = line_next;
        }

        lines_bwd_fallback(beg, end, line, line_stop)
    }
}

#[cfg(target_arch = "aarch64")]
unsafe fn lines_bwd_neon(
    beg: *const u8,
    mut end: *const u8,
    mut line: CoordType,
    line_stop: CoordType,
) -> (*const u8, CoordType) {
    unsafe {
        use std::arch::aarch64::*;

        let lf = vdupq_n_u8(b'\n');
        let line_stop = line_stop.min(line);
        let off = end.addr() & 15;
        if off != 0 && off < end.offset_from_unsigned(beg) {
            (end, line) = lines_bwd_fallback(end.sub(off), end, line, line_stop);
        }

        while end.offset_from_unsigned(beg) >= 64 {
            let chunk_start = end.sub(64);

            let v1 = vld1q_u8(chunk_start.add(0));
            let v2 = vld1q_u8(chunk_start.add(16));
            let v3 = vld1q_u8(chunk_start.add(32));
            let v4 = vld1q_u8(chunk_start.add(48));

            let mut sum = vdupq_n_u8(0);
            sum = vsubq_u8(sum, vceqq_u8(v1, lf));
            sum = vsubq_u8(sum, vceqq_u8(v2, lf));
            sum = vsubq_u8(sum, vceqq_u8(v3, lf));
            sum = vsubq_u8(sum, vceqq_u8(v4, lf));

            let sum = vaddvq_u8(sum);

            let line_next = line - sum as CoordType;
            if line_next <= line_stop {
                break;
            }

            end = chunk_start;
            line = line_next;
        }

        while end.offset_from_unsigned(beg) >= 16 {
            let chunk_start = end.sub(16);
            let v = vld1q_u8(chunk_start);
            let c = vceqq_u8(v, lf);
            let c = vandq_u8(c, vdupq_n_u8(0x01));
            let sum = vaddvq_u8(c);

            let line_next = line - sum as CoordType;
            if line_next <= line_stop {
                break;
            }

            end = chunk_start;
            line = line_next;
        }

        lines_bwd_fallback(beg, end, line, line_stop)
    }
}

#[cfg(test)]
mod test {
    use super::*;
    use crate::helpers::CoordType;
    use crate::simd::test::*;

    #[test]
    fn pseudo_fuzz() {
        let text = generate_random_text(1024);
        let lines = count_lines(&text);
        let mut offset_rng = make_rng();
        let mut line_rng = make_rng();
        let mut line_distance_rng = make_rng();

        for _ in 0..1000 {
            let offset = offset_rng() % (text.len() + 1);
            let line_stop = line_distance_rng() % (lines + 1);
            let line = (line_stop + line_rng() % 100).saturating_sub(5);

            let line = line as CoordType;
            let line_stop = line_stop as CoordType;

            let expected = reference_lines_bwd(text.as_bytes(), offset, line, line_stop);
            let actual = lines_bwd(text.as_bytes(), offset, line, line_stop);

            assert_eq!(expected, actual);
        }
    }

    fn reference_lines_bwd(
        haystack: &[u8],
        mut offset: usize,
        mut line: CoordType,
        line_stop: CoordType,
    ) -> (usize, CoordType) {
        while offset > 0 {
            let c = haystack[offset - 1];
            if c == b'\n' {
                if line <= line_stop {
                    break;
                }
                line -= 1;
            }
            offset -= 1;
        }
        (offset, line)
    }

    #[test]
    fn seeks_to_start() {
        for i in 6..=11 {
            let (off, line) = lines_bwd(b"Hello\nWorld\n", i, 123, 456);
            assert_eq!(off, 6); // After "Hello\n"
            assert_eq!(line, 123); // Still on the same line
        }
    }
}

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

//! Base64 facilities.

use crate::arena::ArenaString;

const CHARSET: [u8; 64] = *b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

/// One aspect of base64 is that the encoded length can be
/// calculated accurately in advance, which is what this returns.
#[inline]
pub fn encode_len(src_len: usize) -> usize {
    src_len.div_ceil(3) * 4
}

/// Encodes the given bytes as base64 and appends them to the destination string.
pub fn encode(dst: &mut ArenaString, src: &[u8]) {
    unsafe {
        let mut inp = src.as_ptr();
        let mut remaining = src.len();
        let dst = dst.as_mut_vec();

        let out_len = encode_len(src.len());
        // ... we can then use this fact to reserve space all at once.
        dst.reserve(out_len);

        // SAFETY: Getting a pointer to the reserved space is only safe
        // *after* calling `reserve()` as it may change the pointer.
        let mut out = dst.as_mut_ptr().add(dst.len());

        if remaining != 0 {
            // Translate chunks of 3 source bytes into 4 base64-encoded bytes.
            while remaining > 3 {
                // SAFETY: Thanks to `remaining > 3`, reading 4 bytes at once is safe.
                // This improves performance massively over a byte-by-byte approach,
                // because it allows us to byte-swap the read and use simple bit-shifts below.
                let val = u32::from_be((inp as *const u32).read_unaligned());
                inp = inp.add(3);
                remaining -= 3;

                *out = CHARSET[(val >> 26) as usize];
                out = out.add(1);
                *out = CHARSET[(val >> 20) as usize & 0x3f];
                out = out.add(1);
                *out = CHARSET[(val >> 14) as usize & 0x3f];
                out = out.add(1);
                *out = CHARSET[(val >> 8) as usize & 0x3f];
                out = out.add(1);
            }

            // Convert the remaining 1-3 bytes.
            let mut in1 = 0;
            let mut in2 = 0;

            // We can simplify the following logic by assuming that there's only 1
            // byte left. If there's >1 byte left, these two '=' will be overwritten.
            *out.add(3) = b'=';
            *out.add(2) = b'=';

            if remaining >= 3 {
                in2 = inp.add(2).read() as usize;
                *out.add(3) = CHARSET[in2 & 0x3f];
            }

            if remaining >= 2 {
                in1 = inp.add(1).read() as usize;
                *out.add(2) = CHARSET[(in1 << 2 | in2 >> 6) & 0x3f];
            }

            let in0 = inp.add(0).read() as usize;
            *out.add(1) = CHARSET[(in0 << 4 | in1 >> 4) & 0x3f];
            *out.add(0) = CHARSET[in0 >> 2];
        }

        dst.set_len(dst.len() + out_len);
    }
}

#[cfg(test)]
mod tests {
    use super::encode;
    use crate::arena::{Arena, ArenaString};

    #[test]
    fn test_basic() {
        let arena = Arena::new(4 * 1024).unwrap();
        let enc = |s: &[u8]| {
            let mut dst = ArenaString::new_in(&arena);
            encode(&mut dst, s);
            dst
        };
        assert_eq!(enc(b""), "");
        assert_eq!(enc(b"a"), "YQ==");
        assert_eq!(enc(b"ab"), "YWI=");
        assert_eq!(enc(b"abc"), "YWJj");
        assert_eq!(enc(b"abcd"), "YWJjZA==");
        assert_eq!(enc(b"abcde"), "YWJjZGU=");
        assert_eq!(enc(b"abcdef"), "YWJjZGVm");
        assert_eq!(enc(b"abcdefg"), "YWJjZGVmZw==");
        assert_eq!(enc(b"abcdefgh"), "YWJjZGVmZ2g=");
        assert_eq!(enc(b"abcdefghi"), "YWJjZGVmZ2hp");
        assert_eq!(enc(b"abcdefghij"), "YWJjZGVmZ2hpag==");
        assert_eq!(enc(b"abcdefghijk"), "YWJjZGVmZ2hpams=");
        assert_eq!(enc(b"abcdefghijkl"), "YWJjZGVmZ2hpamts");
        assert_eq!(enc(b"abcdefghijklm"), "YWJjZGVmZ2hpamtsbQ==");
        assert_eq!(enc(b"abcdefghijklmN"), "YWJjZGVmZ2hpamtsbU4=");
        assert_eq!(enc(b"abcdefghijklmNO"), "YWJjZGVmZ2hpamtsbU5P");
        assert_eq!(enc(b"abcdefghijklmNOP"), "YWJjZGVmZ2hpamtsbU5PUA==");
        assert_eq!(enc(b"abcdefghijklmNOPQ"), "YWJjZGVmZ2hpamtsbU5PUFE=");
        assert_eq!(enc(b"abcdefghijklmNOPQR"), "YWJjZGVmZ2hpamtsbU5PUFFS");
        assert_eq!(enc(b"abcdefghijklmNOPQRS"), "YWJjZGVmZ2hpamtsbU5PUFFSUw==");
        assert_eq!(enc(b"abcdefghijklmNOPQRST"), "YWJjZGVmZ2hpamtsbU5PUFFSU1Q=");
        assert_eq!(enc(b"abcdefghijklmNOPQRSTU"), "YWJjZGVmZ2hpamtsbU5PUFFSU1RV");
        assert_eq!(enc(b"abcdefghijklmNOPQRSTUV"), "YWJjZGVmZ2hpamtsbU5PUFFSU1RVVg==");
        assert_eq!(enc(b"abcdefghijklmNOPQRSTUVW"), "YWJjZGVmZ2hpamtsbU5PUFFSU1RVVlc=");
        assert_eq!(enc(b"abcdefghijklmNOPQRSTUVWX"), "YWJjZGVmZ2hpamtsbU5PUFFSU1RVVldY");
        assert_eq!(enc(b"abcdefghijklmNOPQRSTUVWXY"), "YWJjZGVmZ2hpamtsbU5PUFFSU1RVVldYWQ==");
        assert_eq!(enc(b"abcdefghijklmNOPQRSTUVWXYZ"), "YWJjZGVmZ2hpamtsbU5PUFFSU1RVVldYWVo=");
    }
}

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use std::ptr;

use crate::helpers::CoordType;

/// Starting from the `offset` in `haystack` with a current line index of
/// `line`, this seeks to the `line_stop`-nth line and returns the
/// new offset and the line index at that point.
///
/// It returns an offset *past* the newline.
/// If `line` is already at or past `line_stop`, it returns immediately.
pub fn lines_fwd(
    haystack: &[u8],
    offset: usize,
    line: CoordType,
    line_stop: CoordType,
) -> (usize, CoordType) {
    unsafe {
        let beg = haystack.as_ptr();
        let end = beg.add(haystack.len());
        let it = beg.add(offset.min(haystack.len()));
        let (it, line) = lines_fwd_raw(it, end, line, line_stop);
        (it.offset_from_unsigned(beg), line)
    }
}

unsafe fn lines_fwd_raw(
    beg: *const u8,
    end: *const u8,
    line: CoordType,
    line_stop: CoordType,
) -> (*const u8, CoordType) {
    #[cfg(any(target_arch = "x86_64", target_arch = "loongarch64"))]
    return unsafe { LINES_FWD_DISPATCH(beg, end, line, line_stop) };

    #[cfg(target_arch = "aarch64")]
    return unsafe { lines_fwd_neon(beg, end, line, line_stop) };

    #[allow(unreachable_code)]
    return unsafe { lines_fwd_fallback(beg, end, line, line_stop) };
}

unsafe fn lines_fwd_fallback(
    mut beg: *const u8,
    end: *const u8,
    mut line: CoordType,
    line_stop: CoordType,
) -> (*const u8, CoordType) {
    unsafe {
        if line < line_stop {
            while !ptr::eq(beg, end) {
                let c = *beg;
                beg = beg.add(1);
                if c == b'\n' {
                    line += 1;
                    if line == line_stop {
                        break;
                    }
                }
            }
        }
        (beg, line)
    }
}

#[cfg(any(target_arch = "x86_64", target_arch = "loongarch64"))]
static mut LINES_FWD_DISPATCH: unsafe fn(
    beg: *const u8,
    end: *const u8,
    line: CoordType,
    line_stop: CoordType,
) -> (*const u8, CoordType) = lines_fwd_dispatch;

#[cfg(target_arch = "x86_64")]
unsafe fn lines_fwd_dispatch(
    beg: *const u8,
    end: *const u8,
    line: CoordType,
    line_stop: CoordType,
) -> (*const u8, CoordType) {
    let func = if is_x86_feature_detected!("avx2") { lines_fwd_avx2 } else { lines_fwd_fallback };
    unsafe { LINES_FWD_DISPATCH = func };
    unsafe { func(beg, end, line, line_stop) }
}

#[cfg(target_arch = "x86_64")]
#[target_feature(enable = "avx2")]
unsafe fn lines_fwd_avx2(
    mut beg: *const u8,
    end: *const u8,
    mut line: CoordType,
    line_stop: CoordType,
) -> (*const u8, CoordType) {
    unsafe {
        use std::arch::x86_64::*;

        #[inline(always)]
        unsafe fn horizontal_sum_i64(v: __m256i) -> i64 {
            unsafe {
                let hi = _mm256_extracti128_si256::<1>(v);
                let lo = _mm256_castsi256_si128(v);
                let sum = _mm_add_epi64(lo, hi);
                let shuf = _mm_shuffle_epi32::<0b11_10_11_10>(sum);
                let sum = _mm_add_epi64(sum, shuf);
                _mm_cvtsi128_si64(sum)
            }
        }

        let lf = _mm256_set1_epi8(b'\n' as i8);
        let off = beg.align_offset(32);
        if off != 0 && off < end.offset_from_unsigned(beg) {
            (beg, line) = lines_fwd_fallback(beg, beg.add(off), line, line_stop);
        }

        if line < line_stop {
            // Unrolling the loop by 4x speeds things up by >3x.
            // It allows us to accumulate matches before doing a single `vpsadbw`.
            while end.offset_from_unsigned(beg) >= 128 {
                let v1 = _mm256_loadu_si256(beg.add(0) as *const _);
                let v2 = _mm256_loadu_si256(beg.add(32) as *const _);
                let v3 = _mm256_loadu_si256(beg.add(64) as *const _);
                let v4 = _mm256_loadu_si256(beg.add(96) as *const _);

                // `vpcmpeqb` leaves each comparison result byte as 0 or -1 (0xff).
                // This allows us to accumulate the comparisons by subtracting them.
                let mut sum = _mm256_setzero_si256();
                sum = _mm256_sub_epi8(sum, _mm256_cmpeq_epi8(v1, lf));
                sum = _mm256_sub_epi8(sum, _mm256_cmpeq_epi8(v2, lf));
                sum = _mm256_sub_epi8(sum, _mm256_cmpeq_epi8(v3, lf));
                sum = _mm256_sub_epi8(sum, _mm256_cmpeq_epi8(v4, lf));

                // Calculate the total number of matches in this chunk.
                let sum = _mm256_sad_epu8(sum, _mm256_setzero_si256());
                let sum = horizontal_sum_i64(sum);

                let line_next = line + sum as CoordType;
                if line_next >= line_stop {
                    break;
                }

                beg = beg.add(128);
                line = line_next;
            }

            while end.offset_from_unsigned(beg) >= 32 {
                let v = _mm256_loadu_si256(beg as *const _);
                let c = _mm256_cmpeq_epi8(v, lf);

                // If you ask an LLM, the best way to do this is
                // to do a `vpmovmskb` followed by `popcnt`.
                // One contemporary hardware that's a bad idea though.
                let ones = _mm256_and_si256(c, _mm256_set1_epi8(0x01));
                let sum = _mm256_sad_epu8(ones, _mm256_setzero_si256());
                let sum = horizontal_sum_i64(sum);

                let line_next = line + sum as CoordType;
                if line_next >= line_stop {
                    break;
                }

                beg = beg.add(32);
                line = line_next;
            }
        }

        lines_fwd_fallback(beg, end, line, line_stop)
    }
}

#[cfg(target_arch = "loongarch64")]
unsafe fn lines_fwd_dispatch(
    beg: *const u8,
    end: *const u8,
    line: CoordType,
    line_stop: CoordType,
) -> (*const u8, CoordType) {
    use std::arch::is_loongarch_feature_detected;

    let func = if is_loongarch_feature_detected!("lasx") {
        lines_fwd_lasx
    } else if is_loongarch_feature_detected!("lsx") {
        lines_fwd_lsx
    } else {
        lines_fwd_fallback
    };
    unsafe { LINES_FWD_DISPATCH = func };
    unsafe { func(beg, end, line, line_stop) }
}

#[cfg(target_arch = "loongarch64")]
#[target_feature(enable = "lasx")]
unsafe fn lines_fwd_lasx(
    mut beg: *const u8,
    end: *const u8,
    mut line: CoordType,
    line_stop: CoordType,
) -> (*const u8, CoordType) {
    unsafe {
        use std::arch::loongarch64::*;
        use std::mem::transmute as T;

        #[inline(always)]
        unsafe fn horizontal_sum(sum: v32i8) -> u32 {
            unsafe {
                let sum = lasx_xvhaddw_h_b(sum, sum);
                let sum = lasx_xvhaddw_w_h(sum, sum);
                let sum = lasx_xvhaddw_d_w(sum, sum);
                let sum = lasx_xvhaddw_q_d(sum, sum);
                let tmp = lasx_xvpermi_q::<1>(T(sum), T(sum));
                let sum = lasx_xvadd_w(T(sum), T(tmp));
                lasx_xvpickve2gr_wu::<0>(sum)
            }
        }

        let lf = lasx_xvrepli_b(b'\n' as i32);
        let off = beg.align_offset(32);
        if off != 0 && off < end.offset_from_unsigned(beg) {
            (beg, line) = lines_fwd_fallback(beg, beg.add(off), line, line_stop);
        }

        if line < line_stop {
            while end.offset_from_unsigned(beg) >= 128 {
                let v1 = lasx_xvld::<0>(beg as *const _);
                let v2 = lasx_xvld::<32>(beg as *const _);
                let v3 = lasx_xvld::<64>(beg as *const _);
                let v4 = lasx_xvld::<96>(beg as *const _);

                let mut sum = lasx_xvrepli_b(0);
                sum = lasx_xvsub_b(sum, lasx_xvseq_b(v1, lf));
                sum = lasx_xvsub_b(sum, lasx_xvseq_b(v2, lf));
                sum = lasx_xvsub_b(sum, lasx_xvseq_b(v3, lf));
                sum = lasx_xvsub_b(sum, lasx_xvseq_b(v4, lf));
                let sum = horizontal_sum(sum);

                let line_next = line + sum as CoordType;
                if line_next >= line_stop {
                    break;
                }

                beg = beg.add(128);
                line = line_next;
            }

            while end.offset_from_unsigned(beg) >= 32 {
                let v = lasx_xvld::<0>(beg as *const _);
                let c = lasx_xvseq_b(v, lf);

                let ones = lasx_xvand_v(T(c), T(lasx_xvrepli_b(1)));
                let sum = horizontal_sum(T(ones));

                let line_next = line + sum as CoordType;
                if line_next >= line_stop {
                    break;
                }

                beg = beg.add(32);
                line = line_next;
            }
        }

        lines_fwd_fallback(beg, end, line, line_stop)
    }
}

#[cfg(target_arch = "loongarch64")]
#[target_feature(enable = "lsx")]
unsafe fn lines_fwd_lsx(
    mut beg: *const u8,
    end: *const u8,
    mut line: CoordType,
    line_stop: CoordType,
) -> (*const u8, CoordType) {
    unsafe {
        use std::arch::loongarch64::*;
        use std::mem::transmute as T;

        #[inline(always)]
        unsafe fn horizontal_sum(sum: v16i8) -> u32 {
            unsafe {
                let sum = lsx_vhaddw_h_b(sum, sum);
                let sum = lsx_vhaddw_w_h(sum, sum);
                let sum = lsx_vhaddw_d_w(sum, sum);
                let sum = lsx_vhaddw_q_d(sum, sum);
                lsx_vpickve2gr_wu::<0>(T(sum))
            }
        }

        let lf = lsx_vrepli_b(b'\n' as i32);
        let off = beg.align_offset(16);
        if off != 0 && off < end.offset_from_unsigned(beg) {
            (beg, line) = lines_fwd_fallback(beg, beg.add(off), line, line_stop);
        }

        if line < line_stop {
            while end.offset_from_unsigned(beg) >= 64 {
                let v1 = lsx_vld::<0>(beg as *const _);
                let v2 = lsx_vld::<16>(beg as *const _);
                let v3 = lsx_vld::<32>(beg as *const _);
                let v4 = lsx_vld::<48>(beg as *const _);

                let mut sum = lsx_vrepli_b(0);
                sum = lsx_vsub_b(sum, lsx_vseq_b(v1, lf));
                sum = lsx_vsub_b(sum, lsx_vseq_b(v2, lf));
                sum = lsx_vsub_b(sum, lsx_vseq_b(v3, lf));
                sum = lsx_vsub_b(sum, lsx_vseq_b(v4, lf));
                let sum = horizontal_sum(sum);

                let line_next = line + sum as CoordType;
                if line_next >= line_stop {
                    break;
                }

                beg = beg.add(64);
                line = line_next;
            }

            while end.offset_from_unsigned(beg) >= 16 {
                let v = lsx_vld::<0>(beg as *const _);
                let c = lsx_vseq_b(v, lf);

                let ones = lsx_vand_v(T(c), T(lsx_vrepli_b(1)));
                let sum = horizontal_sum(T(ones));

                let line_next = line + sum as CoordType;
                if line_next >= line_stop {
                    break;
                }

                beg = beg.add(16);
                line = line_next;
            }
        }

        lines_fwd_fallback(beg, end, line, line_stop)
    }
}

#[cfg(target_arch = "aarch64")]
unsafe fn lines_fwd_neon(
    mut beg: *const u8,
    end: *const u8,
    mut line: CoordType,
    line_stop: CoordType,
) -> (*const u8, CoordType) {
    unsafe {
        use std::arch::aarch64::*;

        let lf = vdupq_n_u8(b'\n');
        let off = beg.align_offset(16);
        if off != 0 && off < end.offset_from_unsigned(beg) {
            (beg, line) = lines_fwd_fallback(beg, beg.add(off), line, line_stop);
        }

        if line < line_stop {
            while end.offset_from_unsigned(beg) >= 64 {
                let v1 = vld1q_u8(beg.add(0));
                let v2 = vld1q_u8(beg.add(16));
                let v3 = vld1q_u8(beg.add(32));
                let v4 = vld1q_u8(beg.add(48));

                // `vceqq_u8` leaves each comparison result byte as 0 or -1 (0xff).
                // This allows us to accumulate the comparisons by subtracting them.
                let mut sum = vdupq_n_u8(0);
                sum = vsubq_u8(sum, vceqq_u8(v1, lf));
                sum = vsubq_u8(sum, vceqq_u8(v2, lf));
                sum = vsubq_u8(sum, vceqq_u8(v3, lf));
                sum = vsubq_u8(sum, vceqq_u8(v4, lf));

                let sum = vaddvq_u8(sum);

                let line_next = line + sum as CoordType;
                if line_next >= line_stop {
                    break;
                }

                beg = beg.add(64);
                line = line_next;
            }

            while end.offset_from_unsigned(beg) >= 16 {
                let v = vld1q_u8(beg);
                let c = vceqq_u8(v, lf);
                let c = vandq_u8(c, vdupq_n_u8(0x01));
                let sum = vaddvq_u8(c);

                let line_next = line + sum as CoordType;
                if line_next >= line_stop {
                    break;
                }

                beg = beg.add(16);
                line = line_next;
            }
        }

        lines_fwd_fallback(beg, end, line, line_stop)
    }
}

#[cfg(test)]
mod test {
    use super::*;
    use crate::helpers::CoordType;
    use crate::simd::test::*;

    #[test]
    fn pseudo_fuzz() {
        let text = generate_random_text(1024);
        let lines = count_lines(&text);
        let mut offset_rng = make_rng();
        let mut line_rng = make_rng();
        let mut line_distance_rng = make_rng();

        for _ in 0..1000 {
            let offset = offset_rng() % (text.len() + 1);
            let line = line_rng() % 100;
            let line_stop = (line + line_distance_rng() % (lines + 1)).saturating_sub(5);

            let line = line as CoordType;
            let line_stop = line_stop as CoordType;

            let expected = reference_lines_fwd(text.as_bytes(), offset, line, line_stop);
            let actual = lines_fwd(text.as_bytes(), offset, line, line_stop);

            assert_eq!(expected, actual);
        }
    }

    fn reference_lines_fwd(
        haystack: &[u8],
        mut offset: usize,
        mut line: CoordType,
        line_stop: CoordType,
    ) -> (usize, CoordType) {
        if line < line_stop {
            while offset < haystack.len() {
                let c = haystack[offset];
                offset += 1;
                if c == b'\n' {
                    line += 1;
                    if line == line_stop {
                        break;
                    }
                }
            }
        }
        (offset, line)
    }
}

use std::ops::Range;

use crate::{document::ReadableDocument, simd::memchr2};

/// Cache a line/offset pair every CACHE_EVERY lines to speed up line/offset calculations
const CACHE_EVERY: usize = 1024 * 64;

#[derive(Clone)]
pub struct CachePoint {
    pub index: usize,
    pub line: usize,
    // pub snapshot: ParserSnapshot
}

pub struct LineCache {
    cache: Vec<CachePoint>,
}

impl LineCache {
    pub fn new() -> Self {
        Self { cache: vec![] }
    }

    pub fn from_document<T: ReadableDocument>(&mut self, document: &T) {
        self.cache.clear();

        let mut offset = 0;
        let mut line = 0;
        loop {
            let text = document.read_forward(offset);
            if text.is_empty() { return; }

            let mut off = 0;
            loop {
                off = memchr2(b'\n', b'\n', text, off);
                if off == text.len() { break; }

                if line % CACHE_EVERY == 0 {
                    self.cache.push(CachePoint { index: offset+off, line });
                }
                line += 1;
                off += 1;
            }

            offset += text.len();
        }
    }

    /// Updates the cache after a deletion.
    /// `range` is the deleted byte range, and `text` is the content that was deleted.
    pub fn delete(&mut self, range: Range<usize>, text: &Vec<u8>) {
        let mut newlines = 0;
        for c in text {
            if *c == b'\n' {
                newlines += 1;
            }
        }

        let mut beg_del = None;
        let mut end_del = None;
        for (i, point) in self.cache.iter_mut().enumerate() {
            if point.index >= range.start {
                if point.index < range.end {
                    // cache point is within the deleted range
                    if beg_del.is_none() { beg_del = Some(i); }
                    end_del = Some(i + 1);
                }
                else {
                    point.index -= text.len();
                    point.line -= newlines;
                }
            }
        }

        if let (Some(beg), Some(end)) = (beg_del, end_del) {
            self.cache.drain(beg..end);
        }
    }

    /// Updates the cache after an insertion.
    /// `offset` is where the insertion occurs, and `text` is the inserted content.
    pub fn insert(&mut self, offset: usize, text: &[u8]) {
        // Count how many newlines were inserted
        let mut newlines = 0;
        for c in text {
            if *c == b'\n' {
                newlines += 1;
            }
        }

        let len = text.len();
        for point in &mut self.cache {
            if point.index > offset {
                point.index += len;
                point.line += newlines;
            }
        }

        // TODO: This also needs to insert new cache points
    }

    /// Finds the nearest cached line-offset pair relative to a target line.
    /// If `reverse` is false, it returns the closest *before* the target.
    /// If `reverse` is true, it returns the closest *after or at* the target.
    pub fn nearest_offset(&self, target_count: usize, reverse: bool) -> Option<CachePoint> {
        match self.cache.binary_search_by_key(&target_count, |p| p.line) {
            Ok(i) => Some(self.cache[i].clone()),
            Err(i) => {
                if i == 0 || i == self.cache.len() { None }  // target < lowest cache point || target > highest cache point
                else {
                    Some(self.cache[ if reverse {i} else {i-1} ].clone())
                }
            }
        }
    }
}

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use edit::arena::scratch_arena;
use edit::framebuffer::{Attributes, IndexedColor};
use edit::fuzzy::score_fuzzy;
use edit::helpers::*;
use edit::input::vk;
use edit::tui::*;
use edit::{arena_format, icu};

use crate::localization::*;
use crate::state::*;

pub fn draw_statusbar(ctx: &mut Context, state: &mut State) {
    ctx.table_begin("statusbar");
    ctx.attr_focus_well();
    ctx.attr_background_rgba(state.menubar_color_bg);
    ctx.attr_foreground_rgba(state.menubar_color_fg);
    ctx.table_set_cell_gap(Size { width: 2, height: 0 });
    ctx.attr_intrinsic_size(Size { width: COORD_TYPE_SAFE_MAX, height: 1 });
    ctx.attr_padding(Rect::two(0, 1));

    if let Some(doc) = state.documents.active() {
        let mut tb = doc.buffer.borrow_mut();

        ctx.table_next_row();

        if ctx.button("newline", if tb.is_crlf() { "CRLF" } else { "LF" }, ButtonStyle::default()) {
            let is_crlf = tb.is_crlf();
            tb.normalize_newlines(!is_crlf);
        }
        if state.wants_statusbar_focus {
            state.wants_statusbar_focus = false;
            ctx.steal_focus();
        }

        state.wants_encoding_picker |=
            ctx.button("encoding", tb.encoding(), ButtonStyle::default());
        if state.wants_encoding_picker {
            if doc.path.is_some() {
                ctx.block_begin("frame");
                ctx.attr_float(FloatSpec {
                    anchor: Anchor::Last,
                    gravity_x: 0.0,
                    gravity_y: 1.0,
                    offset_x: 0.0,
                    offset_y: 0.0,
                });
                ctx.attr_padding(Rect::two(0, 1));
                ctx.attr_border();
                {
                    if ctx.button("reopen", loc(LocId::EncodingReopen), ButtonStyle::default()) {
                        state.wants_encoding_change = StateEncodingChange::Reopen;
                    }
                    ctx.focus_on_first_present();
                    if ctx.button("convert", loc(LocId::EncodingConvert), ButtonStyle::default()) {
                        state.wants_encoding_change = StateEncodingChange::Convert;
                    }
                }
                ctx.block_end();
            } else {
                // Can't reopen a file that doesn't exist.
                state.wants_encoding_change = StateEncodingChange::Convert;
            }

            if !ctx.contains_focus() {
                state.wants_encoding_picker = false;
                ctx.needs_rerender();
            }
        }

        state.wants_indentation_picker |= ctx.button(
            "indentation",
            &arena_format!(
                ctx.arena(),
                "{}:{}",
                loc(if tb.indent_with_tabs() {
                    LocId::IndentationTabs
                } else {
                    LocId::IndentationSpaces
                }),
                tb.tab_size(),
            ),
            ButtonStyle::default(),
        );
        if state.wants_indentation_picker {
            ctx.table_begin("indentation-picker");
            ctx.attr_float(FloatSpec {
                anchor: Anchor::Last,
                gravity_x: 0.0,
                gravity_y: 1.0,
                offset_x: 0.0,
                offset_y: 0.0,
            });
            ctx.attr_border();
            ctx.attr_padding(Rect::two(0, 1));
            ctx.table_set_cell_gap(Size { width: 1, height: 0 });
            {
                if ctx.contains_focus() && ctx.consume_shortcut(vk::RETURN) {
                    ctx.toss_focus_up();
                }

                ctx.table_next_row();

                ctx.list_begin("type");
                ctx.focus_on_first_present();
                ctx.attr_padding(Rect::two(0, 1));
                {
                    if ctx.list_item(tb.indent_with_tabs(), loc(LocId::IndentationTabs))
                        != ListSelection::Unchanged
                    {
                        tb.set_indent_with_tabs(true);
                        ctx.needs_rerender();
                    }
                    if ctx.list_item(!tb.indent_with_tabs(), loc(LocId::IndentationSpaces))
                        != ListSelection::Unchanged
                    {
                        tb.set_indent_with_tabs(false);
                        ctx.needs_rerender();
                    }
                }
                ctx.list_end();

                ctx.list_begin("width");
                ctx.attr_padding(Rect::two(0, 2));
                {
                    for width in 1u8..=8 {
                        let ch = [b'0' + width];
                        let label = unsafe { std::str::from_utf8_unchecked(&ch) };

                        if ctx.list_item(tb.tab_size() == width as CoordType, label)
                            != ListSelection::Unchanged
                        {
                            tb.set_tab_size(width as CoordType);
                            ctx.needs_rerender();
                        }
                    }
                }
                ctx.list_end();
            }
            ctx.table_end();

            if !ctx.contains_focus() {
                state.wants_indentation_picker = false;
                ctx.needs_rerender();
            }
        }

        ctx.label(
            "location",
            &arena_format!(
                ctx.arena(),
                "{}:{}",
                tb.cursor_logical_pos().y + 1,
                tb.cursor_logical_pos().x + 1
            ),
        );

        #[cfg(feature = "debug-latency")]
        ctx.label(
            "stats",
            &arena_format!(ctx.arena(), "{}/{}", tb.logical_line_count(), tb.visual_line_count(),),
        );

        if tb.is_overtype() && ctx.button("overtype", "OVR", ButtonStyle::default()) {
            tb.set_overtype(false);
            ctx.needs_rerender();
        }

        if tb.is_dirty() {
            ctx.label("dirty", "*");
        }

        ctx.block_begin("filename-container");
        ctx.attr_intrinsic_size(Size { width: COORD_TYPE_SAFE_MAX, height: 1 });
        {
            let total = state.documents.len();
            let mut filename = doc.filename.as_str();
            let filename_buf;

            if total > 1 {
                filename_buf = arena_format!(ctx.arena(), "{} + {}", filename, total - 1);
                filename = &filename_buf;
            }

            state.wants_go_to_file |= ctx.button("filename", filename, ButtonStyle::default());
            ctx.inherit_focus();
            ctx.attr_overflow(Overflow::TruncateMiddle);
            ctx.attr_position(Position::Right);
        }
        ctx.block_end();
    } else {
        state.wants_statusbar_focus = false;
        state.wants_encoding_picker = false;
        state.wants_indentation_picker = false;
    }

    ctx.table_end();
}

pub fn draw_dialog_encoding_change(ctx: &mut Context, state: &mut State) {
    let encoding = state.documents.active_mut().map_or("", |doc| doc.buffer.borrow().encoding());
    let reopen = state.wants_encoding_change == StateEncodingChange::Reopen;
    let width = (ctx.size().width - 20).max(10);
    let height = (ctx.size().height - 10).max(10);
    let mut change = None;
    let mut done = encoding.is_empty();

    ctx.modal_begin(
        "encode",
        if reopen { loc(LocId::EncodingReopen) } else { loc(LocId::EncodingConvert) },
    );
    {
        ctx.table_begin("encoding-search");
        ctx.table_set_columns(&[0, COORD_TYPE_SAFE_MAX]);
        ctx.table_set_cell_gap(Size { width: 1, height: 0 });
        ctx.inherit_focus();
        {
            ctx.table_next_row();
            ctx.inherit_focus();

            ctx.label("needle-label", loc(LocId::SearchNeedleLabel));

            if ctx.editline("needle", &mut state.encoding_picker_needle) {
                encoding_picker_update_list(state);
            }
            ctx.inherit_focus();
        }
        ctx.table_end();

        ctx.scrollarea_begin("scrollarea", Size { width, height });
        ctx.attr_background_rgba(ctx.indexed_alpha(IndexedColor::Black, 1, 4));
        {
            ctx.list_begin("encodings");
            ctx.inherit_focus();

            for enc in state
                .encoding_picker_results
                .as_deref()
                .unwrap_or_else(|| icu::get_available_encodings().preferred)
            {
                if ctx.list_item(enc.canonical == encoding, enc.label) == ListSelection::Activated {
                    change = Some(enc.canonical);
                    break;
                }
                ctx.attr_overflow(Overflow::TruncateTail);
            }
            ctx.list_end();
        }
        ctx.scrollarea_end();
    }
    done |= ctx.modal_end();
    done |= change.is_some();

    if let Some(encoding) = change
        && let Some(doc) = state.documents.active_mut()
    {
        if reopen && doc.path.is_some() {
            let mut res = Ok(());
            if doc.buffer.borrow().is_dirty() {
                res = doc.save(None);
            }
            if res.is_ok() {
                res = doc.reread(Some(encoding));
            }
            if let Err(err) = res {
                error_log_add(ctx, state, err);
            }
        } else {
            doc.buffer.borrow_mut().set_encoding(encoding);
        }
    }

    if done {
        state.wants_encoding_change = StateEncodingChange::None;
        state.encoding_picker_needle.clear();
        state.encoding_picker_results = None;
        ctx.needs_rerender();
    }
}

fn encoding_picker_update_list(state: &mut State) {
    state.encoding_picker_results = None;

    let needle = state.encoding_picker_needle.trim_ascii();
    if needle.is_empty() {
        return;
    }

    let encodings = icu::get_available_encodings();
    let scratch = scratch_arena(None);
    let mut matches = Vec::new_in(&*scratch);

    for enc in encodings.all {
        let local_scratch = scratch_arena(Some(&scratch));
        let (score, _) = score_fuzzy(&local_scratch, enc.label, needle, true);

        if score > 0 {
            matches.push((score, *enc));
        }
    }

    matches.sort_by(|a, b| b.0.cmp(&a.0));
    state.encoding_picker_results = Some(Vec::from_iter(matches.iter().map(|(_, enc)| *enc)));
}

pub fn draw_go_to_file(ctx: &mut Context, state: &mut State) {
    ctx.modal_begin("go-to-file", loc(LocId::ViewGoToFile));
    {
        let width = (ctx.size().width - 20).max(10);
        let height = (ctx.size().height - 10).max(10);

        ctx.scrollarea_begin("scrollarea", Size { width, height });
        ctx.attr_background_rgba(ctx.indexed_alpha(IndexedColor::Black, 1, 4));
        ctx.inherit_focus();
        {
            ctx.list_begin("documents");
            ctx.inherit_focus();

            if state.documents.update_active(|doc| {
                let tb = doc.buffer.borrow();

                ctx.styled_list_item_begin();
                ctx.attr_overflow(Overflow::TruncateTail);
                ctx.styled_label_add_text(if tb.is_dirty() { "* " } else { "  " });
                ctx.styled_label_add_text(&doc.filename);

                if let Some(path) = &doc.dir {
                    ctx.styled_label_add_text("   ");
                    ctx.styled_label_set_attributes(Attributes::Italic);
                    ctx.styled_label_add_text(path.as_str());
                }

                ctx.styled_list_item_end(false) == ListSelection::Activated
            }) {
                state.wants_go_to_file = false;
                ctx.needs_rerender();
            }

            ctx.list_end();
        }
        ctx.scrollarea_end();
    }
    if ctx.modal_end() {
        state.wants_go_to_file = false;
    }
}

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use std::ops::Range;

use crate::document::ReadableDocument;

#[derive(Clone, Copy, PartialEq, Eq)]
enum CharClass {
    Whitespace,
    Newline,
    Separator,
    Word,
}

const fn construct_classifier(separators: &[u8]) -> [CharClass; 256] {
    let mut classifier = [CharClass::Word; 256];

    classifier[b' ' as usize] = CharClass::Whitespace;
    classifier[b'\t' as usize] = CharClass::Whitespace;
    classifier[b'\n' as usize] = CharClass::Newline;
    classifier[b'\r' as usize] = CharClass::Newline;

    let mut i = 0;
    let len = separators.len();
    while i < len {
        let ch = separators[i];
        assert!(ch < 128, "Only ASCII separators are supported.");
        classifier[ch as usize] = CharClass::Separator;
        i += 1;
    }

    classifier
}

const WORD_CLASSIFIER: [CharClass; 256] =
    construct_classifier(br#"`~!@#$%^&*()-=+[{]}\|;:'",.<>/?"#);

/// Finds the next word boundary given a document cursor offset.
/// Returns the offset of the next word boundary.
pub fn word_forward(doc: &dyn ReadableDocument, offset: usize) -> usize {
    word_navigation(WordForward { doc, offset, chunk: &[], chunk_off: 0 })
}

/// The backward version of `word_forward`.
pub fn word_backward(doc: &dyn ReadableDocument, offset: usize) -> usize {
    word_navigation(WordBackward { doc, offset, chunk: &[], chunk_off: 0 })
}

/// Word navigation implementation. Matches the behavior of VS Code.
fn word_navigation<T: WordNavigation>(mut nav: T) -> usize {
    // First, fill `self.chunk` with at least 1 grapheme.
    nav.read();

    // Skip one newline, if any.
    nav.skip_newline();

    // Skip any whitespace.
    nav.skip_class(CharClass::Whitespace);

    // Skip one word or separator and take note of the class.
    let class = nav.peek(CharClass::Whitespace);
    if matches!(class, CharClass::Separator | CharClass::Word) {
        nav.next();

        let off = nav.offset();

        // Continue skipping the same class.
        nav.skip_class(class);

        // If the class was a separator and we only moved one character,
        // continue skipping characters of the word class.
        if off == nav.offset() && class == CharClass::Separator {
            nav.skip_class(CharClass::Word);
        }
    }

    nav.offset()
}

trait WordNavigation {
    fn read(&mut self);
    fn skip_newline(&mut self);
    fn skip_class(&mut self, class: CharClass);
    fn peek(&self, default: CharClass) -> CharClass;
    fn next(&mut self);
    fn offset(&self) -> usize;
}

struct WordForward<'a> {
    doc: &'a dyn ReadableDocument,
    offset: usize,
    chunk: &'a [u8],
    chunk_off: usize,
}

impl WordNavigation for WordForward<'_> {
    fn read(&mut self) {
        self.chunk = self.doc.read_forward(self.offset);
        self.chunk_off = 0;
    }

    fn skip_newline(&mut self) {
        // We can rely on the fact that the document does not split graphemes across chunks.
        // = If there's a newline it's wholly contained in this chunk.
        // Unlike with `WordBackward`, we can't check for CR and LF separately as only a CR followed
        // by a LF is a newline. A lone CR in the document is just a regular control character.
        self.chunk_off += match self.chunk.get(self.chunk_off) {
            Some(&b'\n') => 1,
            Some(&b'\r') if self.chunk.get(self.chunk_off + 1) == Some(&b'\n') => 2,
            _ => 0,
        }
    }

    fn skip_class(&mut self, class: CharClass) {
        while !self.chunk.is_empty() {
            while self.chunk_off < self.chunk.len() {
                if WORD_CLASSIFIER[self.chunk[self.chunk_off] as usize] != class {
                    return;
                }
                self.chunk_off += 1;
            }

            self.offset += self.chunk.len();
            self.chunk = self.doc.read_forward(self.offset);
            self.chunk_off = 0;
        }
    }

    fn peek(&self, default: CharClass) -> CharClass {
        if self.chunk_off < self.chunk.len() {
            WORD_CLASSIFIER[self.chunk[self.chunk_off] as usize]
        } else {
            default
        }
    }

    fn next(&mut self) {
        self.chunk_off += 1;
    }

    fn offset(&self) -> usize {
        self.offset + self.chunk_off
    }
}

struct WordBackward<'a> {
    doc: &'a dyn ReadableDocument,
    offset: usize,
    chunk: &'a [u8],
    chunk_off: usize,
}

impl WordNavigation for WordBackward<'_> {
    fn read(&mut self) {
        self.chunk = self.doc.read_backward(self.offset);
        self.chunk_off = self.chunk.len();
    }

    fn skip_newline(&mut self) {
        // We can rely on the fact that the document does not split graphemes across chunks.
        // = If there's a newline it's wholly contained in this chunk.
        if self.chunk_off > 0 && self.chunk[self.chunk_off - 1] == b'\n' {
            self.chunk_off -= 1;
        }
        if self.chunk_off > 0 && self.chunk[self.chunk_off - 1] == b'\r' {
            self.chunk_off -= 1;
        }
    }

    fn skip_class(&mut self, class: CharClass) {
        while !self.chunk.is_empty() {
            while self.chunk_off > 0 {
                if WORD_CLASSIFIER[self.chunk[self.chunk_off - 1] as usize] != class {
                    return;
                }
                self.chunk_off -= 1;
            }

            self.offset -= self.chunk.len();
            self.chunk = self.doc.read_backward(self.offset);
            self.chunk_off = self.chunk.len();
        }
    }

    fn peek(&self, default: CharClass) -> CharClass {
        if self.chunk_off > 0 {
            WORD_CLASSIFIER[self.chunk[self.chunk_off - 1] as usize]
        } else {
            default
        }
    }

    fn next(&mut self) {
        self.chunk_off -= 1;
    }

    fn offset(&self) -> usize {
        self.offset - self.chunk.len() + self.chunk_off
    }
}

/// Returns the offset range of the "word" at the given offset.
/// Does not cross newlines. Works similar to VS Code.
pub fn word_select(doc: &dyn ReadableDocument, offset: usize) -> Range<usize> {
    let mut beg = offset;
    let mut end = offset;
    let mut class = CharClass::Newline;

    let mut chunk = doc.read_forward(end);
    if !chunk.is_empty() {
        // Not at the end of the document? Great!
        // We default to using the next char as the class, because in terminals
        // the cursor is usually always to the left of the cell you clicked on.
        class = WORD_CLASSIFIER[chunk[0] as usize];

        let mut chunk_off = 0;

        // Select the word, unless we hit a newline.
        if class != CharClass::Newline {
            loop {
                chunk_off += 1;
                end += 1;

                if chunk_off >= chunk.len() {
                    chunk = doc.read_forward(end);
                    chunk_off = 0;
                    if chunk.is_empty() {
                        break;
                    }
                }

                if WORD_CLASSIFIER[chunk[chunk_off] as usize] != class {
                    break;
                }
            }
        }
    }

    let mut chunk = doc.read_backward(beg);
    if !chunk.is_empty() {
        let mut chunk_off = chunk.len();

        // If we failed to determine the class, because we hit the end of the document
        // or a newline, we fall back to using the previous character, of course.
        if class == CharClass::Newline {
            class = WORD_CLASSIFIER[chunk[chunk_off - 1] as usize];
        }

        // Select the word, unless we hit a newline.
        if class != CharClass::Newline {
            loop {
                if WORD_CLASSIFIER[chunk[chunk_off - 1] as usize] != class {
                    break;
                }

                chunk_off -= 1;
                beg -= 1;

                if chunk_off == 0 {
                    chunk = doc.read_backward(beg);
                    chunk_off = chunk.len();
                    if chunk.is_empty() {
                        break;
                    }
                }
            }
        }
    }

    beg..end
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn test_word_navigation() {
        assert_eq!(word_forward(&"Hello World".as_bytes(), 0), 5);
        assert_eq!(word_forward(&"Hello,World".as_bytes(), 0), 5);
        assert_eq!(word_forward(&"   Hello".as_bytes(), 0), 8);
        assert_eq!(word_forward(&"\n\nHello".as_bytes(), 0), 1);

        assert_eq!(word_backward(&"Hello World".as_bytes(), 11), 6);
        assert_eq!(word_backward(&"Hello,World".as_bytes(), 10), 6);
        assert_eq!(word_backward(&"Hello   ".as_bytes(), 7), 0);
        assert_eq!(word_backward(&"Hello\n\n".as_bytes(), 7), 6);
    }
}

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

#![allow(clippy::missing_safety_doc, clippy::mut_from_ref)]

use std::alloc::{AllocError, Allocator, Layout};
use std::mem::{self, MaybeUninit};
use std::ptr::NonNull;

use super::release;
use crate::apperr;

/// A debug wrapper for [`release::Arena`].
///
/// The problem with [`super::ScratchArena`] is that it only "borrows" an underlying
/// [`release::Arena`]. Once the [`super::ScratchArena`] is dropped it resets the watermark
/// of the underlying [`release::Arena`], freeing all allocations done since borrowing it.
///
/// It is completely valid for the same [`release::Arena`] to be borrowed multiple times at once,
/// *as long as* you only use the most recent borrow. Bad example:
/// ```should_panic
/// use edit::arena::scratch_arena;
///
/// let mut scratch1 = scratch_arena(None);
/// let mut scratch2 = scratch_arena(None);
///
/// let foo = scratch1.alloc_uninit::<usize>();
///
/// // This will also reset `scratch1`'s allocation.
/// drop(scratch2);
///
/// *foo; // BOOM! ...if it wasn't for our debug wrapper.
/// ```
///
/// To avoid this, this wraps the real [`release::Arena`] in a "debug" one, which pretends as if every
/// instance of itself is a distinct [`release::Arena`] instance. Then we use this "debug" [`release::Arena`]
/// for [`super::ScratchArena`] which allows us to track which borrow is the most recent one.
pub enum Arena {
    // Delegate is 'static, because release::Arena requires no lifetime
    // annotations, and so this mere debug helper cannot use them either.
    Delegated { delegate: &'static release::Arena, borrow: usize },
    Owned { arena: release::Arena },
}

impl Drop for Arena {
    fn drop(&mut self) {
        if let Self::Delegated { delegate, borrow } = self {
            let borrows = delegate.borrows.get();
            assert_eq!(*borrow, borrows);
            delegate.borrows.set(borrows - 1);
        }
    }
}

impl Default for Arena {
    fn default() -> Self {
        Self::empty()
    }
}

impl Arena {
    pub const fn empty() -> Self {
        Self::Owned { arena: release::Arena::empty() }
    }

    pub fn new(capacity: usize) -> apperr::Result<Self> {
        Ok(Self::Owned { arena: release::Arena::new(capacity)? })
    }

    pub(super) fn delegated(delegate: &release::Arena) -> Self {
        let borrow = delegate.borrows.get() + 1;
        delegate.borrows.set(borrow);
        Self::Delegated { delegate: unsafe { mem::transmute(delegate) }, borrow }
    }

    #[inline]
    pub(super) fn delegate_target(&self) -> &release::Arena {
        match *self {
            Self::Delegated { delegate, borrow } => {
                assert!(
                    borrow == delegate.borrows.get(),
                    "Arena already borrowed by a newer ScratchArena"
                );
                delegate
            }
            Self::Owned { ref arena } => arena,
        }
    }

    #[inline]
    pub(super) fn delegate_target_unchecked(&self) -> &release::Arena {
        match self {
            Self::Delegated { delegate, .. } => delegate,
            Self::Owned { arena } => arena,
        }
    }

    pub fn offset(&self) -> usize {
        self.delegate_target().offset()
    }

    pub unsafe fn reset(&self, to: usize) {
        unsafe { self.delegate_target().reset(to) }
    }

    pub fn alloc_uninit<T>(&self) -> &mut MaybeUninit<T> {
        self.delegate_target().alloc_uninit()
    }

    pub fn alloc_uninit_slice<T>(&self, count: usize) -> &mut [MaybeUninit<T>] {
        self.delegate_target().alloc_uninit_slice(count)
    }
}

unsafe impl Allocator for Arena {
    fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
        self.delegate_target().alloc_raw(layout.size(), layout.align())
    }

    fn allocate_zeroed(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError> {
        self.delegate_target().allocate_zeroed(layout)
    }

    // While it is possible to shrink the tail end of the arena, it is
    // not very useful given the existence of scoped scratch arenas.
    unsafe fn deallocate(&self, ptr: NonNull<u8>, layout: Layout) {
        unsafe { self.delegate_target().deallocate(ptr, layout) }
    }

    unsafe fn grow(
        &self,
        ptr: NonNull<u8>,
        old_layout: Layout,
        new_layout: Layout,
    ) -> Result<NonNull<[u8]>, AllocError> {
        unsafe { self.delegate_target().grow(ptr, old_layout, new_layout) }
    }

    unsafe fn grow_zeroed(
        &self,
        ptr: NonNull<u8>,
        old_layout: Layout,
        new_layout: Layout,
    ) -> Result<NonNull<[u8]>, AllocError> {
        unsafe { self.delegate_target().grow_zeroed(ptr, old_layout, new_layout) }
    }

    unsafe fn shrink(
        &self,
        ptr: NonNull<u8>,
        old_layout: Layout,
        new_layout: Layout,
    ) -> Result<NonNull<[u8]>, AllocError> {
        unsafe { self.delegate_target().shrink(ptr, old_layout, new_layout) }
    }
}

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use std::ops::Deref;

#[cfg(debug_assertions)]
use super::debug;
use super::{Arena, release};
use crate::apperr;
use crate::helpers::*;

static mut S_SCRATCH: [release::Arena; 2] =
    const { [release::Arena::empty(), release::Arena::empty()] };

/// Initialize the scratch arenas with a given capacity.
/// Call this before using [`scratch_arena`].
pub fn init(capacity: usize) -> apperr::Result<()> {
    unsafe {
        for s in &mut S_SCRATCH[..] {
            *s = release::Arena::new(capacity)?;
        }
    }
    Ok(())
}

/// Need an arena for temporary allocations? [`scratch_arena`] got you covered.
/// Call [`scratch_arena`] and it'll return an [`Arena`] that resets when it goes out of scope.
///
/// ---
///
/// Most methods make just two kinds of allocations:
/// * Interior: Temporary data that can be deallocated when the function returns.
/// * Exterior: Data that is returned to the caller and must remain alive until the caller stops using it.
///
/// Such methods only have two lifetimes, for which you consequently also only need two arenas.
/// ...even if your method calls other methods recursively! This is because the exterior allocations
/// of a callee are simply interior allocations to the caller, and so on, recursively.
///
/// This works as long as the two arenas flip/flop between being used as interior/exterior allocator
/// along the callstack. To ensure that is the case, we use a recursion counter in debug builds.
///
/// This approach was described among others at: <https://nullprogram.com/blog/2023/09/27/>
///
/// # Safety
///
/// If your function takes an [`Arena`] argument, you **MUST** pass it to `scratch_arena` as `Some(&arena)`.
pub fn scratch_arena(conflict: Option<&Arena>) -> ScratchArena<'static> {
    unsafe {
        #[cfg(test)]
        if S_SCRATCH[0].is_empty() {
            init(128 * 1024 * 1024).unwrap();
        }

        #[cfg(debug_assertions)]
        let conflict = conflict.map(|a| a.delegate_target_unchecked());

        let index = opt_ptr_eq(conflict, Some(&S_SCRATCH[0])) as usize;
        let arena = &S_SCRATCH[index];
        ScratchArena::new(arena)
    }
}

/// Borrows an [`Arena`] for temporary allocations.
///
/// See [`scratch_arena`].
#[cfg(debug_assertions)]
pub struct ScratchArena<'a> {
    arena: debug::Arena,
    offset: usize,
    _phantom: std::marker::PhantomData<&'a ()>,
}

#[cfg(not(debug_assertions))]
pub struct ScratchArena<'a> {
    arena: &'a Arena,
    offset: usize,
}

#[cfg(debug_assertions)]
impl<'a> ScratchArena<'a> {
    fn new(arena: &'a release::Arena) -> Self {
        let offset = arena.offset();
        ScratchArena { arena: Arena::delegated(arena), _phantom: std::marker::PhantomData, offset }
    }
}

#[cfg(not(debug_assertions))]
impl<'a> ScratchArena<'a> {
    fn new(arena: &'a release::Arena) -> Self {
        let offset = arena.offset();
        ScratchArena { arena, offset }
    }
}

impl Drop for ScratchArena<'_> {
    fn drop(&mut self) {
        unsafe { self.arena.reset(self.offset) };
    }
}

#[cfg(debug_assertions)]
impl Deref for ScratchArena<'_> {
    type Target = debug::Arena;

    fn deref(&self) -> &Self::Target {
        &self.arena
    }
}

#[cfg(not(debug_assertions))]
impl Deref for ScratchArena<'_> {
    type Target = Arena;

    fn deref(&self) -> &Self::Target {
        self.arena
    }
}

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

//! Abstractions over reading/writing arbitrary text containers.

use std::ffi::OsString;
use std::mem;
use std::ops::Range;
use std::path::PathBuf;

use crate::arena::{ArenaString, scratch_arena};
use crate::helpers::ReplaceRange as _;

/// An abstraction over reading from text containers.
pub trait ReadableDocument {
    /// Read some bytes starting at (including) the given absolute offset.
    ///
    /// # Warning
    ///
    /// * Be lenient on inputs:
    ///   * The given offset may be out of bounds and you MUST clamp it.
    ///   * You should not assume that offsets are at grapheme cluster boundaries.
    /// * Be strict on outputs:
    ///   * You MUST NOT break grapheme clusters across chunks.
    ///   * You MUST NOT return an empty slice unless the offset is at or beyond the end.
    fn read_forward(&self, off: usize) -> &[u8];

    /// Read some bytes before (but not including) the given absolute offset.
    ///
    /// # Warning
    ///
    /// * Be lenient on inputs:
    ///   * The given offset may be out of bounds and you MUST clamp it.
    ///   * You should not assume that offsets are at grapheme cluster boundaries.
    /// * Be strict on outputs:
    ///   * You MUST NOT break grapheme clusters across chunks.
    ///   * You MUST NOT return an empty slice unless the offset is zero.
    fn read_backward(&self, off: usize) -> &[u8];
}

/// An abstraction over writing to text containers.
pub trait WriteableDocument: ReadableDocument {
    /// Replace the given range with the given bytes.
    ///
    /// # Warning
    ///
    /// * The given range may be out of bounds and you MUST clamp it.
    /// * The replacement may not be valid UTF8.
    fn replace(&mut self, range: Range<usize>, replacement: &[u8]);
}

impl ReadableDocument for &[u8] {
    fn read_forward(&self, off: usize) -> &[u8] {
        let s = *self;
        &s[off.min(s.len())..]
    }

    fn read_backward(&self, off: usize) -> &[u8] {
        let s = *self;
        &s[..off.min(s.len())]
    }
}

impl ReadableDocument for String {
    fn read_forward(&self, off: usize) -> &[u8] {
        let s = self.as_bytes();
        &s[off.min(s.len())..]
    }

    fn read_backward(&self, off: usize) -> &[u8] {
        let s = self.as_bytes();
        &s[..off.min(s.len())]
    }
}

impl WriteableDocument for String {
    fn replace(&mut self, range: Range<usize>, replacement: &[u8]) {
        // `replacement` is not guaranteed to be valid UTF-8, so we need to sanitize it.
        let scratch = scratch_arena(None);
        let utf8 = ArenaString::from_utf8_lossy(&scratch, replacement);
        let src = match &utf8 {
            Ok(s) => s,
            Err(s) => s.as_str(),
        };

        // SAFETY: `range` is guaranteed to be on codepoint boundaries.
        unsafe { self.as_mut_vec() }.replace_range(range, src.as_bytes());
    }
}

impl ReadableDocument for PathBuf {
    fn read_forward(&self, off: usize) -> &[u8] {
        let s = self.as_os_str().as_encoded_bytes();
        &s[off.min(s.len())..]
    }

    fn read_backward(&self, off: usize) -> &[u8] {
        let s = self.as_os_str().as_encoded_bytes();
        &s[..off.min(s.len())]
    }
}

impl WriteableDocument for PathBuf {
    fn replace(&mut self, range: Range<usize>, replacement: &[u8]) {
        let mut vec = mem::take(self).into_os_string().into_encoded_bytes();
        vec.replace_range(range, replacement);
        *self = unsafe { Self::from(OsString::from_encoded_bytes_unchecked(vec)) };
    }
}

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use edit::arena::scratch_arena;
use edit::helpers::AsciiStringHelpers;
use edit::sys;

#[derive(Clone, Copy, PartialEq, Eq)]
pub enum LocId {
    Ctrl,
    Alt,
    Shift,

    Ok,
    Yes,
    No,
    Cancel,
    Always,

    // File menu
    File,
    FileNew,
    FileOpen,
    FileSave,
    FileSaveAs,
    FileClose,
    FileExit,
    FileGoto,

    // Edit menu
    Edit,
    EditUndo,
    EditRedo,
    EditCut,
    EditCopy,
    EditPaste,
    EditFind,
    EditReplace,
    EditSelectAll,

    // View menu
    View,
    ViewFocusStatusbar,
    ViewWordWrap,
    ViewGoToFile,

    // Help menu
    Help,
    HelpAbout,

    // Exit dialog
    UnsavedChangesDialogTitle,
    UnsavedChangesDialogDescription,
    UnsavedChangesDialogYes,
    UnsavedChangesDialogNo,

    // About dialog
    AboutDialogTitle,
    AboutDialogVersion,

    // Shown when the clipboard size exceeds the limit for OSC 52
    LargeClipboardWarningLine1,
    LargeClipboardWarningLine2,
    LargeClipboardWarningLine3,
    SuperLargeClipboardWarning,

    // Warning dialog
    WarningDialogTitle,

    // Error dialog
    ErrorDialogTitle,
    ErrorIcuMissing,

    SearchNeedleLabel,
    SearchReplacementLabel,
    SearchMatchCase,
    SearchWholeWord,
    SearchUseRegex,
    SearchReplaceAll,
    SearchClose,

    EncodingReopen,
    EncodingConvert,

    IndentationTabs,
    IndentationSpaces,

    SaveAsDialogPathLabel,
    SaveAsDialogNameLabel,

    FileOverwriteWarning,
    FileOverwriteWarningDescription,

    Count,
}

#[allow(non_camel_case_types)]
#[derive(Clone, Copy, PartialEq, Eq)]
enum LangId {
    // Base language. It's always the first one.
    en,

    // Other languages. Sorted alphabetically.
    de,
    es,
    fr,
    it,
    ja,
    ko,
    pt_br,
    ru,
    zh_hans,
    zh_hant,

    Count,
}

#[rustfmt::skip]
const S_LANG_LUT: [[&str; LangId::Count as usize]; LocId::Count as usize] = [
    // Ctrl (the keyboard key)
    [
        /* en      */ "Ctrl",
        /* de      */ "Strg",
        /* es      */ "Ctrl",
        /* fr      */ "Ctrl",
        /* it      */ "Ctrl",
        /* ja      */ "Ctrl",
        /* ko      */ "Ctrl",
        /* pt_br   */ "Ctrl",
        /* ru      */ "Ctrl",
        /* zh_hans */ "Ctrl",
        /* zh_hant */ "Ctrl",
    ],
    // Alt (the keyboard key)
    [
        /* en      */ "Alt",
        /* de      */ "Alt",
        /* es      */ "Alt",
        /* fr      */ "Alt",
        /* it      */ "Alt",
        /* ja      */ "Alt",
        /* ko      */ "Alt",
        /* pt_br   */ "Alt",
        /* ru      */ "Alt",
        /* zh_hans */ "Alt",
        /* zh_hant */ "Alt",
    ],
    // Shift (the keyboard key)
    [
        /* en      */ "Shift",
        /* de      */ "Umschalt",
        /* es      */ "Mayús",
        /* fr      */ "Maj",
        /* it      */ "Maiusc",
        /* ja      */ "Shift",
        /* ko      */ "Shift",
        /* pt_br   */ "Shift",
        /* ru      */ "Shift",
        /* zh_hans */ "Shift",
        /* zh_hant */ "Shift",
    ],

    // Ok (used as a common dialog button)
    [
        /* en      */ "Ok",
        /* de      */ "OK",
        /* es      */ "Aceptar",
        /* fr      */ "OK",
        /* it      */ "OK",
        /* ja      */ "OK",
        /* ko      */ "확인",
        /* pt_br   */ "OK",
        /* ru      */ "ОК",
        /* zh_hans */ "确定",
        /* zh_hant */ "確定",
    ],
    // Yes (used as a common dialog button)
    [
        /* en      */ "Yes",
        /* de      */ "Ja",
        /* es      */ "Sí",
        /* fr      */ "Oui",
        /* it      */ "Sì",
        /* ja      */ "はい",
        /* ko      */ "예",
        /* pt_br   */ "Sim",
        /* ru      */ "Да",
        /* zh_hans */ "是",
        /* zh_hant */ "是",
    ],
    // No (used as a common dialog button)
    [
        /* en      */ "No",
        /* de      */ "Nein",
        /* es      */ "No",
        /* fr      */ "Non",
        /* it      */ "No",
        /* ja      */ "いいえ",
        /* ko      */ "아니오",
        /* pt_br   */ "Não",
        /* ru      */ "Нет",
        /* zh_hans */ "否",
        /* zh_hant */ "否",
    ],
    // Cancel (used as a common dialog button)
    [
        /* en      */ "Cancel",
        /* de      */ "Abbrechen",
        /* es      */ "Cancelar",
        /* fr      */ "Annuler",
        /* it      */ "Annulla",
        /* ja      */ "キャンセル",
        /* ko      */ "취소",
        /* pt_br   */ "Cancelar",
        /* ru      */ "Отмена",
        /* zh_hans */ "取消",
        /* zh_hant */ "取消",
    ],
    // Always (used as a common dialog button)
    [
        /* en      */ "Always",
        /* de      */ "Immer",
        /* es      */ "Siempre",
        /* fr      */ "Toujours",
        /* it      */ "Sempre",
        /* ja      */ "常に",
        /* ko      */ "항상",
        /* pt_br   */ "Sempre",
        /* ru      */ "Всегда",
        /* zh_hans */ "总是",
        /* zh_hant */ "總是",
    ],

    // File (a menu bar item)
    [
        /* en      */ "File",
        /* de      */ "Datei",
        /* es      */ "Archivo",
        /* fr      */ "Fichier",
        /* it      */ "File",
        /* ja      */ "ファイル",
        /* ko      */ "파일",
        /* pt_br   */ "Arquivo",
        /* ru      */ "Файл",
        /* zh_hans */ "文件",
        /* zh_hant */ "檔案",
    ],
    // FileNew
    [
        /* en      */ "New File",
        /* de      */ "Neue Datei",
        /* es      */ "Nuevo archivo",
        /* fr      */ "Nouveau fichier",
        /* it      */ "Nuovo file",
        /* ja      */ "新規ファイル",
        /* ko      */ "새 파일",
        /* pt_br   */ "Novo arquivo",
        /* ru      */ "Новый файл",
        /* zh_hans */ "新建文件",
        /* zh_hant */ "新增檔案",
    ],
    // FileOpen
    [
        /* en      */ "Open File…",
        /* de      */ "Datei öffnen…",
        /* es      */ "Abrir archivo…",
        /* fr      */ "Ouvrir un fichier…",
        /* it      */ "Apri file…",
        /* ja      */ "ファイルを開く…",
        /* ko      */ "파일 열기…",
        /* pt_br   */ "Abrir arquivo…",
        /* ru      */ "Открыть файл…",
        /* zh_hans */ "打开文件…",
        /* zh_hant */ "開啟檔案…",
    ],
    // FileSave
    [
        /* en      */ "Save",
        /* de      */ "Speichern",
        /* es      */ "Guardar",
        /* fr      */ "Enregistrer",
        /* it      */ "Salva",
        /* ja      */ "保存",
        /* ko      */ "저장",
        /* pt_br   */ "Salvar",
        /* ru      */ "Сохранить",
        /* zh_hans */ "保存",
        /* zh_hant */ "儲存",
    ],
    // FileSaveAs
    [
        /* en      */ "Save As…",
        /* de      */ "Speichern unter…",
        /* es      */ "Guardar como…",
        /* fr      */ "Enregistrer sous…",
        /* it      */ "Salva come…",
        /* ja      */ "名前を付けて保存…",
        /* ko      */ "다른 이름으로 저장…",
        /* pt_br   */ "Salvar como…",
        /* ru      */ "Сохранить как…",
        /* zh_hans */ "另存为…",
        /* zh_hant */ "另存新檔…",
    ],
    // FileClose
    [
        /* en      */ "Close File",
        /* de      */ "Datei schließen",
        /* es      */ "Cerrar archivo",
        /* fr      */ "Fermer le fichier",
        /* it      */ "Chiudi file",
        /* ja      */ "ファイルを閉じる",
        /* ko      */ "파일 닫기",
        /* pt_br   */ "Fechar arquivo",
        /* ru      */ "Закрыть файл",
        /* zh_hans */ "关闭文件",
        /* zh_hant */ "關閉檔案",
    ],
    // FileExit
    [
        /* en      */ "Exit",
        /* de      */ "Beenden",
        /* es      */ "Salir",
        /* fr      */ "Quitter",
        /* it      */ "Esci",
        /* ja      */ "終了",
        /* ko      */ "종료",
        /* pt_br   */ "Sair",
        /* ru      */ "Выход",
        /* zh_hans */ "退出",
        /* zh_hant */ "退出",
    ],
    // FileGoto
    [
        /* en      */ "Go to Line:Column…",
        /* de      */ "Gehe zu Zeile:Spalte…",
        /* es      */ "Ir a línea:columna…",
        /* fr      */ "Aller à la ligne:colonne…",
        /* it      */ "Vai a riga:colonna…",
        /* ja      */ "行:列へ移動…",
        /* ko      */ "행:열로 이동…",
        /* pt_br   */ "Ir para linha:coluna…",
        /* ru      */ "Перейти к строке:столбцу…",
        /* zh_hans */ "转到行:列…",
        /* zh_hant */ "跳至行:列…",
    ],

    // Edit (a menu bar item)
    [
        /* en      */ "Edit",
        /* de      */ "Bearbeiten",
        /* es      */ "Editar",
        /* fr      */ "Édition",
        /* it      */ "Modifica",
        /* ja      */ "編集",
        /* ko      */ "편집",
        /* pt_br   */ "Editar",
        /* ru      */ "Правка",
        /* zh_hans */ "编辑",
        /* zh_hant */ "編輯",
    ],
    // EditUndo
    [
        /* en      */ "Undo",
        /* de      */ "Rückgängig",
        /* es      */ "Deshacer",
        /* fr      */ "Annuler",
        /* it      */ "Annulla",
        /* ja      */ "元に戻す",
        /* ko      */ "실행 취소",
        /* pt_br   */ "Desfazer",
        /* ru      */ "Отменить",
        /* zh_hans */ "撤销",
        /* zh_hant */ "復原",
    ],
    // EditRedo
    [
        /* en      */ "Redo",
        /* de      */ "Wiederholen",
        /* es      */ "Rehacer",
        /* fr      */ "Rétablir",
        /* it      */ "Ripeti",
        /* ja      */ "やり直し",
        /* ko      */ "다시 실행",
        /* pt_br   */ "Refazer",
        /* ru      */ "Повторить",
        /* zh_hans */ "重做",
        /* zh_hant */ "重做",
    ],
    // EditCut
    [
        /* en      */ "Cut",
        /* de      */ "Ausschneiden",
        /* es      */ "Cortar",
        /* fr      */ "Couper",
        /* it      */ "Taglia",
        /* ja      */ "切り取り",
        /* ko      */ "잘라내기",
        /* pt_br   */ "Cortar",
        /* ru      */ "Вырезать",
        /* zh_hans */ "剪切",
        /* zh_hant */ "剪下",
    ],
    // EditCopy
    [
        /* en      */ "Copy",
        /* de      */ "Kopieren",
        /* es      */ "Copiar",
        /* fr      */ "Copier",
        /* it      */ "Copia",
        /* ja      */ "コピー",
        /* ko      */ "복사",
        /* pt_br   */ "Copiar",
        /* ru      */ "Копировать",
        /* zh_hans */ "复制",
        /* zh_hant */ "複製",
    ],
    // EditPaste
    [
        /* en      */ "Paste",
        /* de      */ "Einfügen",
        /* es      */ "Pegar",
        /* fr      */ "Coller",
        /* it      */ "Incolla",
        /* ja      */ "貼り付け",
        /* ko      */ "붙여넣기",
        /* pt_br   */ "Colar",
        /* ru      */ "Вставить",
        /* zh_hans */ "粘贴",
        /* zh_hant */ "貼上",
    ],
    // EditFind
    [
        /* en      */ "Find",
        /* de      */ "Suchen",
        /* es      */ "Buscar",
        /* fr      */ "Rechercher",
        /* it      */ "Trova",
        /* ja      */ "検索",
        /* ko      */ "찾기",
        /* pt_br   */ "Encontrar",
        /* ru      */ "Найти",
        /* zh_hans */ "查找",
        /* zh_hant */ "尋找",
    ],
    // EditReplace
    [
        /* en      */ "Replace",
        /* de      */ "Ersetzen",
        /* es      */ "Reemplazar",
        /* fr      */ "Remplacer",
        /* it      */ "Sostituisci",
        /* ja      */ "置換",
        /* ko      */ "바꾸기",
        /* pt_br   */ "Substituir",
        /* ru      */ "Заменить",
        /* zh_hans */ "替换",
        /* zh_hant */ "取代",
    ],
    // EditSelectAll
    [
        /* en      */ "Select All",
        /* de      */ "Alles auswählen",
        /* es      */ "Seleccionar todo",
        /* fr      */ "Tout sélectionner",
        /* it      */ "Seleziona tutto",
        /* ja      */ "すべて選択",
        /* ko      */ "모두 선택",
        /* pt_br   */ "Selecionar tudo",
        /* ru      */ "Выделить всё",
        /* zh_hans */ "全选",
        /* zh_hant */ "全選"
    ],

    // View (a menu bar item)
    [
        /* en      */ "View",
        /* de      */ "Ansicht",
        /* es      */ "Ver",
        /* fr      */ "Affichage",
        /* it      */ "Visualizza",
        /* ja      */ "表示",
        /* ko      */ "보기",
        /* pt_br   */ "Exibir",
        /* ru      */ "Вид",
        /* zh_hans */ "视图",
        /* zh_hant */ "檢視",
    ],
    // ViewFocusStatusbar
    [
        /* en      */ "Focus Statusbar",
        /* de      */ "Statusleiste fokussieren",
        /* es      */ "Enfocar barra de estado",
        /* fr      */ "Activer la barre d’état",
        /* it      */ "Attiva barra di stato",
        /* ja      */ "ステータスバーにフォーカス",
        /* ko      */ "상태 표시줄로 포커스 이동",
        /* pt_br   */ "Focar barra de status",
        /* ru      */ "Фокус на строку состояния",
        /* zh_hans */ "聚焦状态栏",
        /* zh_hant */ "聚焦狀態列",
    ],
    // ViewWordWrap
    [
        /* en      */ "Word Wrap",
        /* de      */ "Zeilenumbruch",
        /* es      */ "Ajuste de línea",
        /* fr      */ "Retour automatique à la ligne",
        /* it      */ "A capo automatico",
        /* ja      */ "折り返し",
        /* ko      */ "자동 줄 바꿈",
        /* pt_br   */ "Quebra de linha",
        /* ru      */ "Перенос слов",
        /* zh_hans */ "自动换行",
        /* zh_hant */ "自動換行",
    ],
    // ViewGoToFile
    [
        /* en      */ "Go to File…",
        /* de      */ "Gehe zu Datei…",
        /* es      */ "Ir a archivo…",
        /* fr      */ "Aller au fichier…",
        /* it      */ "Vai al file…",
        /* ja      */ "ファイルへ移動…",
        /* ko      */ "파일로 이동…",
        /* pt_br   */ "Ir para arquivo…",
        /* ru      */ "Перейти к файлу…",
        /* zh_hans */ "转到文件…",
        /* zh_hant */ "跳至檔案…",
    ],

    // Help (a menu bar item)
    [
        /* en      */ "Help",
        /* de      */ "Hilfe",
        /* es      */ "Ayuda",
        /* fr      */ "Aide",
        /* it      */ "Aiuto",
        /* ja      */ "ヘルプ",
        /* ko      */ "도움말",
        /* pt_br   */ "Ajuda",
        /* ru      */ "Помощь",
        /* zh_hans */ "帮助",
        /* zh_hant */ "幫助",
    ],
    // HelpAbout
    [
        /* en      */ "About",
        /* de      */ "Über",
        /* es      */ "Acerca de",
        /* fr      */ "À propos",
        /* it      */ "Informazioni",
        /* ja      */ "情報",
        /* ko      */ "정보",
        /* pt_br   */ "Sobre",
        /* ru      */ "О программе",
        /* zh_hans */ "关于",
        /* zh_hant */ "關於",
    ],

    // UnsavedChangesDialogTitle
    [
        /* en      */ "Unsaved Changes",
        /* de      */ "Ungespeicherte Änderungen",
        /* es      */ "Cambios sin guardar",
        /* fr      */ "Modifications non enregistrées",
        /* it      */ "Modifiche non salvate",
        /* ja      */ "未保存の変更",
        /* ko      */ "저장되지 않은 변경 사항",
        /* pt_br   */ "Alterações não salvas",
        /* ru      */ "Несохраненные изменения",
        /* zh_hans */ "未保存的更改",
        /* zh_hant */ "未儲存的變更",
    ],
    // UnsavedChangesDialogDescription
    [
        /* en      */ "Do you want to save the changes you made?",
        /* de      */ "Möchten Sie die vorgenommenen Änderungen speichern?",
        /* es      */ "¿Desea guardar los cambios realizados?",
        /* fr      */ "Voulez-vous enregistrer les modifications apportées ?",
        /* it      */ "Vuoi salvare le modifiche apportate?",
        /* ja      */ "変更内容を保存しますか？",
        /* ko      */ "변경한 내용을 저장하시겠습니까?",
        /* pt_br   */ "Deseja salvar as alterações feitas?",
        /* ru      */ "Вы хотите сохранить внесённые изменения?",
        /* zh_hans */ "您要保存所做的更改吗？",
        /* zh_hant */ "您要保存所做的變更嗎？",
    ],
    // UnsavedChangesDialogYes
    [
        /* en      */ "Save",
        /* de      */ "Speichern",
        /* es      */ "Guardar",
        /* fr      */ "Enregistrer",
        /* it      */ "Salva",
        /* ja      */ "保存する",
        /* ko      */ "저장",
        /* pt_br   */ "Salvar",
        /* ru      */ "Сохранить",
        /* zh_hans */ "保存",
        /* zh_hant */ "儲存",
    ],
    // UnsavedChangesDialogNo
    [
        /* en      */ "Don't Save",
        /* de      */ "Nicht speichern",
        /* es      */ "No guardar",
        /* fr      */ "Ne pas enregistrer",
        /* it      */ "Non salvare",
        /* ja      */ "保存しない",
        /* ko      */ "저장 안 함",
        /* pt_br   */ "Não salvar",
        /* ru      */ "Не сохранять",
        /* zh_hans */ "不保存",
        /* zh_hant */ "不儲存",
    ],

    // AboutDialogTitle
    [
        /* en      */ "About",
        /* de      */ "Über",
        /* es      */ "Acerca de",
        /* fr      */ "À propos",
        /* it      */ "Informazioni",
        /* ja      */ "情報",
        /* ko      */ "정보",
        /* pt_br   */ "Sobre",
        /* ru      */ "О программе",
        /* zh_hans */ "关于",
        /* zh_hant */ "關於",
    ],
    // AboutDialogVersion
    [
        /* en      */ "Version: ",
        /* de      */ "Version: ",
        /* es      */ "Versión: ",
        /* fr      */ "Version : ",
        /* it      */ "Versione: ",
        /* ja      */ "バージョン: ",
        /* ko      */ "버전: ",
        /* pt_br   */ "Versão: ",
        /* ru      */ "Версия: ",
        /* zh_hans */ "版本: ",
        /* zh_hant */ "版本: ",
    ],

    // Shown when the clipboard size exceeds the limit for OSC 52
    // LargeClipboardWarningLine1
    [
        /* en      */ "Text you copy is shared with the terminal clipboard.",
        /* de      */ "Der kopierte Text wird mit der Terminal-Zwischenablage geteilt.",
        /* es      */ "El texto que copies se comparte con el portapapeles del terminal.",
        /* fr      */ "Le texte que vous copiez est partagé avec le presse-papiers du terminal.",
        /* it      */ "Il testo copiato viene condiviso con gli appunti del terminale.",
        /* ja      */ "コピーしたテキストはターミナルのクリップボードと共有されます。",
        /* ko      */ "복사한 텍스트가 터미널 클립보드와 공유됩니다.",
        /* pt_br   */ "O texto copiado é compartilhado com a área de transferência do terminal.",
        /* ru      */ "Скопированный текст передаётся в буфер обмена терминала.",
        /* zh_hans */ "你复制的文本将共享到终端剪贴板。",
        /* zh_hant */ "您複製的文字將會與終端機剪貼簿分享。",
    ],
    // LargeClipboardWarningLine2
    [
        /* en      */ "You copied {size} which may take a long time to share.",
        /* de      */ "Sie haben {size} kopiert. Das Weitergeben könnte länger dauern.",
        /* es      */ "Copiaste {size}, lo que puede tardar en compartirse.",
        /* fr      */ "Vous avez copié {size}, ce qui peut être long à partager.",
        /* it      */ "Hai copiato {size}, potrebbe richiedere molto tempo per condividerlo.",
        /* ja      */ "{size} をコピーしました。共有に時間がかかる可能性があります。",
        /* ko      */ "{size}를 복사했습니다. 공유하는 데 시간이 오래 걸릴 수 있습니다.",
        /* pt_br   */ "Você copiou {size}, o que pode demorar para compartilhar.",
        /* ru      */ "Вы скопировали {size}; передача может занять много времени.",
        /* zh_hans */ "你复制了 {size}，共享可能需要较长时间。",
        /* zh_hant */ "您已複製 {size}，共享可能需要較長時間。",
    ],
    // LargeClipboardWarningLine3
    [
        /* en      */ "Do you want to send it anyway?",
        /* de      */ "Möchten Sie es trotzdem senden?",
        /* es      */ "¿Desea enviarlo de todas formas?",
        /* fr      */ "Voulez-vous quand même l’envoyer ?",
        /* it      */ "Vuoi inviarlo comunque?",
        /* ja      */ "それでも送信しますか？",
        /* ko      */ "그래도 전송하시겠습니까?",
        /* pt_br   */ "Deseja enviar mesmo assim?",
        /* ru      */ "Отправить в любом случае?",
        /* zh_hans */ "仍要发送吗？",
        /* zh_hant */ "仍要傳送嗎？",
    ],
    // SuperLargeClipboardWarning (as an alternative to LargeClipboardWarningLine2 and 3)
    [
        /* en      */ "The text you copied is too large to be shared.",
        /* de      */ "Der kopierte Text ist zu groß, um geteilt zu werden.",
        /* es      */ "El texto que copiaste es demasiado grande para compartirse.",
        /* fr      */ "Le texte que vous avez copié est trop volumineux pour être partagé.",
        /* it      */ "Il testo copiato è troppo grande per essere condiviso.",
        /* ja      */ "コピーしたテキストは大きすぎて共有できません。",
        /* ko      */ "복사한 텍스트가 너무 커서 공유할 수 없습니다.",
        /* pt_br   */ "O texto copiado é grande demais para ser compartilhado.",
        /* ru      */ "Скопированный текст слишком велик для передачи.",
        /* zh_hans */ "你复制的文本过大，无法共享。",
        /* zh_hant */ "您複製的文字過大，無法分享。",
    ],

    // WarningDialogTitle
    [
        /* en      */ "Warning",
        /* de      */ "Warnung",
        /* es      */ "Advertencia",
        /* fr      */ "Avertissement",
        /* it      */ "Avviso",
        /* ja      */ "警告",
        /* ko      */ "경고",
        /* pt_br   */ "Aviso",
        /* ru      */ "Предупреждение",
        /* zh_hans */ "警告",
        /* zh_hant */ "警告",
    ],

    // ErrorDialogTitle
    [
        /* en      */ "Error",
        /* de      */ "Fehler",
        /* es      */ "Error",
        /* fr      */ "Erreur",
        /* it      */ "Errore",
        /* ja      */ "エラー",
        /* ko      */ "오류",
        /* pt_br   */ "Erro",
        /* ru      */ "Ошибка",
        /* zh_hans */ "错误",
        /* zh_hant */ "錯誤",
    ],
    // ErrorIcuMissing
    [
        /* en      */ "This operation requires the ICU library",
        /* de      */ "Diese Operation erfordert die ICU-Bibliothek",
        /* es      */ "Esta operación requiere la biblioteca ICU",
        /* fr      */ "Cette opération nécessite la bibliothèque ICU",
        /* it      */ "Questa operazione richiede la libreria ICU",
        /* ja      */ "この操作にはICUライブラリが必要です",
        /* ko      */ "이 작업에는 ICU 라이브러리가 필요합니다",
        /* pt_br   */ "Esta operação requer a biblioteca ICU",
        /* ru      */ "Эта операция требует наличия библиотеки ICU",
        /* zh_hans */ "此操作需要 ICU 库",
        /* zh_hant */ "此操作需要 ICU 庫",
    ],

    // SearchNeedleLabel (for input field)
    [
        /* en      */ "Find:",
        /* de      */ "Suchen:",
        /* es      */ "Buscar:",
        /* fr      */ "Rechercher :",
        /* it      */ "Trova:",
        /* ja      */ "検索:",
        /* ko      */ "찾기:",
        /* pt_br   */ "Encontrar:",
        /* ru      */ "Найти:",
        /* zh_hans */ "查找:",
        /* zh_hant */ "尋找:",
    ],
    // SearchReplacementLabel (for input field)
    [
        /* en      */ "Replace:",
        /* de      */ "Ersetzen:",
        /* es      */ "Reemplazar:",
        /* fr      */ "Remplacer :",
        /* it      */ "Sostituire:",
        /* ja      */ "置換:",
        /* ko      */ "바꾸기:",
        /* pt_br   */ "Substituir:",
        /* ru      */ "Замена:",
        /* zh_hans */ "替换:",
        /* zh_hant */ "替換:",
    ],
    // SearchMatchCase (toggle)
    [
        /* en      */ "Match Case",
        /* de      */ "Groß/Klein",
        /* es      */ "May/Min",
        /* fr      */ "Resp. la casse",
        /* it      */ "Maius/minus",
        /* ja      */ "大/小文字を区別",
        /* ko      */ "대소문자",
        /* pt_br   */ "Maius/minus",
        /* ru      */ "Регистр",
        /* zh_hans */ "区分大小写",
        /* zh_hant */ "區分大小寫",
    ],
    // SearchWholeWord (toggle)
    [
        /* en      */ "Whole Word",
        /* de      */ "Ganzes Wort",
        /* es      */ "Palabra",
        /* fr      */ "Mot entier",
        /* it      */ "Parola",
        /* ja      */ "単語全体",
        /* ko      */ "전체 단어",
        /* pt_br   */ "Palavra",
        /* ru      */ "Слово",
        /* zh_hans */ "全字匹配",
        /* zh_hant */ "全字匹配",
    ],
    // SearchUseRegex (toggle)
    [
        /* en      */ "Use Regex",
        /* de      */ "RegEx",
        /* es      */ "RegEx",
        /* fr      */ "RegEx",
        /* it      */ "RegEx",
        /* ja      */ "正規表現",
        /* ko      */ "정규식",
        /* pt_br   */ "RegEx",
        /* ru      */ "RegEx",
        /* zh_hans */ "正则",
        /* zh_hant */ "正則",
    ],
    // SearchReplaceAll (button)
    [
        /* en      */ "Replace All",
        /* de      */ "Alle ersetzen",
        /* es      */ "Reemplazar todo",
        /* fr      */ "Remplacer tout",
        /* it      */ "Sostituisci tutto",
        /* ja      */ "すべて置換",
        /* ko      */ "모두 바꾸기",
        /* pt_br   */ "Substituir tudo",
        /* ru      */ "Заменить все",
        /* zh_hans */ "全部替换",
        /* zh_hant */ "全部取代",
    ],
    // SearchClose (button)
    [
        /* en      */ "Close",
        /* de      */ "Schließen",
        /* es      */ "Cerrar",
        /* fr      */ "Fermer",
        /* it      */ "Chiudi",
        /* ja      */ "閉じる",
        /* ko      */ "닫기",
        /* pt_br   */ "Fechar",
        /* ru      */ "Закрыть",
        /* zh_hans */ "关闭",
        /* zh_hant */ "關閉",
    ],

    // EncodingReopen
    [
        /* en      */ "Reopen with encoding…",
        /* de      */ "Mit Kodierung erneut öffnen…",
        /* es      */ "Reabrir con codificación…",
        /* fr      */ "Rouvrir avec un encodage différent…",
        /* it      */ "Riapri con codifica…",
        /* ja      */ "指定エンコーディングで再度開く…",
        /* ko      */ "인코딩으로 다시 열기…",
        /* pt_br   */ "Reabrir com codificação…",
        /* ru      */ "Открыть снова с кодировкой…",
        /* zh_hans */ "使用编码重新打开…",
        /* zh_hant */ "使用編碼重新打開…",
    ],
    // EncodingConvert
    [
        /* en      */ "Convert to encoding…",
        /* de      */ "In Kodierung konvertieren…",
        /* es      */ "Convertir a otra codificación…",
        /* fr      */ "Convertir vers l’encodage…",
        /* it      */ "Converti in codifica…",
        /* ja      */ "エンコーディングを変換…",
        /* ko      */ "인코딩으로 변환…",
        /* pt_br   */ "Converter para codificação…",
        /* ru      */ "Преобразовать в кодировку…",
        /* zh_hans */ "转换为编码…",
        /* zh_hant */ "轉換為編碼…",
    ],

    // IndentationTabs
    [
        /* en      */ "Tabs",
        /* de      */ "Tabs",
        /* es      */ "Tabulaciones",
        /* fr      */ "Tabulations",
        /* it      */ "Tabulazioni",
        /* ja      */ "タブ",
        /* ko      */ "탭",
        /* pt_br   */ "Tabulações",
        /* ru      */ "Табы",
        /* zh_hans */ "制表符",
        /* zh_hant */ "製表符",
    ],
    // IndentationSpaces
    [
        /* en      */ "Spaces",
        /* de      */ "Leerzeichen",
        /* es      */ "Espacios",
        /* fr      */ "Espaces",
        /* it      */ "Spazi",
        /* ja      */ "スペース",
        /* ko      */ "공백",
        /* pt_br   */ "Espaços",
        /* ru      */ "Пробелы",
        /* zh_hans */ "空格",
        /* zh_hant */ "空格",
    ],

    // SaveAsDialogPathLabel
    [
        /* en      */ "Folder:",
        /* de      */ "Ordner:",
        /* es      */ "Carpeta:",
        /* fr      */ "Dossier :",
        /* it      */ "Cartella:",
        /* ja      */ "フォルダ:",
        /* ko      */ "폴더:",
        /* pt_br   */ "Pasta:",
        /* ru      */ "Папка:",
        /* zh_hans */ "文件夹:",
        /* zh_hant */ "資料夾:",
    ],
    // SaveAsDialogNameLabel
    [
        /* en      */ "File name:",
        /* de      */ "Dateiname:",
        /* es      */ "Nombre de archivo:",
        /* fr      */ "Nom du fichier :",
        /* it      */ "Nome del file:",
        /* ja      */ "ファイル名:",
        /* ko      */ "파일 이름:",
        /* pt_br   */ "Nome do arquivo:",
        /* ru      */ "Имя файла:",
        /* zh_hans */ "文件名:",
        /* zh_hant */ "檔案名稱:",
    ],

    // FileOverwriteWarning
    [
        /* en      */ "Confirm Save As",
        /* de      */ "Speichern unter bestätigen",
        /* es      */ "Confirmar Guardar como",
        /* fr      */ "Confirmer Enregistrer sous",
        /* it      */ "Conferma Salva con nome",
        /* ja      */ "名前を付けて保存の確認",
        /* ko      */ "다른 이름으로 저장 확인",
        /* pt_br   */ "Confirmar Salvar como",
        /* ru      */ "Подтвердите «Сохранить как…»",
        /* zh_hans */ "确认另存为",
        /* zh_hant */ "確認另存新檔",
    ],
    // FileOverwriteWarningDescription
    [
        /* en      */ "File already exists. Do you want to overwrite it?",
        /* de      */ "Datei existiert bereits. Möchten Sie sie überschreiben?",
        /* es      */ "El archivo ya existe. ¿Desea sobrescribirlo?",
        /* fr      */ "Le fichier existe déjà. Voulez-vous l’écraser ?",
        /* it      */ "Il file esiste già. Vuoi sovrascriverlo?",
        /* ja      */ "ファイルは既に存在します。上書きしますか？",
        /* ko      */ "파일이 이미 존재합니다. 덮어쓰시겠습니까?",
        /* pt_br   */ "O arquivo já existe. Deseja sobrescrevê-lo?",
        /* ru      */ "Файл уже существует. Перезаписать?",
        /* zh_hans */ "文件已存在。要覆盖它吗？",
        /* zh_hant */ "檔案已存在。要覆蓋它嗎？",
    ],
];

static mut S_LANG: LangId = LangId::en;

pub fn init() {
    // WARNING:
    // Generic language tags such as "zh" MUST be sorted after more specific tags such
    // as "zh-hant" to ensure that the prefix match finds the most specific one first.
    const LANG_MAP: &[(&str, LangId)] = &[
        ("en", LangId::en),
        // ----------------
        ("de", LangId::de),
        ("es", LangId::es),
        ("fr", LangId::fr),
        ("it", LangId::it),
        ("ja", LangId::ja),
        ("ko", LangId::ko),
        ("pt-br", LangId::pt_br),
        ("ru", LangId::ru),
        ("zh-hant", LangId::zh_hant),
        ("zh-tw", LangId::zh_hant),
        ("zh", LangId::zh_hans),
    ];

    let scratch = scratch_arena(None);
    let langs = sys::preferred_languages(&scratch);
    let mut lang = LangId::en;

    'outer: for l in langs {
        for (prefix, id) in LANG_MAP {
            if l.starts_with_ignore_ascii_case(prefix) {
                lang = *id;
                break 'outer;
            }
        }
    }

    unsafe {
        S_LANG = lang;
    }
}

pub fn loc(id: LocId) -> &'static str {
    S_LANG_LUT[id as usize][unsafe { S_LANG as usize }]
}

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use std::num::ParseIntError;

use edit::framebuffer::IndexedColor;
use edit::helpers::*;
use edit::icu;
use edit::input::{kbmod, vk};
use edit::tui::*;

use crate::localization::*;
use crate::state::*;

pub fn draw_editor(ctx: &mut Context, state: &mut State) {
    if !matches!(state.wants_search.kind, StateSearchKind::Hidden | StateSearchKind::Disabled) {
        draw_search(ctx, state);
    }

    let size = ctx.size();
    // TODO: The layout code should be able to just figure out the height on its own.
    let height_reduction = match state.wants_search.kind {
        StateSearchKind::Search => 4,
        StateSearchKind::Replace => 5,
        _ => 2,
    };

    if let Some(doc) = state.documents.active() {
        ctx.textarea("textarea", doc.buffer.clone());
        ctx.inherit_focus();
    } else {
        ctx.block_begin("empty");
        ctx.block_end();
    }

    ctx.attr_intrinsic_size(Size { width: 0, height: size.height - height_reduction });
}

fn draw_search(ctx: &mut Context, state: &mut State) {
    if let Err(err) = icu::init() {
        error_log_add(ctx, state, err);
        state.wants_search.kind = StateSearchKind::Disabled;
        return;
    }

    let Some(doc) = state.documents.active() else {
        state.wants_search.kind = StateSearchKind::Hidden;
        return;
    };

    let mut action = None;
    let mut focus = StateSearchKind::Hidden;

    if state.wants_search.focus {
        state.wants_search.focus = false;
        focus = StateSearchKind::Search;

        // If the selection is empty, focus the search input field.
        // Otherwise, focus the replace input field, if it exists.
        if let Some(selection) = doc.buffer.borrow_mut().extract_user_selection(false) {
            state.search_needle = String::from_utf8_lossy_owned(selection);
            focus = state.wants_search.kind;
        }
    }

    ctx.block_begin("search");
    ctx.attr_focus_well();
    ctx.attr_background_rgba(ctx.indexed(IndexedColor::White));
    ctx.attr_foreground_rgba(ctx.indexed(IndexedColor::Black));
    {
        if ctx.contains_focus() && ctx.consume_shortcut(vk::ESCAPE) {
            state.wants_search.kind = StateSearchKind::Hidden;
        }

        ctx.table_begin("needle");
        ctx.table_set_cell_gap(Size { width: 1, height: 0 });
        {
            {
                ctx.table_next_row();
                ctx.label("label", loc(LocId::SearchNeedleLabel));

                if ctx.editline("needle", &mut state.search_needle) {
                    action = Some(SearchAction::Search);
                }
                if !state.search_success {
                    ctx.attr_background_rgba(ctx.indexed(IndexedColor::Red));
                    ctx.attr_foreground_rgba(ctx.indexed(IndexedColor::BrightWhite));
                }
                ctx.attr_intrinsic_size(Size { width: COORD_TYPE_SAFE_MAX, height: 1 });
                if focus == StateSearchKind::Search {
                    ctx.steal_focus();
                }
                if ctx.is_focused() && ctx.consume_shortcut(vk::RETURN) {
                    action = Some(SearchAction::Search);
                }
            }

            if state.wants_search.kind == StateSearchKind::Replace {
                ctx.table_next_row();
                ctx.label("label", loc(LocId::SearchReplacementLabel));

                ctx.editline("replacement", &mut state.search_replacement);
                ctx.attr_intrinsic_size(Size { width: COORD_TYPE_SAFE_MAX, height: 1 });
                if focus == StateSearchKind::Replace {
                    ctx.steal_focus();
                }
                if ctx.is_focused() {
                    if ctx.consume_shortcut(vk::RETURN) {
                        action = Some(SearchAction::Replace);
                    } else if ctx.consume_shortcut(kbmod::CTRL_ALT | vk::RETURN) {
                        action = Some(SearchAction::ReplaceAll);
                    }
                }
            }
        }
        ctx.table_end();

        ctx.table_begin("options");
        ctx.table_set_cell_gap(Size { width: 2, height: 0 });
        {
            let mut change = false;
            let mut change_action = Some(SearchAction::Search);

            ctx.table_next_row();

            change |= ctx.checkbox(
                "match-case",
                loc(LocId::SearchMatchCase),
                &mut state.search_options.match_case,
            );
            change |= ctx.checkbox(
                "whole-word",
                loc(LocId::SearchWholeWord),
                &mut state.search_options.whole_word,
            );
            change |= ctx.checkbox(
                "use-regex",
                loc(LocId::SearchUseRegex),
                &mut state.search_options.use_regex,
            );
            if state.wants_search.kind == StateSearchKind::Replace
                && ctx.button("replace-all", loc(LocId::SearchReplaceAll), ButtonStyle::default())
            {
                change = true;
                change_action = Some(SearchAction::ReplaceAll);
            }
            if ctx.button("close", loc(LocId::SearchClose), ButtonStyle::default()) {
                state.wants_search.kind = StateSearchKind::Hidden;
            }

            if change {
                action = change_action;
                state.wants_search.focus = true;
                ctx.needs_rerender();
            }
        }
        ctx.table_end();
    }
    ctx.block_end();

    if let Some(action) = action {
        search_execute(ctx, state, action);
    }
}

pub enum SearchAction {
    Search,
    Replace,
    ReplaceAll,
}

pub fn search_execute(ctx: &mut Context, state: &mut State, action: SearchAction) {
    let Some(doc) = state.documents.active_mut() else {
        return;
    };

    state.search_success = match action {
        SearchAction::Search => {
            doc.buffer.borrow_mut().find_and_select(&state.search_needle, state.search_options)
        }
        SearchAction::Replace => doc.buffer.borrow_mut().find_and_replace(
            &state.search_needle,
            state.search_options,
            state.search_replacement.as_bytes(),
        ),
        SearchAction::ReplaceAll => doc.buffer.borrow_mut().find_and_replace_all(
            &state.search_needle,
            state.search_options,
            state.search_replacement.as_bytes(),
        ),
    }
    .is_ok();

    ctx.needs_rerender();
}

pub fn draw_handle_save(ctx: &mut Context, state: &mut State) {
    if let Some(doc) = state.documents.active_mut() {
        if doc.path.is_some() {
            if let Err(err) = doc.save(None) {
                error_log_add(ctx, state, err);
            }
        } else {
            // No path? Show the file picker.
            state.wants_file_picker = StateFilePicker::SaveAs;
            state.wants_save = false;
            ctx.needs_rerender();
        }
    }

    state.wants_save = false;
}

pub fn draw_handle_wants_close(ctx: &mut Context, state: &mut State) {
    let Some(doc) = state.documents.active() else {
        state.wants_close = false;
        return;
    };

    if !doc.buffer.borrow().is_dirty() {
        state.documents.remove_active();
        state.wants_close = false;
        ctx.needs_rerender();
        return;
    }

    enum Action {
        None,
        Save,
        Discard,
        Cancel,
    }
    let mut action = Action::None;

    ctx.modal_begin("unsaved-changes", loc(LocId::UnsavedChangesDialogTitle));
    ctx.attr_background_rgba(ctx.indexed(IndexedColor::Red));
    ctx.attr_foreground_rgba(ctx.indexed(IndexedColor::BrightWhite));
    {
        let contains_focus = ctx.contains_focus();

        ctx.label("description", loc(LocId::UnsavedChangesDialogDescription));
        ctx.attr_padding(Rect::three(1, 2, 1));

        ctx.table_begin("choices");
        ctx.inherit_focus();
        ctx.attr_padding(Rect::three(0, 2, 1));
        ctx.attr_position(Position::Center);
        ctx.table_set_cell_gap(Size { width: 2, height: 0 });
        {
            ctx.table_next_row();
            ctx.inherit_focus();

            if ctx.button(
                "yes",
                loc(LocId::UnsavedChangesDialogYes),
                ButtonStyle::default().accelerator('S'),
            ) {
                action = Action::Save;
            }
            ctx.inherit_focus();
            if ctx.button(
                "no",
                loc(LocId::UnsavedChangesDialogNo),
                ButtonStyle::default().accelerator('N'),
            ) {
                action = Action::Discard;
            }
            if ctx.button("cancel", loc(LocId::Cancel), ButtonStyle::default()) {
                action = Action::Cancel;
            }

            // Handle accelerator shortcuts
            if contains_focus {
                if ctx.consume_shortcut(vk::S) {
                    action = Action::Save;
                } else if ctx.consume_shortcut(vk::N) {
                    action = Action::Discard;
                }
            }
        }
        ctx.table_end();
    }
    if ctx.modal_end() {
        action = Action::Cancel;
    }

    match action {
        Action::None => return,
        Action::Save => {
            state.wants_save = true;
        }
        Action::Discard => {
            state.documents.remove_active();
            state.wants_close = false;
        }
        Action::Cancel => {
            state.wants_exit = false;
            state.wants_close = false;
        }
    }

    ctx.needs_rerender();
}

pub fn draw_goto_menu(ctx: &mut Context, state: &mut State) {
    let mut done = false;

    if let Some(doc) = state.documents.active_mut() {
        ctx.modal_begin("goto", loc(LocId::FileGoto));
        {
            if ctx.editline("goto-line", &mut state.goto_target) {
                state.goto_invalid = false;
            }
            if state.goto_invalid {
                ctx.attr_background_rgba(ctx.indexed(IndexedColor::Red));
                ctx.attr_foreground_rgba(ctx.indexed(IndexedColor::BrightWhite));
            }

            ctx.attr_intrinsic_size(Size { width: 24, height: 1 });
            ctx.steal_focus();

            if ctx.consume_shortcut(vk::RETURN) {
                match validate_goto_point(&state.goto_target) {
                    Ok(point) => {
                        let mut buf = doc.buffer.borrow_mut();
                        buf.cursor_move_to_logical(point);
                        buf.make_cursor_visible();
                        done = true;
                    }
                    Err(_) => state.goto_invalid = true,
                }
                ctx.needs_rerender();
            }
        }
        done |= ctx.modal_end();
    } else {
        done = true;
    }

    if done {
        state.wants_goto = false;
        state.goto_target.clear();
        state.goto_invalid = false;
        ctx.needs_rerender();
    }
}

fn validate_goto_point(line: &str) -> Result<Point, ParseIntError> {
    let mut coords = [0; 2];
    let (y, x) = line.split_once(':').unwrap_or((line, "0"));
    // Using a loop here avoids 2 copies of the str->int code.
    // This makes the binary more compact.
    for (i, s) in [x, y].iter().enumerate() {
        coords[i] = s.parse::<CoordType>()?.saturating_sub(1);
    }
    Ok(Point { x: coords[0], y: coords[1] })
}

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

//! Provides fast, non-cryptographic hash functions.

/// The venerable wyhash hash function.
///
/// It's fast, has good statistical properties, and is in the public domain.
/// See: <https://github.com/wangyi-fudan/wyhash>
/// If you visit the link, you'll find that it was superseded by "rapidhash",
/// but that's not particularly interesting for this project. rapidhash results
/// in way larger assembly and isn't faster when hashing small amounts of data.
pub fn hash(mut seed: u64, data: &[u8]) -> u64 {
    unsafe {
        const S0: u64 = 0xa0761d6478bd642f;
        const S1: u64 = 0xe7037ed1a0b428db;
        const S2: u64 = 0x8ebc6af09c88c6e3;
        const S3: u64 = 0x589965cc75374cc3;

        let len = data.len();
        let mut p = data.as_ptr();
        let a;
        let b;

        seed ^= S0;

        if len <= 16 {
            if len >= 4 {
                a = (wyr4(p) << 32) | wyr4(p.add((len >> 3) << 2));
                b = (wyr4(p.add(len - 4)) << 32) | wyr4(p.add(len - 4 - ((len >> 3) << 2)));
            } else if len > 0 {
                a = wyr3(p, len);
                b = 0;
            } else {
                a = 0;
                b = 0;
            }
        } else {
            let mut i = len;
            if i > 48 {
                let mut seed1 = seed;
                let mut seed2 = seed;
                while {
                    seed = wymix(wyr8(p) ^ S1, wyr8(p.add(8)) ^ seed);
                    seed1 = wymix(wyr8(p.add(16)) ^ S2, wyr8(p.add(24)) ^ seed1);
                    seed2 = wymix(wyr8(p.add(32)) ^ S3, wyr8(p.add(40)) ^ seed2);
                    p = p.add(48);
                    i -= 48;
                    i > 48
                } {}
                seed ^= seed1 ^ seed2;
            }
            while i > 16 {
                seed = wymix(wyr8(p) ^ S1, wyr8(p.add(8)) ^ seed);
                i -= 16;
                p = p.add(16);
            }
            a = wyr8(p.offset(i as isize - 16));
            b = wyr8(p.offset(i as isize - 8));
        }

        wymix(S1 ^ (len as u64), wymix(a ^ S1, b ^ seed))
    }
}

unsafe fn wyr3(p: *const u8, k: usize) -> u64 {
    let p0 = unsafe { p.read() as u64 };
    let p1 = unsafe { p.add(k >> 1).read() as u64 };
    let p2 = unsafe { p.add(k - 1).read() as u64 };
    (p0 << 16) | (p1 << 8) | p2
}

unsafe fn wyr4(p: *const u8) -> u64 {
    unsafe { (p as *const u32).read_unaligned() as u64 }
}

unsafe fn wyr8(p: *const u8) -> u64 {
    unsafe { (p as *const u64).read_unaligned() }
}

// This is a weak mix function on its own. It may be worth considering
// replacing external uses of this function with a stronger one.
// On the other hand, it's very fast.
pub fn wymix(lhs: u64, rhs: u64) -> u64 {
    let lhs = lhs as u128;
    let rhs = rhs as u128;
    let r = lhs * rhs;
    (r >> 64) as u64 ^ (r as u64)
}

pub fn hash_str(seed: u64, s: &str) -> u64 {
    hash(seed, s.as_bytes())
}

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

//! Path related helpers.

use std::ffi::{OsStr, OsString};
use std::path::{Component, MAIN_SEPARATOR_STR, Path, PathBuf};

/// Normalizes a given path by removing redundant components.
/// The given path must be absolute (e.g. by joining it with the current working directory).
pub fn normalize(path: &Path) -> PathBuf {
    let mut res = PathBuf::with_capacity(path.as_os_str().as_encoded_bytes().len());
    let mut root_len = 0;

    for component in path.components() {
        match component {
            Component::Prefix(p) => res.push(p.as_os_str()),
            Component::RootDir => {
                res.push(OsStr::new(MAIN_SEPARATOR_STR));
                root_len = res.as_os_str().as_encoded_bytes().len();
            }
            Component::CurDir => {}
            Component::ParentDir => {
                // Get the length up to the parent directory
                if let Some(len) = res
                    .parent()
                    .map(|p| p.as_os_str().as_encoded_bytes().len())
                    // Ensure we don't pop the root directory
                    && len >= root_len
                {
                    // Pop the last component from `res`.
                    //
                    // This can be replaced with a plain `res.as_mut_os_string().truncate(len)`
                    // once `os_string_truncate` is stabilized (#133262).
                    let mut bytes = res.into_os_string().into_encoded_bytes();
                    bytes.truncate(len);
                    res = PathBuf::from(unsafe { OsString::from_encoded_bytes_unchecked(bytes) });
                }
            }
            Component::Normal(p) => res.push(p),
        }
    }

    res
}

#[cfg(test)]
mod tests {
    use std::ffi::OsString;
    use std::path::Path;

    use super::*;

    fn norm(s: &str) -> OsString {
        normalize(Path::new(s)).into_os_string()
    }

    #[cfg(unix)]
    #[test]
    fn test_unix() {
        assert_eq!(norm("/a/b/c"), "/a/b/c");
        assert_eq!(norm("/a/b/c/"), "/a/b/c");
        assert_eq!(norm("/a/./b"), "/a/b");
        assert_eq!(norm("/a/b/../c"), "/a/c");
        assert_eq!(norm("/../../a"), "/a");
        assert_eq!(norm("/../"), "/");
        assert_eq!(norm("/a//b/c"), "/a/b/c");
        assert_eq!(norm("/a/b/c/../../../../d"), "/d");
        assert_eq!(norm("//"), "/");
    }

    #[cfg(windows)]
    #[test]
    fn test_windows() {
        assert_eq!(norm(r"C:\a\b\c"), r"C:\a\b\c");
        assert_eq!(norm(r"C:\a\b\c\"), r"C:\a\b\c");
        assert_eq!(norm(r"C:\a\.\b"), r"C:\a\b");
        assert_eq!(norm(r"C:\a\b\..\c"), r"C:\a\c");
        assert_eq!(norm(r"C:\..\..\a"), r"C:\a");
        assert_eq!(norm(r"C:\..\"), r"C:\");
        assert_eq!(norm(r"C:\a\\b\c"), r"C:\a\b\c");
        assert_eq!(norm(r"C:/a\b/c"), r"C:\a\b\c");
        assert_eq!(norm(r"C:\a\b\c\..\..\..\..\d"), r"C:\d");
        assert_eq!(norm(r"\\server\share\path"), r"\\server\share\path");
    }
}

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use edit::arena_format;
use edit::helpers::*;
use edit::input::{kbmod, vk};
use edit::tui::*;

use crate::localization::*;
use crate::state::*;

pub fn draw_menubar(ctx: &mut Context, state: &mut State) {
    ctx.menubar_begin();
    ctx.attr_background_rgba(state.menubar_color_bg);
    ctx.attr_foreground_rgba(state.menubar_color_fg);
    {
        let contains_focus = ctx.contains_focus();

        if ctx.menubar_menu_begin(loc(LocId::File), 'F') {
            draw_menu_file(ctx, state);
        }
        if !contains_focus && ctx.consume_shortcut(vk::F10) {
            ctx.steal_focus();
        }
        if state.documents.active().is_some() {
            if ctx.menubar_menu_begin(loc(LocId::Edit), 'E') {
                draw_menu_edit(ctx, state);
            }
            if ctx.menubar_menu_begin(loc(LocId::View), 'V') {
                draw_menu_view(ctx, state);
            }
        }
        if ctx.menubar_menu_begin(loc(LocId::Help), 'H') {
            draw_menu_help(ctx, state);
        }
    }
    ctx.menubar_end();
}

fn draw_menu_file(ctx: &mut Context, state: &mut State) {
    if ctx.menubar_menu_button(loc(LocId::FileNew), 'N', kbmod::CTRL | vk::N) {
        draw_add_untitled_document(ctx, state);
    }
    if ctx.menubar_menu_button(loc(LocId::FileOpen), 'O', kbmod::CTRL | vk::O) {
        state.wants_file_picker = StateFilePicker::Open;
    }
    if state.documents.active().is_some() {
        if ctx.menubar_menu_button(loc(LocId::FileSave), 'S', kbmod::CTRL | vk::S) {
            state.wants_save = true;
        }
        if ctx.menubar_menu_button(loc(LocId::FileSaveAs), 'A', vk::NULL) {
            state.wants_file_picker = StateFilePicker::SaveAs;
        }
        if ctx.menubar_menu_button(loc(LocId::FileClose), 'C', kbmod::CTRL | vk::W) {
            state.wants_close = true;
        }
    }
    if ctx.menubar_menu_button(loc(LocId::FileExit), 'X', kbmod::CTRL | vk::Q) {
        state.wants_exit = true;
    }
    ctx.menubar_menu_end();
}

fn draw_menu_edit(ctx: &mut Context, state: &mut State) {
    let doc = state.documents.active().unwrap();
    let mut tb = doc.buffer.borrow_mut();

    if ctx.menubar_menu_button(loc(LocId::EditUndo), 'U', kbmod::CTRL | vk::Z) {
        tb.undo();
        ctx.needs_rerender();
    }
    if ctx.menubar_menu_button(loc(LocId::EditRedo), 'R', kbmod::CTRL | vk::Y) {
        tb.redo();
        ctx.needs_rerender();
    }
    if ctx.menubar_menu_button(loc(LocId::EditCut), 'T', kbmod::CTRL | vk::X) {
        tb.cut(ctx.clipboard_mut());
        ctx.needs_rerender();
    }
    if ctx.menubar_menu_button(loc(LocId::EditCopy), 'C', kbmod::CTRL | vk::C) {
        tb.copy(ctx.clipboard_mut());
        ctx.needs_rerender();
    }
    if ctx.menubar_menu_button(loc(LocId::EditPaste), 'P', kbmod::CTRL | vk::V) {
        tb.paste(ctx.clipboard_ref());
        ctx.needs_rerender();
    }
    if state.wants_search.kind != StateSearchKind::Disabled {
        if ctx.menubar_menu_button(loc(LocId::EditFind), 'F', kbmod::CTRL | vk::F) {
            state.wants_search.kind = StateSearchKind::Search;
            state.wants_search.focus = true;
        }
        if ctx.menubar_menu_button(loc(LocId::EditReplace), 'L', kbmod::CTRL | vk::R) {
            state.wants_search.kind = StateSearchKind::Replace;
            state.wants_search.focus = true;
        }
    }
    if ctx.menubar_menu_button(loc(LocId::EditSelectAll), 'A', kbmod::CTRL | vk::A) {
        tb.select_all();
        ctx.needs_rerender();
    }
    ctx.menubar_menu_end();
}

fn draw_menu_view(ctx: &mut Context, state: &mut State) {
    if let Some(doc) = state.documents.active() {
        let mut tb = doc.buffer.borrow_mut();
        let word_wrap = tb.is_word_wrap_enabled();

        // All values on the statusbar are currently document specific.
        if ctx.menubar_menu_button(loc(LocId::ViewFocusStatusbar), 'S', vk::NULL) {
            state.wants_statusbar_focus = true;
        }
        if ctx.menubar_menu_button(loc(LocId::ViewGoToFile), 'F', kbmod::CTRL | vk::P) {
            state.wants_go_to_file = true;
        }
        if ctx.menubar_menu_button(loc(LocId::FileGoto), 'G', kbmod::CTRL | vk::G) {
            state.wants_goto = true;
        }
        if ctx.menubar_menu_checkbox(loc(LocId::ViewWordWrap), 'W', kbmod::ALT | vk::Z, word_wrap) {
            tb.set_word_wrap(!word_wrap);
            ctx.needs_rerender();
        }
    }

    ctx.menubar_menu_end();
}

fn draw_menu_help(ctx: &mut Context, state: &mut State) {
    if ctx.menubar_menu_button(loc(LocId::HelpAbout), 'A', vk::NULL) {
        state.wants_about = true;
    }
    ctx.menubar_menu_end();
}

pub fn draw_dialog_about(ctx: &mut Context, state: &mut State) {
    ctx.modal_begin("about", loc(LocId::AboutDialogTitle));
    {
        ctx.block_begin("content");
        ctx.inherit_focus();
        ctx.attr_padding(Rect::three(1, 2, 1));
        {
            ctx.label("description", "Microsoft Edit");
            ctx.attr_overflow(Overflow::TruncateTail);
            ctx.attr_position(Position::Center);

            ctx.label(
                "version",
                &arena_format!(
                    ctx.arena(),
                    "{}{}",
                    loc(LocId::AboutDialogVersion),
                    env!("CARGO_PKG_VERSION")
                ),
            );
            ctx.attr_overflow(Overflow::TruncateHead);
            ctx.attr_position(Position::Center);

            ctx.label("copyright", "Copyright (c) Microsoft Corp 2025");
            ctx.attr_overflow(Overflow::TruncateTail);
            ctx.attr_position(Position::Center);

            ctx.block_begin("choices");
            ctx.inherit_focus();
            ctx.attr_padding(Rect::three(1, 2, 0));
            ctx.attr_position(Position::Center);
            {
                if ctx.button("ok", loc(LocId::Ok), ButtonStyle::default()) {
                    state.wants_about = false;
                }
                ctx.inherit_focus();
            }
            ctx.block_end();
        }
        ctx.block_end();
    }
    if ctx.modal_end() {
        state.wants_about = false;
    }
}

//! Clipboard facilities for the editor.

/// The builtin, internal clipboard of the editor.
///
/// This is useful particularly when the terminal doesn't support
/// OSC 52 or when the clipboard contents are huge (e.g. 1GiB).
#[derive(Default)]
pub struct Clipboard {
    data: Vec<u8>,
    line_copy: bool,
    wants_host_sync: bool,
}

impl Clipboard {
    /// If true, we should emit a OSC 52 sequence to sync the clipboard
    /// with the hosting terminal.
    pub fn wants_host_sync(&self) -> bool {
        self.wants_host_sync
    }

    /// Call this once the clipboard has been synchronized with the host.
    pub fn mark_as_synchronized(&mut self) {
        self.wants_host_sync = false;
    }

    /// The editor has a special behavior when you have no selection and press
    /// Ctrl+C: It copies the current line to the clipboard. Then, when you
    /// paste it, it inserts the line at *the start* of the current line.
    /// This effectively prepends the current line with the copied line.
    /// `clipboard_line_start` is true in that case.
    pub fn is_line_copy(&self) -> bool {
        self.line_copy
    }

    /// Returns the current contents of the clipboard.
    pub fn read(&self) -> &[u8] {
        &self.data
    }

    /// Fill the clipboard with the given data.
    pub fn write(&mut self, data: Vec<u8>) {
        if !data.is_empty() {
            self.data = data;
            self.line_copy = false;
            self.wants_host_sync = true;
        }
    }

    /// See [`Clipboard::is_line_copy`].
    pub fn write_was_line_copy(&mut self, line_copy: bool) {
        self.line_copy = line_copy;
    }
}

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

//! [`std::cell::RefCell`], but without runtime checks in release builds.

#[cfg(debug_assertions)]
pub use debug::*;
#[cfg(not(debug_assertions))]
pub use release::*;

#[allow(unused)]
#[cfg(debug_assertions)]
mod debug {
    pub type SemiRefCell<T> = std::cell::RefCell<T>;
    pub type Ref<'b, T> = std::cell::Ref<'b, T>;
    pub type RefMut<'b, T> = std::cell::RefMut<'b, T>;
}

#[cfg(not(debug_assertions))]
mod release {
    #[derive(Default)]
    #[repr(transparent)]
    pub struct SemiRefCell<T>(std::cell::UnsafeCell<T>);

    impl<T> SemiRefCell<T> {
        #[inline(always)]
        pub const fn new(value: T) -> Self {
            Self(std::cell::UnsafeCell::new(value))
        }

        #[inline(always)]
        pub const fn as_ptr(&self) -> *mut T {
            self.0.get()
        }

        #[inline(always)]
        pub const fn borrow(&self) -> Ref<'_, T> {
            Ref(unsafe { &*self.0.get() })
        }

        #[inline(always)]
        pub const fn borrow_mut(&self) -> RefMut<'_, T> {
            RefMut(unsafe { &mut *self.0.get() })
        }
    }

    #[repr(transparent)]
    pub struct Ref<'b, T>(&'b T);

    impl<'b, T> Ref<'b, T> {
        #[inline(always)]
        pub fn clone(orig: &Self) -> Self {
            Ref(orig.0)
        }
    }

    impl<'b, T> std::ops::Deref for Ref<'b, T> {
        type Target = T;

        #[inline(always)]
        fn deref(&self) -> &Self::Target {
            self.0
        }
    }

    #[repr(transparent)]
    pub struct RefMut<'b, T>(&'b mut T);

    impl<'b, T> std::ops::Deref for RefMut<'b, T> {
        type Target = T;

        #[inline(always)]
        fn deref(&self) -> &Self::Target {
            self.0
        }
    }

    impl<'b, T> std::ops::DerefMut for RefMut<'b, T> {
        #[inline(always)]
        fn deref_mut(&mut self) -> &mut Self::Target {
            self.0
        }
    }
}

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

//! Provides various high-throughput utilities.

pub mod lines_bwd;
pub mod lines_fwd;
mod memchr2;
mod memset;

pub use lines_bwd::*;
pub use lines_fwd::*;
pub use memchr2::*;
pub use memset::*;

#[cfg(test)]
mod test {
    // Knuth's MMIX LCG
    pub fn make_rng() -> impl FnMut() -> usize {
        let mut state = 1442695040888963407u64;
        move || {
            state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
            state as usize
        }
    }

    pub fn generate_random_text(len: usize) -> String {
        const ALPHABET: &[u8; 20] = b"0123456789abcdef\n\n\n\n";

        let mut rng = make_rng();
        let mut res = String::new();

        for _ in 0..len {
            res.push(ALPHABET[rng() % ALPHABET.len()] as char);
        }

        res
    }

    pub fn count_lines(text: &str) -> usize {
        text.lines().count()
    }
}

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

//! Provides a transparent error type for edit.

use std::{io, result};

use crate::sys;

pub const APP_ICU_MISSING: Error = Error::new_app(0);

/// Edit's transparent `Result` type.
pub type Result<T> = result::Result<T, Error>;

/// Edit's transparent `Error` type.
/// Abstracts over system and application errors.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Error {
    App(u32),
    Icu(u32),
    Sys(u32),
}

impl Error {
    pub const fn new_app(code: u32) -> Self {
        Self::App(code)
    }

    pub const fn new_icu(code: u32) -> Self {
        Self::Icu(code)
    }

    pub const fn new_sys(code: u32) -> Self {
        Self::Sys(code)
    }
}

impl From<io::Error> for Error {
    fn from(err: io::Error) -> Self {
        sys::io_error_to_apperr(err)
    }
}

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

#![feature(
    allocator_api,
    breakpoint,
    cold_path,
    linked_list_cursors,
    maybe_uninit_fill,
    maybe_uninit_slice,
    maybe_uninit_uninit_array_transpose
)]
#![cfg_attr(
    target_arch = "loongarch64",
    feature(stdarch_loongarch, stdarch_loongarch_feature_detection, loongarch_target_feature),
    allow(clippy::incompatible_msrv)
)]
#![allow(clippy::missing_transmute_annotations, clippy::new_without_default, stable_features)]

#[macro_use]
pub mod arena;

pub mod apperr;
pub mod base64;
pub mod buffer;
pub mod cell;
pub mod clipboard;
pub mod document;
pub mod framebuffer;
pub mod fuzzy;
pub mod hash;
pub mod helpers;
pub mod icu;
pub mod input;
pub mod oklab;
pub mod path;
pub mod simd;
pub mod sys;
pub mod tui;
pub mod unicode;
pub mod vt;

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

//! Arena allocators. Small and fast.

#[cfg(debug_assertions)]
mod debug;
mod release;
mod scratch;
mod string;

#[cfg(all(not(doc), debug_assertions))]
pub use self::debug::Arena;
#[cfg(any(doc, not(debug_assertions)))]
pub use self::release::Arena;
pub use self::scratch::{ScratchArena, init, scratch_arena};
pub use self::string::ArenaString;

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

//! Everything related to Unicode lives here.

mod measurement;
mod tables;
mod utf8;

pub use measurement::*;
pub use utf8::*;

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

//! Platform abstractions.

#[cfg(unix)]
mod unix;
#[cfg(windows)]
mod windows;

#[cfg(not(windows))]
pub use std::fs::canonicalize;

#[cfg(unix)]
pub use unix::*;
#[cfg(windows)]
pub use windows::*;

This code segment functions to: Perform a case-sensitive comparison of two ASCII byte sequences, reverting to case-insensitive evaluation upon encountering mismatched characters while preserving original ordering when case-folded equivalents match.

```rust
[TASK_BEGIN]
        let mut tiebreaker = None;
        for (a_byte, b_byte) in iter {
            let a_lower = a_byte.to_ascii_lowercase();
            let b_lower = b_byte.to_ascii_lowercase();
            match a_lower.cmp(&b_lower) {
                Ordering::Equal => {
                    if a_byte != b_byte && tiebreaker.is_none() {
                        tiebreaker = Some(a_byte.cmp(b_byte));
                    }
                }
                other => return other,
            }
        }

        if let Some(tie) = tiebreaker {
            tie
        } else {
[TASK_END]
```