#include "wpa_client.h"

#include <string>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdarg.h>
#include <errno.h>
#include <ctype.h>
#include <time.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <zlog.h>

extern zlog_category_t *zct;

namespace wifi {

size_t strlcpy(char *dst, const char *src, size_t dst_sz) {
    size_t n;

    for (n = 0; n < dst_sz; n++) {
        if ((*dst++ = *src++) == '\0') break;
    }

    if (n < dst_sz) return n;
    if (n > 0) *(dst - 1) = '\0';
    return n + strlen(src);
}

static std::string to_string(int val) {
    char *buf = NULL;
    int size;
    int temp;
    if (val < 0) {
        temp = -val;
        size = 2;
    } else {
        temp = val;
        size = 1;
    }
    for (; temp > 0; temp = temp / 10, size++)
        ;
    size++;

    buf = (char *)malloc(size);
    if (buf == NULL) {
        return "";
    }
    memset(buf, 0, size);
    sprintf(buf, "%d", val);
    std::string re(buf);
    free(buf);
    return re;
}

MXDHCP::MXDHCP() { pstream = NULL; }

MXDHCP::~MXDHCP() {
    if (pstream != NULL) {
        pclose(pstream);
        pstream = NULL;
    }
}

bool MXDHCP::Start(const std::string &net_interface) {
    if (pstream != NULL) {
        pclose(pstream);
        pstream = NULL;
    }
    std::string cmd = "udhcpc -b -i " + net_interface + " &";
    system("killall -9 udhcpc &");
    mssleep(1000 * 100);
    pstream = popen(cmd.data(), "r");
    if (pstream == NULL) {
        return false;
    }
    return true;
}

/*
* dhcp 这个类的主要原理是通过读取udhcpc 的输出来判断是否拿到了ip地址，实际情况中可以使用别的方法
*/
bool MXDHCP::GetDHCPStatus() {
    if (pstream == NULL) {
        return false;
    }
    int len = 1024;
    char *buff = (char *)malloc(sizeof(char) * len);
    if (buff == NULL) {
        return false;
    }
    int res = fread(buff, sizeof(char), len, pstream);
    if (res <= 0) {
        free(buff);
        return false;
    }
    if (!CheckString(buff, res)) {
        free(buff);
        return false;
    }
    pclose(pstream);
    pstream = NULL;
    free(buff);
    return true;
}

bool MXDHCP::CheckString(char *buf, int len) {
    if (strstr(buf, "adding dns") == NULL) {
        return false;
    }
    return true;
}

WPAClient::WPAClient(const std::string &wpa_control_path) {
    wpa_context_ = NULL;
    wpa_control_path_ = wpa_control_path;
    SetConnStatus(STEP_SCAN);
    Init();
}

WPAClient::~WPAClient() { Close(wpa_context_); }

bool WPAClient::Init() {
    wpa_context_ = Open(wpa_control_path_);
    if (wpa_context_ == NULL) {
        zlog_error(zct, "open wpa failed");
        return false;
    }
    SetConnStatus(STEP_SCAN);
    return true;
}

std::string WPAClient::GetCurrentSSID() {
    std::string cmd = "STATUS";
    std::string ssid_key = "\nssid=";
    std::string recv;
    std::string ssid = "";

    if (!Request(wpa_context_, cmd, recv)) {
        return "";
    }
    char temp[1024] = {0};
    strcpy(temp, recv.data());
    char *key = NULL;
    key = strstr(temp, ssid_key.data());
    if (key == NULL) {
        return "";
    }
    key += ssid_key.length();
    for (; (*key != '\0') && (*key != '\n') && (*key != '\r'); key++) {
        ssid += *key;
    }

    return ssid;
}

std::string WPAClient::GetNetSsid() {
    std::string cmd = "SCAN";
    std::string recv;
    if (!Request(wpa_context_, cmd, recv)) {
        return "";
    }

    recv.clear();
    cmd = "SCAN_RESULTS";

    if (!Request(wpa_context_, cmd, recv)) {
        return "";
    }
    return recv;
}
int WPAClient::GetWiFiRssi() {
    std::string cmd = "SIGNAL_POLL";
    std::string rssi_key = "RSSI";
    std::string recv;
    if (!Request(wpa_context_, cmd, recv)) {
        return 0;
    }
    char temp[1024] = {0};
    strcpy(temp, recv.data());
    zlog_info(zct, "recv = %s\n", recv.c_str());
    char *key = NULL;
    key = strstr(temp, rssi_key.data());
    if (key == NULL) {
        return 0;
    }
    for (; (*key != '\0') && (*key != '\n') && (*key != '\r'); key++) {
        if ((*key >= '0') && (*key <= '9')) {
            return atoi(key);
        }
    }
    return 0;
}

bool WPAClient::ConnectWiFi(const std::string &ssid, const std::string &password) {
    int net_id;
    SetConnStatus(STEP_SCAN);
    if (!CleanAllWiFi()) {
        return false;
    }
    zlog_info(zct, "CleanAllWiFi ");
    if (!AddWiFi(net_id)) {
        return false;
    }
    zlog_info(zct, "AddWiFi ");
    if (!SetSSID(ssid, net_id)) {
        return false;
    }
    zlog_info(zct, "SetSSID ");
    if (!SetPassword(password, 0)) {
        return false;
    }
    if (!SetProtocol(net_id, 1)) {
        return false;
    }
    zlog_info(zct, "SetProtocol");
    SetScanSSID(net_id);
    if (!EnableWiFi(net_id)) {
        return false;
    }
    zlog_info(zct, "EnableWiFi");
    return CheckCommandWithOk("SAVE_CONFIG");
    // return true;
}

bool WPAClient::ConnectWiFiWithNoPassword(const std::string &ssid) {
    int net_id;
    SetConnStatus(STEP_SCAN);
    if (!CleanAllWiFi()) {
        return false;
    }
    zlog_info(zct, "CleanAllWiFi");
    if (!AddWiFi(net_id)) {
        return false;
    }
    zlog_info(zct, "AddWiFi");
    if (!SetSSID(ssid, net_id)) {
        return false;
    }
    zlog_info(zct, "SetSSID");
    if (!SetProtocol(net_id, 0)) {
        return false;
    }
    zlog_info(zct, "SetProtocol");
    SetScanSSID(net_id);
    if (!EnableWiFi(net_id)) {
        return false;
    }
    zlog_info(zct, "EnableWiFi");

    return CheckCommandWithOk("SAVE_CONFIG");
}

bool WPAClient::ConnectWiFiWithLast() {
    SetConnStatus(STEP_SCAN);
    if (!CheckCommandWithOk("ENABLE_NETWORK all")) {
        return false;
    }
    return true;
}

bool WPAClient::GetConnectStatus() {
    std::string cmd = "STATUS";
    std::string recv;
    int addr;
    switch (step_) {
        case STEP_SCAN:
            if (!Request(wpa_context_, cmd, recv)) {
                return false;
            }
            addr = recv.find("COMPLETED");
            if (addr == -1) {
                return false;
            }
            SetConnStatus(STEP_CONNECT_AP_OK);
        case STEP_CONNECT_AP_OK:
            if (!dhcp_.Start("wlan0")) {
                return false;
            }
            SetConnStatus(STEP_DHCP_IP);
        case STEP_DHCP_IP:
            if (!dhcp_.GetDHCPStatus()) {
                return false;
            }
            SetConnStatus(STEP_SUCCESS);
        case STEP_SUCCESS: return true;
        default: return false;
    }
    return false;
}

void WPAClient::SetConnStatus(ConnectStatus status) { step_ = status; }

void WPAClient::wifiup() {
    system("ifconfig mlan0 up");
    system("ifconfig mlan0 up");
    system("ifconfig mlan0 up");
}

bool WPAClient::CleanWifi() {
    bool flag = CleanAllWiFi();
    CheckCommandWithOk("SAVE_CONFIG");
    return flag;
}

int WPAClient::GetConnStatus() { return step_; }

WPAContext *WPAClient::Open(const std::string &path) {
    struct WPAContext *ctrl;
    ctrl = (struct WPAContext *)malloc(sizeof(struct WPAContext));
    if (ctrl == NULL) {
        zlog_error(zct, "malloc failed");
        return NULL;
    }
    memset(ctrl, 0, sizeof(struct WPAContext));
    static int counter = 0;
    int ret;
    int tries = 0;
    size_t res;
    ctrl->s = socket(PF_UNIX, SOCK_DGRAM, 0);
    if (ctrl->s < 0) {
        zlog_error(zct, "socket failed");
        free(ctrl);
        return NULL;
    }
    ctrl->local.sun_family = AF_UNIX;
    counter++;
try_again:
    ret = snprintf(ctrl->local.sun_path, sizeof(ctrl->local.sun_path), "/tmp"
                                                                       "/"
                                                                       "wpa_ctrl_"
                                                                       "%d-%d",
                   (int)getpid(), counter);
    if (ret < 0 || (size_t)ret >= sizeof(ctrl->local.sun_path)) {
        zlog_error(zct, "snprintf failed");
        close(ctrl->s);
        free(ctrl);
        return NULL;
    }
    tries++;
    if (bind(ctrl->s, (struct sockaddr *)&ctrl->local, sizeof(ctrl->local)) < 0) {
        if (errno == EADDRINUSE && tries < 2) {
            /*
            * getpid() returns unique identifier for this instance
            * of wpa_ctrl, so the existing socket file must have
            * been left by unclean termination of an earlier run.
            * Remove the file and try again.
            */
            unlink(ctrl->local.sun_path);
            goto try_again;
        }
        zlog_error(zct, "bind failed");
        close(ctrl->s);
        free(ctrl);
        return NULL;
    }
    ctrl->dest.sun_family = AF_UNIX;
    res = strlcpy(ctrl->dest.sun_path, wpa_control_path_.data(), sizeof(ctrl->dest.sun_path));
    if (res >= sizeof(ctrl->dest.sun_path)) {
        close(ctrl->s);
        free(ctrl);
        return NULL;
    }
    if (connect(ctrl->s, (struct sockaddr *)&ctrl->dest, sizeof(ctrl->dest)) < 0) {
        zlog_error(zct, "connect failed");
        close(ctrl->s);
        unlink(ctrl->local.sun_path);
        free(ctrl);
        return NULL;
    }
    return ctrl;
}

void WPAClient::Close(WPAContext *context) {
    if (context == NULL) return;
    unlink(context->local.sun_path);
    if (context->s >= 0) close(context->s);
    free(context);
}

bool WPAClient::Request(WPAContext *context, const std::string &cmd, std::string &reply) {
    int res;
    fd_set rfds;
    struct timeval tv;
    const char *_cmd;
    char *cmd_buf = NULL;
    size_t _cmd_len;
    _cmd = cmd.data();
    _cmd_len = cmd.length();
    if (wpa_context_ == NULL) {
        zlog_error(zct, "wpa_context_ is null");
        return false;
    }
    if (send(wpa_context_->s, _cmd, _cmd_len, 0) < 0) {
        free(cmd_buf);
        return -1;
    }
    free(cmd_buf);
    for (;;) {
        tv.tv_sec = 10;
        tv.tv_usec = 0;
        FD_ZERO(&rfds);
        FD_SET(wpa_context_->s, &rfds);
        res = select(wpa_context_->s + 1, &rfds, NULL, NULL, &tv);
        if (res < 0) return false;
        if (FD_ISSET(wpa_context_->s, &rfds)) {
            char temp[1024 * 3] = {0};
            int temp_len = 1024 * 3;
            res = recv(wpa_context_->s, temp, temp_len, 0);
            if (res < 0) return false;
            if (res > 0 && temp[0] == '<') {
                continue;
            }
            reply = temp;
            break;
        } else {
            return false;
        }
    }
    return true;
}

bool WPAClient::CheckCommandWithOk(const std::string cmd) {
    std::string recv;
    if (!Request(wpa_context_, cmd, recv)) {
        zlog_error(zct, "send cmd falied");
        return false;
    }
    zlog_error(zct, "recv cmd %s", recv.data());
    if (strstr(recv.data(), "OK") == NULL) {
        return false;
    }
    return true;
}

bool WPAClient::AddWiFi(int &id) {
    std::string add_cmd = "ADD_NETWORK";
    std::string recv;
    if (!Request(wpa_context_, add_cmd, recv)) {
        return false;
    }
    id = atoi(recv.data());
    return true;
}

bool WPAClient::SetScanSSID(int id) {
    std::string cmd = "SET_NETWORK " + to_string(id) + " scan_ssid 1";
    return CheckCommandWithOk(cmd);
}
bool WPAClient::SetSSID(const std::string &ssid, int id) {
    std::string cmd = "SET_NETWORK " + to_string(id) + " ssid " + "\"" + ssid + "\"";
    return CheckCommandWithOk(cmd);
}
bool WPAClient::SetPassword(const std::string &password, int id) {
    std::string cmd = "SET_NETWORK " + to_string(id) + " psk " + "\"" + password + "\"";
    return CheckCommandWithOk(cmd);
}
bool WPAClient::SetProtocol(int id, int en_crypt) {
    std::string cmd = "SET_NETWORK " + to_string(id);
    if (en_crypt) {
        cmd += " key_mgmt WPA-PSK";
        return CheckCommandWithOk(cmd);
    } else {
        cmd += " key_mgmt NONE";
        return CheckCommandWithOk(cmd);
    }
}
bool WPAClient::CleanAllWiFi() {
    CheckCommandWithOk("REMOVE_NETWORK all");
    CheckCommandWithOk("DISABLE_NETWORK all");
    return true;
}
bool WPAClient::EnableWiFi(int id) {
    std::string cmd = "ENABLE_NETWORK " + to_string(id);
    return CheckCommandWithOk(cmd);
}

bool WPAClient::ReconnectWiFi() {
    std::string cmd = "RECONNECT";
    return CheckCommandWithOk(cmd);
}
}
