// SPDX-FileCopyrightText: 2024 Andrey Kutejko <andy128k@gmail.com>
// SPDX-FileCopyrightText: 2026 Wladimir Palant https://palant.info/
//
// SPDX-License-Identifier: GPL-3.0-or-later

use super::data_presentation::DataPresentation;
use crate::{
    intviewer::{file_input_source::FileInputSource, input_modes::InputMode},
    utils::{CONTROL, Max, NO_MOD},
};
use gtk::{gdk, glib, graphene, pango, prelude::*, subclass::prelude::*};
use std::{path::Path, sync::Arc};

const HEXDUMP_FIXED_LIMIT: u32 = 16;

mod imp {
    use super::*;
    use crate::{
        intviewer::data_presentation::{DataPresentationMode, next_tab_position},
        utils::MinMax,
    };
    use std::{
        cell::{Cell, RefCell},
        sync::OnceLock,
    };

    #[derive(glib::Properties)]
    #[properties(wrapper_type = super::TextRender)]
    pub struct TextRender {
        #[property(get, set = Self::set_hadjustment, override_interface = gtk::Scrollable)]
        hadjustment: RefCell<Option<gtk::Adjustment>>,
        hadjustment_handler_ids: RefCell<Vec<glib::SignalHandlerId>>,

        #[property(get, set = Self::set_vadjustment, override_interface = gtk::Scrollable)]
        vadjustment: RefCell<Option<gtk::Adjustment>>,
        vadjustment_handler_ids: RefCell<Vec<glib::SignalHandlerId>>,

        #[property(get, set = Self::set_hscroll_policy, override_interface = gtk::Scrollable)]
        hscroll_policy: Cell<gtk::ScrollablePolicy>,
        #[property(get, set = Self::set_vscroll_policy, override_interface = gtk::Scrollable)]
        vscroll_policy: Cell<gtk::ScrollablePolicy>,

        #[property(get, set = Self::set_display_mode, builder(TextRenderDisplayMode::default()))]
        display_mode: Cell<TextRenderDisplayMode>,

        pub font_desc: RefCell<pango::FontDescription>,
        #[property(get, set = Self::set_monospaced_font)]
        pub(super) monospaced_font: RefCell<String>,
        pub(super) scale_factor: Cell<f64>,
        #[property(get, set = Self::set_tab_size)]
        tab_size: Cell<u32>,
        #[property(get, set = Self::set_wrap_mode)]
        wrap_mode: Cell<bool>,
        #[property(get, set = Self::set_encoding)]
        encoding: RefCell<String>,
        #[property(get, set = Self::set_fixed_limit)]
        fixed_limit: Cell<u32>,
        #[property(get, set = Self::set_hexadecimal_offset)]
        hexadecimal_offset: Cell<bool>,

        pub input_mode: RefCell<Arc<InputMode>>,
        pub data_presentation: RefCell<DataPresentation>,

        #[property(get, set)]
        chars_per_line: Cell<u32>,
        #[property(get, set)]
        max_column: Cell<u32>,
        displayed_max_column: Cell<u32>,
        pub last_displayed_offset: Cell<u64>,
        #[property(get, set)]
        pub lines_displayed: Cell<i32>,
        pub marker_start: Cell<u64>,
        pub marker_end: Cell<u64>,
        /// The button pressed to start a selection
        button: Cell<Option<u32>>,
        hexmode_marker_on_hexdump: Cell<bool>,

        pub char_width: Cell<i32>,
        pub char_height: Cell<i32>,
    }

    #[glib::object_subclass]
    impl ObjectSubclass for TextRender {
        const NAME: &'static str = "GnomeCmdTextRender";
        type Type = super::TextRender;
        type ParentType = gtk::Widget;
        type Interfaces = (gtk::Scrollable,);

        fn new() -> Self {
            let monospaced_font = String::from("Monospace 12");
            Self {
                hadjustment: Default::default(),
                hadjustment_handler_ids: Default::default(),

                vadjustment: Default::default(),
                vadjustment_handler_ids: Default::default(),

                hscroll_policy: Cell::new(gtk::ScrollablePolicy::Minimum),
                vscroll_policy: Cell::new(gtk::ScrollablePolicy::Minimum),

                display_mode: Cell::default(),
                font_desc: RefCell::new(pango::FontDescription::from_string(&monospaced_font)),
                monospaced_font: RefCell::new(monospaced_font),
                scale_factor: Cell::new(1.0),
                tab_size: Cell::new(8),
                wrap_mode: Default::default(),
                encoding: RefCell::new(String::from("ASCII")),
                fixed_limit: Cell::new(80),
                hexadecimal_offset: Default::default(),

                input_mode: Default::default(),
                data_presentation: RefCell::new(DataPresentation::new()),

                chars_per_line: Default::default(),
                max_column: Default::default(),
                displayed_max_column: Default::default(),
                last_displayed_offset: Default::default(),
                lines_displayed: Default::default(),
                marker_start: Default::default(),
                marker_end: Default::default(),
                button: Default::default(),
                hexmode_marker_on_hexdump: Default::default(),

                char_width: Default::default(),
                char_height: Default::default(),
            }
        }
    }

