/**********************************************************************

  Audacity: A Digital Audio Editor

  Effect/ScienFilter.cpp

  Norm C
  Mitch Golden
  Vaughan Johnson (Preview)

*******************************************************************//**

\file ScienFilter.cpp
\brief Implements EffectScienFilter, EffectScienFilterPanel.

*//****************************************************************//**

\class EffectScienFilter
\brief An Effect that applies 'classical' IIR filters.

  Performs IIR filtering that emulates analog filters, specifically
  Butterworth, Chebyshev Type I and Type II. Highpass and lowpass filters
  are supported, as are filter orders from 1 to 10.

  The filter is applied using biquads

*//****************************************************************//**

\class EffectScienFilterPanel
\brief EffectScienFilterPanel is used with EffectScienFilter and controls
a graph for EffectScienFilter.

*//*******************************************************************/

#include "../Audacity.h"
#include "ScienFilter.h"
#include "LoadEffects.h"

#include <math.h>
#include <float.h>

#include <wx/setup.h> // for wxUSE_* macros

#include <wx/brush.h>
#include <wx/choice.h>
#include <wx/dcclient.h>
#include <wx/dcmemory.h>
#include <wx/intl.h>
#include <wx/settings.h>
#include <wx/slider.h>
#include <wx/stattext.h>
#include <wx/utils.h>
#include <wx/valgen.h>

#include "../AColor.h"
#include "../AllThemeResources.h"
#include "../PlatformCompatibility.h"
#include "../Prefs.h"
#include "../Project.h"
#include "../Shuttle.h"
#include "../ShuttleGui.h"
#include "../WaveTrack.h"
#include "../widgets/valnum.h"
#include "../widgets/AudacityMessageBox.h"
#include "../widgets/Ruler.h"
#include "../widgets/WindowAccessible.h"

#if !defined(M_PI)
#define PI = 3.1415926535897932384626433832795
#else
#define PI M_PI
#endif
#define square(a) ((a)*(a))

enum
{
   ID_FilterPanel = 10000,
   ID_dBMax,
   ID_dBMin,
   ID_Type,
   ID_SubType,
   ID_Order,
   ID_Ripple,
   ID_Cutoff,
   ID_StopbandRipple
};

enum kTypes
{
   kButterworth,
   kChebyshevTypeI,
   kChebyshevTypeII,
   nTypes
};

static const EnumValueSymbol kTypeStrings[nTypes] =
{
   /*i18n-hint: Butterworth is the name of the person after whom the filter type is named.*/
   { XO("Butterworth") },
   /*i18n-hint: Chebyshev is the name of the person after whom the filter type is named.*/
   { XO("Chebyshev Type I") },
   /*i18n-hint: Chebyshev is the name of the person after whom the filter type is named.*/
   { XO("Chebyshev Type II") }
};

enum kSubTypes
{
   kLowPass  = Biquad::kLowPass,
   kHighPass = Biquad::kHighPass,
   nSubTypes = Biquad::nSubTypes
};

static const EnumValueSymbol kSubTypeStrings[nSubTypes] =
{
   // These are acceptable dual purpose internal/visible names
   { XO("Lowpass") },
   { XO("Highpass") }
};

static_assert(nSubTypes == WXSIZEOF(kSubTypeStrings), "size mismatch");

// Define keys, defaults, minimums, and maximums for the effect parameters
//
//     Name       Type     Key                     Def            Min   Max               Scale
Param( Type,      int,     wxT("FilterType"),       kButterworth,  0,    nTypes - 1,    1  );
Param( Subtype,   int,     wxT("FilterSubtype"),    kLowPass,      0,    nSubTypes - 1, 1  );
Param( Order,     int,     wxT("Order"),            1,             1,    10,               1  );
Param( Cutoff,    float,   wxT("Cutoff"),           1000.0,        1.0,  FLT_MAX,          1  );
Param( Passband,  float,   wxT("PassbandRipple"),   1.0,           0.0,  100.0,            1  );
Param( Stopband,  float,   wxT("StopbandRipple"),   30.0,          0.0,  100.0,            1  );

