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

   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 "../Utility/Helpers/jucer_NewFileWizard.h"
#include "jucer_JucerDocument.h"
#include "jucer_ObjectTypes.h"
#include "UI/jucer_JucerDocumentEditor.h"
#include "UI/jucer_TestComponent.h"
#include "jucer_UtilityFunctions.h"
#include "Documents/jucer_ComponentDocument.h"
#include "Documents/jucer_ButtonDocument.h"

const char* const defaultClassName = "NewComponent";
const char* const defaultParentClasses = "public juce::Component";

//==============================================================================
JucerDocument::JucerDocument (SourceCodeDocument* c)
    : cpp (c),
      className (defaultClassName),
      parentClasses (defaultParentClasses)
{
    jassert (cpp != nullptr);
    resources.setDocument (this);

    ProjucerApplication::getCommandManager().commandStatusChanged();
    cpp->getCodeDocument().addListener (this);
}

JucerDocument::~JucerDocument()
{
    cpp->getCodeDocument().removeListener (this);
    ProjucerApplication::getCommandManager().commandStatusChanged();
}

//==============================================================================
void JucerDocument::changed()
{
    sendChangeMessage();
    ProjucerApplication::getCommandManager().commandStatusChanged();
    startTimer (800);
}

struct UserDocChangeTimer  : public Timer
{
    explicit UserDocChangeTimer (JucerDocument& d) : doc (d) {}
    void timerCallback() override       { doc.reloadFromDocument(); }

    JucerDocument& doc;
};

void JucerDocument::userEditedCpp()
{
    if (userDocChangeTimer == nullptr)
        userDocChangeTimer.reset (new UserDocChangeTimer (*this));

    userDocChangeTimer->startTimer (500);
}

void JucerDocument::beginTransaction()
{
    getUndoManager().beginNewTransaction();
}

void JucerDocument::beginTransaction (const String& name)
{
    getUndoManager().beginNewTransaction (name);
}

void JucerDocument::timerCallback()
{
    if (! Component::isMouseButtonDownAnywhere())
    {
        stopTimer();
        beginTransaction();

        flushChangesToDocuments (nullptr, false);
    }
}

void JucerDocument::codeDocumentTextInserted (const String&, int)   { userEditedCpp(); }
void JucerDocument::codeDocumentTextDeleted (int, int)              { userEditedCpp(); }

bool JucerDocument::perform (UndoableAction* const action, const String& actionName)
{
    return undoManager.perform (action, actionName);
}

void JucerDocument::refreshAllPropertyComps()
{
    if (ComponentLayout* l = getComponentLayout())
        l->getSelectedSet().changed();

    for (int i = getNumPaintRoutines(); --i >= 0;)
    {
        getPaintRoutine (i)->getSelectedElements().changed();
        getPaintRoutine (i)->getSelectedPoints().changed();
    }
}

//==============================================================================
void JucerDocument::setClassName (const String& newName)
{
    if (newName != className
        && build_tools::makeValidIdentifier (newName, false, false, true).isNotEmpty())
    {
        className = build_tools::makeValidIdentifier (newName, false, false, true);
        changed();
    }
}

void JucerDocument::setComponentName (const String& newName)
{
    if (newName != componentName)
    {
        componentName = newName;
        changed();
    }
}

void JucerDocument::setParentClasses (const String& classes)
{
    if (classes != parentClasses)
    {
        StringArray parentClassLines (getCleanedStringArray (StringArray::fromTokens (classes, ",", StringRef())));

        for (int i = parentClassLines.size(); --i >= 0;)
        {
            String s (parentClassLines[i]);
            String type;

            if (s.startsWith ("public ")
                || s.startsWith ("protected ")
                || s.startsWith ("private "))
            {
                type = s.upToFirstOccurrenceOf (" ", true, false);
                s = s.fromFirstOccurrenceOf (" ", false, false);

                if (s.trim().isEmpty())
                    type = s = String();
            }

            s = type + build_tools::makeValidIdentifier (s.trim(), false, false, true, true);

            parentClassLines.set (i, s);
        }

        parentClasses = parentClassLines.joinIntoString (", ");
        changed();
    }
}

