/* curb_easy.c - Curl easy mode
 * Copyright (c)2006 Ross Bamford.
 * Licensed under the Ruby License. See LICENSE for details.
 *
 * $Id: curb_easy.c 30 2006-12-09 12:30:24Z roscopeco $
 */
#include "curb_easy.h"
#include "curb_errors.h"
#include "curb_postfield.h"
#include "curb_upload.h"
#include "curb_multi.h"

#include <errno.h>
#include <string.h>
#ifndef _WIN32
#include <strings.h>
#endif

extern VALUE mCurl;

static VALUE idCall;
static VALUE idJoin;
static VALUE rbstrAmp;

#ifdef RDOC_NEVER_DEFINED
  mCurl = rb_define_module("Curl");
#endif

VALUE cCurlEasy;

/* Internal wrapper type for passing pointers through rb_iterate callbacks.
 * No mark/free needed - these are temporary wrappers that don't own memory. */
static const rb_data_type_t curl_slist_ptr_type = {
  "curl_slist_ptr_wrapper",
  { NULL, NULL, NULL },
  NULL, NULL, 0
};

// for Ruby 1.8
#ifndef HAVE_RB_IO_STDIO_FILE
static FILE * rb_io_stdio_file(rb_io_t *fptr) {
  return fptr->f;
}
#endif
static struct curl_slist *duplicate_curl_slist(struct curl_slist *list);
static size_t proc_data_handler(char *stream, size_t size, size_t nmemb, VALUE proc);

/* ================== CURL HANDLER FUNCS ==============*/

static VALUE callback_exception(VALUE unused, VALUE exception) {
  return Qfalse;
}

static VALUE callback_exception_store_on_easy(VALUE arg, VALUE exception) {
  ruby_curl_easy *rbce = (ruby_curl_easy *)arg;

  if (rbce && NIL_P(rbce->callback_error)) {
    rbce->callback_error = exception;
  }

  return Qfalse;
}

VALUE rb_curl_easy_take_callback_error(ruby_curl_easy *rbce) {
  VALUE exception = Qnil;

  if (!rbce) {
    return Qnil;
  }

  exception = rbce->callback_error;
  rbce->callback_error = Qnil;
  return exception;
}

static VALUE ruby_curl_easy_take_callback_error(VALUE self) {
  ruby_curl_easy *rbce = NULL;

  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
  return rb_curl_easy_take_callback_error(rbce);
}

static VALUE ensure_clear_easy_callback_active(VALUE arg) {
  ruby_curl_easy *rbce = (ruby_curl_easy *)arg;
  if (rbce) {
    rbce->callback_active = 0;
  }
  return Qnil;
}

static VALUE with_easy_callback_active(ruby_curl_easy *rbce, VALUE (*func)(VALUE), VALUE arg) {
  rbce->callback_active = 1;
  return rb_ensure(func, arg, ensure_clear_easy_callback_active, (VALUE)rbce);
}

struct stream_read_call_args {
  VALUE stream;
  size_t read_bytes;
};

static VALUE call_stream_read(VALUE argp) {
  struct stream_read_call_args *args = (struct stream_read_call_args *)argp;
  return rb_funcall(args->stream, rb_intern("read"), 1, ULONG2NUM((unsigned long)args->read_bytes));
}

static VALUE call_stream_to_s(VALUE stream) {
  return rb_funcall(stream, rb_intern("to_s"), 0);
}

struct stream_seek_call_args {
  VALUE stream;
  curl_off_t offset;
  int origin;
};

static VALUE call_stream_seek(VALUE argp) {
  struct stream_seek_call_args *args = (struct stream_seek_call_args *)argp;
  return rb_funcall(args->stream, rb_intern("seek"), 2, LL2NUM(args->offset), INT2NUM(args->origin));
}

struct proc_data_call_args {
  char *stream;
  size_t size;
  size_t nmemb;
  VALUE proc;
};

static VALUE call_proc_data_handler_wrapped(VALUE argp) {
  struct proc_data_call_args *args = (struct proc_data_call_args *)argp;
  return ULONG2NUM((unsigned long)proc_data_handler(args->stream, args->size, args->nmemb, args->proc));
}

struct easy_callback_dispatch_args {
  ruby_curl_easy *rbce;
  VALUE (*func)(VALUE);
  VALUE arg;
};

static VALUE call_with_easy_callback_active(VALUE argp) {
  struct easy_callback_dispatch_args *args = (struct easy_callback_dispatch_args *)argp;
  return with_easy_callback_active(args->rbce, args->func, args->arg);
}

/* Default body handler appends to easy.body_data buffer */
static size_t default_body_handler(char *stream,
                                   size_t size,
                                   size_t nmemb,
                                   void *userdata) {
  ruby_curl_easy *rbce = (ruby_curl_easy *)userdata;
  size_t total = size * nmemb;
  VALUE out = rb_easy_get("body_data");
  if (NIL_P(out)) {
    out = rb_easy_set("body_data", rb_str_buf_new(32768));
  }
  rb_str_buf_cat(out, stream, total);
  return total;
}

/* Default header handler appends to easy.header_data buffer */
static size_t default_header_handler(char *stream,
                                     size_t size,
                                     size_t nmemb,
                                     void *userdata) {
  ruby_curl_easy *rbce = (ruby_curl_easy *)userdata;
  size_t total = size * nmemb;
  VALUE out = rb_easy_get("header_data");
  if (NIL_P(out)) {
    out = rb_easy_set("header_data", rb_str_buf_new(16384));
  }
  rb_str_buf_cat(out, stream, total);
  return total;
}

// size_t function( void *ptr, size_t size, size_t nmemb, void *stream);
static size_t read_data_handler(void *ptr,
                                size_t size,
                                size_t nmemb,
                                ruby_curl_easy *rbce) {
  VALUE upload = rb_easy_get("upload");
  size_t read_bytes = (size*nmemb);
  VALUE stream = ruby_curl_upload_stream_get(upload);

  if (rb_respond_to(stream, rb_intern("read"))) {//if (rb_respond_to(stream, rb_intern("to_s"))) {
    /* copy read_bytes from stream into ptr */
    struct stream_read_call_args args;
    args.stream = stream;
    args.read_bytes = read_bytes;
    VALUE str = with_easy_callback_active(rbce, call_stream_read, (VALUE)&args);
    if( str != Qnil ) {
      StringValue(str);
      memcpy(ptr, RSTRING_PTR(str), RSTRING_LEN(str));
      return RSTRING_LEN(str);
    }
    else {
      return 0;
    }
  }
  else if (rb_respond_to(stream, rb_intern("to_s"))) {
    ruby_curl_upload *rbcu;
    VALUE str;
    size_t len;
    size_t remaining;
    char *str_ptr;
    TypedData_Get_Struct(upload, ruby_curl_upload, &ruby_curl_upload_data_type, rbcu);
    str = with_easy_callback_active(rbce, call_stream_to_s, stream);
    StringValue(str);
    len = RSTRING_LEN(str);
    remaining = len - rbcu->offset;
    str_ptr = RSTRING_PTR(str);

    if( remaining <= read_bytes ) {
      if( remaining > 0 ) {
        memcpy(ptr, str_ptr+rbcu->offset, remaining);
        read_bytes = remaining;
        rbcu->offset += remaining;
      }
      return remaining;
    }
    else { // read_bytes < remaining - send what we can fit in the buffer(ptr)
      memcpy(ptr, str_ptr+rbcu->offset, read_bytes);
      rbcu->offset += read_bytes;
    }
    return read_bytes;
  }
  else {
    return 0;
  }
}

int seek_data_handler(ruby_curl_easy *rbce,
                      curl_off_t offset,
                      int origin) {

  VALUE upload = rb_easy_get("upload");
  VALUE stream = ruby_curl_upload_stream_get(upload);

  if (rb_respond_to(stream, rb_intern("seek"))) {
    struct stream_seek_call_args args;
    args.stream = stream;
    args.offset = offset;
    args.origin = SEEK_SET;
    with_easy_callback_active(rbce, call_stream_seek, (VALUE)&args);
  } else {
    ruby_curl_upload *rbcu;
    TypedData_Get_Struct(upload, ruby_curl_upload, &ruby_curl_upload_data_type, rbcu);
    // This OK because curl only uses SEEK_SET as per the documentation
    rbcu->offset = offset;
  }

  return 0;
}

static size_t proc_data_handler(char *stream,
                                size_t size,
                                size_t nmemb,
                                VALUE proc) {
  VALUE procret;

  procret = rb_funcall(proc, idCall, 1, rb_str_new(stream, size * nmemb));

  switch (rb_type(procret)) {
    case T_FIXNUM:
      return FIX2LONG(procret);
    case T_BIGNUM:
      return NUM2LONG(procret);
    default:
      rb_warn("Curl data handlers should return the number of bytes read as an Integer");
      return size * nmemb;
  }
}

static size_t proc_data_handler_body(char *stream,
                                     size_t size,
                                     size_t nmemb,
                                     ruby_curl_easy *rbce)
{
  struct proc_data_call_args args;
  struct easy_callback_dispatch_args dispatch_args;
  VALUE procret;
  args.stream = stream;
  args.size = size;
  args.nmemb = nmemb;
  args.proc = rb_easy_get("body_proc");

  dispatch_args.rbce = rbce;
  dispatch_args.func = call_proc_data_handler_wrapped;
  dispatch_args.arg = (VALUE)&args;
  procret = rb_rescue(call_with_easy_callback_active, (VALUE)&dispatch_args, callback_exception_store_on_easy, (VALUE)rbce);

  return ((procret == Qfalse) || (procret == Qnil)) ? 0 : NUM2ULONG(procret);
}
static size_t proc_data_handler_header(char *stream,
                                       size_t size,
                                       size_t nmemb,
                                       ruby_curl_easy *rbce)
{
  struct proc_data_call_args args;
  struct easy_callback_dispatch_args dispatch_args;
  VALUE procret;
  args.stream = stream;
  args.size = size;
  args.nmemb = nmemb;
  args.proc = rb_easy_get("header_proc");

  dispatch_args.rbce = rbce;
  dispatch_args.func = call_proc_data_handler_wrapped;
  dispatch_args.arg = (VALUE)&args;
  procret = rb_rescue(call_with_easy_callback_active, (VALUE)&dispatch_args, callback_exception_store_on_easy, (VALUE)rbce);

  return ((procret == Qfalse) || (procret == Qnil)) ? 0 : NUM2ULONG(procret);
}


static VALUE call_progress_handler(VALUE ary) {
  return rb_funcall(rb_ary_entry(ary, 0), idCall, 4,
                    rb_ary_entry(ary, 1), // rb_float_new(dltotal),
                    rb_ary_entry(ary, 2), // rb_float_new(dlnow),
                    rb_ary_entry(ary, 3), // rb_float_new(ultotal),
                    rb_ary_entry(ary, 4)); // rb_float_new(ulnow));
}

/* CURLOPT_PROGRESSFUNCTION callback (deprecated since 7.32.0) */
#ifndef HAVE_CURLOPT_XFERINFOFUNCTION
static int proc_progress_handler(void *clientp,
                                 double dltotal,
                                 double dlnow,
                                 double ultotal,
                                 double ulnow) {
  ruby_curl_easy *rbce = (ruby_curl_easy *)clientp;
  VALUE proc = rb_easy_get("progress_proc");
  if (proc == Qnil) {
    return 0;
  }
  VALUE procret;
  VALUE callargs = rb_ary_new2(5);

  rb_ary_store(callargs, 0, proc);
  rb_ary_store(callargs, 1, rb_float_new(dltotal));
  rb_ary_store(callargs, 2, rb_float_new(dlnow));
  rb_ary_store(callargs, 3, rb_float_new(ultotal));
  rb_ary_store(callargs, 4, rb_float_new(ulnow));

  struct easy_callback_dispatch_args dispatch_args;
  dispatch_args.rbce = rbce;
  dispatch_args.func = call_progress_handler;
  dispatch_args.arg = callargs;
  procret = rb_rescue(call_with_easy_callback_active, (VALUE)&dispatch_args, callback_exception, Qnil);

  return(((procret == Qfalse) || (procret == Qnil)) ? -1 : 0);
}
#endif

/* CURLOPT_XFERINFOFUNCTION callback (since 7.32.0, replaces PROGRESSFUNCTION) */
#ifdef HAVE_CURLOPT_XFERINFOFUNCTION
static int proc_xferinfo_handler(void *clientp,
                                 curl_off_t dltotal,
                                 curl_off_t dlnow,
                                 curl_off_t ultotal,
                                 curl_off_t ulnow) {
  ruby_curl_easy *rbce = (ruby_curl_easy *)clientp;
  VALUE proc = rb_easy_get("progress_proc");
  if (proc == Qnil) {
    return 0;
  }
  VALUE procret;
  VALUE callargs = rb_ary_new2(5);

  rb_ary_store(callargs, 0, proc);
  rb_ary_store(callargs, 1, LL2NUM(dltotal));
  rb_ary_store(callargs, 2, LL2NUM(dlnow));
  rb_ary_store(callargs, 3, LL2NUM(ultotal));
  rb_ary_store(callargs, 4, LL2NUM(ulnow));

  struct easy_callback_dispatch_args dispatch_args;
  dispatch_args.rbce = rbce;
  dispatch_args.func = call_progress_handler;
  dispatch_args.arg = callargs;
  procret = rb_rescue(call_with_easy_callback_active, (VALUE)&dispatch_args, callback_exception, Qnil);

  return(((procret == Qfalse) || (procret == Qnil)) ? -1 : 0);
}
#endif

static VALUE call_debug_handler(VALUE ary) {
  return rb_funcall(rb_ary_entry(ary, 0), idCall, 2,
                    rb_ary_entry(ary, 1), // INT2NUM(type),
                    rb_ary_entry(ary, 2)); // rb_str_new(data, data_len)
}
static int proc_debug_handler(CURL *curl,
                              curl_infotype type,
                              char *data,
                              size_t data_len,
                              void *clientp) {
  ruby_curl_easy *rbce = (ruby_curl_easy *)clientp;
  VALUE proc = rb_easy_get("debug_proc");
  if (proc == Qnil) {
    return 0;
  }
  VALUE callargs = rb_ary_new2(3);
  rb_ary_store(callargs, 0, proc);
  rb_ary_store(callargs, 1, INT2NUM(type));
  rb_ary_store(callargs, 2, rb_str_new(data, data_len));
  struct easy_callback_dispatch_args dispatch_args;
  dispatch_args.rbce = rbce;
  dispatch_args.func = call_debug_handler;
  dispatch_args.arg = callargs;
  rb_rescue(call_with_easy_callback_active, (VALUE)&dispatch_args, callback_exception, Qnil);
  /* no way to indicate to libcurl that we should break out given an exception in the on_debug handler...
   * this means exceptions will be swallowed
   */
  //rb_funcall(proc, idCall, 2, INT2NUM(type), rb_str_new(data, data_len));
  return 0;
}

/* ================== MARK/FREE/SIZE FUNCS ==================*/
static void curl_easy_mark(void *ptr) {
  ruby_curl_easy *rbce = (ruby_curl_easy *)ptr;
  if (rbce) {
    if (!NIL_P(rbce->opts)) { rb_gc_mark(rbce->opts); }
    if (!NIL_P(rbce->multi)) { rb_gc_mark(rbce->multi); }
    if (!NIL_P(rbce->callback_error)) { rb_gc_mark(rbce->callback_error); }
  }
}

static void ruby_curl_easy_clear_headers_list(ruby_curl_easy *rbce) {
  if (!rbce || !rbce->curl_headers) {
    return;
  }
  if (rbce->curl) {
    curl_easy_setopt(rbce->curl, CURLOPT_HTTPHEADER, NULL);
  }
  curl_slist_free_all(rbce->curl_headers);
  rbce->curl_headers = NULL;
}

static void ruby_curl_easy_clear_proxy_headers_list(ruby_curl_easy *rbce) {
  if (!rbce || !rbce->curl_proxy_headers) {
    return;
  }
#ifdef HAVE_CURLOPT_PROXYHEADER
  if (rbce->curl) {
    curl_easy_setopt(rbce->curl, CURLOPT_PROXYHEADER, NULL);
  }
#endif
  curl_slist_free_all(rbce->curl_proxy_headers);
  rbce->curl_proxy_headers = NULL;
}

static void ruby_curl_easy_clear_ftp_commands_list(ruby_curl_easy *rbce) {
  if (!rbce || !rbce->curl_ftp_commands) {
    return;
  }
  if (rbce->curl) {
    curl_easy_setopt(rbce->curl, CURLOPT_QUOTE, NULL);
  }
  curl_slist_free_all(rbce->curl_ftp_commands);
  rbce->curl_ftp_commands = NULL;
}

static void ruby_curl_easy_clear_resolve_list(ruby_curl_easy *rbce) {
  if (!rbce || !rbce->curl_resolve) {
    return;
  }
#ifdef HAVE_CURLOPT_RESOLVE
  if (rbce->curl) {
    curl_easy_setopt(rbce->curl, CURLOPT_RESOLVE, NULL);
  }
#endif
  curl_slist_free_all(rbce->curl_resolve);
  rbce->curl_resolve = NULL;
}

/* Legacy wrapper for external callers */
void ruby_curl_easy_mark(ruby_curl_easy *rbce) {
  curl_easy_mark((void *)rbce);
}

static ruby_curl_multi *ruby_curl_multi_pointer_if_compatible(VALUE multi_val) {
  if (NIL_P(multi_val) || !RB_TYPE_P(multi_val, T_DATA)) {
    return NULL;
  }

#if defined(RTYPEDDATA_P) && defined(RTYPEDDATA_TYPE) && defined(RTYPEDDATA_DATA)
  if (!RTYPEDDATA_P(multi_val)) {
    return NULL;
  }

  if (RTYPEDDATA_TYPE(multi_val) != &ruby_curl_multi_data_type) {
    return NULL;
  }

  return (ruby_curl_multi *)RTYPEDDATA_DATA(multi_val);
#else
  if (!rb_typeddata_is_kind_of(multi_val, &ruby_curl_multi_data_type)) {
    return NULL;
  }

  return DATA_PTR(multi_val);
#endif
}

static void ruby_curl_easy_free(ruby_curl_easy *rbce) {
  if (!rbce) {
    return;
  }

  if (!NIL_P(rbce->multi)) {
    VALUE multi_val = rbce->multi;
    ruby_curl_multi *rbcm = ruby_curl_multi_pointer_if_compatible(multi_val);

    rbce->multi = Qnil;

    if (rbcm) {
      /* Best-effort: ensure the handle is detached from the multi to avoid
       * libcurl retaining a dangling pointer to a soon-to-be cleaned-up easy
       * handle. This path runs during GC, so it must not invoke Ruby APIs that
       * can allocate or raise. */
      if (rbcm->handle && rbce->curl) {
        curl_multi_remove_handle(rbcm->handle, rbce->curl);
      }
      rb_curl_multi_forget_easy(rbcm, rbce);
    }
  }

  ruby_curl_easy_clear_headers_list(rbce);
  ruby_curl_easy_clear_proxy_headers_list(rbce);
  ruby_curl_easy_clear_ftp_commands_list(rbce);
  ruby_curl_easy_clear_resolve_list(rbce);

  if (rbce->curl) {
    /* disable any progress or debug events */
    curl_easy_setopt(rbce->curl, CURLOPT_WRITEFUNCTION, NULL);
    curl_easy_setopt(rbce->curl, CURLOPT_WRITEDATA, NULL);
    curl_easy_setopt(rbce->curl, CURLOPT_HEADERFUNCTION, NULL);
    curl_easy_setopt(rbce->curl, CURLOPT_HEADERDATA, NULL);
    curl_easy_setopt(rbce->curl, CURLOPT_DEBUGFUNCTION, NULL);
    curl_easy_setopt(rbce->curl, CURLOPT_DEBUGDATA, NULL);
    curl_easy_setopt(rbce->curl, CURLOPT_VERBOSE, 0);
#ifdef HAVE_CURLOPT_XFERINFOFUNCTION
    curl_easy_setopt(rbce->curl, CURLOPT_XFERINFOFUNCTION, NULL);
#else
    curl_easy_setopt(rbce->curl, CURLOPT_PROGRESSFUNCTION, NULL);
#endif
    curl_easy_setopt(rbce->curl, CURLOPT_NOPROGRESS, 1);
    curl_easy_cleanup(rbce->curl);
    rbce->curl = NULL;
  }

  rbce->self = Qnil;
}

/* TypedData-compatible free function */
static void curl_easy_free(void *ptr) {
  ruby_curl_easy *rbce = (ruby_curl_easy *)ptr;
  if (rbce) {
    ruby_curl_easy_free(rbce);
    free(rbce);
  }
}

/* Legacy wrapper for external callers (e.g., curb_multi) */
void ruby_curl_easy_free_wrapper(ruby_curl_easy *rbce) {
  curl_easy_free((void *)rbce);
}

static size_t curl_easy_memsize(const void *ptr) {
  const ruby_curl_easy *rbce = (const ruby_curl_easy *)ptr;
  size_t size = sizeof(ruby_curl_easy);
  /* Note: We don't count curl_slist or CURL handle memory as they're
   * managed by libcurl and would require complex introspection */
  (void)rbce; /* silence unused warning */
  return size;
}

const rb_data_type_t ruby_curl_easy_data_type = {
  "Curl::Easy",
  {
    curl_easy_mark,
    curl_easy_free,
    curl_easy_memsize,
#ifdef RUBY_TYPED_FREE_IMMEDIATELY
    NULL, /* compact - not needed */
#endif
  },
#ifdef RUBY_TYPED_FREE_IMMEDIATELY
  NULL, NULL, /* parent, data */
  RUBY_TYPED_FREE_IMMEDIATELY
#endif
};


/* ================= ALLOC METHODS ====================*/

static void ruby_curl_easy_zero(ruby_curl_easy *rbce) {
  rbce->opts = rb_hash_new();

  memset(rbce->err_buf, 0, CURL_ERROR_SIZE);

  rbce->self = Qnil;
  rbce->curl_headers = NULL;
  rbce->curl_proxy_headers = NULL;
  rbce->curl_ftp_commands = NULL;
  rbce->curl_resolve = NULL;

  /* various-typed opts */
  rbce->local_port = 0;
  rbce->local_port_range = 0;
  rbce->proxy_port = 0;
  rbce->proxy_type = -1;
  rbce->http_auth_types = 0;
  rbce->proxy_auth_types = 0;
  rbce->max_redirs = -1;
  rbce->timeout = 0;
  rbce->timeout_ms = 0;
  rbce->connect_timeout = 0;
  rbce->connect_timeout_ms = 0;
  rbce->dns_cache_timeout = 60;
  rbce->ftp_response_timeout = 0;
  rbce->low_speed_limit = 0;
  rbce->low_speed_time = 0;
  rbce->max_send_speed_large = 0;
  rbce->max_recv_speed_large = 0;
  rbce->ssl_version = -1;
  rbce->use_ssl = -1;
  rbce->ftp_filemethod = -1;
  rbce->http_version = CURL_HTTP_VERSION_NONE;
  rbce->resolve_mode = CURL_IPRESOLVE_WHATEVER;

  /* bool opts */
  rbce->proxy_tunnel = 0;
  rbce->fetch_file_time = 0;
  rbce->ssl_verify_peer = 1;
  rbce->ssl_verify_host = 2;
  rbce->header_in_body = 0;
  rbce->use_netrc = 0;
  rbce->follow_location = 0;
  rbce->unrestricted_auth = 0;
  rbce->verbose = 0;
  rbce->multipart_form_post = 0;
  rbce->enable_cookies = 0;
  rbce->cookielist_engine_enabled = 0;
  rbce->ignore_content_length = 0;
  rbce->callback_active = 0;
  rbce->callback_error = Qnil;
  rbce->last_result = 0;
}

