#!/usr/bin/env perl

#  Cantata-Dynamic
#
#  Copyright (c) 2011-2013 Craig Drummond <craig.p.drummond@gmail.com>
#
#  ----
#
#  This program 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 2 of the License, or
#  (at your option) any later version.
#
#  This program 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 this program; see the file COPYING.  If not, write to
#  the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
#  Boston, MA 02110-1301, USA.

use IO::Socket::INET;
use POSIX;
use File::stat;
use File::Basename;
use Cwd 'abs_path';
# use LWP::Simple;
use Thread;
use Socket;
use IO::Socket;
use threads;
use threads::shared;
use URI::Escape;
use Encode;
use Sys::Hostname;
use Socket qw(:all);

# UDP multicast message sender - used to communicate with Cantata when cantata-dynamic is run in server mode!
my $serverSender : shared = dirname(__FILE__) . '/message-sender';
my $mcastGroup : shared = '239.123.123.123';
my $mcastPort : shared = 6602;
my $mcastTtl : shared = 1;

my $isServerMode : shared =0;
my $dynamicIsActive : shared =1;
my $currentStatus : shared ="IDLE";

$testMode=0;
$PLAY_QUEUE_DESIRED_LENGTH=10;
$PLAY_QUEUE_CURRENT_POS=5;

my $mpdHost : shared ="localhost";
my $mpdPort : shared ="6600";
my $mpdPasswd : shared ="";
my $idString : shared = hostname();

my $currentStatusTime = time;

# Read MPDs host, port, and password details from env - if set
sub readConnectionDetails() {
    my $hostEnv=$ENV{'MPD_HOST'};
    my $portEnv=$ENV{'MPD_PORT'};
    if (length($portEnv)>2) {
        $mpdPort=$portEnv;
    }

    if (length($hostEnv)>2) {
        my $sep = index($hostEnv, '@');

        if ($sep>0) {
            $mpdPasswd=substr($hostEnv, 0, $sep);
            $mpdHost=substr($hostEnv, $sep+1, length($hostEnv)-$sep);
        } else {
            $mpdHost=$hostEnv;
        }
    }
}

sub readReply() {
    my $sock=shift;
    local $data;
    my $socketData;
    while ($sock->connected()) {
        $sock->recv($data, 1024);
        if (! $data) {
            return '';
        }
        $socketData="${socketData}${data}";
        $data="";

        if (($socketData=~ m/(OK)$/) || ($socketData=~ m/^(OK)/)) {
            return $socketData;
        } elsif ($socketData=~ m/^(ACK)/) {
            return '';
        }
    }
}

# Connect to MPD
sub connectToMpd() {
    my $connDetails="";
    my $sock;
    if ($mpdHost=~ m/^(\/)/) {
        $sock = new IO::Socket::UNIX(Peer => $mpdHost, Type => 0);
        $connDetails=$mpdHost;
    } else {
        $sock = new IO::Socket::INET(PeerAddr => $mpdHost, PeerPort => $mpdPort, Proto => 'tcp');
        $connDetails="${mpdHost}:${mpdPort}";
    }
    if ($sock && $sock->connected()) {
        if (&readReply($sock)) {
            if ($mpdPasswd) {
                $sock->send("password ${mpdPasswd} \n");
                if (! &readReply($sock)) {
                    print "ERROR: Invalid password\n";
                    eval { close $sock; };undef $sock;
                }
            }
        } else {
            print "ERROR: Failed to read connection reply fom MPD (${connDetails})\n";
            close($sock);
        }
    } else {
        print "ERROR: Failed to connect to MPD (${connDetails})\n";
    }
    return $sock;
}

sub sendCommand() {
    my $cmd = shift;
    my $status = 0;
    my $sock=&connectToMpd();
    my $sockData;
    $cmd="${cmd}\n";
    if ($sock && $sock->connected()) {
        print $sock encode('utf-8' => $cmd);
        $sockData=&readReply($sock);
        eval { close $sock; }; undef $sock;
    }
    if ($sockData ne '') {
        return decode_utf8($sockData);
    }
    return $sockData;
}

sub waitForEvent() {
    my $sock=&connectToMpd();
    if ($sock && $sock->connected()) {
        $sock->send("idle player playlist\n");
        &readReply($sock);
        eval { close $sock; };undef $sock;
        return 1;
    }
    return 0;
}

sub baseDir() {
    my $cacheDir=$ENV{'XDG_CACHE_HOME'};
    if (!$cacheDir) {
        $cacheDir="$ENV{'HOME'}/.cache";
    }
    $cacheDir="${cacheDir}/cantata/dynamic";
    return $cacheDir
}

sub lockFile() {
    my $fileName=&baseDir();
    $fileName="${fileName}/lock";
    return $fileName;
}

$lastArtistSearch="";
@artistSearchResults=();

# Query LastFM for artists similar to supplied artist
sub querySimilarArtists() {
    my $artist=uri_escape(shift);
    if ($artist ne $lastArtistSearch) {
        @artistSearchResults=();
#         my $text = get 'http://ws.audioscrobbler.com/1.0/artist/'.$artist.'/similar.txt';
#         my $artistNum=0;
#         open(my $fileHandle, '<', \$text);
#         if (tell($fileHandle) != -1) {
#             my @lines = <$fileHandle>; # Read into an array...
            @lines=`wget \'http://ws.audioscrobbler.com/1.0/artist/${artist}/similar.txt\' -O -`;
            foreach my $line (@lines) {
                @parts = split(/,/, $line);
                if (3==scalar(@parts)) {
                    my $artist=$parts[2];
                    $artist =~ s/&amp;/&/g;
                    $artist =~ s/\n//g;
                    $artistSearchResults[$artistNum]=$artist;
                    $artistNum++;
                }
            }
#         }
#         close($fileHandle);
    }
}

