/*
 * GDevelop C++ Platform
 * Copyright 2008-2016 Florian Rival (Florian.Rival@gmail.com). All rights
 * reserved. This project is released under the MIT License.
 */
#include "GDCpp/Extensions/Builtin/NetworkTools.h"

#include <SFML/Network.hpp>
#include <cstring>
#include <fstream>
#include <iomanip>
#include <string>

#include "GDCpp/Runtime/CommonTools.h"
#include "GDCpp/Runtime/Project/Variable.h"
#include "GDCpp/Runtime/RuntimeScene.h"

using namespace std;

void GD_API SendHttpRequest(const gd::String& host,
                            const gd::String& uri,
                            const gd::String& body,
                            const gd::String& method,
                            const gd::String& contentType,
                            gd::Variable& responseVar) {
  // Separate the host and the port number
  auto hostInfo = host.Split(U':');

  if (hostInfo.size() < 2)
    return;  // Invalid address (there should be two elements: "http" and
             // "//the.domain.com")

  // Create Http
  sf::Http http;
  http.setHost(hostInfo[0].ToUTF8() + ":" + hostInfo[1].ToUTF8(),
               hostInfo.size() > 2 ? hostInfo[2].To<unsigned short>() : 0);

  // Create request
  sf::Http::Request request;
  request.setMethod(method == "POST" ? sf::Http::Request::Post
                                     : sf::Http::Request::Get);
  request.setField("Content-Type",
                   contentType.empty() ? "application/x-www-form-urlencoded"
                                       : contentType.ToUTF8());
  request.setUri(uri.ToUTF8());
  request.setBody(body.ToUTF8());

  // Send request & Get response
  sf::Http::Response response = http.sendRequest(request);

  if (response.getStatus() == sf::Http::Response::Ok) {
    responseVar.SetString(gd::String::FromUTF8(response.getBody()));
  }
  // else request failed.
}

void GD_API DownloadFile(const gd::String& host,
                         const gd::String& uri,
                         const gd::String& outputfilename) {
  // Separate the host and the port number
  auto hostInfo = host.Split(U':');

  if (hostInfo.size() < 2)
    return;  // Invalid address (there should be two elements: "http" and
             // "//the.domain.com")

  // Create Http
  sf::Http http;
  http.setHost(hostInfo[0].ToUTF8() + ":" + hostInfo[1].ToUTF8(),
               hostInfo.size() > 2 ? hostInfo[2].To<unsigned short>() : 0);

  // Create request
  sf::Http::Request Request;
  Request.setMethod(sf::Http::Request::Get);
  Request.setUri(uri.ToUTF8());

  // Send request & Get response
  sf::Http::Response datas = http.sendRequest(Request);

  ofstream ofile(outputfilename.ToLocale().c_str(), ios_base::binary);
  if (ofile.is_open()) {
    ofile.write(datas.getBody().c_str(), datas.getBody().size());
    ofile.close();

    return;
  }

  cout << "Downloading file : Unable to open output file " << outputfilename;
  return;
}

// Private functions for JSON reading
namespace {
/**
 * Adapted from public domain library "jsoncpp"
 * (http://sourceforge.net/projects/jsoncpp/).
 */
static inline bool isControlCharacter(char ch) { return ch > 0 && ch <= 0x1F; }

/**
 * Adapted from public domain library "jsoncpp"
 * (http://sourceforge.net/projects/jsoncpp/).
 */
static bool containsControlCharacter(const char* str) {
  while (*str) {
    if (isControlCharacter(*(str++))) return true;
  }
  return false;
}

/**
 * Tool function converting a string to a quoted string that can be inserted
 * into a JSON file. Adapted from public domain library "jsoncpp"
 * (http://sourceforge.net/projects/jsoncpp/).
 */
std::string StringToQuotedJSONString(const char* value) {
  if (value == NULL) return "";
  // Not sure how to handle unicode...
  if (strpbrk(value, "\"\\\b\f\n\r\t") == NULL &&
      !containsControlCharacter(value))
    return std::string("\"") + value + "\"";
  // We have to walk value and escape any special characters.
  // Appending to std::string is not efficient, but this should be rare.
  // (Note: forward slashes are *not* rare, but I am not escaping them.)
  std::string::size_type maxsize =
      strlen(value) * 2 + 3;  // allescaped+quotes+NULL
  std::string result;
  result.reserve(maxsize);  // to avoid lots of mallocs
  result += "\"";
  for (const char* c = value; *c != 0; ++c) {
    switch (*c) {
      case '\"':
        result += "\\\"";
        break;
      case '\\':
        result += "\\\\";
        break;
      case '\b':
        result += "\\b";
        break;
      case '\f':
        result += "\\f";
        break;
      case '\n':
        result += "\\n";
        break;
      case '\r':
        result += "\\r";
        break;
      case '\t':
        result += "\\t";
        break;
      // case '/':
      // Even though \/ is considered a legal escape in JSON, a bare
      // slash is also legal, so I see no reason to escape it.
      // (I hope I am not misunderstanding something.
      // blep notes: actually escaping \/ may be useful in javascript to avoid
      // </ sequence. Should add a flag to allow this compatibility mode and
      // prevent this sequence from occurring.
      default:
        if (isControlCharacter(*c)) {
          std::ostringstream oss;
          oss << "\\u" << std::hex << std::uppercase << std::setfill('0')
              << std::setw(4) << static_cast<int>(*c);
          result += oss.str();
        } else {
          result += *c;
        }
        break;
    }
  }
  result += "\"";
  return result;
}
}  // namespace

