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

   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 "jucer_PaintRoutine.h"
#include "jucer_JucerDocument.h"
#include "jucer_ObjectTypes.h"
#include "PaintElements/jucer_PaintElementUndoableAction.h"
#include "PaintElements/jucer_PaintElementPath.h"
#include "PaintElements/jucer_PaintElementImage.h"
#include "PaintElements/jucer_PaintElementGroup.h"
#include "UI/jucer_JucerDocumentEditor.h"
#include "../Application/jucer_Application.h"

//==============================================================================
PaintRoutine::PaintRoutine()
    : document (nullptr),
      backgroundColour (ProjucerApplication::getApp().lookAndFeel.findColour (backgroundColourId))
{
    clear();
}

PaintRoutine::~PaintRoutine()
{
    elements.clear(); // do this explicitly before the scalar destructor because these
                      // objects will be listeners on this object
}

//==============================================================================
void PaintRoutine::changed()
{
    if (document != nullptr)
        document->changed();
}

bool PaintRoutine::perform (UndoableAction* action, const String& actionName)
{
    if (document != nullptr)
        return document->getUndoManager().perform (action, actionName);

    std::unique_ptr<UndoableAction> deleter (action);
    action->perform();
    return false;
}

void PaintRoutine::setBackgroundColour (Colour newColour) noexcept
{
    backgroundColour = newColour;
    changed();
}

void PaintRoutine::clear()
{
    if (elements.size() > 0)
    {
        elements.clear();
        changed();
    }
}

//==============================================================================
class AddXmlElementAction   : public UndoableAction
{
public:
    AddXmlElementAction (PaintRoutine& routine_, XmlElement* xml_)
        : routine (routine_), xml (xml_)
    {
    }

    bool perform()
    {
        showCorrectTab();
        PaintElement* newElement = routine.addElementFromXml (*xml, -1, false);
        jassert (newElement != nullptr);

        indexAdded = routine.indexOfElement (newElement);
        jassert (indexAdded >= 0);
        return indexAdded >= 0;
    }

    bool undo()
    {
        showCorrectTab();
        routine.removeElement (routine.getElement (indexAdded), false);
        return true;
    }

    int getSizeInUnits()    { return 10; }

    int indexAdded;

private:
    PaintRoutine& routine;
    std::unique_ptr<XmlElement> xml;

    void showCorrectTab() const
    {
        if (JucerDocumentEditor* const ed = JucerDocumentEditor::getActiveDocumentHolder())
            ed->showGraphics (&routine);
    }

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AddXmlElementAction)
};

PaintElement* PaintRoutine::addElementFromXml (const XmlElement& xml, const int index, const bool undoable)
{
    selectedPoints.deselectAll();

    if (undoable && document != nullptr)
    {
        AddXmlElementAction* action = new AddXmlElementAction (*this, new XmlElement (xml));
        document->getUndoManager().perform (action, "Add new element");

        return elements [action->indexAdded];
    }

    if (PaintElement* const newElement = ObjectTypes::createElementForXml (&xml, this))
    {
        elements.insert (index, newElement);
        changed();

        return newElement;
    }

    return nullptr;
}

PaintElement* PaintRoutine::addNewElement (PaintElement* e, const int index, const bool undoable)
{
    if (e != nullptr)
    {
        std::unique_ptr<PaintElement> deleter (e);
        std::unique_ptr<XmlElement> xml (e->createXml());

        e = addElementFromXml (*xml, index, undoable);
    }

    return e;
}

//==============================================================================
class DeleteElementAction   : public PaintElementUndoableAction <PaintElement>
{
public:
    explicit DeleteElementAction (PaintElement* const element)
        : PaintElementUndoableAction <PaintElement> (element),
          oldIndex (-1)
    {
        xml.reset (element->createXml());
        oldIndex = routine.indexOfElement (element);
    }

    bool perform()
    {
        showCorrectTab();
        routine.removeElement (getElement(), false);
        return true;
    }

    bool undo()
    {
        PaintElement* newElement = routine.addElementFromXml (*xml, oldIndex, false);
        showCorrectTab();
        return newElement != nullptr;
    }

    int getSizeInUnits()    { return 10; }

private:
    std::unique_ptr<XmlElement> xml;
    int oldIndex;
};