    #[glib::derived_properties]
    impl ObjectImpl for TextRender {
        fn constructed(&self) {
            self.parent_constructed();

            let this = self.obj();

            this.set_focusable(true);
            this.setup_current_font();

            let scroll_controller = gtk::EventControllerScroll::builder()
                .flags(gtk::EventControllerScrollFlags::VERTICAL)
                .build();
            scroll_controller.connect_scroll(glib::clone!(
                #[weak(rename_to = imp)]
                self,
                #[upgrade_or]
                glib::Propagation::Proceed,
                move |_, _dx, dy| {
                    imp.scroll(dy);
                    glib::Propagation::Stop
                }
            ));
            this.add_controller(scroll_controller);

            let button_gesture = gtk::GestureClick::new();
            button_gesture.connect_pressed(glib::clone!(
                #[weak(rename_to = imp)]
                self,
                move |gesture, n_press, x, y| imp.button_press(
                    gesture.current_button(),
                    n_press,
                    x,
                    y
                )
            ));
            button_gesture.connect_released(glib::clone!(
                #[weak(rename_to = imp)]
                self,
                move |gesture, _n_press, x, y| imp.button_release(gesture.current_button(), x, y)
            ));
            this.add_controller(button_gesture);

            let motion_controller = gtk::EventControllerMotion::new();
            motion_controller.connect_motion(glib::clone!(
                #[weak(rename_to = imp)]
                self,
                move |_, x, y| imp.motion_notify(x, y)
            ));
            this.add_controller(motion_controller);

            let key_controller = gtk::EventControllerKey::new();
            key_controller.connect_key_pressed(glib::clone!(
                #[weak(rename_to = imp)]
                self,
                #[upgrade_or]
                glib::Propagation::Proceed,
                move |_, key, _, modifiers| imp.key_pressed(key, modifiers)
            ));
            this.add_controller(key_controller);
        }

