#include "ace/Refcounted_Auto_Ptr.h"
#include <map>
#include "boost/format.hpp"
#include "boost/lexical_cast.hpp"
#include "openssl/sha.h"

#include "vn_client_internal.h"

VN_CURL_Event_Handler g_event_handler;

std::string session_id;
std::string csrf_token;

struct login_info_t
{
    login_info_t(const VN_Client_Context_Ptr &c, vn_asynch_result_t *a, const std::string &u, const std::string &p)
        : ctx(c), ar(a), user_name(u), passwd(p)
    {
    }
    VN_Client_Context_Ptr ctx;
    vn_asynch_result_t *ar;
    std::string user_name;
    std::string passwd;
};

struct geo_t
{
    geo_t(vn_asynch_result_t *a, const char* arr[], size_t len)
        : ar(a), obj_arr_len(len)
    {
        obj_arr = new char*[len];
        for(size_t i=0; i<len; i++)
            obj_arr[i] = const_cast<char*>(arr[i]);
    }

    ~geo_t() {
        delete[] obj_arr;
    }

    vn_asynch_result_t *ar;
    char** obj_arr;
    size_t obj_arr_len;
};

static size_t write_cb(char *d, size_t n, size_t l, void *p)
{
    VN_Client_Result_Handler *r = static_cast<VN_Client_Result_Handler *>(p); 
    
    r->data()->append(d, n * l);

    return n * l;
}

static void perform_request(vn_asynch_result_t *ar, const VN_Client_Context_Ptr &ctx, const std::string &path, const std::string &post_data = std::string(), bool bWithToken = true)
{
    VN_Resource<CURL> curl = ctx->curl_pool->get();
    VN_Client_Result_Handler *rh = new VN_Client_Result_Handler(ctx, curl, ar->future_ptr);

    /*fprintf(stdout, "host=%s, port=%d, ssl=%d, timeout=%d, path=%s, postdata=%s\n",
            ctx->cfg->remote_host, ctx->cfg->remote_port,
            ctx->cfg->use_ssl, ctx->cfg->timeout,
            path.c_str(), post_data.c_str());*/

    // write callback
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_cb);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, rh);

    // misc
    curl_easy_setopt(curl, CURLOPT_PRIVATE, rh);
    curl_easy_setopt(curl, CURLOPT_VERBOSE, 0L);
    if (ctx->cfg->timeout)
        curl_easy_setopt(curl, CURLOPT_TIMEOUT, ctx->cfg->timeout);
    curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1);
    curl_easy_setopt(curl, CURLOPT_USERAGENT, "VideoNEXT API Client/1.0");
    curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);

    curl_easy_setopt(curl, CURLOPT_COOKIEFILE, ""); /* just to start the cookie engine */
    curl_easy_setopt(curl, CURLOPT_SHARE, ctx->curl_share);
    curl_easy_setopt(curl, CURLOPT_POST, 0);

    // preparing URL
    std::string url = (ctx->cfg->use_ssl ? std::string("https://") : std::string("http://")) + ctx->cfg->remote_host 
        + (ctx->cfg->remote_port ? (":" + boost::lexical_cast<std::string>(ctx->cfg->remote_port)) : "")
        + path;

    std::string my_cookie;
    if(!session_id.empty())
        my_cookie = std::string("PHPSESSID=") + session_id;
    if(!csrf_token.empty()) {
        my_cookie += my_cookie.empty() ? "token=" : "; token=";
        my_cookie += csrf_token;
    }

    if(!my_cookie.empty())
        curl_easy_setopt(curl, CURLOPT_COOKIE, my_cookie.c_str());

    // Get "token" from cookies and paste to url
    struct curl_slist *cookies;
    std::string token;
    if (bWithToken && curl_easy_getinfo(curl, CURLINFO_COOKIELIST, &cookies) == CURLE_OK)
    {
        struct curl_slist *nc = cookies;
        while (nc) 
        {
            char t[128] = {0};
       
//            printf("[%d]: %s\n", i++, nc->data);

            if (sscanf(nc->data,  "%*s\t%*s\t%*s\t%*s\t%*lu\ttoken\t%s", t))
            {
                token = t;
//                printf("!! token = %s\n", token.c_str());
                break;
            }

            nc = nc->next;
        }

        curl_slist_free_all(cookies);
        if (post_data.empty() && token[0]) // set &token=..
        {
            url += (url.find("?") == std::string::npos ? "?" : "&") + std::string("token=") + token;
        }
    }

    curl_easy_setopt(curl, CURLOPT_URL, url.c_str());

    if (!post_data.empty() && token[0])
    {
        std::string post_data_with_token(post_data + "&token=" + token);
        curl_easy_setopt(curl, CURLOPT_POST, 1);
        curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, post_data_with_token.size());
        curl_easy_setopt(curl, CURLOPT_COPYPOSTFIELDS, post_data_with_token.c_str());
    }

    g_event_handler.perform(curl);
}

