#!/usr/bin/perl -w
#
# Sorune (Synchronization Manager for Neuros)
# Copyright (C) 2004-2005 Darren Smith
# All Rights Reserved.
#
# 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; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
#
# e-mail: sorune2004@yahoo.com

use strict;
use File::Basename;
use File::Find;
use File::Path;
use File::Spec;
use File::Glob qw(:globally :case);
use FileHandle;
use Getopt::Long;
use Digest::MD5;
use Encode;

BEGIN {
    push @INC, dirname(File::Spec->rel2abs($0));
    if ($^O eq 'MSWin32') {
        require Win32::Process;
        require Win32API::File;
        require Win32::DriveInfo;
        require Win32::Registry;
        Win32::Process->import('DETACHED_PROCESS');
        Win32API::File->import('DRIVE_FIXED','DRIVE_REMOVABLE','DRIVE_REMOTE');
    } else {
        require I18N::Langinfo;
        I18N::Langinfo->import('langinfo','CODESET');
    }
    require "mainlib.pm";
    require "guilib.pm";
    require "ndblib.pm";
    require "ogg.pm";
    require "mp3.pm";
    require "mp3frame.pm";
    require "wma.pm";
    require "wavtools.pm";
    require "wavread.pm";
    require "wav.pm";
    MP3::Info::use_mp3_utf8(1);
    MP3::Info::use_winamp_genres();
}

$|=1;

# Start gui if no options are given
if (@ARGV == 0) {
    push @ARGV, "--gui";
}

# Process arguments
my %args = ();
my $cfgFile = undef;
my @addDirs = ();
my @delDirs = ();
my @listDirs = ();

my $stat = GetOptions(\%args, "add=s" => \@addDirs, "backup", "gui",
    "clear=s", "delete=s" => \@delDirs, "duplicates", "export=s", "file=s",
    "help", "import=s", "info", "list=s" => \@listDirs, "new", "rebuild",
    "rebuild_full", "reset", "restore", "sync", "validate", "version");

if (!$stat) { 
    usage();
    exit 1;
}
if (defined $args{'help'}) { 
    usage();
    exit 0;
}
if (defined $args{'version'}) { 
    version();
    exit 0;
}
if (defined $args{'file'}) {
    $cfgFile = $args{'file'};
}
if (defined $args{'gui'}) {
    exit soruneGUI($cfgFile);
}

# CLI Globals
my $musicHome;
my %musicDb = ();
my %playlistDb = ();
my %recordDb = ();
my %cfg = ();
my @findFiles = ();
my $startTime = time;
my $neurosDrive = "C:";

# Find and read configuration file
if (readSoruneCfg($cfgFile,\%cfg) == -1) {
    message('ERR',"Configuration file not found.\n");
    exit 1;
}

if (defined $cfg{'general'}{'musichome'}) {
    $musicHome = File::Spec->canonpath($cfg{'general'}{'musichome'});
    $musicHome =~ s/\\/\//g;
}

# Find Neuros home
my $lastLocation = undef;
if (defined $cfg{'general'}{'lastlocation'}) {
    $lastLocation = $cfg{'general'}{'lastlocation'};
}
my ($neurosHome,$mainSerial,$fwVersion) = locateNeuros($lastLocation);
if (!defined $neurosHome) {
    message('ERR',"Could not locate Neuros. USB cable connected?\n");
    exit 1;
} else {
    message('INFO',"Neuros located at $neurosHome\n");
    message('INFO',"  Serial        : $mainSerial\n");
    message('INFO',"  Firmware      : $fwVersion\n");
    if (!-f "$neurosHome/bkpk.sn") { $neurosDrive = "D:"; }
    $cfg{'general'}{'lastlocation'} = $neurosHome;
    writeSoruneCfg(\%cfg);
}

my $neurosDbHome = "$neurosHome/woid_db/audio";
my $neurosMusicHome = "$neurosHome/music";
my $neurosRecordHome = "$neurosHome/WOID_RECORD";
my $soruneHome = "$neurosHome/sorune";
my $soruneDb = "$neurosHome/sorune/sorune.db";
eval { require Compress::Zlib; };
if (!$@) { $soruneDb .= ".gz"; }