void JucerDocument::setConstructorParams (const String& newParams)
{
    if (constructorParams != newParams)
    {
        constructorParams = newParams;
        changed();
    }
}

void JucerDocument::setVariableInitialisers (const String& newInitlialisers)
{
    if (variableInitialisers != newInitlialisers)
    {
        variableInitialisers = newInitlialisers;
        changed();
    }
}

void JucerDocument::setFixedSize (const bool isFixed)
{
    if (fixedSize != isFixed)
    {
        fixedSize = isFixed;
        changed();
    }
}

void JucerDocument::setInitialSize (int w, int h)
{
    w = jmax (1, w);
    h = jmax (1, h);

    if (initialWidth != w || initialHeight != h)
    {
        initialWidth = w;
        initialHeight = h;
        changed();
    }
}

//==============================================================================
bool JucerDocument::isSnapActive (const bool disableIfCtrlKeyDown) const noexcept
{
    return snapActive != (disableIfCtrlKeyDown && ModifierKeys::currentModifiers.isCtrlDown());
}

int JucerDocument::snapPosition (int pos) const noexcept
{
    if (isSnapActive (true))
    {
        jassert (snapGridPixels > 0);
        pos = ((pos + snapGridPixels * 1024 + snapGridPixels / 2) / snapGridPixels - 1024) * snapGridPixels;
    }

    return pos;
}

void JucerDocument::setSnappingGrid (const int numPixels, const bool active, const bool shown)
{
    if (numPixels != snapGridPixels
         || active != snapActive
         || shown != snapShown)
    {
        snapGridPixels = numPixels;
        snapActive = active;
        snapShown = shown;
        changed();
    }
}

void JucerDocument::setComponentOverlayOpacity (const float alpha)
{
    if (alpha != componentOverlayOpacity)
    {
        componentOverlayOpacity = alpha;
        changed();
    }
}

//==============================================================================
void JucerDocument::addMethod (const String& base, const String& returnVal, const String& method, const String& initialContent,
                               StringArray& baseClasses, StringArray& returnValues, StringArray& methods, StringArray& initialContents)
{
    baseClasses.add (base);
    returnValues.add (returnVal);
    methods.add (method);
    initialContents.add (initialContent);
}

