/// QasTools: Desktop toolset for the Linux sound system ALSA.
/// \copyright See COPYING file.

#include "notifier.hpp"
#include <QDBusConnection>
#include <QDBusInterface>
#include <QDBusPendingCallWatcher>
#include <QDBusReply>
#include <QDBusServiceWatcher>
#include <iostream>
#include <sstream>

namespace Tray
{

Notifier::Notifier ( const Shared & shared_n, QObject * parent_n )
: QObject ( parent_n )
, _shared ( shared_n )
, _dbus_service_name ( "org.freedesktop.Notifications" )
{
  _call_data.method = "Notify";
  _call_data.app_name = _shared.application_title;
  _call_data.actions.append ( "default" );
  _call_data.actions.append ( tr ( "Open mixer" ) );

  _call_elapsed.start ();
  _call_timer.setSingleShot ( true );
  QObject::connect ( &_call_timer,
                     &QTimer::timeout,
                     this,
                     &Notifier::try_submit_notification,
                     Qt::QueuedConnection );

  // DBus service watcher
  {
    _dbus_service_watcher.reset (
        new QDBusServiceWatcher ( _dbus_service_name,
                                  QDBusConnection::sessionBus (),
                                  QDBusServiceWatcher::WatchForOwnerChange,
                                  this ) );

    QObject::connect ( _dbus_service_watcher.get (),
                       &QDBusServiceWatcher::serviceOwnerChanged,
                       this,
                       &Notifier::dbus_service_owner_changed );
  }

  // Initial connection
  dbus_interface_connect ();
}

Notifier::~Notifier () = default;

void
Notifier::set_message_lifetime ( unsigned int milliseconds_n )
{
  _call_data.timeout = milliseconds_n;
}

void
Notifier::dbus_service_owner_changed ( const QString & service_name_n,
                                       const QString & old_owner_n
                                       [[maybe_unused]],
                                       const QString & new_owner_n )
{
  if ( _dbus_service_name == service_name_n ) {
    if ( !new_owner_n.isEmpty () ) {
      dbus_interface_connect ();
    } else {
      dbus_interface_disconnect ();
    }
  }
}

void
Notifier::dbus_interface_disconnect ()
{
  _dbus_interface.reset ();
  _call_pending = false;
  _call_data.replaces_id = 0;
}

bool
Notifier::dbus_interface_connect ()
{
  dbus_interface_disconnect ();

  auto connection = QDBusConnection::sessionBus ();
  _dbus_interface.reset ( new QDBusInterface ( _dbus_service_name,
                                               "/org/freedesktop/Notifications",
                                               "org.freedesktop.Notifications",
                                               connection,
                                               this ) );

  if ( _dbus_interface->isValid () ) {
    // Connect to DBus signals
    connect ( _dbus_interface.get (),
              SIGNAL ( NotificationClosed ( unsigned int, unsigned int ) ),
              this,
              SLOT ( notification_closed ( unsigned int, unsigned int ) ) );

    connect ( _dbus_interface.get (),
              SIGNAL ( ActionInvoked ( unsigned int, QString ) ),
              this,
              SLOT ( notification_action ( unsigned int, QString ) ) );

    // Acquire capabilities
    {
      QDBusReply< QStringList > reply =
          _dbus_interface->call ( "GetCapabilities" );
      if ( reply.isValid () ) {
        _body_supported = reply.value ().contains ( "body" );
      } else {
        std::ostringstream oss;
        oss << "DBus GetCapabilities call failed with the following error\n"
            << "  Type:    " << reply.error ().type () << "\n"
            << "  Name:    " << reply.error ().name ().toStdString () << "\n"
            << "  Message: " << reply.error ().message ().toStdString ()
            << "\n";
        std::cerr << oss.str ();
      }
    }
  } else {
    // Invalid interface
    {
      std::ostringstream oss;
      oss << "Could not connect to DBus desktop notification server: "
          << connection.lastError ().message ().toStdString () << "\n";
      std::cerr << oss.str ();
    }
    _dbus_interface.reset ();
    return false;
  }
  return true;
}

void
Notifier::notify_muted ()
{
  notify ( _shared.icon_names.volume_muted, _shared.l10n_strings.muted );
}

void
Notifier::notify_permille ()
{
  QString message = _shared.l10n_strings.volume_percent_mask.arg (
      _locale.toString ( double ( _shared.volume.permille ) / 10.0, 'f', 1 ) );
  notify ( _shared.volume.icon_name, message );
}

void
Notifier::notify ( const QString & icon_name_n, const QString & message_n )
{
  QString summary;
  QString body;
  if ( _body_supported ) {
    // Message body supported
    summary = _shared.l10n_strings.volume_change;
    body = message_n;
  } else {
    // Message body not supported
    summary = message_n;
  }

  // Compare message to last call to avoid a quick succession of messages
  // with the same content.
  if ( ( _last_call.app_icon == icon_name_n ) &&
       ( _last_call.summary == summary ) && ( _last_call.body == body ) ) {
    // This message has already been sent.  Check if it still diplayed.
    if ( ( _call_data.replaces_id != 0 ) || ( _call_watcher != nullptr ) ) {
      // The message is still being displayed.
      // Don't send it again and also avoid an override with discarded content.
      _call_pending = false;
      return;
    }
    // The message is not displayed anymore.  Show it again.
  }

  // Setup the call data
  _call_data.app_icon = icon_name_n;
  _call_data.summary = summary;
  _call_data.body = body;

  // Set call pending flag
  _call_pending = true;

  // Try to submit
  try_submit_notification ();
}

void
Notifier::try_submit_notification ()
{
  if ( !_call_pending ) {
    // Nothing to submit
    return;
  }
  if ( _call_watcher != nullptr ) {
    // A call is running
    return;
  }
  if ( !_call_elapsed.hasExpired ( _call_intervall ) ) {
    // We don't want to flood the server.  Try again later.
    if ( !_call_timer.isActive () ) {
      qint64 delta = qMax ( _call_intervall - _call_elapsed.elapsed (), 0 );
      _call_timer.start ( delta );
    }
    return;
  }

  // Issue an asynchronous call
  if ( _dbus_interface && _dbus_interface->isValid () ) {

    // Remember this call to avoid message duplication
    _last_call.app_icon = _call_data.app_icon;
    _last_call.summary = _call_data.summary;
    _last_call.body = _call_data.body;

    // Issue a call
    QDBusPendingCall pcall =
        _dbus_interface->asyncCall ( _call_data.method,
                                     _call_data.app_name,
                                     _call_data.replaces_id,
                                     _call_data.app_icon,
                                     _call_data.summary,
                                     _call_data.body,
                                     _call_data.actions,
                                     _call_data.hints,
                                     _call_data.timeout );

    // Create a call watcher and connect it
    _call_watcher = new QDBusPendingCallWatcher ( pcall, this );
    QObject::connect ( _call_watcher,
                       &QDBusPendingCallWatcher::finished,
                       this,
                       &Notifier::call_finished );
  }

  // Clear call pending flag
  _call_pending = false;
  // Remember the send time
  _call_elapsed.start ();
}

void
Notifier::call_finished ( QDBusPendingCallWatcher * watcher_n )
{
  if ( _call_watcher != watcher_n ) {
    // Unexpected
    watcher_n->deleteLater ();
    return;
  }

  // Evaluate the reply
  {
    QDBusPendingReply< std::uint32_t > reply = *watcher_n;
    if ( reply.isValid () ) {
      // Remember the notification id
      _call_data.replaces_id = reply.value ();
      // std::ostringstream oss;
      // oss << "Notifier dbus call succeeded: " << reply.value () << std::endl;
      // std::cout << oss.str();
    } else {
      std::ostringstream oss;
      oss << "Notifier DBus call failed with the following error\n"
          << "  Type:    " << reply.error ().type () << "\n"
          << "  Name:    " << reply.error ().name ().toStdString () << "\n"
          << "  Message: " << reply.error ().message ().toStdString () << "\n";
      std::cerr << oss.str ();
    }
  }

  // Release the call watcher
  _call_watcher->deleteLater ();
  _call_watcher = nullptr;

  // Try to sumit a pending notification again
  try_submit_notification ();
}

void
Notifier::notification_closed ( unsigned int id_n,
                                unsigned int reason_n [[maybe_unused]] )
{
  // std::cout << "Notification closed " << id_n << std::endl;

  //  Reset the notification id
  if ( _call_data.replaces_id == id_n ) {
    _call_data.replaces_id = 0;
  }
}

void
Notifier::notification_action ( unsigned int id_n, QString action_key_n )
{
  // Emit a signal when the default action was triggered
  if ( ( _call_data.replaces_id == id_n ) && ( action_key_n == "default" ) ) {
    Q_EMIT sig_activated ();
  }
}

} // namespace Tray
