#!/usr/bin/perl

use strict;
use warnings;
use Cwd ();
use POSIX ();

my %opts;
my @logs;
my $selected_log;
my %months = (
    Jan => 1, Feb => 2, Mar => 3, Apr => 4, May => 5, Jun => 6,
    Jul => 7, Aug => 8, Sep => 9, Oct => 10, Nov => 11, Dec => 12,
);
my $now;
my $columns;

get_opts();
get_logs();
select_log();

if ($opts{action} eq "list") {
    list();
}
elsif ($opts{action} eq "info") {
    show_info();
}
elsif ($opts{action} eq "print") {
    print_log_file();
}
elsif ($opts{action} eq "tailf") {
    show_tailf();
}
elsif ($opts{action} eq "vim") {
    vim_log();
}
elsif ($opts{action} eq "graph") {
    show_graph();
}
elsif (exists $opts{last}) {
    show_last();
}
else {
    show_log();
}

sub myglob {
    my ($file) = @_;
    my $dir = dirname($file);
    opendir my $dh, $dir or return ();
    my @array;
    for my $dfile (readdir $dh) {
        next if $dfile =~ /^\.\.?$/;
        if ("$dir/$dfile" =~ /^\Q$file\E./) {
            push @array, "$dir/$dfile";
        }
    }
    closedir $dh;
    @array = natural_sort(@array);
    return @array;
}

sub natural_sort {
    my @array;
    for my $entry (@_) {
        my $entry2 = $entry;
        $entry2 =~ s/(\d+)/sprintf("%05d", $1)/ge;
        push @array, [$entry, $entry2];
    }
    @array = sort {$a->[1] cmp $b->[1]} @array;
    return map {$_->[0]} @array;
}

sub vim_log {
    log_file();
    system "vim", $selected_log->{file2};
}

sub print_log_file {
    log_file();
    print "$selected_log->{file2}\n";
}

sub show_tailf {
    log_file();
    system "tail", "-f", $selected_log->{file2};
}

sub show_log {
    log_file();
    if (!-t STDOUT) {
        system "cat", $selected_log->{file2};
    }
    elsif ($ENV{PAGER}) {
        system "$ENV{PAGER} $selected_log->{file2}";
    }
    else {
        system "less", "-RIMS", $selected_log->{file2};
    }
}

sub show_graph {
    log_file();
    my @cmd = ("cat", $selected_log->{file2});
    if ($selected_log->{file2} =~ /\.gz$/) {
        @cmd = ("gzip -dc $selected_log->{file2}");
    }
    if ($opts{last}) {
        @cmd = ("tail -$opts{last} $selected_log->{file2}");
        if ($selected_log->{file2} =~ /\.gz$/) {
            @cmd = ("gzip -dc $selected_log->{file2} | tail -$opts{last}");
        }
    }
    open my $fh, "-|", @cmd
        or die "Can't open $selected_log->{file2}: $!\n";
    $columns = `tput cols`;
    my $count = 0;
    my $entry;
    my @entries;
    while (my $line = <$fh>) {
        chomp $line;
        my $a = parse_request_line($line, $selected_log);
        if (!$a) {
            die "Couldn't parse: $line\n";
        }
        $count++;
        $a->{date} = parse_date($a->{datestr});
        if (!$entry) {
            $entry = {};
            $entry->{count} = 1;
            $entry->{start} = $a->{date};
        }
        if ($a->{date} > $entry->{start} + $opts{interval}) {
            push @entries, $entry;
            while (1) {
                my $entry2 = {};
                $entry2->{count} = $entry->{count} + 1;
                $entry2->{start} = $entry->{start} + $opts{interval};
                $entry = $entry2;
                if ($a->{date} > $entry->{start} + $opts{interval}) {
                    push @entries, $entry;
                    next;
                }
                last;
            }
        }
        if ($a->{status} =~ /^(\d)/) {
            $entry->{$1}++;
        }
        $entry->{total}++;
    }
    if ($entry) {
        push @entries, $entry;
    }
    my $max = 0;
    for my $entry (@entries) {
        $entry->{total} ||= 0;
        if ($entry->{total} > $max) {
            $max = $entry->{total};
        }
    }
    my $entryp;
    for my $entry (@entries) {
        show_graph_entry($entry, $entryp, $max);
        $entryp = $entry;
    }
    close $fh;
}

