#include "EqualizationToolsDialog.hpp"

#include <iterator>
#include <algorithm>
#include <fstream>
#include <limits>
#include <cmath>

#include <QDir>
#include <QVector>
#include <QHBoxLayout>
#include <QDialog>
#include <QDialogButtonBox>
#include <QPushButton>
#include <QFileDialog>
#include <QSettings>

#include "SettingsGroup.hpp"
#include "qcustomplot.h"
#include "pimpl_impl.hpp"

namespace
{
  float constexpr PI = 3.1415927f;
  char const * const title = "Equalization Tools";
  size_t constexpr intervals = 144;

  // plot data loaders - wraps a plot providing value_type and
  // push_back so that a std::back_inserter output iterator can be
  // used to load plot data
  template<typename T, typename A>
  struct plot_data_loader
  {
  public:
    typedef T value_type;

    // the adjust argument is a function that is passed the plot
    // pointer, the graph index and a data point, it returns a
    // possibly adjusted data point and can modify the graph including
    // adding extra points or gaps (quiet_NaN)
    plot_data_loader (QCustomPlot * plot, int graph_index, A adjust)
      : plot_ {plot}
      , index_ {graph_index}
      , adjust_ (adjust)
    {
    }

    // load point into graph
    void push_back (value_type const& d)
    {
      plot_->graph (index_)->data ()->add (adjust_ (plot_, index_, d));
    }

  private:
    QCustomPlot * plot_;
    int index_;
    A adjust_;
  };
  // helper function template to make a plot_data_loader instance
  template<typename A>
  auto make_plot_data_loader (QCustomPlot * plot, int index, A adjust)
    -> plot_data_loader<QCPGraphData, decltype (adjust)>
  {
    return plot_data_loader<QCPGraphData, decltype (adjust)> {plot, index, adjust};
  }
  // identity adjust function when no adjustment is needed with the
  // above instantiation helper
  QCPGraphData adjust_identity (QCustomPlot *, int, QCPGraphData const& v) {return v;}

  // a plot_data_loader adjustment function that wraps Y values of
  // (-1..+1) plotting discontinuities as gaps in the graph data
  auto wrap_pi = [] (QCustomPlot * plot, int index, QCPGraphData d) 
  {
    double constexpr limit {1};
    static unsigned wrap_count {0};
    static double last_x {std::numeric_limits<double>::lowest ()};

    d.value += 2 * limit * wrap_count;
    if (d.value > limit)
      {
        // insert a gap in the graph
        plot->graph (index)->data ()->add ({last_x + (d.key - last_x) / 2
              , std::numeric_limits<double>::quiet_NaN ()});
        while (d.value > limit)
          {
            --wrap_count;
            d.value -= 2 * limit;
          }
      }
    else if (d.value < -limit)
      {
        // insert a gap into the graph
        plot->graph (index)->data ()->add ({last_x + (d.key - last_x) / 2
              , std::numeric_limits<double>::quiet_NaN ()});
        while (d.value < -limit)
          {
            ++wrap_count;
            d.value += 2 * limit;
          }
      }
    last_x = d.key;
    return d;
  };

  // generate points of type R from a function of type F for X in
  // (-1..+1) with N intervals and function of type SX to scale X and
  // of type SY to scale Y
  //
  // it is up to the user to call the generator sufficient times which
  // is interval+1 times to reach +1
  template<typename R, typename F, typename SX, typename SY>
  struct graph_generator
  {
  public:
    graph_generator (F f, size_t intervals, SX x_scaling, SY y_scaling)
      : x_ {0}
      , f_ (f)
      , intervals_ {intervals}
      , x_scaling_ (x_scaling)
      , y_scaling_ (y_scaling)
    {
    }

    R operator () ()
    {
      typename F::value_type x {x_++ * 2.f / intervals_ - 1.f};
      return {x_scaling_ (x), y_scaling_ (f_ (x))};
    }

  private:
    int x_;
    F f_;
    size_t intervals_;
    SX x_scaling_;
    SY y_scaling_;
  };
  // helper function template to make a graph_generator instance for
  // QCPGraphData type points with intervals intervals
  template<typename F, typename SX, typename SY>
  auto make_graph_generator (F function, SX x_scaling, SY y_scaling)
    -> graph_generator<QCPGraphData, F, decltype (x_scaling), decltype (y_scaling)>
  {
    return graph_generator<QCPGraphData, F, decltype (x_scaling), decltype (y_scaling)>
      {function, intervals, x_scaling, y_scaling};
  }

  // template function object for a polynomial with coefficients
  template<typename C>
  class polynomial
  {
  public:
    typedef typename C::value_type value_type;

    explicit polynomial (C const& coefficients)
      : c_ {coefficients}
    {
    }

