#!/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;
use MIME::Base64;
use File::Spec;
use File::Path qw(make_path);
use Cwd qw(abs_path);

# --- 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,
};

my $icon_data_base64 = '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SUUjQ3NB8RS6KzhY0dGB6qScnHuPSvQtN8fWl9I0bW8wYAtwg9v9r3ryHNIa5a2Ep1HfqdlHG1KStuj3WPxTaRSb/Kn6Y+6P8ararr0eqwrDFG67WDZYY9R6n1rxOrFnfy2MheJUJIx8wP8AntXLLLrLSR1xzNOXvR/H/gHQWfgu9klIvZ7fy9uR5LnOfxXp1ru9MtDaFliIIOT85+lZGjazHfwu2xgAxHT6e/vW/a3cQJAD+vIFefWnUcuWZ6lClTjHmh1NezWR5CF2ZxnnNW1ct16+1Z6MUORQrFTxWTtY0s2z/9n/4Q6QaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAgQ29yZSA0LjQuMC1FeGl2MiI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOmlwdGNFeHQ9Imh0dHA6Ly9pcHRjLm9yZy9zdGQvSXB0YzR4bXBFeHQvMjAwOC0wMi0yOS8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdEV2dD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlRXZlbnQjIiB4bWxuczpwbHVzPSJodHRwOi8vbnMudXNlcGx1cy5vcmcvbGRmL3htcC8xLjAvIiB4bWxuczpHSU1QPSJodHRwOi8vd3d3LmdpbXAub3JnL3htcC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bXBNTTpEb2N1bWVudElEPSJnaW1wOmRvY2lkOmdpbXA6MWZhODA5OWUtOGZmMy00MTgzLTk5ZTctYmM5MTM0YTk2MTIxIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjNiZjJmNjUyLWQ3NWUtNDAwMS04MGU5LWFiYzA0ZmQ1ZWQ4ZCIgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ4bXAuZGlkOjkwY2ZmZDUwLWNmOGQtNDc4Ni04NjI5LThlMTlmMTE1YTJhMiIgR0lNUDpBUEk9IjIuMCIgR0lNUDpQbGF0Zm9ybT0iTGludXgiIEdJTVA6VGltZVN0YW1wPSIxNzU2Mjc3NDk0OTc4MDAyIiBHSU1QOlZlcnNpb249IjIuMTAuMjIiIGRjOkZvcm1hdD0iaW1hZ2UvanBlZyIgeG1wOkNyZWF0b3JUb29sPSJHSU1QIDIuMTAiPiA8aXB0Y0V4dDpMb2NhdGlvbkNyZWF0ZWQ+IDxyZGY6QmFnLz4gPC9pcHRjRXh0OkxvY2F0aW9uQ3JlYXRlZD4gPGlwdGNFeHQ6TG9jYXRpb25TaG93bj4gPHJkZjpCYWcvPiA8L2lwdGNFeHQ6TG9jYXRpb25TaG93bj4gPGlwdGNFeHQ6QXJ0d29ya09yT2JqZWN0PiA8cmRmOkJhZy8+IDwvaXB0Y0V4dDpBcnR3b3JrT3JPYmplY3Q+IDxpcHRjRXh0OlJlZ2lzdHJ5SWQ+IDxyZGY6QmFnLz4gPC9pcHRjRXh0OlJlZ2lzdHJ5SWQ+IDx4bXBNTTpIaXN0b3J5PiA8cmRmOlNlcT4gPHJkZjpsaSBzdEV2dDphY3Rpb249InNhdmVkIiBzdEV2dDpjaGFuZ2VkPSIvIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjVkODIzNzk2LWFkOGMtNDJlNS1hMjhhLTMyZDkyNmNmYjBiZSIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iR2ltcCAyLjEwIChMaW51eCkiIHN0RXZ0OndoZW49Ii0wNTowMCIvPiA8L3JkZjpTZXE+IDwveG1wTU06SGlzdG9yeT4gPHBsdXM6SW1hZ2VTdXBwbGllcj4gPHJkZjpTZXEvPiA8L3BsdXM6SW1hZ2VTdXBwbGllcj4gPHBsdXM6SW1hZ2VDcmVhdG9yPiA8cmRmOlNlcS8+IDwvcGx1czpJbWFnZUNyZWF0b3I+IDxwbHVzOkNvcHlyaWdodE93bmVyPiA8cmRmOlNlcS8+IDwvcGx1czpDb3B5cmlnaHRPd25lcj4gPHBsdXM6TGljZW5zb3I+IDxyZGY6U2VxLz4gPC9wbHVzOkxpY2Vuc29yPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICA8P3hwYWNrZXQgZW5kPSJ3Ij8+/+ICsElDQ19QUk9GSUxFAAEBAAACoGxjbXMEMAAAbW50clJHQiBYWVogB+kACAAaAAEAGAA1YWNzcEFQUEwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPbWAAEAAAAA0y1sY21zAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANZGVzYwAAASAAAABAY3BydAAAAWAAAAA2d3RwdAAAAZgAAAAUY2hhZAAAAawAAAAsclhZWgAAAdgAAAAUYlhZWgAAAewAAAAUZ1hZWgAAAgAAAAAUclRSQwAAAhQAAAAgZ1RSQwAAAhQAAAAgYlRSQwAAAhQAAAAgY2hybQAAAjQAAAAkZG1uZAAAAlgAAAAkZG1kZAAAAnwAAAAkbWx1YwAAAAAAAAABAAAADGVuVVMAAAAkAAAAHABHAEkATQBQACAAYgB1AGkAbAB0AC0AaQBuACAAcwBSAEcAQm1sdWMAAAAAAAAAAQAAAAxlblVTAAAAGgAAABwAUAB1AGIAbABpAGMAIABEAG8AbQBhAGkAbgAAWFlaIAAAAAAAAPbWAAEAAAAA0y1zZjMyAAAAAAABDEIAAAXe///zJQAAB5MAAP2Q///7of///aIAAAPcAADAblhZWiAAAAAAAABvoAAAOPUAAAOQWFlaIAAAAAAAACSfAAAPhAAAtsRYWVogAAAAAAAA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';
my $icon_data = decode_base64($icon_data_base64);