static void process_geo_from_get_objects(vn_asynch_result_t *ar, void *user_data)
{
    geo_t *geo = (geo_t*)user_data;

    do
    {
        // get result of call
        vn_result_t *r = vn_asynch_result_get(ar, 0);
        assert(r);

        if (r->error_code)
        {
            vn_result_t *final_result = vn_result_create(r->error_code, r->error_string);
            geo->ar->future_ptr->set(final_result);
            break;
        }

        if(!r->kvl)
            break;

        std::map<std::string, std::string> geo_map;
        std::map<std::string, std::string>::const_iterator iter;
        const char* k_geo_pos = "STAT_GEO_POSITION";
        const size_t k_geo_pos_size = strlen(k_geo_pos) + 1;
        const char* k_geo_calib = "CAM_GEO_CALIBRATION";
        const size_t k_geo_calib_size = strlen(k_geo_calib) + 1;

        for (vn_kv_t **p = r->kvl; *p != 0; ++p)
        {
            vn_kv_t *obj_info = *p;
            char* obj = (char*)obj_info->key;

            bool bSkip = true;
            for(size_t i=0; i<geo->obj_arr_len; i++)
                if(!strcmp(geo->obj_arr[i], obj)) {
                    bSkip = false;
                    break;
                }
                if(!geo->obj_arr_len || !bSkip) {
                    char *geo_pos = NULL, *geo_calib = NULL;
                    for(vn_kv_t **p2 = (vn_kv_t**)(obj_info->value); *p2 != 0; ++p2) {
                        vn_kv_t *kvl = *p2;
                        if(!strncmp((char*)(kvl->key), k_geo_pos, k_geo_pos_size))
                            geo_pos = (char*)(kvl->value);
                        else if(!geo->obj_arr_len && !strncmp((char*)(kvl->key), k_geo_calib, k_geo_calib_size)) {
                            geo_calib = (char*)(kvl->value);
                            if(strncmp(geo_calib, "dynamic", 8))
                                break;
                        }

                        if(geo_pos && (geo->obj_arr_len || geo_calib)) {
                            geo_map[obj] = geo_pos;
                            break;
                        }
                    }
                }
        }

        int i = 0;
        vn_result_t *final_result = vn_result_create(0, NULL);
        final_result->kvl = vn_kv_list_create(geo_map.size());
        //printf("\ngeo_map size=%d\n", geo_map.size());
        for(iter = geo_map.begin(); iter != geo_map.end(); iter++, i++) {
            vn_kv_t& kvl = *final_result->kvl[i];
/*
            printf("\t%d ==> %s\n", iter->first, iter->second.c_str());

            vn_json_t *js = 0;
            js = vn_json_parse(iter->second.c_str());
            if (!js) {
                vn_result_t *final_result = vn_result_create(E_VN_INTERNAL_ERROR, NULL);
                vn_result_set_error(final_result, E_VN_INTERNAL_ERROR, (char*)"JSON parse failed: %s", geo_pos);
                geo->ar->future_ptr->set(final_result);
                break;
            }

            vn_json_t *lat = vn_json_get_object_item(js, "lat");
            assert(lat);
            vn_json_t *lng = vn_json_get_object_item(js, "lng");
            assert(lng);
            vn_json_t *alt = vn_json_get_object_item(js, "alt");
            assert(alt);
            vn_json_t *time = vn_json_get_object_item(js, "time");
            assert(time);
*/
            kvl.key = vn_malloc(iter->first.length()+1, NULL);
            strcpy((char*)kvl.key, iter->first.c_str());
            //vn_kv_t **kvl = vn_kv_list_create(4);
            kvl.value = vn_malloc(iter->second.size() + 1, NULL);
            strcpy((char*)kvl.value, iter->second.c_str());
        }

        geo->ar->future_ptr->set(final_result);
    } while(0);

    delete geo;
    vn_asynch_result_destroy(ar);
}

