#!/usr/bin/perl -w

# This file is part of mpd-hits.
# Copyright (C) 2010 Dmitry Samoyloff.
#
# mpd-hits is free software: you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# mpd-hits is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along
# with mpd-hits. If not, see <http://www.gnu.org/licenses/>.

use strict;

use File::Basename 'basename';
use Getopt::Mixed 1.006; # Customization vars first appeared in this version
use Time::Local;
use Date::Parse;
use List::Util 'max';

my $progname=basename($0);
my $db_dir="/var/lib/mpd-hits"; # Directory of statistics DB
my @months=qw('' Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec);

my $version='0.1.1'; # sed'ed before release

my $db_regex='([0-9]{4})\.([0-9]{2})'; # Regex to identify and parse DB files
my $field_sep=':::'; # Field separator in DB files
# Number of top entries to display (may be changed by c.l. key). 0 means "all".
my $top_n=0;

# Parse command line arguments.
use vars qw($opt_album $opt_artist $opt_day $opt_db_directory $opt_from
            $opt_genre $opt_head $opt_help $opt_log $opt_title $opt_to
            $opt_version);
Getopt::Mixed::init('album b>album artist a>artist day=s d>day '.
                    'db-directory=s from=s f>from genre g>genre '.
                    'head=s H>head help h>help log o>log title l>title '.
                    'to=s t>to version V>version');
$Getopt::Mixed::badOption=sub { usage(1) };
Getopt::Mixed::getOptions();
usage(1) if scalar @ARGV; # Unknown stuff remained in command line
usage(0) if defined $opt_help; # Show usage info and quit
version() if defined $opt_version; # Show version and quit
$db_dir=$opt_db_directory if defined $opt_db_directory;
if (defined $opt_day and
    (defined $opt_from or defined $opt_to)) {
    exitprog(1, "Use either --day or --from/--to");
}
my ($day_ts, $from_ts, $to_ts);
if (defined $opt_day) {
    $day_ts=str2time($opt_day);
} else {
    $from_ts=defined $opt_from ? str2time($opt_from) : '';
    $to_ts=defined $opt_to ? str2time($opt_to) : time();
}
if (defined $opt_head) {
    $top_n=$opt_head;
    if ($top_n!~/^\s*\d+\s*$/) {
        exitprog(1, "--head option needs a positive number.");
    }
}

# Check permissions of the db directory
exitprog(1, "Can't access db directory $db_dir") if not -r $db_dir;

# Get list of DB files.
my @db_files=glob "$db_dir/*";
exitprog(1, "DB files not found in $db_dir.") if not scalar @db_files;
@db_files=sort grep /\/$db_regex$/, @db_files;

# Set from/to dates if exact day has been specified.
if ($day_ts) {
    my ($day_sec, $day_min, $day_hour, $day_mday, $day_mon, $day_year)=
        timestamp_to_strings($day_ts);

    $day_hour=$day_min=$day_sec=0;
    $from_ts=strings_to_timestamp($day_sec, $day_min, $day_hour,
                                  $day_mday, $day_mon, $day_year);

    $day_hour=23;
    $day_min=$day_sec=59;
    $to_ts=strings_to_timestamp($day_sec, $day_min, $day_hour,
                                $day_mday, $day_mon, $day_year);
}

# Determine starting date if it was omitted.
my ($oldest_month)=($db_files[0]=~/($db_regex)$/);
$oldest_month=~s/\./-/;
$oldest_month.='-01';
$from_ts=str2time($oldest_month) if not $from_ts;

# Process from/to timestamps.
unless ($from_ts and $to_ts) {
    exitprog(1, 'Cannot determine time range to display.');
}
my ($from_sec, $from_min, $from_hour, $from_mday, $from_mon, $from_year)=
    timestamp_to_strings($from_ts);
my ($to_sec, $to_min, $to_hour, $to_mday, $to_mon, $to_year)=
    timestamp_to_strings($to_ts);

# Show the time range.
my $header;
$header.=sprintf('Time range: %02d %s %04d [%02d:%02d:%02d]',
                 $from_mday, $months[$from_mon], $from_year,
                 $from_hour, $from_min, $from_sec);
$header.=sprintf(' --- %02d %s %04d [%02d:%02d:%02d]',
                 $to_mday, $months[$to_mon], $to_year,
                 $to_hour, $to_min, $to_sec);
print '-' x length $header, "\n";
print $header."\n";
print '-' x length $header, "\n";

# Check if the starting date is less or equal to the ending one.
unless ($from_ts<=$to_ts) {
    exitprog(1, "The starting date must be less or equal to the ending one.");
}

# Check criteria specified.
my $n_crit=defined($opt_log)+defined($opt_artist)+defined($opt_title)+
    defined($opt_album)+defined($opt_genre);
if ($n_crit>1) {
    exitprog(1, "A single criterion must be used.");
} elsif ($n_crit==0) {
    exitprog(1, "Criterion not specified.");
}

# Select DB files from the specified time range.
my @db_files_selected;
foreach (@db_files) {
    my ($year, $month)=(/$db_regex/);
    if ($year>=$from_year and $month>=$from_mon and
        $year<=$to_year and $month<=$to_mon) {
        push @db_files_selected, $_;
    }
}

# Debugging output.
#print "Selected DB files: @db_files_selected\n\n";

