use std::ops::RangeInclusive;

use eframe::egui::{
    self, pos2, vec2, Align, Align2, Color32, CursorIcon, Event, EventFilter, FontId, Id, Key,
    Margin, NumExt as _, Pos2, Rect, Response, Stroke, StrokeKind, Ui, Widget,
};

use egui::text::{CCursor, CCursorRange, LayoutJob};

use crate::themes::{GorbieNumberFieldStyle, GorbieTextFieldStyle};

type NumFormatter<'a> = dyn Fn(f64, RangeInclusive<usize>) -> String + 'a;
type NumParser<'a> = dyn Fn(&str) -> Option<f64> + 'a;

fn lcd_font_id(ui: &Ui) -> FontId {
    let style = ui.style();
    let lcd_style = egui::TextStyle::Name("LCD".into());
    style
        .text_styles
        .get(&lcd_style)
        .cloned()
        .unwrap_or_else(|| egui::TextStyle::Monospace.resolve(style))
}

fn paint_scanline(painter: &egui::Painter, rect: Rect, color: Color32, height: f32) {
    let inset = 2.0;
    let available_h = (rect.height() - inset * 2.0).max(0.0);
    let height = height.min(available_h);
    if height <= 0.0 {
        return;
    }

    let y1 = rect.bottom() - inset;
    let y0 = y1 - height;
    let scan_rect = Rect::from_min_max(
        pos2(rect.left() + inset, y0),
        pos2(rect.right() - inset, y1),
    );
    if scan_rect.is_positive() {
        painter.rect_filled(scan_rect, 0.0, color);
    }
}

/// Paint a progress bar in the scanline area.
/// `fraction` is 0.0..=1.0 — the filled portion.
fn paint_progress_scanline(painter: &egui::Painter, rect: Rect, color: Color32, height: f32, fraction: f32) {
    let inset = 2.0;
    let available_h = (rect.height() - inset * 2.0).max(0.0);
    let height = height.min(available_h);
    if height <= 0.0 {
        return;
    }

    let y1 = rect.bottom() - inset;
    let y0 = y1 - height;
    let left = rect.left() + inset;
    let right = rect.right() - inset;
    let total_w = right - left;
    if total_w <= 0.0 {
        return;
    }

    // Background (dim).
    let dim = crate::themes::blend(color, Color32::TRANSPARENT, 0.7);
    let bg_rect = Rect::from_min_max(pos2(left, y0), pos2(right, y1));
    if bg_rect.is_positive() {
        painter.rect_filled(bg_rect, 0.0, dim);
    }

    // Filled portion.
    let fill_w = total_w * fraction.clamp(0.0, 1.0);
    if fill_w > 0.0 {
        let fill_rect = Rect::from_min_max(pos2(left, y0), pos2(left + fill_w, y1));
        painter.rect_filled(fill_rect, 0.0, color);
    }
}

fn selection_rects(galley: &egui::Galley, cursor_range: egui::text::CCursorRange) -> Vec<Rect> {
    if cursor_range.is_empty() {
        return Vec::new();
    }

    let [min, max] = cursor_range.sorted_cursors();
    let min = galley.layout_from_cursor(min);
    let max = galley.layout_from_cursor(max);

    let mut rects = Vec::new();

    for row_idx in min.row..=max.row {
        let row = &galley.rows[row_idx];

        let left = if row_idx == min.row {
            row.x_offset(min.column)
        } else {
            0.0
        };

        let right = if row_idx == max.row {
            row.x_offset(max.column)
        } else {
            let newline_size = if row.ends_with_newline {
                row.size.y / 2.0
            } else {
                0.0
            };
            row.size.x + newline_size
        };

        let rect = Rect::from_min_max(pos2(left, 0.0), pos2(right, row.size.y))
            .translate(row.pos.to_vec2());
        if rect.is_positive() {
            rects.push(rect);
        }
    }

    rects
}

#[derive(Debug, Clone, Copy)]
struct GalleyPlacement {
    rect: Rect,
    pos: Pos2,
}

fn place_galley(galley: &egui::Galley, rect: Rect, align: Align2) -> GalleyPlacement {
    let placement_rect = align
        .align_size_within_rect(galley.size(), rect)
        .intersect(rect);
    let galley_pos = placement_rect.min - galley.rect.min.to_vec2();

    GalleyPlacement {
        rect: placement_rect,
        pos: galley_pos,
    }
}

