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

   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.

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

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

    ~ProjectTreeItemBase()
    {
        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
    {
        if (item.isMainGroup())
            item.project.setTitle (newName);
        else
            item.getNameValue() = newName;
    }

    bool isMissing() const override                     { return isFileMissing; }
    virtual File getFile() const                        { return item.getFile(); }

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

    virtual void deleteAllSelectedItems() override
    {
        TreeView* tree = getOwnerView();
        const int numSelected = tree->getNumSelectedItems();
        OwnedArray<File> filesToTrash;
        OwnedArray<Project::Item> itemsToRemove;

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

                if (p->getFile().existsAsFile())
                    filesToTrash.add (new File (p->getFile()));
            }
        }

        if (filesToTrash.size() > 0)
        {
            String fileList;
            const int maxFilesToList = 10;
            for (int 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...";

            int 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 (ProjectTreeItemBase* treeRootItem = dynamic_cast<ProjectTreeItemBase*> (tree->getRootItem()))
        {
            OpenDocumentManager& om = ProjucerApplication::getApp().openDocumentManager;

            for (int i = filesToTrash.size(); --i >= 0;)
            {
                const File f (*filesToTrash.getUnchecked(i));

                om.closeFile (f, false);

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

            for (int i = itemsToRemove.size(); --i >= 0;)
            {
                if (ProjectTreeItemBase* itemToRemove = treeRootItem->findTreeViewItem (*itemsToRemove.getUnchecked(i)))
                {
                    om.closeFile (itemToRemove->getFile(), false);
                    itemToRemove->deleteItem();
                }
            }
        }
        else
        {
            jassertfalse;
        }
    }

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

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

        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)
    {
        const File file (getFile());
        const bool nowMissing = file != File() && ! file.exists();

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

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

    virtual void addFilesRetainingSortOrder (const StringArray& files)
    {
        if (ProjectTreeItemBase* 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, ProjectTreeItemBase* item)
    {
        switch (resultCode)
        {
            case 1:     item->deleteAllSelectedItems(); break;
            default:    break;
        }
    }

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

        const bool wasOpen = isOpen();
        setOpen (true);

        for (int i = getNumSubItems(); --i >= 0;)
        {
            if (ProjectTreeItemBase* pg = dynamic_cast<ProjectTreeItemBase*> (getSubItem(i)))
                if (ProjectTreeItemBase* 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 (ProjectTreeItemBase* 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)
        {
            TreeView* tree = getOwnerView();
            ScopedPointer<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)
        {
            TreeView* tree = dynamic_cast<TreeView*> (dragSourceDetails.sourceComponent.get());

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

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

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

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

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

protected:
    bool isFileMissing;

    virtual ProjectTreeItemBase* 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 (ProjectTreeItemBase* root = dynamic_cast<ProjectTreeItemBase*> (tree->getRootItem()))
                        if (ProjectTreeItemBase* 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 (int i = selectedNodes.size(); --i >= 0;)
        {
            Project::Item* const 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 (int i = selectedNodes.size(); --i >= 0;)
        {
            Project::Item* const n = selectedNodes.getUnchecked(i);

            for (int 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)
        {
            Project::Item* 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 (int i = parent.getNumChildren(); --i >= 0;)
            if (parent.getChild (i) == child)
                return i;

        return -1;
    }
};
