/*
 ==============================================================================
 This file is part of the IEM plug-in suite.
 Author: Daniel Rudrich, Sebastian Grill
 Copyright (c) 2017 - Institute of Electronic Music and Acoustics (IEM)
 https://iem.at

 The IEM plug-in suite 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.

 The IEM plug-in suite 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 software.  If not, see <https://www.gnu.org/licenses/>.
 ==============================================================================
 */

#pragma once

template <typename coefficientsType>
class T60Visualizer : public juce::Component
{
    struct Settings
    {
        float fMin = 20.0f; // minimum displayed frequency
        float fMax = 20000.0f; // maximum displayed frequency
        float yMin = 0.1f; // min displayed seconds
        float yMax = 60.0f; // max displayed seconds
        float gridDiv = 5.0f; // how many seconds per divisions (between grid lines)
    };

    const float mL = 23.0f;
    const float mR = 10.0f;
    const float mT = 7.0f;
    const float mB = 15.0f;
    const float OH = 3.0f;

public:
    T60Visualizer() : juce::Component(), overallGainInDb (0.0f), sampleRate (48000) {};
    T60Visualizer (float fMin, float fMax, float yMin, float yMax, float gridDiv) :
        juce::Component(),
        overallGainInDb (0.0f),
        sampleRate (48000),
        s { fMin, fMax, yMin, yMax, gridDiv } {};
    ~T60Visualizer() {};

    void paint (juce::Graphics& g) override
    {
        g.setColour (juce::Colours::steelblue.withMultipliedAlpha (0.01f));
        g.fillAll();

        //int width = getWidth();

        g.setFont (
            juce::FontOptions (getLookAndFeel().getTypefaceForFont (juce::FontOptions (12.0f, 2))));
        g.setFont (12.0f);

        // time labels
        g.setColour (juce::Colours::white);

        juce::String axislabel = juce::String (0.1f);
        g.drawText (axislabel, 0, t60ToY (0.1f) - 6, 18, 12.0f, juce::Justification::right, false);

        axislabel = juce::String (1.f);
        g.drawText (axislabel, 0, t60ToY (1.f) - 6, 18, 12.0f, juce::Justification::right, false);

        for (float t = s.yMin; t <= s.yMax; t += powf (10, floorf (log10 (t))))
        {
            int ypos = t60ToY (t);

            juce::String axislabel;
            bool drawText = false;

            if ((t == 1) || (t == 10) || (t == 60))
            {
                axislabel = juce::String ((int) t);
                drawText = true;
            }
            if (drawText)
            {
                g.drawText (axislabel, 0, ypos - 6, 18, 12.0f, juce::Justification::right, false);
            }
        }

        // frequency labels
        for (float f = s.fMin; f <= s.fMax; f += powf (10, floorf (log10 (f))))
        {
            int xpos = hzToX (f);

            juce::String axislabel;
            bool drawText = false;

            if ((f == 20) || (f == 50) || (f == 100) || (f == 500))
            {
                axislabel = juce::String ((int) f);
                drawText = true;
            }
            else if ((f == 1000) || (f == 5000) || (f == 10000) || (f == 20000))
            {
                axislabel = juce::String ((int) f / 1000);
                axislabel << "k";
                drawText = true;
            }

            if (drawText)
            {
                g.drawText (axislabel,
                            xpos - 10,
                            t60ToY (s.yMin) + OH + 0.0f,
                            20,
                            12,
                            juce::Justification::centred,
                            false);
            }
        }

        g.setColour (juce::Colours::steelblue.withMultipliedAlpha (0.8f));
        g.strokePath (dbGridPath, juce::PathStrokeType (0.5f));

        g.setColour (juce::Colours::steelblue.withMultipliedAlpha (0.9f));
        g.strokePath (dbGridPathBold, juce::PathStrokeType (1.0f));

        g.setColour (juce::Colours::steelblue.withMultipliedAlpha (0.9f));
        g.strokePath (hzGridPathBold, juce::PathStrokeType (1.0f));

        g.setColour (juce::Colours::steelblue.withMultipliedAlpha (0.8f));
        g.strokePath (hzGridPath, juce::PathStrokeType (0.5f));

        // draw filter magnitude responses
        juce::Path magnitude;
        allMagnitudesInDb.fill (overallGainInDb);

        int xMin = hzToX (s.fMin);
        int xMax = hzToX (s.fMax);
        int yMax = t60ToY (s.yMin);
        int yMin = t60ToY (s.yMax);

        g.excludeClipRegion (
            juce::Rectangle<int> (0.0f, yMax + OH, getWidth(), getHeight() - yMax - OH));

        for (int i = arrayOfCoefficients.size(); --i >= 0;)
        {
            //bool isActive = activeElem == i;

            typename juce::dsp::IIR::Coefficients<coefficientsType>::Ptr handle =
                (typename juce::dsp::IIR::Coefficients<coefficientsType>::Ptr)
                    arrayOfCoefficients.getUnchecked (i);
            magnitude.clear();

            float db = juce::Decibels::gainToDecibels (
                handle->getMagnitudeForFrequency (xToHz (xMin), sampleRate));
            allMagnitudesInDb.setUnchecked (0, allMagnitudesInDb[0] + db);

            for (int x = xMin + 1; x <= xMax; ++x)
            {
                float db = juce::Decibels::gainToDecibels (
                    handle->getMagnitudeForFrequency (xToHz (x), sampleRate));
                allMagnitudesInDb.setUnchecked (x - xMin, allMagnitudesInDb[x - xMin] + db);
            }
        }

        //all magnitudes combined
        magnitude.clear();

        magnitude.startNewSubPath (
            xMin,
            juce::jlimit ((float) yMin,
                          (float) yMax + OH + 1,
                          t60ToYFloat (gainToT60Float (allMagnitudesInDb[0]))));

        for (int x = xMin + 1; x <= xMax; ++x)
        {
            magnitude.lineTo (
                x,
                juce::jlimit ((float) yMin,
                              (float) yMax + OH + 1,
                              t60ToYFloat (gainToT60Float (allMagnitudesInDb[x - xMin]))));
        }
        g.setColour (juce::Colours::white);
        g.strokePath (magnitude, juce::PathStrokeType (1.5f));

        g.setColour (juce::Colours::white.withMultipliedAlpha (0.3f));
        g.fillPath (tolerancePath,
                    juce::AffineTransform::translation (0.0f,
                                                        t60ToY (gainToT60Float (overallGainInDb))
                                                            - t60ToY (10.0f)));
    };