fn is_scrollable_singleline(
    clip_text: bool,
    rect: Rect,
    placement_rect: Rect,
    galley: &egui::Galley,
) -> bool {
    clip_text && placement_rect.left() == rect.left() && galley.rect.left() == 0.0
}

fn cursor_rect_in_galley(galley: &egui::Galley, cursor: CCursor) -> Rect {
    let cursor = galley.layout_from_cursor(cursor);
    galley.rows.get(cursor.row).map_or_else(
        || Rect::ZERO,
        |row| {
            let x = row.pos.x + row.x_offset(cursor.column);
            Rect::from_min_max(pos2(x, row.min_y()), pos2(x, row.max_y()))
        },
    )
}

fn paint_field_frame(
    painter: &egui::Painter,
    rect: Rect,
    fill: Color32,
    outline: Color32,
    rounding: f32,
) {
    painter.rect_filled(rect, rounding, fill);
    painter.rect_stroke(
        rect,
        rounding,
        Stroke::new(1.0, outline),
        StrokeKind::Inside,
    );
}

fn lcd_ink_color(dark_mode: bool) -> Color32 {
    if dark_mode {
        crate::themes::ral(6027)
    } else {
        crate::themes::ral(9011)
    }
}

fn singleline_margin(ui: &Ui, row_height: f32) -> Margin {
    let padding = ui.spacing().button_padding;
    let row_mod = crate::card_ctx::GRID_ROW_MODULE;
    let target_height = (2.0 * row_mod).max(ui.spacing().interact_size.y);
    let vertical = ((target_height - row_height) * 0.5).at_least(0.0);
    let pad_x = padding.x.round().clamp(0.0, i8::MAX as f32) as i8;
    let pad_y = vertical.round().clamp(0.0, i8::MAX as f32) as i8;
    Margin::symmetric(pad_x, pad_y)
}

fn default_event_filter() -> EventFilter {
    EventFilter {
        horizontal_arrows: true,
        vertical_arrows: true,
        tab: false,
        ..Default::default()
    }
}

fn default_parser(text: &str) -> Option<f64> {
    let text: String = text
        .chars()
        .filter(|c| !c.is_whitespace())
        .map(|c| if c == '−' { '-' } else { c })
        .collect();

    text.parse().ok()
}

fn parse_number(custom_parser: Option<&NumParser<'_>>, value_text: &str) -> Option<f64> {
    custom_parser.map_or_else(|| default_parser(value_text), |parser| parser(value_text))
}

fn layout_lcd_job(
    text: &str,
    font_id: FontId,
    normal_color: Color32,
    wrap_width: f32,
    multiline: bool,
    halign: Align,
) -> LayoutJob {
    let mut job = if multiline {
        LayoutJob::simple(text.to_owned(), font_id, normal_color, wrap_width)
    } else {
        LayoutJob::simple_singleline(text.to_owned(), font_id, normal_color)
    };
    job.halign = halign;
    job
}

#[derive(Clone, Default)]
struct LcdTextEditState {
    cursor: egui::text_selection::TextCursorState,
    singleline_offset: f32,
    last_interaction_time: f64,
}

impl LcdTextEditState {
    fn load(ctx: &egui::Context, id: Id) -> Self {
        ctx.data_mut(|data| data.get_temp(id)).unwrap_or_default()
    }

    fn store(self, ctx: &egui::Context, id: Id) {
        ctx.data_mut(|data| data.insert_temp(id, self));
    }
}

struct LcdTextEditOutput {
    response: Response,
    changed: bool,
}

struct LcdStyle {
    fill: Color32,
    outline: Color32,
    rounding: f32,
    ink: Color32,
    text_color: Color32,
    scanline_height: f32,
}

