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

   This file is part of the JUCE library.
   Copyright (c) 2017 - ROLI Ltd.

   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 5 End-User License
   Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
   27th April 2017).

   End User License Agreement: www.juce.com/juce-5-licence
   Privacy Policy: www.juce.com/juce-5-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


//==============================================================================
class FileTreeItemBase   : public JucerTreeViewBase,
                           public ValueTree::Listener
{
public:
    FileTreeItemBase (const Project::Item& projectItem)
        : item (projectItem), isFileMissing (false)
    {
        item.state.addListener (this);
    }

    ~FileTreeItemBase()
    {
        item.state.removeListener (this);
    }

    //==============================================================================
    virtual bool acceptsFileDrop (const StringArray& files) const = 0;
    virtual bool acceptsDragItems (const OwnedArray<Project::Item>& selectedNodes) = 0;

    //==============================================================================
    String getDisplayName() const override              { return item.getName(); }
    String getRenamingName() const override             { return getDisplayName(); }
    void setName (const String& newName) override       { item.getNameValue() = newName; }
    bool isMissing() const override                     { return isFileMissing; }
    virtual File getFile() const                        { return item.getFile(); }

    void deleteItem() override                          { item.removeItemFromProject(); }

    void deleteAllSelectedItems() override
    {
        auto* tree = getOwnerView();
        Array<File> filesToTrash;
        Array<Project::Item> itemsToRemove;

        for (int i = 0; i < tree->getNumSelectedItems(); ++i)
        {
            if (auto* p = dynamic_cast<FileTreeItemBase*> (tree->getSelectedItem (i)))
            {
                itemsToRemove.add (p->item);

                if (p->item.isGroup())
                {
                    for (int j = 0; j < p->item.getNumChildren(); ++j)
                    {
                        auto associatedFile = p->item.getChild (j).getFile();

                        if (associatedFile.existsAsFile())
                            filesToTrash.addIfNotAlreadyThere (associatedFile);
                    }
                }
                else if (p->getFile().existsAsFile())
                {
                    filesToTrash.addIfNotAlreadyThere (p->getFile());
                }
            }
        }

        if (filesToTrash.size() > 0)
        {
            String fileList;
            auto maxFilesToList = 10;
            for (auto i = jmin (maxFilesToList, filesToTrash.size()); --i >= 0;)
                fileList << filesToTrash.getUnchecked(i).getFullPathName() << "\n";

            if (filesToTrash.size() > maxFilesToList)
                fileList << "\n...plus " << (filesToTrash.size() - maxFilesToList) << " more files...";

            auto r = AlertWindow::showYesNoCancelBox (AlertWindow::NoIcon, "Delete Project Items",
                                                      "As well as removing the selected item(s) from the project, do you also want to move their files to the trash:\n\n"
                                                           + fileList,
                                                      "Just remove references",
                                                      "Also move files to Trash",
                                                      "Cancel",
                                                      tree->getTopLevelComponent());

            if (r == 0)
                return;

            if (r != 2)
                filesToTrash.clear();
        }

        if (auto* treeRootItem = dynamic_cast<FileTreeItemBase*> (tree->getRootItem()))
        {
            auto& om = ProjucerApplication::getApp().openDocumentManager;

            for (auto i = filesToTrash.size(); --i >= 0;)
            {
                auto f = filesToTrash.getUnchecked(i);

                om.closeFile (f, false);

                if (! f.moveToTrash())
                {
                    // xxx
                }
            }

            for (auto i = itemsToRemove.size(); --i >= 0;)
            {
                if (auto itemToRemove = treeRootItem->findTreeViewItem (itemsToRemove.getUnchecked (i)))
                {
                    if (auto* pcc = treeRootItem->getProjectContentComponent())
                    {
                        if (auto* fileInfoComp = dynamic_cast<FileGroupInformationComponent*> (pcc->getEditorComponentContent()))
                            if (fileInfoComp->getGroupPath() == itemToRemove->getFile().getFullPathName())
                                pcc->hideEditor();
                    }

                    om.closeFile (itemToRemove->getFile(), false);
                    itemToRemove->deleteItem();
                }
            }
        }
        else
        {
            jassertfalse;
        }
    }

    virtual void revealInFinder() const
    {
        getFile().revealToUser();
    }

    virtual void browseToAddExistingFiles()
    {
        auto location = item.isGroup() ? item.determineGroupFolder() : getFile();
        FileChooser fc ("Add Files to Jucer Project", location, {});

        if (fc.browseForMultipleFilesOrDirectories())
        {
            StringArray files;

            for (int i = 0; i < fc.getResults().size(); ++i)
                files.add (fc.getResults().getReference(i).getFullPathName());

            addFilesRetainingSortOrder (files);
        }
    }

    virtual void checkFileStatus()  // (recursive)
    {
        auto file = getFile();
        auto nowMissing = (file != File() && ! file.exists());

        if (nowMissing != isFileMissing)
        {
            isFileMissing = nowMissing;
            repaintItem();
        }
    }

    virtual void addFilesAtIndex (const StringArray& files, int insertIndex)
    {
        if (auto* p = getParentProjectItem())
            p->addFilesAtIndex (files, insertIndex);
    }

    virtual void addFilesRetainingSortOrder (const StringArray& files)
    {
        if (auto* p = getParentProjectItem())
            p->addFilesRetainingSortOrder (files);
    }

    virtual void moveSelectedItemsTo (OwnedArray <Project::Item>&, int /*insertIndex*/)
    {
        jassertfalse;
    }

    void showMultiSelectionPopupMenu() override
    {
        PopupMenu m;
        m.addItem (1, "Delete");

        m.showMenuAsync (PopupMenu::Options(),
                         ModalCallbackFunction::create (treeViewMultiSelectItemChosen, this));
    }

    static void treeViewMultiSelectItemChosen (int resultCode, FileTreeItemBase* item)
    {
        switch (resultCode)
        {
            case 1:     item->deleteAllSelectedItems(); break;
            default:    break;
        }
    }

    virtual FileTreeItemBase* findTreeViewItem (const Project::Item& itemToFind)
    {
        if (item == itemToFind)
            return this;

        auto wasOpen = isOpen();
        setOpen (true);

        for (auto i = getNumSubItems(); --i >= 0;)
        {
            if (auto* pg = dynamic_cast<FileTreeItemBase*> (getSubItem(i)))
                if (auto* found = pg->findTreeViewItem (itemToFind))
                    return found;
        }

        setOpen (wasOpen);
        return nullptr;
    }

    //==============================================================================
    void valueTreePropertyChanged (ValueTree& tree, const Identifier&) override
    {
        if (tree == item.state)
            repaintItem();
    }

    void valueTreeChildAdded (ValueTree& parentTree, ValueTree&) override         { treeChildrenChanged (parentTree); }
    void valueTreeChildRemoved (ValueTree& parentTree, ValueTree&, int) override  { treeChildrenChanged (parentTree); }
    void valueTreeChildOrderChanged (ValueTree& parentTree, int, int) override    { treeChildrenChanged (parentTree); }
    void valueTreeParentChanged (ValueTree&) override {}

    //==============================================================================
    bool mightContainSubItems() override                { return item.getNumChildren() > 0; }
    String getUniqueName() const override               { jassert (item.getID().isNotEmpty()); return item.getID(); }
    bool canBeSelected() const override                 { return true; }
    String getTooltip() override                        { return {}; }
    File getDraggableFile() const override              { return getFile(); }

    var getDragSourceDescription() override
    {
        cancelDelayedSelectionTimer();
        return projectItemDragType;
    }

    void addSubItems() override
    {
        for (int i = 0; i < item.getNumChildren(); ++i)
            if (auto* p = createSubItem (item.getChild(i)))
                addSubItem (p);
    }

    void itemOpennessChanged (bool isNowOpen) override
    {
        if (isNowOpen)
            refreshSubItems();
    }

    //==============================================================================
    bool isInterestedInFileDrag (const StringArray& files) override
    {
        return acceptsFileDrop (files);
    }

    void filesDropped (const StringArray& files, int insertIndex) override
    {
        if (files.size() == 1 && File (files[0]).hasFileExtension (Project::projectFileExtension))
            ProjucerApplication::getApp().openFile (files[0]);
        else
            addFilesAtIndex (files, insertIndex);
    }

    bool isInterestedInDragSource (const DragAndDropTarget::SourceDetails& dragSourceDetails) override
    {
        OwnedArray<Project::Item> selectedNodes;
        getSelectedProjectItemsBeingDragged (dragSourceDetails, selectedNodes);

        return selectedNodes.size() > 0 && acceptsDragItems (selectedNodes);
    }

    void itemDropped (const DragAndDropTarget::SourceDetails& dragSourceDetails, int insertIndex) override
    {
        OwnedArray<Project::Item> selectedNodes;
        getSelectedProjectItemsBeingDragged (dragSourceDetails, selectedNodes);

        if (selectedNodes.size() > 0)
        {
            auto* tree = getOwnerView();
            std::unique_ptr<XmlElement> oldOpenness (tree->getOpennessState (false));

            moveSelectedItemsTo (selectedNodes, insertIndex);

            if (oldOpenness != nullptr)
                tree->restoreOpennessState (*oldOpenness, false);
        }
    }

    int getMillisecsAllowedForDragGesture() override
    {
        // for images, give the user longer to start dragging before assuming they're
        // clicking to select it for previewing..
        return item.isImageFile() ? 250 : JucerTreeViewBase::getMillisecsAllowedForDragGesture();
    }

    static void getSelectedProjectItemsBeingDragged (const DragAndDropTarget::SourceDetails& dragSourceDetails,
                                                     OwnedArray<Project::Item>& selectedNodes)
    {
        if (dragSourceDetails.description == projectItemDragType)
        {
            auto* tree = dynamic_cast<TreeView*> (dragSourceDetails.sourceComponent.get());

            if (tree == nullptr)
                tree = dragSourceDetails.sourceComponent->findParentComponentOfClass<TreeView>();

            if (tree != nullptr)
            {
                auto numSelected = tree->getNumSelectedItems();

                for (int i = 0; i < numSelected; ++i)
                    if (auto* p = dynamic_cast<FileTreeItemBase*> (tree->getSelectedItem (i)))
                        selectedNodes.add (new Project::Item (p->item));
            }
        }
    }

    FileTreeItemBase* getParentProjectItem() const
    {
        return dynamic_cast<FileTreeItemBase*> (getParentItem());
    }

    //==============================================================================
    Project::Item item;

protected:
    bool isFileMissing;

    virtual FileTreeItemBase* createSubItem (const Project::Item& node) = 0;

    Icon getIcon() const override
    {
        auto colour = getOwnerView()->findColour (isSelected() ? defaultHighlightedTextColourId
                                                               : treeIconColourId);

        return item.getIcon (isOpen()).withColour (colour);
    }

    bool isIconCrossedOut() const override  { return item.isIconCrossedOut(); }

    void treeChildrenChanged (const ValueTree& parentTree)
    {
        if (parentTree == item.state)
        {
            refreshSubItems();
            treeHasChanged();
            setOpen (true);
        }
    }

    void triggerAsyncRename (const Project::Item& itemToRename)
    {
        struct RenameMessage  : public CallbackMessage
        {
            RenameMessage (TreeView* const t, const Project::Item& i)
                : tree (t), itemToRename (i)  {}

            void messageCallback() override
            {
                if (tree != nullptr)
                    if (auto* root = dynamic_cast<FileTreeItemBase*> (tree->getRootItem()))
                        if (auto* found = root->findTreeViewItem (itemToRename))
                            found->showRenameBox();
            }

        private:
            Component::SafePointer<TreeView> tree;
            Project::Item itemToRename;
        };

        (new RenameMessage (getOwnerView(), itemToRename))->post();
    }

    static void moveItems (OwnedArray<Project::Item>& selectedNodes, Project::Item destNode, int insertIndex)
    {
        for (auto i = selectedNodes.size(); --i >= 0;)
        {
            auto* n = selectedNodes.getUnchecked(i);

            if (destNode == *n || destNode.state.isAChildOf (n->state)) // Check for recursion.
                return;

            if (! destNode.canContain (*n))
                selectedNodes.remove (i);
        }

        // Don't include any nodes that are children of other selected nodes..
        for (auto i = selectedNodes.size(); --i >= 0;)
        {
            auto* n = selectedNodes.getUnchecked(i);

            for (auto j = selectedNodes.size(); --j >= 0;)
            {
                if (j != i && n->state.isAChildOf (selectedNodes.getUnchecked(j)->state))
                {
                    selectedNodes.remove (i);
                    break;
                }
            }
        }

        // Remove and re-insert them one at a time..
        for (int i = 0; i < selectedNodes.size(); ++i)
        {
            auto* selectedNode = selectedNodes.getUnchecked(i);

            if (selectedNode->state.getParent() == destNode.state
                  && indexOfNode (destNode.state, selectedNode->state) < insertIndex)
                --insertIndex;

            selectedNode->removeItemFromProject();
            destNode.addChild (*selectedNode, insertIndex++);
        }
    }

    static int indexOfNode (const ValueTree& parent, const ValueTree& child)
    {
        for (auto i = parent.getNumChildren(); --i >= 0;)
            if (parent.getChild (i) == child)
                return i;

        return -1;
    }
};

