/*
 *
 * Copyright 2015 gRPC authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

#include <ruby/ruby.h>

#include "rb_compression_options.h"

#include <grpc/compression.h>
#include <grpc/grpc.h>
#include <grpc/impl/codegen/compression_types.h>
#include <grpc/impl/grpc_types.h>
#include <grpc/support/alloc.h>
#include <grpc/support/string_util.h>
#include <stdbool.h>
#include <string.h>

#include "rb_byte_buffer.h"
#include "rb_grpc.h"
#include "rb_grpc_imports.generated.h"

static VALUE grpc_rb_cCompressionOptions = Qnil;

/* Ruby Ids for the names of valid compression levels. */
static VALUE id_compress_level_none = Qnil;
static VALUE id_compress_level_low = Qnil;
static VALUE id_compress_level_medium = Qnil;
static VALUE id_compress_level_high = Qnil;

/* grpc_rb_compression_options wraps a grpc_compression_options.
 * It can be used to get the channel argument key-values for specific
 * compression settings. */

/* Note that ruby objects of this type don't carry any state in other
 * Ruby objects and don't have a mark for GC. */
typedef struct grpc_rb_compression_options {
  /* The actual compression options that's being wrapped */
  grpc_compression_options* wrapped;
} grpc_rb_compression_options;

static void grpc_rb_compression_options_free_internal(void* p) {
  grpc_rb_compression_options* wrapper = NULL;
  if (p == NULL) {
    return;
  };
  wrapper = (grpc_rb_compression_options*)p;
  if (wrapper->wrapped != NULL) {
    gpr_free(wrapper->wrapped);
    wrapper->wrapped = NULL;
  }
  xfree(p);
}

/* Destroys the compression options instances and free the
 * wrapped grpc compression options. */
static void grpc_rb_compression_options_free(void* p) {
  grpc_rb_compression_options_free_internal(p);
}

/* Ruby recognized data type for the CompressionOptions class. */
static rb_data_type_t grpc_rb_compression_options_data_type = {
    "grpc_compression_options",
    {NULL,
     grpc_rb_compression_options_free,
     GRPC_RB_MEMSIZE_UNAVAILABLE,
     {NULL, NULL}},
    NULL,
    NULL,
#ifdef RUBY_TYPED_FREE_IMMEDIATELY
    RUBY_TYPED_FREE_IMMEDIATELY
#endif
};

/* Allocates CompressionOptions instances.
   Allocate the wrapped grpc compression options and
   initialize it here too. */
static VALUE grpc_rb_compression_options_alloc(VALUE cls) {
  grpc_ruby_init();
  grpc_rb_compression_options* wrapper = NULL;

  wrapper = gpr_malloc(sizeof(grpc_rb_compression_options));
  wrapper->wrapped = NULL;
  wrapper->wrapped = gpr_malloc(sizeof(grpc_compression_options));
  grpc_compression_options_init(wrapper->wrapped);

  return TypedData_Wrap_Struct(cls, &grpc_rb_compression_options_data_type,
                               wrapper);
}

/* Disables a compression algorithm, given the GRPC core internal number of a
 * compression algorithm. */
VALUE grpc_rb_compression_options_disable_compression_algorithm_internal(
    VALUE self, VALUE algorithm_to_disable) {
  grpc_compression_algorithm compression_algorithm = 0;
  grpc_rb_compression_options* wrapper = NULL;

  TypedData_Get_Struct(self, grpc_rb_compression_options,
                       &grpc_rb_compression_options_data_type, wrapper);
  compression_algorithm =
      (grpc_compression_algorithm)NUM2INT(algorithm_to_disable);

  grpc_compression_options_disable_algorithm(wrapper->wrapped,
                                             compression_algorithm);

  return Qnil;
}

/* Gets the compression internal enum value of a compression level given its
 * name. */
grpc_compression_level grpc_rb_compression_options_level_name_to_value_internal(
    VALUE level_name) {
  Check_Type(level_name, T_SYMBOL);

  /* Check the compression level of the name passed in, and see which macro
   * from the GRPC core header files match. */
  if (id_compress_level_none == SYM2ID(level_name)) {
    return GRPC_COMPRESS_LEVEL_NONE;
  } else if (id_compress_level_low == SYM2ID(level_name)) {
    return GRPC_COMPRESS_LEVEL_LOW;
  } else if (id_compress_level_medium == SYM2ID(level_name)) {
    return GRPC_COMPRESS_LEVEL_MED;
  } else if (id_compress_level_high == SYM2ID(level_name)) {
    return GRPC_COMPRESS_LEVEL_HIGH;
  }

  rb_raise(rb_eArgError,
           "Unrecognized compression level name."
           "Valid compression level names are none, low, medium, and high.");

  /* Phony return statement. */
  return GRPC_COMPRESS_LEVEL_NONE;
}