# --- 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;
    }

    eval {
        my $loader = Gtk2::Gdk::PixbufLoader->new();
        $loader->write($icon_data);
        $loader->close();
        my $pixbuf_icon = $loader->get_pixbuf();
        Gtk2::Window->set_default_icon($pixbuf_icon);
        print "Loaded application icon from embedded data.\n";
    };
    if ($@) {
        warn "Could not load application icon from data: $@";
    }

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

        # --- BITMAP/DIGRAPH BUTTONS ---
        my $decode_button = Gtk2::Button->new("Bitmap Decode");
        my $digraph_button = Gtk2::Button->new("Digraph");
        $multi_selection_hbox->pack_start($decode_button, FALSE, FALSE, 15);
        $multi_selection_hbox->pack_start($digraph_button, FALSE, FALSE, 5);
        
        # --- START: NEW PHOTOREC BUTTON ---
        my $photorec_button = Gtk2::Button->new("photorec");
        $multi_selection_hbox->pack_start($photorec_button, FALSE, FALSE, 5);
        
        # Connect its signal to the new handler function
        $photorec_button->signal_connect(clicked => sub { 
            on_photorec_click($popup, $popup_state); 
        });
        # --- END: NEW PHOTOREC BUTTON ---

        # --- 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, 15);
        $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 buttons ---
        $decode_button->signal_connect(clicked => sub { on_bitmap_decode_click($popup, $popup_state); });
        $digraph_button->signal_connect(clicked => sub { show_digraph_view($popup, $popup_state); });

        $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: New Analysis Features ---
