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

   This file is part of the JUCE framework.
   Copyright (c) Raw Material Software Limited

   JUCE is an open source framework subject to commercial or open source
   licensing.

   By downloading, installing, or using the JUCE framework, or combining the
   JUCE framework with any other source code, object code, content or any other
   copyrightable work, you agree to the terms of the JUCE End User Licence
   Agreement, and all incorporated terms including the JUCE Privacy Policy and
   the JUCE Website Terms of Service, as applicable, which will bind you. If you
   do not agree to the terms of these agreements, we will not license the JUCE
   framework to you, and you must discontinue the installation or download
   process and cease use of the JUCE framework.

   JUCE End User Licence Agreement: https://juce.com/legal/juce-9-licence/
   JUCE Privacy Policy: https://juce.com/juce-privacy-policy
   JUCE Website Terms of Service: https://juce.com/juce-website-terms-of-service/

   Or:

   You may also use this code under the terms of the AGPLv3:
   https://www.gnu.org/licenses/agpl-3.0.en.html

   THE JUCE FRAMEWORK IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL
   WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING WARRANTY OF
   MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED.

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

namespace juce
{

KnownPluginList::KnownPluginList()  {}
KnownPluginList::~KnownPluginList() {}

void KnownPluginList::clear()
{
    ScopedLock lock (typesArrayLock);

    if (! types.isEmpty())
    {
        types.clear();
        sendChangeMessage();
    }
}

int KnownPluginList::getNumTypes() const noexcept
{
    ScopedLock lock (typesArrayLock);
    return types.size();
}

Array<PluginDescription> KnownPluginList::getTypes() const
{
    ScopedLock lock (typesArrayLock);
    return types;
}

Array<PluginDescription> KnownPluginList::getTypesForFormat (AudioPluginFormat& format) const
{
    Array<PluginDescription> result;

    for (auto& d : getTypes())
        if (d.pluginFormatName == format.getName())
            result.add (d);

    return result;
}

std::unique_ptr<PluginDescription> KnownPluginList::getTypeForFile (const String& fileOrIdentifier) const
{
    ScopedLock lock (typesArrayLock);

    for (auto& desc : types)
        if (desc.fileOrIdentifier == fileOrIdentifier)
            return std::make_unique<PluginDescription> (desc);

    return {};
}

std::unique_ptr<PluginDescription> KnownPluginList::getTypeForIdentifierString (const String& identifierString) const
{
    ScopedLock lock (typesArrayLock);

    for (auto& desc : types)
        if (desc.matchesIdentifierString (identifierString))
            return std::make_unique<PluginDescription> (desc);

    return {};
}

bool KnownPluginList::addType (const PluginDescription& type)
{
    {
        ScopedLock lock (typesArrayLock);

        for (auto& desc : types)
        {
            if (desc.isDuplicateOf (type))
            {
                // strange - found a duplicate plugin with different info
                jassert (desc.name == type.name);
                jassert (desc.isInstrument == type.isInstrument);

                desc = type;
                return false;
            }
        }

        types.insert (0, type);
    }

    sendChangeMessage();
    return true;
}

void KnownPluginList::removeType (const PluginDescription& type)
{
    {
        ScopedLock lock (typesArrayLock);

        for (int i = types.size(); --i >= 0;)
            if (types.getUnchecked (i).isDuplicateOf (type))
                types.remove (i);
    }

    sendChangeMessage();
}

bool KnownPluginList::isListingUpToDate (const String& fileOrIdentifier,
                                         AudioPluginFormat& formatToUse) const
{
    if (getTypeForFile (fileOrIdentifier) == nullptr)
        return false;

    ScopedLock lock (typesArrayLock);

    for (auto& d : types)
        if (d.fileOrIdentifier == fileOrIdentifier && formatToUse.pluginNeedsRescanning (d))
            return false;

    return true;
}

void KnownPluginList::setCustomScanner (std::unique_ptr<CustomScanner> newScanner)
{
    if (scanner != newScanner)
        scanner = std::move (newScanner);
}

bool KnownPluginList::scanAndAddFile (const String& fileOrIdentifier,
                                      const bool dontRescanIfAlreadyInList,
                                      OwnedArray<PluginDescription>& typesFound,
                                      AudioPluginFormat& format)
{
    const ScopedLock sl (scanLock);

    if (dontRescanIfAlreadyInList
         && getTypeForFile (fileOrIdentifier) != nullptr)
    {
        bool needsRescanning = false;

        ScopedLock lock (typesArrayLock);

        for (auto& d : types)
        {
            if (d.fileOrIdentifier == fileOrIdentifier && d.pluginFormatName == format.getName())
            {
                if (format.pluginNeedsRescanning (d))
                    needsRescanning = true;
                else
                    typesFound.add (new PluginDescription (d));
            }
        }

        if (! needsRescanning)
            return false;
    }

    if (blacklist.contains (fileOrIdentifier))
        return false;

    OwnedArray<PluginDescription> found;

    {
        const ScopedUnlock sl2 (scanLock);

        if (scanner != nullptr)
        {
            if (! scanner->findPluginTypesFor (format, found, fileOrIdentifier))
                addToBlacklist (fileOrIdentifier);
        }
        else
        {
            format.findAllTypesForFile (found, fileOrIdentifier);
        }
    }

    for (auto* desc : found)
    {
        if (desc == nullptr)
        {
            jassertfalse;
            continue;
        }

        addType (*desc);
        typesFound.add (new PluginDescription (*desc));
    }

    return ! found.isEmpty();
}

void KnownPluginList::scanAndAddDragAndDroppedFiles (AudioPluginFormatManager& formatManager,
                                                     const StringArray& files,
                                                     OwnedArray<PluginDescription>& typesFound)
{
    for (const auto& filenameOrID : files)
    {
        bool found = false;

        for (auto format : formatManager.getFormats())
        {
            if (format->fileMightContainThisPluginType (filenameOrID)
                 && scanAndAddFile (filenameOrID, true, typesFound, *format))
            {
                found = true;
                break;
            }
        }

        if (! found)
        {
            File f (filenameOrID);

            if (f.isDirectory())
            {
                StringArray s;

                for (auto& subFile : f.findChildFiles (File::findFilesAndDirectories, false))
                    s.add (subFile.getFullPathName());

                scanAndAddDragAndDroppedFiles (formatManager, s, typesFound);
            }
        }
    }

    scanFinished();
}

void KnownPluginList::scanFinished()
{
    if (scanner != nullptr)
        scanner->scanFinished();
}

const StringArray& KnownPluginList::getBlacklistedFiles() const
{
    return blacklist;
}

void KnownPluginList::addToBlacklist (const String& pluginID)
{
    if (! blacklist.contains (pluginID))
    {
        blacklist.add (pluginID);
        sendChangeMessage();
    }
}

void KnownPluginList::removeFromBlacklist (const String& pluginID)
{
    const int index = blacklist.indexOf (pluginID);

    if (index >= 0)
    {
        blacklist.remove (index);
        sendChangeMessage();
    }
}

void KnownPluginList::clearBlacklistedFiles()
{
    if (blacklist.size() > 0)
    {
        blacklist.clear();
        sendChangeMessage();
    }
}

//==============================================================================
struct PluginSorter
{
    PluginSorter (KnownPluginList::SortMethod sortMethod, bool forwards) noexcept
        : method (sortMethod), direction (forwards ? 1 : -1) {}

