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

   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.

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

namespace juce
{

template <typename T>
int threeWayCompare (const T& a, const T& b)
{
    if (a < b) return -1;
    if (b < a) return 1;
    return 0;
}

int threeWayCompare (const String& a, const String& b);
int threeWayCompare (const String& a, const String& b)
{
    return a.compare (b);
}

struct ReverseCompareString
{
    String value;
};

int threeWayCompare (const ReverseCompareString& a, const ReverseCompareString& b);
int threeWayCompare (const ReverseCompareString& a, const ReverseCompareString& b)
{
    return b.value.compare (a.value);
}

template <size_t position, typename... Ts>
constexpr int threeWayCompareImpl (const std::tuple<Ts...>& a, const std::tuple<Ts...>& b)
{
    if constexpr (position == sizeof... (Ts))
    {
        ignoreUnused (a, b);
        return 0;
    }
    else
    {
        const auto head = threeWayCompare (std::get<position> (a), std::get<position> (b));

        if (head != 0)
            return head;

        return threeWayCompareImpl<position + 1> (a, b);
    }
}

template <typename... Ts>
constexpr int threeWayCompare (const std::tuple<Ts...>& a, const std::tuple<Ts...>& b)
{
    return threeWayCompareImpl<0> (a, b);
}

//==============================================================================
class FileListTreeItem final : public TreeViewItem,
                               private TimeSliceClient,
                               private AsyncUpdater
{
public:
    FileListTreeItem (FileTreeComponent& treeComp,
                      const File& f,
                      TimeSliceThread& t)
        : file (f),
          owner (treeComp),
          thread (t)
    {
    }

    void update (const DirectoryContentsList::FileInfo& fileInfo)
    {
        fileSize = File::descriptionOfSizeInBytes (fileInfo.fileSize);
        modTime = fileInfo.modificationTime.formatted ("%d %b '%y %H:%M");
        isDirectory = fileInfo.isDirectory;
        repaintItem();
    }

    ~FileListTreeItem() override
    {
        thread.removeTimeSliceClient (this);
        clearSubItems();
    }

    //==============================================================================
    bool mightContainSubItems() override                 { return isDirectory; }
    String getUniqueName() const override                { return file.getFullPathName(); }
    int getItemHeight() const override                   { return owner.getItemHeight(); }

    var getDragSourceDescription() override              { return owner.getDragAndDropDescription(); }

    void itemOpennessChanged (bool isNowOpen) override
    {
        NullCheckedInvocation::invoke (onOpennessChanged, file, isNowOpen);
    }

    void paintItem (Graphics& g, int width, int height) override
    {
        ScopedLock lock (iconUpdate);

        if (file != File())
        {
            updateIcon (true);

            if (icon.isNull())
                thread.addTimeSliceClient (this);
        }

        owner.getLookAndFeel().drawFileBrowserRow (g, width, height,
                                                   file, file.getFileName(),
                                                   &icon, fileSize, modTime,
                                                   isDirectory, isSelected(),
                                                   getIndexInParent(), owner);
    }

    String getAccessibilityName() override
    {
        return file.getFileName();
    }

    void itemClicked (const MouseEvent& e) override
    {
        owner.sendMouseClickMessage (file, e);
    }

    void itemDoubleClicked (const MouseEvent& e) override
    {
        TreeViewItem::itemDoubleClicked (e);

        owner.sendDoubleClickMessage (file);
    }

    void itemSelectionChanged (bool) override
    {
        owner.sendSelectionChangeMessage();
    }

    int useTimeSlice() override
    {
        updateIcon (false);
        return -1;
    }

    void handleAsyncUpdate() override
    {
        owner.repaint();
    }

    const File file;
    std::function<void (const File&, bool)> onOpennessChanged;

private:
    FileTreeComponent& owner;
    bool isDirectory = false;
    TimeSliceThread& thread;
    CriticalSection iconUpdate;
    Image icon;
    String fileSize, modTime;

    void updateIcon (const bool onlyUpdateIfCached)
    {
        if (icon.isNull())
        {
            auto hashCode = (file.getFullPathName() + "_iconCacheSalt").hashCode();
            auto im = ImageCache::getFromHashCode (hashCode);

            if (im.isNull() && ! onlyUpdateIfCached)
            {
                im = detail::WindowingHelpers::createIconForFile (file);

                if (im.isValid())
                    ImageCache::addImageToCache (im, hashCode);
            }

            if (im.isValid())
            {
                {
                    ScopedLock lock (iconUpdate);
                    icon = im;
                }

                triggerAsyncUpdate();
            }
        }
    }

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (FileListTreeItem)
};

class DirectoryScanner final : private ChangeListener
{
public:
    struct Listener
    {
        virtual ~Listener() = default;

