package Util;

use strict;
use warnings;
use diagnostics;

use DateTime;
use FindBin;

# Include this for running from runner.pl
use lib "$FindBin::Bin/../../scripts/Queries";

# Include this for running from menu.pl
use lib "$FindBin::Bin/Queries";

use feature 'say';

my $local_root        = '/home/christoph/Notes/TU/MastersThesis/FailNix';
my $local_archive_dir = "$local_root/injections";

my $ntfy_url   = 'https://ntfy.vps.chriphost.de';
my $ntfy_token = 'tk_rx8fd6hojuz4ekcb72j7juugkbmga';    # May be public
my $ntfy_topic = 'fail-alerts';

sub notify {
    my ($msg) = @_;

    system( 'curl', '-H', "Authorization: Bearer $ntfy_token",
        '-d', $msg, "$ntfy_url/$ntfy_topic" );

    sleep(1);
}

sub notify_file {
    my ($file) = @_;

    system(
        'curl', '-H',  "Authorization: Bearer $ntfy_token",
        '-T',   $file, '-H', "Filename: $file",
        "$ntfy_url/$ntfy_topic"
    );

    sleep(1);
}

sub shell_quote {
    my ($string) = @_;
    $string =~ s/'/'"'"'/g;
    return "'$string'";
}

sub date_now {
    my $dt   = DateTime->now( time_zone => 'local' );
    my $date = $dt->iso8601;

    return $date;
}

sub run {
    my @cmd = @_;
    say "Running: @cmd";
    system(@cmd) == 0
      or die "Command failed (exit " . ( $? >> 8 ) . "): @cmd\n";
}

sub read_file {
    my ($file) = @_;
    open( my $readhandle, '<', $file ) or die "failed to open $file: $!";
    local $/;
    my $content = <$readhandle> // die "failed to read $file: $!";
    close($readhandle) or die "failed to close $file: $!";
    return $content;
}

sub write_file {
    my ( $file, $content ) = @_;
    open( my $writehandle, '>', $file ) or die "failed to open $file: $!";
    print $writehandle $content         or die "failed to write $file: $!";
    close($writehandle)                 or die "failed to close $file: $!";
}

sub rewrite_file {
    my ( $file, $matches, $replacement ) = @_;

    open( my $readhandle, '<', $file ) or die "failed to open $file: $!";
    my @lines;
    my $found = 0;
    while ( my $line = <$readhandle> ) {
        if ( index( $line, $matches ) != -1 ) {
            $line  = $replacement;
            $found = 1;
        }
        push @lines, $line;
    }
    close($readhandle) or die "failed to close $file: $!";

    die "no line containing $matches found in $file" unless $found;

    open( my $writehandle, '>', $file ) or die "failed to open $file: $!";
    print $writehandle @lines           or die "failed to write $file: $!";
    close($writehandle)                 or die "failed to close $file: $!";

    say "Updated $file with $replacement";
}

sub cpu_count {
    open( my $handle, '/proc/cpuinfo' ) or die "Can't open cpuinfo: $!\n";
    my $count = scalar( map /^processor/, <$handle> );
    close $handle;

    return $count;
}

sub find_files {
    my ($dir) = @_;

    opendir( my $dhandle, $dir ) or die "opendir($dir): $!";
    my @files = sort grep { -f "$dir/$_" } readdir($dhandle);
    closedir($dhandle);

    return @files;
}

sub find_subdirs {
    my ($dir) = @_;

    opendir( my $dhandle, $dir ) or die "opendir($dir): $!";
    my @subdirs =
      sort grep { $_ ne '.' && $_ ne '..' && -d "$dir/$_" } readdir($dhandle);
    closedir($dhandle);

    return @subdirs;
}