# backup database and exit
if (defined $args{'backup'}) { 
    my $status = backupDb("$neurosHome/woid_db","$soruneHome/database");
    printf("Elapsed time: %s\n",getTime(time - $startTime));
    exit $status;
}

# restore database and exit
if (defined $args{'restore'}) {
    my $status = restoreDb("$soruneHome/database","$neurosHome/woid_db");
    printf("Elapsed time: %s\n",getTime(time - $startTime));
    exit $status;
}

# Reset (remove all files from) the Neuros and exit
if (defined $args{'reset'}) {
    if (queryUser("This will remove all files from the Neuros located at $neurosHome.")) {
        resetNeuros(\%musicDb,\%playlistDb,\%recordDb,\%cfg,
            $neurosHome,$fwVersion);
        writeSoruneDb($soruneDb,\%musicDb,\%playlistDb,\%recordDb,\%cfg);
    }
    printf("Elapsed time: %s\n",getTime(time - $startTime));
    exit 0;
}

# Create new database and exit
if (defined $args{'new'}) {
    createNAM(\%cfg,\%musicDb,\%playlistDb,\%recordDb,$neurosDbHome,
        $fwVersion,undef);
    writeSoruneDb($soruneDb,\%musicDb,\%playlistDb,\%recordDb,\%cfg);
    printf("Elapsed time: %s\n",getTime(time - $startTime));
    exit 0;
}

# Read in existing sorune database
my $dbSize = 0;
if (-r $soruneDb and !defined $args{'rebuild_full'}) {
    if (defined $args{'import'}) {
        importDb($args{'import'},\%musicDb,\%playlistDb,\%recordDb,\%cfg,
            $neurosHome,$musicHome,$neurosDrive);
    } else {
        readSoruneDb($soruneDb,\%musicDb,\%playlistDb,\%recordDb);
    }
    $dbSize = getSize(getMusicSize(\%musicDb,\%recordDb));
    message('INFO',"  Used capacity : $dbSize\n");
}

if (defined $args{'duplicates'}) {
    message("INFO","Searching for duplicates...\n");
    my @fields = ('artist','title');
    my @dups = findDuplicates(\%musicDb,$neurosHome,$neurosDrive,\@fields);
    if (scalar @dups) {
        message("INFO","Possible duplicates found:\n");
        foreach my $key (sort {
            $musicDb{$a}{'title'} cmp $musicDb{$b}{'title'}
        } @dups) {
            (my $pkey = $key) =~ s/$neurosDrive/$neurosHome/;
            message("INFO","  $pkey\n");
        }
    } else {
        message("INFO","No duplicates found.\n");
    }
    printf("Elapsed time: %s\n",getTime(time - $startTime));
    exit 0;
}

# Already printed everything, just exit
if (defined $args{'info'}) { 
    printf("Elapsed time: %s\n",getTime(time - $startTime));
    exit 0;
}

# Correct any database problems
if (defined $args{'validate'}) {
    my $changes = validateSoruneDb(\%musicDb,\%playlistDb,\%recordDb,\%cfg,
        $neurosMusicHome,$neurosHome,$neurosDrive);
    relate(\%musicDb,\%playlistDb);
    if ($changes > 0) {
        message('INFO',"Corrected $changes entries.\n");
        writeSoruneDb($soruneDb,\%musicDb,\%playlistDb,\%recordDb,\%cfg);
    } else {
        message('INFO',"Database appears valid.\n");
    }
    printf("Elapsed time: %s\n",getTime(time - $startTime));
    exit 0;
}

# Export the database to a tab delimited file
if (defined $args{'export'}) {
    my $file = $args{'export'};
    if (-d $file or (-e $file and !queryUser(
        "File $file will be overwritten."))) { exit 1; }
    message('INFO',"Exporting the Sorune database\n");
    my $stat = exportDb($file,\%musicDb,\%recordDb);
    printf("Elapsed time: %s\n",getTime(time - $startTime));
    exit $stat;
}

