// 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-beta1/Stream_support/include/CGAL/IO/Color_ostream.h $
// $Id: include/CGAL/IO/Color_ostream.h 431cfb5357d $
// SPDX-License-Identifier: LGPL-3.0-or-later OR LicenseRef-Commercial
//
//
// Author(s)     : Laurent Rineau
//
// Documentation partially generated by Github Copilot

#ifndef CGAL_IO_COLOR_OSTREAM_H
#define CGAL_IO_COLOR_OSTREAM_H

#include <CGAL/config.h>

#include <array>
#include <cstdlib> // for std::getenv
#include <ios>
#include <iostream>
#include <locale>
#include <optional>
#include <ostream>
#include <stdio.h> // for stdout, stderr
#include <streambuf>
#include <string>
#include <tuple>
#include <type_traits>
#include <utility>


#ifdef _WIN32
#  include <io.h>
#  define CGAL_ISATTY _isatty
#  define CGAL_FILENO _fileno
#else
#  include <unistd.h>
#  define CGAL_ISATTY isatty
#  define CGAL_FILENO fileno
#endif

#if defined(__STDC_LIB_EXT1__) || defined(_WIN32)
#  include <stdlib.h> // for getenv_s
#  define CGAL_USE_GETENV_S 1
#endif

namespace CGAL {
namespace IO {

/**
 * \ingroup PkgStreamSupportEnumRef
 *
 * ANSI color codes for terminal output.
 */
enum class Ansi_color {
  Reset = 0,        ///< Reset all attributes to default
  Bold = 1,         ///< Bold or increased intensity
  Dim = 2,          ///< Faint or decreased intensity
  Underline = 4,    ///< Underlined text
  Blink = 5,        ///< Blinking text
  Reverse = 7,      ///< Swap foreground and background colors
  Hidden = 8,       ///< Hidden or invisible text

  // Foreground colors
  Black = 30,       ///< Black foreground color
  Red = 31,         ///< Red foreground color
  Green = 32,       ///< Green foreground color
  Yellow = 33,      ///< Yellow foreground color
  Blue = 34,        ///< Blue foreground color
  Magenta = 35,     ///< Magenta foreground color
  Cyan = 36,        ///< Cyan foreground color
  White = 37,       ///< White foreground color

  // Bright foreground colors
  BrightBlack = 90,   ///< Bright black (gray) foreground color
  BrightRed = 91,     ///< Bright red foreground color
  BrightGreen = 92,   ///< Bright green foreground color
  BrightYellow = 93,  ///< Bright yellow foreground color
  BrightBlue = 94,    ///< Bright blue foreground color
  BrightMagenta = 95, ///< Bright magenta foreground color
  BrightCyan = 96,    ///< Bright cyan foreground color
  BrightWhite = 97,   ///< Bright white foreground color

  // Background colors
  BgBlack = 40,     ///< Black background color
  BgRed = 41,       ///< Red background color
  BgGreen = 42,     ///< Green background color
  BgYellow = 43,    ///< Yellow background color
  BgBlue = 44,      ///< Blue background color
  BgMagenta = 45,   ///< Magenta background color
  BgCyan = 46,      ///< Cyan background color
  BgWhite = 47,     ///< White background color

  // Bright background colors
  BgBrightBlack = 100,   ///< Bright black (gray) background color
  BgBrightRed = 101,     ///< Bright red background color
  BgBrightGreen = 102,   ///< Bright green background color
  BgBrightYellow = 103,  ///< Bright yellow background color
  BgBrightBlue = 104,    ///< Bright blue background color
  BgBrightMagenta = 105, ///< Bright magenta background color
  BgBrightCyan = 106,    ///< Bright cyan background color
  BgBrightWhite = 107    ///< Bright white background color
};

/**
 * \ingroup PkgStreamSupportRef
 *
 * The class template `Basic_color_streambuf` wraps another `basic_streambuf`
 * and automatically adds ANSI color codes to the output. This is useful for
 * colorizing terminal output with consistent color schemes.
 *
 * \tparam CharT Character type (typically `char` or `wchar_t`)
 * \tparam Traits Character traits type
 *
 * \sa Basic_color_stream_guard
 * \sa make_color_guards
 */
template <typename CharT, typename Traits = std::char_traits<CharT>>
class Basic_color_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 color_code_;
  bool at_line_start_;
  bool colors_enabled_;