//----------------------------------------------------------------------------
// EffectScienFilter
//----------------------------------------------------------------------------

const ComponentInterfaceSymbol EffectScienFilter::Symbol
{ XO("Classic Filters") };

#ifdef EXPERIMENTAL_SCIENCE_FILTERS
// true argument means don't automatically enable this effect
namespace{ BuiltinEffectsModule::Registration< EffectScienFilter > reg( true ); }
#endif

BEGIN_EVENT_TABLE(EffectScienFilter, wxEvtHandler)
   EVT_SIZE(EffectScienFilter::OnSize)

   EVT_SLIDER(ID_dBMax, EffectScienFilter::OnSliderDBMAX)
   EVT_SLIDER(ID_dBMin, EffectScienFilter::OnSliderDBMIN)
   EVT_CHOICE(ID_Order, EffectScienFilter::OnOrder)
   EVT_CHOICE(ID_Type, EffectScienFilter::OnFilterType)
   EVT_CHOICE(ID_SubType, EffectScienFilter::OnFilterSubtype)
   EVT_TEXT(ID_Cutoff, EffectScienFilter::OnCutoff)
   EVT_TEXT(ID_Ripple, EffectScienFilter::OnRipple)
   EVT_TEXT(ID_StopbandRipple, EffectScienFilter::OnStopbandRipple)
END_EVENT_TABLE()

EffectScienFilter::EffectScienFilter()
{
   mOrder = DEF_Order;
   mFilterType = DEF_Type;
   mFilterSubtype = DEF_Subtype;
   mCutoff = DEF_Cutoff;
   mRipple = DEF_Passband;
   mStopbandRipple = DEF_Stopband;

   SetLinearEffectFlag(true);

   mOrderIndex = mOrder - 1;

   mdBMin = -30.0;
   mdBMax = 30.0;

   mLoFreq = 20;              // Lowest frequency to display in response graph
   mNyquist = 44100.0 / 2.0;  // only used during initialization, updated when effect is used
}

EffectScienFilter::~EffectScienFilter()
{
}

// ComponentInterface implementation

ComponentInterfaceSymbol EffectScienFilter::GetSymbol()
{
   return Symbol;
}

TranslatableString EffectScienFilter::GetDescription()
{
   /* i18n-hint: "infinite impulse response" */
   return XO("Performs IIR filtering that emulates analog filters");
}

wxString EffectScienFilter::ManualPage()
{
   return wxT("Classic_Filters");
}


// EffectDefinitionInterface implementation

EffectType EffectScienFilter::GetType()
{
   return EffectTypeProcess;
}

// EffectClientInterface implementation

unsigned EffectScienFilter::GetAudioInCount()
{
   return 1;
}

unsigned EffectScienFilter::GetAudioOutCount()
{
   return 1;
}

bool EffectScienFilter::ProcessInitialize(sampleCount WXUNUSED(totalLen), ChannelNames WXUNUSED(chanMap))
{
   for (int iPair = 0; iPair < (mOrder + 1) / 2; iPair++)
      mpBiquad[iPair].Reset();

   return true;
}

size_t EffectScienFilter::ProcessBlock(float **inBlock, float **outBlock, size_t blockLen)
{
   float *ibuf = inBlock[0];
   for (int iPair = 0; iPair < (mOrder + 1) / 2; iPair++)
   {
      mpBiquad[iPair].Process(ibuf, outBlock[0], blockLen);
      ibuf = outBlock[0];
   }

   return blockLen;
}
bool EffectScienFilter::DefineParams( ShuttleParams & S ){
   S.SHUTTLE_ENUM_PARAM( mFilterType, Type, kTypeStrings, nTypes );
   S.SHUTTLE_ENUM_PARAM( mFilterSubtype, Subtype, kSubTypeStrings, nSubTypes );
   S.SHUTTLE_PARAM( mOrder, Order );
   S.SHUTTLE_PARAM( mCutoff, Cutoff );
   S.SHUTTLE_PARAM( mRipple, Passband );
   S.SHUTTLE_PARAM( mStopbandRipple, Stopband );
   return true;
}

