// Copyright (c) 2023 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef QUICHE_WEB_TRANSPORT_ENCAPSULATED_ENCAPSULATED_WEB_TRANSPORT_H_
#define QUICHE_WEB_TRANSPORT_ENCAPSULATED_ENCAPSULATED_WEB_TRANSPORT_H_

#include <cstddef>
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <vector>

#include "absl/base/attributes.h"
#include "absl/container/node_hash_map.h"
#include "absl/status/status.h"
#include "absl/strings/string_view.h"
#include "absl/time/time.h"
#include "absl/types/span.h"
#include "quiche/common/capsule.h"
#include "quiche/common/http/http_header_block.h"
#include "quiche/common/platform/api/quiche_export.h"
#include "quiche/common/quiche_buffer_allocator.h"
#include "quiche/common/quiche_callbacks.h"
#include "quiche/common/quiche_circular_deque.h"
#include "quiche/common/quiche_mem_slice.h"
#include "quiche/common/quiche_stream.h"
#include "quiche/common/simple_buffer_allocator.h"
#include "quiche/web_transport/web_transport.h"
#include "quiche/web_transport/web_transport_priority_scheduler.h"

namespace webtransport {

constexpr bool IsUnidirectionalId(StreamId id) { return id & 0b10; }
constexpr bool IsBidirectionalId(StreamId id) {
  return !IsUnidirectionalId(id);
}
constexpr bool IsIdOpenedBy(StreamId id, Perspective perspective) {
  return (id & 0b01) ^ (perspective == Perspective::kClient);
}

using FatalErrorCallback = quiche::SingleUseCallback<void(absl::string_view)>;

// Implementation of the WebTransport over HTTP/2 protocol; works over any
// arbitrary bidirectional bytestream that can be prefixed with HTTP headers.
// Specification: https://datatracker.ietf.org/doc/draft-ietf-webtrans-http2/
class QUICHE_EXPORT EncapsulatedSession
    : public webtransport::Session,
      public quiche::WriteStreamVisitor,
      public quiche::ReadStreamVisitor,
      public quiche::CapsuleParser::Visitor {
 public:
  // The state machine of the transport.
  enum State {
    // The transport object has been created, but
    // InitializeClient/InitializeServer has not been called yet.
    kUninitialized,
    // The client has sent its own headers, but haven't received a response yet.
    kWaitingForHeaders,
    // Both the client and the server headers have been processed.
    kSessionOpen,
    // The session close has been requested, but the CLOSE capsule hasn't been
    // sent yet.
    kSessionClosing,
    // The session has been closed; no further data will be exchanged.
    kSessionClosed,
  };

  // The `fatal_error_callback` implies that any state related to the session
  // should be torn down after it's been called.
  EncapsulatedSession(Perspective perspective,
                      FatalErrorCallback fatal_error_callback);

  // WebTransport uses HTTP headers in a similar way to how QUIC uses SETTINGS;
  // thus, the headers are necessary to initialize the session.
  void InitializeClient(std::unique_ptr<SessionVisitor> visitor,
                        quiche::HttpHeaderBlock& outgoing_headers,
                        quiche::WriteStream* writer,
                        quiche::ReadStream* reader);
  void InitializeServer(std::unique_ptr<SessionVisitor> visitor,
                        const quiche::HttpHeaderBlock& incoming_headers,
                        quiche::HttpHeaderBlock& outgoing_headers,
                        quiche::WriteStream* writer,
                        quiche::ReadStream* reader);
  void ProcessIncomingServerHeaders(const quiche::HttpHeaderBlock& headers);

  // webtransport::Session implementation.
  void CloseSession(SessionErrorCode error_code,
                    absl::string_view error_message) override;
  Stream* AcceptIncomingBidirectionalStream() override;
  Stream* AcceptIncomingUnidirectionalStream() override;
  bool CanOpenNextOutgoingBidirectionalStream() override;
  bool CanOpenNextOutgoingUnidirectionalStream() override;
  Stream* OpenOutgoingBidirectionalStream() override;
  Stream* OpenOutgoingUnidirectionalStream() override;
  DatagramStatus SendOrQueueDatagram(absl::string_view datagram) override;
  uint64_t GetMaxDatagramSize() const override;
  void SetDatagramMaxTimeInQueue(absl::Duration max_time_in_queue) override;
  Stream* GetStreamById(StreamId id) override;
  DatagramStats GetDatagramStats() override;
  SessionStats GetSessionStats() override;
  void NotifySessionDraining() override;
  void SetOnDraining(quiche::SingleUseCallback<void()> callback) override;
  std::optional<std::string> GetNegotiatedSubprotocol() const override;

  // quiche::WriteStreamVisitor implementation.
  void OnCanWrite() override;
  // quiche::ReadStreamVisitor implementation.
  void OnCanRead() override;
  // quiche::CapsuleParser::Visitor implementation.
  bool OnCapsule(const quiche::Capsule& capsule) override;
  void OnCapsuleParseFailure(absl::string_view error_message) override;

  State state() const { return state_; }

  // Cleans up the state for all of the streams that have been closed.  QUIC
  // uses timers to safely delete closed streams while minimizing the risk that
  // something on stack holds an active pointer to them; WebTransport over
  // HTTP/2 does not have any timers in it, making that approach inapplicable
  // here. This class does automatically run garbage collection at the end of
  // every OnCanRead() call (since it's a top-level entrypoint that is likely to
  // come directly from I/O handler), but if the application does not happen to
  // read data frequently, manual calls to this function may be requried.
  void GarbageCollectStreams();

 private:
  // If the amount of data buffered in the socket exceeds the amount specified
  // here, CanWrite() will start returning false.
  static constexpr size_t kDefaultMaxBufferedStreamData = 16 * 1024;

  class InnerStream : public Stream {
   public:
    InnerStream(EncapsulatedSession* session, StreamId id);
    InnerStream(const InnerStream&) = delete;
    InnerStream(InnerStream&&) = delete;
    InnerStream& operator=(const InnerStream&) = delete;
    InnerStream& operator=(InnerStream&&) = delete;

    // ReadStream implementation.
    ABSL_MUST_USE_RESULT ReadResult Read(absl::Span<char> output) override;
    ABSL_MUST_USE_RESULT ReadResult Read(std::string* output) override;
    size_t ReadableBytes() const override;
    PeekResult PeekNextReadableRegion() const override;
    bool SkipBytes(size_t bytes) override;

    // WriteStream implementation.
    absl::Status Writev(absl::Span<quiche::QuicheMemSlice> data,
                        const quiche::StreamWriteOptions& options) override;
    bool CanWrite() const override;

    // TerminableStream implementation.
    void AbruptlyTerminate(absl::Status error) override;

    // Stream implementation.
    StreamId GetStreamId() const override { return id_; }
    StreamVisitor* visitor() override { return visitor_.get(); }
    void SetVisitor(std::unique_ptr<StreamVisitor> visitor) override {
      visitor_ = std::move(visitor);
    }

    void ResetWithUserCode(StreamErrorCode error) override;
    void SendStopSending(StreamErrorCode error) override;

    void ResetDueToInternalError() override { ResetWithUserCode(0); }
    void MaybeResetDueToStreamObjectGone() override { ResetWithUserCode(0); }

    void SetPriority(const StreamPriority& priority) override;

    void CloseReadSide(std::optional<StreamErrorCode> error);
    void CloseWriteSide(std::optional<StreamErrorCode> error);
    bool CanBeGarbageCollected() const {
      return read_side_closed_ && write_side_closed_;
    }

    bool HasPendingWrite() const { return !pending_write_.empty(); }
    void FlushPendingWrite();

    void ProcessCapsule(const quiche::Capsule& capsule);

   private:
    // Struct for storing data that can potentially either stored inside the
    // object or inside some other object on the stack. Here is roughly how this
    // works:
    //   1. A read is enqueued with `data` pointing to a temporary buffer, and
    //      `storage` being empty.
    //   2. Visitor::OnCanRead() is called, potentially causing the user to
    //      consume the data from the temporary buffer directly.
    //   3. If user does not consume data immediately, it's copied to `storage`
    //      (and the pointer to `data` is updated) so that it can be read later.
    struct IncomingRead {
      absl::string_view data;
      std::string storage;

      size_t size() const { return data.size(); }
    };

    // Tries to send `data`; may send less if limited by flow control.
    [[nodiscard]] size_t WriteInner(absl::Span<const absl::string_view> data,
                                    bool fin);

    EncapsulatedSession* session_;
    StreamId id_;
    std::unique_ptr<StreamVisitor> visitor_;
    quiche::QuicheCircularDeque<IncomingRead> incoming_reads_;
    std::string pending_write_;
    bool read_side_closed_;
    bool write_side_closed_;
    bool reset_frame_sent_ = false;
    bool stop_sending_sent_ = false;
    bool fin_received_ = false;
    bool fin_consumed_ = false;
    bool fin_buffered_ = false;
  };

  struct BufferedClose {
    SessionErrorCode error_code = 0;
    std::string error_message;
  };

  Perspective perspective_;
  State state_ = kUninitialized;
  std::unique_ptr<SessionVisitor> visitor_ = nullptr;
  FatalErrorCallback fatal_error_callback_;
  quiche::SingleUseCallback<void()> draining_callback_;
  quiche::WriteStream* writer_ = nullptr;  // Not owned.
  quiche::ReadStream* reader_ = nullptr;   // Not owned.
  quiche::QuicheBufferAllocator* allocator_ =
      quiche::SimpleBufferAllocator::Get();
  quiche::CapsuleParser capsule_parser_;

  size_t max_stream_data_buffered_ = kDefaultMaxBufferedStreamData;

  PriorityScheduler scheduler_;
  absl::node_hash_map<StreamId, InnerStream>
      streams_;  // Streams unregister themselves with scheduler on deletion,
                 // and thus have to be above it.
  quiche::QuicheCircularDeque<StreamId> incoming_bidirectional_streams_;
  quiche::QuicheCircularDeque<StreamId> incoming_unidirectional_streams_;
  std::vector<StreamId> streams_to_garbage_collect_;
  StreamId next_outgoing_bidi_stream_;
  StreamId next_outgoing_unidi_stream_;

  bool session_close_notified_ = false;
  bool fin_sent_ = false;

  BufferedClose buffered_session_close_;
  quiche::QuicheCircularDeque<quiche::QuicheBuffer> control_capsule_queue_;

  void OpenSession();
  absl::Status SendFin(absl::string_view data);
  void OnSessionClosed(SessionErrorCode error_code,
                       const std::string& error_message);
  void OnFatalError(absl::string_view error_message);
  void OnWriteError(absl::Status error);

  bool IsOutgoing(StreamId id) { return IsIdOpenedBy(id, perspective_); }
  bool IsIncoming(StreamId id) { return !IsOutgoing(id); }

  template <typename CapsuleType>
  void SendControlCapsule(CapsuleType capsule) {
    control_capsule_queue_.push_back(quiche::SerializeCapsule(
        quiche::Capsule(std::move(capsule)), allocator_));
    OnCanWrite();
  }

  Stream* AcceptIncomingStream(quiche::QuicheCircularDeque<StreamId>& queue);
  Stream* OpenOutgoingStream(StreamId& counter);
  void ProcessStreamCapsule(const quiche::Capsule& capsule, StreamId stream_id);
};

}  // namespace webtransport

#endif  // QUICHE_WEB_TRANSPORT_ENCAPSULATED_ENCAPSULATED_WEB_TRANSPORT_H_
