/**
 * @file evutils.cc
 *
 * @brief this is the main file impelents helpers
 *
 * @ingroup evcamera
 *
 * @author Bruce Lu
 * Contact: lzbgt@icloud.com
 *
 */

#include "evutils.hpp"
#include <sstream>
#include <vector>

using namespace std;
using namespace jsoncons;
using namespace jsoncons::literals;

unsigned short crc16(const unsigned char* data_p, unsigned char length)
{
    unsigned char x;
    unsigned short crc = 0xFFFF;

    while (length--) {
        x = crc >> 8 ^ *data_p++;
        x ^= x>>4;
        crc = (crc << 8) ^ ((unsigned short)(x << 12)) ^ ((unsigned short)(x <<5)) ^ ((unsigned short)x);
    }
    return crc;
}

namespace evutils {
const char consts::version[] = "v20200722";
const string consts::kMsgCmd = "cmd";
const string consts::kMsgCmdConfig = "config";
const string consts::kMsgConfigVgw = "vgw";
const string consts::kMsgConfigMqtt = "mqtt";
const string consts::kMsgConfigUpload = "upload";
const string consts::kMsgConfigFeatures = "features";
const string consts::kMsgConfigEnable = "enable";
const string consts::kMsgConfigLevel = "level";
const string consts::kMsgConfigInterval = "interval";
const string consts::kMsgConfigDuration = "duration";
const string consts::kMsgConfigHumanThreash = "humanThreash";
const string consts::kMsgConfigFaceThreash = "faceThresh";
const string consts::kMsgConfigRegion = "region";
const string consts::kMsgConfigMotion = "motion";
const string consts::kMsgConfigRecord = "record";
const string consts::kMsgConfigAi = "ai";
const string consts::kMsgCmdUploadVideo = "upload_video";
const string consts::kMsgCmdStatus = "status";
const string consts::kMsgCmdGetConfig = "get_config";
const string consts::kMsgCmdReboot = "reboot";
const string consts::kMsgCmdQR = "qr";
const string consts::kMsgCmdPtz = "ptz";
const string consts::kMsgType = "type";
const string consts::kMsgTypeImage = "image";
const string consts::kMsgEncode = "encoding";
const string consts::kMsgEncodeBase64 = "base64";
const string consts::kMsgFormat = "format";
const string consts::kMsgFormatJpeg = "JPG";
const string consts::kMsgCata = "category";
const string consts::kMsgTypeReport = "report";
const string consts::kMsgTypeResponse = "response";
const string consts::kMsgTime = "time";
const string consts::kMsgData = "data";
const string consts::kMsgRid = "rid";
const string consts::kMsgCode = "code";
const string consts::kMsgMsg = "msg";
const string consts::kMsgSn = "sn";
const string consts::recFilePath = "/mnt/sd/records/";
const long long consts::TS_2020 = 1577836800000L; /// ms
const long long consts::TS_MAX = 9999999999999L;  /// ms

/// topics
const string consts::sub_topic = "/evcamera/v1.0/request/";
const string consts::pub_topic_response = "/evcamera/v1.0/response/";
const string consts::pub_topic_eventdata = "/evcamera/v1.0/eventdata/";
const string consts::pub_topic_lastwill = "/evcamera/v1.0/lastwill/";
const string consts::pub_topic_report = "/evcamera/v1.0/report";
const string consts::mqtt_url = "tcp://admin:vJ3zHqWrHbrqxVMT@evcloudsvc.ilabservice.cloud:11883";
const string consts::vgw_addr = "tcp://vgw.ilabservice.cloud:7123";
const string consts::upload_addr = "tcp://evcloudsvc.ilabservice.cloud:10009";
const string consts::strPubUrl = "inproc://frame";

consts &consts::self()
{
    static consts cons;
    return cons;
}

string msg_field(const json &msg, const string &field)
{
    return msg.get_value_or<string>(field, "");
}

bool is_sdcard_avail(char *path)
{
    char p[] = "/sys/block/mmcblk0/size";
    if(path == nullptr) {
        path = p;
    }
    std::ifstream fsize(path);
    unsigned long long size;
    return (fsize >> size) && (size > 0);
}

json make_default_cloud_config()
{
    return R"(
        {
            "vgw":"vgw.ilabservice.cloud:7123",
            "mqtt":"admin:vJ3zHqWrHbrqxVMT@evcloudsvc.ilabservice.cloud:11883",
            "upload":"vup.ilabservice.cloud:10010",
            "features":{
                "push":0,
                "fps":20,
                "motion":{
                    "enabled":1,
                    "region":{
                        "maxX":1,
                        "maxY":1,
                        "minX":0,
                        "minY":0
                    },
                    "level":3
                },
                "recordLen":120,
                "ai":{
                    "enabled":0,
                    "faceThresh":0.75,
                    "humanThresh":0.63,
                    "region":{
                        "maxX":1,
                        "maxY":1,
                        "minX":0,
                        "minY":0
                    }
                }
            }
        }
    )"_json;
}

