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

#include "mixer.hpp"
#include "qsnd/event_types.hpp"
#include "qsnd/mixer/elem.hpp"
#include "qsnd/mixer/filter.hpp"
#include <QCoreApplication>
#include <QEvent>
#include <QSocketNotifier>
#include <iostream>
#include <sstream>

namespace QSnd::Mixer
{

Mixer::Mixer ( QObject * parent_n )
: QObject ( parent_n )
{
}

Mixer::~Mixer ()
{
  close ();
}

void
Mixer::append_elem_filter ( std::unique_ptr< Filter > filter_n )
{
  if ( filter_n ) {
    _elem_filters.push_back ( std::move ( filter_n ) );
  }
}

void
Mixer::close_mixer ()
{
  // Destroy socket notifiers
  _socket_notifiers.clear ();
  _pollfds.clear ();
  _mixer_elems.clear ();

  if ( snd_mixer () != nullptr ) {
    snd_mixer_set_callback ( snd_mixer (), nullptr );
    snd_mixer_set_callback_private ( snd_mixer (), nullptr );

    if ( _snd_hctl != nullptr ) {
      snd_mixer_detach_hctl ( snd_mixer (), _snd_hctl );
    }

    snd_mixer_close ( snd_mixer () );
    _snd_mixer = nullptr;
  }

  if ( _snd_hctl != nullptr ) {
    snd_hctl_close ( _snd_hctl );
    _snd_hctl = nullptr;
  }
}

void
Mixer::close ()
{
  close_mixer ();

  // Clean up
  _device_address.clear ();
  _err_func.clear ();
  _err_message.clear ();
}

bool
Mixer::open ( const QString & device_address_n )
{
  close ();

  _device_address = device_address_n;

  if ( _device_address.isEmpty () ) {
    _err_func = "open";
    _err_message = tr ( "Empty device name" );
    return false;
  }

  {
    int err = snd_mixer_open ( &_snd_mixer, 0 );
    if ( err < 0 ) {
      _err_func = "snd_mixer_open";
      _err_message = snd_error_qstring ( err );
      return false;
    }
  }

  {
    int err = snd_mixer_selem_register ( _snd_mixer, NULL, NULL );
    if ( err < 0 ) {
      _err_func = "snd_mixer_selem_register";
      _err_message = snd_error_qstring ( err );
      close_mixer ();
      return false;
    }
  }

  // Open control handle
  {
    int err = snd_hctl_open ( &_snd_hctl,
                              _device_address.toLocal8Bit ().constData (),
                              SND_CTL_NONBLOCK );
    if ( err < 0 ) {
      _err_func = "snd_hctl_open";
      _err_message = snd_error_qstring ( err );
      close_mixer ();
      return false;
    }
  }

  // Attach hctl handle to mixer
  {
    int err = snd_mixer_attach_hctl ( _snd_mixer, _snd_hctl );
    if ( err < 0 ) {
      _snd_hctl = nullptr;
      _err_func = "snd_mixer_attach_hctl";
      _err_message = snd_error_qstring ( err );
      close_mixer ();
      return false;
    }
  }

  // Load mixer
  {
    int err = snd_mixer_load ( _snd_mixer );
    if ( err < 0 ) {
      _err_func = "snd_mixer_load";
      _err_message = snd_error_qstring ( err );
      close_mixer ();
      return false;
    }
  }

  // Install alsa callback
  snd_mixer_set_callback_private ( snd_mixer (), this );
  snd_mixer_set_callback ( snd_mixer (), &Mixer::alsa_callback_mixer );

  // Create mixer elements
  create_mixer_elems ();

  // Create socket notifiers
  if ( !create_socket_notifiers () ) {
    close_mixer ();
    return false;
  }

  // Success
  return true;
}

void
Mixer::create_mixer_elems ()
{
  // Create and Elem objects
  _mixer_elems.reserve ( 128 );
  {
    snd_mixer_elem_t * snd_elem = snd_mixer_first_elem ( snd_mixer () );
    while ( snd_elem != nullptr ) {
      // Add to list
      _mixer_elems.push_back (
          std::make_unique< Elem > ( this, snd_mixer (), snd_elem ) );
      snd_elem = snd_mixer_elem_next ( snd_elem );
    }
  }

  // Filter Elem objects
  if ( !_elem_filters.empty () ) {
    // List for dropped (filtered) elements
    Element_Vector elems_dropped;
    elems_dropped.reserve ( _mixer_elems.size () );

    // Filter elements
    for ( auto & elem_filter : _elem_filters ) {
      elem_filter->filter ( _mixer_elems, elems_dropped );
    }
  }
  _mixer_elems.shrink_to_fit ();

  // Handle names that occur multiple times
  {
    const QString str_new = QLatin1StringView ( "%1, %2" );
    for ( std::size_t ii = 0; ii < _mixer_elems.size (); ++ii ) {
      const auto & mx_elem = _mixer_elems[ ii ];
      QString str_cur = mx_elem->display_name ();

      bool dbl = false;

      // Detect if there're doubles at all
      for ( std::size_t jj = ii + 1; jj < _mixer_elems.size (); ++jj ) {
        const auto & mx_elem2 = _mixer_elems[ jj ];
        if ( mx_elem2->display_name () == str_cur ) {
          dbl = true;
          break;
        }
      }

      // Rename doubles
      if ( dbl ) {
        for ( std::size_t jj = ii; jj < _mixer_elems.size (); ++jj ) {
          const auto & mx_elem2 = _mixer_elems[ jj ];
          if ( mx_elem2->display_name () == str_cur ) {
            QString nname ( str_new );
            nname = nname.arg ( str_cur );
            nname = nname.arg ( mx_elem2->elem_index () );
            mx_elem2->set_display_name ( nname );
          }
        }
      }
    }
  }
}

bool
Mixer::create_socket_notifiers ()
{
  // std::cout << "Mixer::create_socket_notifiers" << "\n";

  // Setup socket notifiers
  {
    int num_fds = snd_mixer_poll_descriptors_count ( _snd_mixer );
    if ( num_fds >= 0 ) {
      _pollfds.resize ( num_fds );
    } else {
      _err_func = "snd_mixer_poll_descriptors_count";
      _err_message = snd_error_qstring ( num_fds );
      return false;
    }
  }

  if ( _pollfds.size () > 0 ) {
    memset ( _pollfds.data (), 0, _pollfds.size () * sizeof ( pollfd ) );
    int err = snd_mixer_poll_descriptors (
        _snd_mixer, _pollfds.data (), _pollfds.size () );
    if ( err < 0 ) {
      _err_func = "snd_mixer_poll_descriptors";
      _err_message = snd_error_qstring ( err );
      return false;
    }
  }

  for ( const auto & pfd : _pollfds ) {
    int fd = pfd.fd;
    if ( fd != 0 ) {
      auto notifier = std::make_unique< QSocketNotifier > (
          fd, QSocketNotifier::Read, this );
      connect ( notifier.get (),
                &QSocketNotifier::activated,
                this,
                &Mixer::socket_event );
      _socket_notifiers.push_back ( std::move ( notifier ) );
    }
  }

  return true;
}

void
Mixer::set_socked_notifiers_enabled ( bool flag_n )
{
  for ( auto & notifier : _socket_notifiers ) {
    notifier->setEnabled ( flag_n );
  }
}

void
Mixer::signalize_all_changes_later ()
{
  if ( !_signalize_changes_requested ) {
    _signalize_changes_requested = true;
    QCoreApplication::postEvent ( this,
                                  new QEvent ( QSnd::evt_signalize_changes ) );
  }
}

void
Mixer::signalize_all_changes ()
{
  for ( const auto & mx_elem : _mixer_elems ) {
    mx_elem->signalize_changes ();
  }
}

bool
Mixer::event ( QEvent * event_n )
{
  bool res = true;

  if ( event_n->type () == QSnd::evt_signalize_changes ) {

    _signalize_changes_requested = false;
    signalize_all_changes ();

  } else if ( event_n->type () == QSnd::evt_reload ) {

    _reload_requested = false;
    Q_EMIT sig_mixer_reload_request ();

  } else {

    res = QObject::event ( event_n );
  }

  return res;
}

void
Mixer::socket_event ( int )
{
  if ( snd_mixer () == nullptr ) {
    return;
  }

  int num_ev = snd_mixer_handle_events ( snd_mixer () );
  if ( num_ev < 0 ) {
    // Error. Disable socket notifiers
    set_socked_notifiers_enabled ( false );
    print_alsa_error ( "snd_mixer_handle_events ", num_ev );
  }

  signalize_all_changes ();
}

void
Mixer::request_reload_later ()
{
  if ( !_reload_requested ) {
    _reload_requested = true;
    QCoreApplication::postEvent ( this, new QEvent ( QSnd::evt_reload ) );
  }
}

// Alsa callbacks

int
Mixer::alsa_callback_mixer ( snd_mixer_t * snd_mixer_n,
                             unsigned int mask_n,
                             snd_mixer_elem_t * )
{
  int res = 0;

  Mixer * qsnd_mixer = nullptr;
  {
    void * priv ( snd_mixer_get_callback_private ( snd_mixer_n ) );
    qsnd_mixer = reinterpret_cast< Mixer * > ( priv );
  }

  if ( qsnd_mixer != nullptr ) {
    const unsigned int change_mask =
        ( SND_CTL_EVENT_MASK_VALUE | SND_CTL_EVENT_MASK_INFO |
          SND_CTL_EVENT_MASK_ADD | SND_CTL_EVENT_MASK_TLV );

    if ( ( mask_n == SND_CTL_EVENT_MASK_REMOVE ) ||
         ( ( mask_n & change_mask ) != 0 ) ) {
      qsnd_mixer->request_reload_later ();
    } else {
      {
        std::ostringstream msg;
        msg << "Mixer::alsa_callback_mixer: ";
        msg << "Unknown mask ( " << mask_n << " )" << std::endl;
        std::cerr << msg.str ();
      }
      res = -1;
    }
  }

  return res;
}

} // namespace QSnd::Mixer
