#ifdef __cplusplus
extern "C" {
#endif
#define PERL_NO_GET_CONTEXT
#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"
#ifdef __cplusplus
}
#endif

/* Hack to work around "error: declaration of 'Perl___notused' has a different */
/* language linkage" error on Clang */
#ifdef dNOOP
# undef dNOOP
# define dNOOP
#endif

#ifdef do_open
#undef do_open
#endif
#ifdef do_close
#undef do_close
#endif

#define NEED_newCONSTSUB
#include "ppport.h"
#include "uid2/uid2client.h"

static SV*
make_refresh_result(pTHX_ uid2::RefreshResult result)
{
    HV* hv = newHV();
    if (result.IsSuccess()) {
        hv_stores(hv, "is_success", newSViv(1));
    } else {
        hv_stores(hv, "is_success", newSViv(0));
        std::string reason = result.GetReason();
        hv_stores(hv, "reason", newSVpvn(reason.c_str(), reason.size()));
    }
    return newRV_noinc((SV*) hv);
}

static SV*
make_timestamp(pTHX_ uid2::Timestamp t)
{
    SV* res = newSV(0);
    sv_setref_pv(res, "UID2::Client::XS::Timestamp", (void *) new uid2::Timestamp(t));
    return res;
}

MODULE = UID2::Client::XS PACKAGE = UID2::Client::XS

BOOT:
{
    HV* stash = gv_stashpv("UID2::Client::XS::IdentityScope", 1);
    newCONSTSUB(stash, "UID2", newSViv(static_cast<int>(uid2::IdentityScope::UID2)));
    newCONSTSUB(stash, "EUID", newSViv(static_cast<int>(uid2::IdentityScope::EUID)));

    stash = gv_stashpv("UID2::Client::XS::IdentityType", 1);
    newCONSTSUB(stash, "EMAIL", newSViv(static_cast<int>(uid2::IdentityType::EMAIL)));
    newCONSTSUB(stash, "PHONE", newSViv(static_cast<int>(uid2::IdentityType::PHONE)));

    stash = gv_stashpv("UID2::Client::XS::DecryptionStatus", 1);
    newCONSTSUB(stash, "SUCCESS", newSViv(static_cast<int>(uid2::DecryptionStatus::SUCCESS)));
    newCONSTSUB(stash, "NOT_AUTHORIZED_FOR_KEY", newSViv(static_cast<int>(uid2::DecryptionStatus::NOT_AUTHORIZED_FOR_KEY)));
    newCONSTSUB(stash, "NOT_INITIALIZED", newSViv(static_cast<int>(uid2::DecryptionStatus::NOT_INITIALIZED)));
    newCONSTSUB(stash, "INVALID_PAYLOAD", newSViv(static_cast<int>(uid2::DecryptionStatus::INVALID_PAYLOAD)));
    newCONSTSUB(stash, "EXPIRED_TOKEN", newSViv(static_cast<int>(uid2::DecryptionStatus::EXPIRED_TOKEN)));
    newCONSTSUB(stash, "KEYS_NOT_SYNCED", newSViv(static_cast<int>(uid2::DecryptionStatus::KEYS_NOT_SYNCED)));
    newCONSTSUB(stash, "VERSION_NOT_SUPPORTED", newSViv(static_cast<int>(uid2::DecryptionStatus::VERSION_NOT_SUPPORTED)));
    newCONSTSUB(stash, "INVALID_PAYLOAD_TYPE", newSViv(static_cast<int>(uid2::DecryptionStatus::INVALID_PAYLOAD_TYPE)));
    newCONSTSUB(stash, "INVALID_IDENTITY_SCOPE", newSViv(static_cast<int>(uid2::DecryptionStatus::INVALID_IDENTITY_SCOPE)));

    stash = gv_stashpv("UID2::Client::XS::EncryptionStatus", 1);
    newCONSTSUB(stash, "SUCCESS", newSViv(static_cast<int>(uid2::EncryptionStatus::SUCCESS)));
    newCONSTSUB(stash, "NOT_AUTHORIZED_FOR_KEY", newSViv(static_cast<int>(uid2::EncryptionStatus::NOT_AUTHORIZED_FOR_KEY)));
    newCONSTSUB(stash, "NOT_INITIALIZED", newSViv(static_cast<int>(uid2::EncryptionStatus::NOT_INITIALIZED)));
    newCONSTSUB(stash, "KEYS_NOT_SYNCED", newSViv(static_cast<int>(uid2::EncryptionStatus::KEYS_NOT_SYNCED)));
    newCONSTSUB(stash, "TOKEN_DECRYPT_FAILURE", newSViv(static_cast<int>(uid2::EncryptionStatus::TOKEN_DECRYPT_FAILURE)));
    newCONSTSUB(stash, "KEY_INACTIVE", newSViv(static_cast<int>(uid2::EncryptionStatus::KEY_INACTIVE)));
    newCONSTSUB(stash, "ENCRYPTION_FAILURE", newSViv(static_cast<int>(uid2::EncryptionStatus::ENCRYPTION_FAILURE)));

    stash = gv_stashpv("UID2::Client::XS::AdvertisingTokenVersion", 1);
    newCONSTSUB(stash, "V3", newSViv(static_cast<int>(uid2::AdvertisingTokenVersion::V3)));
    newCONSTSUB(stash, "V4", newSViv(static_cast<int>(uid2::AdvertisingTokenVersion::V4)));
}

