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

   This file is part of the JUCE library.
   Copyright (c) 2020 - 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 6 End-User License
   Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).

   End User License Agreement: www.juce.com/juce-6-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.

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

#pragma once

#include "../../Application/jucer_CommonHeaders.h"
#include "../../Application/jucer_Application.h"

//==============================================================================
class MessagesPopupWindow  : public Component,
                             private ComponentMovementWatcher
{
public:
    MessagesPopupWindow (Component& target, Component& parent, Project& project)
        : ComponentMovementWatcher (&parent),
          targetComponent (target),
          parentComponent (parent),
          messagesListComponent (*this, project)
    {
        parentComponent.addAndMakeVisible (this);
        setAlwaysOnTop (true);

        addAndMakeVisible (viewport);
        viewport.setScrollBarsShown (true, false);
        viewport.setViewedComponent (&messagesListComponent, false);
        viewport.setWantsKeyboardFocus (false);

        setOpaque (true);
    }

    void paint (Graphics& g) override
    {
        g.fillAll (findColour (secondaryBackgroundColourId));
    }

    void resized() override
    {
        viewport.setBounds (getLocalBounds());
    }

    bool isListShowing() const
    {
        return messagesListComponent.getRequiredHeight() > 0;
    }

    void updateBounds (bool animate)
    {
        auto targetBounds = parentComponent.getLocalArea (&targetComponent, targetComponent.getLocalBounds());

        auto height = jmin (messagesListComponent.getRequiredHeight(), maxHeight);
        auto yPos = jmax (indent, targetBounds.getY() - height);

        Rectangle<int> bounds (targetBounds.getX(), yPos,
                               jmin (width, parentComponent.getWidth() - targetBounds.getX() - indent), targetBounds.getY() - yPos);

        auto& animator = Desktop::getInstance().getAnimator();

        if (animate)
        {
            setBounds (bounds.withY (targetBounds.getY()));
            animator.animateComponent (this, bounds, 1.0f, 150, false, 1.0, 1.0);
        }
        else
        {
            if (animator.isAnimating (this))
                animator.cancelAnimation (this, false);

            setBounds (bounds);
        }

        messagesListComponent.resized();
    }

private:
    //==============================================================================
    class MessagesListComponent  : public Component,
                                   private ValueTree::Listener,
                                   private AsyncUpdater
    {
    public:
        MessagesListComponent (MessagesPopupWindow& o, Project& currentProject)
            : owner (o),
              project (currentProject)
        {
            messagesTree = project.getProjectMessages();
            messagesTree.addListener (this);

            setOpaque (true);

            messagesChanged();
        }

        void resized() override
        {
            auto bounds = getLocalBounds();
            auto numMessages = messages.size();

            for (size_t i = 0; i < numMessages; ++i)
            {
                messages[i]->setBounds (bounds.removeFromTop (messageHeight));

                if (numMessages > 1 && i != (numMessages - 1))
                    bounds.removeFromTop (messageSpacing);
            }
        }

        void paint (Graphics& g) override
        {
            g.fillAll (findColour (backgroundColourId).contrasting (0.2f));
        }

        int getRequiredHeight() const
        {
            auto numMessages = (int) messages.size();

            if (numMessages > 0)
                return (numMessages * messageHeight) + ((numMessages - 1) * messageSpacing);

            return 0;
        }

        void updateSize (int parentWidth)
        {
            setSize (parentWidth, getRequiredHeight());
        }

    private:
        static constexpr int messageHeight = 65;
        static constexpr int messageSpacing = 2;

        //==============================================================================
        struct MessageComponent  : public Component
        {
            MessageComponent (MessagesListComponent& listComponent,
                              const Identifier& messageToDisplay,
                              std::vector<ProjectMessages::MessageAction> messageActions)
               : message (messageToDisplay)
            {
                for (auto& action : messageActions)
                {
                    auto button = std::make_unique<TextButton> (action.first);
                    addAndMakeVisible (*button);
                    button->onClick = action.second;

                    buttons.push_back (std::move (button));
                }

                icon = (ProjectMessages::getTypeForMessage (message) == ProjectMessages::Ids::warning ? getIcons().warning : getIcons().info);

                messageTitleLabel.setText (ProjectMessages::getTitleForMessage (message), dontSendNotification);
                messageTitleLabel.setFont (Font (11.0f).boldened());
                addAndMakeVisible (messageTitleLabel);

                messageDescriptionLabel.setText (ProjectMessages::getDescriptionForMessage (message), dontSendNotification);
                messageDescriptionLabel.setFont (Font (11.0f));
                messageDescriptionLabel.setJustificationType (Justification::topLeft);
                addAndMakeVisible (messageDescriptionLabel);

                dismissButton.setShape (getLookAndFeel().getCrossShape (1.0f), false, true, false);
                addAndMakeVisible (dismissButton);

                dismissButton.onClick = [this, &listComponent]
                {
                    listComponent.messagesTree.getChildWithName (ProjectMessages::getTypeForMessage (message))
                                              .getChildWithName (message)
                                              .setProperty (ProjectMessages::Ids::isVisible, false, nullptr);
                };
            }

            void paint (Graphics& g) override
            {
                g.fillAll (findColour (secondaryBackgroundColourId).contrasting (0.1f));

                auto bounds = getLocalBounds().reduced (5);

                g.setColour (findColour (defaultIconColourId));
                g.fillPath (icon, icon.getTransformToScaleToFit (bounds.removeFromTop (messageTitleHeight)
                                                                       .removeFromLeft (messageTitleHeight).toFloat(), true));
            }

            void resized() override
            {
                auto bounds = getLocalBounds().reduced (5);

                auto topSlice = bounds.removeFromTop (messageTitleHeight);

                topSlice.removeFromLeft (messageTitleHeight + 5);
                topSlice.removeFromRight (5);

                dismissButton.setBounds (topSlice.removeFromRight (messageTitleHeight));
                messageTitleLabel.setBounds (topSlice);
                bounds.removeFromTop (5);

                auto numButtons = (int) buttons.size();

                if (numButtons > 0)
                {
                    auto buttonBounds = bounds.removeFromBottom (buttonHeight);

                    auto buttonWidth = roundToInt ((float) buttonBounds.getWidth() / 3.5f);
                    auto requiredWidth = (numButtons * buttonWidth) + ((numButtons - 1) * buttonSpacing);
                    buttonBounds.reduce ((buttonBounds.getWidth() - requiredWidth) / 2, 0);

                    for (auto& b : buttons)
                    {
                        b->setBounds (buttonBounds.removeFromLeft (buttonWidth));
                        buttonBounds.removeFromLeft (buttonSpacing);
                    }

                    bounds.removeFromBottom (5);
                }

                messageDescriptionLabel.setBounds (bounds);
            }

            static constexpr int messageTitleHeight = 11;
            static constexpr int buttonHeight = messageHeight / 4;
            static constexpr int buttonSpacing = 5;

            Identifier message;

            Path icon;
            Label messageTitleLabel, messageDescriptionLabel;
            std::vector<std::unique_ptr<TextButton>> buttons;
            ShapeButton dismissButton { {},
                                        findColour (treeIconColourId),
                                        findColour (treeIconColourId).overlaidWith (findColour (defaultHighlightedTextColourId).withAlpha (0.2f)),
                                        findColour (treeIconColourId).overlaidWith (findColour (defaultHighlightedTextColourId).withAlpha (0.4f)) };

            JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MessageComponent)
        };

        //==============================================================================
        void valueTreePropertyChanged   (ValueTree&, const Identifier&) override  { messagesChanged(); }
        void valueTreeChildAdded        (ValueTree&, ValueTree&)        override  { messagesChanged(); }
        void valueTreeChildRemoved      (ValueTree&, ValueTree&, int)   override  { messagesChanged(); }
        void valueTreeChildOrderChanged (ValueTree&, int, int)          override  { messagesChanged(); }
        void valueTreeParentChanged     (ValueTree&)                    override  { messagesChanged(); }
        void valueTreeRedirected        (ValueTree&)                    override  { messagesChanged(); }

        void handleAsyncUpdate() override
        {
            messagesChanged();
        }

        void messagesChanged()
        {
            auto listWasShowing = (getHeight() > 0);

            auto warningsTree = messagesTree.getChildWithName (ProjectMessages::Ids::warning);
            auto notificationsTree = messagesTree.getChildWithName (ProjectMessages::Ids::notification);

            auto removePredicate = [warningsTree, notificationsTree] (std::unique_ptr<MessageComponent>& messageComponent)
            {
                for (int i = 0; i < warningsTree.getNumChildren(); ++i)
                {
                    auto child = warningsTree.getChild (i);

                    if (child.getType() == messageComponent->message
                        && child.getProperty (ProjectMessages::Ids::isVisible))
                    {
                        return false;
                    }
                }

                for (int i = 0; i < notificationsTree.getNumChildren(); ++i)
                {
                    auto child = notificationsTree.getChild (i);

                    if (child.getType() == messageComponent->message
                        && child.getProperty (ProjectMessages::Ids::isVisible))
                    {
                        return false;
                    }
                }

                return true;
            };

            messages.erase (std::remove_if (std::begin (messages), std::end (messages), removePredicate),
                                            std::end (messages));

            for (int i = 0; i < warningsTree.getNumChildren(); ++i)
            {
                auto child = warningsTree.getChild (i);

                if (! child.getProperty (ProjectMessages::Ids::isVisible))
                    continue;

                if (std::find_if (std::begin (messages), std::end (messages),
                                  [child] (const std::unique_ptr<MessageComponent>& messageComponent) { return messageComponent->message == child.getType(); })
                    == std::end (messages))
                {
                    messages.push_back (std::make_unique<MessageComponent> (*this, child.getType(), project.getMessageActions (child.getType())));
                    addAndMakeVisible (*messages.back());
                }
            }

            for (int i = 0; i < notificationsTree.getNumChildren(); ++i)
            {
                auto child = notificationsTree.getChild (i);

                if (! child.getProperty (ProjectMessages::Ids::isVisible))
                    continue;

                if (std::find_if (std::begin (messages), std::end (messages),
                                  [child] (const std::unique_ptr<MessageComponent>& messageComponent) { return messageComponent->message == child.getType(); })
                    == std::end (messages))
                {
                    messages.push_back (std::make_unique<MessageComponent> (*this, child.getType(), project.getMessageActions (child.getType())));
                    addAndMakeVisible (*messages.back());
                }
            }

            auto isNowShowing = (messages.size() > 0);

            owner.updateBounds (isNowShowing != listWasShowing);
            updateSize (owner.getWidth());
        }

        //==============================================================================
        MessagesPopupWindow& owner;
        Project& project;

        ValueTree messagesTree;
        std::vector<std::unique_ptr<MessageComponent>> messages;

        JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MessagesListComponent)
    };

    //==============================================================================
    void componentMovedOrResized (bool, bool) override
    {
        if (isListShowing())
            updateBounds (false);
    }

    using ComponentMovementWatcher::componentMovedOrResized;

    void componentPeerChanged() override
    {
        if (isListShowing())
            updateBounds (false);
    }

    void componentVisibilityChanged() override
    {
        if (isListShowing())
            updateBounds (false);
    }

    using ComponentMovementWatcher::componentVisibilityChanged;

    //==============================================================================
    static constexpr int maxHeight = 500, width = 350, indent = 20;

    Component& targetComponent;
    Component& parentComponent;

    Viewport viewport;
    MessagesListComponent messagesListComponent;

    //==============================================================================
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MessagesPopupWindow)
};

