/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
   file Copyright.txt or https://cmake.org/licensing for details.  */
#pragma once

#include <algorithm>
#include <cstring>
#include <streambuf>
#include <vector>

#include <cm3p/uv.h>

#include "cmUVHandlePtr.h"

/*
 * This file is based on example code from:
 *
 * https://web.archive.org/web/20170515211805/
 *     http://www.mr-edd.co.uk/blog/beginners_guide_streambuf
 *
 * The example code was distributed under the following license:
 *
 * Copyright 2007 Edd Dawson.
 * Distributed under the Boost Software License, Version 1.0.
 *
 * Boost Software License - Version 1.0 - August 17th, 2003
 *
 * Permission is hereby granted, free of charge, to any person or organization
 * obtaining a copy of the software and accompanying documentation covered by
 * this license (the "Software") to use, reproduce, display, distribute,
 * execute, and transmit the Software, and to prepare derivative works of the
 * Software, and to permit third-parties to whom the Software is furnished to
 * do so, all subject to the following:
 *
 * The copyright notices in the Software and this entire statement, including
 * the above license grant, this restriction and the following disclaimer,
 * must be included in all copies of the Software, in whole or in part, and
 * all derivative works of the Software, unless such copies or derivative
 * works are solely in the form of machine-executable object code generated by
 * a source language processor.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
 * SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
 * FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 */

template <typename CharT, typename Traits = std::char_traits<CharT>>
class cmBasicUVStreambuf : public std::basic_streambuf<CharT, Traits>
{
public:
  cmBasicUVStreambuf(std::size_t bufSize = 256, std::size_t putBack = 8);
  ~cmBasicUVStreambuf() override;

  bool is_open() const;

  cmBasicUVStreambuf* open(uv_stream_t* stream);

  cmBasicUVStreambuf* close();

protected:
  typename cmBasicUVStreambuf<CharT, Traits>::int_type underflow() override;
  std::streamsize showmanyc() override;

  // FIXME: Add write support

private:
  uv_stream_t* Stream = nullptr;
  void* OldStreamData = nullptr;
  const std::size_t PutBack = 0;
  std::vector<CharT> InputBuffer;
  bool EndOfFile = false;

  void StreamReadStartStop();

  void StreamRead(ssize_t nread);
  void HandleAlloc(uv_buf_t* buf);
};

template <typename CharT, typename Traits>
cmBasicUVStreambuf<CharT, Traits>::cmBasicUVStreambuf(std::size_t bufSize,
                                                      std::size_t putBack)
  : PutBack(std::max<std::size_t>(putBack, 1))
  , InputBuffer(std::max<std::size_t>(this->PutBack, bufSize) + this->PutBack)
{
  this->close();
}

template <typename CharT, typename Traits>
cmBasicUVStreambuf<CharT, Traits>::~cmBasicUVStreambuf()
{
  this->close();
}

template <typename CharT, typename Traits>
bool cmBasicUVStreambuf<CharT, Traits>::is_open() const
{
  return this->Stream != nullptr;
}

template <typename CharT, typename Traits>
cmBasicUVStreambuf<CharT, Traits>* cmBasicUVStreambuf<CharT, Traits>::open(
  uv_stream_t* stream)
{
  this->close();
  this->Stream = stream;
  this->EndOfFile = false;
  if (this->Stream) {
    this->OldStreamData = this->Stream->data;
    this->Stream->data = this;
  }
  this->StreamReadStartStop();
  return this;
}

template <typename CharT, typename Traits>
cmBasicUVStreambuf<CharT, Traits>* cmBasicUVStreambuf<CharT, Traits>::close()
{
  if (this->Stream) {
    uv_read_stop(this->Stream);
    this->Stream->data = this->OldStreamData;
  }
  this->Stream = nullptr;
  CharT* readEnd = this->InputBuffer.data() + this->InputBuffer.size();
  this->setg(readEnd, readEnd, readEnd);
  return this;
}

template <typename CharT, typename Traits>
typename cmBasicUVStreambuf<CharT, Traits>::int_type
cmBasicUVStreambuf<CharT, Traits>::underflow()
{
  if (!this->is_open()) {
    return Traits::eof();
  }

  if (this->gptr() < this->egptr()) {
    return Traits::to_int_type(*this->gptr());
  }

  this->StreamReadStartStop();
  while (this->in_avail() == 0) {
    uv_run(this->Stream->loop, UV_RUN_ONCE);
  }
  if (this->in_avail() == -1) {
    return Traits::eof();
  }
  return Traits::to_int_type(*this->gptr());
}

template <typename CharT, typename Traits>
std::streamsize cmBasicUVStreambuf<CharT, Traits>::showmanyc()
{
  if (!this->is_open() || this->EndOfFile) {
    return -1;
  }
  return 0;
}

template <typename CharT, typename Traits>
void cmBasicUVStreambuf<CharT, Traits>::StreamReadStartStop()
{
  if (this->Stream) {
    uv_read_stop(this->Stream);
    if (this->gptr() >= this->egptr()) {
      uv_read_start(
        this->Stream,
        [](uv_handle_t* handle, size_t /* unused */, uv_buf_t* buf) {
          auto streambuf =
            static_cast<cmBasicUVStreambuf<CharT, Traits>*>(handle->data);
          streambuf->HandleAlloc(buf);
        },
        [](uv_stream_t* stream2, ssize_t nread, const uv_buf_t* /* unused */) {
          auto streambuf =
            static_cast<cmBasicUVStreambuf<CharT, Traits>*>(stream2->data);
          streambuf->StreamRead(nread);
        });
    }
  }
}

template <typename CharT, typename Traits>
void cmBasicUVStreambuf<CharT, Traits>::HandleAlloc(uv_buf_t* buf)
{
  auto size = this->egptr() - this->gptr();
  std::memmove(this->InputBuffer.data(), this->gptr(),
               this->egptr() - this->gptr());
  this->setg(this->InputBuffer.data(), this->InputBuffer.data(),
             this->InputBuffer.data() + size);
  buf->base = this->egptr();
#ifdef _WIN32
#  define BUF_LEN_TYPE ULONG
#else
#  define BUF_LEN_TYPE size_t
#endif
  buf->len = BUF_LEN_TYPE(
    (this->InputBuffer.data() + this->InputBuffer.size() - this->egptr()) *
    sizeof(CharT));
#undef BUF_LEN_TYPE
}

template <typename CharT, typename Traits>
void cmBasicUVStreambuf<CharT, Traits>::StreamRead(ssize_t nread)
{
  if (nread > 0) {
    this->setg(this->eback(), this->gptr(),
               this->egptr() + nread / sizeof(CharT));
    uv_read_stop(this->Stream);
  } else if (nread < 0 /*|| nread == UV_EOF*/) {
    this->EndOfFile = true;
    uv_read_stop(this->Stream);
  }
}

using cmUVStreambuf = cmBasicUVStreambuf<char>;
