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

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

   JUCE is an open source library subject to commercial or open-source
   licensing.

   By using JUCE, you agree to the terms of both the JUCE 7 End-User License
   Agreement and JUCE Privacy Policy.

   End User License Agreement: www.juce.com/juce-7-licence
   Privacy Policy: www.juce.com/juce-privacy-policy

   Or: You may also use this code under the terms of the GPL v3 (see
   www.gnu.org/licenses).

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

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

namespace juce
{

struct ConcertinaPanel::PanelSizes
{
    struct Panel
    {
        Panel() = default;

        Panel (int sz, int mn, int mx) noexcept
            : size (sz), minSize (mn), maxSize (mx) {}

        int setSize (int newSize) noexcept
        {
            jassert (minSize <= maxSize);
            auto oldSize = size;
            size = jlimit (minSize, maxSize, newSize);
            return size - oldSize;
        }

        int expand (int amount) noexcept
        {
            amount = jmin (amount, maxSize - size);
            size += amount;
            return amount;
        }

        int reduce (int amount) noexcept
        {
            amount = jmin (amount, size - minSize);
            size -= amount;
            return amount;
        }

        bool canExpand() const noexcept     { return size < maxSize; }
        bool isMinimised() const noexcept   { return size <= minSize; }

        int size, minSize, maxSize;
    };

    Array<Panel> sizes;

    Panel& get (int index) noexcept               { return sizes.getReference (index); }
    const Panel& get (int index) const noexcept   { return sizes.getReference (index); }

    PanelSizes withMovedPanel (int index, int targetPosition, int totalSpace) const
    {
        auto num = sizes.size();
        totalSpace = jmax (totalSpace, getMinimumSize (0, num));
        targetPosition = jmax (targetPosition, totalSpace - getMaximumSize (index, num));

        PanelSizes newSizes (*this);
        newSizes.stretchRange (0, index, targetPosition - newSizes.getTotalSize (0, index), stretchLast);
        newSizes.stretchRange (index, num, totalSpace - newSizes.getTotalSize (0, index) - newSizes.getTotalSize (index, num), stretchFirst);
        return newSizes;
    }

    PanelSizes fittedInto (int totalSpace) const
    {
        auto newSizes (*this);
        auto num = newSizes.sizes.size();
        totalSpace = jmax (totalSpace, getMinimumSize (0, num));
        newSizes.stretchRange (0, num, totalSpace - newSizes.getTotalSize (0, num), stretchAll);
        return newSizes;
    }

    PanelSizes withResizedPanel (int index, int panelHeight, int totalSpace) const
    {
        PanelSizes newSizes (*this);

        if (totalSpace <= 0)
        {
            newSizes.get (index).size = panelHeight;
        }
        else
        {
            auto num = sizes.size();
            auto minSize = getMinimumSize (0, num);
            totalSpace = jmax (totalSpace, minSize);

            newSizes.get (index).setSize (panelHeight);
            newSizes.stretchRange (0, index,   totalSpace - newSizes.getTotalSize (0, num), stretchLast);
            newSizes.stretchRange (index, num, totalSpace - newSizes.getTotalSize (0, num), stretchLast);
            newSizes = newSizes.fittedInto (totalSpace);
        }

        return newSizes;
    }

private:
    enum ExpandMode
    {
        stretchAll,
        stretchFirst,
        stretchLast
    };

    void growRangeFirst (int start, int end, int spaceDiff) noexcept
    {
        for (int attempts = 4; --attempts >= 0 && spaceDiff > 0;)
            for (int i = start; i < end && spaceDiff > 0; ++i)
                spaceDiff -= get (i).expand (spaceDiff);
    }

    void growRangeLast (int start, int end, int spaceDiff) noexcept
    {
        for (int attempts = 4; --attempts >= 0 && spaceDiff > 0;)
            for (int i = end; --i >= start && spaceDiff > 0;)
                spaceDiff -= get (i).expand (spaceDiff);
    }

    void growRangeAll (int start, int end, int spaceDiff) noexcept
    {
        Array<Panel*> expandableItems;

        for (int i = start; i < end; ++i)
            if (get (i).canExpand() && ! get (i).isMinimised())
                expandableItems.add (& get (i));

        for (int attempts = 4; --attempts >= 0 && spaceDiff > 0;)
            for (int i = expandableItems.size(); --i >= 0 && spaceDiff > 0;)
                spaceDiff -= expandableItems.getUnchecked (i)->expand (spaceDiff / (i + 1));

        growRangeLast (start, end, spaceDiff);
    }

    void shrinkRangeFirst (int start, int end, int spaceDiff) noexcept
    {
        for (int i = start; i < end && spaceDiff > 0; ++i)
            spaceDiff -= get (i).reduce (spaceDiff);
    }

    void shrinkRangeLast (int start, int end, int spaceDiff) noexcept
    {
        for (int i = end; --i >= start && spaceDiff > 0;)
            spaceDiff -= get (i).reduce (spaceDiff);
    }

