/*
 * For info on how to use libcurl, see:
 * http://curl.haxx.se/libcurl/c/libcurl-tutorial.html
 */

/*
 * FIXME: Support more CURL features...
 */

#ifdef HAVE_CONFIG_H
# include <config.h>
#endif

#include <string.h>
#include <errno.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
#include <libgen.h>

#include "Ecore.h"
#include "ecore_private.h"
#include "Ecore_Con.h"
#include "ecore_con_private.h"
#include "ecore_con_url_curl.h"
#include "Emile.h"

int ECORE_CON_EVENT_URL_DATA = 0;
int ECORE_CON_EVENT_URL_COMPLETE = 0;
int ECORE_CON_EVENT_URL_PROGRESS = 0;

static int _init_count = 0;
static Eina_Bool pipelining = EINA_FALSE;

static Eina_List *_url_con_url_list = NULL;

/**
 * @addtogroup Ecore_Con_Url_Group Ecore URL Connection Functions
 *
 * @{
 */

EAPI int
ecore_con_url_init(void)
{
   if (++_init_count > 1) return _init_count;
   if (!ecore_init()) goto ecore_init_failed;
   if (!ecore_con_init()) goto ecore_con_init_failed;
   if (!emile_init()) goto emile_init_failed;
   if (!emile_cipher_init()) goto emile_cipher_init_failed;
   ECORE_CON_EVENT_URL_DATA = ecore_event_type_new();
   ECORE_CON_EVENT_URL_COMPLETE = ecore_event_type_new();
   ECORE_CON_EVENT_URL_PROGRESS = ecore_event_type_new();
   return _init_count;

 emile_cipher_init_failed:
   emile_shutdown();
 emile_init_failed:
   ecore_con_shutdown();
 ecore_con_init_failed:
   ecore_shutdown();
 ecore_init_failed:
   return --_init_count;
}

EAPI int
ecore_con_url_shutdown(void)
{
   Ecore_Con_Url *url_con_url;
   if (_init_count == 0) return 0;
   --_init_count;
   if (_init_count) return _init_count;
   EINA_LIST_FREE(_url_con_url_list, url_con_url)
     ecore_con_url_free(url_con_url);

   ecore_event_type_flush(ECORE_CON_EVENT_URL_DATA,
                          ECORE_CON_EVENT_URL_COMPLETE,
                          ECORE_CON_EVENT_URL_PROGRESS);

   emile_shutdown(); /* no emile_cipher_shutdown(), handled here */
   ecore_con_shutdown();
   ecore_shutdown();
   return 0;
}

EAPI void
ecore_con_url_pipeline_set(Eina_Bool enable)
{
   if (!_c_init()) return;
   if (enable == pipelining) return;
   _c->curl_multi_setopt(_c->_curlm, CURLMOPT_PIPELINING, !!enable);
   pipelining = enable;
}

EAPI Eina_Bool
ecore_con_url_pipeline_get(void)
{
   return pipelining;
}


/* The rest of this file exists solely to provide ABI compatibility */

struct _Ecore_Con_Url
{
   ECORE_MAGIC;
   Eo *dialer;
   Eo *send_copier;
   Eo *input;
   Ecore_Timer *timer;
   struct {
      Ecore_Animator *animator;
      struct {
         uint64_t total;
         uint64_t now;
      } download, upload;
   } progress;
   Eina_Stringshare *url;
   Eina_Stringshare *custom_request;
   void *data;
   struct {
      Eina_List *files; /* of Eina_Stringshare - read locations */
      Eina_List *cmds; /* of static-const strings - COOKIELIST commands */
      Eina_Stringshare *jar; /* write location */
      Eina_Bool ignore_old_session;
   } cookies;
   struct {
      Eina_Stringshare *url;
      Eina_Stringshare *username;
      Eina_Stringshare *password;
   } proxy;
   struct {
      Ecore_Con_Url_Time condition;
      double stamp;
   } time;
   struct {
      Eina_Stringshare *username;
      Eina_Stringshare *password;
      Efl_Net_Http_Authentication_Method method;
      Eina_Bool restricted;
   } httpauth;
   Eina_Stringshare *ca_path;
   Eina_List *request_headers;
   Eina_List *response_headers;
   unsigned event_count;
   int received_bytes;
   int status;
   int write_fd;
   Efl_Net_Http_Version http_version;
   Eina_Bool ssl_verify_peer;
   Eina_Bool verbose;
   Eina_Bool ftp_use_epsv;
   Eina_Bool delete_me;
};

#define ECORE_CON_URL_CHECK_RETURN(u, ...) \
  do \
    { \
       if (!EINA_MAGIC_CHECK(u, ECORE_MAGIC_CON_URL)) \
         { \
            ECORE_MAGIC_FAIL(u, ECORE_MAGIC_CON_URL, __func__); \
            return __VA_ARGS__; \
         } \
       EINA_SAFETY_ON_TRUE_RETURN_VAL(u->delete_me, __VA_ARGS__); \
    } \
  while (0)


static void
_ecore_con_url_dialer_close(Ecore_Con_Url *url_con)
{
   if (url_con->send_copier)
     {
        efl_del(url_con->send_copier);
        url_con->send_copier = NULL;
     }

   if (url_con->input)
     {
        efl_del(url_con->input);
        url_con->input = NULL;
     }

   if (url_con->timer)
     {
        ecore_timer_del(url_con->timer);
        url_con->timer = NULL;
     }

   if (url_con->progress.animator)
     {
        ecore_animator_del(url_con->progress.animator);
        url_con->progress.animator = NULL;
     }

   if (!url_con->dialer) return;

   if (!efl_io_closer_closed_get(url_con->dialer))
     efl_io_closer_close(url_con->dialer);
   efl_del(url_con->dialer);
   url_con->dialer = NULL;
}

static void
_ecore_con_url_response_headers_free(Ecore_Con_Url *url_con)
{
   char *str;
   EINA_LIST_FREE(url_con->response_headers, str)
     free(str);
}

static void
_ecore_con_url_request_headers_free(Ecore_Con_Url *url_con)
{
   Efl_Net_Http_Header *header;
   EINA_LIST_FREE(url_con->response_headers, header)
     free(header); /* key and value are inline */
}

