/*
  ==============================================================================

   This file is part of the JUCE framework.
   Copyright (c) Raw Material Software Limited

   JUCE is an open source framework subject to commercial or open source
   licensing.

   By downloading, installing, or using the JUCE framework, or combining the
   JUCE framework with any other source code, object code, content or any other
   copyrightable work, you agree to the terms of the JUCE End User Licence
   Agreement, and all incorporated terms including the JUCE Privacy Policy and
   the JUCE Website Terms of Service, as applicable, which will bind you. If you
   do not agree to the terms of these agreements, we will not license the JUCE
   framework to you, and you must discontinue the installation or download
   process and cease use of the JUCE framework.

   JUCE End User Licence Agreement: https://juce.com/legal/juce-8-licence/
   JUCE Privacy Policy: https://juce.com/juce-privacy-policy
   JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/

   Or:

   You may also use this code under the terms of the AGPLv3:
   https://www.gnu.org/licenses/agpl-3.0.en.html

   THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
   WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
   MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.

  ==============================================================================
*/

namespace juce
{

struct ColourComponentSlider final : public Slider
{
    ColourComponentSlider (const String& name)  : Slider (name)
    {
        setRange (0.0, 255.0, 1.0);
    }

    String getTextFromValue (double value) override
    {
        return String::toHexString ((int) value).toUpperCase().paddedLeft ('0', 2);
    }

    double getValueFromText (const String& text) override
    {
        return (double) text.getHexValue32();
    }
};

//==============================================================================
class ColourSelector::ColourSpaceView final : public Component
{
public:
    ColourSpaceView (ColourSelector& cs, float& hue, float& sat, float& val, int edgeSize)
        : owner (cs), h (hue), s (sat), v (val), edge (edgeSize)
    {
        addAndMakeVisible (marker);
        setMouseCursor (MouseCursor::CrosshairCursor);
    }

    void paint (Graphics& g) override
    {
        if (colours.isNull())
        {
            auto width = getWidth() / 2;
            auto height = getHeight() / 2;
            colours = Image (Image::RGB, width, height, false);

            Image::BitmapData pixels (colours, Image::BitmapData::writeOnly);

            for (int y = 0; y < height; ++y)
            {
                auto val = 1.0f - (float) y / (float) height;

                for (int x = 0; x < width; ++x)
                {
                    auto sat = (float) x / (float) width;
                    pixels.setPixelColour (x, y, Colour (h, sat, val, 1.0f));
                }
            }
        }

        g.setOpacity (1.0f);
        g.drawImageTransformed (colours,
                                RectanglePlacement (RectanglePlacement::stretchToFit)
                                    .getTransformToFit (colours.getBounds().toFloat(),
                                                        getLocalBounds().reduced (edge).toFloat()),
                                false);
    }

    void mouseDown (const MouseEvent& e) override
    {
        mouseDrag (e);
    }

    void mouseDrag (const MouseEvent& e) override
    {
        auto sat =        (float) (e.x - edge) / (float) (getWidth()  - edge * 2);
        auto val = 1.0f - (float) (e.y - edge) / (float) (getHeight() - edge * 2);

        owner.setSV (sat, val);
    }

    void updateIfNeeded()
    {
        if (! approximatelyEqual (lastHue, h))
        {
            lastHue = h;
            colours = {};
            repaint();
        }

        updateMarker();
    }

    void resized() override
    {
        colours = {};
        updateMarker();
    }

private:
    ColourSelector& owner;
    float& h;
    float& s;
    float& v;
    float lastHue = 0;
    const int edge;
    Image colours;

    struct ColourSpaceMarker final : public Component
    {
        ColourSpaceMarker()
        {
            setInterceptsMouseClicks (false, false);
        }

        void paint (Graphics& g) override
        {
            g.setColour (Colour::greyLevel (0.1f));
            g.drawEllipse (1.0f, 1.0f, (float) getWidth() - 2.0f, (float) getHeight() - 2.0f, 1.0f);
            g.setColour (Colour::greyLevel (0.9f));
            g.drawEllipse (2.0f, 2.0f, (float) getWidth() - 4.0f, (float) getHeight() - 4.0f, 1.0f);
        }
    };

    ColourSpaceMarker marker;

    void updateMarker()
    {
        auto markerSize = jmax (14, edge * 2);
        auto area = getLocalBounds().reduced (edge);

        marker.setBounds (Rectangle<int> (markerSize, markerSize)
                            .withCentre (area.getRelativePoint (s, 1.0f - v)));
    }

    JUCE_DECLARE_NON_COPYABLE (ColourSpaceView)
};

//==============================================================================
class ColourSelector::HueSelectorComp final : public Component
{
public:
    HueSelectorComp (ColourSelector& cs, float& hue, int edgeSize)
        : owner (cs), h (hue), edge (edgeSize)
    {
        addAndMakeVisible (marker);
    }