sub on_bitmap_decode_click {
    my ($parent_window, $popup_state) = @_;

    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 = 200;
    my $max_width = 800;

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

    # --- START: NEW FORMAT SELECTION WIDGET ---
    my $format_label = Gtk2::Label->new("Format:");
    # Model columns: Display Name, Internal Key, Bytes Per Pixel
    my $format_store = Gtk2::ListStore->new('Glib::String', 'Glib::String', 'Glib::Int'); 
    my $format_combo = Gtk2::ComboBox->new_with_model($format_store);
    my $renderer = Gtk2::CellRendererText->new;
    $format_combo->pack_start($renderer, TRUE);
    $format_combo->add_attribute($renderer, 'text', 0);

    my @formats = (
        ["24-bit RGB",           'rgb24',    3],
        ["32-bit RGBA",          'rgba32',   4],
        ["8-bit Grayscale",      'gray8',    1],
        ["16-bit Grayscale (LE)",'gray16le', 2], # LE = Little-Endian
    );
    for my $format (@formats) {
        my $iter = $format_store->append;
        $format_store->set($iter, 0, $format->[0], 1, $format->[1], 2, $format->[2]);
    }
    $format_combo->set_active(0); # Default to 24-bit RGB
    # --- END: NEW FORMAT SELECTION WIDGET ---

    $offset_toolbar->pack_start($offset_label, FALSE, FALSE, 0);
    $offset_toolbar->pack_start($offset_spin, TRUE, TRUE, 5);
    # --- START: ADD NEW WIDGET TO TOOLBAR ---
    $offset_toolbar->pack_start($format_label, FALSE, FALSE, 10);
    $offset_toolbar->pack_start($format_combo, FALSE, FALSE, 0);
    # --- END: ADD NEW WIDGET TO TOOLBAR ---
    $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);
        
        # --- START: MODIFIED LOGIC ---
        # Get selected format info from the ComboBox
        my $model = $format_combo->get_model;
        my $iter = $format_combo->get_active_iter;
        return unless $iter; # Do nothing if nothing is selected
        my ($format_key, $bpp) = $model->get($iter, 1, 2); # key and bytes-per-pixel
        
        return if $new_width <= 0 or $bpp <= 0;
        
        my $min_bytes_needed = $new_width * $bpp;
        if ($byte_offset >= $data_size - $min_bytes_needed) {
            $byte_offset = $data_size - $min_bytes_needed;
            $byte_offset = 0 if $byte_offset < 0;
            $offset_adj->set_value($byte_offset); 
        }
        
        my $sliced_data = substr($raw_data, $byte_offset);
        my $sliced_data_size = length($sliced_data);

        my $new_height = int( ($sliced_data_size / $bpp) / $new_width );
        return if $new_height <= 0;

        my $pixels_to_render = $new_width * $new_height;
        my $bytes_to_process = $pixels_to_render * $bpp;
        
        my $pixbuf_data;
        my $pixbuf_has_alpha = FALSE;

        if ($format_key eq 'rgb24') {
            $pixbuf_data = substr($sliced_data, 0, $bytes_to_process);
        }
        elsif ($format_key eq 'rgba32') {
            $pixbuf_data = substr($sliced_data, 0, $bytes_to_process);
            $pixbuf_has_alpha = TRUE;
        }
        elsif ($format_key eq 'gray8') {
            my @bytes = unpack 'C*', substr($sliced_data, 0, $bytes_to_process);
            # Replicate each grayscale byte into R, G, and B components
            $pixbuf_data = pack 'C*', map {($_,$_,$_)} @bytes;
        }
        elsif ($format_key eq 'gray16le') {
            # Unpack as little-endian unsigned shorts
            my @words = unpack 'v*', substr($sliced_data, 0, $bytes_to_process);
            # Scale 16-bit value (0-65535) to 8-bit (0-255) and replicate for RGB
            $pixbuf_data = pack 'C*', map { my $v = int($_ / 257); ($v,$v,$v) } @words;
        }

        return unless defined $pixbuf_data and length $pixbuf_data > 0;
        
        my $final_rowstride_bpp = $pixbuf_has_alpha ? 4 : 3;

        $current_pixbuf = eval {
             Gtk2::Gdk::Pixbuf->new_from_data(
                $pixbuf_data, 'rgb', $pixbuf_has_alpha, 8, 
                $new_width, $new_height, $new_width * $final_rowstride_bpp
            );
        };
        # --- END: MODIFIED LOGIC ---
        
        if ($@ or not defined $current_pixbuf) {
            $image->clear(); 
            warn "Could not create pixbuf for width $new_width (format: $format_key): $@";
            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);
    $offset_adj->signal_connect(value_changed => $update_image);
    $format_combo->signal_connect(changed => $update_image); # <-- NEW SIGNAL
    
    $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);
        
        # --- Add format to filename ---
        my $model = $format_combo->get_model;
        my $iter = $format_combo->get_active_iter;
        my $format_key = $model->get($iter, 1);
        my $final_name = $base_name . "_decoded_${current_width}px_${format_key}.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;
    });

    # --- Initial Setup ---
    $update_image->(); # Create the first image
    $window->set_default_size(800, 600);
    $window->show_all;
}