    void stretchRange (int start, int end, int amountToAdd, ExpandMode expandMode) noexcept
    {
        if (end > start)
        {
            if (amountToAdd > 0)
            {
                if (expandMode == stretchAll)        growRangeAll   (start, end, amountToAdd);
                else if (expandMode == stretchFirst) growRangeFirst (start, end, amountToAdd);
                else if (expandMode == stretchLast)  growRangeLast  (start, end, amountToAdd);
            }
            else
            {
                if (expandMode == stretchFirst)  shrinkRangeFirst (start, end, -amountToAdd);
                else                             shrinkRangeLast  (start, end, -amountToAdd);
            }
        }
    }

    int getTotalSize (int start, int end) const noexcept
    {
        int tot = 0;
        while (start < end)  tot += get (start++).size;
        return tot;
    }

    int getMinimumSize (int start, int end) const noexcept
    {
        int tot = 0;
        while (start < end)  tot += get (start++).minSize;
        return tot;
    }

    int getMaximumSize (int start, int end) const noexcept
    {
        int tot = 0;

        while (start < end)
        {
            auto mx = get (start++).maxSize;

            if (mx > 0x100000)
                return mx;

            tot += mx;
        }

        return tot;
    }
};

//==============================================================================
class ConcertinaPanel::PanelHolder final : public Component
{
public:
    PanelHolder (Component* comp, bool takeOwnership)
        : component (comp, takeOwnership)
    {
        setRepaintsOnMouseActivity (true);
        setWantsKeyboardFocus (false);
        addAndMakeVisible (comp);
    }

    void paint (Graphics& g) override
    {
        if (customHeader.get() != nullptr)
            return;

        const Rectangle<int> area (getWidth(), getHeaderSize());
        g.reduceClipRegion (area);

        getLookAndFeel().drawConcertinaPanelHeader (g, area, isMouseOver(), isMouseButtonDown(),
                                                    getPanel(), *component);
    }

    void resized() override
    {
        auto bounds = getLocalBounds();
        auto headerBounds = bounds.removeFromTop (getHeaderSize());

        if (customHeader.get() != nullptr)
            customHeader.get()->setBounds (headerBounds);

        component->setBounds (bounds);
    }

    void mouseDown (const MouseEvent&) override
    {
        mouseDownY = getY();
        dragStartSizes = getPanel().getFittedSizes();
    }

    void mouseDrag (const MouseEvent& e) override
    {
        if (e.mouseWasDraggedSinceMouseDown())
        {
            auto& panel = getPanel();
            panel.setLayout (dragStartSizes.withMovedPanel (panel.holders.indexOf (this),
                                                            mouseDownY + e.getDistanceFromDragStartY(),
                                                            panel.getHeight()), false);
        }
    }

    void mouseDoubleClick (const MouseEvent&) override
    {
        getPanel().panelHeaderDoubleClicked (component);
    }

    void setCustomHeaderComponent (Component* headerComponent, bool shouldTakeOwnership)
    {
        customHeader = CustomHeader (this, OptionalScopedPointer (headerComponent, shouldTakeOwnership));
        addAndMakeVisible (headerComponent);
    }

    OptionalScopedPointer<Component> component;

private:
    PanelSizes dragStartSizes;
    int mouseDownY;

    struct CustomHeader
    {
        CustomHeader() = default;

        CustomHeader (MouseListener* l, OptionalScopedPointer<Component> c)
            : listener (l),
              customHeaderComponent (std::move (c))
        {
            if (customHeaderComponent != nullptr)
                customHeaderComponent->addMouseListener (listener, false);
        }

        CustomHeader (CustomHeader&& other) noexcept
            : listener (std::exchange (other.listener, nullptr)),
              customHeaderComponent (std::exchange (other.customHeaderComponent, {})) {}

        CustomHeader& operator= (CustomHeader&& other) noexcept
        {
            std::swap (other.listener, listener);
            std::swap (other.customHeaderComponent, customHeaderComponent);
            return *this;
        }

        CustomHeader (const CustomHeader& other) = delete;
        CustomHeader& operator= (const CustomHeader& other) = delete;

        ~CustomHeader() noexcept
        {
            if (customHeaderComponent != nullptr)
                customHeaderComponent->removeMouseListener (listener);
        }

        Component* get() const { return customHeaderComponent.get(); }

    private:
        MouseListener* listener = nullptr;
        OptionalScopedPointer<Component> customHeaderComponent;
    };

    CustomHeader customHeader;

    int getHeaderSize() const noexcept
    {
        ConcertinaPanel& panel = getPanel();
        auto ourIndex = panel.holders.indexOf (this);
        return panel.currentSizes->get (ourIndex).minSize;
    }