sub show_graph_entry {
    my ($entry, $entryp, $max) = @_;
    my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime($entry->{start});
    $mon += 1;
    my $hour2;
    if ($hour == 0) {
        $hour2 = 12;
    }
    elsif ($hour > 12) {
        $hour2 = $hour - 12;
    }
    else {
        $hour2 = $hour;
    }
    $entry->{monday} = "$mon/$mday";
    my $offset = 16;
    if ($opts{interval} >= 24 * 60 * 60) {
        printf "%-5s: ", "$mon/$mday";
    }
    elsif ($opts{interval} >= 60 * 60) {
        if (!$entryp || $entryp->{monday} ne $entry->{monday}) {
            printf "%-5s %2d: ", "$mon/$mday", $hour2;
        }
        else {
            printf "      %2d: ", $hour2;
        }
    }
    else {
        $offset += 3;
        if (!$entryp || $entryp->{monday} ne $entry->{monday}) {
            printf "%-5s %2d:%02d: ", "$mon/$mday", $hour2, $min;
        }
        else {
            printf "      %2d:%02d: ", $hour2, $min;
        }
    }
    my $size = int(($entry->{total} / $max) * ($columns - $offset));
    my $bar = "";
    my $bar2 = "";
    if ($entry->{total} && !$size) {
        $bar2 = "$entry->{total}";
    }
    elsif ($entry->{total}) {
        $bar = "#" x $size;
        my $status = 1;
        my $sum = 0;
        my $loc = 0;
        for my $status (1 .. 5) {
            my $amount = $entry->{$status} or next;
            my $loc2 = int(($sum + $amount) / $entry->{total} * $size);
            my $size2 = $loc2 - $loc;
            if ($size2) {
                my $color = $status == 1 ? 244                            # grey
                          : $status == 2 ? 16 + (0 * 36) + (4 * 6) + (0)  # green
                          : $status == 3 ? 16 + (5 * 36) + (4 * 6) + (0)  # yellow
                          : $status == 4 ? 16 + (5 * 36) + (1 * 6) + (0)  # orange
                                         : 1;                             # red
                my $part = substr($bar, $loc, $size2);
                $bar2 .= "\e[38;5;${color}m$part";
            }
            $loc = $loc2;
            $sum += $amount;
        }
        $bar2 .= "\e[0m $entry->{total}";
    }
    print "$bar2\n";
}

sub show_info {
    log_file();
    my $size = -s $selected_log->{file2};
    $size = human_readable($size);
    print "$selected_log->{file2} $size\n";

    my @cmd = ("tac", $selected_log->{file2});
    if ($selected_log->{file2} =~ /\.gz$/) {
        @cmd = ("gzip -dc $selected_log->{file2} | tac");
    }
    open my $fh, "-|", @cmd
        or die "Can't open $selected_log->{file2}: $!\n";
    my $stats = {};
    my $count = 0;
    while (my $line = <$fh>) {
        $count++;
        chomp $line;
        my $a = parse_request_line($line, $selected_log);
        if (!$a || !$a->{ip}) {
            die "Couldn't parse: $line\n";
        }
        if ($count == 1) {
            $stats->{mintime} = $a->{reqtime};
            $stats->{maxtime} = $a->{reqtime};
        }
        update_stats($stats, $a);
    }
    $stats->{count} = $count;
    close $fh;
    show_stats($stats);
}

sub show_stats {
    my ($stats) = @_;
    my $from = parse_date($stats->{fromstr});
    my $fromstr = datestr($from);
    my $to = parse_date($stats->{tostr});
    my $tostr = datestr($to);
    my $time_diff = time_diff($from, $to);
    my $count = $stats->{count} || 0;

    print "$count requests$time_diff between [$fromstr] and [$tostr]\n";
    if (defined $stats->{bytes}) {
        my $bytes = human_readable($stats->{bytes});
        print "total response size $bytes\n";
    }
    if ($stats->{mintime}) {
        my $mintime = int($stats->{mintime} / 1000);
        print "min serving time $mintime ms\n";
    }
    if ($stats->{maxtime}) {
        my $maxtime = int($stats->{maxtime} / 1000);
        print "max serving time $maxtime ms\n";
    }
    if ($stats->{sumtime}) {
        $stats->{avgtime} = $stats->{sumtime} / $stats->{count};
        $stats->{stddevtime} = sqrt(($stats->{sumtime2} - (($stats->{sumtime} ** 2) / $stats->{count})) / $stats->{count});
        my $avgtime = int($stats->{avgtime} / 1000);
        my $stddevtime = int($stats->{stddevtime} / 1000);
        print "average serving time $avgtime ms ± $stddevtime ms\n";
    }
    print "\n";

    my @methods = sort {
        $stats->{methods}{$b} <=> $stats->{methods}{$a}
    } keys %{$stats->{methods}};
    print "methods:\n";
    print join " ", map "$stats->{methods}{$_} $_", @methods;
    print "\n\n";

    my @status = sort {
        $stats->{status}{$b} <=> $stats->{status}{$a}
    } keys %{$stats->{status}};
    print "status:\n";
    print join " ", map "$stats->{status}{$_} $_", @status;
    print "\n\n";

    my $locations = scalar(keys %{$stats->{locations}});
    my @locations = sort {
        $stats->{locations}{$b} <=> $stats->{locations}{$a}
    } keys %{$stats->{locations}};
    @locations = splice @locations, 0, 5;
    print "most requested locations (of $locations):\n";
    print join " ", map "$stats->{locations}{$_} $_", @locations;
    print "\n\n";

    my @types = sort {
        $stats->{types}{$b} <=> $stats->{types}{$a}
    } keys %{$stats->{types}};
    print "types:\n";
    print join " ", map "$stats->{types}{$_} $_", @types;
    print "\n\n";

    my $ips = scalar(keys %{$stats->{ips}});
    my @ips = sort {
        $stats->{ips}{$b} <=> $stats->{ips}{$a}
    } keys %{$stats->{ips}};
    @ips = splice @ips, 0, 5;
    print "most active ips (of $ips):\n";
    print join " ", map "$stats->{ips}{$_} $_", @ips;
    print "\n";
}