static void process_login_info(vn_asynch_result_t *ar, void *user_data)
{
    login_info_t *li = (login_info_t *)user_data;
    
    do
    {
        // get result of call
        vn_result_t *r = vn_asynch_result_get(ar, 0);
        assert(r);

        if (r->error_code)
        {
            vn_result_t *final_result = vn_result_create(r->error_code, r->error_string);
            li->ar->future_ptr->set(final_result);
            break;
        }

        // get encryption key
        char *key  = (char*)r->kvl[0]->value;
//        printf("key: %s\n", key);

        /////// sha512(${encryptionKey} + sha512(${password}) + ${encryptionKey})
        unsigned char digest[SHA512_DIGEST_LENGTH];
        char digest_str[SHA512_DIGEST_LENGTH*2+1];
    
        SHA512((const unsigned char*)li->passwd.c_str(), li->passwd.size(), (unsigned char*)&digest);    
        
        for(int i = 0; i < SHA512_DIGEST_LENGTH; i++)
            sprintf(&digest_str[i*2], "%02x", (unsigned int)digest[i]);
        
        std::string s = std::string(key) + digest_str + std::string(key);
        
        SHA512((const unsigned char*)s.c_str(), s.size(), (unsigned char*)&digest);
        for(int i = 0; i < SHA512_DIGEST_LENGTH; i++)
            sprintf(&digest_str[i*2], "%02x", (unsigned int)digest[i]);

        
//        printf("result digest: %s\n", digest_str);

        std::string post_data = std::string("name=") + li->user_name + std::string("&credentials=") + digest_str;

        perform_request(li->ar, li->ctx, PATH_LOGIN, post_data);

    } while (0);

    li->user_name.assign(65, 'X');
    li->passwd.assign(65, 'X');

    delete li;
    vn_asynch_result_destroy(ar);
}

void* vn_client_create(vn_client_config_t *cfg)
{
    vn_client_context *ctx = new vn_client_context;
    *ctx->cfg = *cfg;

    return new VN_Client_Context_Ptr(ctx);
}

void vn_client_set_auth(const char *php_sess_id, const char *token)
{
    session_id = php_sess_id;
    csrf_token = token;
}

vn_asynch_result_t *vn_client_add_object(void *p, const vn_kv_t **attrs)
{
    VN_Client_Context_Ptr *ctx = (VN_Client_Context_Ptr *)p;

    vn_asynch_result_t *ar = vn_asynch_result_create();

    std::string post_data("type=camera&attributes={");
    for (const vn_kv_t **p = attrs; *p != 0; p++)
    {
        const vn_kv_t *attr = *p;
        post_data += std::string("\"") + (char*)attr->key + std::string("\":\"") + (char*)attr->value + std::string("\",");
    }

    if (post_data.size() > 1)
        post_data[post_data.size() - 1] = '}';
    else
        post_data += '}';

    //vn_asynch_result_set_callback(ar, process_add_object, NULL);
    perform_request(ar, *ctx, PATH_ADD_OBJECT, post_data);

    return ar;
}

vn_asynch_result_t *vn_client_get_geo(void *p, const char* obj_ids[], unsigned len)
{
    VN_Client_Context_Ptr *ctx = (VN_Client_Context_Ptr *)p;

    vn_asynch_result_t *ar = vn_asynch_result_create();

    std::string path = PATH_GET_OBJECTS;
    /* // commented because objList filter is broken
    if(len && obj_ids != NULL) {
        path += "&objList=[";
        for (size_t i=0; i<len; i++)
            path += boost::lexical_cast<std::string>(obj_ids[i]) + std::string(",");
        path[path.size() - 1] = ']';
    }*/

    vn_asynch_result_t *interim_ar = vn_asynch_result_create();

    geo_t *geo = new geo_t(ar, obj_ids, len);

    vn_asynch_result_set_callback(interim_ar, process_geo_from_get_objects, geo);

    perform_request(interim_ar, *ctx, path);

    return ar;
}

vn_asynch_result_t *vn_client_perform_ptz_command(void* p,
                                                  const char* obj_id,
                                                  const char* mode,
                                                  const char* cmd)
{
    VN_Client_Context_Ptr *ctx = (VN_Client_Context_Ptr *)p;

    vn_asynch_result_t *ar = vn_asynch_result_create();

    // preparing request
    std::string req = (boost::format("%s?cam_objid=%s&mode=%s&%s") % PATH_SEND_PTZ_COMMAND % obj_id % mode % cmd).str();

    perform_request(ar, *ctx, req, std::string(), false); // avoid pasting token as it breaks down PTZ functionality

    return ar;
}

