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

#include "elem.hpp"
#include "qsnd/hctl/mixer.hpp"
#include <QCoreApplication>
#include <QEvent>
#include <iostream>
#include <sstream>

namespace QSnd::HCtl
{

Elem::Elem ( const Info_Db & info_db_n,
             QObject * parent_n,
             snd_hctl_t * hctl_n,
             snd_hctl_elem_t * hctl_elem_n )
: QObject ( parent_n )
, _info_db ( info_db_n )
, _snd_hctl ( hctl_n )
, _snd_hctl_elem ( hctl_elem_n )
{
  snd_ctl_elem_info_malloc ( &_snd_ctl_elem_info );
  snd_ctl_elem_value_malloc ( &_snd_ctl_elem_value );

  snd_ctl_elem_info_clear ( _snd_ctl_elem_info );
  snd_ctl_elem_value_clear ( _snd_ctl_elem_value );

  if ( snd_hctl_elem () == nullptr ) {
    return;
  }

  // Load info
  snd_hctl_elem_info ( snd_hctl_elem (), _snd_ctl_elem_info );

  _display_name = QCoreApplication::translate (
      "ALSA::Elem_Name", snd_hctl_elem_get_name ( snd_hctl_elem () ) );

  snd_hctl_elem_set_callback_private ( snd_hctl_elem (), this );
  snd_hctl_elem_set_callback ( snd_hctl_elem (),
                               &Elem::alsa_callback_hctl_elem );

  update_value_mark ();
}

Elem::~Elem ()
{
  if ( snd_hctl_elem () != nullptr ) {
    snd_hctl_elem_set_callback ( snd_hctl_elem (), nullptr );
    snd_hctl_elem_set_callback_private ( snd_hctl_elem (), nullptr );
  }

  snd_ctl_elem_info_free ( _snd_ctl_elem_info );
  snd_ctl_elem_value_free ( _snd_ctl_elem_value );
}

QString
Elem::enum_item_display_name ( unsigned int enum_idx_n ) const
{
  return QCoreApplication::translate ( "ALSA::Enum_Value",
                                       enum_item_name ( enum_idx_n ) );
}

void
Elem::set_display_name ( const QString & name_n )
{
  _display_name = name_n;
}

// Boolean type

bool
Elem::switches_equal () const
{
  bool res = true;
  const std::size_t num = count ();
  if ( num > 1 ) {
    long state0 = switch_state ( 0 );
    for ( std::size_t ii = 1; ii < num; ++ii ) {
      if ( switch_state ( ii ) != state0 ) {
        res = false;
        break;
      }
    }
  }
  return res;
}

void
Elem::set_switch_state ( unsigned int idx_n, bool state_n )
{
  snd_ctl_elem_value_set_boolean ( _snd_ctl_elem_value, idx_n, state_n );
  value_was_set ();
}

void
Elem::set_switch_all ( bool state_n )
{
  std::size_t num = count ();
  for ( std::size_t ii = 0; ii < num; ++ii ) {
    snd_ctl_elem_value_set_boolean ( _snd_ctl_elem_value, ii, state_n );
  }
  value_was_set ();
}

void
Elem::invert_switch_state ( unsigned int idx_n )
{
  bool state ( !switch_state ( idx_n ) );
  snd_ctl_elem_value_set_boolean ( _snd_ctl_elem_value, idx_n, state );
  value_was_set ();
}

void
Elem::invert_switch_all ()
{
  std::size_t num = count ();
  for ( std::size_t ii = 0; ii < num; ++ii ) {
    bool state ( !switch_state ( ii ) );
    snd_ctl_elem_value_set_boolean ( _snd_ctl_elem_value, ii, state );
  }
  value_was_set ();
}

void
Elem::level_switches ()
{
  const std::size_t num_switches = count ();

  std::size_t num_on = 0;
  std::size_t num_off = 0;

  for ( std::size_t ii = 0; ii < num_switches; ++ii ) {
    if ( switch_state ( ii ) ) {
      ++num_on;
    } else {
      ++num_off;
    }
  }

  set_switch_all ( ( num_on >= num_off ) );
}

// Integer type

bool
Elem::integers_equal () const
{
  bool res = true;
  const std::size_t num = count ();
  if ( num > 1 ) {
    long val0 = integer_value ( 0 );
    for ( std::size_t ii = 1; ii < num; ++ii ) {
      if ( integer_value ( ii ) != val0 ) {
        res = false;
        break;
      }
    }
  }
  return res;
}

void
Elem::set_integer ( unsigned int idx_n, long value_n )
{
  // std::cout << "Elem::set_integer " << idx_n << " value " <<
  //  value_n << "\n";

  snd_ctl_elem_value_set_integer ( _snd_ctl_elem_value, idx_n, value_n );
  value_was_set ();
}

void
Elem::set_integer_all ( long value_n )
{
  const std::size_t num = count ();
  for ( std::size_t ii = 0; ii < num; ++ii ) {
    snd_ctl_elem_value_set_integer ( _snd_ctl_elem_value, ii, value_n );
  }
  value_was_set ();
}

void
Elem::level_integers ()
{
  const std::size_t num = count ();

  if ( num < 2 ) {
    return;
  }

  const long num_int ( num );
  long vol_avrg_accu ( 0 );
  long vol_mod_accu ( 0 );

  for ( std::size_t ii = 0; ii < num; ++ii ) {
    const long val = integer_value ( ii );

    long vol_mod;
    if ( ( val >= num_int ) || ( val <= ( -num_int ) ) ) {
      const long vol_avrg ( val / num_int );
      vol_avrg_accu += vol_avrg;
      vol_mod = val;
      // Subtraction performed in two steps to prevent and overflow
      vol_mod -= vol_avrg * ( num_int - 1 );
      vol_mod -= vol_avrg;
    } else {
      vol_mod = val;
    }

    vol_mod_accu += vol_mod;

    if ( ( vol_mod_accu >= num_int ) || ( vol_mod_accu <= ( -num_int ) ) ) {
      const long vol_avrg ( vol_mod_accu / num_int );
      vol_avrg_accu += vol_avrg;
      vol_mod_accu -= ( vol_avrg * num_int );
    }
  }

  set_integer_all ( vol_avrg_accu );
}

// Decibel

bool
Elem::has_dB () const
{
  long tmp[ 2 ];
  return ( dB_range ( &tmp[ 0 ], &tmp[ 1 ] ) == 0 );
}

int
Elem::dB_range ( long * min_n, long * max_n ) const
{
  snd_ctl_elem_id_t * elem_id;
  snd_ctl_elem_id_alloca ( &elem_id );
  snd_ctl_elem_info_get_id ( _snd_ctl_elem_info, elem_id );
  return snd_ctl_get_dB_range (
      snd_hctl_ctl ( _snd_hctl ), elem_id, min_n, max_n );
}

long
Elem::dB_value ( unsigned int idx_n ) const
{
  return ask_dB_from_int ( integer_value ( idx_n ) );
}

long
Elem::ask_dB_from_int ( long int_value_n ) const
{
  long res = 0;

  snd_ctl_elem_id_t * elem_id;
  snd_ctl_elem_id_alloca ( &elem_id );
  snd_ctl_elem_info_get_id ( _snd_ctl_elem_info, elem_id );
  snd_ctl_convert_to_dB (
      snd_hctl_ctl ( _snd_hctl ), elem_id, int_value_n, &res );

  return res;
}

long
Elem::ask_int_from_dB ( long db_value_n, int round_dir_n ) const
{
  long res = 0;

  snd_ctl_elem_id_t * elem_id;
  snd_ctl_elem_id_alloca ( &elem_id );
  snd_ctl_elem_info_get_id ( _snd_ctl_elem_info, elem_id );
  snd_ctl_convert_from_dB (
      snd_hctl_ctl ( _snd_hctl ), elem_id, db_value_n, &res, round_dir_n );

  return res;
}

// Enum type

bool
Elem::enum_idices_equal () const
{
  bool res = true;
  const std::size_t num ( count () );
  if ( num > 1 ) {
    unsigned int idx0 ( enum_index ( 0 ) );
    for ( std::size_t ii = 1; ii < num; ++ii ) {
      if ( enum_index ( ii ) != idx0 ) {
        res = false;
        break;
      }
    }
  }
  return res;
}

void
Elem::set_enum_index ( unsigned int channel_idx_n, unsigned int enum_idx_n )
{
  snd_ctl_elem_value_set_enumerated (
      _snd_ctl_elem_value, channel_idx_n, enum_idx_n );
  value_was_set ();
}

void
Elem::set_enum_index_all ( unsigned int enum_idx_n )
{
  std::size_t num = count ();
  for ( std::size_t ii = 0; ii < num; ++ii ) {
    snd_ctl_elem_value_set_enumerated ( _snd_ctl_elem_value, ii, enum_idx_n );
  }
  value_was_set ();
}

void
Elem::level_enums ()
{
  unsigned int idx = 0;
  if ( count () > 0 ) {
    idx = enum_index ( 0 );
  }
  set_enum_index_all ( idx );
}

// General

bool
Elem::values_equal () const
{
  bool res ( true );
  if ( is_boolean () ) {
    res = switches_equal ();
  } else if ( is_integer () ) {
    res = integers_equal ();
  } else if ( is_enumerated () ) {
    res = enum_idices_equal ();
  }
  return res;
}

void
Elem::level_values ()
{
  if ( is_boolean () ) {
    level_switches ();
  } else if ( is_integer () ) {
    level_integers ();
  } else if ( is_enumerated () ) {
    level_enums ();
  }
}

void
Elem::value_was_set ()
{
  snd_hctl_elem_t * hctl_elem = snd_hctl_elem ();
  if ( hctl_elem != nullptr ) {
    int err = snd_hctl_elem_write ( hctl_elem, _snd_ctl_elem_value );
    if ( err >= 0 ) {
      // Read values back
      update_value_mark ();
      // Call into mixer
      if ( QObject * par = parent (); par != nullptr ) {
        if ( Mixer * mixer = dynamic_cast< Mixer * > ( par );
             mixer != nullptr ) {
          mixer->signalize_all_changes_later ();
        }
      }
    } else {
      print_alsa_error ( "snd_hctl_elem_write", err );
    }
  }
}

void
Elem::update_value ()
{
  snd_hctl_elem_t * hctl_elem = snd_hctl_elem ();
  if ( hctl_elem != nullptr ) {
    int err = snd_hctl_elem_read ( hctl_elem, _snd_ctl_elem_value );
    if ( err < 0 ) {
      print_alsa_error ( "snd_hctl_elem_read", err );
    }
  }
}

void
Elem::update_value_mark ()
{
  _values_changed = true;
  update_value ();
}

void
Elem::signalize_changes ()
{
  if ( _values_changed ) {
    _values_changed = false;
    Q_EMIT sig_values_changed ();
  }
}

void
Elem::signalize_element_changed ()
{
  if ( snd_hctl_elem () != nullptr ) {
    snd_hctl_elem_info ( snd_hctl_elem (), _snd_ctl_elem_info );
  }
  // Call into mixer to request a reload
  if ( QObject * par = parent (); par != nullptr ) {
    if ( Mixer * mixer = dynamic_cast< Mixer * > ( par ); mixer != nullptr ) {
      mixer->request_reload_later ();
    }
  }
}

// Alsa callbacks

int
Elem::alsa_callback_hctl_elem ( snd_hctl_elem_t * elem_n, unsigned int mask_n )
{
  int res = 0;

  Elem * smce = nullptr;
  {
    void * priv ( snd_hctl_elem_get_callback_private ( elem_n ) );
    smce = reinterpret_cast< Elem * > ( priv );
  }

  if ( smce != nullptr ) {
    const unsigned int change_mask =
        ( 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 ) ) {
      smce->signalize_element_changed ();
    } else if ( ( mask_n & SND_CTL_EVENT_MASK_VALUE ) != 0 ) {
      smce->update_value_mark ();
    } else {
      // Unusual mask
      {
        std::ostringstream msg;
        msg << "Elem::alsa_callback_hctl_elem: ";
        msg << "Unknown mask ( " << mask_n << " )" << std::endl;
        std::cerr << msg.str ();
      }
      res = -1;
    }
  }

  return res;
}

} // namespace QSnd::HCtl
