/** @file

  Http2Stream.cc

  @section license License

  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 "proxy/http2/Http2Stream.h"

#include "proxy/http2/HTTP2.h"
#include "proxy/http2/Http2ClientSession.h"
#include "proxy/http2/Http2ServerSession.h"
#include "proxy/http/HttpDebugNames.h"
#include "proxy/http/HttpSM.h"
#include "tscore/Diags.h"
#include "tscore/HTTPVersion.h"
#include "tscore/ink_assert.h"
#include "tsutil/DbgCtl.h"

#include <numeric>

#define REMEMBER(e, r)                                    \
  {                                                       \
    this->_history.push_back(MakeSourceLocation(), e, r); \
  }

namespace
{
DbgCtl dbg_ctl_http2_stream{"http2_stream"};

} // end anonymous namespace

#define Http2StreamDebug(fmt, ...) \
  SsnDbg(_proxy_ssn, dbg_ctl_http2_stream, "[%" PRId64 "] [%u] " fmt, _proxy_ssn->connection_id(), this->get_id(), ##__VA_ARGS__);

ClassAllocator<Http2Stream> http2StreamAllocator("http2StreamAllocator");

Http2Stream::Http2Stream(ProxySession *session, Http2StreamId sid, ssize_t initial_peer_rwnd, ssize_t initial_local_rwnd,
                         bool registered_stream)
  : super(session), _id(sid), _registered_stream(registered_stream), _peer_rwnd(initial_peer_rwnd), _local_rwnd(initial_local_rwnd)
{
  SET_HANDLER(&Http2Stream::main_event_handler);

  this->mark_milestone(Http2StreamMilestone::OPEN);

  this->_sm     = nullptr;
  this->_thread = this_ethread();
  this->_state  = Http2StreamState::HTTP2_STREAM_STATE_IDLE;

  auto const *proxy_session = get_proxy_ssn();
  ink_assert(proxy_session != nullptr);
  auto const *h2_session = dynamic_cast<Http2CommonSession const *>(proxy_session);
  ink_assert(h2_session != nullptr);
  this->_is_outbound = h2_session->is_outbound();

  this->_reader = this->_receive_buffer.alloc_reader();

  if (this->is_outbound_connection()) { // Flip the sense of the expected headers.  Fix naming later
    _receive_header.create(HTTP_TYPE_RESPONSE);
    _send_header.create(HTTP_TYPE_REQUEST, HTTP_2_0);
  } else {
    this->upstream_outbound_options = *(session->accept_options);
    _receive_header.create(HTTP_TYPE_REQUEST);
    _send_header.create(HTTP_TYPE_RESPONSE, HTTP_2_0);
  }

  http_parser_init(&http_parser);
}

Http2Stream::~Http2Stream()
{
  REMEMBER(NO_EVENT, this->reentrancy_count);
  Http2StreamDebug("Destroy stream, sent %" PRIu64 " bytes, registered: %s", this->bytes_sent,
                   (_registered_stream ? "true" : "false"));

  // In the case of a temporary stream used to parse the header to keep the HPACK
  // up to date, there may not be a mutex.  Nothing was set up, so nothing to
  // clean up in the destructor
  if (this->mutex == nullptr) {
    return;
  }

  SCOPED_MUTEX_LOCK(lock, this->mutex, this_ethread());
  // Clean up after yourself if this was an EOS
  ink_release_assert(this->closed);
  ink_release_assert(reentrancy_count == 0);

  uint64_t cid = 0;

  if (_registered_stream) {
    // Safe to initiate SSN_CLOSE if this is the last stream
    if (_proxy_ssn) {
      cid = _proxy_ssn->connection_id();

      SCOPED_MUTEX_LOCK(lock, _proxy_ssn->mutex, this_ethread());
      Http2ConnectionState &connection_state = this->get_connection_state();

      // Make sure the stream is removed from the stream list and priority tree
      // In many cases, this has been called earlier, so this call is a no-op
      connection_state.delete_stream(this);

      connection_state.decrement_peer_stream_count();

      // Update session's stream counts, so it accurately goes into keep-alive state
      connection_state.release_stream();

      // Do not access `_proxy_ssn` in below. It might be freed by `release_stream`.
    }
  } // Otherwise, not registered with the connection_state (i.e. a temporary stream used for HPACK header processing)

  // Clean up the write VIO in case of inactivity timeout
  this->do_io_write(nullptr, 0, nullptr);

  this->_milestones.mark(Http2StreamMilestone::CLOSE);

  ink_hrtime total_time = this->_milestones.elapsed(Http2StreamMilestone::OPEN, Http2StreamMilestone::CLOSE);
  Metrics::Counter::increment(http2_rsb.total_transactions_time, total_time);

  // Slow Log
  if (Http2::stream_slow_log_threshold != 0 && ink_hrtime_from_msec(Http2::stream_slow_log_threshold) < total_time) {
    Error("[%" PRIu64 "] [%" PRIu32 "] [%" PRId64 "] Slow H2 Stream: "
          "open: %" PRIu64 " "
          "dec_hdrs: %.3f "
          "txn: %.3f "
          "enc_hdrs: %.3f "
          "tx_hdrs: %.3f "
          "tx_data: %.3f "
          "close: %.3f",
          cid, static_cast<uint32_t>(this->_id), this->_http_sm_id,
          ink_hrtime_to_msec(this->_milestones[Http2StreamMilestone::OPEN]),
          this->_milestones.difference_sec(Http2StreamMilestone::OPEN, Http2StreamMilestone::START_DECODE_HEADERS),
          this->_milestones.difference_sec(Http2StreamMilestone::OPEN, Http2StreamMilestone::START_TXN),
          this->_milestones.difference_sec(Http2StreamMilestone::OPEN, Http2StreamMilestone::START_ENCODE_HEADERS),
          this->_milestones.difference_sec(Http2StreamMilestone::OPEN, Http2StreamMilestone::START_TX_HEADERS_FRAMES),
          this->_milestones.difference_sec(Http2StreamMilestone::OPEN, Http2StreamMilestone::START_TX_DATA_FRAMES),
          this->_milestones.difference_sec(Http2StreamMilestone::OPEN, Http2StreamMilestone::CLOSE));
  }

  _receive_header.destroy();
  _send_header.destroy();

  // Drop references to all buffer data
  this->_receive_buffer.clear();

  // Free the mutexes in the VIO
  read_vio.mutex.clear();
  write_vio.mutex.clear();

  ats_free(header_blocks);

  _clear_timers();
  clear_io_events();
  http_parser_clear(&http_parser);
}

int
Http2Stream::main_event_handler(int event, void *edata)
{
  SCOPED_MUTEX_LOCK(lock, this->mutex, this_ethread());
  REMEMBER(event, this->reentrancy_count);

  if (!this->_switch_thread_if_not_on_right_thread(event, edata)) {
    // Not on the right thread
    return 0;
  }
  ink_release_assert(this->_thread == this_ethread());

  Event *e = static_cast<Event *>(edata);
  reentrancy_count++;
  if (e == _read_vio_event) {
    _read_vio_event = nullptr;
    this->signal_read_event(e->callback_event);
    reentrancy_count--;
    return 0;
  } else if (e == _write_vio_event) {
    _write_vio_event = nullptr;
    this->signal_write_event(e->callback_event);
    reentrancy_count--;
    return 0;
  } else if (e == cross_thread_event) {
    cross_thread_event = nullptr;
  } else if (e == read_event) {
    read_event = nullptr;
  } else if (e == write_event) {
    write_event = nullptr;
  }

  switch (event) {
  case VC_EVENT_ACTIVE_TIMEOUT:
  case VC_EVENT_INACTIVITY_TIMEOUT:
    if (_sm == nullptr && closed != true) {
      // TIMEOUT without HttpSM - assuming incomplete header timeout
      Http2StreamDebug("timeout event=%d", event);

      ip_port_text_buffer ipb;
      const char         *remote_ip = ats_ip_ntop(this->_proxy_ssn->get_remote_addr(), ipb, sizeof(ipb));

      Error("HTTP/2 stream error timeout remote_ip=%s session_id=%" PRId64 " stream_id=%u event=%d", remote_ip,
            this->_proxy_ssn->connection_id(), this->_id, event);

      // Close stream
      do_io_close();
      terminate_stream = true;

      // Close connection because this stream doesn't read HEADERS/CONTINUATION frames anymore that makes HPACK dynamic table
      // out of sync.
      Http2ConnectionState &connection_state = get_connection_state();
      {
        SCOPED_MUTEX_LOCK(lock, connection_state.mutex, this_ethread());
        Http2Error error(Http2ErrorClass::HTTP2_ERROR_CLASS_CONNECTION, Http2ErrorCode::HTTP2_ERROR_COMPRESSION_ERROR,
                         "stream timeout");
        connection_state.handleEvent(HTTP2_SESSION_EVENT_ERROR, &error);
      }
    } else if (_sm && read_vio.ntodo() > 0) {
      this->signal_read_event(event);
    } else if (_sm && write_vio.ntodo() > 0) {
      this->signal_write_event(event);
    } else {
      Warning("HTTP/2 unknown case of %d event - session_id=%" PRId64 " stream_id=%u", event, this->_proxy_ssn->connection_id(),
              this->_id);
    }
    break;
  case VC_EVENT_WRITE_READY:
  case VC_EVENT_WRITE_COMPLETE:
    _timeout.update_inactivity();
    if (e->cookie == &write_vio) {
      if (write_vio.mutex && write_vio.cont && this->_sm) {
        this->signal_write_event(event);
      }
    } else {
      this->update_write_request(true);
    }
    break;
  case VC_EVENT_READ_COMPLETE:
    read_vio.nbytes = read_vio.ndone;
    /* fall through */
  case VC_EVENT_READ_READY:
    _timeout.update_inactivity();
    if (e->cookie == &read_vio) {
      if (read_vio.mutex && read_vio.cont && this->_sm) {
        this->signal_read_event(event);
      }
    } else {
      this->update_read_request(true);
    }
    break;
  case VC_EVENT_EOS:
    if (e->cookie == &read_vio) {
      SCOPED_MUTEX_LOCK(lock, read_vio.mutex, this_ethread());
      if (read_vio.cont) {
        read_vio.cont->handleEvent(VC_EVENT_EOS, &read_vio);
      }
    } else if (e->cookie == &write_vio) {
      SCOPED_MUTEX_LOCK(lock, write_vio.mutex, this_ethread());
      if (write_vio.cont) {
        write_vio.cont->handleEvent(VC_EVENT_EOS, &write_vio);
      }
    }
    break;
  }
  reentrancy_count--;
  // Clean stream up if the terminate flag is set and we are at the bottom of the handler stack
  terminate_if_possible();

  return 0;
}