static void
_ecore_con_url_free_internal(Ecore_Con_Url *url_con)
{
   const char *s;

   url_con->delete_me = EINA_TRUE;
   if (url_con->event_count > 0) return;

   _ecore_con_url_dialer_close(url_con);

   eina_stringshare_replace(&url_con->url, NULL);
   eina_stringshare_replace(&url_con->custom_request, NULL);

   url_con->data = NULL;

   EINA_LIST_FREE(url_con->cookies.files, s)
     eina_stringshare_del(s);
   eina_list_free(url_con->cookies.cmds); /* data is not to be freed! */
   eina_stringshare_replace(&url_con->cookies.jar, NULL);

   eina_stringshare_replace(&url_con->proxy.url, NULL);
   eina_stringshare_replace(&url_con->proxy.username, NULL);
   eina_stringshare_replace(&url_con->proxy.password, NULL);

   eina_stringshare_replace(&url_con->httpauth.username, NULL);
   eina_stringshare_replace(&url_con->httpauth.password, NULL);

   eina_stringshare_replace(&url_con->ca_path, NULL);

   _ecore_con_url_request_headers_free(url_con);
   _ecore_con_url_response_headers_free(url_con);

   ECORE_MAGIC_SET(url_con, ECORE_MAGIC_NONE);
   free(url_con);
}

static void
_ecore_con_event_url_progress_free(void *data EINA_UNUSED, void *event)
{
   Ecore_Con_Event_Url_Progress *ev = event;
   Ecore_Con_Url *url_con = ev->url_con;

   EINA_SAFETY_ON_TRUE_GOTO(url_con->event_count == 0, end);

   url_con->event_count--;
   if ((url_con->event_count == 0) && (url_con->delete_me))
     _ecore_con_url_free_internal(url_con);

 end:
   free(ev);
}

static void
_ecore_con_event_url_progress_add(Ecore_Con_Url *url_con)
{
   Ecore_Con_Event_Url_Progress *ev;

   if (url_con->delete_me) return;

   ev = malloc(sizeof(*ev));
   EINA_SAFETY_ON_NULL_RETURN(ev);

   ev->url_con = url_con;
   ev->down.total = url_con->progress.download.total;
   ev->down.now = url_con->progress.download.now;
   ev->up.total = url_con->progress.upload.total;
   ev->up.now = url_con->progress.upload.now;
   url_con->event_count++;
   ecore_event_add(ECORE_CON_EVENT_URL_PROGRESS, ev, _ecore_con_event_url_progress_free, NULL);
}

static void
_ecore_con_event_url_complete_free(void *data EINA_UNUSED, void *event)
{
   Ecore_Con_Event_Url_Complete *ev = event;
   Ecore_Con_Url *url_con = ev->url_con;

   EINA_SAFETY_ON_TRUE_GOTO(url_con->event_count == 0, end);

   url_con->event_count--;
   if ((url_con->event_count == 0) && (url_con->delete_me))
     _ecore_con_url_free_internal(url_con);

 end:
   free(ev);
}

static void
_ecore_con_event_url_complete_add(Ecore_Con_Url *url_con, int status)
{
   Ecore_Con_Event_Url_Complete *ev;

   if (url_con->delete_me) return;

   if (url_con->progress.animator)
     {
        ecore_animator_del(url_con->progress.animator);
        url_con->progress.animator = NULL;
        _ecore_con_event_url_progress_add(url_con);
     }

   if (url_con->status)
     {
        DBG("URL '%s' was already complete with status=%d, new=%d", url_con->url, url_con->status, status);
        goto end;
     }

   url_con->status = status;

   ev = malloc(sizeof(Ecore_Con_Event_Url_Complete));
   EINA_SAFETY_ON_NULL_GOTO(ev, end);

   ev->url_con = url_con;
   ev->status = status;
   url_con->event_count++;
   ecore_event_add(ECORE_CON_EVENT_URL_COMPLETE, ev, _ecore_con_event_url_complete_free, NULL);

 end:
   _ecore_con_url_dialer_close(url_con);
}

static void
_ecore_con_url_dialer_error(void *data, const Efl_Event *event)
{
   Ecore_Con_Url *url_con = data;
   Eina_Error *perr = event->info;
   int status;

   status = efl_net_dialer_http_response_status_get(url_con->dialer);
   if ((status < 500) || (status > 599))
     {
        DBG("HTTP error %d reset to 1", status);
        status = 1; /* not a real HTTP error */
     }

   WRN("HTTP dialer error url='%s': %s",
       efl_net_dialer_address_dial_get(url_con->dialer),
       eina_error_msg_get(*perr));

   _ecore_con_event_url_complete_add(url_con, status);
}

static void
_ecore_con_event_url_data_free(void *data EINA_UNUSED, void *event)
{
   Ecore_Con_Event_Url_Data *ev = event;
   Ecore_Con_Url *url_con = ev->url_con;

   EINA_SAFETY_ON_TRUE_GOTO(url_con->event_count == 0, end);

   url_con->event_count--;
   if ((url_con->event_count == 0) && (url_con->delete_me))
     _ecore_con_url_free_internal(url_con);

 end:
   free(ev);
}

static void
_ecore_con_url_dialer_can_read_changed(void *data, const Efl_Event *event EINA_UNUSED)
{
   Ecore_Con_Url *url_con = data;
   Eina_Bool can_read;
   Ecore_Con_Event_Url_Data *ev;
   Eina_Rw_Slice slice;
   Eina_Error err;

   if (url_con->delete_me) return;

   can_read = efl_io_reader_can_read_get(url_con->dialer);
   if (!can_read) return;

   ev = malloc(sizeof(Ecore_Con_Event_Url_Data) + EFL_NET_DIALER_HTTP_BUFFER_RECEIVE_SIZE);
   EINA_SAFETY_ON_NULL_RETURN(ev);

   slice.mem = ev->data;
   slice.len = EFL_NET_DIALER_HTTP_BUFFER_RECEIVE_SIZE;

   err = efl_io_reader_read(url_con->dialer, &slice);
   if (err)
     {
        free(ev);
        if (err == EAGAIN) return;
        WRN("Error reading data from HTTP url='%s': %s",
            efl_net_dialer_address_dial_get(url_con->dialer),
            eina_error_msg_get(err));
        return;
     }

   ev->size = slice.len;
   ev->url_con = url_con;
   url_con->received_bytes += ev->size;

   if (url_con->write_fd == -1)
     {
        url_con->event_count++;
        ecore_event_add(ECORE_CON_EVENT_URL_DATA, ev, _ecore_con_event_url_data_free, NULL);
        return;
     }

   while (slice.len > 0)
     {
        ssize_t r = write(url_con->write_fd, slice.bytes, slice.len);
        if (r == -1)
          {
             ERR("Could not write to fd=%d: %s", url_con->write_fd, eina_error_msg_get(errno));
             break;
          }
        slice.bytes += r;
        slice.len -= r;
     }
   free(ev);
}

