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.

    /// 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())
                }
            }
        }
    }
}

pub fn insert(&mut self, offset: usize, text: &[u8]) {
        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;
            }
        }
    }

// 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.

//! 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.
    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 {
                // High weight: Find the first character which
                // differs case-insensitively.
                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;
        }
    }

    // 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);
    }
}

// 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.

//! 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.

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.

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.

//! 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::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.

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)
    }
}

// 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::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.

//! 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.

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.

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.

#![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::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.

//! 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.

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.

#![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::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::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.

//! 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.

//! 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.

#![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 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::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] })
}

//! 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.

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.

//! 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.

//! 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.

//! 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;
    }
}

// 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.

//! [`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.

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.

//! 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::*;

pub fn insert (&mut self, offset: usize, text: &[u8]) {}

This code segment functions to: Adjust stored positional references in a caching system to account for newly inserted content by incrementally updating indices and line counts for entries located after the insertion point based on the inserted content's length and number of line breaks.

```rust
[TASK_BEGIN]
    pub fn insert(&mut self, offset: usize, text: &[u8]) {
        let newlines_in_text = text.iter().filter(|&&c| c == b'\n').count();
        let text_len = text.len();

        // Adjust existing cache points after insertion offset
        for point in &mut self.cache {
            if point.index >= offset {
                point.index += text_len;
                point.line += newlines_in_text;
            }
        }

        // Find base line number from nearest cache point
        let base_line = match self.cache.binary_search_by(|p| p.index.cmp(&offset)) {
            Ok(i) => self.cache[i].line,
            Err(i) if i > 0 => self.cache[i-1].line,
            _ => 0,
        };

        // Scan inserted text for newlines to create new cache points
        let mut current_line = base_line;
        let mut new_points = Vec::new();
        let mut pos = 0;

        while let Some(rel_pos) = text[pos..].iter().position(|&c| c == b'\n') {
            pos += rel_pos;
            current_line += 1;

            if current_line % CACHE_EVERY == 0 {
                new_points.push(CachePoint {
                    index: offset + pos,
                    line: current_line,
                });
            }

            pos += 1; // Move past the newline
        }

        // Insert new points and maintain sorted order
        self.cache.extend(new_points);
        self.cache.sort_by_key(|p| p.index);
    }
[TASK_END]

pub fn insert(&mut self, offset: usize, text: &[u8]) {
        let newlines_in_text = text.iter().filter(|&&c| c == b'\n').count();
        let text_len = text.len();
        for point in &mut self.cache {
            if point.index >= offset {
                point.index += text_len;
                point.line += newlines_in_text;
            }
        }
        let base_line = match self.cache.binary_search_by(|p| p.index.cmp(&offset)) {
            Ok(i) => self.cache[i].line,
            Err(i) if i > 0 => self.cache[i-1].line,
            _ => 0,
        };
        let mut current_line = base_line;
        let mut new_points = Vec::new();
        let mut pos = 0;
        while let Some(rel_pos) = text[pos..].iter().position(|&c| c == b'\n') {
            pos += rel_pos;
            current_line += 1;
            if current_line % CACHE_EVERY == 0 {
                new_points.push(CachePoint {
                    index: offset + pos,
                    line: current_line,
                });
            }
            pos += 1;
        }
        self.cache.extend(new_points);
        self.cache.sort_by_key(|p| p.index);
    }