#[allow(clippy::too_many_arguments)]
fn lcd_text_edit(
    ui: &mut Ui,
    id: Id,
    text: &mut dyn egui::TextBuffer,
    multiline: bool,
    desired_height_rows: usize,
    max_rows: Option<usize>,
    align: Align2,
    style: &LcdStyle,
    progress: Option<f32>,
) -> LcdTextEditOutput {
    let interactive = ui.is_enabled() && text.is_mutable();
    let event_filter = default_event_filter();

    let font_id = lcd_font_id(ui);
    let row_height = ui.fonts_mut(|fonts| fonts.row_height(&font_id));
    let margin = if multiline {
        ui.spacing().button_padding.into()
    } else {
        singleline_margin(ui, row_height)
    };
    let clip_text = !multiline;
    let fill = style.fill;
    let outline = style.outline;
    let ink = style.ink;
    let text_color = style.text_color;
    let scanline_height = style.scanline_height;

    const MIN_WIDTH: f32 = 24.0;
    let wrap_width = (ui.available_width() - margin.sum().x).at_least(MIN_WIDTH);

    let mut galley = ui.fonts_mut(|fonts| {
        fonts.layout_job(layout_lcd_job(
            text.as_str(),
            font_id.clone(),
            text_color,
            wrap_width,
            multiline,
            align.x(),
        ))
    });

    let desired_inner_width = if clip_text {
        wrap_width
    } else {
        galley.size().x.max(wrap_width)
    };
    let min_rows = desired_height_rows.at_least(1);
    let max_rows = max_rows.map(|rows| rows.max(min_rows));
    let desired_height = (min_rows as f32) * row_height;
    let desired_inner_height = match max_rows {
        Some(max_rows) => {
            let max_height = (max_rows as f32) * row_height;
            desired_height.max(galley.size().y.min(max_height))
        }
        None => galley.size().y.max(desired_height),
    };
    let desired_inner_size = vec2(desired_inner_width, desired_inner_height);
    let row_mod = crate::card_ctx::GRID_ROW_MODULE;
    let min_h = (2.0 * row_mod).max(ui.spacing().interact_size.y);
    let min_size = vec2(ui.spacing().interact_size.x, min_h);
    let desired_outer_size = (desired_inner_size + margin.sum()).at_least(min_size);
    let (_auto_id, outer_rect) = ui.allocate_space(desired_outer_size);
    let rect = outer_rect - margin;
    let text_clip_rect = outer_rect.shrink(1.0);

    let allow_drag_to_select =
        !ui.input(|i| i.has_touch_screen()) || ui.memory(|mem| mem.has_focus(id));
    let sense = if interactive {
        if allow_drag_to_select {
            egui::Sense::click_and_drag()
        } else {
            egui::Sense::click()
        }
    } else {
        egui::Sense::hover()
    };
    let mut response = ui.interact(outer_rect, id, sense);

    let mut state = LcdTextEditState::load(ui.ctx(), id);
    let galley_placement = place_galley(&galley, rect, align);
    let galley_pos_unscrolled = galley_placement.pos;
    let scrollable_singleline =
        is_scrollable_singleline(clip_text, rect, galley_placement.rect, &galley);

    if interactive {
        if let Some(pointer_pos) = response.interact_pointer_pos() {
            let scroll_offset = if scrollable_singleline {
                state.singleline_offset
            } else {
                0.0
            };
            let cursor_at_pointer = galley
                .cursor_from_pos(pointer_pos - galley_pos_unscrolled + vec2(scroll_offset, 0.0));

            let is_being_dragged = ui.ctx().is_being_dragged(response.id);
            let did_interact = state.cursor.pointer_interaction(
                ui,
                &response,
                cursor_at_pointer,
                &galley,
                is_being_dragged,
            );

            if did_interact || response.clicked() {
                ui.memory_mut(|mem| mem.request_focus(response.id));
                state.last_interaction_time = ui.input(|i| i.time);
            }
        }

        if response.hovered() {
            ui.ctx().set_cursor_icon(CursorIcon::Text);
        }
    }

    let mut changed = false;
    let has_focus = ui.memory(|mem| mem.has_focus(id));
    paint_field_frame(ui.painter(), outer_rect, fill, outline, style.rounding);
    let os = ui.ctx().os();

    let mut cursor_range = state
        .cursor
        .range(&galley)
        .unwrap_or_else(|| CCursorRange::one(galley.end()));

    if interactive && has_focus {
        ui.memory_mut(|mem| mem.set_focus_lock_filter(id, event_filter));

        let prev_cursor_range = cursor_range;
        let mut selection_changed = false;

        for event in ui.input(|i| i.filtered_events(&event_filter)).iter() {
            let did_mutate_text = match event {
                event if cursor_range.on_event(os, event, &galley, id) => {
                    selection_changed = selection_changed || prev_cursor_range != cursor_range;
                    None
                }
                Event::Copy => {
                    if !cursor_range.is_empty() {
                        ui.ctx()
                            .copy_text(cursor_range.slice_str(text.as_str()).to_owned());
                    }
                    None
                }
                Event::Cut => {
                    if cursor_range.is_empty() {
                        None
                    } else {
                        ui.ctx()
                            .copy_text(cursor_range.slice_str(text.as_str()).to_owned());
                        Some(CCursorRange::one(text.delete_selected(&cursor_range)))
                    }
                }
                Event::Paste(text_to_insert) => {
                    if text_to_insert.is_empty() {
                        None
                    } else {
                        let mut ccursor = text.delete_selected(&cursor_range);
                        if multiline {
                            text.insert_text_at(&mut ccursor, text_to_insert, usize::MAX);
                        } else {
                            let single_line = text_to_insert.replace(['\r', '\n'], " ");
                            text.insert_text_at(&mut ccursor, &single_line, usize::MAX);
                        }
                        Some(CCursorRange::one(ccursor))
                    }
                }
                Event::Text(text_to_insert) => {
                    if text_to_insert.is_empty() || text_to_insert == "\n" || text_to_insert == "\r"
                    {
                        None
                    } else {
                        let mut ccursor = text.delete_selected(&cursor_range);
                        text.insert_text_at(&mut ccursor, text_to_insert, usize::MAX);
                        Some(CCursorRange::one(ccursor))
                    }
                }
                Event::Key {
                    key: Key::Enter,
                    pressed: true,
                    modifiers,
                    ..
                } if modifiers.is_none() => {
                    if multiline {
                        let mut ccursor = text.delete_selected(&cursor_range);
                        text.insert_text_at(&mut ccursor, "\n", usize::MAX);
                        Some(CCursorRange::one(ccursor))
                    } else {
                        ui.memory_mut(|mem| mem.surrender_focus(id));
                        break;
                    }
                }
                Event::Key {
                    key: Key::Backspace,
                    pressed: true,
                    modifiers,
                    ..
                } => {
                    let ccursor = if modifiers.mac_cmd {
                        text.delete_paragraph_before_cursor(&galley, &cursor_range)
                    } else if let Some(cursor) = cursor_range.single() {
                        if modifiers.alt || modifiers.ctrl {
                            text.delete_previous_word(cursor)
                        } else {
                            text.delete_previous_char(cursor)
                        }
                    } else {
                        text.delete_selected(&cursor_range)
                    };
                    Some(CCursorRange::one(ccursor))
                }
                Event::Key {
                    key: Key::Delete,
                    pressed: true,
                    modifiers,
                    ..
                } if !modifiers.shift || os != egui::os::OperatingSystem::Windows => {
                    let ccursor = if modifiers.mac_cmd {
                        text.delete_paragraph_after_cursor(&galley, &cursor_range)
                    } else if let Some(cursor) = cursor_range.single() {
                        if modifiers.alt || modifiers.ctrl {
                            text.delete_next_word(cursor)
                        } else {
                            text.delete_next_char(cursor)
                        }
                    } else {
                        text.delete_selected(&cursor_range)
                    };
                    Some(CCursorRange::one(CCursor {
                        prefer_next_row: true,
                        ..ccursor
                    }))
                }
                _ => None,
            };

            if let Some(new_cursor_range) = did_mutate_text {
                changed = true;
                selection_changed = true;

                galley = ui.fonts_mut(|fonts| {
                    fonts.layout_job(layout_lcd_job(
                        text.as_str(),
                        font_id.clone(),
                        text_color,
                        wrap_width,
                        multiline,
                        align.x(),
                    ))
                });
                cursor_range = new_cursor_range;
            }
        }

        state.cursor.set_char_range(Some(cursor_range));

        if changed || selection_changed {
            state.last_interaction_time = ui.input(|i| i.time);
        }
    }

    let galley_placement = place_galley(&galley, rect, align);
    let mut galley_pos = galley_placement.pos;
    let scrollable_singleline =
        is_scrollable_singleline(clip_text, rect, galley_placement.rect, &galley);

    if scrollable_singleline {
        let cursor_pos = if has_focus {
            galley.pos_from_cursor(cursor_range.primary).min.x
        } else {
            0.0
        };

        let mut offset_x = state.singleline_offset;
        let visible_range = offset_x..=offset_x + desired_inner_size.x;

        if !visible_range.contains(&cursor_pos) {
            if cursor_pos < *visible_range.start() {
                offset_x = cursor_pos;
            } else {
                offset_x = cursor_pos - desired_inner_size.x;
            }
        }

        offset_x = offset_x
            .at_most(galley.size().x - desired_inner_size.x)
            .at_least(0.0);

        state.singleline_offset = offset_x;
        galley_pos -= vec2(offset_x, 0.0);
    } else {
        state.singleline_offset = 0.0;
    }

    let text_painter = ui.painter_at(text_clip_rect);

    if interactive && has_focus {
        let mut highlight_rects = Vec::new();
        let mut invert_text_rects = Vec::new();

        if !cursor_range.is_empty() {
            for selection_rect in selection_rects(&galley, cursor_range) {
                let selection_rect = selection_rect.translate(galley_pos.to_vec2());
                if selection_rect.is_positive() {
                    highlight_rects.push(selection_rect);
                    invert_text_rects.push(selection_rect);
                }
            }
        } else {
            let now = ui.input(|i| i.time);
            if state.last_interaction_time == 0.0 {
                state.last_interaction_time = now;
            }

            let show_block = if ui.visuals().text_cursor.blink {
                let cursor_style = &ui.visuals().text_cursor;
                let total_duration = cursor_style.on_duration + cursor_style.off_duration;

                let time_since_last_interaction = (now - state.last_interaction_time).max(0.0);
                let time_in_cycle = (time_since_last_interaction % (total_duration as f64)) as f32;

                let (show, wake_in) = if time_in_cycle < cursor_style.on_duration {
                    (true, cursor_style.on_duration - time_in_cycle)
                } else {
                    (false, total_duration - time_in_cycle)
                };

                ui.ctx().request_repaint_after_secs(wake_in);
                show
            } else {
                true
            };

            if show_block {
                let cursor = cursor_range.primary;
                let char_count = text.as_str().chars().count();
                let cursor_rect = cursor_rect_in_galley(&galley, cursor);
                let glyph_width = ui.fonts_mut(|fonts| fonts.glyph_width(&font_id, '0'));

                let cursor_width = if cursor.index < char_count {
                    let next = cursor_rect_in_galley(&galley, cursor + 1);
                    let width = next.min.x - cursor_rect.min.x;
                    if width > 0.0 {
                        width
                    } else {
                        glyph_width
                    }
                } else {
                    glyph_width
                };

                let caret_rect = Rect::from_min_max(
                    pos2(cursor_rect.min.x, cursor_rect.min.y),
                    pos2(cursor_rect.min.x + cursor_width, cursor_rect.max.y),
                )
                .translate(galley_pos.to_vec2());

                if caret_rect.is_positive() {
                    highlight_rects.push(caret_rect);
                    if cursor.index < char_count {
                        invert_text_rects.push(caret_rect);
                    }
                }
            }
        }

        for rect in &highlight_rects {
            text_painter.rect_filled(*rect, 0.0, ink);
        }

        text_painter.galley(galley_pos, galley.clone(), text_color);

        for rect in invert_text_rects {
            let clip = text_clip_rect.intersect(rect);
            if clip.is_positive() {
                let overlay = ui.painter_at(clip);
                overlay.galley_with_override_text_color(galley_pos, galley.clone(), fill);
            }
        }

        if let Some(fraction) = progress {
            paint_progress_scanline(ui.painter(), outer_rect, ink, scanline_height, fraction);
        } else {
            paint_scanline(ui.painter(), outer_rect, ink, scanline_height);
        }
    } else {
        text_painter.galley(galley_pos, galley.clone(), text_color);
        if let Some(fraction) = progress {
            paint_progress_scanline(ui.painter(), outer_rect, ink, scanline_height, fraction);
        }
    }

    if changed {
        response.mark_changed();
    }

    state.store(ui.ctx(), id);

    LcdTextEditOutput { response, changed }
}

