/*
 * Copyright 2017-2026 Espressif Systems (Shanghai) PTE LTD
 * Copyright 2017 Neil Kolban
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/*
 * GeneralUtils.cpp
 *
 *  Created on: May 20, 2017
 *      Author: kolban
 */

#include "GeneralUtils.h"
#include <esp_system.h>
#include <string.h>
#include <stdio.h>
#include <string>
#include <sstream>
#include <iomanip>
#include "RTOS.h"
#include <esp_err.h>
#include <nvs.h>
#include <esp_wifi.h>
#include <esp_heap_caps.h>
#include <esp_system.h>
#include "esp_chip_info.h"
#include "esp32-hal-log.h"
#include <inttypes.h>

static const char kBase64Alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
                                      "abcdefghijklmnopqrstuvwxyz"
                                      "0123456789+/";

static int base64EncodedLength(size_t length) {
  return (length + 2 - ((length + 2) % 3)) / 3 * 4;
}  // base64EncodedLength

static int base64EncodedLength(const String &in) {
  return base64EncodedLength(in.length());
}  // base64EncodedLength

static void a3_to_a4(unsigned char *a4, unsigned char *a3) {
  a4[0] = (a3[0] & 0xfc) >> 2;
  a4[1] = ((a3[0] & 0x03) << 4) + ((a3[1] & 0xf0) >> 4);
  a4[2] = ((a3[1] & 0x0f) << 2) + ((a3[2] & 0xc0) >> 6);
  a4[3] = (a3[2] & 0x3f);
}  // a3_to_a4

static void a4_to_a3(unsigned char *a3, unsigned char *a4) {
  a3[0] = (a4[0] << 2) + ((a4[1] & 0x30) >> 4);
  a3[1] = ((a4[1] & 0xf) << 4) + ((a4[2] & 0x3c) >> 2);
  a3[2] = ((a4[2] & 0x3) << 6) + a4[3];
}  // a4_to_a3

/**
 * @brief Encode a string into base 64.
 * @param [in] in
 * @param [out] out
 */
bool GeneralUtils::base64Encode(const String &in, String *out) {
  std::string std_in(in.c_str());
  std::string std_out(out->c_str());
  int i = 0, j = 0;
  size_t enc_len = 0;
  unsigned char a3[3];
  unsigned char a4[4];
  std_out.resize(base64EncodedLength(in));

  int input_len = std_in.length();
  std::string::const_iterator input = std_in.begin();

  while (input_len--) {
    a3[i++] = *(input++);
    if (i == 3) {
      a3_to_a4(a4, a3);

      for (i = 0; i < 4; i++) {
        (std_out)[enc_len++] = kBase64Alphabet[a4[i]];
      }

      i = 0;
    }
  }

  if (i) {
    for (j = i; j < 3; j++) {
      a3[j] = '\0';
    }

    a3_to_a4(a4, a3);

    for (j = 0; j < i + 1; j++) {
      (std_out)[enc_len++] = kBase64Alphabet[a4[j]];
    }

    while ((i++ < 3)) {
      (std_out)[enc_len++] = '=';
    }
  }
  *out = String(std_out.c_str());

  return (enc_len == out->length());
}  // base64Encode

/**
 * @brief Dump general info to the log.
 * Data includes:
 * * Amount of free RAM
 */
void GeneralUtils::dumpInfo() {
  esp_chip_info_t chipInfo;
  esp_chip_info(&chipInfo);
  log_v("--- dumpInfo ---");
  log_v("Free heap: %lu", (unsigned long)heap_caps_get_free_size(MALLOC_CAP_8BIT));
  log_v("Chip Info: Model: %d, cores: %u, revision: %u", chipInfo.model, chipInfo.cores, chipInfo.revision);
  log_v("ESP-IDF version: %s", esp_get_idf_version());
  log_v("---");
}  // dumpInfo

/**
 * @brief Does the string end with a specific character?
 * @param [in] str The string to examine.
 * @param [in] c The character to look form.
 * @return True if the string ends with the given character.
 */
bool GeneralUtils::endsWith(String str, char c) {
  if (str.length() == 0) {
    return false;
  }
  if (str.charAt(str.length() - 1) == c) {
    return true;
  }
  return false;
}  // endsWidth

