/***************************************************************************
                          nowlisteningclient.cpp -  description
                             -------------------
    begin                : Sat Nov 4 2006
    copyright            : (C) 2006 by Diederik van der Boor
    email                : "vdboor" --at-- "codingdomain.com"
 ***************************************************************************/

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

#include "nowlisteningclient.h"

#include "kmessdebug.h"

#include <QtDBus>

#include <KProcess>
#include <KStandardDirs>


#ifdef KMESSDEBUG_NOWLISTENINGCLIENT
  #define KMESSDEBUG_NOWLISTENINGCLIENT_GENERAL
#endif


// This code is loosely inspired by the "now listening" plugin of Kopete.
// Therefore some parts are also
// (c) 2002-2006 the by Kopete developers <kopete-devel@kde.org>

// Make a Qt meta type from the internal structure which holds Player's status ( mpris compliant )
Q_DECLARE_METATYPE( NowListeningClient::MprisPlayerStatus )



/**
 * Marshall the MprisPlayerStatus structure into a DBus argument
 */
QDBusArgument &operator<<( QDBusArgument &argument, const NowListeningClient::MprisPlayerStatus &status )
{
  argument.beginStructure();
  argument << status.Status << status.hasShuffle << status.hasRepeat << status.hasPlaylistRepeat;
  argument.endStructure();
  return argument;
}



/**
 * Demarshall the MprisPlayerStatus structure from a DBus argument
 */
const QDBusArgument &operator>>( const QDBusArgument &argument, NowListeningClient::MprisPlayerStatus &status )
{
  argument.beginStructure();
  argument >> status.Status >> status.hasShuffle >> status.hasRepeat >> status.hasPlaylistRepeat;
  argument.endStructure();
  return argument;
}



/**
 * Constructor
 */
NowListeningClient::NowListeningClient()
 : playing_(false)
{
  qDBusRegisterMetaType<MprisPlayerStatus>();

  // Connect the update timer event
  connect( &timer_, SIGNAL(timeout()), this, SLOT(slotUpdate()) );
}



/**
 * Enable or disable the update interval timer.
 */
void NowListeningClient::setEnabled( bool enable )
{
  if( enable )
  {
    if( ! timer_.isActive() )
    {
      // Reset the fields to avoid problems when the "now listening information" is
      // re-enabled.
      artist_ = "";
      album_ = "";
      track_ = "";
      playing_ = false;

      // Query directly so the GUI is up to date
      slotUpdate();

      // check every 8 seconds
      timer_.start( 8000 );
    }
  }
  else
  {
    // Emit a null changedSong signal to clean the GUI
    emit changedSong( QString(), QString(), QString(), false );
    timer_.stop();
  }
}



/**
 * Update the current song
 */
void NowListeningClient::slotUpdate()
{
#ifdef KMESSDEBUG_NOWLISTENINGCLIENT_GENERAL
    kDebug() << "Updating Now Playing status";
#endif

  // Detect changes to reduce signal calls.
  const QString prevArtist( artist_ );
  const QString prevAlbum ( album_ );
  const QString prevTrack ( track_ );
  bool prevPlaying   = playing_;

  // Reset until proven otherwise.
  playing_ = false;

  // Query all applications: the first found stops the search
  if(    queryAmarok1()
      || queryMprisPlayers()
      || queryJuk()
      || queryKsCD()   )
  {
    // Found active media player!

#ifdef KMESSDEBUG_NOWLISTENINGCLIENT_GENERAL
    kDebug() << "found song" << artist_ << "-" << track_ << "(extra info: album=" << album_ << ", playing=" << playing_ << ")";
#endif

    // App found and playing, detect change.
    // Check if player is playing ( and if the song was changed ) or if the player was stopped/paused
    // and now play again.
    if( ( playing_ &&
        (   prevArtist != artist_
        ||  prevAlbum  != album_
        ||  prevTrack  != track_ ) ) || ( ! prevPlaying && playing_ )
      )
    {
#ifdef KMESSDEBUG_NOWLISTENINGCLIENT_GENERAL
      kDebug() << "playing information changed, emitting changedSong().";
#endif
      if( artist_.isEmpty() && album_.isEmpty() && track_.isEmpty() && playing_ == true )
      {
#ifdef KMESSDEBUG_NOWLISTENINGCLIENT_GENERAL
        kDebug() << "any informations about this song..";
#endif
        emit changedSong( QString(), QString(), QString(), false );
      }
      else
      {
        emit changedSong( artist_, album_, track_, playing_ );
      }
    }
  }

  // Emit a signal when the player was stopped.
  if( prevPlaying && ! playing_ )
  {
#ifdef KMESSDEBUG_NOWLISTENINGCLIENT_GENERAL
    kDebug() << "player was stopped, emitting changedSong().";
#endif
    emit changedSong( QString(), QString(), QString(), false );
  }
}



