/*
 *
 * 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_grpc.h"

#include <grpc/grpc.h>
#include <grpc/support/log.h>
#include <grpc/support/time.h>
#include <math.h>
#include <ruby/vm.h>
#include <stdbool.h>
#include <sys/types.h>
#include <unistd.h>

#include "rb_call.h"
#include "rb_call_credentials.h"
#include "rb_channel.h"
#include "rb_channel_credentials.h"
#include "rb_compression_options.h"
#include "rb_event_thread.h"
#include "rb_grpc_imports.generated.h"
#include "rb_loader.h"
#include "rb_server.h"
#include "rb_server_credentials.h"
#include "rb_xds_channel_credentials.h"
#include "rb_xds_server_credentials.h"

#ifdef GPR_LINUX
#include <sys/syscall.h>
#include <unistd.h>
#endif

static VALUE grpc_rb_cTimeVal = Qnil;

static rb_data_type_t grpc_rb_timespec_data_type = {
    "gpr_timespec",
    {GRPC_RB_GC_NOT_MARKED,
     GRPC_RB_GC_DONT_FREE,
     GRPC_RB_MEMSIZE_UNAVAILABLE,
     {NULL, NULL}},
    NULL,
    NULL,
#ifdef RUBY_TYPED_FREE_IMMEDIATELY
    RUBY_TYPED_FREE_IMMEDIATELY
#endif
};

/* Alloc func that blocks allocation of a given object by raising an
 * exception. */
VALUE grpc_rb_cannot_alloc(VALUE cls) {
  rb_raise(rb_eTypeError,
           "allocation of %s only allowed from the gRPC native layer",
           rb_class2name(cls));
  return Qnil;
}

/* Init func that fails by raising an exception. */
VALUE grpc_rb_cannot_init(VALUE self) {
  rb_raise(rb_eTypeError,
           "initialization of %s only allowed from the gRPC native layer",
           rb_obj_classname(self));
  return Qnil;
}

/* Init/Clone func that fails by raising an exception. */
VALUE grpc_rb_cannot_init_copy(VALUE copy, VALUE self) {
  (void)self;
  rb_raise(rb_eTypeError, "Copy initialization of %s is not supported",
           rb_obj_classname(copy));
  return Qnil;
}

/* id_tv_{,u}sec are accessor methods on Ruby Time instances. */
static ID id_tv_sec;
static ID id_tv_nsec;

/**
 * grpc_rb_time_timeval creates a timeval from a ruby time object.
 *
 * This func is copied from ruby source, MRI/source/time.c, which is published
 * under the same license as the ruby.h, on which the entire extensions is
 * based.
 */
gpr_timespec grpc_rb_time_timeval(VALUE time, int interval) {
  gpr_timespec t;
  gpr_timespec* time_const;
  const char* tstr = interval ? "time interval" : "time";
  const char* want = " want <secs from epoch>|<Time>|<GRPC::TimeConst.*>";

  t.clock_type = GPR_CLOCK_REALTIME;
  switch (TYPE(time)) {
    case T_DATA:
      if (CLASS_OF(time) == grpc_rb_cTimeVal) {
        TypedData_Get_Struct(time, gpr_timespec, &grpc_rb_timespec_data_type,
                             time_const);
        t = *time_const;
      } else if (CLASS_OF(time) == rb_cTime) {
        t.tv_sec = NUM2INT(rb_funcall(time, id_tv_sec, 0));
        t.tv_nsec = NUM2INT(rb_funcall(time, id_tv_nsec, 0));
      } else {
        rb_raise(rb_eTypeError, "bad input: (%s)->c_timeval, got <%s>,%s", tstr,
                 rb_obj_classname(time), want);
      }
      break;

    case T_FIXNUM:
      t.tv_sec = FIX2LONG(time);
      if (interval && t.tv_sec < 0)
        rb_raise(rb_eArgError, "%s must be positive", tstr);
      t.tv_nsec = 0;
      break;

    case T_FLOAT:
      if (interval && RFLOAT_VALUE(time) < 0.0)
        rb_raise(rb_eArgError, "%s must be positive", tstr);
      else {
        double f, d;

        d = modf(RFLOAT_VALUE(time), &f);
        if (d < 0) {
          d += 1;
          f -= 1;
        }
        t.tv_sec = (int64_t)f;
        if (f != t.tv_sec) {
          rb_raise(rb_eRangeError, "%f out of Time range", RFLOAT_VALUE(time));
        }
        t.tv_nsec = (int)(d * 1e9 + 0.5);
      }
      break;

    case T_BIGNUM:
      t.tv_sec = NUM2LONG(time);
      if (interval && t.tv_sec < 0)
        rb_raise(rb_eArgError, "%s must be positive", tstr);
      t.tv_nsec = 0;
      break;

    default:
      rb_raise(rb_eTypeError, "bad input: (%s)->c_timeval, got <%s>,%s", tstr,
               rb_obj_classname(time), want);
      break;
  }
  return t;
}