void JucerDocument::getOptionalMethods (StringArray& baseClasses,
                                        StringArray& returnValues,
                                        StringArray& methods,
                                        StringArray& initialContents) const
{
    addMethod ("juce::Component", "void", "visibilityChanged()", "", baseClasses, returnValues, methods, initialContents);
    addMethod ("juce::Component", "void", "moved()", "", baseClasses, returnValues, methods, initialContents);
    addMethod ("juce::Component", "void", "parentHierarchyChanged()", "", baseClasses, returnValues, methods, initialContents);
    addMethod ("juce::Component", "void", "parentSizeChanged()", "", baseClasses, returnValues, methods, initialContents);
    addMethod ("juce::Component", "void", "lookAndFeelChanged()", "", baseClasses, returnValues, methods, initialContents);
    addMethod ("juce::Component", "bool", "hitTest (int x, int y)", "return true;", baseClasses, returnValues, methods, initialContents);
    addMethod ("juce::Component", "void", "broughtToFront()", "", baseClasses, returnValues, methods, initialContents);
    addMethod ("juce::Component", "void", "filesDropped (const juce::StringArray& filenames, int mouseX, int mouseY)", "", baseClasses, returnValues, methods, initialContents);
    addMethod ("juce::Component", "void", "handleCommandMessage (int commandId)", "", baseClasses, returnValues, methods, initialContents);
    addMethod ("juce::Component", "void", "childrenChanged()", "", baseClasses, returnValues, methods, initialContents);
    addMethod ("juce::Component", "void", "enablementChanged()", "", baseClasses, returnValues, methods, initialContents);

    addMethod ("juce::Component", "void", "mouseMove (const juce::MouseEvent& e)", "", baseClasses, returnValues, methods, initialContents);
    addMethod ("juce::Component", "void", "mouseEnter (const juce::MouseEvent& e)", "", baseClasses, returnValues, methods, initialContents);
    addMethod ("juce::Component", "void", "mouseExit (const juce::MouseEvent& e)", "", baseClasses, returnValues, methods, initialContents);
    addMethod ("juce::Component", "void", "mouseDown (const juce::MouseEvent& e)", "", baseClasses, returnValues, methods, initialContents);
    addMethod ("juce::Component", "void", "mouseDrag (const juce::MouseEvent& e)", "", baseClasses, returnValues, methods, initialContents);
    addMethod ("juce::Component", "void", "mouseUp (const juce::MouseEvent& e)", "", baseClasses, returnValues, methods, initialContents);
    addMethod ("juce::Component", "void", "mouseDoubleClick (const juce::MouseEvent& e)", "", baseClasses, returnValues, methods, initialContents);
    addMethod ("juce::Component", "void", "mouseWheelMove (const juce::MouseEvent& e, const juce::MouseWheelDetails& wheel)", "", baseClasses, returnValues, methods, initialContents);

    addMethod ("juce::Component", "bool", "keyPressed (const juce::KeyPress& key)", "return false;  // Return true if your handler uses this key event, or false to allow it to be passed-on.", baseClasses, returnValues, methods, initialContents);
    addMethod ("juce::Component", "bool", "keyStateChanged (bool isKeyDown)", "return false;  // Return true if your handler uses this key event, or false to allow it to be passed-on.", baseClasses, returnValues, methods, initialContents);
    addMethod ("juce::Component", "void", "modifierKeysChanged (const juce::ModifierKeys& modifiers)", "", baseClasses, returnValues, methods, initialContents);

    addMethod ("juce::Component", "void", "focusGained (FocusChangeType cause)", "", baseClasses, returnValues, methods, initialContents);
    addMethod ("juce::Component", "void", "focusLost (FocusChangeType cause)", "", baseClasses, returnValues, methods, initialContents);
    addMethod ("juce::Component", "void", "focusOfChildComponentChanged (FocusChangeType cause)", "", baseClasses, returnValues, methods, initialContents);
    addMethod ("juce::Component", "void", "modifierKeysChanged (const juce::ModifierKeys& modifiers)", "", baseClasses, returnValues, methods, initialContents);
    addMethod ("juce::Component", "void", "inputAttemptWhenModal()", "", baseClasses, returnValues, methods, initialContents);
}

void JucerDocument::setOptionalMethodEnabled (const String& methodSignature, const bool enable)
{
    if (enable)
        activeExtraMethods.addIfNotAlreadyThere (methodSignature);
    else
        activeExtraMethods.removeString (methodSignature);

    changed();
}

bool JucerDocument::isOptionalMethodEnabled (const String& sig) const noexcept
{
    return activeExtraMethods.contains (sig)
          || activeExtraMethods.contains (sig.replace ("juce::", {}));
}

void JucerDocument::addExtraClassProperties (PropertyPanel&)
{
}

//==============================================================================
const char* const JucerDocument::jucerCompXmlTag = "JUCER_COMPONENT";

std::unique_ptr<XmlElement> JucerDocument::createXml() const
{
    auto doc = std::make_unique<XmlElement> (jucerCompXmlTag);

    doc->setAttribute ("documentType", getTypeName());
    doc->setAttribute ("className", className);

    if (templateFile.trim().isNotEmpty())
        doc->setAttribute ("template", templateFile);

    doc->setAttribute ("componentName", componentName);
    doc->setAttribute ("parentClasses", parentClasses);
    doc->setAttribute ("constructorParams", constructorParams);
    doc->setAttribute ("variableInitialisers", variableInitialisers);
    doc->setAttribute ("snapPixels", snapGridPixels);
    doc->setAttribute ("snapActive", snapActive);
    doc->setAttribute ("snapShown", snapShown);
    doc->setAttribute ("overlayOpacity", String (componentOverlayOpacity, 3));
    doc->setAttribute ("fixedSize", fixedSize);
    doc->setAttribute ("initialWidth", initialWidth);
    doc->setAttribute ("initialHeight", initialHeight);

    if (activeExtraMethods.size() > 0)
    {
        XmlElement* extraMethods = new XmlElement ("METHODS");
        doc->addChildElement (extraMethods);

        for (int i = 0; i < activeExtraMethods.size(); ++i)
        {
            XmlElement* e = new XmlElement ("METHOD");
            extraMethods ->addChildElement (e);
            e->setAttribute ("name", activeExtraMethods[i]);
        }
    }

    return doc;
}

