// Copyright (c) 2025
// Utrecht University (The Netherlands),
// ETH Zurich (Switzerland),
// INRIA Sophia-Antipolis (France),
// Max-Planck-Institute Saarbruecken (Germany),
// and Tel-Aviv University (Israel).  All rights reserved.
//
// This file is part of CGAL (www.cgal.org)
//
// $URL: https://github.com/CGAL/cgal/blob/v6.2/Stream_support/include/CGAL/IO/Indenting_ostream.h $
// $Id: include/CGAL/IO/Indenting_ostream.h cac3e9d75e2 $
// SPDX-License-Identifier: LGPL-3.0-or-later OR LicenseRef-Commercial
//
//
// Author(s)     : Laurent Rineau
//
// Documentation partially generated by Github Copilot

#ifndef CGAL_IO_INDENTING_OSTREAM_H
#define CGAL_IO_INDENTING_OSTREAM_H

#include <CGAL/config.h>

#include <ios>
#include <streambuf>
#include <string>
#include <tuple>

namespace CGAL {
namespace IO {

/**
 * \ingroup PkgStreamSupportRef
 *
 * The class template `Basic_indenting_streambuf` wraps another `basic_streambuf`
 * and automatically adds indentation at the beginning of each line. This is
 * useful for formatting debug output with consistent indentation levels.
 *
 * \tparam CharT Character type (typically `char` or `wchar_t`)
 * \tparam Traits Character traits type
 *
 * \sa Basic_indenting_stream_guard
 * \sa make_indenting_guards
 */
template <typename CharT, typename Traits = std::char_traits<CharT>>
class Basic_indenting_streambuf : public std::basic_streambuf<CharT, Traits>, protected Traits
{
public:
  using char_type = CharT;                                           ///< Character type
  using traits_type = Traits;                                        ///< Character traits type
  using int_type = typename traits_type::int_type;                   ///< Integer type for character representation
  using pos_type = typename traits_type::pos_type;                   ///< Position type for stream positioning
  using off_type = typename traits_type::off_type;                   ///< Offset type for stream positioning
  using streambuf_type = std::basic_streambuf<char_type, traits_type>; ///< Type of the wrapped streambuf
  using string = std::basic_string<char_type>;                       ///< String type matching character type

private:
  streambuf_type* wrapped_buf_;
  string indent_string_;
  bool at_line_start_;

public:
  /**
   * \brief constructs an indenting streambuf wrapper.
   *
   * \param wrapped_buf The underlying streambuf to wrap
   * \param indent_string The string to use for indentation (default: 2 spaces)
   */
  explicit
  Basic_indenting_streambuf(streambuf_type& wrapped_buf,
                            const string& indent_string = {2, char_type(' ')})
      : wrapped_buf_(&wrapped_buf)
      , indent_string_(indent_string)
      , at_line_start_(true)
  {
  }

  /** \brief gets the current indentation string. */
  const string& indent_string() const { return indent_string_; }

  /**
   * \brief sets a new indentation string.
   *
   * \param new_indent The new indentation string
   */
  void set_indent_string(const string& new_indent) { indent_string_ = new_indent; }

  /**
   * \brief sets indentation level using repeated spaces.
   *
   * \param level Number of indentation levels
   * \param spaces_per_level Number of spaces per level (default: 2)
   */
  void set_indent_level(int level, int spaces_per_level = 2) {
    indent_string_ = string(level * spaces_per_level, char_type(' '));
  }

  /** \brief gets the wrapped streambuf. */
  streambuf_type& wrapped_streambuf() const { return *wrapped_buf_; }

protected:
  using traits_type::eof;
  using traits_type::not_eof;
  using traits_type::to_char_type;
  using traits_type::to_int_type;
  using traits_type::eq_int_type;