/* id_at is the constructor method of the ruby standard Time class. */
static ID id_at;

/* id_inspect is the inspect method found on various ruby objects. */
static ID id_inspect;

/* id_to_s is the to_s method found on various ruby objects. */
static ID id_to_s;

/* Converts a wrapped time constant to a standard time. */
static VALUE grpc_rb_time_val_to_time(VALUE self) {
  gpr_timespec* time_const = NULL;
  gpr_timespec real_time;
  TypedData_Get_Struct(self, gpr_timespec, &grpc_rb_timespec_data_type,
                       time_const);
  real_time = gpr_convert_clock_type(*time_const, GPR_CLOCK_REALTIME);
  return rb_funcall(rb_cTime, id_at, 2, INT2NUM(real_time.tv_sec),
                    INT2NUM(real_time.tv_nsec / 1000));
}

/* Invokes inspect on the ctime version of the time val. */
static VALUE grpc_rb_time_val_inspect(VALUE self) {
  return rb_funcall(grpc_rb_time_val_to_time(self), id_inspect, 0);
}

/* Invokes to_s on the ctime version of the time val. */
static VALUE grpc_rb_time_val_to_s(VALUE self) {
  return rb_funcall(grpc_rb_time_val_to_time(self), id_to_s, 0);
}

static gpr_timespec zero_realtime;
static gpr_timespec inf_future_realtime;
static gpr_timespec inf_past_realtime;

/* Adds a module with constants that map to gpr's static timeval structs. */
static void Init_grpc_time_consts() {
  VALUE grpc_rb_mTimeConsts =
      rb_define_module_under(grpc_rb_mGrpcCore, "TimeConsts");
  grpc_rb_cTimeVal =
      rb_define_class_under(grpc_rb_mGrpcCore, "TimeSpec", rb_cObject);
  rb_undef_alloc_func(grpc_rb_cTimeVal);
  zero_realtime = gpr_time_0(GPR_CLOCK_REALTIME);
  inf_future_realtime = gpr_inf_future(GPR_CLOCK_REALTIME);
  inf_past_realtime = gpr_inf_past(GPR_CLOCK_REALTIME);
  rb_define_const(
      grpc_rb_mTimeConsts, "ZERO",
      TypedData_Wrap_Struct(grpc_rb_cTimeVal, &grpc_rb_timespec_data_type,
                            (void*)&zero_realtime));
  rb_define_const(
      grpc_rb_mTimeConsts, "INFINITE_FUTURE",
      TypedData_Wrap_Struct(grpc_rb_cTimeVal, &grpc_rb_timespec_data_type,
                            (void*)&inf_future_realtime));
  rb_define_const(
      grpc_rb_mTimeConsts, "INFINITE_PAST",
      TypedData_Wrap_Struct(grpc_rb_cTimeVal, &grpc_rb_timespec_data_type,
                            (void*)&inf_past_realtime));
  rb_define_method(grpc_rb_cTimeVal, "to_time", grpc_rb_time_val_to_time, 0);
  rb_define_method(grpc_rb_cTimeVal, "inspect", grpc_rb_time_val_inspect, 0);
  rb_define_method(grpc_rb_cTimeVal, "to_s", grpc_rb_time_val_to_s, 0);
  id_at = rb_intern("at");
  id_inspect = rb_intern("inspect");
  id_to_s = rb_intern("to_s");
  id_tv_sec = rb_intern("tv_sec");
  id_tv_nsec = rb_intern("tv_nsec");
}