json make_default_sys_extra_config()
{
    return R"(
        {
            "ptz":{
                "x": 177.5,
                "y": 0
            }
        }
    )"_json;
}

json make_lastwill_msg(string sn)
{
    json js = R"(
            {
                "category":"lastwill",
                "code":0,
                "data":{
                },
                "msg":"I'M OFFLINE",
                "rid":"",
                "sn":"001231554A20",
                "time":1589537073532,
                "type":"report"
            }
        )"_json;
    js["time"] = chrono::duration_cast<chrono::milliseconds>(chrono::system_clock::now().time_since_epoch()).count();
    js["sn"] = sn;
    return js;
}

json make_online_msg(string sn)
{
    json js = R"(
                {
                    "category":"online",
                    "code":0,
                    "data":{
                    },
                    "msg":"I'M ONLINE",
                    "rid":"",
                    "sn":"001231554A20",
                    "time":1589537073532,
                    "type":"report"
                }
            )"_json;
    js["time"] = chrono::duration_cast<chrono::milliseconds>(chrono::system_clock::now().time_since_epoch()).count();
    js["sn"] = sn;
    return js;
}



void get_mac_addr(char *buf, char *intf)
{
    char p[] = "/sys/class/net/eth0/address";
    if(intf == nullptr) {
        intf = p;
    }

    std::ifstream fsize(intf);
    string s;
    fsize >>s;
    int i = 0;
    for(auto &k:s) {
        if(k != ':') {
            *(buf+ i++) = ::toupper(k);
        }
    }
    *(buf+i) = 0;
}

bool get_sdcard_megabytes(uint64_t &total, uint64_t &free, char* path )
{
    struct statvfs stat;
    char p[] = "/mnt/sd";
    if(path == nullptr) {
        path = p;
    }

    if (statvfs(path, &stat) != 0) {
        // error happens, just quits here
        return false;
    }

    /// NOTES: avoid trucating errors
    total = (stat.f_blocks/1024.0) * (stat.f_bsize /1024.0);
    free = (stat.f_bavail/1024.0) * (stat.f_bsize /1024.0);
    spdlog::info("sd: {} {} {} {} {} {} {}", stat.f_bsize, stat.f_blocks, stat.f_bavail, stat.f_bfree,stat.f_favail, stat.f_ffree, stat.f_frsize);
    return true;
}

/// returns negtive for failure, otherwise success
int setupDealer(void **ctx, void **s, string ident, string addr, int timeoutMs, int sndQS)
{
    int ret = 0;
    *ctx = zmq_ctx_new();
    *s = zmq_socket(*ctx, ZMQ_DEALER);
    ret = 1;
    zmq_setsockopt(*s, ZMQ_TCP_KEEPALIVE, &ret, sizeof (ret));
    ret = 20;
    zmq_setsockopt(*s, ZMQ_TCP_KEEPALIVE_IDLE, &ret, sizeof (ret));
    zmq_setsockopt(*s, ZMQ_TCP_KEEPALIVE_INTVL, &ret, sizeof (ret));
    ret = 2;
    zmq_setsockopt(*s, ZMQ_TCP_KEEPALIVE_CNT, &ret, sizeof (ret));
    if(sndQS!=0) {
        ret = zmq_setsockopt(*s, ZMQ_SNDHWM, &sndQS, sizeof (sndQS));
        if(ret < 0) {
            spdlog::error("failed to setup ZMQ_SNDHWM for Dealer {}@{}: {}", ident, addr, zmq_strerror(zmq_errno()));
        }
    }
    if(timeoutMs > 0) {
        zmq_setsockopt(*s, ZMQ_RCVTIMEO, &timeoutMs, sizeof (timeoutMs));
        zmq_setsockopt(*s, ZMQ_SNDTIMEO, &timeoutMs, sizeof (timeoutMs));
    }

    ret = zmq_setsockopt(*s, ZMQ_IDENTITY, ident.c_str(), ident.size());
    if(ret < 0) {
        zmq_close(*s);
        zmq_ctx_destroy(*ctx);
        *s = nullptr;
        *ctx = nullptr;
        spdlog::debug("{} failed setsockopts ZMQ_IDENTITY to {}: {}", ident, addr, zmq_strerror(zmq_errno()));
    }
    else {
        ret = zmq_connect(*s, addr.c_str());
        if(ret < 0) {
            zmq_close(*s);
            zmq_ctx_destroy(*ctx);
            *s = nullptr;
            *ctx = nullptr;
            spdlog::error("{} failed connect dealer: {}", ident, addr);
        }
    }

    return ret;
}

