/***************************************************************************
 *   Copyright (C) 2005 - 2007 by                                          *
 *      Christian Muehlhaeuser, Last.fm Ltd <chris@last.fm>                *
 *      Erik Jaelevik, Last.fm Ltd <erik@last.fm>                          *
 *                                                                         *
 *   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.,                                       *
 *   51 Franklin Steet, Fifth Floor, Boston, MA  02110-1301, USA.          *
 ***************************************************************************/

#include "itunesscript.h"

#ifdef Q_WS_MAC

#include "MooseCommon.h"
#include "UnicornCommon.h"
#include "playerlistener.h"
#include "logger.h"

#include "UnicornCommon.h"

#include <QDebug>
#include <QTcpSocket>
#include <QHostAddress>
#include <QUrl>

ComponentInstance theComponent = NULL;
OSAID m_OSA_CheckIsMusic = kOSANullScript;

bool ITunesScript::m_paused = false;
QString ITunesScript::m_lastHash;


ITunesScript::ITunesScript( QObject* parent, const CPlayerListener* listener )
        : m_listener( listener )
{
    theComponent = OpenDefaultComponent( kOSAComponentType, typeAppleScript );

    CFNotificationCenterRef center = CFNotificationCenterGetDistributedCenter();
    CFNotificationCenterAddObserver( center, this, ITunesScript::iTunesCallback, CFSTR( "com.apple.iTunes.playerInfo" ), NULL, CFNotificationSuspensionBehaviorDeliverImmediately );
}


void
ITunesScript::iTunesCallback( CFNotificationCenterRef, void* observer, CFStringRef, const void*, CFDictionaryRef info )
{
    ITunesScript* controller = (ITunesScript*) observer;

    CFStringRef cf_state = (CFStringRef) CFDictionaryGetValue( info, CFSTR( "Player State" ) );
    if ( CFStringCompare( cf_state, CFSTR( "Paused" ), 0 ) == kCFCompareEqualTo )
    {
//         qDebug() << "iTunes state: Paused";
        m_paused = true;

        QString data = "PAUSE c=osx\n";
        controller->transmit( data );
    }
    else
    if ( CFStringCompare( cf_state, CFSTR( "Stopped" ), 0 ) == kCFCompareEqualTo )
    {
//         qDebug() << "iTunes state: Stopped";
        m_paused = false;

        QString data = "STOP c=osx\n";
        controller->transmit( data );
    }
    else
    if ( CFStringCompare( cf_state, CFSTR( "Playing" ), 0 ) == kCFCompareEqualTo )
    {
//         qDebug() << "iTunes state: Playing";

        QString const artist = UnicornUtils::CFStringToQString( (CFStringRef) CFDictionaryGetValue( info, CFSTR( "Artist" ) ) );
        QString const track = UnicornUtils::CFStringToQString( (CFStringRef) CFDictionaryGetValue( info, CFSTR( "Name" ) ) );
        QString const album = UnicornUtils::CFStringToQString( (CFStringRef) CFDictionaryGetValue( info, CFSTR( "Album" ) ) );

        // Get path decoded - iTunes encodes the file location as URL
        QByteArray pathEncoded;
        pathEncoded.append( UnicornUtils::CFStringToQString( (CFStringRef) CFDictionaryGetValue( info, CFSTR( "Location" ) ) ) );
        QUrl pathUrl = QUrl::fromEncoded( pathEncoded ); // Converts the URL into a human readable string

        QString const path = pathUrl.toString().remove( "file://localhost" ); // Removes "file://localhost" at the beginning of the string
        bool musicfile = isMusic();

//         qDebug() << "Artist:" << artist << "Name:" << track << "Album:" << album  << "Location:" << path << "Is Music:" << musicfile;
        if ( !musicfile )
        {
            qDebug() << "Ignored, because file seems to be a movie and not a song or a music video.";
            return;
        }

        CFNumberRef cf_time = (CFNumberRef) CFDictionaryGetValue( info, CFSTR( "Total Time" ) );
        int time = 0;
        if ( cf_time != NULL && !CFNumberGetValue( cf_time, kCFNumberIntType, &time ) )
        {
            qWarning() << "ITunesScript: Duration conversion failed.";
        }

        QString data = "START c=osx&a=" + controller->encodeAmp( artist ) +
                        "&t=" + controller->encodeAmp( track ) +
                        "&b=" + controller->encodeAmp( album ) +
                        "&l=" + QString::number( (unsigned int)time / 1000 ) +
                        "&p=" + controller->encodeAmp( path ) + '\n';	

        if ( m_lastHash == UnicornUtils::md5Digest( data.toUtf8() ) )
        {
            if ( m_paused )
            {
                m_paused = false;

                QString const data = "RESUME c=osx\n";
                controller->transmit( data );
            }

            return;
        }

        m_paused = false;
        m_lastHash = UnicornUtils::md5Digest( data.toUtf8() );

        controller->transmit( data );
    }
    else
    {
          qWarning( "ITunesScript: Unknown state." );
    }
}