static bool g_enable_fork_support;

#ifdef GPR_LINUX
static long sys_gettid() { return syscall(__NR_gettid); }
static bool can_enable_fork_support() { return true; }
#else
static long sys_gettid() { return 0; }
static bool can_enable_fork_support() { return false; }
#endif

#if GPR_WINDOWS
static void grpc_ruby_basic_init(void) {}
static bool grpc_ruby_initial_pid(void) { return true; }
static bool grpc_ruby_initial_thread(void) { return true; }
static void grpc_ruby_reset_init_state(void) {}
#else
static pid_t g_init_pid;
static long g_init_tid;

static bool grpc_ruby_initial_pid(void) {
  GRPC_RUBY_ASSERT(g_init_pid != 0);
  return g_init_pid == getpid();
}

static bool grpc_ruby_initial_thread(void) {
  GRPC_RUBY_ASSERT(g_init_tid != 0);
  return sys_gettid() == g_init_tid;
}

static void grpc_ruby_reset_init_state(void) {
  g_init_pid = getpid();
  g_init_tid = sys_gettid();
}

static void grpc_ruby_basic_init(void) {
  GRPC_RUBY_ASSERT(g_init_pid == 0);
  GRPC_RUBY_ASSERT(g_init_tid == 0);
  grpc_ruby_reset_init_state();
  // TODO(apolcyn): ideally, we should share logic with C-core
  // for determining whether or not fork support is enabled, rather
  // than parsing the environment variable ourselves.
  const char* res = getenv("GRPC_ENABLE_FORK_SUPPORT");
  if (res != NULL && strcmp(res, "1") == 0) {
    g_enable_fork_support = can_enable_fork_support();
  }
}
#endif

/* Initialize the GRPC module structs */

/* grpc_rb_sNewServerRpc is the struct that holds new server rpc details. */
VALUE grpc_rb_sNewServerRpc = Qnil;
/* grpc_rb_sStatus is the struct that holds status details. */
VALUE grpc_rb_sStatus = Qnil;

/* Initialize the GRPC module. */
VALUE grpc_rb_mGRPC = Qnil;
VALUE grpc_rb_mGrpcCore = Qnil;

/* cached Symbols for members in Status struct */
VALUE sym_code = Qundef;
VALUE sym_details = Qundef;
VALUE sym_metadata = Qundef;

static gpr_once g_once_init = GPR_ONCE_INIT;
static int64_t g_grpc_rb_prefork_pending;          // synchronized by the GIL
static int64_t g_grpc_rb_num_fork_unsafe_threads;  // synchronized by the GIL

void grpc_ruby_fork_guard() {
  // Check if we're using gRPC between prefork and postfork
  gpr_once_init(&g_once_init, grpc_ruby_basic_init);
  if (g_grpc_rb_prefork_pending) {
    rb_raise(rb_eRuntimeError,
             "grpc cannot be used between calls to GRPC.prefork and "
             "GRPC.postfork_child or GRPC.postfork_parent");
  }
  if (!grpc_ruby_initial_pid()) {
    if (g_enable_fork_support) {
      // Only way we can get here is by enabling for support and forking but not
      // calling prefork
      rb_raise(rb_eRuntimeError,
               "grpc is in a broken state: GRPC.prefork must be called before "
               "calling fork from a process using grpc");
    } else {
      rb_raise(rb_eRuntimeError,
               "grpc cannot be used before and after forking unless the "
               "GRPC_ENABLE_FORK_SUPPORT env var is set to \"1\" and the "
               "platform supports it (linux only)");
    }
  }
}