Http2ErrorCode
Http2Stream::decode_header_blocks(HpackHandle &hpack_handle, uint32_t maximum_table_size)
{
  Http2ErrorCode error =
    http2_decode_header_blocks(&_receive_header, (const uint8_t *)header_blocks, header_blocks_length, nullptr, hpack_handle,
                               _trailing_header_is_possible, maximum_table_size, this->is_outbound_connection());
  if (error != Http2ErrorCode::HTTP2_ERROR_NO_ERROR) {
    Http2StreamDebug("Error decoding header blocks: %u", static_cast<uint32_t>(error));
  }
  return error;
}

void
Http2Stream::send_headers(Http2ConnectionState & /* cstate ATS_UNUSED */)
{
  if (closed) {
    return;
  }
  REMEMBER(NO_EVENT, this->reentrancy_count);

  // Convert header to HTTP/1.1 format. Trailing headers need no conversion
  // because they, by definition, do not contain pseudo headers.
  if (this->trailing_header_is_possible()) {
    Http2StreamDebug("trailing header: Skipping send_headers initialization.");
  } else {
    if (http2_convert_header_from_2_to_1_1(&_receive_header) == PARSE_RESULT_ERROR) {
      Http2StreamDebug("Error converting HTTP/2 headers to HTTP/1.1.");
      if (_receive_header.type_get() == HTTP_TYPE_REQUEST) {
        // There's no way to cause Bad Request directly at this time.
        // Set an invalid method so it causes an error later.
        _receive_header.method_set("\xffVOID", 1);
      }
    }

    if (_receive_header.type_get() == HTTP_TYPE_REQUEST) {
      // Check whether the request uses CONNECT method
      int         method_len;
      const char *method = _receive_header.method_get(&method_len);
      if (method_len == HTTP_LEN_CONNECT && strncmp(method, HTTP_METHOD_CONNECT, HTTP_LEN_CONNECT) == 0) {
        this->_is_tunneling = true;
      }
    }
    ink_release_assert(this->_sm != nullptr);
    this->_http_sm_id = this->_sm->sm_id;
  }

  // Write header to a buffer.  Borrowing logic from HttpSM::write_header_into_buffer.
  // Seems like a function like this ought to be in HTTPHdr directly
  int bufindex;
  int dumpoffset = 0;
  // The name dumpoffset is used here for parity with
  // HttpSM::write_header_into_buffer, but create an alias for clarity in the
  // use of this variable below this loop.
  int &num_header_bytes = dumpoffset;
  int  done, tmp;
  do {
    bufindex             = 0;
    tmp                  = dumpoffset;
    IOBufferBlock *block = this->_receive_buffer.get_current_block();
    if (!block) {
      this->_receive_buffer.add_block();
      block = this->_receive_buffer.get_current_block();
    }
    done        = _receive_header.print(block->end(), block->write_avail(), &bufindex, &tmp);
    dumpoffset += bufindex;
    this->_receive_buffer.fill(bufindex);
    if (!done) {
      this->_receive_buffer.add_block();
    }
  } while (!done);

  if (num_header_bytes == 0) {
    // No data to signal read event
    return;
  }

  // Is the _sm ready to process the header?
  if (this->read_vio.nbytes > 0) {
    if (this->receive_end_stream) {
      // These headers may be standard or trailer headers:
      //
      // * If they are standard, then there is no body (note again that the
      // END_STREAM flag was sent with them), data_length will be 0, and
      // num_header_bytes will simply be the length of the headers.
      //
      // * If they are trailers, then the tunnel behind the SM was set up after
      // the original headers were sent, and thus nbytes should not include the
      // size of the original standard headers. Rather, for trailers, nbytes
      // only needs to include the body length (i.e., DATA frame payload
      // length), and the length of these current trailer headers calculated in
      // num_header_bytes.
      this->read_vio.nbytes = this->data_length + num_header_bytes;
      Http2StreamDebug("nbytes: %" PRId64 ", ndone: %" PRId64 ", num_header_bytes: %d, data_length: %" PRId64,
                       this->read_vio.nbytes, this->read_vio.ndone, num_header_bytes, this->data_length);
      if (this->is_outbound_connection()) {
        // This is a response header.
        // We don't set ndone because the VC_EVENT_EOS will
        // first flush the remaining content to consumers,
        // after which the TUNNEL_EVENT_DONE will be fired
        // and the header handler will be set up.
        // The header handler will read the buffer, and not
        // get its content from the VIO
        // This can break if the implementation
        // changes.
        this->signal_read_event(VC_EVENT_EOS);
      } else {
        // Request headers.
        this->read_vio.ndone = this->read_vio.nbytes;
        this->signal_read_event(VC_EVENT_READ_COMPLETE);
      }
    } else {
      // End of header but not end of stream, must have some body frames coming
      this->has_body = true;
      this->signal_read_event(VC_EVENT_READ_READY);
    }
  }
}