sub execute_query {
    my ( $experiment, $queryname, $db_conf, $builds_dir, $do_notify_file,
        @filter_config_names )
      = @_;

    my $module = "$queryname";
    my $file   = "$module.pm";
    $file =~ s/::/\//g;

    require $file;

    my $query    = $module->can('query')    or die "$module can't query()";
    my $args     = $module->can('args')     or die "$module can't args()";
    my $filename = $module->can('filename') or die "$module can't filename()";
    my $postprocess = $module->can('postprocess')
      or die "$module can't postprocess()";

    my $querystring =
      $query->( $experiment, "$builds_dir/$experiment", @filter_config_names );
    my $argsstring     = $args->();
    my $filenamestring = $filename->(@filter_config_names);

    # TODO: Pass the values instead of rewriting db.conf.
    #       Can also use DBI's database handle directly.
    my $result =
      qx{mariadb --defaults-file=$db_conf $argsstring -e "$querystring"};
    die "Query failed: $?" if $? != 0;

    $postprocess->($result);

    system( 'mkdir', '-p', "$builds_dir/$experiment" );
    open( my $results_handle, '>', "$builds_dir/$experiment/$filenamestring" )
      or die "failed to open file: $!";
    print $results_handle $result;
    close($results_handle) or die "failed to close file: $!";

    if ( defined $do_notify_file and $do_notify_file == 1 ) {
        notify_file("$builds_dir/$experiment/$filenamestring");
    }

    return $result;
}

sub format_number_sep {
    my ($number) = @_;
    1 while $number =~ s/^(-?\d+)(\d{3})/$1.$2/;
    return $number;
}

sub elf_sym_addr {
    my ( $elffile, $sym ) = @_;
    my $line = qx{nm "$elffile" 2>/dev/null | grep " $sym\$"};
    return undef unless $line =~ /^([0-9a-f]+)/i;
    return hex($1);
}

# Determine the address range of a function symbol
sub elf_sym_range {
    my ( $elffile, $sym ) = @_;

    my @lines = qx{nm -nS "$elffile" 2>/dev/null};
    return () unless @lines;

    for my $i ( 0 .. $#lines ) {
        my $line = $lines[$i];

        # With size:    "0010eefe 000001fe T aot_invoke_native"
        # Without size: "001298fe T invokeNative"
        next
          unless $line =~ /^([0-9a-f]+)(?:\s+([0-9a-f]+))?\s+\S\s+(\S+)\s*$/i;
        my ( $addr, $size, $name ) = ( $1, $2, $3 );
        next unless $name eq $sym;

        my $start = hex($addr);

        if ( defined $size ) {
            return ( $start, $start + hex($size) );
        }

        # Those don't have sizes, skip them.
        # They appear inside invokeNative_ia32.s
        my %ELF_INTERNAL_LABELS = (
            stack_aligned  => 1,
            skip_push_args => 1,
        );

        # No size -> take next symbol whose address is > $start.
        for my $j ( $i + 1 .. $#lines ) {
            next
              unless $lines[$j] =~
              /^([0-9a-f]+)(?:\s+[0-9a-f]+)?\s+\S\s+(\S+)\s*$/i;
            my ( $next_addr, $next_name ) = ( hex($1), $2 );
            next if $next_addr <= $start;
            next if $ELF_INTERNAL_LABELS{$next_name};
            return ( $start, $next_addr );
        }

        # No next symbol, don't know size
        return ();
    }

    return ();
}

sub elf_read_sections {
    my ($elffile) = @_;

    my $readelf_out = qx{readelf -S $elffile};
    my @lines       = split "\n", $readelf_out;

    my @sections;
    foreach my $line (@lines) {

# [ 1] .text             PROGBITS        00100000 001000 0000f0 00  AX  0   0  4
        next
          unless $line =~
/^\s*\[\s*\d+\]\s+(\..+?)\s+([A-Z]+)\s+([0-9a-f]+)\s+([0-9a-f]+)\s+([0-9a-f]+)\s+.*$/;

        push @sections, {
            name    => $1,
            type    => $2,
            address => $3,    # Memory location
            offset  => $4,    # File location
            size    => $5,
        };
    }

    return @sections;
}