    void mouseWheelMove (const juce::MouseEvent& e, const juce::MouseWheelDetails& wheel) override
    {
        const double delta =
            100
            * (std::abs (wheel.deltaX) > std::abs (wheel.deltaY) ? -wheel.deltaX : wheel.deltaY);
        //bool positiveDelta = delta >= 0.0;

        float value = s.yMax + juce::roundToInt (delta);
        value = juce::jmin (value, 80.0f);
        value = juce::jmax (value, 5.0f);

        s.yMax = value;
        resized();
        repaint();
    }

    void createTolerancePath()
    {
        const float tRef = 10; //10 is our reference, can be anything
        tolerancePath.clear();

        tolerancePath.startNewSubPath (hzToX (s.fMin),
                                       t60ToYFloat (getToleranceT60 (s.fMin, tRef, true)));
        tolerancePath.lineTo (hzToX (250.0f), t60ToYFloat (1.2f * tRef));
        tolerancePath.lineTo (hzToX (s.fMax), t60ToYFloat (1.2f * tRef));
        tolerancePath.lineTo (hzToX (s.fMax), t60ToYFloat (getToleranceT60 (s.fMax, tRef, false)));
        tolerancePath.lineTo (hzToX (2000.0f), t60ToYFloat (0.8 * tRef));
        tolerancePath.lineTo (hzToX (250.0f), t60ToYFloat (0.8 * tRef));
        tolerancePath.lineTo (hzToX (s.fMin), t60ToYFloat (getToleranceT60 (s.fMin, tRef, false)));
        tolerancePath.closeSubPath();
    }

    float getToleranceT60 (float frequency, float tRef, bool upper)
    {
        if (frequency < 250)
        {
            if (upper)
                return tRef * (frequency * -0.002673797f + 1.868449198f);
            else
                return tRef * (frequency * -0.002139037f + 1.334759358f);
        }
        else if (frequency < 2000)
        {
            if (upper)
                return tRef * 1.2f;
            else
                return tRef * 0.8f;
        }
        else
        {
            if (upper)
                return tRef * 1.2f;
            else
                return tRef * juce::jlimit (0.1f, 1.f, frequency * -0.00005f + 0.9f);
        }
    }

    float gainToT60Float (float db) { return -3.f * 20.f / db; }

    float t60ToGain (float T60) { return -3.f * 20.f / T60; }

    int t60ToY (float db)
    {
        int ypos = t60ToYFloat (db);
        return ypos;
    }

    float t60ToYFloat (float db)
    {
        float height = (float) getHeight() - mB - mT;
        float ypos = mT + height * (1.f - (log (db / s.yMin) / log (s.yMax / s.yMin)));
        return ypos;
    }

    float yToT60 (float y)
    {
        float height = (float) getHeight() - mB - mT;
        float db = s.yMin * powf (s.yMax / s.yMin, 1.f - (y - mT) / height);
        return db;
    }

    int hzToX (float hz)
    {
        float width = (float) getWidth() - mL - mR;
        int xpos = mL + width * (log (hz / s.fMin) / log (s.fMax / s.fMin));
        return xpos;
    }

    float xToHz (int x)
    {
        float width = (float) getWidth() - mL - mR;
        return s.fMin * powf ((s.fMax / s.fMin), ((x - mL) / width));
    }

    void setSampleRate (int newSampleRate)
    {
        jassert (newSampleRate > 0);
        sampleRate = newSampleRate;
    }