# Process list directives
foreach my $dir (@listDirs) {
    my @list = ();
    if (lc($dir) eq "albums") {
        message('INFO',"Albums:\n");
        @list = list(\%musicDb,\%recordDb,$neurosHome,"album");
    } elsif (lc($dir) eq "artists") {
        message('INFO',"Artists:\n");
        @list = list(\%musicDb,\%recordDb,$neurosHome,"artist");
    } elsif (lc($dir) =~ "genres") {
        message('INFO',"Genres:\n");
        @list = list(\%musicDb,\%recordDb,$neurosHome,"genre");
    } elsif (lc($dir) eq "playlists") {
        message('INFO',"Playlists:\n");
        foreach my $list (keys %playlistDb) {
            push @list, $list;
        }
    } elsif (lc($dir) eq "titles") {
        message('INFO',"Titles:\n");
        @list = list(\%musicDb,\%recordDb,$neurosHome,"title");
    } elsif (lc($dir) eq "add") {
        message('INFO',"To be added:\n");
        my @list1 = ();
        @list = list(\%musicDb,\%recordDb,$neurosHome,"add");
        foreach my $file (@list1) {
            push @list,$musicDb{$file}{'localFile'};
        }
    } elsif (lc($dir) eq "delete") {
        message('INFO',"To be deleted:\n");
        my @list1 = ();
        @list = list(\%musicDb,\%recordDb,$neurosHome,"delete");
        foreach my $file (@list1) {
            (my $neurosFile = $file) =~ s/$neurosDrive/$neurosHome/i;
            push @list,$neurosFile;
        }
    } else {
        message('ERR',"Invalid list command \"$dir\", skipping.\n");
        next;
    }
    if (scalar @list) {
        foreach my $item (sort @list) {
            message('INFO',"  $item\n");
        }
    } else {
        message('INFO',"  None\n");
    }
    printf("Elapsed time: %s\n",getTime(time - $startTime));
    exit 0;
}

# Process clear directive
if (defined $args{'clear'}) {
    foreach my $file (keys %musicDb) {
        if (lc($args{'clear'}) eq "add" or
            lc($args{'clear'}) eq "all") {
            if (defined $musicDb{$file}{'add'}) {
                (my $localFile = $file) =~ s/$neurosDrive/$neurosHome/i;
                if (-r $localFile) {
                    delete $musicDb{$file}{'add'};
                } else {
                    delete $musicDb{$file};
                }
            }
        }
        if (lc($args{'clear'}) eq "delete" or
            lc($args{'clear'}) eq "all") {
            if (defined $musicDb{$file}{'delete'}) {
                delete $musicDb{$file}{'delete'};
            }
        }
    }
}

# Process delete directives
foreach my $dir (@delDirs) {
    (my $name = $dir) =~ s/.*? //;
    if ($dir =~ /^Artist /i) {
        delByName(\%musicDb,\%playlistDb,'artist',$name,$neurosHome);
    } elsif ($dir =~ /^Album /i) {
        delByName(\%musicDb,\%playlistDb,'album',$name,$neurosHome);
    } elsif ($dir =~ /^Genre /i) {
        delByName(\%musicDb,\%playlistDb,'genre',$name,$neurosHome);
    } elsif ($dir =~ /^Title /i) {
        delByName(\%musicDb,\%playlistDb,'title',$name,$neurosHome);
    } elsif ($dir =~ /^Playlist /i) {
        (my $list = $dir) =~ s/^Playlist //i;
        delPlaylist(\%musicDb,\%playlistDb,$neurosHome,$list,1);
    } else {
        message('ERR',"Invalid deletion command \"$dir\", skipping.\n");
    }
}

# Process add directives
foreach my $dir (@addDirs) {
    $dir = File::Spec->rel2abs($dir);
    if ($^O eq "MSWin32" and $dir !~ /^[A-Z]:/i) {
        (my $drive = getcwd()) =~ s/^([A-Z]:).*/$1/i;
        $dir = "$drive$dir";
    }
    if (-r $dir) {
        @findFiles = ();
        my @options = (no_chdir => 1);
        if ($^O ne "MSWin32") { push @options, (follow => 1); }
        find({wanted => sub {
            if (/\.ogg$/i or /\.mp3$/i or /\.wma$/i or /\.wav$/i or /\.m3u$/i) {
                if (defined $cfg{'general'}{'recordingshome'}) {
                    if ($File::Find::dir !~
                        /^$cfg{'general'}{'recordingshome'}/) {
                        push @findFiles, $File::Find::name;
                    }
                } else {
                    push @findFiles, $File::Find::name;
                }
            }
        }, @options}, $dir);
        my $fileCount = scalar @findFiles;
        for (my $i = 0; $i < $fileCount; $i++) {
            if ($i % 10 == 0) {
                printf("Adding files to Sorune database %d%%\r",
                    $i*100/$fileCount);
            }
            if ($findFiles[$i] =~ /^$neurosHome/) { next; }
            addFile($findFiles[$i],$musicHome,$neurosHome,\%musicDb,
                \%playlistDb,\%cfg,$neurosDrive,0);
        }
        if (scalar @findFiles) {
            message('INFO',"Adding files to Sorune database 100%\n");
        }
    }
}