# --- START: NEW HELPER FUNCTION for Digraph Drawing ---
sub _generate_digraph_pixbuf {
    my ($digraph_state, $is_weighted) = @_;

    my $counts = $digraph_state->{counts};
    my $max_count = $digraph_state->{max_count};

    my $pixels = "\0" x (256 * 256 * 3); # Pre-allocate a black background

    # Avoid division by zero if max_count is 0 or 1.
    # The log(1 + ...) prevents issues with log(0) or log(1).
    my $log_max = ($max_count > 0) ? log(1 + $max_count) : 1;

    for my $y (0 .. 255) {
        for my $x (0 .. 255) {
            my $count = $counts->[$y][$x];
            next if $count == 0;

            my $green_val;
            if ($is_weighted) {
                # Use a logarithmic scale for brightness to show detail for lower-frequency pairs
                my $brightness = log(1 + $count) / $log_max;
                $green_val = int($brightness * 255);
                $green_val = 255 if $green_val > 255; # Clamp
            } else {
                # Unweighted: any occurrence is full brightness
                $green_val = 255;
            }

            my $pos = $y * (256 * 3) + $x * 3;
            substr($pixels, $pos + 1, 1) = pack('C', $green_val); # Set Green component
        }
    }
    
    return Gtk2::Gdk::Pixbuf->new_from_data($pixels, 'rgb', 0, 8, 256, 256, 256 * 3);
}
# --- END: NEW HELPER FUNCTION ---