bool JucerDocument::loadFromXml (const XmlElement& xml)
{
    if (xml.hasTagName (jucerCompXmlTag)
         && getTypeName().equalsIgnoreCase (xml.getStringAttribute ("documentType")))
    {
        className = xml.getStringAttribute ("className", defaultClassName);
        templateFile = xml.getStringAttribute ("template", String());
        componentName = xml.getStringAttribute ("componentName", String());
        parentClasses = xml.getStringAttribute ("parentClasses", defaultParentClasses);
        constructorParams = xml.getStringAttribute ("constructorParams", String());
        variableInitialisers = xml.getStringAttribute ("variableInitialisers", String());

        fixedSize = xml.getBoolAttribute ("fixedSize", false);
        initialWidth = xml.getIntAttribute ("initialWidth", 300);
        initialHeight = xml.getIntAttribute ("initialHeight", 200);

        snapGridPixels = xml.getIntAttribute ("snapPixels", snapGridPixels);
        snapActive = xml.getBoolAttribute ("snapActive", snapActive);
        snapShown = xml.getBoolAttribute ("snapShown", snapShown);

        componentOverlayOpacity = (float) xml.getDoubleAttribute ("overlayOpacity", 0.0);

        activeExtraMethods.clear();

        if (XmlElement* const methods = xml.getChildByName ("METHODS"))
            for (auto* e : methods->getChildWithTagNameIterator ("METHOD"))
                activeExtraMethods.addIfNotAlreadyThere (e->getStringAttribute ("name"));

        activeExtraMethods.trim();
        activeExtraMethods.removeEmptyStrings();

        changed();
        getUndoManager().clearUndoHistory();
        return true;
    }

    return false;
}


//==============================================================================
void JucerDocument::fillInGeneratedCode (GeneratedCode& code) const
{
    code.className = className;
    code.componentName = componentName;
    code.parentClasses = parentClasses;
    code.constructorParams = constructorParams;
    code.initialisers.addLines (variableInitialisers);

    if (! componentName.isEmpty())
        code.constructorCode << "setName (" + quotedString (componentName, false) + ");\n";

    // call these now, just to make sure they're the first two methods in the list.
    code.getCallbackCode (String(), "void", "paint (juce::Graphics& g)", false)
        << "//[UserPrePaint] Add your own custom painting code here..\n//[/UserPrePaint]\n\n";

    code.getCallbackCode (String(), "void", "resized()", false)
        << "//[UserPreResize] Add your own custom resize code here..\n//[/UserPreResize]\n\n";

    if (ComponentLayout* l = getComponentLayout())
        l->fillInGeneratedCode (code);

    fillInPaintCode (code);

    std::unique_ptr<XmlElement> e (createXml());
    jassert (e != nullptr);
    code.jucerMetadata = e->toString (XmlElement::TextFormat().withoutHeader());

    resources.fillInGeneratedCode (code);

    code.constructorCode
        << "\n//[UserPreSize]\n"
           "//[/UserPreSize]\n";

    if (initialWidth > 0 || initialHeight > 0)
        code.constructorCode << "\nsetSize (" << initialWidth << ", " << initialHeight << ");\n";

    code.getCallbackCode (String(), "void", "paint (juce::Graphics& g)", false)
        << "//[UserPaint] Add your own custom painting code here..\n//[/UserPaint]";

    code.getCallbackCode (String(), "void", "resized()", false)
        << "//[UserResized] Add your own custom resize handling here..\n//[/UserResized]";

    // add optional methods
    StringArray baseClasses, returnValues, methods, initialContents;
    getOptionalMethods (baseClasses, returnValues, methods, initialContents);

    for (int i = 0; i < methods.size(); ++i)
    {
        if (isOptionalMethodEnabled (methods[i]))
        {
            String baseClassToAdd (baseClasses[i]);

            if (baseClassToAdd == "juce::Component" || baseClassToAdd == "juce::Button")
                baseClassToAdd.clear();

            String& s = code.getCallbackCode (baseClassToAdd, returnValues[i], methods[i], false);

            if (! s.contains ("//["))
            {
                String userCommentTag ("UserCode_");
                userCommentTag += methods[i].upToFirstOccurrenceOf ("(", false, false).trim();

                s << "\n//[" << userCommentTag << "] -- Add your code here...\n"
                  << initialContents[i];

                if (initialContents[i].isNotEmpty() && ! initialContents[i].endsWithChar ('\n'))
                    s << '\n';

                s << "//[/" << userCommentTag << "]\n";
            }
        }
    }
}

