/*
 * Copyright 2009-present MongoDB, Inc.
 *
 * Licensed 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 <mongoc/mongoc-stream.h>

#include <mongoc/mongoc-array-private.h>
#include <mongoc/mongoc-buffer-private.h>
#include <mongoc/mongoc-errno-private.h>
#include <mongoc/mongoc-error-private.h>
#include <mongoc/mongoc-rpc-private.h>
#include <mongoc/mongoc-stream-private.h>
#include <mongoc/mongoc-trace-private.h>
#include <mongoc/mongoc-util-private.h>

#include <mongoc/mongoc-flags.h>
#include <mongoc/mongoc-log.h>
#include <mongoc/mongoc-opcode.h>

#include <bson/bson.h>

#include <mlib/cmp.h>
#include <mlib/duration.h>
#include <mlib/timer.h>


#undef MONGOC_LOG_DOMAIN
#define MONGOC_LOG_DOMAIN "stream"

#ifndef MONGOC_DEFAULT_TIMEOUT_MSEC
#define MONGOC_DEFAULT_TIMEOUT_MSEC (60L * 60L * 1000L)
#endif


/**
 * mongoc_stream_close:
 * @stream: A mongoc_stream_t.
 *
 * Closes the underlying file-descriptor used by @stream.
 *
 * Returns: 0 on success, -1 on failure.
 */
int
mongoc_stream_close(mongoc_stream_t *stream)
{
   int ret;

   ENTRY;

   BSON_ASSERT_PARAM(stream);

   BSON_ASSERT(stream->close);

   ret = stream->close(stream);

   RETURN(ret);
}


/**
 * mongoc_stream_failed:
 * @stream: A mongoc_stream_t.
 *
 * Frees any resources referenced by @stream, including the memory allocation
 * for @stream.
 * This handler is called upon stream failure, such as network errors, invalid
 * replies
 * or replicaset reconfigures deleting the stream
 */
void
mongoc_stream_failed(mongoc_stream_t *stream)
{
   ENTRY;

   BSON_ASSERT_PARAM(stream);

   if (stream->failed) {
      stream->failed(stream);
   } else {
      stream->destroy(stream);
   }

   EXIT;
}


/**
 * mongoc_stream_destroy:
 * @stream: A mongoc_stream_t.
 *
 * Frees any resources referenced by @stream, including the memory allocation
 * for @stream.
 */
void
mongoc_stream_destroy(mongoc_stream_t *stream)
{
   ENTRY;

   if (!stream) {
      EXIT;
   }

   BSON_ASSERT(stream->destroy);

   stream->destroy(stream);

   EXIT;
}


/**
 * mongoc_stream_flush:
 * @stream: A mongoc_stream_t.
 *
 * Flushes the data in the underlying stream to the transport.
 *
 * Returns: 0 on success, -1 on failure.
 */
int
mongoc_stream_flush(mongoc_stream_t *stream)
{
   BSON_ASSERT_PARAM(stream);
   return stream->flush(stream);
}


/**
 * mongoc_stream_writev:
 * @stream: A mongoc_stream_t.
 * @iov: An array of iovec to write to the stream.
 * @iovcnt: The number of elements in @iov.
 *
 * Writes an array of iovec buffers to the underlying stream.
 *
 * Returns: the number of bytes written, or -1 upon failure.
 */
ssize_t
mongoc_stream_writev(mongoc_stream_t *stream, mongoc_iovec_t *iov, size_t iovcnt, int32_t timeout_msec)
{
   ssize_t ret;

   ENTRY;

   BSON_ASSERT_PARAM(stream);
   BSON_ASSERT_PARAM(iov);
   BSON_ASSERT(iovcnt);

   BSON_ASSERT(stream->writev);

   // CDRIVER-4781: for backward compatibility.
   if (timeout_msec < 0) {
      timeout_msec = MONGOC_DEFAULT_TIMEOUT_MSEC;
   }

   DUMP_IOVEC(writev, iov, iovcnt);
   ret = stream->writev(stream, iov, iovcnt, timeout_msec);

   RETURN(ret);
}

/**
 * mongoc_stream_write:
 * @stream: A mongoc_stream_t.
 * @buf: A buffer to write.
 * @count: The number of bytes to write into @buf.
 *
 * Simplified access to mongoc_stream_writev(). Creates a single iovec
 * with the buffer provided.
 *
 * Returns: -1 on failure, otherwise the number of bytes written.
 */
