#pragma once

#include <cstdio>
#include <set>

#include <valijson/subschema.hpp>
#include <valijson/exceptions.hpp>

namespace valijson {

/**
 * Represents the root of a JSON Schema
 *
 * The root is distinct from other sub-schemas because it is the canonical
 * starting point for validation of a document against a given a JSON Schema.
 */
class Schema: public Subschema
{
public:
    /**
     * @brief  Construct a new Schema instance with no constraints
     */
    Schema()
      : sharedEmptySubschema(newSubschema()) { }

    /**
     * @brief  Construct a new Schema using custom memory management
     *         functions
     *
     * @param  allocFn  malloc- or new-like function to allocate memory
     *                  within Schema, such as for Subschema instances
     * @param  freeFn   free-like function to free memory allocated with
     *                  the `customAlloc` function
     */
    Schema(CustomAlloc allocFn, CustomFree freeFn)
      : Subschema(allocFn, freeFn),
        sharedEmptySubschema(newSubschema()) { }

    // Disable copy construction
    Schema(const Schema &) = delete;

    // Disable copy assignment
    Schema & operator=(const Schema &) = delete;

    /**
     * @brief Move construct a new Schema
     *
     * @param other Schema that is moved into the new Schema
     */
    Schema(Schema &&other)
      : Subschema(std::move(other)),
        subschemaSet(std::move(other.subschemaSet)),
        sharedEmptySubschema(other.sharedEmptySubschema)
    {
        // Makes other invalid by setting sharedEmptySubschema to nullptr
        other.sharedEmptySubschema = nullptr;
    }

    /**
     * @brief Move assign a Schema
     *
     * @param other Schema that is move assigned to this Schema
     * @return Schema&
     */
    Schema & operator=(Schema &&other)
    {
        // Calls the base class move assignment operator
        Subschema::operator=(std::move(other));

        // Swaps all Schema members
        std::swap(subschemaSet, other.subschemaSet);
        std::swap(sharedEmptySubschema, other.sharedEmptySubschema);

        return *this;
    }

    /**
     * @brief  Clean up and free all memory managed by the Schema
     *
     * Note that any Subschema pointers created and returned by this Schema
     * should be considered invalid.
     */
    ~Schema() override
    {
        if(sharedEmptySubschema != nullptr)
        {
            sharedEmptySubschema->~Subschema();
            m_freeFn(const_cast<Subschema *>(sharedEmptySubschema));
            sharedEmptySubschema = nullptr;
        }

#if VALIJSON_USE_EXCEPTIONS
        try {
#endif
            for (auto subschema : subschemaSet) {
                subschema->~Subschema();
                m_freeFn(subschema);
            }
#if VALIJSON_USE_EXCEPTIONS
        } catch (const std::exception &e) {
            fprintf(stderr, "Caught an exception while destroying Schema: %s",
                    e.what());
        }
#endif
    }

    /**
     * @brief  Copy a constraint to a specific sub-schema
     *
     * @param  constraint  reference to a constraint that will be copied into
     *                     the sub-schema
     * @param  subschema   pointer to the sub-schema that will own the copied
     *                     constraint
     *
     * @throws std::runtime_error if the sub-schema is not owned by this Schema
     *         instance
     */
    void addConstraintToSubschema(const Constraint &constraint,
            const Subschema *subschema)
    {
        // TODO: Check hierarchy for subschemas that do not belong...

        mutableSubschema(subschema)->addConstraint(constraint);
    }

    /**
     * @brief  Create a new Subschema instance that is owned by this Schema
     *
     * @returns  const pointer to the new Subschema instance
     */
    const Subschema * createSubschema()
    {
        Subschema *subschema = newSubschema();

#if VALIJSON_USE_EXCEPTIONS
        try {
#endif
            if (!subschemaSet.insert(subschema).second) {
                throwRuntimeError(
                        "Failed to store pointer for new sub-schema");
            }
#if VALIJSON_USE_EXCEPTIONS
        } catch (...) {
            subschema->~Subschema();
            m_freeFn(subschema);
            throw;
        }
#endif
        return subschema;
    }

    /**
     * @brief  Return a pointer to the shared empty schema
     */
    const Subschema * emptySubschema() const
    {
        return sharedEmptySubschema;
    }

    /**
     * @brief  Get a pointer to the root sub-schema of this Schema instance
     */
    const Subschema * root() const
    {
        return this;
    }

    void setAlwaysInvalid(const Subschema *subschema, bool value)
    {
        mutableSubschema(subschema)->setAlwaysInvalid(value);
    }

    /**
     * @brief  Update the description for one of the sub-schemas owned by this
     *         Schema instance
     *
     * @param  subschema    sub-schema to update
     * @param  description  new description
     */
    void setSubschemaDescription(const Subschema *subschema,
            const std::string &description)
    {
        mutableSubschema(subschema)->setDescription(description);
    }

    /**
     * @brief  Update the ID for one of the sub-schemas owned by this Schema
     *         instance
     *
     * @param  subschema  sub-schema to update
     * @param  id         new ID
     */
    void setSubschemaId(const Subschema *subschema, const std::string &id)
    {
        mutableSubschema(subschema)->setId(id);
    }

    /**
     * @brief  Update the title for one of the sub-schemas owned by this Schema
     *         instance
     *
     * @param  subschema  sub-schema to update
     * @param  title      new title
     */
    void setSubschemaTitle(const Subschema *subschema, const std::string &title)
    {
        mutableSubschema(subschema)->setTitle(title);
    }

private:

    Subschema *newSubschema()
    {
        void *ptr = m_allocFn(sizeof(Subschema));
        if (!ptr) {
            throwRuntimeError(
                    "Failed to allocate memory for shared empty sub-schema");
        }

#if VALIJSON_USE_EXCEPTIONS
        try {
#endif
            return new (ptr) Subschema();
#if VALIJSON_USE_EXCEPTIONS
        } catch (...) {
            m_freeFn(ptr);
            throw;
        }
#endif
    }

    Subschema * mutableSubschema(const Subschema *subschema)
    {
        if (subschema == this) {
            return this;
        }

        if (subschema == sharedEmptySubschema) {
            throwRuntimeError(
                    "Cannot modify the shared empty sub-schema");
        }

        auto *noConst = const_cast<Subschema*>(subschema);
        if (subschemaSet.find(noConst) == subschemaSet.end()) {
            throwRuntimeError(
                    "Subschema pointer is not owned by this Schema instance");
        }

        return noConst;
    }

    /// Set of Subschema instances owned by this schema
    std::set<Subschema*> subschemaSet;

    /// Empty schema that can be reused by multiple constraints
    const Subschema *sharedEmptySubschema;
};

} // namespace valijson