    void paint (Graphics& g) override
    {
        ColourGradient cg;
        cg.isRadial = false;
        cg.point1.setXY (0.0f, (float) edge);
        cg.point2.setXY (0.0f, (float) getHeight());

        for (float i = 0.0f; i <= 1.0f; i += 0.02f)
            cg.addColour (i, Colour (i, 1.0f, 1.0f, 1.0f));

        g.setGradientFill (cg);
        g.fillRect (getLocalBounds().reduced (edge));
    }

    void resized() override
    {
        auto markerSize = jmax (14, edge * 2);
        auto area = getLocalBounds().reduced (edge);

        marker.setBounds (Rectangle<int> (getWidth(), markerSize)
                            .withCentre (area.getRelativePoint (0.5f, h)));
    }

    void mouseDown (const MouseEvent& e) override
    {
        mouseDrag (e);
    }

    void mouseDrag (const MouseEvent& e) override
    {
        owner.setHue ((float) (e.y - edge) / (float) (getHeight() - edge * 2));
    }

    void updateIfNeeded()
    {
        resized();
    }

private:
    ColourSelector& owner;
    float& h;
    const int edge;

    struct HueSelectorMarker final : public Component
    {
        HueSelectorMarker()
        {
            setInterceptsMouseClicks (false, false);
        }

        void paint (Graphics& g) override
        {
            auto cw = (float) getWidth();
            auto ch = (float) getHeight();

            Path p;
            p.addTriangle (1.0f, 1.0f,
                           cw * 0.3f, ch * 0.5f,
                           1.0f, ch - 1.0f);

            p.addTriangle (cw - 1.0f, 1.0f,
                           cw * 0.7f, ch * 0.5f,
                           cw - 1.0f, ch - 1.0f);

            g.setColour (Colours::white.withAlpha (0.75f));
            g.fillPath (p);

            g.setColour (Colours::black.withAlpha (0.75f));
            g.strokePath (p, PathStrokeType (1.2f));
        }
    };

    HueSelectorMarker marker;

    JUCE_DECLARE_NON_COPYABLE (HueSelectorComp)
};

//==============================================================================
class ColourSelector::SwatchComponent final : public Component
{
public:
    SwatchComponent (ColourSelector& cs, int itemIndex)
        : owner (cs), index (itemIndex)
    {
    }

    void paint (Graphics& g) override
    {
        auto col = owner.getSwatchColour (index);

        g.fillCheckerBoard (getLocalBounds().toFloat(), 6.0f, 6.0f,
                            Colour (0xffdddddd).overlaidWith (col),
                            Colour (0xffffffff).overlaidWith (col));
    }

    void mouseDown (const MouseEvent&) override
    {
        PopupMenu m;
        m.addItem (1, TRANS ("Use this swatch as the current colour"));
        m.addSeparator();
        m.addItem (2, TRANS ("Set this swatch to the current colour"));

        m.showMenuAsync (PopupMenu::Options().withTargetComponent (this),
                         ModalCallbackFunction::forComponent (menuStaticCallback, this));
    }

private:
    ColourSelector& owner;
    const int index;

    static void menuStaticCallback (int result, SwatchComponent* comp)
    {
        if (comp != nullptr)
        {
            if (result == 1)  comp->setColourFromSwatch();
            if (result == 2)  comp->setSwatchFromColour();
        }
    }

    void setColourFromSwatch()
    {
        owner.setCurrentColour (owner.getSwatchColour (index));
    }

    void setSwatchFromColour()
    {
        if (owner.getSwatchColour (index) != owner.getCurrentColour())
        {
            owner.setSwatchColour (index, owner.getCurrentColour());
            repaint();
        }
    }

    JUCE_DECLARE_NON_COPYABLE (SwatchComponent)
};

//==============================================================================
class ColourSelector::ColourPreviewComp final : public Component
{
public:
    ColourPreviewComp (ColourSelector& cs, bool isEditable)
        : owner (cs)
    {
        colourLabel.setFont (labelFont);
        colourLabel.setJustificationType (Justification::centred);

        if (isEditable)
        {
            colourLabel.setEditable (true);

            colourLabel.onEditorShow = [this]
            {
                if (auto* ed = colourLabel.getCurrentTextEditor())
                    ed->setInputRestrictions ((owner.flags & showAlphaChannel) ? 8 : 6, "1234567890ABCDEFabcdef");
            };

            colourLabel.onEditorHide = [this]
            {
                updateColourIfNecessary (colourLabel.getText());
            };
        }

        addAndMakeVisible (colourLabel);
    }