ssize_t
mongoc_stream_write(mongoc_stream_t *stream, void *buf, size_t count, int32_t timeout_msec)
{
   mongoc_iovec_t iov;
   ssize_t ret;

   ENTRY;

   BSON_ASSERT_PARAM(stream);
   BSON_ASSERT_PARAM(buf);

   iov.iov_base = buf;
   iov.iov_len = count;

   BSON_ASSERT(stream->writev);

   ret = mongoc_stream_writev(stream, &iov, 1, timeout_msec);

   RETURN(ret);
}

/**
 * mongoc_stream_readv:
 * @stream: A mongoc_stream_t.
 * @iov: An array of iovec containing the location and sizes to read.
 * @iovcnt: the number of elements in @iov.
 * @min_bytes: the minimum number of bytes to return, or -1.
 *
 * Reads into the various buffers pointed to by @iov and associated
 * buffer lengths.
 *
 * If @min_bytes is specified, then at least min_bytes will be returned unless
 * eof is encountered.  This may result in ETIMEDOUT
 *
 * Returns: the number of bytes read or -1 on failure.
 */
ssize_t
mongoc_stream_readv(mongoc_stream_t *stream, mongoc_iovec_t *iov, size_t iovcnt, size_t min_bytes, int32_t timeout_msec)
{
   ssize_t ret;

   ENTRY;

   BSON_ASSERT_PARAM(stream);
   BSON_ASSERT_PARAM(iov);
   BSON_ASSERT(iovcnt);

   BSON_ASSERT(stream->readv);

   ret = stream->readv(stream, iov, iovcnt, min_bytes, timeout_msec);
   if (ret >= 0) {
      DUMP_IOVEC(readv, iov, iovcnt);
   }

   RETURN(ret);
}


/**
 * mongoc_stream_read:
 * @stream: A mongoc_stream_t.
 * @buf: A buffer to write into.
 * @count: The number of bytes to write into @buf.
 * @min_bytes: The minimum number of bytes to receive
 *
 * Simplified access to mongoc_stream_readv(). Creates a single iovec
 * with the buffer provided.
 *
 * If @min_bytes is specified, then at least min_bytes will be returned unless
 * eof is encountered.  This may result in ETIMEDOUT
 *
 * Returns: -1 on failure, otherwise the number of bytes read.
 */
ssize_t
mongoc_stream_read(mongoc_stream_t *stream, void *buf, size_t count, size_t min_bytes, int32_t timeout_msec)
{
   mongoc_iovec_t iov;
   ssize_t ret;

   ENTRY;

   BSON_ASSERT_PARAM(stream);
   BSON_ASSERT_PARAM(buf);

   iov.iov_base = buf;
   iov.iov_len = count;

   BSON_ASSERT(stream->readv);

   ret = mongoc_stream_readv(stream, &iov, 1, min_bytes, timeout_msec);

   RETURN(ret);
}


int
mongoc_stream_setsockopt(mongoc_stream_t *stream, int level, int optname, void *optval, mongoc_socklen_t optlen)
{
   BSON_ASSERT_PARAM(stream);

   if (stream->setsockopt) {
      return stream->setsockopt(stream, level, optname, optval, optlen);
   }

   return 0;
}


mongoc_stream_t *
mongoc_stream_get_base_stream(mongoc_stream_t *stream) /* IN */
{
   BSON_ASSERT_PARAM(stream);

   if (stream->get_base_stream) {
      return stream->get_base_stream(stream);
   }

   return stream;
}


mongoc_stream_t *
mongoc_stream_get_root_stream(mongoc_stream_t *stream)
{
   BSON_ASSERT_PARAM(stream);

   while (stream->get_base_stream) {
      stream = stream->get_base_stream(stream);
   }

   return stream;
}

mongoc_stream_t *
mongoc_stream_get_tls_stream(mongoc_stream_t *stream) /* IN */
{
   BSON_ASSERT_PARAM(stream);

   for (; stream && stream->type != MONGOC_STREAM_TLS; stream = stream->get_base_stream(stream))
      ;

   return stream;
}

