#!/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 DateTime::Format::Strptime;
use List::Util 'max';

my $progname=basename($0);
my $version='0.2.0'; # sed'ed before release

my $db_regex='([0-9]{4})\.([0-9]{2})'; # Regex for DB file names
my $field_sep=':::'; # Field separator in DB files
my $timeperiod_sep='\.\.'; # Field separator for TIMEPERIOD
my $timespec_delta_min=10; # Time around single TIMESPEC with time specified
my @fields_supported=('day', 'time', 'artist', 'title', # Supported DB fields
                      'album', 'date', 'genre', 'length');
my $strp=new DateTime::Format::Strptime(
    pattern => '%F',
    time_zone => 'local',
    on_error => 'undef',
    );

# Default values, changeable by command line arguments.
my @fields=@fields_supported;
my $sortby='day'; # Sort output by this field
my $time_period='today'; # Show today's playbacks by default
my @filter; # Don't filter output by default
my $db_dir="/var/lib/mpd-hits"; # Directory of statistics DB

# Parse command line arguments.
use vars qw($opt_fields $opt_sort $opt_time $opt_filter $opt_db_directory
            $opt_help $opt_version);
Getopt::Mixed::init('fields=s f>fields sort=s s>sort time=s t>time
                     filter=s l>filter db-directory=s d>db-directory
                     help h>help 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
@fields=split /,/, $opt_fields if defined $opt_fields;
foreach my $fld (@fields) {
    exitprog(1, "Bad fields specified.")
        unless grep /^$fld$/, @fields_supported;
}
$sortby=$opt_sort if defined $opt_sort;
$time_period=$opt_time if defined $opt_time;
@filter=split /,/, $opt_filter if defined $opt_filter;
$db_dir=$opt_db_directory if defined $opt_db_directory;

# 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;

# Calculate starting and finishing timestamps.
my ($from_epoch, $to_epoch);
if ($time_period eq 'today') {
    my $dt=DateTime->now(time_zone => 'local');
    $strp->pattern('%F');
    my $today=$strp->format_datetime($dt);
    ($from_epoch, $to_epoch)=timespec2epoch($today, 'around');
} else {
    my $seps=0;
    $seps++ while $time_period=~/$timeperiod_sep/g;
    exitprog(1, "Bad TIMEPERIOD.") if $seps>1;

    if ($seps==0) { # Single TIMESPEC
        ($from_epoch, $to_epoch)=timespec2epoch($time_period, 'around');
    } else { # From TIMESPEC to TIMESPEC
        my ($from_str, $to_str)=split /$timeperiod_sep/, $time_period;

        if ($from_str) {
            $from_epoch=timespec2epoch($from_str, 'from');
        } else {
            $from_epoch=time();
        }
        if ($to_str) {
            $to_epoch=timespec2epoch($to_str, 'to');
        } else {
            $to_epoch=time();
        }
    }
}

# Decompose starting and finishing timestamps.
my ($from_sec, $from_min, $from_hour, $from_mday, $from_mon, $from_year)=
    epoch2strs($from_epoch);
my ($to_sec, $to_min, $to_hour, $to_mday, $to_mon, $to_year)=
    epoch2strs($to_epoch);
my $period_str='Time period: ';
$period_str.=sprintf('%04d/%02d/%02d [%02d:%02d:%02d]',
                     $from_year, $from_mon, $from_mday,
                     $from_hour, $from_min, $from_sec);
$period_str.=sprintf(' .. %04d/%02d/%02d [%02d:%02d:%02d]',
                     $to_year, $to_mon, $to_mday,
                     $to_hour, $to_min, $to_sec);
print "$period_str\n\n";

# Select DB files from the specified time range.
my @db_files_selected;
unless ($from_epoch<=$to_epoch) {
    exitprog(1, "The starting date must be less or equal to the ending one.");
}
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_epoch=strs2epoch($s, $m, $h, $d, $n, $y);

        # Remember the song if it was played during the specified time period.
        if ($song_epoch>=$from_epoch and $song_epoch<=$to_epoch) {
            push @songs_played, \%song;
        }
    }
    close DBFILE;
}