//==============================================================================
class SourceFileItem   : public FileTreeItemBase
{
public:
    SourceFileItem (const Project::Item& projectItem)
        : FileTreeItemBase (projectItem)
    {
    }

    bool acceptsFileDrop (const StringArray&) const override             { return false; }
    bool acceptsDragItems (const OwnedArray <Project::Item>&) override   { return false; }

    String getDisplayName() const override
    {
        return getFile().getFileName();
    }

    static File findCorrespondingHeaderOrCpp (const File& f)
    {
        if (f.hasFileExtension (sourceFileExtensions))  return f.withFileExtension (".h");
        if (f.hasFileExtension (headerFileExtensions))  return f.withFileExtension (".cpp");

        return {};
    }

    void setName (const String& newName) override
    {
        if (newName != File::createLegalFileName (newName))
        {
            AlertWindow::showMessageBox (AlertWindow::WarningIcon, "File Rename",
                                         "That filename contained some illegal characters!");
            triggerAsyncRename (item);
            return;
        }

        auto oldFile = getFile();
        auto newFile = oldFile.getSiblingFile (newName);
        auto correspondingFile = findCorrespondingHeaderOrCpp (oldFile);

        if (correspondingFile.exists() && newFile.hasFileExtension (oldFile.getFileExtension()))
        {
            auto correspondingItem = item.project.getMainGroup().findItemForFile (correspondingFile);

            if (correspondingItem.isValid())
            {
                if (AlertWindow::showOkCancelBox (AlertWindow::NoIcon, "File Rename",
                                                  "Do you also want to rename the corresponding file \"" + correspondingFile.getFileName()
                                                    + "\" to match?"))
                {
                    if (! item.renameFile (newFile))
                    {
                        AlertWindow::showMessageBox (AlertWindow::WarningIcon, "File Rename",
                                                     "Failed to rename \"" + oldFile.getFullPathName() + "\"!\n\nCheck your file permissions!");
                        return;
                    }

                    if (! correspondingItem.renameFile (newFile.withFileExtension (correspondingFile.getFileExtension())))
                    {
                        AlertWindow::showMessageBox (AlertWindow::WarningIcon, "File Rename",
                                                     "Failed to rename \"" + correspondingFile.getFullPathName() + "\"!\n\nCheck your file permissions!");
                    }
                }
            }
        }

        if (! item.renameFile (newFile))
        {
            AlertWindow::showMessageBox (AlertWindow::WarningIcon, "File Rename",
                                         "Failed to rename the file!\n\nCheck your file permissions!");
        }
    }

