#!/usr/bin/perl

use strict;
use warnings;
use Glib qw/TRUE FALSE/;
use Gtk2 '-init';
use Cairo;
use POSIX qw(WNOHANG); # Removed floor and sqrt to avoid prototype conflicts
use File::Temp qw/ tempfile /;
use Fcntl;
use PDL;

# --- Global State ---
my $state = {
    # View management
    view_mode           => 'single',
    detail_mode         => FALSE,
    detail_cache        => {},

    # Single process state
    selected_pid        => undef,

    # Global view state
    global_map          => [],
    global_total_size   => 0,
    zoom_level          => 1.0,
    view_offset_y       => 0,
    drag_info           => {},

    # Common state
    memory_map          => [],
    vma_total_size      => 0,
    map_areas           => [],
    update_timer_id     => undef,
    update_interval_sec => 10,
    key_drawing_area    => undef,
    hilbert_window_ref  => undef,
};

# --- Main Application Setup ---
sub main {
    unless ($> == 0) {
        print "ERROR: This script now requires root privileges to read process memory.\n";
        print "Please run with: sudo perl $0\n";
        exit 1;
    }

    my $window = Gtk2::Window->new('toplevel');
    $window->set_title("memgaze");
    $window->set_default_size(600, 700);
    $window->set_border_width(5);
    $window->signal_connect(destroy => sub { Gtk2->main_quit; });

    my $hpaned = Gtk2::HPaned->new;
    $window->add($hpaned);

    # --- Left Pane: Controls and Process List ---
    my $left_vbox = Gtk2::VBox->new(FALSE, 5);
    $left_vbox->set_border_width(5);

    my $controls_vbox = Gtk2::VBox->new(FALSE, 5);
    
    my $update_entry = Gtk2::Entry->new();
    $update_entry->set_text($state->{update_interval_sec});
    $update_entry->set_width_chars(3);
    my $refresh_button = Gtk2::Button->new('Refresh Procs');
    my $global_view_button = Gtk2::Button->new("All Processes");
    my $detail_toggle_button = Gtk2::ToggleButton->new("Details");
    $detail_toggle_button->set_sensitive(FALSE);
    my $image_button = Gtk2::Button->new("Image (1 left and 2 right click)");
    my $hilbert_button = Gtk2::Button->new("Hilbert Map (right-click)");
    
    $image_button->set_sensitive(TRUE); 
    my $orange = Gtk2::Gdk::Color->parse('orange');
    my $black = Gtk2::Gdk::Color->parse('black');
    $image_button->modify_bg('normal', $orange);
    $image_button->modify_fg('normal', $black);
    $image_button->modify_bg('prelight', $orange);
    $image_button->modify_fg('prelight', $black);
    $image_button->modify_bg('active', $orange);
    $image_button->modify_fg('active', $black);

    $controls_vbox->pack_start($image_button, TRUE, TRUE, 0);
    $controls_vbox->pack_start($hilbert_button, TRUE, TRUE, 0);

    my $other_controls_hbox = Gtk2::HBox->new(FALSE, 5);
    
    my $seconds_label = Gtk2::Label->new('seconds');
    
    $other_controls_hbox->pack_start($detail_toggle_button, FALSE, FALSE, 0);
    $other_controls_hbox->pack_start($global_view_button,   FALSE, FALSE, 5);
    
    $other_controls_hbox->pack_end($refresh_button,       FALSE, FALSE, 0);
    $other_controls_hbox->pack_end($update_entry,         FALSE, FALSE, 5);
    $other_controls_hbox->pack_end($seconds_label,        FALSE, FALSE, 5);
    
    $controls_vbox->pack_start($other_controls_hbox, FALSE, FALSE, 5);
    
    $left_vbox->pack_start($controls_vbox, FALSE, FALSE, 0);

    my $search_hbox = Gtk2::HBox->new(FALSE, 5);
    my $search_label = Gtk2::Label->new("Search:");
    my $search_entry = Gtk2::Entry->new();
    $search_hbox->pack_start($search_label, FALSE, FALSE, 0);
    $search_hbox->pack_start($search_entry, TRUE, TRUE, 0);
    $left_vbox->pack_start($search_hbox, FALSE, FALSE, 5);


    my $scrolled_window = Gtk2::ScrolledWindow->new(undef, undef);
    $scrolled_window->set_policy('automatic', 'automatic');
    $scrolled_window->set_shadow_type('in');

    my $proc_list_store = Gtk2::ListStore->new('Glib::String', 'Glib::Int');
    my $proc_tree_view = Gtk2::TreeView->new($proc_list_store);

    my $renderer = Gtk2::CellRendererText->new;
    my $column = Gtk2::TreeViewColumn->new_with_attributes("Process", $renderer, 'text', 0);
    $proc_tree_view->append_column($column);

    $scrolled_window->add($proc_tree_view);
    $left_vbox->pack_start($scrolled_window, TRUE, TRUE, 5);

    my $color_key_frame = Gtk2::Frame->new("Color Key & Stats");
    $state->{key_drawing_area} = Gtk2::DrawingArea->new;
    $state->{key_drawing_area}->set_size_request(-1, 150);
    $state->{key_drawing_area}->signal_connect(expose_event => \&on_draw_key);
    $color_key_frame->add($state->{key_drawing_area});
    $left_vbox->pack_start($color_key_frame, FALSE, FALSE, 0);
    $color_key_frame->hide();

    $hpaned->add1($left_vbox);

    # --- Right Pane: Drawing Area ---
    my $drawing_area = Gtk2::DrawingArea->new;
    $drawing_area->signal_connect(expose_event => \&on_expose);
    $drawing_area->set_has_tooltip(TRUE);
    $drawing_area->signal_connect(query_tooltip => \&on_query_tooltip);

    $drawing_area->set_events([
        'pointer-motion-mask', 'button-press-mask',
        'button-release-mask', 'scroll-mask'
    ]);
    $drawing_area->signal_connect(scroll_event => \&on_scroll);
    $drawing_area->signal_connect(button_press_event => \&on_button_press);
    $drawing_area->signal_connect(motion_notify_event => \&on_motion);
    $drawing_area->signal_connect(button_release_event => sub { $state->{drag_info} = {}; });

    $hpaned->add2($drawing_area);
    $hpaned->set_position(500);

    # --- Logic ---
    my $selection = $proc_tree_view->get_selection;
    $selection->signal_connect(changed => sub {
        my ($selection) = @_;
        my ($model, $iter) = $selection->get_selected;
        
        my $is_selected = $iter ? TRUE : FALSE;
        $detail_toggle_button->set_sensitive($is_selected);
        $detail_toggle_button->set_active(FALSE);

        if ($iter) {
            $color_key_frame->show();
            my $pid = $model->get($iter, 1);
            if (defined $pid) {
                $state->{view_mode} = 'single';
                $state->{selected_pid} = $pid;
                update_memory_map($drawing_area);

                if (defined $state->{hilbert_window_ref}) {
                    my $h_da = $state->{hilbert_window_ref};
                    my $h_state = $h_da->{'hilbert_state'};
                    if (defined $h_state && !$h_state->{mouse_is_over}) {
                        $h_state->{highlighted_segment} = undef;
                        my $pid_segments = $h_state->{pid_to_segments_cache}->{$pid} || [];
                        $h_state->{highlighted_pid_segments} = $pid_segments;
                        $h_da->queue_draw();
                    }
                }
            }
        } else {
            $color_key_frame->hide();
        }
    });

    my $last_search_text = '';
    $search_entry->signal_connect(activate => sub {
        my $entry = shift;
        my $search_text = lc($entry->get_text);
        return if $search_text eq '';

        my $model = $proc_tree_view->get_model;
        my $selection = $proc_tree_view->get_selection;

        my $start_iter;
        my ($selected_model, $selected_iter) = $selection->get_selected;

        if ($search_text ne $last_search_text || !$selected_iter) {
            # New search or nothing selected, start from the top
            $start_iter = $model->get_iter_first;
        } else {
            # Same search term, start from the item *after* the current selection
            $start_iter = $selected_iter;
            $start_iter = $model->iter_next($start_iter); # Advance one
        }
        
        $last_search_text = $search_text;

        my $search_iter = $start_iter;

        # Loop through the list up to two times to ensure we check every item and wrap around.
        for my $pass (1..2) {
            # If the start iterator was undef (we were at the end of the list),
            # wrap to the beginning immediately for the first pass.
            unless (defined $search_iter) {
                $search_iter = $model->get_iter_first;
            }

            while (defined $search_iter) {
                my $display_text = lc($model->get($search_iter, 0));
                # --- FIX: Use CORE::index to avoid conflict with PDL::index ---
                if (CORE::index($display_text, $search_text) != -1) {
                    $selection->select_iter($search_iter);
                    my $path = $model->get_path($search_iter);
                    $proc_tree_view->scroll_to_cell($path, undef, FALSE, 0, 0);
                    return; # Found a match, we're done.
                }
                $search_iter = $model->iter_next($search_iter);
            }
        }
    });


    $global_view_button->signal_connect(clicked => sub {
        show_global_view($drawing_area, $selection, $detail_toggle_button, $image_button, $color_key_frame);
    });

    $hilbert_button->signal_connect(clicked => sub { 
        show_hilbert_view($window, $proc_list_store, $selection); 
    });

    $detail_toggle_button->signal_connect(toggled => sub {
        my $button = shift;
        $state->{detail_mode} = $button->get_active;
        if ($state->{detail_mode}) {
            build_detail_map($drawing_area);
        } else {
            $state->{detail_cache} = {};
        }
        $state->{key_drawing_area}->queue_draw();
        $drawing_area->queue_draw();
    });
    
    $image_button->signal_connect(clicked => sub {
        show_image_view($window);
    });

    $refresh_button->signal_connect(clicked => sub {
        populate_process_list($proc_list_store, $selection);
    });

    $update_entry->signal_connect(activate => sub {
        my $entry = shift;
        my $text = $entry->get_text;
        if ($text =~ /^\d*\.?\d+$/ && $text > 0) {
            $state->{update_interval_sec} = $text;
        } else {
            $state->{update_interval_sec} = 0;
        }
        setup_proc_list_refresh_timer($proc_list_store, $selection);
    });

    populate_process_list($proc_list_store, $selection);
    setup_proc_list_refresh_timer($proc_list_store, $selection);
    $window->show_all;
    Gtk2->main;
}

# --- View Mode Management ---
sub show_global_view {
    my ($widget, $selection, $detail_button, $image_button, $key_frame) = @_;
    $selection->unselect_all();
    $state->{selected_pid} = undef;
    $state->{view_mode} = 'global';
    $detail_button->set_active(FALSE);
    $detail_button->set_sensitive(FALSE);
    $key_frame->hide();
    $state->{zoom_level} = 1.0;
    $state->{view_offset_y} = 0;
    build_global_map();
    $widget->queue_draw();
}

# --- Hilbert Curve Pop-up ---
sub show_hilbert_view {
    my ($parent_window, $proc_list_store, $selection) = @_;

    if (defined $state->{hilbert_window_ref}) {
        my $existing_window = $state->{hilbert_window_ref}->get_toplevel;
        $existing_window->present();
        return;
    }

    my $popup = Gtk2::Window->new('toplevel');
    $popup->set_title("Hilbert Curve Memory Map (All Processes)");
    $popup->set_transient_for($parent_window);
    $popup->set_default_size(540, 540);
    $popup->set_position('center-on-parent');
    
    my $label = Gtk2::Label->new("\nBuilding global map and generating curve...\n");
    $popup->add($label);
    $popup->show_all;
    Gtk2->main_iteration while Gtk2->events_pending;

    build_global_map();
    
    my $total_ram_str = format_bytes($state->{global_total_size});
    $popup->set_title("Hilbert Curve (All Proc Virtual $total_ram_str)");

    $popup->remove($label);
    
    my $order = 8;
    my $grid_size = 2**$order;
    my $scale = 2;
    my $padding = 10;
    my $canvas_size = $grid_size * $scale + ($padding * 2);

    my $hilbert_state = {
        path                 => generate_hilbert_path($order),
        order                => $order,
        scale                => $scale,
        padding              => $padding,
        map_areas            => [],
        backing_pixmap       => undef,
        highlighted_segment     => undef,
        highlighted_pid_segments => [],
        pid_to_segments_cache => {},
        path_to_segment_map  => [],
        mouse_is_over        => FALSE,
    };
    
    my $drawing_area = Gtk2::DrawingArea->new;
    $drawing_area->set_size_request($canvas_size, $canvas_size);
    $drawing_area->set_has_tooltip(TRUE);
    
    $drawing_area->set_events([
        'pointer-motion-mask', 'button-press-mask',
        'enter-notify-mask', 'leave-notify-mask'
    ]);
    
    $drawing_area->signal_connect(expose_event => \&on_expose_hilbert, $hilbert_state);
    $drawing_area->signal_connect(motion_notify_event => \&on_motion_hilbert, $hilbert_state);
    $drawing_area->signal_connect(query_tooltip => \&on_query_hilbert_tooltip, $hilbert_state);
    
    $drawing_area->signal_connect(enter_notify_event => \&on_hilbert_enter, $hilbert_state);
    $drawing_area->signal_connect(leave_notify_event => \&on_hilbert_leave, $hilbert_state);
    
    my $callback_data = [$hilbert_state, $parent_window, $proc_list_store, $selection];
    $drawing_area->signal_connect(button_press_event => \&on_hilbert_button_press, $callback_data);

    $state->{hilbert_window_ref} = $drawing_area;
    $drawing_area->{'hilbert_state'} = $hilbert_state;

    $popup->signal_connect(destroy => sub { 
        $state->{hilbert_window_ref} = undef; 
    });

    my $sw = Gtk2::ScrolledWindow->new(undef, undef);
    $sw->set_policy('automatic', 'automatic');
    $sw->add_with_viewport($drawing_area);
    $popup->add($sw);
    $popup->show_all;
}

# --- Thumbnail Window ---
sub show_thumbnail_view {
    my ($parent, $popup_state, $hadjustment, $vadjustment) = @_;

    if (defined $popup_state->{thumbnail_window_ref}) {
        my $win = $popup_state->{thumbnail_window_ref}->get_toplevel;
        $win->present if $win;
        return;
    }
    
    my $full_pixbuf = $popup_state->{pixbuf};
    return unless $full_pixbuf;

    my $thumb_width = 500;
    my $orig_w = $full_pixbuf->get_width;
    my $orig_h = $full_pixbuf->get_height;
    return if $orig_w == 0;
    my $scale_factor = $thumb_width / $orig_w;
    my $thumb_height = int($orig_h * $scale_factor);
    
    my $thumb_pixbuf = $full_pixbuf->scale_simple($thumb_width, $thumb_height, 'bilinear');

    my $thumb_state = {
        pixbuf      => $thumb_pixbuf,
        scale_x     => $scale_factor,
        scale_y     => $thumb_height / $orig_h,
        hadjustment => $hadjustment,
        vadjustment => $vadjustment,
    };

    my $thumb_window = Gtk2::Window->new('toplevel');
    $thumb_window->set_title("Thumbnail View");
    $thumb_window->set_transient_for($parent);
    $thumb_window->set_position('center-on-parent');
    
    my $thumb_da = Gtk2::DrawingArea->new;
    $thumb_da->set_size_request($thumb_width, $thumb_height);
    $thumb_da->set_events(['button-press-mask']);
    
    $thumb_da->signal_connect(expose_event => \&on_expose_thumbnail, $thumb_state);
    $thumb_da->signal_connect(button_press_event => \&on_thumbnail_click, $thumb_state);

    $thumb_window->add($thumb_da);

    $popup_state->{thumbnail_window_ref} = $thumb_da;
    $thumb_window->signal_connect(destroy => sub {
        $popup_state->{thumbnail_window_ref} = undef;
    });

    $thumb_window->show_all;
}