/*
 * Allocate space for a Curl::Easy instance.
 */
static VALUE ruby_curl_easy_allocate(VALUE klass) {
  ruby_curl_easy *rbce;
  rbce = ALLOC(ruby_curl_easy);
  if (!rbce) {
    rb_raise(rb_eNoMemError, "Failed to allocate memory for Curl::Easy");
  }
  rbce->curl = NULL;
  rbce->opts  = Qnil;
  rbce->multi = Qnil;
  ruby_curl_easy_zero(rbce);
  return TypedData_Wrap_Struct(klass, &ruby_curl_easy_data_type, rbce);
}

/*
 * call-seq:
 *   Curl::Easy.new                                   => #<Curl::Easy...>
 *   Curl::Easy.new(url = nil)                        => #<Curl::Easy...>
 *   Curl::Easy.new(url = nil) { |self| ... }         => #<Curl::Easy...>
 *
 * Initialize a new Curl::Easy instance, optionally supplying the URL.
 * The block form allows further configuration to be supplied before
 * the instance is returned.
 */
static VALUE ruby_curl_easy_initialize(int argc, VALUE *argv, VALUE self) {
  CURLcode ecode;
  VALUE url, blk;
  ruby_curl_easy *rbce;

  rb_scan_args(argc, argv, "01&", &url, &blk);

  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);

  /* handler */
  rbce->curl = curl_easy_init();
  if (!rbce->curl) {
    rb_raise(eCurlErrFailedInit, "Failed to initialize easy handle");
  }

  rbce->multi = Qnil;
  rbce->opts  = Qnil;

  ruby_curl_easy_zero(rbce);
  rbce->self = self;

  curl_easy_setopt(rbce->curl, CURLOPT_ERRORBUFFER, &rbce->err_buf);

  rb_easy_set("url", url);

  /* set the pointer to the curl handle */
  ecode = curl_easy_setopt(rbce->curl, CURLOPT_PRIVATE, (void*)rbce);
  if (ecode != CURLE_OK) {
    raise_curl_easy_error_exception(ecode);
  }

  if (blk != Qnil) {
    rb_funcall(blk, idCall, 1, self);
  }

  return self;
}

/* Helper to duplicate a curl_slist */
static struct curl_slist *duplicate_curl_slist(struct curl_slist *list) {
    struct curl_slist *dup = NULL;
    struct curl_slist *tmp;
    struct curl_slist *new_list;
    for (tmp = list; tmp; tmp = tmp->next) {
        new_list = curl_slist_append(dup, tmp->data);
        if (!new_list) {
            if (dup) { curl_slist_free_all(dup); }
            rb_raise(rb_eNoMemError, "Failed to duplicate curl_slist");
        }
        dup = new_list;
    }
    return dup;
}

/*
 * call-seq:
 *   easy.clone                                       => <easy clone>
 *   easy.dup                                         => <easy clone>
 *
 * Clone this Curl::Easy instance, creating a new instance.
 * This method duplicates the underlying CURL* handle.
 */
static VALUE ruby_curl_easy_clone(VALUE self) {
  ruby_curl_easy *rbce, *newrbce;

  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);

  newrbce = ALLOC(ruby_curl_easy);
  if (!newrbce) {
    rb_raise(rb_eNoMemError, "Failed to allocate memory for Curl::Easy clone");
  }
  /* shallow copy */
  memcpy(newrbce, rbce, sizeof(ruby_curl_easy));

  /* now deep copy */
  newrbce->curl = curl_easy_duphandle(rbce->curl);
  newrbce->curl_headers = (rbce->curl_headers) ? duplicate_curl_slist(rbce->curl_headers) : NULL;
  newrbce->curl_proxy_headers = (rbce->curl_proxy_headers) ? duplicate_curl_slist(rbce->curl_proxy_headers) : NULL;
  newrbce->curl_ftp_commands = (rbce->curl_ftp_commands) ? duplicate_curl_slist(rbce->curl_ftp_commands) : NULL;
  newrbce->curl_resolve = (rbce->curl_resolve) ? duplicate_curl_slist(rbce->curl_resolve) : NULL;

  /* A cloned easy should not retain ownership reference to the original multi. */
  newrbce->multi = Qnil;
  newrbce->callback_error = Qnil;

  if (rbce->opts != Qnil) {
    newrbce->opts = rb_funcall(rbce->opts, rb_intern("dup"), 0);
  }

  /* Set the error buffer on the new curl handle using the new err_buf */
  curl_easy_setopt(newrbce->curl, CURLOPT_ERRORBUFFER, newrbce->err_buf);

  VALUE clone = TypedData_Wrap_Struct(cCurlEasy, &ruby_curl_easy_data_type, newrbce);
  newrbce->self = clone;
  curl_easy_setopt(newrbce->curl, CURLOPT_PRIVATE, (void*)newrbce);

  return clone;
}

/*
 * call-seq:
 *   easy.close                                      => nil
 *
 * Close the Curl::Easy instance. Any open connections are closed
 * The easy handle is reinitialized.  If a previous multi handle was
 * open it is set to nil and will be cleared after a GC.
 */
static VALUE ruby_curl_easy_close(VALUE self) {
  CURLcode ecode;
  ruby_curl_easy *rbce;

  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);

  if (rbce->callback_active) {
    rb_raise(rb_eRuntimeError, "Cannot close an active curl handle within a callback");
  }

  ruby_curl_easy_free(rbce);

  /* reinit the handle */
  rbce->curl = curl_easy_init();
  if (!rbce->curl) {
    rb_raise(eCurlErrFailedInit, "Failed to initialize easy handle");
  }

  rbce->multi = Qnil;

  ruby_curl_easy_zero(rbce);
  rbce->self = self;

  /* give the new curl handle a reference back to the ruby object */
  ecode = curl_easy_setopt(rbce->curl, CURLOPT_PRIVATE, (void*)rbce);
  if (ecode != CURLE_OK) {
    raise_curl_easy_error_exception(ecode);
  }

  return Qnil;
}

/*
 * call-seq:
 *   easy.reset                                      => Hash
 *
 * Reset the Curl::Easy instance, clears out all settings.
 *
 * from http://curl.haxx.se/libcurl/c/curl_easy_reset.html
 * Re-initializes all options previously set on a specified CURL handle to the default values. This puts back the handle to the same state as it was in when it was just created with curl_easy_init(3).
 * It does not change the following information kept in the handle: live connections, the Session ID cache, the DNS cache, the cookies and shares.
 *
 * The return value contains all settings stored.
 */
static VALUE ruby_curl_easy_reset(VALUE self) {
  CURLcode ecode;
  ruby_curl_easy *rbce;
  VALUE opts_dup;
  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);

  if (rbce->callback_active) {
    rb_raise(rb_eRuntimeError, "Cannot close an active curl handle within a callback");
  }

  opts_dup = rb_funcall(rbce->opts, rb_intern("dup"), 0);

  ruby_curl_easy_cleanup(self, rbce);
  curl_easy_reset(rbce->curl);
  ruby_curl_easy_zero(rbce);
  rbce->self = self;

  curl_easy_setopt(rbce->curl, CURLOPT_ERRORBUFFER, &rbce->err_buf);

  /* reset clobbers the private setting, so reset it to self */
  ecode = curl_easy_setopt(rbce->curl, CURLOPT_PRIVATE, (void*)rbce);
  if (ecode != CURLE_OK) {
    raise_curl_easy_error_exception(ecode);
  }

  return opts_dup;
}


/* ================ OBJ ATTRIBUTES ==================*/

/*
 * call-seq:
 *   easy.url                                         => string
 *
 * Obtain the URL that will be used by subsequent calls to +perform+.
 */
static VALUE ruby_curl_easy_url_get(VALUE self) {
  CURB_OBJECT_HGETTER(ruby_curl_easy, url);
}

/*
 * call-seq:
 *   easy.proxy_url                                   => string
 *
 * Obtain the HTTP Proxy URL that will be used by subsequent calls to +perform+.
 */
static VALUE ruby_curl_easy_proxy_url_get(VALUE self) {
  CURB_OBJECT_HGETTER(ruby_curl_easy, proxy_url);
}

/*
 * call-seq:
 *   easy.headers = "Header: val"                              => "Header: val"
 *   easy.headers = {"Header" => "val" ..., "Header" => "val"} => {"Header: val", ...}
 *   easy.headers = ["Header: val" ..., "Header: val"]         => ["Header: val", ...]
 *
 * Set custom HTTP headers for following requests. This can be used to add
 * custom headers, or override standard headers used by libcurl. It defaults to a
 * Hash.
 *
 * For example to set a standard or custom header:
 *
 *    easy.headers["MyHeader"] = "myval"
 *
 * To remove a standard header (this is useful when removing libcurls default
 * 'Expect: 100-Continue' header when using HTTP form posts):
 *
 *    easy.headers["Expect"] = ''
 *
 * Anything passed to libcurl as a header will be converted to a string during
 * the perform step.
 */
static VALUE ruby_curl_easy_headers_set(VALUE self, VALUE headers) {
  CURB_OBJECT_HSETTER(ruby_curl_easy, headers);
}

static VALUE ruby_curl_easy_proxy_headers_set(VALUE self, VALUE proxy_headers) {
  CURB_OBJECT_HSETTER(ruby_curl_easy, proxy_headers);
}

/*
 * call-seq:
 *   easy.headers                                     => Hash, Array or Str
 *
 * Obtain the custom HTTP headers for following requests.
 */
static VALUE ruby_curl_easy_headers_get(VALUE self) {
  ruby_curl_easy *rbce;
  VALUE headers;
  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
  headers = rb_easy_get("headers");//rb_hash_aref(rbce->opts, rb_intern("headers"));
  if (headers == Qnil) { headers = rb_easy_set("headers", rb_hash_new()); }
  return headers;
}

/*
 * call-seq:
 *   easy.proxy_headers = "Header: val"                              => "Header: val"
 *   easy.proxy_headers = {"Header" => "val" ..., "Header" => "val"} => {"Header: val", ...}
 *   easy.proxy_headers = ["Header: val" ..., "Header: val"]         => ["Header: val", ...]
 *
 *
 * For example to set a standard or custom header:
 *
 *    easy.proxy_headers["MyHeader"] = "myval"
 *
 * To remove a standard header (this is useful when removing libcurls default
 * 'Expect: 100-Continue' header when using HTTP form posts):
 *
 *    easy.proxy_headers["Expect"] = ''
 *
 * Anything passed to libcurl as a header will be converted to a string during
 * the perform step.
 */

/*
 * call-seq:
 *   easy.proxy_headers                                     => Hash, Array or Str
 *
 * Obtain the custom HTTP proxy_headers for following requests.
 */
static VALUE ruby_curl_easy_proxy_headers_get(VALUE self) {
  ruby_curl_easy *rbce;
  VALUE proxy_headers;
  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
  proxy_headers = rb_easy_get("proxy_headers");//rb_hash_aref(rbce->opts, rb_intern("proxy_headers"));
  if (proxy_headers == Qnil) { proxy_headers = rb_easy_set("proxy_headers", rb_hash_new()); }
  return proxy_headers;
}

/*
 * call-seq:
 *   easy.interface                                   => string
 *
 * Obtain the interface name that is used as the outgoing network interface.
 * The name can be an interface name, an IP address or a host name.
 */
static VALUE ruby_curl_easy_interface_get(VALUE self) {
  CURB_OBJECT_HGETTER(ruby_curl_easy, interface_hm);
}

/*
 * call-seq:
 *   easy.userpwd                                     => string
 *
 * Obtain the username/password string that will be used for subsequent
 * calls to +perform+.
 */
static VALUE ruby_curl_easy_userpwd_get(VALUE self) {
  CURB_OBJECT_HGETTER(ruby_curl_easy, userpwd);
}

/*
 * call-seq:
 *   easy.proxypwd                                    => string
 *
 * Obtain the username/password string that will be used for proxy
 * connection during subsequent calls to +perform+. The supplied string
 * should have the form "username:password"
 */
static VALUE ruby_curl_easy_proxypwd_get(VALUE self) {
  CURB_OBJECT_HGETTER(ruby_curl_easy, proxypwd);
}

/*
 * call-seq:
 *   easy.cookies                                     => "name1=content1; name2=content2;"
 *
 * Obtain the manually set Cookie header string for this Curl::Easy instance.
 *
 * Notes:
 * - This corresponds to libcurl's CURLOPT_COOKIE and only affects the outgoing
 *   Cookie request header. It does NOT modify the internal libcurl cookie engine
 *   that stores cookies received via Set-Cookie.
 * - To inspect or modify cookies stored in the cookie engine, use
 *   +easy.cookielist+ (getter) and +easy.cookielist=+ or +easy.set(:cookielist, ...)+ (setter).
 * - To clear a previously set manual Cookie header, assign an empty string.
 *   Assigning +nil+ currently has no effect.
 */
static VALUE ruby_curl_easy_cookies_get(VALUE self) {
  CURB_OBJECT_HGETTER(ruby_curl_easy, cookies);
}

/*
 * call-seq:
 *   easy.cookiefile                                  => string
 *
 * Obtain the cookiefile path for this Curl::Easy instance (used to load cookies when the
 * cookie engine is enabled).
 */
static VALUE ruby_curl_easy_cookiefile_get(VALUE self) {
  CURB_OBJECT_HGETTER(ruby_curl_easy, cookiefile);
}

/*
 * call-seq:
 *   easy.cookiejar                                   => string
 *
 * Obtain the cookiejar path for this Curl::Easy instance (used to persist cookies when the
 * cookie engine is enabled).
 */
static VALUE ruby_curl_easy_cookiejar_get(VALUE self) {
  CURB_OBJECT_HGETTER(ruby_curl_easy, cookiejar);
}

/*
 * call-seq:
 *   easy.cert = string                               => ""
 *
 * Set a cert file to use for this Curl::Easy instance. This file
 * will be used to validate SSL connections.
 *
 */
static VALUE ruby_curl_easy_cert_set(VALUE self, VALUE cert) {
  CURB_OBJECT_HSETTER(ruby_curl_easy, cert);
}

/*
 * call-seq:
 *   easy.cert                                        => string
 *
 * Obtain the cert file to use for this Curl::Easy instance.
 */
static VALUE ruby_curl_easy_cert_get(VALUE self) {
  CURB_OBJECT_HGETTER(ruby_curl_easy, cert);
}

/*
 * call-seq:
 *   easy.cert_key = "cert_key.file"                  => ""
 *
 * Set a cert key to use for this Curl::Easy instance. This file
 * will be used to validate SSL certificates.
 *
 */
static VALUE ruby_curl_easy_cert_key_set(VALUE self, VALUE cert_key) {
  CURB_OBJECT_HSETTER(ruby_curl_easy, cert_key);
}

/*
 * call-seq:
 *   easy.cert_key                                    => "cert_key.file"
 *
 * Obtain the cert key file to use for this Curl::Easy instance.
 */
static VALUE ruby_curl_easy_cert_key_get(VALUE self) {
  CURB_OBJECT_HGETTER(ruby_curl_easy, cert_key);
}

/*
 * call-seq:
 *   easy.cacert = string                             => ""
 *
 * Set a cacert bundle to use for this Curl::Easy instance. This file
 * will be used to validate SSL certificates.
 *
 */
static VALUE ruby_curl_easy_cacert_set(VALUE self, VALUE cacert) {
  CURB_OBJECT_HSETTER(ruby_curl_easy, cacert);
}

/*
 * call-seq:
 *   easy.cacert                                      => string
 *
 * Obtain the cacert file to use for this Curl::Easy instance.
 */
static VALUE ruby_curl_easy_cacert_get(VALUE self) {
  CURB_OBJECT_HGETTER(ruby_curl_easy, cacert);
}

/*
 * call-seq:
 *   easy.certpassword = string                       => ""
 *
 * Set a password used to open the specified cert
 */
static VALUE ruby_curl_easy_certpassword_set(VALUE self, VALUE certpassword) {
  CURB_OBJECT_HSETTER(ruby_curl_easy, certpassword);
}

/*
 * call-seq:
 *   easy.certtype = "PEM|DER"                        => ""
 *
 * Set a cert type to use for this Curl::Easy instance.
 * Default is PEM
 *
 */
static VALUE ruby_curl_easy_certtype_set(VALUE self, VALUE certtype) {
  CURB_OBJECT_HSETTER(ruby_curl_easy, certtype);
}

/*
 * call-seq:
 *   easy.certtype                                    => string
 *
 * Obtain the cert type used for this Curl::Easy instance
 */
static VALUE ruby_curl_easy_certtype_get(VALUE self) {
  CURB_OBJECT_HGETTER(ruby_curl_easy, certtype);
}

/*
 * call-seq:
 *   easy.encoding = string                           => string
 *
 * Set the accepted encoding types, curl will handle all of the decompression
 *
 */
static VALUE ruby_curl_easy_encoding_set(VALUE self, VALUE encoding) {
  CURB_OBJECT_HSETTER(ruby_curl_easy, encoding);
}
/*
 * call-seq:
 *   easy.encoding                                    => string
 *
 * Get the set encoding types
 *
*/
static VALUE ruby_curl_easy_encoding_get(VALUE self) {
  CURB_OBJECT_HGETTER(ruby_curl_easy, encoding);
}

/*
 * call-seq:
 *   easy.useragent = "Ruby/Curb"                     => ""
 *
 * Set the user agent string for this Curl::Easy instance
 *
 */
static VALUE ruby_curl_easy_useragent_set(VALUE self, VALUE useragent) {
  CURB_OBJECT_HSETTER(ruby_curl_easy, useragent);
}

/*
 * call-seq:
 *   easy.useragent                                  => "Ruby/Curb"
 *
 * Obtain the user agent string used for this Curl::Easy instance
 */
static VALUE ruby_curl_easy_useragent_get(VALUE self) {
  CURB_OBJECT_HGETTER(ruby_curl_easy, useragent);
}

/*
 * call-seq:
 *   easy.post_body = "some=form%20data&to=send"      => string or nil
 *
 * Sets the POST body of this Curl::Easy instance.  This is expected to be
 * URL encoded; no additional processing or encoding is done on the string.
 * The content-type header will be set to application/x-www-form-urlencoded.
 *
 * This is handy if you want to perform a POST against a Curl::Multi instance.
 */
static VALUE ruby_curl_easy_post_body_set_with_mode(VALUE self, VALUE post_body, int force_http_get_on_nil) {
  ruby_curl_easy *rbce;
  CURL *curl;

  VALUE body_str;
  VALUE retained_body_str;
  char *data;
  long len;

  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);

  curl = rbce->curl;

  if ( post_body == Qnil ) {
    rb_easy_del("postdata_buffer");
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, NULL);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, 0);
    if (force_http_get_on_nil) {
      curl_easy_setopt(curl, CURLOPT_HTTPGET, 1);
    }

  } else {
    if (rb_type(post_body) == T_STRING) {
      body_str = post_body;
    }
    else if (rb_respond_to(post_body, rb_intern("to_s"))) {
      body_str = rb_funcall(post_body, rb_intern("to_s"), 0);
    }
    else {
      rb_raise(rb_eRuntimeError, "post data must respond_to .to_s");
    }

    StringValue(body_str);

    // Store the string, since it has to hang around for the duration of the
    // request.  See CURLOPT_POSTFIELDS in the libcurl docs.
#ifdef HAVE_CURLOPT_COPYPOSTFIELDS
    /*
     * libcurl copies the bytes immediately for COPYPOSTFIELDS, so retain a
     * matching Ruby snapshot for post_body instead of the caller's mutable
     * source string.
     */
    retained_body_str = rb_str_dup(body_str);
#else
    retained_body_str = body_str;
#endif
    data = StringValuePtr(retained_body_str);
    len = RSTRING_LEN(retained_body_str);
    rb_easy_set("postdata_buffer", retained_body_str);

    curl_easy_setopt(curl, CURLOPT_POST, 1);
    curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, len);
#ifdef HAVE_CURLOPT_COPYPOSTFIELDS
    curl_easy_setopt(curl, CURLOPT_COPYPOSTFIELDS, data);
#else
    curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data);
#endif

    return post_body;
  }

  return Qnil;
}

static VALUE ruby_curl_easy_post_body_set(VALUE self, VALUE post_body) {
  return ruby_curl_easy_post_body_set_with_mode(self, post_body, 1);
}

/*
 * call-seq:
 *   easy.post_body                                  => string or nil
 *
 * Obtain the POST body used in this Curl::Easy instance.
 */
static VALUE ruby_curl_easy_post_body_get(VALUE self) {
  CURB_OBJECT_HGETTER(ruby_curl_easy, postdata_buffer);
}

/*
 * call-seq:
 *   easy.put_data = data                             => ""
 *
 * Points this Curl::Easy instance to data to be uploaded via PUT.  This
 * sets the request to a PUT type request - useful if you want to PUT via
 * a multi handle.
 */
static VALUE ruby_curl_easy_put_data_set(VALUE self, VALUE data) {
  ruby_curl_easy *rbce;
  CURL *curl;
  VALUE upload;
  VALUE headers;

  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);

  upload = ruby_curl_upload_new(cCurlUpload);
  ruby_curl_upload_stream_set(upload,data);

  curl = rbce->curl;
  rb_easy_set("upload", upload); /* keep the upload object alive as long as
                                    the easy handle is active or until the upload
                                    is complete or terminated... */

  curl_easy_setopt(curl, CURLOPT_NOBODY, 0);
  curl_easy_setopt(curl, CURLOPT_POST, 0);
  curl_easy_setopt(curl, CURLOPT_POSTFIELDS, NULL);
  curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, 0);
  curl_easy_setopt(curl, CURLOPT_UPLOAD, 1);
  curl_easy_setopt(curl, CURLOPT_READFUNCTION, (curl_read_callback)read_data_handler);
#ifdef HAVE_CURLOPT_SEEKFUNCTION
  curl_easy_setopt(curl, CURLOPT_SEEKFUNCTION, (curl_seek_callback)seek_data_handler);
#endif
  curl_easy_setopt(curl, CURLOPT_READDATA, rbce);
#ifdef HAVE_CURLOPT_SEEKDATA
  curl_easy_setopt(curl, CURLOPT_SEEKDATA, rbce);
#endif

  /*
   * we need to set specific headers for the PUT to work... so
   * convert the internal headers structure to a HASH if one is set
   */
  if (!rb_easy_nil("headers")) {
    if (rb_easy_type_check("headers", T_ARRAY) || rb_easy_type_check("headers", T_STRING)) {
      rb_raise(rb_eRuntimeError, "Must set headers as a HASH to modify the headers in an PUT request");
    }
  }

  // exit fast if the payload is empty
  if (NIL_P(data)) { return data; }

  headers = rb_easy_get("headers");
  if( headers == Qnil ) {
    headers = rb_hash_new();
  }

  if (rb_respond_to(data, rb_intern("read"))) {
    VALUE stat = rb_funcall(data, rb_intern("stat"), 0);
    if( stat && rb_hash_aref(headers, rb_str_new2("Content-Length")) == Qnil) {
      VALUE size;
      if( rb_hash_aref(headers, rb_str_new2("Expect")) == Qnil ) {
        rb_hash_aset(headers, rb_str_new2("Expect"), rb_str_new2(""));
      }
      size = rb_funcall(stat, rb_intern("size"), 0);
      curl_easy_setopt(curl, CURLOPT_INFILESIZE, NUM2LONG(size));
    }
    else if( rb_hash_aref(headers, rb_str_new2("Content-Length")) == Qnil && rb_hash_aref(headers, rb_str_new2("Transfer-Encoding")) == Qnil ) {
      rb_hash_aset(headers, rb_str_new2("Transfer-Encoding"), rb_str_new2("chunked"));
    }
    else if( rb_hash_aref(headers, rb_str_new2("Content-Length")) ) {
      VALUE size = rb_funcall(rb_hash_aref(headers, rb_str_new2("Content-Length")), rb_intern("to_i"), 0);
      curl_easy_setopt(curl, CURLOPT_INFILESIZE, NUM2LONG(size));
    }
  }
  else if (rb_respond_to(data, rb_intern("to_s"))) {
    curl_easy_setopt(curl, CURLOPT_INFILESIZE, RSTRING_LEN(data));
    if( rb_hash_aref(headers, rb_str_new2("Expect")) == Qnil ) {
      rb_hash_aset(headers, rb_str_new2("Expect"), rb_str_new2(""));
    }
  }
  else {
    rb_raise(rb_eRuntimeError, "PUT data must respond to read or to_s");
  }
  rb_easy_set("headers",headers);

  // if we made it this far, all should be well.
  return data;
}

