/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
   file LICENSE.rst or https://cmake.org/licensing for details.  */
#include "cmStdIoTerminal.h"

#include <array>
#include <functional>
#include <iosfwd>
#include <ostream>
#include <string>
#include <type_traits>

#include <cm/string_view>
#include <cmext/string_view>

#ifdef _WIN32
#  include <windows.h>
#endif

#include <cm/optional>

#include "cmStdIoStream.h"
#include "cmSystemTools.h"

namespace cm {
namespace StdIo {

namespace {

#ifdef _WIN32
WORD const kConsoleAttrMask = FOREGROUND_RED | FOREGROUND_GREEN |
  FOREGROUND_BLUE | FOREGROUND_INTENSITY | BACKGROUND_RED | BACKGROUND_GREEN |
  BACKGROUND_BLUE | BACKGROUND_INTENSITY;
std::array<WORD, kTermAttrCount> const kConsoleAttrs{ {
  0,                                                   // Normal
  FOREGROUND_INTENSITY,                                // ForegroundBold
  0,                                                   // ForegroundBlack
  FOREGROUND_BLUE,                                     // ForegroundBlue
  FOREGROUND_GREEN | FOREGROUND_BLUE,                  // ForegroundCyan
  FOREGROUND_GREEN,                                    // ForegroundGreen
  FOREGROUND_RED | FOREGROUND_BLUE,                    // ForegroundMagenta
  FOREGROUND_RED,                                      // ForegroundRed
  FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE, // ForegroundWhite
  FOREGROUND_RED | FOREGROUND_GREEN,                   // ForegroundYellow
  BACKGROUND_INTENSITY,                                // BackgroundBold
  0,                                                   // BackgroundBlack
  BACKGROUND_BLUE,                                     // BackgroundBlue
  BACKGROUND_GREEN | BACKGROUND_BLUE,                  // BackgroundCyan
  BACKGROUND_GREEN,                                    // BackgroundGreen
  BACKGROUND_RED | BACKGROUND_BLUE,                    // BackgroundMagenta
  BACKGROUND_RED,                                      // BackgroundRed
  BACKGROUND_RED | BACKGROUND_GREEN | BACKGROUND_BLUE, // BackgroundWhite
  BACKGROUND_RED | BACKGROUND_GREEN,                   // BackgroundYellow
} };

WORD ConsoleAttrs(WORD consoleAttrs, TermAttrSet const& attrs)
{
  consoleAttrs =
    attrs.contains(TermAttr::Normal) ? consoleAttrs & kConsoleAttrMask : 0;
  for (TermAttr attr : attrs) {
    auto index = static_cast<std::underlying_type<TermAttr>::type>(attr);
    consoleAttrs |= kConsoleAttrs[index];
  }
  return consoleAttrs;
}
#endif

// VT100 escape sequence strings.
#if defined(__MVS__) // z/OS: assume EBCDIC
#  define ESC "\47"
#else
#  define ESC "\33"
#endif

std::array<cm::string_view, kTermAttrCount> const kVT100Codes{ {
  ESC "[0m"_s,  // Normal
  ESC "[1m"_s,  // ForegroundBold
  ESC "[30m"_s, // ForegroundBlack
  ESC "[34m"_s, // ForegroundBlue
  ESC "[36m"_s, // ForegroundCyan
  ESC "[32m"_s, // ForegroundGreen
  ESC "[35m"_s, // ForegroundMagenta
  ESC "[31m"_s, // ForegroundRed
  ESC "[37m"_s, // ForegroundWhite
  ESC "[33m"_s, // ForegroundYellow
  ""_s,         // BackgroundBold
  ESC "[40m"_s, // BackgroundBlack
  ESC "[44m"_s, // BackgroundBlue
  ESC "[46m"_s, // BackgroundCyan
  ESC "[42m"_s, // BackgroundGreen
  ESC "[45m"_s, // BackgroundMagenta
  ESC "[41m"_s, // BackgroundRed
  ESC "[47m"_s, // BackgroundWhite
  ESC "[43m"_s, // BackgroundYellow
} };

void SetVT100Attrs(std::ostream& os, TermAttrSet const& attrs)
{
  for (TermAttr attr : attrs) {
    auto index = static_cast<std::underlying_type<TermAttr>::type>(attr);
    os << kVT100Codes[index];
  }
}

auto const TermEnv = []() -> cm::optional<TermKind> {
  /* Disable color according to https://bixense.com/clicolors/ convention. */
  if (cm::optional<std::string> noColor =
        cmSystemTools::GetEnvVar("NO_COLOR")) {
    if (!noColor->empty() && *noColor != "0"_s) {
      return TermKind::None;
    }
  }
  /* Force color according to https://bixense.com/clicolors/ convention.  */
  if (cm::optional<std::string> cliColorForce =
        cmSystemTools::GetEnvVar("CLICOLOR_FORCE")) {
    if (!cliColorForce->empty() && *cliColorForce != "0"_s) {
      return TermKind::VT100;
    }
  }
  /* Disable color according to https://bixense.com/clicolors/ convention. */
  if (cm::optional<std::string> cliColor =
        cmSystemTools::GetEnvVar("CLICOLOR")) {
    if (*cliColor == "0"_s) {
      return TermKind::None;
    }
  }
  /* GNU make 4.1+ may tell us that its output is destined for a TTY. */
  if (cm::optional<std::string> makeTermOut =
        cmSystemTools::GetEnvVar("MAKE_TERMOUT")) {
    if (!makeTermOut->empty()) {
      return TermKind::VT100;
    }
  }
  return cm::nullopt;
}();

void Print(OStream& os, TermAttrSet const& attrs,
           std::function<void(std::ostream&)> const& f)
{
  TermKind kind = TermEnv ? *TermEnv : os.Kind();
  switch (kind) {
    case TermKind::None:
      f(os.IOS());
      break;
    case TermKind::VT100:
      if (!attrs.empty()) {
        SetVT100Attrs(os.IOS(), attrs);
        f(os.IOS());
        SetVT100Attrs(os.IOS(), TermAttr::Normal);
      } else {
        f(os.IOS());
      }
      break;
#ifdef _WIN32
    case TermKind::Console: {
      HANDLE console = os.Console();
      CONSOLE_SCREEN_BUFFER_INFO sbi;
      if (!attrs.empty() && GetConsoleScreenBufferInfo(console, &sbi)) {
        Out().IOS().flush();
        Err().IOS().flush();
        SetConsoleTextAttribute(console, ConsoleAttrs(sbi.wAttributes, attrs));
        f(os.IOS());
        Out().IOS().flush();
        Err().IOS().flush();
        SetConsoleTextAttribute(
          console, ConsoleAttrs(sbi.wAttributes, TermAttr::Normal));
      } else {
        f(os.IOS());
      }
    } break;
#endif
  };
}

} // anonymous namespace

void Print(OStream& os, TermAttrSet const& attrs, cm::string_view s)
{
  Print(os, attrs, [s](std::ostream& o) { o << s; });
}

}
}
