#!/usr/bin/env perl

use strict;
use warnings;
use diagnostics;

use FindBin;
use lib "$FindBin::Bin/Modules";

use Util;
use Mars;
use TUI;
use Text::CSV_XS;
use File::Temp qw(tempfile);

use feature 'say';

my $local_wamr        = '/home/christoph/Notes/TU/MastersThesis/05 WAMR';
my $local_newlib      = '/home/christoph/Notes/TU/MastersThesis/07 NewLib';
my $local_root        = '/home/christoph/Notes/TU/MastersThesis/FailNix';
my $local_archive_dir = "$local_root/injections";

# Select experiment to open
my $selected_experiment = Util::select_experiment(0);

my $selected_faults_csv =
  Util::pick_data_file( "$local_archive_dir/$selected_experiment", "faults" );

my $cui = TUI::init_cui();

# TODO: Add a TextEditor panel beside the experiment selection for notes.
#       Store notes in experiment/notes/description.txt
# TODO: Add a TextEditor panel below the markers for notes.
#       Store notes in experiment/notes/benchmark-type-address.txt
# TODO: Add a more exhaustive faults query that also retrieves
#       what fault has been injected. Then add an info popup or
#       display it in the markers panel
# TODO: Add a filter for address ranges
# TODO: Add a filter for segments

# =============================================================================
# Data handling
# =============================================================================

my @marker_values;
my %marker_labels;
my @filtered_marker_values;

my $assembly_text;

my $source_file;
my $source_line;
my $source_text;

my @benchs  = ( "ip", "mem", "regs" );
my @markers = (
    "OK_MARKER",         "DETECTED_MARKER",
    "FAIL_MARKER",       "TRAP",
    "TIMEOUT",           "ACCESS_OUTERSPACE",
    "WRITE_TEXTSEGMENT", "GROUP1_MARKER",
);

# Filter popup state
my $popup_focus = "left";

my @selected_benchs = @benchs;
my %index_of_benchs;
@index_of_benchs{@benchs} = 0 .. $#benchs;

my @selected_markers = ("FAIL_MARKER");
my %index_of_markers;
@index_of_markers{@markers} = 0 .. $#markers;

my $selected_threshold = 0;

my @sections;

sub marker_label {
    my ($marker) = @_;

    return sprintf(
        "%4s %8s %5s %17s %12sx %-40s",
        $marker->{benchmark},
        $marker->{fault_address},
        Util::get_section_name( $marker->{fault_address}, @sections ),
        $marker->{resulttype},
        Util::format_number_sep( $marker->{faults} ),
        Util::read_marker_info(
            $selected_experiment, $marker->{benchmark},
            $marker->{fault_address}
        ),
    );
}

sub load_faults_csv {
    @marker_values = ();
    %marker_labels = ();

    # Schema: benchmark, resulttype, faults, fault_address
    my $data = Text::CSV_XS::csv(
        in => "$local_archive_dir/$selected_experiment/$selected_faults_csv",
        headers => 'auto'
    );

    @sections = Util::elf_read_sections(
        "$local_archive_dir/$selected_experiment/system.elf");

    # Result:
    # [ { benchmark => "ip", resulttype => "OK_MARKER",
    #     faults => "100", fault_address => "0x100074" },
    #   ... ]
    @marker_values          = @$data;
    @filtered_marker_values = @marker_values;
    %marker_labels =
      map { $_ => marker_label($_); } @marker_values;
}

sub filter_marker_values {

    sub is_allowed {
        my ($marker) = @_;

        my $bench = $marker->{benchmark};
        my $type  = $marker->{resulttype};
        my $count = int( $marker->{faults} );

        return (  grep { /^$bench$/ } @selected_benchs
              and grep { /^$type$/ } @selected_markers
              and $count > $selected_threshold );
    }

    @filtered_marker_values = grep { is_allowed($_) } @marker_values;
}

# How much to disassemble around the address / how many lines to show
my $region_size     = int( $cui->height() / 2 ) - 2;
my $asm_region_size = $region_size + 120;

my %assembly_cache;