#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
pub struct NumberField<'a, Num: egui::emath::Numeric> {
    value: &'a mut Num,
    speed: f64,
    constrain_value: Option<&'a dyn Fn(Num, Num) -> Num>,
    prefix: String,
    suffix: String,
    min_decimals: usize,
    max_decimals: Option<usize>,
    custom_formatter: Option<&'a NumFormatter<'a>>,
    custom_parser: Option<&'a NumParser<'a>>,
    update_while_editing: bool,
    gorbie_style: Option<GorbieNumberFieldStyle>,
}

impl<'a, Num: egui::emath::Numeric> NumberField<'a, Num> {
    pub fn new(value: &'a mut Num) -> Self {
        Self {
            value,
            speed: 1.0,
            constrain_value: None,
            prefix: String::new(),
            suffix: String::new(),
            min_decimals: 0,
            max_decimals: None,
            custom_formatter: None,
            custom_parser: None,
            update_while_editing: true,
            gorbie_style: None,
        }
    }

    /// Apply a constraint function to all user-produced changes (drag or committed edit).
    ///
    /// This is useful for clamping, snapping, or other domain-specific normalization without
    /// baking policy into the widget.
    pub fn constrain_value(mut self, constrain: &'a dyn Fn(Num, Num) -> Num) -> Self {
        self.constrain_value = Some(constrain);
        self
    }