sub show_digraph_view {
    my ($parent_window, $main_popup_state) = @_;

    my ($data_ref_for_action, $offsets_str, undef, $path_info) = get_data_for_action($main_popup_state);

    unless (defined $data_ref_for_action and length(${$data_ref_for_action}) > 1) {
        my $err_dialog = Gtk2::MessageDialog->new($parent_window, 'destroy-with-parent', 'error', 'close', "Not enough data selected for digraph analysis.\nSelect at least 2 bytes.");
        $err_dialog->run;
        $err_dialog->destroy;
        return;
    }

    # --- START: NEW - Generate Info Text Early ---
    my $info_text = _generate_filename($main_popup_state, $offsets_str, $path_info);
    # --- END: NEW ---

    # --- Frequency Counting ---
    my @counts = map { [ (0) x 256 ] } (0..255);
    my $max_count = 0;
    my @bytes = unpack 'C*', ${$data_ref_for_action};

    for my $i (0 .. $#bytes - 1) {
        my $x = $bytes[$i];
        my $y = $bytes[$i+1];
        $counts[$y][$x]++;
        $max_count = $counts[$y][$x] if $counts[$y][$x] > $max_count;
    }

    # --- UI and State Setup ---
    my $digraph_popup_state = {
        pixbuf               => undef,
        counts               => \@counts,
        max_count            => $max_count,
        sound_player_pid     => undef,
        sound_child_watch_id => undef,
        sound_temp_file      => undef,
        _is_changing_sound_button => FALSE,
    };
    
    my $initial_pixbuf = _generate_digraph_pixbuf($digraph_popup_state, FALSE);
    $digraph_popup_state->{pixbuf} = $initial_pixbuf;

    my $window = Gtk2::Window->new('toplevel');
    $window->set_title("Digraph View");
    $window->set_transient_for($parent_window);
    $window->set_position('center-on-parent');
    # Give it a bit more vertical space for the label
    $window->set_default_size(256 + 10, 256 + 100); 
    $window->signal_connect(destroy => sub { $_[0]->destroy; });

    my $vbox = Gtk2::VBox->new(FALSE, 5);
    $vbox->set_border_width(5);
    $window->add($vbox);
    
    my $action_area = Gtk2::HBox->new(FALSE, 5);
    my $save_png_button = Gtk2::Button->new_from_stock('gtk-save');
    my $sound_button = Gtk2::ToggleButton->new("Sound");
    my $save_wav_button = Gtk2::Button->new("Save WAV");
    my $weighted_toggle = Gtk2::ToggleButton->new("Weighted");

    $action_area->pack_start($save_png_button, FALSE, FALSE, 0);
    $action_area->pack_start($sound_button, FALSE, FALSE, 5);
    $action_area->pack_start($save_wav_button, FALSE, FALSE, 5);
    $action_area->pack_start($weighted_toggle, FALSE, FALSE, 5);
    $vbox->pack_start($action_area, FALSE, FALSE, 0);

    my $image = Gtk2::Image->new_from_pixbuf($initial_pixbuf);
    $vbox->pack_start($image, TRUE, TRUE, 5);

    # --- START: NEW LABEL WIDGET ---
    my $info_label = Gtk2::Label->new();
    # Use Pango markup for small font and escape the text to prevent errors
    my $escaped_text = Glib::Markup::escape_text($info_text);
    $info_label->set_markup("<small>$escaped_text</small>");
    $info_label->set_line_wrap(TRUE);
    $info_label->set_justify('center');
    $vbox->pack_start($info_label, FALSE, FALSE, 5);
    # --- END: NEW LABEL WIDGET ---
    
    $weighted_toggle->signal_connect(toggled => sub {
        my $button = shift;
        my $is_weighted = $button->get_active;
        my $new_pixbuf = _generate_digraph_pixbuf($digraph_popup_state, $is_weighted);
        $digraph_popup_state->{pixbuf} = $new_pixbuf;
        $image->set_from_pixbuf($new_pixbuf);
    });
    
    $sound_button->signal_connect(toggled => sub {
        my $button = shift;
        my $get_digraph_data = sub {
            my $pixdata = $digraph_popup_state->{pixbuf}->get_pixels;
            return \$pixdata;
        };
        on_sound_toggle($button, $digraph_popup_state, $get_digraph_data);
    });

    $window->signal_connect(destroy => sub {
        if (defined $digraph_popup_state->{sound_player_pid}) {
            kill 'TERM', -($digraph_popup_state->{sound_player_pid});
        }
    });

    $save_wav_button->signal_connect(clicked => sub {
        my $pixdata_ref = \($digraph_popup_state->{pixbuf}->get_pixels);
        
        my $chooser = Gtk2::FileChooserDialog->new("Save Digraph as WAV", $window, 'save', 'gtk-cancel' => 'cancel', 'gtk-save' => 'accept');
        # Use the already-generated info_text for the filename
        $chooser->set_current_name($info_text . "_digraph.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 ${$pixdata_ref});
                print $fh $wav_header;
                print $fh ${$pixdata_ref};
                close $fh;
            } else {
                 my $err_dialog = Gtk2::MessageDialog->new($window, 'destroy-with-parent', 'error', 'close', "Error saving file:\n$!");
                $err_dialog->run;
                $err_dialog->destroy;
            }
        }
        $chooser->destroy;
    });

    $save_png_button->signal_connect(clicked => sub {
        # Use the already-generated info_text for the filename
        my $final_name = $info_text . "_digraph.png";

        my $chooser = Gtk2::FileChooserDialog->new("Save Digraph 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 { $digraph_popup_state->{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;
    });

    $window->show_all;
}

