#include "Configuration.hpp"

//
// Read me!
//
// This file defines a configuration dialog with the user. The general
// strategy is to expose agreed  configuration parameters via a custom
// interface (See  Configuration.hpp). The state exposed  through this
// public   interface  reflects   stored  or   derived  data   in  the
// Configuration::impl object.   The Configuration::impl  structure is
// an implementation of the PIMPL (a.k.a.  Cheshire Cat or compilation
// firewall) implementation hiding idiom that allows internal state to
// be completely removed from the public interface.
//
// There  is a  secondary level  of parameter  storage which  reflects
// current settings UI  state, these parameters are not  copied to the
// state   store  that   the  public   interface  exposes   until  the
// Configuration:impl::accept() operation is  successful. The accept()
// operation is  tied to the settings  OK button. The normal  and most
// convenient place to store this intermediate settings UI state is in
// the data models of the UI  controls, if that is not convenient then
// separate member variables  must be used to store that  state. It is
// important for the user experience that no publicly visible settings
// are changed  while the  settings UI are  changed i.e.  all settings
// changes   must    be   deferred   until   the    "OK"   button   is
// clicked. Conversely, all changes must  be discarded if the settings
// UI "Cancel" button is clicked.
//
// There is  a complication related  to the radio interface  since the
// this module offers  the facility to test the  radio interface. This
// test means  that the  public visibility to  the radio  being tested
// must be  changed.  To  maintain the  illusion of  deferring changes
// until they  are accepted, the  original radio related  settings are
// stored upon showing  the UI and restored if the  UI is dismissed by
// canceling.
//
// It  should be  noted that  the  settings UI  lives as  long as  the
// application client that uses it does. It is simply shown and hidden
// as it  is needed rather than  creating it on demand.  This strategy
// saves a  lot of  expensive UI  drawing at the  expense of  a little
// storage and  gives a  convenient place  to deliver  settings values
// from.
//
// Here is an overview of the high level flow of this module:
//
// 1)  On  construction the  initial  environment  is initialized  and
// initial   values  for   settings  are   read  from   the  QSettings
// database. At  this point  default values for  any new  settings are
// established by  providing a  default value  to the  QSettings value
// queries. This should be the only place where a hard coded value for
// a   settings  item   is   defined.   Any   remaining  one-time   UI
// initialization is also done. At the end of the constructor a method
// initialize_models()  is called  to  load the  UI  with the  current
// settings values.
//
// 2) When the settings UI is displayed by a client calling the exec()
// operation, only temporary state need be stored as the UI state will
// already mirror the publicly visible settings state.
//
// 3) As  the user makes  changes to  the settings UI  only validation
// need be  carried out since the  UI control data models  are used as
// the temporary store of unconfirmed settings.  As some settings will
// depend  on each  other a  validate() operation  is available,  this
// operation implements a check of any complex multi-field values.
//
// 4) If the  user discards the settings changes by  dismissing the UI
// with the  "Cancel" button;  the reject()  operation is  called. The
// reject() operation calls initialize_models()  which will revert all
// the  UI visible  state  to  the values  as  at  the initial  exec()
// operation.  No   changes  are  moved   into  the  data   fields  in
// Configuration::impl that  reflect the  settings state  published by
// the public interface (Configuration.hpp).
//
// 5) If  the user accepts the  settings changes by dismissing  the UI
// with the "OK" button; the  accept() operation is called which calls
// the validate() operation  again and, if it passes,  the fields that
// are used  to deliver  the settings  state are  updated from  the UI
// control models  or other temporary  state variables. At the  end of
// the accept()  operation, just  before hiding  the UI  and returning
// control to the caller; the new  settings values are stored into the
// settings database by a call to the write_settings() operation, thus
// ensuring that  settings changes are  saved even if  the application
// crashes or is subsequently killed.
//
// 6)  On  destruction,  which   only  happens  when  the  application
// terminates,  the settings  are saved  to the  settings database  by
// calling the  write_settings() operation. This is  largely redundant
// but is still done to save the default values of any new settings on
// an initial run.
//
// To add a new setting:
//
// 1) Update the UI with the new widget to view and change the value.
//
// 2)  Add  a member  to  Configuration::impl  to store  the  accepted
// setting state. If the setting state is dynamic; add a new signal to
// broadcast the setting value.
//
// 3) Add a  query method to the  public interface (Configuration.hpp)
// to access the  new setting value. If the settings  is dynamic; this
// step  is optional  since  value  changes will  be  broadcast via  a
// signal.
//
// 4) Add a forwarding operation to implement the new query (3) above.
//
// 5)  Add a  settings read  call to  read_settings() with  a sensible
// default value. If  the setting value is dynamic, add  a signal emit
// call to broadcast the setting value change.
//
// 6) Add  code to  initialize_models() to  load the  widget control's
// data model with the current value.
//
// 7) If there is no convenient data model field, add a data member to
// store the proposed new value. Ensure  this member has a valid value
// on exit from read_settings().
//
// 8)  Add  any  required  inter-field validation  to  the  validate()
// operation.
//
// 9) Add code to the accept()  operation to extract the setting value
// from  the  widget   control  data  model  and  load   it  into  the
// Configuration::impl  member  that  reflects  the  publicly  visible
// setting state. If  the setting value is dynamic; add  a signal emit
// call to broadcast any changed state of the setting.
//
// 10) Add  a settings  write call  to save the  setting value  to the
// settings database.
//

#include <stdexcept>
#include <iterator>
#include <algorithm>
#include <functional>
#include <limits>
#include <cmath>

#include <QApplication>
#include <QCursor>
#include <QMetaType>
#include <QList>
#include <QPair>
#include <QVariant>
#include <QSettings>
#include <QAudioDeviceInfo>
#include <QAudioInput>
#include <QDialog>
#include <QAction>
#include <QFileDialog>
#include <QDir>
#include <QTemporaryFile>
#include <QFormLayout>
#include <QString>
#include <QStringList>
#include <QStringListModel>
#include <QLineEdit>
#include <QRegularExpression>
#include <QRegularExpressionValidator>
#include <QIntValidator>
#include <QThread>
#include <QTimer>
#include <QStandardPaths>
#include <QFont>
#include <QFontDialog>
#include <QSerialPortInfo>
#include <QScopedPointer>
#include <QNetworkInterface>
#include <QHostInfo>
#include <QHostAddress>
#include <QStandardItem>
#include <QDebug>
#include <QDateTimeEdit>
#include <QJsonObject>
#include <QJsonDocument>
#include <QJsonArray>


#include "pimpl_impl.hpp"
#include "Logger.hpp"
#include "qt_helpers.hpp"
#include "MetaDataRegistry.hpp"
#include "SettingsGroup.hpp"
#include "widgets/FrequencyLineEdit.hpp"
#include "widgets/FrequencyDeltaLineEdit.hpp"
#include "item_delegates/CandidateKeyFilter.hpp"
#include "item_delegates/ForeignKeyDelegate.hpp"
#include "item_delegates/FrequencyDelegate.hpp"
#include "item_delegates/FrequencyDeltaDelegate.hpp"
#include "item_delegates/MessageItemDelegate.hpp"
#include "Transceiver/TransceiverFactory.hpp"
#include "Transceiver/Transceiver.hpp"
#include "models/Bands.hpp"
#include "models/IARURegions.hpp"
#include "models/Modes.hpp"
#include "models/FrequencyList.hpp"
#include "models/StationList.hpp"
#include "Network/NetworkServerLookup.hpp"
#include "widgets/MessageBox.hpp"
#include "validators/MaidenheadLocatorValidator.hpp"
#include "validators/CallsignValidator.hpp"
#include "Network/LotWUsers.hpp"
#include "models/DecodeHighlightingModel.hpp"
#include "logbook/logbook.h"
#include "widgets/LazyFillComboBox.hpp"

#include "ui_Configuration.h"
#include "moc_Configuration.cpp"

namespace
{
  // these undocumented flag values when stored in (Qt::UserRole - 1)
  // of a ComboBox item model index allow the item to be enabled or
  // disabled
  int const combo_box_item_enabled (32 | 1);
  int const combo_box_item_disabled (0);

//  QRegExp message_alphabet {"[- A-Za-z0-9+./?]*"};
  QRegularExpression message_alphabet {"[- @A-Za-z0-9+./?#<>;$]*"};
  QRegularExpression RTTY_roundup_exchange_re {
    R"(
        (
           AL|AZ|AR|CA|CO|CT|DE|FL|GA      # 48 contiguous states
          |ID|IL|IN|IA|KS|KY|LA|ME|MD
          |MA|MI|MN|MS|MO|MT|NE|NV|NH|NJ
          |NM|NY|NC|ND|OH|OK|OR|PA|RI|SC
          |SD|TN|TX|UT|VT|VA|WA|WV|WI|WY
          |NB|NS|QC|ON|MB|SK|AB|BC|NWT|NF  # VE provinces
          |LB|NU|YT|PEI
          |DC                              # District of Columbia
          |DX                              # anyone else
          |SCC                             # Slovenia Contest Club contest
          |DR|FR|GD|GR|OV|ZH|ZL            # Dutch provinces (also FL,NH,UT,NB,LB)
          |X01|X02|X03|X04|X05|X06|X07     # 99 neutral exchanges
          |X08|X09|X10|X11|X12|X13|X14
          |X15|X16|X17|X18|X19|X20|X21
          |X22|X23|X24|X25|X26|X27|X28
          |X29|X30|X31|X32|X33|X34|X35
          |X36|X37|X38|X39|X40|X41|X42
          |X43|X44|X45|X46|X47|X48|X49
          |X50|X51|X52|X53|X54|X55|X56
          |X57|X58|X59|X60|X61|X62|X63
          |X64|X65|X66|X67|X68|X69|X70
          |X71|X72|X73|X74|X75|X76|X77
          |X78|X79|X80|X81|X82|X83|X84
          |X85|X86|X87|X88|X89|X90|X91
          |X92|X93|X94|X95|X96|X97|X98
          |X99
        )
      )", QRegularExpression::CaseInsensitiveOption | QRegularExpression::ExtendedPatternSyntaxOption};

  QRegularExpression field_day_exchange_re {
    R"(
        (
           [1-9]                          # # transmitters (1 to 32 inc.)
          |[0-2]\d
          |3[0-2]
        )
        [A-F]\ *                          # class and optional space
        (
           AB|AK|AL|AR|AZ|BC|CO|CT|DE|EB  # ARRL/RAC section
          |EMA|ENY|EPA|EWA|GA|GH|IA|ID
          |IL|IN|KS|KY|LA|LAX|MB|MDC|ME
          |MI|MN|MO|MS|MT|NB|NC|ND|NE|NFL
          |NH|NL|NLI|NM|NNJ|NNY|NS|NTX|NV
          |OH|OK|ONE|ONN|ONS|OR|ORG|PAC
          |PE|PR|QC|RI|SB|SC|SCV|SD|SDG
          |SF|SFL|SJV|SK|SNJ|STX|SV|TER
          |TN|UT|VA|VI|VT|WCF|WI|WMA|WNY
          |WPA|WTX|WV|WWA|WY
          |DX                             # anyone else
        )
      )", QRegularExpression::CaseInsensitiveOption | QRegularExpression::ExtendedPatternSyntaxOption};

  // Magic numbers for file validation
  constexpr quint32 qrg_magic {0xadbccbdb};
  constexpr quint32 qrg_version {101}; // M.mm
  constexpr quint32 qrg_version_100 {100};
}


//
// Dialog to get a new Frequency item
//
class FrequencyDialog final
  : public QDialog
{
  Q_OBJECT

public:
  using Item = FrequencyList_v2_101::Item;

  explicit FrequencyDialog (IARURegions * regions_model, Modes * modes_model, QWidget * parent = nullptr)
    : QDialog {parent}
  {
    start_date_time_edit_ = new QDateTimeEdit(QDateTime(QDate::currentDate(), QTime(0,0,0,0), Qt::UTC), parent);
    end_date_time_edit_ = new QDateTimeEdit(QDateTime(QDate::currentDate().addDays(2), QTime(0,0,0,0), Qt::UTC), parent);

    enable_dates_checkbox_ = new QCheckBox {tr ("")};
    start_date_time_edit_->setDisplayFormat("yyyy.MM.dd hh:mm:ss 'UTC'");
    start_date_time_edit_->setTimeSpec(Qt::UTC);
    start_date_time_edit_->setMinimumDate(QDate::currentDate().addDays(-365));

    end_date_time_edit_->setDisplayFormat("yyyy.MM.dd hh:mm:ss 'UTC'");
    end_date_time_edit_->setTimeSpec(Qt::UTC);
    end_date_time_edit_->setMinimumDate(QDate::currentDate().addDays(-365));
    preferred_frequency_checkbox_ = new QCheckBox {tr ("")};

    setWindowTitle (QApplication::applicationName () + " - " +
                    tr ("Add Frequency"));

    region_combo_box_.setModel (regions_model);
    mode_combo_box_.setModel (modes_model);

    auto form_layout = new QFormLayout ();
    form_layout->addRow (tr ("IARU &Region:"), &region_combo_box_);
    form_layout->addRow (tr ("&Mode:"), &mode_combo_box_);
    form_layout->addRow (tr ("&Frequency (MHz):"), &frequency_line_edit_);
    form_layout->addRow (tr ("&Preferred for Band/Mode:"), preferred_frequency_checkbox_);
    form_layout->addRow (tr ("&Description:"), &description_line_edit_);
    form_layout->addRow (tr ("&Enable Date Range:"), enable_dates_checkbox_);
    form_layout->addRow (tr ("S&tart:"), start_date_time_edit_);
    form_layout->addRow (tr ("&End:"), end_date_time_edit_);
    form_layout->addRow (tr ("&Source:"), &source_line_edit_);

    auto main_layout = new QVBoxLayout (this);
    main_layout->addLayout (form_layout);

    button_box = new QDialogButtonBox {QDialogButtonBox::Ok | QDialogButtonBox::Cancel};
    main_layout->addWidget (button_box);

    connect (button_box, &QDialogButtonBox::accepted, this, &FrequencyDialog::accept);
    connect (button_box, &QDialogButtonBox::rejected, this, &FrequencyDialog::reject);
    connect(start_date_time_edit_, &QDateTimeEdit::dateTimeChanged, this, &FrequencyDialog::checkSaneDates);
    connect(end_date_time_edit_, &QDateTimeEdit::dateTimeChanged, this, &FrequencyDialog::checkSaneDates);
    connect(enable_dates_checkbox_, &QCheckBox::stateChanged, this, &FrequencyDialog::toggleValidity);
    toggleValidity();
  }

    void toggleValidity()
    {
        start_date_time_edit_->setEnabled(enable_dates_checkbox_->isChecked());
        end_date_time_edit_->setEnabled(enable_dates_checkbox_->isChecked());
        checkSaneDates();
    }

    void checkSaneDates()
    {
        if (enable_dates_checkbox_->isChecked() && start_date_time_edit_->dateTime().isValid() && end_date_time_edit_->dateTime().isValid())
        {
            if (start_date_time_edit_->dateTime() > end_date_time_edit_->dateTime())
            {
                QMessageBox::warning(this, tr("Invalid Date Range"), tr("Start date must be before end date"));
                button_box->button(QDialogButtonBox::Ok)->setEnabled(false);
                return;
            }
        }
        button_box->button(QDialogButtonBox::Ok)->setEnabled(true);
    }

  Item item () const
  {
    QDateTime start_time = enable_dates_checkbox_->isChecked() ? start_date_time_edit_->dateTime() : QDateTime();
    QDateTime end_time = enable_dates_checkbox_->isChecked() ? end_date_time_edit_->dateTime()  : QDateTime();
    return {
            frequency_line_edit_.frequency(),
            Modes::value(mode_combo_box_.currentText()),
            IARURegions::value(region_combo_box_.currentText()),
            description_line_edit_.text(), source_line_edit_.text(),
            start_time,
            end_time,
            preferred_frequency_checkbox_->isChecked()
    };
  }

private:
  QComboBox region_combo_box_;
  QComboBox mode_combo_box_;
  FrequencyLineEdit frequency_line_edit_;
  QLineEdit description_line_edit_;
  QLineEdit source_line_edit_;
  QDialogButtonBox * button_box;
  QCheckBox *enable_dates_checkbox_;
  QCheckBox *preferred_frequency_checkbox_;
  QDateTimeEdit *end_date_time_edit_;
  QDateTimeEdit *start_date_time_edit_;
};


//
// Dialog to get a new Station item
//
class StationDialog final
  : public QDialog
{
  Q_OBJECT

public:
  explicit StationDialog (StationList const * stations, Bands * bands, QWidget * parent = nullptr)
    : QDialog {parent}
    , filtered_bands_ {new CandidateKeyFilter {bands, stations, 0, 0}}
  {
    setWindowTitle (QApplication::applicationName () + " - " + tr ("Add Station"));

    band_.setModel (filtered_bands_.data ());
      
    auto form_layout = new QFormLayout ();
    form_layout->addRow (tr ("&Band:"), &band_);
    form_layout->addRow (tr ("&Offset (MHz):"), &delta_);
    form_layout->addRow (tr ("&Antenna:"), &description_);

    auto main_layout = new QVBoxLayout (this);
    main_layout->addLayout (form_layout);

    auto button_box = new QDialogButtonBox {QDialogButtonBox::Ok | QDialogButtonBox::Cancel};
    main_layout->addWidget (button_box);

    connect (button_box, &QDialogButtonBox::accepted, this, &StationDialog::accept);
    connect (button_box, &QDialogButtonBox::rejected, this, &StationDialog::reject);

    if (delta_.text ().isEmpty ())
      {
        delta_.setText ("0");
      }
  }

  StationList::Station station () const
  {
    return {band_.currentText (), delta_.frequency_delta (), description_.text ()};
  }

  int exec () override
  {
    filtered_bands_->set_active_key ();
    return QDialog::exec ();
  }

private:
  QScopedPointer<CandidateKeyFilter> filtered_bands_;

  QComboBox band_;
  FrequencyDeltaLineEdit delta_;
  QLineEdit description_;
};

class RearrangableMacrosModel
  : public QStringListModel
{
public:
  Qt::ItemFlags flags (QModelIndex const& index) const override
  {
    auto flags = QStringListModel::flags (index);
    if (index.isValid ())
      {
        // disallow drop onto existing items
        flags &= ~Qt::ItemIsDropEnabled;
      }
    return flags;
  }
};