void JucerDocument::fillInPaintCode (GeneratedCode& code) const
{
    for (int i = 0; i < getNumPaintRoutines(); ++i)
        getPaintRoutine (i)
            ->fillInGeneratedCode (code, code.getCallbackCode (String(), "void", "paint (juce::Graphics& g)", false));
}

void JucerDocument::setTemplateFile (const String& newFile)
{
    if (templateFile != newFile)
    {
        templateFile = newFile;
        changed();
    }
}

//==============================================================================
bool JucerDocument::findTemplateFiles (String& headerContent, String& cppContent) const
{
    if (templateFile.isNotEmpty())
    {
        const File f (getCppFile().getSiblingFile (templateFile));

        const File templateCpp (f.withFileExtension (".cpp"));
        const File templateH   (f.withFileExtension (".h"));

        headerContent = templateH.loadFileAsString();
        cppContent = templateCpp.loadFileAsString();

        if (headerContent.isNotEmpty() && cppContent.isNotEmpty())
            return true;
    }

    headerContent = BinaryData::jucer_ComponentTemplate_h;
    cppContent    = BinaryData::jucer_ComponentTemplate_cpp;
    return true;
}

bool JucerDocument::flushChangesToDocuments (Project* project, bool isInitial)
{
    String headerTemplate, cppTemplate;
    if (! findTemplateFiles (headerTemplate, cppTemplate))
        return false;

    GeneratedCode generated (this);
    fillInGeneratedCode (generated);

    const File headerFile (getHeaderFile());
    generated.includeFilesCPP.insert (0, headerFile);

    OpenDocumentManager& odm = ProjucerApplication::getApp().openDocumentManager;

    if (SourceCodeDocument* header = dynamic_cast<SourceCodeDocument*> (odm.openFile (nullptr, headerFile)))
    {
        String existingHeader (header->getCodeDocument().getAllContent());
        String existingCpp (cpp->getCodeDocument().getAllContent());

        generated.applyToCode (headerTemplate, headerFile, existingHeader);
        generated.applyToCode (cppTemplate, headerFile.withFileExtension (".cpp"), existingCpp);

        if (isInitial)
        {
            jassert (project != nullptr);

            auto lineFeed = project->getProjectLineFeed();

            headerTemplate = replaceLineFeeds (headerTemplate, lineFeed);
            cppTemplate    = replaceLineFeeds (cppTemplate,    lineFeed);
        }
        else
        {
            headerTemplate = replaceLineFeeds (headerTemplate, getLineFeedForFile (existingHeader));
            cppTemplate    = replaceLineFeeds (cppTemplate,    getLineFeedForFile (existingCpp));
        }

        if (header->getCodeDocument().getAllContent() != headerTemplate)
            header->getCodeDocument().replaceAllContent (headerTemplate);

        if (cpp->getCodeDocument().getAllContent() != cppTemplate)
            cpp->getCodeDocument().replaceAllContent (cppTemplate);
    }

    userDocChangeTimer.reset();
    return true;
}

