/* === This file is part of Tomahawk Player - <http://tomahawk-player.org> ===
 *
 *   Copyright 2010-2011, Christian Muehlhaeuser <muesli@tomahawk-player.org>
 *
 *   Tomahawk 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 3 of the License, or
 *   (at your option) any later version.
 *
 *   Tomahawk 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 Tomahawk. If not, see <http://www.gnu.org/licenses/>.
 */

#include "audioengine.h"

#include <QUrl>
#include <QNetworkReply>

#include "playlistinterface.h"
#include "sourceplaylistinterface.h"
#include "tomahawksettings.h"

#include "database/database.h"
#include "database/databasecommand_logplayback.h"
#include "network/servent.h"
#include "utils/qnr_iodevicestream.h"

#include "album.h"

#include "utils/logger.h"

using namespace Tomahawk;

AudioEngine* AudioEngine::s_instance = 0;

static QString s_aeInfoIdentifier = QString( "AUDIOENGINE" );


AudioEngine*
AudioEngine::instance()
{
    return s_instance;
}


AudioEngine::AudioEngine()
    : QObject()
    , m_isPlayingHttp( false )
    , m_queue( 0 )
    , m_timeElapsed( 0 )
    , m_expectStop( false )
    , m_waitingOnNewTrack( false )
    , m_infoSystemConnected( false )
    , m_state( Stopped )
{
    s_instance = this;
    tDebug() << "Init AudioEngine";

    qRegisterMetaType< AudioErrorCode >("AudioErrorCode");
    qRegisterMetaType< AudioState >("AudioState");

    m_mediaObject = new Phonon::MediaObject( this );
    m_audioOutput = new Phonon::AudioOutput( Phonon::MusicCategory, this );
    Phonon::createPath( m_mediaObject, m_audioOutput );

    m_mediaObject->setTickInterval( 150 );
    connect( m_mediaObject, SIGNAL( stateChanged( Phonon::State, Phonon::State ) ), SLOT( onStateChanged( Phonon::State, Phonon::State ) ) );
    connect( m_mediaObject, SIGNAL( tick( qint64 ) ), SLOT( timerTriggered( qint64 ) ) );
    connect( m_mediaObject, SIGNAL( aboutToFinish() ), SLOT( onAboutToFinish() ) );

    connect( m_audioOutput, SIGNAL( volumeChanged( qreal ) ), this, SLOT( onVolumeChanged( qreal ) ) );

    onVolumeChanged( m_audioOutput->volume() );

#ifndef Q_WS_X11
    // On mac & win, phonon volume is independent from system volume, so the onVolumeChanged call above just sets our volume to 100%.
    // Since it's indendent, we'll set it to 75% since that's nicer.
    setVolume( 75 );
#endif
}


AudioEngine::~AudioEngine()
{
    tDebug() << Q_FUNC_INFO;
    m_mediaObject->stop();
//    stop();

    delete m_audioOutput;
    delete m_mediaObject;
}


QStringList
AudioEngine::supportedMimeTypes() const
{
    if ( m_supportedMimeTypes.isEmpty() )
    {
        m_supportedMimeTypes = Phonon::BackendCapabilities::availableMimeTypes();
        m_supportedMimeTypes << "audio/basic";

        return m_supportedMimeTypes;
    }
    else
        return m_supportedMimeTypes;
}


void
AudioEngine::playPause()
{
    if ( isPlaying() )
        pause();
    else
        play();
}


void
AudioEngine::play()
{
    tDebug( LOGEXTRA ) << Q_FUNC_INFO;

    if ( isPaused() )
    {
        m_mediaObject->play();
        emit resumed();
        Tomahawk::InfoSystem::InfoStringHash trackInfo;

        trackInfo["title"] = m_currentTrack->track();
        trackInfo["artist"] = m_currentTrack->artist()->name();
        trackInfo["album"] = m_currentTrack->album()->name();
        Tomahawk::InfoSystem::InfoSystem::instance()->pushInfo(
        s_aeInfoIdentifier, Tomahawk::InfoSystem::InfoNowResumed,
        QVariant::fromValue< Tomahawk::InfoSystem::InfoStringHash >( trackInfo ) );
    }
    else
        next();
}