    pub fn speed(mut self, speed: f64) -> Self {
        self.speed = speed;
        self
    }

    pub fn prefix(mut self, prefix: impl Into<String>) -> Self {
        self.prefix = prefix.into();
        self
    }

    pub fn suffix(mut self, suffix: impl Into<String>) -> Self {
        self.suffix = suffix.into();
        self
    }

    pub fn min_decimals(mut self, min_decimals: usize) -> Self {
        self.min_decimals = min_decimals;
        self
    }

    pub fn max_decimals(mut self, max_decimals: usize) -> Self {
        self.max_decimals = Some(max_decimals);
        self
    }

    pub fn max_decimals_opt(mut self, max_decimals: Option<usize>) -> Self {
        self.max_decimals = max_decimals;
        self
    }

    pub fn update_while_editing(mut self, update: bool) -> Self {
        self.update_while_editing = update;
        self
    }

    pub fn custom_formatter(mut self, formatter: &'a NumFormatter) -> Self {
        self.custom_formatter = Some(formatter);
        self
    }

    pub fn custom_parser(mut self, parser: &'a NumParser<'a>) -> Self {
        self.custom_parser = Some(parser);
        self
    }
}

impl<Num: egui::emath::Numeric> Widget for NumberField<'_, Num> {
    fn ui(self, ui: &mut Ui) -> Response {
        let Self {
            value,
            speed,
            constrain_value,
            prefix,
            suffix,
            min_decimals,
            max_decimals,
            custom_formatter,
            custom_parser,
            update_while_editing,
            gorbie_style,
        } = self;

        let enabled = ui.is_enabled();
        let gstyle =
            gorbie_style.unwrap_or_else(|| GorbieNumberFieldStyle::from(ui.style().as_ref()));

        let dark_mode = ui.visuals().dark_mode;
        let ink = lcd_ink_color(dark_mode);

        let outline = gstyle.outline;
        let fill = if enabled {
            gstyle.fill
        } else {
            crate::themes::blend(gstyle.fill, ui.visuals().window_fill, 0.65)
        };
        let text_color = if enabled {
            ink
        } else {
            crate::themes::blend(ink, fill, 0.55)
        };

        let font_id = lcd_font_id(ui);
        let row_height = ui.fonts_mut(|fonts| fonts.row_height(&font_id));
        let margin = singleline_margin(ui, row_height);
        let mut desired_width = (ui.spacing().interact_size.x - margin.sum().x).at_least(24.0);

        let id = ui.next_auto_id();
        let is_editing = enabled
            && ui.memory_mut(|mem| {
                mem.interested_in_focus(id, ui.layer_id());
                mem.has_focus(id)
            });

        let aim_rad = ui.input(|i| i.aim_radius() as f64);
        let is_slow_speed = ui.input(|i| i.modifiers.shift_only()) && ui.ctx().is_being_dragged(id);

        let auto_decimals = if Num::INTEGRAL {
            0
        } else {
            (aim_rad / speed.abs()).log10().ceil().clamp(0.0, 15.0) as usize
        };
        let auto_decimals = auto_decimals + is_slow_speed as usize;
        let max_decimals = max_decimals
            .unwrap_or(auto_decimals + 2)
            .at_least(min_decimals);
        let auto_decimals = auto_decimals.clamp(min_decimals, max_decimals);

        let mut value_f64 = value.to_f64();
        let kb_change = if is_editing {
            ui.input_mut(|input| {
                input.count_and_consume_key(egui::Modifiers::NONE, Key::ArrowUp) as f64
                    - input.count_and_consume_key(egui::Modifiers::NONE, Key::ArrowDown) as f64
            })
        } else {
            0.0
        };

        let mut kb_changed = false;
        if kb_change != 0.0 {
            let kb_step = if Num::INTEGRAL { 1.0 } else { speed };
            let mut proposed_f64 = value_f64 + kb_step * kb_change;
            if Num::INTEGRAL {
                proposed_f64 = proposed_f64.round();
            } else {
                proposed_f64 = egui::emath::round_to_decimals(proposed_f64, auto_decimals);
            }

            let proposed_value = Num::from_f64(proposed_f64);
            let mut new_value = proposed_value;
            if let Some(constrain_value) = constrain_value {
                new_value = constrain_value(*value, new_value);
            }

            if new_value != *value {
                *value = new_value;
                value_f64 = new_value.to_f64();
                kb_changed = true;
                ui.data_mut(|data| {
                    data.remove::<String>(id);
                    data.remove_temp::<String>(id);
                });
            }
        }

        let value_text = match custom_formatter {
            Some(formatter) => formatter(value_f64, auto_decimals..=max_decimals),
            None => ui
                .style()
                .number_formatter
                .format(value_f64, auto_decimals..=max_decimals),
        };
        let display_text = format!("{prefix}{value_text}{suffix}");
        let display_galley = ui.fonts_mut(|fonts| {
            fonts.layout_job(layout_lcd_job(
                &display_text,
                font_id.clone(),
                text_color,
                desired_width,
                false,
                Align::Center,
            ))
        });
        desired_width = desired_width.max(display_galley.size().x);

        if is_editing {
            let mut edit_text = ui
                .data_mut(|data| data.remove_temp::<String>(id))
                .unwrap_or_else(|| value_text.clone());

            let output = lcd_text_edit(
                ui,
                id,
                &mut edit_text,
                false,
                1,
                None,
                Align2::CENTER_CENTER,
                &LcdStyle { fill, outline, rounding: gstyle.rounding, ink, text_color, scanline_height: gstyle.scanline_height },
                None,
            );
            let mut response = output.response;
            if kb_changed {
                response.mark_changed();
            }

            let commit = if update_while_editing {
                output.changed
            } else {
                response.lost_focus() && !ui.input(|i| i.key_pressed(Key::Escape))
            };

            if commit {
                if let Some(mut parsed_value) = parse_number(custom_parser, &edit_text) {
                    if Num::INTEGRAL {
                        parsed_value = parsed_value.round();
                    }
                    let mut new_value = Num::from_f64(parsed_value);
                    if let Some(constrain_value) = constrain_value {
                        new_value = constrain_value(*value, new_value);
                    }
                    if new_value != *value {
                        *value = new_value;
                        response.mark_changed();
                    }
                }
            }

            if ui.memory(|mem| mem.has_focus(id)) {
                ui.data_mut(|data| data.insert_temp(id, edit_text));
            } else {
                ui.data_mut(|data| data.remove::<String>(id));
            }

            response
        } else {
            let desired_inner_width = desired_width.max(display_galley.size().x).max(ui.available_width() - margin.sum().x);
            let desired_inner_height = (ui.spacing().interact_size.y - margin.sum().y)
                .max(row_height)
                .max(display_galley.size().y);
            let desired_inner_size = vec2(desired_inner_width, desired_inner_height);
            let row_mod = crate::card_ctx::GRID_ROW_MODULE;
            let min_height = (2.0 * row_mod).max(ui.spacing().interact_size.y);
            let desired_outer_size = (desired_inner_size + margin.sum())
                .at_least(vec2(ui.spacing().interact_size.x, min_height));
            let (_auto_id, outer_rect) = ui.allocate_space(desired_outer_size);
            let rect = outer_rect - margin;
            let mut response = ui.interact(outer_rect, id, egui::Sense::click_and_drag());

            paint_field_frame(ui.painter(), outer_rect, fill, outline, gstyle.rounding);

            let galley_pos = place_galley(&display_galley, rect, Align2::CENTER_CENTER).pos;
            ui.painter_at(outer_rect.shrink(1.0))
                .galley(galley_pos, display_galley, text_color);

            if enabled {
                let precise_drag_id = id.with("precise_drag_value");
                if ui.input(|i| i.pointer.any_pressed() || i.pointer.any_released()) {
                    ui.data_mut(|data| data.remove::<f64>(precise_drag_id));
                }

                if response.clicked() {
                    ui.memory_mut(|mem| mem.request_focus(id));
                    ui.data_mut(|data| data.insert_temp(id, value_text.clone()));

                    let mut state = LcdTextEditState::load(ui.ctx(), id);
                    let len = value_text.chars().count();
                    state.cursor.set_char_range(Some(CCursorRange::two(
                        CCursor::default(),
                        CCursor::new(len),
                    )));
                    state.last_interaction_time = ui.input(|i| i.time);
                    state.store(ui.ctx(), id);
                } else if response.dragged() {
                    ui.ctx().set_cursor_icon(CursorIcon::ResizeHorizontal);
                    let mdelta = response.drag_delta();
                    let delta_points = mdelta.x - mdelta.y;
                    let speed = if is_slow_speed { speed / 10.0 } else { speed };
                    let delta_value = delta_points as f64 * speed;
                    if delta_value != 0.0 {
                        let precise_value =
                            ui.data_mut(|data| data.get_temp::<f64>(precise_drag_id));
                        let mut precise_value = precise_value.unwrap_or(value_f64) + delta_value;

                        let proposed_f64 = if Num::INTEGRAL {
                            precise_value.round()
                        } else {
                            precise_value
                        };

                        let proposed_value = Num::from_f64(proposed_f64);
                        let mut new_value = proposed_value;
                        if let Some(constrain_value) = constrain_value {
                            new_value = constrain_value(*value, new_value);
                        }

                        if new_value != *value {
                            *value = new_value;
                            response.mark_changed();
                        }

                        if new_value != proposed_value {
                            precise_value += new_value.to_f64() - proposed_value.to_f64();
                        }
                        ui.data_mut(|data| data.insert_temp::<f64>(precise_drag_id, precise_value));
                    }
                }
            }

            response
        }
    }
}