static VALUE g_bg_thread_init_rb_mu = Qundef;
static bool g_bg_thread_init_done;

static void grpc_ruby_init_threads() {
  // Avoid calling into ruby library (when creating threads here)
  // in gpr_once_init. In general, it appears to be unsafe to call
  // into the ruby library while holding a non-ruby mutex, because a gil yield
  // could end up trying to lock onto that same mutex and deadlocking.
  grpc_absl_log_int(GPR_DEBUG,
                    "GRPC_RUBY: grpc_ruby_init_threads g_bg_thread_init_done=",
                    g_bg_thread_init_done);
  rb_mutex_lock(g_bg_thread_init_rb_mu);
  if (!g_bg_thread_init_done) {
    grpc_rb_event_queue_thread_start();
    g_bg_thread_init_done = true;
  }
  rb_mutex_unlock(g_bg_thread_init_rb_mu);
}

static int64_t g_grpc_ruby_init_count;

void grpc_ruby_init() {
  gpr_once_init(&g_once_init, grpc_ruby_basic_init);
  grpc_ruby_fork_guard();
  grpc_init();
  grpc_ruby_init_threads();
  // (only log after logging has been initialized)
  grpc_absl_log_int(GPR_DEBUG,
                    "GRPC_RUBY: grpc_ruby_init - g_enable_fork_support=",
                    g_enable_fork_support);
  grpc_absl_log_int(GPR_DEBUG,
                    "prev g_grpc_ruby_init_count:", g_grpc_ruby_init_count++);
}

// fork APIs, useable on linux with env var: GRPC_ENABLE_FORK_SUPPORT=1
//
// Must be called once and only once before forking. Must be called on the
// same threads that gRPC was (lazy-)initialized on. One must not call
// into the gRPC library during or after prefork has been called, until
// the corresponding postfork_{parent,child} APIs have been called.
static VALUE grpc_rb_prefork(VALUE self) {
  // This might be the first time we've called into the grpc library, so make
  // sure basic one-time initialization is taken care of. Note that if this is
  // the case, then grpc_init() will start up c-core threads; that's OK since
  // they will be shut down in C-core's pthread_atfork handler.
  gpr_once_init(&g_once_init, grpc_ruby_basic_init);
  grpc_init();
  if (!g_enable_fork_support) {
    rb_raise(rb_eRuntimeError,
             "forking with gRPC/Ruby is only supported on linux with env var: "
             "GRPC_ENABLE_FORK_SUPPORT=1");
  }
  if (g_grpc_rb_prefork_pending) {
    rb_raise(rb_eRuntimeError,
             "GRPC.prefork already called without a matching "
             "GRPC.postfork_{parent,child}");
  }
  if (!grpc_ruby_initial_thread()) {
    rb_raise(rb_eRuntimeError,
             "GRPC.prefork and fork need to be called from the same thread "
             "that GRPC was initialized on (GRPC lazy-initializes when when "
             "the first GRPC object is created)");
  }
  if (g_grpc_rb_num_fork_unsafe_threads > 0) {
    rb_raise(
        rb_eRuntimeError,
        "Detected at least %ld threads actively using grpc, so it is not safe "
        "call GRPC.prefork or fork. Note that grpc-ruby servers and "
        "bidirectional "
        "streams manage background threads and are not fork safe.",
        g_grpc_rb_num_fork_unsafe_threads);
  }
  g_grpc_rb_prefork_pending = true;
  rb_mutex_lock(g_bg_thread_init_rb_mu);
  if (g_bg_thread_init_done) {
    grpc_rb_event_queue_thread_stop();
    // all ruby-level background threads joined at this point
    g_bg_thread_init_done = false;
  }
  rb_mutex_unlock(g_bg_thread_init_rb_mu);
  return Qnil;
}

