/*
 * Copyright (C) 2025, Robert Patterson
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
#pragma once

#include <memory>
#include <unordered_map>
#include <cassert>
#include <iostream>
#include <optional>
#include <string>
#include <string_view>
#include <type_traits>
#include <utility>

// mnxdom provides enum (de)serialization; disable nlohmann's generic enum support.
#if defined(NLOHMANN_JSON_HPP) && !defined(JSON_DISABLE_ENUM_SERIALIZATION)
#error "nlohmann/json.hpp included before mnxdom without JSON_DISABLE_ENUM_SERIALIZATION=1"
#endif

#ifndef JSON_DISABLE_ENUM_SERIALIZATION
#define JSON_DISABLE_ENUM_SERIALIZATION 1
#endif

#ifdef NLOHMANN_JSON_SYSTEM
#include <nlohmann/json.hpp>
#else
#include "nlohmann/json.hpp"
#endif
#include "BoilerplateMacros.h"

/**
 * @namespace mnx
 * @brief object model for MNX format
 * 
 * See: https://w3c.github.io/mnx/docs/
 */
namespace mnx {

/// @brief Returns the MNX schema id from the embedded schema.
[[nodiscard]] const std::string& getMnxSchemaId();

/// @brief Returns the MNX version extracted from the trailing segment of schema `$id`.
[[nodiscard]] int getMnxSchemaVersion();

/// @brief The MNX version for files generated by the DOM.
inline const int MNX_VERSION = getMnxSchemaVersion();

using json = nlohmann::json;                ///< JSON class for MNX
using json_pointer = json::json_pointer;    ///< JSON pointer class for MNX

class Object;
class Document;
class Base;
template <typename T> class Array;
template <typename T> class Dictionary;

namespace sequence {
class Event;
class Space;
class MultiNoteTremolo;
class Tuplet;
} // namespace sequence

namespace validation {
class SemanticValidator;
}; // namespace validation

#ifndef DOXYGEN_SHOULD_IGNOR_THIS

namespace scope {
struct Default {};
struct SequenceContent {};
struct LayoutContent {};
} // namespace scope

#endif

namespace detail {

/// @brief Adds an `append(...)` overload that mirrors a type's static `make(...)` signature for arrays.
template <typename T, auto MakeFunc>
struct ArrayAppendFromMake;

/// @brief Implements Array append overloads from a type's `make(...)` signature.
template <typename T, typename R, typename... Args, R(*MakeFunc)(Args...)>
struct ArrayAppendFromMake<T, MakeFunc>
{
    /**
     * @brief Append a new element at the end of the array. (Available only for Base types)
     * @return The newly created element.
    */
    T append(Args... args)
    {
        return static_cast<Array<T>&>(*this).template appendImpl<T>(std::forward<Args>(args)...);
    }
};

/// @brief Base append implementation for arrays of Base-derived types.
template <typename Derived, typename T>
struct ArrayAppendBase
{
    /**
     * @brief Append a new element at the end of the array. (Available only for Base types)
     * @return The newly created element.
    */
    template <typename U = T, typename... Args,
              std::enable_if_t<std::is_base_of_v<Base, U>, int> = 0>
    U append(Args&&... args)
    {
        return static_cast<Derived&>(*this).template appendImpl<U>(std::forward<Args>(args)...);
    }
};

template <typename Derived, typename T, typename = void>
struct ArrayAppendOverloads : ArrayAppendBase<Derived, T> {};

template <typename Derived, typename T>
struct ArrayAppendOverloads<Derived, T, std::void_t<decltype(&T::make)>>
    : ArrayAppendBase<Derived, T>,
      ArrayAppendFromMake<T, &T::make>
{
    using ArrayAppendBase<Derived, T>::append;
    using ArrayAppendFromMake<T, &T::make>::append;
};

/// @brief Adds an `append(key, ...)` overload that mirrors a type's static `make(...)` signature for dictionaries.
template <typename T, auto MakeFunc>
struct DictionaryAppendFromMake;

/// @brief Implements Dictionary append overloads from a type's `make(...)` signature.
template <typename T, typename R, typename... Args, R(*MakeFunc)(Args...)>
struct DictionaryAppendFromMake<T, MakeFunc>
{
    /**
     * @brief Create a new element using the input key. (Available only for Base types)
     * @return The newly created element.
    */
    T append(std::string_view key, Args... args)
    {
        return static_cast<Dictionary<T>&>(*this).template appendImpl<T>(key, std::forward<Args>(args)...);
    }
};

/// @brief Base append implementation for dictionaries of Base-derived types.
template <typename Derived, typename T>
struct DictionaryAppendBase
{
    /**
     * @brief Append a new element at the end of the array. (Available only for Base types)
     * @return The newly created element.
    */
    template <typename U = T, typename... Args,
              std::enable_if_t<std::is_base_of_v<Base, U>, int> = 0>
    U append(std::string_view key, Args&&... args)
    {
        return static_cast<Derived&>(*this).template appendImpl<U>(key, std::forward<Args>(args)...);
    }
};

template <typename Derived, typename T, typename = void>
struct DictionaryAppendOverloads : DictionaryAppendBase<Derived, T> {};

template <typename Derived, typename T>
struct DictionaryAppendOverloads<Derived, T, std::void_t<decltype(&T::make)>>
    : DictionaryAppendBase<Derived, T>,
      DictionaryAppendFromMake<T, &T::make>
{
    using DictionaryAppendBase<Derived, T>::append;
    using DictionaryAppendFromMake<T, &T::make>::append;
};

} // namespace detail


/**
 * @brief Base class wrapper for all MNX JSON nodes.
 */
class Base
{
public:
    virtual ~Base() = default;