impl<Num: egui::emath::Numeric> crate::themes::Styled for NumberField<'_, Num> {
    type Style = GorbieNumberFieldStyle;

    fn set_style(&mut self, style: Option<Self::Style>) {
        self.gorbie_style = style;
    }
}

#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
pub struct TextField<'a> {
    text: &'a mut dyn egui::TextBuffer,
    multiline: bool,
    gorbie_style: Option<GorbieTextFieldStyle>,
    rows: Option<usize>,
    max_rows: Option<usize>,
    progress: Option<f32>,
}

impl<'a> TextField<'a> {
    pub fn singleline(text: &'a mut dyn egui::TextBuffer) -> Self {
        Self {
            text,
            multiline: false,
            gorbie_style: None,
            rows: None,
            max_rows: None,
            progress: None,
        }
    }

    pub fn multiline(text: &'a mut dyn egui::TextBuffer) -> Self {
        Self {
            text,
            multiline: true,
            gorbie_style: None,
            rows: None,
            max_rows: None,
            progress: None,
        }
    }

    /// Show a progress bar in the scanline area (0.0..=1.0).
    /// `None` = no progress bar, `Some(fraction)` = filled to that fraction.
    pub fn progress(mut self, progress: Option<f32>) -> Self {
        self.progress = progress;
        self
    }