void PaintRoutine::removeElement (PaintElement* element, const bool undoable)
{
    if (elements.contains (element))
    {
        if (undoable)
        {
            perform (new DeleteElementAction (element),
                     "Delete " + element->getTypeName());
        }
        else
        {
            selectedElements.deselect (element);
            selectedPoints.deselectAll();

            selectedPoints.changed (true);
            selectedElements.changed (true);

            elements.removeObject (element);
            changed();
        }
    }
}

//==============================================================================
class FrontOrBackElementAction  : public PaintElementUndoableAction <PaintElement>
{
public:
    FrontOrBackElementAction (PaintElement* const element, int newIndex_)
        : PaintElementUndoableAction <PaintElement> (element),
          newIndex (newIndex_)
    {
        oldIndex = routine.indexOfElement (element);
    }

    bool perform()
    {
        showCorrectTab();

        PaintElement* e = routine.getElement (oldIndex);
        routine.moveElementZOrder (oldIndex, newIndex);
        newIndex = routine.indexOfElement (e);
        return true;
    }

    bool undo()
    {
        showCorrectTab();
        routine.moveElementZOrder (newIndex, oldIndex);
        return true;
    }

private:
    int newIndex, oldIndex;
};

void PaintRoutine::moveElementZOrder (int oldIndex, int newIndex)
{
    jassert (elements [oldIndex] != nullptr);

    if (oldIndex != newIndex && elements [oldIndex] != nullptr)
    {
        elements.move (oldIndex, newIndex);
        changed();
    }
}

void PaintRoutine::elementToFront (PaintElement* element, const bool undoable)
{
    if (element != nullptr && elements.contains (element))
    {
        if (undoable)
            perform (new FrontOrBackElementAction (element, -1), "Move elements to front");
        else
            moveElementZOrder (elements.indexOf (element), -1);
    }
}

void PaintRoutine::elementToBack (PaintElement* element, const bool undoable)
{
    if (element != nullptr && elements.contains (element))
    {
        if (undoable)
            perform (new FrontOrBackElementAction (element, 0), "Move elements to back");
        else
            moveElementZOrder (elements.indexOf (element), 0);
    }
}

//==============================================================================
const char* const PaintRoutine::clipboardXmlTag = "PAINTELEMENTS";

void PaintRoutine::copySelectedToClipboard()
{
    if (selectedElements.getNumSelected() == 0)
        return;

    XmlElement clip (clipboardXmlTag);

    for (auto* pe : elements)
        if (selectedElements.isSelected (pe))
            clip.addChildElement (pe->createXml());

    SystemClipboard::copyTextToClipboard (clip.toString());
}

void PaintRoutine::paste()
{
    if (auto doc = parseXMLIfTagMatches (SystemClipboard::getTextFromClipboard(), clipboardXmlTag))
    {
        selectedElements.deselectAll();
        selectedPoints.deselectAll();

        for (auto* e : doc->getChildIterator())
            if (PaintElement* newElement = addElementFromXml (*e, -1, true))
                selectedElements.addToSelection (newElement);
    }
}

void PaintRoutine::deleteSelected()
{
    const SelectedItemSet<PaintElement*> temp1 (selectedElements);
    const SelectedItemSet<PathPoint*> temp2 (selectedPoints);

    if (temp2.getNumSelected() > 0)
    {
        selectedPoints.deselectAll();
        selectedPoints.changed (true); // synchronous message to get rid of any property components

        // if any points are selected, just delete them, and not the element, which may
        // also be selected..
        for (int i = temp2.getNumSelected(); --i >= 0;)
            temp2.getSelectedItem (i)->deleteFromPath();

        changed();
    }
    else if (temp1.getNumSelected() > 0)
    {
        selectedElements.deselectAll();
        selectedElements.changed (true);

        for (int i = temp1.getNumSelected(); --i >= 0;)
            removeElement (temp1.getSelectedItem (i), true);

        changed();
    }
}