vn_asynch_result_t *vn_client_register_device(void *p,
                                              const char *hwid,
                                              const char *name,
                                              const char *telnum,
                                              const char *mediaFormat)
{
    VN_Client_Context_Ptr *ctx = (VN_Client_Context_Ptr *)p;

    vn_asynch_result_t *ar = vn_asynch_result_create();

    // preparing register request
    std::string post_data =
            "type=camera&attributes={\"MEDIA_FORMAT\":\"" + std::string(mediaFormat)
            + "\",\"CAMERAMODEL\":\"iStream\",\"NAME\":\"" + std::string(name)
            + "\",\"HWID\":\"" + std::string(hwid)
            + "\",\"TELEPHONE_NUMBER\":\"" + std::string(telnum)
            + "\",\"FRAMERATE\":\"30\",\"ARCFRAMERATE\":\"30\"}";

    //vn_asynch_result_set_callback(ar, process_register_device, NULL);
    perform_request(ar, *ctx, PATH_ADD_OBJECT, post_data);

    return ar;
}

vn_asynch_result_t *vn_client_login(void *p, const char *user_name, const char *password)
{
    VN_Client_Context_Ptr *ctx = (VN_Client_Context_Ptr *)p;

    vn_asynch_result_t *li_ar = vn_asynch_result_create();

    vn_asynch_result_t *ar = vn_asynch_result_create();
    login_info_t *li = new login_info_t(*ctx, ar, user_name, password);

    vn_asynch_result_set_callback(li_ar, process_login_info, li);
    perform_request(li_ar, *ctx, PATH_LOGIN_INFO);

    return ar;
}


vn_asynch_result_t *vn_client_get_objects(void *p, const char* set_id, bool* pool_full)
{
    VN_Client_Context_Ptr *ctx = (VN_Client_Context_Ptr *)p;

    if(pool_full) {
        if((*ctx)->curl_pool->is_full()) {
            *pool_full = true;
            return NULL;
        } else *pool_full = false;
    }

    vn_asynch_result_t *ar = vn_asynch_result_create(); 

    std::string path = PATH_GET_OBJECTS + (set_id ? "&setid=" + std::string(set_id) : "");

    perform_request(ar, *ctx, path);

    return ar;   
}

vn_asynch_result_t *vn_client_get_resource_tree(void *p, const char* role_id)
{
    VN_Client_Context_Ptr *ctx = (VN_Client_Context_Ptr *)p;

    vn_asynch_result_t *ar = vn_asynch_result_create();

    std::string path = PATH_GET_RESOURCE_TREE
            + (role_id ? "?roleid=" + std::string(role_id) : "");

    perform_request(ar, *ctx, path);

    return ar;
}

vn_asynch_result_t *vn_client_get_roles(void *p)
{
    VN_Client_Context_Ptr *ctx = (VN_Client_Context_Ptr *)p;

    vn_asynch_result_t *ar = vn_asynch_result_create(); 

    perform_request(ar, *ctx, PATH_GET_ROLES);

    return ar;
}

vn_asynch_result_t *vn_client_get_sets(void *p, const char* role_id)
{
    VN_Client_Context_Ptr *ctx = (VN_Client_Context_Ptr *)p;

    vn_asynch_result_t *ar = vn_asynch_result_create(); 

    std::string path = PATH_GET_SETS + (role_id ? "&roleid=" + std::string(role_id) : "");

    perform_request(ar, *ctx, path);

    return ar;
}

vn_asynch_result_t *vn_client_get_events(void *p, const char* role_id, const char* filter)
{
    VN_Client_Context_Ptr *ctx = (VN_Client_Context_Ptr *)p;

    vn_asynch_result_t *ar = vn_asynch_result_create();

    std::string path = PATH_GET_EVENTS + "?" + (role_id ? std::string("role_id=") + role_id : std::string(""));

    if(filter)
        path += "&filter=" + std::string(filter);

    perform_request(ar, *ctx, path);

    return ar;
}