/* Sets the default compression level, given the name of a compression level.
 * Throws an error if no algorithm matched. */
void grpc_rb_compression_options_set_default_level(
    grpc_compression_options* options, VALUE new_level_name) {
  options->default_level.level =
      grpc_rb_compression_options_level_name_to_value_internal(new_level_name);
  options->default_level.is_set = 1;
}

/* Gets the internal value of a compression algorithm suitable as the value
 * in a GRPC core channel arguments hash.
 * algorithm_value is an out parameter.
 * Raises an error if the name of the algorithm passed in is invalid. */
void grpc_rb_compression_options_algorithm_name_to_value_internal(
    grpc_compression_algorithm* algorithm_value, VALUE algorithm_name) {
  grpc_slice name_slice;
  VALUE algorithm_name_as_string = Qnil;

  Check_Type(algorithm_name, T_SYMBOL);

  /* Convert the algorithm symbol to a ruby string, so that we can get the
   * correct C string out of it. */
  algorithm_name_as_string = rb_funcall(algorithm_name, rb_intern("to_s"), 0);

  name_slice =
      grpc_slice_from_copied_buffer(RSTRING_PTR(algorithm_name_as_string),
                                    RSTRING_LEN(algorithm_name_as_string));

  /* Raise an error if the name isn't recognized as a compression algorithm by
   * the algorithm parse function
   * in GRPC core. */
  if (!grpc_compression_algorithm_parse(name_slice, algorithm_value)) {
    char* name_slice_str = grpc_slice_to_c_string(name_slice);
    char* error_message_str = NULL;
    VALUE error_message_ruby_str = Qnil;
    GRPC_RUBY_ASSERT(gpr_asprintf(&error_message_str,
                                  "Invalid compression algorithm name: %s",
                                  name_slice_str) != -1);
    gpr_free(name_slice_str);
    error_message_ruby_str =
        rb_str_new(error_message_str, strlen(error_message_str));
    gpr_free(error_message_str);
    rb_raise(rb_eNameError, "%s", StringValueCStr(error_message_ruby_str));
  }

  grpc_slice_unref(name_slice);
}

/* Indicates whether a given algorithm is enabled on this instance, given the
 * readable algorithm name. */
VALUE grpc_rb_compression_options_is_algorithm_enabled(VALUE self,
                                                       VALUE algorithm_name) {
  grpc_rb_compression_options* wrapper = NULL;
  grpc_compression_algorithm internal_algorithm_value;

  TypedData_Get_Struct(self, grpc_rb_compression_options,
                       &grpc_rb_compression_options_data_type, wrapper);
  grpc_rb_compression_options_algorithm_name_to_value_internal(
      &internal_algorithm_value, algorithm_name);

  if (grpc_compression_options_is_algorithm_enabled(wrapper->wrapped,
                                                    internal_algorithm_value)) {
    return Qtrue;
  }
  return Qfalse;
}

/* Sets the default algorithm to the name of the algorithm passed in.
 * Raises an error if the name is not a valid compression algorithm name. */
void grpc_rb_compression_options_set_default_algorithm(
    grpc_compression_options* options, VALUE algorithm_name) {
  grpc_rb_compression_options_algorithm_name_to_value_internal(
      &options->default_algorithm.algorithm, algorithm_name);
  options->default_algorithm.is_set = 1;
}

/* Disables an algorithm on the current instance, given the name of an
 * algorithm.
 * Fails if the algorithm name is invalid. */
void grpc_rb_compression_options_disable_algorithm(
    grpc_compression_options* compression_options, VALUE algorithm_name) {
  grpc_compression_algorithm internal_algorithm_value;

  grpc_rb_compression_options_algorithm_name_to_value_internal(
      &internal_algorithm_value, algorithm_name);
  grpc_compression_options_disable_algorithm(compression_options,
                                             internal_algorithm_value);
}

/* Provides a ruby hash of GRPC core channel argument key-values that
 * correspond to the compression settings on this instance. */