/*
 * call-seq:
 *   easy.ftp_commands = ["CWD /", "MKD directory"]   => ["CWD /", ...]
 *
 * Explicitly sets the list of commands to execute on the FTP server when calling perform.
 *
 * NOTE:
 * - This maps to libcurl CURLOPT_QUOTE; it sends commands on the control connection.
 * - Do not include data-transfer commands like LIST/NLST/RETR/STOR here. libcurl does not
 *   parse PASV/EPSV replies from QUOTE commands and will not establish the required data
 *   connection. For directory listings, set CURLOPT_DIRLISTONLY (via `easy.set(:dirlistonly, true)`)
 *   and request an FTP directory URL (e.g. "ftp://host/path/") so libcurl manages PASV/EPSV
 *   and the data connection for you.
 */
static VALUE ruby_curl_easy_ftp_commands_set(VALUE self, VALUE ftp_commands) {
  CURB_OBJECT_HSETTER(ruby_curl_easy, ftp_commands);
}

/*
 * call-seq:
 *   easy.ftp_commands                                => array or nil
 */
static VALUE ruby_curl_easy_ftp_commands_get(VALUE self) {
  CURB_OBJECT_HGETTER(ruby_curl_easy, ftp_commands);
}

/*
 * call-seq:
 *   easy.resolve = [ "example.com:80:127.0.0.1" ]   => [ "example.com:80:127.0.0.1" ]
 *
 * Set the resolve list to statically resolve hostnames to IP addresses,
 * bypassing DNS for matching hostname/port combinations.
 */
static VALUE ruby_curl_easy_resolve_set(VALUE self, VALUE resolve) {
  CURB_OBJECT_HSETTER(ruby_curl_easy, resolve);
}

/*
 * call-seq:
 *   easy.resolve                                => array or nil
 */
static VALUE ruby_curl_easy_resolve_get(VALUE self) {
  CURB_OBJECT_HGETTER(ruby_curl_easy, resolve);
}

/* ================== IMMED ATTRS ==================*/

/*
 * call-seq:
 *   easy.local_port = fixnum or nil                  => fixnum or nil
 *
 * Set the local port that will be used for the following +perform+ calls.
 *
 * Passing +nil+ will return to the default behaviour (no local port
 * preference).
 *
 * This option is ignored if compiled against libcurl < 7.15.2.
 */
static VALUE ruby_curl_easy_local_port_set(VALUE self, VALUE local_port) {
  CURB_IMMED_PORT_SETTER(ruby_curl_easy, local_port, "port");
}

/*
 * call-seq:
 *   easy.local_port                                  => fixnum or nil
 *
 * Obtain the local port that will be used for the following +perform+ calls.
 *
 * This option is ignored if compiled against libcurl < 7.15.2.
 */
static VALUE ruby_curl_easy_local_port_get(VALUE self) {
  CURB_IMMED_PORT_GETTER(ruby_curl_easy, local_port);
}

/*
 * call-seq:
 *   easy.local_port_range = fixnum or nil            => fixnum or nil
 *
 * Set the local port range that will be used for the following +perform+
 * calls. This is a number (between 0 and 65535) that determines how far
 * libcurl may deviate from the supplied +local_port+ in order to find
 * an available port.
 *
 * If you set +local_port+ it's also recommended that you set this, since
 * it is fairly likely that your specified port will be unavailable.
 *
 * This option is ignored if compiled against libcurl < 7.15.2.
 */
static VALUE ruby_curl_easy_local_port_range_set(VALUE self, VALUE local_port_range) {
  CURB_IMMED_PORT_SETTER(ruby_curl_easy, local_port_range, "port range");
}

/*
 * call-seq:
 *   easy.local_port_range                            => fixnum or nil
 *
 * Obtain the local port range that will be used for the following +perform+
 * calls.
 *
 * This option is ignored if compiled against libcurl < 7.15.2.
 */
static VALUE ruby_curl_easy_local_port_range_get(VALUE self) {
  CURB_IMMED_PORT_GETTER(ruby_curl_easy, local_port_range);
}

/*
 * call-seq:
 *   easy.proxy_port = fixnum or nil                  => fixnum or nil
 *
 * Set the proxy port that will be used for the following +perform+ calls.
 */
static VALUE ruby_curl_easy_proxy_port_set(VALUE self, VALUE proxy_port) {
  CURB_IMMED_PORT_SETTER(ruby_curl_easy, proxy_port, "port");
}

/*
 * call-seq:
 *   easy.proxy_port                                  => fixnum or nil
 *
 * Obtain the proxy port that will be used for the following +perform+ calls.
 */
static VALUE ruby_curl_easy_proxy_port_get(VALUE self) {
  CURB_IMMED_PORT_GETTER(ruby_curl_easy, proxy_port);
}

/*
 * call-seq:
 *   easy.proxy_type = fixnum or nil                  => fixnum or nil
 *
 * Set the proxy type that will be used for the following +perform+ calls.
 * This should be one of the Curl::CURLPROXY constants.
 */
static VALUE ruby_curl_easy_proxy_type_set(VALUE self, VALUE proxy_type) {
  CURB_IMMED_SETTER(ruby_curl_easy, proxy_type, -1);
}

/*
 * call-seq:
 *   easy.proxy_type                                  => fixnum or nil
 *
 * Obtain the proxy type that will be used for the following +perform+ calls.
 */
static VALUE ruby_curl_easy_proxy_type_get(VALUE self) {
  CURB_IMMED_GETTER(ruby_curl_easy, proxy_type, -1);
}

#if defined(HAVE_CURLAUTH_DIGEST_IE)
#define CURL_HTTPAUTH_STR_TO_NUM(node) \
  (!strncmp("basic",node,5)) ? CURLAUTH_BASIC : \
  (!strncmp("digest_ie",node,9)) ? CURLAUTH_DIGEST_IE : \
  (!strncmp("digest",node,6)) ? CURLAUTH_DIGEST : \
  (!strncmp("gssnegotiate",node,12)) ? CURLAUTH_GSSNEGOTIATE : \
  (!strncmp("ntlm",node,4)) ? CURLAUTH_NTLM : \
  (!strncmp("anysafe",node,7)) ? CURLAUTH_ANYSAFE : \
  (!strncmp("any",node,3)) ? CURLAUTH_ANY : 0
#else
#define CURL_HTTPAUTH_STR_TO_NUM(node) \
  (!strncmp("basic",node,5)) ? CURLAUTH_BASIC : \
  (!strncmp("digest",node,6)) ? CURLAUTH_DIGEST : \
  (!strncmp("gssnegotiate",node,12)) ? CURLAUTH_GSSNEGOTIATE : \
  (!strncmp("ntlm",node,4)) ? CURLAUTH_NTLM : \
  (!strncmp("anysafe",node,7)) ? CURLAUTH_ANYSAFE : \
  (!strncmp("any",node,3)) ? CURLAUTH_ANY : 0
#endif
/*
 * call-seq:
 *   easy.http_auth_types = fixnum or nil             => fixnum or nil
 *   easy.http_auth_types = [:basic,:digest,:digest_ie,:gssnegotiate, :ntlm, :any, :anysafe]
 *
 * Set the HTTP authentication types that may be used for the following
 * +perform+ calls. This is a bitmap made by ORing together the
 * Curl::CURLAUTH constants.
 */
static VALUE ruby_curl_easy_http_auth_types_set(int argc, VALUE *argv, VALUE self) {//VALUE self, VALUE http_auth_types) {
  ruby_curl_easy *rbce;
  VALUE args_ary;
  long i, len;
  char* node = NULL;
  long mask = 0;

  rb_scan_args(argc, argv, "*", &args_ary);
  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);

  len = RARRAY_LEN(args_ary);

  if (len == 1 && (rb_ary_entry(args_ary,0) == Qnil || TYPE(rb_ary_entry(args_ary,0)) == T_FIXNUM ||
        TYPE(rb_ary_entry(args_ary,0)) == T_BIGNUM)) {
    if (rb_ary_entry(args_ary,0) == Qnil) {
      rbce->http_auth_types = 0;
    }
    else {
      rbce->http_auth_types = NUM2LONG(rb_ary_entry(args_ary,0));
    }
  }
  else {
    // we could have multiple values, but they should be symbols
    node = RSTRING_PTR(rb_funcall(rb_ary_entry(args_ary,0),rb_intern("to_s"),0));
    mask = CURL_HTTPAUTH_STR_TO_NUM(node);
    for( i = 1; i < len; ++i ) {
      node = RSTRING_PTR(rb_funcall(rb_ary_entry(args_ary,i),rb_intern("to_s"),0));
      mask |= CURL_HTTPAUTH_STR_TO_NUM(node);
    }
    rbce->http_auth_types = mask;
  }
  return LONG2NUM(rbce->http_auth_types);
}

/*
 * call-seq:
 *   easy.http_auth_types                             => fixnum or nil
 *
 * Obtain the HTTP authentication types that may be used for the following
 * +perform+ calls.
 */
static VALUE ruby_curl_easy_http_auth_types_get(VALUE self) {
  CURB_IMMED_GETTER(ruby_curl_easy, http_auth_types, 0);
}

/*
 * call-seq:
 *   easy.proxy_auth_types = fixnum or nil            => fixnum or nil
 *
 * Set the proxy authentication types that may be used for the following
 * +perform+ calls. This is a bitmap made by ORing together the
 * Curl::CURLAUTH constants.
 */
static VALUE ruby_curl_easy_proxy_auth_types_set(VALUE self, VALUE proxy_auth_types) {
  CURB_IMMED_SETTER(ruby_curl_easy, proxy_auth_types, 0);
}

/*
 * call-seq:
 *   easy.proxy_auth_types                            => fixnum or nil
 *
 * Obtain the proxy authentication types that may be used for the following
 * +perform+ calls.
 */
static VALUE ruby_curl_easy_proxy_auth_types_get(VALUE self) {
  CURB_IMMED_GETTER(ruby_curl_easy, proxy_auth_types, 0);
}

/*
 * call-seq:
 *   easy.max_redirects = fixnum or nil               => fixnum or nil
 *
 * Set the maximum number of redirections to follow in the following +perform+
 * calls. Set to nil or -1 allow an infinite number (the default). Setting this
 * option only makes sense if +follow_location+ is also set true.
 *
 * With libcurl >= 7.15.1, setting this to 0 will cause libcurl to refuse any
 * redirect.
 */
static VALUE ruby_curl_easy_max_redirects_set(VALUE self, VALUE max_redirs) {
  CURB_IMMED_SETTER(ruby_curl_easy, max_redirs, -1);
}

/*
 * call-seq:
 *   easy.max_redirects                               => fixnum or nil
 *
 * Obtain the maximum number of redirections to follow in the following
 * +perform+ calls.
 */
static VALUE ruby_curl_easy_max_redirects_get(VALUE self) {
  CURB_IMMED_GETTER(ruby_curl_easy, max_redirs, -1);
}

/*
 * call-seq:
 *   easy.timeout = float, fixnum or nil              => numeric
 *
 * Set the maximum time in seconds that you allow the libcurl transfer
 * operation to take. Normally, name lookups can take a considerable time
 * and limiting operations to less than a few minutes risk aborting
 * perfectly normal operations.
 *
 * Set to nil (or zero) to disable timeout (it will then only timeout
 * on the system's internal timeouts).
 *
 * Uses timeout_ms internally instead of timeout because it allows for
 * better precision and libcurl will use the last set value when both
 * timeout and timeout_ms are set.
 *
 */
static VALUE ruby_curl_easy_timeout_set(VALUE self, VALUE timeout_s) {
  ruby_curl_easy *rbce;
  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);

  if (Qnil == timeout_s || NUM2DBL(timeout_s) <= 0.0) {
    rbce->timeout_ms = 0;
  } else {
    rbce->timeout_ms = (unsigned long)(NUM2DBL(timeout_s) * 1000);
  }

  return DBL2NUM(rbce->timeout_ms / 1000.0);
}

/*
 * call-seq:
 *   easy.timeout                                     => numeric
 *
 * Obtain the maximum time in seconds that you allow the libcurl transfer
 * operation to take.
 *
 * Uses timeout_ms internally instead of timeout.
 *
 */
static VALUE ruby_curl_easy_timeout_get(VALUE self) {
  ruby_curl_easy *rbce;
  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
  return DBL2NUM(rbce->timeout_ms / 1000.0);
}

/*
 * call-seq:
 *   easy.timeout_ms = fixnum or nil                  => fixnum or nil
 *
 * Set the maximum time in milliseconds that you allow the libcurl transfer
 * operation to take. Normally, name lookups can take a considerable time
 * and limiting operations to less than a few minutes risk aborting
 * perfectly normal operations.
 *
 * Set to nil (or zero) to disable timeout (it will then only timeout
 * on the system's internal timeouts).
 */
static VALUE ruby_curl_easy_timeout_ms_set(VALUE self, VALUE timeout_ms) {
  ruby_curl_easy *rbce;
  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);

  if (Qnil == timeout_ms || NUM2DBL(timeout_ms) <= 0.0) {
    rbce->timeout_ms = 0;
  } else {
    rbce->timeout_ms = NUM2ULONG(timeout_ms);
  }

  return ULONG2NUM(rbce->timeout_ms);
}

/*
 * call-seq:
 *   easy.timeout_ms                                  => fixnum or nil
 *
 * Obtain the maximum time in milliseconds that you allow the libcurl transfer
 * operation to take.
 */
static VALUE ruby_curl_easy_timeout_ms_get(VALUE self) {
  ruby_curl_easy *rbce;
  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
  return LONG2NUM(rbce->timeout_ms);
}

/*
 * call-seq:
 *   easy.connect_timeout = fixnum or nil             => fixnum or nil
 *
 * Set the maximum time in seconds that you allow the connection to the
 * server to take. This only limits the connection phase, once it has
 * connected, this option is of no more use.
 *
 * Set to nil (or zero) to disable connection timeout (it will then only
 * timeout on the system's internal timeouts).
 */
static VALUE ruby_curl_easy_connect_timeout_set(VALUE self, VALUE connect_timeout) {
  CURB_IMMED_SETTER(ruby_curl_easy, connect_timeout, 0);
}

/*
 * call-seq:
 *   easy.connect_timeout                             => fixnum or nil
 *
 * Obtain the maximum time in seconds that you allow the connection to the
 * server to take.
 */
static VALUE ruby_curl_easy_connect_timeout_get(VALUE self) {
  CURB_IMMED_GETTER(ruby_curl_easy, connect_timeout, 0);
}

/*
 * call-seq:
 *   easy.connect_timeout_ms = fixnum or nil          => fixnum or nil
 *
 * Set the maximum time in milliseconds that you allow the connection to the
 * server to take. This only limits the connection phase, once it has
 * connected, this option is of no more use.
 *
 * Set to nil (or zero) to disable connection timeout (it will then only
 * timeout on the system's internal timeouts).
 */
static VALUE ruby_curl_easy_connect_timeout_ms_set(VALUE self, VALUE connect_timeout_ms) {
  CURB_IMMED_SETTER(ruby_curl_easy, connect_timeout_ms, 0);
}

/*
 * call-seq:
 *   easy.connect_timeout_ms                          => fixnum or nil
 *
 * Obtain the maximum time in milliseconds that you allow the connection to the
 * server to take.
 */
static VALUE ruby_curl_easy_connect_timeout_ms_get(VALUE self) {
  CURB_IMMED_GETTER(ruby_curl_easy, connect_timeout_ms, 0);
}

/*
 * call-seq:
 *   easy.dns_cache_timeout = fixnum or nil           => fixnum or nil
 *
 * Set the dns cache timeout in seconds. Name resolves will be kept in
 * memory for this number of seconds. Set to zero (0) to completely disable
 * caching, or set to nil (or -1) to make the cached entries remain forever.
 * By default, libcurl caches this info for 60 seconds.
 */
static VALUE ruby_curl_easy_dns_cache_timeout_set(VALUE self, VALUE dns_cache_timeout) {
  CURB_IMMED_SETTER(ruby_curl_easy, dns_cache_timeout, -1);
}

/*
 * call-seq:
 *   easy.dns_cache_timeout                           => fixnum or nil
 *
 * Obtain the dns cache timeout in seconds.
 */
static VALUE ruby_curl_easy_dns_cache_timeout_get(VALUE self) {
  CURB_IMMED_GETTER(ruby_curl_easy, dns_cache_timeout, -1);
}

/*
 * call-seq:
 *   easy.ftp_response_timeout = fixnum or nil        => fixnum or nil
 *
 * Set a timeout period (in seconds) on the amount of time that the server
 * is allowed to take in order to generate a response message for a command
 * before the session is considered hung. While curl is waiting for a
 * response, this value overrides +timeout+. It is recommended that if used
 * in conjunction with +timeout+, you set +ftp_response_timeout+ to a value
 * smaller than +timeout+.
 *
 * Ignored if libcurl version is < 7.10.8.
 */
static VALUE ruby_curl_easy_ftp_response_timeout_set(VALUE self, VALUE ftp_response_timeout) {
  CURB_IMMED_SETTER(ruby_curl_easy, ftp_response_timeout, 0);
}

/*
 * call-seq:
 *   easy.ftp_response_timeout                        => fixnum or nil
 *
 * Obtain the maximum time that libcurl will wait for FTP command responses.
 */
static VALUE ruby_curl_easy_ftp_response_timeout_get(VALUE self) {
  CURB_IMMED_GETTER(ruby_curl_easy, ftp_response_timeout, 0);
}

/*
 * call-seq:
 *   easy.low_speed_limit = fixnum or nil        => fixnum or nil
 *
 * Set the transfer speed (in bytes per second) that the transfer should be
 * below during +low_speed_time+ seconds for the library to consider it too
 * slow and abort.
 */
static VALUE ruby_curl_easy_low_speed_limit_set(VALUE self, VALUE low_speed_limit) {
  CURB_IMMED_SETTER(ruby_curl_easy, low_speed_limit, 0);
}

/*
 * call-seq:
 *   easy.low_speed_limit                        => fixnum or nil
 *
 * Obtain the minimum transfer speed over +low_speed+time+ below which the
 * transfer will be aborted.
 */
static VALUE ruby_curl_easy_low_speed_limit_get(VALUE self) {
  CURB_IMMED_GETTER(ruby_curl_easy, low_speed_limit, 0);
}

/*
 * call-seq:
 *   easy.low_speed_time = fixnum or nil        => fixnum or nil
 *
 * Set the time (in seconds) that the transfer should be below the
 * +low_speed_limit+ for the library to consider it too slow and abort.
 */
static VALUE ruby_curl_easy_low_speed_time_set(VALUE self, VALUE low_speed_time) {
  CURB_IMMED_SETTER(ruby_curl_easy, low_speed_time, 0);
}

/*
 * call-seq:
 *   easy.low_speed_time                        => fixnum or nil
 *
 * Obtain the time that the transfer should be below +low_speed_limit+ for
 * the library to abort it.
 */
static VALUE ruby_curl_easy_low_speed_time_get(VALUE self) {
  CURB_IMMED_GETTER(ruby_curl_easy, low_speed_time, 0);
}

/*
 * call-seq:
 *   easy.max_send_speed_large = fixnum or nil        => fixnum or nil
 *
 * Set the maximal sending transfer speed (in bytes per second) 
 */
static VALUE ruby_curl_easy_max_send_speed_large_set(VALUE self, VALUE max_send_speed_large) {
  CURB_IMMED_SETTER(ruby_curl_easy, max_send_speed_large, 0);
}

/*
 * call-seq:
 *   easy.max_send_speed_large = fixnum or nil        => fixnum or nil
 *
 * Get the maximal sending transfer speed (in bytes per second) 
 */
static VALUE ruby_curl_easy_max_send_speed_large_get(VALUE self) {
  CURB_IMMED_GETTER(ruby_curl_easy, max_send_speed_large, 0);
}

/*
 * call-seq:
 *   easy.max_recv_speed_large = fixnum or nil        => fixnum or nil
 *
 * Set the maximal receiving transfer speed (in bytes per second) 
 */
static VALUE ruby_curl_easy_max_recv_speed_large_set(VALUE self, VALUE max_recv_speed_large) {
  CURB_IMMED_SETTER(ruby_curl_easy, max_recv_speed_large, 0);
}

/*
 * call-seq:
 *   easy.max_recv_speed_large = fixnum or nil        => fixnum or nil
 *
 * Get the maximal receiving transfer speed (in bytes per second) 
 */
static VALUE ruby_curl_easy_max_recv_speed_large_get(VALUE self) {
  CURB_IMMED_GETTER(ruby_curl_easy, max_recv_speed_large, 0);
}

/*
 * call-seq:
 *   easy.username = string                           => string
 *
 * Set the HTTP Authentication username.
 */
static VALUE ruby_curl_easy_username_set(VALUE self, VALUE username) {
#ifdef HAVE_CURLOPT_USERNAME
  CURB_OBJECT_HSETTER(ruby_curl_easy, username);
#else
  return Qnil;
#endif
}

/*
 * call-seq:
 *   easy.username                                    => string
 *
 * Get the current username
 */
static VALUE ruby_curl_easy_username_get(VALUE self) {
#ifdef HAVE_CURLOPT_USERNAME
  CURB_OBJECT_HGETTER(ruby_curl_easy, username);
#else
  return Qnil;
#endif
}

/*
 * call-seq:
 *   easy.password = string                           => string
 *
 * Set the HTTP Authentication password.
 */
static VALUE ruby_curl_easy_password_set(VALUE self, VALUE password) {
#ifdef HAVE_CURLOPT_PASSWORD
  CURB_OBJECT_HSETTER(ruby_curl_easy, password);
#else
  return Qnil;
#endif
}

/*
 * call-seq:
 *   easy.password                                    => string
 *
 * Get the current password
 */
static VALUE ruby_curl_easy_password_get(VALUE self) {
#ifdef HAVE_CURLOPT_PASSWORD
  CURB_OBJECT_HGETTER(ruby_curl_easy, password);
#else
  return Qnil;
#endif
}

/*
 * call-seq:
 *   easy.ssl_version = value                         => fixnum or nil
 *
 * Sets the version of SSL/TLS that libcurl will attempt to use. Valid
 * options are:
 *
 *   Curl::CURL_SSLVERSION_DEFAULT
 *   Curl::CURL_SSLVERSION_TLSv1 (TLS 1.x)
 *   Curl::CURL_SSLVERSION_SSLv2
 *   Curl::CURL_SSLVERSION_SSLv3
 *   Curl::CURL_SSLVERSION_TLSv1_0
 *   Curl::CURL_SSLVERSION_TLSv1_1
 *   Curl::CURL_SSLVERSION_TLSv1_2
 *   Curl::CURL_SSLVERSION_TLSv1_3
 */