// Internal implementation of the Configuration class.
class Configuration::impl final
  : public QDialog
{
  Q_OBJECT;

public:
  using FrequencyDelta = Radio::FrequencyDelta;
  using port_type = Configuration::port_type;
  using audio_info_type = QPair<QAudioDeviceInfo, QList<QVariant> >;

  explicit impl (Configuration * self
                 , QNetworkAccessManager * network_manager
                 , QDir const& temp_directory
                 , QSettings * settings
                 , LogBook * logbook
                 , QWidget * parent);
  ~impl ();

  bool have_rig ();

  void transceiver_frequency (Frequency);
  void transceiver_tx_frequency (Frequency);
  void transceiver_mode (MODE);
  void transceiver_ptt (bool);
  void sync_transceiver (bool force_signal);

  Q_SLOT int exec () override;
  Q_SLOT void accept () override;
  Q_SLOT void reject () override;
  Q_SLOT void done (int) override;

private:
  typedef QList<QAudioDeviceInfo> AudioDevices;

  void read_settings ();
  void write_settings ();

  void find_audio_devices ();
  QAudioDeviceInfo find_audio_device (QAudio::Mode, QComboBox *, QString const& device_name);
  void load_audio_devices (QAudio::Mode, QComboBox *, QAudioDeviceInfo *);
  void update_audio_channels (QComboBox const *, int, QComboBox *, bool);

  void load_network_interfaces (CheckableItemComboBox *, QStringList current);
  Q_SLOT void validate_network_interfaces (QString const&);
  QStringList get_selected_network_interfaces (CheckableItemComboBox *);
  Q_SLOT void host_info_results (QHostInfo);
  void check_multicast (QHostAddress const&);

  void find_tab (QWidget *);

  void initialize_models ();
  bool split_mode () const
  {
    return
      (WSJT_RIG_NONE_CAN_SPLIT || !rig_is_dummy_) &&
      (rig_params_.split_mode != TransceiverFactory::split_mode_none);
  }
  void set_cached_mode ();
  bool open_rig (bool force = false);
  //bool set_mode ();
  void close_rig ();
  TransceiverFactory::ParameterPack gather_rig_data ();
  void enumerate_rigs ();
  void set_rig_invariants ();
  bool validate ();
  void fill_port_combo_box (QComboBox *);
  Frequency apply_calibration (Frequency) const;
  Frequency remove_calibration (Frequency) const;

  void delete_frequencies ();
  void load_frequencies ();
  void merge_frequencies ();
  void save_frequencies ();
  void reset_frequencies ();
  void insert_frequency ();
  void size_frequency_table_columns();

    FrequencyList_v2_101::FrequencyItems read_frequencies_file (QString const&);

  void delete_stations ();
  void insert_station ();

  Q_SLOT void on_font_push_button_clicked ();
  Q_SLOT void on_decoded_text_font_push_button_clicked ();
  Q_SLOT void on_PTT_port_combo_box_activated (int);
  Q_SLOT void on_CAT_port_combo_box_activated (int);
  Q_SLOT void on_CAT_serial_baud_combo_box_currentIndexChanged (int);
  Q_SLOT void on_CAT_data_bits_button_group_buttonClicked (int);
  Q_SLOT void on_CAT_stop_bits_button_group_buttonClicked (int);
  Q_SLOT void on_CAT_handshake_button_group_buttonClicked (int);
  Q_SLOT void on_CAT_poll_interval_spin_box_valueChanged (int);
  Q_SLOT void on_split_mode_button_group_buttonClicked (int);
  Q_SLOT void on_test_CAT_push_button_clicked ();
  Q_SLOT void on_test_PTT_push_button_clicked (bool checked);
  Q_SLOT void on_force_DTR_combo_box_currentIndexChanged (int);
  Q_SLOT void on_force_RTS_combo_box_currentIndexChanged (int);
  Q_SLOT void on_rig_combo_box_currentIndexChanged (int);
  Q_SLOT void on_add_macro_push_button_clicked (bool = false);
  Q_SLOT void on_delete_macro_push_button_clicked (bool = false);
  Q_SLOT void on_PTT_method_button_group_buttonClicked (int);
  Q_SLOT void on_add_macro_line_edit_editingFinished ();
  Q_SLOT void delete_macro ();
  void delete_selected_macros (QModelIndexList);
  Q_SLOT void on_udp_server_line_edit_textChanged (QString const&);
  Q_SLOT void on_udp_server_line_edit_editingFinished ();
  Q_SLOT void on_save_path_select_push_button_clicked (bool);
  Q_SLOT void on_azel_path_select_push_button_clicked (bool);
  Q_SLOT void on_calibration_intercept_spin_box_valueChanged (double);
  Q_SLOT void on_calibration_slope_ppm_spin_box_valueChanged (double);
  Q_SLOT void handle_transceiver_update (TransceiverState const&, unsigned sequence_number);
  Q_SLOT void handle_transceiver_failure (QString const& reason);
  Q_SLOT void on_reset_highlighting_to_defaults_push_button_clicked (bool);
  Q_SLOT void on_rescan_log_push_button_clicked (bool);
  Q_SLOT void on_LotW_CSV_fetch_push_button_clicked (bool);
  Q_SLOT void on_cbx2ToneSpacing_clicked(bool);
  Q_SLOT void on_cbx4ToneSpacing_clicked(bool);
  Q_SLOT void on_prompt_to_log_check_box_clicked(bool);
  Q_SLOT void on_cbAutoLog_clicked(bool);
  Q_SLOT void on_Field_Day_Exchange_textEdited (QString const&);
  Q_SLOT void on_RTTY_Exchange_textEdited (QString const&);
  Q_SLOT void on_Contest_Name_textEdited (QString const&);

  // typenames used as arguments must match registered type names :(
  Q_SIGNAL void start_transceiver (unsigned seqeunce_number) const;
  Q_SIGNAL void set_transceiver (Transceiver::TransceiverState const&,
                                 unsigned sequence_number) const;
  Q_SIGNAL void stop_transceiver () const;

  Configuration * const self_;	// back pointer to public interface

  QThread * transceiver_thread_;
  TransceiverFactory transceiver_factory_;
  QList<QMetaObject::Connection> rig_connections_;

  QScopedPointer<Ui::configuration_dialog> ui_;

  QNetworkAccessManager * network_manager_;
  QSettings * settings_;
  LogBook * logbook_;

  QDir doc_dir_;
  QDir data_dir_;
  QDir temp_dir_;
  QDir writeable_data_dir_;
  QDir default_save_directory_;
  QDir save_directory_;
  QDir default_azel_directory_;
  QDir azel_directory_;

  QFont font_;
  QFont next_font_;

  QFont decoded_text_font_;
  QFont next_decoded_text_font_;

  LotWUsers lotw_users_;

  bool restart_sound_input_device_;
  bool restart_sound_output_device_;

  Type2MsgGen type_2_msg_gen_;

  QStringListModel macros_;
  RearrangableMacrosModel next_macros_;
  QAction * macro_delete_action_;

  Bands bands_;
  IARURegions regions_;
  IARURegions::Region region_;
  Modes modes_;
  FrequencyList_v2_101 frequencies_;
  FrequencyList_v2_101 next_frequencies_;
  StationList stations_;
  StationList next_stations_;
  FrequencyDelta current_offset_;
  FrequencyDelta current_tx_offset_;

  QAction * frequency_delete_action_;
  QAction * frequency_insert_action_;
  QAction * load_frequencies_action_;
  QAction * save_frequencies_action_;
  QAction * merge_frequencies_action_;
  QAction * reset_frequencies_action_;
  FrequencyDialog * frequency_dialog_;

  QAction station_delete_action_;
  QAction station_insert_action_;
  StationDialog * station_dialog_;

  DecodeHighlightingModel decode_highlighing_model_;
  DecodeHighlightingModel next_decode_highlighing_model_;
  bool highlight_by_mode_;
  bool highlight_only_fields_;
  bool include_WAE_entities_;
  bool highlight_73_;
  int LotW_days_since_upload_;

  TransceiverFactory::ParameterPack rig_params_;
  TransceiverFactory::ParameterPack saved_rig_params_;
  TransceiverFactory::Capabilities::PortType last_port_type_;
  bool rig_is_dummy_;
  bool rig_active_;
  bool have_rig_;
  bool rig_changed_;
  TransceiverState cached_rig_state_;
  int rig_resolution_;          // see Transceiver::resolution signal
  CalibrationParams calibration_;
  bool frequency_calibration_disabled_; // not persistent
  unsigned transceiver_command_number_;
  QString dynamic_grid_;

  // configuration fields that we publish
  QString my_callsign_;
  QString my_grid_;
  QString FD_exchange_;
  QString RTTY_exchange_;
  QString Contest_Name_;

  qint32 id_interval_;
  qint32 ntrials_;
  qint32 aggressive_;
  qint32 RxBandwidth_;
  double degrade_;
  double txDelay_;
  bool id_after_73_;
  bool tx_QSY_allowed_;
  bool spot_to_psk_reporter_;
  bool psk_reporter_tcpip_;
  bool monitor_off_at_startup_;
  bool monitor_last_used_;
  bool log_as_RTTY_;
  bool report_in_comments_;
  bool prompt_to_log_;
  bool autoLog_;
  bool decodes_from_top_;
  bool insert_blank_;
  bool DXCC_;
  bool ppfx_;
  bool clear_DX_;
  bool miles_;
  bool quick_call_;
  bool disable_TX_on_73_;
  bool force_call_1st_;
  bool alternate_bindings_;
  int watchdog_;
  bool TX_messages_;
  bool enable_VHF_features_;
  bool decode_at_52s_;
  bool single_decode_;
  bool twoPass_;
  bool Individual_Contest_Name_;
  bool bSpecialOp_;
  int  SelectedActivity_;
  bool x2ToneSpacing_;
  bool x4ToneSpacing_;
  bool use_dynamic_grid_;
  QString opCall_;
  QString udp_server_name_;
  bool udp_server_name_edited_;
  int dns_lookup_id_;
  port_type udp_server_port_;
  QStringList udp_interface_names_;
  QString loopback_interface_name_;
  int udp_TTL_;
  QString n1mm_server_name_;
  port_type n1mm_server_port_;
  bool broadcast_to_n1mm_;
  bool accept_udp_requests_;
  bool udpWindowToFront_;
  bool udpWindowRestore_;
  DataMode data_mode_;
  bool bLowSidelobes_;
  bool pwrBandTxMemory_;
  bool pwrBandTuneMemory_;
  bool highlight_DXcall_;
  bool highlight_DXgrid_;

  QAudioDeviceInfo audio_input_device_;
  QAudioDeviceInfo next_audio_input_device_;
  AudioDevice::Channel audio_input_channel_;
  AudioDevice::Channel next_audio_input_channel_;
  QAudioDeviceInfo audio_output_device_;
  QAudioDeviceInfo next_audio_output_device_;
  AudioDevice::Channel audio_output_channel_;
  AudioDevice::Channel next_audio_output_channel_;

  friend class Configuration;
};

#include "Configuration.moc"


// delegate to implementation class
Configuration::Configuration (QNetworkAccessManager * network_manager, QDir const& temp_directory,
                              QSettings * settings, LogBook * logbook, QWidget * parent)
  : m_ {this, network_manager, temp_directory, settings, logbook, parent}
{
}

Configuration::~Configuration ()
{
}

QDir Configuration::doc_dir () const {return m_->doc_dir_;}
QDir Configuration::data_dir () const {return m_->data_dir_;}
QDir Configuration::writeable_data_dir () const {return m_->writeable_data_dir_;}
QDir Configuration::temp_dir () const {return m_->temp_dir_;}

void Configuration::select_tab (int index) {m_->ui_->configuration_tabs->setCurrentIndex (index);}
int Configuration::exec () {return m_->exec ();}
bool Configuration::is_active () const {return m_->isVisible ();}

QAudioDeviceInfo const& Configuration::audio_input_device () const {return m_->audio_input_device_;}
AudioDevice::Channel Configuration::audio_input_channel () const {return m_->audio_input_channel_;}
QAudioDeviceInfo const& Configuration::audio_output_device () const {return m_->audio_output_device_;}
AudioDevice::Channel Configuration::audio_output_channel () const {return m_->audio_output_channel_;}
bool Configuration::restart_audio_input () const {return m_->restart_sound_input_device_;}
bool Configuration::restart_audio_output () const {return m_->restart_sound_output_device_;}
auto Configuration::type_2_msg_gen () const -> Type2MsgGen {return m_->type_2_msg_gen_;}
QString Configuration::my_callsign () const {return m_->my_callsign_;}
QFont Configuration::text_font () const {return m_->font_;}
QFont Configuration::decoded_text_font () const {return m_->decoded_text_font_;}
qint32 Configuration::id_interval () const {return m_->id_interval_;}
qint32 Configuration::ntrials() const {return m_->ntrials_;}
qint32 Configuration::aggressive() const {return m_->aggressive_;}
double Configuration::degrade() const {return m_->degrade_;}
double Configuration::txDelay() const {return m_->txDelay_;}
qint32 Configuration::RxBandwidth() const {return m_->RxBandwidth_;}
bool Configuration::id_after_73 () const {return m_->id_after_73_;}
bool Configuration::tx_QSY_allowed () const {return m_->tx_QSY_allowed_;}
bool Configuration::spot_to_psk_reporter () const
{
  // rig must be open and working to spot externally
  return is_transceiver_online () && m_->spot_to_psk_reporter_;
}
bool Configuration::psk_reporter_tcpip () const {return m_->psk_reporter_tcpip_;}
bool Configuration::monitor_off_at_startup () const {return m_->monitor_off_at_startup_;}
bool Configuration::monitor_last_used () const {return m_->rig_is_dummy_ || m_->monitor_last_used_;}
bool Configuration::log_as_RTTY () const {return m_->log_as_RTTY_;}
bool Configuration::report_in_comments () const {return m_->report_in_comments_;}
bool Configuration::prompt_to_log () const {return m_->prompt_to_log_;}
bool Configuration::autoLog() const {return m_->autoLog_;}
bool Configuration::decodes_from_top () const {return m_->decodes_from_top_;}
bool Configuration::insert_blank () const {return m_->insert_blank_;}
bool Configuration::DXCC () const {return m_->DXCC_;}
bool Configuration::ppfx() const {return m_->ppfx_;}
bool Configuration::clear_DX () const {return m_->clear_DX_;}
bool Configuration::miles () const {return m_->miles_;}
bool Configuration::quick_call () const {return m_->quick_call_;}
bool Configuration::disable_TX_on_73 () const {return m_->disable_TX_on_73_;}
bool Configuration::force_call_1st() const {return m_->force_call_1st_;}
bool Configuration::alternate_bindings() const {return m_->alternate_bindings_;}
int Configuration::watchdog () const {return m_->watchdog_;}
bool Configuration::TX_messages () const {return m_->TX_messages_;}
bool Configuration::enable_VHF_features () const {return m_->enable_VHF_features_;}
bool Configuration::decode_at_52s () const {return m_->decode_at_52s_;}
bool Configuration::single_decode () const {return m_->single_decode_;}
bool Configuration::twoPass() const {return m_->twoPass_;}
bool Configuration::Individual_Contest_Name() const {return m_->Individual_Contest_Name_;}
bool Configuration::x2ToneSpacing() const {return m_->x2ToneSpacing_;}
bool Configuration::x4ToneSpacing() const {return m_->x4ToneSpacing_;}
bool Configuration::split_mode () const {return m_->split_mode ();}
QString Configuration::opCall() const {return m_->opCall_;}
void Configuration::opCall (QString const& call) {m_->opCall_ = call;}
QString Configuration::udp_server_name () const {return m_->udp_server_name_;}
auto Configuration::udp_server_port () const -> port_type {return m_->udp_server_port_;}
QStringList Configuration::udp_interface_names () const {return m_->udp_interface_names_;}
int Configuration::udp_TTL () const {return m_->udp_TTL_;}
bool Configuration::accept_udp_requests () const {return m_->accept_udp_requests_;}
QString Configuration::n1mm_server_name () const {return m_->n1mm_server_name_;}
auto Configuration::n1mm_server_port () const -> port_type {return m_->n1mm_server_port_;}
bool Configuration::broadcast_to_n1mm () const {return m_->broadcast_to_n1mm_;}
bool Configuration::lowSidelobes() const {return m_->bLowSidelobes_;}
bool Configuration::udpWindowToFront () const {return m_->udpWindowToFront_;}
bool Configuration::udpWindowRestore () const {return m_->udpWindowRestore_;}
Bands * Configuration::bands () {return &m_->bands_;}
Bands const * Configuration::bands () const {return &m_->bands_;}
StationList * Configuration::stations () {return &m_->stations_;}
StationList const * Configuration::stations () const {return &m_->stations_;}
IARURegions::Region Configuration::region () const {return m_->region_;}
FrequencyList_v2_101 * Configuration::frequencies () {return &m_->frequencies_;}
FrequencyList_v2_101 const * Configuration::frequencies () const {return &m_->frequencies_;}
QStringListModel * Configuration::macros () {return &m_->macros_;}
QStringListModel const * Configuration::macros () const {return &m_->macros_;}
QDir Configuration::save_directory () const {return m_->save_directory_;}
QDir Configuration::azel_directory () const {return m_->azel_directory_;}
QString Configuration::rig_name () const {return m_->rig_params_.rig_name;}
bool Configuration::pwrBandTxMemory () const {return m_->pwrBandTxMemory_;}
bool Configuration::pwrBandTuneMemory () const {return m_->pwrBandTuneMemory_;}
LotWUsers const& Configuration::lotw_users () const {return m_->lotw_users_;}
DecodeHighlightingModel const& Configuration::decode_highlighting () const {return m_->decode_highlighing_model_;}
bool Configuration::highlight_by_mode () const {return m_->highlight_by_mode_;}
bool Configuration::highlight_only_fields () const {return m_->highlight_only_fields_;}
bool Configuration::include_WAE_entities () const {return m_->include_WAE_entities_;}
bool Configuration::highlight_73 () const {return m_->highlight_73_;}
bool Configuration::highlight_DXcall () const {return m_->highlight_DXcall_;}
bool Configuration::highlight_DXgrid () const {return m_->highlight_DXgrid_;}

void Configuration::set_calibration (CalibrationParams params)
{
  m_->calibration_ = params;
}

void Configuration::enable_calibration (bool on)
{
  auto target_frequency = m_->remove_calibration (m_->cached_rig_state_.frequency ()) - m_->current_offset_;
  m_->frequency_calibration_disabled_ = !on;
  transceiver_frequency (target_frequency);
}

bool Configuration::is_transceiver_online () const
{
  return m_->rig_active_;
}

bool Configuration::is_dummy_rig () const
{
  return m_->rig_is_dummy_;
}

bool Configuration::transceiver_online ()
{
  LOG_TRACE (m_->cached_rig_state_);
  return m_->have_rig ();
}

int Configuration::transceiver_resolution () const
{
  return m_->rig_resolution_;
}

void Configuration::transceiver_offline ()
{
  LOG_TRACE (m_->cached_rig_state_);
  m_->close_rig ();
}

void Configuration::transceiver_frequency (Frequency f)
{
  LOG_TRACE (f << ' ' << m_->cached_rig_state_);
  m_->transceiver_frequency (f);
}

void Configuration::transceiver_tx_frequency (Frequency f)
{
  LOG_TRACE (f << ' ' << m_->cached_rig_state_);
  m_->transceiver_tx_frequency (f);
}

