#include "ruby.h"
#include "rubyspec.h"

#include "ruby/digest.h"

#ifdef __cplusplus
extern "C" {
#endif

#define DIGEST_LENGTH 20
#define BLOCK_LENGTH 40

const char *init_string = "Initialized\n";
const char *update_string = "Updated: ";
const char *finish_string = "Finished\n";

#define PAYLOAD_SIZE 128

typedef struct CTX {
  uint8_t pos;
  char payload[PAYLOAD_SIZE];
} CTX;

void* context = NULL;

int digest_spec_plugin_init(void *raw_ctx) {
    // Make the context accessible to tests. This isn't safe, but there's no way to access the context otherwise.
    context = raw_ctx;

    struct CTX *ctx = (struct CTX *)raw_ctx;
    size_t len = strlen(init_string);

    // Clear the payload since this init function will be invoked as part of the `reset` operation.
    memset(ctx->payload, 0, PAYLOAD_SIZE);

    // Write a simple value we can verify in tests.
    // This is not what a real digest would do, but we're using a dummy digest plugin to test interactions.
    memcpy(ctx->payload, init_string, len);
    ctx->pos = (uint8_t) len;

    return 1;
}

void digest_spec_plugin_update(void *raw_ctx, unsigned char *ptr, size_t size) {
    struct CTX *ctx = (struct CTX *)raw_ctx;
    size_t update_str_len = strlen(update_string);

    if (ctx->pos + update_str_len + size >= PAYLOAD_SIZE) {
        rb_raise(rb_eRuntimeError, "update size too large; reset the digest and write fewer updates");
    }

    // Write the supplied value to the payload so it can be easily verified in test.
    // This is not what a real digest would do, but we're using a dummy digest plugin to test interactions.
    memcpy(ctx->payload + ctx->pos, update_string, update_str_len);
    ctx->pos += update_str_len;

    memcpy(ctx->payload + ctx->pos, ptr, size);
    ctx->pos += size;

    return;
}

int digest_spec_plugin_finish(void *raw_ctx, unsigned char *ptr) {
    struct CTX *ctx = (struct CTX *)raw_ctx;
    size_t finish_string_len = strlen(finish_string);

    // We're always going to write DIGEST_LENGTH bytes. In a real plugin, this would be the digest value. Here we
    // write out a text string in order to make validation in tests easier.
    //
    // In order to delineate the output more clearly from an `Digest#update` call, we always write out the
    // `finish_string` message. That leaves `DIGEST_LENGTH - finish_string_len` bytes to read out of the context.
    size_t context_bytes = DIGEST_LENGTH - finish_string_len;

    memcpy(ptr, ctx->payload + (ctx->pos - context_bytes), context_bytes);
    memcpy(ptr + context_bytes, finish_string, finish_string_len);

    return 1;
}

static const rb_digest_metadata_t metadata = {
    // The RUBY_DIGEST_API_VERSION value comes from ruby/digest.h and may vary based on the Ruby being tested. Since
    // it isn't publicly exposed in the digest gem, we ignore for these tests. Either the test hard-codes an expected
    // value and is subject to breaking depending on the Ruby being run or we publicly expose `RUBY_DIGEST_API_VERSION`,
    // in which case the test would pass trivially.
    RUBY_DIGEST_API_VERSION,
    DIGEST_LENGTH,
    BLOCK_LENGTH,
    sizeof(CTX),
    (rb_digest_hash_init_func_t) digest_spec_plugin_init,
    (rb_digest_hash_update_func_t) digest_spec_plugin_update,
    (rb_digest_hash_finish_func_t) digest_spec_plugin_finish,
};

// The `get_metadata_ptr` function is not publicly available in the digest gem. However, we need to use
// to extract the `rb_digest_metadata_t*` value set up by the plugin so we reproduce and adjust the
// definition here.
//
// Taken and adapted from https://github.com/ruby/digest/blob/v3.2.0/ext/digest/digest.c#L558-L568
static rb_digest_metadata_t * get_metadata_ptr(VALUE obj) {
    rb_digest_metadata_t *algo;

#ifdef DIGEST_USE_RB_EXT_RESOLVE_SYMBOL
    // In the digest gem there is an additional data type check performed before reading the value out.
    // Since the type definition isn't public, we can't use it as part of a type check here so we omit it.
    // This is safe to do because this code is intended to only load digest plugins written as part of this test suite.
    algo = (rb_digest_metadata_t *) RTYPEDDATA_DATA(obj);
#else
# undef RUBY_UNTYPED_DATA_WARNING
# define RUBY_UNTYPED_DATA_WARNING 0
    Data_Get_Struct(obj, rb_digest_metadata_t, algo);
#endif

    return algo;
}

VALUE digest_spec_rb_digest_make_metadata(VALUE self) {
    return rb_digest_make_metadata(&metadata);
}

VALUE digest_spec_block_length(VALUE self, VALUE meta) {
    rb_digest_metadata_t* algo = get_metadata_ptr(meta);

    return SIZET2NUM(algo->block_len);
}

VALUE digest_spec_digest_length(VALUE self, VALUE meta) {
    rb_digest_metadata_t* algo = get_metadata_ptr(meta);

    return SIZET2NUM(algo->digest_len);
}

VALUE digest_spec_context_size(VALUE self, VALUE meta) {
    rb_digest_metadata_t* algo = get_metadata_ptr(meta);

    return SIZET2NUM(algo->ctx_size);
}

#define PTR2NUM(x) (rb_int2inum((intptr_t)(void *)(x)))

VALUE digest_spec_context(VALUE self, VALUE digest) {
    return PTR2NUM(context);
}

void Init_digest_spec(void) {
    VALUE cls;

    cls = rb_define_class("CApiDigestSpecs", rb_cObject);
    rb_define_method(cls, "rb_digest_make_metadata", digest_spec_rb_digest_make_metadata, 0);
    rb_define_method(cls, "block_length", digest_spec_block_length, 1);
    rb_define_method(cls, "digest_length", digest_spec_digest_length, 1);
    rb_define_method(cls, "context_size", digest_spec_context_size, 1);
    rb_define_method(cls, "context", digest_spec_context, 1);

    VALUE mDigest, cDigest_Base, cDigest;

    mDigest = rb_define_module("Digest");
    mDigest = rb_digest_namespace();
    cDigest_Base = rb_const_get(mDigest, rb_intern_const("Base"));

    cDigest = rb_define_class_under(mDigest, "TestDigest", cDigest_Base);
    rb_iv_set(cDigest, "metadata", rb_digest_make_metadata(&metadata));
}

#ifdef __cplusplus
}
#endif