static VALUE ruby_curl_easy_ssl_version_set(VALUE self, VALUE ssl_version) {
  CURB_IMMED_SETTER(ruby_curl_easy, ssl_version, -1);
}

/*
 * call-seq:
 *   easy.ssl_version                                 => fixnum
 *
 * Get the version of SSL/TLS that libcurl will attempt to use.
 */
static VALUE ruby_curl_easy_ssl_version_get(VALUE self) {
  CURB_IMMED_GETTER(ruby_curl_easy, ssl_version, -1);
}

/*
 * call-seq:
 *   easy.use_ssl = value                             => fixnum or nil
 *
 * Ensure libcurl uses SSL for FTP connections. Valid options are Curl::CURL_USESSL_NONE,
 * Curl::CURL_USESSL_TRY, Curl::CURL_USESSL_CONTROL, and Curl::CURL_USESSL_ALL.
 */
static VALUE ruby_curl_easy_use_ssl_set(VALUE self, VALUE use_ssl) {
  CURB_IMMED_SETTER(ruby_curl_easy, use_ssl, -1);
}

/*
 * call-seq:
 *   easy.use_ssl                                     => fixnum
 *
 * Get the desired level for using SSL on FTP connections.
 */
static VALUE ruby_curl_easy_use_ssl_get(VALUE self) {
  CURB_IMMED_GETTER(ruby_curl_easy, use_ssl, -1);
}

/*
 * call-seq:
 *   easy.ftp_filemethod = value                      => fixnum or nil
 *
 * Controls how libcurl reaches files on the server. Valid options are Curl::CURL_MULTICWD,
 * Curl::CURL_NOCWD, and Curl::CURL_SINGLECWD (see libcurl docs for CURLOPT_FTP_METHOD).
 */
static VALUE ruby_curl_easy_ftp_filemethod_set(VALUE self, VALUE ftp_filemethod) {
  CURB_IMMED_SETTER(ruby_curl_easy, ftp_filemethod, -1);
}

/*
 * call-seq:
 *   easy.ftp_filemethod                              => fixnum
 *
 * Get the configuration for how libcurl will reach files on the server.
 */
static VALUE ruby_curl_easy_ftp_filemethod_get(VALUE self) {
  CURB_IMMED_GETTER(ruby_curl_easy, ftp_filemethod, -1);
}

/* ================== BOOL ATTRS ===================*/

/*
 * call-seq:
 *   easy.proxy_tunnel = boolean                      => boolean
 *
 * Configure whether this Curl instance will use proxy tunneling.
 */
static VALUE ruby_curl_easy_proxy_tunnel_set(VALUE self, VALUE proxy_tunnel) {
  CURB_BOOLEAN_SETTER(ruby_curl_easy, proxy_tunnel);
}

/*
 * call-seq:
 *   easy.proxy_tunnel?                               => boolean
 *
 * Determine whether this Curl instance will use proxy tunneling.
 */
static VALUE ruby_curl_easy_proxy_tunnel_q(VALUE self) {
  CURB_BOOLEAN_GETTER(ruby_curl_easy, proxy_tunnel);
}

/*
 * call-seq:
 *   easy.fetch_file_time = boolean                   => boolean
 *
 * Configure whether this Curl instance will fetch remote file
 * times, if available.
 */
static VALUE ruby_curl_easy_fetch_file_time_set(VALUE self, VALUE fetch_file_time) {
  CURB_BOOLEAN_SETTER(ruby_curl_easy, fetch_file_time);
}

/*
 * call-seq:
 *   easy.fetch_file_time?                            => boolean
 *
 * Determine whether this Curl instance will fetch remote file
 * times, if available.
 */
static VALUE ruby_curl_easy_fetch_file_time_q(VALUE self) {
  CURB_BOOLEAN_GETTER(ruby_curl_easy, fetch_file_time);
}

/*
 * call-seq:
 *   easy.ssl_verify_peer = boolean                   => boolean
 *
 * Configure whether this Curl instance will verify the SSL peer
 * certificate. When true (the default), and the verification fails to
 * prove that the certificate is authentic, the connection fails. When
 * false, the connection succeeds regardless.
 *
 * Authenticating the certificate is not by itself very useful. You
 * typically want to ensure that the server, as authentically identified
 * by its certificate, is the server you mean to be talking to.
 * The ssl_verify_host? options controls that.
 */
static VALUE ruby_curl_easy_ssl_verify_peer_set(VALUE self, VALUE ssl_verify_peer) {
  CURB_BOOLEAN_SETTER(ruby_curl_easy, ssl_verify_peer);
}

/*
 * call-seq:
 *   easy.ssl_verify_peer?                            => boolean
 *
 * Determine whether this Curl instance will verify the SSL peer
 * certificate.
 */
static VALUE ruby_curl_easy_ssl_verify_peer_q(VALUE self) {
  CURB_BOOLEAN_GETTER(ruby_curl_easy, ssl_verify_peer);
}

/*
 * call-seq:
 *   easy.ssl_verify_host = [0, 1, 2]                   => [0, 1, 2]
 *
 * Configure whether this Curl instance will verify that the server cert
 * is for the server it is known as. When true (the default) the server
 * certificate must indicate that the server is the server to which you
 * meant to connect, or the connection fails. When false, the connection
 * will succeed regardless of the names in the certificate.
 *
 * this option controls is of the identity that the server claims.
 * The server could be lying. To control lying, see ssl_verify_peer? .
 */
static VALUE ruby_curl_easy_ssl_verify_host_set(VALUE self, VALUE ssl_verify_host) {
  CURB_IMMED_SETTER(ruby_curl_easy, ssl_verify_host, 0);
}

/*
 * call-seq:
 *   easy.ssl_verify_host                            => number
 *
 * Determine whether this Curl instance will verify that the server cert
 * is for the server it is known as.
 */
static VALUE ruby_curl_easy_ssl_verify_host_get(VALUE self) {
  CURB_IMMED_GETTER(ruby_curl_easy, ssl_verify_host, 0);
}

/*
 * call-seq:
 *   easy.header_in_body = boolean                    => boolean
 *
 * Configure whether this Curl instance will return HTTP headers
 * combined with body data. If this option is set true, both header
 * and body data will go to +body_str+ (or the configured +on_body+ handler).
 */
static VALUE ruby_curl_easy_header_in_body_set(VALUE self, VALUE header_in_body) {
  CURB_BOOLEAN_SETTER(ruby_curl_easy, header_in_body);
}

/*
 * call-seq:
 *   easy.header_in_body?                             => boolean
 *
 * Determine whether this Curl instance will return HTTP headers
 * combined with body data.
 */
static VALUE ruby_curl_easy_header_in_body_q(VALUE self) {
  CURB_BOOLEAN_GETTER(ruby_curl_easy, header_in_body);
}

/*
 * call-seq:
 *   easy.use_netrc = boolean                         => boolean
 *
 * Configure whether this Curl instance will use data from the user's
 * .netrc file for FTP connections.
 */
static VALUE ruby_curl_easy_use_netrc_set(VALUE self, VALUE use_netrc) {
  CURB_BOOLEAN_SETTER(ruby_curl_easy, use_netrc);
}

/*
 * call-seq:
 *   easy.use_netrc?                                  => boolean
 *
 * Determine whether this Curl instance will use data from the user's
 * .netrc file for FTP connections.
 */
static VALUE ruby_curl_easy_use_netrc_q(VALUE self) {
  CURB_BOOLEAN_GETTER(ruby_curl_easy, use_netrc);
}

/*
 * call-seq:
 *
 * easy = Curl::Easy.new
 * easy.autoreferer=true
 */
static VALUE ruby_curl_easy_autoreferer_set(VALUE self, VALUE autoreferer) {
  ruby_curl_easy *rbce;
  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);

  if (Qtrue == autoreferer) {
    curl_easy_setopt(rbce->curl, CURLOPT_AUTOREFERER, 1);
  }
  else {
    curl_easy_setopt(rbce->curl, CURLOPT_AUTOREFERER, 0);
  }

  return autoreferer;
}

/*
 * call-seq:
 *   easy.follow_location?                            => boolean
 *
 * Determine whether this Curl instance will follow Location: headers
 * in HTTP responses.
 */
static VALUE ruby_curl_easy_follow_location_q(VALUE self) {
  CURB_BOOLEAN_GETTER(ruby_curl_easy, follow_location);
}

/*
 * call-seq:
 *   easy.unrestricted_auth = boolean                 => boolean
 *
 * Configure whether this Curl instance may use any HTTP authentication
 * method available when necessary.
 */
static VALUE ruby_curl_easy_unrestricted_auth_set(VALUE self, VALUE unrestricted_auth) {
  CURB_BOOLEAN_SETTER(ruby_curl_easy, unrestricted_auth);
}

/*
 * call-seq:
 *   easy.unrestricted_auth?                          => boolean
 *
 * Determine whether this Curl instance may use any HTTP authentication
 * method available when necessary.
 */
static VALUE ruby_curl_easy_unrestricted_auth_q(VALUE self) {
  CURB_BOOLEAN_GETTER(ruby_curl_easy, unrestricted_auth);
}

/*
 * call-seq:
 *   easy.verbose = boolean                           => boolean
 *
 * Configure whether this Curl instance gives verbose output to STDERR
 * during transfers. Ignored if this instance has an on_debug handler.
 */
static VALUE ruby_curl_easy_verbose_set(VALUE self, VALUE verbose) {
  CURB_BOOLEAN_SETTER(ruby_curl_easy, verbose);
}

/*
 * call-seq:
 *   easy.verbose?                                    => boolean
 *
 * Determine whether this Curl instance gives verbose output to STDERR
 * during transfers.
 */
static VALUE ruby_curl_easy_verbose_q(VALUE self) {
  CURB_BOOLEAN_GETTER(ruby_curl_easy, verbose);
}

/*
 * call-seq:
 *   easy.multipart_form_post = boolean               => boolean
 *
 * Configure whether this Curl instance uses multipart/formdata content
 * type for HTTP POST requests. If this is false (the default), then the
 * application/x-www-form-urlencoded content type is used for the form
 * data.
 *
 * If this is set true, you must pass one or more PostField instances
 * to the http_post method - no support for posting multipart forms from
 * a string is provided.
 */
static VALUE ruby_curl_easy_multipart_form_post_set(VALUE self, VALUE multipart_form_post)
{
  CURB_BOOLEAN_SETTER(ruby_curl_easy, multipart_form_post);
}

/*
 * call-seq:
 *   easy.multipart_form_post?                        => boolean
 *
 * Determine whether this Curl instance uses multipart/formdata content
 * type for HTTP POST requests.
 */
static VALUE ruby_curl_easy_multipart_form_post_q(VALUE self) {
  CURB_BOOLEAN_GETTER(ruby_curl_easy, multipart_form_post);
}

/*
 * call-seq:
 *   easy.enable_cookies = boolean                    => boolean
 *
 * Configure whether the libcurl cookie engine is enabled for this Curl::Easy
 * instance. When enabled, cookies received via Set-Cookie are stored by libcurl
 * and automatically sent on subsequent matching requests. Use +easy.cookiefile+
 * to load cookies and +easy.cookiejar+ to persist them.
 *
 * This setting is independent from the manual Cookie header set via +easy.cookies+.
 * The manual header is additive and can be cleared by assigning an empty string.
 */
static VALUE ruby_curl_easy_enable_cookies_set(VALUE self, VALUE enable_cookies)
{
  CURB_BOOLEAN_SETTER(ruby_curl_easy, enable_cookies);
}

/*
 * call-seq:
 *   easy.enable_cookies?                             => boolean
 *
 * Determine whether the libcurl cookie engine is enabled for this
 * Curl::Easy instance.
 */
static VALUE ruby_curl_easy_enable_cookies_q(VALUE self) {
  CURB_BOOLEAN_GETTER(ruby_curl_easy, enable_cookies);
}

/*
 * call-seq:
 *   easy.ignore_content_length = boolean
 *
 * Configure whether this Curl::Easy instance should ignore the content
 * length header.
 */
static VALUE ruby_curl_easy_ignore_content_length_set(VALUE self, VALUE ignore_content_length)
{
  CURB_BOOLEAN_SETTER(ruby_curl_easy, ignore_content_length);
}

/*
 * call-seq:
 *   easy.ignore_content_length?                             => boolean
 *
 * Determine whether this Curl::Easy instance ignores the content
 * length header.
 */
static VALUE ruby_curl_easy_ignore_content_length_q(VALUE self) {
  CURB_BOOLEAN_GETTER(ruby_curl_easy, ignore_content_length);
}

/*
 * call-seq:
 *   easy.resolve_mode                                      => symbol
 *
 * Determines what type of IP address this Curl::Easy instance
 * resolves DNS names to.
 */
static VALUE ruby_curl_easy_resolve_mode(VALUE self) {
  ruby_curl_easy *rbce;
  unsigned short rm;
  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);

  rm = rbce->resolve_mode;

  switch(rm) {
    case CURL_IPRESOLVE_V4:
      return rb_easy_sym("ipv4");
    case CURL_IPRESOLVE_V6:
      return rb_easy_sym("ipv6");
    default:
      return rb_easy_sym("auto");
  }
}

/*
 * call-seq:
 *   easy.resolve_mode = symbol                             => symbol
 *
 * Configures what type of IP address this Curl::Easy instance
 * resolves DNS names to. Valid options are:
 *
 * [:auto]  resolves DNS names to all IP versions your system allows
 * [:ipv4]  resolves DNS names to IPv4 only
 * [:ipv6]  resolves DNS names to IPv6 only
 */
static VALUE ruby_curl_easy_resolve_mode_set(VALUE self, VALUE resolve_mode) {
  if (TYPE(resolve_mode) != T_SYMBOL) {
    rb_raise(rb_eTypeError, "Must pass a symbol");
    return Qnil;
  } else {
    ruby_curl_easy *rbce;
    ID resolve_mode_id;
    TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);

    resolve_mode_id = rb_to_id(resolve_mode);

    if (resolve_mode_id == rb_intern("auto")) {
      rbce->resolve_mode = CURL_IPRESOLVE_WHATEVER;
      return resolve_mode;
    } else if (resolve_mode_id == rb_intern("ipv4")) {
      rbce->resolve_mode = CURL_IPRESOLVE_V4;
      return resolve_mode;
    } else if (resolve_mode_id == rb_intern("ipv6")) {
      rbce->resolve_mode = CURL_IPRESOLVE_V6;
      return resolve_mode;
    } else {
      rb_raise(rb_eArgError, "Must set to one of :auto, :ipv4, :ipv6");
      return Qnil;
    }
  }
}

/*
 * call-seq:
 *   easy.http_version = Curl::HTTP_1_1               => Curl::HTTP_1_1
 *
 * Force libcurl to use a specific HTTP protocol version. By default libcurl
 * negotiates the highest version supported by both peers. Supported constants
 * include Curl::HTTP_NONE, Curl::HTTP_1_0, Curl::HTTP_1_1, Curl::HTTP_2_0,
 * Curl::HTTP_2TLS, and Curl::HTTP_2_PRIOR_KNOWLEDGE (when provided by libcurl).
 */
static VALUE ruby_curl_easy_http_version_set(VALUE self, VALUE version) {
  ruby_curl_easy *rbce;
  long http_version;

  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);

  if (NIL_P(version)) {
    http_version = CURL_HTTP_VERSION_NONE;
  } else {
    http_version = NUM2LONG(version);
  }

  rbce->http_version = http_version;

  return version;
}

/*
 * call-seq:
 *   easy.http_version                                => integer
 *
 * Returns the HTTP protocol version currently configured.
 */
static VALUE ruby_curl_easy_http_version_get(VALUE self) {
  ruby_curl_easy *rbce;

  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);

  return LONG2NUM(rbce->http_version);
}


/* ================= EVENT PROCS ================== */

/*
 * call-seq:
 *   easy.on_body { |body_data| ... }                 => <old handler>
 *
 * Assign or remove the +on_body+ handler for this Curl::Easy instance.
 * To remove a previously-supplied handler, call this method with no
 * attached block.
 *
 * The +on_body+ handler is called for each chunk of response body passed back
 * by libcurl during +perform+. It should perform any processing necessary,
 * and return the actual number of bytes handled. Normally, this will
 * equal the length of the data string, and CURL will continue processing.
 * If the returned length does not equal the input length, CURL will abort
 * the processing with a Curl::Err::AbortedByCallbackError.
 */
static VALUE ruby_curl_easy_on_body_set(int argc, VALUE *argv, VALUE self) {
  CURB_HANDLER_PROC_HSETTER(ruby_curl_easy, body_proc);
}

/*
 * call-seq:
 *   easy.on_success { |easy| ... }                   => <old handler>
 *
 * Assign or remove the +on_success+ handler for this Curl::Easy instance.
 * To remove a previously-supplied handler, call this method with no
 * attached block.
 *
 * The +on_success+ handler is called when the request is finished with a
 * status of 20x
 */
static VALUE ruby_curl_easy_on_success_set(int argc, VALUE *argv, VALUE self) {
  CURB_HANDLER_PROC_HSETTER(ruby_curl_easy, success_proc);
}

/*
 * call-seq:
 *   easy.on_failure {|easy,code| ... }               => <old handler>
 *
 * Assign or remove the +on_failure+ handler for this Curl::Easy instance.
 * To remove a previously-supplied handler, call this method with no
 * attached block.
 *
 * The +on_failure+ handler is called when the request is finished with a
 * status of 50x
 */
static VALUE ruby_curl_easy_on_failure_set(int argc, VALUE *argv, VALUE self) {
  CURB_HANDLER_PROC_HSETTER(ruby_curl_easy, failure_proc);
}

/*
 * call-seq:
 *  easy.on_missing {|easy,code| ... }                => <old handler;>
 *
 *  Assign or remove the on_missing handler for this Curl::Easy instance.
 *  To remove a previously-supplied handler, call this method with no attached
 *  block.
 *
 *  The +on_missing+ handler is called when request is finished with a
 *  status of 40x
 */
static VALUE ruby_curl_easy_on_missing_set(int argc, VALUE *argv, VALUE self) {
  CURB_HANDLER_PROC_HSETTER(ruby_curl_easy, missing_proc);
}

/*
 * call-seq:
 *  easy.on_redirect {|easy,code| ... }                => <old handler;>
 *
 *  Assign or remove the on_redirect handler for this Curl::Easy instance.
 *  To remove a previously-supplied handler, call this method with no attached
 *  block.
 *
 *  The +on_redirect+ handler is called when request is finished with a
 *  status of 30x
 */
static VALUE ruby_curl_easy_on_redirect_set(int argc, VALUE *argv, VALUE self) {
  CURB_HANDLER_PROC_HSETTER(ruby_curl_easy, redirect_proc);
}

/*
 * call-seq:
 *   easy.on_complete {|easy| ... }                   => <old handler>
 *
 * Assign or remove the +on_complete+ handler for this Curl::Easy instance.
 * To remove a previously-supplied handler, call this method with no
 * attached block.
 *
 * The +on_complete+ handler is called when the request is finished.
 */
static VALUE ruby_curl_easy_on_complete_set(int argc, VALUE *argv, VALUE self) {
  CURB_HANDLER_PROC_HSETTER(ruby_curl_easy, complete_proc);
}

/*
 * call-seq:
 *   easy.on_header { |header_data| ... }             => <old handler>
 *
 * Assign or remove the +on_header+ handler for this Curl::Easy instance.
 * To remove a previously-supplied handler, call this method with no
 * attached block.
 *
 * The +on_header+ handler is called for each chunk of response header passed
 * back by libcurl during +perform+. The semantics are the same as for the
 * block supplied to +on_body+.
 */
static VALUE ruby_curl_easy_on_header_set(int argc, VALUE *argv, VALUE self) {
  CURB_HANDLER_PROC_HSETTER(ruby_curl_easy, header_proc);
}

/*
 * call-seq:
 *   easy.on_progress { |dl_total, dl_now, ul_total, ul_now| ... } => <old handler>
 *
 * Assign or remove the +on_progress+ handler for this Curl::Easy instance.
 * To remove a previously-supplied handler, call this method with no
 * attached block.
 *
 * The +on_progress+ handler is called regularly by libcurl (approximately once
 * per second) during transfers to allow the application to receive progress
 * information. There is no guarantee that the reported progress will change
 * between calls.
 *
 * The result of the block call determines whether libcurl continues the transfer.
 * Returning a non-true value (i.e. nil or false) will cause the transfer to abort,
 * throwing a Curl::Err::AbortedByCallbackError.
 */
static VALUE ruby_curl_easy_on_progress_set(int argc, VALUE *argv, VALUE self) {
  CURB_HANDLER_PROC_HSETTER(ruby_curl_easy, progress_proc);
}

/*
 * call-seq:
 *   easy.on_debug { |type, data| ... }               => <old handler>
 *
 * Assign or remove the +on_debug+ handler for this Curl::Easy instance.
 * To remove a previously-supplied handler, call this method with no
 * attached block.
 *
 * The +on_debug+ handler, if configured, will receive detailed information
 * from libcurl during the perform call. This can be useful for debugging.
 * Setting a debug handler overrides libcurl's internal handler, disabling
 * any output from +verbose+, if set.
 *
 * The type argument will match one of the Curl::Easy::CURLINFO_XXXX
 * constants, and specifies the kind of information contained in the
 * data. The data is passed as a String.
 */
static VALUE ruby_curl_easy_on_debug_set(int argc, VALUE *argv, VALUE self) {
  CURB_HANDLER_PROC_HSETTER(ruby_curl_easy, debug_proc);
}


/* =================== PERFORM =====================*/

/***********************************************
 * This is an rb_iterate callback used to set up http headers.
 */
static VALUE cb_each_http_header(VALUE header, VALUE wrap, int _c, const VALUE *_ptr, VALUE unused) {
  struct curl_slist **list;
  VALUE header_str = Qnil;

  TypedData_Get_Struct(wrap, struct curl_slist *, &curl_slist_ptr_type, list);

  //rb_p(header);

  if (rb_type(header) == T_ARRAY) {
    // we're processing a hash, header is [name, val]
    VALUE name, value;

    name = rb_obj_as_string(rb_ary_entry(header, 0));
    value = rb_obj_as_string(rb_ary_entry(header, 1));
    if (rb_str_strlen(value) == 0) { // removing the header e.g. Accept: with nothing trailing should remove it see: https://curl.se/libcurl/c/CURLOPT_HTTPHEADER.html
      header_str = rb_str_plus(name, rb_str_new2(":"));
    } else {
      // This is a bit inefficient, but we don't want to be modifying
      // the actual values in the original hash.
      header_str = rb_str_plus(name, rb_str_new2(": "));
      header_str = rb_str_plus(header_str, value);
    }
  } else {
    header_str = rb_obj_as_string(header);
  }

  //rb_p(header_str);

  struct curl_slist *new_list = curl_slist_append(*list, StringValuePtr(header_str));
  if (!new_list) {
    rb_raise(rb_eNoMemError, "Failed to append to header list");
  }
  *list = new_list;
  return header_str;
}

/***********************************************
 * This is an rb_iterate callback used to set up http proxy headers.
 */