void Configuration::transceiver_mode (MODE mode)
{
  LOG_TRACE (mode << ' ' << m_->cached_rig_state_);
  m_->transceiver_mode (mode);
}

void Configuration::transceiver_ptt (bool on)
{
  LOG_TRACE (on << ' ' << m_->cached_rig_state_);
  m_->transceiver_ptt (on);
}

void Configuration::sync_transceiver (bool force_signal, bool enforce_mode_and_split)
{
  LOG_TRACE ("force signal: " << force_signal << " enforce_mode_and_split: " << enforce_mode_and_split << ' ' << m_->cached_rig_state_);
  m_->sync_transceiver (force_signal);
  if (!enforce_mode_and_split)
    {
      m_->transceiver_tx_frequency (0);
    }
}

void Configuration::invalidate_audio_input_device (QString /* error */)
{
  m_->audio_input_device_ = QAudioDeviceInfo {};
}

void Configuration::invalidate_audio_output_device (QString /* error */)
{
  m_->audio_output_device_ = QAudioDeviceInfo {};
}

bool Configuration::valid_n1mm_info () const
{
  // do very rudimentary checking on the n1mm server name and port number.
  //
  auto server_name = m_->n1mm_server_name_;
  auto port_number = m_->n1mm_server_port_;
  return(!(server_name.trimmed().isEmpty() || port_number == 0));
}

QString Configuration::my_grid() const
{
  auto the_grid = m_->my_grid_;
  if (m_->use_dynamic_grid_ && m_->dynamic_grid_.size () >= 4) {
    the_grid = m_->dynamic_grid_;
  }
  return the_grid;
}

QString Configuration::Field_Day_Exchange() const
{
  return m_->FD_exchange_;
}
/*
void Configuration::setEU_VHF_Contest()
{ 
  m_->bSpecialOp_=true;
  m_->ui_->gbSpecialOpActivity->setChecked(m_->bSpecialOp_);
  m_->ui_->rbEU_VHF_Contest->setChecked(true);
  m_->SelectedActivity_ = static_cast<int> (SpecialOperatingActivity::EU_VHF);
  m_->write_settings();
}
*/

QString Configuration::RTTY_Exchange() const
{
  return m_->RTTY_exchange_;
}

QString Configuration::Contest_Name() const
{
  return m_->Contest_Name_;
}

auto Configuration::special_op_id () const -> SpecialOperatingActivity
{
  return m_->bSpecialOp_ ? static_cast<SpecialOperatingActivity> (m_->SelectedActivity_) : SpecialOperatingActivity::NONE;
}

void Configuration::set_location (QString const& grid_descriptor)
{
  // change the dynamic grid
  // qDebug () << "Configuration::set_location - location:" << grid_descriptor;
  m_->dynamic_grid_ = grid_descriptor.trimmed ();
}

void Configuration::setSpecial_Hound()
{
  m_->bSpecialOp_=true;
  m_->ui_->gbSpecialOpActivity->setChecked(m_->bSpecialOp_);
  m_->ui_->rbHound->setChecked(true);
  m_->SelectedActivity_ = static_cast<int> (SpecialOperatingActivity::HOUND);
  m_->write_settings();
}

void Configuration::setSpecial_Fox()
{
  m_->bSpecialOp_=true;
  m_->ui_->gbSpecialOpActivity->setChecked(m_->bSpecialOp_);
  m_->ui_->rbFox->setChecked(true);
  m_->SelectedActivity_ = static_cast<int> (SpecialOperatingActivity::FOX);
  m_->write_settings();
}

void Configuration::setSpecial_None()
{
  m_->bSpecialOp_=false;
  m_->ui_->gbSpecialOpActivity->setChecked(m_->bSpecialOp_);
  m_->write_settings();
}
namespace
{
#if defined (Q_OS_MAC)
  char const * app_root = "/../../../";
#else
  char const * app_root = "/../";
#endif
  QString doc_path ()
  {
#if CMAKE_BUILD
    if (QDir::isRelativePath (CMAKE_INSTALL_DOCDIR))
      {
	return QApplication::applicationDirPath () + app_root + CMAKE_INSTALL_DOCDIR;
      }
    return CMAKE_INSTALL_DOCDIR;
#else
    return QApplication::applicationDirPath ();
#endif
  }

  QString data_path ()
  {
#if CMAKE_BUILD
    if (QDir::isRelativePath (CMAKE_INSTALL_DATADIR))
      {
	return QApplication::applicationDirPath () + app_root + CMAKE_INSTALL_DATADIR + QChar {'/'} + CMAKE_PROJECT_NAME;
      }
    return CMAKE_INSTALL_DATADIR;
#else
    return QApplication::applicationDirPath ();
#endif
  }
}

Configuration::impl::impl (Configuration * self, QNetworkAccessManager * network_manager
                           , QDir const& temp_directory, QSettings * settings, LogBook * logbook
                           , QWidget * parent)
  : QDialog {parent}
  , self_ {self}
  , transceiver_thread_ {nullptr}
  , ui_ {new Ui::configuration_dialog}
  , network_manager_ {network_manager}
  , settings_ {settings}
  , logbook_ {logbook}
  , doc_dir_ {doc_path ()}
  , data_dir_ {data_path ()}
  , temp_dir_ {temp_directory}
  , writeable_data_dir_ {QStandardPaths::writableLocation (QStandardPaths::DataLocation)}
  , lotw_users_ {network_manager_}
  , restart_sound_input_device_ {false}
  , restart_sound_output_device_ {false}
  , frequencies_ {&bands_}
  , next_frequencies_ {&bands_}
  , stations_ {&bands_}
  , next_stations_ {&bands_}
  , current_offset_ {0}
  , current_tx_offset_ {0}
  , frequency_dialog_ {new FrequencyDialog {&regions_, &modes_, this}}
  , station_delete_action_ {tr ("&Delete"), nullptr}
  , station_insert_action_ {tr ("&Insert ..."), nullptr}
  , station_dialog_ {new StationDialog {&next_stations_, &bands_, this}}
  , highlight_by_mode_ {false}
  , highlight_only_fields_ {false}
  , include_WAE_entities_ {false}
  , highlight_73_ {false}
  , LotW_days_since_upload_ {0}
  , last_port_type_ {TransceiverFactory::Capabilities::none}
  , rig_is_dummy_ {false}
  , rig_active_ {false}
  , have_rig_ {false}
  , rig_changed_ {false}
  , rig_resolution_ {0}
  , frequency_calibration_disabled_ {false}
  , transceiver_command_number_ {0}
  , degrade_ {0.}               // initialize to zero each run, not
                                // saved in settings
  , udp_server_name_edited_ {false}
  , dns_lookup_id_ {-1}
{
  ui_->setupUi (this);

  {
    // Make sure the default save directory exists
    QString save_dir {"save"};
    default_save_directory_ = writeable_data_dir_;
    default_azel_directory_ = writeable_data_dir_;
    if (!default_save_directory_.mkpath (save_dir) || !default_save_directory_.cd (save_dir))
      {
        MessageBox::critical_message (this, tr ("Failed to create save directory"),
                                      tr ("path: \"%1\%")
                                      .arg (default_save_directory_.absoluteFilePath (save_dir)));
        throw std::runtime_error {"Failed to create save directory"};
      }

    // we now have a default save path that exists

    // make sure samples directory exists
    QString samples_dir {"samples"};
    if (!default_save_directory_.mkpath (samples_dir))
      {
        MessageBox::critical_message (this, tr ("Failed to create samples directory"),
                                      tr ("path: \"%1\"")
                                      .arg (default_save_directory_.absoluteFilePath (samples_dir)));
        throw std::runtime_error {"Failed to create samples directory"};
      }

    // copy in any new sample files to the sample directory
    QDir dest_dir {default_save_directory_};
    dest_dir.cd (samples_dir);
    
    QDir source_dir {":/" + samples_dir};
    source_dir.cd (save_dir);
    source_dir.cd (samples_dir);
    auto list = source_dir.entryInfoList (QStringList {{"*.wav"}}, QDir::Files | QDir::Readable);
    Q_FOREACH (auto const& item, list)
      {
        if (!dest_dir.exists (item.fileName ()))
          {
            QFile file {item.absoluteFilePath ()};
            file.copy (dest_dir.absoluteFilePath (item.fileName ()));
          }
      }
  }

  // this must be done after the default paths above are set
  read_settings ();

  // set up dynamic loading of audio devices
  connect (ui_->sound_input_combo_box, &LazyFillComboBox::about_to_show_popup, [this] () {
      QGuiApplication::setOverrideCursor (QCursor {Qt::WaitCursor});
      load_audio_devices (QAudio::AudioInput, ui_->sound_input_combo_box, &next_audio_input_device_);
      update_audio_channels (ui_->sound_input_combo_box, ui_->sound_input_combo_box->currentIndex (), ui_->sound_input_channel_combo_box, false);
      ui_->sound_input_channel_combo_box->setCurrentIndex (next_audio_input_channel_);
      QGuiApplication::restoreOverrideCursor ();
    });
  connect (ui_->sound_output_combo_box, &LazyFillComboBox::about_to_show_popup, [this] () {
      QGuiApplication::setOverrideCursor (QCursor {Qt::WaitCursor});
      load_audio_devices (QAudio::AudioOutput, ui_->sound_output_combo_box, &next_audio_output_device_);
      update_audio_channels (ui_->sound_output_combo_box, ui_->sound_output_combo_box->currentIndex (), ui_->sound_output_channel_combo_box, true);
      ui_->sound_output_channel_combo_box->setCurrentIndex (next_audio_output_channel_);
      QGuiApplication::restoreOverrideCursor ();
    });

  // set up dynamic loading of network interfaces
  connect (ui_->udp_interfaces_combo_box, &LazyFillComboBox::about_to_show_popup, [this] () {
      QGuiApplication::setOverrideCursor (QCursor {Qt::WaitCursor});
      load_network_interfaces (ui_->udp_interfaces_combo_box, udp_interface_names_);
      QGuiApplication::restoreOverrideCursor ();
    });
  connect (ui_->udp_interfaces_combo_box, &QComboBox::currentTextChanged, this, &Configuration::impl::validate_network_interfaces);

  // set up LoTW users CSV file fetching
  connect (&lotw_users_, &LotWUsers::load_finished, [this] () {
      ui_->LotW_CSV_fetch_push_button->setEnabled (true);
    });
  lotw_users_.set_local_file_path (writeable_data_dir_.absoluteFilePath ("lotw-user-activity.csv"));

  //
  // validation
  //
  ui_->callsign_line_edit->setValidator (new CallsignValidator {this});
  ui_->grid_line_edit->setValidator (new MaidenheadLocatorValidator {this});
  ui_->add_macro_line_edit->setValidator (new QRegularExpressionValidator {message_alphabet, this});
  ui_->Field_Day_Exchange->setValidator (new QRegularExpressionValidator {field_day_exchange_re, this});
  ui_->RTTY_Exchange->setValidator (new QRegularExpressionValidator {RTTY_roundup_exchange_re, this});

  //
  // assign ids to radio buttons
  //
  ui_->CAT_data_bits_button_group->setId (ui_->CAT_default_bit_radio_button, TransceiverFactory::default_data_bits);
  ui_->CAT_data_bits_button_group->setId (ui_->CAT_7_bit_radio_button, TransceiverFactory::seven_data_bits);
  ui_->CAT_data_bits_button_group->setId (ui_->CAT_8_bit_radio_button, TransceiverFactory::eight_data_bits);

  ui_->CAT_stop_bits_button_group->setId (ui_->CAT_default_stop_bit_radio_button, TransceiverFactory::default_stop_bits);
  ui_->CAT_stop_bits_button_group->setId (ui_->CAT_one_stop_bit_radio_button, TransceiverFactory::one_stop_bit);
  ui_->CAT_stop_bits_button_group->setId (ui_->CAT_two_stop_bit_radio_button, TransceiverFactory::two_stop_bits);

  ui_->CAT_handshake_button_group->setId (ui_->CAT_handshake_default_radio_button, TransceiverFactory::handshake_default);
  ui_->CAT_handshake_button_group->setId (ui_->CAT_handshake_none_radio_button, TransceiverFactory::handshake_none);
  ui_->CAT_handshake_button_group->setId (ui_->CAT_handshake_xon_radio_button, TransceiverFactory::handshake_XonXoff);
  ui_->CAT_handshake_button_group->setId (ui_->CAT_handshake_hardware_radio_button, TransceiverFactory::handshake_hardware);

  ui_->PTT_method_button_group->setId (ui_->PTT_VOX_radio_button, TransceiverFactory::PTT_method_VOX);
  ui_->PTT_method_button_group->setId (ui_->PTT_CAT_radio_button, TransceiverFactory::PTT_method_CAT);
  ui_->PTT_method_button_group->setId (ui_->PTT_DTR_radio_button, TransceiverFactory::PTT_method_DTR);
  ui_->PTT_method_button_group->setId (ui_->PTT_RTS_radio_button, TransceiverFactory::PTT_method_RTS);

  ui_->TX_audio_source_button_group->setId (ui_->TX_source_mic_radio_button, TransceiverFactory::TX_audio_source_front);
  ui_->TX_audio_source_button_group->setId (ui_->TX_source_data_radio_button, TransceiverFactory::TX_audio_source_rear);

  ui_->TX_mode_button_group->setId (ui_->mode_none_radio_button, data_mode_none);
  ui_->TX_mode_button_group->setId (ui_->mode_USB_radio_button, data_mode_USB);
  ui_->TX_mode_button_group->setId (ui_->mode_data_radio_button, data_mode_data);

  ui_->split_mode_button_group->setId (ui_->split_none_radio_button, TransceiverFactory::split_mode_none);
  ui_->split_mode_button_group->setId (ui_->split_rig_radio_button, TransceiverFactory::split_mode_rig);
  ui_->split_mode_button_group->setId (ui_->split_emulate_radio_button, TransceiverFactory::split_mode_emulate);

  ui_->special_op_activity_button_group->setId (ui_->rbNA_VHF_Contest, static_cast<int> (SpecialOperatingActivity::NA_VHF));
  ui_->special_op_activity_button_group->setId (ui_->rbEU_VHF_Contest, static_cast<int> (SpecialOperatingActivity::EU_VHF));
  ui_->special_op_activity_button_group->setId (ui_->rbField_Day, static_cast<int> (SpecialOperatingActivity::FIELD_DAY));
  ui_->special_op_activity_button_group->setId (ui_->rbRTTY_Roundup, static_cast<int> (SpecialOperatingActivity::RTTY));
  ui_->special_op_activity_button_group->setId (ui_->rbWW_DIGI, static_cast<int> (SpecialOperatingActivity::WW_DIGI));
  ui_->special_op_activity_button_group->setId (ui_->rbARRL_Digi, static_cast<int> (SpecialOperatingActivity::ARRL_DIGI));
  ui_->special_op_activity_button_group->setId (ui_->rbFox, static_cast<int> (SpecialOperatingActivity::FOX));
  ui_->special_op_activity_button_group->setId (ui_->rbHound, static_cast<int> (SpecialOperatingActivity::HOUND));

  //
  // setup PTT port combo box drop down content
  //
  fill_port_combo_box (ui_->PTT_port_combo_box);
  ui_->PTT_port_combo_box->addItem ("CAT");
  ui_->PTT_port_combo_box->setItemData (ui_->PTT_port_combo_box->count () - 1, "Delegate to proxy CAT service", Qt::ToolTipRole);

  //
  // setup hooks to keep audio channels aligned with devices
  //
  {
    using namespace std;
    using namespace std::placeholders;

    function<void (int)> cb (bind (&Configuration::impl::update_audio_channels, this, ui_->sound_input_combo_box, _1, ui_->sound_input_channel_combo_box, false));
    connect (ui_->sound_input_combo_box, static_cast<void (QComboBox::*)(int)> (&QComboBox::currentIndexChanged), cb);
    cb = bind (&Configuration::impl::update_audio_channels, this, ui_->sound_output_combo_box, _1, ui_->sound_output_channel_combo_box, true);
    connect (ui_->sound_output_combo_box, static_cast<void (QComboBox::*)(int)> (&QComboBox::currentIndexChanged), cb);
  }

  //
  // setup macros list view
  //
  ui_->macros_list_view->setModel (&next_macros_);
  ui_->macros_list_view->setItemDelegate (new MessageItemDelegate {this});

  macro_delete_action_ = new QAction {tr ("&Delete"), ui_->macros_list_view};
  ui_->macros_list_view->insertAction (nullptr, macro_delete_action_);
  connect (macro_delete_action_, &QAction::triggered, this, &Configuration::impl::delete_macro);

  // setup IARU region combo box model
  ui_->region_combo_box->setModel (&regions_);

  //
  // setup working frequencies table model & view
  //
  frequencies_.sort (FrequencyList_v2_101::frequency_column);

  ui_->frequencies_table_view->setModel (&next_frequencies_);
  ui_->frequencies_table_view->horizontalHeader ()->setSectionResizeMode (QHeaderView::ResizeToContents);

  ui_->frequencies_table_view->horizontalHeader ()->setResizeContentsPrecision (0);
  ui_->frequencies_table_view->horizontalHeader ()->moveSection(8, 3); // swap preferred to be in front of description
  ui_->frequencies_table_view->verticalHeader ()->setSectionResizeMode (QHeaderView::ResizeToContents);
  ui_->frequencies_table_view->verticalHeader ()->setResizeContentsPrecision (0);
  ui_->frequencies_table_view->sortByColumn (FrequencyList_v2_101::frequency_column, Qt::AscendingOrder);
  ui_->frequencies_table_view->setColumnHidden (FrequencyList_v2_101::frequency_mhz_column, true);
  ui_->frequencies_table_view->setColumnHidden (FrequencyList_v2_101::source_column, true);

  // delegates
  ui_->frequencies_table_view->setItemDelegateForColumn (FrequencyList_v2_101::frequency_column, new FrequencyDelegate {this});
  ui_->frequencies_table_view->setItemDelegateForColumn (FrequencyList_v2_101::region_column, new ForeignKeyDelegate {&regions_, 0, this});
  ui_->frequencies_table_view->setItemDelegateForColumn (FrequencyList_v2_101::mode_column, new ForeignKeyDelegate {&modes_, 0, this});

  // actions
  frequency_delete_action_ = new QAction {tr ("&Delete"), ui_->frequencies_table_view};
  ui_->frequencies_table_view->insertAction (nullptr, frequency_delete_action_);
  connect (frequency_delete_action_, &QAction::triggered, this, &Configuration::impl::delete_frequencies);

  frequency_insert_action_ = new QAction {tr ("&Insert ..."), ui_->frequencies_table_view};
  ui_->frequencies_table_view->insertAction (nullptr, frequency_insert_action_);
  connect (frequency_insert_action_, &QAction::triggered, this, &Configuration::impl::insert_frequency);

  load_frequencies_action_ = new QAction {tr ("&Load ..."), ui_->frequencies_table_view};
  ui_->frequencies_table_view->insertAction (nullptr, load_frequencies_action_);
  connect (load_frequencies_action_, &QAction::triggered, this, &Configuration::impl::load_frequencies);

  save_frequencies_action_ = new QAction {tr ("&Save as ..."), ui_->frequencies_table_view};
  ui_->frequencies_table_view->insertAction (nullptr, save_frequencies_action_);
  connect (save_frequencies_action_, &QAction::triggered, this, &Configuration::impl::save_frequencies);

  merge_frequencies_action_ = new QAction {tr ("&Merge ..."), ui_->frequencies_table_view};
  ui_->frequencies_table_view->insertAction (nullptr, merge_frequencies_action_);
  connect (merge_frequencies_action_, &QAction::triggered, this, &Configuration::impl::merge_frequencies);

  reset_frequencies_action_ = new QAction {tr ("&Reset"), ui_->frequencies_table_view};
  ui_->frequencies_table_view->insertAction (nullptr, reset_frequencies_action_);
  connect (reset_frequencies_action_, &QAction::triggered, this, &Configuration::impl::reset_frequencies);

  //
  // setup stations table model & view
  //
  stations_.sort (StationList::band_column);
  ui_->stations_table_view->setModel (&next_stations_);
  ui_->stations_table_view->horizontalHeader ()->setSectionResizeMode (QHeaderView::ResizeToContents);
  ui_->stations_table_view->horizontalHeader ()->setResizeContentsPrecision (0);
  ui_->stations_table_view->verticalHeader ()->setSectionResizeMode (QHeaderView::ResizeToContents);
  ui_->stations_table_view->verticalHeader ()->setResizeContentsPrecision (0);
  ui_->stations_table_view->sortByColumn (StationList::band_column, Qt::AscendingOrder);

  // stations delegates
  ui_->stations_table_view->setItemDelegateForColumn (StationList::offset_column, new FrequencyDeltaDelegate {this});
  ui_->stations_table_view->setItemDelegateForColumn (StationList::band_column, new ForeignKeyDelegate {&bands_, &next_stations_, 0, StationList::band_column, this});

  // stations actions
  ui_->stations_table_view->addAction (&station_delete_action_);
  connect (&station_delete_action_, &QAction::triggered, this, &Configuration::impl::delete_stations);

  ui_->stations_table_view->addAction (&station_insert_action_);
  connect (&station_insert_action_, &QAction::triggered, this, &Configuration::impl::insert_station);

  //
  // colours and highlighting setup
  //
  ui_->highlighting_list_view->setModel (&next_decode_highlighing_model_);

  enumerate_rigs ();
  initialize_models ();

  audio_input_device_ = next_audio_input_device_;
  audio_input_channel_ = next_audio_input_channel_;
  audio_output_device_ = next_audio_output_device_;
  audio_output_channel_ = next_audio_output_channel_;

  bool fetch_if_needed {false};
  for (auto const& item : decode_highlighing_model_.items ())
    {
      if (DecodeHighlightingModel::Highlight::LotW == item.type_)
        {
          fetch_if_needed = item.enabled_;
          break;
        }
    }
  // load the LoTW users dictionary if it exists, fetch and load if it
  // doesn't and we need it
  lotw_users_.load (ui_->LotW_CSV_URL_line_edit->text (), fetch_if_needed);

  transceiver_thread_ = new QThread {this};
  transceiver_thread_->start ();
}

