#!/usr/bin/perl
use POSIX qw(mktime strftime);
#use Data::Dumper;
use strict;

#########################################################################
#                             Options
#   Busy histogram scale in kbytes
my $max = 2;
#   0 - compact sessions graph, 1 - T-Hist style graph, compact graph otherwise
my $group = 2;
#
#########################################################################

my ($stat1, $stat2, $graph1, $graph2, $bad, $no_stat, $log);
my (@out, @in, @out2, @in2, @bad);
my %node;
my $usage = <<EOL
binkdstat - binkd statistic generator v1.9.0 (C) Dmitry V. Kolvakh, 2:5054/89
            based on binkdstat v1.22, (c)opyright by val khokhlov

    binkdstat [-l <log>] [-s <start>|- <period>|-] [-g <day>] [-b] [-n]
       -l <log>, --log=<log>                           use binkd.log <log>
       -s <start> <period>, --stat=<start>,<period>    set period for summary
       -g <day>, --graph=<day>                         set day to draw a graph
       -m <mode>                                       session graph style (0-2)
       -b, --bad                                       display failures table
       -n, --nostat                                    don't display tables

    If <log> isn't specified, stdin is used
    Date/time consists of token(s): [+-]<NN>[hdwmy]
       use 15x to set value to 15 (h - hour, d - day, m - month, y - year)
       use +2d to advance day forward by 2, -6d to advance day backward by 6
       use -1w to set date to Monday of previous week, +1w - next week
       (if letter [hdwmy] is omitted 'd' is assumed)
    <start> and/or <period> can be '-' not to limit corresponding parameter
    <period> for --stat is relative to the <start> date/time (or current day)
    <day> for --graph is relative to the day after --stat (or current day)

    Examples:
       binkdstat -s -1w +7d            statistics for the previous week
       binkdstat -s -1 +1 -g -1        stats and graph for yesterday