static VALUE cb_each_http_proxy_header(VALUE proxy_header, VALUE wrap, int _c, const VALUE *_ptr, VALUE unused) {
  struct curl_slist **list;
  VALUE proxy_header_str = Qnil;

  TypedData_Get_Struct(wrap, struct curl_slist *, &curl_slist_ptr_type, list);

  //rb_p(proxy_header);

  if (rb_type(proxy_header) == T_ARRAY) {
    // we're processing a hash, proxy header is [name, val]
    VALUE name, value;

    name = rb_obj_as_string(rb_ary_entry(proxy_header, 0));
    value = rb_obj_as_string(rb_ary_entry(proxy_header, 1));

    // This is a bit inefficient, but we don't want to be modifying
    // the actual values in the original hash.
    proxy_header_str = rb_str_plus(name, rb_str_new2(": "));
    proxy_header_str = rb_str_plus(proxy_header_str, value);
  } else {
    proxy_header_str = rb_obj_as_string(proxy_header);
  }

  //rb_p(header_str);

  struct curl_slist *new_list = curl_slist_append(*list, StringValuePtr(proxy_header_str));
  if (!new_list) {
    rb_raise(rb_eNoMemError, "Failed to append to proxy header list");
  }
  *list = new_list;
  return proxy_header_str;
}

/***********************************************
 * This is an rb_iterate callback used to set up ftp commands.
 */
static VALUE cb_each_ftp_command(VALUE ftp_command, VALUE wrap, int _c, const VALUE *_ptr, VALUE unused) {
  struct curl_slist **list;
  VALUE ftp_command_string;
  TypedData_Get_Struct(wrap, struct curl_slist *, &curl_slist_ptr_type, list);

  ftp_command_string = rb_obj_as_string(ftp_command);
  struct curl_slist *new_list = curl_slist_append(*list, StringValuePtr(ftp_command));
  if (!new_list) {
    rb_raise(rb_eNoMemError, "Failed to append to FTP command list");
  }
  *list = new_list;

  return ftp_command_string;
}

/***********************************************
 * This is an rb_iterate callback used to set up the resolve list.
 */
static VALUE cb_each_resolve(VALUE resolve, VALUE wrap, int _c, const VALUE *_ptr, VALUE unused) {
  struct curl_slist **list;
  VALUE resolve_string;
  TypedData_Get_Struct(wrap, struct curl_slist *, &curl_slist_ptr_type, list);

  resolve_string = rb_obj_as_string(resolve);
  struct curl_slist *new_list = curl_slist_append(*list, StringValuePtr(resolve));
  if (!new_list) {
    rb_raise(rb_eNoMemError, "Failed to append to resolve list");
  }
  *list = new_list;

  return resolve_string;
}

/***********************************************
 *
 * Setup a connection
 *
 * Always returns Qtrue, rb_raise on error.
 */
VALUE ruby_curl_easy_setup(ruby_curl_easy *rbce) {
  // TODO this could do with a bit of refactoring...
  CURL *curl;
  VALUE url, _url = rb_easy_get("url");
  struct curl_slist **hdrs = &(rbce->curl_headers);
  struct curl_slist **phdrs = &(rbce->curl_proxy_headers);
  struct curl_slist **cmds = &(rbce->curl_ftp_commands);
  struct curl_slist **rslv = &(rbce->curl_resolve);

  curl = rbce->curl;
  rbce->callback_error = Qnil;

  if (_url == Qnil) {
    rb_raise(eCurlErrError, "No URL supplied");
  }

  url = rb_check_string_type(_url);
  curl_easy_setopt(curl, CURLOPT_URL, StringValuePtr(url));

  // network stuff and auth
  if (!rb_easy_nil("interface_hm")) {
    curl_easy_setopt(curl, CURLOPT_INTERFACE, rb_easy_get_str("interface_hm"));
  } else {
    curl_easy_setopt(curl, CURLOPT_INTERFACE, NULL);
  }

#if defined(HAVE_CURLOPT_USERNAME) && defined(HAVE_CURLOPT_PASSWORD)
  if (!rb_easy_nil("username")) {
    curl_easy_setopt(curl, CURLOPT_USERNAME, rb_easy_get_str("username"));
  } else {
    curl_easy_setopt(curl, CURLOPT_USERNAME, NULL);
  }
  if (!rb_easy_nil("password")) {
    curl_easy_setopt(curl, CURLOPT_PASSWORD, rb_easy_get_str("password"));
  }
  else {
    curl_easy_setopt(curl, CURLOPT_PASSWORD, NULL);
  }
#endif

  if (!rb_easy_nil("userpwd")) {
    curl_easy_setopt(curl, CURLOPT_USERPWD, rb_easy_get_str("userpwd"));
#if defined(HAVE_CURLOPT_USERNAME) && defined(HAVE_CURLOPT_PASSWORD)
  } else if (rb_easy_nil("username") && rb_easy_nil("password")) { /* don't set this even to NULL if we have set username and password */
#else
  } else {
#endif
    curl_easy_setopt(curl, CURLOPT_USERPWD, NULL);
  }

  if (rb_easy_nil("proxy_url")) {
    curl_easy_setopt(curl, CURLOPT_PROXY, NULL);
  } else {
    curl_easy_setopt(curl, CURLOPT_PROXY, rb_easy_get_str("proxy_url"));
  }

  if (rb_easy_nil("proxypwd")) {
    curl_easy_setopt(curl, CURLOPT_PROXYUSERPWD, NULL);
  } else {
    curl_easy_setopt(curl, CURLOPT_PROXYUSERPWD, rb_easy_get_str("proxypwd"));
  }

#ifdef HAVE_CURLOPT_NOPROXY
  if (rb_easy_nil("noproxy")) {
    curl_easy_setopt(curl, CURLOPT_NOPROXY, NULL);
  } else {
    curl_easy_setopt(curl, CURLOPT_NOPROXY, rb_easy_get_str("noproxy"));
  }
#endif

  // body/header procs
  if (!rb_easy_nil("body_proc")) {
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, (curl_write_callback)&proc_data_handler_body);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, rbce);
    /* clear out the body_data if it was set */
    rb_easy_del("body_data");
  } else {
    rb_easy_set("body_data", rb_str_buf_new(32768));
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, (curl_write_callback)&default_body_handler);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, rbce);
  }

  if (!rb_easy_nil("header_proc")) {
    curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, (curl_write_callback)&proc_data_handler_header);
    curl_easy_setopt(curl, CURLOPT_HEADERDATA, rbce);
    /* clear out the header_data if it was set */
    rb_easy_del("header_data");
  } else {
    rb_easy_set("header_data", rb_str_buf_new(16384));
    curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, (curl_write_callback)&default_header_handler);
    curl_easy_setopt(curl, CURLOPT_HEADERDATA, rbce);
  }

  /* encoding */
  if (!rb_easy_nil("encoding")) {
    curl_easy_setopt(curl, CURLOPT_ENCODING, rb_easy_get_str("encoding"));
  }

  // progress and debug procs
  if (!rb_easy_nil("progress_proc")) {
#ifdef HAVE_CURLOPT_XFERINFOFUNCTION
    curl_easy_setopt(curl, CURLOPT_XFERINFOFUNCTION, &proc_xferinfo_handler);
    curl_easy_setopt(curl, CURLOPT_XFERINFODATA, rbce);
#else
    curl_easy_setopt(curl, CURLOPT_PROGRESSFUNCTION, (curl_progress_callback)&proc_progress_handler);
    curl_easy_setopt(curl, CURLOPT_PROGRESSDATA, rbce);
#endif
    curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0);
  } else {
    curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 1);
#ifdef HAVE_CURLOPT_XFERINFOFUNCTION
    curl_easy_setopt(curl, CURLOPT_XFERINFODATA, rbce);
#else
    curl_easy_setopt(curl, CURLOPT_PROGRESSDATA, rbce);
#endif
  }

  if (!rb_easy_nil("debug_proc")) {
    curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION, (curl_debug_callback)&proc_debug_handler);
    curl_easy_setopt(curl, CURLOPT_DEBUGDATA, rbce);
    curl_easy_setopt(curl, CURLOPT_VERBOSE, 1);
  } else {
    // have to remove handler to re-enable standard verbosity
    curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION, NULL);
    curl_easy_setopt(curl, CURLOPT_DEBUGDATA, rbce);
    curl_easy_setopt(curl, CURLOPT_VERBOSE, rbce->verbose);
  }

  /* general opts */

  curl_easy_setopt(curl, CURLOPT_HEADER, rbce->header_in_body);
  curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, rbce->follow_location);
#if LIBCURL_VERSION_NUM == 0x081000
  /* Work around 8.16.0 regression that clamps -1 (infinite) to zero */
  if (rbce->max_redirs >= 0) {
    curl_easy_setopt(curl, CURLOPT_MAXREDIRS, rbce->max_redirs);
  }
#else
  curl_easy_setopt(curl, CURLOPT_MAXREDIRS, rbce->max_redirs);
#endif

  curl_easy_setopt(curl, CURLOPT_HTTPPROXYTUNNEL, rbce->proxy_tunnel);
  curl_easy_setopt(curl, CURLOPT_FILETIME, rbce->fetch_file_time);
  curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, rbce->ssl_verify_peer);
  curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, rbce->ssl_verify_host);

  if ((rbce->use_netrc != Qnil) && (rbce->use_netrc != Qfalse)) {
    curl_easy_setopt(curl, CURLOPT_NETRC, CURL_NETRC_OPTIONAL);
  } else {
    curl_easy_setopt(curl, CURLOPT_NETRC, CURL_NETRC_IGNORED);
  }

  curl_easy_setopt(curl, CURLOPT_UNRESTRICTED_AUTH, rbce->unrestricted_auth);

#ifdef HAVE_CURLOPT_TIMEOUT_MS
  curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, rbce->timeout_ms);
#endif

  curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, rbce->connect_timeout);
#ifdef HAVE_CURLOPT_CONNECTTIMEOUT_MS
  curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, rbce->connect_timeout_ms);
#endif
  curl_easy_setopt(curl, CURLOPT_DNS_CACHE_TIMEOUT, rbce->dns_cache_timeout);

  curl_easy_setopt(curl, CURLOPT_IGNORE_CONTENT_LENGTH, rbce->ignore_content_length);

  curl_easy_setopt(curl, CURLOPT_IPRESOLVE, rbce->resolve_mode);
#if HAVE_CURLOPT_HTTP_VERSION
  curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, rbce->http_version);
#endif


#if LIBCURL_VERSION_NUM >= 0x070a08
  curl_easy_setopt(curl, CURLOPT_FTP_RESPONSE_TIMEOUT, rbce->ftp_response_timeout);
#else
  if (rbce->ftp_response_timeout > 0) {
    rb_warn("Installed libcurl is too old to support ftp_response_timeout");
  }
#endif

  curl_easy_setopt(curl, CURLOPT_LOW_SPEED_LIMIT, rbce->low_speed_limit);
  curl_easy_setopt(curl, CURLOPT_LOW_SPEED_TIME, rbce->low_speed_time);

  curl_easy_setopt(curl, CURLOPT_MAX_RECV_SPEED_LARGE, rbce->max_recv_speed_large);
  curl_easy_setopt(curl, CURLOPT_MAX_SEND_SPEED_LARGE, rbce->max_send_speed_large);

  // Set up localport / proxy port
  // FIXME these won't get returned to default if they're unset Ruby
  if (rbce->proxy_port > 0) {
    curl_easy_setopt(curl, CURLOPT_PROXYPORT, rbce->proxy_port);
  }

  if (rbce->local_port > 0) {
#if LIBCURL_VERSION_NUM >= 0x070f02
    curl_easy_setopt(curl, CURLOPT_LOCALPORT, rbce->local_port);

    if (rbce->local_port_range > 0) {
      curl_easy_setopt(curl, CURLOPT_LOCALPORTRANGE, rbce->local_port_range);
    }
#else
    rb_warn("Installed libcurl is too old to support local_port");
#endif
  }

  if (rbce->proxy_type != -1) {
#if LIBCURL_VERSION_NUM >= 0x070a00
    if (rbce->proxy_type == -2) {
      rb_warn("Installed libcurl is too old to support the selected proxy type");
    } else {
      curl_easy_setopt(curl, CURLOPT_PROXYTYPE, rbce->proxy_type);
    }
  } else {
    curl_easy_setopt(curl, CURLOPT_PROXYTYPE, CURLPROXY_HTTP);
#else
    rb_warn("Installed libcurl is too old to support proxy_type");
#endif
  }

  /*
   * NOTE: we used to set CURLAUTH_ANY but see: http://curl.haxx.se/mail/lib-2015-06/0033.html
   */
  if (rbce->http_auth_types != 0) {
#if LIBCURL_VERSION_NUM >= 0x070a06
    curl_easy_setopt(curl, CURLOPT_HTTPAUTH, rbce->http_auth_types);
#else
    rb_warn("Installed libcurl is too old to support http_auth_types");
#endif
  }

  if (rbce->proxy_auth_types != 0) {
#if LIBCURL_VERSION_NUM >= 0x070a07
    curl_easy_setopt(curl, CURLOPT_PROXYAUTH, rbce->proxy_auth_types);
#else
    rb_warn("Installed libcurl is too old to support proxy_auth_types");
#endif
  }

  /* Set up HTTP cookie handling if necessary */
  /* Enable/attach cookie engine if requested, or implicitly via COOKIELIST usage */
  if (rbce->enable_cookies) {
    if (!rb_easy_nil("cookiejar")) {
      curl_easy_setopt(curl, CURLOPT_COOKIEJAR, rb_easy_get_str("cookiejar"));
    }

    if (!rb_easy_nil("cookiefile")) {
      curl_easy_setopt(curl, CURLOPT_COOKIEFILE, rb_easy_get_str("cookiefile"));
    } else {
      curl_easy_setopt(curl, CURLOPT_COOKIEFILE, ""); /* "" = magic to just enable */
    }
  } else if (rbce->cookielist_engine_enabled) {
    /* Ensure cookie engine is enabled even if enable_cookies? is false. */
    curl_easy_setopt(curl, CURLOPT_COOKIEFILE, "");
  }

  if (!rb_easy_nil("cookies")) {
    curl_easy_setopt(curl, CURLOPT_COOKIE, rb_easy_get_str("cookies"));
  }

  /* Set up HTTPS cert handling if necessary */
  if (!rb_easy_nil("cert")) {
    if (!rb_easy_nil("certtype")) {
      curl_easy_setopt(curl, CURLOPT_SSLCERTTYPE, rb_easy_get_str("certtype"));
    }
    curl_easy_setopt(curl, CURLOPT_SSLCERT, rb_easy_get_str("cert"));
    if (!rb_easy_nil("certpassword")) {
      curl_easy_setopt(curl, CURLOPT_SSLCERTPASSWD, rb_easy_get_str("certpassword"));
    }
    if (!rb_easy_nil("cert_key")) {
      curl_easy_setopt(curl, CURLOPT_SSLKEY, rb_easy_get_str("cert_key"));
    }
  }

  if (!rb_easy_nil("cacert")) {
    curl_easy_setopt(curl, CURLOPT_CAINFO, rb_easy_get_str("cacert"));
  }
#ifdef HAVE_CURL_CONFIG_CA
  else {
    curl_easy_setopt(curl, CURLOPT_CAINFO, CURL_CONFIG_CA);
  }
#endif

#ifdef CURL_VERSION_SSL
  if (rbce->ssl_version > 0) {
    curl_easy_setopt(curl, CURLOPT_SSLVERSION, rbce->ssl_version);
  }

  if (rbce->use_ssl > 0) {
    curl_easy_setopt(curl, CURB_FTPSSL, rbce->use_ssl);
  }
#else
  if (rbce->ssl_version > 0 || rbce->use_ssl > 0) {
    rb_warn("libcurl is not configured with SSL support");
  }
#endif

  if (rbce->ftp_filemethod > 0) {
    curl_easy_setopt(curl, CURLOPT_FTP_FILEMETHOD, rbce->ftp_filemethod);
  }

  /* Set the user-agent string if specified */
  if (!rb_easy_nil("useragent")) {
    curl_easy_setopt(curl, CURLOPT_USERAGENT, rb_easy_get_str("useragent"));
  }

  /* Setup HTTP headers if necessary */
  curl_easy_setopt(curl, CURLOPT_HTTPHEADER, NULL);   // XXX: maybe we shouldn't be clearing this?

  if (!rb_easy_nil("headers")) {
    if (rb_easy_type_check("headers", T_ARRAY) || rb_easy_type_check("headers", T_HASH)) {
      VALUE wrap = TypedData_Wrap_Struct(rb_cObject, &curl_slist_ptr_type, hdrs);
      rb_block_call(rb_easy_get("headers"), rb_intern("each"), 0, NULL, cb_each_http_header, wrap);
    } else {
      VALUE headers_str = rb_obj_as_string(rb_easy_get("headers"));
      struct curl_slist *new_list = curl_slist_append(*hdrs, StringValuePtr(headers_str));
      if (!new_list) {
        rb_raise(rb_eNoMemError, "Failed to append to headers list");
      }
      *hdrs = new_list;
    }

    if (*hdrs) {
      curl_easy_setopt(curl, CURLOPT_HTTPHEADER, *hdrs);
    }
  }

#ifdef HAVE_CURLOPT_PROXYHEADER
  /* Setup HTTP proxy headers if necessary */
  curl_easy_setopt(curl, CURLOPT_PROXYHEADER, NULL);   // XXX: maybe we shouldn't be clearing this?

  if (!rb_easy_nil("proxy_headers")) {
    if (rb_easy_type_check("proxy_headers", T_ARRAY) || rb_easy_type_check("proxy_headers", T_HASH)) {
      VALUE wrap = TypedData_Wrap_Struct(rb_cObject, &curl_slist_ptr_type, phdrs);
      rb_block_call(rb_easy_get("proxy_headers"), rb_intern("each"), 0, NULL, cb_each_http_proxy_header, wrap);
    } else {
      VALUE proxy_headers_str = rb_obj_as_string(rb_easy_get("proxy_headers"));
      struct curl_slist *new_list = curl_slist_append(*phdrs, StringValuePtr(proxy_headers_str));
      if (!new_list) {
        rb_raise(rb_eNoMemError, "Failed to append to proxy headers list");
      }
      *phdrs = new_list;
    }

    if (*phdrs) {
      curl_easy_setopt(curl, CURLOPT_PROXYHEADER, *phdrs);
    }
  }
#endif

  /* Setup FTP commands if necessary */
  if (!rb_easy_nil("ftp_commands")) {
    if (rb_easy_type_check("ftp_commands", T_ARRAY)) {
      VALUE wrap = TypedData_Wrap_Struct(rb_cObject, &curl_slist_ptr_type, cmds);
      rb_block_call(rb_easy_get("ftp_commands"), rb_intern("each"), 0, NULL, cb_each_ftp_command, wrap);
    }

    if (*cmds) {
      curl_easy_setopt(curl, CURLOPT_QUOTE, *cmds);
    }
  }

#ifdef HAVE_CURLOPT_RESOLVE
  /* Setup resolve list if necessary */
  if (!rb_easy_nil("resolve")) {
    if (rb_easy_type_check("resolve", T_ARRAY)) {
      VALUE wrap = TypedData_Wrap_Struct(rb_cObject, &curl_slist_ptr_type, rslv);
      rb_block_call(rb_easy_get("resolve"), rb_intern("each"), 0, NULL, cb_each_resolve, wrap);
    }

    if (*rslv) {
      curl_easy_setopt(curl, CURLOPT_RESOLVE, *rslv);
    }
  }
#endif

  return Qnil;
}
/***********************************************
 *
 * Clean up a connection
 *
 * Always returns Qnil.
 */
VALUE ruby_curl_easy_cleanup( VALUE self, ruby_curl_easy *rbce ) {

  CURL *curl = rbce->curl;
  struct curl_slist *ftp_commands;
  struct curl_slist *resolve;

  ruby_curl_easy_clear_headers_list(rbce);
  ruby_curl_easy_clear_proxy_headers_list(rbce);

  ftp_commands = rbce->curl_ftp_commands;
  if (ftp_commands) {
    ruby_curl_easy_clear_ftp_commands_list(rbce);
  }

  resolve = rbce->curl_resolve;
  if (resolve) {
    ruby_curl_easy_clear_resolve_list(rbce);
  }

  /* clean up a PUT request's curl options. */
  if (!rb_easy_nil("upload")) {
    rb_easy_del("upload"); // set the upload object to Qnil to let the GC clean up
    curl_easy_setopt(curl, CURLOPT_UPLOAD, 0);
    curl_easy_setopt(curl, CURLOPT_READFUNCTION, NULL);
    curl_easy_setopt(curl, CURLOPT_READDATA, NULL);
    curl_easy_setopt(curl, CURLOPT_INFILESIZE, 0);
  }

  // set values on cleanup to nil
  //rb_easy_del("multi");

  return Qnil;
}

/*
 * Common implementation of easy.http(verb) and easy.http_delete
 */
static VALUE ruby_curl_easy_perform_verb_str(VALUE self, const char *verb) {
  ruby_curl_easy *rbce;
  CURL *curl;
  VALUE retval;

  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
  curl = rbce->curl;

  memset(rbce->err_buf, 0, CURL_ERROR_SIZE);

  /* Use method override and adjust related options for special verbs. */
  curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, verb);

  /* For HEAD, ensure no body is requested/downloaded, as some servers
   * include a Content-Length header which should not cause libcurl to
   * wait for a body that will never arrive. */
  int is_head = (verb && (
#ifdef _WIN32
    _stricmp(verb, "HEAD") == 0
#else
    strcasecmp(verb, "HEAD") == 0
#endif
  ));
  if (is_head) {
    curl_easy_setopt(curl, CURLOPT_NOBODY, 1L);
    curl_easy_setopt(curl, CURLOPT_HTTPGET, 0L);
    curl_easy_setopt(curl, CURLOPT_POST, 0L);
  }

  retval = rb_funcall(self, rb_intern("perform"), 0);

  /* Restore state after request. */
  if (is_head) {
    curl_easy_setopt(curl, CURLOPT_NOBODY, 0L);
  }
  curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, NULL);

  return retval;
}

/*
 * call-seq:
 *   easy.http(verb)
 *
 * Send an HTTP request with method set to verb, using the current options set for this Curl::Easy instance.
 * This method always returns true or raises an exception (defined under Curl::Err) on error.
 */
static VALUE ruby_curl_easy_perform_verb(VALUE self, VALUE verb) {
  VALUE str_verb;
  if (rb_type(verb) == T_STRING) {
    return ruby_curl_easy_perform_verb_str(self, StringValueCStr(verb));
  }
  else if (rb_respond_to(verb,rb_intern("to_s"))) {
    str_verb = rb_funcall(verb, rb_intern("to_s"), 0);
    return ruby_curl_easy_perform_verb_str(self, StringValueCStr(str_verb));
  }
  else {
    rb_raise(rb_eRuntimeError, "Invalid HTTP VERB, must response to 'to_s'");
  }
}

static VALUE call_easy_perform(VALUE self) {
  return rb_funcall(self, rb_intern("perform"), 0);
}

struct easy_form_perform_args {
  CURL *curl;
  struct curl_httppost *first;
};

static VALUE ensure_free_form_post(VALUE argp) {
  struct easy_form_perform_args *args = (struct easy_form_perform_args *)argp;
  if (args->curl) {
    curl_easy_setopt(args->curl, CURLOPT_HTTPPOST, NULL);
  }
  if (args->first) {
    curl_formfree(args->first);
    args->first = NULL;
  }
  return Qnil;
}

