/*
 * This program source code file is part of KiCad, a free EDA CAD application.
 *
 * Copyright (C) 2016 CERN
 * Copyright (C) 2021 KiCad Developers, see AUTHORS.txt for contributors.
 *
 * @author Maciej Suminski <maciej.suminski@cern.ch>
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 3
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, you may find one here:
 * https://www.gnu.org/licenses/gpl-3.0.html
 * or you may search the http://www.gnu.org website for the version 3 license,
 * or you may write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
 */

#include "tuner_slider.h"

#include <sim/sim_plot_frame.h>
#include <sch_symbol.h>
#include <template_fieldnames.h>
#include <sim/ngspice_helpers.h>

#include <cmath>   // log log1p expm1
#include <complex> // norm


// characteristic curves (default: 0B Lin)
static const struct { double m_law, m_mid; } CURVES[] =
{
    // same order as choices in m_curve
    { 1.00, 0.10 }, // 10A Log
    { 1.00, 0.15 }, // 15A Log
    { 0.90, 0.13 }, // 15A Log S
    { 0.00, 0.10 }, // 10C Rev Log
    { 0.00, 0.15 }, // 15C Rev Log
    { 0.10, 0.13 }, // 15C Rev Log S
    { 0.50, 0.50 }, // 0B Lin
    { 0.50, 0.15 }, // 4B S-Curve
    { 0.50, 0.10 }, // 5B S-Curve
    { 0.50, 0.00 }  // Switch
};

template <typename T, int S>
static inline int arraysize( const T ( &v )[S] ) { return S; }


/**
 * Transform ratio according to linear, logarithmic or reverse logarithmic laws
 * (characteristic curve or taper) of rotary or slide potentiometer (pot or fader).
 *
 * Parameters corresponding to *IEC 60393-1:2008* and *JIS C 5260-1* code letters:
 *
 * | @p aLaw | @p aMid | Code | Resistance Law (Taper)
 * | ------: | ------: | ---: | :---------------------
 * |    1.00 |    0.10 |  10A | CW logarithmic
 * |     ^   |    0.15 |  15A | CW logarithmic (audio)
 * |    0.90 |    0.13 |   ^  | CW logarithmic (high-end audio)
 * |    0.00 |    0.10 |  10C | CCW/reverse logarithmic
 * |     ^   |    0.15 |  15C | CCW/reverse logarithmic (reverse audio)
 * |    0.10 |    0.13 |   ^  | CCW/reverse logarithmic (high-end reverse audio)
 * |    0.50 |    0.50 |   0B | (ideal) linear
 * |     ^   |    0.15 |   4B | symmetric (audio S-curve)
 * |     ^   |    0.10 |   5B | symmetric (S-curve)
 * |     ^   |    0.00 |   -  | switch
 *
 * Standards code letters cross-reference:
 *
 * |  IEC 60393-1:2008 | IEC 60393-1:1989 | MIL-R-94 | Resistance Law
 * | ----------------: | :--------------: | :------: | :-------------
 * |                0B |         A        |     A    | linear
 * |               10A |         B        |     C    | logarithmic
 * |               15A |         ^        |     -    | ^
 * |               10C |         C        |     F    | reverse logarithmic
 * |               15C |         ^        |     -    | ^
 *
 * **Logarithmic Law** is for *levels* (logarithmic units) and is actually an exponential curve.
 * **Reverse** refers to a reverse-mounted resistive element or shaft on a potentiometer
 * (resulting in a reflected curve). An **S-curve** is a curve joined to its (scaled) reflection,
 * and *may* be symmetric or linear. **Inverse** refers to the mathematical inverse of a function.
 *
 * @tparam F is a floating point type.
 * @param  aRatio    is the input (travel) ratio or moving contact (wiper) position, from
 *                   0%/CCW/left (0) through 50%/mid-travel/center (1/2) to 100%/CW/right (1).
 * @param  aMid      is the logarithmic laws' output ratio at 50%/mid-travel/center (1/2)
 *                   input ratio.
 * @param  aLaw      is the (resistance) law, interpolating from *reverse logarithmic* (0)
 *                   through *symmetric/linear* (1/2) to *logarithmic* (1).
 * @param  aInverse  swaps input and output ratios (inverse function, where possible),
 *                   if @c true.
 * @return the output (resistance or voltage) ratio in [0, 1].
 */