void PaintRoutine::selectAll()
{
    if (selectedPoints.getNumSelected() > 0)
    {
        if (const PaintElementPath* path = selectedPoints.getSelectedItem (0)->owner)
            for (int i = 0; i < path->getNumPoints(); ++i)
                selectedPoints.addToSelection (path->getPoint (i));
    }
    else
    {
        for (int i = 0; i < elements.size(); ++i)
            selectedElements.addToSelection (elements.getUnchecked (i));
    }
}

void PaintRoutine::selectedToFront()
{
    const SelectedItemSet<PaintElement*> temp (selectedElements);

    for (int i = temp.getNumSelected(); --i >= 0;)
        elementToFront (temp.getSelectedItem(i), true);
}

void PaintRoutine::selectedToBack()
{
    const SelectedItemSet<PaintElement*> temp (selectedElements);

    for (int i = 0; i < temp.getNumSelected(); ++i)
        elementToBack (temp.getSelectedItem(i), true);
}

void PaintRoutine::alignTop()
{
    if (selectedElements.getNumSelected() > 1)
    {
        auto* main = selectedElements.getSelectedItem (0);
        auto yPos = main->getY();

        for (auto* other : selectedElements)
        {
            if (other != main)
                other->setPaintElementBoundsAndProperties (other, other->getBounds().withPosition (other->getX(),
                                                                                                   yPos), main, true);
        }
    }
}
void PaintRoutine::alignRight()
{
    if (selectedElements.getNumSelected() > 1)
    {
        auto* main = selectedElements.getSelectedItem (0);
        auto rightPos = main->getRight();

        for (auto* other : selectedElements)
        {
            if (other != main)
                other->setPaintElementBoundsAndProperties (other, other->getBounds().withPosition (rightPos - other->getWidth(),
                                                                                                   other->getY()), main, true);
        }
    }
}

void PaintRoutine::alignBottom()
{
    if (selectedElements.getNumSelected() > 1)
    {
        auto* main = selectedElements.getSelectedItem (0);
        auto bottomPos = main->getBottom();

        for (auto* other : selectedElements)
        {
            if (other != main)
                other->setPaintElementBoundsAndProperties (other, other->getBounds().withPosition (other->getX(),
                                                                                                   bottomPos - other->getHeight()), main, true);
        }
    }
}
void PaintRoutine::alignLeft()
{
    if (selectedElements.getNumSelected() > 1)
    {
        auto* main = selectedElements.getSelectedItem (0);
        auto xPos = main->getX();

        for (auto* other : selectedElements)
        {
            if (other != main)
                other->setPaintElementBoundsAndProperties (other, other->getBounds().withPosition (xPos,
                                                                                                   other->getY()), main, true);
        }
    }
}

void PaintRoutine::groupSelected()
{
    PaintElementGroup::groupSelected (this);
}

void PaintRoutine::ungroupSelected()
{
    const SelectedItemSet<PaintElement*> temp (selectedElements);

    for (int i = 0; i < temp.getNumSelected(); ++i)
        if (PaintElementGroup* const pg = dynamic_cast<PaintElementGroup*> (temp.getSelectedItem (i)))
            pg->ungroup (true);
}

void PaintRoutine::bringLostItemsBackOnScreen (const Rectangle<int>& parentArea)
{
    for (auto* c : elements)
    {
        auto r = c->getCurrentBounds (parentArea);

        if (! r.intersects (parentArea))
        {
            r.setPosition (parentArea.getCentreX(), parentArea.getCentreY());
            c->setCurrentBounds (r, parentArea, true);
        }
    }
}

void PaintRoutine::startDragging (const Rectangle<int>& parentArea)
{
    for (auto* c : elements)
    {
        auto r = c->getCurrentBounds (parentArea);

        c->getProperties().set ("xDragStart", r.getX());
        c->getProperties().set ("yDragStart", r.getY());
    }

    getDocument()->beginTransaction();
}

void PaintRoutine::dragSelectedComps (int dx, int dy, const Rectangle<int>& parentArea)
{
    getDocument()->getUndoManager().undoCurrentTransactionOnly();

    if (document != nullptr && selectedElements.getNumSelected() > 1)
    {
        dx = document->snapPosition (dx);
        dy = document->snapPosition (dy);
    }

    for (int i = 0; i < selectedElements.getNumSelected(); ++i)
    {
        PaintElement* const c = selectedElements.getSelectedItem (i);

        const int startX = c->getProperties() ["xDragStart"];
        const int startY = c->getProperties() ["yDragStart"];

        Rectangle<int> r (c->getCurrentBounds (parentArea));

        if (document != nullptr && selectedElements.getNumSelected() == 1)
        {
            r.setPosition (document->snapPosition (startX + dx),
                           document->snapPosition (startY + dy));
        }
        else
        {
            r.setPosition (startX + dx,
                           startY + dy);
        }

        c->setCurrentBounds (r, parentArea, true);
    }

    changed();
}