Configuration::impl::~impl ()
{
  transceiver_thread_->quit ();
  transceiver_thread_->wait ();
  write_settings ();
}

void Configuration::impl::initialize_models ()
{
  next_audio_input_device_ = audio_input_device_;
  next_audio_input_channel_ = audio_input_channel_;
  next_audio_output_device_ = audio_output_device_;
  next_audio_output_channel_ = audio_output_channel_;
  restart_sound_input_device_ = false;
  restart_sound_output_device_ = false;
  {
    SettingsGroup g {settings_, "Configuration"};
    find_audio_devices ();
  }
  auto pal = ui_->callsign_line_edit->palette ();
  if (my_callsign_.isEmpty ())
    {
      pal.setColor (QPalette::Base, "#ffccff");
    }
  else
    {
      pal.setColor (QPalette::Base, Qt::white);
    }
  ui_->callsign_line_edit->setPalette (pal);
  ui_->grid_line_edit->setPalette (pal);
  ui_->callsign_line_edit->setText (my_callsign_);
  ui_->grid_line_edit->setText (my_grid_);
  ui_->use_dynamic_grid->setChecked(use_dynamic_grid_);
  ui_->CW_id_interval_spin_box->setValue (id_interval_);
  ui_->sbNtrials->setValue (ntrials_);
  ui_->sbTxDelay->setValue (txDelay_);
  ui_->sbAggressive->setValue (aggressive_);
  ui_->sbDegrade->setValue (degrade_);
  ui_->sbBandwidth->setValue (RxBandwidth_);
  ui_->PTT_method_button_group->button (rig_params_.ptt_type)->setChecked (true);

  ui_->save_path_display_label->setText (save_directory_.absolutePath ());
  ui_->azel_path_display_label->setText (azel_directory_.absolutePath ());
  ui_->CW_id_after_73_check_box->setChecked (id_after_73_);
  ui_->tx_QSY_check_box->setChecked (tx_QSY_allowed_);
  ui_->psk_reporter_check_box->setChecked (spot_to_psk_reporter_);
  ui_->psk_reporter_tcpip_check_box->setChecked (psk_reporter_tcpip_);
  ui_->monitor_off_check_box->setChecked (monitor_off_at_startup_);
  ui_->monitor_last_used_check_box->setChecked (monitor_last_used_);
  ui_->log_as_RTTY_check_box->setChecked (log_as_RTTY_);
  ui_->report_in_comments_check_box->setChecked (report_in_comments_);
  ui_->prompt_to_log_check_box->setChecked (prompt_to_log_);
  ui_->cbAutoLog->setChecked(autoLog_);
  ui_->decodes_from_top_check_box->setChecked (decodes_from_top_);
  ui_->insert_blank_check_box->setChecked (insert_blank_);
  ui_->DXCC_check_box->setChecked (DXCC_);
  ui_->ppfx_check_box->setChecked (ppfx_);
  ui_->clear_DX_check_box->setChecked (clear_DX_);
  ui_->miles_check_box->setChecked (miles_);
  ui_->quick_call_check_box->setChecked (quick_call_);
  ui_->disable_TX_on_73_check_box->setChecked (disable_TX_on_73_);
  ui_->force_call_1st_check_box->setChecked (force_call_1st_);
  ui_->alternate_bindings_check_box->setChecked (alternate_bindings_);
  ui_->tx_watchdog_spin_box->setValue (watchdog_);
  ui_->TX_messages_check_box->setChecked (TX_messages_);
  ui_->enable_VHF_features_check_box->setChecked(enable_VHF_features_);
  ui_->decode_at_52s_check_box->setChecked(decode_at_52s_);
  ui_->single_decode_check_box->setChecked(single_decode_);
  ui_->cbTwoPass->setChecked(twoPass_);
  ui_->cbContestName->setChecked(Individual_Contest_Name_);
  ui_->gbSpecialOpActivity->setChecked(bSpecialOp_);
  ui_->special_op_activity_button_group->button (SelectedActivity_)->setChecked (true);
  ui_->cbx2ToneSpacing->setChecked(x2ToneSpacing_);
  ui_->cbx4ToneSpacing->setChecked(x4ToneSpacing_);
  ui_->type_2_msg_gen_combo_box->setCurrentIndex (type_2_msg_gen_);
  ui_->rig_combo_box->setCurrentText (rig_params_.rig_name);
  ui_->TX_mode_button_group->button (data_mode_)->setChecked (true);
  ui_->split_mode_button_group->button (rig_params_.split_mode)->setChecked (true);
  ui_->CAT_serial_baud_combo_box->setCurrentText (QString::number (rig_params_.baud));
  ui_->CAT_data_bits_button_group->button (rig_params_.data_bits)->setChecked (true);
  ui_->CAT_stop_bits_button_group->button (rig_params_.stop_bits)->setChecked (true);
  ui_->CAT_handshake_button_group->button (rig_params_.handshake)->setChecked (true);
  ui_->checkBoxPwrBandTxMemory->setChecked(pwrBandTxMemory_);
  ui_->checkBoxPwrBandTuneMemory->setChecked(pwrBandTuneMemory_);
  if (rig_params_.force_dtr)
    {
      ui_->force_DTR_combo_box->setCurrentIndex (rig_params_.dtr_high ? 1 : 2);
    }
  else
    {
      ui_->force_DTR_combo_box->setCurrentIndex (0);
    }
  if (rig_params_.force_rts)
    {
      ui_->force_RTS_combo_box->setCurrentIndex (rig_params_.rts_high ? 1 : 2);
    }
  else
    {
      ui_->force_RTS_combo_box->setCurrentIndex (0);
    }
  ui_->TX_audio_source_button_group->button (rig_params_.audio_source)->setChecked (true);
  ui_->CAT_poll_interval_spin_box->setValue (rig_params_.poll_interval);
  ui_->opCallEntry->setText (opCall_);
  ui_->udp_server_line_edit->setEnabled(false);
  ui_->udp_server_line_edit->setText (udp_server_name_);
  ui_->udp_server_line_edit->setEnabled(true);
  on_udp_server_line_edit_editingFinished ();
  ui_->udp_server_port_spin_box->setValue (udp_server_port_);
  load_network_interfaces (ui_->udp_interfaces_combo_box, udp_interface_names_);
  if (!udp_interface_names_.size ())
    {
      udp_interface_names_ = get_selected_network_interfaces (ui_->udp_interfaces_combo_box);
    }
  ui_->udp_TTL_spin_box->setValue (udp_TTL_);
  ui_->accept_udp_requests_check_box->setChecked (accept_udp_requests_);
  ui_->n1mm_server_name_line_edit->setText (n1mm_server_name_);
  ui_->n1mm_server_port_spin_box->setValue (n1mm_server_port_);
  ui_->enable_n1mm_broadcast_check_box->setChecked (broadcast_to_n1mm_);
  ui_->udpWindowToFront->setChecked(udpWindowToFront_);
  ui_->udpWindowRestore->setChecked(udpWindowRestore_);
  ui_->calibration_intercept_spin_box->setValue (calibration_.intercept);
  ui_->calibration_slope_ppm_spin_box->setValue (calibration_.slope_ppm);
  ui_->rbLowSidelobes->setChecked(bLowSidelobes_);
  if(!bLowSidelobes_) ui_->rbMaxSensitivity->setChecked(true);

  if (rig_params_.ptt_port.isEmpty ())
    {
      if (ui_->PTT_port_combo_box->count ())
        {
          ui_->PTT_port_combo_box->setCurrentText (ui_->PTT_port_combo_box->itemText (0));
        }
    }
  else
    {
      ui_->PTT_port_combo_box->setCurrentText (rig_params_.ptt_port);
    }

  ui_->region_combo_box->setCurrentIndex (region_);

  next_macros_.setStringList (macros_.stringList ());
  next_frequencies_.frequency_list (frequencies_.frequency_list ());
  next_stations_.station_list (stations_.station_list ());

  next_decode_highlighing_model_.items (decode_highlighing_model_.items ());
  ui_->highlight_by_mode_check_box->setChecked (highlight_by_mode_);
  ui_->only_fields_check_box->setChecked (highlight_only_fields_);
  ui_->include_WAE_check_box->setChecked (include_WAE_entities_);
  ui_->highlight_73_check_box->setChecked (highlight_73_);
  ui_->LotW_days_since_upload_spin_box->setValue (LotW_days_since_upload_);
  ui_->cbHighlightDXcall->setChecked(highlight_DXcall_);
  ui_->cbHighlightDXgrid->setChecked(highlight_DXgrid_);

  set_rig_invariants ();
}

void Configuration::impl::done (int r)
{
  // do this here since window is still on screen at this point
  SettingsGroup g {settings_, "Configuration"};
  settings_->setValue ("window/geometry", saveGeometry ());

  QDialog::done (r);
}

void Configuration::impl::read_settings ()
{
  SettingsGroup g {settings_, "Configuration"};
  LOG_INFO(QString{"Configuration Settings (%1)"}.arg(settings_->fileName()));
  QStringList keys = settings_->allKeys();
  //Q_FOREACH (auto const& item, keys)
  //{
  //  LOG_INFO(QString{"  %1 = %2"}.arg(item).arg(settings_->value(item).toString()));
  //}

  restoreGeometry (settings_->value ("window/geometry").toByteArray ());

  my_callsign_ = settings_->value ("MyCall", QString {}).toString ();
  my_grid_ = settings_->value ("MyGrid", QString {}).toString ();
  FD_exchange_ = settings_->value ("Field_Day_Exchange",QString {}).toString ();
  RTTY_exchange_ = settings_->value ("RTTY_Exchange",QString {}).toString ();
  Contest_Name_ = settings_->value ("Contest_Name",QString {}).toString ();
  ui_->Field_Day_Exchange->setText(FD_exchange_);
  ui_->RTTY_Exchange->setText(RTTY_exchange_);
  if (next_font_.fromString (settings_->value ("Font", QGuiApplication::font ().toString ()).toString ())
      && next_font_ != font_)
    {
      font_ = next_font_;
      Q_EMIT self_->text_font_changed (font_);
    }
  else
    {
      next_font_ = font_;
    }
  if (next_decoded_text_font_.fromString (settings_->value ("DecodedTextFont", "Courier, 10").toString ())
      && next_decoded_text_font_ != decoded_text_font_)
    {
      decoded_text_font_ = next_decoded_text_font_;
      next_decode_highlighing_model_.set_font (decoded_text_font_);
      ui_->highlighting_list_view->reset ();
      Q_EMIT self_->decoded_text_font_changed (decoded_text_font_);
    }
  else
    {
      next_decoded_text_font_ = decoded_text_font_;
    }

  id_interval_ = settings_->value ("IDint", 0).toInt ();
  ntrials_ = settings_->value ("nTrials", 6).toInt ();
  txDelay_ = settings_->value ("TxDelay",0.2).toDouble();
  aggressive_ = settings_->value ("Aggressive", 0).toInt ();
  RxBandwidth_ = settings_->value ("RxBandwidth", 2500).toInt ();
  save_directory_.setPath (settings_->value ("SaveDir", default_save_directory_.absolutePath ()).toString ());
  azel_directory_.setPath (settings_->value ("AzElDir", default_azel_directory_.absolutePath ()).toString ());

  type_2_msg_gen_ = settings_->value ("Type2MsgGen", QVariant::fromValue (Configuration::type_2_msg_3_full)).value<Configuration::Type2MsgGen> ();

  monitor_off_at_startup_ = settings_->value ("MonitorOFF", false).toBool ();
  monitor_last_used_ = settings_->value ("MonitorLastUsed", false).toBool ();
  spot_to_psk_reporter_ = settings_->value ("PSKReporter", false).toBool ();
  psk_reporter_tcpip_ = settings_->value ("PSKReporterTCPIP", false).toBool ();
  id_after_73_ = settings_->value ("After73", false).toBool ();
  tx_QSY_allowed_ = settings_->value ("TxQSYAllowed", false).toBool ();
  use_dynamic_grid_ = settings_->value ("AutoGrid", false).toBool ();

  macros_.setStringList (settings_->value ("Macros", QStringList {"TNX 73 GL"}).toStringList ());

  region_ = settings_->value ("Region", QVariant::fromValue (IARURegions::ALL)).value<IARURegions::Region> ();

  LOG_INFO(QString{"Reading frequencies"});

  if (settings_->contains ("FrequenciesForRegionModes_v2"))
    {
      LOG_INFO(QString{"read_settings found FrequenciesForRegionModes_v2"});
      if (settings_->contains ("FrequenciesForRegionModes"))
        {
          LOG_INFO(QString{"read_settings ALSO found FrequenciesForRegionModes"});
        }

      auto const& v = settings_->value ("FrequenciesForRegionModes_v2");
      if (v.isValid ())
        {
          frequencies_.frequency_list (v.value<FrequencyList_v2_101::FrequencyItems> ());
        }
      else
        {
          frequencies_.reset_to_defaults ();
        }
    }
  else
    {
      LOG_INFO(QString{"read_settings looking for FrequenciesForRegionModes"});
      if (settings_->contains ("FrequenciesForRegionModes")) // has the old ones.
        {
          LOG_INFO(QString{"found FrequenciesForRegionModes"});
          auto const& v = settings_->value("FrequenciesForRegionModes");
          LOG_INFO(QString{"v is %1"}.arg(v.typeName()));
          if (v.isValid())
            {
              LOG_INFO(QString{"read_settings found VALID FrequenciesForRegionModes"});
              FrequencyList_v2_101::FrequencyItems list;
              FrequencyList_v2::FrequencyItems v100 = v.value<FrequencyList_v2::FrequencyItems>();
              LOG_INFO(QString{"read_settings read %1 old_format items from FrequenciesForRegionModes"}.arg(v100.size()));

              Q_FOREACH (auto const& item, v100)
              {
                list << FrequencyList_v2_101::Item{item.frequency_, item.mode_, item.region_, QString(), QString(), QDateTime(),
                                               QDateTime(), false};
              }
              LOG_INFO(QString{"converted %1 items to FrequenciesForRegionModes_v2"}.arg(list.size()));

              frequencies_.frequency_list(list);
            }
            else
            {
              LOG_INFO(QString{"read_settings INVALID FrequenciesForRegionModes"});
              frequencies_.reset_to_defaults();
            }
        }
      else
        {
          frequencies_.reset_to_defaults();
        }
    }

  stations_.station_list (settings_->value ("stations").value<StationList::Stations> ());

  auto highlight_items = settings_->value ("DecodeHighlighting", QVariant::fromValue (DecodeHighlightingModel::default_items ())).value<DecodeHighlightingModel::HighlightItems> ();
  if (!highlight_items.size ()) highlight_items = DecodeHighlightingModel::default_items ();
  decode_highlighing_model_.items (highlight_items);
  highlight_by_mode_ = settings_->value("HighlightByMode", false).toBool ();
  highlight_only_fields_ = settings_->value("OnlyFieldsSought", false).toBool ();
  include_WAE_entities_ = settings_->value("IncludeWAEEntities", false).toBool ();
  highlight_73_ = settings_->value("Highlight73", false).toBool ();
  LotW_days_since_upload_ = settings_->value ("LotWDaysSinceLastUpload", 365).toInt ();
  lotw_users_.set_age_constraint (LotW_days_since_upload_);

  log_as_RTTY_ = settings_->value ("toRTTY", false).toBool ();
  report_in_comments_ = settings_->value("dBtoComments", false).toBool ();
  rig_params_.rig_name = settings_->value ("Rig", TransceiverFactory::basic_transceiver_name_).toString ();
  rig_is_dummy_ = TransceiverFactory::basic_transceiver_name_ == rig_params_.rig_name;
  rig_params_.network_port = settings_->value ("CATNetworkPort").toString ();
  rig_params_.usb_port = settings_->value ("CATUSBPort").toString ();
  rig_params_.serial_port = settings_->value ("CATSerialPort").toString ();
  rig_params_.baud = settings_->value ("CATSerialRate", 4800).toInt ();
  rig_params_.data_bits = settings_->value ("CATDataBits", QVariant::fromValue (TransceiverFactory::default_data_bits)).value<TransceiverFactory::DataBits> ();
  rig_params_.stop_bits = settings_->value ("CATStopBits", QVariant::fromValue (TransceiverFactory::default_stop_bits)).value<TransceiverFactory::StopBits> ();
  rig_params_.handshake = settings_->value ("CATHandshake", QVariant::fromValue (TransceiverFactory::handshake_default)).value<TransceiverFactory::Handshake> ();
  rig_params_.force_dtr = settings_->value ("CATForceDTR", false).toBool ();
  rig_params_.dtr_high = settings_->value ("DTR", false).toBool ();
  rig_params_.force_rts = settings_->value ("CATForceRTS", false).toBool ();
  rig_params_.rts_high = settings_->value ("RTS", false).toBool ();
  rig_params_.ptt_type = settings_->value ("PTTMethod", QVariant::fromValue (TransceiverFactory::PTT_method_VOX)).value<TransceiverFactory::PTTMethod> ();
  rig_params_.audio_source = settings_->value ("TXAudioSource", QVariant::fromValue (TransceiverFactory::TX_audio_source_front)).value<TransceiverFactory::TXAudioSource> ();
  rig_params_.ptt_port = settings_->value ("PTTport").toString ();
  data_mode_ = settings_->value ("DataMode", QVariant::fromValue (data_mode_none)).value<Configuration::DataMode> ();
  bLowSidelobes_ = settings_->value("LowSidelobes",true).toBool();
  prompt_to_log_ = settings_->value ("PromptToLog", false).toBool ();
  autoLog_ = settings_->value ("AutoLog", false).toBool ();
  decodes_from_top_ = settings_->value ("DecodesFromTop", false).toBool ();
  insert_blank_ = settings_->value ("InsertBlank", false).toBool ();
  DXCC_ = settings_->value ("DXCCEntity", false).toBool ();
  ppfx_ = settings_->value ("PrincipalPrefix", false).toBool ();
  clear_DX_ = settings_->value ("ClearCallGrid", false).toBool ();
  miles_ = settings_->value ("Miles", false).toBool ();
  quick_call_ = settings_->value ("QuickCall", false).toBool ();
  disable_TX_on_73_ = settings_->value ("73TxDisable", false).toBool ();
  force_call_1st_ = settings_->value ("ForceCallFirst", false).toBool ();
  alternate_bindings_ = settings_->value ("AlternateBindings", false).toBool ();
  watchdog_ = settings_->value ("TxWatchdog", 6).toInt ();
  TX_messages_ = settings_->value ("Tx2QSO", true).toBool ();
  enable_VHF_features_ = settings_->value("VHFUHF",false).toBool ();
  decode_at_52s_ = settings_->value("Decode52",false).toBool ();
  single_decode_ = settings_->value("SingleDecode",false).toBool ();
  twoPass_ = settings_->value("TwoPass",true).toBool ();
  Individual_Contest_Name_ = settings_->value("Individual_Contest_Name",true).toBool ();
  bSpecialOp_ = settings_->value("SpecialOpActivity",false).toBool ();
  SelectedActivity_ = settings_->value("SelectedActivity",1).toInt (); 
  x2ToneSpacing_ = settings_->value("x2ToneSpacing",false).toBool ();
  x4ToneSpacing_ = settings_->value("x4ToneSpacing",false).toBool ();
  rig_params_.poll_interval = settings_->value ("Polling", 0).toInt ();
  rig_params_.split_mode = settings_->value ("SplitMode", QVariant::fromValue (TransceiverFactory::split_mode_none)).value<TransceiverFactory::SplitMode> ();
  opCall_ = settings_->value ("OpCall", "").toString ();
  udp_server_name_ = settings_->value ("UDPServer", "127.0.0.1").toString ();
  udp_interface_names_ = settings_->value ("UDPInterface").toStringList ();
  udp_TTL_ = settings_->value ("UDPTTL", 1).toInt ();
  udp_server_port_ = settings_->value ("UDPServerPort", 2237).toUInt ();
  n1mm_server_name_ = settings_->value ("N1MMServer", "127.0.0.1").toString ();
  n1mm_server_port_ = settings_->value ("N1MMServerPort", 2333).toUInt ();
  broadcast_to_n1mm_ = settings_->value ("BroadcastToN1MM", false).toBool ();
  accept_udp_requests_ = settings_->value ("AcceptUDPRequests", false).toBool ();
  udpWindowToFront_ = settings_->value ("udpWindowToFront",false).toBool ();
  udpWindowRestore_ = settings_->value ("udpWindowRestore",false).toBool ();
  calibration_.intercept = settings_->value ("CalibrationIntercept", 0.).toDouble ();
  calibration_.slope_ppm = settings_->value ("CalibrationSlopePPM", 0.).toDouble ();
  pwrBandTxMemory_ = settings_->value("pwrBandTxMemory",false).toBool ();
  pwrBandTuneMemory_ = settings_->value("pwrBandTuneMemory",false).toBool ();
  highlight_DXcall_ = settings_->value("highlight_DXcall",false).toBool ();
  highlight_DXgrid_ = settings_->value("highlight_DXgrid",false).toBool ();
}