    FileTreeItemBase* createSubItem (const Project::Item&) override
    {
        jassertfalse;
        return nullptr;
    }

    void showDocument() override
    {
        auto f = getFile();

        if (f.exists())
            if (auto* pcc = getProjectContentComponent())
                pcc->showEditorForFile (f, false);
    }

    void showPopupMenu() override
    {
        PopupMenu m;

        m.addItem (1, "Open in external editor");
        m.addItem (2,
                     #if JUCE_MAC
                      "Reveal in Finder");
                     #else
                      "Reveal in Explorer");
                     #endif

        m.addItem (4, "Rename File...");
        m.addSeparator();

        if (auto* group = dynamic_cast<GroupItem*> (getParentItem()))
        {
            if (group->isRoot())
            {
                m.addItem (5, "Binary Resource", true, item.shouldBeAddedToBinaryResources());
                m.addItem (6, "Xcode Resource",  true, item.shouldBeAddedToXcodeResources());
                m.addItem (7, "Compile",         true, item.shouldBeCompiled());
                m.addSeparator();
            }
        }

        m.addItem (3, "Delete");

        launchPopupMenu (m);
    }

    void showAddMenu() override
    {
        if (auto* group = dynamic_cast<GroupItem*> (getParentItem()))
            group->showAddMenu();
    }

    void handlePopupMenuResult (int resultCode) override
    {
        switch (resultCode)
        {
            case 1:     getFile().startAsProcess(); break;
            case 2:     revealInFinder(); break;
            case 3:     deleteAllSelectedItems(); break;
            case 4:     triggerAsyncRename (item); break;
            case 5:     item.getShouldAddToBinaryResourcesValue().setValue (! item.shouldBeAddedToBinaryResources()); break;
            case 6:     item.getShouldAddToXcodeResourcesValue().setValue (! item.shouldBeAddedToXcodeResources()); break;
            case 7:     item.getShouldCompileValue().setValue (! item.shouldBeCompiled()); break;

            default:
                if (auto* parentGroup = dynamic_cast<GroupItem*> (getParentProjectItem()))
                    parentGroup->processCreateFileMenuItem (resultCode);

                break;
        }
    }
};

