#include "json_util.h"

#include "common/log/log.h"
#include "common/util/Assert.h"

/*!
 * Strip out // and / * comments
 * Does not strip comments from within a string.
 * Assumes \" is used to escape quotes inside of a string.
 */
std::string strip_cpp_style_comments(const std::string& input) {
  std::string output;

  enum State { NORMAL, BLOCK_COMMENT, LINE_COMMENT, STRING } state = NORMAL;

  for (size_t i = 0; i < input.size(); i++) {
    char c = input[i];
    char next = i < (input.size() - 1) ? input[i + 1] : '\0';
    char prev = i > 0 ? input[i - 1] : '\0';

    switch (state) {
      case NORMAL:
        if (c == '/' && next == '*') {
          // Normal -> block
          state = BLOCK_COMMENT;
          i++;  // skip over *
        } else if (c == '/' && next == '/') {
          // Normal -> line
          state = LINE_COMMENT;
          i++;
        } else if (c == '"') {
          // Normal -> string
          state = STRING;
          output.push_back(c);
        } else {
          // Normal -> normal
          output.push_back(c);
        }
        break;
      case BLOCK_COMMENT:
        if (c == '*' && next == '/') {
          // Block -> normal
          state = NORMAL;
          i++;
        }
        // otherwise stay in block comment
        break;
      case LINE_COMMENT:
        if (c == '\n') {
          state = NORMAL;
          output.push_back(c);
        }
        break;
      case STRING:
        if (c == '"' && prev != '\\') {
          state = NORMAL;
        }
        output.push_back(c);
        break;
      default:
        ASSERT(false);
    }
  }

  if (state == BLOCK_COMMENT) {
    throw std::runtime_error("strip_cpp_style_comments ended in a block comment");
  }

  if (state == STRING) {
    throw std::runtime_error("strip_cpp_style_comments ended in a string.");
  }

  return output;
}

/*!
 * Parse JSON file with comments stripped. Unlike the default comment stripping feature
 * of nlohmann::json, this allows you to have multiple comments in a row!
 */
nlohmann::json parse_commented_json(const std::string& input, const std::string& source_name) {
  try {
    return nlohmann::json::parse(strip_cpp_style_comments(input));
  } catch (const std::exception& e) {
    if (source_name.empty()) {
      lg::error("Error parsing JSON file: {}", e.what());
    } else {
      lg::error("Error parsing JSON in file {}: {}", source_name, e.what());
    }
    throw;
  }
}

/*!
 * Parse something like:
 * 2 -> Range(2, 3)
 * [2, 3] -> Range(2, 3)
 * [2, 10] -> Range(2, 10)
 */
Range<int> parse_json_optional_integer_range(const nlohmann::json& json) {
  if (json.is_number_integer()) {
    auto first = json.get<int>();
    return Range<int>(first, first + 1);
  } else if (json.is_array()) {
    if (json.size() != 2) {
      throw std::runtime_error("Invalid array size in parse_json_optional_integer_range");
    }
    return {json[0].get<int>(), json[1].get<int>()};
  } else {
    throw std::runtime_error("Invalid json as input to parse_json_optional_integer_range");
  }
}