void Configuration::impl::find_audio_devices ()
{
  //
  // retrieve audio input device
  //
  auto saved_name = settings_->value ("SoundInName").toString ();
  if (next_audio_input_device_.deviceName () != saved_name || next_audio_input_device_.isNull ())
    {
      next_audio_input_device_ = find_audio_device (QAudio::AudioInput, ui_->sound_input_combo_box, saved_name);
      next_audio_input_channel_ = AudioDevice::fromString (settings_->value ("AudioInputChannel", "Mono").toString ());
      update_audio_channels (ui_->sound_input_combo_box, ui_->sound_input_combo_box->currentIndex (), ui_->sound_input_channel_combo_box, false);
      ui_->sound_input_channel_combo_box->setCurrentIndex (next_audio_input_channel_);
    }

  //
  // retrieve audio output device
  //
  saved_name = settings_->value("SoundOutName").toString();
  if (next_audio_output_device_.deviceName () != saved_name || next_audio_output_device_.isNull ())
    {
      next_audio_output_device_ = find_audio_device (QAudio::AudioOutput, ui_->sound_output_combo_box, saved_name);
      next_audio_output_channel_ = AudioDevice::fromString (settings_->value ("AudioOutputChannel", "Mono").toString ());
      update_audio_channels (ui_->sound_output_combo_box, ui_->sound_output_combo_box->currentIndex (), ui_->sound_output_channel_combo_box, true);
      ui_->sound_output_channel_combo_box->setCurrentIndex (next_audio_output_channel_);
    }
}

void Configuration::impl::write_settings ()
{
  SettingsGroup g {settings_, "Configuration"};

  settings_->setValue ("MyCall", my_callsign_);
  settings_->setValue ("MyGrid", my_grid_);
  settings_->setValue ("Field_Day_Exchange", FD_exchange_);
  settings_->setValue ("RTTY_Exchange", RTTY_exchange_);
  settings_->setValue ("Contest_Name", Contest_Name_);
  settings_->setValue ("Font", font_.toString ());
  settings_->setValue ("DecodedTextFont", decoded_text_font_.toString ());
  settings_->setValue ("IDint", id_interval_);
  settings_->setValue ("nTrials", ntrials_);
  settings_->setValue ("TxDelay", txDelay_);
  settings_->setValue ("Aggressive", aggressive_);
  settings_->setValue ("RxBandwidth", RxBandwidth_);
  settings_->setValue ("PTTMethod", QVariant::fromValue (rig_params_.ptt_type));
  settings_->setValue ("PTTport", rig_params_.ptt_port);
  settings_->setValue ("SaveDir", save_directory_.absolutePath ());
  settings_->setValue ("AzElDir", azel_directory_.absolutePath ());
  if (!audio_input_device_.isNull ())
    {
      settings_->setValue ("SoundInName", audio_input_device_.deviceName ());
      settings_->setValue ("AudioInputChannel", AudioDevice::toString (audio_input_channel_));
    }
  if (!audio_output_device_.isNull ())
    {
      settings_->setValue ("SoundOutName", audio_output_device_.deviceName ());
      settings_->setValue ("AudioOutputChannel", AudioDevice::toString (audio_output_channel_));
    }
  settings_->setValue ("Type2MsgGen", QVariant::fromValue (type_2_msg_gen_));
  settings_->setValue ("MonitorOFF", monitor_off_at_startup_);
  settings_->setValue ("MonitorLastUsed", monitor_last_used_);
  settings_->setValue ("PSKReporter", spot_to_psk_reporter_);
  settings_->setValue ("PSKReporterTCPIP", psk_reporter_tcpip_);
  settings_->setValue ("After73", id_after_73_);
  settings_->setValue ("TxQSYAllowed", tx_QSY_allowed_);
  settings_->setValue ("Macros", macros_.stringList ());
  settings_->setValue ("stations", QVariant::fromValue (stations_.station_list ()));
  settings_->setValue ("FrequenciesForRegionModes_v2", QVariant::fromValue (frequencies_.frequency_list ()));
  settings_->setValue ("DecodeHighlighting", QVariant::fromValue (decode_highlighing_model_.items ()));
  settings_->setValue ("HighlightByMode", highlight_by_mode_);
  settings_->setValue ("OnlyFieldsSought", highlight_only_fields_);
  settings_->setValue ("IncludeWAEEntities", include_WAE_entities_);
  settings_->setValue ("Highlight73", highlight_73_);
  settings_->setValue ("LotWDaysSinceLastUpload", LotW_days_since_upload_);
  settings_->setValue ("toRTTY", log_as_RTTY_);
  settings_->setValue ("dBtoComments", report_in_comments_);
  settings_->setValue ("Rig", rig_params_.rig_name);
  settings_->setValue ("CATNetworkPort", rig_params_.network_port);
  settings_->setValue ("CATUSBPort", rig_params_.usb_port);
  settings_->setValue ("CATSerialPort", rig_params_.serial_port);
  settings_->setValue ("CATSerialRate", rig_params_.baud);
  settings_->setValue ("CATDataBits", QVariant::fromValue (rig_params_.data_bits));
  settings_->setValue ("CATStopBits", QVariant::fromValue (rig_params_.stop_bits));
  settings_->setValue ("CATHandshake", QVariant::fromValue (rig_params_.handshake));
  settings_->setValue ("DataMode", QVariant::fromValue (data_mode_));
  settings_->setValue ("LowSidelobes",bLowSidelobes_);
  settings_->setValue ("PromptToLog", prompt_to_log_);
  settings_->setValue ("AutoLog", autoLog_);
  settings_->setValue ("DecodesFromTop", decodes_from_top_);
  settings_->setValue ("InsertBlank", insert_blank_);
  settings_->setValue ("DXCCEntity", DXCC_);
  settings_->setValue ("PrincipalPrefix", ppfx_);
  settings_->setValue ("ClearCallGrid", clear_DX_);
  settings_->setValue ("Miles", miles_);
  settings_->setValue ("QuickCall", quick_call_);
  settings_->setValue ("73TxDisable", disable_TX_on_73_);
  settings_->setValue ("ForceCallFirst", force_call_1st_);
  settings_->setValue ("AlternateBindings", alternate_bindings_);
  settings_->setValue ("TxWatchdog", watchdog_);
  settings_->setValue ("Tx2QSO", TX_messages_);
  settings_->setValue ("CATForceDTR", rig_params_.force_dtr);
  settings_->setValue ("DTR", rig_params_.dtr_high);
  settings_->setValue ("CATForceRTS", rig_params_.force_rts);
  settings_->setValue ("RTS", rig_params_.rts_high);
  settings_->setValue ("TXAudioSource", QVariant::fromValue (rig_params_.audio_source));
  settings_->setValue ("Polling", rig_params_.poll_interval);
  settings_->setValue ("SplitMode", QVariant::fromValue (rig_params_.split_mode));
  settings_->setValue ("VHFUHF", enable_VHF_features_);
  settings_->setValue ("Decode52", decode_at_52s_);
  settings_->setValue ("SingleDecode", single_decode_);
  settings_->setValue ("TwoPass", twoPass_);
  settings_->setValue ("Individual_Contest_Name", Individual_Contest_Name_);
  settings_->setValue ("SelectedActivity", SelectedActivity_);
  settings_->setValue ("SpecialOpActivity", bSpecialOp_);
  settings_->setValue ("x2ToneSpacing", x2ToneSpacing_);
  settings_->setValue ("x4ToneSpacing", x4ToneSpacing_);
  settings_->setValue ("OpCall", opCall_);
  settings_->setValue ("UDPServer", udp_server_name_);
  settings_->setValue ("UDPServerPort", udp_server_port_);
  settings_->setValue ("UDPInterface", QVariant::fromValue (udp_interface_names_));
  settings_->setValue ("UDPTTL", udp_TTL_);
  settings_->setValue ("N1MMServer", n1mm_server_name_);
  settings_->setValue ("N1MMServerPort", n1mm_server_port_);
  settings_->setValue ("BroadcastToN1MM", broadcast_to_n1mm_);
  settings_->setValue ("AcceptUDPRequests", accept_udp_requests_);
  settings_->setValue ("udpWindowToFront", udpWindowToFront_);
  settings_->setValue ("udpWindowRestore", udpWindowRestore_);
  settings_->setValue ("CalibrationIntercept", calibration_.intercept);
  settings_->setValue ("CalibrationSlopePPM", calibration_.slope_ppm);
  settings_->setValue ("pwrBandTxMemory", pwrBandTxMemory_);
  settings_->setValue ("pwrBandTuneMemory", pwrBandTuneMemory_);
  settings_->setValue ("Region", QVariant::fromValue (region_));
  settings_->setValue ("AutoGrid", use_dynamic_grid_);
  settings_->setValue ("highlight_DXcall", highlight_DXcall_);
  settings_->setValue ("highlight_DXgrid", highlight_DXgrid_);
  settings_->sync ();
}

void Configuration::impl::set_rig_invariants ()
{
  auto const& rig = ui_->rig_combo_box->currentText ();
  auto const& ptt_port = ui_->PTT_port_combo_box->currentText ();
  auto ptt_method = static_cast<TransceiverFactory::PTTMethod> (ui_->PTT_method_button_group->checkedId ());

  auto CAT_PTT_enabled = transceiver_factory_.has_CAT_PTT (rig);
  auto CAT_indirect_serial_PTT = transceiver_factory_.has_CAT_indirect_serial_PTT (rig);
  auto asynchronous_CAT = transceiver_factory_.has_asynchronous_CAT (rig);
  auto is_hw_handshake = ui_->CAT_handshake_group_box->isEnabled ()
    && TransceiverFactory::handshake_hardware == static_cast<TransceiverFactory::Handshake> (ui_->CAT_handshake_button_group->checkedId ());

  ui_->test_CAT_push_button->setStyleSheet ({});

  ui_->CAT_poll_interval_label->setEnabled (!asynchronous_CAT);
  ui_->CAT_poll_interval_spin_box->setEnabled (!asynchronous_CAT);

  auto port_type = transceiver_factory_.CAT_port_type (rig);

  bool is_serial_CAT (TransceiverFactory::Capabilities::serial == port_type);
  auto const& cat_port = ui_->CAT_port_combo_box->currentText ();

  // only enable CAT option if transceiver has CAT PTT
  ui_->PTT_CAT_radio_button->setEnabled (CAT_PTT_enabled);

  auto enable_ptt_port = TransceiverFactory::PTT_method_CAT != ptt_method && TransceiverFactory::PTT_method_VOX != ptt_method;
  ui_->PTT_port_combo_box->setEnabled (enable_ptt_port);
  ui_->PTT_port_label->setEnabled (enable_ptt_port);

  if (CAT_indirect_serial_PTT)
    {
      ui_->PTT_port_combo_box->setItemData (ui_->PTT_port_combo_box->findText ("CAT")
                                            , combo_box_item_enabled, Qt::UserRole - 1);
    }
  else
    {
      ui_->PTT_port_combo_box->setItemData (ui_->PTT_port_combo_box->findText ("CAT")
                                            , combo_box_item_disabled, Qt::UserRole - 1);
      if ("CAT" == ui_->PTT_port_combo_box->currentText () && ui_->PTT_port_combo_box->currentIndex () > 0)
        {
          ui_->PTT_port_combo_box->setCurrentIndex (ui_->PTT_port_combo_box->currentIndex () - 1);
        }
    }
  ui_->PTT_RTS_radio_button->setEnabled (!(is_serial_CAT && ptt_port == cat_port && is_hw_handshake));

  if (TransceiverFactory::basic_transceiver_name_ == rig)
    {
      // makes no sense with rig as "None"
      ui_->monitor_last_used_check_box->setEnabled (false);

      ui_->CAT_control_group_box->setEnabled (false);
      ui_->test_CAT_push_button->setEnabled (false);
      ui_->test_PTT_push_button->setEnabled (TransceiverFactory::PTT_method_DTR == ptt_method
                                             || TransceiverFactory::PTT_method_RTS == ptt_method);
      ui_->TX_audio_source_group_box->setEnabled (false);
    }
  else
    {
      ui_->monitor_last_used_check_box->setEnabled (true);
      ui_->CAT_control_group_box->setEnabled (true);
      ui_->test_CAT_push_button->setEnabled (true);
      ui_->test_PTT_push_button->setEnabled (false);
      ui_->TX_audio_source_group_box->setEnabled (transceiver_factory_.has_CAT_PTT_mic_data (rig) && TransceiverFactory::PTT_method_CAT == ptt_method);
      if (port_type != last_port_type_)
        {
          last_port_type_ = port_type;
          switch (port_type)
            {
            case TransceiverFactory::Capabilities::serial:
              fill_port_combo_box (ui_->CAT_port_combo_box);
              ui_->CAT_port_combo_box->setCurrentText (rig_params_.serial_port);
              if (ui_->CAT_port_combo_box->currentText ().isEmpty () && ui_->CAT_port_combo_box->count ())
                {
                  ui_->CAT_port_combo_box->setCurrentText (ui_->CAT_port_combo_box->itemText (0));
                }
              ui_->CAT_port_label->setText (tr ("Serial Port:"));
              ui_->CAT_port_combo_box->setToolTip (tr ("Serial port used for CAT control"));
              ui_->CAT_port_combo_box->setEnabled (true);
              break;

            case TransceiverFactory::Capabilities::network:
              ui_->CAT_port_combo_box->clear ();
              ui_->CAT_port_combo_box->setCurrentText (rig_params_.network_port);
              ui_->CAT_port_label->setText (tr ("Network Server:"));
              ui_->CAT_port_combo_box->setToolTip (tr ("Optional hostname and port of network service.\n"
                                                       "Leave blank for a sensible default on this machine.\n"
                                                       "Formats:\n"
                                                       "\thostname:port\n"
                                                       "\tIPv4-address:port\n"
                                                       "\t[IPv6-address]:port"));
              ui_->CAT_port_combo_box->setEnabled (true);
              break;

            case TransceiverFactory::Capabilities::usb:
              ui_->CAT_port_combo_box->clear ();
              ui_->CAT_port_combo_box->setCurrentText (rig_params_.usb_port);
              ui_->CAT_port_label->setText (tr ("USB Device:"));
              ui_->CAT_port_combo_box->setToolTip (tr ("Optional device identification.\n"
                                                       "Leave blank for a sensible default for the rig.\n"
                                                       "Format:\n"
                                                       "\t[VID[:PID[:VENDOR[:PRODUCT]]]]"));
              ui_->CAT_port_combo_box->setEnabled (true);
              break;

            default:
              ui_->CAT_port_combo_box->clear ();
              ui_->CAT_port_combo_box->setEnabled (false);
              break;
            }
        }
      ui_->CAT_serial_port_parameters_group_box->setEnabled (is_serial_CAT);
      ui_->force_DTR_combo_box->setEnabled (is_serial_CAT
                                            && (cat_port != ptt_port
                                                || !ui_->PTT_DTR_radio_button->isEnabled ()
                                                || !ui_->PTT_DTR_radio_button->isChecked ()));
      ui_->force_RTS_combo_box->setEnabled (is_serial_CAT
                                            && !is_hw_handshake
                                            && (cat_port != ptt_port
                                                || !ui_->PTT_RTS_radio_button->isEnabled ()
                                                || !ui_->PTT_RTS_radio_button->isChecked ()));
    }
  ui_->mode_group_box->setEnabled (WSJT_RIG_NONE_CAN_SPLIT
                                   || TransceiverFactory::basic_transceiver_name_ != rig);
  ui_->split_operation_group_box->setEnabled (WSJT_RIG_NONE_CAN_SPLIT
                                              || TransceiverFactory::basic_transceiver_name_ != rig);
}