    bool operator() (const PluginDescription& first, const PluginDescription& second) const
    {
        int diff = 0;

        switch (method)
        {
            case KnownPluginList::sortByCategory:           diff = first.category.compareNatural (second.category, false); break;
            case KnownPluginList::sortByManufacturer:       diff = first.manufacturerName.compareNatural (second.manufacturerName, false); break;
            case KnownPluginList::sortByFormat:             diff = first.pluginFormatName.compare (second.pluginFormatName); break;
            case KnownPluginList::sortByFileSystemLocation: diff = lastPathPart (first.fileOrIdentifier).compare (lastPathPart (second.fileOrIdentifier)); break;
            case KnownPluginList::sortByInfoUpdateTime:     diff = compare (first.lastInfoUpdateTime, second.lastInfoUpdateTime); break;
            case KnownPluginList::sortAlphabetically:
            case KnownPluginList::defaultOrder:
            default: break;
        }

        if (diff == 0)
            diff = first.name.compareNatural (second.name, false);

        return diff * direction < 0;
    }

private:
    static String lastPathPart (const String& path)
    {
        return path.replaceCharacter ('\\', '/').upToLastOccurrenceOf ("/", false, false);
    }

    static int compare (Time a, Time b) noexcept
    {
        if (a < b)   return -1;
        if (b < a)   return 1;

        return 0;
    }