vn_asynch_result_t  *vn_client_get_media_url(void *p, const char* video_obj_id, const char* audio_obj_id,
                                             time_t start, time_t end, const char *transcode, const char *resolution,
                                             int analytics, bool isLocal)
{
   char starttime[sizeof("yyyymmddhhmmss")+1] = {0};
   char endtime[sizeof("yyyymmddhhmmss")+1]   = {0};

   if(start && end)
   {
#if defined(__MINGW32CE__) || defined(WIN32)
      struct tm *ptm;
      ptm = gmtime(&start);
      strftime(starttime, sizeof starttime, "%Y%m%d%H%M%S", ptm);
#else
      struct tm ptm;
      gmtime_r(&start, &ptm);    
      strftime(starttime, sizeof starttime, "%Y%m%d%H%M%S", &ptm);
#endif

#if defined(__MINGW32CE__) || defined(WIN32)
      ptm = gmtime(&end);
      strftime(endtime, sizeof endtime, "%Y%m%d%H%M%S", ptm);
#else
      gmtime_r(&end, &ptm);
      strftime(endtime, sizeof endtime, "%Y%m%d%H%M%S", &ptm);
#endif
   }

   std::string path = PATH_GET_MOBILE_MEDIA_URL +
       + "?cameraid=" + std::string(video_obj_id)
       + (starttime[0] ? ("&startTime="+std::string(starttime)) : "")
       + (endtime[0] ?("&endTime="+std::string(endtime)) : "")
       + (isLocal ? "&isLocal=true" : "");
/*
   std::string path = PATH_AUTH_MGR +
       std::string("?return=mediastreamauth&streamtype=")
       + (starttime[0] ? "archive" : "live") 
       + std::string("&objid=") + video_obj_id
       + (audio_obj_id ? (std::string("&audioobjid=") + audio_obj_id) : std::string(""))
       + (starttime[0] ? ("&time_start="+std::string(starttime)) : "")
       + (endtime[0] ?("&time_end="+std::string(endtime)) : "")
       + (transcode ? "&transcode=" + std::string(transcode) : "")
       + (resolution ? "&dimensions=" + std::string(resolution) : "")
       + (analytics  ? "&analytics=" + boost::lexical_cast<std::string>(analytics) : "");
*/
    VN_Client_Context_Ptr *ctx = (VN_Client_Context_Ptr *)p;

    vn_asynch_result_t *ar = vn_asynch_result_create(); 

    perform_request(ar, *ctx, path);

    return ar;
}

vn_asynch_result_t *vn_client_get_snapshot(void *p, const char* video_obj_id, time_t ts, unsigned stream_num, int metadata)
{
    VN_Client_Context_Ptr *ctx = (VN_Client_Context_Ptr *)p;

    vn_asynch_result_t *ar = vn_asynch_result_create(); 

    std::string path = PATH_GET_SNAPSHOT + "?objid=" + std::string(video_obj_id)
        + (ts ? "&ts=" + boost::lexical_cast<std::string>(ts) : "")
        + (metadata ? "&metadata" : "")
        + (stream_num == 0 ? "&downscale" : "");

    perform_request(ar, *ctx, path);

    return ar;
}

vn_asynch_result_t *vn_client_set_object_attrs(void *p, const char* obj_id, const vn_kv_t **attrs)
{
    VN_Client_Context_Ptr *ctx = (VN_Client_Context_Ptr *)p;

    vn_asynch_result_t *ar = vn_asynch_result_create();

    std::string post_data = std::string("obj=") +  std::string(obj_id) + "&attributes={";
    for (const vn_kv_t **p = attrs; *p != 0; p++)
    {
        const vn_kv_t *attr = *p;
        post_data += std::string("\"") + (char*)attr->key + std::string("\":\"") + (char*)attr->value + std::string("\",");
    }

    if (post_data.size() > 1)
        post_data[post_data.size() - 1] = '}';
    else
        post_data += '}';

    perform_request(ar, *ctx, PATH_SET_OBJECT_ATTRS, post_data);
    
    return ar;
}

vn_asynch_result_t *vn_client_get_users(void *p)
{
    VN_Client_Context_Ptr *ctx = (VN_Client_Context_Ptr *)p;

    vn_asynch_result_t *ar = vn_asynch_result_create();

    perform_request(ar, *ctx, PATH_GET_USERS);

    return ar;
}

vn_asynch_result_t *vn_client_get_server_info(void *p)
{
    VN_Client_Context_Ptr *ctx = (VN_Client_Context_Ptr *)p;

    vn_asynch_result_t *ar = vn_asynch_result_create();

    std::string path = PATH_GET_SERVER_INFO;

    perform_request(ar, *ctx, path);

    return ar;
}

vn_asynch_result_t *vn_client_submit_action(void *p, int event_id, const char* action, const char* role_id, const char* post_data)
{
    VN_Client_Context_Ptr *ctx = (VN_Client_Context_Ptr *)p;

    vn_asynch_result_t *ar = vn_asynch_result_create();

    std::string path = PATH_EVENT + "/" + boost::lexical_cast<std::string>(event_id) + "/do-action/" + std::string(action) + "?roleid=" + std::string(role_id);

    perform_request(ar, *ctx, path, post_data);

    return ar;
}


void vn_client_destroy(void *p)
{
    VN_Client_Context_Ptr *ctx = (VN_Client_Context_Ptr *)p;

    vn_asynch_result_t *ar = vn_asynch_result_create(); 
    perform_request(ar, *ctx, PATH_LOGOUT);
    vn_asynch_result_destroy(ar);

    delete ctx;
}
