#include "quiche/http2/adapter/nghttp2_callbacks.h"

#include <cstdint>
#include <cstring>
#include <utility>

#include "absl/strings/string_view.h"
#include "quiche/http2/adapter/data_source.h"
#include "quiche/http2/adapter/http2_protocol.h"
#include "quiche/http2/adapter/http2_visitor_interface.h"
#include "quiche/http2/adapter/nghttp2_data_provider.h"
#include "quiche/http2/adapter/nghttp2_util.h"
#include "quiche/common/platform/api/quiche_bug_tracker.h"
#include "quiche/common/platform/api/quiche_logging.h"
#include "quiche/common/quiche_endian.h"

namespace http2 {
namespace adapter {
namespace callbacks {

using OnHeaderResult = ::http2::adapter::Http2VisitorInterface::OnHeaderResult;

ssize_t OnReadyToSend(nghttp2_session* /* session */, const uint8_t* data,
                      size_t length, int flags, void* user_data) {
  QUICHE_CHECK_NE(user_data, nullptr);
  auto* visitor = static_cast<Http2VisitorInterface*>(user_data);
  const int64_t result = visitor->OnReadyToSend(ToStringView(data, length));
  QUICHE_VLOG(1) << "callbacks::OnReadyToSend(length=" << length
                 << ", flags=" << flags << ") returning " << result;
  if (result > 0) {
    return result;
  } else if (result == Http2VisitorInterface::kSendBlocked) {
    return -504;  // NGHTTP2_ERR_WOULDBLOCK
  } else {
    return -902;  // NGHTTP2_ERR_CALLBACK_FAILURE
  }
}

int OnBeginFrame(nghttp2_session* /* session */, const nghttp2_frame_hd* header,
                 void* user_data) {
  QUICHE_VLOG(1) << "callbacks::OnBeginFrame(stream_id=" << header->stream_id
                 << ", type=" << int(header->type)
                 << ", length=" << header->length
                 << ", flags=" << int(header->flags) << ")";
  QUICHE_CHECK_NE(user_data, nullptr);
  auto* visitor = static_cast<Http2VisitorInterface*>(user_data);
  bool result = visitor->OnFrameHeader(header->stream_id, header->length,
                                       header->type, header->flags);
  if (!result) {
    return NGHTTP2_ERR_CALLBACK_FAILURE;
  }
  if (header->type == NGHTTP2_DATA) {
    result = visitor->OnBeginDataForStream(header->stream_id, header->length);
  } else if (header->type == kMetadataFrameType) {
    visitor->OnBeginMetadataForStream(header->stream_id, header->length);
  }
  return result ? 0 : NGHTTP2_ERR_CALLBACK_FAILURE;
}

int OnFrameReceived(nghttp2_session* /* session */, const nghttp2_frame* frame,
                    void* user_data) {
  QUICHE_VLOG(1) << "callbacks::OnFrameReceived(stream_id="
                 << frame->hd.stream_id << ", type=" << int(frame->hd.type)
                 << ", length=" << frame->hd.length
                 << ", flags=" << int(frame->hd.flags) << ")";
  QUICHE_CHECK_NE(user_data, nullptr);
  auto* visitor = static_cast<Http2VisitorInterface*>(user_data);
  const Http2StreamId stream_id = frame->hd.stream_id;
  switch (frame->hd.type) {
    // The beginning of the DATA frame is handled in OnBeginFrame(), and the
    // beginning of the header block is handled in client/server-specific
    // callbacks. This callback handles the point at which the entire logical
    // frame has been received and processed.
    case NGHTTP2_DATA:
      if ((frame->hd.flags & NGHTTP2_FLAG_PADDED) != 0) {
        visitor->OnDataPaddingLength(stream_id, frame->data.padlen);
      }
      if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) {
        const bool result = visitor->OnEndStream(stream_id);
        if (!result) {
          return NGHTTP2_ERR_CALLBACK_FAILURE;
        }
      }
      break;
    case NGHTTP2_HEADERS: {
      if (frame->hd.flags & NGHTTP2_FLAG_END_HEADERS) {
        const bool result = visitor->OnEndHeadersForStream(stream_id);
        if (!result) {
          return NGHTTP2_ERR_CALLBACK_FAILURE;
        }
      }
      if (frame->hd.flags & NGHTTP2_FLAG_END_STREAM) {
        const bool result = visitor->OnEndStream(stream_id);
        if (!result) {
          return NGHTTP2_ERR_CALLBACK_FAILURE;
        }
      }
      break;
    }
    case NGHTTP2_PRIORITY: {
      nghttp2_priority_spec priority_spec = frame->priority.pri_spec;
      visitor->OnPriorityForStream(stream_id, priority_spec.stream_id,
                                   priority_spec.weight,
                                   priority_spec.exclusive != 0);
      break;
    }
    case NGHTTP2_RST_STREAM: {
      visitor->OnRstStream(stream_id,
                           ToHttp2ErrorCode(frame->rst_stream.error_code));
      break;
    }
    case NGHTTP2_SETTINGS:
      if (frame->hd.flags & NGHTTP2_FLAG_ACK) {
        visitor->OnSettingsAck();
      } else {
        visitor->OnSettingsStart();
        for (size_t i = 0; i < frame->settings.niv; ++i) {
          nghttp2_settings_entry entry = frame->settings.iv[i];
          // The nghttp2_settings_entry uses int32_t for the ID; we must cast.
          visitor->OnSetting(Http2Setting{
              static_cast<Http2SettingsId>(entry.settings_id), entry.value});
        }
        visitor->OnSettingsEnd();
      }
      break;
    case NGHTTP2_PUSH_PROMISE:
      // This case is handled by headers-related callbacks:
      //   1. visitor->OnPushPromiseForStream() is invoked in the client-side
      //      OnHeadersStart() adapter callback, as nghttp2 only allows clients
      //      to receive PUSH_PROMISE frames.
      //   2. visitor->OnHeaderForStream() is invoked for each server push
      //      request header in the PUSH_PROMISE header block.
      //   3. This switch statement is reached once all server push request
      //      headers have been parsed.
      break;
    case NGHTTP2_PING: {
      Http2PingId ping_id;
      std::memcpy(&ping_id, frame->ping.opaque_data, sizeof(Http2PingId));
      visitor->OnPing(quiche::QuicheEndian::NetToHost64(ping_id),
                      (frame->hd.flags & NGHTTP2_FLAG_ACK) != 0);
      break;
    }
    case NGHTTP2_GOAWAY: {
      absl::string_view opaque_data(
          reinterpret_cast<const char*>(frame->goaway.opaque_data),
          frame->goaway.opaque_data_len);
      const bool result = visitor->OnGoAway(
          frame->goaway.last_stream_id,
          ToHttp2ErrorCode(frame->goaway.error_code), opaque_data);
      if (!result) {
        return NGHTTP2_ERR_CALLBACK_FAILURE;
      }
      break;
    }
    case NGHTTP2_WINDOW_UPDATE: {
      visitor->OnWindowUpdate(stream_id,
                              frame->window_update.window_size_increment);
      break;
    }
    case NGHTTP2_CONTINUATION:
      // This frame type should not be passed to any callbacks, according to
      // https://nghttp2.org/documentation/enums.html#c.NGHTTP2_CONTINUATION.
      QUICHE_LOG(ERROR) << "Unexpected receipt of NGHTTP2_CONTINUATION type!";
      break;
    case NGHTTP2_ALTSVC:
      break;
    case NGHTTP2_ORIGIN:
      break;
  }

