/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you 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.
 */

#include <thrift/protocol/TDebugProtocol.h>

#include <thrift/TToString.h>
#include <cassert>
#include <cctype>
#include <cstdio>
#include <stdexcept>

using std::string;

static string byte_to_hex(const uint8_t byte) {
  char buf[3];
  int ret = std::sprintf(buf, "%02x", (int)byte);
  THRIFT_UNUSED_VARIABLE(ret);
  assert(ret == 2);
  assert(buf[2] == '\0');
  return buf;
}

namespace apache {
namespace thrift {
namespace protocol {

string TDebugProtocol::fieldTypeName(TType type) {
  switch (type) {
  case T_STOP:
    return "stop";
  case T_VOID:
    return "void";
  case T_BOOL:
    return "bool";
  case T_BYTE:
    return "byte";
  case T_I16:
    return "i16";
  case T_I32:
    return "i32";
  case T_U64:
    return "u64";
  case T_I64:
    return "i64";
  case T_DOUBLE:
    return "double";
  case T_STRING:
    return "string";
  case T_STRUCT:
    return "struct";
  case T_MAP:
    return "map";
  case T_SET:
    return "set";
  case T_LIST:
    return "list";
  case T_UUID:
    return "uuid";
  default:
    return "unknown";
  }
}

void TDebugProtocol::indentUp() {
  indent_str_ += string(indent_inc, ' ');
}

void TDebugProtocol::indentDown() {
  if (indent_str_.length() < (string::size_type)indent_inc) {
    throw TProtocolException(TProtocolException::INVALID_DATA);
  }
  indent_str_.erase(indent_str_.length() - indent_inc);
}

uint32_t TDebugProtocol::writePlain(const string& str) {
  if (str.length() > (std::numeric_limits<uint32_t>::max)())
    throw TProtocolException(TProtocolException::SIZE_LIMIT);
  trans_->write((uint8_t*)str.data(), static_cast<uint32_t>(str.length()));
  return static_cast<uint32_t>(str.length());
}

uint32_t TDebugProtocol::writeIndented(const string& str) {
  if (str.length() > (std::numeric_limits<uint32_t>::max)())
    throw TProtocolException(TProtocolException::SIZE_LIMIT);
  if (indent_str_.length() > (std::numeric_limits<uint32_t>::max)())
    throw TProtocolException(TProtocolException::SIZE_LIMIT);
  uint64_t total_len = indent_str_.length() + str.length();
  if (total_len > (std::numeric_limits<uint32_t>::max)())
    throw TProtocolException(TProtocolException::SIZE_LIMIT);
  trans_->write((uint8_t*)indent_str_.data(), static_cast<uint32_t>(indent_str_.length()));
  trans_->write((uint8_t*)str.data(), static_cast<uint32_t>(str.length()));
  return static_cast<uint32_t>(indent_str_.length() + str.length());
}

uint32_t TDebugProtocol::startItem() {
  uint32_t size;

  switch (write_state_.back()) {
  case UNINIT:
    // XXX figure out what to do here.
    // throw TProtocolException(TProtocolException::INVALID_DATA);
    // return writeIndented(str);
    return 0;
  case STRUCT:
    return 0;
  case SET:
    return writeIndented("");
  case MAP_KEY:
    return writeIndented("");
  case MAP_VALUE:
    return writePlain(" -> ");
  case LIST:
    size = writeIndented("[" + to_string(list_idx_.back()) + "] = ");
    list_idx_.back()++;
    return size;
  default:
    throw std::logic_error("Invalid enum value.");
  }
}

uint32_t TDebugProtocol::endItem() {
  // uint32_t size;

  switch (write_state_.back()) {
  case UNINIT:
    // XXX figure out what to do here.
    // throw TProtocolException(TProtocolException::INVALID_DATA);
    // return writeIndented(str);
    return 0;
  case STRUCT:
    return writePlain(",\n");
  case SET:
    return writePlain(",\n");
  case MAP_KEY:
    write_state_.back() = MAP_VALUE;
    return 0;
  case MAP_VALUE:
    write_state_.back() = MAP_KEY;
    return writePlain(",\n");
  case LIST:
    return writePlain(",\n");
  default:
    throw std::logic_error("Invalid enum value.");
  }
}

uint32_t TDebugProtocol::writeItem(const std::string& str) {
  uint32_t size = 0;
  size += startItem();
  size += writePlain(str);
  size += endItem();
  return size;
}

uint32_t TDebugProtocol::writeMessageBegin(const std::string& name,
                                           const TMessageType messageType,
                                           const int32_t seqid) {
  (void)seqid;
  string mtype;
  switch (messageType) {
  case T_CALL:
    mtype = "call";
    break;
  case T_REPLY:
    mtype = "reply";
    break;
  case T_EXCEPTION:
    mtype = "exn";
    break;
  case T_ONEWAY:
    mtype = "oneway";
    break;
  }

  uint32_t size = writeIndented("(" + mtype + ") " + name + "(");
  indentUp();
  return size;
}

uint32_t TDebugProtocol::writeMessageEnd() {
  indentDown();
  return writeIndented(")\n");
}

uint32_t TDebugProtocol::writeStructBegin(const char* name) {
  uint32_t size = 0;
  size += startItem();
  size += writePlain(string(name) + " {\n");
  indentUp();
  write_state_.push_back(STRUCT);
  return size;
}

uint32_t TDebugProtocol::writeStructEnd() {
  indentDown();
  write_state_.pop_back();
  uint32_t size = 0;
  size += writeIndented("}");
  size += endItem();
  return size;
}

uint32_t TDebugProtocol::writeFieldBegin(const char* name,
                                         const TType fieldType,
                                         const int16_t fieldId) {
  // sprintf(id_str, "%02d", fieldId);
  string id_str = to_string(fieldId);
  if (id_str.length() == 1)
    id_str = '0' + id_str;

  return writeIndented(id_str + ": " + name + " (" + fieldTypeName(fieldType) + ") = ");
}

uint32_t TDebugProtocol::writeFieldEnd() {
  assert(write_state_.back() == STRUCT);
  return 0;
}

uint32_t TDebugProtocol::writeFieldStop() {
  return 0;
  // writeIndented("***STOP***\n");
}

uint32_t TDebugProtocol::writeMapBegin(const TType keyType,
                                       const TType valType,
                                       const uint32_t size) {
  // TODO(dreiss): Optimize short maps?
  uint32_t bsize = 0;
  bsize += startItem();
  bsize += writePlain(
      "map<" + fieldTypeName(keyType) + "," + fieldTypeName(valType) + ">"
      "[" + to_string(size) + "] {\n");
  indentUp();
  write_state_.push_back(MAP_KEY);
  return bsize;
}

uint32_t TDebugProtocol::writeMapEnd() {
  indentDown();
  write_state_.pop_back();
  uint32_t size = 0;
  size += writeIndented("}");
  size += endItem();
  return size;
}

uint32_t TDebugProtocol::writeListBegin(const TType elemType, const uint32_t size) {
  // TODO(dreiss): Optimize short arrays.
  uint32_t bsize = 0;
  bsize += startItem();
  bsize += writePlain(
      "list<" + fieldTypeName(elemType) + ">"
      "[" + to_string(size) + "] {\n");
  indentUp();
  write_state_.push_back(LIST);
  list_idx_.push_back(0);
  return bsize;
}

uint32_t TDebugProtocol::writeListEnd() {
  indentDown();
  write_state_.pop_back();
  list_idx_.pop_back();
  uint32_t size = 0;
  size += writeIndented("}");
  size += endItem();
  return size;
}

uint32_t TDebugProtocol::writeSetBegin(const TType elemType, const uint32_t size) {
  // TODO(dreiss): Optimize short sets.
  uint32_t bsize = 0;
  bsize += startItem();
  bsize += writePlain(
      "set<" + fieldTypeName(elemType) + ">"
      "[" + to_string(size) + "] {\n");
  indentUp();
  write_state_.push_back(SET);
  return bsize;
}

uint32_t TDebugProtocol::writeSetEnd() {
  indentDown();
  write_state_.pop_back();
  uint32_t size = 0;
  size += writeIndented("}");
  size += endItem();
  return size;
}

uint32_t TDebugProtocol::writeBool(const bool value) {
  return writeItem(value ? "true" : "false");
}

uint32_t TDebugProtocol::writeByte(const int8_t byte) {
  return writeItem("0x" + byte_to_hex(byte));
}

uint32_t TDebugProtocol::writeI16(const int16_t i16) {
  return writeItem(to_string(i16));
}

uint32_t TDebugProtocol::writeI32(const int32_t i32) {
  return writeItem(to_string(i32));
}

uint32_t TDebugProtocol::writeI64(const int64_t i64) {
  return writeItem(to_string(i64));
}

uint32_t TDebugProtocol::writeDouble(const double dub) {
  return writeItem(to_string(dub));
}

uint32_t TDebugProtocol::writeString(const string& str) {
  // XXX Raw/UTF-8?

  string to_show = str;
  if (to_show.length() > (string::size_type)string_limit_) {
    to_show = str.substr(0, string_prefix_size_);
    to_show += "[...](" + to_string(str.length()) + ")";
  }

  string output = "\"";

  for (string::const_iterator it = to_show.begin(); it != to_show.end(); ++it) {
    if (*it == '\\') {
      output += "\\\\";
    } else if (*it == '"') {
      output += "\\\"";
      // passing characters <0 to std::isprint causes asserts. isprint takes an
      // int, so we need to be careful of sign extension
    } else if (std::isprint((unsigned char)*it)) {
      output += *it;
    } else {
      switch (*it) {
      case '\a':
        output += "\\a";
        break;
      case '\b':
        output += "\\b";
        break;
      case '\f':
        output += "\\f";
        break;
      case '\n':
        output += "\\n";
        break;
      case '\r':
        output += "\\r";
        break;
      case '\t':
        output += "\\t";
        break;
      case '\v':
        output += "\\v";
        break;
      default:
        output += "\\x";
        output += byte_to_hex(*it);
      }
    }
  }

  output += '\"';
  return writeItem(output);
}

uint32_t TDebugProtocol::writeBinary(const string& str) {
  // XXX Hex?
  return TDebugProtocol::writeString(str);
}

uint32_t TDebugProtocol::writeUUID(const TUuid& uuid) {
  size_t size = writePlain("{\n");
  indentUp();
  size += writeIndented("[raw] = ");
  size += writeString(std::string(std::begin(uuid), std::end(uuid)));
  size += writeIndented("[enc] = \"" + to_string(uuid) + "\"\n");
  indentDown();
  size += writeIndented("}\n");
  return size;
}
}
}
} // apache::thrift::protocol