    KnownPluginList::SortMethod method;
    int direction;
};

void KnownPluginList::sort (const SortMethod method, bool forwards)
{
    if (method != defaultOrder)
    {
        Array<PluginDescription> oldOrder, newOrder;

        {
            ScopedLock lock (typesArrayLock);

            oldOrder.addArray (types);
            std::stable_sort (types.begin(), types.end(), PluginSorter (method, forwards));
            newOrder.addArray (types);
        }

        auto hasOrderChanged = [&]
        {
            for (int i = 0; i < oldOrder.size(); ++i)
                 if (! oldOrder[i].isDuplicateOf (newOrder[i]))
                     return true;

            return false;
        }();

        if (hasOrderChanged)
            sendChangeMessage();
    }
}

//==============================================================================
std::unique_ptr<XmlElement> KnownPluginList::createXml() const
{
    auto e = std::make_unique<XmlElement> ("KNOWNPLUGINS");

    {
        ScopedLock lock (typesArrayLock);

        for (int i = types.size(); --i >= 0;)
            e->prependChildElement (types.getUnchecked (i).createXml().release());
    }

    for (auto& b : blacklist)
        e->createNewChildElement ("BLACKLISTED")->setAttribute ("id", b);

    return e;
}

void KnownPluginList::recreateFromXml (const XmlElement& xml)
{
    clear();
    clearBlacklistedFiles();

    if (xml.hasTagName ("KNOWNPLUGINS"))
    {
        for (auto* e : xml.getChildIterator())
        {
            PluginDescription info;

            if (e->hasTagName ("BLACKLISTED"))
                blacklist.add (e->getStringAttribute ("id"));
            else if (info.loadFromXml (*e))
                addType (info);
        }
    }
}

//==============================================================================
struct PluginTreeUtils
{
    enum { menuIdBase = 0x324503f4 };

    static void buildTreeByFolder (KnownPluginList::PluginTree& tree, const Array<PluginDescription>& allPlugins)
    {
        for (auto& pd : allPlugins)
        {
            auto path = pd.fileOrIdentifier.replaceCharacter ('\\', '/')
                                           .upToLastOccurrenceOf ("/", false, false);

            if (path.substring (1, 2) == ":")
                path = path.substring (2);

            addPlugin (tree, pd, path);
        }

        optimiseFolders (tree, false);
    }

    static void optimiseFolders (KnownPluginList::PluginTree& tree, bool concatenateName)
    {
        for (int i = tree.subFolders.size(); --i >= 0;)
        {
            auto& sub = *tree.subFolders.getUnchecked (i);
            optimiseFolders (sub, concatenateName || (tree.subFolders.size() > 1));

            if (sub.plugins.isEmpty())
            {
                for (auto* s : sub.subFolders)
                {
                    if (concatenateName)
                        s->folder = sub.folder + "/" + s->folder;

                    tree.subFolders.add (s);
                }

                sub.subFolders.clear (false);
                tree.subFolders.remove (i);
            }
        }
    }

    static void buildTreeByCategory (KnownPluginList::PluginTree& tree,
                                     const Array<PluginDescription>& sorted,
                                     const KnownPluginList::SortMethod sortMethod)
    {
        String lastType;
        auto current = std::make_unique<KnownPluginList::PluginTree>();

        for (auto& pd : sorted)
        {
            auto thisType = (sortMethod == KnownPluginList::sortByCategory ? pd.category
                                                                           : pd.manufacturerName);

            if (! thisType.containsNonWhitespaceChars())
                thisType = "Other";

            if (! thisType.equalsIgnoreCase (lastType))
            {
                if (current->plugins.size() + current->subFolders.size() > 0)
                {
                    current->folder = lastType;
                    tree.subFolders.add (std::move (current));
                    current = std::make_unique<KnownPluginList::PluginTree>();
                }

                lastType = thisType;
            }

            current->plugins.add (pd);
        }

        if (current->plugins.size() + current->subFolders.size() > 0)
        {
            current->folder = lastType;
            tree.subFolders.add (std::move (current));
        }
    }

    static void addPlugin (KnownPluginList::PluginTree& tree, PluginDescription pd, String path)
    {
       #if JUCE_MAC
        if (path.containsChar (':'))
            path = path.fromFirstOccurrenceOf (":", false, false); // avoid the special AU formatting nonsense on Mac
       #endif

        if (path.isEmpty())
        {
            tree.plugins.add (pd);
        }
        else
        {
            auto firstSubFolder = path.upToFirstOccurrenceOf ("/", false, false);
            auto remainingPath  = path.fromFirstOccurrenceOf ("/", false, false);

            for (int i = tree.subFolders.size(); --i >= 0;)
            {
                auto& subFolder = *tree.subFolders.getUnchecked (i);

                if (subFolder.folder.equalsIgnoreCase (firstSubFolder))
                {
                    addPlugin (subFolder, pd, remainingPath);
                    return;
                }
            }

            auto* newFolder = new KnownPluginList::PluginTree();
            newFolder->folder = firstSubFolder;
            tree.subFolders.add (newFolder);
            addPlugin (*newFolder, pd, remainingPath);
        }
    }