bool
Http2Stream::change_state(uint8_t type, uint8_t flags)
{
  auto const initial_state = _state;
  switch (_state) {
  case Http2StreamState::HTTP2_STREAM_STATE_IDLE:
    if (type == HTTP2_FRAME_TYPE_HEADERS) {
      if (receive_end_stream) {
        _state = Http2StreamState::HTTP2_STREAM_STATE_HALF_CLOSED_REMOTE;
      } else if (send_end_stream) {
        _state = Http2StreamState::HTTP2_STREAM_STATE_HALF_CLOSED_LOCAL;
      } else {
        _state = Http2StreamState::HTTP2_STREAM_STATE_OPEN;
      }
    } else if (type == HTTP2_FRAME_TYPE_CONTINUATION) {
      if (receive_end_stream) {
        _state = Http2StreamState::HTTP2_STREAM_STATE_HALF_CLOSED_REMOTE;
      } else if (send_end_stream) {
        _state = Http2StreamState::HTTP2_STREAM_STATE_HALF_CLOSED_LOCAL;
      } else {
        _state = Http2StreamState::HTTP2_STREAM_STATE_OPEN;
      }
    } else if (type == HTTP2_FRAME_TYPE_PUSH_PROMISE) {
      _state = Http2StreamState::HTTP2_STREAM_STATE_RESERVED_LOCAL;
    } else if (type == HTTP2_FRAME_TYPE_RST_STREAM) {
      _state = Http2StreamState::HTTP2_STREAM_STATE_CLOSED;
    } else {
      return false;
    }
    break;

  case Http2StreamState::HTTP2_STREAM_STATE_OPEN:
    if (type == HTTP2_FRAME_TYPE_RST_STREAM) {
      _state = Http2StreamState::HTTP2_STREAM_STATE_CLOSED;
    } else if (type == HTTP2_FRAME_TYPE_HEADERS || type == HTTP2_FRAME_TYPE_DATA) {
      if (receive_end_stream) {
        if (send_end_stream) {
          _state = Http2StreamState::HTTP2_STREAM_STATE_CLOSED;
        } else {
          _state = Http2StreamState::HTTP2_STREAM_STATE_HALF_CLOSED_REMOTE;
        }
      } else if (send_end_stream) {
        if (receive_end_stream) {
          _state = Http2StreamState::HTTP2_STREAM_STATE_CLOSED;
        } else {
          _state = Http2StreamState::HTTP2_STREAM_STATE_HALF_CLOSED_LOCAL;
        }
      } else {
        // Do not change state
      }
    } else {
      // A stream in the "open" state may be used by both peers to send frames of any type.
      return true;
    }
    break;

  case Http2StreamState::HTTP2_STREAM_STATE_RESERVED_LOCAL:
    if (type == HTTP2_FRAME_TYPE_HEADERS) {
      if (flags & HTTP2_FLAGS_HEADERS_END_HEADERS) {
        _state = Http2StreamState::HTTP2_STREAM_STATE_HALF_CLOSED_REMOTE;
      }
    } else if (type == HTTP2_FRAME_TYPE_CONTINUATION) {
      if (flags & HTTP2_FLAGS_CONTINUATION_END_HEADERS) {
        _state = Http2StreamState::HTTP2_STREAM_STATE_HALF_CLOSED_REMOTE;
      }
    } else {
      return false;
    }
    break;

  case Http2StreamState::HTTP2_STREAM_STATE_RESERVED_REMOTE:
    // Currently ATS supports only HTTP/2 server features
    return false;

  case Http2StreamState::HTTP2_STREAM_STATE_HALF_CLOSED_LOCAL:
    if (type == HTTP2_FRAME_TYPE_RST_STREAM || receive_end_stream) {
      _state = Http2StreamState::HTTP2_STREAM_STATE_CLOSED;
    }
    break;

  case Http2StreamState::HTTP2_STREAM_STATE_HALF_CLOSED_REMOTE:
    if (type == HTTP2_FRAME_TYPE_RST_STREAM || send_end_stream) {
      _state = Http2StreamState::HTTP2_STREAM_STATE_CLOSED;
    } else if (type == HTTP2_FRAME_TYPE_HEADERS) { // w/o END_STREAM flag
      // No state change here. Expect a following DATA frame with END_STREAM flag.
      return true;
    } else if (type == HTTP2_FRAME_TYPE_CONTINUATION) { // w/o END_STREAM flag
      // No state change here. Expect a following DATA frame with END_STREAM flag.
      return true;
    }
    break;

  case Http2StreamState::HTTP2_STREAM_STATE_CLOSED:
    // No state changing
    return true;

  default:
    return false;
  }

  Http2StreamDebug("%s -> %s", Http2DebugNames::get_state_name(initial_state), Http2DebugNames::get_state_name(_state));

  return true;
}