void PaintRoutine::endDragging()
{
    getDocument()->beginTransaction();
}

//==============================================================================
void PaintRoutine::fillWithBackground (Graphics& g, const bool drawOpaqueBackground)
{
    if ((! backgroundColour.isOpaque()) && drawOpaqueBackground)
    {
        g.fillCheckerBoard (Rectangle<float> ((float) g.getClipBounds().getRight(),
                                              (float) g.getClipBounds().getBottom()),
                            50.0f, 50.0f,
                            Colour (0xffdddddd).overlaidWith (backgroundColour),
                            Colour (0xffffffff).overlaidWith (backgroundColour));
    }
    else
    {
        g.fillAll (backgroundColour);
    }
}

void PaintRoutine::drawElements (Graphics& g, const Rectangle<int>& relativeTo)
{
    Component temp;
    temp.setBounds (relativeTo);

    for (auto* e : elements)
        e->draw (g, getDocument()->getComponentLayout(), relativeTo);
}

//==============================================================================
void PaintRoutine::dropImageAt (const File& f, int x, int y)
{
    std::unique_ptr<Drawable> d (Drawable::createFromImageFile (f));

    if (d != nullptr)
    {
        auto bounds = d->getDrawableBounds();
        d.reset();

        auto* newElement = addNewElement (ObjectTypes::createNewImageElement (this), -1, true);

        if (auto* pei = dynamic_cast<PaintElementImage*> (newElement))
        {
            String resourceName (getDocument()->getResources().findUniqueName (f.getFileName()));

            if (auto* existingResource = getDocument()->getResources().getResourceForFile (f))
            {
                resourceName = existingResource->name;
            }
            else
            {
                MemoryBlock data;
                f.loadFileAsData (data);

                getDocument()->getResources().add (resourceName, f.getFullPathName(), data);
            }

            pei->setResource (resourceName, true);

            const int imageW = (int) (bounds.getRight() + 0.999f);
            const int imageH = (int) (bounds.getBottom() + 0.999f);

            RelativePositionedRectangle pr;
            pr.rect.setX (x - imageW / 2);
            pr.rect.setY (y - imageH / 2);
            pr.rect.setWidth (imageW);
            pr.rect.setHeight (imageH);

            pei->setPosition (pr, true);

            getSelectedElements().selectOnly (pei);
        }
    }
}

//==============================================================================
const char* PaintRoutine::xmlTagName = "BACKGROUND";

XmlElement* PaintRoutine::createXml() const
{
    auto* xml = new XmlElement (xmlTagName);
    xml->setAttribute ("backgroundColour", backgroundColour.toString());

    for (auto* e : elements)
        xml->addChildElement (e->createXml());

    return xml;
}

bool PaintRoutine::loadFromXml (const XmlElement& xml)
{
    if (xml.hasTagName (xmlTagName))
    {
        backgroundColour = Colour::fromString (xml.getStringAttribute ("backgroundColour", Colours::white.toString()));

        clear();

        for (auto* e : xml.getChildIterator())
            if (auto* newElement = ObjectTypes::createElementForXml (e, this))
                elements.add (newElement);

        return true;
    }

    return false;
}

void PaintRoutine::fillInGeneratedCode (GeneratedCode& code, String& paintMethodCode) const
{
    if (! backgroundColour.isTransparent())
        paintMethodCode << "g.fillAll (" << CodeHelpers::colourToCode (backgroundColour) << ");\n\n";

    for (auto* e : elements)
        e->fillInGeneratedCode (code, paintMethodCode);
}

void PaintRoutine::applyCustomPaintSnippets (StringArray& snippets)
{
    for (auto* e : elements)
        e->applyCustomPaintSnippets (snippets);
}
