#include "call.h"

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "php.h"
#include "php_ini.h"
#include "ext/standard/info.h"
#include "ext/spl/spl_exceptions.h"
#include "php_grpc.h"

#include "zend_exceptions.h"
#include "zend_hash.h"

#include <stdbool.h>

#include "grpc/support/log.h"
#include "grpc/grpc.h"

#include "timeval.h"
#include "channel.h"
#include "completion_queue.h"
#include "byte_buffer.h"

/* Frees and destroys an instance of wrapped_grpc_call */
void free_wrapped_grpc_call(void *object TSRMLS_DC) {
  wrapped_grpc_call *call = (wrapped_grpc_call *)object;
  if (call->owned && call->wrapped != NULL) {
    grpc_call_destroy(call->wrapped);
  }
  efree(call);
}

/* Initializes an instance of wrapped_grpc_call to be associated with an object
 * of a class specified by class_type */
zend_object_value create_wrapped_grpc_call(zend_class_entry *class_type
                                               TSRMLS_DC) {
  zend_object_value retval;
  wrapped_grpc_call *intern;

  intern = (wrapped_grpc_call *)emalloc(sizeof(wrapped_grpc_call));
  memset(intern, 0, sizeof(wrapped_grpc_call));

  zend_object_std_init(&intern->std, class_type TSRMLS_CC);
  object_properties_init(&intern->std, class_type);
  retval.handle = zend_objects_store_put(
      intern, (zend_objects_store_dtor_t)zend_objects_destroy_object,
      free_wrapped_grpc_call, NULL TSRMLS_CC);
  retval.handlers = zend_get_std_object_handlers();
  return retval;
}

/* Wraps a grpc_call struct in a PHP object. Owned indicates whether the struct
   should be destroyed at the end of the object's lifecycle */
zval *grpc_php_wrap_call(grpc_call *wrapped, bool owned) {
  zval *call_object;
  MAKE_STD_ZVAL(call_object);
  object_init_ex(call_object, grpc_ce_call);
  wrapped_grpc_call *call =
      (wrapped_grpc_call *)zend_object_store_get_object(call_object TSRMLS_CC);
  call->wrapped = wrapped;
  return call_object;
}

zval *grpc_call_create_metadata_array(int count, grpc_metadata *elements) {
  int i;
  zval *array;
  zval **data = NULL;
  HashTable *array_hash;
  zval *inner_array;
  char *str_key;
  char *str_val;
  size_t key_len;
  MAKE_STD_ZVAL(array);
  array_init(array);
  array_hash = Z_ARRVAL_P(array);
  grpc_metadata *elem;
  for (i = 0; i < count; i++) {
    elem = &elements[i];
    key_len = strlen(elem->key);
    str_key = ecalloc(key_len + 1, sizeof(char));
    memcpy(str_key, elem->key, key_len);
    str_val = ecalloc(elem->value_length + 1, sizeof(char));
    memcpy(str_val, elem->value, elem->value_length);
    if (zend_hash_find(array_hash, str_key, key_len, (void **)data) ==
        SUCCESS) {
      if (Z_TYPE_P(*data) != IS_ARRAY) {
        zend_throw_exception(zend_exception_get_default(),
                             "Metadata hash somehow contains wrong types.",
                             1 TSRMLS_CC);
          efree(str_key);
          efree(str_val);
          return NULL;
      }
      add_next_index_stringl(*data, str_val, elem->value_length, false);
    } else {
      MAKE_STD_ZVAL(inner_array);
      array_init(inner_array);
      add_next_index_stringl(inner_array, str_val, elem->value_length, false);
      add_assoc_zval(array, str_key, inner_array);
    }
  }
  return array;
}

/**
 * Constructs a new instance of the Call class.
 * @param Channel $channel The channel to associate the call with. Must not be
 *     closed.
 * @param string $method The method to call
 * @param Timeval $absolute_deadline The deadline for completing the call
 */