PROTOTYPES: DISABLE

uid2::UID2Client*
uid2::UID2Client::new(options)
    HV* options;
ALIAS:
    new_euid = 1
CODE:
    SV** ent;
    const char* endpoint = nullptr;
    const char* auth_key = nullptr;
    const char* secret_key = nullptr;
    uid2::IdentityScope identity_scope = uid2::IdentityScope::UID2;
    if (ix == 1) {
         identity_scope = uid2::IdentityScope::EUID;
    }
    if ((ent = hv_fetchs(options, "endpoint", 0)) != NULL) {
        endpoint = SvPV_nolen(*ent);
    }
    if ((ent = hv_fetchs(options, "auth_key", 0)) != NULL) {
        auth_key = SvPV_nolen(*ent);
    }
    if ((ent = hv_fetchs(options, "secret_key", 0)) != NULL) {
        secret_key = SvPV_nolen(*ent);
    }
    if ((ent = hv_fetchs(options, "identity_scope", 0)) != NULL) {
        identity_scope = (uid2::IdentityScope) SvIV(*ent);
    }
    if (endpoint == nullptr || auth_key == nullptr || secret_key == nullptr) {
        croak("endpoint, auth_key, secret_key are required");
    }
    uid2::UID2Client* client = nullptr;
    try {
        client = new uid2::UID2Client(endpoint, auth_key, secret_key, identity_scope);
    }
    catch (std::exception& e) {
        croak("%s", e.what());
    }
    catch (const char * str) {
        croak("%s", str);
    }
    catch (...) {
        croak("exception occurred during new()");
    }
    RETVAL = client;
OUTPUT:
    RETVAL

SV*
uid2::UID2Client::refresh()
CODE:
    uid2::RefreshResult result = uid2::RefreshResult::MakeError("");
    try {
        result = THIS->Refresh();
    }
    catch (std::exception& e) {
        croak("%s", e.what());
    }
    catch (const char * str) {
        croak("%s", str);
    }
    catch (...) {
        croak("exception occurred during refresh()");
    }
    RETVAL = make_refresh_result(aTHX_ result);
OUTPUT:
    RETVAL

SV*
uid2::UID2Client::refresh_json(json)
    const char* json;
CODE:
    uid2::RefreshResult result = uid2::RefreshResult::MakeError("");
    try {
        result = THIS->RefreshJson(json);
    }
    catch (std::exception& e) {
        croak("%s", e.what());
    }
    catch (const char * str) {
        croak("%s", str);
    }
    catch (...) {
        croak("exception occurred during refresh_json()");
    }
    RETVAL = make_refresh_result(aTHX_ result);
OUTPUT:
    RETVAL

SV*
uid2::UID2Client::decrypt(token, now = nullptr)
    const char* token;
    uid2::Timestamp* now;
CODE:
    uid2::Timestamp timestamp;
    if (now == nullptr) {
        timestamp = uid2::Timestamp::Now();
    } else {
        timestamp = *now;
    }
    uid2::DecryptionResult result = uid2::DecryptionResult::MakeError(uid2::DecryptionStatus::NOT_INITIALIZED);
    try {
        result = THIS->Decrypt(token, timestamp);
    }
    catch (std::exception& e) {
        croak("%s", e.what());
    }
    catch (const char * str) {
        croak("%s", str);
    }
    catch (...) {
        croak("exception occurred during decrypt()");
    }
    HV* res = newHV();
    hv_stores(res, "is_success", result.IsSuccess() ? newSViv(1) : newSV(0));
    hv_stores(res, "status", newSViv(static_cast<int>(result.GetStatus())));
    std::string uid = result.GetUid();
    hv_stores(res, "uid", newSVpvn(uid.c_str(), uid.size()));
    hv_stores(res, "site_id", newSViv(result.GetSiteId()));
    hv_stores(res, "site_key_site_id", newSViv(result.GetSiteKeySiteId()));
    hv_stores(res, "established", make_timestamp(aTHX_ result.GetEstablished()));
    RETVAL = newRV_noinc((SV *) res);
OUTPUT:
    RETVAL

SV*
uid2::UID2Client::encrypt_data(data, req)
    SV* data;
    HV* req;
