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

   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.

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

#include "../../Application/jucer_Headers.h"
#include "../../Application/jucer_Application.h"
#include "jucer_ComponentLayoutEditor.h"
#include "../UI/jucer_JucerCommandIDs.h"
#include "../jucer_ObjectTypes.h"
#include "../Components/jucer_JucerComponentHandler.h"

//==============================================================================
class SubComponentHolderComp    : public Component
{
public:
    SubComponentHolderComp (JucerDocument& doc,
                            SnapGridPainter& g)
       : document (doc), grid (g),
         dontFillBackground (false)
    {
        setInterceptsMouseClicks (false, false);
        setWantsKeyboardFocus (false);
        setFocusContainerType (FocusContainerType::keyboardFocusContainer);
    }

    void paint (Graphics& g) override
    {
        if (! dontFillBackground)
        {
            if (PaintRoutine* const background = document.getPaintRoutine (0))
            {
                background->fillWithBackground (g, false);
                background->drawElements (g, getLocalBounds());

                grid.draw (g, background);
            }
            else
            {
                grid.draw (g, nullptr);
            }
        }
    }

    void resized() override
    {
        if (! getBounds().isEmpty())
        {
            int numTimesToTry = 10;

            while (--numTimesToTry >= 0)
            {
                bool anyCompsMoved = false;

                for (int i = 0; i < getNumChildComponents(); ++i)
                {
                    Component* comp = getChildComponent (i);

                    if (ComponentTypeHandler* const type = ComponentTypeHandler::getHandlerFor (*comp))
                    {
                        const Rectangle<int> newBounds (type->getComponentPosition (comp)
                                                          .getRectangle (getLocalBounds(),
                                                                         document.getComponentLayout()));

                        anyCompsMoved = anyCompsMoved || (comp->getBounds() != newBounds);
                        comp->setBounds (newBounds);
                    }
                }

                // repeat this loop until they've all stopped shuffling (might require a few
                // loops for all the relative positioned comps to settle down)
                if (! anyCompsMoved)
                    break;
            }
        }
    }

    void moved() override
    {
        ((ComponentLayoutEditor*) getParentComponent())->updateOverlayPositions();
    }

    JucerDocument& document;
    SnapGridPainter& grid;

    bool dontFillBackground;
};

//==============================================================================
ComponentLayoutEditor::ComponentLayoutEditor (JucerDocument& doc, ComponentLayout& cl)
    : document (doc), layout (cl), firstResize (true)
{
    setWantsKeyboardFocus (true);

    addAndMakeVisible (subCompHolder = new SubComponentHolderComp (document, grid));

    refreshAllComponents();

    setSize (document.getInitialWidth(),
             document.getInitialHeight());
}

ComponentLayoutEditor::~ComponentLayoutEditor()
{
    document.removeChangeListener (this);

    removeChildComponent (&lassoComp);
    deleteAllChildren();
}

//==============================================================================
void ComponentLayoutEditor::visibilityChanged()
{
    document.beginTransaction();

    if (isVisible())
    {
        refreshAllComponents();
        document.addChangeListener (this);
    }
    else
    {
        document.removeChangeListener (this);
    }
}

void ComponentLayoutEditor::changeListenerCallback (ChangeBroadcaster*)
{
    refreshAllComponents();
}

void ComponentLayoutEditor::paint (Graphics&)
{
}

void ComponentLayoutEditor::resized()
{
    if (firstResize && getWidth() > 0 && getHeight() > 0)
    {
        firstResize = false;
        refreshAllComponents();
    }

    subCompHolder->setBounds (getComponentArea());
    updateOverlayPositions();
}

Rectangle<int> ComponentLayoutEditor::getComponentArea() const
{
    const int editorEdgeGap = 4;

    if (document.isFixedSize())
        return Rectangle<int> ((getWidth() - document.getInitialWidth()) / 2,
                               (getHeight() - document.getInitialHeight()) / 2,
                               document.getInitialWidth(),
                               document.getInitialHeight());

    return Rectangle<int> (editorEdgeGap, editorEdgeGap,
                           getWidth() - editorEdgeGap * 2,
                           getHeight() - editorEdgeGap * 2);
}

