package Pheno::Ranker::Compare::Matrix;

use strict;
use warnings;
use autodie;
use feature qw(say);

use Sort::Naturally qw(nsort);

use Pheno::Ranker::Metrics;

use Exporter 'import';
our @EXPORT_OK = qw(cohort_comparison);

sub cohort_comparison {
    my ( $ref_binary_hash, $self ) = @_;
    my $out_file          = $self->{out_file};
    my $similarity_metric = $self->{similarity_metric_cohort};
    my $matrix_format     = $self->{matrix_format} || 'dense';

    # Define limit #items for switching to whole matrix calculation
    my $max_items = $self->{max_matrix_records_in_ram};

    # Inform about the start of the comparison process
    say "Performing COHORT comparison"
      if ( $self->{debug} || $self->{verbose} );

    # Define the subroutine to be used
    my %similarity_function = (
        'hamming' => \&hd_fast,
        'jaccard' => \&jaccard_similarity_formatted
    );

    # Define values for diagonal elements depending on metric
    my %similarity_diagonal = (
        'hamming' => 0,
        'jaccard' => 1
    );

    # Use previous hashes to define stuff
    my $metric              = $similarity_function{$similarity_metric};
    my $similarity_diagonal = $similarity_diagonal{$similarity_metric};

    # Sorting keys of the hash
    my @ids     = nsort( keys %{$ref_binary_hash} );
    my @strings = map { $ref_binary_hash->{$_}{binary_digit_string_weighted} } @ids;
    my $num_items = scalar @ids;

    # Define $switch for going from RAM to all calculations
    my $switch = $num_items > $max_items ? 1 : 0;

    say "RAM efficient mode is: "
      . ( $switch ? "on" : "off" )
      . " (max_matrix_records_in_ram: $max_items)"
      if ( $self->{debug} || $self->{verbose} );

    if ( $matrix_format eq 'mtx' ) {
        _write_matrix_market(
            {
                out_file            => $out_file,
                ids                 => \@ids,
                strings             => \@strings,
                metric              => $metric,
                similarity_diagonal => $similarity_diagonal,
                verbose             => $self->{verbose},
            }
        );
        say "Matrix saved to <$out_file>" if ( $self->{debug} || $self->{verbose} );
        return 1;
    }

    # Opening file for output
    open( my $fh, '>:encoding(UTF-8)', $out_file );
    say $fh "\t", join "\t", @ids;

    # Initialize matrix for storing similarity
    my @matrix;

    # Iterate over items (I elements)
    for my $i ( 0 .. $#ids ) {
        say "Calculating <" . $ids[$i] . "> against the cohort..."
          if $self->{verbose};
        my $str1 = $strings[$i];

        my @row = ( $ids[$i] );

        # Iterate for pairwise comparisons (J elements)
        for my $j ( 0 .. $#ids ) {
            my $str2 = $strings[$j];
            my $similarity;

            if ($switch) {

                # For large datasets compute upper and lower triange
                $similarity =
                  $i == $j ? $similarity_diagonal : $metric->( $str1, $str2 );
            }
            else {
                if ( $i == $j ) {

                    # Similarity for diagonal elements
                    $similarity = $similarity_diagonal;
                }
                elsif ( $j > $i ) {

                    # Compute similarity for large cohorts or upper triangle
                    $similarity = $metric->( $str1, $str2 );
                    $matrix[$i][$j] = $similarity;
                }
                else {
                    # Use precomputed similarity from lower triangle
                    $similarity = $matrix[$j][$i];
                }
            }

            push @row, $similarity;
        }

        say $fh join "\t", @row;
    }

    # Close the file handle
    close $fh;

    # Inform about the completion of the matrix computation
    say "Matrix saved to <$out_file>" if ( $self->{debug} || $self->{verbose} );
    return 1;
}

sub _write_matrix_market {
    my $arg                 = shift;
    my $out_file            = $arg->{out_file};
    my $ids                 = $arg->{ids};
    my $strings             = $arg->{strings};
    my $metric              = $arg->{metric};
    my $similarity_diagonal = $arg->{similarity_diagonal};
    my $verbose             = $arg->{verbose};
    my $num_items           = scalar @$ids;

    open( my $fh, '+>:encoding(UTF-8)', $out_file );
    say $fh '%%MatrixMarket matrix coordinate real symmetric';
    say $fh '% Generated by Pheno-Ranker';
    say $fh '% IDs use 1-based matrix indexes: % id <index> <identifier>';
    for my $i ( 0 .. $#$ids ) {
        say $fh '% id ' . ( $i + 1 ) . ' ' . $ids->[$i];
    }

    my $size_line_offset = tell $fh;
    printf {$fh} "%20d %20d %20d\n", $num_items, $num_items, 0;

    my $nonzero_count = 0;
    for my $i ( 0 .. $#$ids ) {
        say "Calculating <" . $ids->[$i] . "> against the cohort..."
          if $verbose;
        my $str1 = $strings->[$i];

        # Matrix Market symmetric coordinate format stores one triangle only.
        for my $j ( $i .. $#$ids ) {
            my $similarity =
              $i == $j ? $similarity_diagonal : $metric->( $str1, $strings->[$j] );
            next if $similarity == 0;

            printf {$fh} "%d %d %s\n", $i + 1, $j + 1, $similarity;
            $nonzero_count++;
        }
    }

    seek $fh, $size_line_offset, 0;
    printf {$fh} "%20d %20d %20d\n", $num_items, $num_items, $nonzero_count;
    close $fh;

    return 1;
}

1;