VIO *
Http2Stream::do_io_read(Continuation *c, int64_t nbytes, MIOBuffer *buf)
{
  if (buf) {
    read_vio.set_writer(buf);
  } else {
    read_vio.buffer.clear();
  }

  read_vio.mutex     = c ? c->mutex : this->mutex;
  read_vio.cont      = c;
  read_vio.nbytes    = nbytes;
  read_vio.ndone     = 0;
  read_vio.vc_server = this;
  read_vio.op        = VIO::READ;

  // TODO: re-enable read_vio

  return &read_vio;
}

VIO *
Http2Stream::do_io_write(Continuation *c, int64_t nbytes, IOBufferReader *abuffer, bool /* owner ATS_UNUSED */)
{
  if (abuffer) {
    write_vio.set_reader(abuffer);
  } else {
    write_vio.buffer.clear();
  }
  write_vio.mutex     = c ? c->mutex : this->mutex;
  write_vio.cont      = c;
  write_vio.nbytes    = nbytes;
  write_vio.ndone     = 0;
  write_vio.vc_server = this;
  write_vio.op        = VIO::WRITE;
  _send_reader        = abuffer;

  if (c != nullptr && nbytes > 0 && this->is_state_writeable()) {
    update_write_request(false);
  } else if (!this->is_state_writeable()) {
    // Cannot start a write on a closed stream
    return nullptr;
  }
  return &write_vio;
}

// Initiated from SM
void
Http2Stream::do_io_close(int /* flags */)
{
  SCOPED_MUTEX_LOCK(lock, this->mutex, this_ethread());

  if (!closed) {
    REMEMBER(NO_EVENT, this->reentrancy_count);
    Http2StreamDebug("do_io_close");

    // Let the other end know we are going away.
    // We only need to do this for the client side since we only need to pass through RST_STREAM
    // from the server. If a client sends a RST_STREAM, we need to keep the server side alive so
    // the background fill can function as intended.
    //
    // In the half-closed(local) state, server can close stream actively by sending RST_STREAM frame
    if (!this->is_outbound_connection() &&
        (this->is_state_writeable() || _state == Http2StreamState::HTTP2_STREAM_STATE_HALF_CLOSED_LOCAL)) {
      this->get_connection_state().send_rst_stream_frame(_id, Http2ErrorCode::HTTP2_ERROR_NO_ERROR);
    }

    // When we get here, the SM has initiated the shutdown.  Either it received a WRITE_COMPLETE, or it is shutting down.  Any
    // remaining IO operations back to client should be abandoned.  The SM-side buffers backing these operations will be deleted
    // by the time this is called from transaction_done.
    closed = true;

    // Adjust state, so we don't process any more data
    _state = Http2StreamState::HTTP2_STREAM_STATE_CLOSED;

    _clear_timers();
    clear_io_events();

    // Otherwise, Wait until transaction_done is called from HttpSM to signal that the TXN_CLOSE hook has been executed
  }
}

/*
 *  HttpSM has called TXN_close hooks.
 */