int setupRouter(void **ctx, void **s, string addr, int rcvQS)
{
    int ret = 0;
    *ctx = zmq_ctx_new();
    *s = zmq_socket(*ctx, ZMQ_ROUTER);
    ret = 1;
    zmq_setsockopt(*s, ZMQ_TCP_KEEPALIVE, &ret, sizeof (ret));
    ret = 5;
    zmq_setsockopt(*s, ZMQ_TCP_KEEPALIVE_IDLE, &ret, sizeof (ret));
    zmq_setsockopt(*s, ZMQ_TCP_KEEPALIVE_INTVL, &ret, sizeof (ret));
    ret = 2;
    zmq_setsockopt(*s, ZMQ_TCP_KEEPALIVE_CNT, &ret, sizeof (ret));
    if(rcvQS !=0) {
        zmq_setsockopt(*s, ZMQ_RCVHWM, &rcvQS, sizeof(rcvQS));
    }
    ret = zmq_bind(*s, addr.c_str());
    if(ret < 0) {
        spdlog::debug("failed to bind zmq at {} for reason: {}, retrying load configuration...", addr, zmq_strerror(zmq_errno()));
    }
    return ret;
}

int z_recv_multiple(void *s, vector<uint8_t> &buf, int &frames)
{
    int64_t more = 1;
    size_t more_size = sizeof(more);
    int ret = 0;
    int cnt = 0;
    while(more > 0) {
        cnt++;
        zmq_msg_t msg;
        ret = zmq_msg_init(&msg);
        if(ret < 0) {
            spdlog::debug("failed to receive multiple msg on zmq_msg_init: {}", zmq_strerror(zmq_errno()));
            break;
        }
        ret = zmq_msg_recv(&msg, s, 0);
        if(ret < 0) {
            spdlog::debug("z_recv_multiple: {}", zmq_strerror(zmq_errno()));
            break;
        }
        buf.insert(buf.end(), (uint8_t*)zmq_msg_data(&msg), (uint8_t*)zmq_msg_data(&msg)+ret);
        zmq_msg_close(&msg);
        ret = zmq_getsockopt(s, ZMQ_RCVMORE, &more, &more_size);
        if(ret < 0) {
            spdlog::debug("z_recv_multiple: {}", zmq_strerror(zmq_errno()));
            break;
        }
    }

    if(ret < 0 || (frames != 0 && cnt != frames)) {
        spdlog::error("failed to recv msg: {}",  ret < 0? zmq_strerror(ret): "invalid frames");
        return -1;
    }
    else {
        frames = cnt;
    }

    return 0;
}

namespace _internal_ {
recursive_mutex mutConfigFiles;
}


/// mk MUST NOT be null when isMerge is true
string save_json(json &data, string path, bool isMerge, fn_mk_default_json mk)
{
    lock_guard<recursive_mutex> lk(_internal_::mutConfigFiles);
    string rc;
    json js;
    json target = data;
    if(isMerge) {
        /// load configuration first
        spdlog::info("save_json merge");
        rc = load_json(js, path, mk);
        if(!rc.empty()) {
            return rc;
        }
        try {
            js.merge_or_update(data);
            target = js;
        }
        catch(exception &e) {
            rc = fmt::format("failed to merge json: {}, {} + {}", e.what(), js.to_string(), data.to_string());
            return rc;
        }
        spdlog::info("save_json merge end");
    }

    ofstream ocfg_(path, std::ios::trunc);
    if(ocfg_.is_open()) {
        ocfg_ << pretty_print(target);
        ocfg_.flush();
        ocfg_.close();
    }
    else {
        rc = fmt::format("failed to open configuration file: {}", path);
        spdlog::error(rc);
    }
    return rc;
}

string load_json(json &config, string path, fn_mk_default_json mk)
{
    lock_guard<recursive_mutex> lk(_internal_::mutConfigFiles);
    string rc;
    try {
        ifstream cfg_(path);
        config = json::parse(cfg_);
    }
    catch(exception &e) {
        spdlog::error("failed loading config {}: {}, force to make default config:\n{}", path, e.what(), config.to_string());
        if(mk != nullptr) {
            config = mk();
            rc = save_json(config, path);
            if(!rc.empty()) {
                spdlog::error("failed to save_json {}", path);
            }
        }
        else {
            rc = "no default make config function provided";
            spdlog::error(rc);
        }
    }

    return rc;
}

vector<string> split(const string &s, char delim)
{
    vector<string> result;
    stringstream ss (s);
    string item;
    while (getline (ss, item, delim)) {
        result.push_back (item);
    }

    return result;
}

}