/*
static int DecodedLength(const String& in) {
	int numEq = 0;
	int n = (int) in.length();

	//for (String::const_reverse_iterator it = in.rbegin(); *it == '='; ++it) {
	for (int it = in.length()-1; in.charAt(it) == '='; --it) {
		++numEq;
	}
	return ((6 * n) / 8) - numEq;
} // DecodedLength
*/

static unsigned char b64_lookup(unsigned char c) {
  if (c >= 'A' && c <= 'Z') {
    return c - 'A';
  }
  if (c >= 'a' && c <= 'z') {
    return c - 71;
  }
  if (c >= '0' && c <= '9') {
    return c + 4;
  }
  if (c == '+') {
    return 62;
  }
  if (c == '/') {
    return 63;
  }
  return 255;
};  // b64_lookup

/**
 * @brief Decode a chunk of data that is base64 encoded.
 * @param [in] in The string to be decoded.
 * @param [out] out The resulting data.
 */
bool GeneralUtils::base64Decode(const String &in, String *out) {
  int i = 0, j = 0;
  size_t dec_len = 0;
  unsigned char a3[3];
  unsigned char a4[4];

  int input_len = in.length();
  int input_iterator = 0;

  //out->resize(DecodedLength(in));

  while (input_len--) {
    //if (*input == '=') {
    if (in[input_iterator] == '=') {
      break;
    }

    a4[i++] = in[input_iterator++];
    if (i == 4) {
      for (i = 0; i < 4; i++) {
        a4[i] = b64_lookup(a4[i]);
      }

      a4_to_a3(a3, a4);

      for (i = 0; i < 3; i++) {
        out->concat(a3[i]);
        dec_len++;
      }

      i = 0;
    }
  }

  if (i) {
    for (j = i; j < 4; j++) {
      a4[j] = '\0';
    }

    for (j = 0; j < 4; j++) {
      a4[j] = b64_lookup(a4[j]);
    }

    a4_to_a3(a3, a4);

    for (j = 0; j < i - 1; j++) {
      (*out)[dec_len++] = a3[j];
    }
  }

  return (dec_len == out->length());
}  // base64Decode

/*
void GeneralUtils::hexDump(uint8_t* pData, uint32_t length) {
	uint32_t index=0;
	Stringstream ascii;
	Stringstream hex;
	char asciiBuf[80];
	char hexBuf[80];
	hex.str("");
	ascii.str("");
	while(index < length) {
		hex << std::setfill('0') << std::setw(2) << std::hex << (int)pData[index] << ' ';
		if (std::isprint(pData[index])) {
			ascii << pData[index];
		} else {
			ascii << '.';
		}
		index++;
		if (index % 16 == 0) {
			strcpy(hexBuf, hex.str().c_str());
			strcpy(asciiBuf, ascii.str().c_str());
			log_v("%s %s", hexBuf, asciiBuf);
			hex.str("");
			ascii.str("");
		}
	}
	if (index %16 != 0) {
		while(index % 16 != 0) {
			hex << "   ";
			index++;
		}
		strcpy(hexBuf, hex.str().c_str());
		strcpy(asciiBuf, ascii.str().c_str());
		log_v("%s %s", hexBuf, asciiBuf);
		//log_v("%s %s", hex.str().c_str(), ascii.str().c_str());
	}
	FreeRTOS::sleep(1000);
}
*/

/*
void GeneralUtils::hexDump(uint8_t* pData, uint32_t length) {
	uint32_t index=0;
	static Stringstream ascii;
	static Stringstream hex;
	hex.str("");
	ascii.str("");
	while(index < length) {
		hex << std::setfill('0') << std::setw(2) << std::hex << (int)pData[index] << ' ';
		if (std::isprint(pData[index])) {
			ascii << pData[index];
		} else {
			ascii << '.';
		}
		index++;
		if (index % 16 == 0) {
			log_v("%s %s", hex.str().c_str(), ascii.str().c_str());
			hex.str("");
			ascii.str("");
		}
	}
	if (index %16 != 0) {
		while(index % 16 != 0) {
			hex << "   ";
			index++;
		}
		log_v("%s %s", hex.str().c_str(), ascii.str().c_str());
	}
	FreeRTOS::sleep(1000);
}
*/