void
AudioEngine::pause()
{
    tDebug( LOGEXTRA ) << Q_FUNC_INFO;

    m_mediaObject->pause();
    emit paused();

    Tomahawk::InfoSystem::InfoSystem::instance()->pushInfo( s_aeInfoIdentifier, Tomahawk::InfoSystem::InfoNowPaused, QVariant() );
}


void
AudioEngine::stop()
{
    tDebug( LOGEXTRA ) << Q_FUNC_INFO;
    if ( isStopped() )
        return;

    setState( Stopped );
    m_mediaObject->stop();

    if ( !m_playlist.isNull() )
        m_playlist.data()->reset();
    if ( !m_currentTrack.isNull() )
        emit timerPercentage( ( (double)m_timeElapsed / (double)m_currentTrack->duration() ) * 100.0 );

    emit stopped();
    setCurrentTrack( Tomahawk::result_ptr() );

    Tomahawk::InfoSystem::InfoTypeMap map;
    map[ Tomahawk::InfoSystem::InfoNowStopped ] = QVariant();

    if ( m_waitingOnNewTrack )
    {
        sendWaitingNotification();
    }
    else if ( TomahawkSettings::instance()->verboseNotifications() )
    {
        QVariantMap stopInfo;
        stopInfo["message"] = QString( "Tomahawk is stopped." );
        map[ Tomahawk::InfoSystem::InfoNotifyUser ] = QVariant::fromValue< QVariantMap >( stopInfo );
    }

    Tomahawk::InfoSystem::InfoSystem::instance()->pushInfo( s_aeInfoIdentifier, map );
}


void
AudioEngine::previous()
{
    tDebug( LOGEXTRA ) << Q_FUNC_INFO;

    if ( canGoPrevious() )
        loadPreviousTrack();
}


void
AudioEngine::next()
{
    tDebug( LOGEXTRA ) << Q_FUNC_INFO;

    if ( canGoNext() )
        loadNextTrack();
}


bool
AudioEngine::canGoNext()
{
    tDebug( LOGVERBOSE ) << Q_FUNC_INFO;

    if ( m_queue && m_queue->trackCount() )
        return true;

    if ( m_playlist.isNull() )
        return false;

    if ( m_playlist.data()->skipRestrictions() == PlaylistInterface::NoSkip ||
         m_playlist.data()->skipRestrictions() == PlaylistInterface::NoSkipForwards )
        return false;

    if ( !m_currentTrack.isNull() && !m_playlist.data()->hasNextItem() &&
         ( m_playlist.data()->currentItem().isNull() || ( m_currentTrack->id() == m_playlist.data()->currentItem()->id() ) ) )
    {
        //For instance, when doing a catch-up while listening along, but the person
        //you're following hasn't started a new track yet...don't do anything
        tDebug( LOGEXTRA ) << Q_FUNC_INFO << "catch up";
        return false;
    }

    return m_playlist.data()->hasNextItem();
}


bool
AudioEngine::canGoPrevious()
{
    if ( m_playlist.isNull() )
        return false;

    if ( m_playlist.data()->skipRestrictions() == PlaylistInterface::NoSkip ||
         m_playlist.data()->skipRestrictions() == PlaylistInterface::NoSkipBackwards )
        return false;

    return true;
}


bool
AudioEngine::canSeek()
{
    bool phononCanSeek = true;
    /* TODO: When phonon properly reports this, re-enable it
    if ( m_mediaObject && m_mediaObject->isValid() )
        phononCanSeek = m_mediaObject->isSeekable();
    */
    return !m_playlist.isNull() && ( m_playlist.data()->seekRestrictions() != PlaylistInterface::NoSeek ) && phononCanSeek;
}


void
AudioEngine::seek( qint64 ms )
{
    if ( !canSeek() )
    {
        tDebug( LOGEXTRA ) << "Could not seek!";
        return;
    }

    if ( isPlaying() || isPaused() )
    {
        tDebug( LOGVERBOSE ) << Q_FUNC_INFO << ms;
        m_mediaObject->seek( ms );
        emit seeked( ms );
    }
}


void
AudioEngine::seek( int ms )
{
    seek( (qint64) ms );
}


void
AudioEngine::setVolume( int percentage )
{
    tDebug( LOGVERBOSE ) << Q_FUNC_INFO << percentage;

    percentage = qBound( 0, percentage, 100 );
    m_audioOutput->setVolume( (qreal)percentage / 100.0 );
    emit volumeChanged( percentage );
}