    /// @brief Copy constructor
    Base(const Base& src) : m_root(src.m_root), m_pointer(src.m_pointer)
    {}

    /// @brief Move constructor
    Base(Base&& src) noexcept : m_root(src.m_root),    // m_root must be copied (not moved)
        m_pointer(std::move(src.m_pointer))
    {}

    /// @brief Copy assignment operator
    Base& operator=(const Base& src)
    {
        if (this != &src) {
            if (m_root != src.m_root) {
                throw std::logic_error("Assignment from a different JSON document is not allowed.");
            }
            m_pointer = src.m_pointer;
        }
        return *this;
    }

    /// @brief Move assignment operator
    Base& operator=(Base&& src)
    {
        if (this != &src) {
            if (m_root != src.m_root) {
                throw std::logic_error("Assignment from a different JSON document is not allowed.");
            }
            m_pointer = std::move(src.m_pointer);
        }
        return *this;
    }

    /// @brief Dumps the branch to a string. Useful in debugging.
    /// @param indents Number of indents or -1 for no indents 
    [[nodiscard]] std::string dump(int indents = -1) const
    {
        return ref().dump(indents);
    }

    /// @brief Returns the parent object for this node
    /// @tparam T The type to create. Must correctly match whether it is an array or object.
    /// @throws std::invalid_argument if the type of T does not match the type of the underlying pointer.
    template <typename T>
    [[nodiscard]] T parent() const
    {
        static_assert(std::is_base_of_v<Base, T>, "Template type mush be derived from Base.");
        return T(m_root, m_pointer.parent_pointer());
    }

    /// @brief Returns the enclosing array element for this instance. If T is a type that can be nested (e.g. ContentObject), the highest
    /// level instance is returned. (To get the lowest level immediate container, use #ContentObject::container.)
    /// @tparam T The type to find. A limited list of types are supported, including @ref Part and @ref Sequence. Others may be added as needed.
    /// @return the enclosing element, or std::nullopt if not found.
    template <typename T, typename Scope = scope::Default>
    [[nodiscard]] std::optional<T> getEnclosingElement() const;

    /// @brief Returns the json_pointer for this node.
    [[nodiscard]] json_pointer pointer() const { return m_pointer; }

    /// @brief Returns the document root
    [[nodiscard]] Document document() const;

protected:
    /**
     * @brief Convert this node for retrieval.
     *
     * @return A reference to the JSON node.
     */
    [[nodiscard]] json& ref() const { return resolve_pointer(); }

    /**
     * @brief Access the JSON node for modification.
     * @return A reference to the JSON node.
     */
    [[nodiscard]] json& ref() { return resolve_pointer(); }

    /// @brief Returns the root.
    [[nodiscard]] const std::shared_ptr<json>& root() const { return m_root; }

    /**
     * @brief Wrap a Base instance around a specific JSON reference using a json_pointer.
     * @param root Reference to the root JSON object.
     * @param pointer JSON pointer to the specific node.
     */
    Base(const std::shared_ptr<json>& root, json_pointer pointer)
        : m_root(root), m_pointer(std::move(pointer)) {}