template <typename F>
static F taper( F aRatio, F aMid = 0.5, F aLaw = 1.0, bool aInverse = false )
{
    static_assert( std::is_floating_point<F>::value, "F must be floating point" );

    // clamp to [0, 1] and short-cut
    if( aRatio <= 0 )
        return 0;
    if( aRatio >= 1 )
        return 1;

    // short-cut for ideal linear or at S-curve inflection point
    if( aMid == 0.5 || aRatio == aLaw )
        return aRatio;

    F t = aRatio;

    // clamp to [0, 1] and short-cut at (non-invertible) limits
    if( aMid <= 0 )
    {
        t = aInverse ? 1 : 0;
    }
    else if( aMid >= 1 )
    {
        t = aInverse ? 0 : 1;
    }
    else
    {
        // clamp, and reflect and/or scale for reverse...symmetric...normal laws
        if( aLaw >= 1 )
        {
            // Do nothing, leave t = t
        }
        else if( aLaw <= 0 )
        {
            t = 1 - t;
        }
        else if( aRatio <= aLaw )
        {
            t = t / aLaw;
        }
        else
        {
            t = ( 1 - t ) / ( 1 - aLaw );
        }

        // scaling factors for domain and range in [0, 1]
        F a = std::norm( 1 - 1 / aMid );
        F b = std::log( a );
        F c = a - 1;

        // scaling: a = (1 - 1/m)^2
        // log law: (a^t - 1) / (a - 1)
        // inverse: log_a(1 + t (a - 1))
        t = aInverse ? ( std::log1p( t * c ) / b ) : ( std::expm1( t * b ) / c );
    }

    // clamp, and scale and/or reflect for reverse...symmetric...normal laws
    if( aLaw >= 1 )
    {
        // Do nothing, leave t = t
    }
    else if( aLaw <= 0 )
    {
        t = 1 - t;
    }
    else if( aRatio <= aLaw )
    {
        t = t * aLaw;
    }
    else
    {
        t = 1 - t * ( 1-aLaw );
    }

    return t;
}


TUNER_SLIDER::TUNER_SLIDER( SIM_PLOT_FRAME* aFrame, wxWindow* aParent, SCH_SYMBOL* aSymbol ) :
    TUNER_SLIDER_BASE( aParent ),
    m_symbol( aSymbol ),
    m_fieldId( MANDATORY_FIELD_T::VALUE_FIELD ),
    m_min( 0.0 ),
    m_max( 0.0 ),
    m_value( 0.0 ),
    m_changed( false ),
    m_frame ( aFrame )
{
    const wxString compName = aSymbol->GetField( REFERENCE_FIELD )->GetText();
    m_name->SetLabel( compName );
    m_spiceTuningCommand = aFrame->GetExporter()->GetSpiceTuningCommand( compName );

    if( m_spiceTuningCommand.second )
    {
        // model parameter, with fixed %-range and unknown initial value
        m_min   =   0;
        m_max   = 100;
        m_value = ( m_max - m_min ) / 2.0; // midpoint
        m_minText->Disable();
        m_maxText->Disable();
        m_saveBtn->Disable(); // not an instance parameter that could be updated (invalid m_fieldId)
    }
    else
    {
        // instance parameter
        if( aSymbol->FindField( NETLIST_EXPORTER_PSPICE::GetSpiceFieldName( SF_MODEL ) ) )
            m_fieldId = aSymbol->FindField( NETLIST_EXPORTER_PSPICE::GetSpiceFieldName( SF_MODEL ) )->GetId();
        else
            m_fieldId = aSymbol->GetField( VALUE_FIELD )->GetId();

        m_value = SPICE_VALUE( aSymbol->GetFieldById( m_fieldId )->GetText() );
        m_min   = SPICE_VALUE( 0.5 ) * m_value;
        m_max   = SPICE_VALUE( 2.0 ) * m_value;
    }

    // Call Set*() methods to update fields and slider
    m_minText->SetValue( m_min.ToOrigString() );
    m_maxText->SetValue( m_max.ToOrigString() );

    updateValueText();
    updateSlider();

    m_simTimer.SetOwner( this );
    Connect( wxEVT_TIMER, wxTimerEventHandler( TUNER_SLIDER::onSimTimer ), nullptr, this );
}


bool TUNER_SLIDER::SetValue( const SPICE_VALUE& aVal )
{
    // Get the value into the current range boundaries
    if( aVal > m_max )
        m_value = m_max;
    else if( aVal < m_min )
        m_value = m_min;
    else
        m_value = aVal;

    updateValueText();
    updateSlider();
    updateComponentValue();

    return true;
}


bool TUNER_SLIDER::SetMin( const SPICE_VALUE& aVal )
{
    if( aVal >= m_max )
        return false;

    m_min = aVal;

    if( m_value < aVal )      // Limit the current value
        SetValue( aVal );

    m_minText->SetValue( aVal.ToOrigString() );
    updateSlider();

    return true;
}