sub on_expose_thumbnail {
    my ($widget, $event, $thumb_state) = @_;
    my $cr = Gtk2::Gdk::Cairo::Context->create($widget->window);

    # Draw the thumbnail image
    $cr->set_source_pixbuf($thumb_state->{pixbuf}, 0, 0);
    $cr->paint;

    # Draw the viewport rectangle
    my $h_adj = $thumb_state->{hadjustment};
    my $v_adj = $thumb_state->{vadjustment};
    
    my $h_value = $h_adj->get_value;
    my $v_value = $v_adj->get_value;
    my $h_page = $h_adj->get('page-size');
    my $v_page = $v_adj->get('page-size');

    my $rect_x = $h_value * $thumb_state->{scale_x};
    my $rect_y = $v_value * $thumb_state->{scale_y};
    my $rect_w = $h_page * $thumb_state->{scale_x};
    my $rect_h = $v_page * $thumb_state->{scale_y};

    $cr->set_source_rgba(1, 0, 1, 0.4); # Magenta, semi-transparent
    $cr->rectangle($rect_x, $rect_y, $rect_w, $rect_h);
    $cr->fill;
    
    $cr->set_source_rgba(1, 0, 1, 0.9); # Opaque outline
    $cr->set_line_width(1.5);
    $cr->rectangle($rect_x, $rect_y, $rect_w, $rect_h);
    $cr->stroke;

    return TRUE;
}

sub on_thumbnail_click {
    my ($widget, $event, $thumb_state) = @_;
    
    my $h_adj = $thumb_state->{hadjustment};
    my $v_adj = $thumb_state->{vadjustment};
    
    # Translate click on thumbnail to full image coordinates
    my $full_x = $event->x / $thumb_state->{scale_x};
    my $full_y = $event->y / $thumb_state->{scale_y};

    # Center the viewport on the clicked point
    my $h_page = $h_adj->get('page-size');
    my $v_page = $v_adj->get('page-size');
    
    my $new_h_val = $full_x - ($h_page / 2);
    my $new_v_val = $full_y - ($v_page / 2);

    # Clamp values to be within scrollable range
    $new_h_val = 0 if $new_h_val < 0;
    $new_v_val = 0 if $new_v_val < 0;
    
    my $max_h_scroll = $h_adj->get('upper') - $h_page;
    my $max_v_scroll = $v_adj->get('upper') - $v_page;
    
    $new_h_val = $max_h_scroll if $new_h_val > $max_h_scroll;
    $new_v_val = $max_v_scroll if $new_v_val > $max_v_scroll;

    $h_adj->set_value($new_h_val);
    $v_adj->set_value($new_v_val);
    
    return TRUE;
}

# --- Image View Pop-up ---
sub show_image_view {
    my ($parent_window) = @_;
    my $pid = $state->{selected_pid};
    return unless defined $pid;

    my $pname = "unknown";
    if (open(my $fh, '<', "/proc/$pid/comm")) {
        $pname = <$fh>;
        chomp $pname;
        close $fh;
    }

    my $popup_state = {
        pid                   => $pid,
        pname                 => $pname,
        timer_id              => undef,
        pixbuf                => undef,
        image_map_info        => [],
        raw_data_ref          => undef,
        loaded_raw_data_ref   => undef,
        last_saved_raw_file   => undef,
        diff_points           => undef,
        diff_toggle_button    => undef,
        overlay_map_areas     => [],
        drawing_area          => undef,
        update_toggle_button  => undef,
        overlay_toggle_button => undef,
        star_info             => undef,
        star_timer_id         => undef,
        sound_player_pid      => undef,
        sound_child_watch_id  => undef,
        sound_temp_file       => undef,
        _is_changing_sound_button => FALSE, 
        update_baseline_ref   => undef,
        selection_start_offset => undef,
        selection_end_offset   => undef,
        selection_highlight_rects => [],
        selection_toggle_button => undef,
        start_offset_entry     => undef,
        end_offset_entry       => undef,
        extra_selection_widgets => [],
        thumbnail_window_ref  => undef,
        infobar_labels        => {},
    };
    
    my $popup = Gtk2::Window->new('toplevel');
    $popup->set_title("Memory Image for PID: $pid, $pname");
    $popup->set_transient_for($parent_window);
    $popup->set_default_size(800, 600);
    $popup->set_position('center-on-parent');
    
    my $vbox = Gtk2::VBox->new(FALSE, 5);
    $vbox->set_border_width(5);
    $popup->add($vbox);
    
    my $label = Gtk2::Label->new("\nReading process memory and generating image...\n");
    $vbox->pack_start($label, TRUE, TRUE, 0);
    $popup->show_all;
    
    Gtk2->main_iteration while Gtk2->events_pending;

    my ($pixbuf, $map_info, $raw_data_ref) = generate_image_from_pid($pid);
    $popup_state->{pixbuf} = $pixbuf;
    $popup_state->{image_map_info} = $map_info;
    $popup_state->{raw_data_ref} = $raw_data_ref;

    $vbox->remove($label);
    
    if (defined $popup_state->{pixbuf}) {
        my $toolbar = Gtk2::HBox->new(FALSE, 5);
        
        # --- Create all widgets for the toolbar first ---
        my $save_png_button = Gtk2::Button->new("Save PNG");
        my $save_ram_button = Gtk2::Button->new("Save RAM Raw");
        my $load_ram_button = Gtk2::Button->new("Load RAM Raw");
        my $status_box = Gtk2::DrawingArea->new;
        $status_box->set_size_request(20, -1);
        $status_box->modify_bg('normal', Gtk2::Gdk::Color->parse('red'));
        my $diff_button = Gtk2::Button->new("Diff");
        my $diff_toggle = Gtk2::CheckButton->new("Show Diffs");
        $popup_state->{diff_toggle_button} = $diff_toggle;
        
        my $strings_button = Gtk2::Button->new("Strings");
        my $sound_button = Gtk2::ToggleButton->new("Sound");
        my $save_wav_button = Gtk2::Button->new("Save wav");
        
        # --- Create the "Show:" group ---
        my $show_hbox = Gtk2::HBox->new(FALSE, 5);
        my $show_label = Gtk2::Label->new("Show:");
        my $overlay_toggle = Gtk2::CheckButton->new("Overlays");
        $overlay_toggle->set_active(TRUE);
        $popup_state->{overlay_toggle_button} = $overlay_toggle;
        my $thumbnail_check = Gtk2::CheckButton->new("Thumbnail");
        my $infobar_toggle = Gtk2::CheckButton->new("Infobar");

        # --- Create the update controls ---
        my $update_label = Gtk2::Label->new("Update(s):");
        my $update_entry = Gtk2::Entry->new();
        $update_entry->set_text("1.0");
        $update_entry->set_width_chars(4);
        my $update_toggle = Gtk2::ToggleButton->new("Start Updating");
        $popup_state->{update_toggle_button} = $update_toggle;

        # --- Pack everything into the toolbars ---
        $toolbar->pack_start($save_png_button, FALSE, FALSE, 0);
        $toolbar->pack_start($save_ram_button, FALSE, FALSE, 5);
        $toolbar->pack_start($load_ram_button, FALSE, FALSE, 5);
        $toolbar->pack_start($status_box, FALSE, FALSE, 5);
        $toolbar->pack_start($diff_button, FALSE, FALSE, 0);
        $toolbar->pack_start($diff_toggle, FALSE, FALSE, 5);
        $toolbar->pack_start($strings_button, FALSE, FALSE, 5);
        $toolbar->pack_start($sound_button, FALSE, FALSE, 0);
        $toolbar->pack_start($save_wav_button, FALSE, FALSE, 5);
        
        # Pack the "Show:" group
        $show_hbox->pack_start($show_label, FALSE, FALSE, 0);
        $show_hbox->pack_start($overlay_toggle, FALSE, FALSE, 5);
        $show_hbox->pack_start($thumbnail_check, FALSE, FALSE, 5);
        $show_hbox->pack_start($infobar_toggle, FALSE, FALSE, 5);
        $toolbar->pack_start($show_hbox, FALSE, FALSE, 10);
        
        # Pack the update controls
        $toolbar->pack_start($update_label, FALSE, FALSE, 10);
        $toolbar->pack_start($update_entry, FALSE, FALSE, 0);
        $toolbar->pack_start($update_toggle, FALSE, FALSE, 5);
        
        $vbox->pack_start($toolbar, FALSE, FALSE, 0);

        my $multi_selection_hbox = Gtk2::HBox->new(FALSE, 5);
        $vbox->pack_start($multi_selection_hbox, FALSE, FALSE, 0);

        # --- NEW: BITMAP DECODE WIDGETS ---
        my $decode_hbox = Gtk2::HBox->new(FALSE, 5);
        my $decode_button = Gtk2::Button->new("Bitmap Decode");
        
        my $min_adj = Gtk2::Adjustment->new(200, 8, 4096, 1, 10, 0);
        my $min_spin = Gtk2::SpinButton->new($min_adj, 0, 0);
        $min_spin->set_width_chars(5);
        
        my $max_adj = Gtk2::Adjustment->new(800, 8, 4096, 1, 10, 0);
        my $max_spin = Gtk2::SpinButton->new($max_adj, 0, 0);
        $max_spin->set_width_chars(5);
        
        $decode_hbox->pack_start($decode_button, FALSE, FALSE, 0);
        $decode_hbox->pack_start(Gtk2::Label->new(" Min:"), FALSE, FALSE, 5);
        $decode_hbox->pack_start($min_spin, FALSE, FALSE, 0);
        $decode_hbox->pack_start(Gtk2::Label->new(" Max:"), FALSE, FALSE, 5);
        $decode_hbox->pack_start($max_spin, FALSE, FALSE, 0);
        
        $multi_selection_hbox->pack_start($decode_hbox, FALSE, FALSE, 15);

        # --- EXISTING: OFFSET SELECTION WIDGETS ---
        my $start_label = Gtk2::Label->new("Selected Offsets start:");
        my $start_entry = Gtk2::Entry->new();
        $start_entry->set_width_chars(10);
        $popup_state->{start_offset_entry} = $start_entry;

        my $stop_label = Gtk2::Label->new("stop:");
        my $end_entry = Gtk2::Entry->new();
        $end_entry->set_width_chars(10);
        $popup_state->{end_offset_entry} = $end_entry;

        my $selection_toggle = Gtk2::CheckButton->new();
        $selection_toggle->set_active(TRUE);
        $popup_state->{selection_toggle_button} = $selection_toggle;
        
        $multi_selection_hbox->pack_start($start_label, FALSE, FALSE, 0);
        $multi_selection_hbox->pack_start($start_entry, FALSE, FALSE, 5);
        $multi_selection_hbox->pack_start($stop_label, FALSE, FALSE, 5);
        $multi_selection_hbox->pack_start($end_entry, FALSE, FALSE, 5);
        $multi_selection_hbox->pack_start($selection_toggle, FALSE, FALSE, 5);

        my $add_more_button = Gtk2::Button->new("Add more");
        $multi_selection_hbox->pack_start($add_more_button, FALSE, FALSE, 10);
        
        my $extra_selections_hbox = Gtk2::HBox->new(FALSE, 5);
        $multi_selection_hbox->pack_start($extra_selections_hbox, TRUE, TRUE, 0);
        
        $add_more_button->signal_connect(clicked => sub {
            _add_extra_selection_ui($extra_selections_hbox, $popup_state);
        });

        my $infobar_hbox = Gtk2::HBox->new(FALSE, 5);
        
        my $path_title_label = Gtk2::Label->new;
        $path_title_label->set_markup("<b>Path:</b> ");
        my $path_info_label = Gtk2::Label->new;

        my $size_title_label = Gtk2::Label->new;
        $size_title_label->set_markup("<b>Size:</b> ");
        my $size_info_label = Gtk2::Label->new;
        
        my $perms_title_label = Gtk2::Label->new;
        $perms_title_label->set_markup("<b>Perms:</b> ");
        my $perms_info_label = Gtk2::Label->new;

        my $range_title_label = Gtk2::Label->new;
        $range_title_label->set_markup("<b>Range:</b> ");
        my $range_info_label = Gtk2::Label->new;
        
        $infobar_hbox->pack_start($path_title_label, FALSE, FALSE, 5);
        $infobar_hbox->pack_start($path_info_label, FALSE, FALSE, 5);
        $infobar_hbox->pack_start($size_title_label, FALSE, FALSE, 5);
        $infobar_hbox->pack_start($size_info_label, FALSE, FALSE, 5);
        $infobar_hbox->pack_start($perms_title_label, FALSE, FALSE, 5);
        $infobar_hbox->pack_start($perms_info_label, FALSE, FALSE, 5);
        $infobar_hbox->pack_start($range_title_label, FALSE, FALSE, 5);
        $infobar_hbox->pack_start($range_info_label, FALSE, FALSE, 5);

        $vbox->pack_start($infobar_hbox, FALSE, FALSE, 5);

        $popup_state->{infobar_labels} = {
            path  => $path_info_label,
            size  => $size_info_label,
            perms => $perms_info_label,
            range => $range_info_label,
        };

        $infobar_toggle->signal_connect(toggled => sub {
            my $button = shift;
            if ($button->get_active) {
                $infobar_hbox->show();
            } else {
                $infobar_hbox->hide();
            }
        });
        
        my $aplay_path = `which aplay`;
        chomp $aplay_path;
        unless (-x $aplay_path) {
            $sound_button->set_sensitive(FALSE);
            $sound_button->set_tooltip_text("Could not find 'aplay' executable in your PATH.");
            $save_wav_button->set_sensitive(FALSE);
            $save_wav_button->set_tooltip_text("Sound support ('aplay') is missing.");
        }
        
        my $img_w = $popup_state->{pixbuf}->get_width;
        my $img_h = $popup_state->{pixbuf}->get_height;

        $popup_state->{drawing_area} = Gtk2::DrawingArea->new;
        $popup_state->{drawing_area}->set_size_request($img_w, $img_h);
        $popup_state->{drawing_area}->set_events(['button-press-mask', 'pointer-motion-mask']);
        $popup_state->{drawing_area}->signal_connect(button_press_event => \&on_image_button_press, $popup_state);
        $popup_state->{drawing_area}->signal_connect(expose_event => \&draw_image_with_overlays, $popup_state);
        $popup_state->{drawing_area}->signal_connect(motion_notify_event => \&on_image_motion, $popup_state);
        $popup_state->{drawing_area}->set_has_tooltip(TRUE);
        $popup_state->{drawing_area}->signal_connect(query_tooltip => \&on_query_image_tooltip, $popup_state);

        my $sw = Gtk2::ScrolledWindow->new(undef, undef);
        $sw->set_policy('automatic', 'automatic');
        $sw->add_with_viewport($popup_state->{drawing_area});
        $vbox->pack_start($sw, TRUE, TRUE, 0);
        
        my $hadjustment = $sw->get_hadjustment;
        my $vadjustment = $sw->get_vadjustment;
        my $update_thumb_cb = sub {
            if (my $thumb_da = $popup_state->{thumbnail_window_ref}) {
                $thumb_da->queue_draw();
            }
        };
        $hadjustment->signal_connect(value_changed => $update_thumb_cb);
        $vadjustment->signal_connect(value_changed => $update_thumb_cb);

        # --- Connect signals for new Bitmap Decode button ---
        $decode_button->signal_connect(clicked => sub {
            on_bitmap_decode_click($popup, $popup_state, $min_spin, $max_spin);
        });

        $thumbnail_check->signal_connect(toggled => sub {
            my $checkbox = shift;
            if ($checkbox->get_active) {
                show_thumbnail_view($popup, $popup_state, $hadjustment, $vadjustment);
            } else {
                if (my $thumb_da = $popup_state->{thumbnail_window_ref}) {
                    if (my $win = $thumb_da->get_toplevel) {
                        $win->destroy;
                    }
                }
            }
        });

        if ($img_w > 1900 or $img_h > 1900) {
            show_thumbnail_view($popup, $popup_state, $hadjustment, $vadjustment);
            $thumbnail_check->set_active(TRUE);
        }

        $overlay_toggle->signal_connect(toggled => sub { $popup_state->{drawing_area}->queue_draw(); });
        $diff_toggle->signal_connect(toggled => sub { $popup_state->{drawing_area}->queue_draw(); });
        $selection_toggle->signal_connect(toggled => sub { _update_selection_visuals($popup_state); });
        $start_entry->signal_connect(activate => \&on_offset_entry_activate, $popup_state);
        $end_entry->signal_connect(activate => \&on_offset_entry_activate, $popup_state);
        
        $sound_button->signal_connect(toggled => \&on_sound_toggle, $popup_state);

        $save_png_button->signal_connect(clicked => sub {
            my ($data_ref_for_action, $filename_offsets, $range_map, $path_info) = get_data_for_action($popup_state);
            return unless (defined $data_ref_for_action and length ${$data_ref_for_action} > 0);
            
            my $pixbuf_to_save = create_pixbuf_from_data($data_ref_for_action);
            unless ($pixbuf_to_save) {
                my $err_dialog = Gtk2::MessageDialog->new($popup, 'destroy-with-parent', 'error', 'close', "Could not create image from selected data.\nRegion may be too small.");
                $err_dialog->run;
                $err_dialog->destroy;
                return;
            }

            my $chooser = Gtk2::FileChooserDialog->new("Save Memory Image", $popup, 'save', 'gtk-cancel' => 'cancel', 'gtk-save' => 'accept');
            my $name = _generate_filename($popup_state, $filename_offsets, $path_info);
            $chooser->set_current_name($name . ".png");

            my $filter = Gtk2::FileFilter->new;
            $filter->set_name("PNG Images");
            $filter->add_mime_type("image/png");
            $chooser->add_filter($filter);
            if ($chooser->run eq 'accept') {
                my $filename = $chooser->get_filename;
                $filename .= ".png" unless $filename =~ /\.png$/i;
                eval { $pixbuf_to_save->save($filename, 'png'); };
                if ($@) {
                    my $err_dialog = Gtk2::MessageDialog->new($popup, 'destroy-with-parent', 'error', 'close', "Error saving file:\n$@");
                    $err_dialog->run;
                    $err_dialog->destroy;
                }
            }
            $chooser->destroy;
        });

        $save_ram_button->signal_connect(clicked => sub {
            my ($data_ref_for_action, $filename_offsets, $range_map, $path_info) = get_data_for_action($popup_state);
            return unless (defined $data_ref_for_action and length ${$data_ref_for_action} > 0);

            my $chooser = Gtk2::FileChooserDialog->new("Save Raw Memory Dump", $popup, 'save', 'gtk-cancel' => 'cancel', 'gtk-save' => 'accept');
            my $name = _generate_filename($popup_state, $filename_offsets, $path_info);
            $chooser->set_current_name($name . ".raw");

            if ($chooser->run eq 'accept') {
                my $filename = $chooser->get_filename;
                $popup_state->{last_saved_raw_file} = $filename;
                if (open(my $fh, '>', $filename)) {
                    binmode $fh;
                    print $fh ${$data_ref_for_action};
                    close $fh;
                } else {
                    my $err_dialog = Gtk2::MessageDialog->new($popup, 'destroy-with-parent', 'error', 'close', "Error saving file:\n$!");
                    $err_dialog->run;
                    $err_dialog->destroy;
                }
            }
            $chooser->destroy;
        });
        
        $save_wav_button->signal_connect(clicked => sub {
            my ($data_ref_for_action, $filename_offsets, $range_map, $path_info) = get_data_for_action($popup_state);
            return unless (defined $data_ref_for_action and length ${$data_ref_for_action} > 0);
            
            my $chooser = Gtk2::FileChooserDialog->new("Save Memory as WAV", $popup, 'save', 'gtk-cancel' => 'cancel', 'gtk-save' => 'accept');
            my $name = _generate_filename($popup_state, $filename_offsets, $path_info);
            $chooser->set_current_name($name . ".wav");

            if ($chooser->run eq 'accept') {
                my $filename = $chooser->get_filename;
                $filename .= ".wav" unless $filename =~ /\.wav$/i;
                if (open(my $fh, '>', $filename)) {
                    binmode $fh;
                    my $wav_header = _create_wav_header(length ${$data_ref_for_action});
                    print $fh $wav_header;
                    print $fh ${$data_ref_for_action};
                    close $fh;
                } else {
                     my $err_dialog = Gtk2::MessageDialog->new($popup, 'destroy-with-parent', 'error', 'close', "Error saving file:\n$!");
                    $err_dialog->run;
                    $err_dialog->destroy;
                }
            }
            $chooser->destroy;
        });

        $load_ram_button->signal_connect(clicked => sub {
            $popup_state->{update_baseline_ref} = undef; 
            
            my $chooser = Gtk2::FileChooserDialog->new("Load Raw Memory Dump", $popup, 'open', 'gtk-cancel' => 'cancel', 'gtk-open' => 'accept');
            if (defined $popup_state->{last_saved_raw_file}) {
                $chooser->set_filename($popup_state->{last_saved_raw_file});
            }
            if ($chooser->run eq 'accept') {
                my $filename = $chooser->get_filename;
                if (open(my $fh, '<', $filename)) {
                    binmode $fh;
                    my $loaded_data;
                    read $fh, $loaded_data, -s $fh;
                    close $fh;
                    
                    $popup_state->{loaded_raw_data_ref} = \$loaded_data;
                    $status_box->modify_bg('normal', Gtk2::Gdk::Color->parse('green'));
                    $diff_button->show;
                    $diff_toggle->show;
                } else {
                    my $err_dialog = Gtk2::MessageDialog->new($popup, 'destroy-with-parent', 'error', 'close', "Error loading file:\n$!");
                    $err_dialog->run;
                    $err_dialog->destroy;
                }
            }
            $chooser->destroy;
        });
        
        $diff_button->signal_connect(clicked => sub {
            perform_ram_diff($popup_state);
            $popup_state->{diff_toggle_button}->set_active(TRUE);
        });
        
        $strings_button->signal_connect(clicked => \&show_strings_view, $popup_state);

        $update_toggle->signal_connect(toggled => sub {
            my $button = shift;
            if ($button->get_active) {
                # --- STARTING ---
                my $interval_sec = $update_entry->get_text;
                if ($interval_sec =~ /^\d*\.?\d+$/ && $interval_sec > 0) {
                    $button->set_label("Stop Updating");
                    
                    my $baseline_data = ${$popup_state->{raw_data_ref}};
                    $popup_state->{update_baseline_ref} = \$baseline_data;
                    
                    $popup_state->{loaded_raw_data_ref} = undef;
                    $popup_state->{diff_points} = undef;
                    $diff_button->hide;
                    $diff_toggle->hide;
                    $status_box->modify_bg('normal', Gtk2::Gdk::Color->parse('red'));
                    $popup_state->{drawing_area}->queue_draw();

                    $popup_state->{timer_id} = Glib::Timeout->add($interval_sec * 1000, \&update_image_widget, $popup_state);
                } else {
                    $button->set_active(FALSE);
                }
            } else {
                # --- STOPPING ---
                if (defined $popup_state->{timer_id}) {
                    Glib::Source->remove($popup_state->{timer_id});
                    $popup_state->{timer_id} = undef;
                }
                $button->set_label("Start Updating");

                if (defined $popup_state->{update_baseline_ref}) {
                    $popup_state->{loaded_raw_data_ref} = $popup_state->{update_baseline_ref};
                    $popup_state->{update_baseline_ref} = undef;

                    $status_box->modify_bg('normal', Gtk2::Gdk::Color->parse('green'));
                    $diff_button->show;
                    $diff_toggle->show;
                }
            }
        });

        # Show all widgets now that the UI is fully constructed.
        $popup->show_all();
        
        # Explicitly hide widgets that should start hidden, overriding show_all's effect.
        $diff_button->hide();
        $diff_toggle->hide();
        $infobar_hbox->hide();
        
    } else {
        my $error_label = Gtk2::Label->new("\nCould not read memory for PID $pid.\nProcess may have ended or permissions are insufficient.\n");
        $vbox->pack_start($error_label, TRUE, TRUE, 0);
        $popup->set_default_size(400, 100);
        $popup->show_all();
    }
    
    $popup->signal_connect(destroy => sub {
        # --- START: Comprehensive Memory Cleanup ---

        # 1. Stop any running timers to prevent them from firing after
        #    the window is gone and holding references to the state.
        if (defined $popup_state->{timer_id}) {
            Glib::Source->remove($popup_state->{timer_id});
            $popup_state->{timer_id} = undef;
        }
        if (defined $popup_state->{star_timer_id}) {
            Glib::Source->remove($popup_state->{star_timer_id});
            $popup_state->{star_timer_id} = undef;
        }

        # 2. Terminate external processes (like the sound player) and clean up temp files.
        if (defined $popup_state->{sound_player_pid}) {
            kill 'TERM', -($popup_state->{sound_player_pid});
            unlink $popup_state->{sound_temp_file} if defined $popup_state->{sound_temp_file};
            $popup_state->{sound_player_pid} = undef;
        }

        # 3. Explicitly destroy any separate top-level windows we created,
        #    like the thumbnail viewer. This will trigger their own cleanup.
        if (my $thumb_da = $popup_state->{thumbnail_window_ref}) {
            my $win = $thumb_da->get_toplevel;
            $win->destroy if $win;
            $popup_state->{thumbnail_window_ref} = undef;
        }

        # 4. Undefine references to large data structures to allow Perl's garbage
        #    collector to reclaim the memory immediately. This is the most
        #    critical part for fixing the memory leak.
        $popup_state->{raw_data_ref}        = undef;
        $popup_state->{loaded_raw_data_ref} = undef;
        $popup_state->{pixbuf}              = undef;
        $popup_state->{update_baseline_ref} = undef;
        $popup_state->{image_map_info}      = [];
        $popup_state->{overlay_map_areas}   = [];
        $popup_state->{diff_points}         = undef;
        $popup_state->{selection_highlight_rects} = [];

        # NOTE: It is not necessary to undefine references to widgets that are
        # children of this popup window (e.g., buttons, entries). Gtk2 handles
        # their destruction automatically when the parent window is destroyed.
        
        # --- END: Comprehensive Memory Cleanup ---
    });
}