//==============================================================================
class GroupItem   : public FileTreeItemBase
{
public:
    GroupItem (const Project::Item& projectItem, const String& filter = {})
        : FileTreeItemBase (projectItem),
          searchFilter (filter)
    {
    }

    bool isRoot() const override                                 { return item.isMainGroup(); }
    bool acceptsFileDrop (const StringArray&) const override     { return true; }

    void addNewGroup()
    {
        auto newGroup = item.addNewSubGroup ("New Group", 0);
        triggerAsyncRename (newGroup);
    }

    bool acceptsDragItems (const OwnedArray<Project::Item>& selectedNodes) override
    {
        for (auto i = selectedNodes.size(); --i >= 0;)
            if (item.canContain (*selectedNodes.getUnchecked(i)))
                return true;

        return false;
    }

    void addFilesAtIndex (const StringArray& files, int insertIndex) override
    {
        for (auto f : files)
        {
            if (item.addFileAtIndex (f, insertIndex, true))
                ++insertIndex;
        }
    }

    void addFilesRetainingSortOrder (const StringArray& files) override
    {
        for (auto i = files.size(); --i >= 0;)
            item.addFileRetainingSortOrder (files[i], true);
    }

    void moveSelectedItemsTo (OwnedArray<Project::Item>& selectedNodes, int insertIndex) override
    {
        moveItems (selectedNodes, item, insertIndex);
    }