/**
 * @brief Dump a representation of binary data to the console.
 *
 * @param [in] pData Pointer to the start of data to be logged.
 * @param [in] length Length of the data (in bytes) to be logged.
 * @return N/A.
 */
void GeneralUtils::hexDump(const uint8_t *pData, uint32_t length) {
  char ascii[80];
  char hex[80];
  char tempBuf[80];
  uint32_t lineNumber = 0;

  log_v("     00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f");
  log_v("     -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --");
  ascii[0] = '\0';
  hex[0] = '\0';
  uint32_t index = 0;
  while (index < length) {
    snprintf(tempBuf, sizeof(tempBuf), "%02x ", pData[index]);
    strncat(hex, tempBuf, sizeof(hex) - strlen(hex) - 1);
    if (isprint(pData[index])) {
      snprintf(tempBuf, sizeof(tempBuf), "%c", pData[index]);
    } else {
      snprintf(tempBuf, sizeof(tempBuf), ".");
    }
    strncat(ascii, tempBuf, sizeof(ascii) - strlen(ascii) - 1);
    index++;
    if (index % 16 == 0) {
      log_v("%04" PRIx32 " %s %s", lineNumber * 16, hex, ascii);
      ascii[0] = '\0';
      hex[0] = '\0';
      lineNumber++;
    }
  }
  if (index % 16 != 0) {
    while (index % 16 != 0) {
      strncat(hex, "   ", sizeof(hex) - strlen(hex) - 1);
      index++;
    }
    log_v("%.4" PRIx32 " %s %s", lineNumber * 16, hex, ascii);
  }
}  // hexDump

/**
 * @brief Convert an IP address to string.
 * @param ip The 4 byte IP address.
 * @return A string representation of the IP address.
 */
String GeneralUtils::ipToString(uint8_t *ip) {
  auto size = 16;
  char *val = (char *)malloc(size);
  snprintf(val, size, "%u.%u.%u.%u", ip[0], ip[1], ip[2], ip[3]);
  String res(val);
  free(val);
  return res;
}  // ipToString

/**
 * @brief Split a string into parts based on a delimiter.
 * @param [in] source The source string to split.
 * @param [in] delimiter The delimiter characters.
 * @return A vector of strings that are the split of the input.
 */
std::vector<String> GeneralUtils::split(String source, char delimiter) {
  // See also: https://stackoverflow.com/questions/5167625/splitting-a-c-stdstring-using-tokens-e-g
  std::vector<String> strings;
  std::size_t current, previous = 0;
  std::string std_source(source.c_str());
  current = std_source.find(delimiter);
  while (current != std::string::npos) {
    strings.push_back(trim(source.substring(previous, current)));
    previous = current + 1;
    current = std_source.find(delimiter, previous);
  }
  strings.push_back(trim(source.substring(previous, current)));
  return strings;
}  // split

/**
 * @brief Convert an ESP error code to a string.
 * @param [in] errCode The errCode to be converted.
 * @return A string representation of the error code.
 */