/*
 * call-seq:
 *   easy.http_post("url=encoded%20form%20data;and=so%20on") => true
 *   easy.http_post("url=encoded%20form%20data", "and=so%20on", ...) => true
 *   easy.http_post("url=encoded%20form%20data", Curl::PostField, "and=so%20on", ...) => true
 *   easy.http_post(Curl::PostField, Curl::PostField ..., Curl::PostField) => true
 *
 * POST the specified formdata to the currently configured URL using
 * the current options set for this Curl::Easy instance. This method
 * always returns true, or raises an exception (defined under
 * Curl::Err) on error.
 *
 * The Content-type of the POST is determined by the current setting
 * of multipart_form_post? , according to the following rules:
 * * When false (the default): the form will be POSTed with a
 *   content-type of 'application/x-www-form-urlencoded', and any of the
 *   four calling forms may be used.
 * * When true: the form will be POSTed with a content-type of
 *   'multipart/formdata'. Only the last calling form may be used,
 *   i.e. only PostField instances may be POSTed. In this mode,
 *   individual fields' content-types are recognised, and file upload
 *   fields are supported.
 *
 */
static VALUE ruby_curl_easy_perform_post(int argc, VALUE *argv, VALUE self) {
  ruby_curl_easy *rbce;
  CURL *curl;
  int i;
  VALUE args_ary;

  rb_scan_args(argc, argv, "*", &args_ary);

  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
  curl = rbce->curl;

  memset(rbce->err_buf, 0, CURL_ERROR_SIZE);

  curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, NULL);

  if (rbce->multipart_form_post) {
    VALUE ret;
    struct curl_httppost *first = NULL, *last = NULL;

    // Make the multipart form
    for (i = 0; i < argc; i++) {
      if (rb_obj_is_instance_of(argv[i], cCurlPostField)) {
        append_to_form(argv[i], &first, &last);
      } else if (rb_type(argv[i]) == T_ARRAY) {
        // see: https://github.com/rvanlieshout/curb/commit/8bcdefddc0162484681ebd1a92d52a642666a445
        long c = 0, argv_len = RARRAY_LEN(argv[i]);
        for (; c < argv_len; ++c) {
          if (rb_obj_is_instance_of(rb_ary_entry(argv[i],c), cCurlPostField)) {
            append_to_form(rb_ary_entry(argv[i],c), &first, &last);
          } else {
            rb_raise(eCurlErrInvalidPostField, "You must use PostFields only with multipart form posts");
            return Qnil;
          }
        }
      } else {
        rb_raise(eCurlErrInvalidPostField, "You must use PostFields only with multipart form posts");
        return Qnil;
      }
    }

    curl_easy_setopt(curl, CURLOPT_POST, 0);
    curl_easy_setopt(curl, CURLOPT_HTTPPOST, first);
    struct easy_form_perform_args perform_args = { curl, first };
    ret = rb_ensure(call_easy_perform, self, ensure_free_form_post, (VALUE)&perform_args);

    return ret;
  } else {
    VALUE post_body = Qnil;
    /* TODO: check for PostField.file and raise error before to_s fails */
    if ((post_body = rb_funcall(args_ary, idJoin, 1, rbstrAmp)) == Qnil) {
      rb_raise(eCurlErrError, "Failed to join arguments");
      return Qnil;
    } else {
      /* if the function call above returns an empty string because no additional arguments were passed this makes sure
         a previously set easy.post_body = "arg=foo&bar=bin"  will be honored */
      if( post_body != Qnil && rb_type(post_body) == T_STRING && RSTRING_LEN(post_body) > 0 ) {
        ruby_curl_easy_post_body_set(self, post_body);
      }

      /* if post body is not defined, set it so we enable POST header, even though the request body is empty */
      if( rb_easy_nil("postdata_buffer") ) {
        ruby_curl_easy_post_body_set(self, post_body);
      }

      return rb_funcall(self, rb_intern("perform"), 0);
    }
  }
}

/*
 * call-seq:
 *   easy.http_patch("url=encoded%20form%20data;and=so%20on") => true
 *   easy.http_patch("url=encoded%20form%20data", "and=so%20on", ...) => true
 *   easy.http_patch("url=encoded%20form%20data", Curl::PostField, "and=so%20on", ...) => true
 *   easy.http_patch(Curl::PostField, Curl::PostField ..., Curl::PostField) => true
 *
 * PATCH the specified formdata to the currently configured URL using
 * the current options set for this Curl::Easy instance. This method
 * always returns true, or raises an exception (defined under
 * Curl::Err) on error.
 *
 * When multipart_form_post is true the multipart logic is used; otherwise,
 * the arguments are joined into a raw PATCH body.
 */
static VALUE ruby_curl_easy_perform_patch(int argc, VALUE *argv, VALUE self) {
  ruby_curl_easy *rbce;
  CURL *curl;
  int i;
  VALUE args_ary;

  rb_scan_args(argc, argv, "*", &args_ary);
  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
  curl = rbce->curl;

  /* Clear the error buffer */
  memset(rbce->err_buf, 0, CURL_ERROR_SIZE);

  /* Set the custom HTTP method to PATCH */
  curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PATCH");

  if (rbce->multipart_form_post) {
    VALUE ret;
    struct curl_httppost *first = NULL, *last = NULL;

    /* Build the multipart form (same logic as for POST) */
    for (i = 0; i < argc; i++) {
      if (rb_obj_is_instance_of(argv[i], cCurlPostField)) {
        append_to_form(argv[i], &first, &last);
      } else if (rb_type(argv[i]) == T_ARRAY) {
        long j, argv_len = RARRAY_LEN(argv[i]);
        for (j = 0; j < argv_len; ++j) {
          if (rb_obj_is_instance_of(rb_ary_entry(argv[i], j), cCurlPostField)) {
            append_to_form(rb_ary_entry(argv[i], j), &first, &last);
          } else {
            rb_raise(eCurlErrInvalidPostField,
                     "You must use PostFields only with multipart form posts");
            return Qnil;
          }
        }
      } else {
        rb_raise(eCurlErrInvalidPostField,
                 "You must use PostFields only with multipart form posts");
        return Qnil;
      }
    }
    /* Disable the POST flag */
    curl_easy_setopt(curl, CURLOPT_POST, 0);
    /* Use the built multipart form as the request body */
    curl_easy_setopt(curl, CURLOPT_HTTPPOST, first);
    struct easy_form_perform_args perform_args = { curl, first };
    ret = rb_ensure(call_easy_perform, self, ensure_free_form_post, (VALUE)&perform_args);
    return ret;
  } else {
    /* Join arguments into a raw PATCH body */
    VALUE patch_body = rb_funcall(args_ary, idJoin, 1, rbstrAmp);
    if (patch_body == Qnil) {
      rb_raise(eCurlErrError, "Failed to join arguments");
      return Qnil;
    } else {
      if (rb_type(patch_body) == T_STRING && RSTRING_LEN(patch_body) > 0) {
        ruby_curl_easy_post_body_set(self, patch_body);
      }
      /* If postdata_buffer is still nil, set it so that the PATCH header is enabled */
      if (rb_easy_nil("postdata_buffer")) {
        ruby_curl_easy_post_body_set(self, patch_body);
      }
      return rb_funcall(self, rb_intern("perform"), 0);
    }
  }
}

/*
 * call-seq:
 *   easy.http_put(data)                              => true
 *
 * PUT the supplied data to the currently configured URL using the
 * current options set for this Curl::Easy instance. This method always
 * returns true, or raises an exception (defined under Curl::Err) on error.
 */
static VALUE ruby_curl_easy_perform_put(int argc, VALUE *argv, VALUE self) {
  ruby_curl_easy *rbce;
  CURL *curl;
  VALUE args_ary;
  int i;

  rb_scan_args(argc, argv, "*", &args_ary);
  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
  curl = rbce->curl;

  memset(rbce->err_buf, 0, CURL_ERROR_SIZE);
  curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT");

  /* New: if no arguments were provided, treat as an empty PUT */
  if (RARRAY_LEN(args_ary) == 0) {
    /* Option 1: explicitly set an empty body */
    ruby_curl_easy_put_data_set(self, rb_str_new2(""));
  }
  /* If a single argument is given and it is a String or responds to read, use legacy behavior */
  else if (RARRAY_LEN(args_ary) == 1 &&
           (rb_type(rb_ary_entry(args_ary, 0)) == T_STRING ||
            rb_respond_to(rb_ary_entry(args_ary, 0), rb_intern("read")))) {
    ruby_curl_easy_put_data_set(self, rb_ary_entry(args_ary, 0));
  }
  /* Otherwise, if multipart_form_post is true, use multipart logic */
  else if (rbce->multipart_form_post) {
    VALUE ret;
    struct curl_httppost *first = NULL, *last = NULL;
    for (i = 0; i < RARRAY_LEN(args_ary); i++) {
      VALUE field = rb_ary_entry(args_ary, i);
      if (rb_obj_is_instance_of(field, cCurlPostField)) {
        append_to_form(field, &first, &last);
      } else if (rb_type(field) == T_ARRAY) {
        long j;
        for (j = 0; j < RARRAY_LEN(field); j++) {
          VALUE subfield = rb_ary_entry(field, j);
          if (rb_obj_is_instance_of(subfield, cCurlPostField)) {
            append_to_form(subfield, &first, &last);
          } else {
            rb_raise(eCurlErrInvalidPostField,
                     "You must use PostFields only with multipart form posts");
          }
        }
      } else {
        rb_raise(eCurlErrInvalidPostField,
                 "You must use PostFields only with multipart form posts");
      }
    }
    curl_easy_setopt(curl, CURLOPT_POST, 0);
    curl_easy_setopt(curl, CURLOPT_HTTPPOST, first);
    /* Set the method explicitly to PUT */
    curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT");
    struct easy_form_perform_args perform_args = { curl, first };
    ret = rb_ensure(call_easy_perform, self, ensure_free_form_post, (VALUE)&perform_args);
    return ret;
  }
  /* Fallback: join all arguments */
  else {
    VALUE post_body = rb_funcall(args_ary, idJoin, 1, rbstrAmp);
    if (post_body != Qnil && rb_type(post_body) == T_STRING &&
        RSTRING_LEN(post_body) > 0) {
      ruby_curl_easy_put_data_set(self, post_body);
    }
  }
  return rb_funcall(self, rb_intern("perform"), 0);
}


/* =================== DATA FUNCS =============== */

/*
 * call-seq:
 *   easy.body_str                                    => "response body"
 *
 * Return the response body from the previous call to +perform+. This
 * is populated by the default +on_body+ handler - if you supply
 * your own body handler, this string will be empty.
 */
static VALUE ruby_curl_easy_body_str_get(VALUE self) {
  /*
     TODO: can we force_encoding on the return here if we see charset=utf-8 in the content-type header?
     Content-Type: application/json; charset=utf-8
  */
  CURB_OBJECT_HGETTER(ruby_curl_easy, body_data);
}

/*
 * call-seq:
 *   easy.header_str                                  => "response header"
 *
 * Return the response header from the previous call to +perform+. This
 * is populated by the default +on_header+ handler - if you supply
 * your own header handler, this string will be empty.
 */
static VALUE ruby_curl_easy_header_str_get(VALUE self) {
  CURB_OBJECT_HGETTER(ruby_curl_easy, header_data);
}


/* ============== LASTCONN INFO FUNCS ============ */

/*
 * call-seq:
 *   easy.last_effective_url                          => "http://some.url" or nil
 *
 * Retrieve the last effective URL used by this instance.
 * This is the URL used in the last +perform+ call,
 * and may differ from the value of easy.url.
 */
static VALUE ruby_curl_easy_last_effective_url_get(VALUE self) {
  ruby_curl_easy *rbce;
  char* url;

  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
  curl_easy_getinfo(rbce->curl, CURLINFO_EFFECTIVE_URL, &url);

  if (url && url[0]) {    // curl returns empty string if none
    return rb_str_new2(url);
  } else {
    return Qnil;
  }
}

/*
 * call-seq:
 *   easy.response_code                               => fixnum
 *
 * Retrieve the last received HTTP or FTP code. This will be zero
 * if no server response code has been received. Note that a proxy's
 * CONNECT response should be read with +http_connect_code+
 * and not this method.
 */
static VALUE ruby_curl_easy_response_code_get(VALUE self) {
  ruby_curl_easy *rbce;
  long code;

  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
#ifdef HAVE_CURLINFO_RESPONSE_CODE
  curl_easy_getinfo(rbce->curl, CURLINFO_RESPONSE_CODE, &code);
#else
  // old libcurl
  curl_easy_getinfo(rbce->curl, CURLINFO_HTTP_CODE, &code);
#endif

  return LONG2NUM(code);
}

#if defined(HAVE_CURLINFO_PRIMARY_IP)
/*
 * call-seq:
 *   easy.primary_ip                                  => "xx.xx.xx.xx" or nil
 *
 *   Retrieve the resolved IP of the most recent connection
 *   done with this curl handle. This string may be  IPv6 if
 *   that's enabled. This feature requires curl 7.19.x and above
 */
static VALUE ruby_curl_easy_primary_ip_get(VALUE self) {
  ruby_curl_easy *rbce;
  char* ip;

  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
  curl_easy_getinfo(rbce->curl, CURLINFO_PRIMARY_IP, &ip);

  if (ip && ip[0]) {    // curl returns empty string if none
    return rb_str_new2(ip);
  } else {
    return Qnil;
  }
}
#endif

/*
 * call-seq:
 *   easy.http_connect_code                           => fixnum
 *
 * Retrieve the last received proxy response code to a CONNECT request.
 */
static VALUE ruby_curl_easy_http_connect_code_get(VALUE self) {
  ruby_curl_easy *rbce;
  long code;

  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
  curl_easy_getinfo(rbce->curl, CURLINFO_HTTP_CONNECTCODE, &code);

  return LONG2NUM(code);
}

/*
 * call-seq:
 *   easy.file_time                                   => fixnum
 *
 * Retrieve the remote time of the retrieved document (in number of
 * seconds since 1 jan 1970 in the GMT/UTC time zone). If you get -1,
 * it can be because of many reasons (unknown, the server hides it
 * or the server doesn't support the command that tells document time
 * etc) and the time of the document is unknown.
 *
 * Note that you must tell the server to collect this information
 * before the transfer is made, by setting +fetch_file_time?+ to true,
 * or you will unconditionally get a -1 back.
 *
 * This requires libcurl 7.5 or higher - otherwise -1 is unconditionally
 * returned.
 */
static VALUE ruby_curl_easy_file_time_get(VALUE self) {
#ifdef HAVE_CURLINFO_FILETIME
  ruby_curl_easy *rbce;
  long time;

  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
  curl_easy_getinfo(rbce->curl, CURLINFO_FILETIME, &time);

  return LONG2NUM(time);
#else
  rb_warn("Installed libcurl is too old to support file_time");
  return LONG2NUM(0);
#endif
}

/*
 * call-seq:
 *   easy.total_time                                  => float
 *
 * Retrieve the total time in seconds for the previous transfer,
 * including name resolving, TCP connect etc.
 */
static VALUE ruby_curl_easy_total_time_get(VALUE self) {
  ruby_curl_easy *rbce;
  double time;

  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
  curl_easy_getinfo(rbce->curl, CURLINFO_TOTAL_TIME, &time);

  return rb_float_new(time);
}

/*
 * call-seq:
 *   easy.name_lookup_time                            => float
 *
 * Retrieve the time, in seconds, it took from the start until the
 * name resolving was completed.
 */
static VALUE ruby_curl_easy_name_lookup_time_get(VALUE self) {
  ruby_curl_easy *rbce;
  double time;

  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
  curl_easy_getinfo(rbce->curl, CURLINFO_NAMELOOKUP_TIME, &time);

  return rb_float_new(time);
}

/*
 * call-seq:
 *   easy.connect_time                                => float
 *
 * Retrieve the time, in seconds, it took from the start until the
 * connect to the remote host (or proxy) was completed.
 */
static VALUE ruby_curl_easy_connect_time_get(VALUE self) {
  ruby_curl_easy *rbce;
  double time;

  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
  curl_easy_getinfo(rbce->curl, CURLINFO_CONNECT_TIME, &time);

  return rb_float_new(time);
}

/*
 * call-seq:
 *   easy.app_connect_time                         => float
 *
 * Retrieve the time, in seconds, it took from the start until the SSL/SSH
 * connect/handshake to the remote host was completed. This time is most often
 * very near to the pre transfer time, except for cases such as HTTP
 * pipelining where the pretransfer time can be delayed due to waits in line
 * for the pipeline and more.
 */
#if defined(HAVE_CURLINFO_APPCONNECT_TIME)
static VALUE ruby_curl_easy_app_connect_time_get(VALUE self) {
  ruby_curl_easy *rbce;
  double time;

  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
  curl_easy_getinfo(rbce->curl, CURLINFO_APPCONNECT_TIME, &time);

  return rb_float_new(time);
}
#endif


/*
 * call-seq:
 *   easy.pre_transfer_time                           => float
 *
 * Retrieve the time, in seconds, it took from the start until the
 * file transfer is just about to begin. This includes all pre-transfer
 * commands and negotiations that are specific to the particular protocol(s)
 * involved.
 */
static VALUE ruby_curl_easy_pre_transfer_time_get(VALUE self) {
  ruby_curl_easy *rbce;
  double time;

  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
  curl_easy_getinfo(rbce->curl, CURLINFO_PRETRANSFER_TIME, &time);

  return rb_float_new(time);
}

/*
 * call-seq:
 *   easy.start_transfer_time                         => float
 *
 * Retrieve the time, in seconds, it took from the start until the first byte
 * is just about to be transferred. This includes the +pre_transfer_time+ and
 * also the time the server needs to calculate the result.
 */
static VALUE ruby_curl_easy_start_transfer_time_get(VALUE self) {
  ruby_curl_easy *rbce;
  double time;

  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
  curl_easy_getinfo(rbce->curl, CURLINFO_STARTTRANSFER_TIME, &time);

  return rb_float_new(time);
}

/*
 * call-seq:
 *   easy.redirect_time                               => float
 *
 * Retrieve the total time, in seconds, it took for all redirection steps
 * include name lookup, connect, pretransfer and transfer before final
 * transaction was started. +redirect_time+ contains the complete
 * execution time for multiple redirections.
 *
 * Requires libcurl 7.9.7 or higher, otherwise -1 is always returned.
 */
static VALUE ruby_curl_easy_redirect_time_get(VALUE self) {
#ifdef HAVE_CURLINFO_REDIRECT_TIME
  ruby_curl_easy *rbce;
  double time;

  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
  curl_easy_getinfo(rbce->curl, CURLINFO_REDIRECT_TIME, &time);

  return rb_float_new(time);
#else
  rb_warn("Installed libcurl is too old to support redirect_time");
  return rb_float_new(-1);
#endif
}

/*
 * call-seq:
 *   easy.redirect_count                            => integer
 *
 * Retrieve the total number of redirections that were actually followed.
 *
 * Requires libcurl 7.9.7 or higher, otherwise -1 is always returned.
 */
static VALUE ruby_curl_easy_redirect_count_get(VALUE self) {
#ifdef HAVE_CURLINFO_REDIRECT_COUNT
  ruby_curl_easy *rbce;
  long count;

  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
  curl_easy_getinfo(rbce->curl, CURLINFO_REDIRECT_COUNT, &count);

  return LONG2NUM(count);
#else
  rb_warn("Installed libcurl is too old to support redirect_count");
  return LONG2NUM(-1);
#endif

}

/*
 * call-seq:
 *   easy.redirect_url                               => "http://some.url" or nil
 *
 * Retrieve  the URL a redirect would take you to if you
 * would enable CURLOPT_FOLLOWLOCATION.
 *
 * Requires libcurl 7.18.2 or higher, otherwise -1 is always returned.
 */
static VALUE ruby_curl_easy_redirect_url_get(VALUE self) {
#ifdef HAVE_CURLINFO_REDIRECT_URL
  ruby_curl_easy *rbce;
  char* url;

  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
  curl_easy_getinfo(rbce->curl, CURLINFO_REDIRECT_URL, &url);

  if (url && url[0]) {    // curl returns empty string if none
    return rb_str_new2(url);
  } else {
    return Qnil;
  }
#else
  rb_warn("Installed libcurl is too old to support redirect_url");
  return LONG2NUM(-1);
#endif
}



/*
 * call-seq:
 *   easy.uploaded_bytes                              => float
 *
 * Retrieve the total amount of bytes that were uploaded in the
 * preceeding transfer.
 */
static VALUE ruby_curl_easy_uploaded_bytes_get(VALUE self) {
  ruby_curl_easy *rbce;
  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);

#ifdef HAVE_CURLINFO_SIZE_UPLOAD_T
  curl_off_t bytes;
  curl_easy_getinfo(rbce->curl, CURLINFO_SIZE_UPLOAD_T, &bytes);
  return LL2NUM(bytes);
#else
  double bytes;
  curl_easy_getinfo(rbce->curl, CURLINFO_SIZE_UPLOAD, &bytes);
  return rb_float_new(bytes);
#endif
}

/*
 * call-seq:
 *   easy.downloaded_bytes                            => float
 *
 * Retrieve the total amount of bytes that were downloaded in the
 * preceeding transfer.
 */
static VALUE ruby_curl_easy_downloaded_bytes_get(VALUE self) {
  ruby_curl_easy *rbce;
  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);

#ifdef HAVE_CURLINFO_SIZE_DOWNLOAD_T
  curl_off_t bytes;
  curl_easy_getinfo(rbce->curl, CURLINFO_SIZE_DOWNLOAD_T, &bytes);
  return LL2NUM(bytes);
#else
  double bytes;
  curl_easy_getinfo(rbce->curl, CURLINFO_SIZE_DOWNLOAD, &bytes);
  return rb_float_new(bytes);
#endif
}

/*
 * call-seq:
 *   easy.upload_speed                                => float
 *
 * Retrieve the average upload speed that curl measured for the
 * preceeding complete upload.
 */
static VALUE ruby_curl_easy_upload_speed_get(VALUE self) {
  ruby_curl_easy *rbce;
  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);

#ifdef HAVE_CURLINFO_SPEED_UPLOAD_T
  curl_off_t bytes;
  curl_easy_getinfo(rbce->curl, CURLINFO_SPEED_UPLOAD_T, &bytes);
  return LL2NUM(bytes);
#else
  double bytes;
  curl_easy_getinfo(rbce->curl, CURLINFO_SPEED_UPLOAD, &bytes);
  return rb_float_new(bytes);
#endif
}

/*
 * call-seq:
 *   easy.download_speed                              => float
 *
 * Retrieve the average download speed that curl measured for
 * the preceeding complete download.
 */
static VALUE ruby_curl_easy_download_speed_get(VALUE self) {
  ruby_curl_easy *rbce;
  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);

#ifdef HAVE_CURLINFO_SPEED_DOWNLOAD_T
  curl_off_t bytes;
  curl_easy_getinfo(rbce->curl, CURLINFO_SPEED_DOWNLOAD_T, &bytes);
  return LL2NUM(bytes);
#else
  double bytes;
  curl_easy_getinfo(rbce->curl, CURLINFO_SPEED_DOWNLOAD, &bytes);
  return rb_float_new(bytes);
#endif
}

/*
 * call-seq:
 *   easy.header_size                                 => fixnum
 *
 * Retrieve the total size of all the headers received in the
 * preceeding transfer.
 */
static VALUE ruby_curl_easy_header_size_get(VALUE self) {
  ruby_curl_easy *rbce;
  long size;

  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
  curl_easy_getinfo(rbce->curl, CURLINFO_HEADER_SIZE, &size);

  return LONG2NUM(size);
}

/*
 * call-seq:
 *   easy.request_size                                => fixnum
 *
 * Retrieve the total size of the issued requests. This is so far
 * only for HTTP requests. Note that this may be more than one request
 * if +follow_location?+ is true.
 */
static VALUE ruby_curl_easy_request_size_get(VALUE self) {
  ruby_curl_easy *rbce;
  long size;

  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
  curl_easy_getinfo(rbce->curl, CURLINFO_REQUEST_SIZE, &size);

  return LONG2NUM(size);
}