CODE:
    STRLEN len;
    const char* data_char = SvPV(data, len);
    uid2::EncryptionDataRequest request((uint8_t *) data_char, len);
    SV** ent;
    if ((ent = hv_fetchs(req, "site_id", 0)) != NULL) {
        request = request.WithSiteId(SvIV(*ent));
    }
    if ((ent = hv_fetchs(req, "advertising_token", 0)) != NULL) {
        const char* at = SvPV(*ent, len);
        request = request.WithAdvertisingToken(std::string(at, len));
    }
    if ((ent = hv_fetchs(req, "initialization_vector", 0)) != NULL) {
        const char* iv = SvPV(*ent, len);
        request = request.WithInitializationVector((uint8_t *) iv, len);
    }
    if ((ent = hv_fetchs(req, "now", 0)) != NULL) {
        if (!(sv_isobject(*ent) && (SvTYPE(SvRV(*ent)) == SVt_PVMG))) {
            croak("invalid type: now");
        }
        uid2::Timestamp* t = (uid2::Timestamp *) SvIV((SV*) SvRV(*ent));
        request = request.WithNow(*t);
    }
    uid2::EncryptionDataResult result = uid2::EncryptionDataResult::MakeError(uid2::EncryptionStatus::NOT_INITIALIZED);
    try {
        result = THIS->EncryptData(request);
    }
    catch (std::exception& e) {
        croak("%s", e.what());
    }
    catch (const char * str) {
        croak("%s", str);
    }
    catch (...) {
        croak("exception occurred during encrypt_data()");
    }
    HV* res = newHV();
    hv_stores(res, "is_success", result.IsSuccess() ? newSViv(1) : newSV(0));
    hv_stores(res, "status", newSViv(static_cast<int>(result.GetStatus())));
    if (result.IsSuccess()) {
        std::string str = result.GetEncryptedData();
        hv_stores(res, "encrypted_data", newSVpvn(str.c_str(), str.size()));
    }
    RETVAL = newRV_noinc((SV *) res);
OUTPUT:
    RETVAL

SV*
uid2::UID2Client::decrypt_data(encrypted_data)
    const char* encrypted_data;
CODE:
    uid2::DecryptionDataResult result = uid2::DecryptionDataResult::MakeError(uid2::DecryptionStatus::NOT_INITIALIZED);
    try {
        result = THIS->DecryptData(encrypted_data);
    }
    catch (std::exception& e) {
        croak("%s", e.what());
    }
    catch (const char * str) {
        croak("%s", str);
    }
    catch (...) {
        croak("exception occurred during decrypt_data()");
    }
    HV* res = newHV();
    hv_stores(res, "is_success", result.IsSuccess() ? newSViv(1) : newSV(0));
    hv_stores(res, "status", newSViv(static_cast<int>(result.GetStatus())));
    if (result.IsSuccess()) {
        const std::vector<std::uint8_t> data = result.GetDecryptedData();
        const std::string str(data.begin(), data.end());
        hv_stores(res, "decrypted_data", newSVpvn(str.c_str(), str.size()));
        hv_stores(res, "encrypted_at", make_timestamp(aTHX_ result.GetEncryptedAt()));
    }
    RETVAL = newRV_noinc((SV *) res);
OUTPUT:
    RETVAL

void
uid2::UID2Client::DESTROY()
CODE:
    delete THIS;

MODULE = UID2::Client::XS PACKAGE = UID2::Client::XS::Timestamp

PROTOTYPES: DISABLE

static uid2::Timestamp*
UID2::Client::XS::Timestamp::now()
CODE:
    RETVAL = new uid2::Timestamp(uid2::Timestamp::Now());
OUTPUT:
    RETVAL

static uid2::Timestamp*
UID2::Client::XS::Timestamp::from_epoch_second(epoch_second)
    int64_t epoch_second;
CODE:
    RETVAL = new uid2::Timestamp(uid2::Timestamp::FromEpochSecond(epoch_second));
OUTPUT:
    RETVAL

static uid2::Timestamp*
UID2::Client::XS::Timestamp::from_epoch_milli(epoch_milli)
    int64_t epoch_milli;
CODE:
    RETVAL = new uid2::Timestamp(uid2::Timestamp::FromEpochMilli(epoch_milli));
OUTPUT:
    RETVAL

int64_t
uid2::Timestamp::get_epoch_second()
CODE:
    RETVAL = THIS->GetEpochSecond();
OUTPUT:
    RETVAL

int64_t
uid2::Timestamp::get_epoch_milli()
CODE:
    RETVAL = THIS->GetEpochMilli();
OUTPUT:
    RETVAL

bool
uid2::Timestamp::is_zero()
CODE:
    RETVAL = THIS->IsZero();
OUTPUT:
    RETVAL

uid2::Timestamp*
uid2::Timestamp::add_seconds(seconds)
    int seconds;
PREINIT:
    const char* CLASS = "UID2::Client::XS::Timestamp";
CODE:
    RETVAL = new uid2::Timestamp(THIS->AddSeconds(seconds));
OUTPUT:
    RETVAL

uid2::Timestamp*
uid2::Timestamp::add_days(days)
    int days;
PREINIT:
    const char* CLASS = "UID2::Client::XS::Timestamp";
CODE:
    RETVAL = new uid2::Timestamp(THIS->AddDays(days));
OUTPUT:
    RETVAL

void
uid2::Timestamp::DESTROY()
CODE:
    delete THIS;