$mpdDbUpdated=0;
$rulesChanged=1;
$includeRules;
$excludeRules;
$lastIncludeRules;
$lastExcludeRules;
$initialRead=1;
$rulesTimestamp=0;

# Determine if rules file has been updated
sub checkRulesChanged() {
    if ($initialRead==1) { # Always changed on first run...
        $rulesChanged=1;
        $initialRead=0;
    } elsif ( scalar(@lastIncludeRules)!=scalar(@includeRules) ||
              scalar(@lastExcludeRules)!=scalar(@excludeRules)) { # Different number of rules
        $rulesChanged=1;
    } else { # Same number of rules, so need to check if the rules themselves have changed or not...
        $rulesChanged=0;
        for (my $i=0; $i<scalar(@includeRules) && $rulesChanged==0; $i++) {
            if ($includeRules[$i] ne $lastIncludeRules[$i]) {
                $rulesChanged=1;
            }
        }
        for (my $i=0; $i<scalar(@excludeRules) && $rulesChanged==0; $i++) {
            if ($excludeRules[$i] ne $lastExcludeRules[$i]) {
                $rulesChanged=1;
            }
        }
    }
    @lastIncludeRules=@includeRules;
    @lastExcludeRules=@excludeRules;
}

# Add a rule to the list of rules that will be used to query MPD
sub saveRule() {
    my $rule=$_[0];
    my @dates=@{ $_[1] };
    my @artistList=@{ $_[2] };
    my @genreList=@{ $_[3] };
    my $ruleMatch=$_[4];
    my $isInclude=$_[5];
    my @type=();

    if ($isInclude == 1) {
        @type=@includeRules;
    } else {
        @type=@excludeRules;
    }

    # We iterate through the list of artists - so if this is empty, add a blank artist.
    # artistList will only be set if we have been told to find tracks by similar artists...
    if (scalar(@artistList)==0) {
        $artistList[0]="";
    }
    if (scalar(@genreList)==0) {
        $genreList[0]="";
    }
    my $ruleNum=scalar(@type);
    for my $genre (@genreList) {
        for my $artist (@artistList) {
            $line =~ s/\"//g;
            if (scalar(@dates)>0) { # Create rule for each date (as MPDs search does not take ranges)
                my $baseRule=$rule;
                foreach my $date (@dates) {
                    $type[$ruleNum]="${ruleMatch} ${baseRule} Date \"${date}\"";
                    if ($artist ne "") {
                        $type[$ruleNum]=$type[$ruleNum]." Artist \"${artist}\"";
                    }
                    if ($genre ne "") {
                        $type[$ruleNum]=$type[$ruleNum]." Genre \"${genre}\"";
                    }
                    $ruleNum++;
                }
            } elsif ($artist ne "" || $genre ne "" || $rule ne "") {
                $type[$ruleNum]="${ruleMatch} $rule";
                if ($artist ne "") {
                    $type[$ruleNum]=$type[$ruleNum]." Artist \"${artist}\"";
                }
                if ($genre ne "") {
                    $type[$ruleNum]=$type[$ruleNum]." Genre \"${genre}\"";
                }
                $ruleNum++;
            }
        }
    }
    if ($isInclude == 1) {
        @includeRules=@type;
    } else {
        @excludeRules=@type;
    }
}