/**
 * Query Amarok 1.x for track information.
 */
bool NowListeningClient::queryAmarok1()
{
  // Check for executable to avoid problem of defunct fork
  if( KStandardDirs::findExe( "dcop" ).isEmpty() )
  {
    return false;
  }

  QStringList defaultParams( QStringList() << "dcop" << "amarok" << "default" );

  KProcess proc;
  proc.setOutputChannelMode( KProcess::OnlyStdoutChannel );

  // Read the 'is playing' answer. This will filter out the case of a missing "dcop" command, and of
  // a "amarok is not playing/paused" answer
  proc.setProgram( defaultParams + QStringList( "isPlaying" ) );
  proc.execute( 250 );
  if( ! proc.readAllStandardOutput().contains( "true" ) )
  {
    proc.kill();
    return false;
  }

  playing_ = true;

  // Run the DCOP client and selectively ask the artist, album and title
  proc.setProgram( defaultParams + QStringList( "artist" ) );
  proc.execute( 250 );
  artist_ = QString::fromUtf8( proc.readAllStandardOutput() ).simplified();

  proc.setProgram( defaultParams + QStringList( "album" ) );
  proc.execute( 250 );
  album_  = QString::fromUtf8( proc.readAllStandardOutput() ).simplified();

  proc.setProgram( defaultParams + QStringList( "title" ) );
  proc.execute( 250 );
  track_ = QString::fromUtf8( proc.readAllStandardOutput() ).simplified();

  proc.kill();
  return true;
}



/**
 * Query Amarok for track information.
 */
bool NowListeningClient::queryMprisPlayers()
{
  mutex_.lock();

  // Connect and search all services that implent mpris specifications
  QStringList services;
  const QDBusConnection& sessionConn( QDBusConnection::sessionBus() );

  // Check if the connection is successful
  if( sessionConn.isConnected() )
  {
    const QDBusConnectionInterface* bus = sessionConn.interface();
    const QDBusReply<QStringList>& reply( bus->registeredServiceNames() );

    if( reply.isValid() )
    {
      // Search for "org.mpris" string
      services = reply.value().filter( "org.mpris." );
    }
  }

  // If no service was found then return false and unlock the mutex
  if( services.isEmpty() )
  {
    mutex_.unlock();
    return false;
  }

  // Start the d-bus interface, needed to check the application status and make calls to it
  QDBusInterface dbusMprisPlayer( services.at(0), "/Player", "org.freedesktop.MediaPlayer" );
  QDBusInterface dbusMprisRoot  ( services.at(0), "/", "org.freedesktop.MediaPlayer" );

  // See if the application is registered.
  if( ! dbusMprisPlayer.isValid() )
  {
    mutex_.unlock();
    return false;
  }

  QString player;

  if (! dbusMprisRoot.isValid() )
  {
    player = QString( "Unknown Player" );
  }
  else
  {
    QDBusReply<QString> playerName = dbusMprisPlayer.call("Identity");
    player = playerName.value();
  }

#ifdef KMESSDEBUG_NOWLISTENINGCLIENT_GENERAL
  kDebug() << "querying " << player << " for now listening information...";
#endif

  // query the MPRIS player to know what is currently playing.
  QDBusReply<QVariantMap>        metadata = dbusMprisPlayer.call("GetMetadata");
  QDBusReply<MprisPlayerStatus> isPlaying = dbusMprisPlayer.call("GetStatus"); 

  if( ! isPlaying.isValid() || ! metadata.isValid() )
  {
#ifdef KMESSDEBUG_NOWLISTENINGCLIENT_GENERAL
    kDebug() << "Invalid response from " << player << "!";
    kDebug() << "GetStatus Error: " << isPlaying.error().message();
    kDebug() << "GetMetadata Error: " << metadata.error().message();
#endif

    mutex_.unlock();
    return false;
  }

  // Get the list of properties from the map
  const QVariantMap& map( metadata.value() );

  // Save the returned values
  album_   = map[ "album"  ].toString();
  track_   = map[ "title"  ].toString();
  artist_  = map[ "artist" ].toString();
  playing_ = ( isPlaying.value().Status == 0 ); // 0 playing, 1 paused, 2 stopped

  mutex_.unlock();
  return true;
}