void
Http2Stream::transaction_done()
{
  SCOPED_MUTEX_LOCK(lock, this->mutex, this_ethread());
  super::transaction_done();

  if (!closed && _state == Http2StreamState::HTTP2_STREAM_STATE_CLOSED) {
    do_io_close(); // Make sure we've been closed.  If we didn't close the _proxy_ssn session better still be open
  }
  Http2ConnectionState &state = this->get_connection_state();
  ink_release_assert(closed || !state.is_state_closed());

  // HttpSM is gone, no more reading or writing
  _sm = nullptr;
  read_vio.disable();
  write_vio.disable();

  if (closed) {
    // Safe to initiate SSN_CLOSE if this is the last stream
    ink_assert(cross_thread_event == nullptr);
    // Schedule the destroy to occur after we unwind here.  IF we call directly, may delete with reference on the stack.
    terminate_stream = true;
    terminate_if_possible();
  }
}

void
Http2Stream::terminate_if_possible()
{
  // if (terminate_stream && reentrancy_count == 0) {
  if (reentrancy_count == 0 && closed && terminate_stream) {
    REMEMBER(NO_EVENT, this->reentrancy_count);

    SCOPED_MUTEX_LOCK(lock, _proxy_ssn->mutex, this_ethread());
    THREAD_FREE(this, http2StreamAllocator, this_ethread());
  }
}

// Initiated from the Http2 side
void
Http2Stream::initiating_close()
{
  if (!closed) {
    SCOPED_MUTEX_LOCK(lock, this->mutex, this_ethread());
    REMEMBER(NO_EVENT, this->reentrancy_count);
    Http2StreamDebug("initiating_close client_window=%zd session_window=%zd", _peer_rwnd,
                     this->get_connection_state().get_peer_rwnd());

    if (!this->is_outbound_connection() && this->is_state_writeable()) { // Let the other end know we are going away
      this->get_connection_state().send_rst_stream_frame(_id, Http2ErrorCode::HTTP2_ERROR_NO_ERROR);
    }

    // Set the state of the connection to closed
    // TODO - these states should be combined
    closed = true;
    _state = Http2StreamState::HTTP2_STREAM_STATE_CLOSED;

    // leaving the reference to the SM, so we can detach from the SM when we actually destroy
    // _sm = NULL;
    // Leaving reference to client session as well, so we can signal once the
    // TXN_CLOSE has been sent
    // _proxy_ssn = NULL;

    _clear_timers();
    clear_io_events();

    // This should result in do_io_close or release being called.  That will schedule the final
    // kill yourself signal
    // We are sending signals rather than calling the handlers directly to avoid the case where
    // the HttpTunnel handler causes the HttpSM to be deleted on the stack.
    bool sent_write_complete = false;
    if (_sm) {
      // Push out any last IO events
      // First look for active write or read
      if (write_vio.cont && write_vio.nbytes > 0 && write_vio.ndone == write_vio.nbytes &&
          (!is_outbound_connection() || get_state() == Http2StreamState::HTTP2_STREAM_STATE_OPEN)) {
        SCOPED_MUTEX_LOCK(lock, write_vio.mutex, this_ethread());
        Http2StreamDebug("Send tracked event VC_EVENT_WRITE_COMPLETE on write_vio. sm_id: %" PRId64, _sm->sm_id);
        write_event         = send_tracked_event(write_event, VC_EVENT_WRITE_COMPLETE, &write_vio);
        sent_write_complete = true;
      }

      if (!sent_write_complete) {
        if (write_vio.cont && write_vio.get_writer() &&
            (!is_outbound_connection() || get_state() == Http2StreamState::HTTP2_STREAM_STATE_OPEN ||
             get_state() == Http2StreamState::HTTP2_STREAM_STATE_HALF_CLOSED_LOCAL)) {
          SCOPED_MUTEX_LOCK(lock, write_vio.mutex, this_ethread());
          Http2StreamDebug("Send tracked event VC_EVENT_EOS on write_vio. sm_id: %" PRId64, _sm->sm_id);
          write_event = send_tracked_event(write_event, VC_EVENT_EOS, &write_vio);
        } else if (read_vio.cont && read_vio.get_writer()) {
          SCOPED_MUTEX_LOCK(lock, read_vio.mutex, this_ethread());
          Http2StreamDebug("Send tracked event VC_EVENT_EOS on read_vio. sm_id: %" PRId64, _sm->sm_id);
          read_event = send_tracked_event(read_event, VC_EVENT_EOS, &read_vio);
        } else {
          Http2StreamDebug("send EOS to SM");
          // Just send EOS to the _sm
          _sm->handleEvent(VC_EVENT_EOS, nullptr);
        }
      }
    } else {
      Http2StreamDebug("No SM to signal");
      // Transaction is already gone or not started. Kill yourself
      terminate_stream = true;
      terminate_if_possible();
    }
  }
}

bool
Http2Stream::is_outbound_connection() const
{
  return _is_outbound;
}

bool
Http2Stream::is_tunneling() const
{
  return _is_tunneling;
}

/* Replace existing event only if the new event is different than the inprogress event */
Event *
Http2Stream::send_tracked_event(Event *event, int send_event, VIO *vio)
{
  if (event != nullptr) {
    if (event->callback_event != send_event) {
      event->cancel();
      event = nullptr;
    }
  }

  if (event == nullptr) {
    REMEMBER(send_event, this->reentrancy_count);
    event = this_ethread()->schedule_imm(this, send_event, vio);
  }

  return event;
}

