// Copyright 2015 by Thorsten von Eicken, see LICENSE.txt
//
// Adapted from: github.com/tuanpmt/esp_bridge, Created on: Jan 9, 2015, Author: Minh

#include "esp8266.h"
#include "sntp.h"
#include "cmd.h"
#include "uart.h"
#include <cgiwifi.h>
#ifdef MQTT
#include <mqtt_cmd.h>
#endif
#ifdef REST
#include <rest.h>
#endif
#include <web-server.h>
#ifdef SOCKET
#include <socket.h>
#endif
#include <ip_addr.h>
#include "esp-link/cgi.h"

#include "config.h"

#ifdef CMD_DBG
#define DBG(format, ...) do { os_printf(format, ## __VA_ARGS__); } while(0)
#else
#define DBG(format, ...) do { } while(0)
#endif

static void cmdNull(CmdPacket *cmd);
static void cmdSync(CmdPacket *cmd);
static void cmdWifiStatus(CmdPacket *cmd);
static void cmdGetTime(CmdPacket *cmd);
static void cmdGetWifiInfo(CmdPacket *cmd);
// static void cmdSetWifiInfo(CmdPacket *cmd);
static void cmdAddCallback(CmdPacket *cmd);

static void cmdWifiGetApCount(CmdPacket *cmd);
static void cmdWifiGetApName(CmdPacket *cmd);
static void cmdWifiSelectSSID(CmdPacket *cmd);
static void cmdWifiSignalStrength(CmdPacket *cmd);
static void cmdWifiQuerySSID(CmdPacket *cmd);
static void cmdWifiStartScan(CmdPacket *cmd);

void cmdMqttGetClientId(CmdPacket *cmd);

// keep track of last status sent to uC so we can notify it when it changes
static uint8_t lastWifiStatus = wifiIsDisconnected;
// keep track of whether we have registered our cb handler with the wifi subsystem
static bool wifiCbAdded = false;
// keep track of whether we received a sync command from uC
bool cmdInSync = false;

// Command dispatch table for serial -> ESP commands
const CmdList commands[] = {
  {CMD_NULL,            "NULL",           cmdNull},        // no-op
  {CMD_SYNC,            "SYNC",           cmdSync},        // synchronize
  {CMD_WIFI_STATUS,     "WIFI_STATUS",    cmdWifiStatus},
  {CMD_CB_ADD,          "ADD_CB",         cmdAddCallback},
  {CMD_GET_TIME,        "GET_TIME",       cmdGetTime},
  {CMD_GET_WIFI_INFO,   "GET_WIFI_INFO",  cmdGetWifiInfo},
  // {CMD_SET_WIFI_INFO,   "SET_WIFI_INFO",  cmdSetWifiInfo},

  {CMD_WIFI_GET_APCOUNT,	"WIFI_GET_APCOUNT",	cmdWifiGetApCount},
  {CMD_WIFI_GET_APNAME,		"WIFI_GET_APNAME",	cmdWifiGetApName},
  {CMD_WIFI_SELECT_SSID,	"WIFI_SELECT_SSID",	cmdWifiSelectSSID},
  {CMD_WIFI_SIGNAL_STRENGTH,	"WIFI_SIGNAL_STRENGTH",	cmdWifiSignalStrength},
  {CMD_WIFI_GET_SSID,		"WIFI_GET_SSID",	cmdWifiQuerySSID},
  {CMD_WIFI_START_SCAN,		"WIFI_START_SCAN",	cmdWifiStartScan},

#ifdef MQTT
  {CMD_MQTT_SETUP,      "MQTT_SETUP",     MQTTCMD_Setup},
  {CMD_MQTT_PUBLISH,    "MQTT_PUB",       MQTTCMD_Publish},
  {CMD_MQTT_SUBSCRIBE , "MQTT_SUB",       MQTTCMD_Subscribe},
  {CMD_MQTT_LWT,        "MQTT_LWT",       MQTTCMD_Lwt},
  {CMD_MQTT_GET_CLIENTID,"MQTT_CLIENTID", cmdMqttGetClientId},
#endif
#ifdef REST
  {CMD_REST_SETUP,      "REST_SETUP",     REST_Setup},
  {CMD_REST_REQUEST,    "REST_REQ",       REST_Request},
  {CMD_REST_SETHEADER,  "REST_SETHDR",    REST_SetHeader},
#endif
  {CMD_WEB_SETUP,       "WEB_SETUP",      WEB_Setup},
  {CMD_WEB_DATA,        "WEB_DATA",       WEB_Data},
#ifdef SOCKET
  {CMD_SOCKET_SETUP,    "SOCKET_SETUP",   SOCKET_Setup},
  {CMD_SOCKET_SEND,     "SOCKET_SEND",    SOCKET_Send},
#endif
};

