#include "nix/expr/eval.hh"
#include "nix/expr/value.hh"

#include "nix_api_expr.h"
#include "nix_api_expr_internal.h"
#include "nix_api_external.h"
#include "nix_api_util.h"
#include "nix_api_util_internal.h"
#include "nix_api_value.h"
#include "nix/expr/value/context.hh"

#include <nlohmann/json.hpp>

extern "C" {

void nix_set_string_return(nix_string_return * str, const char * c)
{
    str->str = c;
}

nix_err nix_external_print(nix_c_context * context, nix_printer * printer, const char * c)
{
    if (context)
        context->last_err_code = NIX_OK;
    try {
        printer->s << c;
    }
    NIXC_CATCH_ERRS
}

nix_err nix_external_add_string_context(nix_c_context * context, nix_string_context * ctx, const char * c)
{
    if (context)
        context->last_err_code = NIX_OK;
    try {
        auto r = nix::NixStringContextElem::parse(c);
        ctx->ctx.insert(r);
    }
    NIXC_CATCH_ERRS
}

} // extern "C"

namespace {

class NixCExternalValue : public nix::ExternalValueBase
{
    NixCExternalValueDesc & desc;
    void * v;

public:
    NixCExternalValue(NixCExternalValueDesc & desc, void * v)
        : desc(desc)
        , v(v) {};

    void * get_ptr()
    {
        return v;
    }

    /**
     * Print out the value
     */
    virtual std::ostream & print(std::ostream & str) const override
    {
        nix_printer p{str};
        desc.print(v, &p);
        return str;
    }

    /**
     * Return a simple string describing the type
     */
    virtual std::string showType() const override
    {
        nix_string_return res;
        desc.showType(v, &res);
        return std::move(res.str);
    }

    /**
     * Return a string to be used in builtins.typeOf
     */
    virtual std::string typeOf() const override
    {
        nix_string_return res;
        desc.typeOf(v, &res);
        return std::move(res.str);
    }

    /**
     * Coerce the value to a string.
     */
    virtual std::string coerceToString(
        nix::EvalState & state,
        const nix::PosIdx & pos,
        nix::NixStringContext & context,
        bool copyMore,
        bool copyToStore) const override
    {
        if (!desc.coerceToString) {
            return nix::ExternalValueBase::coerceToString(state, pos, context, copyMore, copyToStore);
        }
        nix_string_context ctx{context};
        nix_string_return res{""};
        // todo: pos, errors
        desc.coerceToString(v, &ctx, copyMore, copyToStore, &res);
        if (res.str.empty()) {
            return nix::ExternalValueBase::coerceToString(state, pos, context, copyMore, copyToStore);
        }
        return std::move(res.str);
    }

    /**
     * Compare to another value of the same type.
     */
    virtual bool operator==(const ExternalValueBase & b) const noexcept override
    {
        if (!desc.equal) {
            return false;
        }
        auto r = dynamic_cast<const NixCExternalValue *>(&b);
        if (!r)
            return false;
        return desc.equal(v, r->v);
    }

    /**
     * Print the value as JSON.
     */
    virtual nlohmann::json printValueAsJSON(
        nix::EvalState & state, bool strict, nix::NixStringContext & context, bool copyToStore = true) const override
    {
        if (!desc.printValueAsJSON) {
            return nix::ExternalValueBase::printValueAsJSON(state, strict, context, copyToStore);
        }
        nix_string_context ctx{context};
        nix_string_return res{""};
        EvalState wrapper{state};
        desc.printValueAsJSON(v, &wrapper, strict, &ctx, copyToStore, &res);
        if (res.str.empty()) {
            return nix::ExternalValueBase::printValueAsJSON(state, strict, context, copyToStore);
        }
        return nlohmann::json::parse(res.str);
    }

    /**
     * Print the value as XML.
     */
    virtual void printValueAsXML(
        nix::EvalState & state,
        bool strict,
        bool location,
        nix::XMLWriter & doc,
        nix::NixStringContext & context,
        nix::StringSet & drvsSeen,
        const nix::PosIdx pos) const override
    {
        if (!desc.printValueAsXML) {
            return nix::ExternalValueBase::printValueAsXML(state, strict, location, doc, context, drvsSeen, pos);
        }
        nix_string_context ctx{context};
        EvalState wrapper{state};
        desc.printValueAsXML(
            v, &wrapper, strict, location, &doc, &ctx, &drvsSeen, *reinterpret_cast<const uint32_t *>(&pos));
    }

    virtual ~NixCExternalValue() override {};
};

} // namespace

extern "C" {

ExternalValue * nix_create_external_value(nix_c_context * context, NixCExternalValueDesc * desc, void * v)
{
    if (context)
        context->last_err_code = NIX_OK;
    try {
        auto ret = new
#if NIX_USE_BOEHMGC
            (GC)
#endif
                NixCExternalValue(*desc, v);
        nix_gc_incref(nullptr, ret);
        return (ExternalValue *) ret;
    }
    NIXC_CATCH_ERRS_NULL
}

void * nix_get_external_value_content(nix_c_context * context, ExternalValue * b)
{
    if (context)
        context->last_err_code = NIX_OK;
    try {
        auto r = dynamic_cast<NixCExternalValue *>((nix::ExternalValueBase *) b);
        if (r)
            return r->get_ptr();
        return nullptr;
    }
    NIXC_CATCH_ERRS_NULL
}

} // extern "C"