    value_type operator () (value_type const& x)
    {
      value_type y {};
      for (typename C::size_type i = c_.size (); i > 0; --i)
        {
          y = c_[i - 1] + x * y;
        }
      return y;
    }

  private:
    C c_;
  };
  // helper function template to instantiate a polynomial instance
  template<typename C>
  auto make_polynomial (C const& coefficients) -> polynomial<C>
  {
    return polynomial<C> (coefficients);
  }

  // template function object for a group delay with coefficients
  template<typename C>
  class group_delay
  {
  public:
    typedef typename C::value_type value_type;

    explicit group_delay (C const& coefficients)
      : c_ {coefficients}
    {
    }

    value_type operator () (value_type const& x)
    {
      value_type tau {};
      for (typename C::size_type i = 2; i < c_.size (); ++i)
        {
          tau += i * c_[i] * std::pow (x, i - 1);
        }
      return -1 / (2 * PI) * tau;
    }

  private:
    C c_;
  };
  // helper function template to instantiate a group_delay function
  // object
  template<typename C>
  auto make_group_delay (C const& coefficients) -> group_delay<C>
  {
    return group_delay<C> (coefficients);
  }

  // handy identity function
  template<typename T> T identity (T const& v) {return v;}

  // a lambda that scales the X axis from normalized to (500..2500)Hz
  auto freq_scaling = [] (float v) -> float {return 1500.f + 1000.f * v;};

  // a lambda that scales the phase Y axis from radians to units of Pi
  auto pi_scaling = [] (float v) -> float {return v / PI;};
}

class EqualizationToolsDialog::impl final
  : public QDialog
{
  Q_OBJECT

public:
  explicit impl (EqualizationToolsDialog * self, QSettings * settings
                 , QDir const& data_directory, QVector<double> const& coefficients
                 , QWidget * parent);
  ~impl () {save_window_state ();}

protected:
  void closeEvent (QCloseEvent * e) override
  {
    save_window_state ();
    QDialog::closeEvent (e);
  }

private:
  void save_window_state ()
  {
    SettingsGroup g (settings_, title);
    settings_->setValue ("geometry", saveGeometry ());
  }

  void plot_current ();
  void plot_phase ();
  void plot_amplitude ();

  EqualizationToolsDialog * self_;
  QSettings * settings_;
  QDir data_directory_;
  QHBoxLayout layout_;
  QVector<double> current_coefficients_;
  QVector<double> new_coefficients_;
  unsigned amp_poly_low_;
  unsigned amp_poly_high_;
  QVector<double> amp_coefficients_;
  QCustomPlot plot_;
  QDialogButtonBox button_box_;
};

#include "EqualizationToolsDialog.moc"

EqualizationToolsDialog::EqualizationToolsDialog (QSettings * settings
                                                  , QDir const& data_directory
                                                  , QVector<double> const& coefficients
                                                  , QWidget * parent)
  : m_ {this, settings, data_directory, coefficients, parent}
{
}

void EqualizationToolsDialog::show ()
{
  m_->show ();
}