    /**
     * @brief Construct a Base reference as a child inside a parent node.
     * @param jsonRef Rvalue reference to a new JSON object or array.
     * @param parent Reference to the parent instance.
     * @param key The key under which the new node is stored.
     */
    Base(json&& jsonRef, Base& parent, std::string_view key)
        : m_root(parent.m_root), m_pointer(parent.m_pointer / std::string(key))
    {
        (*m_root)[m_pointer] = std::move(jsonRef);
    }

    /**
     * @brief Retrieves and validates a required child node.
     * @tparam T The expected MNX type (`Object` or `Array<T>`).
     * @param key The key of the child node.
     * @return An instance of the requested type.
     * @throws std::runtime_error if the key is missing or the type is incorrect.
     */
    template <typename T>
    [[nodiscard]] T getChild(std::string_view key) const
    {
        static_assert(std::is_base_of_v<Base, T>, "template type must be derived from Base");

        json_pointer childPointer = m_pointer / std::string(key);
        if (!checkKeyIsValid<T>(childPointer)) {
            throw std::out_of_range("Child node not found: " + std::string(key));
        }

        return T(m_root, childPointer);
    }

    /**
     * @brief Sets a child node.
     * @tparam T The expected MNX type (`Object` or `Array<T>`).
     * @param key The key of the child node.
     * @param value The value to set.
     * @return The newly created child.
     */
    template <typename T>
    T setChild(std::string_view key, const T& value)
    {
        static_assert(std::is_base_of_v<Base, T>, "template type must be derived from Base");

        json_pointer childPointer = m_pointer / std::string(key);
        (*m_root)[childPointer] = value.ref();
        return T(m_root, childPointer);
    }

    /**
     * @brief Retrieves an optional child node.
     * @tparam T The expected MNX type (`Object` or `Array<T>`).
     * @param key The key of the child node.
     * @return An `std::optional<T>`, or `std::nullopt` if the node does not exist or is invalid.
     * @throws std::runtime_error if the type is incorrect.
     */
    template <typename T>
    [[nodiscard]] std::optional<T> getOptionalChild(std::string_view key) const
    {
        static_assert(std::is_base_of_v<Base, T>, "template type must be derived from Base");

        json_pointer childPointer = m_pointer / std::string(key);
        if (!checkKeyIsValid<T>(childPointer)) {
            return std::nullopt;
        }

        return T(m_root, childPointer);
    }

private:
    /**
     * @brief Checks whether a key is valid for the given type.
     * @tparam T The expected MNX type.
     * @param pointer JSON pointer to the key.
     * @return True if the key is valid, false otherwise.
     * @throws std::runtime_error if the type is incorrect.
     */
    template <typename T>
    [[nodiscard]] bool checkKeyIsValid(const json_pointer& pointer) const
    {
        if (!(*m_root).contains(pointer)) {
            return false;
        }

        const json& node = (*m_root).at(pointer);

        if constexpr (std::is_base_of_v<Object, T>) {
            if (!node.is_object()) {
                throw std::runtime_error("Expected an object for: " + pointer.to_string());
            }
        } else if constexpr (std::is_base_of_v<Array<typename T::value_type>, T>) {
            if (!node.is_array()) {
                throw std::runtime_error("Expected an array for: " + pointer.to_string());
            }
        }

        return true;
    }

    /**
     * @brief Resolves the JSON node using the stored pointer.
     * @return Reference to the JSON node.
     * @throws json::out_of_range if the node does not exist.
     */
    [[nodiscard]] json& resolve_pointer() const
    {
        return (*m_root).at(m_pointer);  // Throws if invalid
    }

    const std::shared_ptr<json> m_root;  ///< Shared pointer to the root JSON object.
    json_pointer m_pointer;          ///< JSON pointer to the specific node.

    friend class validation::SemanticValidator;
};

/// @brief Error handler type for reporting errors
using ErrorHandler = std::function<void(const std::string& message, const Base& location)>;

/**
 * @brief Represents an MNX object, encapsulating property access.
 */
class Object : public Base
{
public:
    /// @brief Wraps an Object class around an existing JSON object node
    /// @param root Reference to the document root
    /// @param pointer The json_pointer value for the node
    Object(const std::shared_ptr<json>& root, json_pointer pointer) : Base(root, pointer)
    {
        if (!ref().is_object()) {
            throw std::invalid_argument("mnx::Object must wrap a JSON object.");
        }
    }

    /// @brief Creates a new Object class as a child of a JSON node
    /// @param parent The parent class instance
    /// @param key The JSON key to use for embedding in parent.
    Object(Base& parent, std::string_view key)
        : Base(json::object(), parent, key) {}