        virtual void rootChanged() = 0;
        virtual void directoryChanged (const DirectoryContentsList&) = 0;
    };

    DirectoryScanner (DirectoryContentsList& rootIn, Listener& listenerIn)
        : root (rootIn), listener (listenerIn)
    {
        root.addChangeListener (this);
    }

    ~DirectoryScanner() override
    {
        root.removeChangeListener (this);
    }

    void refresh()
    {
        root.refresh();
    }

    void open (const File& f)
    {
        auto& contentsList = [&]() -> auto&
        {
            if (auto it = contentsLists.find (f); it != contentsLists.end())
                return it->second;

            auto insertion = contentsLists.emplace (std::piecewise_construct,
                                                    std::forward_as_tuple (f),
                                                    std::forward_as_tuple (root.getFilter(),
                                                                           root.getTimeSliceThread()));
            return insertion.first->second;
        }();

        contentsList.addChangeListener (this);
        contentsList.setDirectory (f, true, true);
        contentsList.refresh();
    }

    void close (const File& f)
    {
        if (auto it = contentsLists.find (f); it != contentsLists.end())
            contentsLists.erase (it);
    }

    File getRootDirectory() const
    {
        return root.getDirectory();
    }

    bool isStillLoading() const
    {
        return std::any_of (contentsLists.begin(),
                            contentsLists.end(),
                            [] (const auto& it)
                            {
                                return it.second.isStillLoading();
                            });
    }

private:
    void changeListenerCallback (ChangeBroadcaster* source) override
    {
        auto* sourceList = static_cast<DirectoryContentsList*> (source);

        if (sourceList == &root)
        {
            if (std::exchange (lastDirectory, root.getDirectory()) != root.getDirectory())
            {
                contentsLists.clear();
                listener.rootChanged();
            }
            else
            {
                for (auto& contentsList : contentsLists)
                    contentsList.second.refresh();
            }
        }

        listener.directoryChanged (*sourceList);
    }

    DirectoryContentsList& root;
    Listener& listener;
    File lastDirectory;
    std::map<File, DirectoryContentsList> contentsLists;
};

struct FileEntry
{
    String path;
    bool isDirectory;

    int compareWindows (const FileEntry& other) const
    {
        const auto toTuple = [] (const auto& x) { return std::tuple (! x.isDirectory, x.path.toLowerCase()); };
        return threeWayCompare (toTuple (*this), toTuple (other));
    }

    int compareLinux (const FileEntry& other) const
    {
        const auto toTuple = [] (const auto& x) { return std::tuple (x.path.toUpperCase(), ReverseCompareString { x.path }); };
        return threeWayCompare (toTuple (*this), toTuple (other));
    }

    int compareDefault (const FileEntry& other) const
    {
        return threeWayCompare (path.toLowerCase(), other.path.toLowerCase());
    }
};

class OSDependentFileComparisonRules
{
public:
    explicit OSDependentFileComparisonRules (SystemStats::OperatingSystemType systemTypeIn)
        : systemType (systemTypeIn)
    {}

    int compare (const FileEntry& first, const FileEntry& second) const
    {
        if ((systemType & SystemStats::OperatingSystemType::Windows) != 0)
            return first.compareWindows (second);

        if ((systemType & SystemStats::OperatingSystemType::Linux) != 0)
            return first.compareLinux (second);

        return first.compareDefault (second);
    }

    bool operator() (const FileEntry& first, const FileEntry& second) const
    {
        return compare (first, second) < 0;
    }

private:
    SystemStats::OperatingSystemType systemType;
};

class FileTreeComponent::Controller final : private DirectoryScanner::Listener
{
public:
    explicit Controller (FileTreeComponent& ownerIn)
        : owner (ownerIn),
          scanner (owner.directoryContentsList, *this)
    {
        refresh();
    }

    ~Controller() override
    {
        owner.deleteRootItem();
    }