# for dates that look like 09/Sep/2015:11:01:24 -0500
sub parse_date {
    my ($str) = @_;
    return if !$str;
    $str =~ m{(\d+) / (\w+) / (\d+) : (\d+) : (\d+) : (\d+) \s+ (\S+)}x
        or die "Can't parse date \"$str\"\n";
    my $day = $1;
    my $monthstr = $2;
    my $year = $3;
    my $hour = $4;
    my $min = $5;
    my $sec = $6;
    my $tz = $7;
    my $month = $months{$monthstr} or die "Unknown month $monthstr\n";
    $month -= 1;
    $year -= 1900;
    my $time = POSIX::mktime($sec, $min, $hour, $day, $month, $year, -1, -1, -1);
    return $time;
}

sub update_stats {
    my ($stats, $a) = @_;
    if (!$stats->{tostr}) {
        $stats->{tostr} = $a->{datestr};
    }
    $stats->{fromstr} = $a->{datestr};
    $stats->{locations}{$a->{loc}}++;
    $stats->{methods}{$a->{method}}++;
    $stats->{status}{$a->{status}}++;
    if (defined $a->{bytes}) {
        $stats->{bytes} += $a->{bytes};
    }
    $stats->{ips}{$a->{ip}}++;
    $stats->{types}{$a->{type}}++;
    if ($a->{reqtime}) {
        if ($a->{reqtime} < $stats->{mintime}) {
            $stats->{mintime} = $a->{reqtime};
        }
        if ($a->{reqtime} > $stats->{maxtime}) {
            $stats->{maxtime} = $a->{reqtime};
        }
        $stats->{sumtime} += $a->{reqtime};
        $stats->{sumtime2} += $a->{reqtime} ** 2;
    }
}

sub parse_request_line {
    my ($line, $log) = @_;
    if ($log->{format}) {
        return parse_request_line_format($line, $log);
    }
    else {
        return parse_request_line_default($line);
    }
}

sub parse_request_line_format {
    my ($line, $log) = @_;
    if (!$log->{regexp}) {
        parse_log_format($log);
    }
    my $a = {};
    my @captures = $line =~ $log->{regexp};
    if (!@captures) {
        return $a;
    }
    for my $i (0 .. $#captures) {
        my $c = $captures[$i];
        my $f = $log->{cformats}[$i];
        if ($f->{letter} eq "a") {
            $a->{ip} = $c;
        }
        elsif ($f->{letter} eq "A") {
            $a->{local_ip} = $c;
        }
        elsif ($f->{letter} eq "B") {
            $a->{bytes} = $c;
        }
        elsif ($f->{letter} eq "D") {
            $a->{reqtime} = $c;
        }
        elsif ($f->{letter} eq "e") {
            my $name = lc $f->{curly};
            $name =~ s/-/_/g;
            $a->{$name} = $c;
        }
        elsif ($f->{letter} eq "f") {
            $a->{file} = $c;
        }
        elsif ($f->{letter} eq "h") {
            $a->{host} = $c;
            if (!$a->{ip}) {
                $a->{ip} = $a->{host};
            }
        }
        elsif ($f->{letter} eq "i") {
            my $name = lc $f->{curly};
            $name =~ s/-/_/g;
            $a->{$name} = $c;
        }
        elsif ($f->{letter} eq "I") {
            $a->{req_bytes} = $c;
        }
        elsif ($f->{letter} eq "k") {
            $a->{keepalives} = $c;
        }
        elsif ($f->{letter} eq "l") {
            $a->{ident} = $c;
        }
        elsif ($f->{letter} eq "L") {
            $a->{errid} = $c;
        }
        elsif ($f->{letter} eq "m") {
            $a->{method} = $c;
        }
        elsif ($f->{letter} eq "O") {
            $a->{bytes2} = $c;
        }
        elsif ($f->{letter} eq "p") {
            if ($f->{curly} && $f->{curly} eq "local") {
                $a->{local_port} = $c;
            }
            elsif ($f->{curly} && $f->{curly} eq "remote") {
                $a->{remote_port} = $c;
            }
        }
        elsif ($f->{letter} eq "P") {
            if ($f->{curly} && $f->{curly} eq "tid") {
                $a->{tid} = $c;
            }
            else {
                $a->{pid} = $c;
            }
        }
        elsif ($f->{letter} eq "q") {
            $a->{query} = $c;
        }
        elsif ($f->{letter} eq "r") {
            $a->{request} = $c;
        }
        elsif ($f->{letter} eq "R") {
            $a->{handler} = $c;
        }
        elsif ($f->{letter} eq "s") {
            if ($f->{angle} && $f->{angle} eq ">") {
                $a->{final_status} = $c;
                if (!$a->{status}) {
                    $a->{status} = $c;
                }
            }
            else {
                $a->{status} = $c;
            }
        }
        elsif ($f->{letter} eq "t") {
            if ($c =~ /^\[([^]]*)\]/) {
                $a->{datestr} = $1;
            }
            else {
                $a->{datestr} = $c;
            }
        }
        elsif ($f->{letter} eq "u") {
            $a->{user} = $c;
        }
        elsif ($f->{letter} eq "U") {
            if ($f->{angle} && $f->{angle} eq ">") {
                $a->{url_final} = $c;
            }
            else {
                $a->{url} = $c;
            }
        }
        elsif ($f->{letter} eq "v") {
            $a->{vhost} = $c;
        }
        elsif ($f->{letter} eq "V") {
            $a->{server_name} = $c;
        }
    }
    process_request($a);
    return $a;
}