PHP_METHOD(Call, __construct) {
  wrapped_grpc_call *call =
      (wrapped_grpc_call *)zend_object_store_get_object(getThis() TSRMLS_CC);
  zval *channel_obj;
  char *method;
  int method_len;
  zval *deadline_obj;
  /* "OsO" == 1 Object, 1 string, 1 Object */
  if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "OsO", &channel_obj,
                            grpc_ce_channel, &method, &method_len,
                            &deadline_obj, grpc_ce_timeval) == FAILURE) {
    zend_throw_exception(spl_ce_InvalidArgumentException,
                         "Call expects a Channel, a String, and a Timeval",
                         1 TSRMLS_CC);
    return;
  }
  wrapped_grpc_channel *channel =
      (wrapped_grpc_channel *)zend_object_store_get_object(
          channel_obj TSRMLS_CC);
  if (channel->wrapped == NULL) {
    zend_throw_exception(spl_ce_InvalidArgumentException,
                         "Call cannot be constructed from a closed Channel",
                         1 TSRMLS_CC);
    return;
  }
  add_property_zval(getThis(), "channel", channel_obj);
  wrapped_grpc_timeval *deadline =
      (wrapped_grpc_timeval *)zend_object_store_get_object(
          deadline_obj TSRMLS_CC);
  call->wrapped = grpc_channel_create_call_old(
      channel->wrapped, method, channel->target, deadline->wrapped);
}

/**
 * Add metadata to the call. All array keys must be strings. If the value is a
 * string, it is added as a key/value pair. If it is an array, each value is
 * added paired with the same string
 * @param array $metadata The metadata to add
 * @param long $flags A bitwise combination of the Grpc\WRITE_* constants
 * (optional)
 * @return Void
 */
PHP_METHOD(Call, add_metadata) {
  wrapped_grpc_call *call =
      (wrapped_grpc_call *)zend_object_store_get_object(getThis() TSRMLS_CC);
  grpc_metadata metadata;
  grpc_call_error error_code;
  zval *array;
  zval **inner_array;
  zval **value;
  HashTable *array_hash;
  HashPosition array_pointer;
  HashTable *inner_array_hash;
  HashPosition inner_array_pointer;
  char *key;
  uint key_len;
  ulong index;
  long flags = 0;
  /* "a|l" == 1 array, 1 optional long */
  if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "a|l", &array, &flags) ==
      FAILURE) {
    zend_throw_exception(spl_ce_InvalidArgumentException,
                         "add_metadata expects an array and an optional long",
                         1 TSRMLS_CC);
    return;
  }
  array_hash = Z_ARRVAL_P(array);
  for (zend_hash_internal_pointer_reset_ex(array_hash, &array_pointer);
       zend_hash_get_current_data_ex(array_hash, (void**)&inner_array,
                                     &array_pointer) == SUCCESS;
       zend_hash_move_forward_ex(array_hash, &array_pointer)) {
    if (zend_hash_get_current_key_ex(array_hash, &key, &key_len, &index, 0,
                                     &array_pointer) != HASH_KEY_IS_STRING) {
      zend_throw_exception(spl_ce_InvalidArgumentException,
                           "metadata keys must be strings", 1 TSRMLS_CC);
      return;
    }
    if (Z_TYPE_P(*inner_array) != IS_ARRAY) {
      zend_throw_exception(spl_ce_InvalidArgumentException,
                           "metadata values must be arrays",
                           1 TSRMLS_CC);
      return;
    }
    inner_array_hash = Z_ARRVAL_P(*inner_array);
    for (zend_hash_internal_pointer_reset_ex(inner_array_hash,
                                             &inner_array_pointer);
         zend_hash_get_current_data_ex(inner_array_hash, (void**)&value,
                                       &inner_array_pointer) == SUCCESS;
         zend_hash_move_forward_ex(inner_array_hash, &inner_array_pointer)) {
      if (Z_TYPE_P(*value) != IS_STRING) {
        zend_throw_exception(spl_ce_InvalidArgumentException,
                             "metadata values must be arrays of strings",
                             1 TSRMLS_CC);
        return;
      }
      metadata.key = key;
      metadata.value = Z_STRVAL_P(*value);
      metadata.value_length = Z_STRLEN_P(*value);
      error_code = grpc_call_add_metadata_old(call->wrapped, &metadata, 0u);
      MAYBE_THROW_CALL_ERROR(add_metadata, error_code);
    }
  }
}

/**
 * Invoke the RPC. Starts sending metadata and request headers over the wire
 * @param CompletionQueue $queue The completion queue to use with this call
 * @param long $metadata_tag The tag to associate with returned metadata
 * @param long $finished_tag The tag to associate with the finished event
 * @param long $flags A bitwise combination of the Grpc\WRITE_* constants
 * (optional)
 * @return Void
 */