VALUE grpc_rb_compression_options_to_hash(VALUE self) {
  grpc_rb_compression_options* wrapper = NULL;
  grpc_compression_options* compression_options = NULL;
  VALUE channel_arg_hash = rb_hash_new();
  VALUE key = Qnil;
  VALUE value = Qnil;

  TypedData_Get_Struct(self, grpc_rb_compression_options,
                       &grpc_rb_compression_options_data_type, wrapper);
  compression_options = wrapper->wrapped;

  /* Add key-value pairs to the new Ruby hash. It can be used
   * as GRPC core channel arguments. */
  if (compression_options->default_level.is_set) {
    key = rb_str_new2(GRPC_COMPRESSION_CHANNEL_DEFAULT_LEVEL);
    value = INT2NUM((int)compression_options->default_level.level);
    rb_hash_aset(channel_arg_hash, key, value);
  }

  if (compression_options->default_algorithm.is_set) {
    key = rb_str_new2(GRPC_COMPRESSION_CHANNEL_DEFAULT_ALGORITHM);
    value = INT2NUM((int)compression_options->default_algorithm.algorithm);
    rb_hash_aset(channel_arg_hash, key, value);
  }

  key = rb_str_new2(GRPC_COMPRESSION_CHANNEL_ENABLED_ALGORITHMS_BITSET);
  value = INT2NUM((int)compression_options->enabled_algorithms_bitset);
  rb_hash_aset(channel_arg_hash, key, value);

  return channel_arg_hash;
}

/* Converts an internal enum level value to a readable level name.
 * Fails if the level value is invalid. */
VALUE grpc_rb_compression_options_level_value_to_name_internal(
    grpc_compression_level compression_value) {
  switch (compression_value) {
    case GRPC_COMPRESS_LEVEL_NONE:
      return ID2SYM(id_compress_level_none);
    case GRPC_COMPRESS_LEVEL_LOW:
      return ID2SYM(id_compress_level_low);
    case GRPC_COMPRESS_LEVEL_MED:
      return ID2SYM(id_compress_level_medium);
    case GRPC_COMPRESS_LEVEL_HIGH:
      return ID2SYM(id_compress_level_high);
    default:
      rb_raise(
          rb_eArgError,
          "Failed to convert compression level value to name for value: %d",
          (int)compression_value);
      /* return something to avoid compiler error about no return */
      return Qnil;
  }
}

/* Converts an algorithm internal enum value to a readable name.
 * Fails if the enum value is invalid. */
VALUE grpc_rb_compression_options_algorithm_value_to_name_internal(
    grpc_compression_algorithm internal_value) {
  const char* algorithm_name = NULL;

  if (!grpc_compression_algorithm_name(internal_value, &algorithm_name)) {
    rb_raise(rb_eArgError, "Failed to convert algorithm value to name");
  }

  return ID2SYM(rb_intern(algorithm_name));
}

/* Gets the readable name of the default algorithm if one has been set.
 * Returns nil if no algorithm has been set. */
VALUE grpc_rb_compression_options_get_default_algorithm(VALUE self) {
  grpc_compression_algorithm internal_value;
  grpc_rb_compression_options* wrapper = NULL;

  TypedData_Get_Struct(self, grpc_rb_compression_options,
                       &grpc_rb_compression_options_data_type, wrapper);

  if (wrapper->wrapped->default_algorithm.is_set) {
    internal_value = wrapper->wrapped->default_algorithm.algorithm;
    return grpc_rb_compression_options_algorithm_value_to_name_internal(
        internal_value);
  }

  return Qnil;
}

/* Gets the internal value of the default compression level that is to be passed
 * to the GRPC core as a channel argument value.
 * A nil return value means that it hasn't been set. */
VALUE grpc_rb_compression_options_get_default_level(VALUE self) {
  grpc_compression_level internal_value;
  grpc_rb_compression_options* wrapper = NULL;

  TypedData_Get_Struct(self, grpc_rb_compression_options,
                       &grpc_rb_compression_options_data_type, wrapper);

  if (wrapper->wrapped->default_level.is_set) {
    internal_value = wrapper->wrapped->default_level.level;
    return grpc_rb_compression_options_level_value_to_name_internal(
        internal_value);
  }

  return Qnil;
}

/* Gets a list of the disabled algorithms as readable names.
 * Returns an empty list if no algorithms have been disabled. */
VALUE grpc_rb_compression_options_get_disabled_algorithms(VALUE self) {
  VALUE disabled_algorithms = rb_ary_new();
  grpc_compression_algorithm internal_value;
  grpc_rb_compression_options* wrapper = NULL;

  TypedData_Get_Struct(self, grpc_rb_compression_options,
                       &grpc_rb_compression_options_data_type, wrapper);

  for (internal_value = GRPC_COMPRESS_NONE;
       internal_value < GRPC_COMPRESS_ALGORITHMS_COUNT; internal_value++) {
    if (!grpc_compression_options_is_algorithm_enabled(wrapper->wrapped,
                                                       internal_value)) {
      rb_ary_push(disabled_algorithms,
                  grpc_rb_compression_options_algorithm_value_to_name_internal(
                      internal_value));
    }
  }
  return disabled_algorithms;
}

