/*
 *  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
 */

/*
 * NOTES: 
 * 
 * Need a way to update the song credits in format 2 playlists when
 * the sid file has been updated. One way to achieve this could be
 * to let the player do the comparison of credits found in playlist
 * and sid file and provide the playlist with an updated entry.
 */

#ifdef XSID_WB_DEBUG
#include <iomanip>
#endif
#include <fstream>
#include <iostream>
#include <cstdlib>
using namespace std;

#include <qdir.h>

#include "Playlist.h"
#include "wrapper/SidTuneWrapper.h"

const char* const Playlist::keys[] =
{
    "[SIDPLAY/Windows Playlist]",
    "Format", "Basedir",
    // Format 1
    "Filename", "Subsong",
    // Format 1 & 2
    "time",  // Format 1: "Time"  // but we do case-insensitive comparison
    // Format 2
    "file", "name", "author", "copyright", "songs", "subtune", "fadeout"
};

Playlist::Playlist()
: TextFile(), baseDir(""), saveFormat(2)
{
    list.setAutoDelete(true);
    clear();
}

void Playlist::lock()
{
    listMutex.lock();
}

void Playlist::unlock()
{
    listMutex.unlock();
}

void Playlist::setModified()
{
    modified = true;
}

bool Playlist::isModified() const
{
    return modified;
}

uint Playlist::getCurrentPlayPos() const
{
    return currentPlayPos;
}

void Playlist::clear()
{
    lock();
    list.clear();
    currentPlayPos = 0;
    modified = false;  // no one would want to save an empty list
    unlock();
}

void Playlist::add(PlaylistItem* item)
{
    lock();
    list.append(item);
    modified = true;
    unlock();
}

void Playlist::remove(uint pos)
{
    lock();
    list.remove(pos);
    modified = true;
    // Adjust current play position if necessary, so next
    // item to be played is correct.
    if (currentPlayPos >= pos)
        --currentPlayPos;
    unlock();
}

void Playlist::setCurrentPlayPos(uint pos)
{
    lock();
    if ( pos>=0 && pos<list.count() )
        currentPlayPos = pos;
    unlock();
}

void Playlist::setMaxBasePrefix( const QString& prefix )
{
    maxBasePrefix = prefix;
}

// Get rid of baseDir during list iteration.
// Prepend it to every file name. When the playlist gets
// saved, it will be tried to find a common baseDir again.
void Playlist::removeBaseDir()
{
    if ( list.isEmpty() || baseDir.isEmpty() )
        return;
    lock();
    QString tmpBaseDir = baseDir;
    baseDir = "";
    // This modification is not counted. It does not really
    // modify the playlist because the baseDir would be reconstructed
    // upon save(). Only add/delete/modify and other operations modify
    // the list contents.
    QListIterator<PlaylistItem> it(list);
    for ( it.toFirst(); it.current(); ++it ) 
    {
        PlaylistItem* item = it.current();
        item->fileNameString.prepend(tmpBaseDir);
    }
    unlock();
}