bool
ITunesScript::isMusic()
{
    if ( m_OSA_CheckIsMusic == kOSANullScript )
    {
        QString script =
        "set content to \"\"\n"
        "with timeout of 1 seconds\n"
            "tell application \"iTunes\"\n"
                "set content to video kind of current track\n"
            "end tell\n"
        "end timeout\n"
        "return content\n";

        m_OSA_CheckIsMusic = LowCompileAppleScript( script.toUtf8(), script.toUtf8().length() );
    }

    QString r;
    LowExecAppleScript( m_OSA_CheckIsMusic, r );

    if ( r == "none" || r == "music video" || r == "" )
        return true;
    else
        return false;
}


void
ITunesScript::transmit( QString data )
{

    QTcpSocket socket;
    int port = m_listener->GetPort();

    LOGL( 3, "ITunesScript data being sent: " << data << " to port: " << port );

    socket.connectToHost( QHostAddress::LocalHost, port );
    if ( socket.waitForConnected( 1000 ) )
    {
        int bytesWritten = socket.write( data.toUtf8() );
        if ( bytesWritten == -1 )
        {
            LOGL( 1, "Sending submission through socket failed." )
        }
        socket.flush();
        socket.waitForDisconnected( 1000 );
    }
}


/* AppleScriptAvailable returns true if AppleScript is available
and the routines defined herein can be called. */
bool AppleScriptAvailable()
{
    long response;
    if ( Gestalt( gestaltAppleScriptAttr, &response ) != noErr )
        response = 0;

    return ( ( response & ( 1 << gestaltAppleScriptPresent ) ) != 0 );
}


OSAID LowCompileAppleScript( const void* text, long textLength )
{
    QString result;
    OSStatus err;
    AEDesc scriptTextDesc;
    OSAID scriptID = kOSANullScript;

    AECreateDesc( typeNull, NULL, 0, &scriptTextDesc );

    /* put the script text into an aedesc */
    err = AECreateDesc( typeChar, text, textLength, &scriptTextDesc );
    if ( err != noErr )
        goto bail;

    /* compile the script */
    err = OSACompile( theComponent, &scriptTextDesc, kOSAModeNull, &scriptID );
    if ( err != noErr )
    {
        qDebug() << "Compiling err";
        goto bail;
    }

bail:
    return scriptID;
}


/* LowRunAppleScript compiles and runs an AppleScript
provided as text in the buffer pointed to by text.  textLength
bytes will be compiled from this buffer and run as an AppleScript
using all of the default environment and execution settings.  If
resultData is not NULL, then the result returned by the execution
command will be returned as typeChar in this descriptor record
(or typeNull if there is no result information).  If the function
returns errOSAScriptError, then resultData will be set to a
descriptive error message describing the error (if one is
available). */
bool LowExecAppleScript( OSAID scriptID, QString& resultToken )
{
    QString s;
    char result[4096] = "\0";

    OSStatus err;
    AEDesc resultData;
    OSAID resultID = kOSANullScript;

    /* run the script/get the result */
    err = OSAExecute( theComponent, scriptID, kOSANullScript, kOSAModeAlwaysInteract, &resultID );

    if ( err == errOSAScriptError )
    {
        OSAScriptError( theComponent, kOSAErrorMessage, typeUTF8Text, &resultData );
        int length = AEGetDescDataSize( &resultData );
        {
            AEGetDescData( &resultData, result, length < 4096 ? length : 4096 );
            result[ length ] = '\0';
            s = QString::fromUtf8( (char *)&result, length );
            LOG( 1, "AppleScript error: " << s << "\n" );
        }

        return false;
    }
    else if ( err == noErr && resultID != kOSANullScript )
    {
        OSADisplay( theComponent, resultID, typeUTF8Text, kOSAModeNull, &resultData );

        int length = AEGetDescDataSize( &resultData );
        {
            AEGetDescData( &resultData, result, length < 4096 ? length : 4096 );
            s = QString::fromUtf8( (char*)&result, length );

            // Strip surrounding quotes
            if ( s.startsWith( "\"" ) && s.endsWith( "\"" ) )
            {
                s = s.mid( 1, s.length() - 2 );
            }

            // iTunes sometimes gives us strings with null terminators which
            // fucks things up if we don't remove them.
            while ( s.endsWith( QChar( QChar::Null ) ) )
                s.chop( 1 );

            // It also escapes quotes so convert those too
            s.replace( "\\\"", "\"" );
        }
    }
    else
    {
        // AppleScript not responding
        return false;
    }

    if ( resultID != kOSANullScript )
        OSADispose( theComponent, resultID );

    resultToken = s;
    return true;
}

#endif // end Q_WS_MAC