void
Http2Stream::update_read_request(bool call_update)
{
  if (closed || _proxy_ssn == nullptr || _sm == nullptr || read_vio.mutex == nullptr) {
    return;
  }

  if (!this->_switch_thread_if_not_on_right_thread(VC_EVENT_READ_READY, nullptr)) {
    // Not on the right thread
    return;
  }
  ink_release_assert(this->_thread == this_ethread());

  SCOPED_MUTEX_LOCK(lock, read_vio.mutex, this_ethread());
  if (read_vio.nbytes == 0 || read_vio.is_disabled()) {
    return;
  }

  // Try to be smart and only signal if there was additional data
  int send_event = VC_EVENT_READ_READY;
  if (read_vio.ntodo() == 0 || (this->receive_end_stream && this->read_vio.nbytes != INT64_MAX)) {
    send_event = VC_EVENT_READ_COMPLETE;
  }

  int64_t read_avail = this->read_vio.get_writer()->max_read_avail();
  if (read_avail > 0 || send_event == VC_EVENT_READ_COMPLETE) {
    if (call_update) { // Safe to call vio handler directly
      _timeout.update_inactivity();
      if (read_vio.cont && this->_sm) {
        read_vio.cont->handleEvent(send_event, &read_vio);
      }
    } else { // Called from do_io_read.  Still setting things up.  Send event
      // to handle this after the dust settles
      read_event = send_tracked_event(read_event, send_event, &read_vio);
    }
  }
}

void
Http2Stream::restart_sending()
{
  // Make sure the stream is in a good state to be sending
  if (this->is_closed()) {
    return;
  }
  if (!this->parsing_header_done) {
    this->update_write_request(true);
    return;
  }
  if (this->is_outbound_connection()) {
    if (this->get_state() != Http2StreamState::HTTP2_STREAM_STATE_OPEN || write_vio.ntodo() == 0) {
      return;
    }
  } else {
    if (this->get_state() != Http2StreamState::HTTP2_STREAM_STATE_HALF_CLOSED_REMOTE) {
      return;
    }
  }

  IOBufferReader *reader = this->get_data_reader_for_send();
  if (reader && !reader->is_read_avail_more_than(0)) {
    return;
  }

  if (this->write_vio.mutex && this->write_vio.ntodo() == 0) {
    return;
  }

  this->send_body(true);
}

void
Http2Stream::update_write_request(bool call_update)
{
  if (!this->is_state_writeable() || closed || _proxy_ssn == nullptr || write_vio.mutex == nullptr ||
      write_vio.get_reader() == nullptr || this->_send_reader == nullptr) {
    return;
  }

  if (!this->_switch_thread_if_not_on_right_thread(VC_EVENT_WRITE_READY, nullptr)) {
    // Not on the right thread
    return;
  }
  ink_release_assert(this->_thread == this_ethread());

  Http2StreamDebug("update_write_request parse_done=%d", parsing_header_done);

  Http2ConnectionState &connection_state = this->get_connection_state();

  SCOPED_MUTEX_LOCK(lock, write_vio.mutex, this_ethread());

  IOBufferReader *vio_reader = write_vio.get_reader();

  if (write_vio.ntodo() > 0 && (!vio_reader->is_read_avail_more_than(0))) {
    Http2StreamDebug("update_write_request give up without doing anything ntodo=%" PRId64 " is_read_avail=%d client_window=%zd"
                     " session_window=%zd",
                     write_vio.ntodo(), vio_reader->is_read_avail_more_than(0), _peer_rwnd,
                     this->get_connection_state().get_peer_rwnd());
    return;
  }

  // Process the new data
  if (!this->parsing_header_done) {
    // Still parsing the request or response header
    int bytes_used = 0;
    int state;
    if (this->is_outbound_connection()) {
      state = this->_send_header.parse_req(&http_parser, this->_send_reader, &bytes_used, false);
    } else {
      state = this->_send_header.parse_resp(&http_parser, this->_send_reader, &bytes_used, false);
    }
    // HTTPHdr::parse_resp() consumed the send_reader in above
    write_vio.ndone += bytes_used;

    switch (state) {
    case PARSE_RESULT_DONE: {
      this->parsing_header_done = true;
      Http2StreamDebug("update_write_request parsing done, read %d bytes", bytes_used);

      // Schedule session shutdown if response header has "Connection: close"
      MIMEField *field = this->_send_header.field_find(MIME_FIELD_CONNECTION, MIME_LEN_CONNECTION);
      if (field) {
        auto value{field->value_get()};
        if (value == std::string_view{HTTP_VALUE_CLOSE, static_cast<std::string_view::size_type>(HTTP_LEN_CLOSE)}) {
          SCOPED_MUTEX_LOCK(lock, _proxy_ssn->mutex, this_ethread());
          if (connection_state.get_shutdown_state() == HTTP2_SHUTDOWN_NONE) {
            connection_state.set_shutdown_state(HTTP2_SHUTDOWN_NOT_INITIATED, Http2ErrorCode::HTTP2_ERROR_NO_ERROR);
          }
        }
      }

      {
        SCOPED_MUTEX_LOCK(lock, _proxy_ssn->mutex, this_ethread());
        // Send the response header back
        connection_state.send_headers_frame(this);
      }

      // Roll back states of response header to read final response
      if (!this->is_outbound_connection() && this->_send_header.expect_final_response()) {
        this->parsing_header_done = false;
      }
      if (this->is_outbound_connection() || this->_send_header.expect_final_response()) {
        _send_header.destroy();
        _send_header.create(this->is_outbound_connection() ? HTTP_TYPE_REQUEST : HTTP_TYPE_RESPONSE, HTTP_2_0);
        http_parser_clear(&http_parser);
        http_parser_init(&http_parser);
      }
      bool final_write = this->write_vio.ntodo() == 0;
      if (final_write) {
        this->signal_write_event(VC_EVENT_WRITE_COMPLETE, !CALL_UPDATE);
      }

      if (!final_write && this->_send_reader->is_read_avail_more_than(0)) {
        Http2StreamDebug("update_write_request done parsing, still more to send");
        this->_milestones.mark(Http2StreamMilestone::START_TX_DATA_FRAMES);
        this->send_body(call_update);
      }
      break;
    }
    case PARSE_RESULT_CONT:
      // Let it ride for next time
      Http2StreamDebug("update_write_request still parsing, read %d bytes", bytes_used);
      break;
    default:
      Http2StreamDebug("update_write_request  state %d, read %d bytes", state, bytes_used);
      break;
    }
  } else {
    this->_milestones.mark(Http2StreamMilestone::START_TX_DATA_FRAMES);
    this->send_body(call_update);
  }

  return;
}

