#include "ruby.h"
#ifdef HAVE_RUBY_DIGEST_H
#include "ruby/digest.h"
#else
#include "digest.h"
#endif
#include "KeccakNISTInterface.h"

#define MAX_DIGEST_SIZE 64
#define DEFAULT_DIGEST_LEN 512

static int keccak_init_func(hashState *ctx);
static void keccak_update_func(hashState *ctx, unsigned char *str, size_t len);
static int keccak_finish_func(hashState *ctx, unsigned char *digest);

/*
  Metadata definition for the Keccak algorithm.
  Defines the Version, sizes for block and digest as well as
  the entry points for the algorithms
*/
static rb_digest_metadata_t keccak = {
	RUBY_DIGEST_API_VERSION,
	DEFAULT_DIGEST_LEN,
	KeccakPermutationSize - (2 * DEFAULT_DIGEST_LEN), //size of blocks
	sizeof(hashState), //size of context for the object we'll be passed in below functions.
	(rb_digest_hash_init_func_t)keccak_init_func,
	(rb_digest_hash_update_func_t)keccak_update_func,
	(rb_digest_hash_finish_func_t)keccak_finish_func,
};

/* Initialization function for the algorithm,
   gets called during allocation of the digest object.
   we override initialize to do custom hash size, so we don't care too much here.
*/
static int
keccak_init_func(hashState *ctx) {
  // Just return a 1 ' successful' we override the init function
  // so this is not necessary
  // the base class alloc calls this to initialize the algorithm
  return 1;
}

/* Update function, take the current context and add str to it */
static void
keccak_update_func(hashState *ctx, unsigned char *str, size_t len) {
	Update(ctx, str, len * 8);
}

/* Finish the hash calculation and return the finished string */
static int
keccak_finish_func(hashState *ctx, unsigned char *digest) {
	Final(ctx, digest);
	return 1;
}

/* Ruby method.  Digest::Keccak#finish()
 * No Arguments
 * @returns [String] Encoded Digest String
 */
static VALUE
rb_keccak_finish(VALUE self) {
  hashState *ctx;
  VALUE digest;

  ctx = (hashState *)RTYPEDDATA_DATA(self);
  digest = rb_str_new(0, ctx->capacity / 2 / 8);
  keccak_finish_func(ctx, (unsigned char *)RSTRING_PTR(digest));

  return digest;
}

/* :nodoc: private method
 * initialize the ctx with the bitlength
 */
static void
keccak_init(hashState *ctx, size_t bitlen) {
	switch (Init(ctx, bitlen)) {
	case SUCCESS:
		return;
	case FAIL:
		rb_raise(rb_eRuntimeError, "Unknown error");
	case BAD_HASHLEN:
		rb_raise(rb_eArgError, "Bad hash length (must be 0, 224, 256, 384 or 512)");
	default:
		rb_raise(rb_eRuntimeError, "Unknown error code");
	}
}

/* Ruby method.  Digest::Keccak.new(hashlen)
 * @param hashlen The length of hash, only supports 224, 256, 384 or 512
 * @returns [Digest::Keccak] new object.
 */
static VALUE
rb_keccak_initialize(int argc, VALUE *argv, VALUE self) {
	hashState *ctx;
	VALUE hashlen;
	int i_hashlen;

	if (rb_scan_args(argc, argv, "01", &hashlen) == 0) {
		i_hashlen = DEFAULT_DIGEST_LEN;
	} else {
		i_hashlen = NUM2INT(hashlen);
	}
	if ( i_hashlen == 0) {
		rb_raise(rb_eArgError, "Unsupported hash length");
  }

  ctx = (hashState *)RTYPEDDATA_DATA(self);
	keccak_init(ctx, i_hashlen);

	return rb_call_super(0, NULL);
}

/* Ruby method.  Digest::Keccak#digest_length
 * @returns [Numeric] Length of the digest.
 */
static VALUE
rb_keccak_digest_length(VALUE self) {
	hashState *ctx;

	ctx = (hashState *)RTYPEDDATA_DATA(self);
	return INT2FIX(ctx->capacity / 2 / 8);
}

/* Ruby method.  Digest::Keccak#block_length
 * @returns [Numeric] Length of blocks in this digest.
 */
static VALUE
rb_keccak_block_length(VALUE self) {
	hashState *ctx;

	ctx = (hashState *)RTYPEDDATA_DATA(self);
	return INT2FIX(ctx->rate / 8);
}

void __attribute__((visibility("default")))
Init_keccak() {
	VALUE mDigest, cDigest_Base, cKeccak;

	rb_require("digest");

	mDigest = rb_path2class("Digest");
	cDigest_Base = rb_path2class("Digest::Base");

	cKeccak = rb_define_class_under(mDigest, "Keccak", cDigest_Base);

	rb_iv_set(cKeccak, "metadata", Data_Wrap_Struct(0, 0, 0, (void *)&keccak));

  rb_define_method(cKeccak, "initialize", rb_keccak_initialize, -1);
  rb_define_method(cKeccak, "digest_length", rb_keccak_digest_length, 0);
  rb_define_method(cKeccak, "block_length", rb_keccak_block_length, 0);
  rb_define_method(cKeccak, "finish", rb_keccak_finish, 0);
}