bool EffectScienFilter::GetAutomationParameters(CommandParameters & parms)
{
   parms.Write(KEY_Type, kTypeStrings[mFilterType].Internal());
   parms.Write(KEY_Subtype, kSubTypeStrings[mFilterSubtype].Internal());
   parms.Write(KEY_Order, mOrder);
   parms.WriteFloat(KEY_Cutoff, mCutoff);
   parms.WriteFloat(KEY_Passband, mRipple);
   parms.WriteFloat(KEY_Stopband, mStopbandRipple);

   return true;
}

bool EffectScienFilter::SetAutomationParameters(CommandParameters & parms)
{
   ReadAndVerifyEnum(Type, kTypeStrings, nTypes);
   ReadAndVerifyEnum(Subtype, kSubTypeStrings, nSubTypes);
   ReadAndVerifyInt(Order);
   ReadAndVerifyFloat(Cutoff);
   ReadAndVerifyFloat(Passband);
   ReadAndVerifyFloat(Stopband);

   mFilterType = Type;
   mFilterSubtype = Subtype;
   mOrder = Order;
   mCutoff = Cutoff;
   mRipple = Passband;
   mStopbandRipple = Stopband;

   mOrderIndex = mOrder - 1;

   CalcFilter();

   return true;
}

// Effect implementation

bool EffectScienFilter::Startup()
{
   wxString base = wxT("/SciFilter/");

   // Migrate settings from 2.1.0 or before

   // Already migrated, so bail
   if (gPrefs->Exists(base + wxT("Migrated")))
   {
      return true;
   }

   // Load the old "current" settings
   if (gPrefs->Exists(base))
   {
	   double dTemp;
      gPrefs->Read(base + wxT("Order"), &mOrder, 1);
      mOrder = wxMax (1, mOrder);
      mOrder = wxMin (MAX_Order, mOrder);
      gPrefs->Read(base + wxT("FilterType"), &mFilterType, 0);
      mFilterType = wxMax (0, mFilterType);
      mFilterType = wxMin (2, mFilterType);
      gPrefs->Read(base + wxT("FilterSubtype"), &mFilterSubtype, 0);
      mFilterSubtype = wxMax (0, mFilterSubtype);
      mFilterSubtype = wxMin (1, mFilterSubtype);
      gPrefs->Read(base + wxT("Cutoff"), &dTemp, 1000.0);
      mCutoff = (float)dTemp;
      mCutoff = wxMax (1, mCutoff);
      mCutoff = wxMin (100000, mCutoff);
      gPrefs->Read(base + wxT("Ripple"), &dTemp, 1.0);
      mRipple = dTemp;
      mRipple = wxMax (0, mRipple);
      mRipple = wxMin (100, mRipple);
      gPrefs->Read(base + wxT("StopbandRipple"), &dTemp, 30.0);
      mStopbandRipple = dTemp;
      mStopbandRipple = wxMax (0, mStopbandRipple);
      mStopbandRipple = wxMin (100, mStopbandRipple);

      SaveUserPreset(GetCurrentSettingsGroup());

      // Do not migrate again
      gPrefs->Write(base + wxT("Migrated"), true);
      gPrefs->Flush();
   }

   return true;
}

bool EffectScienFilter::Init()
{
   int selcount = 0;
   double rate = 0.0;

   auto trackRange = inputTracks()->Selected< const WaveTrack >();

   {
      auto t = *trackRange.begin();
      mNyquist =
         (t
            ? t->GetRate()
            : mProjectRate)
         / 2.0;
   }

   for (auto t : trackRange)
   {
      if (selcount == 0)
      {
         rate = t->GetRate();
      }
      else
      {
         if (t->GetRate() != rate)
         {
            Effect::MessageBox(
               XO(
"To apply a filter, all selected tracks must have the same sample rate.") );
            return false;
         }
      }
      selcount++;
   }

   return true;
}