    void updateIfNeeded()
    {
        auto newColour = owner.getCurrentColour();

        if (currentColour != newColour)
        {
            currentColour = newColour;
            auto textColour = (Colours::white.overlaidWith (currentColour).contrasting());

            colourLabel.setColour (Label::textColourId,            textColour);
            colourLabel.setColour (Label::textWhenEditingColourId, textColour);
            colourLabel.setText (currentColour.toDisplayString ((owner.flags & showAlphaChannel) != 0), dontSendNotification);

            labelWidth = GlyphArrangement::getStringWidthInt (labelFont, colourLabel.getText());

            repaint();
        }
    }

    void paint (Graphics& g) override
    {
        g.fillCheckerBoard (getLocalBounds().toFloat(), 10.0f, 10.0f,
                            Colour (0xffdddddd).overlaidWith (currentColour),
                            Colour (0xffffffff).overlaidWith (currentColour));
    }

    void resized() override
    {
        colourLabel.centreWithSize (labelWidth + 10, (int) labelFont.getHeight() + 10);
    }

private:
    void updateColourIfNecessary (const String& newColourString)
    {
        auto newColour = Colour::fromString (newColourString);

        if (newColour != currentColour)
            owner.setCurrentColour (newColour);
    }

    ColourSelector& owner;

    Colour currentColour;
    Font labelFont { withDefaultMetrics (FontOptions { 14.0f, Font::bold }) };
    int labelWidth = 0;
    Label colourLabel;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ColourPreviewComp)
};

//==============================================================================
ColourSelector::ColourSelector (int sectionsToShow, int edge, int gapAroundColourSpaceComponent)
    : colour (Colours::white),
      flags (sectionsToShow),
      edgeGap (edge)
{
    // not much point having a selector with no components in it!
    jassert ((flags & (showColourAtTop | showSliders | showColourspace)) != 0);

    updateHSV();

    if ((flags & showColourAtTop) != 0)
    {
        previewComponent.reset (new ColourPreviewComp (*this, (flags & editableColour) != 0));
        addAndMakeVisible (previewComponent.get());
    }

    if ((flags & showSliders) != 0)
    {
        sliders[0].reset (new ColourComponentSlider (TRANS ("red")));
        sliders[1].reset (new ColourComponentSlider (TRANS ("green")));
        sliders[2].reset (new ColourComponentSlider (TRANS ("blue")));
        sliders[3].reset (new ColourComponentSlider (TRANS ("alpha")));

        addAndMakeVisible (sliders[0].get());
        addAndMakeVisible (sliders[1].get());
        addAndMakeVisible (sliders[2].get());
        addChildComponent (sliders[3].get());

        sliders[3]->setVisible ((flags & showAlphaChannel) != 0);

        for (auto& slider : sliders)
            slider->onValueChange = [this] { changeColour(); };
    }

    if ((flags & showColourspace) != 0)
    {
        colourSpace.reset (new ColourSpaceView (*this, h, s, v, gapAroundColourSpaceComponent));
        hueSelector.reset (new HueSelectorComp (*this, h, gapAroundColourSpaceComponent));

        addAndMakeVisible (colourSpace.get());
        addAndMakeVisible (hueSelector.get());
    }

    update (dontSendNotification);
}

ColourSelector::~ColourSelector()
{
    dispatchPendingMessages();
    swatchComponents.clear();
}

//==============================================================================
Colour ColourSelector::getCurrentColour() const
{
    return ((flags & showAlphaChannel) != 0) ? colour : colour.withAlpha ((uint8) 0xff);
}

void ColourSelector::setCurrentColour (Colour c, NotificationType notification)
{
    if (c != colour)
    {
        colour = ((flags & showAlphaChannel) != 0) ? c : c.withAlpha ((uint8) 0xff);

        updateHSV();
        update (notification);
    }
}

void ColourSelector::setHue (float newH)
{
    newH = jlimit (0.0f, 1.0f, newH);

    if (! approximatelyEqual (h, newH))
    {
        h = newH;
        colour = Colour (h, s, v, colour.getFloatAlpha());
        update (sendNotification);
    }
}

void ColourSelector::setSV (float newS, float newV)
{
    newS = jlimit (0.0f, 1.0f, newS);
    newV = jlimit (0.0f, 1.0f, newV);

    if (! approximatelyEqual (s, newS) || ! approximatelyEqual (v, newV))
    {
        s = newS;
        v = newV;
        colour = Colour (h, s, v, colour.getFloatAlpha());
        update (sendNotification);
    }
}

//==============================================================================
void ColourSelector::updateHSV()
{
    colour.getHSB (h, s, v);
}