# Sort songs by specified field.
sub sort_songs {
    if ($sortby eq 'length') {
        $a->{$sortby} <=> $b->{$sortby}
    } else {
        $a->{$sortby} cmp $b->{$sortby}
    }
}
@songs_played=sort sort_songs @songs_played;

# Show the report.
foreach my $song (@songs_played) {
    foreach my $fld (@fields) {
        print qq/"$song->{$fld}" /;
    }
    print "\n";
}

exit 0;

# Parameters:
#   1. TIMESPEC, as it specified in command line;
#   2. one of:
#      'from':   if time is not specified in TIMESPEC, it will be set to
#                00:00:00; the function will return epoch time;
#      'to':     if time is not specified in TIMESPEC, it will be set to
#                23:59:59; the function will return epoch time;
#      'around': if time is not specified in TIMESPEC, the function will return
#                array of two epoch times, the first one is TIMESPEC with time
#                set to 00:00:00 and the second one is TIMESPEC with time set
#                to 23:59:59;
#                if time *is* specified in TIMESPEC, the function will return
#                array of two epoch times, the first one is
#                TIMESPEC-$timespec_delta_min, the second one is
#                TIMESPEC+$timespec_delta_min.
#
# Return value:
#   Depends on the second parameter: one or two epoch times.
#
sub timespec2epoch {
    my $timespec=shift @_;
    my $hint=shift @_;

    $strp->pattern('%FT%T');
    my $dt=$strp->parse_datetime($timespec);
    if (defined $dt) { # Both date and time specified
        if ($hint eq 'around') {
            $dt->subtract(DateTime::Duration->new(minutes =>
                                                  $timespec_delta_min));
            my $from=$dt->epoch();
            $dt->add(DateTime::Duration->new(minutes =>
                                             $timespec_delta_min*2));
            my $to=$dt->epoch();
            return ($from, $to);
        } elsif ($hint eq 'from' or $hint eq 'to') {
            return $dt->epoch();
        } else {
            exitprog(1, "Internal error: unknown hint '$hint' for ".
                     "timespec2epoch().\n");
        }
    } else {
        $strp->pattern('%F');
        $dt=$strp->parse_datetime($timespec);
        if (defined $dt) { # Only date specified
            if ($hint eq 'around') {
                $dt->set(hour => 0, minute => 0, second => 0);
                my $from=$dt->epoch();
                $dt->set(hour => 23, minute => 59, second => 59);
                my $to=$dt->epoch();
                return ($from, $to);
            } elsif ($hint eq 'from') {
                $dt->set(hour => 0, minute => 0, second => 0);
            } elsif ($hint eq 'to') {
                $dt->set(hour => 23, minute => 59, second => 59);
            } else {
                exitprog(1, "Internal error: unknown hint '$hint' for ".
                         "timespec2epoch().\n");
            }

            return $dt->epoch()
        } else {
            exitprog(1, "Failed to parse TIMEPERIOD.");
        }
    }
}

sub epoch2strs {
    my $epoch=shift @_;

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

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

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

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

    return $epoch;
}

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 [OPTION]...                \n";
    print "                                            \n";
    print "Options:                                    \n";
    print "  -f, --fields=FIELD1[,FIELD2]...           \n";
    print "  -s, --sort=FIELD                          \n";
    print "  -t, --time=TIMEPERIOD                     \n";
    # print "  -l, --filter=FIELD1=VAL1[,FIELD2=VAL2]... \n";
    print "                                            \n";
    print "  -d, --db-directory=DBDIR                  \n";
    print "                                            \n";
    print "  -h, --help                                \n";
    print "  -V, --version                             \n";

    exitprog($code);
}

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