    MNX_OPTIONAL_PROPERTY(std::string, _c);     ///< An optional comment. This serves a similar function as XML or HTML comments.
    MNX_OPTIONAL_CHILD(Object, _x);             ///< Vendor-defined dictionary.
    MNX_OPTIONAL_PROPERTY(std::string, id);     ///< Uniquely identifies the object

    /// @brief Sets a vendor extension value in `_x`, creating `_x` when needed.
    void setExtension(const std::string& key, const json& value)
    {
        auto extensions = ensure__x();
        extensions.ref()[key] = value;
    }

    /// @brief Gets a vendor extension value from `_x`.
    [[nodiscard]] std::optional<json> getExtension(const std::string& key) const
    {
        if (auto extensions = _x()) {
            const auto it = extensions->ref().find(key);
            if (it != extensions->ref().end()) {
                return *it;
            }
        }
        return std::nullopt;
    }
};

/// @brief Allows access to a fundamental type (number, boolean, string) in a JSON node
/// @tparam T The fundamental type to wrap. 
template <typename T, std::enable_if_t<!std::is_base_of_v<Base, T>, int> = 0>
class SimpleType : public Base
{
    static_assert(std::is_arithmetic_v<T> || std::is_same_v<T, std::string>, "This template is for simple JSON classes");

public:
    using value_type = T; ///< value type of this SimpleType

    /// @brief Wraps a SimpleType class around an existing JSON object node
    /// @param root Reference to the document root
    /// @param pointer The json_pointer value for the node
    SimpleType(const std::shared_ptr<json>& root, json_pointer pointer) : Base(root, pointer)
    {
    }

    /// @brief Implicit conversion to simple type
    operator T() const
    {
        return ref().template get<T>();
    }

    /// @brief Allow assignment to underlying json reference
    /// @param src The simple type to assign.
    SimpleType& operator=(const T& src)
    {
        ref() = src;
        return *this;
    }