    void refresh()
    {
        scanner.refresh();
    }

    void selectFile (const File& target)
    {
        pendingFileSelection.emplace (target);
        tryResolvePendingFileSelection();
    }

private:
    template <typename ItemCallback>
    static void forEachItemRecursive (TreeViewItem* item, ItemCallback&& cb)
    {
        if (item == nullptr)
            return;

        if (auto* fileListItem = dynamic_cast<FileListTreeItem*> (item))
            cb (fileListItem);

        for (int i = 0; i < item->getNumSubItems(); ++i)
            forEachItemRecursive (item->getSubItem (i), cb);
    }

    //==============================================================================
    void rootChanged() override
    {
        owner.deleteRootItem();
        treeItemForFile.clear();
        owner.setRootItem (createNewItem (scanner.getRootDirectory()).release());
    }

    void directoryChanged (const DirectoryContentsList& contentsList) override
    {
        auto* parentItem = [&]() -> FileListTreeItem*
        {
            if (auto it = treeItemForFile.find (contentsList.getDirectory()); it != treeItemForFile.end())
                return it->second;

            return nullptr;
        }();

        if (parentItem == nullptr)
        {
            jassertfalse;
            return;
        }

        for (int i = 0; i < contentsList.getNumFiles(); ++i)
        {
            auto file = contentsList.getFile (i);

            DirectoryContentsList::FileInfo fileInfo;
            contentsList.getFileInfo (i, fileInfo);

            auto* item = [&]
            {
                if (auto it = treeItemForFile.find (file); it != treeItemForFile.end())
                    return it->second;

                auto* newItem = createNewItem (file).release();
                parentItem->addSubItem (newItem);
                return newItem;
            }();

            if (item->isOpen() && fileInfo.isDirectory)
                scanner.open (item->file);

            item->update (fileInfo);
        }

        if (contentsList.isStillLoading())
            return;

        std::set<File> allFiles;

        for (int i = 0; i < contentsList.getNumFiles(); ++i)
            allFiles.insert (contentsList.getFile (i));

        for (int i = 0; i < parentItem->getNumSubItems();)
        {
            auto* fileItem = dynamic_cast<FileListTreeItem*> (parentItem->getSubItem (i));

            if (fileItem != nullptr && allFiles.count (fileItem->file) == 0)
            {
                forEachItemRecursive (parentItem->getSubItem (i),
                                      [this] (auto* item)
                                      {
                                          scanner.close (item->file);
                                          treeItemForFile.erase (item->file);
                                      });

                parentItem->removeSubItem (i);
            }
            else
            {
                ++i;
            }
        }

        struct Comparator
        {
            // The different OSes compare and order files in different ways. This function aims
            // to match these different rules of comparison to mimic other FileBrowserComponent
            // view modes where we don't need to order the results, and can just rely on the
            // ordering of the list provided by the OS.
            static int compareElements (TreeViewItem* first, TreeViewItem* second)
            {
                auto* item1 = dynamic_cast<FileListTreeItem*> (first);
                auto* item2 = dynamic_cast<FileListTreeItem*> (second);

                if (item1 == nullptr || item2 == nullptr)
                    return 0;

                static const OSDependentFileComparisonRules comparisonRules { SystemStats::getOperatingSystemType() };

                return comparisonRules.compare ({ item1->file.getFullPathName(), item1->file.isDirectory() },
                                                { item2->file.getFullPathName(), item2->file.isDirectory() });
            }
        };

        static Comparator comparator;
        parentItem->sortSubItems (comparator);
        tryResolvePendingFileSelection();
    }

    std::unique_ptr<FileListTreeItem> createNewItem (const File& file)
    {
        auto newItem = std::make_unique<FileListTreeItem> (owner,
                                                           file,
                                                           owner.directoryContentsList.getTimeSliceThread());

        newItem->onOpennessChanged = [this, itemPtr = newItem.get()] (const auto& f, auto isOpen)
        {
            if (isOpen)
            {
                scanner.open (f);
            }
            else
            {
                forEachItemRecursive (itemPtr,
                                      [this] (auto* item)
                                      {
                                          scanner.close (item->file);
                                      });
            }
        };

        treeItemForFile[file] = newItem.get();
        return newItem;
    }