# --- START: Bitmap Decode Feature ---
sub on_bitmap_decode_click {
    my ($parent_window, $popup_state, $min_spin, $max_spin) = @_;

    my ($data_ref_for_action, undef, undef, undef) = get_data_for_action($popup_state);
    
    unless (defined $data_ref_for_action and ${$data_ref_for_action}) {
        my $err_dialog = Gtk2::MessageDialog->new($parent_window, 'destroy-with-parent', 'error', 'close', "No data selected to analyze.\nUse the selection tools to choose a memory region first.");
        $err_dialog->run;
        $err_dialog->destroy;
        return;
    }
    
    my $min_width = $min_spin->get_value_as_int;
    my $max_width = $max_spin->get_value_as_int;

    if ($min_width >= $max_width) {
        my $err_dialog = Gtk2::MessageDialog->new($parent_window, 'destroy-with-parent', 'error', 'close', "Min width must be less than Max width.");
        $err_dialog->run;
        $err_dialog->destroy;
        return;
    }

    # Immediately show the window with the min_width as the initial guess.
    show_bitmap_decode_result($parent_window, $popup_state, $data_ref_for_action, $min_width, $min_width, $max_width);
}

sub run_bitmap_decode {
    my ($raw_data_ref, $min_width_to_test, $max_width_to_test, $progress_callback, $cancel_flag_ref) = @_;

    my $raw_data = ${$raw_data_ref};
    my $data_size = length($raw_data);
    
    return -1 if $data_size < ($min_width_to_test * 3 * 2);

    my $pdl_data = PDL->pdl( unpack 'C*', $raw_data );

    my $best_width = -1;
    my $lowest_score = -1;

    my $start_msg = "\n--- Starting Bitmap Analysis ---\n";
    print $start_msg;
    $progress_callback->($start_msg) if defined $progress_callback;

    for my $test_width ( $min_width_to_test .. $max_width_to_test ) {
        if ($cancel_flag_ref and ${$cancel_flag_ref}) {
            my $cancel_msg = "\n--- Analysis Cancelled by User ---\n";
            print $cancel_msg;
            $progress_callback->($cancel_msg) if defined $progress_callback;
            return undef; # Signal cancellation
        }

        my $num_pixels = $data_size / 3;
        next if $test_width > $num_pixels;
        
        my $test_height = int($num_pixels / $test_width);
        next if $test_height < 2;

        my $trimmed_size = $test_width * $test_height * 3;
        my $image_pdl = $pdl_data->slice("0:" . ($trimmed_size - 1));

        my $reshaped = $image_pdl->reshape(3 * $test_width, $test_height);
        my $top_rows = $reshaped->slice(":,0:-2");
        my $bottom_rows = $reshaped->slice(":,1:-1");
        my $score = sum(abs($top_rows - $bottom_rows));
        
        my $normalized_score = $score / $test_height;
        my $progress_line = sprintf("Width: %4d | Height: %4d | Score: %.2f\n", $test_width, $test_height, $normalized_score);
        print $progress_line;
        $progress_callback->($progress_line) if defined $progress_callback;


        if ($lowest_score == -1 or $normalized_score < $lowest_score) {
            $lowest_score = $normalized_score;
            $best_width = $test_width;
        }
    }

    my $end_msg;
    if ($best_width != -1) {
        $end_msg = sprintf("\n--- Analysis Complete ---\nLowest normalized score was %.2f at a width of %d\n", $lowest_score, $best_width);
    } else {
        $end_msg = "\n--- Analysis Complete ---\nCould not find a suitable width in the given range.\n";
    }
    print $end_msg;
    $progress_callback->($end_msg) if defined $progress_callback;

    return $best_width;
}