/**
 * Query KsCD for track information.
 */
bool NowListeningClient::queryKsCD()
{
  mutex_.lock();

  // Start the d-bus interface, needed to check the application status and make calls to it
  QDBusInterface dbusKsCD( "org.kde.kscd", "/CDPlayer", "org.kde.KSCD" );

  // See if the application is registered.
  if( ! dbusKsCD.isValid() )
  {
    mutex_.unlock();
    return false;
  }

#ifdef KMESSDEBUG_NOWLISTENINGCLIENT_GENERAL
  kDebug() << "querying KsCD for now listening information...";
#endif

  // Call KsCD to know whether it's playing, and what
  const QDBusReply<QString>& album ( dbusKsCD.call( QDBus::BlockWithGui, "currentAlbum"      ) );
  const QDBusReply<QString>& track ( dbusKsCD.call( QDBus::BlockWithGui, "currentTrackTitle" ) );
  const QDBusReply<QString>& artist( dbusKsCD.call( QDBus::BlockWithGui, "currentArtist"     ) );
  const QDBusReply<bool>& isPlaying( dbusKsCD.call( QDBus::BlockWithGui, "playing"           ) );

  if( ! isPlaying.isValid() || ! album.isValid() || ! track.isValid() || ! artist.isValid() )
  {
#ifdef KMESSDEBUG_NOWLISTENINGCLIENT_GENERAL
    kDebug() << "Invalid response from KsCD!";
#endif

    mutex_.unlock();
    return false;
  }

  // Save the returned values
  album_   = album;
  track_   = track;
  artist_  = artist;
  playing_ = isPlaying;

  mutex_.unlock();
  return true;
}



/**
 * Query Juk for track information.
 */
bool NowListeningClient::queryJuk()
{
  mutex_.lock();

  // Start the d-bus interface, needed to check the application status and make calls to it
  QDBusInterface dbusJuk( "org.kde.juk", "/Player", "org.kde.juk.player" );

  // See if the application is registered.
  if( ! dbusJuk.isValid() )
  {
    mutex_.unlock();
    return false;
  }

#ifdef KMESSDEBUG_NOWLISTENINGCLIENT_GENERAL
  kDebug() << "querying Juk for now listening information...";
#endif

  // Call KsCD to know whether it's playing, and what
  const QDBusReply<QString>& album ( dbusJuk.call( QDBus::BlockWithGui, "trackProperty", "Album"  ) );
  const QDBusReply<QString>& track ( dbusJuk.call( QDBus::BlockWithGui, "trackProperty", "Title"  ) );
  const QDBusReply<QString>& artist( dbusJuk.call( QDBus::BlockWithGui, "trackProperty", "Artist" ) );
  const QDBusReply<bool>& isPlaying( dbusJuk.call( QDBus::BlockWithGui, "playing"                 ) );

  if( ! isPlaying.isValid() || ! album.isValid() || ! track.isValid() || ! artist.isValid() )
  {
#ifdef KMESSDEBUG_NOWLISTENINGCLIENT_GENERAL
    kDebug() << "Invalid response from Juk!";
#endif

    mutex_.unlock();
    return false;
  }

  // Save the returned values
  album_   = album;
  track_   = track;
  artist_  = artist;
  playing_ = isPlaying;

  mutex_.unlock();
  return true;
}



#include "nowlisteningclient.moc"
