#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"

/* Prevent a clash between some Perl and libstdc++ macros. */
#undef do_open
#undef do_close

#include "nix/store/derivations.hh"
#include "nix/store/realisation.hh"
#include "nix/store/globals.hh"
#include "nix/store/store-open.hh"
#include "nix/util/source-accessor.hh"
#include <nlohmann/json.hpp>

using namespace nix;

static bool libStoreInitialized = false;

struct StoreWrapper {
    ref<Store> store;
};

MODULE = Nix::Store PACKAGE = Nix::Store
PROTOTYPES: ENABLE

TYPEMAP: <<HERE
StoreWrapper *      O_OBJECT

OUTPUT
O_OBJECT
    sv_setref_pv( $arg, CLASS, (void*)$var );

INPUT
O_OBJECT
    if ( sv_isobject($arg) && (SvTYPE(SvRV($arg)) == SVt_PVMG) ) {
        $var = ($type)SvIV((SV*)SvRV( $arg ));
    }
    else {
        warn( \"${Package}::$func_name() -- \"
		\"$var not a blessed SV reference\");
        XSRETURN_UNDEF;
    }
HERE

#undef dNOOP // Hack to work around "error: declaration of 'Perl___notused' has a different language linkage" error message on clang.
#define dNOOP

void
StoreWrapper::DESTROY()

StoreWrapper *
StoreWrapper::new(char * s = nullptr)
    CODE:
        static std::shared_ptr<Store> _store;
        try {
            if (!libStoreInitialized) {
                initLibStore();
                libStoreInitialized = true;
            }
            if (items == 1) {
                _store = openStore();
                RETVAL = new StoreWrapper {
                    .store = ref<Store>{_store}
                };
            } else {
                RETVAL = new StoreWrapper {
                    .store = openStore(s)
                };
            }
        } catch (Error & e) {
            croak("%s", e.what());
        }
    OUTPUT:
        RETVAL


void init()
    CODE:
        if (!libStoreInitialized) {
            initLibStore();
            libStoreInitialized = true;
        }


int
StoreWrapper::isValidPath(char * path)
    CODE:
        try {
            RETVAL = THIS->store->isValidPath(THIS->store->parseStorePath(path));
        } catch (Error & e) {
            croak("%s", e.what());
        }
    OUTPUT:
        RETVAL


SV *
StoreWrapper::queryReferences(char * path)
    PPCODE:
        try {
            for (auto & i : THIS->store->queryPathInfo(THIS->store->parseStorePath(path))->references)
                XPUSHs(sv_2mortal(newSVpv(THIS->store->printStorePath(i).c_str(), 0)));
        } catch (Error & e) {
            croak("%s", e.what());
        }


SV *
StoreWrapper::queryPathHash(char * path)
    PPCODE:
        try {
            auto s = THIS->store->queryPathInfo(THIS->store->parseStorePath(path))->narHash.to_string(HashFormat::Nix32, true);
            XPUSHs(sv_2mortal(newSVpv(s.c_str(), 0)));
        } catch (Error & e) {
            croak("%s", e.what());
        }


SV *
StoreWrapper::queryPathInfo(char * path, int base32)
    PPCODE:
        try {
            auto info = THIS->store->queryPathInfo(THIS->store->parseStorePath(path));
            if (!info->deriver)
                XPUSHs(&PL_sv_undef);
            else
                XPUSHs(sv_2mortal(newSVpv(THIS->store->printStorePath(*info->deriver).c_str(), 0)));
            auto s = info->narHash.to_string(base32 ? HashFormat::Nix32 : HashFormat::Base16, true);
            XPUSHs(sv_2mortal(newSVpv(s.c_str(), 0)));
            mXPUSHi(info->registrationTime);
            mXPUSHi(info->narSize);
            AV * refs = newAV();
            for (auto & i : info->references)
                av_push(refs, newSVpv(THIS->store->printStorePath(i).c_str(), 0));
            XPUSHs(sv_2mortal(newRV((SV *) refs)));
            AV * sigs = newAV();
            for (auto & i : info->sigs)
                av_push(sigs, newSVpv(i.to_string().c_str(), 0));
            XPUSHs(sv_2mortal(newRV((SV *) sigs)));
        } catch (Error & e) {
            croak("%s", e.what());
        }

SV *
StoreWrapper::queryRawRealisation(char * outputId)
    PPCODE:
      try {
        auto realisation = THIS->store->queryRealisation(DrvOutput::parse(*THIS->store, outputId));
        if (realisation)
            XPUSHs(sv_2mortal(newSVpv(static_cast<nlohmann::json>(*realisation).dump().c_str(), 0)));
        else
            XPUSHs(sv_2mortal(newSVpv("", 0)));
      } catch (Error & e) {
        croak("%s", e.what());
      }


SV *
StoreWrapper::queryPathFromHashPart(char * hashPart)
    PPCODE:
        try {
            auto path = THIS->store->queryPathFromHashPart(hashPart);
            XPUSHs(sv_2mortal(newSVpv(path ? THIS->store->printStorePath(*path).c_str() : "", 0)));
        } catch (Error & e) {
            croak("%s", e.what());
        }


SV *
StoreWrapper::computeFSClosure(int flipDirection, int includeOutputs, ...)
    PPCODE:
        try {
            StorePathSet paths;
            for (int n = 3; n < items; ++n)
                THIS->store->computeFSClosure(THIS->store->parseStorePath(SvPV_nolen(ST(n))), paths, flipDirection, includeOutputs);
            for (auto & i : paths)
                XPUSHs(sv_2mortal(newSVpv(THIS->store->printStorePath(i).c_str(), 0)));
        } catch (Error & e) {
            croak("%s", e.what());
        }


SV *
StoreWrapper::topoSortPaths(...)
    PPCODE:
        try {
            StorePathSet paths;
            for (int n = 1; n < items; ++n) paths.insert(THIS->store->parseStorePath(SvPV_nolen(ST(n))));
            auto sorted = THIS->store->topoSortPaths(paths);
            for (auto & i : sorted)
                XPUSHs(sv_2mortal(newSVpv(THIS->store->printStorePath(i).c_str(), 0)));
        } catch (Error & e) {
            croak("%s", e.what());
        }


SV *
StoreWrapper::followLinksToStorePath(char * path)
    CODE:
        try {
            RETVAL = newSVpv(THIS->store->printStorePath(THIS->store->followLinksToStorePath(path)).c_str(), 0);
        } catch (Error & e) {
            croak("%s", e.what());
        }
    OUTPUT:
        RETVAL


SV * convertHash(char * algo, char * s, int toBase32)
    PPCODE:
        try {
            auto h = Hash::parseAny(s, parseHashAlgo(algo));
            auto s = h.to_string(toBase32 ? HashFormat::Nix32 : HashFormat::Base16, false);
            XPUSHs(sv_2mortal(newSVpv(s.c_str(), 0)));
        } catch (Error & e) {
            croak("%s", e.what());
        }


SV * signString(char * secretKey_, char * msg)
    PPCODE:
        try {
            auto sig = SecretKey(secretKey_).signDetached(msg).to_string();
            XPUSHs(sv_2mortal(newSVpv(sig.c_str(), sig.size())));
        } catch (Error & e) {
            croak("%s", e.what());
        }


SV *
StoreWrapper::addToStore(char * srcPath, int recursive, char * algo)
    PPCODE:
        try {
            auto method = recursive ? ContentAddressMethod::Raw::NixArchive : ContentAddressMethod::Raw::Flat;
            auto path = THIS->store->addToStore(
                std::string(baseNameOf(srcPath)),
                {makeFSSourceAccessor(absPath(srcPath)), CanonPath::root},
                method, parseHashAlgo(algo));
            XPUSHs(sv_2mortal(newSVpv(THIS->store->printStorePath(path).c_str(), 0)));
        } catch (Error & e) {
            croak("%s", e.what());
        }


SV *
StoreWrapper::derivationFromPath(char * drvPath)
    PREINIT:
        HV *hash;
    CODE:
        try {
            Derivation drv = THIS->store->derivationFromPath(THIS->store->parseStorePath(drvPath));
            hash = newHV();

            HV * outputs = newHV();
            for (auto & i : drv.outputsAndOptPaths(*THIS->store)) {
                hv_store(
                    outputs, i.first.c_str(), i.first.size(),
                    !i.second.second
                        ? newSV(0) /* null value */
                        : newSVpv(THIS->store->printStorePath(*i.second.second).c_str(), 0),
                    0);
            }
            hv_stores(hash, "outputs", newRV((SV *) outputs));

            AV * inputDrvs = newAV();
            for (auto & i : drv.inputDrvs.map)
                av_push(inputDrvs, newSVpv(THIS->store->printStorePath(i.first).c_str(), 0)); // !!! ignores i->second
            hv_stores(hash, "inputDrvs", newRV((SV *) inputDrvs));

            AV * inputSrcs = newAV();
            for (auto & i : drv.inputSrcs)
                av_push(inputSrcs, newSVpv(THIS->store->printStorePath(i).c_str(), 0));
            hv_stores(hash, "inputSrcs", newRV((SV *) inputSrcs));

            hv_stores(hash, "platform", newSVpv(drv.platform.c_str(), 0));
            hv_stores(hash, "builder", newSVpv(drv.builder.c_str(), 0));

            AV * args = newAV();
            for (auto & i : drv.args)
                av_push(args, newSVpv(i.c_str(), 0));
            hv_stores(hash, "args", newRV((SV *) args));

            HV * env = newHV();
            for (auto & i : drv.env)
                hv_store(env, i.first.c_str(), i.first.size(), newSVpv(i.second.c_str(), 0), 0);
            hv_stores(hash, "env", newRV((SV *) env));

            RETVAL = newRV_noinc((SV *)hash);
        } catch (Error & e) {
            croak("%s", e.what());
        }
    OUTPUT:
        RETVAL


void
StoreWrapper::addTempRoot(char * storePath)
    PPCODE:
        try {
            THIS->store->addTempRoot(THIS->store->parseStorePath(storePath));
        } catch (Error & e) {
            croak("%s", e.what());
        }


SV * getStoreDir()
    PPCODE:
        XPUSHs(sv_2mortal(newSVpv(resolveStoreConfig(StoreReference{settings.storeUri.get()})->storeDir.c_str(), 0)));