    /// @brief Equality comparison with value type
    bool operator==(const T& src) const
    {
        return src == ref().template get<T>();
    }
};

class ArrayElementObject;
/**
 * @brief Represents an MNX array, encapsulating property access.
 */
template <typename T>
class Array : public Base,
              public detail::ArrayAppendOverloads<Array<T>, T>
{
    static_assert(std::is_arithmetic_v<T> || std::is_same_v<T, std::string> ||
                  std::is_base_of_v<ArrayElementObject, T>, "Invalid MNX array element type.");

private:    
    template<typename ArrayType>
    struct iter
    {
    private:
        ArrayType* m_ptr;
        mutable size_t m_idx;

    public:
        using iterator_category = std::input_iterator_tag;
        using value_type        = T;
        using difference_type   = std::ptrdiff_t;
        using pointer           = void; // not meaningful for proxy/value-returning iterators
        using reference         = T;    // since operator* returns by value

        iter(ArrayType* ptr, size_t idx) : m_ptr(ptr), m_idx(idx) {}
        T operator*() const { return (*m_ptr)[m_idx]; }
        iter& operator++() { ++m_idx; return *this; }
        bool operator!=(const iter& o) const { return m_idx != o.m_idx; }
    };

public:
    /// @brief The type for elements in this Array.
    using value_type = T;

    using iterator = iter<Array>;               ///< non-const iterator type
    using const_iterator = iter<const Array>;   ///< const iterator type

    /// @brief Wraps an Array class around an existing JSON array node
    /// @param root Reference to the document root
    /// @param pointer The json_pointer value for the node
    Array(const std::shared_ptr<json>& root, json_pointer pointer) : Base(root, pointer)
    {
        if (!ref().is_array()) {
            throw std::invalid_argument("mnx::Array must wrap a JSON array.");
        }
    }

    /// @brief Creates a new Array class as a child of a JSON node
    /// @param parent The parent class instance
    /// @param key The JSON key to use for embedding in parent.
    Array(Base& parent, std::string_view key)
        : Base(json::array(), parent, key) {}

    /** @brief Get the size of the array. */
    [[nodiscard]] size_t size() const { return ref().size(); }

    /** @brief Check if the array is empty. */
    [[nodiscard]] bool empty() const { return ref().empty(); }

    /** @brief Clear all elements. */
    void clear() { ref().clear(); }

    /** @brief Direct getter for a particular element. */
    [[nodiscard]] T at(size_t index) const
    {
        return operator[](index);
    }

    /** @brief Access the first element.
     *  @throws std::out_of_range if the array is empty.
     */
    [[nodiscard]] T front() const { return (*this)[0]; }

    /** @brief Access the last element.
     *  @throws std::out_of_range if the array is empty.
     */
    [[nodiscard]] T back() const { return (*this)[size() - 1]; }
    
    /// @brief const operator[]
    [[nodiscard]] auto operator[](size_t index) const
    {
        checkIndex(index);
        if constexpr (std::is_base_of_v<Base, T>) {
            return getChild<T>(std::to_string(index));
        } else {
            return getChild<SimpleType<T>>(std::to_string(index));
        }
    }

    /// @brief non-const operator[]
    [[nodiscard]] auto operator[](size_t index)
    {
        checkIndex(index);
        if constexpr (std::is_base_of_v<Base, T>) {
            return getChild<T>(std::to_string(index));
        } else {
            return getChild<SimpleType<T>>(std::to_string(index));
        }
    }

    /** @brief Append a new value to the array. (Available only for primitive types) */
    template <typename U = T>
    std::enable_if_t<!std::is_base_of_v<Base, U>, void>
    push_back(const U& value)
    {
        ref().push_back(value);
    }

    /** @brief Remove an element at a given index. */
    void erase(size_t index)
    {
        checkIndex(index);
        ref().erase(ref().begin() + index);
    }

    /// @brief Converts the Array to an owning std::vector.
    /// @details Intended only for value-like element types. The returned vector
    ///          is detached from the document and may be used as a grouped
    ///          snapshot of values (e.g., for passing to external APIs).
    template <typename U = T>
    std::enable_if_t<!std::is_base_of_v<Base, U>, std::vector<U>>
    toStdVector() const
    { return std::vector<U>(begin(), end()); }

    /// @brief Returns an iterator to the beginning of the array.
    [[nodiscard]] auto begin() { return iterator(this, 0); }

    /// @brief Returns an iterator to the end of the array.
    [[nodiscard]] auto end() { return iterator(this, size()); }

    /// @brief Returns a const iterator to the beginning of the array.
    [[nodiscard]] auto begin() const { return const_iterator(this, 0); }

    /// @brief Returns a const iterator to the end of the array.
    [[nodiscard]] auto end() const { return const_iterator(this, size()); }

protected:
    /// @brief validates that an index is not out of range
    /// @throws std::out_of_range if the index is out of range
    void checkIndex(size_t index) const
    {
        assert(index < ref().size());
        if (index >= ref().size()) {
            throw std::out_of_range("Index out of range");
        }
    }

private:
    template <typename U, typename... Args>
    U appendImpl(Args&&... args)
    {
        static_assert(std::is_base_of_v<Base, U>, "Array::appendImpl requires a Base-derived element type.");
        if constexpr (std::is_base_of_v<Object, U>) {
            ref().push_back(json::object());
        } else {
            ref().push_back(json::array());
        }
        return U(*this, std::to_string(ref().size() - 1), std::forward<Args>(args)...);
    }

    template <typename, auto>
    friend struct detail::ArrayAppendFromMake;
    template <typename, typename>
    friend struct detail::ArrayAppendBase;
};

/**
 * @brief Represents an MNX object that is included as an array element.
 */
class ArrayElementObject : public Object
{
public:
    using Object::Object;

    /// @brief Calculates the array index of the current instance within the array.
    size_t calcArrayIndex() const
    {
        return std::stoul(pointer().back());
    }

    /// @brief Returns the object that owns the content array this element belongs to wrapped as the specified template type.
    /// @tparam ContainerType The type to wrap around the container.
    /// @throws std::invalid_argument If @p ContainerType is derived from @ref ContentObject and its
    ///         @c ContentTypeValue does not match the retrieved object's `type` field.
    template <typename ContainerType>
    ContainerType container() const;
};

class ContentArray;
/// @brief Base class for objects that are elements of content arrays
class ContentObject : public ArrayElementObject
{
protected:
    static constexpr std::string_view ContentTypeValueDefault = "event"; ///< default type value that identifies the type within the content array

public:
    using ArrayElementObject::ArrayElementObject;

    MNX_OPTIONAL_PROPERTY_WITH_DEFAULT(std::string, type, std::string(ContentTypeValueDefault));   ///< determines our type in the JSON

    /// @brief Retrieve an element as a specific type
    template <typename T, std::enable_if_t<std::is_base_of_v<ContentObject, T>, int> = 0>
    [[nodiscard]] T get() const
    {
        return getTypedObject<T>();
    }

