/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
   file LICENSE.rst or https://cmake.org/licensing for details.  */

#include "cmJSONState.h"

#include <iterator>
#include <memory>
#include <sstream>

#include <cm3p/json/reader.h>
#include <cm3p/json/value.h>
#include <cm3p/json/version.h>

#include "cmsys/FStream.hxx"

#include "cmStringAlgorithms.h"
#include "cmSystemTools.h"

cmJSONState::cmJSONState(std::string jsonFile, Json::Value* root)
  : Filename(std::move(jsonFile))
{
  cmsys::ifstream fin(this->Filename.c_str(), std::ios::in | std::ios::binary);
  if (!fin) {
    this->AddError(cmStrCat("File not found: ", this->Filename));
    return;
  }
  // If there's a BOM, toss it.
  cmsys::FStream::ReadBOM(fin);

  // Save the entire document.
  std::streampos finBegin = fin.tellg();
  this->doc = std::string(std::istreambuf_iterator<char>(fin),
                          std::istreambuf_iterator<char>());
  if (this->doc.empty()) {
    this->AddError("A JSON document cannot be empty");
    return;
  }
  fin.seekg(finBegin);

  Json::CharReaderBuilder builder;
  Json::CharReaderBuilder::strictMode(&builder.settings_);
  std::string errMsg;

#if JSONCPP_VERSION_HEXA >= 0x01090600
  // Has StructuredError
  std::unique_ptr<Json::CharReader> const reader(builder.newCharReader());
  reader->parse(doc.data(), doc.data() + doc.size(), root, &errMsg);
  std::vector<Json::CharReader::StructuredError> structuredErrors =
    reader->getStructuredErrors();
  for (auto const& structuredError : structuredErrors) {
    this->AddErrorAtOffset(structuredError.message,
                           structuredError.offset_start);
  }
#else
  // No StructuredError Available, Use error string from jsonCpp
  if (!Json::parseFromStream(builder, fin, root, &errMsg)) {
    errMsg = cmStrCat("JSON Parse Error: ", this->Filename, ":\n", errMsg);
    this->AddError(errMsg);
  }
#endif
}

void cmJSONState::AddError(std::string const& errMsg)
{
  this->errors.emplace_back(errMsg);
}

void cmJSONState::AddErrorAtValue(std::string const& errMsg,
                                  Json::Value const* value)
{
  if (value && !value->isNull()) {
    this->AddErrorAtOffset(errMsg, value->getOffsetStart());
  } else {
    this->AddError(errMsg);
  }
}

void cmJSONState::AddErrorAtOffset(std::string const& errMsg,
                                   std::ptrdiff_t offset)
{
  if (doc.empty()) {
    this->AddError(errMsg);
  } else {
    Location loc = LocateInDocument(offset);
    this->errors.emplace_back(loc, errMsg);
  }
}

std::string cmJSONState::GetErrorMessage(bool showContext)
{
  std::string message;
  std::string filenameName = cmSystemTools::GetFilenameName(this->Filename);
  for (auto const& error : this->errors) {
    Location loc = error.GetLocation();
    if (!filenameName.empty() && loc.line > 0) {
      message = cmStrCat(message, filenameName, ':', loc.line, ": ");
    }
    message = cmStrCat(message, error.GetErrorMessage(), '\n');
    if (showContext && loc.line > 0) {
      message = cmStrCat(message, GetJsonContext(loc), '\n');
    }
  }
  message.pop_back();
  return message;
}

std::string cmJSONState::key()
{
  if (!this->parseStack.empty()) {
    return this->parseStack.back().first;
  }
  return "";
}

std::string cmJSONState::key_after(std::string const& k)
{
  for (auto it = this->parseStack.begin(); it != this->parseStack.end();
       ++it) {
    if (it->first == k && (++it) != this->parseStack.end()) {
      return it->first;
    }
  }
  return "";
}

Json::Value const* cmJSONState::value_after(std::string const& k)
{
  for (auto it = this->parseStack.begin(); it != this->parseStack.end();
       ++it) {
    if (it->first == k && (++it) != this->parseStack.end()) {
      return it->second;
    }
  }
  return nullptr;
}

void cmJSONState::push_stack(std::string const& k, Json::Value const* value)
{
  this->parseStack.emplace_back(k, value);
}

void cmJSONState::pop_stack()
{
  this->parseStack.pop_back();
}

std::string cmJSONState::GetJsonContext(Location loc)
{
  std::string line;
  std::stringstream sstream(doc);
  for (int i = 0; i < loc.line; ++i) {
    std::getline(sstream, line, '\n');
  }
  return cmStrCat(line, '\n', std::string(loc.column - 1, ' '), '^');
}

cmJSONState::Location cmJSONState::LocateInDocument(ptrdiff_t offset)
{
  int line = 1;
  int col = 1;
  char const* beginDoc = doc.data();
  char const* last = beginDoc + offset;
  for (; beginDoc != last; ++beginDoc) {
    switch (*beginDoc) {
      case '\r':
        if (beginDoc + 1 != last && beginDoc[1] == '\n') {
          continue; // consume CRLF as a single token.
        }
        CM_FALLTHROUGH; // CR without a following LF is same as LF
      case '\n':
        col = 1;
        ++line;
        break;
      default:
        ++col;
        break;
    }
  }
  return { line, col };
}
