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

   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::detail
{

constexpr char colourPropertyPrefix[] = "jcclr_";

//==============================================================================
struct ComponentHelpers
{
    using SH = ScalingHelpers;

   #if JUCE_MODAL_LOOPS_PERMITTED
    static void* runModalLoopCallback (void* userData)
    {
        return (void*) (pointer_sized_int) static_cast<Component*> (userData)->runModalLoop();
    }
   #endif

    static Identifier getColourPropertyID (int colourID)
    {
        char buffer[32];
        auto* end = buffer + numElementsInArray (buffer) - 1;
        auto* t = end;
        *t = 0;

        for (auto v = (uint32) colourID;;)
        {
            *--t = "0123456789abcdef" [v & 15];
            v >>= 4;

            if (v == 0)
                break;
        }

        for (int i = (int) sizeof (colourPropertyPrefix) - 1; --i >= 0;)
            *--t = colourPropertyPrefix[i];

        return t;
    }

    //==============================================================================
    static bool hitTest (Component& comp, Point<float> localPoint)
    {
        const auto intPoint = localPoint.roundToInt();
        return Rectangle<int> { comp.getWidth(), comp.getHeight() }.contains (intPoint)
            && comp.hitTest (intPoint.x, intPoint.y);
    }

    // converts an unscaled position within a peer to the local position within that peer's component
    template <typename PointOrRect>
    static PointOrRect rawPeerPositionToLocal (const Component& comp, PointOrRect pos) noexcept
    {
        if (comp.isTransformed())
            pos = pos.transformedBy (comp.getTransform().inverted());

        return SH::unscaledScreenPosToScaled (comp, pos);
    }

    // converts a position within a peer's component to the unscaled position within the peer
    template <typename PointOrRect>
    static PointOrRect localPositionToRawPeerPos (const Component& comp, PointOrRect pos) noexcept
    {
        if (comp.isTransformed())
            pos = pos.transformedBy (comp.getTransform());

        return SH::scaledScreenPosToUnscaled (comp, pos);
    }

    template <typename PointOrRect>
    static PointOrRect convertFromParentSpace (const Component& comp, const PointOrRect pointInParentSpace)
    {
        const auto transformed = comp.affineTransform != nullptr ? pointInParentSpace.transformedBy (comp.affineTransform->inverted())
                                                                 : pointInParentSpace;

        if (comp.isOnDesktop())
        {
            if (auto* peer = comp.getPeer())
                return SH::unscaledScreenPosToScaled (comp, peer->globalToLocal (SH::scaledScreenPosToUnscaled (transformed)));

            jassertfalse;
            return transformed;
        }

        if (comp.getParentComponent() == nullptr)
            return SH::subtractPosition (SH::unscaledScreenPosToScaled (comp, SH::scaledScreenPosToUnscaled (transformed)), comp);

        return SH::subtractPosition (transformed, comp);
    }

    template <typename PointOrRect>
    static PointOrRect convertToParentSpace (const Component& comp, const PointOrRect pointInLocalSpace)
    {
        const auto preTransform = [&]
        {
            if (comp.isOnDesktop())
            {
                if (auto* peer = comp.getPeer())
                    return SH::unscaledScreenPosToScaled (peer->localToGlobal (SH::scaledScreenPosToUnscaled (comp, pointInLocalSpace)));

                jassertfalse;
                return pointInLocalSpace;
            }

            if (comp.getParentComponent() == nullptr)
                return SH::unscaledScreenPosToScaled (SH::scaledScreenPosToUnscaled (comp, SH::addPosition (pointInLocalSpace, comp)));

            return SH::addPosition (pointInLocalSpace, comp);
        }();

        return comp.affineTransform != nullptr ? preTransform.transformedBy (*comp.affineTransform)
                                               : preTransform;
    }

    template <typename PointOrRect>
    static PointOrRect convertFromDistantParentSpace (const Component* parent, const Component& target, PointOrRect coordInParent)
    {
        auto* directParent = target.getParentComponent();
        jassert (directParent != nullptr);

        if (directParent == parent)
            return convertFromParentSpace (target, coordInParent);

        JUCE_BEGIN_IGNORE_WARNINGS_MSVC (6011)
        return convertFromParentSpace (target, convertFromDistantParentSpace (parent, *directParent, coordInParent));
        JUCE_END_IGNORE_WARNINGS_MSVC
    }

    template <typename PointOrRect>
    static PointOrRect convertCoordinate (const Component* target, const Component* source, PointOrRect p)
    {
        while (source != nullptr)
        {
            if (source == target)
                return p;

            JUCE_BEGIN_IGNORE_WARNINGS_MSVC (6011)

            if (source->isParentOf (target))
                return convertFromDistantParentSpace (source, *target, p);

            JUCE_END_IGNORE_WARNINGS_MSVC

            p = convertToParentSpace (*source, p);
            source = source->getParentComponent();
        }

        jassert (source == nullptr);
        if (target == nullptr)
            return p;

        auto* topLevelComp = target->getTopLevelComponent();

        p = convertFromParentSpace (*topLevelComp, p);

        if (topLevelComp == target)
            return p;

        return convertFromDistantParentSpace (topLevelComp, *target, p);
    }

    static bool clipObscuredRegions (const Component& comp, Graphics& g,
                                     const Rectangle<int> clipRect, Point<int> delta)
    {
        bool wasClipped = false;

        for (int i = comp.childComponentList.size(); --i >= 0;)
        {
            auto& child = *comp.childComponentList.getUnchecked (i);

            if (child.isVisible() && ! child.isTransformed())
            {
                auto newClip = clipRect.getIntersection (child.boundsRelativeToParent);

                if (! newClip.isEmpty())
                {
                    if (child.isOpaque() && child.componentTransparency == 0)
                    {
                        g.excludeClipRegion (newClip + delta);
                        wasClipped = true;
                    }
                    else
                    {
                        auto childPos = child.getPosition();

                        if (clipObscuredRegions (child, g, newClip - childPos, childPos + delta))
                            wasClipped = true;
                    }
                }
            }
        }

        return wasClipped;
    }

    static Rectangle<int> getParentOrMainMonitorBounds (const Component& comp)
    {
        if (auto* p = comp.getParentComponent())
            return p->getLocalBounds();

        return Desktop::getInstance().getDisplays().getPrimaryDisplay()->userArea;
    }

    static void releaseAllCachedImageResources (Component& c)
    {
        if (auto* cached = c.getCachedComponentImage())
            cached->releaseResources();

        for (auto* child : c.childComponentList)
            releaseAllCachedImageResources (*child);
    }

    //==============================================================================
    static bool modalWouldBlockComponent (const Component& maybeBlocked, Component* modal)
    {
        return modal != nullptr
            && modal != &maybeBlocked
            && ! modal->isParentOf (&maybeBlocked)
            && ! modal->canModalEventBeSentToComponent (&maybeBlocked);
    }

    template <typename Function>
    static void sendMouseEventToComponentsThatAreBlockedByModal (Component& modal, Function&& function)
    {
        for (auto& ms : Desktop::getInstance().getMouseSources())
            if (auto* c = ms.getComponentUnderMouse())
                if (modalWouldBlockComponent (*c, &modal))
                    (c->*function) (ms, SH::screenPosToLocalPos (*c, ms.getScreenPosition()), Time::getCurrentTime());
    }

    class ModalComponentManagerChangeNotifier
    {
    public:
        static auto& getInstance()
        {
            static ModalComponentManagerChangeNotifier instance;
            return instance;
        }

        ErasedScopeGuard addListener (std::function<void()> l)
        {
            return listeners.addListener (std::move (l));
        }

        void modalComponentManagerChanged()
        {
            listeners.call();
        }

    private:
        ModalComponentManagerChangeNotifier() = default;

        detail::CallbackListenerList<> listeners;
    };
};

} // namespace juce::detail