  return 0;
}

int OnBeginHeaders(nghttp2_session* /* session */, const nghttp2_frame* frame,
                   void* user_data) {
  QUICHE_VLOG(1) << "callbacks::OnBeginHeaders(stream_id="
                 << frame->hd.stream_id << ")";
  QUICHE_CHECK_NE(user_data, nullptr);
  auto* visitor = static_cast<Http2VisitorInterface*>(user_data);
  const bool result = visitor->OnBeginHeadersForStream(frame->hd.stream_id);
  return result ? 0 : NGHTTP2_ERR_CALLBACK_FAILURE;
}

int OnHeader(nghttp2_session* /* session */, const nghttp2_frame* frame,
             nghttp2_rcbuf* name, nghttp2_rcbuf* value, uint8_t /*flags*/,
             void* user_data) {
  QUICHE_VLOG(2) << "callbacks::OnHeader(stream_id=" << frame->hd.stream_id
                 << ", name=[" << absl::CEscape(ToStringView(name))
                 << "], value=[" << absl::CEscape(ToStringView(value)) << "])";
  QUICHE_CHECK_NE(user_data, nullptr);
  auto* visitor = static_cast<Http2VisitorInterface*>(user_data);
  const OnHeaderResult result = visitor->OnHeaderForStream(
      frame->hd.stream_id, ToStringView(name), ToStringView(value));
  switch (result) {
    case OnHeaderResult::HEADER_OK:
      return 0;
    case OnHeaderResult::HEADER_CONNECTION_ERROR:
    case OnHeaderResult::HEADER_COMPRESSION_ERROR:
      return NGHTTP2_ERR_CALLBACK_FAILURE;
    case OnHeaderResult::HEADER_RST_STREAM:
    case OnHeaderResult::HEADER_FIELD_INVALID:
    case OnHeaderResult::HEADER_HTTP_MESSAGING:
      return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE;
  }
  // Unexpected value.
  return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE;
}

int OnBeforeFrameSent(nghttp2_session* /* session */,
                      const nghttp2_frame* frame, void* user_data) {
  QUICHE_VLOG(1) << "callbacks::OnBeforeFrameSent(stream_id="
                 << frame->hd.stream_id << ", type=" << int(frame->hd.type)
                 << ", length=" << frame->hd.length
                 << ", flags=" << int(frame->hd.flags) << ")";
  QUICHE_CHECK_NE(user_data, nullptr);
  LogBeforeSend(*frame);
  auto* visitor = static_cast<Http2VisitorInterface*>(user_data);
  return visitor->OnBeforeFrameSent(frame->hd.type, frame->hd.stream_id,
                                    frame->hd.length, frame->hd.flags);
}

int OnFrameSent(nghttp2_session* /* session */, const nghttp2_frame* frame,
                void* user_data) {
  QUICHE_VLOG(1) << "callbacks::OnFrameSent(stream_id=" << frame->hd.stream_id
                 << ", type=" << int(frame->hd.type)
                 << ", length=" << frame->hd.length
                 << ", flags=" << int(frame->hd.flags) << ")";
  QUICHE_CHECK_NE(user_data, nullptr);
  auto* visitor = static_cast<Http2VisitorInterface*>(user_data);
  uint32_t error_code = 0;
  if (frame->hd.type == NGHTTP2_RST_STREAM) {
    error_code = frame->rst_stream.error_code;
  } else if (frame->hd.type == NGHTTP2_GOAWAY) {
    error_code = frame->goaway.error_code;
  }
  return visitor->OnFrameSent(frame->hd.type, frame->hd.stream_id,
                              frame->hd.length, frame->hd.flags, error_code);
}

int OnFrameNotSent(nghttp2_session* /* session */, const nghttp2_frame* frame,
                   int /* lib_error_code */, void* /* user_data */) {
  QUICHE_VLOG(1) << "callbacks::OnFrameNotSent(stream_id="
                 << frame->hd.stream_id << ", type=" << int(frame->hd.type)
                 << ", length=" << frame->hd.length
                 << ", flags=" << int(frame->hd.flags) << ")";
  if (frame->hd.type == kMetadataFrameType) {
    auto* source = static_cast<MetadataSource*>(frame->ext.payload);
    if (source == nullptr) {
      QUICHE_BUG(not_sent_payload_is_nullptr)
          << "Extension frame payload for stream " << frame->hd.stream_id
          << " is null!";
    } else {
      source->OnFailure();
    }
  }
  return 0;
}

int OnInvalidFrameReceived(nghttp2_session* /* session */,
                           const nghttp2_frame* frame, int lib_error_code,
                           void* user_data) {
  QUICHE_VLOG(1) << "callbacks::OnInvalidFrameReceived(stream_id="
                 << frame->hd.stream_id << ", InvalidFrameError="
                 << int(ToInvalidFrameError(lib_error_code)) << ")";
  QUICHE_CHECK_NE(user_data, nullptr);
  auto* visitor = static_cast<Http2VisitorInterface*>(user_data);
  const bool result = visitor->OnInvalidFrame(
      frame->hd.stream_id, ToInvalidFrameError(lib_error_code));
  return result ? 0 : NGHTTP2_ERR_CALLBACK_FAILURE;
}

int OnDataChunk(nghttp2_session* /* session */, uint8_t /*flags*/,
                Http2StreamId stream_id, const uint8_t* data, size_t len,
                void* user_data) {
  QUICHE_VLOG(1) << "callbacks::OnDataChunk(stream_id=" << stream_id
                 << ", length=" << len << ")";
  QUICHE_CHECK_NE(user_data, nullptr);
  auto* visitor = static_cast<Http2VisitorInterface*>(user_data);
  const bool result = visitor->OnDataForStream(
      stream_id, absl::string_view(reinterpret_cast<const char*>(data), len));
  return result ? 0 : NGHTTP2_ERR_CALLBACK_FAILURE;
}

int OnStreamClosed(nghttp2_session* /* session */, Http2StreamId stream_id,
                   uint32_t error_code, void* user_data) {
  QUICHE_VLOG(1) << "callbacks::OnStreamClosed(stream_id=" << stream_id
                 << ", error_code=" << error_code << ")";
  QUICHE_CHECK_NE(user_data, nullptr);
  auto* visitor = static_cast<Http2VisitorInterface*>(user_data);
  const bool result =
      visitor->OnCloseStream(stream_id, ToHttp2ErrorCode(error_code));
  return result ? 0 : NGHTTP2_ERR_CALLBACK_FAILURE;
}

int OnExtensionChunkReceived(nghttp2_session* /*session*/,
                             const nghttp2_frame_hd* hd, const uint8_t* data,
                             size_t len, void* user_data) {
  QUICHE_CHECK_NE(user_data, nullptr);
  auto* visitor = static_cast<Http2VisitorInterface*>(user_data);
  if (hd->type != kMetadataFrameType) {
    QUICHE_LOG(ERROR) << "Unexpected frame type: "
                      << static_cast<int>(hd->type);
    return NGHTTP2_ERR_CANCEL;
  }
  const bool result =
      visitor->OnMetadataForStream(hd->stream_id, ToStringView(data, len));
  return result ? 0 : NGHTTP2_ERR_CALLBACK_FAILURE;
}

int OnUnpackExtensionCallback(nghttp2_session* /*session*/, void** /*payload*/,
                              const nghttp2_frame_hd* hd, void* user_data) {
  QUICHE_CHECK_NE(user_data, nullptr);
  auto* visitor = static_cast<Http2VisitorInterface*>(user_data);
  if (hd->flags == kMetadataEndFlag) {
    const bool result = visitor->OnMetadataEndForStream(hd->stream_id);
    if (!result) {
      return NGHTTP2_ERR_CALLBACK_FAILURE;
    }
  }
  return 0;
}

ssize_t OnPackExtensionCallback(nghttp2_session* /*session*/, uint8_t* buf,
                                size_t len, const nghttp2_frame* frame,
                                void* user_data) {
  QUICHE_CHECK_NE(user_data, nullptr);
  auto* source = static_cast<MetadataSource*>(frame->ext.payload);
  if (source == nullptr) {
    QUICHE_BUG(payload_is_nullptr) << "Extension frame payload for stream "
                                   << frame->hd.stream_id << " is null!";
    return NGHTTP2_ERR_CALLBACK_FAILURE;
  }
  const std::pair<int64_t, bool> result = source->Pack(buf, len);
  if (result.first < 0) {
    return NGHTTP2_ERR_CALLBACK_FAILURE;
  }
  const bool end_metadata_flag = (frame->hd.flags & kMetadataEndFlag);
  QUICHE_LOG_IF(DFATAL, result.second != end_metadata_flag)
      << "Metadata ends: " << result.second
      << " has kMetadataEndFlag: " << end_metadata_flag;
  return result.first;
}

int OnError(nghttp2_session* /*session*/, int /*lib_error_code*/,
            const char* msg, size_t len, void* user_data) {
  QUICHE_VLOG(1) << "callbacks::OnError(" << absl::string_view(msg, len) << ")";
  QUICHE_CHECK_NE(user_data, nullptr);
  auto* visitor = static_cast<Http2VisitorInterface*>(user_data);
  visitor->OnErrorDebug(absl::string_view(msg, len));
  return 0;
}

nghttp2_session_callbacks_unique_ptr Create(
    nghttp2_send_data_callback send_data_callback) {
  nghttp2_session_callbacks* callbacks;
  nghttp2_session_callbacks_new(&callbacks);

  nghttp2_session_callbacks_set_send_callback(callbacks, &OnReadyToSend);
  nghttp2_session_callbacks_set_on_begin_frame_callback(callbacks,
                                                        &OnBeginFrame);
  nghttp2_session_callbacks_set_on_frame_recv_callback(callbacks,
                                                       &OnFrameReceived);
  nghttp2_session_callbacks_set_on_begin_headers_callback(callbacks,
                                                          &OnBeginHeaders);
  nghttp2_session_callbacks_set_on_header_callback2(callbacks, &OnHeader);
  nghttp2_session_callbacks_set_on_data_chunk_recv_callback(callbacks,
                                                            &OnDataChunk);
  nghttp2_session_callbacks_set_on_stream_close_callback(callbacks,
                                                         &OnStreamClosed);
  nghttp2_session_callbacks_set_before_frame_send_callback(callbacks,
                                                           &OnBeforeFrameSent);
  nghttp2_session_callbacks_set_on_frame_send_callback(callbacks, &OnFrameSent);
  nghttp2_session_callbacks_set_on_frame_not_send_callback(callbacks,
                                                           &OnFrameNotSent);
  nghttp2_session_callbacks_set_on_invalid_frame_recv_callback(
      callbacks, &OnInvalidFrameReceived);
  nghttp2_session_callbacks_set_error_callback2(callbacks, &OnError);
  nghttp2_session_callbacks_set_send_data_callback(callbacks,
                                                   send_data_callback);
  nghttp2_session_callbacks_set_pack_extension_callback(
      callbacks, &OnPackExtensionCallback);
  nghttp2_session_callbacks_set_unpack_extension_callback(
      callbacks, &OnUnpackExtensionCallback);
  nghttp2_session_callbacks_set_on_extension_chunk_recv_callback(
      callbacks, &OnExtensionChunkReceived);
  return MakeCallbacksPtr(callbacks);
}

}  // namespace callbacks
}  // namespace adapter
}  // namespace http2