//==============================================================================
class ProjectMessagesComponent  : public Component
{
public:
    ProjectMessagesComponent()
    {
        setFocusContainerType (FocusContainerType::focusContainer);
        setTitle ("Project Messages");

        addAndMakeVisible (warningsComponent);
        addAndMakeVisible (notificationsComponent);

        warningsComponent.addMouseListener (this, true);
        notificationsComponent.addMouseListener (this, true);

        setOpaque (true);
    }

    //==============================================================================
    void resized() override
    {
        auto b = getLocalBounds();

        warningsComponent.setBounds (b.removeFromLeft (b.getWidth() / 2).reduced (5));
        notificationsComponent.setBounds (b.reduced (5));
    }

    void paint (Graphics& g) override
    {
        auto backgroundColour = findColour (backgroundColourId);

        if (isMouseDown || isMouseOver)
            backgroundColour = backgroundColour.overlaidWith (findColour (defaultHighlightColourId)
                                                                 .withAlpha (isMouseDown ? 1.0f : 0.8f));

        g.fillAll (backgroundColour);
    }

    //==============================================================================
    void mouseEnter (const MouseEvent&) override
    {
        isMouseOver = true;
        repaint();
    }

    void mouseExit (const MouseEvent&) override
    {
        isMouseOver = false;
        repaint();
    }