EOL
;
# --------------------------------------------------------------------
# parse binkd log: parse_log($dt_start, $dt_finish)
sub parse_log {
    my %MON = (
        Jan => 0,
        Feb => 1,
        Mar => 2,
        Apr => 3,
        May => 4,
        Jun => 5,
        Jul => 6,
        Aug => 7,
        Sep => 8,
        Oct => 9,
        Nov => 10,
        Dec => 11
    );
    my $thrsh = 30 * 60; # threshold
    my ($dt_start, $dt_finish, $gr_start, $gr_finish) = @_;
    $dt_start = 0 if !defined $dt_start;
    $dt_finish = 0x7fffffff if !defined $dt_finish;
    unless(defined $gr_start) {
        $gr_start = 0x7fffffff;
        $gr_finish = 0;
    }
    my $Y = (localtime)[5];
    my %data; my $found = 0; my $cur = 0;
    if (defined $log) {
        open F, $log or die "fatal: can't open log file $log";
    } else { *F = *STDIN; }
    while (<F>) {
        study $_;
        my ($day, $mon, $h, $m, $s, $pid, $cmd) = /^..(..).(...).(..):(..):(..).\[(\d+)\].(.*)/ or next;
        my $dt = mktime($s, $m, $h, $day, $MON{$mon}, $Y, -1, -1, -1);
        if ($dt_start == 0 && !defined $stat1) { $stat1 = $dt; }
        if ($dt_finish == 0x7fffffff) { $stat2 = $dt; }
        next if ($dt < $dt_start - $thrsh);
        last if ($dt > $dt_finish && $cur <= 0);
        study $cmd;
        if ($cmd =~ /^(incoming|outgoing) session with/o && $dt <= $dt_finish) {
            my $ip;
            my $domain;
            if ($1 eq "incoming") {
                ($ip) = $cmd =~ /^incoming session with ([^\s]+)/o;
                $data{$pid}{'incoming'} = 1;
            } else {
                ($domain, $ip) = $cmd =~ /^outgoing session with ([^\s]+) \[([^\s]+)\]/o;
            }
            $data{$pid}{'sthh'} = $h*2 + ($m > 29);
            $data{$pid}{'stqh'} = $h*4 + int($m / 15);
            $data{$pid}{'stdt'} = $dt;
            $data{$pid}{'ip'} = $ip;
            $cur++;
        }
        elsif ($cmd =~ /^call to/o) {
            my ($addr) = $cmd =~ /^call to ([^\@\s]+)/o;
            $addr =~ s/\.0+$//o;
            $data{$pid}{'addr'} = $addr;
        }
        # assure defined $data{$pid}
        elsif (!defined $data{$pid}) {
            next;
        }
        elsif ($cmd =~ /^trying/o && $dt <= $dt_finish) {
            my ($domain, $ip) = $cmd =~ /^trying ([^\@\s]+) \[([^\@\s]+)\]/o;
            $data{$pid}{'ip'} = $ip;
        }
        elsif ($cmd =~ /^connected/o) {
            $node{$data{$pid}{'addr'}}{'call'}++;
        }
        elsif ($cmd =~ /^connection to/o) {
            my ($addr, $reason) = $cmd =~ /^connection to ([\d\:\/]+)\@[\w\.]+ failed: (.*)/o;
            $reason =~ tr/[A-Z]/[a-z]/;
            $data{$pid}{'addr'} = $addr;
            $node{$addr}{'call'}++;
            $node{$addr}{'to_failed'}++;
            put_bad(\@bad, $data{$pid}{'ip'}, $data{$pid}{'addr'}, $reason, $pid);
            $data{$pid}{'bad'} = 1;
        }
        # $data{$pid} defined
        elsif ($cmd =~ /pwd protected/o) { $data{$pid}{'pwd'} = 1; }
        elsif ($cmd =~ /^(?:rcvd|sent): /o) {
            my ($sz, $cps) = $cmd =~ /(\d+), (\d+\.\d+) CPS/o;
            if (defined $cps) { $data{$pid}{'xcps'} += $cps * $sz; }
        }
        elsif ($dt >= $dt_start && $cmd =~ /^done/o) {
            my ($cmd2, $st, $sf, $rf, $sb, $rb) = $cmd =~ /^done \(([^,]+), (\S+), S\/R: (\d+)\/(\d+) \((\d+)\/(\d+)/o;
            my ($dir, $addr) = $cmd2 =~ /(to|from) ([^\@\s]+)\S*/o;
            if ($dir ne 'from' && $dir ne 'to' && $data{$pid}{'incoming'}) {
                $dir = 'from';
            }
            if ($st ne 'OK') {
                $st = 'failed';
            }
            $addr =~ s/\.0+$//o;
            my $w = $h*2 + ($m > 29);
            if ($data{$pid}{'stdt'} < $graph2 && $dt > $graph1) {
                put_traf($data{$pid}{'stdt'} < $graph1 ? 0 : $data{$pid}{'sthh'},
                    $dt > $graph2 ? 0 : $w,
                    $sb, $rb, $data{$pid}{'pwd'} ? (\@out, \@in) : (\@out2, \@in2));
                unless(exists $node{$addr}{'s_hist'}) {
                    $node{$addr}{'s_hist'} = [];
                    $node{$addr}{'s_num'} = 0;
                }
                my $stqh = $data{$pid}{'stqh'};
                my $fnqh = $h*4 + int($m / 15);
                for(my $qh = $stqh; $qh <= $fnqh; $qh++) {
                    ${$node{$addr}{'s_hist'}}[$qh] = 1;
                }
                $node{$addr}{'s_num'}++;
            }
            $node{$addr}{'sb'} += $sb; $node{$addr}{'rb'} += $rb;
            $node{$addr}{'sec'} += $dt - $data{$pid}{'stdt'};
            $node{$addr}{ $data{$pid}{'pwd'} ? 'pwok' : 'unpw' }++;
            $node{$addr}{'last'} = ($st eq 'OK') ? 1 : 0;
            $node{$addr}{"${dir}_${st}"}++;
            if ($sb + $rb > 0) {
                $node{$addr}{'xcps'} += $data{$pid}{'xcps'};
            }
            if ($st eq 'failed' && !$data{$pid}{'bad'}) {
                put_bad(\@bad, $data{$pid}{'ip'}, $addr, defined $addr ? 'unknown' : 'connection failure', $pid);
            }
            undef $data{$pid}; $cur--;
        }
        elsif ($cmd =~ /^addr/) {
            my ($addr, $st) = $cmd =~ /^addr: ([^\@\s]+)[^(]*(\([^)]+)?/o;
            $addr =~ s/\.0+$//o;
            $data{$pid}{'addr'} = $addr unless defined $data{$pid}{'addr'};
            if ($st =~ /not from allowed remote (?:address|IP)/o) {
                put_bad(\@bad, $data{$pid}{'ip'}, $addr, 'not from allowed remote address', $pid);
                $data{$pid}{'bad'} = 1;
            }
        }
        elsif ($cmd =~ /^no AKAs in common domains or all AKAs are busy$/o) {
            put_bad(\@bad, $data{$pid}{'ip'}, $data{$pid}{'addr'}, 'remote is busy', $pid);
            $data{$pid}{'bad'} = 1;
        }
        elsif ($cmd =~ /remote has no such AKA$/o) {
            my ($addr, $st) = $cmd =~ /^called\s+([^,]+)/o;
            $addr =~ s/\.0+$//o;
#            put_bad(\@bad, $data{$pid}{'ip'}, $addr, 'no such AKA on remote; actual is '.$data{$pid}{'addr'}, $pid);
            put_bad(\@bad, $data{$pid}{'ip'}, $addr, 'no such AKA on remote', $pid);
            $data{$pid}{'ignore'} = 1;
        }
        elsif ($cmd =~ /: (?:Bad|incorrect) password$/o) {
            put_bad(\@bad, $data{$pid}{'ip'}, $data{$pid}{'addr'}, 'incorrect password', $pid);
            $data{$pid}{'bad'} = 1;
        }
        elsif ($cmd =~ /^skipping/o) {
            my ($type, $mask) = $cmd =~ /(\w+)-?mask\s+(\S+)\)$/;
            if (defined $mask) {
                put_bad(\@bad, $data{$pid}{'ip'}, $data{$pid}{'addr'}, "skipped by ${type}mask $mask", $pid);
            }
        }
        elsif ($cmd =~ /^bad pkt addr/o) {
            my ($addr, $st) = $cmd =~ /^bad pkt addr: ([^\@\s]+)/o;
            $addr =~ s/\.0+$//o;
            put_bad(\@bad, $data{$pid}{'ip'}, $data{$pid}{'addr'}, "bad pkt sender: $addr", $pid);
        }
        elsif ($cmd =~ /^cannot rename/o) {
            put_bad(\@bad, $data{$pid}{'ip'}, $data{$pid}{'addr'}, 'local filesystem problems', $pid);
            $data{$pid}{'bad'} = 1;
        }
        elsif ($cmd =~ /^recv: /o) {
            my ($reason) = $cmd =~ /^recv: (.*)/o;
            $reason =~ tr/[A-Z]/[a-z]/;
            put_bad(\@bad, $data{$pid}{'ip'}, $data{$pid}{'addr'}, $reason, $pid);
            $data{$pid}{'bad'} = 1;
        }
        elsif ($cmd eq 'timeout!') {
            put_bad(\@bad, $data{$pid}{'ip'}, $data{$pid}{'addr'}, 'connection timed out', $pid);
            $data{$pid}{'bad'} = 1;
        }
    }
    close F;
    return $found;
}

# --------------------------------------------------------------------
# bytes to string nice conversion: traf2str(@arg)
sub traf2str {
    my $s = '';
    for my $c (@_) {
        if ($c < 1000) { $s .= sprintf "%7d ", $c; }
        elsif ($c < 100000) { $s .= sprintf "%3d,%03d ", int($c / 1000), $c%1000; }
        elsif ($c < 1000*1024) { $s .= sprintf "%7dK", int($c / 1024); }
        elsif ($c < 100000*1024) { $s .= sprintf "%3d,%03dK", int($c/1024 / 1000), int($c/1024 % 1000); }
        else { $s .= sprintf "%7dM", int($c/1024/1024); }
    }
    return $s;
}

# --------------------------------------------------------------------
# cps to string nice conversion: cps2str(@arg)
sub cps2str {
    my ($c) = @_;
    $c = int($c + 0.5);
    return ' -- ' if ($c <= 0);
    return sprintf "%4d", $c if ($c <= 9999);
    return sprintf "%3dK", int($c / 1024) if ($c <= 999*1024);
    return sprintf "%3dM", int($c / 1024 / 1024);
    return sprintf "%3dG", int($c / 1024 / 1024 / 1024);
}

# --------------------------------------------------------------------
# summary: out_summary(\%node)
sub out_summary {
    my @out; my @outa; my %tot; my $n = 0; my $xcps = 0;
    while ( my ($addr, $rec) = each %{$_[0]} ) {
        $n++ if $addr;
        my ($s, $pwd, $last); my $cps = 0;
        my $secs = $rec->{'sec'} % 60;
        my $mins = int($rec->{'sec'} / 60) % 60;
        my $hours = int($rec->{'sec'} / 60 / 60);
        if ($rec->{'pwok'} && !$rec->{'unpw'}) { $pwd = '*'; }
        elsif ($rec->{'unpw'} && !$rec->{'pwok'}) { $pwd = ' '; }
        else { $pwd = '?'; }
        $last = $rec->{'last'} ? 'ú' : '+';
        if ($rec->{'sec'} > 0) { $cps = ($rec->{'rb'}+$rec->{'sb'}) / $rec->{'sec'}; }
        if ($rec->{'xcps'}) { $rec->{'xcps'} /= $rec->{'rb'}+$rec->{'sb'}; }
        $s = sprintf "º%s%4d %3d %4d %4d %3d³%s%-15s³%02d:%02d:%02d³%s³%s³%sº",
                 $pwd,
                 $rec->{'from_OK'}, $rec->{'from_failed'}, $rec->{'call'}, $rec->{'to_OK'}, $rec->{'to_failed'},
                 $last, ($addr||'failure'), $hours, $mins, $secs,
                 traf2str($rec->{'rb'}, $rec->{'sb'}),
                 cps2str($rec->{'xcps'}), cps2str($cps);
        if ($rec->{'xcps'} > 0) { $xcps += $rec->{'xcps'}*($rec->{'sb'}+$rec->{'rb'}); }
        # insert into sorted out
        my ($Z,$N,$F,$P) = $addr =~ /^(\d+):(\d+)\/(\d+)(?:\.(\d+))?$/o;
        for (my $i = 0; $i <= @outa; $i++) {
            if ($i == @outa) { push @out, $s; push @outa, $addr; last; }
            my ($z,$n,$f,$p) = $outa[$i] =~ /^(\d+):(\d+)\/(\d+)(?:\.(\d+))?$/o;
#            my $less = ($Z < $z) || ($Z == $z && $N < $n)
#                     || ($Z == $z && $N == $n && $F < $f)
#                     || ($Z == $z && $N == $n && $F == $f && $P < $p);
            my $less = $addr le $outa[$i];
            if ($less) { splice @out, $i, 0, $s; splice @outa, $i, 0, $addr; last; }
        }
        # add to totals
        for my $s (qw'sec from_OK from_failed call to_OK to_failed sb rb') {
            $tot{$s} += $rec->{$s};
        }
    }
    # hdr
    my @hdr = (
        'Summary link statistics',
#        '('.strftime("%a, %d %b %H:%M:%S", localtime $stat1)." - ".strftime("%a, %d %b %H:%M:%S", localtime $stat2).')',
        '('.e_strftime($stat1)." - ".e_strftime($stat2).')',
        '',
        " ÚÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ Password: '*' - Present, ' ' - Absent, '?' - Error",
        " ³                       Ú Last Session Result: 'ú' - Success, '+' - Aborted",
        'É'.('Í'x23).'Ñ'.('Í'x16).'Ñ'.('Í'x8).'Ñ'.('Í'x16).'Ñ'.('Í'x9).'»',
        'ºP   In       Out       ³L    Address    ³  Time  ³  Bytes  Bytes  ³   CPS   º',
        'º   ok err  call  ok err³                ³ Online ³received  sent  ³Xfer³Efctº',
        'Ç'.('Ä'x23).'Å'.('Ä'x16).'Å'.('Ä'x8).'Å'.('Ä'x16).'Å'.('Ä'x4).'Å'.('Ä'x4).'¶'
    );
    $hdr[0] = (' 'x(39 - length($hdr[0]) / 2)).$hdr[0];
    $hdr[1] = (' 'x(39 - length($hdr[0]) / 2)).$hdr[1];
    splice @out, 0, 0, @hdr;
    # totals
    push @out, 'Ç'.('Ä'x23).'Å'.('Ä'x16).'Å'.('Ä'x8).'Å'.('Ä'x16).'Å'.('Ä'x4).'Å'.('Ä'x4).'¶';
    my $secs = $tot{'sec'} % 60;
    my $mins = int($tot{'sec'} / 60) % 60;
    my $hours = int($tot{'sec'} / 60 / 60);
    my $cps = 0;
    if ($tot{'sec'} > 0) { $cps = ($tot{'rb'}+$tot{'sb'}) / $tot{'sec'}; }
    if ($tot{'sb'}+$tot{'rb'} > 0) { $xcps /= $tot{'sb'} + $tot{'rb'}; } else { $xcps = 0; }
    push @out, sprintf "º %4d %3d %4d %4d %3d³ Stations: %4d ³%02d:%02d:%02d³%s³%s³%sº",
                 $tot{'from_OK'}, $tot{'from_failed'}, $tot{'call'}, $tot{'to_OK'}, $tot{'to_failed'},
                 $n, $hours, $mins, $secs,
                 traf2str($tot{'rb'}, $tot{'sb'}), cps2str($xcps), cps2str($cps);
    push @out, 'È'.('Í'x23).'Ï'.('Í'x16).'Ï'.('Í'x8).'Ï'.('Í'x16).'Ï'.('Í'x4).'Ï'.('Í'x4).'¼';
    return @out;
}

# --------------------------------------------------------------------
# put traffic into hourly records
#   put_traf($start_hh, $end_hh, $sent, $recv, \@out, \@in)
sub put_traf {
    my ($sthh, $ehh, $sb, $rb, $out, $in) = @_;
    $ehh += 48 if ($ehh < $sthh);
    my $n = $ehh - $sthh;
    for (my $i = 0; $i <= $n; $i++) {
        $out->[($sthh+$i) % 48] += $sb / ($n+1);
        $in->[($sthh+$i) % 48]  += $rb / ($n+1);
    }
}

# --------------------------------------------------------------------
# traffic graph: out_graph(\@out,\@in,\@out_unsec,\@in_unsec)
sub out_graph {
    my $MAX = $max * 1024 / 8 * 60 * 30;
    my $trans = 100 / 16;
    my @symb = ('Û', '²', '±', '°', '.');
    my (@out, $l, $h, $i);
    my ($title, @traf) = @_;
    # graph
    for ($l = 15; $l >= 0; $l--) {
        if (($l+1) % 4 == 0) {
            my $x = ($l + 1) * $max / 16;
            my $y = $x > 10 ? "%3.0fk" : "%3.1fk";
            $out[15 - $l] = sprintf "$y \xB4", ($l + 1) * $max / 16;
        }
        else { $out[15-$l] = "     ³"; }
        for ($h = 0; $h < 48; $h++) {
            my $c = 0;
            for ($i = 0; $i < @symb; $i++) {
                if ($i == @symb-1) { $out[15 - $l] .= $symb[-1]; last; }
                $c += $traf[$i]->[$h];
                if ($c/$MAX > $l / 16) { $out[15 - $l] .= $symb[$i]; last; }
            }
        }
    }
    $out[16] = "   0 Å".("ÄÂ"x23)."\xC4\xBF";
#    $out[17] = "     "; for (my $h = 0; $h < 24; $h++) {
#        if ($h < 10) { $out[17] .= $h.' '; } elsif ($h % 2) { $out[17] .= ' '.$h.' '; }
#    }
    $out[17] = "  ";
    for (my $h = 0; $h <= 24; $h+= 2) {
        $out[17] .= sprintf("%4d", $h);
    }
    # table
    $l = 1;
    for ($i = 0; $i < @symb-1; $i++) {
        my $s = ' ÚÄ['.$symb[$i].'] '.$title->[$i].' ';
        $s .= 'Ä'x(22 - length($s));
        $out[$l++] .= $s.'¿';
        my $c = 0;
        for ($h = 0; $h < 48; $h++) { $c += $traf[$i]->[$h]; }
        $out[$l++] .= " ³ traffic : ".traf2str($c)." ³";
        $out[$l++] .= " ³ avg.load: ".sprintf("%7.02f", 8 * $c / $max / 1024 / 60 / 60 / 24 * 100)."% ³";
        $out[$l++] .= " ÀÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÙ";
    }
    # hdr
#    my $s = 'Station busy graph for '.strftime("%a, %d %b %Y", localtime $graph1);
    my $s = 'Station busy graph for '.e_strftime($graph1, 'short');
    $s = (' 'x(6 + 24 - length($s) / 2)).$s;
    splice @out, 0, 0, ($s, '');
    return @out;
}
# --------------------------------------------------------------------
# node time graph: out_node_graph()
sub out_node_graph {
    my @out;
    my @nodes = sort keys %node;
    my $l = 0;
    my %table = ('00' => " ", '01' => "\xDE", '10' => "\xDD", '11' => "\xDB");
    my $lf = 5;
    my $digits = ' ' x ($lf-3);
    for(my $i = 0; $i <= 24; $i += 2){$digits .= sprintf("%4d", $i);}
    my $sep = (' ' x $lf)."\xC3".("\xC4\xC4\xC4\xC5" x 11)."\xC4\xC4\xC4\xB4";
    my $tit = 'Sessions history graph for '.e_strftime($graph1, 'short');
    my $net_last;
    $out[$l++] = '';
    $out[$l++] = (' ' x ($lf + 24 - length($tit) / 2)).$tit;
    $out[$l++] = '';
    $out[$l++] = $digits;
    $out[$l++] = (' Ses ')."\xC3".("\xC4\xC1\xC4\xC5" x 11).("\xC4\xC1\xC4\xB4");
    foreach my $addr(@nodes){
        next unless $node{$addr}{'s_hist'};
        my ($net_current) = $addr =~ /(([^\/]+))/o;
        $net_current = 'failure' if $addr eq '';
        if (length($net_last) > 0) {
            $out[$l++] .= $sep if $group == 1 || ($group == 2 && $net_last ne $net_current);
        }
#        $out[$l] = (' ' x $lf);
        $out[$l] = sprintf("%4d ", $node{$addr}{'s_num'});
        $out[$l] .= ${$node{$addr}{'s_hist'}}[0] ? "\xDE" : "\xB3";
        for(my $i = 1; $i < 24 * 4 - 2; $i += 2){
            my $ch = ($node{$addr}{'s_hist'}[$i] ? '1':'0').($node{$addr}{'s_hist'}[$i+1] ? '1':'0');
            if($ch eq '00' and (($i+1) % 8 == 0)) {$out[$l] .= "\xB3";}
            else {$out[$l] .= $table{$ch};}
        }
        $out[$l] .= $node{$addr}{'s_hist'}[24 * 4 - 1] ? "\xDD" : "\xB3";
        $out[$l] .= sprintf(" %s", $addr||'failure');
        $l++;
        $net_last = $net_current;
    }
    $out[$l++] = (' ' x $lf)."\xC3".("\xC4\xC2\xC4\xC5" x 11).("\xC4\xC2\xC4\xB4");
    $out[$l++] = $digits;
    return @out;
}

# --------------------------------------------------------------------
# put bad report to list: put_bad(\@bads, $ip, $addr, $reason)
sub put_bad {
    my ($a, $ip, $addr, $reason, $pid) = @_;
    my $i;
    for ($i = 0; $i < @$a; $i++) {
        last if ($a->[$i][1] eq $ip)
            && ($a->[$i][2] eq $addr)
            && ($a->[$i][3] eq $reason);
    }
    if ($i == @$a) {
        if (!$ip) {
            $ip = "unknown";
        }
        push @$a, [1, $ip, $addr, $reason];
    } else {
        $a->[$i][0]++;
    }
}

# --------------------------------------------------------------------
# bad list
sub out_bad {
    my (@out, @outc, $s);
    my $count = 0;
    my %uniq;

    for my $elem (@bad) {
        my ($cnt, $ip, $addr, $reason) = @$elem;
        next if $addr ne '';
        if (length($ip) > 18) {
            $ip = ".".substr($ip, length($ip) - 18);
        }
        $reason = 'unknown' if $reason eq '0';
        if (length $reason > 36) {
            substr $reason, 33, length($reason) - 33, '...';
        }
        push @out, sprintf "º%4d³%19s³ %-13s³ %-35sº", $cnt, $ip, 'unknown', $reason;
        $count+=$cnt;
        $uniq{'addr'}{$addr}++;
        $uniq{'ip'}{$ip}++;
        $uniq{'reason'}{$reason}++;
    }

    @bad = sort { $b->[0] <=> $a->[0] } @bad;
    for my $elem (@bad) {
        my ($cnt, $ip, $addr, $reason) = @$elem;
        next if $addr eq '';
        if (length($ip) > 18) {
            $ip = ".".substr($ip, length($ip) - 18);
        }
        $reason = 'unknown' if $reason eq '0';
        if (length $reason > 36) { substr $reason, 33, length($reason) - 33, '...'; }
        push @out, sprintf "º%4d³%19s³ %-13s³ %-35sº", $cnt, $ip, $addr, $reason;
        $count+=$cnt;
        $uniq{'addr'}{$addr}++;
        $uniq{'ip'}{$ip}++;
        $uniq{'reason'}{$reason}++;
    }
    my @hdr = (
        'Session failures and problems',
#        '('.strftime("%a, %d %b %H:%M:%S", localtime $stat1)." - ".strftime("%a, %d %b %H:%M:%S", localtime $stat2).')',
        '('.e_strftime($stat1)." - ".e_strftime($stat2).')',
        'É'.('Í'x4).'Ñ'.('Í'x19).'Ñ'.('Í'x14).'Ñ'.('Í'x36).'»',
        'º ## ³    IP address     ³    Address   ³           Reason                   º',
        'Ç'.('Ä'x4).'Å'.('Ä'x19).'Å'.('Ä'x14).'Å'.('Ä'x36).'¶'
    );
    $hdr[0] = (' 'x(39 - length($hdr[0]) / 2)).$hdr[0];
    $hdr[1] = (' 'x(39 - length($hdr[0]) / 2)).$hdr[1];
    splice @out, 0, 0, @hdr;
    push @out, 'Ç'.('Ä'x4).'Å'.('Ä'x19).'Å'.('Ä'x14).'Å'.('Ä'x36).'¶';
    push @out, sprintf "º%4d³ %18s³ %13s³%36sº", $count, scalar(%{$uniq{'ip'}}), scalar(%{$uniq{'addr'}}), scalar(%{$uniq{'reason'}});
    push @out, 'È'.('Í'x4).'Ï'.('Í'x19).'Ï'.('Í'x14).'Ï'.('Í'x36).'¼';
    return @out;
}

# --------------------------------------------------------------------
# ersatz of POSIX::strftime e_strftime($time[, $short])
sub e_strftime {
    my ($time, $short) = @_;
    my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime($time);
    my %weekdays = (
        0 => 'Sun',
        1 => 'Mon',
        2 => 'Tue',
        3 => 'Wed',
        4 => 'Thu',
        5 => 'Fri',
        6 => 'Sat'
    );
    my %months = (
        0 => 'Jan',
        1 => 'Feb',
        2 => 'Mar',
        3 => 'Apr',
        4 => 'May',
        5 => 'Jun',
        6 => 'Jul',
        7 => 'Aug',
        8 => 'Sep',
        9 => 'Oct',
        10 => 'Nov',
        11 => 'Dec'
    );
    if ($short) {
        return sprintf("%s, %d %s %d", $weekdays{$wday}, $mday, $months{$mon}, $year + 1900);
    } else {
        return sprintf("%s, %d %s %02d:%02d:%02d", $weekdays{$wday}, $mday, $months{$mon}, $hour, $min, $sec);
    }
}

# --------------------------------------------------------------------
# convert string to datetime: str2time($s[, $base])
sub str2time {
    my ($s, $base) = @_;
    $base = time if !defined $base;
    my ($h, $d, $m, $y, $w) = (localtime $base)[2..6];
    $w = 7 if $w == 0;
    $h = 0 unless $s =~ /[Hh]/o;
    while (length $s > 0) {
        my @a = $s =~ /^([+-]?)(\d+)([hHdDwWmMyY])?/o or return undef;
        substr $s, 0, length(join '', @a), '';
        $a[2] = 'd' if !defined $a[2];
        if (lc $a[2] eq 'y') {
            if ($a[0] eq '-') { $y -= $a[1]; }
            elsif ($a[0] eq '+') { $y += $a[1]; }
            elsif ($a[1] < 1900) { $y = $a[1] + 100; }
            else { $y = $a[1] - 1900; }
        }
        elsif (lc $a[2] eq 'm') {
            $m = $a[0] eq '-' ? $m - $a[1] : $a[1] - 1;
            if ($a[0] eq '-') { $m -= $a[1]; }
            elsif ($a[0] eq '+') { $m += $a[1]; }
            else { $m = $a[1] - 1; }
        }
        elsif (lc $a[2] eq 'w') {
            if ($a[0] eq '-') { $d -= $w + 7 * $a[1] - 1; $w = 1; }
            elsif ($a[0] eq '+') { $d += 7 * $a[1] - $w + 1; $w = 1; }
            else { return undef; }
        }
        elsif (lc $a[2] eq 'd') {
            if ($a[0] eq '-') { $d -= $a[1]; }
            elsif ($a[0] eq '+') { $d += $a[1]; }
            else { $d = $a[1]; }
        }
        elsif (lc $a[2] eq 'h') {
            if ($a[0] eq '-') { $h -= $a[1]; }
            elsif ($a[0] eq '+') { $h += $a[1]; }
            else { $h = $a[1]; }
        }
    }
    return mktime(0, 0, $h, $d, $m, $y, $w, -1, -1);
}

# --------------------------------------------------------------------
# parse command line
sub parse_cmdline {
    my $i = 0;
    my ($dts, $dtf, $dtg);
    my $n = @ARGV;
    while ($i < $n) {
        $_ = lc $ARGV[$i];
        if ($_ eq '-s') {
            die "use: -s <start-time> <end-time>\n" unless $i + 2 < $n;
            ($dts, $dtf) = @ARGV[$i + 1, $i + 2];
            $i += 3;
        }
        elsif (/^--stat/o) {
            $i++;
            ($dts, $dtf) = /^--stat=([^,]+),(.+)$/o or die "use: --stat=<start-time>,<end-time>\n";
        }
        elsif ($_ eq '-g') {
            die "use: -g <time>\n" unless $i++ < $n;
            $dtg = $ARGV[$i++];
        }
        elsif ($_ eq '-m') {
            die "use: -m <mode>\n" unless $i++ < $n;
            $group = $ARGV[$i++];
        }
        elsif (/^--graph/o) {
            $i++;
            ($dtg) =~ /^--graph=(.+)$/o or die "use: --graph=<time>\n";
        }
        elsif ($_ eq '--bad' || $_ eq '-b') { $bad = 1; $i++; }
        elsif ($_ eq '--nostat' || $_ eq '-n') { $no_stat = 1; $i++; }
        elsif ($_ eq '-l') {
            die "use: -l <binkd-log>\n" unless $i++ < $n;
            $log = $ARGV[$i++];
        }
        elsif (/^--log/o) {
            ($log) = $ARGV[$i++] =~ /^--log=(.+)/io or die "use: --log=<binkd-log>";
        }
        elsif (/^(--help|-[h?])/) { print $usage; exit 0; }
        else { die "unknown parameter: $ARGV[$i]\n"; }
    }
    # init values
    if (defined $dts && $dts ne '-') {
        $stat1 = str2time($dts) or die "wrong format of time: '$dts'\n";
    }
    if (defined $dtf && $dtf ne '-') {
        $stat2 = str2time($dtf, $stat1) or die "wrong format of time: '$dtf'\n";
    }
    if (defined $stat1 && defined $stat2) {
        die "<end-time> should be later than <start-time>\n" if ($stat2 < $stat1);
    }
    if (defined $dtg && $dtg ne '-') {
        $graph1 = str2time($dtg, $stat2) or die "wrong format of time: '$dtg'\n";
        $graph2 = $graph1 + 60 * 60 * 24 - 1; # 23:59:59
    }
}

parse_cmdline @ARGV;
parse_log($stat1, $stat2, $graph1, $graph2);
# graph
print "\x01CHRS: CP866 2\n";
if (defined $graph1) {
    print "\n";
    print join "\n", out_graph(['incoming', 'incoming unsec', 'outgoing', 'outgoing unsec'], \@in, \@in2, \@out, \@out2);
    print "\n";

    print "\n";
    print join "\n", out_node_graph();
    print "\n";
}
# stat
unless($no_stat){
    print "\n"; print join "\n", out_summary(\%node); print "\n";
}
# bad
if ($bad) {
    print "\n";
    print join "\n", out_bad();
    print "\n";
}