  static std::optional<std::string> safe_getenv(const char* name) {
#if CGAL_USE_GETENV_S
    static constexpr std::size_t buffer_size = 256;
    std::array<char, buffer_size> buffer;
    std::size_t size = 0;
    if(getenv_s(&size, buffer.data(), buffer.size(), name) == 0 && size > 0) {
      return std::string(buffer.data());
    } else {
      return std::nullopt;
    }
#else
    const char* value = std::getenv(name);
    if(value) {
      return std::string(value);
    } else {
      return {};
    }
#endif
  }

  /** \brief Generate ANSI escape sequence for the set of colors. */
  template <std::size_t N>
  static string make_color_code(std::array<Ansi_color, N> colors) {
    string code;
    if(colors.empty()) {
      return code;
    }
    code += char_type('\033');
    code += char_type('[');
    for(std::size_t i = 0; i < N; ++i) {
      if(i > 0) code += char_type(';');
      int color_value = static_cast<int>(colors[i]);
      // Convert integer to string
      if(color_value == 0) {
        code += char_type('0');
      } else {
        string digits;
        while(color_value > 0) {
          digits = char_type('0' + (color_value % 10)) + digits;
          color_value /= 10;
        }
        code += digits;
      }
    }
    code += char_type('m');
    return code;
  }

public:
  /**
   * \brief constructs a color streambuf wrapper with one or more colors.
   *
   * Colors are automatically disabled if the wrapped buffer is not connected
   * to a color-capable terminal (checked via `isatty()` and `TERM` variable),
   * or if the `NO_COLOR` environment variable is set and non-empty.
   *
   * \tparam AnsiColors Variadic list of `Ansi_color` values
   * \param wrapped_buf The underlying streambuf to wrap
   * \param colors One or more colors to combine (e.g., `Ansi_color::Bold`, `Ansi_color::Red`)
   */
  template <typename ...AnsiColors>
  explicit
  Basic_color_streambuf(streambuf_type& wrapped_buf,
                        AnsiColors... colors)
      : wrapped_buf_(&wrapped_buf)
      , color_code_(make_color_code(std::array<Ansi_color, sizeof...(colors)>{colors...}))
      , at_line_start_(true)
      , colors_enabled_(detect_color_support(&wrapped_buf))
  {
  }

  // Non-copyable
  Basic_color_streambuf(const Basic_color_streambuf&) = delete;
  Basic_color_streambuf& operator=(const Basic_color_streambuf&) = delete;

  // Move constructor
  Basic_color_streambuf(Basic_color_streambuf&& other) noexcept
      : wrapped_buf_(std::exchange(other.wrapped_buf_, nullptr))
      , color_code_(std::move(other.color_code_))
      , at_line_start_(std::exchange(other.at_line_start_, true))
      , colors_enabled_(std::exchange(other.colors_enabled_, false))
  {
  }

  // Move assignment
  Basic_color_streambuf& operator=(Basic_color_streambuf&& other) noexcept {
    if(this != &other) {
      // If currently wrapping a buffer, try to sync it first.
      if(wrapped_buf_) {
        wrapped_buf_->pubsync();
      }
      wrapped_buf_ = std::exchange(other.wrapped_buf_, nullptr);
      color_code_ = std::move(other.color_code_);
      at_line_start_ = std::exchange(other.at_line_start_, true);
      colors_enabled_ = std::exchange(other.colors_enabled_, false);
    }
    return *this;
  }