sub parse_log_format {
    my ($log) = @_;
    $log->{regexp} = "^";
    while (1) {
        if ($log->{format} =~ /\G([^%\\]+)/gc) {
            $log->{regexp} .= quotemeta($1);
        }
        elsif ($log->{format} =~ /\G\\t/gc) {
            $log->{regexp} .= "\t";
        }
        elsif ($log->{format} =~ /\G%t/gc) {
            my $c = {};
            $c->{letter} = "t";
            $log->{regexp} .= "\\[([^\\]]*)\\]";
            push @{$log->{cformats}}, $c;
        }
        elsif ($log->{format} =~ /\G%r/gc) {
            my $c = {};
            $c->{letter} ="r";
            if ($log->{regexp} && $log->{regexp} =~ /"$/) {
                $log->{regexp} .= "((?:\\\\?.)*?)";
            }
            else {
                $log->{regexp} .= "(\\S*)";
            }
            push @{$log->{cformats}}, $c;
        }
        elsif ($log->{format} =~ /\G%(\{([^\}]*)\}|([<>]))?(\w)/gc) {
            my $c = {};
            $c->{curly} = $2;
            $c->{angle} = $3;
            $c->{letter} = $4;
            if ($log->{regexp} && $log->{regexp} =~ /"$/) {
                $log->{regexp} .= "([^\"]*)";
            }
            else {
                $log->{regexp} .= "(\\S*)";
            }
            push @{$log->{cformats}}, $c;
        }
        else {
            last;
        }
    }
}

sub parse_request_line_default {
    my ($line) = @_;
    my $a = {};
    if ($line !~ m{
            ^(\S+) \s+ (\S+) \s+ (\S+) \s+ \[([^\]]+)\] \s+
            "(.+?)" \s+ (\S+) \s+ (\S+) \s+ "([^"]*)" \s+ "([^"]*)"
        }x) {
        return $a;
    }
    $a->{ip} = $1;
    $a->{ident} = $2;
    $a->{user} = $3;
    $a->{datestr} = $4;
    $a->{request} = $5;
    $a->{status} = $6;
    $a->{bytes} = $7;
    $a->{referer} = $8;
    $a->{user_agent} = $9;
    process_request($a);
    return $a;
}

sub process_request {
    my ($a) = @_;
    delete $a->{ident} if $a->{ident} && $a->{ident} eq "-";
    delete $a->{user} if $a->{user} && $a->{user} eq "-";
    $a->{bytes} = 0 if $a->{bytes} && $a->{bytes} eq "-";
    delete $a->{referer} if $a->{referer} eq "-";
    if ($a->{request} =~ /^ (\S+) \s+ (\S+) \s+ (\S+) $/x) {
        $a->{method} = $1;
        $a->{uri} = $2;
        $a->{protocol} = $3;
        $a->{loc} = $a->{uri};
        $a->{loc} =~ s/\?.*//;
        delete $a->{request};
    }
    else {
        $a->{method} = $a->{uri} = $a->{protocol} = $a->{loc} = "";
    }
    if ($a->{loc} =~ /\.(jpe?g|gif|png|ico)$/i) {
        $a->{type} = "image";
    }
    elsif ($a->{loc} =~ /\.(cgi)$/i) {
        $a->{type} = "cgi";
    }
    elsif ($a->{loc} =~ /\.(php)$/i) {
        $a->{type} = "php";
    }
    elsif ($a->{loc} =~ /\.(html?)$/i) {
        $a->{type} = "html";
    }
    elsif ($a->{loc} =~ /\.(js)$/i) {
        $a->{type} = "js";
    }
    elsif ($a->{loc} =~ /\.(css)$/i) {
        $a->{type} = "css";
    }
    else {
        $a->{type} = "other";
    }
}

sub datestr {
    my ($date) = @_;
    return "" if !$date;
    my $datestr = POSIX::strftime("%a %b %d, %Y %I:%M:%S%P", localtime($date));
    return $datestr;
}