void EffectScienFilter::PopulateOrExchange(ShuttleGui & S)
{
   S.AddSpace(5);
   S.SetSizerProportion(1);
   S.StartMultiColumn(3, wxEXPAND);
   {
      S.SetStretchyCol(1);
      S.SetStretchyRow(0);

      // -------------------------------------------------------------------
      // ROW 1: Freq response panel and sliders for vertical scale
      // -------------------------------------------------------------------

      S.StartVerticalLay();
      {
         mdBRuler = safenew RulerPanel(
            S.GetParent(), wxID_ANY, wxVERTICAL,
            wxSize{ 100, 100 }, // Ruler can't handle small sizes
            RulerPanel::Range{ 30.0, -120.0 },
            Ruler::LinearDBFormat,
            XO("dB"),
            RulerPanel::Options{}
               .LabelEdges(true)
         );

         S.SetBorder(1);
         S.AddSpace(1, 1);
         S.Prop(1)
            .Position(wxALIGN_RIGHT | wxTOP)
            .AddWindow(mdBRuler);
         S.AddSpace(1, 1);
      }
      S.EndVerticalLay();

      mPanel = safenew EffectScienFilterPanel(
         S.GetParent(), wxID_ANY,
         this, mLoFreq, mNyquist
      );

      S.SetBorder(5);
      S.Prop(1)
         .Position(wxEXPAND | wxRIGHT)
         .MinSize( { -1, -1 } )
         .AddWindow(mPanel);

      S.StartVerticalLay();
      {
         S.AddVariableText(XO("+ dB"), false, wxCENTER);
         mdBMaxSlider = S.Id(ID_dBMax)
            .Name(XO("Max dB"))
            .Style(wxSL_VERTICAL | wxSL_INVERSE)
            .AddSlider( {}, 10, 20, 0);
#if wxUSE_ACCESSIBILITY
         mdBMaxSlider->SetAccessible(safenew SliderAx(mdBMaxSlider, XO("%d dB")));
#endif
         mdBMinSlider = S.Id(ID_dBMin)
            .Name(XO("Min dB"))
            .Style(wxSL_VERTICAL | wxSL_INVERSE)
            .AddSlider( {}, -10, -10, -120);
#if wxUSE_ACCESSIBILITY
         mdBMinSlider->SetAccessible(safenew SliderAx(mdBMinSlider, XO("%d dB")));
#endif

         S.AddVariableText(XO("- dB"), false, wxCENTER);
      }
      S.EndVerticalLay();

      // -------------------------------------------------------------------
      // ROW 2: Frequency ruler
      // -------------------------------------------------------------------

      S.AddSpace(1, 1);

      mfreqRuler = safenew RulerPanel(
         S.GetParent(), wxID_ANY, wxHORIZONTAL,
         wxSize{ 100, 100 }, // Ruler can't handle small sizes
         RulerPanel::Range{ mLoFreq, mNyquist },
         Ruler::IntFormat,
         {},
         RulerPanel::Options{}
            .Log(true)
            .Flip(true)
            .LabelEdges(true)
      );

      S.Prop(1)
         .Position(wxEXPAND | wxALIGN_LEFT | wxRIGHT)
         .AddWindow(mfreqRuler);

      S.AddSpace(1, 1);

      // -------------------------------------------------------------------
      // ROW 3 and 4: Type, Order, Ripple, Subtype, Cutoff
      // -------------------------------------------------------------------

      S.AddSpace(1, 1);
      S.SetSizerProportion(0);
      S.StartMultiColumn(8, wxALIGN_CENTER);
      {
         wxASSERT(nTypes == WXSIZEOF(kTypeStrings));

         mFilterTypeCtl = S.Id(ID_Type)
            .Focus()
            .Validator<wxGenericValidator>(&mFilterType)
            .MinSize( { -1, -1 } )
            .AddChoice(XO("&Filter Type:"),
               Msgids(kTypeStrings, nTypes)
            );

         mFilterOrderCtl = S.Id(ID_Order)
            .Validator<wxGenericValidator>(&mOrderIndex)
            .MinSize( { -1, -1 } )
            /*i18n-hint: 'Order' means the complexity of the filter, and is a number between 1 and 10.*/
            .AddChoice(XO("O&rder:"),
               []{
                  TranslatableStrings orders;
                  for (int i = 1; i <= 10; i++)
                     orders.emplace_back( Verbatim("%d").Format( i ) );
                  return orders;
               }()
            );
         S.AddSpace(1, 1);

         mRippleCtlP = S.AddVariableText( XO("&Passband Ripple:"),
            false, wxALL | wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL);
         mRippleCtl = S.Id(ID_Ripple)
            .Name(XO("Passband Ripple (dB)"))
            .Validator<FloatingPointValidator<float>>(
               1, &mRipple, NumValidatorStyle::DEFAULT,
               MIN_Passband, MAX_Passband)
            .AddTextBox( {}, wxT(""), 10);
         mRippleCtlU = S.AddVariableText(XO("dB"),
            false, wxALL | wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);

         mFilterSubTypeCtl = S.Id(ID_SubType)
            .Validator<wxGenericValidator>(&mFilterSubtype)
            .MinSize( { -1, -1 } )
            .AddChoice(XO("&Subtype:"),
               Msgids(kSubTypeStrings, nSubTypes)
            );

         mCutoffCtl = S.Id(ID_Cutoff)
            .Name(XO("Cutoff (Hz)"))
            .Validator<FloatingPointValidator<float>>(
               1, &mCutoff, NumValidatorStyle::DEFAULT,
               MIN_Cutoff, mNyquist - 1)
            .AddTextBox(XO("C&utoff:"), wxT(""), 10);
         S.AddUnits(XO("Hz"));

         mStopbandRippleCtlP =
            S.AddVariableText(XO("Minimum S&topband Attenuation:"),
            false, wxALL | wxALIGN_RIGHT | wxALIGN_CENTER_VERTICAL);
         mStopbandRippleCtl = S.Id(ID_StopbandRipple)
            .Name(XO("Minimum S&topband Attenuation (dB)"))
            .Validator<FloatingPointValidator<float>>(
               1, &mStopbandRipple, NumValidatorStyle::DEFAULT,
               MIN_Stopband, MAX_Stopband)
            .AddTextBox( {}, wxT(""), 10);
         mStopbandRippleCtlU =
            S.AddVariableText(XO("dB"),
            false, wxALL | wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL);
      }
      S.EndMultiColumn();
      S.AddSpace(1, 1);
   }
   S.EndMultiColumn();

   return;
}