    void mouseDown (const MouseEvent&) override
    {
        isMouseDown = true;
        repaint();
    }

    void mouseUp (const MouseEvent&) override
    {
        isMouseDown = false;
        repaint();

        showOrHideMessagesWindow();
    }

    std::unique_ptr<AccessibilityHandler> createAccessibilityHandler() override
    {
        return std::make_unique<AccessibilityHandler> (*this,
                                                       AccessibilityRole::button,
                                                       AccessibilityActions().addAction (AccessibilityActionType::press,
                                                                                         [this] { showOrHideMessagesWindow(); }));
    }

    //==============================================================================
    void setProject (Project* newProject)
    {
        if (currentProject != newProject)
        {
            currentProject = newProject;

            if (currentProject != nullptr)
            {
                if (auto* projectWindow = ProjucerApplication::getApp().mainWindowList.getMainWindowForFile (currentProject->getFile()))
                    messagesWindow = std::make_unique<MessagesPopupWindow> (*this, *projectWindow, *currentProject);

                auto projectMessagesTree = currentProject->getProjectMessages();

                warningsComponent.setTree (projectMessagesTree.getChildWithName (ProjectMessages::Ids::warning));
                notificationsComponent.setTree (projectMessagesTree.getChildWithName (ProjectMessages::Ids::notification));
            }
            else
            {
                warningsComponent.setTree ({});
                notificationsComponent.setTree ({});
            }
        }
    }