        fn signals() -> &'static [glib::subclass::Signal] {
            static SIGNALS: OnceLock<Vec<glib::subclass::Signal>> = OnceLock::new();
            SIGNALS.get_or_init(|| {
                vec![glib::subclass::Signal::builder("text-status-changed").build()]
            })
        }
    }

    impl WidgetImpl for TextRender {
        fn size_allocate(&self, width: i32, height: i32, baseline: i32) {
            self.parent_size_allocate(width, height, baseline);
            self.update_viewport_size(width, height);
        }

        fn measure(&self, orientation: gtk::Orientation, _for_size: i32) -> (i32, i32, i32, i32) {
            const DEFAULT_WIDTH: i32 = 100;
            const DEFAULT_HEIGHT: i32 = 200;
            if orientation == gtk::Orientation::Horizontal {
                (DEFAULT_WIDTH, DEFAULT_WIDTH, -1, -1)
            } else {
                (DEFAULT_HEIGHT, DEFAULT_HEIGHT, -1, -1)
            }
        }

        fn snapshot(&self, snapshot: &gtk::Snapshot) {
            let display_mode = self.obj().display_mode();

            let char_height = self.char_height.get();
            let width = self.obj().width();
            let height = self.obj().height();

            snapshot.push_clip(&graphene::Rect::new(0.0, 0.0, width as f32, height as f32));

            let column = self.obj().column();

            let mut offset = self.obj().current_offset();
            let mut y = 0;

            let mut marker_start = self.marker_start.get();
            let mut marker_end = self.marker_end.get();
            if marker_start > marker_end {
                std::mem::swap(&mut marker_start, &mut marker_end);
            }

            let dp = self.data_presentation.borrow();
            let text_color = self.obj().color();
            let display_options = DisplayOptions {
                text_color,
                ..Default::default()
            };
            let display_options_alternate = DisplayOptions {
                text_color,
                ..DisplayOptions::alternate_marker()
            };
            self.displayed_max_column.set(0);
            loop {
                let eol_offset = dp.end_of_line_offset(&self.input_mode.borrow(), offset);
                if eol_offset == offset {
                    break;
                }

                snapshot.save();
                snapshot.translate(&graphene::Point::new(
                    -(self.char_width.get() * column) as f32,
                    y as f32,
                ));
                match display_mode {
                    TextRenderDisplayMode::Text => self.text_mode_display_line(
                        snapshot,
                        offset,
                        eol_offset,
                        marker_start,
                        marker_end,
                        &display_options,
                    ),
                    TextRenderDisplayMode::FixedWidth => self.fixed_width_mode_display_line(
                        snapshot,
                        offset,
                        eol_offset,
                        marker_start,
                        marker_end,
                        &display_options,
                    ),
                    TextRenderDisplayMode::Hexdump => self.hex_mode_display_line(
                        snapshot,
                        offset,
                        eol_offset,
                        marker_start,
                        marker_end,
                        &display_options,
                        &display_options_alternate,
                    ),
                }
                snapshot.restore();

                offset = eol_offset;

                y += char_height;
                if y >= height {
                    break;
                }
            }

            snapshot.pop();

            self.last_displayed_offset.set(offset);
        }
    }

    impl ScrollableImpl for TextRender {}

    impl TextRender {
        fn set_hadjustment(&self, adjustment: Option<gtk::Adjustment>) {
            if let Some(adjustment) = self.obj().hadjustment() {
                for handler_id in self.hadjustment_handler_ids.take() {
                    adjustment.disconnect(handler_id);
                }
            }

            let adjustment = adjustment.unwrap_or_default();
            self.hadjustment.replace(Some(adjustment.clone()));
            self.update_hadjustment_limits(&adjustment);

            self.hadjustment_handler_ids
                .borrow_mut()
                .push(adjustment.connect_changed(glib::clone!(
                    #[weak(rename_to = imp)]
                    self,
                    move |_| imp.hadjustment_update()
                )));
            self.hadjustment_handler_ids
                .borrow_mut()
                .push(adjustment.connect_value_changed(glib::clone!(
                    #[weak(rename_to = imp)]
                    self,
                    move |_| imp.hadjustment_update()
                )));

            self.hadjustment_update();
        }

        fn hadjustment_update(&self) {
            self.obj().emit_text_status_changed();
            self.obj().queue_draw();
        }

        fn set_vadjustment(&self, adjustment: Option<gtk::Adjustment>) {
            if let Some(adjustment) = self.obj().vadjustment() {
                for handler_id in self.vadjustment_handler_ids.take() {
                    adjustment.disconnect(handler_id);
                }
            }

            let adjustment = adjustment.unwrap_or_default();
            self.vadjustment.replace(Some(adjustment.clone()));
            self.update_vadjustment_limits(&adjustment);

            self.vadjustment_handler_ids
                .borrow_mut()
                .push(adjustment.connect_changed(glib::clone!(
                    #[weak(rename_to = imp)]
                    self,
                    move |_| imp.vadjustment_update()
                )));
            self.vadjustment_handler_ids
                .borrow_mut()
                .push(adjustment.connect_value_changed(glib::clone!(
                    #[weak(rename_to = imp)]
                    self,
                    move |_| imp.vadjustment_update()
                )));

            self.vadjustment_update();
        }

        fn vadjustment_update(&self) {
            if let Some(vadjustment) = self.obj().vadjustment() {
                let value = vadjustment.value() as u64;
                let new_value = self
                    .data_presentation
                    .borrow()
                    .align_offset_to_line_start(&self.input_mode.borrow(), value);
                if new_value != value {
                    vadjustment.set_value(new_value as f64);
                }
            }

            self.obj().emit_text_status_changed();
            self.obj().queue_draw();
        }

        pub fn update_adjustments_limits(&self) {
            if let Some(vadjustment) = self.obj().vadjustment() {
                self.update_vadjustment_limits(&vadjustment);
            }
            if let Some(hadjustment) = self.obj().hadjustment() {
                self.update_hadjustment_limits(&hadjustment);
            }
        }

        fn update_hadjustment_limits(&self, adjustment: &gtk::Adjustment) {
            adjustment.set_lower(0.0);
            adjustment.set_upper(match self.data_presentation.borrow().mode() {
                DataPresentationMode::NoWrap => self.max_column.get() as f64,
                DataPresentationMode::Wrap => 0.0,
                DataPresentationMode::Fixed => self.fixed_limit.get() as f64,
                DataPresentationMode::Binary => {
                    (hex_mode_column_layout().1 + HEXDUMP_FIXED_LIMIT) as f64
                }
            });
            adjustment.set_step_increment(1.0);
            adjustment.set_page_increment(5.0);
            adjustment.set_page_size(self.chars_per_line.get() as f64 - 1.0);
        }

        fn update_vadjustment_limits(&self, adjustment: &gtk::Adjustment) {
            adjustment.set_lower(0.0);
            adjustment.set_upper(self.input_mode.borrow().max_offset().saturating_sub(1) as f64);
            adjustment.set_step_increment(1.0);
            adjustment.set_page_increment(10.0);
            adjustment.set_page_size(10.0);
        }

        fn set_hscroll_policy(&self, policy: gtk::ScrollablePolicy) {
            self.hscroll_policy.set(policy);
            self.obj().queue_resize();
        }

        fn set_vscroll_policy(&self, policy: gtk::ScrollablePolicy) {
            self.vscroll_policy.set(policy);
            self.obj().queue_resize();
        }

        pub(super) fn update_viewport_size(&self, width: i32, height: i32) {
            let char_width = self.char_width.get();
            let char_height = self.char_height.get();

            if width > 0 && char_width > 0 {
                let chars = (width / char_width) as u32;
                self.chars_per_line.set(chars);
                self.data_presentation.borrow_mut().set_wrap_limit(chars);
                self.obj().queue_draw();
            }

            self.lines_displayed.set(if char_height > 0 {
                height / char_height
            } else {
                10
            });

            if let Some(adjustment) = self.obj().hadjustment() {
                self.update_hadjustment_limits(&adjustment);
            }
            if let Some(adjustment) = self.obj().vadjustment() {
                self.update_vadjustment_limits(&adjustment);
            }
        }

        fn set_display_mode(&self, mode: TextRenderDisplayMode) {
            self.display_mode.set(mode);

            {
                let mut dp = self.data_presentation.borrow_mut();
                match mode {
                    TextRenderDisplayMode::Text => {
                        dp.set_mode(if self.wrap_mode.get() {
                            DataPresentationMode::Wrap
                        } else {
                            DataPresentationMode::NoWrap
                        });
                    }
                    TextRenderDisplayMode::FixedWidth => {
                        if let Some(char_size) = self.input_mode.borrow().char_size()
                            && char_size > 0
                        {
                            dp.set_fixed_count(self.fixed_limit.get() * char_size);
                            dp.set_mode(DataPresentationMode::Binary);
                        } else {
                            dp.set_fixed_count(self.fixed_limit.get());
                            dp.set_mode(DataPresentationMode::Fixed);
                        }
                    }
                    TextRenderDisplayMode::Hexdump => {
                        dp.set_fixed_count(HEXDUMP_FIXED_LIMIT);
                        dp.set_mode(DataPresentationMode::Binary);
                    }
                }
            }
            // self.obj().setup_current_font();
            if let Some(hadjustment) = self.obj().hadjustment() {
                self.update_hadjustment_limits(&hadjustment);
                hadjustment.set_value(0.0);
            }
            if let Some(vadjustment) = self.obj().vadjustment() {
                vadjustment.set_value(self.data_presentation.borrow().align_offset_to_line_start(
                    &self.input_mode.borrow(),
                    self.obj().current_offset(),
                ) as f64);
            }

            self.obj().queue_draw();
        }

        fn set_monospaced_font(&self, font: &str) {
            self.monospaced_font.replace(font.to_owned());
            self.obj().setup_current_font();
            self.obj().queue_draw();
        }

        fn set_tab_size(&self, tab_size: u32) {
            if tab_size > 0 {
                self.tab_size.set(tab_size);
                self.data_presentation.borrow_mut().set_tab_size(tab_size);
                self.obj().queue_draw();
            }
        }

        fn set_wrap_mode(&self, wrap_mode: bool) {
            self.wrap_mode.set(wrap_mode);
            if self.obj().display_mode() == TextRenderDisplayMode::Text {
                if let Some(hadjustment) = self.obj().hadjustment() {
                    hadjustment.set_value(0.0);
                }
                self.data_presentation.borrow_mut().set_mode(if wrap_mode {
                    DataPresentationMode::Wrap
                } else {
                    DataPresentationMode::NoWrap
                });
                self.update_adjustments_limits();
                self.obj().emit_text_status_changed();
            }
            self.obj().queue_draw();
        }

        fn set_encoding(&self, encoding: String) {
            {
                let mut input_mode = self.input_mode.borrow_mut();
                if let Some(input_mode) = Arc::get_mut(&mut input_mode) {
                    self.encoding.replace(encoding.clone());
                    input_mode.set_mode(&encoding);
                    self.filter_undisplayable_chars(input_mode);
                } else {
                    eprintln!(
                        "Failed setting encoding, could not get mutable input mode reference"
                    );
                    return;
                }
            }

            if self.display_mode.get() == TextRenderDisplayMode::FixedWidth {
                // Encoding could change data presentation mode, do the necessary updates.
                self.set_display_mode(self.display_mode.get());
            } else {
                if let Some(vadjustment) = self.obj().vadjustment() {
                    vadjustment.set_value(
                        self.data_presentation.borrow().align_offset_to_line_start(
                            &self.input_mode.borrow(),
                            self.obj().current_offset(),
                        ) as f64,
                    );
                }

                self.obj().queue_draw();
            }
        }

        fn filter_undisplayable_chars(&self, input_mode: &mut InputMode) {
            let layout = self.obj().create_pango_layout(None);
            layout.set_font_description(Some(&*self.font_desc.borrow()));
            input_mode.adjust_invisible_characters(move |ch| {
                ch != '\0' && {
                    layout.set_text(&ch.to_string());
                    let (_ink_rect, logical_rect) = layout.pixel_extents();
                    // Pango displays something
                    logical_rect.width() > 0
                }
            })
        }

        fn set_fixed_limit(&self, fixed_limit: u32) {
            self.fixed_limit.set(fixed_limit);
            // always 16 bytes in hex dump
            let fixed_limit = match self.obj().display_mode() {
                TextRenderDisplayMode::Text | TextRenderDisplayMode::FixedWidth => fixed_limit,
                TextRenderDisplayMode::Hexdump => HEXDUMP_FIXED_LIMIT,
            };
            self.data_presentation
                .borrow_mut()
                .set_fixed_count(fixed_limit);
            if let Some(adjustment) = self.obj().hadjustment() {
                self.update_hadjustment_limits(&adjustment);
            }

            self.obj().queue_draw();
        }

        fn set_hexadecimal_offset(&self, hexadecimal_offset: bool) {
            self.hexadecimal_offset.set(hexadecimal_offset);
            self.obj().queue_draw();
        }

        fn scroll(&self, dy: f64) {
            let Some(vadjustment) = self.obj().vadjustment() else {
                return;
            };

            let mut current_offset = self.obj().current_offset();
            current_offset = self.data_presentation.borrow().scroll_lines(
                &self.input_mode.borrow(),
                current_offset,
                (4.0 * dy) as i32,
            );
            vadjustment.set_value(current_offset as f64);

            self.obj().emit_text_status_changed();
            self.obj().queue_draw();
        }

        fn button_press(&self, button: u32, n_press: i32, x: f64, y: f64) {
            if !self.obj().has_focus() {
                self.obj().grab_focus();
            }

            if n_press == 1 && self.button.get().is_none() {
                self.button.set(Some(button));
                self.marker_start.set(match self.obj().display_mode() {
                    TextRenderDisplayMode::Text | TextRenderDisplayMode::FixedWidth => {
                        self.text_mode_pixel_to_offset(x, y)
                    }
                    TextRenderDisplayMode::Hexdump => self.hex_mode_pixel_to_offset(x, y, true),
                });
            }
        }

        fn button_release(&self, button: u32, x: f64, y: f64) {
            if self.button.get() == Some(button) {
                self.button.set(None);
                self.marker_end.set(match self.obj().display_mode() {
                    TextRenderDisplayMode::Text | TextRenderDisplayMode::FixedWidth => {
                        self.text_mode_pixel_to_offset(x, y)
                    }
                    TextRenderDisplayMode::Hexdump => self.hex_mode_pixel_to_offset(x, y, false),
                });
                self.obj().queue_draw();
            }
        }

        fn motion_notify(&self, x: f64, y: f64) {
            if self.button.get().is_some() {
                let new_marker = match self.obj().display_mode() {
                    TextRenderDisplayMode::Text | TextRenderDisplayMode::FixedWidth => {
                        self.text_mode_pixel_to_offset(x, y)
                    }
                    TextRenderDisplayMode::Hexdump => self.hex_mode_pixel_to_offset(x, y, false),
                };
                if new_marker != self.marker_end.get() {
                    self.marker_end.set(new_marker);
                    self.obj().queue_draw();
                }
            }
        }

        fn key_pressed(&self, key: gdk::Key, modifiers: gdk::ModifierType) -> glib::Propagation {
            let Some(hadjustment) = self.obj().hadjustment() else {
                return glib::Propagation::Proceed;
            };
            let Some(vadjustment) = self.obj().vadjustment() else {
                return glib::Propagation::Proceed;
            };

            let dp = self.data_presentation.borrow();
            let column = self.obj().column();
            let current_offset = self.obj().current_offset();

            match (modifiers, key) {
                (NO_MOD, gdk::Key::Up | gdk::Key::KP_Up) => vadjustment.set_value(dp.scroll_lines(
                    &self.input_mode.borrow(),
                    current_offset,
                    -1,
                )
                    as f64),
                (NO_MOD, gdk::Key::Down | gdk::Key::KP_Down) => vadjustment.set_value(
                    dp.scroll_lines(&self.input_mode.borrow(), current_offset, 1) as f64,
                ),
                (NO_MOD, gdk::Key::Page_Up | gdk::Key::KP_Page_Up) => {
                    vadjustment.set_value(dp.scroll_lines(
                        &self.input_mode.borrow(),
                        current_offset,
                        -(self.lines_displayed.get() - 1),
                    ) as f64)
                }
                (NO_MOD, gdk::Key::Page_Down | gdk::Key::KP_Page_Down) => {
                    vadjustment.set_value(dp.scroll_lines(
                        &self.input_mode.borrow(),
                        current_offset,
                        self.lines_displayed.get() - 1,
                    ) as f64)
                }
                (NO_MOD, gdk::Key::Left | gdk::Key::KP_Left) => {
                    if !self.wrap_mode.get() && column > 0 {
                        hadjustment.set_value((column - 1) as f64);
                    }
                }
                (NO_MOD, gdk::Key::Right | gdk::Key::KP_Right) => {
                    if !self.wrap_mode.get() {
                        hadjustment.set_value((column + 1) as f64);
                    }
                }
                (NO_MOD, gdk::Key::Home | gdk::Key::KP_Home) => hadjustment.set_value(0.0),
                (NO_MOD, gdk::Key::End | gdk::Key::KP_End) => {
                    let max_column = self.displayed_max_column.get();
                    let chars_per_line = self.chars_per_line.get();
                    hadjustment.set_value(if max_column > chars_per_line - 1 {
                        (max_column - chars_per_line + 1) as f64
                    } else {
                        0.0
                    });
                }
                (CONTROL, gdk::Key::Home | gdk::Key::KP_Home) => vadjustment.set_value(0.0),
                (CONTROL, gdk::Key::End | gdk::Key::KP_End) => {
                    vadjustment.set_value(dp.align_offset_to_line_start(
                        &self.input_mode.borrow(),
                        self.input_mode.borrow().max_offset() - 1,
                    ) as f64)
                }
                _ => return glib::Propagation::Proceed,
            }

            self.obj().emit_text_status_changed();
            self.obj().queue_draw();

            glib::Propagation::Stop
        }

        pub fn text_mode_pixel_to_offset(&self, x: f64, y: f64) -> u64 {
            let char_width = self.char_width.get();
            let char_height = self.char_height.get();
            let tab_size = self.tab_size.get();

            let current_offset: u64 = self.obj().current_offset();

            if char_height <= 0 || char_width <= 0 {
                return current_offset;
            }

            let line = (y.max(0.0) / f64::from(char_height)) as i32;
            let column =
                (x.max(0.0) / f64::from(char_width) + f64::from(self.obj().column())) as u32;

            let dp = self.data_presentation.borrow();
            let line_offset = dp.scroll_lines(&self.input_mode.borrow(), current_offset, line);
            let next_line_offset = dp.scroll_lines(&self.input_mode.borrow(), line_offset, 1);

            text_mode_line_iter(
                &self.input_mode.borrow(),
                line_offset,
                next_line_offset,
                tab_size,
            )
            .find_map(move |(offset, c, _)| (c >= column).then_some(offset))
            .unwrap_or(next_line_offset)
        }

        pub fn hex_mode_pixel_to_offset(&self, x: f64, y: f64, start_marker: bool) -> u64 {
            let char_width = self.char_width.get();
            let char_height = self.char_height.get();

            let current_offset: u64 = self.obj().current_offset();

            if x < 0.0 || y < 0.0 || char_height <= 0 || char_width <= 0 {
                return current_offset;
            }

            let line = (y / char_height as f64) as i32;
            let column = (x as i32 / char_width + self.obj().column()) as u32;

            let dp = self.data_presentation.borrow();
            let line_offset = dp.scroll_lines(&self.input_mode.borrow(), current_offset, line);
            let next_line_offset = dp.scroll_lines(&self.input_mode.borrow(), line_offset, 1);

            let (hex_offset, bin_offset) = hex_mode_column_layout();

            if column < hex_offset {
                return line_offset;
            }

            let mut byte_offset = if start_marker {
                if column < bin_offset {
                    // the user selected the hex dump portion
                    self.hexmode_marker_on_hexdump.set(true);
                    (column - hex_offset).div_ceil(3)
                } else {
                    // the user selected the ascii portion
                    self.hexmode_marker_on_hexdump.set(false);
                    column - bin_offset
                }
            } else if self.hexmode_marker_on_hexdump.get() {
                if column < bin_offset {
                    // the user selected the hex dump portion
                    (column - hex_offset).div_ceil(3)
                } else {
                    HEXDUMP_FIXED_LIMIT
                }
            } else {
                column.saturating_sub(bin_offset)
            };

            let input_mode = self.input_mode.borrow();
            let mut offset = line_offset;
            while byte_offset > 0 && offset < next_line_offset {
                offset = input_mode.next_char_offset(offset);
                byte_offset -= 1;
            }

            offset
        }

        fn display_line(
            &self,
            snapshot: &gtk::Snapshot,
            chars: &[(u64, u32, char)],
            marker_start: u64,
            marker_end: u64,
            display_options: &DisplayOptions,
        ) {
            let layout = self.obj().create_pango_layout(None);
            layout.set_font_description(Some(&*self.font_desc.borrow()));

            let marked: MinMax<u32> = chars
                .iter()
                .filter(|(offset, _, _)| *offset >= marker_start && *offset < marker_end)
                .map(|(_, column, _)| *column)
                .collect();

            if let Some((start, end)) = marked.take() {
                snapshot.save();
                snapshot.translate(&graphene::Point::new(
                    (self.char_width.get() * (start as i32)) as f32,
                    0_f32,
                ));
                snapshot.append_color(
                    &display_options.marker_background_color,
                    &graphene::Rect::new(
                        0.0,
                        0.0,
                        (self.char_width.get() * (end + 1 - start) as i32) as f32,
                        self.char_height.get() as f32,
                    ),
                );
                snapshot.restore();
            }

            for (current, column, character) in chars {
                layout.set_text(&character.to_string());
                snapshot.save();
                snapshot.translate(&graphene::Point::new(
                    (self.char_width.get() * (*column as i32)) as f32,
                    0_f32,
                ));
                if *current >= marker_start && *current < marker_end {
                    snapshot.append_layout(&layout, &display_options.marker_text_color);
                } else {
                    snapshot.append_layout(&layout, &display_options.text_color);
                }
                snapshot.restore();
            }

            let max_column = chars
                .iter()
                .map(|(_, column, _)| *column)
                .max()
                .unwrap_or_default();
            if max_column > self.displayed_max_column.get() {
                self.displayed_max_column.set(max_column);
            }
            if max_column > self.max_column.get() {
                self.max_column.set(max_column);
                self.update_adjustments_limits();
            }
        }

        fn text_mode_chars(&self, start_of_line: u64, end_of_line: u64) -> Vec<(u64, u32, char)> {
            text_mode_line_iter(
                &self.input_mode.borrow(),
                start_of_line,
                end_of_line,
                self.tab_size.get(),
            )
            .filter_map(|(offset, column, character)| match character {
                Some('\r') | Some('\n') => None,
                Some(ch) if ch == '\0' || ch.is_whitespace() => Some((offset, column, ' ')),
                Some(ch) => Some((offset, column, ch)),
                None => Some((offset, column, '\u{FFFD}')),
            })
            .collect::<Vec<_>>()
        }

        fn text_mode_display_line(
            &self,
            snapshot: &gtk::Snapshot,
            start_of_line: u64,
            end_of_line: u64,
            marker_start: u64,
            marker_end: u64,
            display_options: &DisplayOptions,
        ) {
            let chars = self.text_mode_chars(start_of_line, end_of_line);
            self.display_line(snapshot, &chars, marker_start, marker_end, display_options);
        }

        pub fn text_mode_copy_to_clipboard(&self, start_offset: u64, end_offset: u64) {
            let input_mode = self.input_mode.borrow();
            let text: String = input_mode
                .offsets(start_offset, end_offset)
                .filter_map(|offset| input_mode.character(offset))
                .collect();
            let provider = gdk::ContentProvider::for_bytes(
                "text/plain;charset=utf-8",
                &text.as_bytes().into(),
            );
            if let Err(error) = self.obj().clipboard().set_content(Some(&provider)) {
                eprintln!("Failed copying text to clipboard: {error}");
            }
        }

        fn fixed_width_mode_chars(
            &self,
            start_of_line: u64,
            end_of_line: u64,
        ) -> Vec<(u64, u32, char)> {
            fixed_width_mode_line_iter(&self.input_mode.borrow(), start_of_line, end_of_line)
                .map(|(offset, column, character)| {
                    (
                        offset,
                        column,
                        character.map_or('.', |c| if c.is_control() { '.' } else { c }),
                    )
                })
                .collect()
        }

        fn fixed_width_mode_display_line(
            &self,
            snapshot: &gtk::Snapshot,
            start_of_line: u64,
            end_of_line: u64,
            marker_start: u64,
            marker_end: u64,
            display_options: &DisplayOptions,
        ) {
            let chars = self.fixed_width_mode_chars(start_of_line, end_of_line);
            self.display_line(snapshot, &chars, marker_start, marker_end, display_options);
        }

        fn hex_mode_display_line(
            &self,
            snapshot: &gtk::Snapshot,
            start_of_line: u64,
            end_of_line: u64,
            marker_start: u64,
            marker_end: u64,
            display_options: &DisplayOptions,
            display_options_alternate: &DisplayOptions,
        ) {
            let layout = self.obj().create_pango_layout(None);
            layout.set_font_description(Some(&*self.font_desc.borrow()));
            layout.set_text(&if self.hexadecimal_offset.get() {
                format!("{:08X}", start_of_line)
            } else {
                format!("{:09}", start_of_line)
            });
            snapshot.append_layout(&layout, &display_options.text_color);

            let (display_options, display_options_alternate) =
                if self.hexmode_marker_on_hexdump.get() {
                    (display_options, display_options_alternate)
                } else {
                    (display_options_alternate, display_options)
                };

            let mut hex_chars = Vec::new();
            let mut bin_chars = Vec::new();
            for (offset, column, byte, character) in
                hex_mode_line_iter(&self.input_mode.borrow(), start_of_line, end_of_line)
            {
                if let Some(byte) = byte {
                    hex_chars.extend(
                        format!("{byte:02X}")
                            .chars()
                            .enumerate()
                            .map(|(i, c)| (offset, column * 3 + i as u32, c)),
                    );
                    bin_chars.push((
                        offset,
                        column,
                        character.filter(|c| *c != '\0').unwrap_or('.'),
                    ));
                }
            }

            let (hex_offset, bin_offset) = hex_mode_column_layout();

            snapshot.save();
            snapshot.translate(&graphene::Point::new(
                (self.char_width.get() * hex_offset as i32) as f32,
                0_f32,
            ));
            self.display_line(
                snapshot,
                &hex_chars,
                marker_start,
                marker_end,
                display_options,
            );
            snapshot.restore();

            snapshot.save();
            snapshot.translate(&graphene::Point::new(
                (self.char_width.get() * bin_offset as i32) as f32,
                0_f32,
            ));
            self.display_line(
                snapshot,
                &bin_chars,
                marker_start,
                marker_end,
                display_options_alternate,
            );
            snapshot.restore();
        }

        pub fn hex_mode_copy_to_clipboard(&self, start_offset: u64, end_offset: u64) {
            if self.hexmode_marker_on_hexdump.get() {
                let input_mode = self.input_mode.borrow();
                let text = input_mode
                    .offsets(start_offset, end_offset)
                    .filter_map(|offset| input_mode.raw_byte(offset))
                    .map(|b| format!("{:02X}", b))
                    .collect::<Vec<_>>();
                self.obj().clipboard().set_text(&text.join(" "));
            } else {
                self.text_mode_copy_to_clipboard(start_offset, end_offset);
            }
        }
    }

    fn text_mode_line_iter(
        input_mode: &Arc<InputMode>,
        start: u64,
        end: u64,
        tab_size: u32,
    ) -> impl Iterator<Item = (u64, u32, Option<char>)> + use<'_> {
        input_mode
            .offsets(start, end)
            .scan(0, move |column, offset| {
                let current = *column;
                let character = input_mode.character(offset);
                if character == Some('\t') {
                    *column = next_tab_position(*column, tab_size);
                } else {
                    *column += 1;
                }
                Some((offset, current, character))
            })
    }

    fn fixed_width_mode_line_iter(
        input_mode: &Arc<InputMode>,
        start: u64,
        end: u64,
    ) -> impl Iterator<Item = (u64, u32, Option<char>)> + use<'_> {
        input_mode
            .offsets(start, end)
            .scan(0, move |column, offset| {
                let current = *column;
                let character = input_mode.character(offset);
                *column += 1;
                Some((offset, current, character))
            })
    }

    fn hex_mode_line_iter(
        input_mode: &Arc<InputMode>,
        start: u64,
        end: u64,
    ) -> impl Iterator<Item = (u64, u32, Option<u8>, Option<char>)> + use<'_> {
        let use_encoding = input_mode.char_size().is_some_and(|size| size == 1);
        input_mode
            .byte_offsets(start, end)
            .scan(0, move |column, offset| {
                let current = *column;
                let byte = input_mode.raw_byte(offset);
                let character = if use_encoding {
                    input_mode.character(offset)
                } else {
                    byte.and_then(|byte| char::from_u32(byte.into()))
                };
                *column += 1;
                Some((offset, current, byte, character))
            })
    }

    const fn hex_mode_column_layout() -> (u32, u32) {
        (10, 10 + ((HEXDUMP_FIXED_LIMIT * 3) - 1) + 2)
    }

    struct DisplayOptions {
        text_color: gdk::RGBA,
        marker_text_color: gdk::RGBA,
        marker_background_color: gdk::RGBA,
    }

    impl Default for DisplayOptions {
        fn default() -> Self {
            Self {
                text_color: gdk::RGBA::BLACK,
                marker_text_color: gdk::RGBA::WHITE,
                marker_background_color: gdk::RGBA::BLUE,
            }
        }
    }

    impl DisplayOptions {
        fn alternate_marker() -> Self {
            Self {
                text_color: gdk::RGBA::BLACK,
                marker_text_color: gdk::RGBA::BLACK,
                marker_background_color: gdk::RGBA::new(0_f32, 1_f32, 1_f32, 1_f32),
            }
        }
    }
}