sub time_diff {
    my ($from, $to) = @_;
    return "" if !$from || !$to;
    my $diff = $to - $from;
    if ($diff < 60) {
        return " in the $diff seconds";
    }
    elsif ($diff < 60 * 60) {
        return " in the " . int($diff / 60) . " minutes";
    }
    elsif ($diff < 24 * 60 * 60) {
        return " in the " . int($diff / (60 * 60)) . " hours";
    }
    else {
        return " in the " . int($diff / (24 * 60 * 60)) . " days";
    }
}

sub natural_time {
    my ($date) = @_;
    $now = time if !$now;
    my $delta = $now - $date;
    if ($delta < -32 * 24 * 60 * 60) {
        return POSIX::strftime("%b %Y", localtime($date));
    }
    elsif ($delta < -2 * 24 * 60 * 60) {
        return "in " . int(-$delta / (24 * 60 * 60)) . " days";
    }
    elsif ($delta < -24 * 60 * 60) {
        return "in 1 day";
    }
    elsif ($delta < -2 * 60 * 60) {
        return "in " . int(-$delta / (60 * 60)) . " hours";
    }
    elsif ($delta < -60 * 60) {
        return "in 1 hour";
    }
    elsif ($delta < -2 * 60) {
        return "in " . int(-$delta / 60) . " minutes";
    }
    elsif ($delta < -60) {
        return "in 1 minute";
    }
    elsif ($delta < -1) {
        return "in $delta seconds";
    }
    elsif ($delta < 0) {
        return "in 1 second";
    }
    elsif ($delta < 1) {
        return "right now";
    }
    elsif ($delta < 2) {
        return "1 second ago";
    }
    if ($delta < 60) {
        return "$delta seconds ago";
    }
    elsif ($delta < 2 * 60) {
        return "1 minute ago";
    }
    elsif ($delta < 60 * 60) {
        return int($delta / 60) . " minutes ago";
    }
    elsif ($delta < 2 * 60 * 60) {
        return "1 hour ago";
    }
    elsif ($delta < 24 * 60 * 60) {
        return int($delta / (60 * 60)) . " hours ago";
    }
    elsif ($delta < 2 * 24 * 60 * 60) {
        return "1 day ago";
    }
    elsif ($delta < 32 * 24 * 60 * 60) {
        return int($delta / (24 * 60 * 60)) . " days ago";
    }
    else {
        return POSIX::strftime("%b %Y", localtime($date));
    }
}

sub show_last {
    log_file();
    my @cmd = ("cat", $selected_log->{file2});
    if ($selected_log->{file2} =~ /\.gz$/) {
        @cmd = ("gzip -dc $selected_log->{file2}");
    }
    if ($opts{last}) {
        @cmd = ("tail -$opts{last} $selected_log->{file2}");
        if ($selected_log->{file2} =~ /\.gz$/) {
            @cmd = ("gzip -dc $selected_log->{file2} | tail -$opts{last}");
        }
    }
    open my $fh, "-|", @cmd
        or die "Can't open $selected_log->{file2}: $!\n";
    while (my $line = <$fh>) {
        chomp $line;
        my $a = parse_request_line($line, $selected_log);
        if (!$a) {
            die "Couldn't parse: $line\n";
        }
        $a->{date} = parse_date($a->{datestr});
        $a->{datestr} = datestr($a->{date});
        print "[$a->{datestr}] from IP $a->{ip}:\n";
        print "agent $a->{user_agent}\n";
        if ($a->{referer}) {
            print "referer $a->{referer}\n";
        }
        print "status $a->{status}\n";
        if (defined $a->{bytes}) {
            print "bytes " . human_readable($a->{bytes}) . "\n";
        }
        if ($a->{reqtime}) {
            my $reqtime = int($a->{reqtime} / 1000);
            print "time ${reqtime}ms\n";
        }
        print "$a->{method} $a->{uri}\n";
        print "\n";
    }
    close $fh;
}

sub log_file {
    if (!$selected_log) {
        die "No log selected\n";
    }
    if ($selected_log->{file2}) {
        return;
    }
    elsif (!$opts{rotation}) {
        $selected_log->{file2} = $selected_log->{file};
    }
    elsif (-e "$selected_log->{file}.$opts{rotation}.gz") {
        $selected_log->{file2} = "$selected_log->{file}.$opts{rotation}.gz";
    }
    elsif (-e "$selected_log->{file}.$opts{rotation}") {
        $selected_log->{file2} = "$selected_log->{file}.$opts{rotation}";
    }
    elsif (-e "$selected_log->{file}-$opts{rotation}.gz") {
        $selected_log->{file2} = "$selected_log->{file}-$opts{rotation}.gz";
    }
    elsif (-e "$selected_log->{file}-$opts{rotation}") {
        $selected_log->{file2} = "$selected_log->{file}-$opts{rotation}";
    }
    else {
        die "Unable to find log file.\n";
    }
}

sub list {
    for my $log (@logs) {
        if ($opts{verbose}) {
            show_log_info_verbose($log);
        }
        else {
            show_log_info($log);
        }
    }
}