sub align_region {
    my ( $assembly_region, $objdump_address ) = @_;

    my @region;
    my @lines = split "\n", $assembly_region;
    my $center;
    for my $i ( 1 .. $#lines ) {
        my $line = $lines[ $i - 1 ];
        chomp $line;

        # Mark the center line
        if ( grep { /^\s*$objdump_address/ } $line ) {
            push @region, sprintf( ">> %s", $line );
            $center = $i;
        }
        else {
            push @region, sprintf( "   %s", $line );
        }
    }

    if ( defined $center ) {
        my $offset = $center - $region_size;
        if ( $offset > 0 ) {

            # Remove lines ($center > $region_size)
            @region = @region[ $offset .. $#region ];
        }
        elsif ( $offset < 0 ) {

            # Add lines
            for ( 1 .. abs($offset) + 1 ) {
                unshift @region, "";
            }
        }
    }

    return join "\n", @region;
}

sub load_assembly_elf {
    my ($marker) = @_;

    return unless defined $marker;

    my $address = hex( $marker->{fault_address} );

    if ( !exists $assembly_cache{$address} ) {
        my $file  = "$local_archive_dir/$selected_experiment/system.elf";
        my $start = $address - $asm_region_size;
        my $end   = $address + $asm_region_size;

        my $objdump_address = lc substr( $marker->{fault_address}, 2 );

        my $assembly_region =
qx{objdump $file -D -M intel -C --start-address=$start --stop-address=$end}
          =~ s/.*\n.*\n.*\n.*\n.*\n.*\n//r;

        $assembly_cache{$address} =
          align_region( $assembly_region, $objdump_address );
    }

    $assembly_text = $assembly_cache{$address};
}

my %remappings = (
    "/build/source/core"      => "$local_wamr/core",
    "/build/newlib-.*/newlib" => "$local_newlib/newlib",
    ".*/module_host.c"        =>
      "$local_archive_dir/$selected_experiment/module_host.c",
);

sub remap_source_path {
    my ($path) = @_;

    foreach my $source ( keys %remappings ) {
        my $target = $remappings{$source};
        $path =~ s/$source/$target/;
    }

    return $path;
}

sub read_source_file {
    my ( $sourcefile, $lineno ) = @_;

    return unless ( -f $sourcefile );

    open( my $fhandle, '<', $sourcefile ) or return;

    my @lines = <$fhandle>;
    close($fhandle);

    my $start = int($lineno) - $region_size;
    $start = 1 if $start < 1;
    my $end = int($lineno) + $region_size;
    $end = scalar(@lines) if $end > scalar(@lines);

    my @region;
    for my $i ( $start .. $end ) {
        my $line = $lines[ $i - 1 ];
        chomp $line;

        # Mark the center line
        if ( $i == $lineno ) {
            push @region, sprintf( ">> %6d | %s", $i, $line );
        }
        else {
            push @region, sprintf( "   %6d | %s", $i, $line );
        }
    }

    return join( "\n", @region );
}

my %source_cache;
my %source_file_cache;
my %source_line_cache;

sub load_source_c {
    my ($marker) = @_;

    return unless defined $marker;

    my $address = $marker->{fault_address};

    if ( !exists $source_cache{$address} ) {
        my $elffile   = "$local_archive_dir/$selected_experiment/system.elf";
        my $host_file = "$local_archive_dir/$selected_experiment/module_host.c";
        my $module_file =
          "$local_archive_dir/$selected_experiment/wasm-module.cpp";

        # Example:
        # wasm_interp_call_func_bytecode
        # /build/source/core/iwasm/interpreter/wasm_interp_classic.c:1685
        my $location = qx{addr2line -e $elffile -f -C -i $address};
        my @lines    = split( "\n", $location );
        if ( scalar(@lines) < 2 ) {
            $source_text = "Addr2Line Error\n";
            return;
        }

        my $line = $lines[1];    # Ignore possible further lines
        my ( $sourcefile, $lineno ) = split ":", $line;
        ($lineno) = $lineno =~ /^(\d+)/;
        my $remapped = remap_source_path($sourcefile);

        my $source_region = read_source_file( $remapped, $lineno );
        if ( !defined $source_region or length($source_region) == 0 ) {
            $source_text = "Failed to Read Source: $location";
            $source_file = "Unknown";
            $source_line = "Unknown";
            return;
        }

        $source_cache{$address}      = $source_region;
        $source_file_cache{$address} = $remapped;
        $source_line_cache{$address} = $lineno;
    }

    $source_text = $source_cache{$address};
    $source_file = $source_file_cache{$address};
    $source_line = $source_line_cache{$address};
}

# =============================================================================
# UI Layout
# =============================================================================

my $win = $cui->add( 'explorer', 'Window' );

my $left   = $win->add( 'left',   'Container' );    # , -padbottom => 1,
my $middle = $win->add( 'middle', 'Container' );    # , -padbottom => 1,
my $right  = $win->add( 'right',  'Container' );    # , -padbottom => 1,

my $markers_panel;
my $assembly_panel;
my $source_panel;

my $refresh_panels = sub {
    load_assembly_elf( $markers_panel->get_active_value() );
    load_source_c( $markers_panel->get_active_value() );

    $assembly_panel->text($assembly_text);
    $source_panel->text($source_text);

    $source_panel->title("Source View: $source_file");

    $assembly_panel->draw();
    $source_panel->draw();
};

$markers_panel = $left->add(
    'markers_list',
    'Listbox',
    -title       => "Markers",
    -border      => 1,
    -values      => \@filtered_marker_values,
    -labels      => \%marker_labels,
    -multi       => 0,
    -radio       => 0,
    -vscrollbar  => 'right',
    -onselchange => $refresh_panels,
);

$assembly_panel = $middle->add(
    'assembly_viewer',
    'TextViewer',
    -title  => "Assembly View",
    -border => 1,
    -text   => $assembly_text,
);

$source_panel = $right->add(
    'source_viewer',
    'TextViewer',
    -title  => "Source View",
    -border => 1,
    -text   => $source_text,
);

$win->add(
    'titlebar', 'Label',
    -y    => 0,
    -text => "Experiment: $selected_experiment",
);

$win->add(
    'info', 'Label',
    -y    => -1,
    -text =>
"Space = toggle, F = filter markers, S = open source, A = open assembly, N = edit notes, R = resize, Q = quit",
);

sub set_geometry {
    my ( $widget, $x, $y, $w, $h ) = @_;

    $widget->{-x}      = $x;
    $widget->{-y}      = $y;
    $widget->{-width}  = $w;
    $widget->{-height} = $h;

    # $widget->layout();
}

sub layout_resize {
    my $w = $win->width();
    my $h = $win->height();

    my $w1 = int( $w * 0.3 );
    my $w2 = int( $w * 0.3 );
    my $w3 = int( $w - $w1 - $w2 );

    set_geometry( $left,   0,         1, $w1, $h - 2 );
    set_geometry( $middle, $w1,       1, $w2, $h - 2 );
    set_geometry( $right,  $w1 + $w2, 1, $w3, $h - 2 );

    $cui->layout();
}

# =============================================================================
# Filter Popup
# =============================================================================

sub markers_filter_popup {
    my $w  = 60;
    my $h  = 24;
    my $h1 = $h - 7;    # top listboxes height

    my @bench_values = @benchs;
    my %bench_labels = map { $_ => $_ } @benchs;

    my @marker_values = @markers;
    my %marker_labels = map { $_ => $_ } @markers;

    my $popup = $cui->add(
        'popup', 'Window',
        -x      => int( $cui->width() / 2 - $w / 2 ),
        -y      => int( $cui->height() / 2 - $h / 2 ),
        -width  => $w,
        -height => $h,
        -border => 1,
        -title  => "Filter Markers",
    );

    my $popup_left = $popup->add(
        'popup_left', 'Container',
        -x      => 0,
        -y      => 1,
        -width  => $w / 2,
        -height => $h1,
    );

    my $popup_right = $popup->add(
        'popup_right', 'Container',
        -x      => $w / 2,
        -y      => 1,
        -width  => $w / 2,
        -height => $h1,
    );

    my $popup_bottom = $popup->add(
        'popup_bottom', 'Container',
        -x      => 0,
        -y      => $h1 + 1,
        -width  => $w,
        -height => 3,
    );

    my $benchmark_filter = $popup_left->add(
        'benchmark_filter',
        'Listbox',
        -title  => "By Benchmark",
        -border => 1,
        -values => \@bench_values,
        -labels => \%bench_labels,
        -multi  => 1,
        -radio  => 0,
    );

    my $marker_filter = $popup_right->add(
        'marker_filter',
        'Listbox',
        -title  => "By Type",
        -border => 1,
        -values => \@marker_values,
        -labels => \%marker_labels,
        -multi  => 1,
        -radio  => 0,
    );

    my $threshold = $popup_bottom->add(
        'threshold', 'TextEntry',
        -title  => "By Count Threshold",
        -border => 1,
        -text   => $selected_threshold,
        -pos    => length($selected_threshold),
        -regexp => '/^\d*$/',
    );

    $popup->add(
        'popup_info', 'Label',
        -y    => -1,
        -text => "Space = toggle, F = cycle focus, C = confirm, Q = quit",
    );

    $benchmark_filter->set_selection( map { $index_of_benchs{$_} }
          @selected_benchs );
    $marker_filter->set_selection( map { $index_of_markers{$_} }
          @selected_markers );

    my $focus = sub {
        if ( $popup_focus eq "left" ) {
            $benchmark_filter->focus();
        }
        elsif ( $popup_focus eq "right" ) {
            $marker_filter->focus();
        }
        else {
            $threshold->focus();
        }
    };

    $popup->set_binding(
        sub {
            @selected_benchs    = $benchmark_filter->get();
            @selected_markers   = $marker_filter->get();
            $selected_threshold = int( $threshold->get() ) or 0;

            filter_marker_values( @selected_benchs, @selected_markers );

            $cui->delete('popup');
            $cui->layout();
        },
        'c',
    );

    $popup->set_binding(
        sub {
            if ( $popup_focus eq "left" ) {
                $popup_focus = "right";
            }
            elsif ( $popup_focus eq "right" ) {
                $popup_focus = "bottom";
            }
            else {
                $popup_focus = "left";
            }
            $focus->();
        },
        'f',
    );

    $popup->set_binding(
        sub {
            $cui->delete('popup');
            $cui->layout();
        },
        'q'
    );

    $cui->layout();
    $focus->();
}

# =============================================================================
# Notes Popup
# =============================================================================

sub edit_notes_popup {
    my $w = 60;
    my $h = 24;

    my $marker = $markers_panel->get_active_value();
    my $marker_text =
      Util::read_marker_info( $selected_experiment, $marker->{benchmark},
        $marker->{fault_address} );

    my $popup = $cui->add(
        'popup', 'Window',
        -x      => int( $cui->width() / 2 - $w / 2 ),
        -y      => int( $cui->height() / 2 - $h / 2 ),
        -width  => $w,
        -height => $h,
        -border => 1,
        -title  => "Edit Notes",
    );

    my $notes = $popup->add(
        'marker_notes',
        'TextEditor',
        -height => $h - 3,
        -border => 1,
        -text   => $marker_text,
        -pos    => length($marker_text),
    );

    $popup->add(
        'popup_info', 'Label',
        -y    => -1,
        -text => "^S/^C = save, ^Q = quit",
    );

    $popup->set_binding(
        sub {
            my $text = $notes->get();

            if ( length($text) > 0 ) {
                Util::overwrite_marker_info( $selected_experiment,
                    $marker->{benchmark}, $marker->{fault_address}, $text, );
            }
            else {
                Util::delete_marker_info( $selected_experiment,
                    $marker->{benchmark}, $marker->{fault_address} );
            }

            $marker_labels{$marker} = marker_label($marker);

            $cui->delete('popup');
            $cui->layout();
            $markers_panel->draw();
        },
        "\cS",
        "\cC",
    );

    $popup->set_binding(
        sub {
            $cui->delete('popup');
            $cui->layout();
        },
        "\cQ"
    );

    $cui->layout();
    $notes->focus();
}

# =============================================================================
# Bindings
# =============================================================================

$win->set_binding(
    sub {
        markers_filter_popup();
    },
    'f',
);

$win->set_binding(
    sub {
        edit_notes_popup();
    },
    'n',
);

$win->set_binding(
    sub {
        layout_resize();
    },
    'r',
);

$win->set_binding(
    sub {
        system( 'neovide', '--fork', $source_file, '--', "+$source_line" );
    },
    's',
);

my $objdump_out;
my @objdump_lines;
my $temp_file;

$win->set_binding(
    sub {
        my $file = "$local_archive_dir/$selected_experiment/system.elf";

        my $marker          = $markers_panel->get_active_value();
        my $objdump_address = lc substr( $marker->{fault_address}, 2 );

        if ( !defined $objdump_out ) {
            $objdump_out = qx{objdump $file -D -M intel -C --source};
            $objdump_out =~ s/.*\n.*\n.*\n.*\n.*\n.*\n//;
            @objdump_lines = split "\n", $objdump_out;

            ( my $fhandle, $temp_file ) = tempfile();
            print $fhandle $objdump_out;
            close($fhandle);
        }

        my $temp_line;
        for my $i ( 1 .. scalar(@objdump_lines) ) {
            my $line = $objdump_lines[ $i - 1 ];

            if ( grep { /^\s*$objdump_address/ } $line ) {
                $temp_line = $i;
                last;
            }
        }

        if ( defined $temp_line ) {
            system( 'neovide', '--fork', $temp_file, '--', "+$temp_line" );
        }
        else {
            system( 'neovide', '--fork', $temp_file );
        }
    },
    'a',
);

$win->set_binding(
    sub {

    },
    'g',
);

$win->set_binding(
    sub {
        $cui->mainloopExit();
    },
    'q',
);

# =============================================================================
# Main Loop
# =============================================================================

load_faults_csv();
filter_marker_values();

layout_resize();
$markers_panel->focus();
$refresh_panels->();
$cui->mainloop();

$cui->leave_curses();
$cui->delete('explorer');
