/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

// HttpLog.h should generally be included first
#include "HttpLog.h"

// Log on level :5, instead of default :4.
#undef LOG
#define LOG(args) LOG5(args)
#undef LOG_ENABLED
#define LOG_ENABLED() LOG5_ENABLED()

#include "ConnectionAttemptPool.h"
#include "ConnectionEntry.h"
#include "DnsAndConnectSocket.h"
#include "HappyEyeballsConnectionAttempt.h"
#include "nsHttpHandler.h"
#include "nsHttpConnectionMgr.h"

namespace mozilla::net {

ConnectionAttemptPool::ConnectionAttemptPool(ConnectionEntry* aEntry)
    : mEntry(aEntry) {
  LOG(("ConnectionAttemptPool ctor %p", this));
}

ConnectionAttemptPool::~ConnectionAttemptPool() {
  LOG(("ConnectionAttemptPool dtor %p", this));
  MOZ_DIAGNOSTIC_ASSERT(mAttempts.IsEmpty());
}

nsresult ConnectionAttemptPool::StartConnectionEstablishment(
    ConnectionEntry* entry, nsAHttpTransaction* trans, uint32_t caps,
    bool speculative, bool urgentStart, bool allow1918,
    PendingTransactionInfo* pendingTransInfo, bool retryWithoutTRR) {
  MOZ_ASSERT(OnSocketThread(), "not on socket thread");
  MOZ_ASSERT((speculative && !pendingTransInfo) ||
             (!speculative && pendingTransInfo));

  RefPtr<ConnectionAttempt> connAttempt;
  nsHttpConnectionInfo* ci = trans->ConnectionInfo();
  if (ci->GetHappyEyeballsEnabled()) {
    connAttempt = new HappyEyeballsConnectionAttempt(
        ci, trans, caps, speculative, urgentStart, retryWithoutTRR);
  } else {
    connAttempt = new DnsAndConnectSocket(entry->mConnInfo, trans, caps,
                                          speculative, urgentStart);
  }

  if (speculative) {
    connAttempt->SetAllow1918(allow1918);
  }

  nsresult rv = connAttempt->Init(entry);
  if (NS_FAILED(rv)) {
    connAttempt->Abandon();
    return rv;
  }

  InsertIntoConnectionAttempts(connAttempt);

  if (pendingTransInfo) {
    bool claimed = connAttempt->Claim();
    if (!claimed) {
      // We should always be able to claim this.
      MOZ_ASSERT(false, "Failed to claim a connAttempt");
      return NS_ERROR_UNEXPECTED;
    }
    pendingTransInfo->RememberConnectionAttempt(connAttempt);
  }
  return NS_OK;
}

void ConnectionAttemptPool::InsertIntoConnectionAttempts(
    ConnectionAttempt* sock) {
  mAttempts.AppendElement(sock);
  ++mUnconnectedCount;
  gHttpHandler->ConnMgr()->IncreaseNumDnsAndConnectSockets();
}

void ConnectionAttemptPool::RemoveConnectionAttempt(ConnectionAttempt* sock,
                                                    bool abandon) {
  RefPtr<ConnectionAttempt> keepAlive(sock);
  if (abandon) {
    sock->Abandon();
  }

  if (mAttempts.RemoveElement(sock)) {
    if (!sock->HasConnected()) {
      MOZ_ASSERT(mUnconnectedCount > 0);
      --mUnconnectedCount;
    }
    gHttpHandler->ConnMgr()->DecreaseNumDnsAndConnectSockets();
  }

  if (!UnconnectedConnectionAttempts()) {
    // perhaps this reverted RestrictConnections()
    // use the PostEvent version of processpendingq to avoid
    // altering the pending q vector from an arbitrary stack
    RefPtr<ConnectionEntry> entry(mEntry);
    if (entry) {
      gHttpHandler->ConnMgr()->ProcessPendingQForEntry(entry);
    }
  }
}

uint32_t ConnectionAttemptPool::UnconnectedConnectionAttempts() const {
#ifdef DEBUG
  uint32_t computed = 0;
  for (uint32_t i = 0; i < mAttempts.Length(); ++i) {
    if (!mAttempts[i]->HasConnected()) {
      ++computed;
    }
  }
  MOZ_ASSERT(mUnconnectedCount == computed);
#endif
  return mUnconnectedCount;
}

void ConnectionAttemptPool::OnConnectionAttemptConnected() {
  MOZ_ASSERT(mUnconnectedCount > 0);
  --mUnconnectedCount;
}

void ConnectionAttemptPool::CloseAllConnectionAttempts() {
  for (const auto& sock : mAttempts) {
    sock->Abandon();
    gHttpHandler->ConnMgr()->DecreaseNumDnsAndConnectSockets();
  }

  mAttempts.Clear();
  mUnconnectedCount = 0;

  RefPtr<ConnectionEntry> entry(mEntry);
  if (entry) {
    gHttpHandler->ConnMgr()->ProcessPendingQForEntry(entry);
  }
}

bool ConnectionAttemptPool::FindConnToClaim(
    PendingTransactionInfo* pendingTransInfo) {
  nsHttpTransaction* trans = pendingTransInfo->Transaction();
  for (const auto& sock : mAttempts) {
    if (sock->AcceptsTransaction(trans) && sock->Claim(trans)) {
      pendingTransInfo->RememberConnectionAttempt(sock);
      // We've found a speculative connection or a connection that
      // is free to be used in the mAttempts list.
      // A free to be used connection is a connection that was
      // open for a concrete transaction, but that trunsaction
      // ended up using another connection.
      LOG(
          ("ConnectionAttemptPool::FindConnToClaim [ci = %s]\n"
           "Found a speculative or a free-to-use ConnectionAttempt\n",
           trans->ConnectionInfo()->HashKey().get()));

      // return OK because we have essentially opened a new connection
      // by converting a speculative connection to general use
      return true;
    }
  }
  return false;
}

void ConnectionAttemptPool::TimeoutTick() {
  if (mAttempts.IsEmpty()) {
    return;
  }

  TimeStamp currentTime = TimeStamp::Now();
  double maxConnectTime_ms = gHttpHandler->ConnectTimeout();

  // Iterate a snapshot: OnTimeout / RemoveConnectionAttempt below can
  // mutate mAttempts (directly or via reentrant Close paths), which
  // would invalidate iterators over the live array.
  nsTArray<RefPtr<ConnectionAttempt>> snapshot(mAttempts.Clone());
  for (const auto& sock : snapshot) {
    double delta = sock->Duration(currentTime);
    // If the socket has timed out, close it so the waiting
    // transaction will get the proper signal.
    if (delta > maxConnectTime_ms) {
      LOG(("Force timeout of ConnectionAttempt to %p after %.2fms.\n",
           sock.get(), delta));
      sock->OnTimeout();
    }

    // If this ConnectionAttempt hangs around for 5 seconds after we've
    // closed() it then just abandon the socket.
    if (delta > maxConnectTime_ms + 5000) {
      LOG(("Abandon ConnectionAttempt to %p after %.2fms.\n", sock.get(),
           delta));
      RemoveConnectionAttempt(sock, true);
    }
  }
}

void ConnectionAttemptPool::PrintDiagnostics(nsCString& log) {
  if (mAttempts.IsEmpty()) {
    return;
  }

  uint32_t count = 0;
  for (const auto& sock : Reversed(mAttempts)) {
    log.AppendPrintf("   :: Half Open #%u\n", count);
    sock->PrintDiagnostics(log);
    count++;
  }
}

void ConnectionAttemptPool::GetConnectionData(HttpRetParams& data) {
  for (uint32_t i = 0; i < mAttempts.Length(); i++) {
    DnsAndConnectSockets dnsAndSock{};
    dnsAndSock.speculative = mAttempts[i]->IsSpeculative();
    data.dnsAndSocks.AppendElement(dnsAndSock);
  }
}

uint32_t ConnectionAttemptPool::UnconnectedUDPConnsLength() const {
  uint32_t len = 0;
  for (const auto& sock : mAttempts) {
    len += sock->UnconnectedUDPConnsLength();
  }
  return len;
}

}  // namespace mozilla::net