sub show_bitmap_decode_result {
    my ($parent_window, $main_popup_state, $raw_data_ref, $initial_width, $initial_min, $initial_max) = @_;

    my $window = Gtk2::Window->new('toplevel');
    $window->set_transient_for($parent_window);
    $window->set_position('center-on-parent');
    $window->signal_connect(destroy => sub { $_[0]->destroy; });
    
    my $vbox = Gtk2::VBox->new(FALSE, 2);
    $window->add($vbox);
    
    # --- Data and widgets that need to be in scope for callbacks ---
    my $raw_data = ${$raw_data_ref};
    my $data_size = length($raw_data);
    my $current_pixbuf; 
    my $image = Gtk2::Image->new;
    
    # --- Toolbar for Width Control ---
    my $toolbar = Gtk2::HBox->new(FALSE, 5);
    $toolbar->set_border_width(5);
    my $save_button = Gtk2::Button->new("Save PNG");
    my $find_button = Gtk2::Button->new("Find Width");
    
    my $width_adj = Gtk2::Adjustment->new($initial_width, $initial_min, $initial_max + 1, 1, 10, 0);
    
    my $min_adj = Gtk2::Adjustment->new($initial_min, 8, 8192, 1, 10, 0);
    my $min_spin = Gtk2::SpinButton->new($min_adj, 0, 0);
    $min_spin->set_width_chars(5);

    my $max_adj = Gtk2::Adjustment->new($initial_max, 8, 8192, 1, 10, 0);
    my $max_spin = Gtk2::SpinButton->new($max_adj, 0, 0);
    $max_spin->set_width_chars(5);
    
    my $width_scrollbar = Gtk2::HScrollbar->new($width_adj);
    my $current_width_label = Gtk2::Label->new($initial_width);
    $current_width_label->set_width_chars(5);

    $toolbar->pack_start($save_button, FALSE, FALSE, 0);
    $toolbar->pack_start($min_spin, FALSE, FALSE, 5);
    $toolbar->pack_start($width_scrollbar, TRUE, TRUE, 5);
    $toolbar->pack_start($max_spin, FALSE, FALSE, 5);
    $toolbar->pack_start($current_width_label, FALSE, FALSE, 5);
    $toolbar->pack_end($find_button, FALSE, FALSE, 10);
    $vbox->pack_start($toolbar, FALSE, FALSE, 0);

    # --- Toolbar for Offset Control ---
    my $offset_toolbar = Gtk2::HBox->new(FALSE, 5);
    $offset_toolbar->set_border_width(5);

    my $offset_label = Gtk2::Label->new("Pixel Byte Offset: ");
    my $offset_adj = Gtk2::Adjustment->new(0, 0, 2048, 1, 10, 0); 
    my $offset_spin = Gtk2::SpinButton->new($offset_adj, 0, 0);
    $offset_spin->set_width_chars(8);

    $offset_toolbar->pack_start($offset_label, FALSE, FALSE, 0);
    $offset_toolbar->pack_start($offset_spin, FALSE, FALSE, 5);
    $vbox->pack_start($offset_toolbar, FALSE, FALSE, 0);
    
    # --- Image Display ---
    my $sw = Gtk2::ScrolledWindow->new (undef, undef);
    $sw->set_policy ('automatic', 'automatic');
    $sw->add_with_viewport ($image);
    $vbox->pack_start($sw, TRUE, TRUE, 0);

    # --- Callback to regenerate the image ---
    my $update_image = sub {
        my $new_width = int($width_adj->get_value);
        my $byte_offset = int($offset_adj->get_value);
        
        return if $new_width <= 0;
        
        return if $byte_offset >= $data_size - 3;
        
        my $sliced_data = substr($raw_data, $byte_offset);
        my $sliced_data_size = length($sliced_data);

        my $new_height = int( ($sliced_data_size / 3) / $new_width );
        return if $new_height <= 0;
        
        $current_pixbuf = eval {
             Gtk2::Gdk::Pixbuf->new_from_data(
                $sliced_data, 'rgb', 0, 8, $new_width, $new_height, $new_width * 3
            );
        };
        
        if ($@ or not defined $current_pixbuf) {
            $image->clear(); 
            warn "Could not create pixbuf for width $new_width with offset $byte_offset: $@";
            return;
        }
        
        $image->set_from_pixbuf($current_pixbuf);
        $current_width_label->set_text($new_width);
        $window->set_title("Decoded Bitmap: $new_width x $new_height (Offset: $byte_offset)");
    };

    # --- Signal Connections ---
    $width_adj->signal_connect(value_changed => $update_image);
    
    $min_spin->signal_connect(value_changed => sub {
        my $new_min = $min_spin->get_value_as_int;
        my $current_max = $max_spin->get_value_as_int;
        $width_adj->set('lower', $new_min) if $new_min < $current_max;
    });

    $max_spin->signal_connect(value_changed => sub {
        my $new_max = $max_spin->get_value_as_int;
        my $current_min = $min_spin->get_value_as_int;
        $width_adj->set('upper', $new_max + 1) if $new_max > $current_min;
    });

    $find_button->signal_connect(clicked => sub {
        my $min_w = $min_spin->get_value_as_int;
        my $max_w = $max_spin->get_value_as_int;

        if ($min_w >= $max_w) {
            my $err_dialog = Gtk2::MessageDialog->new($window, 'destroy-with-parent', 'error', 'close', "Min width must be less than Max width.");
            $err_dialog->run;
            $err_dialog->destroy;
            return;
        }

        my $progress_dialog = Gtk2::Window->new('toplevel');
        $progress_dialog->set_title("Analyzing...");
        $progress_dialog->set_transient_for($window);
        $progress_dialog->set_position('center-on-parent');
        $progress_dialog->set_default_size(400, 300);
        
        my $vbox_progress = Gtk2::VBox->new(FALSE, 5);
        $vbox_progress->set_border_width(5);
        $progress_dialog->add($vbox_progress);

        my $sw_progress = Gtk2::ScrolledWindow->new(undef, undef);
        $sw_progress->set_policy('automatic', 'automatic');
        
        my $tv = Gtk2::TextView->new();
        $tv->set_editable(FALSE);
        $sw_progress->add($tv);
        $vbox_progress->pack_start($sw_progress, TRUE, TRUE, 0);

        my $cancel_button = Gtk2::Button->new_from_stock('gtk-cancel');
        my $action_area = Gtk2::HBox->new(FALSE, 5);
        $action_area->pack_end($cancel_button, FALSE, FALSE, 0);
        $vbox_progress->pack_start($action_area, FALSE, FALSE, 5);

        $progress_dialog->show_all;
        Gtk2->main_iteration while Gtk2->events_pending;

        my $cancel_flag = 0;
        my $cancel_cid = $cancel_button->signal_connect(clicked => sub {
            $cancel_flag = 1;
        });

        my $buffer = $tv->get_buffer;
        my $update_callback = sub {
            my ($line) = @_;
            my $end_iter = $buffer->get_end_iter;
            $buffer->insert($end_iter, $line);
            # Auto-scroll to the end
            my $mark = $buffer->create_mark('end_mark', $buffer->get_end_iter, FALSE);
            $tv->scroll_to_mark($mark, 0.0, TRUE, 0.0, 1.0);
            $buffer->delete_mark($mark);
            Gtk2->main_iteration while Gtk2->events_pending;
        };

        my $found_width = run_bitmap_decode($raw_data_ref, $min_w, $max_w, $update_callback, \$cancel_flag);
        
        $cancel_button->signal_disconnect($cancel_cid) if $Gtk2::Widget::VERSION;
        $progress_dialog->destroy;

        if (defined $found_width) {
            $width_adj->set_value($found_width);
        } elsif (!$cancel_flag) {
            my $err_dialog = Gtk2::MessageDialog->new($window, 'destroy-with-parent', 'info', 'close', "Could not determine an optimal width in the tested range.");
            $err_dialog->run;
            $err_dialog->destroy;
        }
        # If cancelled, do nothing.
    });
    
    $save_button->signal_connect(clicked => sub {
        return unless defined $current_pixbuf;
        my $current_width = int($width_adj->get_value);

        my ($data_ref, $offsets_str, $range_map, $path_info) = get_data_for_action($main_popup_state);
        my $base_name = _generate_filename($main_popup_state, $offsets_str, $path_info);
        my $final_name = $base_name . "_decoded_${current_width}px.png";

        my $chooser = Gtk2::FileChooserDialog->new("Save Decoded Image", $window, 'save', 'gtk-cancel' => 'cancel', 'gtk-save' => 'accept');
        $chooser->set_current_name($final_name);
        
        my $filter = Gtk2::FileFilter->new;
        $filter->set_name("PNG Images");
        $filter->add_mime_type("image/png");
        $chooser->add_filter($filter);
        
        if ($chooser->run eq 'accept') {
            my $filename = $chooser->get_filename;
            $filename .= ".png" unless $filename =~ /\.png$/i;
            eval { $current_pixbuf->save($filename, 'png'); };
            if ($@) {
                my $err_dialog = Gtk2::MessageDialog->new($window, 'destroy-with-parent', 'error', 'close', "Error saving file:\n$@");
                $err_dialog->run;
                $err_dialog->destroy;
            }
        }
        $chooser->destroy;
    });

    $offset_adj->signal_connect(value_changed => sub {
        $update_image->();
    });

    # --- Initial Setup ---
    $update_image->(); # Create the first image
    $window->set_default_size(800, 600);
    $window->show_all;
}
# --- END: New Bitmap Decode Feature ---

sub on_image_motion {
    my ($widget, $event, $popup_state) = @_;
    my ($x, $y) = ($event->x, $event->y);
    
    my $labels = $popup_state->{infobar_labels};
    my $found_segment = FALSE;

    for my $area (@{$popup_state->{overlay_map_areas}}) {
        for my $rect (@{$area->{rects}}) {
            my ($rx, $ry, $rw, $rh) = @$rect;
            if ($x >= $rx && $x < ($rx + $rw) && $y >= $ry && $y < ($ry + $rh)) {
                my $seg = $area->{data};
                my $path = $seg->{path} || "[anonymous]";
                $path =~ s/&/&amp;/g; $path =~ s/</&lt;/g; $path =~ s/>/&gt;/g;

                $labels->{path}->set_text($path);
                $labels->{size}->set_text(format_bytes($seg->{size}));
                $labels->{perms}->set_text($seg->{perms});
                $labels->{range}->set_text(sprintf("0x%x - 0x%x", $seg->{start}, $seg->{end}));
                
                $found_segment = TRUE;
                last; 
            }
        }
        last if $found_segment;
    }

    if (!$found_segment) {
        $labels->{path}->set_text("");
        $labels->{size}->set_text("");
        $labels->{perms}->set_text("");
        $labels->{range}->set_text("");
    }

    return TRUE;
}

sub on_image_button_press {
    my ($widget, $event, $popup_state) = @_;

    # Handle left-click to select/deselect an entire segment
    if ($event->button == 1) {
        my ($x, $y) = ($event->x, $event->y);
        my $found_and_handled = FALSE;

        for my $area (@{$popup_state->{overlay_map_areas}}) {
            for my $rect (@{$area->{rects}}) {
                my ($rx, $ry, $rw, $rh) = @$rect;
                if ($x >= $rx && $x < ($rx + $rw) && $y >= $ry && $y < ($ry + $rh)) {
                    my $seg = $area->{data};
                    
                    my $clicked_start = $seg->{offset_in_image};
                    my $clicked_end = $seg->{offset_in_image} + $seg->{length_in_image} - 1;
                    $clicked_end = $clicked_start if $clicked_end < $clicked_start;

                    my $current_start = $popup_state->{selection_start_offset};
                    my $current_end   = $popup_state->{selection_end_offset};
                    
                    # If start/end are set, ensure they are in the correct order for comparison
                    if (defined $current_start && defined $current_end && $current_start > $current_end) {
                        ($current_start, $current_end) = ($current_end, $current_start);
                    }

                    # Check if the currently selected region matches the clicked segment
                    if (defined $current_start && defined $current_end &&
                        $current_start == $clicked_start &&
                        $current_end == $clicked_end) 
                    {
                        # It's a match, so unselect
                        $popup_state->{selection_start_offset} = undef;
                        $popup_state->{selection_end_offset}   = undef;
                    } else {
                        # Not a match, so select the new segment
                        $popup_state->{selection_start_offset} = $clicked_start;
                        $popup_state->{selection_end_offset}   = $clicked_end;
                    }
                    
                    _update_selection_visuals($popup_state);
                    
                    $found_and_handled = TRUE;
                    last; 
                }
            }
            last if $found_and_handled;
        }
        return TRUE;
    }
    # Handle right-click to set selection points manually
    elsif ($event->button == 3) {
        my $image_width = $popup_state->{pixbuf}->get_width;
        my $pixel_index = POSIX::floor($event->y * $image_width + $event->x);
        my $byte_offset = $pixel_index * 3;

        if (!defined $popup_state->{selection_start_offset}) {
            # First click: set start point
            $popup_state->{selection_start_offset} = $byte_offset;
            $popup_state->{selection_end_offset} = undef;
        } elsif (!defined $popup_state->{selection_end_offset}) {
            # Second click: set end point
            $popup_state->{selection_end_offset} = $byte_offset;
        } else {
            # Third click: reset and start a new selection
            $popup_state->{selection_start_offset} = $byte_offset;
            $popup_state->{selection_end_offset} = undef;
        }

        _update_selection_visuals($popup_state);
        return TRUE;
    }

    return FALSE; # Not a handled button press
}

sub _add_extra_selection_ui {
    my ($container, $popup_state) = @_;

    my $hbox = Gtk2::HBox->new(FALSE, 5);
    my $start_label = Gtk2::Label->new("Start:");
    my $start_entry = Gtk2::Entry->new();
    $start_entry->set_width_chars(10);

    my $stop_label = Gtk2::Label->new("Stop:");
    my $end_entry = Gtk2::Entry->new();
    $end_entry->set_width_chars(10);

    my $toggle = Gtk2::CheckButton->new();
    $toggle->set_active(TRUE);

    $hbox->pack_start($start_label, FALSE, FALSE, 5);
    $hbox->pack_start($start_entry, FALSE, FALSE, 0);
    $hbox->pack_start($stop_label, FALSE, FALSE, 5);
    $hbox->pack_start($end_entry, FALSE, FALSE, 0);
    $hbox->pack_start($toggle, FALSE, FALSE, 5);

    push @{$popup_state->{extra_selection_widgets}}, {
        start_entry => $start_entry,
        end_entry   => $end_entry,
        toggle      => $toggle,
    };
    
    my $update_callback = sub { _update_selection_visuals($popup_state); };
    $start_entry->signal_connect(activate => $update_callback);
    $end_entry->signal_connect(activate => $update_callback);
    $toggle->signal_connect(toggled => $update_callback);

    $container->pack_start($hbox, FALSE, FALSE, 0);
    $container->show_all;
}

sub _get_segment_info_for_offset {
    my ($popup_state, $target_offset) = @_;
    return "" unless defined $target_offset;

    for my $seg (@{$popup_state->{image_map_info}}) {
        my $start = $seg->{offset_in_image};
        my $end   = $start + $seg->{length_in_image};
        if ($target_offset >= $start && $target_offset < $end) {
            my $path = $seg->{path} || "anonymous";
            $path =~ s/\[heap\]/HEAP/i;
            
            if ($path =~ m|/|) {
                 $path =~ s/^.+?([^\/]+)$/$1/; 
            }
            
            $path =~ s/[^A-Za-z0-9\._-]+/_/g; 
            return $path;
        }
    }
    return "unknown-segment";
}

sub _generate_filename {
    my ($popup_state, $offsets_str, $path_info) = @_;
    
    my $pname = $popup_state->{pname};
    $pname =~ s/[^A-Za-z0-9\._-]+/_/g;

    my @parts = ($pname, 'pid', $popup_state->{pid}, 'memory');
    push @parts, $offsets_str if defined $offsets_str and $offsets_str ne '';
    push @parts, $path_info if defined $path_info and $path_info ne '';

    return join('_', @parts);
}


sub get_data_for_action {
    my ($popup_state) = @_;
    
    my $concatenated_data = "";
    my @filename_parts;
    my @range_map; # Stores [absolute_start, length, concatenated_start]
    my $first_offset;

    if ($popup_state->{selection_toggle_button}->get_active &&
        defined $popup_state->{selection_start_offset} &&
        defined $popup_state->{selection_end_offset})
    {
        my $start = $popup_state->{selection_start_offset};
        my $end = $popup_state->{selection_end_offset};
        ($start, $end) = ($end, $start) if $start > $end;
        my $length = $end - $start + 1; # +1 to be inclusive of the end byte

        if ($length > 0 && $start + $length <= length(${$popup_state->{raw_data_ref}})) {
            $first_offset = $start unless defined $first_offset;
            push @range_map, [$start, $length, length($concatenated_data)];
            $concatenated_data .= substr(${$popup_state->{raw_data_ref}}, $start, $length);
            push @filename_parts, sprintf("0x%X-0x%X", $start, $end);
        }
    }

    for my $widget_set (@{$popup_state->{extra_selection_widgets}}) {
        if ($widget_set->{toggle}->get_active) {
            my $start_text = $widget_set->{start_entry}->get_text;
            my $end_text   = $widget_set->{end_entry}->get_text;
            
            my ($start, $end);
            $start = hex($1) if $start_text =~ /^0x([0-9a-f]+)$/i;
            $end   = hex($1) if $end_text   =~ /^0x([0-9a-f]+)$/i;

            if (defined $start && defined $end) {
                ($start, $end) = ($end, $start) if $start > $end;
                my $length = $end - $start + 1; # +1 to be inclusive
                if ($length > 0 && $start + $length <= length(${$popup_state->{raw_data_ref}})) {
                    $first_offset = $start unless defined $first_offset;
                    push @range_map, [$start, $length, length($concatenated_data)];
                    $concatenated_data .= substr(${$popup_state->{raw_data_ref}}, $start, $length);
                    push @filename_parts, sprintf("0x%X-0x%X", $start, $end);
                }
            }
        }
    }
    
    my $path_info = "";
    if (defined $first_offset) {
        $path_info = _get_segment_info_for_offset($popup_state, $first_offset);
    }

    if (length $concatenated_data > 0) {
        return (\$concatenated_data, join('_', @filename_parts), \@range_map, $path_info);
    } else {
        return ($popup_state->{raw_data_ref}, undef, undef, undef);
    }
}

