#!/usr/bin/perl
##############################################################################
#
#  Disk Freespace ETA Estimation
#  (c) Vladi Belperchinov-Shabanski "Cade" 2010
#  <cade@bis.bg> <cade@biscom.net> <cade@datamax.bg>
#  http://cade.datamax.bg/away/dfeta
#
##############################################################################
use strict;
use Data::Dumper;
use Storable qw( nstore retrieve );
use Math::BigFloat;

our $BIN_DF     = '/bin/df';
our $BIN_DF_OPT = '-lP';
our $HOME       = $ENV{ 'HOME' };

our $HELP = <<END;
usage: $0 <options> command
options:
    -h        -- print help
    -d path   -- path to df(1), (default: /bin/df)
    --        -- end of options
commands:
    eta       -- print estimates (default)
    sample    -- samples current df(1) info
END

our @args;
while( @ARGV )
  {
  $_ = shift;
  if( /^--+$/io )
    {
    push @args, @ARGV;
    last;
    }
  if( /^-d/ )
    {
    $BIN_DF = shift;
    next;
    }
  if( /^(--?h(elp)?|help)$/io )
    {
    print $HELP;
    exit;
    }
  push @args, $_;
  }

die "error: df(1) cannot be executed, use -d to specify correct location\n" unless -x $BIN_DF;

my $cmd = shift @args;

$cmd = 'eta' unless $cmd;

sample_data() if $cmd eq 'sample';
print_eta()   if $cmd eq 'eta';

###############################################################################

sub sample_data
{
  my @df = `$BIN_DF $BIN_DF_OPT`;
  shift @df; # skip headers

  for( @df )
    {
    chomp;
    my @d = split /\s+/, $_;
    # print Dumper( $_, \@d );
    my %d;

    $d{ ctime } = time();
    $d{ dev   } = shift @d;
    $d{ total } = shift @d;
    $d{ used  } = shift @d;
    $d{ free  } = shift @d;
    $d{ prc   } = shift @d;
    $d{ mount } = shift @d;

    $d{ full  } = $d{ used } / $d{ total };

    sample_add( $d{ dev   }, \%d );
    sample_add( $d{ mount }, \%d );
    }
}

sub print_eta
{
  my @df = `$BIN_DF $BIN_DF_OPT`;
  shift @df; # skip headers

  my @table;

  push @table, [ 'Device', 'Mount point', 'Estimate full at', 'Full in' ];

  for( @df )
    {
    chomp;
    my @d = split /\s+/, $_;
    # print Dumper( $_, \@d );
    my %d;

    $d{ ctime } = time();
    $d{ dev   } = shift @d;
    $d{ total } = shift @d;
    $d{ used  } = shift @d;
    $d{ free  } = shift @d;
    $d{ prc   } = shift @d;
    $d{ mount } = shift @d;

    my $eta = calc_eta( $d{ dev } );

    my @row;

    my $eta_str;
    my $diff_str;
    if( $eta > 0 )
      {
      $eta_str  = scalar localtime $eta;
      substr( $eta_str, 11, 8 ) = undef;
      $diff_str = time_diff_in_words2( time() - $eta );
      }
    else
      {
      $eta_str  = '[never]';
      $diff_str = '[n/a]';
      }

    @row = ( $d{ dev }, $d{ mount }, $eta_str, $diff_str );

    push @table, \@row;
    }

  print_term_table( \@table );
}

###############################################################################

sub sample_add
{
  my $key = shift;
  my $hr  = shift;

  mkdir( "$HOME/.dfeta" );
  mkdir( "$HOME/.dfeta/data" );

  -d "$HOME/.dfeta/data" or "error: cannot find directory [$HOME/.dfeta/data]\n";

  my $fn = "$HOME/.dfeta/data/" . str_hex( $key );

  nstore( [], $fn ) unless -e $fn; # create empty sample data

  my $data = retrieve( $fn );

  nstore( $data, "$fn.backup" ) or die "error: cannot create backup file [$fn.backup]\n";

  push @$data, $hr;

  nstore( $data, $fn ) or die "error: cannot create data file [$fn]\n";

  my $c = @$data;

  print "[$key] data, $c samples\n";
}

sub calc_eta
{
  my $key = shift;

  my $fn = "$HOME/.dfeta/data/" . str_hex( $key );

  die "error: not enough data to calculate eta, run 'dfeta sample' more\n" unless -e $fn;

  my $data = retrieve( $fn ) or return undef;

  my $eta;

  $eta = stat_linear( $data );

  return $eta;
}

###############################################################################