sub get_section_name {
    my ( $address, @sections ) = @_;

    my $name;
    my $last_address = 0;
    foreach my $section (@sections) {
        my $cur_address = hex( $section->{address} );
        if ( hex($address) >= $cur_address && $cur_address > $last_address ) {
            $name         = $section->{name};
            $last_address = $cur_address;
        }
    }

    return $name;
}

sub read_experiment_info {
    my ($exp) = @_;

    return unless ( -f "$local_archive_dir/$exp/0.info" );

    open( my $fhandle, '<', "$local_archive_dir/$exp/0.info" )
      or die "Failed to open 0.info: $!";
    my $info = <$fhandle>;
    chomp $info if defined $info;
    close($fhandle);

    return defined $info ? $info : "";
}

sub read_marker_info {
    my ( $experiment, $benchmark, $address ) = @_;

    return ""
      unless (
        -f "$local_archive_dir/$experiment/markers/$benchmark-$address.info" );

    open( my $fhandle, '<',
        "$local_archive_dir/$experiment/markers/$benchmark-$address.info" )
      or die "Failed to open $benchmark-$address.info: $!";
    local $/;
    my $info = <$fhandle>;
    chomp $info;
    close($fhandle);

    return $info;
}

sub overwrite_marker_info {
    my ( $experiment, $benchmark, $address, $info ) = @_;

    system( 'mkdir', '-p', "$local_archive_dir/$experiment/markers" );

    open( my $fhandle, '>',
        "$local_archive_dir/$experiment/markers/$benchmark-$address.info" )
      or die "Failed to open $benchmark-$address.info: $!";
    print $fhandle $info;
    close($fhandle);
}

sub delete_marker_info {
    my ( $experiment, $benchmark, $address ) = @_;

    system( 'rm',
        "$local_archive_dir/$experiment/markers/$benchmark-$address.info" );
}

sub pick_data_file {
    my ( $dir, $prefix ) = @_;

    # \Q...\E treats ... as literal string
    my @files = sort grep { /^\Q$prefix\E.*\.csv$/ } find_files($dir);

    return "$prefix.csv" unless @files > 1;

    # Make sure the unfiltered file is at the top
    my @sorted = sort {
            ( $a eq "$prefix.csv" ) ? -1
          : ( $b eq "$prefix.csv" ) ? 1
          : $a cmp $b
    } @files;

    my @selected =
      TUI::select_from_list( "Select $prefix CSV file", 0, @sorted );
    die "No $prefix CSV file selected" unless @selected;
    return $selected[0];
}

sub select_experiment {
    my ($multi) = @_;

    my @experiments = find_subdirs($local_archive_dir);

    my @exp_with_notes;
    foreach my $exp (@experiments) {
        my $info = read_experiment_info($exp);

        push @exp_with_notes,
          ( defined $info && length($info) > 0 )
          ? sprintf( "%-50s (%s)", $exp, $info )
          : $exp;
    }

    my @selected_experiments =
      TUI::select_from_list( "Select Experiment", $multi, @exp_with_notes );
    die "No experiment selected" unless @selected_experiments;

    @selected_experiments =
      map { s/(.*?)\s+\(.+\)$/$1/r } @selected_experiments;

    return $multi == 1 ? @selected_experiments : $selected_experiments[0];
}

sub module_is_tacle {
    my ($module) = @_;
    return $module =~ /^tacle-/;
}

sub find_tacle_sources {
    my ($module) = @_;

    return () unless $module =~ /^tacle-([^-]+)-(.+)$/;
    my ( $category, $bench ) = ( $1, $2 );

    # TODO: Also have modified_sources/inline
    my $src_dir =
      "targets/wasm-tacle/$category/$bench/generated/modified_sources/default";

    die "TACLe source not found for '$module': $src_dir\n" unless -d $src_dir;

    my @srcs = sort glob("$src_dir/*.c");
    die "No sources found for '$module' in '$src_dir'\n" unless @srcs;

    return ( \@srcs, $src_dir );
}

1;