EqualizationToolsDialog::impl::impl (EqualizationToolsDialog * self
                                     , QSettings * settings
                                     , QDir const& data_directory
                                     , QVector<double> const& coefficients
                                     , QWidget * parent)
  : QDialog {parent}
  , self_ {self}
  , settings_ {settings}
  , data_directory_ {data_directory}
  , current_coefficients_ {coefficients}
  , amp_poly_low_ {0}
  , amp_poly_high_ {6000}
  , button_box_ {QDialogButtonBox::Apply
        | QDialogButtonBox::RestoreDefaults | QDialogButtonBox::Close
        , Qt::Vertical}
{
  setWindowTitle (windowTitle () + ' ' + tr ("Equalization Tools"));
  resize (500, 600);
  {
    SettingsGroup g {settings_, title};
    restoreGeometry (settings_->value ("geometry", saveGeometry ()).toByteArray ());
  }

  auto legend_title = new QCPTextElement {&plot_, tr ("Phase"), QFont {"sans", 9, QFont::Bold}};
  legend_title->setLayer (plot_.legend->layer ());
  plot_.legend->addElement (0, 0, legend_title);
  plot_.legend->setVisible (true);

  plot_.xAxis->setLabel (tr ("Freq (Hz)"));
  plot_.xAxis->setRange (500, 2500);
  plot_.yAxis->setLabel (tr ("Phase (Π)"));
  plot_.yAxis->setRange (-1, +1);
  plot_.yAxis2->setLabel (tr ("Delay (ms)"));
  plot_.axisRect ()->setRangeDrag (Qt::Vertical);
  plot_.axisRect ()->setRangeZoom (Qt::Vertical);
  plot_.yAxis2->setVisible (true);
  plot_.axisRect ()->setRangeDragAxes (nullptr, plot_.yAxis2);
  plot_.axisRect ()->setRangeZoomAxes (nullptr, plot_.yAxis2);
  plot_.axisRect ()->insetLayout ()->setInsetAlignment (0, Qt::AlignBottom|Qt::AlignRight);
  plot_.setInteractions (QCP::iRangeDrag | QCP::iRangeZoom | QCP::iSelectPlottables);

  plot_.addGraph ()->setName (tr ("Measured"));
  plot_.graph ()->setPen (QPen {Qt::blue});
  plot_.graph ()->setVisible (false);
  plot_.graph ()->removeFromLegend ();

  plot_.addGraph ()->setName (tr ("Proposed"));
  plot_.graph ()->setPen (QPen {Qt::red});
  plot_.graph ()->setVisible (false);
  plot_.graph ()->removeFromLegend ();

  plot_.addGraph ()->setName (tr ("Current"));
  plot_.graph ()->setPen (QPen {Qt::green});

  plot_.addGraph (plot_.xAxis, plot_.yAxis2)->setName (tr ("Group Delay"));
  plot_.graph ()->setPen (QPen {Qt::darkGreen});

  plot_.plotLayout ()->addElement (new QCPAxisRect {&plot_});
  plot_.plotLayout ()->setRowStretchFactor (1, 0.5);

  auto amp_legend = new QCPLegend;
  plot_.axisRect (1)->insetLayout ()->addElement (amp_legend, Qt::AlignTop | Qt::AlignRight);
  plot_.axisRect (1)->insetLayout ()->setMargins (QMargins {12, 12, 12, 12});
  amp_legend->setVisible (true);
  amp_legend->setLayer (QLatin1String {"legend"});
  legend_title = new QCPTextElement {&plot_, tr ("Amplitude"), QFont {"sans", 9, QFont::Bold}};
  legend_title->setLayer (amp_legend->layer ());
  amp_legend->addElement (0, 0, legend_title);

  plot_.axisRect (1)->axis (QCPAxis::atBottom)->setLabel (tr ("Freq (Hz)"));
  plot_.axisRect (1)->axis (QCPAxis::atBottom)->setRange (0, 6000);
  plot_.axisRect (1)->axis (QCPAxis::atLeft)->setLabel (tr ("Relative Power (dB)"));
  plot_.axisRect (1)->axis (QCPAxis::atLeft)->setRangeLower (0);
  plot_.axisRect (1)->setRangeDragAxes (nullptr, nullptr);
  plot_.axisRect (1)->setRangeZoomAxes (nullptr, nullptr);

  plot_.addGraph (plot_.axisRect (1)->axis (QCPAxis::atBottom)
                  , plot_.axisRect (1)->axis (QCPAxis::atLeft))->setName (tr ("Reference"));
  plot_.graph ()->setPen (QPen {Qt::blue});
  plot_.graph ()->removeFromLegend ();
  plot_.graph ()->addToLegend (amp_legend);

  layout_.addWidget (&plot_);

  auto load_phase_button = button_box_.addButton (tr ("Phase ..."), QDialogButtonBox::ActionRole);
  auto refresh_button = button_box_.addButton (tr ("Refresh"), QDialogButtonBox::ActionRole);
  auto discard_measured_button = button_box_.addButton (tr ("Discard Measured"), QDialogButtonBox::ActionRole);
  layout_.addWidget (&button_box_);
  setLayout (&layout_);

  connect (&button_box_, &QDialogButtonBox::rejected, this, &QDialog::reject);
  connect (&button_box_, &QDialogButtonBox::clicked, [=] (QAbstractButton * button) {
      if (button == load_phase_button)
        {
          plot_phase ();
        }
      else if (button == refresh_button)
        {
          plot_current ();
        }
      else if (button == button_box_.button (QDialogButtonBox::Apply))
        {
          if (plot_.graph (0)->dataCount ()) // something loaded
            {
              current_coefficients_ = new_coefficients_;
              Q_EMIT self_->phase_equalization_changed (current_coefficients_);
              plot_current ();
            }
        }
      else if (button == button_box_.button (QDialogButtonBox::RestoreDefaults))
        {
          current_coefficients_ = QVector<double> {0., 0., 0., 0., 0.};
          Q_EMIT self_->phase_equalization_changed (current_coefficients_);
          plot_current ();
        }
      else if (button == discard_measured_button)
        {
          new_coefficients_ = QVector<double> {0., 0., 0., 0., 0.};

          plot_.graph (0)->data ()->clear ();
          plot_.graph (0)->setVisible (false);
          plot_.graph (0)->removeFromLegend ();

          plot_.graph (1)->data ()->clear ();
          plot_.graph (1)->setVisible (false);
          plot_.graph (1)->removeFromLegend ();

          plot_.replot ();
        }
    });

  plot_current ();
}

struct PowerSpectrumPoint
{
  operator QCPGraphData () const
  {
    return QCPGraphData {freq_, power_};
  }