//
// Populate the window with relevant variables
//
bool EffectScienFilter::TransferDataToWindow()
{
   mOrderIndex = mOrder - 1;

   if (!mUIParent->TransferDataToWindow())
   {
      return false;
   }

   mdBMinSlider->SetValue((int) mdBMin);
   mdBMin = 0.0;                     // force refresh in TransferGraphLimitsFromWindow()

   mdBMaxSlider->SetValue((int) mdBMax);
   mdBMax = 0.0;                    // force refresh in TransferGraphLimitsFromWindow()

   EnableDisableRippleCtl(mFilterType);

   return TransferGraphLimitsFromWindow();
}

bool EffectScienFilter::TransferDataFromWindow()
{
   if (!mUIParent->Validate() || !mUIParent->TransferDataFromWindow())
   {
      return false;
   }

   mOrder = mOrderIndex + 1;

   CalcFilter();

   return true;
}

// EffectScienFilter implementation

//
// Retrieve data from the window
//
bool EffectScienFilter::TransferGraphLimitsFromWindow()
{
   // Read the sliders and send to the panel
   wxString tip;

   bool rr = false;
   int dB = mdBMinSlider->GetValue();
   if (dB != mdBMin) {
      rr = true;
      mdBMin = dB;
      tip.Printf(_("%d dB"), (int)mdBMin);
      mdBMinSlider->SetToolTip(tip);
   }

   dB = mdBMaxSlider->GetValue();
   if (dB != mdBMax) {
      rr = true;
      mdBMax = dB;
      tip.Printf(_("%d dB"),(int)mdBMax);
      mdBMaxSlider->SetToolTip(tip);
   }

   if (rr) {
      mPanel->SetDbRange(mdBMin, mdBMax);
   }

   // Refresh ruler if values have changed
   if (rr) {
      int w1, w2, h;
      mdBRuler->ruler.GetMaxSize(&w1, &h);
      mdBRuler->ruler.SetRange(mdBMax, mdBMin);
      mdBRuler->ruler.GetMaxSize(&w2, &h);
      if( w1 != w2 )   // Reduces flicker
      {
         mdBRuler->SetSize(wxSize(w2,h));
         mUIParent->Layout();
         mfreqRuler->Refresh(false);
      }
      mdBRuler->Refresh(false);
   }

   mPanel->Refresh(false);

   return true;
}