    /// @brief Constructs an object of type `T` if its type matches the JSON type
    /// @throws std::invalid_argument if there is a type mismatch
    template <typename T, std::enable_if_t<std::is_base_of_v<ContentObject, T>, int> = 0>
    [[nodiscard]] T getTypedObject() const
    {
        if (type() != T::ContentTypeValue) {
            throw std::invalid_argument("Type mismatch: expected " + std::string(T::ContentTypeValue) +
                                        ", got " + type());
        }
        return T(root(), pointer());
    }

    friend class ContentArray;
};

template <typename ContainerType>
inline ContainerType ArrayElementObject::container() const
{
    if constexpr (std::is_base_of_v<ContainerType, ContentObject>) {
        const auto obj = parent<Array<ArrayElementObject>>().template parent<ContentObject>();
        if constexpr (std::is_same_v<ContainerType, ContentObject>) {
            return obj;
        } else {
            MNX_ASSERT_IF(obj.type() != ContainerType::ContentTypeValue) {
                throw std::invalid_argument(
                    "container(): requested type does not match underlying content object type");
            }
            return obj.template get<ContainerType>();
        }
    } else {
        return parent<Array<ArrayElementObject>>().template parent<ContainerType>();
    }
}

/**
 * @class ContentArray
 * @brief Class for content arrays.
 *
 * Allows arrays of any type that derives from @ref ContentObject. An exampled of how
 * to get type instances is:
 * 
 * @code{.cpp}
 * auto next = content[index]; // gets the base ContentObject instance.
 * if (next.type() == layout::Group::ContentTypeValue) {
 *     auto group = next.get<layout::Group>(); // gets the instance typed as a layout::Group.
 *     // process group
 * } else if (next.type() == layout::Staff::ContentTypeValue) {
 *     auto staff = next.get<layout::Staff>(); // gets the instance typed as a layout::Staff.
 *     // process staff
 * }
 * @endcode
 *
 * To add instances to the array, use the template paramter to specify the type to add.
 *
 * @code{.cpp}
 * auto newElement = content.append<layout::Staff>();
 * @endcode
 *
 * The `append` method automatically gives the instance the correct `type` value.
 *
*/
class ContentArray : public Array<ContentObject>
{
public:
    using BaseArray = Array<ContentObject>;     ///< The base array type
    using BaseArray::BaseArray;  // Inherit constructors

    /// @brief Retrieve an element from the array as a specific type
    template <typename T, std::enable_if_t<std::is_base_of_v<ContentObject, T>, int> = 0>
    [[nodiscard]] T get(size_t index) const
    {
        this->checkIndex(index);
        return operator[](index).get<T>();
    }

    /// @brief Append an element of the specified type (default overload for no-arg content types).
    template <typename T,
              std::enable_if_t<std::is_base_of_v<ContentObject, T> &&
                               !std::is_same_v<T, sequence::Event> &&
                               !std::is_same_v<T, sequence::Space> &&
                               !std::is_same_v<T, sequence::MultiNoteTremolo> &&
                               !std::is_same_v<T, sequence::Tuplet>, int> = 0>
    T append()
    {
        return appendWithType<T>();
    }

    /// @brief Append overload entry point for explicitly specialized argful content types.
    template <typename T, typename... Args,
              std::enable_if_t<std::is_base_of_v<ContentObject, T> && (sizeof...(Args) > 0), int> = 0>
    T append(const Args&... args)
    {
        static_assert(!std::is_same_v<T, T>,
                      "ContentArray::append requires explicit specialization for each content type.");
        return appendWithType<T>(args...);
    }

    // Prevent untemplated append() calls; callers must use append<T>(...).
    ContentObject append(...) = delete;

private:
    template <typename T, typename... Args>
    T appendWithType(Args&&... args)
    {
        auto result = BaseArray::append<T>(std::forward<Args>(args)...);
        if constexpr (T::ContentTypeValue != ContentObject::ContentTypeValueDefault) {
            result.set_type(std::string(T::ContentTypeValue));
        }
        return result;
    }