    static bool containsDuplicateNames (const Array<PluginDescription>& plugins, const String& name)
    {
        int matches = 0;

        for (auto& p : plugins)
            if (p.name == name && ++matches > 1)
                return true;

        return false;
    }

    static bool addToMenu (const KnownPluginList::PluginTree& tree, PopupMenu& m,
                           const Array<PluginDescription>& allPlugins,
                           const String& currentlyTickedPluginID)
    {
        bool isTicked = false;

        for (auto* sub : tree.subFolders)
        {
            PopupMenu subMenu;
            auto isItemTicked = addToMenu (*sub, subMenu, allPlugins, currentlyTickedPluginID);
            isTicked = isTicked || isItemTicked;

            m.addSubMenu (sub->folder, subMenu, true, nullptr, isItemTicked, 0);
        }

        auto getPluginMenuIndex = [&] (const PluginDescription& d)
        {
            int i = 0;

            for (auto& p : allPlugins)
            {
                if (p.isDuplicateOf (d))
                    return i + menuIdBase;

                ++i;
            }

            return 0;
        };

        for (auto& plugin : tree.plugins)
        {
            auto name = plugin.name;

            if (containsDuplicateNames (tree.plugins, name))
                name << " (" << plugin.pluginFormatName << ')';

            auto isItemTicked = plugin.matchesIdentifierString (currentlyTickedPluginID);
            isTicked = isTicked || isItemTicked;

            m.addItem (getPluginMenuIndex (plugin), name, true, isItemTicked);
        }

        return isTicked;
    }
};

std::unique_ptr<KnownPluginList::PluginTree> KnownPluginList::createTree (const Array<PluginDescription>& types, SortMethod sortMethod)
{
    Array<PluginDescription> sorted;
    sorted.addArray (types);

    std::stable_sort (sorted.begin(), sorted.end(), PluginSorter (sortMethod, true));

    auto tree = std::make_unique<PluginTree>();

    if (sortMethod == sortByCategory || sortMethod == sortByManufacturer || sortMethod == sortByFormat)
    {
        PluginTreeUtils::buildTreeByCategory (*tree, sorted, sortMethod);
    }
    else if (sortMethod == sortByFileSystemLocation)
    {
        PluginTreeUtils::buildTreeByFolder (*tree, sorted);
    }
    else
    {
        for (auto& p : sorted)
            tree->plugins.add (p);
    }

    return tree;
}

//==============================================================================
void KnownPluginList::addToMenu (PopupMenu& menu, const Array<PluginDescription>& types, SortMethod sortMethod,
                                 const String& currentlyTickedPluginID)
{
    auto tree = createTree (types, sortMethod);
    PluginTreeUtils::addToMenu (*tree, menu, types, currentlyTickedPluginID);
}

int KnownPluginList::getIndexChosenByMenu (const Array<PluginDescription>& types, int menuResultCode)
{
    auto i = menuResultCode - PluginTreeUtils::menuIdBase;
    return isPositiveAndBelow (i, types.size()) ? i : -1;
}

//==============================================================================
KnownPluginList::CustomScanner::CustomScanner() {}
KnownPluginList::CustomScanner::~CustomScanner() {}

void KnownPluginList::CustomScanner::scanFinished() {}

bool KnownPluginList::CustomScanner::shouldExit() const noexcept
{
    if (auto* job = ThreadPoolJob::getCurrentThreadPoolJob())
        return job->shouldExit();

    return false;
}

//==============================================================================
void KnownPluginList::addToMenu (PopupMenu& menu, SortMethod sortMethod, const String& currentlyTickedPluginID) const
{
    addToMenu (menu, getTypes(), sortMethod, currentlyTickedPluginID);
}

int KnownPluginList::getIndexChosenByMenu (int menuResultCode) const
{
    return getIndexChosenByMenu (getTypes(), menuResultCode);
}

std::unique_ptr<KnownPluginList::PluginTree> KnownPluginList::createTree (const SortMethod sortMethod) const
{
    return createTree (getTypes(), sortMethod);
}


} // namespace juce