/*
 * call-seq:
 *   easy.ssl_verify_result                           => integer
 *
 * Retrieve the result of the certification verification that was requested
 * (by setting +ssl_verify_peer?+ to +true+).
 */
static VALUE ruby_curl_easy_ssl_verify_result_get(VALUE self) {
  ruby_curl_easy *rbce;
  long result;

  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
  curl_easy_getinfo(rbce->curl, CURLINFO_SSL_VERIFYRESULT, &result);

  return LONG2NUM(result);
}

/* TODO CURLINFO_SSL_ENGINES

Pass the address of a 'struct curl_slist *' to receive a linked-list of OpenSSL crypto-engines supported.
Note that engines are normally implemented in separate dynamic libraries.
Hence not all the returned engines may be available at run-time.
NOTE: you must call curl_slist_free_all(3) on the list pointer once you're done with it, as libcurl will not free the data for you. (Added in 7.12.3)
*/

/*
 * call-seq:
 *   easy.downloaded_content_length                   => float
 *
 * Retrieve the content-length of the download. This is the value read
 * from the Content-Length: field.
 */
static VALUE ruby_curl_easy_downloaded_content_length_get(VALUE self) {
  ruby_curl_easy *rbce;
  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);

#ifdef HAVE_CURLINFO_CONTENT_LENGTH_DOWNLOAD_T
  curl_off_t bytes;
  curl_easy_getinfo(rbce->curl, CURLINFO_CONTENT_LENGTH_DOWNLOAD_T, &bytes);
  return LL2NUM(bytes);
#else
  double bytes;
  curl_easy_getinfo(rbce->curl, CURLINFO_CONTENT_LENGTH_DOWNLOAD, &bytes);
  return rb_float_new(bytes);
#endif
}

/*
 * call-seq:
 *   easy.uploaded_content_length                     => float
 *
 * Retrieve the content-length of the upload.
 */
static VALUE ruby_curl_easy_uploaded_content_length_get(VALUE self) {
  ruby_curl_easy *rbce;
  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);

#ifdef HAVE_CURLINFO_CONTENT_LENGTH_UPLOAD_T
  curl_off_t bytes;
  curl_easy_getinfo(rbce->curl, CURLINFO_CONTENT_LENGTH_UPLOAD_T, &bytes);
  return LL2NUM(bytes);
#else
  double bytes;
  curl_easy_getinfo(rbce->curl, CURLINFO_CONTENT_LENGTH_UPLOAD, &bytes);
  return rb_float_new(bytes);
#endif
}

/*
 * call-seq:
 *   easy.content_type                                => "content/type" or nil
 *
 * Retrieve the content-type of the downloaded object. This is the value read
 * from the Content-Type: field. If you get +nil+, it means that the server
 * didn't send a valid Content-Type header or that the protocol used doesn't
 * support this.
 */
static VALUE ruby_curl_easy_content_type_get(VALUE self) {
  ruby_curl_easy *rbce;
  char* type;

  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
  curl_easy_getinfo(rbce->curl, CURLINFO_CONTENT_TYPE, &type);

  if (type && type[0]) {    // curl returns empty string if none
    return rb_str_new2(type);
  } else {
    return Qnil;
  }
}


/* NOT REQUIRED?
CURLINFO_PRIVATE

Pass a pointer to a 'char *' to receive the pointer to the private data associated with the curl handle (set with the CURLOPT_PRIVATE option to curl_easy_setopt(3)). (Added in 7.10.3)
*/

/* TODO these will need constants setting up too for checking the bits.
 *
 * Alternatively, could return an object that wraps the long, and has
 * question methods to query the auth types. Could return long from to_i(nt)
 *
CURLINFO_HTTPAUTH_AVAIL

Pass a pointer to a long to receive a bitmask indicating the authentication method(s) available. The meaning of the bits is explained in the CURLOPT_HTTPAUTH option for curl_easy_setopt(3). (Added in 7.10.8)

CURLINFO_PROXYAUTH_AVAIL

Pass a pointer to a long to receive a bitmask indicating the authentication method(s) available for your proxy authentication. (Added in 7.10.8)
*/

/*
 * call-seq:
 *   easy.os_errno                                    => integer
 *
 * Retrieve the errno variable from a connect failure (requires
 * libcurl 7.12.2 or higher, otherwise 0 is always returned).
 */
static VALUE ruby_curl_easy_os_errno_get(VALUE self) {
#ifdef HAVE_CURLINFO_OS_ERRNO
  ruby_curl_easy *rbce;
  long result;

  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
  curl_easy_getinfo(rbce->curl, CURLINFO_OS_ERRNO, &result);

  return LONG2NUM(result);
#else
  rb_warn("Installed libcurl is too old to support os_errno");
  return LONG2NUM(0);
#endif
}

/*
 * call-seq:
 *   easy.num_connects                                => integer
 *
 * Retrieve the number of new connections libcurl had to create to achieve
 * the previous transfer (only the successful connects are counted).
 * Combined with +redirect_count+ you are able to know how many times libcurl
 * successfully reused existing connection(s) or not.
 *
 * See the Connection Options of curl_easy_setopt(3) to see how libcurl tries
 * to make persistent connections to save time.
 *
 * (requires libcurl 7.12.3 or higher, otherwise -1 is always returned).
 */
static VALUE ruby_curl_easy_num_connects_get(VALUE self) {
#ifdef HAVE_CURLINFO_NUM_CONNECTS
  ruby_curl_easy *rbce;
  long result;

  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
  curl_easy_getinfo(rbce->curl, CURLINFO_NUM_CONNECTS, &result);

  return LONG2NUM(result);
#else
  rb_warn("Installed libcurl is too old to support num_connects");
  return LONG2NUM(-1);
#endif
}


/*
 * call-seq:
 *   easy.cookielist                                => cookielist
 *
 * Retrieves the cookies curl knows in an array of strings.
 * Returned strings are in Netscape cookiejar format or in Set-Cookie format.
 * Since 7.43.0 cookies in the Set-Cookie format without a domain name are not exported.
 *
 * To modify the cookie engine (add/replace/remove), use +easy.cookielist= string+
 * or +easy.set(:cookielist, string)+ with one of the following accepted inputs:
 * - A Set-Cookie style header string: "Set-Cookie: name=value; Domain=example.com; Path=/; Expires=..."
 * - One or more lines in Netscape cookie file format (tab-separated fields)
 * - Special commands: "ALL" (clear all), "SESS" (remove session cookies), "FLUSH" (write to jar), "RELOAD" (reload from file)
 *
 * @see https://curl.se/libcurl/c/CURLINFO_COOKIELIST.html option <code>CURLINFO_COOKIELIST</code> of 
 *   <code>curl_easy_getopt(3)</code> to see how libcurl behaves.
 * @note requires libcurl 7.14.1 or higher, otherwise +-1+ is always returned
 * @return [Array<String>, nil, -1] array of strings, or +nil+ if there are no cookies, or +-1+ if the libcurl version is too old
*/
static VALUE ruby_curl_easy_cookielist_get(VALUE self) {
#ifdef HAVE_CURLINFO_COOKIELIST
  ruby_curl_easy *rbce;
  struct curl_slist *cookies;
  struct curl_slist *cookie;
  VALUE rb_cookies;

  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
  curl_easy_getinfo(rbce->curl, CURLINFO_COOKIELIST, &cookies);
  if (!cookies)
    return Qnil;
  rb_cookies = rb_ary_new();
  for (cookie = cookies; cookie; cookie = cookie->next)
    rb_ary_push(rb_cookies, rb_str_new2(cookie->data));
  curl_slist_free_all(cookies);
  return rb_cookies;

#else
    rb_warn("Installed libcurl is too old to support cookielist");
    return INT2FIX(-1);
#endif
}


/* TODO this needs to be implemented. Could probably support CONNECT_ONLY by having this
 *      return an open Socket or something.
 *
CURLINFO_LASTSOCKET

Pass a pointer to a long to receive the last socket used by this curl session. If the socket is no longer valid, -1 is returned. When you finish working with the socket, you must call curl_easy_cleanup() as usual and let libcurl close the socket and cleanup other resources associated with the handle. This is typically used in combination with CURLOPT_CONNECT_ONLY. (Added in 7.15.2)
*/

/*
 * call-seq:
 *   easy.ftp_entry_path                                => "C:\ftp\root\" or nil
 *
 * Retrieve the path of the entry path. That is the initial path libcurl ended
 * up in when logging on to the remote FTP server. This returns +nil+ if
 * something is wrong.
 *
 * (requires libcurl 7.15.4 or higher, otherwise +nil+ is always returned).
 */
static VALUE ruby_curl_easy_ftp_entry_path_get(VALUE self) {
#ifdef HAVE_CURLINFO_FTP_ENTRY_PATH
  ruby_curl_easy *rbce;
  char* path = NULL;

  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
  curl_easy_getinfo(rbce->curl, CURLINFO_FTP_ENTRY_PATH, &path);

  if (path && path[0]) {    // curl returns NULL or empty string if none
    return rb_str_new2(path);
  } else {
    return Qnil;
  }
#else
  rb_warn("Installed libcurl is too old to support ftp_entry_path");
  return Qnil;
#endif
}

/*
 * call-seq:
 *   easy.multi                                     => "#<Curl::Multi>"
 */
static VALUE ruby_curl_easy_multi_get(VALUE self) {
  ruby_curl_easy *rbce;
  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
  return rbce->multi;
}

/*
 * call-seq:
 *   easy.multi=multi                                    => "#<Curl::Multi>"
 */
static VALUE ruby_curl_easy_multi_set(VALUE self, VALUE multi) {
  ruby_curl_easy *rbce;
  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);

  if (!NIL_P(multi) && rb_obj_is_kind_of(multi, cCurlMulti) != Qtrue) {
    rb_raise(rb_eTypeError, "expected Curl::Multi or nil");
  }

  rbce->multi = multi;
  return rbce->multi;
}

/*
 * call-seq:
 *   easy.last_result                                    => 0
 */
static VALUE ruby_curl_easy_last_result(VALUE self) {
  ruby_curl_easy *rbce;
  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
  return LONG2NUM(rbce->last_result);
}

/*
 * call-seq:
 *   easy.last_error                                     => "Error details" or nil
 */
static VALUE ruby_curl_easy_last_error(VALUE self) {
  ruby_curl_easy *rbce;
  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);

  if (rbce->err_buf[0]) {    // curl returns NULL or empty string if none
    return rb_str_new2(rbce->err_buf);
  } else {
    return Qnil;
  }
}

/*
 * call-seq:
 *   easy.setopt(opt, val)  => val
 *
 * Initial access to libcurl curl_easy_setopt
 *
 * @param [Fixnum] opt The option to set, see +Curl::CURLOPT_*+ constants
 * @param [Object] val
 * @return [Object] val
 * @raise [TypeError] if the option is not supported
 * @note Some options - like url or cookie - aren't set directly throught +curl_easy_setopt+, but stored in the Ruby object state.
 * @note When +curl_easy_setopt+ is called, return value is not checked here.
 */
static VALUE ruby_curl_easy_set_opt(VALUE self, VALUE opt, VALUE val) {
  ruby_curl_easy *rbce;
  long option = NUM2LONG(opt);
  rb_io_t *open_f_ptr;

  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);

  switch (option) {
  /* BEHAVIOR OPTIONS */
  case CURLOPT_VERBOSE: {
    VALUE verbose = val;
    CURB_BOOLEAN_SETTER(ruby_curl_easy, verbose);
    } break;
  case CURLOPT_FOLLOWLOCATION: {
    VALUE follow_location = val;
    CURB_BOOLEAN_SETTER(ruby_curl_easy, follow_location);
    } break;
  /* TODO: CALLBACK OPTIONS */
  /* TODO: ERROR OPTIONS */
  /* NETWORK OPTIONS */
  case CURLOPT_URL: {
    VALUE url = val;
    CURB_OBJECT_HSETTER(ruby_curl_easy, url);
    } break;
  case CURLOPT_CUSTOMREQUEST:
    curl_easy_setopt(rbce->curl, CURLOPT_CUSTOMREQUEST, NIL_P(val) ? NULL : StringValueCStr(val));
    break;
  case CURLOPT_HTTP_VERSION: {
    long http_version = NIL_P(val) ? CURL_HTTP_VERSION_NONE : NUM2LONG(val);
    rbce->http_version = http_version;
    curl_easy_setopt(rbce->curl, CURLOPT_HTTP_VERSION, http_version);
    } break;
  case CURLOPT_PROXY: {
    VALUE proxy_url = val;
    CURB_OBJECT_HSETTER(ruby_curl_easy, proxy_url);
    } break;
  case CURLOPT_INTERFACE: {
    VALUE interface_hm = val;
    CURB_OBJECT_HSETTER(ruby_curl_easy, interface_hm);
    } break;
  case CURLOPT_HEADER:
  case CURLOPT_NOPROGRESS:
  case CURLOPT_NOSIGNAL:
#ifdef HAVE_CURLOPT_PATH_AS_IS
  case CURLOPT_PATH_AS_IS:
#endif
#ifdef HAVE_CURLOPT_PIPEWAIT
  case CURLOPT_PIPEWAIT:
#endif
  case CURLOPT_HTTPGET:
  case CURLOPT_NOBODY: {
    int type = rb_type(val);
    VALUE value;
    if (type == T_TRUE) {
      value = rb_int_new(1);
    } else if (type == T_FALSE) {
      value = rb_int_new(0);
    } else {
      value = rb_funcall(val, rb_intern("to_i"), 0);
    }
    curl_easy_setopt(rbce->curl, option, NUM2LONG(value));
    } break;
  case CURLOPT_POST: {
    curl_easy_setopt(rbce->curl, CURLOPT_POST, rb_type(val) == T_TRUE);
  } break;
  case CURLOPT_MAXCONNECTS: {
    curl_easy_setopt(rbce->curl, CURLOPT_MAXCONNECTS, NUM2LONG(val));
  } break;
  case CURLOPT_POSTFIELDS: {
    ruby_curl_easy_post_body_set_with_mode(self, val, 0);
  } break;
  case CURLOPT_USERPWD: {
    VALUE userpwd = val;
    CURB_OBJECT_HSETTER(ruby_curl_easy, userpwd);
    } break;
  case CURLOPT_PROXYUSERPWD: {
    VALUE proxypwd = val;
    CURB_OBJECT_HSETTER(ruby_curl_easy, proxypwd);
    } break;
#ifdef HAVE_CURLOPT_NOPROXY
  case CURLOPT_NOPROXY: {
    VALUE noproxy = val;
    CURB_OBJECT_HSETTER(ruby_curl_easy, noproxy);
    } break;
#endif
  case CURLOPT_COOKIE: {
    VALUE cookies = val;
    CURB_OBJECT_HSETTER(ruby_curl_easy, cookies);
    } break;
  case CURLOPT_COOKIEFILE: {
    VALUE cookiefile = val;
    CURB_OBJECT_HSETTER(ruby_curl_easy, cookiefile);
    } break;
  case CURLOPT_COOKIEJAR: {
    VALUE cookiejar = val;
    CURB_OBJECT_HSETTER(ruby_curl_easy, cookiejar);
    } break;
#ifdef HAVE_CURLOPT_REQUEST_TARGET
  case CURLOPT_REQUEST_TARGET: {
    /* Forward request-target directly to libcurl as a string. */
    curl_easy_setopt(rbce->curl, CURLOPT_REQUEST_TARGET, NIL_P(val) ? NULL : StringValueCStr(val));
    } break;
#endif
  case CURLOPT_TCP_NODELAY: {
    curl_easy_setopt(rbce->curl, CURLOPT_TCP_NODELAY, NUM2LONG(val));
    } break;
  /* FTP-specific toggles */
#ifdef HAVE_CURLOPT_DIRLISTONLY
  case CURLOPT_DIRLISTONLY: {
    int type = rb_type(val);
    VALUE value;
    if (type == T_TRUE) {
      value = rb_int_new(1);
    } else if (type == T_FALSE) {
      value = rb_int_new(0);
    } else {
      value = rb_funcall(val, rb_intern("to_i"), 0);
    }
    curl_easy_setopt(rbce->curl, CURLOPT_DIRLISTONLY, NUM2LONG(value));
    } break;
#endif
#ifdef HAVE_CURLOPT_FTP_USE_EPSV
  case CURLOPT_FTP_USE_EPSV: {
    int type = rb_type(val);
    VALUE value;
    if (type == T_TRUE) {
      value = rb_int_new(1);
    } else if (type == T_FALSE) {
      value = rb_int_new(0);
    } else {
      value = rb_funcall(val, rb_intern("to_i"), 0);
    }
    curl_easy_setopt(rbce->curl, CURLOPT_FTP_USE_EPSV, NUM2LONG(value));
    } break;
#endif
#ifdef HAVE_CURLOPT_FTP_USE_EPRT
  case CURLOPT_FTP_USE_EPRT: {
    int type = rb_type(val);
    VALUE value;
    if (type == T_TRUE) {
      value = rb_int_new(1);
    } else if (type == T_FALSE) {
      value = rb_int_new(0);
    } else {
      value = rb_funcall(val, rb_intern("to_i"), 0);
    }
    curl_easy_setopt(rbce->curl, CURLOPT_FTP_USE_EPRT, NUM2LONG(value));
    } break;
#endif
#ifdef HAVE_CURLOPT_FTP_SKIP_PASV_IP
  case CURLOPT_FTP_SKIP_PASV_IP: {
    int type = rb_type(val);
    VALUE value;
    if (type == T_TRUE) {
      value = rb_int_new(1);
    } else if (type == T_FALSE) {
      value = rb_int_new(0);
    } else {
      value = rb_funcall(val, rb_intern("to_i"), 0);
    }
    curl_easy_setopt(rbce->curl, CURLOPT_FTP_SKIP_PASV_IP, NUM2LONG(value));
    } break;
#endif
  case CURLOPT_RANGE: {
    curl_easy_setopt(rbce->curl, CURLOPT_RANGE, StringValueCStr(val));
    } break;
  case CURLOPT_RESUME_FROM: {
    curl_easy_setopt(rbce->curl, CURLOPT_RESUME_FROM, NUM2LONG(val));
    } break;
  case CURLOPT_FAILONERROR: {
    curl_easy_setopt(rbce->curl, CURLOPT_FAILONERROR, NUM2LONG(val));
    } break;
  case CURLOPT_SSL_CIPHER_LIST: {
    curl_easy_setopt(rbce->curl, CURLOPT_SSL_CIPHER_LIST, StringValueCStr(val));
    } break;
  case CURLOPT_FORBID_REUSE: {
    curl_easy_setopt(rbce->curl, CURLOPT_FORBID_REUSE, NUM2LONG(val));
    } break;
#ifdef HAVE_CURLOPT_GSSAPI_DELEGATION
  case CURLOPT_GSSAPI_DELEGATION: {
    curl_easy_setopt(rbce->curl, CURLOPT_GSSAPI_DELEGATION, NUM2LONG(val));
    } break;
#endif
#ifdef HAVE_CURLOPT_UNIX_SOCKET_PATH
  case CURLOPT_UNIX_SOCKET_PATH: {
	curl_easy_setopt(rbce->curl, CURLOPT_UNIX_SOCKET_PATH, StringValueCStr(val));
    } break;
#endif
#ifdef HAVE_CURLOPT_MAX_SEND_SPEED_LARGE
  case CURLOPT_MAX_SEND_SPEED_LARGE: {
    curl_easy_setopt(rbce->curl, CURLOPT_MAX_SEND_SPEED_LARGE, (curl_off_t) NUM2LL(val));
    } break;
#endif
#ifdef HAVE_CURLOPT_MAX_RECV_SPEED_LARGE
  case CURLOPT_MAX_RECV_SPEED_LARGE: {
    curl_easy_setopt(rbce->curl, CURLOPT_MAX_RECV_SPEED_LARGE, (curl_off_t) NUM2LL(val));
    } break;
#endif
#ifdef HAVE_CURLOPT_MAXFILESIZE
  case CURLOPT_MAXFILESIZE:
    curl_easy_setopt(rbce->curl, CURLOPT_MAXFILESIZE, NUM2LONG(val));
    break;
#endif
#ifdef HAVE_CURLOPT_TCP_KEEPALIVE
  case CURLOPT_TCP_KEEPALIVE:
    curl_easy_setopt(rbce->curl, CURLOPT_TCP_KEEPALIVE, NUM2LONG(val));
    break;
  case CURLOPT_TCP_KEEPIDLE:
    curl_easy_setopt(rbce->curl, CURLOPT_TCP_KEEPIDLE, NUM2LONG(val));
    break;
  case CURLOPT_TCP_KEEPINTVL:
    curl_easy_setopt(rbce->curl, CURLOPT_TCP_KEEPINTVL, NUM2LONG(val));
    break;
#endif
#ifdef HAVE_CURLOPT_HAPROXYPROTOCOL
  case CURLOPT_HAPROXYPROTOCOL:
    curl_easy_setopt(rbce->curl, CURLOPT_HAPROXYPROTOCOL, NUM2LONG(val));
    break;
#endif
  case CURLOPT_STDERR:
    // libcurl requires raw FILE pointer and this should be IO object in Ruby.
    // Tempfile or StringIO won't work.
    Check_Type(val, T_FILE);
    GetOpenFile(val, open_f_ptr);
    curl_easy_setopt(rbce->curl, CURLOPT_STDERR, rb_io_stdio_file(open_f_ptr));
    /* Retain a Ruby reference to the IO to prevent GC/finalization
     * while libcurl still holds and writes to the underlying FILE*. */
    rb_easy_set("stderr_io", val);
    break;
  case CURLOPT_PROTOCOLS:
  case CURLOPT_REDIR_PROTOCOLS:
    curl_easy_setopt(rbce->curl, option, NUM2LONG(val));
    break;
#ifdef HAVE_CURLOPT_SSL_SESSIONID_CACHE
  case CURLOPT_SSL_SESSIONID_CACHE:
    curl_easy_setopt(rbce->curl, CURLOPT_SSL_SESSIONID_CACHE, NUM2LONG(val));
    break;
#endif
#ifdef HAVE_CURLOPT_COOKIELIST
  case CURLOPT_COOKIELIST: {
	/* Forward to libcurl */
	curl_easy_setopt(rbce->curl, CURLOPT_COOKIELIST, StringValueCStr(val));
	/* Track whether the cookie engine should be enabled for requests.
	 * According to libcurl docs, CURLOPT_COOKIELIST also enables the cookie engine
	 * when provided with a non-command string. Some environments may still require
	 * an explicit "enable" via CURLOPT_COOKIEFILE="" to send cookies on requests.
	 * We do that in the perform setup when this flag is set.
	 */
	if (RB_TYPE_P(val, T_STRING)) {
	  const char *s = StringValueCStr(val);
	  if (!(strcmp(s, "ALL") == 0 || strcmp(s, "SESS") == 0 || strcmp(s, "FLUSH") == 0 || strcmp(s, "RELOAD") == 0)) {
	    rbce->cookielist_engine_enabled = 1;
	  }
	} else {
	  /* Non-string values are unexpected; be conservative and do not enable. */
	}
  } break;
#endif
#ifdef HAVE_CURLOPT_PROXY_SSL_VERIFYHOST
  case CURLOPT_PROXY_SSL_VERIFYHOST:
    curl_easy_setopt(rbce->curl, CURLOPT_PROXY_SSL_VERIFYHOST, NUM2LONG(val));
    break;
#endif
#ifdef HAVE_CURLOPT_RESOLVE
  case CURLOPT_RESOLVE: {
    struct curl_slist *list = NULL;
    ruby_curl_easy_clear_resolve_list(rbce);
    if (NIL_P(val)) {
      /* When nil is passed, we clear any previous resolve list */
      list = NULL;
    } else if (TYPE(val) == T_ARRAY) {
      long i, len = RARRAY_LEN(val);
      for (i = 0; i < len; i++) {
        VALUE item = rb_ary_entry(val, i);
        struct curl_slist *new_list = curl_slist_append(list, StringValueCStr(item));
        if (!new_list) {
          curl_slist_free_all(list);
          rb_raise(rb_eNoMemError, "Failed to append to resolve list");
        }
        list = new_list;
      }
    } else {
      /* If a single string is passed, use it directly */
      list = curl_slist_append(NULL, StringValueCStr(val));
      if (!list) {
        rb_raise(rb_eNoMemError, "Failed to create resolve list");
      }
    }
    /* Save the list pointer in the ruby_curl_easy structure for cleanup later */
    rbce->curl_resolve = list;
    curl_easy_setopt(rbce->curl, CURLOPT_RESOLVE, list);
  } break;
#endif
  default:
    rb_raise(rb_eTypeError, "Curb unsupported option");
  }

  return val;
}

