/*----------------------------------------------------------------------------

   libtunepimp -- The MusicBrainz tagging library.  
                  Let a thousand taggers bloom!
   
   Copyright (C) Robert Kaye 2003
   
   This file is part of libtunepimp.

   libtunepimp 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.

   libtunepimp 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 libtunepimp; if not, write to the Free Software
   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

   $Id: analyzer.cpp 7868 2006-06-17 15:28:23Z luks $

----------------------------------------------------------------------------*/
#ifdef WIN32
#if _MSC_VER == 1200
#pragma warning(disable:4786)
#endif
#endif

#include <stdio.h>
#ifndef WIN32
#include <unistd.h>
#endif
#include "ofa1/ofa.h"
#include "../config.h"
#include "analyzer.h"
#include "tunepimp.h"
#include "watchdog.h"
#include "protocol.h"

const long chunkSize = 8192;

// supposedly we only need 130 seconds, but I will add a little extra in case we have silence at the beginning
const int PUIDDecodeSeconds = 135;

#define DB printf("%s:%d\n", __FILE__, __LINE__);

//---------------------------------------------------------------------------

Analyzer::Analyzer(TunePimp       *tunePimpArg,
                   Plugins        *plugins, 
                   FileCache      *cacheArg,
                   WatchdogThread *watchdog) : Thread()
{
   tunePimp = tunePimpArg;
   this->plugins = plugins;
   cache = cacheArg;
   dog = watchdog;
   exitThread = false;
   sem = new Semaphore();
}

//---------------------------------------------------------------------------

Analyzer::~Analyzer(void)
{
   exitThread = true;
   sem->signal();
   join();
   delete sem;
}

//---------------------------------------------------------------------------

void Analyzer::wake(void)
{
    sem->signal();
}

//---------------------------------------------------------------------------

void Analyzer::threadMain(void)
{
    string  fileName, status, puid;
    Track  *track;

    dog->setAnalyzerThread(getId());
    setPriority(tunePimp->context.getAnalyzerPriority());
    for(; !exitThread;)
    {
        unsigned long  duration = 0;
        char          *ptr;
        Plugin        *plugin;
        PUIDResult     ret = eOtherError;
        puid = "";

        track = cache->getNextItem(ePending);
        if (track == NULL)
        {
           sem->wait();
           continue;
        }

        track->lock();

        dog->setAnalyzerTask(cache->getFileIdFromTrack(track));
        
        track->getFileName(fileName);

        // Check to see if we pulled up a musicbrainz id
        Metadata data, id3;
        track->getServerMetadata(data);
        track->getLocalMetadata(id3);
        track->getPUID(puid);
        if (puid == string(ANALYZER_REDO_STRING))
        {
            data.clear();
            data.trackId = "";
            track->setServerMetadata(data);

            track->getLocalMetadata(id3);
            id3.trackId = "";
            track->setLocalMetadata(id3);
        }

        ptr = strrchr((char *)fileName.c_str(), '.');
        if (ptr)
            plugin = plugins->get(string(ptr), TP_PLUGIN_FUNCTION_DECODE);
        else
            plugin = NULL;

        if (plugin)
        {
            string err;

            track->unlock();
            status = "Analyzing " + fileName;
            tunePimp->setStatus(status);
            ret = calculatePUID(plugin, fileName, err, puid, duration, id3);
            track->lock();

            // Check to make sure no one has messed with this track since we started
            if (track->getStatus() == ePending)
            {
                if (duration > 0)
                {
                    Metadata fileData;

                    track->getLocalMetadata(fileData);
                    fileData.duration = duration;
                    track->setLocalMetadata(fileData);
                }

                if (ret != eOk)
                {
                    if (ret == eNoPUID)
                        track->setStatus(eUnrecognized);
                    else
                    {
                        tunePimp->setStatus("Failed to generate puid from " + fileName);
                        if (err.empty())
                            setError(track, ret);
                        else
                            track->setError(err);
                        track->setStatus(eError);
                    }
                    tunePimp->setStatus(string(" "));
                }
                else
                {
                    track->setPUID(puid);
                    track->setStatus(ePUIDLookup);
                }
            }
        }
        else
        {
            string err;

            if (ptr)
                err = string("Fingerprinting of ") + string(ptr) + (" files is not supported.");
            else
                err = string(fileName) + string(": cannot determine filetype.");
            tunePimp->setStatus(err);
            track->setStatus(eError);
            track->setError(err);
        }
        track->unlock();
        tunePimp->wake(track);
        cache->release(track);
        dog->setAnalyzerTask(-1);
    }
}