gd::String GD_API VariableStructureToJSON(const gd::Variable& variable) {
  if (variable.GetType() == gd::Variable::Type::Number)
    return gd::String::From(variable.GetValue());
  else if (variable.GetType() == gd::Variable::Type::String)
    return gd::String::FromUTF8(
        StringToQuotedJSONString(variable.GetString().c_str()));
  else if (variable.GetType() == gd::Variable::Type::Boolean) {
    gd::String str = variable.GetBool() ? "true" : "false";
    return str;
  }
  else if (variable.GetType() == gd::Variable::Type::Structure) {
    gd::String str = "{";
    bool firstChild = true;
    for (auto i = variable.GetAllChildren().begin();
         i != variable.GetAllChildren().end();
         ++i) {
      if (!firstChild) str += ",";
      str += gd::String::FromUTF8(StringToQuotedJSONString(i->first.c_str())) +
             ": " + VariableStructureToJSON(*i->second);

      firstChild = false;
    }

    str += "}";
    return str;
  } else {
    gd::String str = "[";
    bool firstChild = true;
    for (auto i = variable.GetAllChildrenArray().begin();
         i != variable.GetAllChildrenArray().end();
         ++i) {
      if (!firstChild) str += ",";
      str += VariableStructureToJSON(**i);

      firstChild = false;
    }

    str += "]";
    return str;
  };
}

gd::String GD_API ObjectVariableStructureToJSON(RuntimeObject* object,
                                                const gd::Variable& variable) {
  return VariableStructureToJSON(variable);
}