    void setOverallGain (float newGain)
    {
        overallGainInDb = juce::Decibels::gainToDecibels (newGain, -500.0f);
        repaint();
    }

    void mouseDrag (const juce::MouseEvent& event) override
    {
        juce::Point<int> pos = event.getPosition();
        float frequency = xToHz (pos.x);
        float gain = yToT60 (pos.y);

        if (activeElem != -1)
        {
            juce::Slider* slHandle;

            slHandle = arrayOfFrequencySliders[activeElem];
            if (slHandle != nullptr)
                slHandle->setValue (frequency);

            slHandle = arrayOfGainSliders[activeElem];
            if (slHandle != nullptr)
                slHandle->setValue (gain);
        }
    }

    void mouseMove (const juce::MouseEvent& event) override
    {
        juce::Point<int> pos = event.getPosition();
        int oldActiveElem = activeElem;
        activeElem = -1;
        for (int i = arrayOfCoefficients.size(); --i >= 0;)
        {
            juce::Point<int> filterPos (arrayOfFrequencySliders[i] == nullptr
                                            ? hzToX (0.0f)
                                            : hzToX (arrayOfFrequencySliders[i]->getValue()),
                                        arrayOfGainSliders[i] == nullptr
                                            ? t60ToY (0.0f)
                                            : t60ToY (arrayOfGainSliders[i]->getValue()));
            if (pos.getDistanceSquaredFrom (filterPos) < 45)
            {
                activeElem = i;
                break;
            }
        }

        if (oldActiveElem != activeElem)
            repaint();
    }

    void resized() override
    {
        int xMin = hzToX (s.fMin);
        int xMax = hzToX (s.fMax);
        allMagnitudesInDb.resize (xMax - xMin + 1);

        const float width = getWidth() - mL - mR;
        dbGridPath.clear();
        dbGridPathBold.clear();

        dbGridPathBold.startNewSubPath (mL - OH, t60ToY (0.1f));
        dbGridPathBold.lineTo (mL + width + OH, t60ToY (0.1f));

        dbGridPathBold.startNewSubPath (mL - OH, t60ToY (1.f));
        dbGridPathBold.lineTo (mL + width + OH, t60ToY (1.f));

        for (float t = s.yMin; t <= s.yMax; t += powf (10, floorf (log10 (t))))
        {
            int ypos = t60ToY (t);

            if ((t == 10) || (t == 60))
            {
                dbGridPathBold.startNewSubPath (mL - OH, ypos);
                dbGridPathBold.lineTo (mL + width + OH, ypos);
            }
            else
            {
                dbGridPath.startNewSubPath (mL - OH, ypos);
                dbGridPath.lineTo (mL + width + OH, ypos);
            }
        }

        hzGridPath.clear();
        hzGridPathBold.clear();

        for (float f = s.fMin; f <= s.fMax; f += powf (10, floorf (log10 (f))))
        {
            int xpos = hzToX (f);

            if ((f == 20) || (f == 50) || (f == 100) || (f == 500) || (f == 1000) || (f == 5000)
                || (f == 10000) || (f == 20000))
            {
                hzGridPathBold.startNewSubPath (xpos, t60ToY (s.yMax) - OH);
                hzGridPathBold.lineTo (xpos, t60ToY (s.yMin) + OH);
            }
            else
            {
                hzGridPath.startNewSubPath (xpos, t60ToY (s.yMax) - OH);
                hzGridPath.lineTo (xpos, t60ToY (s.yMin) + OH);
            }
        }

        createTolerancePath();
    }

    void addCoefficients (typename juce::dsp::IIR::Coefficients<coefficientsType>::Ptr newCoeffs,
                          juce::Colour newColourForCoeffs,
                          juce::Slider* frequencySlider = nullptr,
                          juce::Slider* gainSlider = nullptr)
    {
        arrayOfCoefficients.add (newCoeffs);
        arrayOfColours.add (newColourForCoeffs);
        arrayOfGainSliders.add (gainSlider);
        arrayOfFrequencySliders.add (frequencySlider);
    }

    void replaceCoefficients (
        int filterIdx,
        typename juce::dsp::IIR::Coefficients<coefficientsType>::Ptr newCoefficients)
    {
        if (filterIdx < arrayOfCoefficients.size())
        {
            arrayOfCoefficients.setUnchecked (filterIdx, newCoefficients);
            repaint();
        }
    }

private:
    float overallGainInDb;

    int sampleRate;

    int activeElem = 0;

    Settings s;
    juce::Path dbGridPath;
    juce::Path dbGridPathBold;
    juce::Path hzGridPath;
    juce::Path hzGridPathBold;
    juce::Path tolerancePath;

    juce::Array<float> allMagnitudesInDb;
    juce::Array<typename juce::dsp::IIR::Coefficients<coefficientsType>::Ptr> arrayOfCoefficients;
    juce::Array<juce::Slider*> arrayOfGainSliders, arrayOfFrequencySliders;

    juce::Array<juce::Colour> arrayOfColours;
};