sub create_pixbuf_from_data {
    my ($raw_data_ref) = @_;
    my $total_bytes = length($$raw_data_ref);
    return undef if $total_bytes < 3;

    my $total_pixels = POSIX::floor($total_bytes / 3);
    my $width = POSIX::floor(POSIX::sqrt($total_pixels));
    return undef if $width == 0;
    my $height = POSIX::floor($total_pixels / $width);
    return undef if $height == 0;

    my $bytes_to_use = $width * $height * 3;
    my $image_data = substr($$raw_data_ref, 0, $bytes_to_use);
    my $rowstride = $width * 3;
    
    my $pixbuf = Gtk2::Gdk::Pixbuf->new_from_data($image_data, 'rgb', FALSE, 8, $width, $height, $rowstride);
    return $pixbuf;
}

sub _create_wav_header {
    my ($data_length) = @_;
    my $num_channels = 1;
    my $sample_rate = 8000;
    my $bits_per_sample = 8;

    my $byte_rate = $sample_rate * $num_channels * ($bits_per_sample / 8);
    my $block_align = $num_channels * ($bits_per_sample / 8);

    my $header;
    # RIFF chunk descriptor
    $header .= pack('A4', 'RIFF');
    $header .= pack('V', 36 + $data_length); # ChunkSize
    $header .= pack('A4', 'WAVE');
    # "fmt " sub-chunk
    $header .= pack('A4', 'fmt ');
    $header .= pack('V', 16); # Subchunk1Size for PCM
    $header .= pack('v', 1);  # AudioFormat (1=PCM)
    $header .= pack('v', $num_channels);
    $header .= pack('V', $sample_rate);
    $header .= pack('V', $byte_rate);
    $header .= pack('v', $block_align);
    $header .= pack('v', $bits_per_sample);
    # "data" sub-chunk
    $header .= pack('A4', 'data');
    $header .= pack('V', $data_length); # Subchunk2Size

    return $header;
}

sub on_sound_finished {
    my ($pid, $status, $data) = @_;
    my ($button, $popup_state) = @$data;

    return unless (defined $popup_state->{sound_player_pid} && $popup_state->{sound_player_pid} == $pid);

    $popup_state->{_is_changing_sound_button} = TRUE;
    $button->set_active(FALSE);
    $popup_state->{_is_changing_sound_button} = FALSE;
    
    Glib::Source->remove($popup_state->{sound_child_watch_id}) if defined $popup_state->{sound_child_watch_id};
    unlink $popup_state->{sound_temp_file} if defined $popup_state->{sound_temp_file};

    $popup_state->{sound_player_pid}     = undef;
    $popup_state->{sound_child_watch_id} = undef;
    $popup_state->{sound_temp_file}      = undef;
}

sub on_sound_toggle {
    my ($button, $popup_state) = @_;

    if ($popup_state->{_is_changing_sound_button}) {
        return;
    }

    if ($button->get_active) {
        # --- START PLAYING ---
        return if defined $popup_state->{sound_player_pid};
        
        my ($data_ref_to_play, undef, undef, undef) = get_data_for_action($popup_state);
        my $data_to_play = ${$data_ref_to_play};

        unless (length $data_to_play > 0) {
            warn "No data to play as sound.\n";
            $popup_state->{_is_changing_sound_button} = TRUE;
            $button->set_active(FALSE);
            $popup_state->{_is_changing_sound_button} = FALSE;
            return;
        }
        
        my ($fh, $filename) = tempfile(UNLINK => 0);
        binmode $fh;
        print $fh $data_to_play;
        close $fh;
        $popup_state->{sound_temp_file} = $filename;
        
        chmod 0644, $filename or warn "Could not chmod temp sound file: $!";
        
        my $user = qx(loginctl list-sessions --no-legend | grep "seat0" | awk '{print \$3}');
        chomp $user;
        my $uid = $user ? getpwnam($user) : undef;
        
        my $pid = fork();
        die "Cannot fork: $!" unless defined $pid;

        if ($pid == 0) {
            # CHILD PROCESS
            setpgrp(0, 0) or die "Can't set process group: $!";
            if ($user && defined $uid) {
                my $runtime_dir = "XDG_RUNTIME_DIR=/run/user/$uid";
                exec("sudo", "-u", $user, "env", $runtime_dir, "aplay", "-q", "-f", "U8", "-r", "8000", "-c", "1", $filename);
                die "Failed to exec sudo/aplay: $!";
            } else {
                warn "Could not find active graphical user, falling back to direct aplay call.\n";
                exec('aplay', '-q', '-f', 'U8', '-r', '8000', '-c', '1', $filename);
                die "Failed to exec aplay: $!";
            }
        } else {
            # PARENT PROCESS
            $popup_state->{sound_player_pid} = $pid;
            $popup_state->{sound_child_watch_id} = Glib::Child->watch_add(
                $pid, \&on_sound_finished, [$button, $popup_state]
            );
        }

    } else {
        # --- STOP PLAYING ---
        if (defined $popup_state->{sound_player_pid}) {
            kill 'TERM', -($popup_state->{sound_player_pid});
        }
    }
}

sub on_offset_entry_activate {
    my ($entry, $popup_state) = @_;
    
    my $start_text = $popup_state->{start_offset_entry}->get_text;
    my $end_text = $popup_state->{end_offset_entry}->get_text;

    $popup_state->{selection_start_offset} = ($start_text =~ /^0x([0-9a-f]+)$/i) ? hex($1) : undef;
    $popup_state->{selection_end_offset}   = ($end_text =~ /^0x([0-9a-f]+)$/i) ? hex($1) : undef;
    
    _update_selection_visuals($popup_state);
}

sub _update_selection_visuals {
    my ($popup_state) = @_;
    
    my $start = $popup_state->{selection_start_offset};
    my $end   = $popup_state->{selection_end_offset};
    $popup_state->{start_offset_entry}->set_text(defined $start ? sprintf("0x%X", $start) : '');
    $popup_state->{end_offset_entry}->set_text(defined $end ? sprintf("0x%X", $end) : '');

    my @all_rects;
    my $image_width = $popup_state->{pixbuf}->get_width;

    my $calculate_rects = sub {
        my ($s, $e) = @_;
        return () unless (defined $s && defined $e);
        ($s, $e) = ($e, $s) if $s > $e;
        
        my $start_pixel = POSIX::floor($s / 3);
        my $end_pixel   = POSIX::floor($e / 3);
        my $start_y = POSIX::floor($start_pixel / $image_width);
        my $start_x = $start_pixel % $image_width;
        my $end_y = POSIX::floor($end_pixel / $image_width);
        my $end_x = $end_pixel % $image_width;
        
        my @rects;
        if ($start_y == $end_y) {
            push @rects, [$start_x, $start_y, $end_x - $start_x + 1, 1];
        } else {
            push @rects, [$start_x, $start_y, $image_width - $start_x, 1];
            if ($end_y > $start_y + 1) {
                push @rects, [0, $start_y + 1, $image_width, $end_y - ($start_y + 1)];
            }
            push @rects, [0, $end_y, $end_x + 1, 1];
        }
        return @rects;
    };

    if ($popup_state->{selection_toggle_button}->get_active) {
        push @all_rects, $calculate_rects->($start, $end);
    }

    for my $widget_set (@{$popup_state->{extra_selection_widgets}}) {
        if ($widget_set->{toggle}->get_active) {
            my $s_text = $widget_set->{start_entry}->get_text;
            my $e_text = $widget_set->{end_entry}->get_text;
            my $s = ($s_text =~ /^0x([0-9a-f]+)$/i) ? hex($1) : undef;
            my $e = ($e_text =~ /^0x([0-9a-f]+)$/i) ? hex($1) : undef;
            push @all_rects, $calculate_rects->($s, $e);
        }
    }
    
    $popup_state->{selection_highlight_rects} = \@all_rects;
    $popup_state->{drawing_area}->queue_draw();
}

sub _draw_x_marker {
    my ($cr, $cx, $cy) = @_;
    my $size = 8;
    $cr->save;
    $cr->translate($cx, $cy);
    
    $cr->move_to(-$size, -$size); $cr->line_to($size, $size);
    $cr->move_to(-$size, $size);  $cr->line_to($size, -$size);

    $cr->set_line_width(5);
    $cr->set_source_rgb(1, 1, 1);
    $cr->stroke_preserve;

    $cr->set_line_width(2.5);
    $cr->set_source_rgb(0, 0, 0);
    $cr->stroke_preserve;

    $cr->set_line_width(1.5);
    $cr->set_source_rgba(1, 1, 0, 0.8); # Yellow, slightly transparent
    $cr->stroke;
    
    $cr->restore;
}


sub show_strings_view {
    my ($button, $popup_state) = @_;
    my $parent_window = $button->get_toplevel;

    my ($data_ref_for_action, undef, $range_map_ref, undef) = get_data_for_action($popup_state);

    unless (defined $data_ref_for_action && ${$data_ref_for_action}) {
        my $err_dialog = Gtk2::MessageDialog->new($parent_window, 'destroy-with-parent', 'error', 'close', "No memory data available to scan for strings.");
        $err_dialog->run;
        $err_dialog->destroy;
        return;
    }

    my ($fh, $filename) = tempfile(UNLINK => 1);
    binmode $fh;
    print $fh ${$data_ref_for_action};
    close $fh;

    my $strings_output = `strings -n 8 "$filename"`;

    my $strings_popup = Gtk2::Window->new('toplevel');
    $strings_popup->set_title("Strings for PID " . $popup_state->{pid});
    $strings_popup->set_transient_for($parent_window);
    $strings_popup->set_default_size(700, 500);
    $strings_popup->set_position('center-on-parent');
    $strings_popup->set_border_width(5);
    
    my $vbox = Gtk2::VBox->new(FALSE, 5);
    $strings_popup->add($vbox);

    my $search_hbox = Gtk2::HBox->new(FALSE, 5);
    my $search_label = Gtk2::Label->new("Find:");
    my $search_entry = Gtk2::Entry->new();
    my $search_button = Gtk2::Button->new_from_stock('gtk-find');
    $search_hbox->pack_start($search_label, FALSE, FALSE, 0);
    $search_hbox->pack_start($search_entry, TRUE, TRUE, 0);
    $search_hbox->pack_start($search_button, FALSE, FALSE, 0);
    $vbox->pack_start($search_hbox, FALSE, FALSE, 0);
    
    my $sw = Gtk2::ScrolledWindow->new(undef, undef);
    $sw->set_policy('automatic', 'automatic');
    $vbox->pack_start($sw, TRUE, TRUE, 0);

    my $text_view = Gtk2::TextView->new();
    $text_view->set_editable(FALSE);
    $text_view->set_cursor_visible(TRUE);
    my $buffer = $text_view->get_buffer();
    $buffer->set_text($strings_output || "No strings found.");
    $sw->add($text_view);
    
    my $search_state = {
        original_text       => $strings_output,
        last_search_term    => undef,
        byte_offsets        => [], # Will store absolute offsets
        current_match_index => -1,
    };
    
    my $callback_data = [$search_entry, $text_view, $search_state, $popup_state, $data_ref_for_action, $range_map_ref];
    $search_button->signal_connect(clicked => \&search_and_highlight, $callback_data);
    $search_entry->signal_connect(activate => \&search_and_highlight, $callback_data);
    
    $strings_popup->show_all;
}

sub _translate_concatenated_offset {
    my ($relative_offset, $range_map_ref) = @_;
    return $relative_offset unless (defined $range_map_ref && @$range_map_ref);

    for my $range (reverse @$range_map_ref) {
        my ($absolute_start, $length, $concatenated_start) = @$range;
        if ($relative_offset >= $concatenated_start) {
            my $offset_in_chunk = $relative_offset - $concatenated_start;
            return $absolute_start + $offset_in_chunk;
        }
    }
    return $relative_offset;
}

sub search_and_highlight {
    my ($widget, $data) = @_;
    my ($entry, $textview, $search_state, $image_popup_state, $data_ref_for_search, $range_map_ref) = @$data;

    my $buffer = $textview->get_buffer();
    my $tag_name = 'search_highlight';
    my $search_text = $entry->get_text();

    unless (length $search_text) {
        $buffer->set_text($search_state->{original_text} || "No strings found.");
        $search_state->{last_search_term} = undef;
        $search_state->{byte_offsets} = [];
        $search_state->{current_match_index} = -1;
        return;
    }
    
    if (!defined $search_state->{last_search_term} || $search_text ne $search_state->{last_search_term}) {
        $search_state->{byte_offsets} = [];
        $search_state->{current_match_index} = -1;
        $search_state->{last_search_term} = $search_text;
        
        my $concatenated_data = ${$data_ref_for_search};
        my $relative_offset = -1;
        my $results_text = "";
        
        while (($relative_offset = CORE::index($concatenated_data, $search_text, $relative_offset + 1)) != -1) {
            my $absolute_offset = _translate_concatenated_offset($relative_offset, $range_map_ref);
            push @{$search_state->{byte_offsets}}, $absolute_offset;
            
            my $context_start_in_full = $absolute_offset - 40;
            $context_start_in_full = 0 if $context_start_in_full < 0;
            
            my $context_length = length($search_text) + 80;
            my $context = substr(${$image_popup_state->{raw_data_ref}}, $context_start_in_full, $context_length);
            
            $context =~ s/[^\x20-\x7E]/./g;
            
            $results_text .= sprintf("0x%X: %s\n", $absolute_offset, $context);
        }

        $buffer->set_text($results_text || "String not found in memory.");
    }

    return unless @{$search_state->{byte_offsets}};

    $search_state->{current_match_index}++;
    if ($search_state->{current_match_index} >= @{$search_state->{byte_offsets}}) {
        $search_state->{current_match_index} = 0;
    }
    
    my $current_index = $search_state->{current_match_index};

    my ($start_iter, $end_iter) = $buffer->get_bounds();
    my $tag_table = $buffer->get_tag_table();
    my $tag = $tag_table->lookup($tag_name);
    unless ($tag) {
        $tag = Gtk2::TextTag->new($tag_name);
        $tag->set_property('background', 'yellow');
        $tag_table->add($tag);
    }
    $buffer->remove_tag_by_name($tag_name, $start_iter, $end_iter);
    my $line_start_iter = $buffer->get_iter_at_line($current_index);
    my $line_end_iter = $line_start_iter->copy;
    $line_end_iter->forward_to_line_end();
    $buffer->apply_tag_by_name($tag_name, $line_start_iter, $line_end_iter);
    
    $textview->scroll_to_iter($line_start_iter, 0.0, TRUE, 0.0, 0.5);
    $buffer->place_cursor($line_start_iter);
    
    my $byte_offset = $search_state->{byte_offsets}->[$current_index];
    if (defined $byte_offset && defined $image_popup_state->{pixbuf}) {
        my $image_width = $image_popup_state->{pixbuf}->get_width;
        my $pixel_index = POSIX::floor($byte_offset / 3);
        my $x = $pixel_index % $image_width;
        my $y = POSIX::floor($pixel_index / $image_width);
        
        $image_popup_state->{star_info} = { x => $x, y => $y };
        
        if (defined $image_popup_state->{star_timer_id}) {
            Glib::Source->remove($image_popup_state->{star_timer_id});
        }
        
        $image_popup_state->{star_timer_id} = Glib::Timeout->add(15000, sub {
            $image_popup_state->{star_info} = undef;
            $image_popup_state->{drawing_area}->queue_draw();
            $image_popup_state->{star_timer_id} = undef;
            return FALSE;
        });

        $image_popup_state->{drawing_area}->queue_draw();

        if (my $drawing_area = $image_popup_state->{drawing_area}) {
            if (my $viewport = $drawing_area->get_parent) {
                if (my $scrolled_window = $viewport->get_parent) {
                    if (my $vadjustment = $scrolled_window->get_vadjustment) {
                        my $visible_height = $vadjustment->get('page-size');
                        my $new_scroll_y = $y - ($visible_height / 2);
                        
                        my $max_scroll = $vadjustment->get('upper') - $visible_height;
                        $new_scroll_y = 0 if $new_scroll_y < 0;
                        $new_scroll_y = $max_scroll if $new_scroll_y > $max_scroll;
                        
                        $vadjustment->set_value($new_scroll_y);
                    }
                    if (my $hadjustment = $scrolled_window->get_hadjustment) {
                        my $visible_width = $hadjustment->get('page-size');
                        my $new_scroll_x = $x - ($visible_width / 2);

                        my $max_scroll = $hadjustment->get('upper') - $visible_width;
                        $new_scroll_x = 0 if $new_scroll_x < 0;
                        $new_scroll_x = $max_scroll if $new_scroll_x > $max_scroll;

                        $hadjustment->set_value($new_scroll_x);
                    }
                }
            }
        }
    }
}

