#pragma once

#include <nall/http/message.hpp>

namespace nall { namespace HTTP {

struct Response : Message {
  using type = Response;

  Response() = default;
  Response(const Request& request) { setRequest(request); }

  explicit operator bool() const { return responseType() != 0; }
  auto operator()(uint responseType) -> type& { return setResponseType(responseType); }

  inline auto head(const function<bool (const uint8_t* data, uint size)>& callback) const -> bool override;
  inline auto setHead() -> bool override;

  inline auto body(const function<bool (const uint8_t* data, uint size)>& callback) const -> bool override;
  inline auto setBody() -> bool override;

  auto request() const -> const Request* { return _request; }
  auto setRequest(const Request& value) -> type& { _request = &value; return *this; }

  auto responseType() const -> uint { return _responseType; }
  auto setResponseType(uint value) -> type& { _responseType = value; return *this; }

  auto hasData() const -> bool { return (bool)_data; }
  auto data() const -> const vector<uint8_t>& { return _data; }
  inline auto setData(const vector<uint8_t>& value) -> type&;

  auto hasFile() const -> bool { return (bool)_file; }
  auto file() const -> const string& { return _file; }
  inline auto setFile(const string& value) -> type&;

  auto hasText() const -> bool { return (bool)_text; }
  auto text() const -> const string& { return _text; }
  inline auto setText(const string& value) -> type&;

  inline auto hasBody() const -> bool;
  inline auto findContentLength() const -> uint;
  inline auto findContentType() const -> string;
  inline auto findContentType(const string& suffix) const -> string;
  inline auto findResponseType() const -> string;
  inline auto setFileETag() -> void;