void
AudioEngine::mute()
{
    setVolume( 0 );
}


void
AudioEngine::sendWaitingNotification() const
{
    QVariantMap retryInfo;
    retryInfo["message"] = QString( "The current track could not be resolved. Tomahawk will pick back up with the next resolvable track from this source." );
    Tomahawk::InfoSystem::InfoSystem::instance()->pushInfo(
        s_aeInfoIdentifier, Tomahawk::InfoSystem::InfoNotifyUser,
        QVariant::fromValue< QVariantMap >( retryInfo ) );
}


void
AudioEngine::sendNowPlayingNotification()
{
    tDebug( LOGEXTRA ) << Q_FUNC_INFO;

    if ( ! m_infoSystemConnected )
    {
        connect( Tomahawk::InfoSystem::InfoSystem::instance(),
             SIGNAL( info( Tomahawk::InfoSystem::InfoRequestData, QVariant ) ),
             SLOT( infoSystemInfo( Tomahawk::InfoSystem::InfoRequestData, QVariant ) ) );

        connect( Tomahawk::InfoSystem::InfoSystem::instance(), SIGNAL( finished( QString ) ), SLOT( infoSystemFinished( QString ) ) );

        m_infoSystemConnected = true;
    }

    Tomahawk::InfoSystem::InfoStringHash trackInfo;
    trackInfo["artist"] = m_currentTrack->album()->artist()->name();
    trackInfo["album"] = m_currentTrack->album()->name();

    Tomahawk::InfoSystem::InfoRequestData requestData;
    requestData.caller = s_aeInfoIdentifier;
    requestData.type = Tomahawk::InfoSystem::InfoAlbumCoverArt;
    requestData.input = QVariant::fromValue< Tomahawk::InfoSystem::InfoStringHash >( trackInfo );
    requestData.customData = QVariantMap();

    Tomahawk::InfoSystem::InfoSystem::instance()->getInfo( requestData );
}


void
AudioEngine::infoSystemInfo( Tomahawk::InfoSystem::InfoRequestData requestData, QVariant output )
{
    if ( requestData.caller != s_aeInfoIdentifier ||
         requestData.type != Tomahawk::InfoSystem::InfoAlbumCoverArt )
    {
        return;
    }

    if ( m_currentTrack.isNull() ||
         m_currentTrack->track().isNull() ||
         m_currentTrack->artist().isNull() )
        return;

    QVariantMap playInfo;
    playInfo["message"] = tr( "Tomahawk is playing \"%1\" by %2%3." )
                        .arg( m_currentTrack->track() )
                        .arg( m_currentTrack->artist()->name() )
                        .arg( m_currentTrack->album().isNull() ? QString() : tr( " on album %1" ).arg( m_currentTrack->album()->name() ) );
    if ( !output.isNull() && output.isValid() )
    {
        QVariantMap returnedData = output.value< QVariantMap >();
        const QByteArray ba = returnedData["imgbytes"].toByteArray();
        if ( ba.length() )
        {
            QPixmap pm;
            pm.loadFromData( ba );
            playInfo["image"] = QVariant( pm.toImage() );
        }
    }

    Tomahawk::InfoSystem::InfoSystem::instance()->pushInfo(
        s_aeInfoIdentifier, Tomahawk::InfoSystem::InfoNotifyUser,
        QVariant::fromValue< QVariantMap >( playInfo ) );
}


void
AudioEngine::infoSystemFinished( QString caller )
{
    Q_UNUSED( caller );
}