sub show_log_info {
    my ($log) = @_;
    my $selected = $log->{selected} ? "*" : " ";
    my $size = "-";
    my $updated = "";
    my $rinfo = "";
    if (-e $log->{file}) {
        $size = human_readable(-s $log->{file});
        my $mtime = (stat($log->{file}))[9];
        $updated = " " . natural_time($mtime);
        my @rotations = myglob($log->{file});
        my $rcount = @rotations;
        if ($rcount) {
            my $rsize = 0;
            for my $rotation (@rotations) {
                $rsize += -s $rotation || 0;
            }
            $rsize = human_readable($rsize);
            $rinfo = " +$rcount $rsize";
        }
    }
    my $name = $log->{name};
    my $line = sprintf "%s %-33s %-16s %s", $selected, $name, "$size$rinfo", $updated;
    print "$line\n";
    return if !$opts{verbose};

}

sub show_log_info_verbose {
    my ($log) = @_;
    my $selected = $log->{selected} ? "* " : "";
    my $size = "-";
    my $updated = "";
    my $rinfo = "";
    my @rotations2;
    if (-e $log->{file}) {
        $size = human_readable(-s $log->{file});
        my $mtime = (stat($log->{file}))[9];
        $updated = natural_time($mtime);
        my @rotations = myglob($log->{file});
        my $rcount = @rotations;
        if ($rcount) {
            my $rsize = 0;
            for my $rotation (@rotations) {
                $rsize += -s $rotation || 0;
                if ($rotation =~ /^\Q$log->{file}\E.(.*?)(\.gz)?$/) {
                    my $r = {};
                    $r->{name} = $1;
                    $r->{size} = human_readable(-s $rotation);
                    my $mtime = (stat($rotation))[9];
                    $r->{updated} = natural_time($mtime);
                    push @rotations2, $r;
                }
            }
            $rsize = human_readable($rsize);
            $rinfo = " +$rcount $rsize";
        }
    }
    my $docroot = "";
    my $docroot2 = "";
    if ($log->{vhost} && $log->{vhost}{docroot}) {
        $docroot = $log->{vhost}{docroot};
        $docroot2 = Cwd::abs_path($docroot);
    }
    print "$selected$log->{name}\n";
    print "    file $log->{file}\n";
    print "    size $size$rinfo\n";
    if ($updated) {
        print "    updated $updated\n";
    }
    if ($log->{fname}) {
        print "    fname $log->{fname}\n";
    }
    if ($log->{format}) {
        print "    format $log->{format}\n";
    }
    if ($docroot) {
        print "    docroot $docroot\n";
    }
    if ($docroot2 && $docroot2 ne $docroot) {
        print "    docroot2 $docroot2\n";
    }
    for my $r (@rotations2) {
        print "    rotation $r->{name} ($r->{size} $r->{updated})\n";
    }
}

sub human_readable {
    my ($size) = @_;
    $size ||= 0;
    my @power = ("B", "K", "M", "G", "T", "P", "E", "Z", "Y");
    my $i = 0;
    my $abs_size = abs $size;
    for ($i = 0; $i < @power; $i++) {
        last if $abs_size < 1024;
        $abs_size /= 1024;
    }
    my $str = sprintf("%.1f", $abs_size) . $power[$i];
    $str =~ s/\.0//;
    $str = "-$str" if $size < 0;
    return $str;
}

sub select_log {
    if ($opts{name}) {
        select_log_by_name($opts{name});
        return;
    }
    select_log_by_cwd();
    if (!$selected_log && $opts{default}) {
        select_log_by_name($opts{default});
    }
    if (!$selected_log && @logs) {
        $selected_log = $logs[0];
        $selected_log->{selected} = 1;
        return;
    }
}

sub select_log_by_name {
    my ($name) = @_;
    if ($name =~ /\//) {
        for my $log (@logs) {
            if ($log->{file} eq $name) {
                $log->{selected} = 1;
                $selected_log = $log;
                return;
            }
        }
        $selected_log = add_log($name);
        $selected_log->{selected} = 1;
    }
    else {
        my $regexp = qr/^$name/;
        for my $log (@logs) {
            if ($log->{name} =~ $regexp) {
                $log->{selected} = 1;
                $selected_log = $log;
                return;
            }
        }
    }
}

sub add_log {
    my ($file) = @_;
    my $log = {};
    $log->{file} = $file;
    $log->{name} = basename($file);
    push @logs, $log;
    return $log;
}

sub select_log_by_cwd {
    my $cwd = Cwd::cwd();
    $cwd =~ s{/+$}{};
    for my $log (@logs) {
        next if !$log || !$log->{vhost} || !$log->{vhost}{docroot};
        my $docroot = Cwd::abs_path($log->{vhost}{docroot});
        next if !$docroot;
        $docroot =~ s/\/+$//;
        $docroot =~ s/\/public_html$//;
        if ($cwd =~ /^$docroot(\/|$)/) {
            $log->{selected} = 1;
            $selected_log = $log;
            return;
        }
    }
}