glib::wrapper! {
    pub struct TextRender(ObjectSubclass<imp::TextRender>)
        @extends gtk::Widget,
        @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::Scrollable;
}

impl TextRender {
    pub fn new() -> Self {
        glib::Object::builder().build()
    }

    pub fn scale_factor(&self) -> f64 {
        self.imp().scale_factor.get()
    }

    pub fn set_scale_factor(&self, factor: f64) {
        self.imp().scale_factor.set(factor.max(0.1));
        self.setup_current_font();
        self.queue_draw();
    }

    pub fn ensure_offset_visible(&self, offset: u64) {
        let dp = self.imp().data_presentation.borrow();
        let input_mode = self.imp().input_mode.borrow();
        let line_start = dp.align_offset_to_line_start(&input_mode, offset);
        let mut changed_adjustment = false;

        if let Some(vadjustment) = self.vadjustment()
            && (offset < self.current_offset() || offset >= self.imp().last_displayed_offset.get())
        {
            vadjustment.set_value(dp.scroll_lines(
                &input_mode,
                offset,
                -self.imp().lines_displayed.get() / 2,
            ) as f64);
            changed_adjustment = true;
        }

        let hoffset = (offset - line_start) as f64;
        let chars_per_line = self.chars_per_line() as f64;
        if let Some(hadjustment) = self.hadjustment()
            && (hoffset < hadjustment.value() || hoffset >= hadjustment.value() + chars_per_line)
        {
            hadjustment.set_value(hoffset - chars_per_line / 2.0);
            changed_adjustment = true;
        }

        if changed_adjustment {
            self.queue_draw();
            self.emit_text_status_changed();
        }
    }