bool
AudioEngine::loadTrack( const Tomahawk::result_ptr& result )
{
    bool err = false;
    {
        QSharedPointer<QIODevice> io;

        if ( result.isNull() )
            err = true;
        else
        {
            setCurrentTrack( result );

            if ( !isHttpResult( m_currentTrack->url() ) && !isLocalResult( m_currentTrack->url() ) )
            {
                io = Servent::instance()->getIODeviceForUrl( m_currentTrack );

                if ( !io || io.isNull() )
                {
                    tLog() << "Error getting iodevice for" << result->url();
                    err = true;
                }
            }
        }

        if ( !err )
        {
            tLog() << "Starting new song:" << m_currentTrack->url();
            emit loading( m_currentTrack );

            if ( !isHttpResult( m_currentTrack->url() ) && !isLocalResult( m_currentTrack->url() ) )
            {
                if ( QNetworkReply* qnr_io = qobject_cast< QNetworkReply* >( io.data() ) )
                    m_mediaObject->setCurrentSource( new QNR_IODeviceStream( qnr_io, this ) );
                else
                    m_mediaObject->setCurrentSource( io.data() );
                m_mediaObject->currentSource().setAutoDelete( false );
                m_isPlayingHttp = false;
            }
            else
            {
                if ( !isLocalResult( m_currentTrack->url() ) )
                {
                    QUrl furl = m_currentTrack->url();
                    if ( m_currentTrack->url().contains( "?" ) )
                    {
                        furl = QUrl( m_currentTrack->url().left( m_currentTrack->url().indexOf( '?' ) ) );
                        furl.setEncodedQuery( QString( m_currentTrack->url().mid( m_currentTrack->url().indexOf( '?' ) + 1 ) ).toLocal8Bit() );
                    }
                    m_mediaObject->setCurrentSource( furl );
                }
                else
                {
                    QString furl = m_currentTrack->url();
#ifdef Q_WS_WIN
                    if ( furl.startsWith( "file://" ) )
                        furl = furl.right( furl.length() - 7 );
#endif
                    m_mediaObject->setCurrentSource( furl );
                }

                m_mediaObject->currentSource().setAutoDelete( true );
                m_isPlayingHttp = true;
            }

            if ( !m_input.isNull() )
            {
                m_input->close();
                m_input.clear();
            }
            m_input = io;
            m_mediaObject->play();
            emit started( m_currentTrack );

            DatabaseCommand_LogPlayback* cmd = new DatabaseCommand_LogPlayback( m_currentTrack, DatabaseCommand_LogPlayback::Started );
            Database::instance()->enqueue( QSharedPointer<DatabaseCommand>(cmd) );

            Tomahawk::InfoSystem::InfoStringHash trackInfo;
            trackInfo["title"] = m_currentTrack->track();
            trackInfo["artist"] = m_currentTrack->artist()->name();
            trackInfo["album"] = m_currentTrack->album()->name();

            if ( TomahawkSettings::instance()->verboseNotifications() )
                sendNowPlayingNotification();

            Tomahawk::InfoSystem::InfoSystem::instance()->pushInfo(
                s_aeInfoIdentifier,
                Tomahawk::InfoSystem::InfoNowPlaying,
                QVariant::fromValue< Tomahawk::InfoSystem::InfoStringHash >( trackInfo ) );
        }
    }

    if ( err )
    {
        stop();
        return false;
    }

    m_waitingOnNewTrack = false;
    return true;
}


void
AudioEngine::loadPreviousTrack()
{
    tDebug( LOGEXTRA ) << Q_FUNC_INFO;

    if ( m_playlist.isNull() )
    {
        stop();
        return;
    }

    Tomahawk::result_ptr result = m_playlist.data()->previousItem();
    if ( !result.isNull() )
        loadTrack( result );
    else
        stop();
}


void
AudioEngine::loadNextTrack()
{
    tDebug( LOGEXTRA ) << Q_FUNC_INFO;

    Tomahawk::result_ptr result;

    if ( m_queue && m_queue->trackCount() )
    {
        result = m_queue->nextItem();
    }

    if ( !m_playlist.isNull() && result.isNull() )
    {
        tDebug( LOGEXTRA ) << Q_FUNC_INFO << "Loading playlist's next item";
        result = m_playlist.data()->nextItem();
        m_currentTrackPlaylist = m_playlist;
    }

    if ( !result.isNull() )
    {
        tDebug( LOGEXTRA ) << Q_FUNC_INFO << "Got next item, loading track";
        loadTrack( result );
    }
    else
    {
        if ( !m_playlist.isNull() && m_playlist.data()->retryMode() == Tomahawk::PlaylistInterface::Retry )
            m_waitingOnNewTrack = true;

        stop();
    }
}


void
AudioEngine::playItem( Tomahawk::PlaylistInterface* playlist, const Tomahawk::result_ptr& result )
{
    tDebug( LOGEXTRA ) << Q_FUNC_INFO << ( result.isNull() ? QString() : result->url() );

    if ( !m_playlist.isNull() )
        m_playlist.data()->reset();

    setPlaylist( playlist );
    m_currentTrackPlaylist = playlist->getSharedPointer();

    if ( !result.isNull() )
        loadTrack( result );
    else if ( !m_playlist.isNull() && m_playlist.data()->retryMode() == PlaylistInterface::Retry )
    {
        m_waitingOnNewTrack = true;
        if ( isStopped() )
            sendWaitingNotification();
        else
            stop();
    }
}


