/*
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * 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 <folly/io/async/AsyncPipe.h>

#include <folly/FileUtil.h>
#include <folly/Utility.h>
#include <folly/detail/FileUtilDetail.h>
#include <folly/io/async/AsyncSocketException.h>

using std::string;
using std::unique_ptr;

namespace folly {

AsyncPipeReader::~AsyncPipeReader() {
  close();
}

void AsyncPipeReader::failRead(const AsyncSocketException& ex) {
  VLOG(5) << "AsyncPipeReader(this=" << this << ", fd=" << fd_
          << "): failed while reading: " << ex.what();

  DCHECK(readCallback_ != nullptr);
  AsyncReader::ReadCallback* callback = readCallback_;
  readCallback_ = nullptr;
  callback->readErr(ex);
  close();
}

void AsyncPipeReader::close() {
  unregisterHandler();
  if (fd_ != NetworkSocket()) {
    changeHandlerFD(NetworkSocket());

    if (closeCb_) {
      closeCb_(fd_);
    } else {
      netops::close(fd_);
    }
    fd_ = NetworkSocket();
  }
}

#ifdef _WIN32
static int recv_internal(NetworkSocket s, void* buf, size_t count) {
  auto r = netops::recv(s, buf, count, 0);
  if (r == -1 && WSAGetLastError() == WSAEWOULDBLOCK) {
    errno = EAGAIN;
  }
  return folly::to_narrow(r);
}
#endif

void AsyncPipeReader::handlerReady(uint16_t events) noexcept {
  DestructorGuard dg(this);
  CHECK(events & EventHandler::READ);

  VLOG(5) << "AsyncPipeReader::handlerReady() this=" << this << ", fd=" << fd_;
  assert(readCallback_ != nullptr);

  while (readCallback_) {
    // - What API does callback support?
    const auto movable = readCallback_->isBufferMovable(); // noexcept

    // Get the buffer to read into.
    void* buf = nullptr;
    size_t buflen = 0;
    std::unique_ptr<IOBuf> ioBuf;

    if (movable) {
      ioBuf = IOBuf::create(readCallback_->maxBufferSize());
      buf = ioBuf->writableBuffer();
      buflen = ioBuf->capacity();
    } else {
      try {
        readCallback_->getReadBuffer(&buf, &buflen);
      } catch (const std::exception& ex) {
        AsyncSocketException aex(
            AsyncSocketException::BAD_ARGS,
            string("ReadCallback::getReadBuffer() "
                   "threw exception: ") +
                ex.what());
        failRead(aex);
        return;
      } catch (...) {
        AsyncSocketException aex(
            AsyncSocketException::BAD_ARGS,
            string("ReadCallback::getReadBuffer() "
                   "threw non-exception type"));
        failRead(aex);
        return;
      }
      if (buf == nullptr || buflen == 0) {
        AsyncSocketException aex(
            AsyncSocketException::INVALID_STATE,
            string("ReadCallback::getReadBuffer() "
                   "returned empty buffer"));
        failRead(aex);
        return;
      }
    }

    // Perform the read
#ifdef _WIN32
    // On Windows you can't call read on a socket, so call recv instead.
    ssize_t bytesRead =
        folly::fileutil_detail::wrapNoInt(recv_internal, fd_, buf, buflen);
#else
    ssize_t bytesRead = folly::readNoInt(fd_.toFd(), buf, buflen);
#endif

    if (bytesRead > 0) {
      if (movable) {
        ioBuf->append(std::size_t(bytesRead));
        readCallback_->readBufferAvailable(std::move(ioBuf));
      } else {
        readCallback_->readDataAvailable(size_t(bytesRead));
      }
      // Fall through and continue around the loop if the read
      // completely filled the available buffer.
      // Note that readCallback_ may have been uninstalled or changed inside
      // readDataAvailable().
      if (static_cast<size_t>(bytesRead) < buflen) {
        return;
      }
    } else if (bytesRead < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
      // No more data to read right now.
      return;
    } else if (bytesRead < 0) {
      AsyncSocketException ex(
          AsyncSocketException::INVALID_STATE, "read failed", errno);
      failRead(ex);
      return;
    } else {
      assert(bytesRead == 0);
      // EOF

      unregisterHandler();
      AsyncReader::ReadCallback* callback = readCallback_;
      readCallback_ = nullptr;
      callback->readEOF();
      return;
    }
    // Max reads per loop?
  }
}

void AsyncPipeWriter::write(
    unique_ptr<folly::IOBuf> buf, AsyncWriter::WriteCallback* callback) {
  if (closed()) {
    if (callback) {
      AsyncSocketException ex(
          AsyncSocketException::NOT_OPEN, "attempt to write to closed pipe");
      callback->writeErr(0, ex);
    }
    return;
  }
  bool wasEmpty = (queue_.empty());
  folly::IOBufQueue iobq;
  iobq.append(std::move(buf));
  std::pair<folly::IOBufQueue, AsyncWriter::WriteCallback*> p(
      std::move(iobq), callback);
  queue_.emplace_back(std::move(p));
  if (wasEmpty) {
    handleWrite();
  } else {
    CHECK(!queue_.empty());
    CHECK(isHandlerRegistered());
  }
}

void AsyncPipeWriter::writeChain(
    folly::AsyncWriter::WriteCallback* callback,
    std::unique_ptr<folly::IOBuf>&& buf,
    WriteFlags) {
  write(std::move(buf), callback);
}

void AsyncPipeWriter::closeOnEmpty() {
  VLOG(5) << "close on empty";
  if (queue_.empty()) {
    closeNow();
  } else {
    closeOnEmpty_ = true;
    CHECK(isHandlerRegistered());
  }
}

void AsyncPipeWriter::closeNow() {
  VLOG(5) << "close now";
  if (!queue_.empty()) {
    failAllWrites(AsyncSocketException(
        AsyncSocketException::NOT_OPEN, "closed with pending writes"));
  }
  if (fd_ != NetworkSocket()) {
    unregisterHandler();
    changeHandlerFD(NetworkSocket());
    if (closeCb_) {
      closeCb_(fd_);
    } else {
      netops::close(fd_);
    }
    fd_ = NetworkSocket();
  }
}

void AsyncPipeWriter::failAllWrites(const AsyncSocketException& ex) {
  DestructorGuard dg(this);
  while (!queue_.empty()) {
    // the first entry of the queue could have had a partial write, but needs to
    // be tracked.
    if (queue_.front().second) {
      queue_.front().second->writeErr(0, ex);
    }
    queue_.pop_front();
  }
}

void AsyncPipeWriter::handlerReady(uint16_t events) noexcept {
  CHECK(events & EventHandler::WRITE);

  handleWrite();
}

#ifdef _WIN32
static int send_internal(NetworkSocket s, const void* buf, size_t count) {
  auto r = netops::send(s, buf, count, 0);
  if (r == -1 && WSAGetLastError() == WSAEWOULDBLOCK) {
    errno = EAGAIN;
  }
  return folly::to_narrow(r);
}
#endif

void AsyncPipeWriter::handleWrite() {
  DestructorGuard dg(this);
  assert(!queue_.empty());
  do {
    auto& front = queue_.front();
    folly::IOBufQueue& curQueue = front.first;
    DCHECK(!curQueue.empty());
    // someday, support writev.  The logic for partial writes is a bit complex
    const IOBuf* head = curQueue.front();
    CHECK(head->length());
#ifdef _WIN32
    // On Windows you can't call write on a socket.
    ssize_t rc = folly::fileutil_detail::wrapNoInt(
        send_internal, fd_, head->data(), head->length());
#else
    ssize_t rc = folly::writeNoInt(fd_.toFd(), head->data(), head->length());
#endif
    if (rc < 0) {
      if (errno == EAGAIN || errno == EWOULDBLOCK) {
        // pipe is full
        VLOG(5) << "write blocked";
        registerHandler(EventHandler::WRITE);
        return;
      } else {
        failAllWrites(AsyncSocketException(
            AsyncSocketException::INTERNAL_ERROR, "write failed", errno));
        closeNow();
        return;
      }
    } else if (rc == 0) {
      registerHandler(EventHandler::WRITE);
      return;
    }
    curQueue.trimStart(size_t(rc));
    if (curQueue.empty()) {
      auto cb = front.second;
      queue_.pop_front();
      if (cb) {
        cb->writeSuccess();
      }
    } else {
      VLOG(5) << "partial write blocked";
    }
  } while (!queue_.empty());

  if (closeOnEmpty_) {
    closeNow();
  } else {
    unregisterHandler();
  }
}

} // namespace folly