//===== List of registered callbacks (to uC)

// WifiCb plus 10 for other stuff
#define MAX_CALLBACKS 12
CmdCallback callbacks[MAX_CALLBACKS]; // cleared in cmdSync

uint32_t ICACHE_FLASH_ATTR
cmdAddCb(char* name, uint32_t cb) {
  for (uint8_t i = 0; i < MAX_CALLBACKS; i++) {
    //DBG("cmdAddCb: index %d name=%s cb=%p\n", i, callbacks[i].name,
    //  (void *)callbacks[i].callback);
    // find existing callback or add to the end
    if (os_strncmp(callbacks[i].name, name, CMD_CBNLEN) == 0 || callbacks[i].name[0] == '\0') {
      os_strncpy(callbacks[i].name, name, sizeof(callbacks[i].name));
      callbacks[i].name[CMD_CBNLEN-1] = 0; // strncpy doesn't null terminate
      callbacks[i].callback = cb;
      DBG("cmdAddCb: '%s'->0x%x added at %d\n", callbacks[i].name, cb, i);
      return 1;
    }
  }
  return 0;
}

CmdCallback* ICACHE_FLASH_ATTR
cmdGetCbByName(char* name) {
  for (uint8_t i = 0; i < MAX_CALLBACKS; i++) {
    //DBG("cmdGetCbByName: index %d name=%s cb=%p\n", i, callbacks[i].name,
    //  (void *)callbacks[i].callback);
    // if callback doesn't exist or it's null
    if (os_strncmp(callbacks[i].name, name, CMD_CBNLEN) == 0) {
      DBG("cmdGetCbByName: cb %s found at index %d\n", name, i);
      return &callbacks[i];
    }
  }
  DBG("cmdGetCbByName: cb %s not found\n", name);
  return 0;
}

//===== Wifi callback

// Callback from wifi subsystem to notify us of status changes
static void ICACHE_FLASH_ATTR
cmdWifiCb(uint8_t wifiStatus) {
  if (wifiStatus != lastWifiStatus){
    DBG("cmdWifiCb: wifiStatus=%d\n", wifiStatus);
    lastWifiStatus = wifiStatus;
    CmdCallback *wifiCb = cmdGetCbByName("wifiCb");
    if ((uint32_t)wifiCb->callback != -1) {
      uint8_t status = wifiStatus == wifiGotIP ? 5 : 1;
      cmdResponseStart(CMD_RESP_CB, (uint32_t)wifiCb->callback, 1);
      cmdResponseBody((uint8_t*)&status, 1);
      cmdResponseEnd();
    }
  }
}

//===== Command handlers

// Command handler for Null command
static void ICACHE_FLASH_ATTR
cmdNull(CmdPacket *cmd) {
}