    pub fn marker(&self) -> Option<(u64, u64)> {
        let mut marker_start = self.imp().marker_start.get();
        let mut marker_end = self.imp().marker_end.get();
        if marker_start == marker_end {
            return None;
        }
        if marker_start > marker_end {
            std::mem::swap(&mut marker_start, &mut marker_end);
        }
        Some((marker_start, marker_end))
    }

    pub fn set_marker(&self, start: u64, end: u64) {
        self.imp().marker_start.set(start);
        self.imp().marker_end.set(end);
        self.queue_draw();
    }

    pub fn current_offset(&self) -> u64 {
        self.vadjustment()
            .map(|a| a.value() as u64)
            .unwrap_or_default()
    }

    pub fn end_offset(&self) -> u64 {
        self.imp().last_displayed_offset.get()
    }

    pub fn size(&self) -> u64 {
        self.imp().input_mode.borrow().max_offset()
    }

    pub fn column(&self) -> i32 {
        self.hadjustment()
            .map(|a| a.value() as i32)
            .unwrap_or_default()
    }

    pub fn input_mode(&self) -> Arc<InputMode> {
        self.imp().input_mode.borrow().clone()
    }

    pub fn load_file(&self, filename: &Path) {
        let source = match FileInputSource::open(filename) {
            Ok(source) => source,
            Err(error) => {
                eprintln!("Failed to load file {}: {}", filename.display(), error);
                self.imp().input_mode.replace(Default::default());
                return;
            }
        };

        if let Some(hadjustment) = self.hadjustment() {
            hadjustment.set_value(0.0);
        }
        if let Some(vadjustment) = self.vadjustment() {
            vadjustment.set_value(0.0);
        }
        self.set_max_column(0);

        // Setup the input mode translations
        let mut input_mode = InputMode::new(source);
        input_mode.set_mode(&self.encoding());
        self.imp().input_mode.replace(Arc::new(input_mode));

        self.set_display_mode(TextRenderDisplayMode::Text);

        self.imp().update_adjustments_limits();
    }