# Retrieve info from the selected DB files.
my @songs_played;
foreach my $file (@db_files_selected) {
    open DBFILE, $file or exitprog(1, "Can't read $file.");
    while (<DBFILE>) {
        chomp;
        next if /^$/; # Skip empty lines.

        # Get song info.
        my %song;
        ($song{'day'}, $song{'time'}, $song{'artist'}, $song{'title'},
         $song{'album'}, $song{'date'}, $song{'genre'}, $song{'length'})=
             split /$field_sep/;

        # Create timestamp for this playback.
        my ($h, $m, $s)=split /:/, $song{'time'};
        my ($y, $n, $d)=split /\./, $song{'day'};
        my $song_ts=strings_to_timestamp($s, $m, $h, $d, $n, $y);

        # Remember this song if it was played during the specified time range.
        if ($song_ts>=$from_ts and $song_ts<=$to_ts) {
            push @songs_played, \%song;
        }
    }
    close DBFILE;
}

## Show the summary.
if (defined $opt_log) {
    foreach my $song (@songs_played) {
        print "$song->{'day'} $song->{'time'}: ".
            "$song->{'artist'} - $song->{'album'} - $song->{'title'}\n";
    }
} elsif (defined $opt_artist) {
    my ($max_entry_length, $entries, $playbacks)=
        collect_stats_by_field(\@songs_played, 'artist');
    print_chart_table('Artist', 'Playbacks', $max_entry_length,
                      $entries, $playbacks);
} elsif (defined $opt_title) {
    my ($max_entry_length, $entries, $playbacks)=
        collect_stats_by_field(\@songs_played, 'title', 1);
    print_chart_table('Title', 'Playbacks', $max_entry_length,
                      $entries, $playbacks);
} elsif (defined $opt_album) {
    my ($max_entry_length, $entries, $playbacks)=
        collect_stats_by_field(\@songs_played, 'album', 1);
    print_chart_table('Album', 'Playbacks', $max_entry_length,
                      $entries, $playbacks);
} elsif (defined $opt_genre) {
    my ($max_entry_length, $entries, $playbacks)=
        collect_stats_by_field(\@songs_played, 'genre');
    print_chart_table('Genre', 'Playbacks', $max_entry_length,
                      $entries, $playbacks);
}

exit 0;

sub timestamp_to_strings {
    my $ts=shift @_;

    my ($sec, $min, $hour, $mday, $mon, $year)=localtime($ts);
    $mon++;
    $year+=1900;

    return ($sec, $min, $hour, $mday, $mon, $year);
}

sub strings_to_timestamp {
    my ($sec, $min, $hour, $mday, $mon, $year)=@_;

    $mon--;
    $year-=1900;
    my $ts=timelocal($sec, $min, $hour, $mday, $mon, $year);

    return $ts;
}

sub collect_stats_by_field {
    my @songs_played=@{shift @_};
    my $field=shift @_;
    my $show_artist=shift @_;

    # Count playbacks by field.
    my %playbacks;
    my $max_entry_length=0;
    foreach my $song (@songs_played) {
        my $entry=$song->{$field};
        $entry="$song->{'artist'} / ".$entry if $show_artist;
        $playbacks{$entry}++;
        if (length($entry)>$max_entry_length) {
            $max_entry_length=length $entry;
        }
    }

    # Put statistics into arrays, sorted by number of playbacks.
    my (@entries, @playbacks);
    foreach my $entry (sort {$playbacks{$b}<=>$playbacks{$a}}
                        sort keys %playbacks) {
        push @entries, $entry;
        push @playbacks, $playbacks{$entry};
    }

    return ($max_entry_length, \@entries, \@playbacks);
}

sub print_chart_table {
    my $c1_header=shift @_;
    my $c2_header=shift @_;
    my $c1_max=shift @_;
    my @c1_data=@{shift @_};
    my @c2_data=@{shift @_};

    my $table_offset=4;
    my $c0_header='Pos.';
    my $header_fill_char=' ';
    my $data_fill_char=' ';

    $c1_header.=$header_fill_char;

    # Print table header.
    print "\n";
    print $c0_header.
        ($header_fill_char x $table_offset).
        $c1_header.
        ($header_fill_char x ($c1_max-length($c1_header)+$table_offset)).
        $c2_header;
    print "\n\n";

    # Print table data.
    my $pos=1;
    while (my $c1=shift @c1_data) {
        print sprintf('%'.length(${c0_header}).'d', $pos++).
            ($data_fill_char x $table_offset).
            $c1.
            ($data_fill_char x ($c1_max-length($c1)+$table_offset)).
            (shift @c2_data).
            "\n";

        last if $top_n and $pos>$top_n;
    }
}

sub exitprog {
    my $code=shift @_;
    my $message=shift @_ if defined $_[0];

    print "$message\n" if $message;
    exit $code;
}

sub usage {
    my $code=shift @_;

    my $p_spaces=' ' x length $progname;
    print "Usage: $progname [-d DATE | -f DATETIME -t DATETIME]\n";
    print "       $p_spaces [-H N] [-o|-a|-l|-b|-g]\n";
    print "       $p_spaces [--db-directory DBDIR]\n";
    print "       $p_spaces [-h|-V]\n";
    print "\n";
    print "Options:\n";
    print "  -d, --day=DATE\n";
    print "  -f, --from=DATETIME\n";
    print "  -t, --to=DATETIME\n";
    print "\n";
    print "  -H, --head=N\n";
    print "\n";
    print "  -o, --log\n";
    print "  -a, --artist=ARTIST\n";
    print "  -l, --title=TITLE\n";
    print "  -b, --album=ALBUM\n";
    print "  -g, --genre=GENRE\n";
    print "\n";
    print "      --db-directory=DBDIR\n";
    print "\n";
    print "  -h, --help\n";
    print "  -V, --version\n";

    exitprog($code);
}

sub version {
    exitprog(0, "$progname $version");
}