sub update_image_widget {
    my ($popup_state) = @_;
    my ($new_pixbuf, $new_map_info, $new_raw_data_ref) = generate_image_from_pid($popup_state->{pid});

    if (defined $new_pixbuf) {
        if (defined $popup_state->{sound_player_pid}) {
             kill 'TERM', -($popup_state->{sound_player_pid});
        }

        $popup_state->{pixbuf} = $new_pixbuf;
        $popup_state->{image_map_info} = $new_map_info;
        $popup_state->{raw_data_ref} = $new_raw_data_ref;

        my $img_w = $new_pixbuf->get_width;
        my $img_h = $new_pixbuf->get_height;
        
        $popup_state->{drawing_area}->set_size_request($img_w, $img_h);
        $popup_state->{drawing_area}->queue_draw();

        return TRUE;
    } else {
        my $button = $popup_state->{update_toggle_button};
        if ($button) {
            $button->set_label("Process Ended");
            $button->set_sensitive(FALSE);
        }
        return FALSE;
    }
}


# --- Event Handlers for Zoom and Pan ---
sub on_scroll {
    my ($widget, $event) = @_;
    return FALSE if $state->{view_mode} ne 'global';

    my ($width, $height) = $widget->window->get_size;
    my $scale = ($height / $state->{global_total_size}) * $state->{zoom_level};
    my $cursor_y_bytes = $event->y / $scale;
    my $cursor_abs_bytes = $state->{view_offset_y} + $cursor_y_bytes;
    my $zoom_factor = ($event->direction eq 'up') ? 1.5 : 1 / 1.5;
    $state->{zoom_level} *= $zoom_factor;
    $state->{zoom_level} = 1.0 if $state->{zoom_level} < 1.0;
    my $new_scale = ($height / $state->{global_total_size}) * $state->{zoom_level};
    $state->{view_offset_y} = $cursor_abs_bytes - ($event->y / $new_scale);
    my $max_offset = $state->{global_total_size} - ($height / $new_scale);
    $state->{view_offset_y} = 0 if $state->{view_offset_y} < 0;
    $state->{view_offset_y} = $max_offset if $state->{view_offset_y} > $max_offset;
    $widget->queue_draw();
    return TRUE;
}

sub on_button_press {
    my ($widget, $event) = @_;
    return FALSE if $state->{view_mode} ne 'global';
    if ($event->button == 1) {
        $state->{drag_info} = { y => $event->y };
        return TRUE;
    }
    return FALSE;
}

sub on_motion {
    my ($widget, $event) = @_;
    if ($state->{view_mode} eq 'global' && exists $state->{drag_info}->{y}) {
        my ($width, $height) = $widget->window->get_size;
        my $scale = ($height / $state->{global_total_size}) * $state->{zoom_level};
        my $dy = $event->y - $state->{drag_info}->{y};
        $state->{view_offset_y} -= ($dy / $scale);
        $state->{drag_info}->{y} = $event->y;
        my $max_offset = $state->{global_total_size} - ($height / $scale);
        $state->{view_offset_y} = 0 if $state->{view_offset_y} < 0;
        $state->{view_offset_y} = $max_offset if $state->{view_offset_y} > $max_offset;
        $widget->queue_draw();
    }
}

# --- Timer Management ---
sub stop_update_timer {
    if (defined $state->{update_timer_id}) {
        Glib::Source->remove($state->{update_timer_id});
        $state->{update_timer_id} = undef;
    }
}

sub setup_proc_list_refresh_timer {
    my ($list_store, $selection) = @_;
    stop_update_timer();
    
    if ($state->{update_interval_sec} > 0) {
        my $interval_ms = $state->{update_interval_sec} * 1000;
        $state->{update_timer_id} = Glib::Timeout->add($interval_ms, sub {
            populate_process_list($list_store, $selection);
            return TRUE;
        });
    }
}

# --- Data Handling ---
sub populate_process_list {
    my ($list_store, $selection) = @_;
    my $previously_selected_pid = $state->{selected_pid};
    $list_store->clear();
    my %process_list_data;
    opendir(my $dh, '/proc') or die "Can't open /proc: $!";
    while (my $pid = readdir $dh) {
        next unless $pid =~ /^\d+$/;
        my $comm_file = "/proc/$pid/comm";
        if (open(my $fh, '<', $comm_file)) {
            my $comm = <$fh>;
            chomp $comm;
            close $fh;
            my $cmdline_str = "";
            my $cmdline_file = "/proc/$pid/cmdline";
            if (open(my $cmd_fh, '<', $cmdline_file)) {
                local $/;
                $cmdline_str = <$cmd_fh>;
                close $cmd_fh;
                $cmdline_str =~ s/\0/ /g;
                $cmdline_str =~ s/\s+$//;
            }
            my $display_text = "$comm (PID: $pid)";
            if ($cmdline_str) {
                $display_text .= " $cmdline_str";
            }
            $process_list_data{$pid} = { comm => $comm, display => $display_text, };
        }
    }
    closedir $dh;
    my @sorted_pids = sort { lc($process_list_data{$a}{comm}) cmp lc($process_list_data{$b}{comm}) } keys %process_list_data;
    my $iter_to_select;
    foreach my $pid (@sorted_pids) {
        my $iter = $list_store->append;
        $list_store->set($iter, 0 => $process_list_data{$pid}{display}, 1 => $pid);
        if (defined $previously_selected_pid && $pid == $previously_selected_pid) {
            $iter_to_select = $iter;
        }
    }
    $selection->select_iter($iter_to_select) if defined $iter_to_select;
}

sub build_global_map {
    $state->{global_map} = [];
    $state->{global_total_size} = 0;
    opendir(my $dh, '/proc') or die "Can't open /proc: $!";
    while (my $pid = readdir $dh) {
        next unless $pid =~ /^\d+$/;
        my $comm = "";
        if (open(my $fh, '<', "/proc/$pid/comm")) {
            $comm = <$fh>;
            chomp $comm;
            close $fh;
        }
        
        my $cmdline_str = "";
        my $cmdline_file = "/proc/$pid/cmdline";
        if (open(my $cmd_fh, '<', $cmdline_file)) {
            local $/;
            $cmdline_str = <$cmd_fh>;
            close $cmd_fh;
            $cmdline_str =~ s/\0/ /g;
            $cmdline_str =~ s/\s+$//;
        }

        my $maps_file = "/proc/$pid/maps";
        my $fh;
        next unless open($fh, '<', $maps_file);
        {
            no warnings 'portable';
            while (my $line = <$fh>) {
                chomp $line;
                if ($line =~ /^([0-9a-f]+)-([0-9a-f]+)\s+([rwxp-]+)\s+(.*)$/) {
                    my ($start, $end, $perms, $rest) = (hex($1), hex($2), $3, $4);
                    my ($path) = $rest =~ /\s*(.*)$/;
                    my $size = $end - $start;
                    next if $size == 0;
                    my $color;
                    if ($perms =~ /x/)    { $color = [0.2, 0.8, 0.2]; }
                    elsif ($perms =~ /w/) { $color = [0.9, 0.2, 0.2]; }
                    else                  { $color = [0.5, 0.5, 0.5]; }
                    if ($path =~ /\[stack\]/) { $color = [0.2, 0.2, 0.9]; }
                    
                    push @{$state->{global_map}}, { 
                        size    => $size, 
                        perms   => $perms, 
                        path    => $path, 
                        color   => $color, 
                        pid     => $pid, 
                        comm    => $comm, 
                        cmdline => $cmdline_str,
                    };
                    $state->{global_total_size} += $size;
                }
            }
        }
        close $fh;
    }
    closedir $dh;
}

sub update_memory_map {
    my ($widget) = @_;
    my $pid = $state->{selected_pid};
    return unless defined $pid;
    my $maps_file = "/proc/$pid/maps";
    my $fh;
    unless (open($fh, '<', $maps_file)) {
        set_status_message($widget, "Process PID $pid may have ended.");
        return;
    }
    $state->{memory_map} = [];
    $state->{vma_total_size} = 0;
    {
        no warnings 'portable';
        while (my $line = <$fh>) {
            if ($line =~ /^([0-9a-f]+)-([0-9a-f]+)\s+([rwxp-]+)\s+([0-9a-f]+)\s+[\d:]+\s+\d+\s*(.*)$/) {
                my ($start, $end, $perms, $offset, $path) = (hex($1), hex($2), $3, hex($4), $5);
                my $size = $end - $start;
                my $color;
                if ($perms =~ /x/)    { $color = [0.2, 0.8, 0.2]; }
                elsif ($perms =~ /w/) { $color = [0.9, 0.2, 0.2]; }
                else                  { $color = [0.5, 0.5, 0.5]; }
                if ($path =~ /\[stack\]/) { $color = [0.2, 0.2, 0.9]; }
                push @{$state->{memory_map}}, { start => $start, end => $end, size => $size, perms => $perms, path => $path, color => $color, };
                $state->{vma_total_size} += $size;
            }
        }
    }
    close $fh;
    $widget->queue_draw();
    $state->{key_drawing_area}->queue_draw() if $state->{key_drawing_area};
}

sub build_detail_map {
    my ($widget) = @_;
    my $pid = $state->{selected_pid};
    return unless defined $pid;
    $state->{detail_cache} = {};
    my $mem_file = "/proc/$pid/mem";
    my $fh;
    unless (open($fh, '<', $mem_file)) {
        warn "Could not open $mem_file: $!. Details will not be shown.";
        set_status_message($widget, "Error: Could not read process memory.\nPermission denied?");
        return;
    }
    binmode $fh;
    for my $segment (@{$state->{memory_map}}) {
        next unless $segment->{perms} =~ /r/;

        my ($h, $s, $v) = rgb_to_hsv(@{$segment->{color}});
        my $min_s = 0.2;
        my $min_v = 0.3;

        my $buffer;
        my $seek_result = sysseek($fh, $segment->{start}, 0);
        unless (defined $seek_result) {
            next;
        }
        my $bytes_read = sysread($fh, $buffer, $segment->{size});

        if (defined $bytes_read and $bytes_read > 0) {
            my @micro_blocks;
            my $kb_size = 1024;
            my $max_checksum = $kb_size * 255;
            for (my $offset = 0; $offset < $bytes_read; $offset += $kb_size) {
                my $chunk = substr($buffer, $offset, $kb_size);
                my $checksum = 0;
                $checksum += $_ for unpack("C*", $chunk);
                my $complexity = $max_checksum > 0 ? $checksum / $max_checksum : 0;
                my $new_s = $min_s + ($complexity * ($s - $min_s));
                my $new_v = $min_v + ($complexity * ($v - $min_v));
                my $new_rgb_ref = hsv_to_rgb($h, $new_s, $new_v);
                push @micro_blocks, $new_rgb_ref;
            }
            $state->{detail_cache}->{$segment->{start}} = \@micro_blocks;
        }
    }
    close $fh;
}

sub generate_image_from_pid {
    my ($pid) = @_;
    my $all_ram_data = "";
    my @image_map_info;
    
    my $maps_file = "/proc/$pid/maps";
    my $fh_maps;
    return (undef, undef, undef) unless open($fh_maps, '<', $maps_file);
    my @segments_to_read;
    {
        no warnings 'portable';
        while (my $line = <$fh_maps>) {
            if ($line =~ /^([0-9a-f]+)-([0-9a-f]+)\s+(r[wxp-]+)\s+([0-9a-f]+)\s+[\d:]+\s+\d+\s*(.*)$/) {
                my ($start, $end, $perms, $path) = (hex($1), hex($2), $3, $5);
                my $size = $end - $start;
                next if $size == 0;
                my $color;
                if ($perms =~ /x/)    { $color = [0.2, 0.8, 0.2]; }
                elsif ($perms =~ /w/) { $color = [0.9, 0.2, 0.2]; }
                else                  { $color = [0.5, 0.5, 0.5]; }
                if ($path =~ /\[stack\]/) { $color = [0.2, 0.2, 0.9]; }
                push @segments_to_read, { start => $start, end => $end, size => $size, perms => $perms, path => $path, color => $color };
            }
        }
    }
    close $fh_maps;
    
    my $mem_file = "/proc/$pid/mem";
    my $fh_mem;
    unless(open($fh_mem, '<', $mem_file)) {
        return (undef, undef, undef);
    };
    binmode $fh_mem;
    
    my $current_offset_in_image = 0;
    for my $seg (@segments_to_read) {
        my $buffer;
        my $seek_result = sysseek($fh_mem, $seg->{start}, 0);
        unless(defined $seek_result) {
            next;
        }
        my $bytes_read = sysread($fh_mem, $buffer, $seg->{size});
        
        if (defined $bytes_read and $bytes_read > 0) {
            $all_ram_data .= $buffer;
            my $seg_info_for_map = { %$seg };
            $seg_info_for_map->{offset_in_image} = $current_offset_in_image;
            $seg_info_for_map->{length_in_image} = $bytes_read;
            push @image_map_info, $seg_info_for_map;
            $current_offset_in_image += $bytes_read;
        }
    }
    close $fh_mem;

    return (undef, undef, undef) if length($all_ram_data) == 0;
    my $pixbuf = create_pixbuf_from_data(\$all_ram_data);
    
    return ($pixbuf, \@image_map_info, \$all_ram_data);
}


# --- Drawing and UI Callbacks ---
sub on_expose {
    my ($widget, $event) = @_;
    if ($state->{view_mode} eq 'single') {
        draw_single_map($widget, $event);
    }
    elsif ($state->{view_mode} eq 'global') {
        draw_global_map($widget, $event);
    }
}