void ColourSelector::update (NotificationType notification)
{
    if (sliders[0] != nullptr)
    {
        sliders[0]->setValue ((int) colour.getRed(),   notification);
        sliders[1]->setValue ((int) colour.getGreen(), notification);
        sliders[2]->setValue ((int) colour.getBlue(),  notification);
        sliders[3]->setValue ((int) colour.getAlpha(), notification);
    }

    if (colourSpace != nullptr)
    {
        colourSpace->updateIfNeeded();
        hueSelector->updateIfNeeded();
    }

    if (previewComponent != nullptr)
        previewComponent->updateIfNeeded();

    if (notification != dontSendNotification)
        sendChangeMessage();

    if (notification == sendNotificationSync)
        dispatchPendingMessages();
}

//==============================================================================
void ColourSelector::paint (Graphics& g)
{
    g.fillAll (findColour (backgroundColourId));

    if ((flags & showSliders) != 0)
    {
        g.setColour (findColour (labelTextColourId));
        g.setFont (11.0f);

        for (auto& slider : sliders)
        {
            if (slider->isVisible())
                g.drawText (slider->getName() + ":",
                            0, slider->getY(),
                            slider->getX() - 8, slider->getHeight(),
                            Justification::centredRight, false);
        }
    }
}

void ColourSelector::resized()
{
    const int swatchesPerRow = 8;
    const int swatchHeight = 22;

    const int numSliders = ((flags & showAlphaChannel) != 0) ? 4 : 3;
    const int numSwatches = getNumSwatches();

    const int swatchSpace = numSwatches > 0 ? edgeGap + swatchHeight * ((numSwatches + 7) / swatchesPerRow) : 0;
    const int sliderSpace = ((flags & showSliders) != 0)  ? jmin (22 * numSliders + edgeGap, proportionOfHeight (0.3f)) : 0;
    const int topSpace = ((flags & showColourAtTop) != 0) ? jmin (30 + edgeGap * 2, proportionOfHeight (0.2f)) : edgeGap;

    if (previewComponent != nullptr)
        previewComponent->setBounds (edgeGap, edgeGap, getWidth() - edgeGap * 2, topSpace - edgeGap * 2);

    int y = topSpace;

    if ((flags & showColourspace) != 0)
    {
        const int hueWidth = jmin (50, proportionOfWidth (0.15f));

        colourSpace->setBounds (edgeGap, y,
                                getWidth() - hueWidth - edgeGap - 4,
                                getHeight() - topSpace - sliderSpace - swatchSpace - edgeGap);

        hueSelector->setBounds (colourSpace->getRight() + 4, y,
                                getWidth() - edgeGap - (colourSpace->getRight() + 4),
                                colourSpace->getHeight());

        y = getHeight() - sliderSpace - swatchSpace - edgeGap;
    }

    if ((flags & showSliders) != 0)
    {
        auto sliderHeight = jmax (4, sliderSpace / numSliders);

        for (int i = 0; i < numSliders; ++i)
        {
            sliders[i]->setBounds (proportionOfWidth (0.2f), y,
                                   proportionOfWidth (0.72f), sliderHeight - 2);

            y += sliderHeight;
        }
    }

    if (numSwatches > 0)
    {
        const int startX = 8;
        const int xGap = 4;
        const int yGap = 4;
        const int swatchWidth = (getWidth() - startX * 2) / swatchesPerRow;
        y += edgeGap;

        if (swatchComponents.size() != numSwatches)
        {
            swatchComponents.clear();

            for (int i = 0; i < numSwatches; ++i)
            {
                auto* sc = new SwatchComponent (*this, i);
                swatchComponents.add (sc);
                addAndMakeVisible (sc);
            }
        }

        int x = startX;

        for (int i = 0; i < swatchComponents.size(); ++i)
        {
            auto* sc = swatchComponents.getUnchecked (i);

            sc->setBounds (x + xGap / 2,
                           y + yGap / 2,
                           swatchWidth - xGap,
                           swatchHeight - yGap);

            if (((i + 1) % swatchesPerRow) == 0)
            {
                x = startX;
                y += swatchHeight;
            }
            else
            {
                x += swatchWidth;
            }
        }
    }
}

void ColourSelector::changeColour()
{
    if (sliders[0] != nullptr)
        setCurrentColour (Colour ((uint8) sliders[0]->getValue(),
                                  (uint8) sliders[1]->getValue(),
                                  (uint8) sliders[2]->getValue(),
                                  (uint8) sliders[3]->getValue()));
}

//==============================================================================
int ColourSelector::getNumSwatches() const
{
    return 0;
}

Colour ColourSelector::getSwatchColour (int) const
{
    jassertfalse; // if you've overridden getNumSwatches(), you also need to implement this method
    return Colours::black;
}

void ColourSelector::setSwatchColour (int, const Colour&)
{
    jassertfalse; // if you've overridden getNumSwatches(), you also need to implement this method
}

} // namespace juce