    /// @brief Constructs an object of type `T` if its type matches the JSON type
    /// @throws std::invalid_argument if there is a type mismatch
    template <typename T, std::enable_if_t<std::is_base_of_v<ContentObject, T>, int> = 0>
    [[nodiscard]] T getTypedObject(size_t index) const
    {
        this->checkIndex(index);
        auto element = (*this)[index];
        if (element.type() != T::ContentTypeValue) {
            throw std::invalid_argument("Type mismatch: expected " + std::string(T::ContentTypeValue) +
                                        ", got " + element.type());
        }
        return T(root(), pointer() / std::to_string(index));
    }
};

/**
 * @class EnumStringMapping
 * @brief Supplies enum string mappings to nlohmann json's serializer.
 */
template <typename E, typename = std::enable_if_t<std::is_enum_v<E>>>
struct EnumStringMapping
{
    static const std::unordered_map<std::string, E> stringToEnum();     ///< @brief maps strings to enum values

    /// @brief maps enum values to strings
    static const std::unordered_map<E, std::string> enumToString()
    {
        static const std::unordered_map<E, std::string> reverseMap = []() {
            std::unordered_map<E, std::string> result;
            for (const auto& element : EnumStringMapping<E>::stringToEnum()) {
                result.emplace(element.second, element.first);
            }
            return result;
        }();
        return reverseMap;
    }
};

/**
 * @brief Represents an MNX dictionary, where each key is a user-defined string.
 */
template <typename T>
class Dictionary : public Object,
                   public detail::DictionaryAppendOverloads<Dictionary<T>, T>
{
    static_assert(std::is_arithmetic_v<T> || std::is_same_v<T, std::string> ||
                  std::is_base_of_v<ArrayElementObject, T>, "Invalid MNX dictionary element type.");

private:    
    template <typename DictionaryType, typename IteratorType>
    struct iter
    {
        using value_type = std::pair<const std::string, T>;
        using difference_type = std::ptrdiff_t;
        using iterator_category = std::forward_iterator_tag;
        using pointer = value_type*;
        using reference = value_type&;

    private:
        DictionaryType* m_ptr {};
        IteratorType m_it {};
        mutable std::optional<value_type> m_pair; // cached key/value

        void update_pair() const
        {
            m_pair.reset();
            if (m_it != m_ptr->ref().end()) {
                m_pair.emplace(m_it.key(), m_ptr->valueForKey(m_it.key()));
            }
        }

    public:
        iter(DictionaryType* ptr, IteratorType it)
            : m_ptr(ptr), m_it(it)
        {
            update_pair();
        }

        [[nodiscard]] reference operator*() const { return *m_pair; }
        [[nodiscard]] pointer operator->() const { return &*m_pair; }

        iter& operator++() { ++m_it; update_pair(); return *this; }
        iter operator++(int) { iter tmp(*this); ++(*this); return tmp; }

        [[nodiscard]] bool operator!=(const iter& o) const { return m_it != o.m_it; }
        [[nodiscard]] bool operator==(const iter& o) const { return m_it == o.m_it; }
    };

public:
    /// @brief The type for elements in this Array.
    using value_type = T;

    using iterator = iter<Dictionary, json::iterator>;  ///< non-const iterator type
    using const_iterator = iter<const Dictionary, json::const_iterator>; ///< const iterator type

    /// @brief Wraps an Dictionary class around an existing JSON node
    /// @param root Reference to the document root
    /// @param pointer The json_pointer value for the node
    Dictionary(const std::shared_ptr<json>& root, json_pointer pointer)
        : Object(root, pointer)
    {
    }

    /// @brief Creates a new Dictionary class as a child of a JSON node
    /// @param parent The parent class instance
    /// @param key The JSON key to use for embedding in parent.
    Dictionary(Base& parent, std::string_view key)
        : Object(parent, key) {}

    /** @brief Get the size of the array. */
    [[nodiscard]] size_t size() const { return ref().size(); }

    /** @brief Check if the array is empty. */
    [[nodiscard]] bool empty() const { return ref().empty(); }

    /** @brief Clear all elements. */
    void clear() { ref().clear(); }

    /** @brief Direct getter for a particular element.
     *  @throws std::out_of_range when the key does not exist.
     */
    [[nodiscard]] T at(std::string_view key) const
    { return valueForKey(key); }

    /** @brief Add a new value to the dictonary. (Available only for primitive types) */
    template <typename U = T>
    std::enable_if_t<!std::is_base_of_v<Base, U>, void>
    emplace(std::string_view key, const U& value)
    {
        ref()[key] = value;
    }

    /** @brief Remove an element at a given key. */
    void erase(std::string_view key)
    {
        ref().erase(key);
    }

    /// @brief Finds an element by key and returns an iterator.
    /// @param key The key to search for.
    /// @return Iterator to the found element or end() if not found.
    [[nodiscard]] auto find(std::string_view key)
    {
        auto it = ref().find(key);
        return (it != ref().end()) ? iterator(this, it) : end();
    }

    /// @brief Finds an element by key and returns a const iterator.
    /// @param key The key to search for.
    /// @return Const iterator to the found element or end() if not found.
    [[nodiscard]] auto find(std::string_view key) const
    {
        auto it = ref().find(key);
        return (it != ref().end()) ? const_iterator(this, it) : end();
    }

    /// @brief Returns true if the key exists in in the dictionary.
    /// @param key  The key to search for.
    [[nodiscard]] bool contains(std::string_view key) const
    { return find(key) != end(); }

    /// @brief Returns an iterator to the beginning of the dictionary.
    [[nodiscard]] auto begin() { return iterator(this, ref().begin()); }

    /// @brief Returns an iterator to the end of the dictionary.
    [[nodiscard]] auto end() { return iterator(this, ref().end()); }

    /// @brief Returns a const iterator to the beginning of the dictionary.
    [[nodiscard]] auto begin() const { return const_iterator(this, ref().begin()); }

    /// @brief Returns a const iterator to the end of the dictionary.
    [[nodiscard]] auto end() const { return const_iterator(this, ref().end()); }

private:
    /// @brief Returns the dictionary element for the given key.
    [[nodiscard]] T valueForKey(std::string_view key) const
    {
        if constexpr (std::is_base_of_v<Base, T>) {
            return getChild<T>(key);
        } else {
            return getChild<SimpleType<T>>(key);
        }
    }

    template <typename U, typename... Args>
    U appendImpl(std::string_view key, Args&&... args)
    {
        static_assert(std::is_base_of_v<Base, U>, "Dictionary::appendImpl requires a Base-derived element type.");
        if constexpr (std::is_base_of_v<Object, U>) {
            ref()[key] = json::object();
        } else {
            ref()[key] = json::array();
        }
        return U(*this, key, std::forward<Args>(args)...);
    }

    template <typename, auto>
    friend struct detail::DictionaryAppendFromMake;
    template <typename, typename>
    friend struct detail::DictionaryAppendBase;
};

} // namespace mnx

