#include "DeepPluckPresetManager.h"

#include <algorithm>
#include <cmath>

namespace deeppluck
{
namespace
{
constexpr auto presetExtension = ".deeppluckpreset";
constexpr auto packFormat = "JustALilPluckPack";
constexpr auto schemaVersion = 1;
constexpr std::int64_t maximumPresetBytes = 1024 * 1024;
constexpr int maximumFiles = 2000;
constexpr int maximumDepth = 8;

juce::String sourcePrefix (PresetSource source)
{
    switch (source)
    {
        case PresetSource::user: return "user";
        case PresetSource::pack: return "pack";
        case PresetSource::imported: return "imported";
    }
    return "unknown";
}
}

struct DeepPluckPresetManager::ScanContext
{
    UserPresetCatalog result;
    int discoveredFiles = 0;
};

DeepPluckPresetManager::DeepPluckPresetManager (
    const juce::AudioProcessorValueTreeState& state,
    juce::String writingPluginVersion,
    juce::File injectedRoot)
    : juce::Thread ("DeepPluck Preset Scanner"),
      rootDirectory (injectedRoot == juce::File {}
                         ? juce::File::getSpecialLocation (juce::File::userDocumentsDirectory)
                               .getChildFile ("Tonetrax")
                               .getChildFile ("Just a Lil' Pluck Synth")
                               .getChildFile ("Presets")
                         : std::move (injectedRoot)),
      parameterState (state),
      pluginVersion (std::move (writingPluginVersion)),
      defaultValues (captureParameterDefaults (state))
{
}

DeepPluckPresetManager::~DeepPluckPresetManager()
{
    signalThreadShouldExit();
    scanWake.signal();
    stopThread (5000);
}

const juce::File& DeepPluckPresetManager::getRootDirectory() const noexcept
{
    return rootDirectory;
}

juce::File DeepPluckPresetManager::getUserDirectory() const
{
    return rootDirectory.getChildFile ("User");
}

UserPresetCatalog DeepPluckPresetManager::getCatalog() const
{
    const juce::ScopedLock lock (catalogLock);
    return catalog;
}

std::uint64_t DeepPluckPresetManager::getCatalogGeneration() const noexcept
{
    return catalogGeneration.load (std::memory_order_acquire);
}

void DeepPluckPresetManager::requestScan()
{
    if (! isThreadRunning())
        startThread (juce::Thread::Priority::low);
    scanRequested.store (true, std::memory_order_release);
    scanComplete.reset();
    scanWake.signal();
}

bool DeepPluckPresetManager::waitForScanForTests (int timeoutMilliseconds)
{
    if (! scanRequested.load (std::memory_order_acquire)
        && ! scanInProgress.load (std::memory_order_acquire))
        return true;
    return scanComplete.wait (juce::jmax (0, timeoutMilliseconds));
}

void DeepPluckPresetManager::run()
{
    while (! threadShouldExit())
    {
        scanWake.wait (-1);
        if (threadShouldExit())
            break;

        do
        {
            scanRequested.store (false, std::memory_order_release);
            scanInProgress.store (true, std::memory_order_release);
            auto nextCatalog = scanCatalog();
            nextCatalog.generation = catalogGeneration.fetch_add (
                                         1, std::memory_order_acq_rel) + 1;
            {
                const juce::ScopedLock lock (catalogLock);
                catalog = std::move (nextCatalog);
            }
            scanInProgress.store (false, std::memory_order_release);
        }
        while (scanRequested.exchange (false, std::memory_order_acq_rel)
               && ! threadShouldExit());

        scanComplete.signal();
    }
}

UserPresetCatalog DeepPluckPresetManager::scanCatalog() const
{
    ScanContext context;
    juce::String error;
    if (! ensureRootDirectories (error))
    {
        context.result.skippedErrors = 1;
        return context.result;
    }

    recordFileOperation();
    auto children = rootDirectory.findChildFiles (
        juce::File::findFilesAndDirectories, false);
    std::sort (children.begin(), children.end(), [] (const auto& left, const auto& right)
    {
        return left.getFileName().compareIgnoreCase (right.getFileName()) < 0;
    });

    for (const auto& child : children)
    {
        if (context.discoveredFiles >= maximumFiles)
            break;
        if (child.isSymbolicLink())
        {
            ++context.result.skippedErrors;
            continue;
        }

        if (child.isDirectory())
        {
            if (child.getFileName().equalsIgnoreCase ("User"))
                scanDirectory (child, child, PresetSource::user, "User", 0, context);
            else
                scanDirectory (child, child, PresetSource::pack,
                               readPackName (child), 0, context);
        }
        else if (hasPresetExtension (child))
        {
            ++context.discoveredFiles;
            UserPresetData data;
            if (! loadPresetFile (child, data, error))
            {
                ++context.result.skippedErrors;
                continue;
            }
            const auto relative = child.getRelativePathFrom (rootDirectory)
                                      .replaceCharacter ('\\', '/');
            context.result.entries.push_back ({
                "imported:" + relative.toLowerCase(), data.name, data.author,
                "Imported", {}, relative, child, PresetSource::imported, false, data.values
            });
        }
    }

    std::stable_sort (context.result.entries.begin(), context.result.entries.end(),
                      [] (const auto& left, const auto& right)
    {
        if (left.source != right.source)
            return static_cast<int> (left.source) < static_cast<int> (right.source);
        if (left.group.compareIgnoreCase (right.group) != 0)
            return left.group.compareIgnoreCase (right.group) < 0;
        return left.relativePath.compareIgnoreCase (right.relativePath) < 0;
    });
    return context.result;
}

void DeepPluckPresetManager::scanDirectory (const juce::File& directory,
                                            const juce::File& sourceRoot,
                                            PresetSource source,
                                            const juce::String& group,
                                            int depth,
                                            ScanContext& context) const
{
    if (depth > maximumDepth || context.discoveredFiles >= maximumFiles
        || ! isContainedByRoot (directory))
        return;

    recordFileOperation();
    auto children = directory.findChildFiles (
        juce::File::findFilesAndDirectories, false);
    std::sort (children.begin(), children.end(), [] (const auto& left, const auto& right)
    {
        return left.getFileName().compareIgnoreCase (right.getFileName()) < 0;
    });

    for (const auto& child : children)
    {
        if (context.discoveredFiles >= maximumFiles)
            break;
        if (child.isSymbolicLink() || ! isContainedByRoot (child))
        {
            ++context.result.skippedErrors;
            continue;
        }

        if (child.isDirectory())
        {
            if (depth < maximumDepth)
                scanDirectory (child, sourceRoot, source, group, depth + 1, context);
            continue;
        }

        if (! hasPresetExtension (child))
            continue;

        ++context.discoveredFiles;
        juce::String error;
        UserPresetData data;
        if (! loadPresetFile (child, data, error))
        {
            ++context.result.skippedErrors;
            continue;
        }

        const auto relativeToRoot = child.getRelativePathFrom (rootDirectory)
                                         .replaceCharacter ('\\', '/');
        const auto relativeToSource = child.getRelativePathFrom (sourceRoot)
                                           .replaceCharacter ('\\', '/');
        const auto category = child.getParentDirectory().getRelativePathFrom (sourceRoot)
                                  .replaceCharacter ('\\', '/');
        context.result.entries.push_back ({
            sourcePrefix (source) + ":" + relativeToRoot.toLowerCase(),
            data.name,
            data.author,
            group,
            category == "." ? juce::String {} : category,
            relativeToRoot,
            child,
            source,
            source == PresetSource::user,
            data.values
        });
    }
}

bool DeepPluckPresetManager::loadPreset (const UserPresetEntry& entry,
                                         UserPresetData& data,
                                         juce::String& error) const
{
    if (! isContainedByRoot (entry.file))
    {
        error = "Preset path is outside the configured root.";
        return false;
    }
    return loadPresetFile (entry.file, data, error);
}

bool DeepPluckPresetManager::loadPresetFile (const juce::File& file,
                                             UserPresetData& data,
                                             juce::String& error) const
{
    recordFileOperation();
    if (! file.existsAsFile() || file.getSize() < 0 || file.getSize() > maximumPresetBytes)
    {
        error = "Preset file is missing or exceeds the 1 MiB limit.";
        return false;
    }

    return decodePreset (parameterState, defaultValues, file.loadFileAsString(), data, error);
}

PresetSaveResult DeepPluckPresetManager::savePreset (
    const juce::AudioProcessorValueTreeState& state,
    juce::String name,
    juce::String author,
    juce::String description,
    bool allowOverwrite,
    UserPresetEntry& savedEntry,
    juce::String& error,
    juce::File explicitUserTarget)
{
    recordFileOperation();
    name = name.trim();
    author = author.trim();
    if (name.isEmpty() || name.length() > 80 || author.length() > 80)
    {
        error = "Preset name or author is invalid.";
        return PresetSaveResult::invalidName;
    }
    if (author.isEmpty())
        author = "User";

    const auto safeName = sanitizePresetBaseName (name);
    if (safeName.isEmpty())
    {
        error = "Preset name does not produce a safe Windows filename.";
        return PresetSaveResult::invalidName;
    }

    if (! ensureRootDirectories (error))
        return PresetSaveResult::failed;

    const auto target = explicitUserTarget == juce::File {}
                            ? targetFileForName (safeName)
                            : std::move (explicitUserTarget);
    if (! isContainedByUserDirectory (target))
    {
        error = "Preset target escaped the User directory.";
        return PresetSaveResult::failed;
    }
    if (target.existsAsFile() && ! allowOverwrite)
        return PresetSaveResult::needsOverwriteConfirmation;

    const auto rawValues = captureParameterValues (state);
    auto values = rawValues;
    if (! canonicalizeParameterValues (state, values))
    {
        error = "Current parameter state is invalid.";
        return PresetSaveResult::failed;
    }

    juce::String json;
    // The codec canonicalizes once, as the former inline writer did. Keep the
    // same once-canonicalized values above for the saved identity fingerprint.
    const UserPresetData data { name, author, pluginVersion, description, rawValues };
    if (! encodePreset (state, data, json, error))
        return PresetSaveResult::failed;

    const auto temporary = target.getSiblingFile (
        target.getFileName() + ".tmp-" + juce::String::toHexString (
            juce::Random::getSystemRandom().nextInt64()));
    if (! temporary.replaceWithText (json, false, false, "\n"))
    {
        error = "Could not write temporary preset file.";
        return PresetSaveResult::failed;
    }

    const auto replaced = target.existsAsFile()
                              ? temporary.replaceFileIn (target)
                              : temporary.moveFileTo (target);
    if (! replaced)
    {
        temporary.deleteFile();
        error = "Could not atomically replace the preset file.";
        return PresetSaveResult::failed;
    }

    const auto relative = target.getRelativePathFrom (rootDirectory)
                              .replaceCharacter ('\\', '/');
    savedEntry = {
        "user:" + relative.toLowerCase(), name, author, "User", {}, relative,
        target, PresetSource::user, true, values
    };
    requestScan();
    return PresetSaveResult::saved;
}

bool DeepPluckPresetManager::deleteUserPreset (const juce::File& file,
                                               juce::String& error)
{
    recordFileOperation();
    if (! file.existsAsFile() || ! isContainedByUserDirectory (file))
    {
        error = "Only User presets can be deleted.";
        return false;
    }
    if (! file.moveToTrash())
    {
        error = "The preset could not be moved to the recycle bin.";
        return false;
    }
    requestScan();
    return true;
}

bool DeepPluckPresetManager::revealRootToUser (juce::String& error)
{
    if (! ensureRootDirectories (error))
        return false;
    rootDirectory.revealToUser();
    return true;
}

juce::String DeepPluckPresetManager::sanitizePresetBaseName (juce::String name)
{
    name = name.trim();
    juce::String result;
    for (const auto character : name)
    {
        const auto invalid = character < 32
                          || juce::String ("<>:\"/\\|?*").containsChar (character);
        result += invalid ? '_' : character;
    }
    while (result.endsWithChar ('.') || result.endsWithChar (' '))
        result = result.dropLastCharacters (1);
    result = result.substring (0, 80);

    const auto upper = result.toUpperCase();
    const auto reserved = upper == "CON" || upper == "PRN" || upper == "AUX" || upper == "NUL"
                       || (upper.startsWith ("COM") && upper.substring (3).getIntValue() >= 1
                           && upper.substring (3).getIntValue() <= 9 && upper.length() == 4)
                       || (upper.startsWith ("LPT") && upper.substring (3).getIntValue() >= 1
                           && upper.substring (3).getIntValue() <= 9 && upper.length() == 4);
    if (reserved)
        result = "_" + result;
    return result.substring (0, 80);
}

bool DeepPluckPresetManager::hasPresetExtension (const juce::File& file)
{
    return file.getFileExtension().equalsIgnoreCase (presetExtension);
}

juce::String DeepPluckPresetManager::readPackName (const juce::File& packDirectory) const
{
    recordFileOperation();
    const auto fallback = packDirectory.getFileName();
    const auto metadataFile = packDirectory.getChildFile ("pack.json");
    if (! metadataFile.existsAsFile() || metadataFile.getSize() > maximumPresetBytes)
        return fallback;

    juce::var parsed;
    if (juce::JSON::parse (metadataFile.loadFileAsString(), parsed).failed())
        return fallback;
    const auto* object = parsed.getDynamicObject();
    if (object == nullptr || object->getProperty ("format").toString() != packFormat
        || ! object->getProperty ("schemaVersion").isInt()
        || static_cast<int> (object->getProperty ("schemaVersion")) != schemaVersion
        || ! object->getProperty ("packName").isString())
        return fallback;
    const auto packName = object->getProperty ("packName").toString().trim();
    return packName.isNotEmpty() && packName.length() <= 80 ? packName : fallback;
}

juce::String DeepPluckPresetManager::normalizedPath (const juce::File& file)
{
    auto path = file.getFullPathName().replaceCharacter ('\\', '/');
#if JUCE_WINDOWS
    path = path.toLowerCase();
#endif
    while (path.endsWithChar ('/'))
        path = path.dropLastCharacters (1);
    return path;
}

bool DeepPluckPresetManager::isContainedByRoot (const juce::File& file) const
{
    const auto root = normalizedPath (rootDirectory);
    const auto candidate = normalizedPath (file);
    return candidate == root || candidate.startsWith (root + "/");
}

bool DeepPluckPresetManager::isContainedByUserDirectory (const juce::File& file) const
{
    const auto user = normalizedPath (getUserDirectory());
    const auto candidate = normalizedPath (file);
    return candidate == user || candidate.startsWith (user + "/");
}

juce::File DeepPluckPresetManager::targetFileForName (const juce::String& safeBaseName) const
{
    return getUserDirectory().getChildFile (safeBaseName + presetExtension);
}

bool DeepPluckPresetManager::ensureRootDirectories (juce::String& error) const
{
    recordFileOperation();
    if (rootDirectory.createDirectory().failed()
        || getUserDirectory().createDirectory().failed())
    {
        error = "Could not create the preset root or User directory.";
        return false;
    }
    return true;
}

void DeepPluckPresetManager::setFileOperationObserverForTests (
    std::function<void()> observer)
{
    jassert (! isThreadRunning());
    fileOperationObserver = std::move (observer);
}

void DeepPluckPresetManager::recordFileOperation() const
{
    if (fileOperationObserver)
        fileOperationObserver();
}
}