static void
_ecore_con_url_dialer_eos(void *data, const Efl_Event *event EINA_UNUSED)
{
   Ecore_Con_Url *url_con = data;

   DBG("HTTP EOS url='%s'", efl_net_dialer_address_dial_get(url_con->dialer));

   if (url_con->send_copier && (!efl_io_copier_done_get(url_con->send_copier)))
     {
        DBG("done receiving, waiting for send copier...");
        return;
     }

   _ecore_con_event_url_complete_add(url_con, efl_net_dialer_http_response_status_get(url_con->dialer));
}

static void
_ecore_con_url_dialer_headers_done(void *data, const Efl_Event *event EINA_UNUSED)
{
   Ecore_Con_Url *url_con = data;
   Eina_Iterator *it;
   Efl_Net_Http_Header *header;
   size_t len;
   int status = efl_net_dialer_http_response_status_get(url_con->dialer);
   char *str;

   DBG("HTTP headers done, status=%d url='%s'",
       status,
       efl_net_dialer_address_dial_get(url_con->dialer));

   _ecore_con_url_response_headers_free(url_con);

   it = efl_net_dialer_http_response_headers_all_get(url_con->dialer);
   EINA_SAFETY_ON_NULL_RETURN(it);
   EINA_ITERATOR_FOREACH(it, header)
     {
        if (header->key)
          {
             len = strlen(header->key) + strlen(header->value) + strlen(": \r\n") + 1;
             str = malloc(len);
             EINA_SAFETY_ON_NULL_GOTO(str, end);
             snprintf(str, len, "%s: %s\r\n", header->key, header->value);
             url_con->response_headers = eina_list_append(url_con->response_headers, str);
          }
        else
          {
             if (url_con->response_headers)
               {
                  str = malloc(strlen("\r\n") + 1);
                  EINA_SAFETY_ON_NULL_GOTO(str, end);
                  memcpy(str, "\r\n", strlen("\r\n") + 1);
                  url_con->response_headers = eina_list_append(url_con->response_headers, str);
               }

             len = strlen(header->value) + strlen("\r\n") + 1;
             str = malloc(len);
             EINA_SAFETY_ON_NULL_GOTO(str, end);
             snprintf(str, len, "%s\r\n", header->value);
             url_con->response_headers = eina_list_append(url_con->response_headers, str);
          }
     }

   str = malloc(strlen("\r\n") + 1);
   EINA_SAFETY_ON_NULL_GOTO(str, end);
   memcpy(str, "\r\n", strlen("\r\n") + 1);
   url_con->response_headers = eina_list_append(url_con->response_headers, str);

 end:
   eina_iterator_free(it);
}

EFL_CALLBACKS_ARRAY_DEFINE(ecore_con_url_dialer_cbs,
                           { EFL_IO_READER_EVENT_CAN_READ_CHANGED, _ecore_con_url_dialer_can_read_changed },
                           { EFL_IO_READER_EVENT_EOS, _ecore_con_url_dialer_eos },
                           { EFL_NET_DIALER_EVENT_DIALER_ERROR, _ecore_con_url_dialer_error },
                           { EFL_NET_DIALER_HTTP_EVENT_HEADERS_DONE, _ecore_con_url_dialer_headers_done });

static Eina_Bool
_ecore_con_url_progress_animator_cb(void *data)
{
   Ecore_Con_Url *url_con = data;
   uint64_t dn, dt, un, ut;

   efl_net_dialer_http_progress_download_get(url_con->dialer, &dn, &dt);
   efl_net_dialer_http_progress_upload_get(url_con->dialer, &un, &ut);

   if ((dn == url_con->progress.download.now) &&
       (dt == url_con->progress.download.total) &&
       (un == url_con->progress.upload.now) &&
       (ut == url_con->progress.upload.total))
     return EINA_TRUE;

   url_con->progress.download.now = dn;
   url_con->progress.download.total = dt;
   url_con->progress.upload.now = un;
   url_con->progress.upload.total = ut;

   _ecore_con_event_url_progress_add(url_con);

   return EINA_TRUE;
}

/*
 * creates a new efl_net_dialer_proxy_set() URL based on legacy parameters:
 *  - proxy url (that could contain user + pass encoded, optional protocol)
 *    - no protocol = http://
 *  - username
 *  - password
 *
 * May return NULL (= use envvar http_proxy)
 */
static char *
_ecore_con_url_proxy_url_new(const Ecore_Con_Url *url_con)
{
   Eina_Slice protocol, user = {}, pass = {}, address;
   char buf[4096];
   const char *p;

   if (!url_con->proxy.url) return NULL; /* use from envvar */

   p = strstr(url_con->proxy.url, "://");
   if (!p)
     {
        protocol = (Eina_Slice)EINA_SLICE_STR_LITERAL("http");
        address = (Eina_Slice)EINA_SLICE_STR(url_con->proxy.url);
     }
   else
     {
        const char *s;

        protocol.mem = url_con->proxy.url;
        protocol.len = p - url_con->proxy.url;
        if (protocol.len == 0)
          protocol = (Eina_Slice)EINA_SLICE_STR_LITERAL("http");

        p += strlen("://");
        s = strchr(p, '@');
        if (!s)
          {
             address = (Eina_Slice)EINA_SLICE_STR(p);
          }
        else
          {
             address = (Eina_Slice)EINA_SLICE_STR(s + 1);

             user.mem = p;
             user.len = s - p;

             s = eina_slice_strchr(user, ':');
             if (s)
               {
                  pass.mem = s + 1;
                  pass.len = user.len - (user.bytes - pass.bytes);
                  user.len = s - (const char *)user.bytes;
               }
          }
     }

   if (url_con->proxy.username)
     user = eina_stringshare_slice_get(url_con->proxy.username);

   if (url_con->proxy.password)
     pass = eina_stringshare_slice_get(url_con->proxy.password);

   if (user.len && pass.len)
     {
        snprintf(buf, sizeof(buf),
                 EINA_SLICE_STR_FMT "://"
                 EINA_SLICE_STR_FMT ":"
                 EINA_SLICE_STR_FMT "@"
                 EINA_SLICE_STR_FMT,
                 EINA_SLICE_STR_PRINT(protocol),
                 EINA_SLICE_STR_PRINT(user),
                 EINA_SLICE_STR_PRINT(pass),
                 EINA_SLICE_STR_PRINT(address));
     }
   else if (user.len)
     {
        snprintf(buf, sizeof(buf),
                 EINA_SLICE_STR_FMT "://"
                 EINA_SLICE_STR_FMT "@"
                 EINA_SLICE_STR_FMT,
                 EINA_SLICE_STR_PRINT(protocol),
                 EINA_SLICE_STR_PRINT(user),
                 EINA_SLICE_STR_PRINT(address));
     }
   else
     {
        snprintf(buf, sizeof(buf),
                 EINA_SLICE_STR_FMT "://" EINA_SLICE_STR_FMT,
                 EINA_SLICE_STR_PRINT(protocol),
                 EINA_SLICE_STR_PRINT(address));
     }

   return strdup(buf);
}