    void numMessagesChanged()
    {
        const auto total = warningsComponent.getNumMessages()
                           + notificationsComponent.getNumMessages();

        setHelpText (String (total) + (total == 1 ? " message" : " messages"));
    }

    void showOrHideMessagesWindow()
    {
        if (messagesWindow != nullptr)
            showOrHideAllMessages (! messagesWindow->isListShowing());
    }

private:
    //==============================================================================
    struct MessageCountComponent  : public Component,
                                    private ValueTree::Listener
    {
        MessageCountComponent (ProjectMessagesComponent& o, Path pathToUse)
          : owner (o),
            path (pathToUse)
        {
            setInterceptsMouseClicks (false, false);
        }

        void paint (Graphics& g) override
        {
            auto b = getLocalBounds().toFloat();

            g.setColour (findColour ((owner.isMouseDown || owner.isMouseOver) ? defaultHighlightedTextColourId : treeIconColourId));
            g.fillPath (path, path.getTransformToScaleToFit (b.removeFromLeft (b.getWidth() / 2.0f), true));

            b.removeFromLeft (5);
            g.drawFittedText (String (numMessages), b.getSmallestIntegerContainer(), Justification::centredLeft, 1);
        }

        void setTree (ValueTree tree)
        {
            messagesTree = tree;

            if (messagesTree.isValid())
                messagesTree.addListener (this);

            updateNumMessages();
        }

        void updateNumMessages()
        {
            numMessages = messagesTree.getNumChildren();
            owner.numMessagesChanged();
            repaint();
        }

        int getNumMessages() const noexcept  { return numMessages; }

    private:
        void valueTreeChildAdded   (ValueTree&, ValueTree&)        override  { updateNumMessages(); }
        void valueTreeChildRemoved (ValueTree&, ValueTree&, int)   override  { updateNumMessages(); }

        ProjectMessagesComponent& owner;
        ValueTree messagesTree;

        Path path;
        int numMessages = 0;
    };

    void showOrHideAllMessages (bool shouldBeVisible)
    {
        if (currentProject != nullptr)
        {
            auto messagesTree = currentProject->getProjectMessages();

            auto setVisible = [shouldBeVisible] (ValueTree subTree)
            {
                for (int i = 0; i < subTree.getNumChildren(); ++i)
                    subTree.getChild (i).setProperty (ProjectMessages::Ids::isVisible, shouldBeVisible, nullptr);
            };

            setVisible (messagesTree.getChildWithName (ProjectMessages::Ids::warning));
            setVisible (messagesTree.getChildWithName (ProjectMessages::Ids::notification));
        }
    }

    //==============================================================================
    Project* currentProject = nullptr;
    bool isMouseOver = false, isMouseDown = false;

    MessageCountComponent warningsComponent { *this, getIcons().warning },
                          notificationsComponent { *this, getIcons().info };

    std::unique_ptr<MessagesPopupWindow> messagesWindow;

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ProjectMessagesComponent)
};
