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

#include "qsnd/mixer/mixer.hpp"
#include "mixer.hpp"
#include "mwdg/mixer_device_setup.hpp"
#include "qastools_config.hpp"
#include "qsnd/card_info.hpp"
#include "qsnd/cards_db.hpp"
#include "qsnd/ctl_address.hpp"
#include "qsnd/event_types.hpp"
#include "qsnd/mixer/elem.hpp"
#include "qsnd/mixer/filters/first_playback_volume.hpp"
#include "tray/icon.hpp"
#include "tray/notifier.hpp"
#include "tray/setup.hpp"
#include "wdg/uint_mapper.hpp"
#include <QCoreApplication>
#include <QEvent>
#include <cmath>
#include <iostream>
#include <memory>
#include <sstream>

namespace Tray
{

Mixer::Mixer ( const Tray::Setup * setup_n,
               const MWdg::Mixer_Device_Setup * mixer_device_setup_n,
               QSnd::Cards_Db * cards_db_n,
               QObject * parent_n )
: QObject ( parent_n )
, _setup ( setup_n )
, _mixer_device_setup ( mixer_device_setup_n )
, _shared ( this )
{
  // Connect cards database
  connect (
      cards_db_n, &QSnd::Cards_Db::sig_card_new, this, &Mixer::check_card_new );
  connect ( cards_db_n,
            &QSnd::Cards_Db::sig_card_removed,
            this,
            &Mixer::check_card_removed );

  _snd_mixer = new QSnd::Mixer::Mixer ( this );
  _snd_mixer->append_elem_filter (
      std::make_unique< QSnd::Mixer::Filters::First_Playback_Volume > () );
  connect ( _snd_mixer,
            &QSnd::Mixer::Mixer::sig_mixer_reload_request,
            this,
            &Mixer::reload_mixer,
            Qt::QueuedConnection );

  // Tray icon
  {
    _tray_icon.reset ( new Icon ( _shared, this ) );

    connect ( _tray_icon.get (), &Icon::sig_quit, this, &Mixer::sig_quit );

    connect ( _tray_icon.get (),
              &Icon::sig_activated,
              this,
              &Mixer::sig_toggle_mixer );

    connect ( _tray_icon.get (),
              &Icon::sig_wheel_delta,
              this,
              &Mixer::mouse_wheel_delta );

    connect ( _tray_icon.get (),
              &Icon::sig_middle_click,
              this,
              &Mixer::mixer_toggle_switch );

    _tray_icon->show ();
  }

  load_mixer ();
  update_balloon_setup ();
}

Mixer::~Mixer () = default;

void
Mixer::reload_current_mixer_device ()
{
  if ( _setup->device_mode () == Tray::Setup::MIXER_DEV_CURRENT ) {
    reload_mixer ();
  }
}

void
Mixer::update_balloon_setup ()
{
  if ( _setup->balloon_enabled () ) {
    // Create notifier on demand
    if ( !_notifier ) {
      _notifier = std::make_unique< Notifier > ( _shared, this );
      connect ( _notifier.get (),
                &Notifier::sig_activated,
                this,
                &Mixer::sig_toggle_mixer );
    }
    // Setup notifier
    _notifier->set_message_lifetime ( _setup->balloon_lifetime () );
  } else {
    _notifier.reset ();
  }
}

void
Mixer::update_input_setup ()
{
  // std::ostringstream oss;
  // oss << "Tray::Mixer::update_input_setup: Wheel degrees: "
  //     << _setup->wheel_degrees << "\n";
  // std::cout << oss.str();
}

void
Mixer::update_icon_setup ()
{
  // std::cout << "Tray::Mixer::update_icon_setup\n";
  update_volume_values ();
}

bool
Mixer::is_visible () const
{
  return ( QSystemTrayIcon::isSystemTrayAvailable () &&
           _tray_icon->isVisible () );
}

QString
Mixer::acquire_mixer_device () const
{
  QString mdev;
  switch ( _setup->device_mode () ) {
  case Tray::Setup::MIXER_DEV_CURRENT:
    mdev = _mixer_device_setup->control_address ().addr_str ();
    break;
  case Tray::Setup::MIXER_DEV_USER:
    mdev = _setup->user_device ();
    break;
  default:
    break;
  }
  if ( mdev.isEmpty () ) {
    mdev = QStringLiteral ( "default" );
  }
  return mdev;
}

void
Mixer::close_mixer ()
{
  // Clear mixer element
  if ( _mx_elem != nullptr ) {
    disconnect ( _mx_elem, 0, this, 0 );
  }
  _mx_elem = nullptr;
  _snd_mixer->close ();

  update_volume_values ();
}

void
Mixer::load_mixer ()
{
  if ( _snd_mixer->open ( acquire_mixer_device () ) ) {
    // Update mixer element
    for ( std::size_t ii = 0; ii < _snd_mixer->num_elems (); ++ii ) {
      if ( _snd_mixer->elem ( ii )->has_volume ( QSnd::Mixer::SD_PLAYBACK ) ) {
        _mx_elem = _snd_mixer->elem ( ii );
        break;
      }
    }
    if ( _mx_elem != nullptr ) {
      // Connect mixer element
      connect ( _mx_elem,
                &QSnd::Mixer::Elem::sig_values_changed,
                this,
                &Mixer::mixer_values_changed );
    } else {
      // No usable mixer element foud
      _snd_mixer->close ();
    }
  }

  update_volume_values ();
}

void
Mixer::reload_mixer ()
{
  close_mixer ();
  load_mixer ();
}

bool
Mixer::check_card_match ( const QSnd::Card_Info & info_n ) const
{
  const QSnd::Ctl_Address ctl_addr ( acquire_mixer_device () );
  if ( ( ctl_addr.ctl_name () != QLatin1StringView ( "hw" ) ) ||
       ( ctl_addr.num_args () != 1 ) ) {
    return false;
  }
  const auto & arg0 = ctl_addr.arg ( 0 );
  if ( !( arg0.name ().isEmpty () ||
          arg0.name () == QLatin1StringView ( "CARD" ) ) ) {
    return false;
  }
  bool match = false;
  {
    bool is_numeric = false;
    int index = arg0.value ().toInt ( &is_numeric );
    if ( is_numeric ) {
      // Check card index
      if ( index == info_n.index () ) {
        match = true;
      }
    } else {
      // Check card id string
      if ( arg0.value () == info_n.id () ) {
        match = true;
      }
    }
  }
  return match;
}

void
Mixer::check_card_new ( std::shared_ptr< const QSnd::Card_Info > handle_n )
{
  if ( check_card_match ( *handle_n ) ) {
    reload_mixer ();
  }
}

void
Mixer::check_card_removed ( std::shared_ptr< const QSnd::Card_Info > handle_n )
{
  if ( check_card_match ( *handle_n ) ) {
    close_mixer ();
  }
}

void
Mixer::update_volume_values ( bool notify_n )
{
  bool muted = _shared.volume.muted;
  unsigned int perm = _shared.volume.permille;

  if ( _mx_elem != nullptr ) {
    const std::size_t snd_dir = QSnd::Mixer::SD_PLAYBACK;
    unsigned long dist_current = Wdg::integer_distance (
        _mx_elem->volume_min ( snd_dir ), _mx_elem->volume ( snd_dir, 0 ) );

    unsigned long dist_total = Wdg::integer_distance (
        _mx_elem->volume_min ( snd_dir ), _mx_elem->volume_max ( snd_dir ) );

    perm = Wdg::permille ( dist_current, dist_total );

    if ( _mx_elem->has_switch ( snd_dir ) ) {
      muted = !_mx_elem->switch_state ( snd_dir, 0 );
    }
  } else {
    muted = false;
    perm = 0;
  }

  // Notification flags
  bool was_muted = false;
  bool was_unmuted = false;
  bool perm_changed = false;

  if ( _shared.volume.muted != muted ) {
    _shared.volume.muted = muted;
    _shared.update_volume_icon ();
    if ( muted ) {
      was_muted = true;
    } else {
      was_unmuted = true;
    }
  }
  if ( _shared.volume.permille != perm ) {
    _shared.volume.permille = perm;
    _shared.update_volume_icon ();
    perm_changed = true;
  }
  if ( _setup != nullptr ) {
    _shared.show_volume_icon = _setup->show_volume_icon ();
  }

  // Notify on demand
  if ( notify_n && _notifier ) {
    if ( was_muted ) {
      _notifier->notify_muted ();
    } else if ( was_unmuted || ( !muted && perm_changed ) ) {
      _notifier->notify_permille ();
    }
  }

  // Update tray icon
  _tray_icon->update_icon ();
}

void
Mixer::mixer_values_changed ()
{
  if ( !_updating_scheduled ) {
    _updating_scheduled = true;
    QEvent * ev_req = new QEvent ( QSnd::evt_update_values );
    QCoreApplication::postEvent ( this, ev_req );
  }
}

void
Mixer::mixer_toggle_switch ()
{
  if ( _mx_elem != nullptr ) {
    const std::size_t snd_dir = QSnd::Mixer::SD_PLAYBACK;
    if ( _mx_elem->has_switch ( snd_dir ) ) {
      _mx_elem->invert_switches ( snd_dir );
    }
  }
}

void
Mixer::mouse_wheel_delta ( int wheel_delta_n )
{
  if ( ( _setup == nullptr ) || ( _mx_elem == nullptr ) ) {
    return;
  }

  const std::size_t snd_dir = QSnd::Mixer::SD_PLAYBACK;
  const long vol_min = _mx_elem->volume_min ( snd_dir );
  const long vol_max = _mx_elem->volume_max ( snd_dir );
  long vol_old = _mx_elem->volume ( snd_dir, 0 );

  long delta;
  {
    const double range = ( double ( vol_max ) - double ( vol_min ) );
    double amount = ( range / double ( _setup->wheel_degrees () ) );
    amount *= ( wheel_delta_n / 8.0 );
    if ( amount > 0.0 ) {
      delta = std::ceil ( amount );
    } else {
      delta = std::floor ( amount );
    }
  }
  if ( delta == 0 ) {
    if ( wheel_delta_n > 0 ) {
      delta = 1;
    } else {
      delta = -1;
    }
  }

  long vol = vol_old;
  if ( delta > 0 ) {
    if ( ( vol_max - vol ) > delta ) {
      vol += delta;
    } else {
      vol = vol_max;
    }
  } else {
    if ( ( vol_min - vol ) < delta ) {
      vol += delta;
    } else {
      vol = vol_min;
    }
  }

  if ( vol != vol_old ) {
    _mx_elem->set_volume_all ( snd_dir, vol );
  }
}

bool
Mixer::event ( QEvent * event_n )
{
  if ( event_n->type () == QSnd::evt_update_values ) {
    _updating_scheduled = false;
    update_volume_values ( true );
    return true;
  }

  return QObject::event ( event_n );
}

} // namespace Tray