bool Configuration::impl::validate ()
{
  if (ui_->sound_input_combo_box->currentIndex () < 0
      && next_audio_input_device_.isNull ())
    {
      find_tab (ui_->sound_input_combo_box);
      MessageBox::critical_message (this, tr ("Invalid audio input device"));
      return false;
    }

  if (ui_->sound_input_channel_combo_box->currentIndex () < 0
      && next_audio_input_device_.isNull ())
    {
      find_tab (ui_->sound_input_combo_box);
      MessageBox::critical_message (this, tr ("Invalid audio input device"));
      return false;
    }

  if (ui_->sound_output_combo_box->currentIndex () < 0
      && next_audio_output_device_.isNull ())
    {
      find_tab (ui_->sound_output_combo_box);
      MessageBox::information_message (this, tr ("Invalid audio output device"));
      // don't reject as we can work without an audio output
    }

  if (!ui_->PTT_method_button_group->checkedButton ()->isEnabled ())
    {
      MessageBox::critical_message (this, tr ("Invalid PTT method"));
      return false;
    }

  auto ptt_method = static_cast<TransceiverFactory::PTTMethod> (ui_->PTT_method_button_group->checkedId ());
  auto ptt_port = ui_->PTT_port_combo_box->currentText ();
  if ((TransceiverFactory::PTT_method_DTR == ptt_method || TransceiverFactory::PTT_method_RTS == ptt_method)
      && (ptt_port.isEmpty ()
          || combo_box_item_disabled == ui_->PTT_port_combo_box->itemData (ui_->PTT_port_combo_box->findText (ptt_port), Qt::UserRole - 1)))
    {
      MessageBox::critical_message (this, tr ("Invalid PTT port"));
      return false;
    }

  if (ui_->rbField_Day->isEnabled () && ui_->rbField_Day->isChecked () &&
      !ui_->Field_Day_Exchange->hasAcceptableInput ())
    {
      find_tab (ui_->Field_Day_Exchange);
      MessageBox::critical_message (this, tr ("Invalid Contest Exchange")
                                    , tr ("You must input a valid ARRL Field Day exchange"));
      return false;
    }

  if (ui_->rbRTTY_Roundup->isEnabled () && ui_->rbRTTY_Roundup->isChecked () &&
      !ui_->RTTY_Exchange->hasAcceptableInput ())
    {
      find_tab (ui_->RTTY_Exchange);
      MessageBox::critical_message (this, tr ("Invalid Contest Exchange")
                                    , tr ("You must input a valid ARRL RTTY Roundup exchange"));
      return false;
    }

  if (dns_lookup_id_ > -1)
    {
      MessageBox::information_message (this, tr ("Pending DNS lookup, please try again later"));
      return false;
    }

  return true;
}

int Configuration::impl::exec ()
{
  // macros can be modified in the main window
  next_macros_.setStringList (macros_.stringList ());

  have_rig_ = rig_active_;	// record that we started with a rig open
  saved_rig_params_ = rig_params_; // used to detect changes that
                                   // require the Transceiver to be
                                   // re-opened
  rig_changed_ = false;

  initialize_models ();

  return QDialog::exec();
}

TransceiverFactory::ParameterPack Configuration::impl::gather_rig_data ()
{
  TransceiverFactory::ParameterPack result;
  result.rig_name = ui_->rig_combo_box->currentText ();

  switch (transceiver_factory_.CAT_port_type (result.rig_name))
    {
    case TransceiverFactory::Capabilities::network:
      result.network_port = ui_->CAT_port_combo_box->currentText ();
      result.usb_port = rig_params_.usb_port;
      result.serial_port = rig_params_.serial_port;
      break;

    case TransceiverFactory::Capabilities::usb:
      result.usb_port = ui_->CAT_port_combo_box->currentText ();
      result.network_port = rig_params_.network_port;
      result.serial_port = rig_params_.serial_port;
      break;

    default:
      result.serial_port = ui_->CAT_port_combo_box->currentText ();
      result.network_port = rig_params_.network_port;
      result.usb_port = rig_params_.usb_port;
      break;
    }

  result.baud = ui_->CAT_serial_baud_combo_box->currentText ().toInt ();
  result.data_bits = static_cast<TransceiverFactory::DataBits> (ui_->CAT_data_bits_button_group->checkedId ());
  result.stop_bits = static_cast<TransceiverFactory::StopBits> (ui_->CAT_stop_bits_button_group->checkedId ());
  result.handshake = static_cast<TransceiverFactory::Handshake> (ui_->CAT_handshake_button_group->checkedId ());
  result.force_dtr = ui_->force_DTR_combo_box->isEnabled () && ui_->force_DTR_combo_box->currentIndex () > 0;
  result.dtr_high = ui_->force_DTR_combo_box->isEnabled () && 1 == ui_->force_DTR_combo_box->currentIndex ();
  result.force_rts = ui_->force_RTS_combo_box->isEnabled () && ui_->force_RTS_combo_box->currentIndex () > 0;
  result.rts_high = ui_->force_RTS_combo_box->isEnabled () && 1 == ui_->force_RTS_combo_box->currentIndex ();
  result.poll_interval = ui_->CAT_poll_interval_spin_box->value ();
  result.ptt_type = static_cast<TransceiverFactory::PTTMethod> (ui_->PTT_method_button_group->checkedId ());
  result.ptt_port = ui_->PTT_port_combo_box->currentText ();
  result.audio_source = static_cast<TransceiverFactory::TXAudioSource> (ui_->TX_audio_source_button_group->checkedId ());
  result.split_mode = static_cast<TransceiverFactory::SplitMode> (ui_->split_mode_button_group->checkedId ());
  return result;
}

void Configuration::impl::accept ()
{
  // Called when OK button is clicked.
  if (!validate ())
    {
      return;			// not accepting
    }

  // extract all rig related configuration parameters into temporary
  // structure for checking if the rig needs re-opening without
  // actually updating our live state
  auto temp_rig_params = gather_rig_data ();

  // open_rig() uses values from models so we use it to validate the
  // Transceiver settings before agreeing to accept the configuration
  if (temp_rig_params != rig_params_ && !open_rig ())
    {
      return;			// not accepting
    }

  QDialog::accept();            // do this before accessing custom
                                // models so that any changes in
                                // delegates in views get flushed to
                                // the underlying models before we
                                // access them

  sync_transceiver (true);	// force an update

  //
  // from here on we are bound to accept the new configuration
  // parameters so extract values from models and make them live
  //

  if (next_font_ != font_)
    {
      font_ = next_font_;
      Q_EMIT self_->text_font_changed (font_);
    }

  if (next_decoded_text_font_ != decoded_text_font_)
    {
      decoded_text_font_ = next_decoded_text_font_;
      next_decode_highlighing_model_.set_font (decoded_text_font_);
      ui_->highlighting_list_view->reset ();
      Q_EMIT self_->decoded_text_font_changed (decoded_text_font_);
    }

  rig_params_ = temp_rig_params; // now we can go live with the rig
                                 // related configuration parameters
  rig_is_dummy_ = TransceiverFactory::basic_transceiver_name_ == rig_params_.rig_name;

  {
    auto const& selected_device = ui_->sound_input_combo_box->currentData ().value<audio_info_type> ().first;
    if (selected_device != next_audio_input_device_)
      {
        next_audio_input_device_ = selected_device;
      }
  }

  {
    auto const& selected_device = ui_->sound_output_combo_box->currentData ().value<audio_info_type> ().first;
    if (selected_device != next_audio_output_device_)
      {
        next_audio_output_device_ = selected_device;
      }
  }

  if (next_audio_input_channel_ != static_cast<AudioDevice::Channel> (ui_->sound_input_channel_combo_box->currentIndex ()))
    {
      next_audio_input_channel_ = static_cast<AudioDevice::Channel> (ui_->sound_input_channel_combo_box->currentIndex ());
    }
  Q_ASSERT (next_audio_input_channel_ <= AudioDevice::Right);

  if (next_audio_output_channel_ != static_cast<AudioDevice::Channel> (ui_->sound_output_channel_combo_box->currentIndex ()))
    {
      next_audio_output_channel_ = static_cast<AudioDevice::Channel> (ui_->sound_output_channel_combo_box->currentIndex ());
    }
  Q_ASSERT (next_audio_output_channel_ <= AudioDevice::Both);

  if (audio_input_device_ != next_audio_input_device_ || next_audio_input_device_.isNull ())
    {
      audio_input_device_ = next_audio_input_device_;
      restart_sound_input_device_ = true;
    }
  if (audio_input_channel_ != next_audio_input_channel_)
    {
      audio_input_channel_ = next_audio_input_channel_;
      restart_sound_input_device_ = true;
    }
  if (audio_output_device_ != next_audio_output_device_ || next_audio_output_device_.isNull ())
    {
      audio_output_device_ = next_audio_output_device_;
      restart_sound_output_device_ = true;
    }
  if (audio_output_channel_ != next_audio_output_channel_)
    {
      audio_output_channel_ = next_audio_output_channel_;
      restart_sound_output_device_ = true;
    }
  // qDebug () << "Configure::accept: audio i/p:" << audio_input_device_.deviceName ()
  //           << "chan:" << audio_input_channel_
  //           << "o/p:" << audio_output_device_.deviceName ()
  //           << "chan:" << audio_output_channel_
  //           << "reset i/p:" << restart_sound_input_device_
  //           << "reset o/p:" << restart_sound_output_device_;

  my_callsign_ = ui_->callsign_line_edit->text ();
  my_grid_ = ui_->grid_line_edit->text ();
  FD_exchange_= ui_->Field_Day_Exchange->text ().toUpper ();
  RTTY_exchange_= ui_->RTTY_Exchange->text ().toUpper ();
  Contest_Name_= ui_->Contest_Name->text ().toUpper ();
  spot_to_psk_reporter_ = ui_->psk_reporter_check_box->isChecked ();
  psk_reporter_tcpip_ = ui_->psk_reporter_tcpip_check_box->isChecked ();
  id_interval_ = ui_->CW_id_interval_spin_box->value ();
  ntrials_ = ui_->sbNtrials->value ();
  txDelay_ = ui_->sbTxDelay->value ();
  aggressive_ = ui_->sbAggressive->value ();
  degrade_ = ui_->sbDegrade->value ();
  RxBandwidth_ = ui_->sbBandwidth->value ();
  id_after_73_ = ui_->CW_id_after_73_check_box->isChecked ();
  tx_QSY_allowed_ = ui_->tx_QSY_check_box->isChecked ();
  monitor_off_at_startup_ = ui_->monitor_off_check_box->isChecked ();
  monitor_last_used_ = ui_->monitor_last_used_check_box->isChecked ();
  type_2_msg_gen_ = static_cast<Type2MsgGen> (ui_->type_2_msg_gen_combo_box->currentIndex ());
  log_as_RTTY_ = ui_->log_as_RTTY_check_box->isChecked ();
  report_in_comments_ = ui_->report_in_comments_check_box->isChecked ();
  prompt_to_log_ = ui_->prompt_to_log_check_box->isChecked ();
  autoLog_ = ui_->cbAutoLog->isChecked();
  decodes_from_top_ = ui_->decodes_from_top_check_box->isChecked ();
  insert_blank_ = ui_->insert_blank_check_box->isChecked ();
  DXCC_ = ui_->DXCC_check_box->isChecked ();
  ppfx_ = ui_->ppfx_check_box->isChecked ();
  clear_DX_ = ui_->clear_DX_check_box->isChecked ();
  miles_ = ui_->miles_check_box->isChecked ();
  quick_call_ = ui_->quick_call_check_box->isChecked ();
  disable_TX_on_73_ = ui_->disable_TX_on_73_check_box->isChecked ();
  force_call_1st_ = ui_->force_call_1st_check_box->isChecked ();
  alternate_bindings_ = ui_->alternate_bindings_check_box->isChecked ();
  watchdog_ = ui_->tx_watchdog_spin_box->value ();
  TX_messages_ = ui_->TX_messages_check_box->isChecked ();
  data_mode_ = static_cast<DataMode> (ui_->TX_mode_button_group->checkedId ());
  bLowSidelobes_ = ui_->rbLowSidelobes->isChecked();
  save_directory_.setPath (ui_->save_path_display_label->text ());
  azel_directory_.setPath (ui_->azel_path_display_label->text ());
  enable_VHF_features_ = ui_->enable_VHF_features_check_box->isChecked ();
  decode_at_52s_ = ui_->decode_at_52s_check_box->isChecked ();
  single_decode_ = ui_->single_decode_check_box->isChecked ();
  twoPass_ = ui_->cbTwoPass->isChecked ();
  Individual_Contest_Name_ = ui_->cbContestName->isChecked ();
  bSpecialOp_ = ui_->gbSpecialOpActivity->isChecked ();
  SelectedActivity_ = ui_->special_op_activity_button_group->checkedId();
  x2ToneSpacing_ = ui_->cbx2ToneSpacing->isChecked ();
  x4ToneSpacing_ = ui_->cbx4ToneSpacing->isChecked ();
  calibration_.intercept = ui_->calibration_intercept_spin_box->value ();
  calibration_.slope_ppm = ui_->calibration_slope_ppm_spin_box->value ();
  pwrBandTxMemory_ = ui_->checkBoxPwrBandTxMemory->isChecked ();
  pwrBandTuneMemory_ = ui_->checkBoxPwrBandTuneMemory->isChecked ();
  opCall_=ui_->opCallEntry->text();

  auto new_server = ui_->udp_server_line_edit->text ().trimmed ();
  auto new_interfaces = get_selected_network_interfaces (ui_->udp_interfaces_combo_box);
  if (new_server != udp_server_name_ || new_interfaces != udp_interface_names_)
    {
      udp_server_name_ = new_server;
      udp_interface_names_ = new_interfaces;
      Q_EMIT self_->udp_server_changed (udp_server_name_, udp_interface_names_);
    }

  auto new_port = ui_->udp_server_port_spin_box->value ();
  if (new_port != udp_server_port_)
    {
      udp_server_port_ = new_port;
      Q_EMIT self_->udp_server_port_changed (udp_server_port_);
    }

  auto new_TTL = ui_->udp_TTL_spin_box->value ();
  if (new_TTL != udp_TTL_)
    {
      udp_TTL_ = new_TTL;
      Q_EMIT self_->udp_TTL_changed (udp_TTL_);
    }

  if (ui_->accept_udp_requests_check_box->isChecked () != accept_udp_requests_)
    {
      accept_udp_requests_ = ui_->accept_udp_requests_check_box->isChecked ();
      Q_EMIT self_->accept_udp_requests_changed (accept_udp_requests_);
    }

  n1mm_server_name_ = ui_->n1mm_server_name_line_edit->text ();
  n1mm_server_port_ = ui_->n1mm_server_port_spin_box->value ();
  broadcast_to_n1mm_ = ui_->enable_n1mm_broadcast_check_box->isChecked ();

  udpWindowToFront_ = ui_->udpWindowToFront->isChecked ();
  udpWindowRestore_ = ui_->udpWindowRestore->isChecked ();

  if (macros_.stringList () != next_macros_.stringList ())
    {
      macros_.setStringList (next_macros_.stringList ());
    }

  region_ = IARURegions::value (ui_->region_combo_box->currentText ());

  if (frequencies_.frequency_list () != next_frequencies_.frequency_list ())
    {
      frequencies_.frequency_list (next_frequencies_.frequency_list ());
      frequencies_.sort (FrequencyList_v2_101::frequency_column);
    }

  if (stations_.station_list () != next_stations_.station_list ())
    {
      stations_.station_list (next_stations_.station_list ());
      stations_.sort (StationList::band_column);
    }

  if (decode_highlighing_model_.items () != next_decode_highlighing_model_.items ())
    {
      decode_highlighing_model_.items (next_decode_highlighing_model_.items ());
      Q_EMIT self_->decode_highlighting_changed (decode_highlighing_model_);
    }
  highlight_by_mode_ = ui_->highlight_by_mode_check_box->isChecked ();
  highlight_only_fields_ = ui_->only_fields_check_box->isChecked ();
  include_WAE_entities_ = ui_->include_WAE_check_box->isChecked ();
  highlight_73_ = ui_->highlight_73_check_box->isChecked ();
  LotW_days_since_upload_ = ui_->LotW_days_since_upload_spin_box->value ();
  lotw_users_.set_age_constraint (LotW_days_since_upload_);

  if (ui_->use_dynamic_grid->isChecked() && !use_dynamic_grid_ )
  {
    // turning on so clear it so only the next location update gets used
    dynamic_grid_.clear ();
  }
  use_dynamic_grid_ = ui_->use_dynamic_grid->isChecked();
  highlight_DXcall_ = ui_->cbHighlightDXcall->isChecked();
  highlight_DXgrid_ = ui_->cbHighlightDXgrid->isChecked();
  Individual_Contest_Name_ = ui_->cbContestName->isChecked();

  write_settings ();		// make visible to all
}