# Read rules from ~/.cache/cantata/dynamic/rules
#  (or from ${filesDir}/rules in HTTP mode)
#
# File format:
#
#   Rule
#   <Tag>:<Value>
#   <Tag>:<Value>
#   Rule
#
# e.g.
#
#   Rule
#   AlbumArtist:Various Artists
#   Genre:Dance
#   Rule
#   AlbumArtist:Wibble
#   Date:1980-1989
#   Exact:false
#   Exclude:true
#
$activeFile="";
$activeLinksTo="";
sub readRules() {
    if ($activeFile eq "") {
        $activeFile=&baseDir();
        $activeFile="${activeFile}/rules";
    }

    unless (-e $activeFile) {
        $rulesChanged=0;
        return;
    }

    # Check if rules (well, the file it points to), has changed since the last read...
    my $currentActiveLink=abs_path($activeFile);
    $fileTime = stat($currentActiveLink)->mtime;
    if ($initialRead!=1 && $fileTime==$rulesTimestamp && $activeLinksTo eq $currentActiveLink) {
        # No change, so no need to read it again!
        $rulesChanged=0;
        return;
    }
    $activeLinksTo=$currentActiveLink;
    $rulesTimestamp=$fileTime;

    for(my $i=0; $i<10; $i++) {
        open(my $fileHandle, "<:encoding(utf8)", $activeFile);
        if (tell($fileHandle) != -1) {
            my @lines = <$fileHandle>; # Read into an array...
            my $ruleMatch="find";
            my @dates=();
            my @similarArtists=();
            my $isInclude=1;
            my $currentRule="";
            @includeRules=();
            @excludeRules=();
            close($fileHandle);
            foreach my $line (@lines) {
                if (! ($line=~ m/^(#)/)) {
                    $line =~ s/\n//g;
                    my $sep = index($line, ':');

                    if ($sep>0) {
                        $key=substr($line, 0, $sep);
                        $val=substr($line, $sep+1, length($line)-$sep);
                    } else {
                        $key=$line;
                        $val="";
                    }
                    if ($key=~ m/^(Rule)/) { # New rule...
                        if (length($currentRule)>1 || scalar(@similarArtists)>0 || scalar(@dates)>0 || scalar(@genres)>0) {
                            &saveRule($currentRule, \@dates, \@similarArtists, \@genres, $ruleMatch, $isInclude);
                        }
                        $currentRule="";
                        @dates=();
                        @similarArtists=();
                        @genres=();
                    } else {
                        if ($key eq "Date") {
                            my @dateVals = split("-", $val);
                            if (scalar(@dateVals)==2) {
                                my $fromDate=scalar($dateVals[0]);
                                my $toDate=scalar($dateVals[1]);
                                if ($fromDate > $toDate) { # Fix dates if from>to!!!
                                    my $tmp=$fromDate;
                                    $fromDate=$toDate;
                                    $toDate=$tmp;
                                }
                                my $pos=0;
                                for(my $d=$fromDate; $d<=$toDate; $d++) {
                                    $dates[$pos]=$d;
                                    $pos++;
                                }
                            } else {
                                @dates=($val)
                            }
                        } elsif ($key eq "Genre" && $val =~ m{\*}) {
                            # Wildcard genre - get list of genres from MPD, and find the ones that contain the genre string.
                            $val =~ s/\*//g;
                            my $socketData=&sendCommand("list genre");
                            my @mpdGenres = split("\n", $socketData);
                            my $pos=0;
                            foreach my $genre (@mpdGenres) {
                                $genre =~ s/Genre: //g;
                                if ($genre ne "OK" && $genre ne "" && $genre =~/$val/i) {
                                    $genres[$pos]=$genre;
                                    $pos++;
                                }
                            }
                        } elsif ($key eq "Artist" || $key eq "Album" || $key eq "AlbumArtist" || $key eq "Title" || $key eq "Genre") {
                            $currentRule="${currentRule} ${key} \"${val}\"";
                        } elsif ($key eq "SimilarArtists") {
                            &querySimilarArtists($val); # Perform a last.fm query to find similar artists
                            @artistSearchResults; # Save results of query
                            @artistSearchResults=uniq(@artistSearchResults);        # Ensure we only have unique entries...
                            if (scalar(@artistSearchResults)>1) {
                                my @mpdArtists = ();
                                my $pos=0;
                                # Get MPD artists...
                                my $socketData=&sendCommand("list artist");
                                my @mpdResponse=split("\n", $socketData);
                                foreach my $artist (@mpdResponse) {
                                    $artist =~ s/Artist: //g;
                                    if ($artist ne "OK" && $artist ne "" && $artist ne $val) {
                                        $mpdArtists[$pos]=$artist;
                                        $pos++;
                                    }
                                }

                                ## Get MPD album-artists...
                                #$socketData=&sendCommand("list albumartist");
                                #@mpdResponse=split("\n", $socketData);
                                #foreach my $artist (@mpdResponse) {
                                #    $artist =~ s/AlbumArtist: //g;
                                #    if ($artist ne "OK" && $artist ne "" && $artist ne $val) {
                                #        $mpdArtists[$pos]=$artist;
                                #        $pos++;
                                #    }
                                #}
                                @mpdArtists=uniq(@mpdArtists); 

                                # Now chec which last.fm artists MPD actually has...    
                                my $pos=0;
                                foreach my $artist (@artistSearchResults) {
                                    my @match = grep(/^$artist/i, @mpdArtists);
                                    if (scalar(@match)>0) {
                                        $similarArtists[$pos]=$artist;
                                        $pos++;
                                    }
                                }
                            }
                            $similarArtists[scalar(@similarArtists)]=$val; # Add ourselves!!!
                        } elsif ($key eq "Exact" && $val eq "false") {
                            $ruleMatch="search";
                        } elsif ($key eq "Exclude" && $val eq "true") {
                            $isInclude=0;
                        }
                    }
                }
            }

            if (length($currentRule)>1 || scalar(@similarArtists)>0 || scalar(@dates)>0 || scalar(@genres)>0) {
                &saveRule($currentRule, \@dates, \@similarArtists, \@genres, $ruleMatch, $isInclude);
            }

            if (1==$testMode) {
                print "INCLUDE--------------\n";
                foreach my $rule (@includeRules) {
                    print "${rule}\n";
                }
                print "EXCLUDE--------------\n";
                foreach my $rule (@excludeRules) {
                    print "${rule}\n";
                }
                print "---------------------\n"
            }

            &checkRulesChanged();
            return 1;
        }
        if (0==$isServerMode) {
            sleep 1;
        }
    }
    &checkRulesChanged();
    return 0;
}

# Remove duplicate entries from an array...
sub uniq {
    return keys %{{ map { $_ => 1 } @_ }};
}

# Send message to Cantata application - ued when run in server mode
sub sendServerMessage() {
    my $message=shift;
    if (length($message)<=0) {
        $message="STATUS:STATE:${currentStatus}\nRULES:${activeRules}\nTIME:${currentStatusTime}";
    }
    my $sock = IO::Socket::INET->new(Proto=>'udp', PeerAddr=>${mcastGroup}, PeerPort=>${mcastPort});
    if ($sock && $sock->connected()) {
        setsockopt($sock, IPPROTO_IP, IP_TTL, $mcastTtl);
        $sock->send("{CANTATA/${idString}}${message}");
        eval { close $sock; }; undef $sock;
    }
}

# Send message to Cantata application...
sub sendMessage() {
    my $method=shift;
    my $argument=shift;
    system("qdbus com.googlecode.cantata /cantata ${method} ${argument}");
    if ( $? == -1 ) {
        # Maybe qdbus is not installed? Try dbus-send...
        system("dbus-send --type=method_call --session --dest=com.googlecode.cantata /cantata com.googlecode.cantata.${method} string:${argument}");
    }
}

# Use rules to obtain a list of songs from MPD...
sub getSongs() {
    # If we have no current songs, or rules have changed, or MPD has been updated - then we need to run the rules against MPD to get song list...
    if (scalar(@mpdSongs)<1 || $rulesChanged==1 || $mpdDbUpdated==1) {
        my @excludeSongs=();
        if (scalar(@excludeRules)>0) {
            # Get list of songs that should be removed from the song list...
            my $mpdSong=0;
            foreach my $rule (@excludeRules) {
                my $socketData=&sendCommand($rule);
                if (defined($socketData)) {
                    my @lines = split('\n', $socketData);
                    foreach my $line (@lines) {
                        if ($line=~ m/^(file\:)/) {
                            my $sep = index($line, ':');
                            if ($sep>0) {
                                $excludeSongs[$mpdSong]=substr($line, $sep+2, length($line)-($sep+1));
                                $mpdSong++;
                            }
                        }
                    }
                }
                @excludeSongs=uniq(@excludeSongs);
            }
        }

        my %excludeSongSet = map { $_ => 1 } @excludeSongs;

        @mpdSongs=();
        my $mpdSong=0;
        if (scalar(@includeRules)>0) {
            foreach my $rule (@includeRules) {
                my $socketData=&sendCommand($rule);
                if (defined($socketData)) {
                    my @lines = split('\n', $socketData);
                        foreach my $line (@lines) {
                        if ($line=~ m/^(file\:)/) {
                            my $sep = index($line, ':');
                            if ($sep>0) {
                                my $song=substr($line, $sep+2, length($line)-($sep+1));
                                if (! $excludeSongSet{$song}) {
                                    $mpdSongs[$mpdSong]=$song;
                                    $mpdSong++;
                                }
                            }
                        }
                    }
                }
                @mpdSongs=uniq(@mpdSongs);
            }
        } else {
            # No 'include' rules => get all songs!
            my $socketData=&sendCommand("listall");
            if (defined($socketData)) {
                my @lines = split('\n', $socketData);
                foreach my $line (@lines) {
                    if ($line=~ m/^(file\:)/) {
                        my $sep = index($line, ':');
                        if ($sep>0) {
                            my $song=substr($line, $sep+2, length($line)-($sep+1));
                            if (! $excludeSongSet{$song}) {
                                $mpdSongs[$mpdSong]=$song;
                                $mpdSong++;
                            }
                        }
                    }
                }
            }
        }
        if (scalar(@mpdSongs)<1) {
            if (1==$isServerMode) {
                $currentStatus="NO_SONGS";
                &sendServerMessage();
            } else {
                &sendMessage("showError", "NO_SONGS");
                exit(0);
            }
        } elsif (1==$isServerMode) {
            $currentStatus="HAVE_SONGS";
            &sendServerMessage();
        }

        if (1==$testMode) {
            print "SONGS--------------\n";
            foreach my $song (@mpdSongs) {
                print "${song}\n";
            }
            print "---------------------\n"
        }
    }
}

#
# Following canAdd/storeSong are used to remember songs that have been added to the playqueue, so that
# we don't re-add them too soon!
#
@playQueueHistory=();
$playQueueHistoryLimit=0;
$playQueueHistoryPos=0;
sub canAdd() {
    my $file=shift;
    my $numSongs=shift;
    my $pqLimit=0;

    # Calculate a reasonable level for the history...
    if (1==$numSongs) {
        return 1;
    } elsif ($numSongs<5) {
        $pqLimit=int(($numSongs/2)+0.5);
    } else {
        $pqLimit=int(($numSongs*0.75)+0.5);
        if ($pqLimit>200) {
            $pqLimit=200;
        }
    }

    # If the history level has changed, then so must have the rules/mpd/whatever, so add this song anyway...
    if ($pqLimit != $playQueueHistoryLimit) {
        $playQueueHistoryLimit=$pqLimit;
        @playQueueHistory=();
        return 1;
    }

    my $size=scalar(@playQueueHistory);
    if ($size>$playQueueHistoryLimit) {
        $size=$playQueueHistoryLimit;
    }

    for (my $i=0; $i<$size; ++$i) {
        if ($playQueueHistory[$i] eq $file) {
            return 0;
        }
    }
    return 1;
}

sub storeSong() {
    my $file=shift;
    if ($playQueueHistoryLimit<=0) {
        $playQueueHistoryLimit=5;
    }

    if ($playQueueHistoryPos>=$playQueueHistoryLimit) {
        $playQueueHistoryPos=0;
    }
    $playQueueHistory[$playQueueHistoryPos]=$file;
    $playQueueHistoryPos++;
}

#
# This is the 'main' function of the dynamizer
#
sub populatePlayQueue() {
    &readConnectionDetails();
    my $lastMpdDbUpdate=-1;
    while (1) {
        if (0==$dynamicIsActive && 1==$isServerMode) {
            while (0==$dynamicIsActive) {
                if (0==&waitForEvent()) {
                    # TODO: Could not connect to MPD, and dynamic is not active, so wait??? Not sure aobut this...
                    sleep(2);
                }
            }
        }

        my $socketData='';
        if (1==$dynamicIsActive) {
             # Use status to obtain the current song pos, and to check that MPD is running...
            $socketData=&sendCommand("status");
        }
        if (defined($socketData)) {
            my @lines = split('\n', $socketData);
            my $playQueueLength=0;
            my $playQueueCurrentTrackPos=0;
            my $isPlaying=0;
            foreach my $val (@lines) {
                if ($val=~ m/^(song\:)/) {
                    my @vals = split(": ", $val);
                    if (scalar(@vals)==2) {
                        $playQueueCurrentTrackPos=scalar($vals[1]);
                    }
                } elsif ($val=~ m/^(state\:)/) {
                    my @vals = split(": ", $val);
                    if (scalar(@vals)==2 && $vals[1]=~ m/^(play)/) {
                        $isPlaying=1;
                    }
                }
            }

            # Call stats, so that we can obtain the last time MPD was updated.
            # We use this to determine when we need to refresh the searched set of songs
            $mpdDbUpdated=0;
            $socketData=&sendCommand("stats");
            if (defined($socketData)) {
                my @lines = split('\n', $socketData);
                foreach my $val (@lines) {
                    if ($val=~ m/^(db_update\:)/) {
                        my @vals = split(": ", $val);
                        if (scalar(@vals)==2) {
                            my $mpdDbUpdate=scalar($vals[1]);
                            if ($mpdDbUpdate!=$lastMpdDbUpdate) {
                                $lastMpdDbUpdate=$mpdDbUpdate;
                                $mpdDbUpdated=1;
                            }
                        }
                        break;
                    }
                }
            }

            # Get current playlist info
            $socketData=&sendCommand("playlist");
            if (defined($socketData)) {
                my @lines = split('\n', $socketData);
                my $playQueueLength=scalar(@lines);
                if ($playQueueLength>0 && $lines[$playQueueLength-1]=~ m/^(OK)/) {
                    $playQueueLength--;
                }

                # trim playlist start so that current becomes <=$PLAY_QUEUE_CURRENT_POS
                for (my $i=0; $i < $playQueueCurrentTrackPos - ($PLAY_QUEUE_CURRENT_POS-1); $i++) {
                    &sendCommand("delete 0");
                    $playQueueLength--;
                }
                if ($playQueueLength<0) {
                    $playQueueLength=0;
                }

                &readRules();
                &getSongs();
                my $numMpdSongs=scalar(@mpdSongs);
                if ($numMpdSongs>0) {
                    # fill up playlist to 10 random tunes
                    my $failues=0;
                    my $added=0;
                    while ($playQueueLength < $PLAY_QUEUE_DESIRED_LENGTH) {
                        my $pos=int(rand($numMpdSongs));
                        if ($failues > 100 || &canAdd(${mpdSongs[$pos]}, $numMpdSongs)) {
                            my $file=${mpdSongs[$pos]};
                            $file =~ s/\\/\\\\/g;
                            $file =~ s/\"/\\\"/g;
                            if (&sendCommand("add \"${file}\"") ne '') {
                                &storeSong(${mpdSongs[$pos]});
                                $playQueueLength++;
                                $failues=0;
                                $added++;
                            }
                        } else { # Song is already in playqueue history...
                            $failues++;
                        }
                    }
                    # If we are not currently playing and we filled playqueue - then play first!
                    if ($isPlaying==0 && $added==$PLAY_QUEUE_DESIRED_LENGTH) {
                        &sendCommand("play 0")
                    }
                }
                &waitForEvent();
            } elsif (0==$isServerMode) {
                sleep 2;
            }
        } elsif (0==$isServerMode) {
            sleep 2;
        }
    }
}

sub readPid() {
    my $fileName=shift;

    if (-e $fileName) {
        open(my $fileHandle, $fileName);
        my @lines = <$fileHandle>;
        close($fileHandle);
        if (scalar(@lines)>0) {
            my $pid=$lines[0];
            return scalar($pid);
        }
    }
    return 0;
}

sub daemonize() {
    my $fileName=shift;
    # daemonize process...
    chdir '/';
    umask 0;
    open STDIN,  '/dev/null'   or die "Can't read /dev/null: $!";
    open STDOUT, '>>/dev/null' or die "Can't write to /dev/null: $!";
    open STDERR, '>>/dev/null' or die "Can't write to /dev/null: $!";
    defined( my $pid = fork ) or die "Can't fork: $!";
    exit if $pid;

    # dissociate this process from the controlling terminal that started it and stop being part
    # of whatever process group this process was a part of.
    POSIX::setsid() or die "Can't start a new session.";

    # callback signal handler for signals.
    $SIG{INT} = $SIG{TERM} = $SIG{HUP} = \&signalHandler;
    $SIG{PIPE} = 'ignore';

    # Write our PID the lock file, so that 'stop' knows which PID to kill...
    open(my $fileHandle, ">${fileName}");
    print $fileHandle $$;
    close $fileHandle;
}

sub start() {
    my $pidFile=&lockFile();
    my $pid=&readPid($pidFile);
    if ($pid>0) {
        $exists = kill 0, $pid;
        if ($exists) {
            print "PROCESS $pid is running!\n";
            return;
        }
    }

    my $fileName=&lockFile();
    &daemonize($fileName);
    &sendMessage("dynamicStatus", "running");
    &populatePlayQueue();
}

sub signalHandler {
    if (0==$isServerMode) {
        unlink(&lockFile());
        &sendMessage("dynamicStatus", "stopped");
    } else {
        unlink($pidFile);
    }
    exit(0);
}

sub stop() {
    my $pidFile=&lockFile();
    my $pid=&readPid($pidFile);
    if ($pid>0) {
        system("kill", $pid);
        system("pkill", "-P", $pid);
    }
}

# #####################################
# HTTP SERVER MODE
#
#  GetID:            GET     http://host:port/id
#  GetStatus:        GET     http://host:port/status
#  ListPlaylists:    GET     http://host:port/list http://host:port/list?withDetails={1/0}
#  GetPlaylist:      GET     http://host:port/${playlist}
#  DeletePlaylist:   DELETE  http://host:port/${playlist}
#  SavePlaylist:     POST    http://host:port/save?name=${playlist}                            [BODY:Content]
#  SetActive:        POST    http://host:port/setActive?name=${playlist}&start={1/0}
#  ControlDynamizer: POST    http://host:port/control?state={start/stop}
#
# #####################################
$filesDir="/var/lib/mpd/dynamic";
$httpPort=6601;
$httpControlPage=0;
$pidFile="/var/run/cantata-dynamic/pid";

# Attempt to load a config file that will specify MPD connection settings and dynamic folder location
sub loadConfig() {
    my $config=shift;
    if (!$config || ($config=~ m/^(default)/)) {
        $config="/etc/cantata-dynamic.conf";
    }
    open(my $fileHandle, $config)  || die "ERROR: Failed to load config $config - $!\n";
    $activeFile="/var/run/cantata-dynamic/rules";
    if (tell($fileHandle) != -1) {
        my @lines = <$fileHandle>; # Read into an array...
        close($fileHandle);
        foreach my $line (@lines) {
            if (! ($line=~ m/^(#)/)) {
                $line =~ s/\n//g;
                my $sep = index($line, '=');

                if ($sep>0) {
                    $key=substr($line, 0, $sep);
                    $val=substr($line, $sep+1, length($line)-$sep);
                    if ($key=~ m/^(filesDir)/) {
                        $filesDir=$val;
                    } elsif ($key=~ m/^(activeFile)/) {
                        $activeFile=$val;
                    } elsif ($key=~ m/^(mpdHost)/) {
                        $mpdHost=$val;
                    } elsif ($key=~ m/^(mpdPort)/) {
                        $mpdPort=$val;
                    } elsif ($key=~ m/^(mpdPassword)/) {
                        $mpdPasswd=$val;
                    } elsif ($key=~ m/^(httpPort)/) {
                        $httpPort=$val;
                    } elsif ($key=~ m/^(httpControlPage)/) {
                        $httpControlPage=$val;
                    } elsif ($key=~ m/^(pidFile)/) {
                        $pidFile=$val;
                    } elsif ($key=~ m/^(id)/ && length($val)>0) {
                        $idString=$val;
                    } elsif ($key=~ m/^(msgPort)/) {
                        $mcastPort=$val;
                    } elsif ($key=~ m/^(msgGroup)/) {
                        $mcastGroup=$val;
                    } elsif ($key=~ m/^(msgTtl)/ && length($val)>0 && $val>0 && $val<128) {
                        $mcastTtl=$val;
                    }
                }
            }
        }
    }

    # Create folders, if these do not already exist...
    my $pidDir=dirname($pidFile);
    my $activeFileDir=dirname($activeFile);
    unless (-d $pidDir) {
        mkdir $pidDir or die;
    }
    unless (-d $activeFileDir) {
        mkdir $activeFileDir or die;
    }
    unless (-d $filesDir) {
        mkdir $filesDir or die;
    }
}

sub readRuleFile() {
    my @result=();
    my $fileName=uri_unescape(shift);
    open(my $fileHandle, $fileName);
    if (tell($fileHandle) != -1) {
        my @lines = <$fileHandle>; # Read into an array...
        close($fileHandle);
        foreach my $line (@lines) {
            if ($line =~ /\n$/) {
                push(@result, $line);
            } else {
                push(@result, $line."\n");
            }
        }
    }
    return @result;
}

sub listRules() {
    my @result=();
    my $showContents=shift;
    opendir(D, "$filesDir");
    while (my $f = readdir(D)) {
        if ($f=~m/.rules$/) {
            push(@result, "FILENAME:${f}\n");
            if ($showContents>0) {
                push(@result, &readRuleFile($filesDir."/".$f));
            }
        }
    }
    closedir(D);
    push(@result, "\nTIME:${currentStatusTime}");
    return @result;
}

sub determineActiveRules() {
    local $fileName="";
    if (-f $activeFile && -l $activeFile) {
        $fileName=basename abs_path($activeFile);
        $fileName =~ s/.rules//g;
    }
    return $fileName;
}

sub saveRulesToFile() {
    my $name=uri_unescape(shift);
    if (! $name) {
        return "ERROR: No name specified";
    }
    if ($name =~ m/\.rules/ || $name =~ m/\//) {
        return "ERROR: Invalid name";
    }
    my $content=shift;
    # TODO: Parse content!!!
    my $rulesName=$name;
    $rulesName="${filesDir}/${rulesName}.rules";

    open (my $fileHandle, '>'.$rulesName);
    if (tell($fileHandle) != -1) {
        print $fileHandle $content;
        close($fileHandle);
        $currentStatusTime = time;
        &sendServerMessage();
        return "OK";
    } else {
        return "ERROR: Failed to create file";
    }
}

sub deleteRules() {
    my $name=uri_unescape(shift);
    $name =~ s/\///g;
    my $active=&determineActiveRules();
    my $rulesName=$name;
    $rulesName="${filesDir}/${rulesName}.rules";
    if (!unlink($rulesName)) {
        return "ERROR: Failed to remove file";
    }
    $currentStatusTime = time;
    if ($name eq $active) {
        &control("stop");
    }
    &sendServerMessage();
    return "OK";
}

sub control() {
    my $command=shift;
    if ($command eq "start") {
        $dynamicIsActive=1;
        $currentStatus="STARTING";
        &sendCommand("clear");
        &sendServerMessage();
        return "OK";
    } elsif ($command eq "stop") {
        $dynamicIsActive=0;
        $currentStatus="IDLE";
        &sendServerMessage();
        return "OK";
    }
    return "ERROR: Invalid command";
}

sub setActiveRules() {
    my $name=uri_unescape(shift);
    if ($name eq "") {
        return "ERROR: No name supplied";
    }
    my $rulesName=$name;
    my $active=&determineActiveRules();
    if ($rulesName eq $active) {
        return "OK";
    }

    $rulesName="${filesDir}/${rulesName}.rules";
    if (-f $rulesName) {
        if (-l $activeFile) {
            if (!unlink($activeFile)) {
                return "ERROR: Failed to remove link";
            }
        } elsif (-f $activeFile) {
            return "ERROR: 'rules' is not a link";
        }
        system("ln -s \"${rulesName}\" \"${activeFile}\"");
        if (0!=$?) {
            return "ERROR: Failed to create 'rules' symlink";
        }
        &sendServerMessage();
        return "OK";
    } else {
        return "ERROR: Could not file ${name}";
    }
}

sub buildControlPage() {
    my $body="<html><head><title>Dynamic Playlists</title></head><body><h2>Dynamic Playlists</h2>"
             . "<p><i>Click on a playlist name to load</i></p>";

    my @rules=&listRules(0);
    my $active=&determineActiveRules();
    $body = $body . "<p><ul>";
    my $num=1;

    foreach my $rule (@rules) {
        $rule =~ s/FILENAME://;
        $rule =~ s/.rules//;
        $rule =~ s/\n//;
        if ($rule=~ m/^(TIME:)/) {
        } else {
            $body = $body . "<li>";
            if ($rule eq $active) {
                $body = $body . "<b>";
            }
            $body = $body . "<form name=\"ruleForm". ${num} ."\" method=\"post\" action=\"/setActive?name=" . $rule . "&start=1&showStartPage=1\">"
                            . "<a href=\"javascript: loadRule" .${num} ."()\">" .$rule ."</a></form>"
                            . "<script type=\"text/javascript\">function loadRule" .${num} ."() {  document.ruleForm". ${num} .".submit(); }</script>";
            if ($rule eq $active) {
                $body = $body . "</b>";
            }
            $body = $body ."</li>";
            $num=$num+1;
        }
    }
    $body = $body . "</ul></p>";
    if (1==$dynamicIsActive) {
        $body = $body . "<br/><p><form method=post enctype=\"text/plain\" action=\"/control?state=stop&showStartPage=1\">"
                      . "<input type=\"submit\" name=\"submit\" value=\"Stop Dynamizer\"></form></p>";
    }
    $body = $body . "</body></html>";
    return $body;
}

sub writeToClient() {
    my $client=shift;
    my $message=shift;
    my $addCrlf=shift;
    if ($client->connected()) {
        if (1==$addCrlf) {
            print $client $message, Socket::CRLF;
        } else {
            print $client $message;
        }
    }
}

sub httpServer() {
    my $server = new IO::Socket::INET(Proto => 'tcp', LocalPort => $httpPort, Listen => SOMAXCONN, Reuse => 1);
    $server or die "ERROR: Unable to create HTTP socket (${httpPort}): $!";

    print "Starting HTTP server on ${httpPort}\n";
    while (my $client = $server->accept()) {
        $client->autoflush(1);
        my $prevStatusTime=$currentStatusTime;
        my %request = ();
        my %data;
        local $/ = Socket::CRLF;
        while (<$client>) {
            chomp; # Main http request
            if (/\s*(\w+)\s*([^\s]+)\s*HTTP\/(\d.\d)/) {
                $request{METHOD} = uc $1;
                $request{URL} = $2;
                $request{HTTP_VERSION} = $3;
            } # Standard headers
            elsif (/:/) {
                (my $type, my $val) = split /:/, $_, 2;
                $type =~ s/^\s+//;
                foreach ($type, $val) {
                    s/^\s+//;
                    s/\s+$//;
                }
                $request{lc $type} = $val;
            } # POST data
            elsif (/^$/) {
                read($client, $request{CONTENT}, $request{'content-length'})
                    if defined $request{'content-length'};
                last;
            }
        }
        local $response="";
        local $responseType="text/plain";
        $queryItems{statusTime}=0;

        if ($request{URL} =~ /(.*)\?(.*)/) {
            $request{URL} = $1;
            $request{QUERY} = $2;
            my @args=split("&", $request{QUERY});
            for my $arg (@args) {
                (my $type, my $val) = split /=/, $arg, 2;
                $queryItems{$type}=$val;
            }
        }
#             print "${request{METHOD}} URL:${request{URL}} QUERY:${request{QUERY}}\n";
        if ($request{METHOD} eq 'GET') {
            if ($request{URL} eq '/status') {
                local $activeRules=&determineActiveRules();
                $response="STATE:${currentStatus}\nRULES:${activeRules}\nTIME:${currentStatusTime}\n";
            } elsif ($request{URL} eq '/id') {
                $response="ID:${idString}\nGROUP:${mcastGroup}\nPORT:${mcastPort}";
            } elsif ($request{URL} eq '/list') {
                $response = join('', &listRules($queryItems{withDetails}));
            } elsif ($request{URL} eq '/' && 1==$httpControlPage) {
                $responseType="text/html";
                $response = &buildControlPage();
            } else {
                $response="ERROR: Invalid URL";
            }
        } elsif ($request{METHOD} eq 'POST') {
            if ($request{URL} eq '/setActive') {
                my @args=split("&", $request{QUERY});

                $response=&setActiveRules($queryItems{name});
                if ($response eq "OK" && ($queryItems{start} eq "true" || $queryItems{start} eq "1")) {
                    $response=&control("start");
                }
            } elsif ($request{URL} eq '/save') {
                $response=&saveRulesToFile($queryItems{name}, $request{CONTENT});
            } elsif ($request{URL} eq '/control') {
                $response=&control($queryItems{state});
            }
        } elsif ($request{METHOD} eq 'DELETE') {
            $response=&deleteRules($request{URL});
        } elsif (1==$httpControlPage) {
            $responseType="text/html";
            $response = &buildControlPage();
        }

        if ($response eq "") {
            &writeToClient($client, "HTTP/1.0 404 Not Found", 1);
            &writeToClient($client, Socket::CRLF, 0);
            &writeToClient($client, "<html><body>404 Not Found</body></html>", 0);
        } elsif ($response =~ m/^ERROR/) {
            &writeToClient($client, "HTTP/1.0 404 Not Found", 1);
            &writeToClient($client, Socket::CRLF);
            &writeToClient($client, "<html><body>${response}</body></html>", 0);
        } elsif ($request{METHOD} eq 'POST') {
            &writeToClient($client, "HTTP/1.0 201 Created", 1);
            if ($queryItems{statusTime}!=0) {
                &writeToClient($client, "Content-type: ${responseType}", 1);
                &writeToClient($client, Socket::CRLF);
                if ($prevStatusTime > $queryItems{statusTime}) {
                    &writeToClient($client, "UPDATE_REQUIRED", 1);
                }
                &writeToClient($client, "TIME:".$currentStatusTime, 0);
            } elsif ($queryItems{showStartPage} eq "1") {
                # Reload start page :-)
                $response =  "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\">"
                            . "<html><head><meta http-equiv=\"REFRESH\" content=\"0;url=http://" . $request{host} . "\"></head></body></html>";
                &writeToClient($client, "Content-type: text/html", 1);
                &writeToClient($client, Socket::CRLF);
                &writeToClient($client, $response, 1);
            }
        } else {
            &writeToClient($client, "HTTP/1.0 200 OK", 1);
            &writeToClient($client, "Content-type: ${responseType}", 1);
            &writeToClient($client, Socket::CRLF);
            if ($queryItems{statusTime}!=0 && $response eq "OK") {
                &writeToClient($client, "OK", 1);
                if ($prevStatusTime > $queryItems{statusTime}) {
                    &writeToClient($client, "UPDATE_REQUIRED", 1);
                }
                &writeToClient($client, "TIME:".$currentStatusTime, 0);
            } else {
                &writeToClient($client, $response);
            }
        }
        close $client;
    }
}

sub serverMode() {
    &loadConfig(shift);
    &daemonize($pidFile);
    $isServerMode=1;
    $dynamicIsActive=0;
    threads->create(\&populatePlayQueue, 1);
    &httpServer();
    &sendServerMessage();
}

sub stopServer() {
    &loadConfig(shift);
    my $pid=&readPid($pidFile);
    if ($pid>0) {
        system("kill", $pid);
        system("pkill", "-P", $pid);
    }
    $currentStatus="TERMINATED";
    &sendServerMessage();
}

if ($ARGV[0] eq "start") {
    &start();
} elsif ($ARGV[0] eq "stop") {
    &stop();
} elsif ($ARGV[0] eq "server") {
    &serverMode($ARGV[1]);
} elsif ($ARGV[0] eq "stopserver") {
    &stopServer($ARGV[1]);
} elsif ($ARGV[0] eq "test") {
    $testMode=1;
    &populatePlayQueue();
} else {
    print "Cantata MPD Dynamizer script\n";
    print "\n";
    print "Usage: $0 start|stop|server|stopserver\n";
}