#ifndef DOXYGEN_SHOULD_IGNORE_THIS

namespace nlohmann {

#if defined(_WIN32)
// This general adl_serializer is enabled only for enum types.
// For some reason MSC does not like the direct function definitions below.
template<typename EnumType>
struct adl_serializer<EnumType, std::enable_if_t<std::is_enum_v<EnumType>>>
{
    template<typename BasicJsonType>
    static EnumType from_json(const BasicJsonType& j)
    {
        // Lookup the string in the specialized map.
        const auto& map = ::mnx::EnumStringMapping<EnumType>::stringToEnum();
        auto it = map.find(j.get<std::string>());
        if (it != map.end()) {
            return it->second;
        }
        /// @todo throw or log unmapped string
        return EnumType{};
    }

    template<typename BasicJsonType>
    static void to_json(BasicJsonType& j, const EnumType& value)
    {
        const auto& map = ::mnx::EnumStringMapping<EnumType>::enumToString();
        auto it = map.find(value);
        if (it == map.end()) {
            /// @todo log or throw unmapped enum.
            j = BasicJsonType();
            return;
        }
        j = it->second;
    }
};
#else
// Clang works with the adl_specialization above, but GCC does not.
namespace detail {

template<typename BasicJsonType, typename EnumType,
         std::enable_if_t<std::is_enum<EnumType>::value, int> = 0>
inline void from_json(const BasicJsonType& j, EnumType& value)
{
    // Lookup the string in the specialized map.
    const auto& map = ::mnx::EnumStringMapping<EnumType>::stringToEnum();
    auto it = map.find(j.template get<std::string>());
    if (it != map.end()) {
        value = it->second;
    } else {
        /// @todo throw or log unmapped string
        value = EnumType{};
    }
}

template<typename BasicJsonType, typename EnumType,
         std::enable_if_t<std::is_enum<EnumType>::value, int> = 0>
inline void to_json(BasicJsonType& j, EnumType value) noexcept
{
    const auto& map = ::mnx::EnumStringMapping<EnumType>::enumToString();
    auto it = map.find(value);
    if (it != map.end()) {
        j = it->second;
    } else {
        /// @todo log or throw unmapped enum.
        j = BasicJsonType();
    }
}

} // namespace detail
#endif // defined(_WIN32)

} // namespace nlohmann

#endif // DOXYGEN_SHOULD_IGNORE_THIS