    pub fn notify_status_changed(&self) {
        self.emit_text_status_changed();
    }

    pub fn copy_selection(&self) {
        let Some((marker_start, marker_end)) = self.marker() else {
            return;
        };
        match self.display_mode() {
            TextRenderDisplayMode::Text | TextRenderDisplayMode::FixedWidth => self
                .imp()
                .text_mode_copy_to_clipboard(marker_start, marker_end),
            TextRenderDisplayMode::Hexdump => self
                .imp()
                .hex_mode_copy_to_clipboard(marker_start, marker_end),
        }
    }

    fn setup_current_font(&self) {
        let mut font = pango::FontDescription::from_string(&self.imp().monospaced_font.borrow());
        font.set_size((font.size() as f64 * self.scale_factor()) as i32);
        self.setup_font(font);
    }

    fn setup_font(&self, new_desc: pango::FontDescription) {
        let (char_width, char_height) = get_max_char_width_and_height(self.upcast_ref(), &new_desc);
        self.imp().char_width.set(char_width);
        self.imp().char_height.set(char_height);
        self.imp().font_desc.replace(new_desc);

        let width = self.width();
        let height = self.height();
        if width > 0 && height > 0 {
            self.imp().update_viewport_size(width, height);
        }
    }

    fn emit_text_status_changed(&self) {
        self.emit_by_name::<()>("text-status-changed", &[]);
    }