PHP_METHOD(Call, invoke) {
  grpc_call_error error_code;
  long tag1;
  long tag2;
  zval *queue_obj;
  long flags = 0;
  /* "Oll|l" == 1 Object, 3 mandatory longs, 1 optional long */
  if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "Oll|l", &queue_obj,
                            grpc_ce_completion_queue, &tag1, &tag2,
                            &flags) == FAILURE) {
    zend_throw_exception(
        spl_ce_InvalidArgumentException,
        "invoke needs a CompletionQueue, 2 longs, and an optional long",
        1 TSRMLS_CC);
    return;
  }
  add_property_zval(getThis(), "completion_queue", queue_obj);
  wrapped_grpc_call *call =
      (wrapped_grpc_call *)zend_object_store_get_object(getThis() TSRMLS_CC);
  wrapped_grpc_completion_queue *queue =
      (wrapped_grpc_completion_queue *)zend_object_store_get_object(
          queue_obj TSRMLS_CC);
  error_code = grpc_call_invoke_old(call->wrapped, queue->wrapped, (void *)tag1,
                                    (void *)tag2, (gpr_uint32)flags);
  MAYBE_THROW_CALL_ERROR(invoke, error_code);
}

/**
 * Accept an incoming RPC, binding a completion queue to it. To be called after
 * adding metadata to the call, but before sending messages. Can only be called
 * on the server
 * @param CompletionQueue $queue The completion queue to use with this call
 * @param long $finished_tag The tag to associate with the finished event
 * @param long $flags A bitwise combination of the Grpc\WRITE_* constants
 * (optional)
 * @return Void
 */
PHP_METHOD(Call, server_accept) {
  long tag;
  zval *queue_obj;
  grpc_call_error error_code;
  /* "Ol|l" == 1 Object, 1 long */
  if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "Ol", &queue_obj,
                            grpc_ce_completion_queue, &tag) == FAILURE) {
    zend_throw_exception(
        spl_ce_InvalidArgumentException,
        "server_accept expects a CompletionQueue, a long, and an optional long",
        1 TSRMLS_CC);
    return;
  }
  add_property_zval(getThis(), "completion_queue", queue_obj);
  wrapped_grpc_call *call =
      (wrapped_grpc_call *)zend_object_store_get_object(getThis() TSRMLS_CC);
  wrapped_grpc_completion_queue *queue =
      (wrapped_grpc_completion_queue *)zend_object_store_get_object(
          queue_obj TSRMLS_CC);
  error_code =
      grpc_call_server_accept_old(call->wrapped, queue->wrapped, (void *)tag);
  MAYBE_THROW_CALL_ERROR(server_accept, error_code);
}

PHP_METHOD(Call, server_end_initial_metadata) {
  grpc_call_error error_code;
  long flags = 0;
  /* "|l" == 1 optional long */
  if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "|l", &flags) ==
      FAILURE) {
    zend_throw_exception(spl_ce_InvalidArgumentException,
                         "server_end_initial_metadata expects an optional long",
                         1 TSRMLS_CC);
  }
  wrapped_grpc_call *call =
      (wrapped_grpc_call *)zend_object_store_get_object(getThis() TSRMLS_CC);
  error_code = grpc_call_server_end_initial_metadata_old(call->wrapped, flags);
  MAYBE_THROW_CALL_ERROR(server_end_initial_metadata, error_code);
}

/**
 * Called by clients to cancel an RPC on the server.
 * @return Void
 */
PHP_METHOD(Call, cancel) {
  wrapped_grpc_call *call =
      (wrapped_grpc_call *)zend_object_store_get_object(getThis() TSRMLS_CC);
  grpc_call_error error_code = grpc_call_cancel(call->wrapped);
  MAYBE_THROW_CALL_ERROR(cancel, error_code);
}

/**
 * Queue a byte buffer for writing
 * @param string $buffer The buffer to queue for writing
 * @param long $tag The tag to associate with this write
 * @param long $flags A bitwise combination of the Grpc\WRITE_* constants
 * (optional)
 * @return Void
 */