  ~Basic_color_streambuf() noexcept {
    if(wrapped_buf_) {
      if(colors_enabled_ && !color_code_.empty() && !at_line_start_) {
        string reset_code = make_color_code(std::array<Ansi_color, 1>{Ansi_color::Reset});
        (void)put_a_string(reset_code);
      }
      wrapped_buf_->pubsync();
    }
  }

  /** \brief gets the current color code string. */
  const string& color_code() const { return color_code_; }

  /**
   * \brief sets a new color code.
   *
   * \param color_code The new color code to apply
   */
  void set_color_code(string color_code) {
    color_code_ = std::move(color_code);
  }

  /** \brief checks if colors are enabled for this streambuf. */
  bool colors_enabled() const { return colors_enabled_; }

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

  /** \brief detects if the wrapped buffer supports color output. */
  static bool detect_color_support(streambuf_type* buf) {
    if(safe_getenv("NO_COLOR").value_or("").size() > 0) {
      return false;
    }

    if(safe_getenv("CLICOLOR_FORCE").value_or("").size() > 0) {
      return true;
    }

    // Try to determine if this is stdout or stderr
    // by comparing pointers (works for standard streams)
    if(buf == std::cout.rdbuf() || buf == std::clog.rdbuf()) {
      int fd = CGAL_FILENO(stdout);
      if(!CGAL_ISATTY(fd)) {
        return false;
      }
    } else if(buf == std::cerr.rdbuf()) {
      int fd = CGAL_FILENO(stderr);
      if(!CGAL_ISATTY(fd)) {
        return false;
      }
    } else {
      // Unknown buffer, conservatively disable colors
      return false;
    }

#ifdef _WIN32
    // Windows: assume modern Windows
    return  true;
#endif
    // POSIX: check TERM environment variable
    const std::string term_str = safe_getenv("TERM").value_or("");

    // Check for common color-capable terminals
    return term_str.find("color") != std::string::npos ||
           term_str.find("xterm") != std::string::npos ||
           term_str.find("rxvt") != std::string::npos ||
           term_str.find("screen") != std::string::npos ||
           term_str.find("tmux") != std::string::npos ||
           term_str.find("linux") != std::string::npos;
  }

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;

  bool put_a_string(const string& str) {
    for(char_type ch : str) {
      if(eq_int_type(wrapped_buf_->sputc(ch), eof())) {
        return false;
      }
    }
    return true;
  }

  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 color code first (if colors enabled)
    if(colors_enabled_ && at_line_start_ && !color_code_.empty()) {
      if(!put_a_string(color_code_)) {
        return eof();
      }
      at_line_start_ = false;
    }

    // Check if this character is a newline
    if(to_char_type(ch) == char_type('\n')) {
      // Output reset code before newline (if colors enabled)
      if(colors_enabled_ && !color_code_.empty()) {
        string reset_code = make_color_code(std::array<Ansi_color, 1>{Ansi_color::Reset});
        if(!put_a_string(reset_code)) {
          return eof();
        }
      }
      at_line_start_ = true;
    }