void
Http2Stream::signal_read_event(int event)
{
  if (this->_sm == nullptr || this->read_vio.cont == nullptr || this->read_vio.cont->mutex == nullptr ||
      this->read_vio.op == VIO::NONE || this->terminate_stream) {
    return;
  }

  reentrancy_count++;
  MUTEX_TRY_LOCK(lock, read_vio.cont->mutex, this_ethread());
  if (lock.is_locked()) {
    if (read_event) {
      read_event->cancel();
      read_event = nullptr;
    }
    _timeout.update_inactivity();
    this->read_vio.cont->handleEvent(event, &this->read_vio);
  } else {
    if (this->_read_vio_event) {
      this->_read_vio_event->cancel();
    }
    this->_read_vio_event = this_ethread()->schedule_in(this, retry_delay, event, &read_vio);
  }
  reentrancy_count--;
  // Clean stream up if the terminate flag is set and we are at the bottom of the handler stack
  terminate_if_possible();
}

void
Http2Stream::signal_write_event(int event, bool call_update)
{
  // Don't signal a write event if in fact nothing was written
  if (this->_sm == nullptr || this->write_vio.cont == nullptr || this->write_vio.cont->mutex == nullptr ||
      this->write_vio.op == VIO::NONE || this->terminate_stream) {
    return;
  }

  reentrancy_count++;
  if (call_update) {
    MUTEX_TRY_LOCK(lock, write_vio.cont->mutex, this_ethread());
    if (lock.is_locked()) {
      if (write_event) {
        write_event->cancel();
        write_event = nullptr;
      }
      _timeout.update_inactivity();
      this->write_vio.cont->handleEvent(event, &this->write_vio);
    } else {
      if (this->_write_vio_event) {
        this->_write_vio_event->cancel();
      }
      this->_write_vio_event = this_ethread()->schedule_in(this, retry_delay, event, &write_vio);
    }
  } else {
    // Called from do_io_write. Might still be setting up state. Send an event to let the dust settle
    write_event = send_tracked_event(write_event, event, &write_vio);
  }
  reentrancy_count--;
  // Clean stream up if the terminate flag is set and we are at the bottom of the handler stack
  terminate_if_possible();
}

bool
Http2Stream::push_promise(URL &url, const MIMEField *accept_encoding)
{
  SCOPED_MUTEX_LOCK(lock, _proxy_ssn->mutex, this_ethread());
  return this->get_connection_state().send_push_promise_frame(this, url, accept_encoding);
}

void
Http2Stream::send_body(bool /* call_update ATS_UNUSED */)
{
  Http2ConnectionState &connection_state = this->get_connection_state();
  _timeout.update_inactivity();

  reentrancy_count++;

  SCOPED_MUTEX_LOCK(lock, _proxy_ssn->mutex, this_ethread());
  if (Http2::stream_priority_enabled) {
    connection_state.schedule_stream_to_send_priority_frames(this);
    // signal_write_event() will be called from `Http2ConnectionState::send_data_frames_depends_on_priority()`
    // when write_vio is consumed
  } else {
    connection_state.send_data_frames(this);
    // XXX The call to signal_write_event can destroy/free the Http2Stream.
    // Don't modify the Http2Stream after calling this method.
  }

  reentrancy_count--;
  terminate_if_possible();
}

void
Http2Stream::reenable_write()
{
  if (this->_proxy_ssn) {
    SCOPED_MUTEX_LOCK(lock, this->mutex, this_ethread());
    update_write_request(true);
  }
}

void
Http2Stream::reenable(VIO *vio)
{
  if (this->_proxy_ssn) {
    if (vio->op == VIO::WRITE) {
      SCOPED_MUTEX_LOCK(lock, this->mutex, this_ethread());
      update_write_request(true);
    } else if (vio->op == VIO::READ) {
      SCOPED_MUTEX_LOCK(ssn_lock, _proxy_ssn->mutex, this_ethread());
      Http2ConnectionState &connection_state = this->get_connection_state();
      connection_state.restart_receiving(this);
    }
  }
}

IOBufferReader *
Http2Stream::get_data_reader_for_send() const
{
  return this->_send_reader;
}

void
Http2Stream::set_active_timeout(ink_hrtime timeout_in)
{
  _timeout.set_active_timeout(timeout_in);
}

void
Http2Stream::set_inactivity_timeout(ink_hrtime timeout_in)
{
  _timeout.set_inactive_timeout(timeout_in);
}

void
Http2Stream::cancel_active_timeout()
{
  _timeout.cancel_active_timeout();
}

void
Http2Stream::cancel_inactivity_timeout()
{
  _timeout.cancel_inactive_timeout();
}

bool
Http2Stream::is_active_timeout_expired(ink_hrtime now)
{
  return _timeout.is_active_timeout_expired(now);
}

bool
Http2Stream::is_inactive_timeout_expired(ink_hrtime now)
{
  return _timeout.is_inactive_timeout_expired(now);
}

void
Http2Stream::clear_io_events()
{
  if (cross_thread_event) {
    cross_thread_event->cancel();
    cross_thread_event = nullptr;
  }

  if (read_event) {
    read_event->cancel();
    read_event = nullptr;
  }

  if (write_event) {
    write_event->cancel();
    write_event = nullptr;
  }

  if (this->_read_vio_event) {
    this->_read_vio_event->cancel();
    this->_read_vio_event = nullptr;
  }

  if (this->_write_vio_event) {
    this->_write_vio_event->cancel();
    this->_write_vio_event = nullptr;
  }
}