static void
_ecore_con_url_copier_done(void *data, const Efl_Event *event)
{
   Ecore_Con_Url *url_con = data;
   int status = efl_net_dialer_http_response_status_get(url_con->dialer);

   DBG("copier %s %p for url='%s' is done", efl_name_get(event->object), event->object, efl_net_dialer_address_dial_get(url_con->dialer));

   if (!efl_io_reader_eos_get(url_con->dialer))
     {
        DBG("done sending, waiting for dialer EOS...");
        return;
     }

   _ecore_con_event_url_complete_add(url_con, status);
}

static void
_ecore_con_url_copier_error(void *data, const Efl_Event *event)
{
   Ecore_Con_Url *url_con = data;
   Eina_Error *perr = event->info;
   int status;

   status = efl_net_dialer_http_response_status_get(url_con->dialer);
   if ((status < 500) || (status > 599))
     {
        DBG("HTTP error %d reset to 1", status);
        status = 1; /* not a real HTTP error */
     }

   WRN("HTTP copier %s %p error url='%s': %s",
       efl_name_get(event->object), event->object,
       efl_net_dialer_address_dial_get(url_con->dialer),
       eina_error_msg_get(*perr));

   _ecore_con_event_url_complete_add(url_con, status);
}

EFL_CALLBACKS_ARRAY_DEFINE(_ecore_con_url_copier_cbs,
                           { EFL_IO_COPIER_EVENT_ERROR, _ecore_con_url_copier_error },
                           { EFL_IO_COPIER_EVENT_DONE, _ecore_con_url_copier_done });

/*
 * Ecore_Con_Url is documented as 'reusable', while Efl.Net.Dialers
 * are one-shot and must be recreated on every usage.
 *
 * Then _ecore_con_url_request_prepare() will close (cancel) any
 * previous dialer and create a new one with all parameters set.
 */
static Eina_Bool
_ecore_con_url_request_prepare(Ecore_Con_Url *url_con, const char *method)
{
   const Efl_Net_Http_Header *header;
   const Eina_List *n;
   const char *s;
   char *proxy_url = NULL;
   CURL *curl_easy;

   _ecore_con_url_dialer_close(url_con);

   url_con->status = 0;
   url_con->received_bytes = 0;
   url_con->progress.download.now = 0;
   url_con->progress.download.total = 0;
   url_con->progress.upload.now = 0;
   url_con->progress.upload.total = 0;
   _ecore_con_url_response_headers_free(url_con);

   proxy_url = _ecore_con_url_proxy_url_new(url_con);
   if (proxy_url)
     DBG("proxy_url='%s'", proxy_url);

   url_con->dialer = efl_add(EFL_NET_DIALER_HTTP_CLASS, efl_main_loop_get(),
                             efl_net_dialer_http_method_set(efl_added, url_con->custom_request ? url_con->custom_request : method),
                             efl_net_dialer_http_primary_mode_set(efl_added, (strcmp(method, "PUT") == 0) ? EFL_NET_DIALER_HTTP_PRIMARY_MODE_UPLOAD : EFL_NET_DIALER_HTTP_PRIMARY_MODE_DOWNLOAD),
                             efl_net_dialer_proxy_set(efl_added, proxy_url),
                             efl_net_dialer_http_authentication_set(efl_added, url_con->httpauth.username, url_con->httpauth.password, url_con->httpauth.method, url_con->httpauth.restricted),
                             efl_net_dialer_http_version_set(efl_added, url_con->http_version),
                             efl_net_dialer_http_allow_redirects_set(efl_added, EINA_TRUE),
                             efl_net_dialer_http_ssl_verify_set(efl_added, url_con->ssl_verify_peer, url_con->ssl_verify_peer),
                             efl_net_dialer_http_ssl_certificate_authority_set(efl_added, url_con->ca_path),
                             efl_event_callback_array_add(efl_added, ecore_con_url_dialer_cbs(), url_con));
   EINA_SAFETY_ON_NULL_GOTO(url_con->dialer, error);

   curl_easy = efl_net_dialer_http_curl_get(url_con->dialer);
   EINA_SAFETY_ON_NULL_GOTO(curl_easy, error_curl_easy);

   if (url_con->verbose)
     {
        _c->curl_easy_setopt(curl_easy, CURLOPT_DEBUGFUNCTION, NULL);
        _c->curl_easy_setopt(curl_easy, CURLOPT_DEBUGDATA, NULL);
        _c->curl_easy_setopt(curl_easy, CURLOPT_VERBOSE, 1L);
        DBG("HTTP Dialer %p is set to legacy debug function (CURL's default, no eina_log)", url_con->dialer);
     }

   _c->curl_easy_setopt(curl_easy, CURLOPT_FTP_USE_EPSV, (long)url_con->ftp_use_epsv);

   /* previously always set encoding to gzip,deflate */
   efl_net_dialer_http_request_header_add(url_con->dialer, "Accept-Encoding", "gzip,deflate");

   if (url_con->time.condition != ECORE_CON_URL_TIME_NONE)
     {
        char *ts = efl_net_dialer_http_date_serialize(url_con->time.stamp);
        if (ts)
          {
             efl_net_dialer_http_request_header_add(url_con->dialer,
                                                    url_con->time.condition == ECORE_CON_URL_TIME_IFMODSINCE ? "If-Modified-Since" : "If-Unmodified-Since",
                                                    ts);
             free(ts);
          }
     }

   EINA_LIST_FOREACH(url_con->request_headers, n, header)
     efl_net_dialer_http_request_header_add(url_con->dialer, header->key, header->value);

   _c->curl_easy_setopt(curl_easy, CURLOPT_COOKIESESSION, (long)url_con->cookies.ignore_old_session);

   EINA_LIST_FOREACH(url_con->cookies.files, n, s)
     _c->curl_easy_setopt(curl_easy, CURLOPT_COOKIEFILE, s);

   if (url_con->cookies.jar)
     _c->curl_easy_setopt(curl_easy, CURLOPT_COOKIEJAR, url_con->cookies.jar);

   EINA_LIST_FREE(url_con->cookies.cmds, s) /* free: only to execute once! */
     _c->curl_easy_setopt(curl_easy, CURLOPT_COOKIELIST, s);

   // Users should hook to their window animator if they want to show in real-time,
   // or have a slower timer... but the old API requested a period event, so add it
   // based on global animator timeout
   url_con->progress.animator = ecore_animator_add(_ecore_con_url_progress_animator_cb, url_con);

   DBG("prepared %p %s (%s), proxy=%s, primary_mode=%d",
       url_con->dialer,
       method,
       efl_net_dialer_http_method_get(url_con->dialer),
       efl_net_dialer_proxy_get(url_con->dialer),
       efl_net_dialer_http_primary_mode_get(url_con->dialer));

   free(proxy_url);
   return EINA_TRUE;

 error_curl_easy:
   _ecore_con_url_dialer_close(url_con);
 error:
   free(proxy_url);
   return EINA_FALSE;
}