    void tryResolvePendingFileSelection()
    {
        if (! pendingFileSelection.has_value())
            return;

        if (auto item = treeItemForFile.find (*pendingFileSelection); item != treeItemForFile.end())
        {
            item->second->setSelected (true, true);
            pendingFileSelection.reset();
            return;
        }

        if (owner.directoryContentsList.isStillLoading() || scanner.isStillLoading())
            return;

        owner.clearSelectedItems();
    }

    FileTreeComponent& owner;
    std::map<File, FileListTreeItem*> treeItemForFile;
    DirectoryScanner scanner;
    std::optional<File> pendingFileSelection;
};

//==============================================================================
FileTreeComponent::FileTreeComponent (DirectoryContentsList& listToShow)
    : DirectoryContentsDisplayComponent (listToShow),
      itemHeight (22)
{
    controller = std::make_unique<Controller> (*this);
    setRootItemVisible (false);
    refresh();
}

FileTreeComponent::~FileTreeComponent()
{
    deleteRootItem();
}

void FileTreeComponent::refresh()
{
    controller->refresh();
}

//==============================================================================
File FileTreeComponent::getSelectedFile (const int index) const
{
    if (auto* item = dynamic_cast<const FileListTreeItem*> (getSelectedItem (index)))
        return item->file;

    return {};
}

void FileTreeComponent::deselectAllFiles()
{
    clearSelectedItems();
}

void FileTreeComponent::scrollToTop()
{
    getViewport()->getVerticalScrollBar().setCurrentRangeStart (0);
}

void FileTreeComponent::setDragAndDropDescription (const String& description)
{
    dragAndDropDescription = description;
}

void FileTreeComponent::setSelectedFile (const File& target)
{
    controller->selectFile (target);
}

void FileTreeComponent::setItemHeight (int newHeight)
{
    if (itemHeight != newHeight)
    {
        itemHeight = newHeight;

        if (auto* root = getRootItem())
            root->treeHasChanged();
    }
}

#if JUCE_UNIT_TESTS

class FileTreeComponentTests final : public UnitTest
{
public:
    //==============================================================================
    FileTreeComponentTests() : UnitTest ("FileTreeComponentTests", UnitTestCategories::gui) {}

    void runTest() override
    {
        const auto checkOrder = [] (const auto& orderedFiles, const std::vector<String>& expected)
        {
            return std::equal (orderedFiles.begin(), orderedFiles.end(),
                               expected.begin(), expected.end(),
                               [] (const auto& entry, const auto& expectedPath) { return entry.path == expectedPath; });
        };

        const auto doSort = [] (const auto platform, auto& range)
        {
            std::sort (range.begin(), range.end(), OSDependentFileComparisonRules { platform });
        };

        beginTest ("Test Linux filename ordering");
        {
            std::vector<FileEntry> filesToOrder { { "_test", false },
                                                  { "Atest", false },
                                                  { "atest", false } };

            doSort (SystemStats::OperatingSystemType::Linux, filesToOrder);

            expect (checkOrder (filesToOrder, { "atest", "Atest", "_test" }));
        }

        beginTest ("Test Windows filename ordering");
        {
            std::vector<FileEntry> filesToOrder { { "cmake_install.cmake", false },
                                                  { "CMakeFiles", true },
                                                  { "JUCEConfig.cmake", false },
                                                  { "tools", true },
                                                  { "cmakefiles.cmake", false } };

            doSort (SystemStats::OperatingSystemType::Windows, filesToOrder);

            expect (checkOrder (filesToOrder, { "CMakeFiles",
                                                "tools",
                                                "cmake_install.cmake",
                                                "cmakefiles.cmake",
                                                "JUCEConfig.cmake" }));
        }

        beginTest ("Test MacOS filename ordering");
        {
            std::vector<FileEntry> filesToOrder { { "cmake_install.cmake", false },
                                                  { "CMakeFiles", true },
                                                  { "tools", true },
                                                  { "JUCEConfig.cmake", false } };

            doSort (SystemStats::OperatingSystemType::MacOSX, filesToOrder);

            expect (checkOrder (filesToOrder, { "cmake_install.cmake",
                                                "CMakeFiles",
                                                "JUCEConfig.cmake",
                                                "tools" }));
        }
    }
};

static FileTreeComponentTests fileTreeComponentTests;

#endif

} // namespace juce