static VALUE grpc_rb_postfork_child(VALUE self) {
  if (!g_grpc_rb_prefork_pending) {
    rb_raise(rb_eRuntimeError,
             "GRPC::postfork_child can only be called once following a "
             "GRPC::prefork");
  }
  if (grpc_ruby_initial_pid()) {
    rb_raise(rb_eRuntimeError,
             "GRPC.postfork_child must be called only from the child process "
             "after a fork");
  }
  grpc_ruby_reset_init_state();
  grpc_ruby_init_threads();
  g_grpc_rb_prefork_pending = false;
  return Qnil;
}

static VALUE grpc_rb_postfork_parent(VALUE self) {
  // TODO(apolcyn): check calling thread vs. thread that gRPC was initialized on
  if (!g_grpc_rb_prefork_pending) {
    rb_raise(rb_eRuntimeError,
             "GRPC::postfork_parent can only be called once following a "
             "GRPC::prefork");
  }
  if (!grpc_ruby_initial_pid()) {
    rb_raise(rb_eRuntimeError,
             "GRPC.postfork_parent must be called only from the parent process "
             "after a fork");
  }
  if (!grpc_ruby_initial_thread()) {
    rb_raise(rb_eRuntimeError,
             "GRPC.postfork_parent needs to be called from the same thread "
             "that GRPC.prefork (and fork) was called from");
  }
  grpc_ruby_init_threads();
  g_grpc_rb_prefork_pending = false;
  return Qnil;
}

// APIs to mark fork-unsafe sections from C-extension code
void grpc_rb_fork_unsafe_begin() { g_grpc_rb_num_fork_unsafe_threads++; }

void grpc_rb_fork_unsafe_end() { g_grpc_rb_num_fork_unsafe_threads--; }

// APIs to mark fork-unsafe sections from ruby code
static VALUE grpc_rb_fork_unsafe_begin_api() {
  grpc_rb_fork_unsafe_begin();
  return Qnil;
}

static VALUE grpc_rb_fork_unsafe_end_api() {
  grpc_rb_fork_unsafe_end();
  return Qnil;
}

// One-time initialization
void Init_grpc_c() {
  if (!grpc_rb_load_core()) {
    rb_raise(rb_eLoadError, "Couldn't find or load gRPC's dynamic C core");
    return;
  }

  rb_global_variable(&g_bg_thread_init_rb_mu);
  g_bg_thread_init_rb_mu = rb_mutex_new();

  grpc_rb_mGRPC = rb_define_module("GRPC");
  grpc_rb_mGrpcCore = rb_define_module_under(grpc_rb_mGRPC, "Core");
  grpc_rb_sNewServerRpc = rb_struct_define(
      "NewServerRpc", "method", "host", "deadline", "metadata", "call", NULL);
  rb_global_variable(&grpc_rb_sStatus);
  grpc_rb_sStatus = rb_const_get(rb_cStruct, rb_intern("Status"));
  sym_code = ID2SYM(rb_intern("code"));
  sym_details = ID2SYM(rb_intern("details"));
  sym_metadata = ID2SYM(rb_intern("metadata"));
  // init C-defined classes
  Init_grpc_channel();
  Init_grpc_call();
  Init_grpc_call_credentials();
  Init_grpc_channel_credentials();
  Init_grpc_xds_channel_credentials();
  Init_grpc_server();
  Init_grpc_server_credentials();
  Init_grpc_xds_server_credentials();
  Init_grpc_time_consts();
  Init_grpc_compression_options();
  // define fork APIs
  rb_define_module_function(grpc_rb_mGRPC, "prefork", grpc_rb_prefork, 0);
  rb_define_module_function(grpc_rb_mGRPC, "postfork_child",
                            grpc_rb_postfork_child, 0);
  rb_define_module_function(grpc_rb_mGRPC, "postfork_parent",
                            grpc_rb_postfork_parent, 0);
  rb_define_module_function(grpc_rb_mGrpcCore, "fork_unsafe_begin",
                            grpc_rb_fork_unsafe_begin_api, 0);
  rb_define_module_function(grpc_rb_mGrpcCore, "fork_unsafe_end",
                            grpc_rb_fork_unsafe_end_api, 0);
}