void EffectScienFilter::CalcFilter()
{
   switch (mFilterType)
   {
   case kButterworth:
      mpBiquad = Biquad::CalcButterworthFilter(mOrder, mNyquist, mCutoff, mFilterSubtype);
      break;
   case kChebyshevTypeI:
      mpBiquad = Biquad::CalcChebyshevType1Filter(mOrder, mNyquist, mCutoff, mRipple, mFilterSubtype);
      break;
   case kChebyshevTypeII:
      mpBiquad = Biquad::CalcChebyshevType2Filter(mOrder, mNyquist, mCutoff, mStopbandRipple, mFilterSubtype);
      break;
   }
}

float EffectScienFilter::FilterMagnAtFreq(float Freq)
{
   float Magn;
   if (Freq >= mNyquist)
      Freq = mNyquist - 1;	// prevent tan(PI/2)
   float FreqWarped = tan (PI * Freq/(2*mNyquist));
   if (mCutoff >= mNyquist)
      mCutoff = mNyquist - 1;
   float CutoffWarped = tan (PI * mCutoff/(2*mNyquist));
   float fOverflowThresh = pow (10.0, 12.0 / (2*mOrder));    // once we exceed 10^12 there's not much to be gained and overflow could happen

   switch (mFilterType)
   {
   case kButterworth:		// Butterworth
   default:
      switch (mFilterSubtype)
      {
      case kLowPass:	// lowpass
      default:
         if (FreqWarped/CutoffWarped > fOverflowThresh)	// prevent pow() overflow
            Magn = 0;
         else
            Magn = sqrt (1 / (1 + pow (FreqWarped/CutoffWarped, 2*mOrder)));
         break;
      case kHighPass:	// highpass
         if (FreqWarped/CutoffWarped > fOverflowThresh)
            Magn = 1;
         else
            Magn = sqrt (pow (FreqWarped/CutoffWarped, 2*mOrder) / (1 + pow (FreqWarped/CutoffWarped, 2*mOrder)));
         break;
      }
      break;

   case kChebyshevTypeI:     // Chebyshev Type 1
      double eps; eps = sqrt(pow (10.0, wxMax(0.001, mRipple)/10.0) - 1);
      double chebyPolyVal;
      switch (mFilterSubtype)
      {
      case 0:	// lowpass
      default:
         chebyPolyVal = Biquad::ChebyPoly(mOrder, FreqWarped/CutoffWarped);
         Magn = sqrt (1 / (1 + square(eps) * square(chebyPolyVal)));
         break;
      case 1:
         chebyPolyVal = Biquad::ChebyPoly(mOrder, CutoffWarped/FreqWarped);
         Magn = sqrt (1 / (1 + square(eps) * square(chebyPolyVal)));
         break;
      }
      break;

   case kChebyshevTypeII:     // Chebyshev Type 2
      eps = 1 / sqrt(pow (10.0, wxMax(0.001, mStopbandRipple)/10.0) - 1);
      switch (mFilterSubtype)
      {
      case kLowPass:	// lowpass
      default:
         chebyPolyVal = Biquad::ChebyPoly(mOrder, CutoffWarped/FreqWarped);
         Magn = sqrt (1 / (1 + 1 / (square(eps) * square(chebyPolyVal))));
         break;
      case kHighPass:
         chebyPolyVal = Biquad::ChebyPoly(mOrder, FreqWarped/CutoffWarped);
         Magn = sqrt (1 / (1 + 1 / (square(eps) * square(chebyPolyVal))));
         break;
      }
      break;
   }

   return Magn;
}

void EffectScienFilter::OnOrder(wxCommandEvent & WXUNUSED(evt))
{
   mOrderIndex = mFilterOrderCtl->GetSelection();
   mOrder = mOrderIndex + 1;	// 0..n-1 -> 1..n
   mPanel->Refresh(false);
}