bool JucerDocument::reloadFromDocument()
{
    const String cppContent (cpp->getCodeDocument().getAllContent());

    std::unique_ptr<XmlElement> newXML (pullMetaDataFromCppFile (cppContent));

    if (newXML == nullptr || ! newXML->hasTagName (jucerCompXmlTag))
        return false;

    if (currentXML != nullptr && currentXML->isEquivalentTo (newXML.get(), true))
        return true;

    currentXML.reset (newXML.release());
    stopTimer();

    resources.loadFromCpp (getCppFile(), cppContent);

    bool result = loadFromXml (*currentXML);
    extractCustomPaintSnippetsFromCppFile (cppContent);
    return result;
}

void JucerDocument::refreshCustomCodeFromDocument()
{
    const String cppContent (cpp->getCodeDocument().getAllContent());
    extractCustomPaintSnippetsFromCppFile (cppContent);
}

void JucerDocument::extractCustomPaintSnippetsFromCppFile (const String& cppContent)
{
    StringArray customPaintSnippets;

    auto lines = StringArray::fromLines (cppContent);
    int last = 0;

    while (last >= 0)
    {
        const int start = indexOfLineStartingWith (lines, "//[UserPaintCustomArguments]", last);
        if (start < 0)
            break;

        const int end = indexOfLineStartingWith (lines, "//[/UserPaintCustomArguments]", start);
        if (end < 0)
            break;

        last = end + 1;
        String result;

        for (int i = start + 1; i < end; ++i)
            result << lines [i] << newLine;

        customPaintSnippets.add (CodeHelpers::unindent (result, 4));
    }

    applyCustomPaintSnippets (customPaintSnippets);
}

std::unique_ptr<XmlElement> JucerDocument::pullMetaDataFromCppFile (const String& cpp)
{
    auto lines = StringArray::fromLines (cpp);
    auto startLine = indexOfLineStartingWith (lines, "BEGIN_JUCER_METADATA", 0);

    if (startLine > 0)
    {
        auto endLine = indexOfLineStartingWith (lines, "END_JUCER_METADATA", startLine);

        if (endLine > startLine)
            return parseXML (lines.joinIntoString ("\n", startLine + 1, endLine - startLine - 1));
    }

    return nullptr;
}

bool JucerDocument::isValidJucerCppFile (const File& f)
{
    if (f.hasFileExtension (cppFileExtensions))
    {
        std::unique_ptr<XmlElement> xml (pullMetaDataFromCppFile (f.loadFileAsString()));

        if (xml != nullptr)
            return xml->hasTagName (jucerCompXmlTag);
    }

    return false;
}

static JucerDocument* createDocument (SourceCodeDocument* cpp)
{
    auto& codeDoc = cpp->getCodeDocument();

    std::unique_ptr<XmlElement> xml (JucerDocument::pullMetaDataFromCppFile (codeDoc.getAllContent()));

    if (xml == nullptr || ! xml->hasTagName (JucerDocument::jucerCompXmlTag))
        return nullptr;

    const String docType (xml->getStringAttribute ("documentType"));

    std::unique_ptr<JucerDocument> newDoc;

    if (docType.equalsIgnoreCase ("Button"))
        newDoc.reset (new ButtonDocument (cpp));

    if (docType.equalsIgnoreCase ("Component") || docType.isEmpty())
        newDoc.reset (new ComponentDocument (cpp));

    if (newDoc != nullptr && newDoc->reloadFromDocument())
        return newDoc.release();

    return nullptr;
}

JucerDocument* JucerDocument::createForCppFile (Project* p, const File& file)
{
    OpenDocumentManager& odm = ProjucerApplication::getApp().openDocumentManager;

    if (SourceCodeDocument* cpp = dynamic_cast<SourceCodeDocument*> (odm.openFile (p, file)))
        if (dynamic_cast<SourceCodeDocument*> (odm.openFile (p, file.withFileExtension (".h"))) != nullptr)
            return createDocument (cpp);

    return nullptr;
}

//==============================================================================
class JucerComponentDocument  : public SourceCodeDocument
{
public:
    JucerComponentDocument (Project* p, const File& f)
        : SourceCodeDocument (p, f)
    {
    }