ssize_t
mongoc_stream_poll(mongoc_stream_poll_t *streams, size_t nstreams, int32_t timeout_ms)
{
   return _mongoc_stream_poll_internal(streams, nstreams, mlib_expires_after(mlib_duration(timeout_ms, ms)));
}

ssize_t
_mongoc_stream_poll_internal(mongoc_stream_poll_t *streams, size_t nstreams, mlib_timer until)
{
   mongoc_stream_poll_t *poller = BSON_ARRAY_ALLOC(nstreams, mongoc_stream_poll_t);

   int last_type = 0;
   ssize_t rval = -1;

   errno = 0;

   for (size_t i = 0u; i < nstreams; i++) {
      poller[i].stream = mongoc_stream_get_root_stream(streams[i].stream);
      poller[i].events = streams[i].events;
      poller[i].revents = 0;

      if (i == 0u) {
         last_type = poller[i].stream->type;
      } else if (last_type != poller[i].stream->type) {
         errno = EINVAL;
         goto CLEANUP;
      }
   }

   if (!poller[0].stream->poll) {
      errno = EINVAL;
      goto CLEANUP;
   }

   int32_t time_remain_ms = 0;
   if (mlib_narrow(&time_remain_ms, mlib_milliseconds_count(mlib_timer_remaining(until)))) {
      // Too many ms, just use the max
      time_remain_ms = INT32_MAX;
   }
   rval = poller[0].stream->poll(poller, nstreams, time_remain_ms);

   if (rval > 0) {
      for (size_t i = 0u; i < nstreams; i++) {
         streams[i].revents = poller[i].revents;
      }
   }

CLEANUP:
   bson_free(poller);

   return rval;
}

bool
mongoc_stream_check_closed(mongoc_stream_t *stream)
{
   int ret;

   ENTRY;

   if (!stream) {
      return true;
   }

   ret = stream->check_closed(stream);

   RETURN(ret);
}

bool
mongoc_stream_timed_out(mongoc_stream_t *stream)
{
   ENTRY;

   BSON_ASSERT_PARAM(stream);

   /* for e.g. a file stream there is no timed_out function */
   RETURN(stream->timed_out && stream->timed_out(stream));
}

bool
mongoc_stream_should_retry(mongoc_stream_t *stream)
{
   ENTRY;

   BSON_ASSERT_PARAM(stream);

   /* for e.g. a file stream there is no should_retry function */
   RETURN(stream->should_retry && stream->should_retry(stream));
}

bool
_mongoc_stream_writev_full(
   mongoc_stream_t *stream, mongoc_iovec_t *iov, size_t iovcnt, int64_t timeout_msec, bson_error_t *error)
{
   size_t total_bytes = 0;
   ssize_t r;
   ENTRY;

   for (size_t i = 0u; i < iovcnt; i++) {
      total_bytes += iov[i].iov_len;
   }

   if (BSON_UNLIKELY(!mlib_in_range(int32_t, timeout_msec))) {
      // CDRIVER-4589
      _mongoc_set_error(error,
                        MONGOC_ERROR_STREAM,
                        MONGOC_ERROR_STREAM_SOCKET,
                        "timeout_msec value %" PRId64 " exceeds supported 32-bit range",
                        timeout_msec);
      RETURN(false);
   }

   r = mongoc_stream_writev(stream, iov, iovcnt, (int32_t)timeout_msec);
   TRACE("writev returned: %zd", r);

   if (r < 0) {
      if (error) {
         char buf[128];
         char *errstr;

         errstr = bson_strerror_r(errno, buf, sizeof(buf));

         _mongoc_set_error(error,
                           MONGOC_ERROR_STREAM,
                           MONGOC_ERROR_STREAM_SOCKET,
                           "Failure during socket delivery: %s (%d)",
                           errstr,
                           errno);
      }

      RETURN(false);
   }

   if (mlib_cmp(r, !=, total_bytes)) {
      _mongoc_set_error(error,
                        MONGOC_ERROR_STREAM,
                        MONGOC_ERROR_STREAM_SOCKET,
                        "Failure to send all requested bytes (only sent: %" PRIu64 "/%zu in %" PRId64
                        "ms) during socket delivery",
                        (uint64_t)r,
                        total_bytes,
                        timeout_msec);

      RETURN(false);
   }

   RETURN(true);
}
