/***************************************************************************
*   Copyright (C) 2007 by                                		           *
*      Philipp Maihart, Last.fm Ltd <phil@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 "growlextension.h"
#include "lastfmapplication.h"
#include "logger.h"
#include "Settings.h"
#include "MooseCommon.h"

#include <QtPlugin>
#include <QFileInfo>

#include <Carbon/Carbon.h>

static ComponentInstance theComponent = NULL;

static OSAID LowCompileAppleScript( const void* text, long textLength );
static bool LowExecAppleScript( OSAID scriptID, QString& resultToken );
static QString convertUnicodeToHex( QString string );


GrowlNotifyExtension::GrowlNotifyExtension() :
    m_parent( NULL ),
    m_settingsPanel( NULL ),
    m_config( 0 ),
    m_enabledByDefault( false )
{
    connect( qApp, SIGNAL(event( int, QVariant )), SLOT(onAppEvent( int, QVariant )) );
}


QWidget*
GrowlNotifyExtension::settingsPane()
{
    if ( !m_settingsPanel )
    {
        initSettingsPanel();
    }

    return m_settingsPanel;
}

void
GrowlNotifyExtension::initSettingsPanel()
{
    LOG( 3, "Initialising Growl GUI\n" );

    m_settingsPanel = new QWidget( m_parent );
    ui.setupUi( m_settingsPanel );

    // Loaded from resources to avoid having to ship a separate file
    m_icon.load( ":/options_growl.png" );

    connect( ui.enabledCheck,     SIGNAL( stateChanged( int ) ),
             this,                SIGNAL( settingsChanged() ) );
}

QPixmap*
GrowlNotifyExtension::settingsIcon()
{
    Q_ASSERT( !m_icon.isNull() );

    return &m_icon;
}

void
GrowlNotifyExtension::populateSettings()
{
    // This won't get called until the options pane is displayed for 
    // the first time.
    Q_ASSERT( m_config != 0 );

    m_config->beginGroup( name() );

    ui.enabledCheck->setChecked( m_config->value( "Enabled", m_enabledByDefault ).toBool() );

    m_config->endGroup();
}

void
GrowlNotifyExtension::saveSettings()
{
    Q_ASSERT( m_config != 0 );

    m_config->beginGroup( name() );

    bool switchedOff =
        !ui.enabledCheck->isChecked() &&
        m_config->value( "Enabled", m_enabledByDefault ).toBool();
    bool switchedOn =
        ui.enabledCheck->isChecked() &&
        !m_config->value( "Enabled", m_enabledByDefault ).toBool();

    m_config->setValue( "Enabled", ui.enabledCheck->isChecked() );

    if ( switchedOff )
    {
        LOGL( 3, "Disabled Growl plugin" );
    }

    // Must call endGroup before we do the notify below, otherwise the newly
    // enabled state won't be persisted in time.
    m_config->endGroup();

    if ( switchedOn )
    {
        LOGL( 3, "Enabled Growl plugin, sending current track");

        TrackInfo info = m_cachedMetadata;
        onAppEvent( Event::PlaybackStarted, QVariant::fromValue( info ) );
    }
}

bool
GrowlNotifyExtension::isEnabled()
{
    Q_ASSERT( m_config != 0 );

    m_config->beginGroup( name() );
    bool en = m_config->value( "Enabled", m_enabledByDefault ).toBool();
    m_config->endGroup();

    return en;
}

void
GrowlNotifyExtension::onAppEvent( int e, QVariant data )
{
    switch (e)
    {
    case Event::PlaybackStarted:
    case Event::TrackChanged:
        break;
    default:
        return;
    }

    TrackInfo metaData = data.value<TrackInfo>();

    // Always store this so that we can send it immediately if we get switched
    // from disabled to enabled during a track.
    m_cachedMetadata = metaData;

    if ( !isEnabled() )
        return;

    if (metaData.sameAs( m_previousMetadata ) || metaData.isEmpty())
        return;

    m_previousMetadata = metaData;

    //     QString albumPicUrl = metaData.albumPicUrl().host() + metaData.albumPicUrl().path();
    // if (!metaData.albumPicUrl().encodedQuery().isEmpty())
    //         albumPicUrl += '?' + metaData.albumPicUrl().encodedQuery();
    // albumPicUrl = pathToCachedCopy( albumPicUrl );
    // if (!QFile::exists( albumPicUrl ))
    QString albumPicUrl = MooseUtils::dataPath( "app_55.png" );

    QString desc;
#define APPEND_IF_NOT_EMPTY( x ) { QString s = x; if (!s.isEmpty()) { desc += s; desc += '\n'; } }
    APPEND_IF_NOT_EMPTY( metaData.artist() );
    APPEND_IF_NOT_EMPTY( metaData.album() );
    desc += metaData.durationString();

    QString title = metaData.track();
    if (title.isEmpty())
        title = QFileInfo( metaData.path() ).fileName();

    QString script =

  // The growl detection code is broken and causes a script compile error.
  //      "tell application \"System Events\" to set GrowlRunning to ((application processes whose (name is equal to \"GrowlHelperApp\")) count)\n"
  //    "if GrowlRunning > 0 then\n"	


        "tell application \"GrowlHelperApp\"\n"
        // this bit only needs to be done on startup really
        "	set the allNotificationsList to {\"Track Notification\"}\n"
        "	set the enabledNotificationsList to {\"Track Notification\"}\n"
        "	register as application \"Last.fm\""
        " all notifications allNotificationsList"
        " default notifications enabledNotificationsList"
        " icon of application \"Last.fm.app\"\n"
        // the actual notification
        "	notify with name \"Track Notification\""
        " title " + convertUnicodeToHex( title ) +
        " description " + convertUnicodeToHex( desc ) + 
        " application name \"Last.fm\"\n"
		"end tell\n";
 //       "end if\n";

    if (!theComponent)
        theComponent = OpenDefaultComponent( kOSAComponentType, typeAppleScript );

    QByteArray const utf8 = script.toUtf8();
    OSAID id = LowCompileAppleScript( utf8, utf8.length() );

    QString executionResult;
    LowExecAppleScript( id, executionResult );

    qDebug() << script << executionResult;
}


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( typeUTF8Text, text, textLength, &scriptTextDesc );
    if ( err != noErr )
    {
        qDebug() << "Aedesc err";    
        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 )
{
    qDebug() << "Executing script";

    QString s;
    char result[4096] = "\0";

    OSStatus err;
    AEDesc resultData;
    OSAID resultID = kOSANullScript;

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

    qDebug() << "Running script done" << err;
    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 )
    {
        qDebug() << "Getting script result";

        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;
    }

    qDebug() << "Executing script done";
    if ( resultID != kOSANullScript )
        OSADispose( theComponent, resultID );

    resultToken = s;
    return true;
}

/* Due to the fact Apple Script can't work with Unicode 
characters directly written by Qt into the script, I've done a small 
function, which converts the Unicode data from a QString into an Apple 
Script function + the hex values of the QString data 
(for example: "<<data utxt00AB00BB>> as unicode text") */
QString
convertUnicodeToHex( QString string )
{	
    if ( string.isEmpty() ) 
        return "";

    QString openFunction = QChar( 0x00AB ); 
    openFunction += "data utxt";
    QString closeFunction = QChar( 0x00BB );
    closeFunction += " as unicode text";

    QString conversionReturn = "";
    int bytesAlreadyTranslated = 0;

    for( int i = 0; i < string.length(); i++ )
    {
        QChar singleChar = string[i];
        QString hexadecimal = QString( "%1" ).arg( (int)singleChar.unicode(), 0, 16 );
        if( hexadecimal.length() == 1 ) hexadecimal.prepend("000");
        if( hexadecimal.length() == 2 ) hexadecimal.prepend("00");
        if( hexadecimal.length() == 3 ) hexadecimal.prepend("0");

        conversionReturn += hexadecimal;
        bytesAlreadyTranslated++;

        // AppleScript is only able to work with 252 / 4 unicode chars at once
        // (haha, yeah not 64 - the "utxt" prefix counts also and is 4 chars long,
        // so 64 - 1 = 63), everything else would cause a compilation error. 
        // Therefore we split the string every 63 bytes and begin a new "<<data 
        // utxt..." function.
        if ( bytesAlreadyTranslated == 63 && i != string.length() - 1 )
        {
            conversionReturn += closeFunction + " & " + openFunction;		
            bytesAlreadyTranslated = 0;
        }
    }

    return openFunction + conversionReturn + closeFunction;
}

Q_EXPORT_PLUGIN2( growl, GrowlNotifyExtension )