/**
  Callback from HttpSM

  release and do_io_close are the same for the HTTP/2 protocol
 */
void
Http2Stream::release()
{
  if (_state == Http2StreamState::HTTP2_STREAM_STATE_CLOSED) {
    this->do_io_close();
    return;
  }

  Http2StreamDebug("Delaying do_io_close() until stream is in the closed state");
}

void
Http2Stream::increment_transactions_stat()
{
  if (this->is_outbound_connection()) {
    Metrics::Gauge::increment(http2_rsb.current_server_stream_count);
    Metrics::Counter::increment(http2_rsb.total_server_stream_count);
  } else {
    Metrics::Gauge::increment(http2_rsb.current_client_stream_count);
    Metrics::Counter::increment(http2_rsb.total_client_stream_count);
  }
}

void
Http2Stream::decrement_transactions_stat()
{
  if (this->is_outbound_connection()) {
    Metrics::Gauge::decrement(http2_rsb.current_server_stream_count);
  } else {
    Metrics::Gauge::decrement(http2_rsb.current_client_stream_count);
  }
}

ssize_t
Http2Stream::get_peer_rwnd() const
{
  return this->_peer_rwnd;
}

Http2ErrorCode
Http2Stream::increment_peer_rwnd(size_t amount)
{
  this->_peer_rwnd += amount;

  this->_recent_rwnd_increment[this->_recent_rwnd_increment_index] = amount;
  ++this->_recent_rwnd_increment_index;
  this->_recent_rwnd_increment_index %= this->_recent_rwnd_increment.size();
  double sum = std::accumulate(this->_recent_rwnd_increment.begin(), this->_recent_rwnd_increment.end(), 0.0);
  double avg = sum / this->_recent_rwnd_increment.size();
  if (avg < Http2::min_avg_window_update) {
    return Http2ErrorCode::HTTP2_ERROR_ENHANCE_YOUR_CALM;
  }
  return Http2ErrorCode::HTTP2_ERROR_NO_ERROR;
}

Http2ErrorCode
Http2Stream::decrement_peer_rwnd(size_t amount)
{
  this->_peer_rwnd -= amount;
  if (this->_peer_rwnd < 0) {
    return Http2ErrorCode::HTTP2_ERROR_PROTOCOL_ERROR;
  } else {
    return Http2ErrorCode::HTTP2_ERROR_NO_ERROR;
  }
}

ssize_t
Http2Stream::get_local_rwnd() const
{
  return this->_local_rwnd;
}

Http2ErrorCode
Http2Stream::increment_local_rwnd(size_t amount)
{
  this->_local_rwnd += amount;
  return Http2ErrorCode::HTTP2_ERROR_NO_ERROR;
}

Http2ErrorCode
Http2Stream::decrement_local_rwnd(size_t amount)
{
  this->_local_rwnd -= amount;
  if (this->_local_rwnd < 0) {
    return Http2ErrorCode::HTTP2_ERROR_PROTOCOL_ERROR;
  } else {
    return Http2ErrorCode::HTTP2_ERROR_NO_ERROR;
  }
}

bool
Http2Stream::_switch_thread_if_not_on_right_thread(int event, void *edata)
{
  if (this->_thread != this_ethread()) {
    SCOPED_MUTEX_LOCK(stream_lock, this->mutex, this_ethread());
    if (cross_thread_event == nullptr) {
      // Send to the right thread
      cross_thread_event = this->_thread->schedule_imm(this, event, edata);
    }
    return false;
  }
  return true;
}

int
Http2Stream::get_transaction_priority_weight() const
{
  return priority_node ? priority_node->weight : 0;
}

int
Http2Stream::get_transaction_priority_dependence() const
{
  if (!priority_node) {
    return -1;
  } else {
    return priority_node->parent ? priority_node->parent->id : 0;
  }
}

int64_t
Http2Stream::read_vio_read_avail()
{
  MIOBuffer *writer = this->read_vio.get_writer();
  if (writer) {
    return writer->max_read_avail();
  }

  return 0;
}

bool
Http2Stream::has_request_body(int64_t /* content_length ATS_UNUSED */, bool /* is_chunked_set ATS_UNUSED */) const
{
  return has_body;
}

Http2ConnectionState &
Http2Stream::get_connection_state()
{
  if (this->is_outbound_connection()) {
    Http2ServerSession *session = static_cast<Http2ServerSession *>(_proxy_ssn);
    return session->connection_state;
  } else {
    Http2ClientSession *session = static_cast<Http2ClientSession *>(_proxy_ssn);
    return session->connection_state;
  }
}

bool
Http2Stream::is_read_closed() const
{
  return this->receive_end_stream;
}

bool
Http2Stream::expect_send_trailer() const
{
  return this->_expect_send_trailer;
}

void
Http2Stream::set_expect_send_trailer()
{
  _expect_send_trailer = true;
  parsing_header_done  = false;
  reset_send_headers();
}
bool
Http2Stream::expect_receive_trailer() const
{
  return this->_expect_receive_trailer;
}

void
Http2Stream::set_expect_receive_trailer()
{
  _expect_receive_trailer = true;
}

void
Http2Stream::set_rx_error_code(ProxyError e)
{
  if (!this->is_outbound_connection() && this->_sm) {
    this->_sm->t_state.client_info.rx_error_code = e;
  }
}

void
Http2Stream::set_tx_error_code(ProxyError e)
{
  if (!this->is_outbound_connection() && this->_sm) {
    if (!(this->_sm->t_state.client_info.tx_error_code.cls == ProxyErrorClass::SSN && e.cls == ProxyErrorClass::TXN)) {
      // if there is not an error already set for the session set the transaction level error
      this->_sm->t_state.client_info.tx_error_code = e;
    }
  }
}

HTTPVersion
Http2Stream::get_version(HTTPHdr & /* hdr ATS_UNUSED */) const
{
  return HTTP_2_0;
}