void
AudioEngine::playlistNextTrackReady()
{
    if ( !m_waitingOnNewTrack )
        return;

    m_waitingOnNewTrack = false;
    loadNextTrack();
}


void
AudioEngine::onAboutToFinish()
{
    tDebug( LOGVERBOSE ) << Q_FUNC_INFO;
    m_expectStop = true;
}


void
AudioEngine::onStateChanged( Phonon::State newState, Phonon::State oldState )
{
    tDebug( LOGVERBOSE ) << Q_FUNC_INFO << oldState << newState << m_expectStop;

    if ( newState == Phonon::ErrorState )
    {
        tLog() << "Phonon Error:" << m_mediaObject->errorString() << m_mediaObject->errorType();
        return;
    }
    if ( newState == Phonon::PlayingState )
        setState( Playing );

    if ( oldState == Phonon::PlayingState )
    {
        bool stopped = false;
        switch ( newState )
        {
            case Phonon::PausedState:
            {
                qint64 duration = m_mediaObject->totalTime() > 0 ? m_mediaObject->totalTime() : m_currentTrack->duration() * 1000;
                stopped = ( duration - 1000 < m_mediaObject->currentTime() );
                if ( !stopped )
                    setState( Paused );

                break;
            }
            case Phonon::StoppedState:
            {
                stopped = true;
                break;
            }
            default:
                break;
        }

        if ( stopped && m_expectStop )
        {
            m_expectStop = false;
            tDebug( LOGEXTRA ) << "Finding next track.";
            if ( canGoNext() )
                loadNextTrack();
            else
            {
                if ( !m_playlist.isNull() && m_playlist.data()->retryMode() == Tomahawk::PlaylistInterface::Retry )
                    m_waitingOnNewTrack = true;
                stop();
            }
        }
    }
}


void
AudioEngine::timerTriggered( qint64 time )
{
    emit timerMilliSeconds( time );

    if ( m_timeElapsed != time / 1000 )
    {
        m_timeElapsed = time / 1000;
        emit timerSeconds( m_timeElapsed );

        if ( !m_currentTrack.isNull() )
        {
            if ( m_currentTrack->duration() == 0 )
            {
                emit timerPercentage( 0 );
            }
            else
            {
                emit timerPercentage( ( (double)m_timeElapsed / (double)m_currentTrack->duration() ) * 100.0 );
            }
        }
    }
}


void
AudioEngine::setPlaylist( PlaylistInterface* playlist )
{
    if ( !m_playlist.isNull() )
    {
        if ( m_playlist.data()->object() && m_playlist.data()->retryMode() == PlaylistInterface::Retry )
            disconnect( m_playlist.data()->object(), SIGNAL( nextTrackReady() ) );
        m_playlist.data()->reset();
    }

    if ( !playlist )
        return;

    m_playlist = playlist->getSharedPointer();

    if ( m_playlist.data()->object() && m_playlist.data()->retryMode() == PlaylistInterface::Retry )
        connect( m_playlist.data()->object(), SIGNAL( nextTrackReady() ), SLOT( playlistNextTrackReady() ) );

    emit playlistChanged( playlist );
}


void
AudioEngine::setCurrentTrack( const Tomahawk::result_ptr& result )
{
    m_lastTrack = m_currentTrack;
    if ( !m_lastTrack.isNull() )
    {
        DatabaseCommand_LogPlayback* cmd = new DatabaseCommand_LogPlayback( m_lastTrack, DatabaseCommand_LogPlayback::Finished, m_timeElapsed );
        Database::instance()->enqueue( QSharedPointer<DatabaseCommand>(cmd) );

        emit finished( m_lastTrack );
    }

    m_currentTrack = result;
}


bool
AudioEngine::isHttpResult( const QString& url ) const
{
    return url.startsWith( "http://" ) || url.startsWith( "https://" );
}


bool
AudioEngine::isLocalResult( const QString& url ) const
{
    return url.startsWith( "file://" );
}


void
AudioEngine::setState( AudioState state )
{
    AudioState oldState = m_state;
    m_state = state;

    emit stateChanged( state, oldState );
}