  int_type overflow(int_type ch = eof()) override {
    if(eq_int_type(ch, eof())) {
      return wrapped_buf_->pubsync() == 0 ? not_eof(ch) : eof();
    }

    // If we're at the start of a line, output the indentation first
    if(at_line_start_ && !indent_string_.empty()) {
      for(char_type indent_char : indent_string_) {
        if(eq_int_type(wrapped_buf_->sputc(indent_char), eof())) {
          return eof();
        }
      }
      at_line_start_ = false;
    }

    // Output the actual character
    int_type result = wrapped_buf_->sputc(to_char_type(ch));

    // Check if this character is a newline
    if(!eq_int_type(result, eof()) && to_char_type(ch) == char_type('\n')) {
      at_line_start_ = true;
    }

    return result;
  }

  int sync() override { return wrapped_buf_->pubsync(); }

  std::streamsize xsputn(const char_type* s, std::streamsize count) override {
    std::streamsize written = 0;

    for(std::streamsize i = 0; i < count; ++i) {
      if(eq_int_type(overflow(to_int_type(s[i])), eof())) {
        break;
      }
      ++written;
    }

    return written;
  }

  pos_type seekoff(off_type off,
                   std::ios_base::seekdir dir,
                   std::ios_base::openmode which = std::ios_base::in | std::ios_base::out) override {
    return wrapped_buf_->pubseekoff(off, dir, which);
  }

  pos_type seekpos(pos_type pos, std::ios_base::openmode which = std::ios_base::in | std::ios_base::out) override {
    return wrapped_buf_->pubseekpos(pos, which);
  }

  std::streamsize showmanyc() override {
    return wrapped_buf_->in_avail();
  }

  std::streamsize xsgetn(char_type* s, std::streamsize count) override {
    return wrapped_buf_->sgetn(s, count);
  }

  int_type underflow() override {
    return wrapped_buf_->sgetc();
  }

  int_type uflow() override {
    return wrapped_buf_->sbumpc();
  }

  int_type pbackfail(int_type c = eof()) override {
    return wrapped_buf_->sputbackc(to_char_type(c));
  }

  void imbue(const std::locale& loc) override {
    wrapped_buf_->pubimbue(loc);
  }

  streambuf_type* setbuf(char_type* s, std::streamsize n) override {
    return wrapped_buf_->pubsetbuf(s, n);
  }
};

/// \ingroup PkgStreamSupportRef
/// Type alias for `Basic_indenting_streambuf<char>`
using Indenting_streambuf = Basic_indenting_streambuf<char>;

/// \ingroup PkgStreamSupportRef
/// Type alias for `Basic_indenting_streambuf<wchar_t>`
using Indenting_wstreambuf = Basic_indenting_streambuf<wchar_t>;

/**
 * \ingroup PkgStreamSupportRef
 *
 * RAII helper class for temporarily installing an indenting streambuf on a stream.
 *
 * This class automatically restores the original streambuf when it goes out of scope.
 * It provides a convenient way to add indentation to output streams within a specific scope.
 *
 * \tparam StreamT The stream type (e.g., `std::ostream`, `std::wostream`)
 *
 * \sa Basic_indenting_streambuf
 * \sa make_indenting_guards
 */
template <typename StreamT>
class Basic_indenting_stream_guard
{
public:
  using char_type = typename StreamT::char_type;                                 ///< Character type
  using traits_type = typename StreamT::traits_type;                             ///< Character traits type
  using streambuf_type = Basic_indenting_streambuf<char_type, traits_type>;      ///< Type of the indenting streambuf
  using wrapped_streambuf_type = std::basic_streambuf<char_type, traits_type>;   ///< Type of the wrapped streambuf
  using string = std::basic_string<char_type>;                                   ///< String type matching character type
private:
  StreamT& stream_;
  wrapped_streambuf_type* original_buf_;
  string original_indent_string_;
  streambuf_type indenting_buf_;

public:
  /**
   * \brief constructs and installs an indenting streambuf on the given stream.
   *
   * \param stream The stream to modify
   * \param indent_string The indentation string to use
   */
  explicit Basic_indenting_stream_guard(StreamT& stream,
                                        const string& indent_string)
      : stream_(stream)
      , original_buf_(stream.rdbuf())
      , indenting_buf_(*original_buf_, indent_string)
  {
    if(auto old_buf = dynamic_cast<streambuf_type*>(original_buf_)) {
      original_indent_string_ = old_buf->indent_string();
      old_buf->set_indent_string(original_indent_string_ + indent_string);
    } else {
      stream.rdbuf(&indenting_buf_);
    }
  }