void Configuration::impl::reject ()
{
  if (dns_lookup_id_ > -1)
    {
      QHostInfo::abortHostLookup (dns_lookup_id_);
      dns_lookup_id_ = -1;
    }

  initialize_models ();		// reverts to settings as at exec ()

  // check if the Transceiver instance changed, in which case we need
  // to re open any prior Transceiver type
  if (rig_changed_)
    {
      if (have_rig_)
        {
          // we have to do this since the rig has been opened since we
          // were exec'ed even though it might fail
          open_rig ();
        }
      else
        {
          close_rig ();
        }
    }

  // qDebug () << "Configure::reject: audio i/p:" << audio_input_device_.deviceName ()
  //           << "chan:" << audio_input_channel_
  //           << "o/p:" << audio_output_device_.deviceName ()
  //           << "chan:" << audio_output_channel_
  //           << "reset i/p:" << restart_sound_input_device_
  //           << "reset o/p:" << restart_sound_output_device_;

  QDialog::reject ();
}

void Configuration::impl::on_font_push_button_clicked ()
{
  next_font_ = QFontDialog::getFont (0, next_font_, this);
}

void Configuration::impl::on_reset_highlighting_to_defaults_push_button_clicked (bool /*checked*/)
{
  if (MessageBox::Yes == MessageBox::query_message (this
                                                    , tr ("Reset Decode Highlighting")
                                                    , tr ("Reset all decode highlighting and priorities to default values")))
    {
      next_decode_highlighing_model_.items (DecodeHighlightingModel::default_items ());
    }
}

void Configuration::impl::on_rescan_log_push_button_clicked (bool /*clicked*/)
{
  if (logbook_) logbook_->rescan ();
}

void Configuration::impl::on_LotW_CSV_fetch_push_button_clicked (bool /*checked*/)
{
  lotw_users_.load (ui_->LotW_CSV_URL_line_edit->text (), true, true);
  ui_->LotW_CSV_fetch_push_button->setEnabled (false);
}

void Configuration::impl::on_decoded_text_font_push_button_clicked ()
{
  next_decoded_text_font_ = QFontDialog::getFont (0, decoded_text_font_ , this
                                                  , tr ("WSJT-X Decoded Text Font Chooser")
                                                  , QFontDialog::MonospacedFonts
                                                  );
}

void Configuration::impl::on_PTT_port_combo_box_activated (int /* index */)
{
  set_rig_invariants ();
}

void Configuration::impl::on_CAT_port_combo_box_activated (int /* index */)
{
  set_rig_invariants ();
}

void Configuration::impl::on_CAT_serial_baud_combo_box_currentIndexChanged (int /* index */)
{
  set_rig_invariants ();
}

void Configuration::impl::on_CAT_handshake_button_group_buttonClicked (int /* id */)
{
  set_rig_invariants ();
}

void Configuration::impl::on_rig_combo_box_currentIndexChanged (int /* index */)
{
  set_rig_invariants ();
}

void Configuration::impl::on_CAT_data_bits_button_group_buttonClicked (int /* id */)
{
  set_rig_invariants ();
}

void Configuration::impl::on_CAT_stop_bits_button_group_buttonClicked (int /* id */)
{
  set_rig_invariants ();
}

void Configuration::impl::on_CAT_poll_interval_spin_box_valueChanged (int /* value */)
{
  set_rig_invariants ();
}

void Configuration::impl::on_split_mode_button_group_buttonClicked (int /* id */)
{
  set_rig_invariants ();
}

void Configuration::impl::on_test_CAT_push_button_clicked ()
{
  if (!validate ())
    {
      return;
    }

  ui_->test_CAT_push_button->setStyleSheet ({});
  if (open_rig (true))
    {
      //Q_EMIT sync (true);
    }

  set_rig_invariants ();
}

void Configuration::impl::on_test_PTT_push_button_clicked (bool checked)
{
  ui_->test_PTT_push_button->setChecked (!checked); // let status
                                                    // update check us
  if (!validate ())
    {
      return;
    }

  if (open_rig ())
    {
      Q_EMIT self_->transceiver_ptt (checked);
    }
}

void Configuration::impl::on_force_DTR_combo_box_currentIndexChanged (int /* index */)
{
  set_rig_invariants ();
}

void Configuration::impl::on_force_RTS_combo_box_currentIndexChanged (int /* index */)
{
  set_rig_invariants ();
}

void Configuration::impl::on_PTT_method_button_group_buttonClicked (int /* id */)
{
  set_rig_invariants ();
}

void Configuration::impl::on_add_macro_line_edit_editingFinished ()
{
  ui_->add_macro_line_edit->setText (ui_->add_macro_line_edit->text ().toUpper ());
}

void Configuration::impl::on_delete_macro_push_button_clicked (bool /* checked */)
{
  auto selection_model = ui_->macros_list_view->selectionModel ();
  if (selection_model->hasSelection ())
    {
      // delete all selected items
      delete_selected_macros (selection_model->selectedRows ());
    }
}

void Configuration::impl::delete_macro ()
{
  auto selection_model = ui_->macros_list_view->selectionModel ();
  if (!selection_model->hasSelection ())
    {
      // delete item under cursor if any
      auto index = selection_model->currentIndex ();
      if (index.isValid ())
        {
          next_macros_.removeRow (index.row ());
        }
    }
  else
    {
      // delete the whole selection
      delete_selected_macros (selection_model->selectedRows ());
    }
}

void Configuration::impl::delete_selected_macros (QModelIndexList selected_rows)
{
  // sort in reverse row order so that we can delete without changing
  // indices underneath us
  std::sort (selected_rows.begin (), selected_rows.end (), [] (QModelIndex const& lhs, QModelIndex const& rhs)
             {
               return rhs.row () < lhs.row (); // reverse row ordering
             });

  // now delete them
  Q_FOREACH (auto index, selected_rows)
    {
      next_macros_.removeRow (index.row ());
    }
}

void Configuration::impl::on_add_macro_push_button_clicked (bool /* checked */)
{
  if (next_macros_.insertRow (next_macros_.rowCount ()))
    {
      auto index = next_macros_.index (next_macros_.rowCount () - 1);
      ui_->macros_list_view->setCurrentIndex (index);
      next_macros_.setData (index, ui_->add_macro_line_edit->text ());
      ui_->add_macro_line_edit->clear ();
    }
}

void Configuration::impl::on_udp_server_line_edit_textChanged (QString const&)
{
  udp_server_name_edited_ = true;
}

void Configuration::impl::on_udp_server_line_edit_editingFinished ()
{
  if (this->isVisible())
  {
    int q1,q2,q3,q4;
    char tmpbuf[2];
    int n = sscanf(ui_->udp_server_line_edit->text ().trimmed ().toLatin1(), "%d.%d.%d.%d.%1s", &q1, &q2, &q3, &q4, tmpbuf);
    const char *iperr;
    switch(n)
    {
      case 0: iperr = "Error before first number";break;
      case 1: iperr = "Error between first and second number";break;
      case 2: iperr = "Error between second and third number";break;
      case 3: iperr = "Error between third and fourth number";break;
      case 4: iperr = ""; break;
      case 5: iperr = "Invalid characters after IP address"; break;
      default: iperr = "Unknown error parsing network address";
    }
    if (n != 4)
    {
       MessageBox::warning_message (this, tr ("Error in network address"), tr (iperr));
       return;
    }

  if (udp_server_name_edited_)
    {
      auto const& server = ui_->udp_server_line_edit->text ().trimmed ();
      QHostAddress ha {server};
      if (server.size () && ha.isNull ())
        {
          // queue a host address lookup
          // qDebug () << "server host DNS lookup:" << server;
#if QT_VERSION >= QT_VERSION_CHECK(5, 9, 0)
          dns_lookup_id_ = QHostInfo::lookupHost (server, this, &Configuration::impl::host_info_results);
#else
          dns_lookup_id_ = QHostInfo::lookupHost (server, this, SLOT (host_info_results (QHostInfo)));
#endif
        }
      else
        {
          check_multicast (ha);
        }
    }
  }
}

void Configuration::impl::host_info_results (QHostInfo host_info)
{
  if (host_info.lookupId () != dns_lookup_id_) return;
  dns_lookup_id_ = -1;
  if (QHostInfo::NoError != host_info.error ())
    {
      MessageBox::critical_message (this, tr ("UDP server DNS lookup failed"), host_info.errorString ());
    }
  else
    {
      auto const& server_addresses = host_info.addresses ();
      // qDebug () << "message server addresses:" << server_addresses;
      if (server_addresses.size ())
        {
          check_multicast (server_addresses[0]);
        }
    }
}

void Configuration::impl::check_multicast (QHostAddress const& ha)
{
  auto is_multicast = is_multicast_address (ha);
  ui_->udp_interfaces_label->setVisible (is_multicast);
  ui_->udp_interfaces_combo_box->setVisible (is_multicast);
  ui_->udp_TTL_label->setVisible (is_multicast);
  ui_->udp_TTL_spin_box->setVisible (is_multicast);
  if (isVisible ())
    {
      if (is_MAC_ambiguous_multicast_address (ha))
        {
          MessageBox::warning_message (this, tr ("MAC-ambiguous multicast groups addresses not supported"));
          find_tab (ui_->udp_server_line_edit);
          ui_->udp_server_line_edit->clear ();
        }
    }
  udp_server_name_edited_ = false;
}

void Configuration::impl::size_frequency_table_columns()
{
  ui_->frequencies_table_view->setVisible(false);
  ui_->frequencies_table_view->resizeColumnsToContents();
  ui_->frequencies_table_view->setVisible(true);
}

void Configuration::impl::delete_frequencies ()
{
  auto selection_model = ui_->frequencies_table_view->selectionModel ();
  selection_model->select (selection_model->selection (), QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows);
  next_frequencies_.removeDisjointRows (selection_model->selectedRows ());
  ui_->frequencies_table_view->resizeColumnToContents (FrequencyList_v2_101::mode_column);
  size_frequency_table_columns ();
}

void Configuration::impl::load_frequencies ()
{
  auto file_name = QFileDialog::getOpenFileName (this, tr ("Load Working Frequencies"), writeable_data_dir_.absolutePath (), tr ("Frequency files (*.qrg *.qrg.json);;All files (*.*)"));
  if (!file_name.isNull ())
    {
      auto const list = read_frequencies_file (file_name);
      if (list.size ()
          && (!next_frequencies_.frequency_list ().size ()
              || MessageBox::Yes == MessageBox::query_message (this
                                                               , tr ("Replace Working Frequencies")
                                                               , tr ("Are you sure you want to discard your current "
                                                                     "working frequencies and replace them with the "
                                                                     "loaded ones?"))))
        {
          next_frequencies_.frequency_list (list); // update the model
        }
      size_frequency_table_columns();
    }
}

void Configuration::impl::merge_frequencies ()
{
  auto file_name = QFileDialog::getOpenFileName (this, tr ("Merge Working Frequencies"), writeable_data_dir_.absolutePath (), tr ("Frequency files (*.qrg *.qrg.json);;All files (*.*)"));
  if (!file_name.isNull ())
    {
      next_frequencies_.frequency_list_merge (read_frequencies_file (file_name)); // update the model
      size_frequency_table_columns();
    }
}

FrequencyList_v2_101::FrequencyItems Configuration::impl::read_frequencies_file (QString const& file_name)
{
  QFile frequencies_file {file_name};
  frequencies_file.open (QFile::ReadOnly);
  QDataStream ids {&frequencies_file};
  FrequencyList_v2_101::FrequencyItems list;
  FrequencyList_v2::FrequencyItems list_v100;

  // read file as json if ends with qrg.json.
  if (file_name.endsWith(".qrg.json", Qt::CaseInsensitive))
    {
      try
        {
          list = FrequencyList_v2_101::from_json_file(&frequencies_file);
        }
      catch (ReadFileException const &e)
        {
          MessageBox::critical_message(this, tr("Error reading frequency file"), e.message_);
        }
      return list;
    }

  quint32 magic;
  ids >> magic;
  if (qrg_magic != magic)
    {
      MessageBox::warning_message (this, tr ("Not a valid frequencies file"), tr ("Incorrect file magic"));
      return list;
    }
  quint32 version;
  ids >> version;
  // handle version checks and QDataStream version here if
  // necessary
  if (version > qrg_version)
    {
      MessageBox::warning_message (this, tr ("Not a valid frequencies file"), tr ("Version is too new"));
      return list;
    }

  // de-serialize the data using version if necessary to
  // handle old schema
  if (version == qrg_version_100)
    {
      ids >> list_v100;
      Q_FOREACH (auto const& item, list_v100)
        {
          list << FrequencyList_v2_101::Item{item.frequency_, item.mode_, item.region_, QString(), QString(), QDateTime(),
                                         QDateTime(), false};
        }
    }
    else
    {
      ids >> list;
    }

  if (ids.status () != QDataStream::Ok || !ids.atEnd ())
    {
      MessageBox::warning_message (this, tr ("Not a valid frequencies file"), tr ("Contents corrupt"));
      list.clear ();
      return list;
    }
  return list;
}

void Configuration::impl::save_frequencies ()
{
  auto file_name = QFileDialog::getSaveFileName (this, tr ("Save Working Frequencies"), writeable_data_dir_.absolutePath (), tr ("Frequency files (*.qrg *.qrg.json);;All files (*.*)"));
  if (!file_name.isNull ())
    {
      bool b_write_json = file_name.endsWith(".qrg.json", Qt::CaseInsensitive);

      QFile frequencies_file {file_name};
      frequencies_file.open (QFile::WriteOnly);

      QDataStream ods {&frequencies_file};

      auto selection_model = ui_->frequencies_table_view->selectionModel ();
      if (selection_model->hasSelection ()
          && MessageBox::Yes == MessageBox::query_message (this
                                                           , tr ("Only Save Selected  Working Frequencies")
                                                           , tr ("Are you sure you want to save only the "
                                                                 "working frequencies that are currently selected? "
                                                                 "Click No to save all.")))
        {
          selection_model->select (selection_model->selection (), QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows);
          if (b_write_json)
            {
              next_frequencies_.to_json_file(&frequencies_file, "0x" + QString::number(qrg_magic, 16).toUpper(),
                                               "0x" + QString::number(qrg_version, 16).toUpper(),
                                               next_frequencies_.frequency_list(selection_model->selectedRows()));
            } else
            {
              ods << qrg_magic << qrg_version << next_frequencies_.frequency_list(selection_model->selectedRows());
            }
        }
      else
        {
          if (b_write_json)
            {
              next_frequencies_.to_json_file(&frequencies_file,
                                               "0x" + QString::number(qrg_magic, 16).toUpper(),
                                               "0x" + QString::number(qrg_version, 16).toUpper(),
                                                             next_frequencies_.frequency_list());
            } else
            {
              ods << qrg_magic << qrg_version << next_frequencies_.frequency_list();
            }
        }
    }
}

void Configuration::impl::reset_frequencies ()
{
  if (MessageBox::Yes == MessageBox::query_message (this, tr ("Reset Working Frequencies")
                                                    , tr ("Are you sure you want to discard your current "
                                                          "working frequencies and replace them with default "
                                                          "ones?")))
    {
      next_frequencies_.reset_to_defaults ();
    }
    size_frequency_table_columns ();
}

void Configuration::impl::insert_frequency ()
{
  if (QDialog::Accepted == frequency_dialog_->exec ())
    {
      ui_->frequencies_table_view->setCurrentIndex (next_frequencies_.add (frequency_dialog_->item ()));
      ui_->frequencies_table_view->resizeColumnToContents (FrequencyList_v2_101::mode_column);
      size_frequency_table_columns();
    }
}

void Configuration::impl::delete_stations ()
{
  auto selection_model = ui_->stations_table_view->selectionModel ();
  selection_model->select (selection_model->selection (), QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows);
  next_stations_.removeDisjointRows (selection_model->selectedRows ());
  ui_->stations_table_view->resizeColumnToContents (StationList::band_column);
  ui_->stations_table_view->resizeColumnToContents (StationList::offset_column);
}

void Configuration::impl::insert_station ()
{
  if (QDialog::Accepted == station_dialog_->exec ())
    {
      ui_->stations_table_view->setCurrentIndex (next_stations_.add (station_dialog_->station ()));
      ui_->stations_table_view->resizeColumnToContents (StationList::band_column);
      ui_->stations_table_view->resizeColumnToContents (StationList::offset_column);
    }
}

void Configuration::impl::on_save_path_select_push_button_clicked (bool /* checked */)
{
  QFileDialog fd {this, tr ("Save Directory"), ui_->save_path_display_label->text ()};
  fd.setFileMode (QFileDialog::Directory);
  fd.setOption (QFileDialog::ShowDirsOnly);
  if (fd.exec ())
    {
      if (fd.selectedFiles ().size ())
        {
          ui_->save_path_display_label->setText (fd.selectedFiles ().at (0));
        }
    }
}

void Configuration::impl::on_azel_path_select_push_button_clicked (bool /* checked */)
{
  QFileDialog fd {this, tr ("AzEl Directory"), ui_->azel_path_display_label->text ()};
  fd.setFileMode (QFileDialog::Directory);
  fd.setOption (QFileDialog::ShowDirsOnly);
  if (fd.exec ()) {
    if (fd.selectedFiles ().size ()) {
      ui_->azel_path_display_label->setText(fd.selectedFiles().at(0));
    }
  }
}

void Configuration::impl::on_calibration_intercept_spin_box_valueChanged (double)
{
  rig_active_ = false;          // force reset
}

void Configuration::impl::on_calibration_slope_ppm_spin_box_valueChanged (double)
{
  rig_active_ = false;          // force reset
}

void Configuration::impl::on_prompt_to_log_check_box_clicked(bool checked)
{
  if(checked) ui_->cbAutoLog->setChecked(false);
}

void Configuration::impl::on_cbAutoLog_clicked(bool checked)
{
  if(checked) ui_->prompt_to_log_check_box->setChecked(false);
}

void Configuration::impl::on_cbx2ToneSpacing_clicked(bool b)
{
  if(b) ui_->cbx4ToneSpacing->setChecked(false);
}

void Configuration::impl::on_cbx4ToneSpacing_clicked(bool b)
{
  if(b) ui_->cbx2ToneSpacing->setChecked(false);
}

void Configuration::impl::on_Field_Day_Exchange_textEdited (QString const& exchange)
{
  auto text = exchange.simplified ().toUpper ();
  auto class_pos = text.indexOf (QRegularExpression {R"([A-H])"});
  if (class_pos >= 0 && text.size () >= class_pos + 2 && text.at (class_pos + 1) != QChar {' '})
    {
      text.insert (class_pos + 1, QChar {' '});
    }
  ui_->Field_Day_Exchange->setText (text);
}

void Configuration::impl::on_RTTY_Exchange_textEdited (QString const& exchange)
{
  ui_->RTTY_Exchange->setText (exchange.toUpper ());
}

void Configuration::impl::on_Contest_Name_textEdited (QString const& exchange)
{
  ui_->Contest_Name->setText (exchange.toUpper ());
}