//---------------------------------------------------------------------------

void Analyzer::setError(Track *track, PUIDResult retVal)
{
    switch(retVal)
    {
        case eFileNotFound:
           track->setError("Audio file not found.");
           break;
        case eDecodeError:
           track->setError("Cannot decode audio file.");
           break;
        case eCannotConnect:
           track->setError("Cannot connect to the PUID signature server.");
           break;
        case eOutOfMemory:
           track->setError("Out of memory while creating acoustic fingerprint.");
           break;
        case eNoClientId:
           track->setError("No MusicDNS client id was provided.");
           break;
        case eNoPUID:
           track->setError("There is no PUID available for this track.");
           break;
        default:
           track->setError("Unknown error. Sorry, this program sucks.");
           break;
    }
}

//---------------------------------------------------------------------------

PUIDResult Analyzer::calculatePUID(Plugin *plugin, const string &fileName, string &err, 
                                   string &puid, unsigned long &duration, Metadata &mdata)
{
    PUIDResult  ret = eOk;
    void       *decode;
    string      print;

    if (tunePimp->context.getMusicDNSClientId().length() == 0)
        return eNoClientId;

    // The only thing the try block should catch is something
    // we know nothing about or a segfault and even then only
    // under windows. Thus, exit the thread and let the
    // watchdog skip this file and start a new analyzer.
    try
    {
        unsigned int samplesPerSecond, bitsPerSample, channels;
        int      numRead;
        string   proxyServer, encoding;
        int      flags = 0;

        encoding = tunePimp->context.getFileNameEncoding();

        decode = plugin->decodeStart(fileName.c_str(), flags, encoding.c_str());
        if (!decode)
        {
            err = string(plugin->getError());
            ret = eDecodeError;
        }
        else
        {
            if (!plugin->decodeInfo(decode, &duration, &samplesPerSecond, &bitsPerSample, &channels))
            {
                err = string(plugin->getError());
                ret = eDecodeError;
            }
            else
            {
                long size = PUIDDecodeSeconds * samplesPerSecond * (bitsPerSample / 8) * channels;
                // Round this block up to the next nearest chunk size!
                long blockSize = ((size / chunkSize) + 1) * chunkSize;

                unsigned char *buffer = new unsigned char[blockSize];
                if (!buffer)
                {
                    plugin->decodeEnd(decode);
                    return eOutOfMemory;
                }

                long readOffset = 0, bytesLeft = size;
                while(bytesLeft > 0)
                {
                    long thisBlock = min(bytesLeft, chunkSize);
                    numRead = plugin->decodeRead(decode, (char *)(buffer + readOffset), thisBlock);
                    if (numRead < 0)
                    {
                        err = string(plugin->getError());
                        ret = eDecodeError;
                        break;
                    }
                    readOffset += numRead;
                    bytesLeft -= numRead;
                    if (numRead == 0)
                        break;
                }

                if (ret == eOk)
                {
                    AudioData data(tunePimp->context.getMusicDNSClientId(), "libtunepimp-" VERSION);

                    data.setData(buffer, OFA_LITTLE_ENDIAN, readOffset / sizeof(short), samplesPerSecond, channels == 2);
                    // TODO: report correct bitrate
                    data.info.setBitrate(0);

                    data.info.setFormat(mdata.fileFormat);
                    data.info.setLengthInMS(duration);
                    data.info.setArtist(mdata.artist);
                    data.info.setTrack(mdata.track);
                    data.info.setAlbum(mdata.album);
                    data.info.setTrackNum(mdata.trackNum);
                    char year[5];
                    sprintf(year, "%d", mdata.releaseYear);
                    data.info.setYear(year);
                    data.info.setMetadataFlag(true);
                    if (data.createPrint())
                    {
                        delete [] buffer;
                        if (retrieve_metadata(&data.info))
                        {
                            puid = data.info.getPUID();
                            if (puid.length() == 0)
                            {
                                ret = eNoPUID;
                            }
                        }
                        else
                            ret = eNoPUID;
                    }
                    else
                    {
                        delete [] buffer;
                        ret = eNoPUID;
                    }
                }
                else
                {
                    delete [] buffer;
                }
            }
        }

        plugin->decodeEnd(decode);
    }
    catch(...)
    {
        terminate();
    }

    return ret;
}