// Try to find a common 'baseDir' path prefix by examining all
// file names in the playlist and modifying them if necessary.
// If the maxBasePrefix argument is a sub-string of the found
// prefix, return maxBasePrefix (=> HVSC root hack).
void Playlist::findBaseDir()
{
    if ( list.isEmpty() )
        return;
    lock();
    QListIterator<PlaylistItem> it(list);
    QString tmpBase = it.toFirst()->fileNameString;
    ++it;
    // Determine a first path to start with.
    int lastSlashPos = tmpBase.findRev( QDir::separator() );
    if (lastSlashPos < 0)
        lastSlashPos = 0;  // slash only = no common path prefix
    tmpBase.truncate(lastSlashPos);
    if ( !tmpBase.isEmpty() )
    {
        // Now tmpBase is the string we try to find at the start
        // of each other file name. If we find it, we may be able
        // to determine a common path prefix. If we cannot find
        // it, we truncate the search string until nothing is left.
        for ( ; it.current(); ++it ) 
        {
            QString& s = it.current()->fileNameString;
            while ( !s.startsWith(tmpBase) && !tmpBase.isEmpty() )
            {
                // Cut off one directory name.
                lastSlashPos = tmpBase.findRev( QDir::separator() );
                if (lastSlashPos < 0)
                    lastSlashPos = 0;
                tmpBase.truncate(lastSlashPos);
            };
        }
        // Prefix length hack.
        if ( tmpBase.startsWith(maxBasePrefix) )
        {
            tmpBase = maxBasePrefix;
        }
        // Now remove the common path prefix from every file name.
        if ( !tmpBase.isEmpty() )
        {
            for ( it.toFirst(); it.current(); ++it ) 
            {
                it.current()->fileNameString.remove(0,tmpBase.length() );
            }
        }
        if ( baseDir!=tmpBase )
        {
            modified = true;
            baseDir = tmpBase;
        }
    }
    unlock();
}

void Playlist::fixMissingInfo()
{
    if ( list.isEmpty() )
        return;
    lock();
    SidTuneWrapper* mySidLoader = new SidTuneWrapper;  // unchecked alloc
    QListIterator<PlaylistItem> it(list);
    for ( it.toFirst(); it.current(); ++it ) 
    {
        PlaylistItem* item = it.current();
        if ( mySidLoader->open(item->fileNameString) )
        {
            // Copy sid info from file.
            item->titleString = mySidLoader->getInfoString(0);
            item->authorString = mySidLoader->getInfoString(1);
            item->copyrightString = mySidLoader->getInfoString(2);
            item->songs = mySidLoader->getSongs();
        }
        else
        {
            // Sidtune not found. Something is terribly bad!
            // Insert some defaults. However, this will not
            // help in case of corrupted format=1 playlists.
            // But who uses that outdated format anyway?
            item->titleString = item->fileNameString;
            item->authorString = item->copyrightString = "";
            item->songs = item->subtune;  // better than nothing
        }
        setModified();
    }
    delete mySidLoader;
    unlock();
}

bool Playlist::haveFileName()
{
    return ( !fileName.isEmpty() );
}