sub get_logs {
    get_logs_from_apache_conf();
    if (-e "$ENV{HOME}/access-logs") {
        get_logs_from_dir("$ENV{HOME}/access-logs");
    }
}

sub get_logs_from_dir {
    my ($dir) = @_;
    opendir my $dh, $dir or return;
    for my $dfile (sort readdir $dh) {
        next if $dfile =~ /^\.\.?$/;
        $dfile = "$dir/$dfile";
        add_log($dfile);
    }
    closedir $dh;
}

sub get_logs_from_apache_conf {
    my @files = `find /etc/apache2 /etc/httpd 2>/dev/null`;
    my @logs_all;
    my $host = {};
    my $vhost = $host;
    my %vhosts;
    for my $file (@files) {
        chomp $file;
        next if $file =~ /\bavailable\b/;
        open my $fh, $file or next;
        while (my $line = <$fh>) {
            chomp $line;
            if ($line =~ /(.*)\\$/) {
                $line = $1;
                my $line2 = <$fh>;
                $line .= $line2;
                redo;
            }
            if ($line =~ /^\s*<VirtualHost(|\s+([^>]*))>/ims) {
                $vhost = {vname => $2};
            }
            elsif ($line =~ /^\s*<\/VirtualHost>/ims) {
                $vhost->{name} = vhost_name($vhost);
                delete $vhost->{vname};
                delete $vhost->{sname};
                # The first <VirtualHost> is used if multiple identical named ones exist
                if ($vhosts{$vhost->{name}}) {
                    $vhost->{invalid} = 1;
                }
                else {
                    $vhosts{$vhost->{name}} = $vhost;
                }
                $vhost = $host;
            }
            elsif ($line =~ /^\s*ServerName\s+(\S+)/ims) {
                $vhost->{sname} = $1;
            }
            elsif ($line =~ /^\s*DocumentRoot\s+(.+)/ims) {
                $vhost->{docroot} = unquote($1);
            }
            elsif ($line =~ /^\s*ServerRoot\s+(\S+)/ims) {
                $vhost->{servroot} = unquote($1);
            }
            elsif ($line =~ /^\s*CustomLog\s+("[^"]*"|\S+)(\s+("(\\"|[^"])*"|\S+))?/ims) {
                my $log = {};
                $log->{vhost} = $vhost;
                my $file = $1;
                my $arg2 = $3;
                $file = unquote($file);
                $arg2 = unquote($arg2);
                $log->{file} = $file;
                if ($arg2) {
                    if ($arg2 =~ /%/) {
                        $log->{format} = $arg2;
                    }
                    else {
                        $log->{fname} = $arg2;
                    }
                }
                env_replace($log);
                $log->{name} = basename($log->{file});
                push @logs_all, $log;
            }
            elsif ($line =~ /^\s*LogFormat\s+("(\\"|[^"])*"|\S+)(\s+(\S+))?/ims) {
                my $format = $1;
                my $fname = $4;
                $format = unquote($format);
                $fname = unquote($fname);
                if ($fname) {
                    $host->{fnames}{$fname} = $format;
                }
                else {
                    $vhost->{format} = $format;
                }
            }
        }
        close $fh;
    }
    my %logs;
    for my $log (sort byfile @logs_all) {
        if ($log && $log->{vhost}) {
            if ($log->{vhost}{invalid}) {
                next;
            }
            if ($host->{servroot} && $log->{file} !~ /^\//) {
                $log->{file} = "$host->{servroot}/$log->{file}";
            }
            if ($log->{fname} && $host->{fnames}{$log->{fname}}) {
                $log->{format} = $host->{fnames}{$log->{fname}};
            }
            if (!$log->{format}) {
                $log->{format} = $log->{vhost}{format};
            }
            if ($logs{$log->{file}}) {
                next;
            }
            $logs{$log->{file}} = $log;
        }
        push @logs, $log;
    }
}

sub env_replace {
    my ($log) = @_;
    $ENV{APACHE_LOG_DIR} ||= "/var/log/apache2";
    $log->{file} =~ s/\$\{(\w+)\}/$ENV{$1}/ge;
}

sub byfile {
    return $a->{name} cmp $b->{name};
}

sub basename {
    my ($file) = @_;
    $file =~ /([^\/]+)$/;
    my $name = $1;
    return $name;
}

sub dirname {
    my ($file) = @_;
    $file =~ /^((.*)\/)/;
    my $dir = $2 || ".";
    return $dir;
}