    pub fn connect_text_status_changed<F: Fn(&Self) + 'static>(
        &self,
        f: F,
    ) -> glib::SignalHandlerId {
        self.connect_closure(
            "text-status-changed",
            false,
            glib::closure_local!(move |this| (f)(this)),
        )
    }
}

fn get_max_char_width_and_height(
    widget: &gtk::Widget,
    font_desc: &pango::FontDescription,
) -> (i32, i32) {
    let layout = widget.create_pango_layout(None);
    layout.set_font_description(Some(font_desc));

    let (max_width, max_height): (Max<i32>, Max<i32>) = (0..256)
        .filter_map(char::from_u32)
        .filter(|c| *c == ' ' || c.is_ascii_graphic())
        .map(|ch| {
            layout.set_text(&ch.to_string());
            let (_ink_rect, logical_rect) = layout.pixel_extents();
            (logical_rect.width(), logical_rect.height())
        })
        .unzip();

    (
        max_width.take().unwrap_or_default(),
        max_height.take().unwrap_or_default(),
    )
}

#[derive(Clone, Copy, Default, PartialEq, Eq, Debug, glib::Enum)]
#[enum_type(name = "GnomeCmdTextRenderDisplayMode")]
#[repr(C)]
pub enum TextRenderDisplayMode {
    #[default]
    Text = 0,
    FixedWidth,
    Hexdump,
}