EAPI Ecore_Con_Url *
ecore_con_url_new(const char *url)
{
   Ecore_Con_Url *url_con;

   EINA_SAFETY_ON_NULL_RETURN_VAL(url, NULL);

   url_con = calloc(1, sizeof(Ecore_Con_Url));
   EINA_SAFETY_ON_NULL_RETURN_VAL(url_con, NULL);

   url_con->url = eina_stringshare_add(url);
   url_con->http_version = EFL_NET_HTTP_VERSION_V1_1;
   url_con->write_fd = -1;

   EINA_MAGIC_SET(url_con, ECORE_MAGIC_CON_URL);
   _url_con_url_list = eina_list_append(_url_con_url_list, url_con);

   return url_con;
}

EAPI Ecore_Con_Url *
ecore_con_url_custom_new(const char *url,
                         const char *custom_request)
{
   Ecore_Con_Url *url_con;

   EINA_SAFETY_ON_NULL_RETURN_VAL(url, NULL);
   EINA_SAFETY_ON_NULL_RETURN_VAL(custom_request, NULL);

   url_con = ecore_con_url_new(url);
   EINA_SAFETY_ON_NULL_RETURN_VAL(url_con, NULL);

   url_con->custom_request = eina_stringshare_add(custom_request);

   return url_con;
}

EAPI void
ecore_con_url_free(Ecore_Con_Url *url_con)
{
   ECORE_CON_URL_CHECK_RETURN(url_con);
   /* remove from list as early as possible, we don't want to call
    * ecore_con_url_free() again on pending handles in ecore_con_url_shutdown()
    */
   _url_con_url_list = eina_list_remove(_url_con_url_list, url_con);
   _ecore_con_url_free_internal(url_con);
}

EAPI void *
ecore_con_url_data_get(Ecore_Con_Url *url_con)
{
   ECORE_CON_URL_CHECK_RETURN(url_con, NULL);
   return url_con->data;
}

EAPI void
ecore_con_url_data_set(Ecore_Con_Url *url_con,
                       void *data)
{
   ECORE_CON_URL_CHECK_RETURN(url_con);
   url_con->data = data;
}

EAPI Eina_Bool
ecore_con_url_url_set(Ecore_Con_Url *url_con,
                      const char *url)
{
   ECORE_CON_URL_CHECK_RETURN(url_con, EINA_FALSE);
   eina_stringshare_replace(&url_con->url, url ? url : "");
   return EINA_TRUE;
}

EAPI const char *
ecore_con_url_url_get(Ecore_Con_Url *url_con)
{
   ECORE_CON_URL_CHECK_RETURN(url_con, NULL);
   return url_con->url;
}

/* LEGACY: HTTP requests */
EAPI Eina_Bool
ecore_con_url_get(Ecore_Con_Url *url_con)
{
   Eina_Error err;

   ECORE_CON_URL_CHECK_RETURN(url_con, EINA_FALSE);

   if (!_ecore_con_url_request_prepare(url_con, "GET"))
     return EINA_FALSE;

   err = efl_net_dialer_dial(url_con->dialer, url_con->url);
   if (err)
     {
        WRN("failed to HTTP GET '%s': %s", url_con->url, eina_error_msg_get(err));
        _ecore_con_url_dialer_close(url_con);
        return EINA_FALSE;
     }

   return EINA_TRUE;
}

EAPI Eina_Bool
ecore_con_url_head(Ecore_Con_Url *url_con)
{
   Eina_Error err;

   ECORE_CON_URL_CHECK_RETURN(url_con, EINA_FALSE);

   if (!_ecore_con_url_request_prepare(url_con, "HEAD"))
     return EINA_FALSE;

   err = efl_net_dialer_dial(url_con->dialer, url_con->url);
   if (err)
     {
        WRN("failed to HTTP HEAD '%s': %s", url_con->url, eina_error_msg_get(err));
        _ecore_con_url_dialer_close(url_con);
        return EINA_FALSE;
     }

   return EINA_TRUE;
}

