#include "DeepPluckPresetCodec.h"

#include <cmath>

namespace deeppluck
{
namespace
{
constexpr auto presetFormat = "JustALilPluckPreset";
constexpr int schemaVersion = 1;
bool isJsonNumber (const juce::var& value) noexcept
{
    return value.isInt() || value.isInt64() || value.isDouble() || value.isBool();
}

bool readRequiredString (const juce::DynamicObject& object,
                         const juce::Identifier& property,
                         juce::String& destination,
                         bool allowEmpty,
                         juce::String& error)
{
    if (! object.hasProperty (property))
    {
        error = "Missing required field: " + property.toString();
        return false;
    }

    const auto value = object.getProperty (property);
    if (! value.isString())
    {
        error = "Invalid string field: " + property.toString();
        return false;
    }

    destination = value.toString().trim();
    if ((! allowEmpty && destination.isEmpty()) || destination.length() > 80)
    {
        error = "Invalid length for field: " + property.toString();
        return false;
    }
    return true;
}

}

bool decodePreset (const juce::AudioProcessorValueTreeState& state,
                    const DeepPluckParameterValues& defaults,
                    const juce::String& json,
                    UserPresetData& data, juce::String& error)
{
    juce::var parsed;
    const auto parseResult = juce::JSON::parse (json, parsed);
    if (parseResult.failed() || ! parsed.isObject())
    {
        error = "Preset JSON is invalid.";
        return false;
    }

    const auto* object = parsed.getDynamicObject();
    if (object == nullptr
        || object->getProperty ("format").toString() != presetFormat
        || ! object->getProperty ("schemaVersion").isInt()
        || static_cast<int> (object->getProperty ("schemaVersion")) != schemaVersion)
    {
        error = "Preset format or schema version is not supported.";
        return false;
    }

    if (! readRequiredString (*object, "name", data.name, false, error)
        || ! readRequiredString (*object, "author", data.author, true, error)
        || ! readRequiredString (*object, "pluginVersion", data.pluginVersion, false, error))
        return false;

    const auto parametersValue = object->getProperty ("parameters");
    const auto* parameters = parametersValue.getDynamicObject();
    if (parameters == nullptr)
    {
        error = "Preset parameters object is missing.";
        return false;
    }

    data.values = defaults;
    for (auto index = 0; index < static_cast<int> (allParameterIds.size()); ++index)
    {
        const juce::Identifier id { allParameterIds[static_cast<std::size_t> (index)] };
        if (! parameters->hasProperty (id))
            continue;
        const auto value = parameters->getProperty (id);
        if (! isJsonNumber (value))
        {
            error = "Known parameter is not numeric: " + id.toString();
            return false;
        }
        const auto numeric = static_cast<double> (value);
        if (! std::isfinite (numeric))
        {
            error = "Known parameter is not finite: " + id.toString();
            return false;
        }
        data.values[static_cast<std::size_t> (index)] = static_cast<float> (numeric);
    }

    if (! canonicalizeParameterValues (state, data.values))
    {
        error = "Preset contains invalid parameter values.";
        return false;
    }

    const auto metadataValue = object->getProperty ("metadata");
    if (const auto* metadata = metadataValue.getDynamicObject())
        if (metadata->getProperty ("description").isString())
            data.description = metadata->getProperty ("description").toString();
    return true;
}

bool encodePreset (const juce::AudioProcessorValueTreeState& state,
                    const UserPresetData& data,
                    juce::String& json, juce::String& error, bool pretty)
{
    if (data.name.trim().isEmpty() || data.name.length() > 80 || data.author.length() > 80
        || data.pluginVersion.trim().isEmpty() || data.pluginVersion.length() > 80)
    {
        error = "Invalid preset name, author or writer version.";
        return false;
    }
    auto values = data.values;
    if (! canonicalizeParameterValues (state, values))
    {
        error = "Preset contains invalid parameter values.";
        return false;
    }
    auto rootObject = std::make_unique<juce::DynamicObject>();
    rootObject->setProperty ("format", presetFormat);
    rootObject->setProperty ("schemaVersion", schemaVersion);
    rootObject->setProperty ("name", data.name);
    rootObject->setProperty ("author", data.author);
    rootObject->setProperty ("pluginVersion", data.pluginVersion);

    auto parameterObject = std::make_unique<juce::DynamicObject>();
    for (auto index = 0; index < static_cast<int> (allParameterIds.size()); ++index)
    {
        auto value = static_cast<double> (values[static_cast<std::size_t> (index)]);
        const auto* parameter = state.getParameter (
            allParameterIds[static_cast<std::size_t> (index)]);
        if (dynamic_cast<const juce::AudioParameterBool*> (parameter) != nullptr
            || dynamic_cast<const juce::AudioParameterChoice*> (parameter) != nullptr
            || dynamic_cast<const juce::AudioParameterInt*> (parameter) != nullptr)
            parameterObject->setProperty (allParameterIds[static_cast<std::size_t> (index)],
                                          static_cast<int> (std::lround (value)));
        else
            parameterObject->setProperty (allParameterIds[static_cast<std::size_t> (index)], value);
    }
    rootObject->setProperty ("parameters", juce::var (parameterObject.release()));

    if (data.description.isNotEmpty())
    {
        auto metadata = std::make_unique<juce::DynamicObject>();
        metadata->setProperty ("description", data.description);
        rootObject->setProperty ("metadata", juce::var (metadata.release()));
    }

    json = juce::JSON::toString (juce::var (rootObject.release()), ! pretty);
    // Pretty developer content is hash-addressed UTF-8/LF on every platform.
    // The existing compact production writer has no formatting newlines.
    if (pretty) json = json.replace ("\r\n", "\n");
    return true;
}
}