sub on_photorec_click {
    my ($parent_window, $popup_state) = @_;

    # 1. Check for required executables and user context
    my $photorec_path = _find_executable('photorec');
    unless ($photorec_path) {
        my $err_dialog = Gtk2::MessageDialog->new($parent_window, 'destroy-with-parent', 'error', 'close', 
            "Required command not found: 'photorec'.\nPlease ensure it is installed and in your system's PATH.");
        $err_dialog->run;
        $err_dialog->destroy;
        return;
    }
    
    my $sudo_user = $ENV{SUDO_USER};
    unless ($sudo_user) {
        my $err_dialog = Gtk2::MessageDialog->new($parent_window, 'destroy-with-parent', 'error', 'close', 
            "Cannot determine original user. Please run this script with 'sudo'.");
        $err_dialog->run;
        $err_dialog->destroy;
        return;
    }
    
    my ($uid, $gid) = (getpwnam $sudo_user)[2,3];
    unless (defined $uid) {
        my $err_dialog = Gtk2::MessageDialog->new($parent_window, 'destroy-with-parent', 'error', 'close', 
            "Could not find user info for '$sudo_user'.");
        $err_dialog->run;
        $err_dialog->destroy;
        return;
    }

    # 2. Get data for action
    my ($data_ref_for_action, $offsets_str, undef, $path_info) = 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;
    }

    # 3. Prepare directories with correct ownership
    my $base_name = _generate_filename($popup_state, $offsets_str, $path_info);
    my $captures_dir = File::Spec->catfile(Cwd::cwd(), 'captures');
    my $output_dir = File::Spec->catfile($captures_dir, $base_name);

    eval {
        unless (-d $captures_dir) {
            make_path($captures_dir);
            chown $uid, $gid, $captures_dir;
        }
        make_path($output_dir);
        chown $uid, $gid, $output_dir;
    };
    if ($@) {
        my $err_dialog = Gtk2::MessageDialog->new($parent_window, 'destroy-with-parent', 'error', 'close', 
            "Could not create/own output directory:\n$output_dir\n\nError: $@");
        $err_dialog->run;
        $err_dialog->destroy;
        return;
    }

    # 4. Create temporary file with readable permissions
    my ($fh, $temp_image_file) = tempfile(UNLINK => 0);
    binmode $fh;
    print $fh ${$data_ref_for_action};
    close $fh;
    chmod 0644, $temp_image_file or warn "Could not chmod temp file '$temp_image_file': $!";
    
    # 5. Create the results window
    my $window = Gtk2::Window->new('toplevel');
    $window->set_title("photorec results");
    $window->set_transient_for($parent_window);
    $window->set_position('center-on-parent');
    $window->set_default_size(600, 400);

    my $vbox = Gtk2::VBox->new(FALSE, 5);
    $vbox->set_border_width(5);
    $window->add($vbox);

    my $main_pane_sw = Gtk2::ScrolledWindow->new(undef, undef);
    $main_pane_sw->set_policy('automatic', 'automatic');
    $vbox->pack_start($main_pane_sw, TRUE, TRUE, 0);

    my $tv = Gtk2::TextView->new();
    $tv->set_editable(FALSE);
    $tv->set_wrap_mode('char');
    my $buffer = $tv->get_buffer;
    $buffer->set_text("Starting photorec recovery as user '$sudo_user'...\nThis may take a few moments.\n");
    $main_pane_sw->add($tv);
    
    my $action_area = Gtk2::HBox->new(FALSE, 5);
    my $path_label = Gtk2::Label->new("Output Path: ");
    my $path_entry = Gtk2::Entry->new();
    $path_entry->set_editable(FALSE);
    
    $action_area->pack_start($path_label, FALSE, FALSE, 0);
    $action_area->pack_start($path_entry, TRUE, TRUE, 5);
    $vbox->pack_start($action_area, FALSE, FALSE, 5);
    
    my $status_label = Gtk2::Label->new("Running photorec, please wait...");
    $vbox->pack_start($status_label, FALSE, FALSE, 0);
    
    $action_area->hide;
    $window->show_all;

    # 6. Run photorec asynchronously as the correct user
    my ($io_watch_id, $child_watch_id);
    my $temp_file_unlinked = FALSE; 

    my $photorec_args = 'partition_none,options,paranoid_no,keep_corrupted_file,wholespace,fileopt,everything,enable,gsm,disable,dovecot,disable,search';
    my @base_cmd = ($photorec_path, '/d', "$output_dir/", '/log', '/cmd', $temp_image_file, $photorec_args);
    
    pipe(my $read_pipe, my $write_pipe);
    my $pid = fork();
    die "Cannot fork: $!" unless defined $pid;

    if ($pid == 0) { # Child process
        close $read_pipe;
        open STDOUT, '>&', $write_pipe or die "Can't redirect STDOUT: $!";
        open STDERR, '>&', $write_pipe or die "Can't redirect STDERR: $!";
        my @exec_cmd = ('sudo', '-u', $sudo_user, @base_cmd);
        exec(@exec_cmd);
        die "Failed to exec photorec: $!";
    }
    
    # Parent process
    close $write_pipe;
    
    $io_watch_id = Glib::IO->add_watch(fileno($read_pipe), ['in', 'hup'], sub {
        my $line = <$read_pipe>;
        return FALSE unless defined $line;
        $buffer->insert($buffer->get_end_iter, $line);
        return TRUE;
    });

    $child_watch_id = Glib::Child->watch_add($pid, sub {
        my ($pid, $status) = @_;
        Glib::Source->remove($io_watch_id) if defined $io_watch_id;
        close($read_pipe);

        my $results_dir;
        opendir(my $dh, $output_dir) or die "Cannot open output dir '$output_dir': $!";
        my @subdirs = grep { /^recup_dir\.\d+$/ && -d "$output_dir/$_" } readdir($dh);
        closedir $dh;
        $results_dir = File::Spec->catfile($output_dir, $subdirs[0]) if @subdirs;

        my @files;
        if ($results_dir && opendir(my $rdh, $results_dir)) {
            @files = sort grep { !/^\./ && -f "$results_dir/$_" } readdir($rdh);
            closedir $rdh;
        }
        
        $main_pane_sw->remove($tv);
        my $list_store = Gtk2::ListStore->new('Glib::String');
        my $tree_view = Gtk2::TreeView->new($list_store);
        my $renderer = Gtk2::CellRendererText->new;
        my $column = Gtk2::TreeViewColumn->new_with_attributes("Recovered Files", $renderer, 'text', 0);
        $tree_view->append_column($column);
        
        for my $file (@files) { $list_store->set($list_store->append, 0, $file); }
        $main_pane_sw->add($tree_view);
        $tree_view->show;
        
        if ($results_dir) {
            $path_entry->set_text($results_dir);
            $action_area->show;
        }

        $status_label->set_text(@files ? (scalar(@files) . " file(s) recovered.") : "Recovery finished, but no files were found.");
        
        if (!$temp_file_unlinked) {
            unlink($temp_image_file) or warn "Could not unlink temp file '$temp_image_file': $!";
            $temp_file_unlinked = TRUE;
        }
        
        return FALSE; # Automatically remove this watch
    });

    $window->signal_connect(destroy => sub {
        if (kill(0, $pid)) { kill 'TERM', $pid; }
        Glib::Source->remove($io_watch_id) if defined $io_watch_id;
        Glib::Source->remove($child_watch_id) if defined $child_watch_id;
        if (!$temp_file_unlinked) {
            unlink($temp_image_file) or warn "Could not unlink temp file '$temp_image_file': $!";
            $temp_file_unlinked = TRUE;
        }
    });
}
# --- END: New Analysis Features ---

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, $data_getter) = @_;
    $data_getter ||= \&get_data_for_action;

    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) = $data_getter->($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 _find_executable {
    my ($cmd) = @_;
    # This function is for non-Windows systems, as photorec is primarily a Linux tool.
    return $cmd if ($cmd =~ m#/# && -x $cmd && !-d $cmd);
    my @paths = split /:/, ($ENV{PATH} || '');
    push @paths, '/usr/sbin', '/sbin', '.'; # Add common admin paths
    for my $p (@paths) {
        my $full_path = File::Spec->catfile($p, $cmd);
        return $full_path if -x $full_path && !-d $full_path;
    }
    return;
}

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();