# All music files/playlists added, relate the two
if (scalar @addDirs and scalar keys %playlistDb) {
    message('INFO',"Relating files and playlists\n");
    relate(\%musicDb,\%playlistDb);
}

# Rebuild the Sorune database
if (defined $args{'rebuild'} or defined $args{'rebuild_full'}) {
    message('INFO',"Rebuilding the Sorune database\n");
    # Add music
    if (-d $neurosMusicHome) {
        @findFiles = ();
        find({no_chdir => 1, wanted => sub {
            if (/\.ogg$/i or /\.mp3$/i or /\.wma$/i or /\.wav$/i or /\.m3u$/i) {
                push @findFiles, $File::Find::name;
            }
        }}, $neurosMusicHome);
        my $fileCount = scalar @findFiles;
        for (my $i = 0; $i < $fileCount; $i++) {
            if ($i % 10 == 0) {
                my $text =
                    sprintf("Adding music files to Sorune database %d%%\r",
                    $i*100/$fileCount);
                message('INFO',$text);
            }
            addFile($findFiles[$i],$neurosMusicHome,$neurosHome,\%musicDb,
                \%playlistDb,\%cfg,$neurosDrive,0);
        }
        if (scalar @findFiles) {
            message('INFO',"Adding music files to Sorune database 100%\n");
        }
    }

    # Remove music files that no longer exist
    my $found;
    foreach my $file (keys %musicDb) {
        $found = 0;
        foreach my $foundFile (@findFiles) {
            $foundFile =~ s/$neurosHome/$neurosDrive/;
            if ($file eq $foundFile) {
                $found = 1;
                last;
            }
        }
        if (!$found) { delete $musicDb{$file}; }
    }
}

# Synchronize
if (defined $args{'sync'} or defined $args{'rebuild'} or
    defined $args{'rebuild_full'}) {
    my $freeSpace = getFreeSpace($neurosHome);
    my $xferSize = getXferSize(\%musicDb,\%recordDb);

    # subtract 16MB from free space to account for databases/playlists
    if (($freeSpace - (16 * 1024 * 1024))  < $xferSize) {
        message('ERR',"Not enough free space on Neuros to complete sync, aborting.\n");
    } else {
        my $xferTime = time;
        my $xferBytes = syncFiles(\%musicDb,\%playlistDb,\%recordDb,\%cfg,
            $neurosHome);
        $xferTime = time - $xferTime;
        if ($xferBytes and $xferTime) {
            printf("Transferred : %s\n",getSize($xferBytes));
            printf("Transfer Rate : %s/s\n",getSize($xferBytes/$xferTime));
        }

        # Handle recordings
        syncRecordings(\%recordDb,\%cfg,$neurosHome,$neurosDrive,undef,undef);
        if (defined $cfg{'general'}{'duplicates'} and
            $cfg{'general'}{'duplicates'} eq '1') {
            message('INFO',"Checking for duplicate album names.\n");
            fixDuplicateAlbums(\%musicDb);
        }
        message('INFO',"Creating Neuros databases.\n");
        createNAM(\%cfg,\%musicDb,\%playlistDb,\%recordDb,$neurosDbHome,
            $fwVersion,undef);
        message('INFO',"Exporting playlists.\n");
        createPlaylists($neurosHome,\%musicDb,\%playlistDb);
    }
}

# Write out the sorune database
message('INFO',"Writing the Sorune database\n");
writeSoruneDb($soruneDb,\%musicDb,\%playlistDb,\%recordDb,\%cfg);

my $elapsedTime = time - $startTime;
printf("Total elapsed time: %s\n",getTime($elapsedTime));