  const Request* _request = nullptr;
  uint _responseType = 0;
  vector<uint8_t> _data;
  string _file;
  string _text;
};

auto Response::head(const function<bool (const uint8_t*, uint)>& callback) const -> bool {
  if(!callback) return false;
  string output;

  if(auto request = this->request()) {
    if(auto eTag = header["ETag"]) {
      if(eTag.value() == request->header["If-None-Match"].value()) {
        output.append("HTTP/1.1 304 Not Modified\r\n");
        output.append("Connection: close\r\n");
        output.append("\r\n");
        return callback(output.data<uint8_t>(), output.size());
      }
    }
  }

  output.append("HTTP/1.1 ", findResponseType(), "\r\n");
  for(auto& variable : header) {
    output.append(variable.name(), ": ", variable.value(), "\r\n");
  }
  if(hasBody()) {
    if(!header["Content-Length"] && !header["Transfer-Encoding"].value().iequals("chunked")) {
      output.append("Content-Length: ", findContentLength(), "\r\n");
    }
    if(!header["Content-Type"]) {
      output.append("Content-Type: ", findContentType(), "\r\n");
    }
  }
  if(!header["Connection"]) {
    output.append("Connection: close\r\n");
  }
  output.append("\r\n");

  return callback(output.data<uint8_t>(), output.size());
}

auto Response::setHead() -> bool {
  auto headers = _head.split("\n");
  string response = headers.takeLeft().trimRight("\r");

       if(response.ibeginsWith("HTTP/1.0 ")) response.itrimLeft("HTTP/1.0 ", 1L);
  else if(response.ibeginsWith("HTTP/1.1 ")) response.itrimLeft("HTTP/1.1 ", 1L);
  else return false;

  setResponseType(response.natural());

  for(auto& header : headers) {
    if(header.beginsWith(" ") || header.beginsWith("\t")) continue;
    auto variable = header.split(":", 1L).strip();
    if(variable.size() != 2) continue;
    this->header.append(variable[0], variable[1]);
  }

  return true;
}

auto Response::body(const function<bool (const uint8_t*, uint)>& callback) const -> bool {
  if(!callback) return false;
  if(!hasBody()) return true;
  bool chunked = header["Transfer-Encoding"].value() == "chunked";

  if(chunked) {
    string prefix = {hex(findContentLength()), "\r\n"};
    if(!callback(prefix.data<uint8_t>(), prefix.size())) return false;
  }

  if(_body) {
    if(!callback(_body.data<uint8_t>(), _body.size())) return false;
  } else if(hasData()) {
    if(!callback(data().data(), data().size())) return false;
  } else if(hasFile()) {
    filemap map(file(), filemap::mode::read);
    if(!callback(map.data(), map.size())) return false;
  } else if(hasText()) {
    if(!callback(text().data<uint8_t>(), text().size())) return false;
  } else {
    string response = findResponseType();
    if(!callback(response.data<uint8_t>(), response.size())) return false;
  }

  if(chunked) {
    string suffix = {"\r\n0\r\n\r\n"};
    if(!callback(suffix.data<uint8_t>(), suffix.size())) return false;
  }

  return true;
}

auto Response::setBody() -> bool {
  return true;
}

auto Response::hasBody() const -> bool {
  if(auto request = this->request()) {
    if(request->requestType() == Request::RequestType::Head) return false;
  }
  if(responseType() == 301) return false;
  if(responseType() == 302) return false;
  if(responseType() == 303) return false;
  if(responseType() == 304) return false;
  if(responseType() == 307) return false;
  return true;
}

auto Response::findContentLength() const -> uint {
  if(auto contentLength = header["Content-Length"]) return contentLength.value().natural();
  if(_body) return _body.size();
  if(hasData()) return data().size();
  if(hasFile()) return file::size(file());
  if(hasText()) return text().size();
  return findResponseType().size();
}

auto Response::findContentType() const -> string {
  if(auto contentType = header["Content-Type"]) return contentType.value();
  if(hasData()) return "application/octet-stream";
  if(hasFile()) return findContentType(Location::suffix(file()));
  return "text/html; charset=utf-8";
}

auto Response::findContentType(const string& s) const -> string {
  if(s == ".7z"  ) return "application/x-7z-compressed";
  if(s == ".avi" ) return "video/avi";
  if(s == ".bml" ) return "text/plain; charset=utf-8";
  if(s == ".bz2" ) return "application/x-bzip2";
  if(s == ".css" ) return "text/css; charset=utf-8";
  if(s == ".gif" ) return "image/gif";
  if(s == ".gz"  ) return "application/gzip";
  if(s == ".htm" ) return "text/html; charset=utf-8";
  if(s == ".html") return "text/html; charset=utf-8";
  if(s == ".jpg" ) return "image/jpeg";
  if(s == ".jpeg") return "image/jpeg";
  if(s == ".js"  ) return "application/javascript";
  if(s == ".mka" ) return "audio/x-matroska";
  if(s == ".mkv" ) return "video/x-matroska";
  if(s == ".mp3" ) return "audio/mpeg";
  if(s == ".mp4" ) return "video/mp4";
  if(s == ".mpeg") return "video/mpeg";
  if(s == ".mpg" ) return "video/mpeg";
  if(s == ".ogg" ) return "audio/ogg";
  if(s == ".pdf" ) return "application/pdf";
  if(s == ".png" ) return "image/png";
  if(s == ".rar" ) return "application/x-rar-compressed";
  if(s == ".svg" ) return "image/svg+xml";
  if(s == ".tar" ) return "application/x-tar";
  if(s == ".txt" ) return "text/plain; charset=utf-8";
  if(s == ".wav" ) return "audio/vnd.wave";
  if(s == ".webm") return "video/webm";
  if(s == ".xml" ) return "text/xml; charset=utf-8";
  if(s == ".xz"  ) return "application/x-xz";
  if(s == ".zip" ) return "application/zip";
  return "application/octet-stream";  //binary
}

auto Response::findResponseType() const -> string {
  switch(responseType()) {
  case 200: return "200 OK";
  case 301: return "301 Moved Permanently";
  case 302: return "302 Found";
  case 303: return "303 See Other";
  case 304: return "304 Not Modified";
  case 307: return "307 Temporary Redirect";
  case 400: return "400 Bad Request";
  case 403: return "403 Forbidden";
  case 404: return "404 Not Found";
  case 500: return "500 Internal Server Error";
  case 501: return "501 Not Implemented";
  case 503: return "503 Service Unavailable";
  }
  return "501 Not Implemented";
}

auto Response::setData(const vector<uint8_t>& value) -> type& {
  _data = value;
  header.assign("Content-Length", value.size());
  return *this;
}

auto Response::setFile(const string& value) -> type& {
  //block path escalation exploits ("../" and "..\" in the file location)
  bool valid = true;
  for(uint n : range(value.size())) {
    if(value(n + 0, '\0') != '.') continue;
    if(value(n + 1, '\0') != '.') continue;
    if(value(n + 2, '\0') != '/' && value(n + 2, '\0') != '\\') continue;
    valid = false;
    break;
  }
  if(!valid) return *this;

  _file = value;
  string eTag = {"\"", chrono::utc::datetime(file::timestamp(value, file::time::modify)), "\""};
  header.assign("Content-Length", file::size(value));
  header.assign("Cache-Control", "public");
  header.assign("ETag", eTag);
  return *this;
}

auto Response::setText(const string& value) -> type& {
  _text = value;
  header.assign("Content-Length", value.size());
  return *this;
}

}}