/* Initializes the compression options wrapper.
 * Takes an optional hash parameter.
 *
 * Example call-seq:
 *   options = CompressionOptions.new(
 *     default_level: :none,
 *     disabled_algorithms: [:gzip]
 *   )
 *   channel_arg hash = Hash.new[...]
 *   channel_arg_hash_with_compression_options = channel_arg_hash.merge(options)
 */
VALUE grpc_rb_compression_options_init(int argc, VALUE* argv, VALUE self) {
  grpc_rb_compression_options* wrapper = NULL;
  VALUE default_algorithm = Qnil;
  VALUE default_level = Qnil;
  VALUE disabled_algorithms = Qnil;
  VALUE algorithm_name = Qnil;
  VALUE hash_arg = Qnil;

  rb_scan_args(argc, argv, "01", &hash_arg);

  /* Check if the hash parameter was passed, or if invalid arguments were
   * passed. */
  if (hash_arg == Qnil) {
    return self;
  } else if (TYPE(hash_arg) != T_HASH || argc > 1) {
    rb_raise(rb_eArgError,
             "Invalid arguments. Expecting optional hash parameter");
  }

  TypedData_Get_Struct(self, grpc_rb_compression_options,
                       &grpc_rb_compression_options_data_type, wrapper);

  /* Set the default algorithm if one was chosen. */
  default_algorithm =
      rb_hash_aref(hash_arg, ID2SYM(rb_intern("default_algorithm")));
  if (default_algorithm != Qnil) {
    grpc_rb_compression_options_set_default_algorithm(wrapper->wrapped,
                                                      default_algorithm);
  }

  /* Set the default level if one was chosen. */
  default_level = rb_hash_aref(hash_arg, ID2SYM(rb_intern("default_level")));
  if (default_level != Qnil) {
    grpc_rb_compression_options_set_default_level(wrapper->wrapped,
                                                  default_level);
  }

  /* Set the disabled algorithms if any were chosen. */
  disabled_algorithms =
      rb_hash_aref(hash_arg, ID2SYM(rb_intern("disabled_algorithms")));
  if (disabled_algorithms != Qnil) {
    Check_Type(disabled_algorithms, T_ARRAY);

    for (int i = 0; i < RARRAY_LEN(disabled_algorithms); i++) {
      algorithm_name = rb_ary_entry(disabled_algorithms, i);
      grpc_rb_compression_options_disable_algorithm(wrapper->wrapped,
                                                    algorithm_name);
    }
  }

  return self;
}

void Init_grpc_compression_options() {
  grpc_rb_cCompressionOptions = rb_define_class_under(
      grpc_rb_mGrpcCore, "CompressionOptions", rb_cObject);

  /* Allocates an object managed by the ruby runtime. */
  rb_define_alloc_func(grpc_rb_cCompressionOptions,
                       grpc_rb_compression_options_alloc);

  /* Initializes the ruby wrapper. #new method takes an optional hash argument.
   */
  rb_define_method(grpc_rb_cCompressionOptions, "initialize",
                   grpc_rb_compression_options_init, -1);

  /* Methods for getting the default algorithm, default level, and disabled
   * algorithms as readable names. */
  rb_define_method(grpc_rb_cCompressionOptions, "default_algorithm",
                   grpc_rb_compression_options_get_default_algorithm, 0);
  rb_define_method(grpc_rb_cCompressionOptions, "default_level",
                   grpc_rb_compression_options_get_default_level, 0);
  rb_define_method(grpc_rb_cCompressionOptions, "disabled_algorithms",
                   grpc_rb_compression_options_get_disabled_algorithms, 0);

  /* Determines whether or not an algorithm is enabled, given a readable
   * algorithm name.*/
  rb_define_method(grpc_rb_cCompressionOptions, "algorithm_enabled?",
                   grpc_rb_compression_options_is_algorithm_enabled, 1);

  /* Provides a hash of the compression settings suitable
   * for passing to server or channel args. */
  rb_define_method(grpc_rb_cCompressionOptions, "to_hash",
                   grpc_rb_compression_options_to_hash, 0);
  rb_define_alias(grpc_rb_cCompressionOptions, "to_channel_arg_hash",
                  "to_hash");

  /* Ruby ids for the names of the different compression levels. */
  id_compress_level_none = rb_intern("none");
  id_compress_level_low = rb_intern("low");
  id_compress_level_medium = rb_intern("medium");
  id_compress_level_high = rb_intern("high");
}