PHP_METHOD(Call, start_write) {
  grpc_call_error error_code;
  wrapped_grpc_call *call =
      (wrapped_grpc_call *)zend_object_store_get_object(getThis() TSRMLS_CC);
  char *buffer;
  int buffer_len;
  long tag;
  long flags = 0;
  /* "Ol|l" == 1 Object, 1 mandatory long, 1 optional long */
  if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "sl|l", &buffer,
                            &buffer_len, &tag, &flags) == FAILURE) {
    zend_throw_exception(spl_ce_InvalidArgumentException,
                         "start_write expects a string and an optional long",
                         1 TSRMLS_CC);
    return;
  }
  error_code = grpc_call_start_write_old(
      call->wrapped, string_to_byte_buffer(buffer, buffer_len), (void *)tag,
      (gpr_uint32)flags);
  MAYBE_THROW_CALL_ERROR(start_write, error_code);
}

/**
 * Queue a status for writing
 * @param long $status_code The status code to send
 * @param string $status_details The status details to send
 * @param long $tag The tag to associate with this status
 * @return Void
 */
PHP_METHOD(Call, start_write_status) {
  grpc_call_error error_code;
  wrapped_grpc_call *call =
      (wrapped_grpc_call *)zend_object_store_get_object(getThis() TSRMLS_CC);
  long status_code;
  int status_details_length;
  long tag;
  char *status_details;
  /* "lsl" == 1 long, 1 string, 1 long */
  if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "lsl", &status_code,
                            &status_details, &status_details_length,
                            &tag) == FAILURE) {
    zend_throw_exception(
        spl_ce_InvalidArgumentException,
        "start_write_status expects a long, a string, and a long", 1 TSRMLS_CC);
    return;
  }
  error_code = grpc_call_start_write_status_old(call->wrapped,
                                                (grpc_status_code)status_code,
                                                status_details, (void *)tag);
  MAYBE_THROW_CALL_ERROR(start_write_status, error_code);
}

/**
 * Indicate that there are no more messages to send
 * @return Void
 */
PHP_METHOD(Call, writes_done) {
  grpc_call_error error_code;
  wrapped_grpc_call *call =
      (wrapped_grpc_call *)zend_object_store_get_object(getThis() TSRMLS_CC);
  long tag;
  /* "l" == 1 long */
  if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "l", &tag) == FAILURE) {
    zend_throw_exception(spl_ce_InvalidArgumentException,
                         "writes_done expects a long", 1 TSRMLS_CC);
    return;
  }
  error_code = grpc_call_writes_done_old(call->wrapped, (void *)tag);
  MAYBE_THROW_CALL_ERROR(writes_done, error_code);
}

/**
 * Initiate a read on a call. Output event contains a byte buffer with the
 * result of the read
 * @param long $tag The tag to associate with this read
 * @return Void
 */
PHP_METHOD(Call, start_read) {
  grpc_call_error error_code;
  wrapped_grpc_call *call =
      (wrapped_grpc_call *)zend_object_store_get_object(getThis() TSRMLS_CC);
  long tag;
  /* "l" == 1 long */
  if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "l", &tag) == FAILURE) {
    zend_throw_exception(spl_ce_InvalidArgumentException,
                         "start_read expects a long", 1 TSRMLS_CC);
    return;
  }
  error_code = grpc_call_start_read_old(call->wrapped, (void *)tag);
  MAYBE_THROW_CALL_ERROR(start_read, error_code);
}

static zend_function_entry call_methods[] = {
    PHP_ME(Call, __construct, NULL, ZEND_ACC_PUBLIC | ZEND_ACC_CTOR)
    PHP_ME(Call, server_accept, NULL, ZEND_ACC_PUBLIC)
    PHP_ME(Call, server_end_initial_metadata, NULL, ZEND_ACC_PUBLIC)
    PHP_ME(Call, add_metadata, NULL, ZEND_ACC_PUBLIC)
    PHP_ME(Call, cancel, NULL, ZEND_ACC_PUBLIC)
    PHP_ME(Call, invoke, NULL, ZEND_ACC_PUBLIC)
    PHP_ME(Call, start_read, NULL, ZEND_ACC_PUBLIC)
    PHP_ME(Call, start_write, NULL, ZEND_ACC_PUBLIC)
    PHP_ME(Call, start_write_status, NULL, ZEND_ACC_PUBLIC)
    PHP_ME(Call, writes_done, NULL, ZEND_ACC_PUBLIC) PHP_FE_END};

void grpc_init_call(TSRMLS_D) {
  zend_class_entry ce;
  INIT_CLASS_ENTRY(ce, "Grpc\\Call", call_methods);
  ce.create_object = create_wrapped_grpc_call;
  grpc_ce_call = zend_register_internal_class(&ce TSRMLS_CC);
}