bool Configuration::impl::have_rig ()
{
  if (!open_rig ())
    {
      MessageBox::critical_message (this, tr ("Rig control error")
                                    , tr ("Failed to open connection to rig"));
    }
  return rig_active_;
}

bool Configuration::impl::open_rig (bool force)
{
  auto result = false;

  auto const rig_data = gather_rig_data ();
  if (force || !rig_active_ || rig_data != saved_rig_params_)
    {
      try
        {
          close_rig ();

          // create a new Transceiver object
          auto rig = transceiver_factory_.create (rig_data, transceiver_thread_);
          cached_rig_state_ = Transceiver::TransceiverState {};

          // hook up Configuration transceiver control signals to Transceiver slots
          //
          // these connections cross the thread boundary
          rig_connections_ << connect (this, &Configuration::impl::set_transceiver,
                                       rig.get (), &Transceiver::set);

          // hook up Transceiver signals to Configuration signals
          //
          // these connections cross the thread boundary
          rig_connections_ << connect (rig.get (), &Transceiver::resolution, this, [=] (int resolution) {
              rig_resolution_ = resolution;
            });
          rig_connections_ << connect (rig.get (), &Transceiver::update, this, &Configuration::impl::handle_transceiver_update);
          rig_connections_ << connect (rig.get (), &Transceiver::failure, this, &Configuration::impl::handle_transceiver_failure);

          // setup thread safe startup and close down semantics
          rig_connections_ << connect (this, &Configuration::impl::start_transceiver, rig.get (), &Transceiver::start);
          rig_connections_ << connect (this, &Configuration::impl::stop_transceiver, rig.get (), &Transceiver::stop);

          auto p = rig.release ();	// take ownership

          // schedule destruction on thread quit
          connect (transceiver_thread_, &QThread::finished, p, &QObject::deleteLater);

          // schedule eventual destruction for non-closing situations
          //
          // must   be   queued    connection   to   avoid   premature
          // self-immolation  since finished  signal  is  going to  be
          // emitted from  the object that  will get destroyed  in its
          // own  stop  slot  i.e.  a   same  thread  signal  to  slot
          // connection which by  default will be reduced  to a method
          // function call.
          connect (p, &Transceiver::finished, p, &Transceiver::deleteLater, Qt::QueuedConnection);

          ui_->test_CAT_push_button->setStyleSheet ({});
          rig_active_ = true;
          LOG_TRACE ("emitting startup_transceiver");
          Q_EMIT start_transceiver (++transceiver_command_number_); // start rig on its thread
          result = true;
        }
      catch (std::exception const& e)
        {
          handle_transceiver_failure (e.what ());
        }

      saved_rig_params_ = rig_data;
      rig_changed_ = true;
    }
  else
    {
      result = true;
    }
  return result;
}

void Configuration::impl::set_cached_mode ()
{
  MODE mode {Transceiver::UNK};
  // override cache mode with what we want to enforce which includes
  // UNK (unknown) where we want to leave the rig mode untouched
  switch (data_mode_)
    {
    case data_mode_USB: mode = Transceiver::USB; break;
    case data_mode_data: mode = Transceiver::DIG_U; break;
    default: break;
    }

  cached_rig_state_.mode (mode);
}

void Configuration::impl::transceiver_frequency (Frequency f)
{
  cached_rig_state_.online (true); // we want the rig online
  set_cached_mode ();

  // apply any offset & calibration
  // we store the offset here for use in feedback from the rig, we
  // cannot absolutely determine if the offset should apply but by
  // simply picking an offset when the Rx frequency is set and
  // sticking to it we get sane behaviour
  current_offset_ = stations_.offset (f);
  cached_rig_state_.frequency (apply_calibration (f + current_offset_));

  // qDebug () << "Configuration::impl::transceiver_frequency: n:" << transceiver_command_number_ + 1 << "f:" << f;
  LOG_TRACE ("emitting set_transceiver: requested state:" << cached_rig_state_);
  Q_EMIT set_transceiver (cached_rig_state_, ++transceiver_command_number_);
}

void Configuration::impl::transceiver_tx_frequency (Frequency f)
{
  Q_ASSERT (!f || split_mode ());
  if (split_mode ())
    {
      cached_rig_state_.online (true); // we want the rig online
      set_cached_mode ();
      cached_rig_state_.split (f);
      cached_rig_state_.tx_frequency (f);

      // lookup offset for tx and apply calibration
      if (f)
        {
          // apply and offset and calibration
          // we store the offset here for use in feedback from the
          // rig, we cannot absolutely determine if the offset should
          // apply but by simply picking an offset when the Rx
          // frequency is set and sticking to it we get sane behaviour
          current_tx_offset_ = stations_.offset (f);
          cached_rig_state_.tx_frequency (apply_calibration (f + current_tx_offset_));
        }

      // qDebug () << "Configuration::impl::transceiver_tx_frequency: n:" << transceiver_command_number_ + 1 << "f:" << f;
      LOG_TRACE ("emitting set_transceiver: requested state:" << cached_rig_state_);
      Q_EMIT set_transceiver (cached_rig_state_, ++transceiver_command_number_);
    }
}

void Configuration::impl::transceiver_mode (MODE m)
{
  cached_rig_state_.online (true); // we want the rig online
  cached_rig_state_.mode (m);
  // qDebug () << "Configuration::impl::transceiver_mode: n:" << transceiver_command_number_ + 1 << "m:" << m;
  LOG_TRACE ("emitting set_transceiver: requested state:" << cached_rig_state_);
  Q_EMIT set_transceiver (cached_rig_state_, ++transceiver_command_number_);
}

void Configuration::impl::transceiver_ptt (bool on)
{
  cached_rig_state_.online (true); // we want the rig online
  set_cached_mode ();
  cached_rig_state_.ptt (on);
  // qDebug () << "Configuration::impl::transceiver_ptt: n:" << transceiver_command_number_ + 1 << "on:" << on;
  LOG_TRACE ("emitting set_transceiver: requested state:" << cached_rig_state_);
  Q_EMIT set_transceiver (cached_rig_state_, ++transceiver_command_number_);
}

void Configuration::impl::sync_transceiver (bool /*force_signal*/)
{
  // pass this on as cache must be ignored
  // Q_EMIT sync (force_signal);
}

void Configuration::impl::handle_transceiver_update (TransceiverState const& state,
                                                     unsigned sequence_number)
{
  LOG_TRACE ("#: " << sequence_number << ' ' << state);

  // only follow rig on some information, ignore other stuff
  cached_rig_state_.online (state.online ());
  cached_rig_state_.frequency (state.frequency ());
  cached_rig_state_.mode (state.mode ());
  cached_rig_state_.split (state.split ());

  if (state.online ())
    {
      ui_->test_PTT_push_button->setChecked (state.ptt ());

      if (isVisible ())
        {
          ui_->test_CAT_push_button->setStyleSheet ("QPushButton {background-color: green;}");

          auto const& rig = ui_->rig_combo_box->currentText ();
          auto ptt_method = static_cast<TransceiverFactory::PTTMethod> (ui_->PTT_method_button_group->checkedId ());
          auto CAT_PTT_enabled = transceiver_factory_.has_CAT_PTT (rig);
          ui_->test_PTT_push_button->setEnabled ((TransceiverFactory::PTT_method_CAT == ptt_method && CAT_PTT_enabled)
                                                 || TransceiverFactory::PTT_method_DTR == ptt_method
                                                 || TransceiverFactory::PTT_method_RTS == ptt_method);
        }
    }
  else
    {
      close_rig ();
    }

  // pass on to clients if current command is processed
  if (sequence_number == transceiver_command_number_)
    {
      TransceiverState reported_state {state};
      // take off calibration & offset
      reported_state.frequency (remove_calibration (reported_state.frequency ()) - current_offset_);

      if (reported_state.tx_frequency ())
        {
          // take off calibration & offset
          reported_state.tx_frequency (remove_calibration (reported_state.tx_frequency ()) - current_tx_offset_);
        }

      Q_EMIT self_->transceiver_update (reported_state);
    }
}

void Configuration::impl::handle_transceiver_failure (QString const& reason)
{
  LOG_ERROR ("handle_transceiver_failure: reason: " << reason);
  close_rig ();
  ui_->test_PTT_push_button->setChecked (false);

  if (isVisible ())
    {
      MessageBox::critical_message (this, tr ("Rig failure"), reason);
    }
  else
    {
      // pass on if our dialog isn't active
      Q_EMIT self_->transceiver_failure (reason);
    }
}

void Configuration::impl::close_rig ()
{
  ui_->test_PTT_push_button->setEnabled (false);

  // revert to no rig configured
  if (rig_active_)
    {
      ui_->test_CAT_push_button->setStyleSheet ("QPushButton {background-color: red;}");
      LOG_TRACE ("emitting stop_transceiver");
      Q_EMIT stop_transceiver ();
      for (auto const& connection: rig_connections_)
        {
          disconnect (connection);
        }
      rig_connections_.clear ();
      rig_active_ = false;
    }
}

// find the audio device that matches the specified name, also
// populate into the selection combo box with any devices we find in
// the search
QAudioDeviceInfo Configuration::impl::find_audio_device (QAudio::Mode mode, QComboBox * combo_box
                                                         , QString const& device_name)
{
  using std::copy;
  using std::back_inserter;

  if (device_name.size ())
    {
      Q_EMIT self_->enumerating_audio_devices ();
      auto const& devices = QAudioDeviceInfo::availableDevices (mode);
      Q_FOREACH (auto const& p, devices)
        {
          // qDebug () << "Configuration::impl::find_audio_device: input:" << (QAudio::AudioInput == mode) << "name:" << p.deviceName () << "preferred format:" << p.preferredFormat () << "endians:" << p.supportedByteOrders () << "codecs:" << p.supportedCodecs () << "channels:" << p.supportedChannelCounts () << "rates:" << p.supportedSampleRates () << "sizes:" << p.supportedSampleSizes () << "types:" << p.supportedSampleTypes ();
          if (p.deviceName () == device_name)
            {
              // convert supported channel counts into something we can store in the item model
              QList<QVariant> channel_counts;
              auto scc = p.supportedChannelCounts ();
              copy (scc.cbegin (), scc.cend (), back_inserter (channel_counts));
              combo_box->insertItem (0, device_name, QVariant::fromValue (audio_info_type {p, channel_counts}));
              combo_box->setCurrentIndex (0);
              return p;
            }
        }
      // insert a place holder for the not found device
      combo_box->insertItem (0, device_name + " (" + tr ("Not found", "audio device missing") + ")", QVariant::fromValue (audio_info_type {}));
      combo_box->setCurrentIndex (0);
    }
  return {};
}

// load the available audio devices into the selection combo box
void Configuration::impl::load_audio_devices (QAudio::Mode mode, QComboBox * combo_box
                                              , QAudioDeviceInfo * device)
{
  using std::copy;
  using std::back_inserter;

  combo_box->clear ();

  Q_EMIT self_->enumerating_audio_devices ();
  int current_index = -1;
  auto const& devices = QAudioDeviceInfo::availableDevices (mode);
  Q_FOREACH (auto const& p, devices)
    {
      // qDebug () << "Configuration::impl::load_audio_devices: input:" << (QAudio::AudioInput == mode) << "name:" << p.deviceName () << "preferred format:" << p.preferredFormat () << "endians:" << p.supportedByteOrders () << "codecs:" << p.supportedCodecs () << "channels:" << p.supportedChannelCounts () << "rates:" << p.supportedSampleRates () << "sizes:" << p.supportedSampleSizes () << "types:" << p.supportedSampleTypes ();

      // convert supported channel counts into something we can store in the item model
      QList<QVariant> channel_counts;
      auto scc = p.supportedChannelCounts ();
      copy (scc.cbegin (), scc.cend (), back_inserter (channel_counts));

      combo_box->addItem (p.deviceName (), QVariant::fromValue (audio_info_type {p, channel_counts}));
      if (p == *device)
        {
          current_index = combo_box->count () - 1;
        }
    }
  combo_box->setCurrentIndex (current_index);
}

// load the available network interfaces into the selection combo box
void Configuration::impl::load_network_interfaces (CheckableItemComboBox * combo_box, QStringList current)
{
  combo_box->clear ();
  for (auto const& net_if : QNetworkInterface::allInterfaces ())
    {
      auto flags = QNetworkInterface::IsUp | QNetworkInterface::CanMulticast;
      if ((net_if.flags () & flags) == flags)
        {
          bool check_it = current.contains (net_if.name ());
          if (net_if.flags () & QNetworkInterface::IsLoopBack)
            {
              loopback_interface_name_ = net_if.name ();
              if (!current.size ())
                {
                  check_it = true;
                }
            }
          auto item = combo_box->addCheckItem (net_if.humanReadableName ()
                                               , net_if.name ()
                                               , check_it ? Qt::Checked : Qt::Unchecked);
          auto tip = QString {"name(index): %1(%2) - %3"}.arg (net_if.name ()).arg (net_if.index ())
                       .arg (net_if.flags () & QNetworkInterface::IsUp ? "Up" : "Down");
          auto hw_addr = net_if.hardwareAddress ();
          if (hw_addr.size ())
            {
              tip += QString {"\nhw: %1"}.arg (net_if.hardwareAddress ());
            }
          auto aes = net_if.addressEntries ();
          if (aes.size ())
            {
              tip += "\naddresses:";
              for (auto const& ae : aes)
                {
                  tip += QString {"\n  ip: %1/%2"}.arg (ae.ip ().toString ()).arg (ae.prefixLength ());
                }
            }
          item->setToolTip (tip);
        }
    }
}

// get the select network interfaces from the selection combo box
void Configuration::impl::validate_network_interfaces (QString const& /*text*/)
{
  auto model = static_cast<QStandardItemModel *> (ui_->udp_interfaces_combo_box->model ());
  bool has_checked {false};
  int loopback_row {-1};
  for (int row = 0; row < model->rowCount (); ++row)
    {
      if (model->item (row)->data ().toString () == loopback_interface_name_)
        {
          loopback_row = row;
        }
      else if (Qt::Checked == model->item (row)->checkState ())
        {
          has_checked = true;
        }
    }
  if (loopback_row >= 0)
    {
      if (!has_checked)
        {
          model->item (loopback_row)->setCheckState (Qt::Checked);
        }
      model->item (loopback_row)->setEnabled (has_checked);
    }
}

// get the select network interfaces from the selection combo box
QStringList Configuration::impl::get_selected_network_interfaces (CheckableItemComboBox * combo_box)
{
  QStringList interfaces;
  auto model = static_cast<QStandardItemModel *> (combo_box->model ());
  for (int row = 0; row < model->rowCount (); ++row)
    {
      if (Qt::Checked == model->item (row)->checkState ())
        {
          interfaces << model->item (row)->data ().toString ();
        }
    }
  return interfaces;
}

// enable only the channels that are supported by the selected audio device
void Configuration::impl::update_audio_channels (QComboBox const * source_combo_box, int index, QComboBox * combo_box, bool allow_both)
{
  // disable all items
  for (int i (0); i < combo_box->count (); ++i)
    {
      combo_box->setItemData (i, combo_box_item_disabled, Qt::UserRole - 1);
    }

  Q_FOREACH (QVariant const& v
             , (source_combo_box->itemData (index).value<audio_info_type> ().second))
    {
      // enable valid options
      int n {v.toInt ()};
      if (2 == n)
        {
          combo_box->setItemData (AudioDevice::Left, combo_box_item_enabled, Qt::UserRole - 1);
          combo_box->setItemData (AudioDevice::Right, combo_box_item_enabled, Qt::UserRole - 1);
          if (allow_both)
            {
              combo_box->setItemData (AudioDevice::Both, combo_box_item_enabled, Qt::UserRole - 1);
            }
        }
      else if (1 == n)
        {
          combo_box->setItemData (AudioDevice::Mono, combo_box_item_enabled, Qt::UserRole - 1);
        }
    }
}

void Configuration::impl::find_tab (QWidget * target)
{
  for (auto * parent = target->parentWidget (); parent; parent = parent->parentWidget ())
    {
      auto index = ui_->configuration_tabs->indexOf (parent);
      if (index != -1)
        {
          ui_->configuration_tabs->setCurrentIndex (index);
          break;
        }
    }
  target->setFocus ();
}

// load all the supported rig names into the selection combo box
void Configuration::impl::enumerate_rigs ()
{
  ui_->rig_combo_box->clear ();

  auto rigs = transceiver_factory_.supported_transceivers ();

  for (auto r = rigs.cbegin (); r != rigs.cend (); ++r)
    {
      if ("None" == r.key ())
        {
          // put None first
          ui_->rig_combo_box->insertItem (0, r.key (), r.value ().model_number_);
        }
      else
        {
          int i;
          for(i=1;i<ui_->rig_combo_box->count() && (r.key().toLower() > ui_->rig_combo_box->itemText(i).toLower());++i);
          if (i < ui_->rig_combo_box->count())  ui_->rig_combo_box->insertItem (i, r.key (), r.value ().model_number_);
          else ui_->rig_combo_box->addItem (r.key (), r.value ().model_number_);
        }
    }

  ui_->rig_combo_box->setCurrentText (rig_params_.rig_name);
}

void Configuration::impl::fill_port_combo_box (QComboBox * cb)
{
  auto current_text = cb->currentText ();
  cb->clear ();
  Q_FOREACH (auto const& p, QSerialPortInfo::availablePorts ())
    {
      if (!p.portName ().contains ( "NULL" )) // virtual serial port pairs
        {
          // remove possibly confusing Windows device path (OK because
          // it gets added back by Hamlib)
          cb->addItem (p.systemLocation ().remove (QRegularExpression {R"(^\\\\\.\\)"}));
          auto tip = QString {"%1 %2 %3"}.arg (p.manufacturer ()).arg (p.serialNumber ()).arg (p.description ()).trimmed ();
          if (tip.size ())
            {
              cb->setItemData (cb->count () - 1, tip, Qt::ToolTipRole);
            }
        }
    }
  cb->addItem ("USB");
  cb->setItemData (cb->count () - 1, "Custom USB device", Qt::ToolTipRole);
  cb->setEditText (current_text);
}

auto Configuration::impl::apply_calibration (Frequency f) const -> Frequency
{
  if (frequency_calibration_disabled_) return f;
  return std::llround (calibration_.intercept
                       + (1. + calibration_.slope_ppm / 1.e6) * f);
}

auto Configuration::impl::remove_calibration (Frequency f) const -> Frequency
{
  if (frequency_calibration_disabled_) return f;
  return std::llround ((f - calibration_.intercept)
                       / (1. + calibration_.slope_ppm / 1.e6));
}

ENUM_QDATASTREAM_OPS_IMPL (Configuration, DataMode);
ENUM_QDATASTREAM_OPS_IMPL (Configuration, Type2MsgGen);

ENUM_CONVERSION_OPS_IMPL (Configuration, DataMode);
ENUM_CONVERSION_OPS_IMPL (Configuration, Type2MsgGen);