sub unquote {
    my ($file) = @_;
    return $file if !$file;
    if ($file =~ /^"((\\"|[^"])*)"/) {
        $file = $1;
    }
    elsif ($file =~ /^'((\\'|[^'])*)'/) {
        $file = $1;
    }
    $file =~ s/\\(["'])/$1/g;
    return $file;
}

sub vhost_name {
    my ($vhost) = @_;
    my @name;
    if ($vhost->{sname}) {
        push @name, $vhost->{sname};
    }
    if ($vhost->{vname}) {
        push @name, $vhost->{vname};
    }
    my $name = join " ", @name;
    return $name;
}

sub get_opts {
    my @args;
    $opts{action} = "less";
    while (my $arg = shift @ARGV) {
        if ($arg =~ /^--?$/) {
            push @args, @ARGV;
            last;
        }
        elsif ($arg =~ /^-a(\d*)$/) {
            $opts{last} = $1;
        }
        elsif ($arg =~ /^-d(=(.*))?$/) {
            $opts{default} = $1 ? $2 : shift(@ARGV);
        }
        elsif ($arg =~ /^-f$/) {
            $opts{action} = "tailf";
        }
        elsif ($arg =~ /^-g(\d+|[dh]|)$/) {
            $opts{action} = "graph";
            my $interval = $1 || "";
            if ($interval eq "d") {
                $opts{interval} = 24 * 60 * 60;
            }
            elsif ($interval eq "h" || !$interval) {
                $opts{interval} = 60 * 60;
            }
            else {
                $opts{interval} = $interval;
            }
        }
        elsif ($arg =~ /^(--?help|-h|-\?)$/) {
            usage();
        }
        elsif ($arg =~ /^-i$/) {
            $opts{action} = "info";
        }
        elsif ($arg =~ /^-l(l)?$/) {
            $opts{action} = "list";
            $opts{verbose} = $1;
        }
        elsif ($arg =~ /^-p$/) {
            $opts{action} = "print";
        }
        elsif ($arg =~ /^-r(\w+)$/) {
            $opts{rotation} = $1;
        }
        elsif ($arg =~ /^-v$/) {
            $opts{action} = "vim";
        }
        elsif ($arg =~ /^-/) {
            die "Invalid argument '$arg'\n";
        }
        else {
            push @args, $arg;
        }
    }
    if (@args > 1) {
        die "Too many arguments\n";
    }
    $opts{name} = shift @args;
}

sub usage {
    print <<EOUSAGE;
Usage: alog [<options>] [<name>]

-a[<n>]    show last n accesses with info spread vertically
-d=<file>  default log when one isn't found for cwd
-f         tail -f the log
-g         graph requests at hourly intervals
-gd        graph requests at daily intervals
-h         displays this help text
-i         info and statistics
-l         list available logs
-ll        list available logs verbosely
-p         print log path
-r<n>      rotation number
-v         vim the log

<name>     name of the log you are trying to access (regexp),
           if name contains a "/", name is treated as a file name,
           default is the error log for the cwd.

by default, this command will open the log in \$PAGER or less(1)
EOUSAGE
    exit;
}

__END__

=head1 NAME

alog - An Apache access log viewer

=head1 SYNOPSIS

    alog [<options>] [<name>]

=head1 OPTIONS

    -a[<n>]    show last n accesses with info spread vertically
    -d=<file>  default log when one isn't found for cwd
    -f         tail -f the log
    -g         graph requests at hourly intervals
    -gd        graph requests at daily intervals
    -h         displays this help text
    -i         info and statistics
    -l         list available logs
    -ll        list available logs verbosely
    -p         print log path
    -r<n>      rotation number
    -v         vim the log

    <name>     name of the log you are trying to access (regexp),
               if name contains a "/", name is treated as a file name,
               default is the error log for the cwd.

=head1 DESCRIPTION

This program will show the Apache access log associated with the
directory you are currently inside of.

Many people set up web servers with each website inside their own
directory in $HOME or /var/www. While working on these sites, for
example /var/www/coolsite.com/, you can run `alog` with no arguments
and it will show the access log for that site inside of less(1).

If you define the $PAGER environment variable, `alog` will use that
program instead of less(1).

If you want to view another site's access log, provide `alog` with an
expression that partially matches the name of that website's log
after the `alog` command. For example, `alog foo`.

To see a list of all the access logs on the server use `alog -l`.
More detailed information, such as what rotations exist for each
log, use `alog -ll`.

To specify an older rotation of an access log, use the -r option.
For example `alog -r2`, might show the /var/log/httpd/foo.access_log.2.gz
file.

The way it determines which access log to show is by parsing Apache
config files in either /etc/httpd or /etc/apache2. A CustomLog line
tells where the access log is, a DocRoot line tells which directory
that access log is for, a LogFormat line tells what format the
access log uses.

The -p option will show the path the selected access log file.

The -f option will open the log in `tail -f`.

The -v option will open the log in `vim`.

The -i option will show statistics about the access log file such
as how many requests there were, their time frame, and most active
uris.

The -a option will show the data fields of the access log entry on
their own line, so you don't have to scroll right to see the part
you are interested in.

The -g option will show a graph of the number of requests in an hourly
interval.

The -gd option will show a graph of the number of requests in a daily
interval.

=head1 METACPAN

L<https://metacpan.org/pod/App::Elog>

=head1 AUTHOR

Jacob Gelbman E<lt>gelbman@gmail.comE<gt>

=head1 COPYRIGHT AND LICENSE

Copyright (C) 2017 by Jacob Gelbman

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself, either Perl version 5.18.2 or,
at your option, any later version of Perl 5 you may have available.

=cut