/*
 * call-seq:
 *   easy.getinfo(opt) => nil
 *
 * Iniital access to libcurl curl_easy_getinfo, remember getinfo doesn't return the same values as setopt
 *
 * @note This method is not implemented yet.
 * @param [Fixnum] code Constant +CURLINFO_*+ from libcurl
 * @return [nil]
 */
static VALUE ruby_curl_easy_get_opt(VALUE self, VALUE opt) {
  return Qnil;
}

/*
 * call-seq:
 *   easy.inspect                                     => "#<Curl::Easy http://google.com/>"
 */
static VALUE ruby_curl_easy_inspect(VALUE self) {
  char buf[64];
  ruby_curl_easy *rbce;
  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);
  /* if we don't have a url set... we'll crash... */
  if( !rb_easy_nil("url") && rb_easy_type_check("url", T_STRING)) {
    VALUE url = rb_easy_get("url");
    size_t len = 13+((RSTRING_LEN(url) > 50) ? 50 : RSTRING_LEN(url));
    /* "#<Net::HTTP http://www.google.com/:80 open=false>" */
    memcpy(buf,"#<Curl::Easy ", 13);
    memcpy(buf+13,StringValueCStr(url), (len - 13));
    buf[len++] = '>';
    return rb_str_new(buf,len);
  }
  return rb_str_new2("#<Curl::Easy>");
}


/* ================== ESCAPING FUNCS ==============*/

/*
 * call-seq:
 *   easy.escape("some text")                         => "some%20text"
 *
 * Convert the given input string to a URL encoded string and return
 * the result. All input characters that are not a-z, A-Z or 0-9 are
 * converted to their "URL escaped" version (%NN where NN is a
 * two-digit hexadecimal number).
 */
static VALUE ruby_curl_easy_escape(VALUE self, VALUE svalue) {
  ruby_curl_easy *rbce;
  char *result;
  VALUE rresult;
  VALUE str = svalue;

  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);

  /* NOTE: make sure the value is a string, if not call to_s */
  if( rb_type(str) != T_STRING ) { str = rb_funcall(str,rb_intern("to_s"),0); }

#if (LIBCURL_VERSION_NUM >= 0x070f04)
  result = (char*)curl_easy_escape(rbce->curl, StringValuePtr(str), (int)RSTRING_LEN(str));
#else
  result = (char*)curl_escape(StringValuePtr(str), (int)RSTRING_LEN(str));
#endif

  rresult = rb_str_new2(result);
  curl_free(result);

  return rresult;
}

/*
 * call-seq:
 *   easy.unescape("some%20text")                     => "some text"
 *
 * Convert the given URL encoded input string to a "plain string" and return
 * the result. All input characters that are URL encoded (%XX where XX is a
 * two-digit hexadecimal number) are converted to their binary versions.
 */
static VALUE ruby_curl_easy_unescape(VALUE self, VALUE str) {
  ruby_curl_easy *rbce;
  int rlen;
  char *result;
  VALUE rresult;

  TypedData_Get_Struct(self, ruby_curl_easy, &ruby_curl_easy_data_type, rbce);

#if (LIBCURL_VERSION_NUM >= 0x070f04)
  result = (char*)curl_easy_unescape(rbce->curl, StringValuePtr(str), (int)RSTRING_LEN(str), &rlen);
#else
  result = (char*)curl_unescape(StringValuePtr(str), (int)RSTRING_LEN(str));
  rlen = strlen(result);
#endif

  rresult = rb_str_new(result, rlen);
  curl_free(result);

  return rresult;
}


/* ================= CLASS METHODS ==================*/

/*
 * call-seq:
 *   Curl::Easy.error(code)    => [ErrCode, String]
 *
 * translate an internal libcurl error to ruby error class
 */
static VALUE ruby_curl_easy_error_message(VALUE klass, VALUE code) {
  return rb_curl_easy_error(NUM2INT(code));
}

/* =================== INIT LIB =====================*/
// TODO: https://bugs.ruby-lang.org/issues/18007
void init_curb_easy() {
  idCall = rb_intern("call");
  idJoin = rb_intern("join");

  rbstrAmp = rb_str_new2("&");
  rb_global_variable(&rbstrAmp);

  cCurlEasy = rb_define_class_under(mCurl, "Easy", rb_cObject);

  /* Class methods */
  rb_define_alloc_func(cCurlEasy, ruby_curl_easy_allocate);
  rb_define_singleton_method(cCurlEasy, "error", ruby_curl_easy_error_message, 1);

  /* Initialize method */
  rb_define_method(cCurlEasy, "initialize", ruby_curl_easy_initialize, -1);

  /* Attributes for config next perform */
  rb_define_method(cCurlEasy, "url", ruby_curl_easy_url_get, 0);
  rb_define_method(cCurlEasy, "proxy_url", ruby_curl_easy_proxy_url_get, 0);
  rb_define_method(cCurlEasy, "http_version=", ruby_curl_easy_http_version_set, 1);
  rb_define_method(cCurlEasy, "http_version", ruby_curl_easy_http_version_get, 0);

  rb_define_method(cCurlEasy, "proxy_headers=", ruby_curl_easy_proxy_headers_set, 1);
  rb_define_method(cCurlEasy, "proxy_headers", ruby_curl_easy_proxy_headers_get, 0);

  rb_define_method(cCurlEasy, "headers=", ruby_curl_easy_headers_set, 1);
  rb_define_method(cCurlEasy, "headers", ruby_curl_easy_headers_get, 0);
  rb_define_method(cCurlEasy, "interface", ruby_curl_easy_interface_get, 0);
  rb_define_method(cCurlEasy, "userpwd", ruby_curl_easy_userpwd_get, 0);
  rb_define_method(cCurlEasy, "proxypwd", ruby_curl_easy_proxypwd_get, 0);
  rb_define_method(cCurlEasy, "cookies", ruby_curl_easy_cookies_get, 0);
  rb_define_method(cCurlEasy, "cookiefile", ruby_curl_easy_cookiefile_get, 0);
  rb_define_method(cCurlEasy, "cookiejar", ruby_curl_easy_cookiejar_get, 0);
  rb_define_method(cCurlEasy, "cert=", ruby_curl_easy_cert_set, 1);
  rb_define_method(cCurlEasy, "cert", ruby_curl_easy_cert_get, 0);
  rb_define_method(cCurlEasy, "cert_key=", ruby_curl_easy_cert_key_set, 1);
  rb_define_method(cCurlEasy, "cert_key", ruby_curl_easy_cert_key_get, 0);
  rb_define_method(cCurlEasy, "cacert=", ruby_curl_easy_cacert_set, 1);
  rb_define_method(cCurlEasy, "cacert", ruby_curl_easy_cacert_get, 0);
  rb_define_method(cCurlEasy, "certpassword=", ruby_curl_easy_certpassword_set, 1);
  rb_define_method(cCurlEasy, "certtype=", ruby_curl_easy_certtype_set, 1);
  rb_define_method(cCurlEasy, "certtype", ruby_curl_easy_certtype_get, 0);
  rb_define_method(cCurlEasy, "encoding=", ruby_curl_easy_encoding_set, 1);
  rb_define_method(cCurlEasy, "encoding", ruby_curl_easy_encoding_get, 0);
  rb_define_method(cCurlEasy, "useragent=", ruby_curl_easy_useragent_set, 1);
  rb_define_method(cCurlEasy, "useragent", ruby_curl_easy_useragent_get, 0);
  rb_define_method(cCurlEasy, "post_body=", ruby_curl_easy_post_body_set, 1);
  rb_define_method(cCurlEasy, "post_body", ruby_curl_easy_post_body_get, 0);
  rb_define_method(cCurlEasy, "put_data=", ruby_curl_easy_put_data_set, 1);
  rb_define_method(cCurlEasy, "ftp_commands=", ruby_curl_easy_ftp_commands_set, 1);
  rb_define_method(cCurlEasy, "ftp_commands", ruby_curl_easy_ftp_commands_get, 0);
  rb_define_method(cCurlEasy, "resolve=", ruby_curl_easy_resolve_set, 1);
  rb_define_method(cCurlEasy, "resolve", ruby_curl_easy_resolve_get, 0);

  rb_define_method(cCurlEasy, "local_port=", ruby_curl_easy_local_port_set, 1);
  rb_define_method(cCurlEasy, "local_port", ruby_curl_easy_local_port_get, 0);
  rb_define_method(cCurlEasy, "local_port_range=", ruby_curl_easy_local_port_range_set, 1);
  rb_define_method(cCurlEasy, "local_port_range", ruby_curl_easy_local_port_range_get, 0);
  rb_define_method(cCurlEasy, "proxy_port=", ruby_curl_easy_proxy_port_set, 1);
  rb_define_method(cCurlEasy, "proxy_port", ruby_curl_easy_proxy_port_get, 0);
  rb_define_method(cCurlEasy, "proxy_type=", ruby_curl_easy_proxy_type_set, 1);
  rb_define_method(cCurlEasy, "proxy_type", ruby_curl_easy_proxy_type_get, 0);
  rb_define_method(cCurlEasy, "http_auth_types=", ruby_curl_easy_http_auth_types_set, -1);
  rb_define_method(cCurlEasy, "http_auth_types", ruby_curl_easy_http_auth_types_get, 0);
  rb_define_method(cCurlEasy, "proxy_auth_types=", ruby_curl_easy_proxy_auth_types_set, 1);
  rb_define_method(cCurlEasy, "proxy_auth_types", ruby_curl_easy_proxy_auth_types_get, 0);
  rb_define_method(cCurlEasy, "max_redirects=", ruby_curl_easy_max_redirects_set, 1);
  rb_define_method(cCurlEasy, "max_redirects", ruby_curl_easy_max_redirects_get, 0);
  rb_define_method(cCurlEasy, "timeout=", ruby_curl_easy_timeout_set, 1);
  rb_define_method(cCurlEasy, "timeout", ruby_curl_easy_timeout_get, 0);
  rb_define_method(cCurlEasy, "timeout_ms=", ruby_curl_easy_timeout_ms_set, 1);
  rb_define_method(cCurlEasy, "timeout_ms", ruby_curl_easy_timeout_ms_get, 0);
  rb_define_method(cCurlEasy, "connect_timeout=", ruby_curl_easy_connect_timeout_set, 1);
  rb_define_method(cCurlEasy, "connect_timeout", ruby_curl_easy_connect_timeout_get, 0);
  rb_define_method(cCurlEasy, "connect_timeout_ms=", ruby_curl_easy_connect_timeout_ms_set, 1);
  rb_define_method(cCurlEasy, "connect_timeout_ms", ruby_curl_easy_connect_timeout_ms_get, 0);
  rb_define_method(cCurlEasy, "dns_cache_timeout=", ruby_curl_easy_dns_cache_timeout_set, 1);
  rb_define_method(cCurlEasy, "dns_cache_timeout", ruby_curl_easy_dns_cache_timeout_get, 0);
  rb_define_method(cCurlEasy, "ftp_response_timeout=", ruby_curl_easy_ftp_response_timeout_set, 1);
  rb_define_method(cCurlEasy, "ftp_response_timeout", ruby_curl_easy_ftp_response_timeout_get, 0);
  rb_define_method(cCurlEasy, "low_speed_limit=", ruby_curl_easy_low_speed_limit_set, 1);
  rb_define_method(cCurlEasy, "low_speed_limit", ruby_curl_easy_low_speed_limit_get, 0);
  rb_define_method(cCurlEasy, "low_speed_time=", ruby_curl_easy_low_speed_time_set, 1);
  rb_define_method(cCurlEasy, "low_speed_time", ruby_curl_easy_low_speed_time_get, 0);
  rb_define_method(cCurlEasy, "max_send_speed_large=", ruby_curl_easy_max_send_speed_large_set, 1);
  rb_define_method(cCurlEasy, "max_send_speed_large", ruby_curl_easy_max_send_speed_large_get, 0);
  rb_define_method(cCurlEasy, "max_recv_speed_large=", ruby_curl_easy_max_recv_speed_large_set, 1);
  rb_define_method(cCurlEasy, "max_recv_speed_large", ruby_curl_easy_max_recv_speed_large_get, 0);
  rb_define_method(cCurlEasy, "ssl_version=", ruby_curl_easy_ssl_version_set, 1);
  rb_define_method(cCurlEasy, "ssl_version", ruby_curl_easy_ssl_version_get, 0);
  rb_define_method(cCurlEasy, "use_ssl=", ruby_curl_easy_use_ssl_set, 1);
  rb_define_method(cCurlEasy, "use_ssl", ruby_curl_easy_use_ssl_get, 0);
  rb_define_method(cCurlEasy, "ftp_filemethod=", ruby_curl_easy_ftp_filemethod_set, 1);
  rb_define_method(cCurlEasy, "ftp_filemethod", ruby_curl_easy_ftp_filemethod_get, 0);

  rb_define_method(cCurlEasy, "username=", ruby_curl_easy_username_set, 1);
  rb_define_method(cCurlEasy, "username", ruby_curl_easy_username_get, 0);
  rb_define_method(cCurlEasy, "password=", ruby_curl_easy_password_set, 1);
  rb_define_method(cCurlEasy, "password", ruby_curl_easy_password_get, 0);

  rb_define_method(cCurlEasy, "proxy_tunnel=", ruby_curl_easy_proxy_tunnel_set, 1);
  rb_define_method(cCurlEasy, "proxy_tunnel?", ruby_curl_easy_proxy_tunnel_q, 0);
  rb_define_method(cCurlEasy, "fetch_file_time=", ruby_curl_easy_fetch_file_time_set, 1);
  rb_define_method(cCurlEasy, "fetch_file_time?", ruby_curl_easy_fetch_file_time_q, 0);
  rb_define_method(cCurlEasy, "ssl_verify_peer=", ruby_curl_easy_ssl_verify_peer_set, 1);
  rb_define_method(cCurlEasy, "ssl_verify_peer?", ruby_curl_easy_ssl_verify_peer_q, 0);
  rb_define_method(cCurlEasy, "ssl_verify_host_integer=", ruby_curl_easy_ssl_verify_host_set, 1);
  rb_define_method(cCurlEasy, "ssl_verify_host", ruby_curl_easy_ssl_verify_host_get, 0);
  rb_define_method(cCurlEasy, "header_in_body=", ruby_curl_easy_header_in_body_set, 1);
  rb_define_method(cCurlEasy, "header_in_body?", ruby_curl_easy_header_in_body_q, 0);
  rb_define_method(cCurlEasy, "use_netrc=", ruby_curl_easy_use_netrc_set, 1);
  rb_define_method(cCurlEasy, "use_netrc?", ruby_curl_easy_use_netrc_q, 0);
  rb_define_method(cCurlEasy, "follow_location?", ruby_curl_easy_follow_location_q, 0);
  rb_define_method(cCurlEasy, "autoreferer=", ruby_curl_easy_autoreferer_set, 1);
  rb_define_method(cCurlEasy, "unrestricted_auth=", ruby_curl_easy_unrestricted_auth_set, 1);
  rb_define_method(cCurlEasy, "unrestricted_auth?", ruby_curl_easy_unrestricted_auth_q, 0);
  rb_define_method(cCurlEasy, "verbose=", ruby_curl_easy_verbose_set, 1);
  rb_define_method(cCurlEasy, "verbose?", ruby_curl_easy_verbose_q, 0);
  rb_define_method(cCurlEasy, "multipart_form_post=", ruby_curl_easy_multipart_form_post_set, 1);
  rb_define_method(cCurlEasy, "multipart_form_post?", ruby_curl_easy_multipart_form_post_q, 0);
  rb_define_method(cCurlEasy, "enable_cookies=", ruby_curl_easy_enable_cookies_set, 1);
  rb_define_method(cCurlEasy, "enable_cookies?", ruby_curl_easy_enable_cookies_q, 0);
  rb_define_method(cCurlEasy, "ignore_content_length=", ruby_curl_easy_ignore_content_length_set, 1);
  rb_define_method(cCurlEasy, "ignore_content_length?", ruby_curl_easy_ignore_content_length_q, 0);
  rb_define_method(cCurlEasy, "resolve_mode", ruby_curl_easy_resolve_mode, 0);
  rb_define_method(cCurlEasy, "resolve_mode=", ruby_curl_easy_resolve_mode_set, 1);

  rb_define_method(cCurlEasy, "on_body", ruby_curl_easy_on_body_set, -1);
  rb_define_method(cCurlEasy, "on_header", ruby_curl_easy_on_header_set, -1);
  rb_define_method(cCurlEasy, "on_progress", ruby_curl_easy_on_progress_set, -1);
  rb_define_method(cCurlEasy, "on_debug", ruby_curl_easy_on_debug_set, -1);
  rb_define_method(cCurlEasy, "on_success", ruby_curl_easy_on_success_set, -1);
  rb_define_method(cCurlEasy, "on_failure", ruby_curl_easy_on_failure_set, -1);
  rb_define_method(cCurlEasy, "on_missing", ruby_curl_easy_on_missing_set, -1);
  rb_define_method(cCurlEasy, "on_redirect", ruby_curl_easy_on_redirect_set, -1);
  rb_define_method(cCurlEasy, "on_complete", ruby_curl_easy_on_complete_set, -1);

  rb_define_method(cCurlEasy, "http", ruby_curl_easy_perform_verb, 1);
  rb_define_method(cCurlEasy, "http_post", ruby_curl_easy_perform_post, -1);
  rb_define_method(cCurlEasy, "http_put", ruby_curl_easy_perform_put, -1);
  rb_define_method(cCurlEasy, "http_patch", ruby_curl_easy_perform_patch, -1);

  /* Post-perform info methods */
  rb_define_method(cCurlEasy, "body_str", ruby_curl_easy_body_str_get, 0);
  rb_define_method(cCurlEasy, "header_str", ruby_curl_easy_header_str_get, 0);

  rb_define_method(cCurlEasy, "last_effective_url", ruby_curl_easy_last_effective_url_get, 0);
  rb_define_method(cCurlEasy, "response_code", ruby_curl_easy_response_code_get, 0);
  rb_define_method(cCurlEasy, "code", ruby_curl_easy_response_code_get, 0);
#if defined(HAVE_CURLINFO_PRIMARY_IP)
  rb_define_method(cCurlEasy, "primary_ip", ruby_curl_easy_primary_ip_get, 0);
#endif
  rb_define_method(cCurlEasy, "http_connect_code", ruby_curl_easy_http_connect_code_get, 0);
  rb_define_method(cCurlEasy, "file_time", ruby_curl_easy_file_time_get, 0);
  rb_define_method(cCurlEasy, "total_time", ruby_curl_easy_total_time_get, 0);
  rb_define_method(cCurlEasy, "name_lookup_time", ruby_curl_easy_name_lookup_time_get, 0);
  rb_define_method(cCurlEasy, "connect_time", ruby_curl_easy_connect_time_get, 0);
#if defined(HAVE_CURLINFO_APPCONNECT_TIME)
  rb_define_method(cCurlEasy, "app_connect_time", ruby_curl_easy_app_connect_time_get, 0);
#endif
  rb_define_method(cCurlEasy, "pre_transfer_time", ruby_curl_easy_pre_transfer_time_get, 0);
  rb_define_method(cCurlEasy, "start_transfer_time", ruby_curl_easy_start_transfer_time_get, 0);
  rb_define_method(cCurlEasy, "redirect_time", ruby_curl_easy_redirect_time_get, 0);
  rb_define_method(cCurlEasy, "redirect_count", ruby_curl_easy_redirect_count_get, 0);
  rb_define_method(cCurlEasy, "redirect_url", ruby_curl_easy_redirect_url_get, 0);
  rb_define_method(cCurlEasy, "downloaded_bytes", ruby_curl_easy_downloaded_bytes_get, 0);
  rb_define_method(cCurlEasy, "uploaded_bytes", ruby_curl_easy_uploaded_bytes_get, 0);
  rb_define_method(cCurlEasy, "download_speed", ruby_curl_easy_download_speed_get, 0);
  rb_define_method(cCurlEasy, "upload_speed", ruby_curl_easy_upload_speed_get, 0);
  rb_define_method(cCurlEasy, "header_size", ruby_curl_easy_header_size_get, 0);
  rb_define_method(cCurlEasy, "request_size", ruby_curl_easy_request_size_get, 0);
  rb_define_method(cCurlEasy, "ssl_verify_result", ruby_curl_easy_ssl_verify_result_get, 0);
  rb_define_method(cCurlEasy, "downloaded_content_length", ruby_curl_easy_downloaded_content_length_get, 0);
  rb_define_method(cCurlEasy, "uploaded_content_length", ruby_curl_easy_uploaded_content_length_get, 0);
  rb_define_method(cCurlEasy, "content_type", ruby_curl_easy_content_type_get, 0);
  rb_define_method(cCurlEasy, "os_errno", ruby_curl_easy_os_errno_get, 0);
  rb_define_method(cCurlEasy, "num_connects", ruby_curl_easy_num_connects_get, 0);
  rb_define_method(cCurlEasy, "cookielist", ruby_curl_easy_cookielist_get, 0);
  rb_define_method(cCurlEasy, "ftp_entry_path", ruby_curl_easy_ftp_entry_path_get, 0);

  rb_define_method(cCurlEasy, "close", ruby_curl_easy_close, 0);
  rb_define_method(cCurlEasy, "reset", ruby_curl_easy_reset, 0);

  /* Curl utils */
  rb_define_method(cCurlEasy, "escape", ruby_curl_easy_escape, 1);
  rb_define_method(cCurlEasy, "unescape", ruby_curl_easy_unescape, 1);

  /* Runtime support */
  rb_define_method(cCurlEasy, "clone", ruby_curl_easy_clone, 0);
  rb_define_alias(cCurlEasy, "dup", "clone");
  rb_define_method(cCurlEasy, "inspect", ruby_curl_easy_inspect, 0);

  rb_define_method(cCurlEasy, "multi", ruby_curl_easy_multi_get, 0);
  rb_define_method(cCurlEasy, "multi=", ruby_curl_easy_multi_set, 1);
  rb_define_private_method(cCurlEasy, "_take_callback_error", ruby_curl_easy_take_callback_error, 0);
  rb_define_method(cCurlEasy, "last_result", ruby_curl_easy_last_result, 0);
  rb_define_method(cCurlEasy, "last_error", ruby_curl_easy_last_error, 0);

  rb_define_method(cCurlEasy, "setopt", ruby_curl_easy_set_opt, 2);
  rb_define_method(cCurlEasy, "getinfo", ruby_curl_easy_get_opt, 1);
}