  float freq_;
  float power_;
};

// read an amplitude point line from a stream (refspec.dat)
std::istream& operator >> (std::istream& is, PowerSpectrumPoint& r)
{
  float y1, y3, y4;             // discard these
  is >> r.freq_ >> y1 >> r.power_ >> y3 >> y4;
  return is;
}

void EqualizationToolsDialog::impl::plot_current ()
{
  auto phase_graph = make_plot_data_loader (&plot_, 2, wrap_pi);
  plot_.graph (2)->data ()->clear ();
  std::generate_n (std::back_inserter (phase_graph), intervals + 1
                   , make_graph_generator (make_polynomial (current_coefficients_), freq_scaling, pi_scaling));

  auto group_delay_graph = make_plot_data_loader (&plot_, 3, adjust_identity);
  plot_.graph (3)->data ()->clear ();
  std::generate_n (std::back_inserter (group_delay_graph), intervals + 1
                   , make_graph_generator (make_group_delay (current_coefficients_), freq_scaling, identity<double>));
  plot_.graph (3)->rescaleValueAxis ();

  QFileInfo refspec_file_info {data_directory_.absoluteFilePath ("refspec.dat")};
  std::ifstream refspec_file (refspec_file_info.absoluteFilePath ().toLatin1 ().constData (), std::ifstream::in);
  unsigned n;
  if (refspec_file >> amp_poly_low_ >> amp_poly_high_ >> n)
    {
      std::istream_iterator<double> isi {refspec_file};
      amp_coefficients_.clear ();
      std::copy_n (isi, n, std::back_inserter (amp_coefficients_));
    }
  else
    {
      // may be old format refspec.dat with no header so rewind
      refspec_file.clear ();
      refspec_file.seekg (0);
    }

  auto reference_spectrum_graph = make_plot_data_loader (&plot_, 4, adjust_identity);
  plot_.graph (4)->data ()->clear ();
  std::copy (std::istream_iterator<PowerSpectrumPoint> {refspec_file},
             std::istream_iterator<PowerSpectrumPoint> {},
             std::back_inserter (reference_spectrum_graph));
  plot_.graph (4)->rescaleValueAxis (true);

  plot_.replot ();
}

struct PhasePoint
{
  operator QCPGraphData () const
  {
    return QCPGraphData {freq_, phase_};
  }

  double freq_;
  double phase_;
};

// read a phase point line from a stream (pcoeff file)
std::istream& operator >> (std::istream& is, PhasePoint& c)
{
  double pp, sigmay;            // discard these
  if (is >> c.freq_ >> pp >> c.phase_ >> sigmay)
    {
      c.freq_ = 1500. + 1000. * c.freq_; // scale frequency to Hz
      c.phase_ /= PI;                    // scale to units of Pi
    }
  return is;
}

void EqualizationToolsDialog::impl::plot_phase ()
{
  auto const& phase_file_name = QFileDialog::getOpenFileName (this
                                                              , "Select Phase Response Coefficients"
                                                              , data_directory_.absolutePath ()
                                                              , "Phase Coefficient Files (*.pcoeff)");
  if (!phase_file_name.size ()) return;

  std::ifstream phase_file (phase_file_name.toLatin1 ().constData (), std::ifstream::in);
  int n;
  float chi;
  float rmsdiff;
  unsigned freq_low;
  unsigned freq_high;
  unsigned terms;
  // read header information
  if (phase_file >> n >> chi >> rmsdiff >> freq_low >> freq_high >> terms)
    {
      std::istream_iterator<double> isi {phase_file};
      new_coefficients_.clear ();
      std::copy_n (isi, terms, std::back_inserter (new_coefficients_));

      if (phase_file)
        {
          plot_.graph (0)->data ()->clear ();
          plot_.graph (1)->data ()->clear ();

          // read the phase data and plot as graph 0
          auto graph = make_plot_data_loader (&plot_, 0, adjust_identity);
          std::copy_n (std::istream_iterator<PhasePoint> {phase_file},
                       intervals + 1, std::back_inserter (graph));

          if (phase_file)
            {
              plot_.graph(0)->setLineStyle(QCPGraph::lsNone);
              plot_.graph(0)->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssDisc, 4));
              plot_.graph (0)->setVisible (true);
              plot_.graph (0)->addToLegend ();

              // generate the proposed polynomial plot as graph 1
              auto graph = make_plot_data_loader (&plot_, 1, wrap_pi);
              std::generate_n (std::back_inserter (graph), intervals + 1
                               , make_graph_generator (make_polynomial (new_coefficients_)
                                                       , freq_scaling, pi_scaling));
              plot_.graph (1)->setVisible (true);
              plot_.graph (1)->addToLegend ();
            }

          plot_.replot ();
        }
    }
}

#include "moc_EqualizationToolsDialog.cpp"