sub stat_linear
{
  my $data = shift;

  my $N = @$data;

  die "error: not enough data to calculate eta, run 'dfeta sample' more\n" if $N < 2;

#print Dumper( $data );

  my $sx  = s_sum( $data, 'ctime' );
  my $sy  = s_sum( $data, 'full'  );

  my $sx2 = s_sum2( $data, 'ctime' );
  my $sy2 = s_sum2( $data, 'full'  );

  my $sxy = s_sum_xy( $data, 'ctime', 'full'  );

  my $Sxx = $sx2 - $sx * $sx / $N;

  my $Sxy = $sxy - $sx * $sy / $N;


  my $a = $Sxy / $Sxx;
  my $b = $sy / $N - $a * $sx / $N;

#die "$Sxx $Sxy $a $b\n";

  return '-1' if $a == 0;

  my $x = ( 1 - $b ) / $a;

  return $x;
}

sub s_sum
{
  my $data = shift;
  my $key  = shift;

  my $sum = Math::BigFloat->new();
  for my $hr ( @$data )
    {
#    $sum += $hr->{ $key };
    $sum += Math::BigFloat->new( $hr->{ $key } );
    }

  return $sum;
}

sub s_sum2
{
  my $data = shift;
  my $key  = shift;

  my $sum = Math::BigFloat->new();
  for my $hr ( @$data )
    {
    #$sum += $hr->{ $key } * $hr->{ $key };
    $sum += Math::BigFloat->new( $hr->{ $key } ) * Math::BigFloat->new( $hr->{ $key } );
    }

  return $sum;
}

sub s_sum_xy
{
  my $data = shift;
  my $x    = shift;
  my $y    = shift;

  my $sum = Math::BigFloat->new();
  for my $hr ( @$data )
    {
    $sum += Math::BigFloat->new( $hr->{ $x } ) * Math::BigFloat->new( $hr->{ $y } );
    }

  return $sum;
}

###############################################################################

sub str_hex
{
  my $text = shift;
  $text =~ s/(.)/sprintf("%02X", ord($1) )/ges;
  return $text;
}

sub str_unhex
{
  my $text = shift;
  $text =~ s/([0-9A-F][0-9A-F])/chr(hex($1))/ges;
  return $text;
}

sub time_diff_in_words
{
  my $td = abs( int( shift() ) ); # time difference in seconds

  if( $td < 1 )
    {
    return "now";
    }
  if( $td < 60   )
    {
    my $s_str = countables_str( $td, "second", "seconds" );
    return "$td $s_str";
    };
  if( $td < 60*60 )
    {
    my $m = int( $td / 60 );
    my $m_str = countables_str( $m, "minute", "minutes" );
    return "$m $m_str";
    };
  if( $td < 2*24*60*60 )
    {
    my $h = int( $td / ( 60 * 60 ) );
    my $m = int( $td % ( 60 * 60 ) / 60 );
    my $h_str = countables_str( $h, "hour",   "hours"   );
    my $m_str = countables_str( $m, "minute", "minutes" );
    return "$h $h_str, $m $m_str";
    };
  if( $td < 7*24*60*60 )
    {
    my $d = int( $td / ( 24 * 60 * 60 ) );
    my $h = int( $td % ( 24 * 60 * 60 ) / ( 60 * 60 ) );
    my $d_str = countables_str( $d, "day",    "days"    );
    my $h_str = countables_str( $h, "hour",   "hours"   );
    return "$d $d_str, $h $h_str";
    };
  if( $td < 60*24*60*60 )
    {
    my $d = int( $td / ( 24 * 60 * 60 ) );
    my $d_str = countables_str( $d, "day",    "days"    );
    return "$d $d_str";
    };
  if( 4 )
    {
    my $m = int( $td / ( 30*24*60*60 ) );
    my $m_str = countables_str( $m, "month", "months" );
    return "$m $m_str";
    }
}

sub time_diff_in_words2
{
  my $td = int( shift() ); # time difference in seconds

  my $str = time_diff_in_words( $td );

  if( $td < 0 )
    {
    return "in $str";
    }
  elsif( $td > 0 )
    {
    # FIXME: bg trans for 'ago'!!!!!!!!
    return "$str ago";
    }
  else
    {
    return $str;
    }
}

sub countables_str
{
  my $cnt  = shift;
  my $one  = shift;
  my $many = shift;

  return $cnt == 1 ? $one : $many;

}

sub print_term_table
{
  my $table = shift;

  my @width;

  # calc widths
  for my $row ( @$table )
    {
    for my $i ( 0 .. scalar( @$row ) - 1 )
      {
      my $w = length $row->[ $i ];
      $width[ $i ] = $w if $w > $width[ $i ];
      }
    }

  # print
  for my $row ( @$table )
    {
    for my $i ( 0 .. scalar( @$row ) - 1 )
      {
      my $w = $width[ $i ];
      my $d = $row->[ $i ];
      print sprintf " %-${w}s |", $d;
      }
    print "\n";
    }

}

###EOF#########################################################################