EAPI Eina_Bool
ecore_con_url_post(Ecore_Con_Url *url_con,
                   const void *data,
                   long length,
                   const char *content_type)
{
   Eo *buffer, *copier;
   Eina_Slice slice = { .mem = data, .len = length };
   Eina_Error err;

   ECORE_CON_URL_CHECK_RETURN(url_con, EINA_FALSE);

   if (!_ecore_con_url_request_prepare(url_con, "POST"))
     return EINA_FALSE;

   if (content_type)
     efl_net_dialer_http_request_header_add(url_con->dialer, "Content-Type", content_type);

   buffer = efl_add(EFL_IO_BUFFER_CLASS, efl_loop_get(url_con->dialer),
                    efl_name_set(efl_added, "post-buffer"),
                    efl_io_closer_close_on_invalidate_set(efl_added, EINA_TRUE),
                    efl_io_closer_close_on_exec_set(efl_added, EINA_TRUE));
   EINA_SAFETY_ON_NULL_GOTO(buffer, error_buffer);

   err = efl_io_writer_write(buffer, &slice, NULL);
   if (err)
     {
        WRN("could not populate buffer %p with %ld bytes: %s",
            buffer, length, eina_error_msg_get(err));
        goto error_copier;
     }

   copier = efl_add(EFL_IO_COPIER_CLASS, efl_loop_get(url_con->dialer),
                    efl_name_set(efl_added, "send-copier"),
                    efl_io_copier_source_set(efl_added, buffer),
                    efl_io_copier_destination_set(efl_added, url_con->dialer),
                    efl_io_closer_close_on_invalidate_set(efl_added, EINA_FALSE),
                    efl_event_callback_array_add(efl_added, _ecore_con_url_copier_cbs(), url_con));
   EINA_SAFETY_ON_NULL_GOTO(copier, error_copier);

   err = efl_net_dialer_dial(url_con->dialer, url_con->url);
   if (err)
     {
        WRN("failed to post to '%s': %s", url_con->url, eina_error_msg_get(err));
        goto error_dialer;
     }

   url_con->input = buffer;
   url_con->send_copier = copier;
   DBG("posting to '%s' using an Efl.Io.Copier=%p", url_con->url, copier);

   return EINA_TRUE;

 error_dialer:
   efl_del(copier);
 error_copier:
   efl_del(buffer);
 error_buffer:
   _ecore_con_url_dialer_close(url_con);
   return EINA_FALSE;
}

/* LEGACY: headers */
EAPI void
ecore_con_url_additional_header_add(Ecore_Con_Url *url_con,
                                    const char *key,
                                    const char *value)
{
   Efl_Net_Http_Header *header;
   char *s;

   ECORE_CON_URL_CHECK_RETURN(url_con);
   EINA_SAFETY_ON_NULL_RETURN(key);
   EINA_SAFETY_ON_NULL_RETURN(value);

   header = malloc(sizeof(Efl_Net_Http_Header) +
                   strlen(key) + 1 +
                   strlen(value) + 1);
   EINA_SAFETY_ON_NULL_RETURN(header);

   header->key = s = (char *)header + sizeof(Efl_Net_Http_Header);
   memcpy(s, key, strlen(key) + 1);

   header->value = s = s + strlen(key) + 1;
   memcpy(s, value, strlen(value) + 1);

   url_con->request_headers = eina_list_append(url_con->request_headers,
                                               header);
}

EAPI void
ecore_con_url_additional_headers_clear(Ecore_Con_Url *url_con)
{
   ECORE_CON_URL_CHECK_RETURN(url_con);
   _ecore_con_url_request_headers_free(url_con);
}

EAPI void
ecore_con_url_time(Ecore_Con_Url *url_con,
                   Ecore_Con_Url_Time time_condition,
                   double timestamp)
{
   ECORE_CON_URL_CHECK_RETURN(url_con);
   url_con->time.condition = time_condition;
   url_con->time.stamp = timestamp;
}

/* LEGACY: cookies */
EAPI void
ecore_con_url_cookies_init(Ecore_Con_Url *url_con)
{
   CURL *curl_easy;

   ECORE_CON_URL_CHECK_RETURN(url_con);

   /* meaningful before and after dial, persist and apply */
   url_con->cookies.files = eina_list_append(url_con->cookies.files, eina_stringshare_add(""));

   if (!url_con->dialer) return;

   curl_easy = efl_net_dialer_http_curl_get(url_con->dialer);
   EINA_SAFETY_ON_NULL_RETURN(curl_easy);

   _c->curl_easy_setopt(curl_easy, CURLOPT_COOKIEFILE, "");
}

EAPI void
ecore_con_url_cookies_file_add(Ecore_Con_Url *url_con,
                               const char * const file_name)
{
   CURL *curl_easy;

   ECORE_CON_URL_CHECK_RETURN(url_con);
   EINA_SAFETY_ON_NULL_RETURN(file_name);

   /* meaningful before and after dial, persist and apply */
   url_con->cookies.files = eina_list_append(url_con->cookies.files, eina_stringshare_add(file_name));

   if (!url_con->dialer) return;

   curl_easy = efl_net_dialer_http_curl_get(url_con->dialer);
   EINA_SAFETY_ON_NULL_RETURN(curl_easy);

   _c->curl_easy_setopt(curl_easy, CURLOPT_COOKIEFILE, file_name);
}

EAPI void
ecore_con_url_cookies_clear(Ecore_Con_Url *url_con)
{
   static const char cookielist_cmd_all[] = "ALL";
   CURL *curl_easy;

   ECORE_CON_URL_CHECK_RETURN(url_con);

   /* only meaningful once, if not dialed, queue, otherwise execute */
   if (!url_con->dialer)
     {
        url_con->cookies.cmds = eina_list_append(url_con->cookies.cmds, cookielist_cmd_all);
        return;
     }

   curl_easy = efl_net_dialer_http_curl_get(url_con->dialer);
   EINA_SAFETY_ON_NULL_RETURN(curl_easy);

   _c->curl_easy_setopt(curl_easy, CURLOPT_COOKIELIST, cookielist_cmd_all);
}

EAPI void
ecore_con_url_cookies_session_clear(Ecore_Con_Url *url_con)
{
   static const char cookielist_cmd_sess[] = "SESS";
   CURL *curl_easy;

   ECORE_CON_URL_CHECK_RETURN(url_con);

   /* only meaningful once, if not dialed, queue, otherwise execute */
   if (!url_con->dialer)
     {
        url_con->cookies.cmds = eina_list_append(url_con->cookies.cmds, cookielist_cmd_sess);
        return;
     }

   curl_easy = efl_net_dialer_http_curl_get(url_con->dialer);
   EINA_SAFETY_ON_NULL_RETURN(curl_easy);

   _c->curl_easy_setopt(curl_easy, CURLOPT_COOKIELIST, cookielist_cmd_sess);
}