    void checkFileStatus() override
    {
        for (int i = 0; i < getNumSubItems(); ++i)
            if (auto* p = dynamic_cast<FileTreeItemBase*> (getSubItem(i)))
                p->checkFileStatus();
    }

    bool isGroupEmpty (const Project::Item& group) // recursive
    {
        for (int i = 0; i < group.getNumChildren(); ++i)
        {
            auto child = group.getChild (i);

            if ((child.isGroup() && ! isGroupEmpty (child))
                   || (child.isFile() && child.getName().containsIgnoreCase (searchFilter)))
                return false;
        }

        return true;
    }

    FileTreeItemBase* createSubItem (const Project::Item& child) override
    {
        if (child.isGroup())
        {
            if (searchFilter.isNotEmpty() && isGroupEmpty (child))
                return nullptr;

            return new GroupItem (child, searchFilter);
        }

        if (child.isFile())
        {
            if (child.getName().containsIgnoreCase (searchFilter))
                return new SourceFileItem (child);

            return nullptr;
        }

        jassertfalse;
        return nullptr;
    }

    void showDocument() override
    {
        if (auto* pcc = getProjectContentComponent())
            pcc->setEditorComponent (new FileGroupInformationComponent (item), nullptr);
    }

    static void openAllGroups (TreeViewItem* root)
    {
        for (int i = 0; i < root->getNumSubItems(); ++i)
            if (auto* sub = root->getSubItem (i))
                openOrCloseAllSubGroups (*sub, true);
    }