sub draw_single_map {
    my ($widget, $event) = @_;
    my $cr = Gtk2::Gdk::Cairo::Context->create($widget->window);
    my ($width, $height) = $widget->window->get_size;
    $cr->set_source_rgb(0.1, 0.1, 0.1);
    $cr->paint;
    unless (defined $state->{selected_pid} && @{$state->{memory_map}}) {
        draw_centered_text($cr, $width, $height, "Select a process from the list.");
        return;
    }
    $state->{map_areas} = [];
    my $current_y = 0;
    return if $state->{vma_total_size} == 0;
    my $pixels_per_byte = $height / $state->{vma_total_size};
    for my $segment (@{$state->{memory_map}}) {
        my $segment_height = $segment->{size} * $pixels_per_byte;
        $segment_height = 1 if $segment_height < 1 && $segment_height > 0;
        $cr->set_source_rgb(@{$segment->{color}});
        $cr->rectangle(0, $current_y, $width, $segment_height);
        $cr->fill;
        if ($state->{detail_mode} && exists $state->{detail_cache}->{$segment->{start}}) {
            my $micro_blocks = $state->{detail_cache}->{$segment->{start}};
            my $num_blocks = @$micro_blocks;
            next if $num_blocks == 0;
            my $micro_block_height = $segment_height / $num_blocks;
            $micro_block_height = 1 if $micro_block_height < 1 && $micro_block_height > 0;
            my $micro_y = $current_y;
            for my $color (@$micro_blocks) {
                $cr->set_source_rgb(@$color);
                $cr->rectangle(0, $micro_y, $width, $micro_block_height);
                $cr->fill;
                $micro_y += $micro_block_height;
            }
        }
        push @{$state->{map_areas}}, { y => $current_y, h => $segment_height, data => $segment };
        $current_y += $segment_height;
    }
}

sub draw_global_map {
    my ($widget, $event) = @_;
    my $cr = Gtk2::Gdk::Cairo::Context->create($widget->window);
    my ($width, $height) = $widget->window->get_size;
    $cr->set_source_rgb(0.1, 0.1, 0.1);
    $cr->paint;
    unless (@{$state->{global_map}}) {
        draw_centered_text($cr, $width, $height, "Click 'Show All Processes' to build map.");
        return;
    }
    $state->{map_areas} = [];
    my $current_y_bytes = 0;
    my $scale = ($height / $state->{global_total_size}) * $state->{zoom_level};
    for my $segment (@{$state->{global_map}}) {
        my $segment_start_bytes = $current_y_bytes;
        my $segment_end_bytes = $current_y_bytes + $segment->{size};
        if ($segment_end_bytes < $state->{view_offset_y}) {
            $current_y_bytes = $segment_end_bytes;
            next;
        }
        my $view_end_bytes = $state->{view_offset_y} + ($height / $scale);
        if ($segment_start_bytes > $view_end_bytes) {
            last;
        }
        my $y_on_screen = ($segment_start_bytes - $state->{view_offset_y}) * $scale;
        my $height_on_screen = $segment->{size} * $scale;
        $height_on_screen = 1 if $height_on_screen < 1 && $height_on_screen > 0;
        $cr->set_source_rgb(@{$segment->{color}});
        $cr->rectangle(0, $y_on_screen, $width, $height_on_screen);
        $cr->fill;
        push @{$state->{map_areas}}, { y => $y_on_screen, h => $height_on_screen, data => $segment };
        $current_y_bytes = $segment_end_bytes;
    }
}

sub draw_image_with_overlays {
    my ($widget, $event, $popup_state) = @_;
    my $cr = Gtk2::Gdk::Cairo::Context->create($widget->window);

    unless ($popup_state->{pixbuf}) {
        $cr->set_source_rgb(0.1, 0.1, 0.1);
        $cr->paint;
        return TRUE;
    }

    $cr->set_source_pixbuf($popup_state->{pixbuf}, 0, 0);
    $cr->paint;

    my $image_width = $popup_state->{pixbuf}->get_width;
    $popup_state->{overlay_map_areas} = [];
    for my $seg (@{$popup_state->{image_map_info}}) {
        my $start_byte = $seg->{offset_in_image};
        my $end_byte = $start_byte + $seg->{length_in_image};
        my $start_pixel = POSIX::floor($start_byte / 3);
        my $end_pixel   = POSIX::floor($end_byte / 3);
        my $start_y = POSIX::floor($start_pixel / $image_width);
        my $start_x = $start_pixel % $image_width;
        my $end_y = POSIX::floor($end_pixel / $image_width);
        my $end_x = $end_pixel % $image_width;

        my @rects_for_tooltip;
        if ($start_y == $end_y) {
            push @rects_for_tooltip, [$start_x, $start_y, $end_x - $start_x + 1, 1];
        } else {
            push @rects_for_tooltip, [$start_x, $start_y, $image_width - $start_x, 1];
            if ($end_y > $start_y + 1) {
                my $full_rows_height = $end_y - ($start_y + 1);
                push @rects_for_tooltip, [0, $start_y + 1, $image_width, $full_rows_height];
            }
            push @rects_for_tooltip, [0, $end_y, $end_x + 1, 1];
        }
        push @{$popup_state->{overlay_map_areas}}, { rects => \@rects_for_tooltip, data => $seg };
    }
    
    if ($popup_state->{overlay_toggle_button} && $popup_state->{overlay_toggle_button}->get_active) {
        for my $i (0 .. $#{$popup_state->{image_map_info}}) {
            my $seg = $popup_state->{image_map_info}->[$i];
            my ($r, $g, $b) = @{$seg->{color}};
            $cr->set_source_rgba($r, $g, $b, 0.15);
            for my $rect (@{$popup_state->{overlay_map_areas}->[$i]->{rects}}) {
                $cr->rectangle(@$rect);
                $cr->fill;
            }
        }
    }
    
    if (defined $popup_state->{diff_points} && $popup_state->{diff_toggle_button} && $popup_state->{diff_toggle_button}->get_active) {
        $cr->set_source_rgba(1, 0, 1, 0.6);
        for my $offset (@{$popup_state->{diff_points}}) {
            my $pixel_index = POSIX::floor($offset / 3);
            my $x = $pixel_index % $image_width;
            my $y = POSIX::floor($pixel_index / $image_width);
            $cr->rectangle($x - 1, $y - 1, 3, 3);
            $cr->fill;
        }
    }

    if (@{$popup_state->{selection_highlight_rects}}) {
        $cr->set_source_rgba(0, 1, 1, 0.3); # Transparent Cyan
        for my $rect (@{$popup_state->{selection_highlight_rects}}) {
            $cr->rectangle(@$rect);
            $cr->fill;
        }
    }
    
    if (defined $popup_state->{selection_start_offset}) {
        my $pixel_index = POSIX::floor($popup_state->{selection_start_offset} / 3);
        my $x = $pixel_index % $image_width;
        my $y = POSIX::floor($pixel_index / $image_width);
        _draw_x_marker($cr, $x, $y);
    }
    if (defined $popup_state->{selection_end_offset}) {
        my $pixel_index = POSIX::floor($popup_state->{selection_end_offset} / 3);
        my $x = $pixel_index % $image_width;
        my $y = POSIX::floor($pixel_index / $image_width);
        _draw_x_marker($cr, $x, $y);
    }

    if (defined $popup_state->{star_info}) {
        _draw_star($cr, $popup_state->{star_info}->{x}, $popup_state->{star_info}->{y});
    }

    return TRUE;
}

sub on_query_tooltip {
    my ($widget, $x, $y, $keyboard_mode, $tooltip) = @_;
    for my $area (@{$state->{map_areas}}) {
        if ($y >= $area->{y} && $y <= ($area->{y} + $area->{h})) {
            my $seg = $area->{data};
            my $path = $seg->{path} || "[anonymous]";
            $path = "[heap]" if $path =~ /heap/;
            my $tooltip_text;
            if ($state->{view_mode} eq 'global') {
                $tooltip_text = sprintf("<b>Process:</b> %s (PID: %d)\n" . "<b>Path:</b> %s\n" . "<b>Size:</b> %s | <b>Perms:</b> %s", $seg->{comm}, $seg->{pid}, $path, format_bytes($seg->{size}), $seg->{perms});
            } else {
                $tooltip_text = sprintf("<b>Path:</b> %s\n" . "<b>Size:</b> %s | <b>Perms:</b> %s\n" . "<b>Range:</b> 0x%x - 0x%x", $path, format_bytes($seg->{size}), $seg->{perms}, $seg->{start}, $seg->{end});
            }
            $tooltip->set_markup($tooltip_text);
            return TRUE;
        }
    }
    return FALSE;
}

sub on_query_image_tooltip {
    my ($widget, $x, $y, $keyboard_mode, $tooltip, $popup_state) = @_;
    for my $area (@{$popup_state->{overlay_map_areas}}) {
        for my $rect (@{$area->{rects}}) {
            my ($rx, $ry, $rw, $rh) = @$rect;
            if ($x >= $rx && $x <= ($rx + $rw) && $y >= $ry && $y <= ($ry + $rh)) {
                my $seg = $area->{data};
                my $path = $seg->{path} || "[anonymous]";
                $path = "[heap]" if $path =~ /heap/;
                my $tooltip_text = sprintf("<b>Path:</b> %s\n" . "<b>Size:</b> %s | <b>Perms:</b> %s\n" . "<b>Range:</b> 0x%x - 0x%x", $path, format_bytes($seg->{size}), $seg->{perms}, $seg->{start}, $seg->{end});
                $tooltip->set_markup($tooltip_text);
                return TRUE;
            }
        }
    }
    return FALSE;
}

sub set_status_message {
    my ($widget, $message) = @_;
    stop_update_timer();
    $state->{selected_pid} = undef;
    $state->{view_mode} = 'single';
    my ($cr, $w, $h) = (Gtk2::Gdk::Cairo::Context->create($widget->window), $widget->window->get_size);
    $cr->set_source_rgb(0.1, 0.1, 0.1);
    $cr->paint;
    draw_centered_text($cr, $w, $h, $message);
}

sub draw_centered_text {
    my ($cr, $width, $height, $text) = @_;
    $cr->set_source_rgb(0.9, 0.9, 0.9);
    $cr->select_font_face('Sans', 'normal', 'normal');
    $cr->set_font_size(14);
    
    my $extents = $cr->text_extents($text);

    $cr->save;
    $cr->translate($width / 2, $height / 2);
    $cr->rotate(-3.14159265 / 2.0);
    $cr->move_to(-$extents->{width} / 2, $extents->{height} / 2);
    $cr->show_text($text);
    $cr->restore;
}

sub on_draw_key {
    my ($widget, $event) = @_;
    my $cr = Gtk2::Gdk::Cairo::Context->create($widget->window);
    my ($width, $height) = $widget->window->get_size;

    # Left Side: The Color Key
    if ($state->{detail_mode}) {
        my @key_items = (
            { label => 'Code',  color => [0.2, 0.8, 0.2] },
            { label => 'Data',  color => [0.9, 0.2, 0.2] },
            { label => 'Stack', color => [0.2, 0.2, 0.9] },
            { label => 'R/O',   color => [0.5, 0.5, 0.5] },
        );
        my $padding = 5;
        my $gradient_height = $height - 45;
        my $gradient_width = (($width/2) - ($padding * (scalar(@key_items) + 1))) / scalar(@key_items);
        my $x_pos = $padding;

        for my $item (@key_items) {
            my ($h, $s, $v) = rgb_to_hsv(@{$item->{color}});
            my $min_s = 0.2; my $min_v = 0.3;
            
            for my $y (0 .. $gradient_height) {
                my $complexity = $y / $gradient_height;
                my $new_s = $min_s + ($complexity * ($s - $min_s));
                my $new_v = $min_v + ($complexity * ($v - $min_v));
                my $rgb_ref = hsv_to_rgb($h, $new_s, $new_v);
                $cr->set_source_rgb(@$rgb_ref);
                $cr->rectangle($x_pos, $y + $padding, $gradient_width, 1);
                $cr->fill;
            }
            $cr->set_source_rgb(0, 0, 0);
            $cr->select_font_face('Sans', 'normal', 'bold');
            $cr->set_font_size(10);
            $cr->move_to($x_pos + 2, $height - $padding - 15);
            $cr->show_text($item->{label});
            $x_pos += $gradient_width + $padding;
        }
        
        $cr->set_source_rgb(0, 0, 0);
        $cr->select_font_face('Sans', 'normal', 'bold');
        $cr->set_font_size(12);

        my $center_x_of_gradients = $padding + (($width/2) - $padding*2) / 2;

        my $text = "Low Complexity";
        my $extents = $cr->text_extents($text);
        $cr->move_to($center_x_of_gradients - $extents->{width}/2, $padding + 12);
        $cr->show_text($text);

        $text = "High Complexity";
        $extents = $cr->text_extents($text);
        $cr->move_to($center_x_of_gradients - $extents->{width}/2, $height - 5);
        $cr->show_text($text);

    } else {
        my @key_items = (
            { label => 'Executable Code',    color => [0.2, 0.8, 0.2] },
            { label => 'Writable Data/Heap', color => [0.9, 0.2, 0.2] },
            { label => 'Stack',              color => [0.2, 0.2, 0.9] },
            { label => 'Read-only Data',     color => [0.5, 0.5, 0.5] },
        );
        my $padding = 5;
        my $box_size = 15;
        my $line_height = 22;
        my $y_pos = $padding;
        $cr->select_font_face('Sans', 'normal', 'normal');
        $cr->set_font_size(11);

        for my $item (@key_items) {
            $cr->set_source_rgb(@{$item->{color}});
            $cr->rectangle($padding, $y_pos, $box_size, $box_size);
            $cr->fill;
            $cr->set_source_rgb(0, 0, 0);
            $cr->move_to($padding + $box_size + 5, $y_pos + $box_size - 2);
            $cr->show_text($item->{label});
            $y_pos += $line_height;
        }
    }
    
    # Right Side: Process Statistics
    if (defined $state->{selected_pid} && $state->{vma_total_size} > 0) {
        my $pid = $state->{selected_pid};
        my $pname = "unknown";
        if (open(my $fh, '<', "/proc/$pid/comm")) {
            $pname = <$fh>;
            chomp $pname;
            close $fh;
        }
    
        my ($exe_size, $wri_size, $stk_size, $ro_size) = (0, 0, 0, 0);
        for my $segment (@{$state->{memory_map}}) {
            if ($segment->{path} =~ /\[stack\]/) { $stk_size += $segment->{size}; }
            elsif ($segment->{perms} =~ /x/)    { $exe_size += $segment->{size}; }
            elsif ($segment->{perms} =~ /w/)    { $wri_size += $segment->{size}; }
            else                                { $ro_size  += $segment->{size}; }
        }

        my $exe_pct = $state->{vma_total_size} > 0 ? ($exe_size / $state->{vma_total_size}) * 100 : 0;
        my $wri_pct = $state->{vma_total_size} > 0 ? ($wri_size / $state->{vma_total_size}) * 100 : 0;
        my $stk_pct = $state->{vma_total_size} > 0 ? ($stk_size / $state->{vma_total_size}) * 100 : 0;
        my $ro_pct  = $state->{vma_total_size} > 0 ? ($ro_size  / $state->{vma_total_size}) * 100 : 0;
        
        my $stats_x = $width / 2;
        my $y_pos = 18;
        my $line_height = 16;
        
        $cr->set_source_rgb(0, 0, 0);
        $cr->select_font_face('Sans', 'normal', 'bold');
        $cr->set_font_size(11);
        
        my $title = "Stats for: $pname ($pid)";
        $cr->move_to($stats_x, $y_pos);
        $cr->show_text($title);
        $y_pos += ($line_height * 1.5);
        
        $cr->move_to($stats_x, $y_pos);
        $cr->show_text("Total VMA:");
        $cr->select_font_face('Sans', 'normal', 'normal');
        $cr->move_to($stats_x + 75, $y_pos);
        $cr->show_text(format_bytes($state->{vma_total_size}));
        
        $y_pos += ($line_height * 1.5);

        $cr->move_to($stats_x, $y_pos);
        $cr->show_text(sprintf("Code:  %5.1f%%", $exe_pct));
        $y_pos += $line_height;
        $cr->move_to($stats_x, $y_pos);
        $cr->show_text(sprintf("Data:  %5.1f%%", $wri_pct));
        $y_pos += $line_height;
        $cr->move_to($stats_x, $y_pos);
        $cr->show_text(sprintf("Stack: %5.1f%%", $stk_pct));
        $y_pos += $line_height;
        $cr->move_to($stats_x, $y_pos);
        $cr->show_text(sprintf("R/O:   %5.1f%%", $ro_pct));
    }
    
    return TRUE;
}