// Command handler for sync command
static void ICACHE_FLASH_ATTR
cmdSync(CmdPacket *cmd) {
  CmdRequest req;
  uart0_write_char(SLIP_END); // prefix with a SLIP END to ensure we get a clean start
  cmdRequest(&req, cmd);
  if(cmd->argc != 0 || cmd->value == 0) {
    cmdResponseStart(CMD_RESP_V, 0, 0);
    cmdResponseEnd();
    return;
  }

  // clear callbacks table
  os_memset(callbacks, 0, sizeof(callbacks));

  // TODO: call other protocols back to tell them to reset

  // register our callback with wifi subsystem
  if (!wifiCbAdded) {
    wifiAddStateChangeCb(cmdWifiCb);
    wifiCbAdded = true;
  }

  // send OK response
  cmdResponseStart(CMD_RESP_V, cmd->value, 0);
  cmdResponseEnd();
  cmdInSync = true;

  // save the MCU's callback and trigger an initial callback
  cmdAddCb("wifiCb", cmd->value);
  lastWifiStatus = 0xff; // set to invalid value so we immediately send status cb in all cases
  cmdWifiCb(wifiState);

  return;
}

// Command handler for wifi status command
static void ICACHE_FLASH_ATTR
cmdWifiStatus(CmdPacket *cmd) {
  cmdResponseStart(CMD_RESP_V, wifiState, 0);
  cmdResponseEnd();
  return;
}

// Command handler for time
static void ICACHE_FLASH_ATTR
cmdGetTime(CmdPacket *cmd) {
  cmdResponseStart(CMD_RESP_V, sntp_get_current_timestamp(), 0);
  cmdResponseEnd();
  return;
}

// Command handler for IP information
static void ICACHE_FLASH_ATTR
cmdGetWifiInfo(CmdPacket *cmd) {
  CmdRequest req;

  cmdRequest(&req, cmd);
  if(cmd->argc != 0 || cmd->value == 0) {
    cmdResponseStart(CMD_RESP_V, 0, 0);
    cmdResponseEnd();
    return;
  }

  uint32_t callback = req.cmd->value;

  struct ip_info info;
  wifi_get_ip_info(0, &info);
  uint8_t mac[6];
  wifi_get_macaddr(0, mac);

  cmdResponseStart(CMD_RESP_CB, callback, 4);
  cmdResponseBody(&info.ip.addr, sizeof(info.ip.addr));
  cmdResponseBody(&info.netmask.addr, sizeof(info.netmask.addr));
  cmdResponseBody(&info.gw.addr, sizeof(info.gw.addr));
  cmdResponseBody(mac, sizeof(mac));
  cmdResponseEnd();
}

// Command handler to add a callback to the named-callbacks list, this is for a callback to the uC
static void ICACHE_FLASH_ATTR
cmdAddCallback(CmdPacket *cmd) {
  CmdRequest req;
  cmdRequest(&req, cmd);
  if (cmd->argc != 1 || cmd->value == 0) return;

  char name[16];
  uint16_t len;

  // get the callback name
  len = cmdArgLen(&req);
  if (len > 15) return; // max size of name is 15 characters
  if (cmdPopArg(&req, (uint8_t *)name, len)) return;
  name[len] = 0;
  DBG("cmdAddCallback: name=%s\n", name);

  cmdAddCb(name, cmd->value); // save the sensor callback
}

// Query the number of wifi access points
static void ICACHE_FLASH_ATTR cmdWifiGetApCount(CmdPacket *cmd) {
  int n = wifiGetApCount();
  DBG("WifiGetApCount : %d\n", n);
  cmdResponseStart(CMD_RESP_V, n, 0);
  cmdResponseEnd();
}

// Query the name of a wifi access point
static void ICACHE_FLASH_ATTR cmdWifiGetApName(CmdPacket *cmd) {
  CmdRequest req;

  cmdRequest(&req, cmd);

  int argc = cmdGetArgc(&req);
  DBG("cmdWifiGetApName: argc %d\n", argc);
  if (argc != 1)
    return;

  uint16_t i;
  cmdPopArg(&req, (uint8_t*)&i, 2);

  uint32_t callback = req.cmd->value;

  char myssid[33];
  wifiGetApName(i, myssid);
  myssid[32] = '\0';
  DBG("wifiGetApName(%d) -> {%s}\n", i, myssid);

  cmdResponseStart(CMD_RESP_CB, callback, 1);
  cmdResponseBody(myssid, strlen(myssid)+1);
  cmdResponseEnd();
}