Image ComponentLayoutEditor::createComponentLayerSnapshot() const
{
    ((SubComponentHolderComp*) subCompHolder)->dontFillBackground = true;
    Image im = subCompHolder->createComponentSnapshot (Rectangle<int> (0, 0, subCompHolder->getWidth(), subCompHolder->getHeight()));
    ((SubComponentHolderComp*) subCompHolder)->dontFillBackground = false;

    return im;
}

void ComponentLayoutEditor::updateOverlayPositions()
{
    for (int i = getNumChildComponents(); --i >= 0;)
        if (ComponentOverlayComponent* const overlay = dynamic_cast<ComponentOverlayComponent*> (getChildComponent (i)))
            overlay->updateBoundsToMatchTarget();
}

void ComponentLayoutEditor::refreshAllComponents()
{
    for (int i = getNumChildComponents(); --i >= 0;)
    {
        std::unique_ptr<ComponentOverlayComponent> overlay (dynamic_cast<ComponentOverlayComponent*> (getChildComponent (i)));

        if (overlay != nullptr && layout.containsComponent (overlay->target))
            overlay.release();
    }

    for (int i = subCompHolder->getNumChildComponents(); --i >= 0;)
    {
        Component* const comp = subCompHolder->getChildComponent (i);

        if (! layout.containsComponent (comp))
            subCompHolder->removeChildComponent (comp);
    }

    Component* lastComp = nullptr;
    Component* lastOverlay = nullptr;

    for (int i = layout.getNumComponents(); --i >= 0;)
    {
        auto c = layout.getComponent (i);
        jassert (c != nullptr);

        auto overlay = getOverlayCompFor (c);

        bool isNewOverlay = false;

        if (overlay == nullptr)
        {
            auto handler = ComponentTypeHandler::getHandlerFor (*c);
            jassert (handler != nullptr);

            overlay = handler->createOverlayComponent (c, layout);

            addAndMakeVisible (overlay);
            isNewOverlay = true;
        }

        if (lastOverlay != nullptr)
            overlay->toBehind (lastOverlay);
        else
            overlay->toFront (false);

        lastOverlay = overlay;

        subCompHolder->addAndMakeVisible (c);

        if (lastComp != nullptr)
            c->toBehind (lastComp);
        else
            c->toFront (false);

        lastComp = c;

        c->setWantsKeyboardFocus (false);
        c->setFocusContainerType (FocusContainerType::keyboardFocusContainer);

        if (isNewOverlay)
            overlay->updateBoundsToMatchTarget();
    }

    if (grid.updateFromDesign (document))
        subCompHolder->repaint();

    subCompHolder->setBounds (getComponentArea());
    subCompHolder->resized();
}

void ComponentLayoutEditor::mouseDown (const MouseEvent& e)
{
    if (e.mods.isPopupMenu())
    {
        auto commandManager = &ProjucerApplication::getCommandManager();

        PopupMenu m;

        m.addCommandItem (commandManager, JucerCommandIDs::editCompLayout);
        m.addCommandItem (commandManager, JucerCommandIDs::editCompGraphics);
        m.addSeparator();

        for (int i = 0; i < ObjectTypes::numComponentTypes; ++i)
            m.addCommandItem (commandManager, JucerCommandIDs::newComponentBase + i);

        m.showMenuAsync (PopupMenu::Options());
    }
    else
    {
        addChildComponent (lassoComp);
        lassoComp.beginLasso (e, this);
    }
}

void ComponentLayoutEditor::mouseDrag (const MouseEvent& e)
{
    lassoComp.toFront (false);
    lassoComp.dragLasso (e);
}

void ComponentLayoutEditor::mouseUp (const MouseEvent& e)
{
    lassoComp.endLasso();
    removeChildComponent (&lassoComp);

    if (! (e.mouseWasDraggedSinceMouseDown() || e.mods.isAnyModifierKeyDown()))
        layout.getSelectedSet().deselectAll();
}