# --- Hilbert Curve Generation and Drawing ---
sub xy_to_hilbert_d {
    my ($n, $x, $y) = @_;
    my $d = 0;
    my $s = $n >> 1;
    while ($s > 0) {
        my $rx = ($x & $s) > 0 ? 1 : 0;
        my $ry = ($y & $s) > 0 ? 1 : 0;
        $d += $s * $s * ((3 * $rx) ^ $ry);
        ($x, $y) = _hilbert_rot($s, $x, $y, $rx, $ry);
        $s >>= 1;
    }
    return $d;
}

sub generate_hilbert_path {
    my ($order) = @_;
    my @path;
    my $N = 2**$order;

    for my $i (0 .. ($N * $N - 1)) {
        push @path, [ _d_to_hilbert_xy($N, $i) ];
    }
    return \@path;
}

sub _d_to_hilbert_xy {
    my ($n, $d) = @_;
    my ($x, $y) = (0, 0);
    my $s = 1;
    while ($s < $n) {
        my $rx = 1 & ($d >> 1);
        my $ry = 1 & ($d ^ $rx);
        ($x, $y) = _hilbert_rot($s, $x, $y, $rx, $ry);
        $x += $s * $rx;
        $y += $s * $ry;
        $d >>= 2;
        $s <<= 1;
    }
    return ($x, $y);
}

sub _hilbert_rot {
    my ($n, $x, $y, $rx, $ry) = @_;
    if ($ry == 0) {
        if ($rx == 1) {
            $x = $n - 1 - $x;
            $y = $n - 1 - $y;
        }
        return ($y, $x);
    }
    return ($x, $y);
}

sub _render_hilbert_curve_and_data {
    my ($cr, $h_state) = @_;
    
    $cr->set_source_rgb(0.1, 0.1, 0.1);
    $cr->paint;

    return TRUE unless @{$state->{global_map}} && $state->{global_total_size} > 0;
    
    my $points = $h_state->{path};
    my $scale = $h_state->{scale};
    my $padding = $h_state->{padding};
    
    my $total_path_length = @$points - 1;
    my $bytes_per_path_unit = $state->{global_total_size} / $total_path_length;
    my $current_byte_pos = 0;

    $cr->set_line_width($scale > 1 ? $scale - 0.5 : 1);
    $cr->set_line_cap('round');
    
    $h_state->{map_areas} = [];
    $h_state->{pid_to_segments_cache} = {};
    $h_state->{path_to_segment_map} = [];

    for my $segment (@{$state->{global_map}}) {
        $cr->set_source_rgb(@{$segment->{color}});
        
        my $segment_end_byte_pos = $current_byte_pos + $segment->{size};
        my $start_path_index = POSIX::floor($current_byte_pos / $bytes_per_path_unit);
        my $end_path_index = POSIX::floor($segment_end_byte_pos / $bytes_per_path_unit);
        
        my $area = {
            start_index => $start_path_index,
            end_index   => $end_path_index,
            data        => $segment,
        };
        push @{$h_state->{map_areas}}, $area;

        my $pid = $segment->{pid};
        push @{$h_state->{pid_to_segments_cache}->{$pid}}, $area;

        for (my $i = $start_path_index; $i < $end_path_index; $i++) {
            last if $i + 1 >= @$points;
            
            $h_state->{path_to_segment_map}->[$i] = $area;

            my ($x1, $y1) = @{ $points->[$i] };
            my ($x2, $y2) = @{ $points->[$i + 1] };
            
            $cr->move_to($x1 * $scale + $padding, $y1 * $scale + $padding);
            $cr->line_to($x2 * $scale + $padding, $y2 * $scale + $padding);
            $cr->stroke;
        }
        
        $current_byte_pos = $segment_end_byte_pos;
    }
}

sub on_expose_hilbert {
    my ($widget, $event, $h_state) = @_;
    
    unless (defined $h_state->{backing_pixmap}) {
        my ($width, $height) = $widget->window->get_size;
        $h_state->{backing_pixmap} = Gtk2::Gdk::Pixmap->new($widget->window, $width, $height, -1);
        my $cr = Gtk2::Gdk::Cairo::Context->create($h_state->{backing_pixmap});
        _render_hilbert_curve_and_data($cr, $h_state);
    }

    my $cr = Gtk2::Gdk::Cairo::Context->create($widget->window);
    $cr->set_source_pixmap($h_state->{backing_pixmap}, 0, 0);
    $cr->paint;

    if (@{$h_state->{highlighted_pid_segments}}) {
        my $points = $h_state->{path};
        my $scale = $h_state->{scale};
        my $padding = $h_state->{padding};
        
        $cr->set_source_rgba(1, 1, 1, 0.5);
        $cr->set_line_width($scale + 0.5);
        $cr->set_line_cap('round');
        
        for my $segment (@{$h_state->{highlighted_pid_segments}}) {
            for (my $i = $segment->{start_index}; $i < $segment->{end_index}; $i++) {
                last if $i + 1 >= @$points;
                my ($x1, $y1) = @{ $points->[$i] };
                my ($x2, $y2) = @{ $points->[$i + 1] };
                $cr->move_to($x1 * $scale + $padding, $y1 * $scale + $padding);
                $cr->line_to($x2 * $scale + $padding, $y2 * $scale + $padding);
                $cr->stroke;
            }
        }
    }
    
    if (defined $h_state->{highlighted_segment}) {
        my $segment = $h_state->{highlighted_segment};
        my $points = $h_state->{path};
        my $scale = $h_state->{scale};
        my $padding = $h_state->{padding};

        $cr->set_source_rgb(1, 1, 1);
        $cr->set_line_width($scale + 1);
        $cr->set_line_cap('round');

        for (my $i = $segment->{start_index}; $i < $segment->{end_index}; $i++) {
            last if $i + 1 >= @$points;
            my ($x1, $y1) = @{ $points->[$i] };
            my ($x2, $y2) = @{ $points->[$i + 1] };
            $cr->move_to($x1 * $scale + $padding, $y1 * $scale + $padding);
            $cr->line_to($x2 * $scale + $padding, $y2 * $scale + $padding);
            $cr->stroke;
        }
    }
    
    return TRUE;
}

sub on_hilbert_enter {
    my ($widget, $event, $h_state) = @_;
    $h_state->{mouse_is_over} = TRUE;
    return TRUE;
}

sub on_hilbert_leave {
    my ($widget, $event, $h_state) = @_;
    $h_state->{mouse_is_over} = FALSE;
    
    # Clear the mouse-based highlight
    $h_state->{highlighted_segment} = undef;

    # Resync with the main window's selection
    my $pid = $state->{selected_pid};
    if (defined $pid) {
        my $pid_segments = $h_state->{pid_to_segments_cache}->{$pid} || [];
        $h_state->{highlighted_pid_segments} = $pid_segments;
    } else {
        $h_state->{highlighted_pid_segments} = [];
    }
    
    $widget->queue_draw();
    return TRUE;
}

sub on_motion_hilbert {
    my ($widget, $event, $h_state) = @_;
    
    my $grid_size = 2**$h_state->{order};
    my $scale = $h_state->{scale};
    my $padding = $h_state->{padding};

    my $grid_x = POSIX::floor(($event->x - $padding) / $scale);
    my $grid_y = POSIX::floor(($event->y - $padding) / $scale);
    
    my $current_area = undef;
    my @pid_segments = ();

    if ($grid_x >= 0 and $grid_x < $grid_size and $grid_y >= 0 and $grid_y < $grid_size) {
        my $path_index = xy_to_hilbert_d($grid_size, $grid_x, $grid_y);
        
        $current_area = $h_state->{path_to_segment_map}->[$path_index];
        
        if (defined $current_area) {
            my $pid_to_find = $current_area->{data}->{pid};
            @pid_segments = @{$h_state->{pid_to_segments_cache}->{$pid_to_find}};
        }
    }
    
    my $old_id = defined $h_state->{highlighted_segment} ? $h_state->{highlighted_segment}->{start_index} : undef;
    my $new_id = defined $current_area ? $current_area->{start_index} : undef;

    if (($old_id // -1) != ($new_id // -1)) {
        $h_state->{highlighted_segment} = $current_area;
        $h_state->{highlighted_pid_segments} = \@pid_segments;
        $widget->queue_draw();
    }
    
    return TRUE;
}

sub on_hilbert_button_press {
    my ($widget, $event, $data) = @_;
    my ($h_state, $parent_window, $proc_list_store, $selection) = @$data;

    return FALSE if ($event->button != 3);

    my $grid_size = 2**$h_state->{order};
    my $scale = $h_state->{scale};
    my $padding = $h_state->{padding};

    my $grid_x = POSIX::floor(($event->x - $padding) / $scale);
    my $grid_y = POSIX::floor(($event->y - $padding) / $scale);
    
    return FALSE if ($grid_x < 0 or $grid_x >= $grid_size or $grid_y < 0 or $grid_y >= $grid_size);

    my $path_index = xy_to_hilbert_d($grid_size, $grid_x, $grid_y);

    my $area = $h_state->{path_to_segment_map}->[$path_index];

    if (defined $area) {
        my $pid = $area->{data}->{pid};
        
        $state->{selected_pid} = $pid;
        
        my $iter = $proc_list_store->get_iter_first;
        while (defined $iter) {
            my $current_pid_in_list = $proc_list_store->get($iter, 1);
            if ($current_pid_in_list == $pid) {
                $selection->select_iter($iter);
                last;
            }
            $iter = $proc_list_store->iter_next($iter);
        }

        show_image_view($parent_window);

        return TRUE;
    }

    return FALSE;
}

sub on_query_hilbert_tooltip {
    my ($widget, $x, $y, $keyboard_mode, $tooltip, $h_state) = @_;
    
    my $grid_size = 2**$h_state->{order};
    my $scale = $h_state->{scale};
    my $padding = $h_state->{padding};

    my $grid_x = POSIX::floor(($x - $padding) / $scale);
    my $grid_y = POSIX::floor(($y - $padding) / $scale);

    return FALSE if ($grid_x < 0 or $grid_x >= $grid_size or $grid_y < 0 or $grid_y >= $grid_size);

    my $path_index = xy_to_hilbert_d($grid_size, $grid_x, $grid_y);

    my $area = $h_state->{path_to_segment_map}->[$path_index];

    if (defined $area) {
        my $seg = $area->{data};
        my $path = $seg->{path} || "[anonymous]";
        $path = "[heap]" if $path =~ /heap/;

        my $comm_escaped = $seg->{comm};
        $comm_escaped =~ s/&/&amp;/g; $comm_escaped =~ s/</&lt;/g; $comm_escaped =~ s/>/&gt;/g;
        my $path_escaped = $path;
        $path_escaped =~ s/&/&amp;/g; $path_escaped =~ s/</&lt;/g; $path_escaped =~ s/>/&gt;/g;
        
        my $tooltip_text = sprintf("<b>Process:</b> %s (PID: %d)\n" .
                                   "<b>Path:</b> %s\n" .
                                   "<b>Size:</b> %s | <b>Perms:</b> %s",
                                   $comm_escaped, $seg->{pid}, $path_escaped,
                                   format_bytes($seg->{size}), $seg->{perms});

        if (defined $seg->{cmdline} && $seg->{cmdline} ne '') {
            my $cmdline_escaped = $seg->{cmdline};
            $cmdline_escaped =~ s/&/&amp;/g; $cmdline_escaped =~ s/</&lt;/g; $cmdline_escaped =~ s/>/&gt;/g;
            if (length($cmdline_escaped) > 100) {
                $cmdline_escaped = substr($cmdline_escaped, 0, 100) . "...";
            }
            $tooltip_text .= "\n<b>Cmdline:</b> " . $cmdline_escaped;
        }

        $tooltip->set_markup($tooltip_text);
        return TRUE;
    }

    return FALSE;
}


# --- Color Space Conversion Utilities ---
sub rgb_to_hsv {
    my ($r, $g, $b) = @_;
    my $max = $r; $max = $g if $g > $max; $max = $b if $b > $max;
    my $min = $r; $min = $g if $g < $min; $min = $b if $b < $min;
    my $h = 0; my $s = 0; my $v = $max;
    my $delta = $max - $min;
    
    $s = $max > 0 ? $delta / $max : 0;
    
    if ($delta != 0) {
        if    ($r == $max) { $h = ($g - $b) / $delta; }
        elsif ($g == $max) { $h = 2 + ($b - $r) / $delta; }
        else               { $h = 4 + ($r - $g) / $delta; }
        $h *= 60;
        $h += 360 if $h < 0;
    }
    return ($h/360, $s, $v);
}

sub hsv_to_rgb {
    my ($h, $s, $v) = @_;
    my ($r, $g, $b) = ($v, $v, $v);
    return [$r, $g, $b] if $s == 0;
    $h *= 360;
    my $i = POSIX::floor($h / 60) % 6;
    my $f = $h / 60 - $i;
    my $p = $v * (1 - $s);
    my $q = $v * (1 - $f * $s);
    my $t = $v * (1 - (1 - $f) * $s);
    if    ($i == 0) { ($r, $g, $b) = ($v, $t, $p); }
    elsif ($i == 1) { ($r, $g, $b) = ($q, $v, $p); }
    elsif ($i == 2) { ($r, $g, $b) = ($p, $v, $t); }
    elsif ($i == 3) { ($r, $g, $b) = ($p, $q, $v); }
    elsif ($i == 4) { ($r, $g, $b) = ($t, $p, $v); }
    else            { ($r, $g, $b) = ($v, $p, $q); }
    return [$r, $g, $b];
}

# --- General Utilities ---
sub format_bytes {
    my ($bytes) = @_;
    return "0 B" if $bytes == 0;
    my @units = qw(B KB MB GB TB);
    my $i = 0;
    while ($bytes >= 1024 && $i < $#units) {
        $bytes /= 1024;
        $i++;
    }
    return sprintf("%.2f %s", $bytes, $units[$i]);
}

sub _draw_star {
    my ($cr, $cx, $cy) = @_;
    my $radius1 = 12;
    my $radius2 = 6;
    my $points = 5;
    $cr->save;
    $cr->translate($cx, $cy);
    
    $cr->move_to($radius1, 0);
    for my $i (1 .. $points * 2) {
        my $angle = $i * 3.14159265 / $points;
        my $r = ($i % 2) == 0 ? $radius1 : $radius2;
        $cr->line_to($r * cos($angle), $r * sin($angle));
    }
    $cr->close_path;

    # White outline
    $cr->set_line_width(5);
    $cr->set_source_rgb(1, 1, 1);
    $cr->stroke_preserve;

    # Black outline
    $cr->set_line_width(2.5);
    $cr->set_source_rgb(0, 0, 0);
    $cr->stroke_preserve;
    
    # Gold fill
    $cr->set_source_rgb(1.0, 0.84, 0);
    $cr->fill;
    
    $cr->restore;
}

sub perform_ram_diff {
    my ($popup_state) = @_;
    
    unless (defined $popup_state->{raw_data_ref} && defined $popup_state->{loaded_raw_data_ref}) {
        warn "Cannot perform diff: live or loaded RAM data is missing.\n";
        return;
    }
    
    my $live_data = ${$popup_state->{raw_data_ref}};
    my $loaded_data = ${$popup_state->{loaded_raw_data_ref}};
    my @diffs;

    my $len = length($live_data) < length($loaded_data) ? length($live_data) : length($loaded_data);
    
    for my $i (0 .. $len - 1) {
        if (substr($live_data, $i, 1) ne substr($loaded_data, $i, 1)) {
            push @diffs, $i;
        }
    }
    
    $popup_state->{diff_points} = \@diffs;
    $popup_state->{drawing_area}->queue_draw();
}

main();