void EffectScienFilter::OnFilterType(wxCommandEvent & WXUNUSED(evt))
{
   mFilterType = mFilterTypeCtl->GetSelection();
   EnableDisableRippleCtl(mFilterType);
   mPanel->Refresh(false);
}

void EffectScienFilter::OnFilterSubtype(wxCommandEvent & WXUNUSED(evt))
{
   mFilterSubtype = mFilterSubTypeCtl->GetSelection();
   mPanel->Refresh(false);
}

void EffectScienFilter::OnCutoff(wxCommandEvent & WXUNUSED(evt))
{
   if (!EnableApply(mUIParent->TransferDataFromWindow()))
   {
      return;
   }

   mPanel->Refresh(false);
}

void EffectScienFilter::OnRipple(wxCommandEvent & WXUNUSED(evt))
{
   if (!EnableApply(mUIParent->TransferDataFromWindow()))
   {
      return;
   }

   mPanel->Refresh(false);
}

void EffectScienFilter::OnStopbandRipple(wxCommandEvent & WXUNUSED(evt))
{
   if (!EnableApply(mUIParent->TransferDataFromWindow()))
   {
      return;
   }

   mPanel->Refresh(false);
}

void EffectScienFilter::OnSliderDBMIN(wxCommandEvent & WXUNUSED(evt))
{
   TransferGraphLimitsFromWindow();
}

void EffectScienFilter::OnSliderDBMAX(wxCommandEvent & WXUNUSED(evt))
{
   TransferGraphLimitsFromWindow();
}

void EffectScienFilter::OnSize(wxSizeEvent & evt)
{
   // On Windows the Passband and Stopband boxes do not refresh properly
   // on a resize...no idea why.
   mUIParent->Refresh();
   evt.Skip();
}

void EffectScienFilter::EnableDisableRippleCtl(int FilterType)
{
   bool ripple;
   bool stop;

   if (FilterType == kButterworth)    // Butterworth
   {
      ripple = false;
      stop = false;
   }
   else if (FilterType == kChebyshevTypeI)    // Chebyshev Type1
   {
      ripple = true;
      stop = false;
   }
   else                        // Chebyshev Type2
   {
      ripple = false;
      stop = true;
   }

   mRippleCtlP->Enable(ripple);
   mRippleCtl->Enable(ripple);
   mRippleCtlU->Enable(ripple);
   mStopbandRippleCtlP->Enable(stop);
   mStopbandRippleCtl->Enable(stop);
   mStopbandRippleCtlU->Enable(stop);
}

//----------------------------------------------------------------------------
// EffectScienFilterPanel
//----------------------------------------------------------------------------

BEGIN_EVENT_TABLE(EffectScienFilterPanel, wxPanelWrapper)
    EVT_PAINT(EffectScienFilterPanel::OnPaint)
    EVT_SIZE(EffectScienFilterPanel::OnSize)
END_EVENT_TABLE()

EffectScienFilterPanel::EffectScienFilterPanel(
   wxWindow *parent, wxWindowID winid,
   EffectScienFilter *effect, double lo, double hi)
:  wxPanelWrapper(parent, winid, wxDefaultPosition, wxSize(400, 200))
{
   mEffect = effect;
   mParent = parent;

   mBitmap = NULL;
   mWidth = 0;
   mHeight = 0;
   mLoFreq = 0.0;
   mHiFreq = 0.0;
   mDbMin = 0.0;
   mDbMax = 0.0;

   SetFreqRange(lo, hi);
}

EffectScienFilterPanel::~EffectScienFilterPanel()
{
}

void EffectScienFilterPanel::SetFreqRange(double lo, double hi)
{
   mLoFreq = lo;
   mHiFreq = hi;
   Refresh(false);
}

void EffectScienFilterPanel::SetDbRange(double min, double max)
{
   mDbMin = min;
   mDbMax = max;
   Refresh(false);
}

bool EffectScienFilterPanel::AcceptsFocus() const
{
   return false;
}