    pub fn rows(mut self, rows: usize) -> Self {
        self.rows = Some(rows.max(1));
        self
    }

    pub fn max_rows(mut self, rows: usize) -> Self {
        self.max_rows = Some(rows.max(1));
        self
    }
}

impl Widget for TextField<'_> {
    fn ui(self, ui: &mut Ui) -> Response {
        let Self {
            text,
            multiline,
            gorbie_style,
            rows,
            max_rows,
            progress,
        } = self;

        let enabled = ui.is_enabled();
        let gstyle =
            gorbie_style.unwrap_or_else(|| GorbieTextFieldStyle::from(ui.style().as_ref()));

        let outline = gstyle.outline;
        let fill = if enabled {
            gstyle.fill
        } else {
            crate::themes::blend(gstyle.fill, ui.visuals().window_fill, 0.65)
        };

        let ink = lcd_ink_color(ui.visuals().dark_mode);
        let text_color = if enabled {
            ink
        } else {
            crate::themes::blend(ink, fill, 0.55)
        };

        let align = if multiline {
            Align2::LEFT_TOP
        } else {
            Align2::LEFT_CENTER
        };
        let min_rows = if multiline { rows.unwrap_or(4) } else { 1 };
        let output = lcd_text_edit(
            ui,
            ui.next_auto_id(),
            text,
            multiline,
            min_rows,
            max_rows,
            align,
            &LcdStyle { fill, outline, rounding: gstyle.rounding, ink, text_color, scanline_height: gstyle.scanline_height },
            progress,
        );

        output.response
    }
}

impl crate::themes::Styled for TextField<'_> {
    type Style = GorbieTextFieldStyle;

    fn set_style(&mut self, style: Option<Self::Style>) {
        self.gorbie_style = style;
    }
}