EAPI void
ecore_con_url_cookies_ignore_old_session_set(Ecore_Con_Url *url_con,
                                             Eina_Bool ignore)
{
   ECORE_CON_URL_CHECK_RETURN(url_con);
   url_con->cookies.ignore_old_session = ignore;
}

EAPI Eina_Bool
ecore_con_url_cookies_jar_file_set(Ecore_Con_Url *url_con,
                                   const char * const cookiejar_file)
{
   CURL *curl_easy;

   ECORE_CON_URL_CHECK_RETURN(url_con, EINA_FALSE);
   EINA_SAFETY_ON_NULL_RETURN_VAL(cookiejar_file, EINA_FALSE);

   /* meaningful before and after dial, persist and apply */
   eina_stringshare_replace(&url_con->cookies.jar, cookiejar_file);

   if (!url_con->dialer) return EINA_TRUE;

   curl_easy = efl_net_dialer_http_curl_get(url_con->dialer);
   EINA_SAFETY_ON_NULL_RETURN_VAL(curl_easy, EINA_FALSE);

   _c->curl_easy_setopt(curl_easy, CURLOPT_COOKIEJAR, url_con->cookies.jar);
   return EINA_TRUE;
}

EAPI void
ecore_con_url_cookies_jar_write(Ecore_Con_Url *url_con)
{
   CURL *curl_easy;

   ECORE_CON_URL_CHECK_RETURN(url_con);

   /* only meaningful after dialed */
   if (!url_con->dialer) return;

   curl_easy = efl_net_dialer_http_curl_get(url_con->dialer);
   EINA_SAFETY_ON_NULL_RETURN(curl_easy);

   _c->curl_easy_setopt(curl_easy, CURLOPT_COOKIELIST, "FLUSH");
}

/* LEGACY: file upload/download */
EAPI void
ecore_con_url_fd_set(Ecore_Con_Url *url_con, int fd)
{
   ECORE_CON_URL_CHECK_RETURN(url_con);

   if (url_con->write_fd == fd) return;

   url_con->write_fd = fd;
   if (!url_con->dialer) return;
}

EAPI Eina_Bool
ecore_con_url_ftp_upload(Ecore_Con_Url *url_con,
                         const char *filename,
                         const char *user,
                         const char *pass,
                         const char *upload_dir)
{
   char tmp[4096];
   char *bname;
   Eo *file, *copier;
   Eina_Error err;

   ECORE_CON_URL_CHECK_RETURN(url_con, EINA_FALSE);
   EINA_SAFETY_ON_NULL_RETURN_VAL(filename, EINA_FALSE);
   EINA_SAFETY_ON_TRUE_RETURN_VAL(filename[0] == '\0', EINA_FALSE);

   bname = strdup(filename);
   if (upload_dir)
     snprintf(tmp, sizeof(tmp), "%s/%s/%s", url_con->url, upload_dir, basename(bname));
   else
     snprintf(tmp, sizeof(tmp), "%s/%s", url_con->url, basename(bname));
   free(bname);

   if (!_ecore_con_url_request_prepare(url_con, "PUT"))
     return EINA_FALSE;

   efl_net_dialer_http_authentication_set(url_con->dialer, user, pass, EFL_NET_HTTP_AUTHENTICATION_METHOD_ANY, EINA_FALSE);

   file = efl_add(EFL_IO_FILE_CLASS, efl_loop_get(url_con->dialer),
                  efl_name_set(efl_added, "upload-file"),
                  efl_file_set(efl_added, filename),
                  efl_io_file_flags_set(efl_added, O_RDONLY),
                  efl_io_closer_close_on_invalidate_set(efl_added, EINA_TRUE),
                  efl_io_closer_close_on_exec_set(efl_added, EINA_TRUE));
   EINA_SAFETY_ON_NULL_GOTO(file, error_file);

   copier = efl_add(EFL_IO_COPIER_CLASS, efl_loop_get(url_con->dialer),
                    efl_name_set(efl_added, "send-copier"),
                    efl_io_copier_source_set(efl_added, file),
                    efl_io_copier_destination_set(efl_added, url_con->dialer),
                    efl_io_closer_close_on_invalidate_set(efl_added, EINA_FALSE),
                    efl_event_callback_array_add(efl_added, _ecore_con_url_copier_cbs(), url_con));
   EINA_SAFETY_ON_NULL_GOTO(copier, error_copier);

   err = efl_net_dialer_dial(url_con->dialer, tmp);
   if (err)
     {
        WRN("failed to upload file '%s' to '%s': %s", filename, tmp, eina_error_msg_get(err));
        goto error_dialer;
     }

   url_con->input = file;
   url_con->send_copier = copier;
   DBG("uploading file '%s' to '%s' using an Efl.Io.Copier=%p", filename, tmp, copier);

   return EINA_TRUE;

 error_dialer:
   efl_del(copier);
 error_copier:
   efl_del(file);
 error_file:
   _ecore_con_url_dialer_close(url_con);
   return EINA_FALSE;
}

EAPI void
ecore_con_url_ftp_use_epsv_set(Ecore_Con_Url *url_con,
                               Eina_Bool use_epsv)
{
   ECORE_CON_URL_CHECK_RETURN(url_con);
   url_con->ftp_use_epsv = use_epsv;
}

EAPI void
ecore_con_url_limit_upload_speed(Ecore_Con_Url *url_con, off_t max_speed)
{
   CURL *curl_easy;

   ECORE_CON_URL_CHECK_RETURN(url_con);
   EINA_SAFETY_ON_NULL_RETURN(_c);

   curl_easy = efl_net_dialer_http_curl_get(url_con->dialer);
   EINA_SAFETY_ON_NULL_RETURN(curl_easy);

   _c->curl_easy_setopt(curl_easy, CURLOPT_MAX_SEND_SPEED_LARGE, max_speed);
}

EAPI void
ecore_con_url_limit_download_speed(Ecore_Con_Url *url_con, off_t max_speed)
{
   CURL *curl_easy;

   ECORE_CON_URL_CHECK_RETURN(url_con);
   EINA_SAFETY_ON_NULL_RETURN(_c);

   curl_easy = efl_net_dialer_http_curl_get(url_con->dialer);
   EINA_SAFETY_ON_NULL_RETURN(curl_easy);

   _c->curl_easy_setopt(curl_easy, CURLOPT_MAX_RECV_SPEED_LARGE, max_speed);
}