// Private functions for JSON parsing
namespace {
size_t SkipBlankChar(const std::string& str, size_t pos) {
  const std::string blankChar = " \n";
  return str.find_first_not_of(blankChar, pos);
}

/**
 * Adapted from https://github.com/hjiang/jsonxx
 */
std::string DecodeString(const std::string& original) {
  std::string value;
  value.reserve(original.size());
  std::istringstream input("\"" + original + "\"");

  char ch = '\0', delimiter = '"';
  input.get(ch);
  if (ch != delimiter) return "";

  while (!input.eof() && input.good()) {
    input.get(ch);
    if (ch == delimiter) {
      break;
    }
    if (ch == '\\') {
      input.get(ch);
      switch (ch) {
        case '\\':
        case '/':
          value.push_back(ch);
          break;
        case 'b':
          value.push_back('\b');
          break;
        case 'f':
          value.push_back('\f');
          break;
        case 'n':
          value.push_back('\n');
          break;
        case 'r':
          value.push_back('\r');
          break;
        case 't':
          value.push_back('\t');
          break;
        case 'u': {
          int i;
          std::stringstream ss;
          for (i = 0; (!input.eof() && input.good()) && i < 4; ++i) {
            input.get(ch);
            ss << ch;
          }
          if (input.good() && (ss >> i)) value.push_back(i);
        } break;
        default:
          if (ch != delimiter) {
            value.push_back('\\');
            value.push_back(ch);
          } else
            value.push_back(ch);
          break;
      }
    } else {
      value.push_back(ch);
    }
  }
  if (input && ch == delimiter) {
    return value;
  } else {
    return "";
  }
}

/**
 * Return the position of the end of the string. Blank are skipped if
 * necessary
 * @param str The string to be used
 * @param startPos The start position
 * @param strContent A reference to a string that will be filled with the
 * string content.
 */
size_t SkipString(const std::string& str,
                  size_t startPos,
                  std::string& strContent) {
  startPos = SkipBlankChar(str, startPos);
  if (startPos >= str.length()) return std::string::npos;

  size_t endPos = startPos;

  if (str[startPos] == '"') {
    if (startPos + 1 >= str.length()) return std::string::npos;

    while (endPos == startPos || (str[endPos - 1] == '\\')) {
      endPos = str.find_first_of('\"', endPos + 1);
      if (endPos == std::string::npos)
        return std::string::npos;  // Invalid string
    }

    strContent = DecodeString(str.substr(startPos + 1, endPos - 1 - startPos));
    return endPos;
  }

  endPos = str.find_first_of(" \n,:");
  if (endPos >= str.length()) return std::string::npos;  // Invalid string

  strContent = DecodeString(str.substr(startPos, endPos - 1 - startPos));
  return endPos - 1;
}

/**
 * Parse a JSON string, starting from pos, and storing the result into the
 * specified variable. Note that the parsing is stopped as soon as a valid
 * object is parsed. \return The position at the end of the valid object
 * stored into the variable.
 */
size_t ParseJSONObject(const std::string& jsonStr,
                       size_t startPos,
                       gd::Variable& variable) {
  size_t pos = SkipBlankChar(jsonStr, startPos);
  if (pos >= jsonStr.length()) return std::string::npos;

  if (jsonStr[pos] == '{')  // Object
  {
    bool firstChild = true;
    while (firstChild || jsonStr[pos] == ',') {
      pos++;
      if (pos < jsonStr.length() && jsonStr[pos] == '}') break;

      std::string childName;
      pos = SkipString(jsonStr, pos, childName);

      pos++;
      pos = SkipBlankChar(jsonStr, pos);
      if (pos >= jsonStr.length() || jsonStr[pos] != ':')
        return std::string::npos;

      pos++;
      pos = ::ParseJSONObject(
          jsonStr, pos, variable.GetChild(gd::String::FromUTF8(childName)));

      pos = SkipBlankChar(jsonStr, pos);
      if (pos >= jsonStr.length()) return std::string::npos;
      firstChild = false;
    }

    if (jsonStr[pos] != '}') {
      std::cout << "Parsing error: Object not properly formed.";
      return std::string::npos;
    }
    return pos + 1;
  } else if (jsonStr[pos] ==
             '[')  // Array are translated into child named 0,1,2...
  {
    std::size_t index = 0;
    while (index == 0 || jsonStr[pos] == ',') {
      pos++;
      if (pos < jsonStr.length() && jsonStr[pos] == ']') break;
      pos = ::ParseJSONObject(
          jsonStr, pos, variable.GetChild(gd::String::From(index)));

      pos = SkipBlankChar(jsonStr, pos);
      if (pos >= jsonStr.length()) {
        std::cout << "Parsing error: element of array not properly formed.";
        return std::string::npos;
      }
      index++;
    }

    if (jsonStr[pos] != ']') {
      std::cout << "Parsing error: array not properly ended";
      return std::string::npos;
    }
    return pos + 1;
  } else if (jsonStr[pos] == '"')  // String
  {
    std::string str;
    pos = SkipString(jsonStr, pos, str);
    if (pos >= jsonStr.length()) {
      std::cout << "Parsing error: Invalid string";
      return std::string::npos;
    }

    variable.SetString(gd::String::FromUTF8(str));
    return pos + 1;
  } else {
    std::string str;
    size_t endPos = pos;
    const std::string separators = " \n,}";
    while (endPos < jsonStr.length() &&
           separators.find_first_of(jsonStr[endPos]) == std::string::npos) {
      endPos++;
    }

    str = jsonStr.substr(pos, endPos - pos);
    if (str == "true")
      variable.SetValue(1);
    else if (str == "false")
      variable.SetValue(0);
    else
      variable.SetValue(gd::String::FromUTF8(str).To<double>());
    return endPos;
  }
}
}  // namespace

void GD_API JSONToVariableStructure(const gd::String& jsonStr,
                                    gd::Variable& variable) {
  if (jsonStr.empty()) return;
  ::ParseJSONObject(jsonStr.c_str(), 0, variable);
}

void GD_API JSONToObjectVariableStructure(const gd::String& JSON,
                                          RuntimeObject* object,
                                          gd::Variable& variable) {
  JSONToVariableStructure(JSON, variable);
}