    ConcertinaPanel& getPanel() const
    {
        auto panel = dynamic_cast<ConcertinaPanel*> (getParentComponent());
        jassert (panel != nullptr);
        return *panel;
    }

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PanelHolder)
};

//==============================================================================
ConcertinaPanel::ConcertinaPanel()
    : currentSizes (new PanelSizes()),
      headerHeight (20)
{
}

ConcertinaPanel::~ConcertinaPanel() = default;

int ConcertinaPanel::getNumPanels() const noexcept
{
    return holders.size();
}

Component* ConcertinaPanel::getPanel (int index) const noexcept
{
    if (PanelHolder* h = holders[index])
        return h->component;

    return nullptr;
}

void ConcertinaPanel::addPanel (int insertIndex, Component* component, bool takeOwnership)
{
    jassert (component != nullptr); // can't use a null pointer here!
    jassert (indexOfComp (component) < 0); // You can't add the same component more than once!

    auto holder = new PanelHolder (component, takeOwnership);
    holders.insert (insertIndex, holder);
    currentSizes->sizes.insert (insertIndex, PanelSizes::Panel (headerHeight, headerHeight, std::numeric_limits<int>::max()));
    addAndMakeVisible (holder);
    resized();
}

void ConcertinaPanel::removePanel (Component* component)
{
    auto index = indexOfComp (component);

    if (index >= 0)
    {
        currentSizes->sizes.remove (index);
        holders.remove (index);
        resized();
    }
}

bool ConcertinaPanel::setPanelSize (Component* panelComponent, int height, bool animate)
{
    auto index = indexOfComp (panelComponent);
    jassert (index >= 0); // The specified component doesn't seem to have been added!

    height += currentSizes->get (index).minSize;
    auto oldSize = currentSizes->get (index).size;
    setLayout (currentSizes->withResizedPanel (index, height, getHeight()), animate);
    return oldSize != currentSizes->get (index).size;
}

bool ConcertinaPanel::expandPanelFully (Component* component, bool animate)
{
    return setPanelSize (component, getHeight(), animate);
}

void ConcertinaPanel::setMaximumPanelSize (Component* component, int maximumSize)
{
    auto index = indexOfComp (component);
    jassert (index >= 0); // The specified component doesn't seem to have been added!

    if (index >= 0)
    {
        currentSizes->get (index).maxSize = currentSizes->get (index).minSize + maximumSize;
        resized();
    }
}

void ConcertinaPanel::setPanelHeaderSize (Component* component, int headerSize)
{
    auto index = indexOfComp (component);
    jassert (index >= 0); // The specified component doesn't seem to have been added!

    if (index >= 0)
    {
        auto oldMin = currentSizes->get (index).minSize;

        currentSizes->get (index).minSize = headerSize;
        currentSizes->get (index).size += headerSize - oldMin;
        resized();
    }
}

void ConcertinaPanel::setCustomPanelHeader (Component* component, Component* customComponent, bool takeOwnership)
{
    OptionalScopedPointer<Component> optional (customComponent, takeOwnership);

    auto index = indexOfComp (component);
    jassert (index >= 0); // The specified component doesn't seem to have been added!

    if (index >= 0)
        holders.getUnchecked (index)->setCustomHeaderComponent (optional.release(), takeOwnership);
}

void ConcertinaPanel::resized()
{
    applyLayout (getFittedSizes(), false);
}

int ConcertinaPanel::indexOfComp (Component* comp) const noexcept
{
    for (int i = 0; i < holders.size(); ++i)
        if (holders.getUnchecked (i)->component == comp)
            return i;

    return -1;
}

ConcertinaPanel::PanelSizes ConcertinaPanel::getFittedSizes() const
{
    return currentSizes->fittedInto (getHeight());
}

void ConcertinaPanel::applyLayout (const PanelSizes& sizes, bool animate)
{
    if (! animate)
        animator.cancelAllAnimations (false);

    const int animationDuration = 150;
    auto w = getWidth();
    int y = 0;

    for (int i = 0; i < holders.size(); ++i)
    {
        PanelHolder& p = *holders.getUnchecked (i);

        auto h = sizes.get (i).size;
        const Rectangle<int> pos (0, y, w, h);

        if (animate)
            animator.animateComponent (&p, pos, 1.0f, animationDuration, false, 1.0, 1.0);
        else
            p.setBounds (pos);

        y += h;
    }
}

void ConcertinaPanel::setLayout (const PanelSizes& sizes, bool animate)
{
    *currentSizes = sizes;
    applyLayout (getFittedSizes(), animate);
}

void ConcertinaPanel::panelHeaderDoubleClicked (Component* component)
{
    if (! expandPanelFully (component, true))
        setPanelSize (component, 0, true);
}

//==============================================================================
std::unique_ptr<AccessibilityHandler> ConcertinaPanel::createAccessibilityHandler()
{
    return std::make_unique<AccessibilityHandler> (*this, AccessibilityRole::group);
}

} // namespace juce