bool TUNER_SLIDER::SetMax( const SPICE_VALUE& aVal )
{
    if( aVal <= m_min )
        return false;

    m_max = aVal;

    if( m_value > aVal )      // Limit the current value
        SetValue( aVal );

    m_maxText->SetValue( aVal.ToOrigString() );
    updateSlider();

    return true;
}


void TUNER_SLIDER::updateComponentValue()
{
    // Start simulation in 100 ms, if the value does not change meanwhile
    m_simTimer.StartOnce( 100 );
}


void TUNER_SLIDER::updateSlider()
{
    wxASSERT( m_max >= m_value && m_value >= m_min );

    int choice = m_curve->GetSelection();
    wxCHECK( choice >= 0 && choice < arraysize( CURVES ), /*void*/ );

    double ratio  = ( ( m_value - m_min ) / ( m_max - m_min ) ).ToDouble();
    double travel = taper( ratio, CURVES[choice].m_mid, CURVES[choice].m_law, true );
    m_slider->SetValue( KiROUND( travel * 100.0 ) );
}


void TUNER_SLIDER::updateValueText()
{
    bool spiceString = m_min.IsSpiceString() || m_max.IsSpiceString();
    m_valueText->SetValue( spiceString ? m_value.ToSpiceString() : m_value.ToString() );
}


void TUNER_SLIDER::updateMax()
{
    try
    {
        SPICE_VALUE newMax( m_maxText->GetValue() );
        SetMax( newMax );
    }
    catch( const KI_PARAM_ERROR& )
    {
        // Restore the previous value
        m_maxText->SetValue( m_max.ToOrigString() );
    }
}


void TUNER_SLIDER::updateValue()
{
    try
    {
        SPICE_VALUE newCur( m_valueText->GetValue() );
        SetValue( newCur );
        m_changed = true;
    }
    catch( const KI_PARAM_ERROR& )
    {
        // Restore the previous value
        m_valueText->SetValue( m_value.ToOrigString() );
    }
}


void TUNER_SLIDER::updateMin()
{
    try
    {
        SPICE_VALUE newMin( m_minText->GetValue() );
        SetMin( newMin );
    }
    catch( const KI_PARAM_ERROR& )
    {
        // Restore the previous value
        m_minText->SetValue( m_min.ToOrigString() );
    }
}


void TUNER_SLIDER::onClose( wxCommandEvent& event )
{
    m_frame->RemoveTuner( this );
}


void TUNER_SLIDER::onSave( wxCommandEvent& event )
{
    m_frame->UpdateTunerValue( m_symbol, m_fieldId, m_value.ToOrigString() );
}


void TUNER_SLIDER::onSliderChanged( wxScrollEvent& event )
{
    int choice = m_curve->GetSelection();
    wxCHECK( choice >= 0 && choice < arraysize( CURVES ), /*void*/ );

    double travel = m_slider->GetValue() / 100.0;
    double ratio  = taper( travel, CURVES[choice].m_mid, CURVES[choice].m_law, false );
    m_value = m_min + ( m_max - m_min ) * SPICE_VALUE( ratio );
    updateValueText();
    updateComponentValue();
    m_changed = true;
}


void TUNER_SLIDER::onCurveChoice( wxCommandEvent& event )
{
    updateValue();
}


void TUNER_SLIDER::onMaxKillFocus( wxFocusEvent& event )
{
    updateMax();
    event.Skip();       // Mandatory in wxFocusEvent
}


void TUNER_SLIDER::onValueKillFocus( wxFocusEvent& event )
{
    updateValue();
    event.Skip();       // Mandatory in wxFocusEvent
}


void TUNER_SLIDER::onMinKillFocus( wxFocusEvent& event )
{
    updateMin();
    event.Skip();       // Mandatory in wxFocusEvent
}


void TUNER_SLIDER::onMaxTextEnter( wxCommandEvent& event )
{
    updateMax();
    event.Skip();       // Mandatory in wxFocusEvent
}


void TUNER_SLIDER::onValueTextEnter( wxCommandEvent& event )
{
    updateValue();
}


void TUNER_SLIDER::onMinTextEnter( wxCommandEvent& event )
{
    updateMin();
}


void TUNER_SLIDER::onSimTimer( wxTimerEvent& event )
{
    if( m_changed )
    {
        wxQueueEvent( m_frame, new wxCommandEvent( EVT_SIM_UPDATE ) );
        m_changed = false;
    }
}
