#include "PSOProtocol.hh"

#include <event2/buffer.h>

#include <stdexcept>
#include <phosg/Strings.hh>

#include "Text.hh"

using namespace std;



extern bool use_terminal_colors;



PSOCommandHeader::PSOCommandHeader() {
  this->bb.size = 0;
  this->bb.command = 0;
  this->bb.flag = 0;
}

uint16_t PSOCommandHeader::command(GameVersion version) const {
  switch (version) {
    case GameVersion::DC:
      return this->dc.command;
    case GameVersion::GC:
      return this->gc.command;
    case GameVersion::PC:
    case GameVersion::PATCH:
      return this->pc.command;
    case GameVersion::BB:
      return this->bb.command;
    default:
      throw logic_error("unknown game version");
  }
}

void PSOCommandHeader::set_command(GameVersion version, uint16_t command) {
  switch (version) {
    case GameVersion::DC:
      this->dc.command = command;
      break;
    case GameVersion::GC:
      this->gc.command = command;
      break;
    case GameVersion::PC:
    case GameVersion::PATCH:
      this->pc.command = command;
      break;
    case GameVersion::BB:
      this->bb.command = command;
      break;
    default:
      throw logic_error("unknown game version");
  }
}

uint16_t PSOCommandHeader::size(GameVersion version) const {
  switch (version) {
    case GameVersion::DC:
      return this->dc.size;
    case GameVersion::GC:
      return this->gc.size;
    case GameVersion::PC:
    case GameVersion::PATCH:
      return this->pc.size;
    case GameVersion::BB:
      return this->bb.size;
    default:
      throw logic_error("unknown game version");
  }
}

void PSOCommandHeader::set_size(GameVersion version, uint32_t size) {
  switch (version) {
    case GameVersion::DC:
      this->dc.size = size;
      break;
    case GameVersion::GC:
      this->gc.size = size;
      break;
    case GameVersion::PC:
    case GameVersion::PATCH:
      this->pc.size = size;
      break;
    case GameVersion::BB:
      this->bb.size = size;
      break;
    default:
      throw logic_error("unknown game version");
  }
}

uint32_t PSOCommandHeader::flag(GameVersion version) const {
  switch (version) {
    case GameVersion::DC:
      return this->dc.flag;
    case GameVersion::GC:
      return this->gc.flag;
    case GameVersion::PC:
    case GameVersion::PATCH:
      return this->pc.flag;
    case GameVersion::BB:
      return this->bb.flag;
    default:
      throw logic_error("unknown game version");
  }
}

void PSOCommandHeader::set_flag(GameVersion version, uint32_t flag) {
  switch (version) {
    case GameVersion::DC:
      this->dc.flag = flag;
      break;
    case GameVersion::GC:
      this->gc.flag = flag;
      break;
    case GameVersion::PC:
    case GameVersion::PATCH:
      this->pc.flag = flag;
      break;
    case GameVersion::BB:
      this->bb.flag = flag;
      break;
    default:
      throw logic_error("unknown game version");
  }
}



void for_each_received_command(
    struct bufferevent* bev,
    GameVersion version,
    PSOEncryption* crypt,
    function<void(uint16_t, uint16_t, string&)> fn) {
  struct evbuffer* buf = bufferevent_get_input(bev);

  size_t header_size = version == GameVersion::BB ? 8 : 4;
  for (;;) {
    PSOCommandHeader header;
    if (evbuffer_copyout(buf, &header, header_size)
        < static_cast<ssize_t>(header_size)) {
      break;
    }

    if (crypt) {
      crypt->decrypt(&header, header_size, false);
    }

    size_t command_logical_size = header.size(version);

    // BB pads commands to 8-byte boundaries, and this is not reflected in the
    // size field
    size_t command_physical_size = (version == GameVersion::BB)
        ? (command_logical_size + header_size - 1) & ~(header_size - 1)
        : command_logical_size;
    if (evbuffer_get_length(buf) < command_physical_size) {
      break;
    }

    // If we get here, then there is a full command in the buffer

    evbuffer_drain(buf, header_size);

    string command_data(command_logical_size - header_size, '\0');
    if (evbuffer_remove(buf, command_data.data(), command_data.size())
        < static_cast<ssize_t>(command_data.size())) {
      throw logic_error("enough bytes available, but could not remove them");
    }
    if (command_logical_size != command_physical_size) {
      evbuffer_drain(buf, command_physical_size - command_logical_size);
    }

    if (crypt) {
      crypt->skip(header_size);
      crypt->decrypt(command_data.data(), command_data.size());
    }

    fn(header.command(version), header.flag(version), command_data);
  }
}

void print_received_command(
    uint16_t command,
    uint32_t flag,
    const void* data,
    size_t size,
    GameVersion version,
    const char* name,
    TerminalFormat color) {
  if (use_terminal_colors) {
    print_color_escape(stderr, color, TerminalFormat::BOLD, TerminalFormat::END);
  }

  string name_token;
  if (name && name[0]) {
    name_token = string(" from ") + name;
  }
  log(INFO, "Received%s (version=%d command=%04hX flag=%08X)",
      name_token.c_str(), static_cast<int>(version), command, flag);

  PSOCommandHeader header;
  size_t header_size = header.header_size(version);
  header.set_command(version, command);
  header.set_flag(version, flag);
  header.set_size(version, size + header_size);

  // TODO: This is unnecessarily slow. It'd be nice to have a print_data_v() so
  // we don't have to copy data around here.
  StringWriter w;
  w.write(&header, header_size);
  w.write(data, size);

  print_data(stderr, w.str());

  if (use_terminal_colors) {
    print_color_escape(stderr, TerminalFormat::NORMAL, TerminalFormat::END);
  }
}

void check_size_v(size_t size, size_t min_size, size_t max_size) {
  if (size < min_size) {
    throw std::runtime_error(string_printf(
        "command too small (expected at least 0x%zX bytes, received 0x%zX bytes)",
        min_size, size));
  }
  if (max_size < min_size) {
    max_size = min_size;
  }
  if (size > max_size) {
    throw std::runtime_error(string_printf(
        "command too large (expected at most 0x%zX bytes, received 0x%zX bytes)",
        max_size, size));
  }
}