const char *GeneralUtils::errorToString(esp_err_t errCode) {
  switch (errCode) {
#if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_DEBUG
    case ESP_OK:                       return "ESP_OK";
    case ESP_FAIL:                     return "ESP_FAIL";
    case ESP_ERR_NO_MEM:               return "ESP_ERR_NO_MEM";
    case ESP_ERR_INVALID_ARG:          return "ESP_ERR_INVALID_ARG";
    case ESP_ERR_INVALID_SIZE:         return "ESP_ERR_INVALID_SIZE";
    case ESP_ERR_INVALID_STATE:        return "ESP_ERR_INVALID_STATE";
    case ESP_ERR_NOT_FOUND:            return "ESP_ERR_NOT_FOUND";
    case ESP_ERR_NOT_SUPPORTED:        return "ESP_ERR_NOT_SUPPORTED";
    case ESP_ERR_TIMEOUT:              return "ESP_ERR_TIMEOUT";
    case ESP_ERR_NVS_NOT_INITIALIZED:  return "ESP_ERR_NVS_NOT_INITIALIZED";
    case ESP_ERR_NVS_NOT_FOUND:        return "ESP_ERR_NVS_NOT_FOUND";
    case ESP_ERR_NVS_TYPE_MISMATCH:    return "ESP_ERR_NVS_TYPE_MISMATCH";
    case ESP_ERR_NVS_READ_ONLY:        return "ESP_ERR_NVS_READ_ONLY";
    case ESP_ERR_NVS_NOT_ENOUGH_SPACE: return "ESP_ERR_NVS_NOT_ENOUGH_SPACE";
    case ESP_ERR_NVS_INVALID_NAME:     return "ESP_ERR_NVS_INVALID_NAME";
    case ESP_ERR_NVS_INVALID_HANDLE:   return "ESP_ERR_NVS_INVALID_HANDLE";
    case ESP_ERR_NVS_REMOVE_FAILED:    return "ESP_ERR_NVS_REMOVE_FAILED";
    case ESP_ERR_NVS_KEY_TOO_LONG:     return "ESP_ERR_NVS_KEY_TOO_LONG";
    case ESP_ERR_NVS_PAGE_FULL:        return "ESP_ERR_NVS_PAGE_FULL";
    case ESP_ERR_NVS_INVALID_STATE:    return "ESP_ERR_NVS_INVALID_STATE";
    case ESP_ERR_NVS_INVALID_LENGTH:   return "ESP_ERR_NVS_INVALID_LENGTH";
    case ESP_ERR_WIFI_NOT_INIT:        return "ESP_ERR_WIFI_NOT_INIT";
    //case ESP_ERR_WIFI_NOT_START:
    //	return "ESP_ERR_WIFI_NOT_START";
    case ESP_ERR_WIFI_IF:        return "ESP_ERR_WIFI_IF";
    case ESP_ERR_WIFI_MODE:      return "ESP_ERR_WIFI_MODE";
    case ESP_ERR_WIFI_STATE:     return "ESP_ERR_WIFI_STATE";
    case ESP_ERR_WIFI_CONN:      return "ESP_ERR_WIFI_CONN";
    case ESP_ERR_WIFI_NVS:       return "ESP_ERR_WIFI_NVS";
    case ESP_ERR_WIFI_MAC:       return "ESP_ERR_WIFI_MAC";
    case ESP_ERR_WIFI_SSID:      return "ESP_ERR_WIFI_SSID";
    case ESP_ERR_WIFI_PASSWORD:  return "ESP_ERR_WIFI_PASSWORD";
    case ESP_ERR_WIFI_TIMEOUT:   return "ESP_ERR_WIFI_TIMEOUT";
    case ESP_ERR_WIFI_WAKE_FAIL: return "ESP_ERR_WIFI_WAKE_FAIL";
#endif
    default: return "Unknown ESP_ERR error";
  }
}  // errorToString

/**
 * @brief Convert a wifi_err_reason_t code to a string.
 * @param [in] errCode The errCode to be converted.
 * @return A string representation of the error code.
 *
 * @note: wifi_err_reason_t values as of April 2018 are: (1-24, 200-204) and are defined in ~/esp-idf/components/esp32/include/esp_wifi_types.h.
 */