/*
 * Select a wireless network.
 * This can be called in two ways :
 * - with a pair of strings (SSID, password)
 * - with a number and a string (index into network array, password)
 */
static void ICACHE_FLASH_ATTR cmdWifiSelectSSID(CmdPacket *cmd) {
  CmdRequest req;
  cmdRequest(&req, cmd);
  int argc = cmdGetArgc(&req);
  char ssid[33], pass[65];

  if (argc != 2) return;

  int len = cmdArgLen(&req);
  if (len == 1) {
    // Assume this is the index
    uint8_t ix;
    cmdPopArg(&req, &ix, 1);
    wifiGetApName(ix, ssid);
    ssid[32] = '\0';
  } else {
    // Longer than 1 byte: must be SSID
    if (len > 32) return;
    cmdPopArg(&req, ssid, len);
    ssid[len] = 0;
  }

  // Extract password from message
  len = cmdArgLen(&req);
  if (len > 64) return;
  cmdPopArg(&req, pass, len);
  pass[len] = 0;

  DBG("SelectSSID(%s,%s)", ssid, pass);
  connectToNetwork(ssid, pass);
}

#if 0
/*
 * Once we're attached to some wireless network, choose not to pick up address from
 * DHCP or so but set our own.
 */
static void ICACHE_FLASH_ATTR cmdSetWifiInfo(CmdPacket *cmd) {
  DBG("SetWifiInfo()\n");
}
#endif

static void ICACHE_FLASH_ATTR cmdWifiSignalStrength(CmdPacket *cmd) {
  CmdRequest req;

  cmdRequest(&req, cmd);

  int argc = cmdGetArgc(&req);
  if (argc != 1) {
    DBG("cmdWifiSignalStrength: argc %d\n", argc);
    return;
  }

  char x;
  cmdPopArg(&req, (uint8_t*)&x, 1);
  int i = x;
  DBG("cmdWifiSignalStrength: argc %d, ", argc);
  DBG("i %d\n", i);

  int rssi = wifiSignalStrength(i);

  cmdResponseStart(CMD_RESP_V, rssi, 0);
  cmdResponseEnd();
}

//
static void ICACHE_FLASH_ATTR cmdWifiQuerySSID(CmdPacket *cmd) {
  CmdRequest req;
  cmdRequest(&req, cmd);
  uint32_t callback = req.cmd->value;

  struct station_config conf;
  bool res = wifi_station_get_config(&conf);
  if (res) {
  // #warning handle me
  } else {
  }

  DBG("QuerySSID : %s\n", conf.ssid);

  cmdResponseStart(CMD_RESP_CB, callback, 1);
  cmdResponseBody(conf.ssid, strlen((char *)conf.ssid)+1);
  cmdResponseEnd();
}

// Start scanning, API interface
static void ICACHE_FLASH_ATTR cmdWifiStartScan(CmdPacket *cmd) {
  // call a function that belongs in esp-link/cgiwifi.c due to variable access
  wifiStartScan();
}

// Command handler for MQTT information
void ICACHE_FLASH_ATTR cmdMqttGetClientId(CmdPacket *cmd) {
  CmdRequest req;

  cmdRequest(&req, cmd);
  if(cmd->argc != 0 || cmd->value == 0) {
    cmdResponseStart(CMD_RESP_V, 0, 0);
    cmdResponseEnd();
    return;
  }

  uint32_t callback = req.cmd->value;

  cmdResponseStart(CMD_RESP_CB, callback, 1);
  cmdResponseBody(flashConfig.mqtt_clientid, strlen(flashConfig.mqtt_clientid)+1);
  cmdResponseEnd();

  os_printf("MqttGetClientId : %s\n", flashConfig.mqtt_clientid);
}