bool EffectScienFilterPanel::AcceptsFocusFromKeyboard() const
{
   return false;
}

void EffectScienFilterPanel::OnSize(wxSizeEvent & WXUNUSED(evt))
{
   Refresh(false);
}

void EffectScienFilterPanel::OnPaint(wxPaintEvent & WXUNUSED(evt))
{
   wxPaintDC dc(this);
   int width, height;
   GetSize(&width, &height);

   if (!mBitmap || mWidth != width || mHeight != height)
   {
      mWidth = width;
      mHeight = height;
      mBitmap = std::make_unique<wxBitmap>(mWidth, mHeight,24);
   }

   wxBrush bkgndBrush(wxSystemSettings::GetColour(wxSYS_COLOUR_3DFACE));

   wxMemoryDC memDC;
   memDC.SelectObject(*mBitmap);

   wxRect bkgndRect;
   bkgndRect.x = 0;
   bkgndRect.y = 0;
   bkgndRect.width = mWidth;
   bkgndRect.height = mHeight;
   memDC.SetBrush(bkgndBrush);
   memDC.SetPen(*wxTRANSPARENT_PEN);
   memDC.DrawRectangle(bkgndRect);

   bkgndRect.y = mHeight;
   memDC.DrawRectangle(bkgndRect);

   wxRect border;
   border.x = 0;
   border.y = 0;
   border.width = mWidth;
   border.height = mHeight;

   memDC.SetBrush(*wxWHITE_BRUSH);
   memDC.SetPen(*wxBLACK_PEN);
   memDC.DrawRectangle(border);

   mEnvRect = border;
   mEnvRect.Deflate(2, 2);

   // Pure blue x-axis line
   memDC.SetPen(wxPen(theTheme.Colour(clrGraphLines), 1, wxPENSTYLE_SOLID));
   int center = (int) (mEnvRect.height * mDbMax / (mDbMax - mDbMin) + 0.5);
   AColor::Line(memDC,
                mEnvRect.GetLeft(), mEnvRect.y + center,
                mEnvRect.GetRight(), mEnvRect.y + center);

   //Now draw the actual response that you will get.
   //mFilterFunc has a linear scale, window has a log one so we have to fiddle about
   memDC.SetPen(wxPen(theTheme.Colour(clrResponseLines), 3, wxPENSTYLE_SOLID));
   double scale = (double) mEnvRect.height / (mDbMax - mDbMin);    // pixels per dB
   double yF;                                                     // gain at this freq

   double loLog = log10(mLoFreq);
   double step = log10(mHiFreq) - loLog;
   step /= ((double) mEnvRect.width - 1.0);
   double freq;                                    // actual freq corresponding to x position
   int x, y, xlast = 0, ylast = 0;
   for (int i = 0; i < mEnvRect.width; i++)
   {
      x = mEnvRect.x + i;
      freq = pow(10.0, loLog + i * step);          //Hz
      yF = mEffect->FilterMagnAtFreq (freq);
      yF = LINEAR_TO_DB(yF);

      if (yF < mDbMin)
      {
         yF = mDbMin;
      }

      yF = center-scale * yF;
      if (yF > mEnvRect.height)
      {
         yF = (double) mEnvRect.height - 1.0;
      }
      if (yF < 0.0)
      {
         yF = 0.0;
      }
      y = (int) (yF + 0.5);

      if (i != 0 && (y < mEnvRect.height - 1 || ylast < mEnvRect.y + mEnvRect.height - 1))
      {
         AColor::Line(memDC, xlast, ylast, x, mEnvRect.y + y);
      }
      xlast = x;
      ylast = mEnvRect.y + y;
   }

   memDC.SetPen(*wxBLACK_PEN);
   mEffect->mfreqRuler->ruler.DrawGrid(memDC, mEnvRect.height + 2, true, true, 0, 1);
   mEffect->mdBRuler->ruler.DrawGrid(memDC, mEnvRect.width + 2, true, true, 1, 2);

   dc.Blit(0, 0, mWidth, mHeight, &memDC, 0, 0, wxCOPY, FALSE);

   memDC.SelectObject(wxNullBitmap);
}