const char *GeneralUtils::wifiErrorToString(uint8_t errCode) {
  if (errCode == ESP_OK) {
    return "ESP_OK (received SYSTEM_EVENT_STA_GOT_IP event)";
  }
  if (errCode == UINT8_MAX) {
    return "Not Connected (default value)";
  }

  switch ((wifi_err_reason_t)errCode) {
#if ARDUHAL_LOG_LEVEL >= ARDUHAL_LOG_LEVEL_DEBUG
    case WIFI_REASON_UNSPECIFIED:              return "WIFI_REASON_UNSPECIFIED";
    case WIFI_REASON_AUTH_EXPIRE:              return "WIFI_REASON_AUTH_EXPIRE";
    case WIFI_REASON_AUTH_LEAVE:               return "WIFI_REASON_AUTH_LEAVE";
    case WIFI_REASON_ASSOC_EXPIRE:             return "WIFI_REASON_ASSOC_EXPIRE";
    case WIFI_REASON_ASSOC_TOOMANY:            return "WIFI_REASON_ASSOC_TOOMANY";
    case WIFI_REASON_NOT_AUTHED:               return "WIFI_REASON_NOT_AUTHED";
    case WIFI_REASON_NOT_ASSOCED:              return "WIFI_REASON_NOT_ASSOCED";
    case WIFI_REASON_ASSOC_LEAVE:              return "WIFI_REASON_ASSOC_LEAVE";
    case WIFI_REASON_ASSOC_NOT_AUTHED:         return "WIFI_REASON_ASSOC_NOT_AUTHED";
    case WIFI_REASON_DISASSOC_PWRCAP_BAD:      return "WIFI_REASON_DISASSOC_PWRCAP_BAD";
    case WIFI_REASON_DISASSOC_SUPCHAN_BAD:     return "WIFI_REASON_DISASSOC_SUPCHAN_BAD";
    case WIFI_REASON_IE_INVALID:               return "WIFI_REASON_IE_INVALID";
    case WIFI_REASON_MIC_FAILURE:              return "WIFI_REASON_MIC_FAILURE";
    case WIFI_REASON_4WAY_HANDSHAKE_TIMEOUT:   return "WIFI_REASON_4WAY_HANDSHAKE_TIMEOUT";
    case WIFI_REASON_GROUP_KEY_UPDATE_TIMEOUT: return "WIFI_REASON_GROUP_KEY_UPDATE_TIMEOUT";
    case WIFI_REASON_IE_IN_4WAY_DIFFERS:       return "WIFI_REASON_IE_IN_4WAY_DIFFERS";
    case WIFI_REASON_GROUP_CIPHER_INVALID:     return "WIFI_REASON_GROUP_CIPHER_INVALID";
    case WIFI_REASON_PAIRWISE_CIPHER_INVALID:  return "WIFI_REASON_PAIRWISE_CIPHER_INVALID";
    case WIFI_REASON_AKMP_INVALID:             return "WIFI_REASON_AKMP_INVALID";
    case WIFI_REASON_UNSUPP_RSN_IE_VERSION:    return "WIFI_REASON_UNSUPP_RSN_IE_VERSION";
    case WIFI_REASON_INVALID_RSN_IE_CAP:       return "WIFI_REASON_INVALID_RSN_IE_CAP";
    case WIFI_REASON_802_1X_AUTH_FAILED:       return "WIFI_REASON_802_1X_AUTH_FAILED";
    case WIFI_REASON_CIPHER_SUITE_REJECTED:    return "WIFI_REASON_CIPHER_SUITE_REJECTED";
    case WIFI_REASON_BEACON_TIMEOUT:           return "WIFI_REASON_BEACON_TIMEOUT";
    case WIFI_REASON_NO_AP_FOUND:              return "WIFI_REASON_NO_AP_FOUND";
    case WIFI_REASON_AUTH_FAIL:                return "WIFI_REASON_AUTH_FAIL";
    case WIFI_REASON_ASSOC_FAIL:               return "WIFI_REASON_ASSOC_FAIL";
    case WIFI_REASON_HANDSHAKE_TIMEOUT:        return "WIFI_REASON_HANDSHAKE_TIMEOUT";
#endif
    default: return "Unknown ESP_ERR error";
  }
}  // wifiErrorToString

/**
 * @brief Convert a string to lower case.
 * @param [in] value The string to convert to lower case.
 * @return A lower case representation of the string.
 */
String GeneralUtils::toLower(String &value) {
  // Question: Could this be improved with a signature of:
  // String& GeneralUtils::toLower(String& value)
  std::transform(value.begin(), value.end(), value.begin(), ::tolower);
  return value;
}  // toLower

/**
 * @brief Remove white space from a string.
 */
String GeneralUtils::trim(const String &str) {
  std::string std_str(str.c_str());
  size_t first = std_str.find_first_not_of(' ');
  if (std::string::npos == first) {
    return str;
  }
  size_t last = std_str.find_last_not_of(' ');
  return str.substring(first, (last + 1));
}  // trim