  /**
   * \brief constructs and installs an indenting streambuf on the given stream
   *
   * \param stream The stream to modify
   * \param spaces_per_level Number of indentation spaces
   */
  Basic_indenting_stream_guard(StreamT& stream, int spaces_per_level = 2)
      : Basic_indenting_stream_guard(stream,
                                     string(spaces_per_level, char_type(' '))) {
  }

  // Non-copyable, movable
  Basic_indenting_stream_guard(const Basic_indenting_stream_guard&) = delete;
  Basic_indenting_stream_guard& operator=(const Basic_indenting_stream_guard&) = delete;

  Basic_indenting_stream_guard(Basic_indenting_stream_guard&& other)
      : stream_(other.stream_)
      , original_buf_(other.original_buf_)
      , original_indent_string_(std::move(other.original_indent_string_))
      , indenting_buf_(std::move(other.indenting_buf_))
  {
    // Update the stream to point to the new buffer location if it was using the old one
    if(stream_.rdbuf() == &other.indenting_buf_) {
      stream_.rdbuf(&indenting_buf_);
    }
    // Mark other as moved-from so it won't restore the stream
    other.original_buf_ = nullptr;
  }

  Basic_indenting_stream_guard& operator=(Basic_indenting_stream_guard&&) = delete;

  /** \brief Destructor - restores the original streambuf. */
  ~Basic_indenting_stream_guard() {
    if(!original_buf_) return; // Moved-from object
    if(auto old_buf = dynamic_cast<streambuf_type*>(original_buf_)) {
      old_buf->set_indent_string(original_indent_string_);
    } else {
      stream_.rdbuf(original_buf_);
    }
  }
};

/// \ingroup PkgStreamSupportRef
/// Type alias for `basic_indenting_stream_guard<std::ostream>`
using Indenting_stream_guard = Basic_indenting_stream_guard<std::ostream>;

/// \ingroup PkgStreamSupportRef
/// Type alias for `basic_indenting_stream_guard<std::wostream>`
using Indenting_wstream_guard = Basic_indenting_stream_guard<std::wostream>;

/**
 * \ingroup PkgStreamSupportRef
 *
 * \brief creates indenting guards for multiple streams simultaneously.
 *
 * This helper function creates `Basic_indenting_stream_guard` objects for
 * multiple streams at once. All streams will use the same indentation settings,
 * and their original streambufs will be automatically restored when the guards
 * go out of scope. The guards are returned in a tuple.
 *
 * \tparam Streams The stream types (deduced from arguments)
 * \param spaces_per_level Number of spaces per indentation level
 * \param streams The streams to apply indentation to
 * \return A tuple of Basic_indenting_stream_guard objects
 *
 * \sa Basic_indenting_stream_guard
 *
 * Example usage:
 * \code
 * auto guards = CGAL::IO::make_indenting_guards(2, std::cout, std::cerr);
 * std::cout << "Indented output\n";
 * std::cerr << "Indented error\n";
 * \endcode
 */
template <typename... Streams>
auto make_indenting_guards(int spaces_per_level, Streams&... streams) {
  return std::make_tuple(Basic_indenting_stream_guard<Streams>(streams, spaces_per_level)...);
}

/**
 * \ingroup PkgStreamSupportRef
 *
 * \brief creates indenting guards for multiple streams with a custom indent string.
 *
 * This overload allows specifying a custom indentation string instead of a number
 * of spaces.
 *
 * \tparam Streams The stream types (deduced from arguments)
 * \param indent_string The indentation string to use
 * \param streams The streams to apply indentation to
 * \return A tuple of Basic_indenting_stream_guard objects
 *
 * \sa Basic_indenting_stream_guard
 */
template <typename... Streams>
auto make_indenting_guards(const std::string& indent_string, Streams&... streams) {
  return std::make_tuple(Basic_indenting_stream_guard<Streams>(streams, indent_string)...);
}

} // namespace IO
} // namespace CGAL

#endif // CGAL_IO_INDENTING_OSTREAM_H