#[cfg(test)]
mod test {
    use super::*;
    use crate::intviewer::data_presentation::next_tab_position;
    use rusty_fork::rusty_fork_test;

    const FILENAME: &str = "./AUTHORS";

    #[test]
    fn test_scan() {
        let c = "ab\tcd"
            .chars()
            .scan(0, move |column, character| {
                let current = *column;
                if character == '\t' {
                    *column = next_tab_position(*column, 4);
                } else {
                    *column += 1;
                }
                Some((current, character))
            })
            .collect::<Vec<_>>();

        assert_eq!(c, vec![(0, 'a'), (1, 'b'), (2, '\t'), (4, 'c'), (5, 'd')]);
    }

    rusty_fork_test! {
        #[test]
        fn display_mode() {
            gtk::init().unwrap();
            let text_render = TextRender::new();
            text_render.load_file(&Path::new(FILENAME));
            for mode in [
                TextRenderDisplayMode::Text,
                TextRenderDisplayMode::FixedWidth,
                TextRenderDisplayMode::Hexdump,
            ] {
                text_render.set_display_mode(mode);
                assert_eq!(text_render.display_mode(), mode);
            }
        }

        #[test]
        fn tab_size() {
            gtk::init().unwrap();
            let text_render = TextRender::new();
            text_render.load_file(&Path::new(FILENAME));
            for tab_size in 1..=10 {
                text_render.set_tab_size(tab_size);
                assert_eq!(text_render.tab_size(), tab_size);
            }
        }

        #[test]
        fn wrap_mode() {
            gtk::init().unwrap();
            let text_render = TextRender::new();
            text_render.load_file(&Path::new(FILENAME));
            for mode in [false, true] {
                text_render.set_wrap_mode(mode);
                assert_eq!(text_render.wrap_mode(), mode);
            }
        }

        #[test]
        fn fixed_limit() {
            gtk::init().unwrap();
            let text_render = TextRender::new();
            text_render.load_file(&Path::new(FILENAME));
            for limit in 1..=10 {
                text_render.set_fixed_limit(limit);
                assert_eq!(text_render.fixed_limit(), limit);
            }
        }

        #[test]
        fn encoding() {
            gtk::init().unwrap();
            let text_render = TextRender::new();
            text_render.load_file(&Path::new(FILENAME));
            for encoding in ["ASCII", "UTF8", "CP437", "CP1251"] {
                text_render.set_encoding(encoding);
                assert_eq!(text_render.encoding(), encoding);
            }
        }
    }
}