static void moveOrStretch (ComponentLayout& layout, int x, int y, bool snap, bool stretch)
{
    if (stretch)
        layout.stretchSelectedComps (x, y, snap);
    else
        layout.moveSelectedComps (x, y, snap);
}

bool ComponentLayoutEditor::keyPressed (const KeyPress& key)
{
    const bool snap = key.getModifiers().isAltDown();
    const bool stretch = key.getModifiers().isShiftDown();

    const int amount = snap ? document.getSnappingGridSize() + 1
                            : 1;

    if (key.isKeyCode (KeyPress::rightKey))
    {
        moveOrStretch (layout, amount, 0, snap, stretch);
    }
    else if (key.isKeyCode (KeyPress::downKey))
    {
        moveOrStretch (layout, 0,amount, snap, stretch);
    }
    else if (key.isKeyCode (KeyPress::leftKey))
    {
        moveOrStretch (layout, -amount, 0, snap, stretch);
    }
    else if (key.isKeyCode (KeyPress::upKey))
    {
        moveOrStretch (layout, 0, -amount, snap, stretch);
    }
    else
    {
        return false;
    }

    return true;
}

bool ComponentLayoutEditor::isInterestedInFileDrag (const StringArray& filenames)
{
    const File f (filenames [0]);
    return f.hasFileExtension (cppFileExtensions);
}

void ComponentLayoutEditor::filesDropped (const StringArray& filenames, int x, int y)
{
    const File f (filenames [0]);

    if (JucerDocument::isValidJucerCppFile (f))
    {
        JucerComponentHandler jucerDocHandler;
        layout.getDocument()->beginTransaction();

        if (TestComponent* newOne = dynamic_cast<TestComponent*> (layout.addNewComponent (&jucerDocHandler,
                                                                                          x - subCompHolder->getX(),
                                                                                          y - subCompHolder->getY())))
        {
            JucerComponentHandler::setJucerComponentFile (*layout.getDocument(), newOne,
                                                          f.getRelativePathFrom (document.getCppFile().getParentDirectory()));
            layout.getSelectedSet().selectOnly (newOne);
        }

        layout.getDocument()->beginTransaction();
    }
}

bool ComponentLayoutEditor::isInterestedInDragSource (const SourceDetails& dragSourceDetails)
{
    if (dragSourceDetails.description != projectItemDragType)
        return false;

    OwnedArray<Project::Item> selectedNodes;
    ProjectContentComponent::getSelectedProjectItemsBeingDragged (dragSourceDetails, selectedNodes);

    return selectedNodes.size() > 0;
}

void ComponentLayoutEditor::itemDropped (const SourceDetails& dragSourceDetails)
{
    OwnedArray<Project::Item> selectedNodes;
    ProjectContentComponent::getSelectedProjectItemsBeingDragged (dragSourceDetails, selectedNodes);

    StringArray filenames;

    for (int i = 0; i < selectedNodes.size(); ++i)
        if (selectedNodes.getUnchecked(i)->getFile().hasFileExtension (cppFileExtensions))
            filenames.add (selectedNodes.getUnchecked(i)->getFile().getFullPathName());

    filesDropped (filenames, dragSourceDetails.localPosition.x, dragSourceDetails.localPosition.y);
}

ComponentOverlayComponent* ComponentLayoutEditor::getOverlayCompFor (Component* compToFind) const
{
    for (int i = getNumChildComponents(); --i >= 0;)
    {
        if (ComponentOverlayComponent* const overlay = dynamic_cast<ComponentOverlayComponent*> (getChildComponent (i)))
            if (overlay->target == compToFind)
                return overlay;
    }

    return nullptr;
}

void ComponentLayoutEditor::findLassoItemsInArea (Array <Component*>& results, const Rectangle<int>& area)
{
    const Rectangle<int> lasso (area - subCompHolder->getPosition());

    for (int i = 0; i < subCompHolder->getNumChildComponents(); ++i)
    {
        Component* c = subCompHolder->getChildComponent (i);

        if (c->getBounds().intersects (lasso))
            results.add (c);
    }
}

SelectedItemSet <Component*>& ComponentLayoutEditor::getLassoSelection()
{
    return layout.getSelectedSet();
}