    // Output the actual character
    return wrapped_buf_->sputc(to_char_type(ch));
  }

  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_color_streambuf<char>`
using Color_streambuf = Basic_color_streambuf<char>;

/// \ingroup PkgStreamSupportRef
/// Type alias for `Basic_color_streambuf<wchar_t>`
using Color_wstreambuf = Basic_color_streambuf<wchar_t>;

/**
 * \ingroup PkgStreamSupportRef
 *
 * RAII helper class for temporarily installing a color streambuf on a stream.
 *
 * This class automatically restores the original streambuf when it goes out of scope.
 * It provides a convenient way to add colors to output streams within a specific scope.
 *
 * \tparam StreamT The stream type (e.g., `std::ostream`, `std::wostream`)
 *
 * \sa Basic_color_streambuf
 * \sa make_color_guards
 */
template <typename StreamT>
class Basic_color_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_color_streambuf<char_type, traits_type>;          ///< Type of the color 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_color_code_;
  streambuf_type color_buf_;

public:
  /**
   * \brief constructs and installs a color streambuf with one or more colors.
   *
   * \tparam AnsiColors Variadic list of \ref Ansi_color values
   * \param stream The stream to modify
   * \param colors One or more colors to combine (e.g., \c Ansi_color::Bold, \c Ansi_color::Red)
   */
  template <typename... AnsiColors>
  explicit
  Basic_color_stream_guard(StreamT& stream,
                           AnsiColors... colors)
      : stream_(stream)
      , original_buf_(stream.rdbuf())
      , color_buf_(*original_buf_, colors...)
  {
    static_assert((std::is_same<AnsiColors, Ansi_color>::value && ...),
                  "Basic_color_stream_guard expects Ansi_color arguments");
    if(auto old_buf = dynamic_cast<streambuf_type*>(original_buf_)) {
      original_color_code_ = old_buf->color_code();
      old_buf->set_color_code(color_buf_.color_code());
    } else {
      stream.rdbuf(&color_buf_);
    }
  }

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

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

  Basic_color_stream_guard& operator=(Basic_color_stream_guard&&) = delete;

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

/// \ingroup PkgStreamSupportRef
/// Type alias for `Basic_color_stream_guard<std::ostream>`
using Color_stream_guard = Basic_color_stream_guard<std::ostream>;

/// \ingroup PkgStreamSupportRef
/// Type alias for `Basic_color_stream_guard<std::wostream>`
using Color_wstream_guard = Basic_color_stream_guard<std::wostream>;

/**
 * \ingroup PkgStreamSupportRef
 *
 * \brief creates color guards for multiple streams simultaneously.
 *
 * This helper function creates `Basic_color_stream_guard` objects for
 * multiple streams at once. All streams will use the same color settings,
 * and their original streambufs will be automatically restored when the guards
 * go out of scope. The guards are returned in a tuple.
 *
 * \tparam colors Non-type template parameters specifying the colors to combine
 * \tparam Streams The stream types (deduced from arguments)
 * \param streams The streams to apply colors to
 * \return A tuple of `Basic_color_stream_guard` objects
 *
 * \sa Basic_color_stream_guard
 *
 * Example usage:
 * \snippet Stream_support/color_ostream.cpp make_color_guards
 */
template <Ansi_color... colors, typename... Streams>
auto make_color_guards(Streams&... streams) {
  return std::make_tuple(Basic_color_stream_guard<Streams>(streams, colors...)...);
}


/**
 * \ingroup PkgStreamSupportRef
 *
 * \brief checks if a stream is attached to a terminal that supports colors.
 *
 * This function checks if the given output stream is connected to a terminal
 * (TTY) that can display ANSI color codes. It performs the following checks:
 *
 * 1. Verifies the stream is attached to a TTY device using `isatty()`
 * 2. Checks if the `TERM` environment variable indicates color support
 * 3. Respects the `NO_COLOR` environment variable (if set, colors are disabled)
 * 4. Respects the `CLICOLOR_FORCE` environment variable (if set, colors are enabled)
 *
 * \param stream The output stream to check (e.g., `std::cout`, `std::cerr`)
 * \return `true` if the stream supports color output, `false` otherwise
 *
 * \note This function may return false negatives on some systems, but should
 *       never return false positives (it won't report color support where
 *       it doesn't exist).
 *
 * \sa Basic_color_streambuf
 *
 * Example usage:
 * \code
 * if(CGAL::IO::stream_supports_color(std::cout)) {
 *   CGAL::IO::Color_stream_guard guard(std::cout, CGAL::IO::Ansi_color::Red);
 *   std::cout << "This text is red!\n";
 * } else {
 *   std::cout << "Plain text output\n";
 * }
 * \endcode
 */
template <typename CharT, typename Traits>
bool stream_supports_color(const std::basic_ostream<CharT, Traits>& stream) {
  return Basic_color_streambuf<CharT, Traits>::detect_color_support(stream.rdbuf());
}

} // namespace IO
} // namespace CGAL

#endif // CGAL_IO_COLOR_OSTREAM_H
