/*
 ==============================================================================
 This file is part of the IEM plug-in suite.
 Author: Daniel Rudrich
 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
#include "../../resources/Conversions.h"
#include "../JuceLibraryCode/JuceHeader.h"
#include "../../resources/Quaternion.h"


class SpherePannerBackground :  public Component
{

public:
    SpherePannerBackground() : Component() {
        setBufferedToImage(true);
    };
    ~SpherePannerBackground() {};

    void resized() override
    {
        const Rectangle<float> sphere(getLocalBounds().reduced(10, 10).toFloat());

        radius = 0.5f * jmin(sphere.getWidth(), sphere.getHeight());
        centre = getLocalBounds().getCentre();
        sphereArea.setBounds(0, 0, 2*radius, 2*radius);
        sphereArea.setCentre(centre.toFloat());
    };

    void paint (Graphics& g) override
    {
        const Rectangle<float> bounds = getLocalBounds().toFloat();
        const float centreX = bounds.getCentreX();
        const float centreY = bounds.getCentreY();
        const Rectangle<float> sphere(bounds.reduced(10, 10));

        g.setColour(Colours::white);
        g.drawEllipse(centreX-radius, centreY - radius, 2.0f * radius, 2.0f * radius, 1.0f);

        g.setFont(getLookAndFeel().getTypefaceForFont (Font(12.0f, 1)));
        g.setFont(12.0f);
        g.drawText("FRONT", centreX-15, centreY-radius-12, 30, 12, Justification::centred);
        g.drawText("BACK", centreX-15, centreY+radius, 30, 12, Justification::centred);
        g.drawFittedText("L\nE\nF\nT", sphereArea.getX()-10, centreY-40, 10, 80, Justification::centred, 4);
        g.drawFittedText("R\nI\nG\nH\nT", sphereArea.getRight(), centreY-40, 10, 80, Justification::centred, 5);

        g.setColour(Colours::steelblue.withMultipliedAlpha(0.2f));
        Path circles;

        for (int deg = 75; deg >= 0; deg -= 15)
        {
            float rCirc;
            if (! linearElevation)
                rCirc = radius * std::cos (Conversions<float>::degreesToRadians(deg));
            else
                rCirc = radius * (90 - deg) / 90;
            circles.addEllipse(centreX - rCirc, centreY - rCirc, 2.0f * rCirc, 2.0f * rCirc);
            g.fillPath(circles);
        }

        g.setColour(Colours::Colours::steelblue.withMultipliedAlpha(0.7f));
        g.strokePath(circles, PathStrokeType(.5f));

        ColourGradient gradient(Colours::black.withMultipliedAlpha(0.7f), centreX, centreY, Colours::black.withMultipliedAlpha(0.1f), 0, 0, true);
        g.setGradientFill(gradient);

        Path line;
        line.startNewSubPath(centreX, centreY-radius);
        line.lineTo(centreX, centreY+radius);

        Path path;
        path.addPath(line);
        path.addPath(line, AffineTransform().rotation(0.25f*M_PI, centreX, centreY));
        path.addPath(line, AffineTransform().rotation(0.5f*M_PI, centreX, centreY));
        path.addPath(line, AffineTransform().rotation(0.75f*M_PI, centreX, centreY));

        g.strokePath(path, PathStrokeType(0.5f));
    }
    void setElevationStyle (bool linear) { linearElevation = linear; };

private:
    float radius = 1.0f;
    Rectangle<float> sphereArea;
    Point<int> centre;
    bool linearElevation = false;
};



class  SpherePanner :  public Component
{
public:
    SpherePanner() : Component() {
        setBufferedToImage(true);

        addAndMakeVisible(background);
        background.addMouseListener(this, false); // could this be risky?
    };
    ~SpherePanner() {};


    class Listener
    {
    public:
        virtual ~Listener() {}
        virtual void mouseWheelOnSpherePannerMoved (SpherePanner* sphere, const MouseEvent &event, const MouseWheelDetails &wheel) {};
    };

    class Element
    {
    public:
        Element() {}
        virtual ~Element() {}

        virtual void startMovement() { };
        virtual void moveElement (const MouseEvent &event, Point<int> centre, float radius, bool upBeforeDrag,  bool linearElevation) = 0;
        virtual void stopMovement() { };

        /**
         Get cartesian coordinates
         */
        virtual Vector3D<float> getCoordinates() = 0;


        void setActive (bool isActive) { active = isActive; }
        bool isActive() { return active; }

        void setColour( Colour newColour) { colour = newColour; }
        void setTextColour( Colour newColour) { textColour = newColour; }
        Colour getColour() { return colour; }
        Colour getTextColour() { return textColour; }

        void setLabel(String newLabel) {label = newLabel;}

        void setGrabPriority(int newPriority) {grabPriority = newPriority;}
        int getGrabPriority() {return grabPriority;}
        void setGrabRadius(float newRadius) {grabRadius = newRadius;}
        float getGrabRadius() {return grabRadius;}


        String getLabel() {return label;};

    private:
        bool active = true;

        float grabRadius = 0.015f;
        int grabPriority = 0;

        Colour colour = Colours::white;
        Colour textColour = Colours::black;
        String label = "";
    };

    class StandardElement : public Element
    {
    public:
        StandardElement() : Element() {}

        void moveElement (const MouseEvent &event, Point<int> centre, float radius, bool upBeforeDrag, bool linearElevation) override
        {
            Point<int> pos = event.getPosition();
            const float azimuth = -1.0f * centre.getAngleToPoint(pos);
            float r = centre.getDistanceFrom(pos) / radius;
            if (r > 1.0f) {
                r = 1.0f / r;
                upBeforeDrag = !upBeforeDrag;
            }

            if (linearElevation)
                r = std::sin(r * 1.570796327f);


            float elevation = std::acos(r);
            if (! upBeforeDrag) elevation *= -1.0f;
            position = Conversions<float>::sphericalToCartesian(azimuth, elevation);
        }

        /*
         Get cartesian coordinates
         */
        Vector3D<float> getCoordinates() override
        {
            return position;
        };

        bool setCoordinates(Vector3D<float> newPosition) // is true when position is updated (use it for repainting)
        {
            if (position.x != newPosition.x || position.y != newPosition.y || position.z != newPosition.z)
            {
                position = newPosition;
                return true;
            }
            return false;
        }


    private:
        Vector3D<float> position;
    };

    class AziumuthElevationParameterElement : public Element
    {
    public:
        AziumuthElevationParameterElement(AudioProcessorParameter& azimuthParameter, NormalisableRange<float> azimuthParameterRange, AudioProcessorParameter& elevationParameter, NormalisableRange<float> elevationParameterRange) : Element(), azimuth(azimuthParameter), azimuthRange(azimuthParameterRange), elevation(elevationParameter), elevationRange(elevationParameterRange) {}

        void startMovement() override
        {
            azimuth.beginChangeGesture();
            elevation.beginChangeGesture();
        };

        void moveElement (const MouseEvent &event, Point<int> centre, float radius, bool upBeforeDrag,  bool linearElevation) override
        {
            Point<int> pos = event.getPosition();
            const float azi = -1.0f * centre.getAngleToPoint(pos);
            float r = centre.getDistanceFrom(pos) / radius;

            if (r > 1.0f) {
                r = 1.0f / r;
                upBeforeDrag = !upBeforeDrag;
            }

            if (linearElevation)
                r = std::sin(r * 1.570796327f);

            float ele = std::acos(r);
            if (! upBeforeDrag) ele *= -1.0f;

            const float azimuthInDegrees { Conversions<float>::radiansToDegrees(azi) };
            const float elevationInDegrees { Conversions<float>::radiansToDegrees(ele) };

            azimuth.setValueNotifyingHost(azimuthRange.convertTo0to1(azimuthInDegrees));
            elevation.setValueNotifyingHost(elevationRange.convertTo0to1(elevationInDegrees));
        }

        void stopMovement() override
        {
            azimuth.endChangeGesture();
            elevation.endChangeGesture();
        };

        const float getAzimuthInRadians()
        {
            const float azimuthInDegrees {azimuthRange.convertFrom0to1(azimuth.getValue())};
            return Conversions<float>::degreesToRadians(azimuthInDegrees);
        }

        const float getElevationInRadians()
        {
            const float elevationInDegrees {elevationRange.convertFrom0to1(elevation.getValue())};
            return Conversions<float>::degreesToRadians(elevationInDegrees);
        }

        /*
         Get cartesian coordinates
         */
        Vector3D<float> getCoordinates() override
        {
            return Conversions<float>::sphericalToCartesian(getAzimuthInRadians(), getElevationInRadians());
        };

    private:
        AudioProcessorParameter& azimuth;
        NormalisableRange<float> azimuthRange;
        AudioProcessorParameter& elevation;
        NormalisableRange<float> elevationRange;
    };

    class RollWidthParameterElement : public Element
    {
    public:
        RollWidthParameterElement(AziumuthElevationParameterElement& center, AudioProcessorParameter& rollParameter, NormalisableRange<float> rollParameterRange, AudioProcessorParameter& widthParameter, NormalisableRange<float> widthParameterRange) : Element(), centerElement(center), roll(rollParameter), rollRange(rollParameterRange), width(widthParameter), widthRange(widthParameterRange) {}

        void startMovement() override
        {
            roll.beginChangeGesture();
            width.beginChangeGesture();
        };

        void moveElement (const MouseEvent &event, Point<int> centre, float radius, bool upBeforeDrag, bool linearElevation) override
        {
            Point<int> pos = event.getPosition();
            const float azi = -1.0f * centre.getAngleToPoint(pos);
            float r = centre.getDistanceFrom(pos) / radius;
            if (r > 1.0f) {
                r = 1.0f / r;
                upBeforeDrag = !upBeforeDrag;
            }

            if (linearElevation)
                r = std::sin(r * 1.570796327f);

            float ele = std::acos(r);
            if (! upBeforeDrag) ele *= -1.0f;

            Vector3D<float> posXYZ {Conversions<float>::sphericalToCartesian(azi, ele)};

            // ==== calculate width
            Vector3D<float> dPos = posXYZ - centerElement.getCoordinates();
            const float alpha = 4.0f*asinf(dPos.length()/2.0f);
            width.setValueNotifyingHost(widthRange.convertTo0to1(Conversions<float>::radiansToDegrees(alpha)));

            // ==== calculate roll
            iem::Quaternion<float> quat;
            float ypr[3];
            ypr[0] = centerElement.getAzimuthInRadians();
            ypr[1] = - centerElement.getElevationInRadians(); // pitch
            ypr[2] = 0.0f;

            quat.fromYPR(ypr);
            quat.conjugate();

            float xyz[3];
            xyz[0] = posXYZ.x;
            xyz[1] = posXYZ.y;
            xyz[2] = posXYZ.z;

            quat.rotateVector(xyz, xyz);

            float rollInRadians = atan2(xyz[2], xyz[1]);
            if (isMirrored) rollInRadians = atan2(-xyz[2], -xyz[1]);

            roll.setValueNotifyingHost(rollRange.convertTo0to1(Conversions<float>::radiansToDegrees(rollInRadians)));
        }

        void stopMovement() override
        {
            roll.endChangeGesture();
            width.endChangeGesture();
        };

        /*
         Get cartesian coordinates
         */
        Vector3D<float> getCoordinates() override
        {
            float ypr[3];
            ypr[0] = centerElement.getAzimuthInRadians();
            ypr[1] = - centerElement.getElevationInRadians(); // pitch
            ypr[2] = Conversions<float>::degreesToRadians(rollRange.convertFrom0to1(roll.getValue()));

            //updating not active params
            iem::Quaternion<float> quat;
            quat.fromYPR(ypr);

            const float widthInRadiansQuarter {Conversions<float>::degreesToRadians(widthRange.convertFrom0to1(width.getValue())) / 4.0f};

            iem::Quaternion<float> quatLRot {iem::Quaternion<float>(cos(widthInRadiansQuarter), 0.0f, 0.0f, sin(widthInRadiansQuarter))};
            if (isMirrored)
                quatLRot.conjugate();

            iem::Quaternion<float> quatL = quat * quatLRot;

            float xyz[3];
            quatL.toCartesian(xyz);

            return Vector3D<float> (xyz[0], xyz[1], xyz[2]);
        };

        void setMirrored (bool mirrored) { isMirrored = mirrored; }

    private:
        AziumuthElevationParameterElement& centerElement;
        AudioProcessorParameter& roll;
        NormalisableRange<float> rollRange;
        AudioProcessorParameter& width;
        NormalisableRange<float> widthRange;
        bool isMirrored = false;
    };

    void resized () override {
        background.setBounds(getLocalBounds());
        const Rectangle<float> sphere(getLocalBounds().reduced(10, 10).toFloat());

        radius = 0.5f * jmin(sphere.getWidth(), sphere.getHeight());
        centre = getLocalBounds().getCentre();
        sphereArea.setBounds(0, 0, 2*radius, 2*radius);
        sphereArea.setCentre(centre.toFloat());
    }

    void paintOverChildren (Graphics& g) override
    {
        const Rectangle<float> bounds = getLocalBounds().toFloat();
        const float centreX = bounds.getCentreX();
        const float centreY = bounds.getCentreY();
        const Rectangle<float> sphere(bounds.reduced(10, 10));

        Path panPos;
        const int size = elements.size();
        for (int i = 0; i < size; ++i) {
            SpherePanner::Element* handle = elements.getUnchecked (i);

            Vector3D<float> pos = handle->getCoordinates();

            const float diam = 15.0f + 4.0f * pos.z;
            g.setColour(handle->isActive() ? handle->getColour() : Colours::grey);

            if (linearElevation)
            {
                const float r = sqrt(pos.y * pos.y + pos.x * pos.x);
                const float factor = std::asin(r) / r / 1.570796327f; // pi / 2
                pos *= factor;
            }

            Rectangle<float> temp(centreX - pos.y * radius - diam / 2, centreY - pos.x * radius - diam / 2, diam, diam);
            panPos.addEllipse(temp);
            g.strokePath(panPos, PathStrokeType(1.0f));
            g.setColour((handle->isActive() ? handle->getColour() : Colours::grey).withMultipliedAlpha(pos.z >= -0.0f ? 1.0f : 0.4f));
            g.fillPath(panPos);
            g.setColour(handle->getTextColour());
            g.drawFittedText(handle->getLabel(), temp.toNearestInt(), Justification::centred, 1);
            panPos.clear();
        }
    };


    void mouseWheelMove(const MouseEvent &event, const MouseWheelDetails &wheel) override
    {
        for (int i = listeners.size(); --i >= 0;)
            listeners.getUnchecked(i)->mouseWheelOnSpherePannerMoved (this, event, wheel);
    }

    void mouseMove (const MouseEvent &event) override
    {
        int oldActiveElem = activeElem;
        activeElem = -1;

        const float centreX = 0.5* (float)getBounds().getWidth();
        const float centreY = 0.5* (float)getBounds().getHeight();

        int nElem = elements.size();

        if (nElem > 0) {
            Point<int> pos = event.getPosition();

            const float mouseX = (centreY-pos.getY()) / radius;
            const float mouseY = (centreX-pos.getX()) / radius;

            float *dist = (float*) malloc(nElem * sizeof(float));

            //int nGrabed = 0;
            int highestPriority = -1;

            float tx,ty;
            for (int i = elements.size(); --i >= 0;) {
                Element* handle(elements.getUnchecked (i));
                Vector3D<float> pos = handle->getCoordinates();

                if (linearElevation)
                {
                    const float r = sqrt(pos.y * pos.y + pos.x * pos.x);
                    const float factor = std::asin(r) / r / 1.570796327f; // pi / 2
                    pos *= factor;
                }

                tx = (mouseX - pos.x);
                ty = (mouseY - pos.y);
                dist[i] = tx*tx + ty*ty;

                if (dist[i] <= handle->getGrabRadius()) {
                    if (handle->getGrabPriority() > highestPriority) {
                        activeElem = i;
                        highestPriority = handle->getGrabPriority();
                    }
                    else if (handle->getGrabPriority() == highestPriority && dist[i] < dist[activeElem]) {
                        activeElem = i;
                    }
                }
            }
        }
        if (activeElem != -1)  activeElemWasUpBeforeDrag = elements.getUnchecked (activeElem)->getCoordinates().z >= 0.0f;
        if (oldActiveElem != activeElem)
            repaint();
    }
    void mouseDrag (const MouseEvent &event) override
    {
        if (activeElem != -1) {
            elements.getUnchecked(activeElem)->moveElement(event, centre, radius, activeElemWasUpBeforeDrag, linearElevation);
            repaint();
        }
    }

    void mouseDown (const MouseEvent &event) override
    {
        if (activeElem != -1) {
            elements.getUnchecked(activeElem)->startMovement();
        }
    }

    void mouseUp (const MouseEvent &event) override
    {
        if (activeElem != -1) {
            elements.getUnchecked(activeElem)->stopMovement();
        }
    }

    void mouseDoubleClick (const MouseEvent &event) override
    {
        setElevationStyle(! linearElevation);
        background.repaint();
        repaint();
    }

    void addListener (Listener* const listener) {
        jassert (listener != nullptr);
        if (listener != nullptr)
            listeners.add (listener);
    };

    void removeListener (Listener* const listener) {
        listeners.removeFirstMatchingValue(listener);
    };

    void addElement (Element* const element) {
        jassert (element != nullptr);
        if (element !=0)
            elements.add (element);
    };
    void removeElement (Element* const element) {
        elements.removeFirstMatchingValue(element);
    };

    int indexofSmallestElement(float *array, int size)
    {
        int index = 0;

        for(int i = 1; i < size; i++)
        {
            if(array[i] < array[index])
                index = i;
        }

        return index;
    }

    void setElevationStyle (bool linear) {
        linearElevation = linear;
        background.setElevationStyle(linear);
    };

private:
    float radius = 1.0f;
    Rectangle<float> sphereArea;
    Point<int> centre;
    int activeElem;
    bool activeElemWasUpBeforeDrag;
    Array<Listener*> listeners;
    Array<Element*> elements;
    bool linearElevation = false;
    SpherePannerBackground background;
};