    static void closeAllGroups (TreeViewItem* root)
    {
        for (int i = 0; i < root->getNumSubItems(); ++i)
            if (auto* sub = root->getSubItem (i))
                openOrCloseAllSubGroups (*sub, false);
    }

    static void openOrCloseAllSubGroups (TreeViewItem& item, bool shouldOpen)
    {
        item.setOpen (shouldOpen);

        for (auto i = item.getNumSubItems(); --i >= 0;)
            if (auto* sub = item.getSubItem (i))
                openOrCloseAllSubGroups (*sub, shouldOpen);
    }

    static void setFilesToCompile (Project::Item item, const bool shouldCompile)
    {
        if (item.isFile())
            item.getShouldCompileValue() = shouldCompile;

        for (auto i = item.getNumChildren(); --i >= 0;)
            setFilesToCompile (item.getChild (i), shouldCompile);
    }

    void showPopupMenu() override
    {
        PopupMenu m;
        addCreateFileMenuItems (m);

        m.addSeparator();

        m.addItem (1, "Collapse all Groups");
        m.addItem (2, "Expand all Groups");

        if (! isRoot())
        {
            if (isOpen())
                m.addItem (3, "Collapse all Sub-groups");
            else
                m.addItem (4, "Expand all Sub-groups");
        }

        m.addSeparator();
        m.addItem (5, "Enable compiling of all enclosed files");
        m.addItem (6, "Disable compiling of all enclosed files");

        m.addSeparator();
        m.addItem (7, "Sort Items Alphabetically");
        m.addItem (8, "Sort Items Alphabetically (Groups first)");
        m.addSeparator();

        if (! isRoot())
        {
            m.addItem (9, "Rename...");
            m.addItem (10, "Delete");
        }

        launchPopupMenu (m);
    }

    void showAddMenu() override
    {
        PopupMenu m;
        addCreateFileMenuItems (m);

        launchPopupMenu (m);
    }

    void handlePopupMenuResult (int resultCode) override
    {
        switch (resultCode)
        {
            case 1:     closeAllGroups (getOwnerView()->getRootItem()); break;
            case 2:     openAllGroups (getOwnerView()->getRootItem()); break;
            case 3:     openOrCloseAllSubGroups (*this, false); break;
            case 4:     openOrCloseAllSubGroups (*this, true); break;
            case 5:     setFilesToCompile (item, true); break;
            case 6:     setFilesToCompile (item, false); break;
            case 7:     item.sortAlphabetically (false, false); break;
            case 8:     item.sortAlphabetically (true, false); break;
            case 9:     triggerAsyncRename (item); break;
            case 10:    deleteAllSelectedItems(); break;
            default:    processCreateFileMenuItem (resultCode); break;
        }
    }

    void addCreateFileMenuItems (PopupMenu& m)
    {
        m.addItem (1001, "Add New Group");
        m.addItem (1002, "Add Existing Files...");

        m.addSeparator();
        NewFileWizard().addWizardsToMenu (m);
    }

    void processCreateFileMenuItem (int menuID)
    {
        switch (menuID)
        {
            case 1001:  addNewGroup(); break;
            case 1002:  browseToAddExistingFiles(); break;

            default:
                jassert (getProject() != nullptr);
                NewFileWizard().runWizardFromMenu (menuID, *getProject(), item);
                break;
        }
    }

    Project* getProject()
    {
        if (auto* tv = getOwnerView())
            if (auto* pcc = tv->findParentComponentOfClass<ProjectContentComponent>())
                return pcc->getProject();

        return nullptr;
    }

    void setSearchFilter (const String& filter) override
    {
        searchFilter = filter;
        refreshSubItems();
    }

    String searchFilter;
};