    void saveAsync (std::function<void (bool)> callback) override
    {
        SourceCodeDocument::saveAsync ([parent = WeakReference<JucerComponentDocument> { this }, callback] (bool saveResult)
        {
            if (parent == nullptr)
                return;

            if (! saveResult)
            {
                callback (false);
                return;
            }

            parent->saveHeaderAsync ([parent, callback] (bool headerSaveResult)
            {
                if (parent != nullptr)
                    callback (headerSaveResult);
            });
        });
    }

    void saveHeaderAsync (std::function<void (bool)> callback)
    {
        auto& odm = ProjucerApplication::getApp().openDocumentManager;

        if (auto* header = odm.openFile (nullptr, getFile().withFileExtension (".h")))
        {
            header->saveAsync ([parent = WeakReference<JucerComponentDocument> { this }, callback] (bool saveResult)
            {
                if (parent == nullptr)
                    return;

                if (saveResult)
                    ProjucerApplication::getApp()
                        .openDocumentManager
                        .closeFileWithoutSaving (parent->getFile().withFileExtension (".h"));

                callback (saveResult);
            });

            return;
        }

        callback (false);
    }

    std::unique_ptr<Component> createEditor() override
    {
        if (ProjucerApplication::getApp().isGUIEditorEnabled())
        {
            std::unique_ptr<JucerDocument> jucerDoc (JucerDocument::createForCppFile (getProject(), getFile()));

            if (jucerDoc != nullptr)
                return std::make_unique<JucerDocumentEditor> (jucerDoc.release());
        }

        return SourceCodeDocument::createEditor();
    }

    struct Type  : public OpenDocumentManager::DocumentType
    {
        Type() {}

        bool canOpenFile (const File& f) override                { return JucerDocument::isValidJucerCppFile (f); }
        Document* openFile (Project* p, const File& f) override  { return new JucerComponentDocument (p, f); }
    };

    JUCE_DECLARE_WEAK_REFERENCEABLE (JucerComponentDocument)
};

OpenDocumentManager::DocumentType* createGUIDocumentType();
OpenDocumentManager::DocumentType* createGUIDocumentType()
{
    return new JucerComponentDocument::Type();
}

//==============================================================================
struct NewGUIComponentWizard  : public NewFileWizard::Type
{
    NewGUIComponentWizard (Project& proj)
        : project (proj)
    {}

    String getName() override  { return "GUI Component"; }

    void createNewFile (Project& p, Project::Item parent) override
    {
        jassert (&p == &project);

        askUserToChooseNewFile (String (defaultClassName) + ".h", "*.h;*.cpp", parent, [this, parent] (File newFile) mutable
        {
            if (newFile != File())
            {
                auto headerFile = newFile.withFileExtension (".h");
                auto cppFile = newFile.withFileExtension (".cpp");

                headerFile.replaceWithText (String());
                cppFile.replaceWithText (String());

                auto& odm = ProjucerApplication::getApp().openDocumentManager;

                if (auto* cpp = dynamic_cast<SourceCodeDocument*> (odm.openFile (&project, cppFile)))
                {
                    if (auto* header = dynamic_cast<SourceCodeDocument*> (odm.openFile (&project, headerFile)))
                    {
                        std::unique_ptr<JucerDocument> jucerDoc (new ComponentDocument (cpp));

                        if (jucerDoc != nullptr)
                        {
                            jucerDoc->setClassName (newFile.getFileNameWithoutExtension());

                            jucerDoc->flushChangesToDocuments (&project, true);
                            jucerDoc.reset();

                            for (auto* doc : { cpp, header })
                            {
                                doc->saveAsync ([doc] (bool)
                                {
                                    ProjucerApplication::getApp()
                                        .openDocumentManager
                                        .closeDocumentAsync (doc, OpenDocumentManager::SaveIfNeeded::yes, nullptr);
                                });
                            }

                            parent.addFileRetainingSortOrder (headerFile, true);
                            parent.addFileRetainingSortOrder (cppFile, true);
                        }
                    }
                }
            }
        });
    }

    Project& project;
};

NewFileWizard::Type* createGUIComponentWizard (Project&);
NewFileWizard::Type* createGUIComponentWizard (Project& p)
{
    return new NewGUIComponentWizard (p);
}