/* LEGACY: proxy */
EAPI Eina_Bool
ecore_con_url_proxy_password_set(Ecore_Con_Url *url_con, const char *password)
{
   ECORE_CON_URL_CHECK_RETURN(url_con, EINA_FALSE);
   EINA_SAFETY_ON_NULL_RETURN_VAL(password, EINA_FALSE);
   eina_stringshare_replace(&url_con->proxy.password, password);
   return EINA_TRUE;
}

EAPI Eina_Bool
ecore_con_url_proxy_username_set(Ecore_Con_Url *url_con, const char *username)
{
   ECORE_CON_URL_CHECK_RETURN(url_con, EINA_FALSE);
   EINA_SAFETY_ON_NULL_RETURN_VAL(username, EINA_FALSE);
   eina_stringshare_replace(&url_con->proxy.username, username);
   return EINA_TRUE;
}

EAPI Eina_Bool
ecore_con_url_proxy_set(Ecore_Con_Url *url_con, const char *proxy_url)
{
   ECORE_CON_URL_CHECK_RETURN(url_con, EINA_FALSE);
   eina_stringshare_replace(&url_con->proxy.url, proxy_url);
   return EINA_TRUE;
}

/* LEGACY: response */
EAPI int
ecore_con_url_received_bytes_get(Ecore_Con_Url *url_con)
{
   ECORE_CON_URL_CHECK_RETURN(url_con, EINA_FALSE);
   return url_con->received_bytes;
}

EAPI int
ecore_con_url_status_code_get(Ecore_Con_Url *url_con)
{
   ECORE_CON_URL_CHECK_RETURN(url_con, 0);
   if (!url_con->dialer) return url_con->status;
   return efl_net_dialer_http_response_status_get(url_con->dialer);
}

EAPI const Eina_List *
ecore_con_url_response_headers_get(Ecore_Con_Url *url_con)
{
   ECORE_CON_URL_CHECK_RETURN(url_con, NULL);
   return url_con->response_headers;
}

/* LEGACY: SSL */
EAPI int
ecore_con_url_ssl_ca_set(Ecore_Con_Url *url_con,
                         const char *ca_path)
{
   ECORE_CON_URL_CHECK_RETURN(url_con, -1);
   eina_stringshare_replace(&url_con->ca_path, ca_path);
   url_con->ssl_verify_peer = !!ca_path;
   return 0;
}

EAPI void
ecore_con_url_ssl_verify_peer_set(Ecore_Con_Url *url_con,
                                  Eina_Bool verify)
{
   ECORE_CON_URL_CHECK_RETURN(url_con);
   url_con->ssl_verify_peer = !!verify;
}

/* LEGACY: misc */
EAPI Eina_Bool
ecore_con_url_httpauth_set(Ecore_Con_Url *url_con,
                           const char *username,
                           const char *password,
                           Eina_Bool safe)
{
   ECORE_CON_URL_CHECK_RETURN(url_con, EINA_FALSE);
   EINA_SAFETY_ON_NULL_RETURN_VAL(username, EINA_FALSE);
   EINA_SAFETY_ON_NULL_RETURN_VAL(password, EINA_FALSE);

   eina_stringshare_replace(&url_con->httpauth.username, username);
   eina_stringshare_replace(&url_con->httpauth.password, password);
   url_con->httpauth.method = safe ? EFL_NET_HTTP_AUTHENTICATION_METHOD_ANY_SAFE : EFL_NET_HTTP_AUTHENTICATION_METHOD_ANY;
   url_con->httpauth.restricted = safe;
   return EINA_TRUE;
}

EAPI Eina_Bool
ecore_con_url_http_version_set(Ecore_Con_Url *url_con, Ecore_Con_Url_Http_Version version)
{
   ECORE_CON_URL_CHECK_RETURN(url_con, EINA_FALSE);
   switch (version)
     {
      case ECORE_CON_URL_HTTP_VERSION_1_0:
         url_con->http_version = EFL_NET_HTTP_VERSION_V1_0;
         break;
      case ECORE_CON_URL_HTTP_VERSION_1_1:
         url_con->http_version = EFL_NET_HTTP_VERSION_V1_1;
         break;
      default:
         ERR("unknown HTTP version enum value %d", version);
         return EINA_FALSE;
     }
   return EINA_TRUE;
}

static Eina_Bool
_ecore_con_url_timeout_cb(void *data)
{
   Ecore_Con_Url *url_con = data;
   int status;

   url_con->timer = NULL;

   WRN("HTTP timeout url='%s'", efl_net_dialer_address_dial_get(url_con->dialer));

   status = efl_net_dialer_http_response_status_get(url_con->dialer);
   if ((status < 500) || (status > 599))
     {
        DBG("HTTP error %d reset to 1", status);
        status = 1; /* not a real HTTP error */
     }

   _ecore_con_event_url_complete_add(url_con, status);

   return EINA_FALSE;
}

EAPI void
ecore_con_url_timeout_set(Ecore_Con_Url *url_con, double timeout)
{
   ECORE_CON_URL_CHECK_RETURN(url_con);

   if (url_con->timer)
     {
        ecore_timer_del(url_con->timer);
        url_con->timer = NULL;
     }

   if (timeout <= 0.0) return;

   // NOTE: it is weird to start the timeout right away here, but it
   // was done like that and we're keeping it for compatibility
   url_con->timer = ecore_timer_add(timeout, _ecore_con_url_timeout_cb, url_con);
}

EAPI void
ecore_con_url_verbose_set(Ecore_Con_Url *url_con,
                          Eina_Bool verbose)
{
   CURL *curl_easy;

   ECORE_CON_URL_CHECK_RETURN(url_con);

   /* meaningful before and after dial, persist and apply */
   url_con->verbose = !!verbose;

   if (!url_con->dialer) return;

   curl_easy = efl_net_dialer_http_curl_get(url_con->dialer);
   EINA_SAFETY_ON_NULL_RETURN(curl_easy);

   if (url_con->verbose)
     {
        _c->curl_easy_setopt(curl_easy, CURLOPT_DEBUGFUNCTION, NULL);
        _c->curl_easy_setopt(curl_easy, CURLOPT_DEBUGDATA, NULL);
        DBG("HTTP Dialer %p is set to legacy debug function (CURL's default, no eina_log)", url_con->dialer);
     }
   _c->curl_easy_setopt(curl_easy, CURLOPT_VERBOSE, (long)url_con->verbose);
}

/**
 * @}
 */