bool Playlist::load(const QString& newFileName)
{
    lock();
    list.clear();
    baseDir = "";
    
    fileName = newFileName;
    open(fileName);
    
    bool haveID = false;
    loadFormat = -1;

    bool haveItem = false;
    PlaylistItem* item;
    
    while (status && isGood && !isEOF())  // line-by-line loop
    {
        getLine();
        // Skip blank and comment lines.
        while (status && !isEOF() && isBlank() || isComment())
        {
            getLine();
        };
        if ( isEOF() )
            break;
#ifdef XSID_WB_DEBUG
        cout << "Line " << getLineNum() << ", " << getLineLen() << ": ";
        cout << getLineBuf() << endl;
        cout << "ParseBuf: " << getParseBuf() << endl;
#endif
        // Evaluate line.
        if ( !haveID && isValue(keys[group_id],false) )
            haveID = true;
        else
        {
            if ( isKey(keys[key_format]) )
                loadFormat = atoi(getCurParseBuf());
            else if ( isKey(keys[key_baseDir]) )
                baseDir = getCurLineBuf();
            
            else if ( isKey(keys[key_file],false) ||
                      (loadFormat==1 && isKey(keys[key_fileName],false)) )
            {
                if (haveItem)
                {
                    list.append(item);
                }
                item = new PlaylistItem();
                haveItem = true;

                if ( isKey(keys[key_file]) )
                    item->fileNameString = getCurLineBuf();
                else if ( isKey(keys[key_fileName]) )
                    item->fileNameString = getCurLineBuf();
#ifdef XSID_WB_DEBUG
                cout << item->fileNameString << endl;
#endif
                // Incomplete: Convert MS Windows file names.
                // Remove driveletter prefix, e.g. C:
                if ( baseDir.isEmpty() &&
                     item->fileNameString[1]==':' &&
                     item->fileNameString[2]=='\\')
                {
                    item->fileNameString.remove(0,2);
                }
                // Convert separators.
                for (uint i=0; i<item->fileNameString.length(); i++)
                {
                    if (item->fileNameString[i] == '\\')
                        item->fileNameString[i] = QDir::separator();
                }
                if ( loadFormat==1 )
                {
                    // Format 1 has less file-specific information stored
                    // in the playlist. We need to fix up these values
                    // later when we are sure we have a good path prefix.
                    item->titleString = "";
                    item->authorString = "";
                    item->copyrightString = "";
                    item->songs = 0;
                    item->fadeout = 0;
                }
#ifdef XSID_WB_DEBUG
                cout << item->fileNameString << endl;
#endif
            }
            else if ( isKey(keys[key_name]) )
                item->titleString = getCurLineBuf();
            else if ( isKey(keys[key_author]) )
                item->authorString = getCurLineBuf();
            else if ( isKey(keys[key_copyright]) )
                item->copyrightString = getCurLineBuf();
            else if ( isKey(keys[key_songs]) )
                item->songs = atoi( getCurParseBuf() );
            else if ( isKey(keys[key_subtune]) )
                item->subtune = atoi( getCurParseBuf() );
            else if ( isKey(keys[key_fadeout]) )
                item->fadeout = atoi( getCurParseBuf() );
            else if ( isKey(keys[key_time]) )
                item->time = atoi( getCurParseBuf() );
            
            else if ( loadFormat==1 && isKey(keys[key_subsong]) )
                item->subtune = atoi( getCurParseBuf() );
        }
    };
    
    if (haveItem)  // this takes care of the last item
    {
        list.append(item);
    }

    close();
    unlock();
    return ( isGood && haveID && loadFormat>=1 && loadFormat<=2 );
}

bool Playlist::save(const QString& newFileName)
{
    fileName = newFileName;
    return save();
}

bool Playlist::save()
{
    findBaseDir();
    
    lock();
    bool wasSuccess = false;

#ifdef XSID_HAVE_IOS_BIN
    ofstream toFile(fileName,ios::out|ios::bin|ios::trunc);
#else
    ofstream toFile(fileName,ios::out|ios::binary|ios::trunc);
#endif
    if ( !toFile.fail() )
    {
        toFile 
            << ";" << endl
            << "; Playlist" << endl
            << "; Saved with SIDPLAY/Linux (X11)." << endl
            << ";" << endl
            << endl;

        toFile << keys[group_id] << endl;
        writeKey(toFile,keys[key_format]) << saveFormat << endl;
        if ( !baseDir.isEmpty() )
            writeKey(toFile,keys[key_baseDir]) << baseDir << endl;
        toFile << endl;
        
        for (unsigned int n=0; n<list.count(); n++)
        {
            writeKey(toFile,keys[key_file]) << list.at(n)->fileNameString << endl;
            writeKey(toFile,keys[key_name]) << list.at(n)->titleString << endl;
            writeKey(toFile,keys[key_author]) << list.at(n)->authorString << endl;
            writeKey(toFile,keys[key_copyright]) << list.at(n)->copyrightString << endl;
            writeKey(toFile,keys[key_songs]) << list.at(n)->songs << endl;
            writeKey(toFile,keys[key_subtune]) << list.at(n)->subtune << endl;
            if (list.at(n)->time != 0)
                writeKey(toFile,keys[key_time]) << list.at(n)->time << endl;
            if (list.at(n)->fadeout != 0)
                writeKey(toFile,keys[key_fadeout]) << list.at(n)->fadeout << endl;
            toFile << endl;
        }
             
        toFile.close();
        wasSuccess = !toFile.fail();
        
        if ( wasSuccess )
            modified = false;
    }
    unlock();
    
    removeBaseDir();
    return wasSuccess;
}
