//===-- IOHandler.cpp -----------------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//

#include "lldb/Core/IOHandler.h"

#if defined(__APPLE__)
#include <deque>
#endif
#include <string>

#include "lldb/Core/Debugger.h"
#include "lldb/Core/StreamFile.h"
#include "lldb/Host/Config.h"
#include "lldb/Host/File.h"
#include "lldb/Utility/AnsiTerminal.h"
#include "lldb/Utility/Predicate.h"
#include "lldb/Utility/Status.h"
#include "lldb/Utility/StreamString.h"
#include "lldb/Utility/StringList.h"
#include "lldb/lldb-forward.h"

#if LLDB_ENABLE_LIBEDIT
#include "lldb/Host/Editline.h"
#endif
#include "lldb/Interpreter/CommandCompletions.h"
#include "lldb/Interpreter/CommandInterpreter.h"
#include "llvm/ADT/StringRef.h"

#ifdef _WIN32
#include "lldb/Host/windows/windows.h"
#endif

#include <memory>
#include <mutex>
#include <optional>

#include <cassert>
#include <cctype>
#include <cerrno>
#include <clocale>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <type_traits>

using namespace lldb;
using namespace lldb_private;
using llvm::StringRef;

IOHandler::IOHandler(Debugger &debugger, IOHandler::Type type)
    : IOHandler(debugger, type,
                FileSP(),       // Adopt STDIN from top input reader
                StreamFileSP(), // Adopt STDOUT from top input reader
                StreamFileSP(), // Adopt STDERR from top input reader
                0               // Flags

      ) {}

IOHandler::IOHandler(Debugger &debugger, IOHandler::Type type,
                     const lldb::FileSP &input_sp,
                     const lldb::StreamFileSP &output_sp,
                     const lldb::StreamFileSP &error_sp, uint32_t flags)
    : m_debugger(debugger), m_input_sp(input_sp), m_output_sp(output_sp),
      m_error_sp(error_sp), m_popped(false), m_flags(flags), m_type(type),
      m_user_data(nullptr), m_done(false), m_active(false) {
  // If any files are not specified, then adopt them from the top input reader.
  if (!m_input_sp || !m_output_sp || !m_error_sp)
    debugger.AdoptTopIOHandlerFilesIfInvalid(m_input_sp, m_output_sp,
                                             m_error_sp);
}

IOHandler::~IOHandler() = default;

int IOHandler::GetInputFD() {
  return (m_input_sp ? m_input_sp->GetDescriptor() : -1);
}

int IOHandler::GetOutputFD() {
  return (m_output_sp ? m_output_sp->GetFile().GetDescriptor() : -1);
}

int IOHandler::GetErrorFD() {
  return (m_error_sp ? m_error_sp->GetFile().GetDescriptor() : -1);
}

FILE *IOHandler::GetInputFILE() {
  return (m_input_sp ? m_input_sp->GetStream() : nullptr);
}

FILE *IOHandler::GetOutputFILE() {
  return (m_output_sp ? m_output_sp->GetFile().GetStream() : nullptr);
}

FILE *IOHandler::GetErrorFILE() {
  return (m_error_sp ? m_error_sp->GetFile().GetStream() : nullptr);
}

FileSP IOHandler::GetInputFileSP() { return m_input_sp; }

StreamFileSP IOHandler::GetOutputStreamFileSP() { return m_output_sp; }

StreamFileSP IOHandler::GetErrorStreamFileSP() { return m_error_sp; }

bool IOHandler::GetIsInteractive() {
  return GetInputFileSP() ? GetInputFileSP()->GetIsInteractive() : false;
}

bool IOHandler::GetIsRealTerminal() {
  return GetInputFileSP() ? GetInputFileSP()->GetIsRealTerminal() : false;
}

void IOHandler::SetPopped(bool b) { m_popped.SetValue(b, eBroadcastOnChange); }

void IOHandler::WaitForPop() { m_popped.WaitForValueEqualTo(true); }

void IOHandler::PrintAsync(const char *s, size_t len, bool is_stdout) {
  std::lock_guard<std::recursive_mutex> guard(m_output_mutex);
  lldb::StreamFileSP stream = is_stdout ? m_output_sp : m_error_sp;
  stream->Write(s, len);
  stream->Flush();
}

bool IOHandlerStack::PrintAsync(const char *s, size_t len, bool is_stdout) {
  std::lock_guard<std::recursive_mutex> guard(m_mutex);
  if (!m_top)
    return false;
  m_top->PrintAsync(s, len, is_stdout);
  return true;
}

IOHandlerConfirm::IOHandlerConfirm(Debugger &debugger, llvm::StringRef prompt,
                                   bool default_response)
    : IOHandlerEditline(
          debugger, IOHandler::Type::Confirm,
          nullptr, // nullptr editline_name means no history loaded/saved
          llvm::StringRef(), // No prompt
          llvm::StringRef(), // No continuation prompt
          false,             // Multi-line
          false, // Don't colorize the prompt (i.e. the confirm message.)
          0, *this),
      m_default_response(default_response), m_user_response(default_response) {
  StreamString prompt_stream;
  prompt_stream.PutCString(prompt);
  if (m_default_response)
    prompt_stream.Printf(": [Y/n] ");
  else
    prompt_stream.Printf(": [y/N] ");

  SetPrompt(prompt_stream.GetString());
}

IOHandlerConfirm::~IOHandlerConfirm() = default;

void IOHandlerConfirm::IOHandlerComplete(IOHandler &io_handler,
                                         CompletionRequest &request) {
  if (request.GetRawCursorPos() != 0)
    return;
  request.AddCompletion(m_default_response ? "y" : "n");
}

void IOHandlerConfirm::IOHandlerInputComplete(IOHandler &io_handler,
                                              std::string &line) {
  if (line.empty()) {
    // User just hit enter, set the response to the default
    m_user_response = m_default_response;
    io_handler.SetIsDone(true);
    return;
  }

  if (line.size() == 1) {
    switch (line[0]) {
    case 'y':
    case 'Y':
      m_user_response = true;
      io_handler.SetIsDone(true);
      return;
    case 'n':
    case 'N':
      m_user_response = false;
      io_handler.SetIsDone(true);
      return;
    default:
      break;
    }
  }

  if (line == "yes" || line == "YES" || line == "Yes") {
    m_user_response = true;
    io_handler.SetIsDone(true);
  } else if (line == "no" || line == "NO" || line == "No") {
    m_user_response = false;
    io_handler.SetIsDone(true);
  }
}

std::optional<std::string>
IOHandlerDelegate::IOHandlerSuggestion(IOHandler &io_handler,
                                       llvm::StringRef line) {
  return io_handler.GetDebugger()
      .GetCommandInterpreter()
      .GetAutoSuggestionForCommand(line);
}

void IOHandlerDelegate::IOHandlerComplete(IOHandler &io_handler,
                                          CompletionRequest &request) {
  switch (m_completion) {
  case Completion::None:
    break;
  case Completion::LLDBCommand:
    io_handler.GetDebugger().GetCommandInterpreter().HandleCompletion(request);
    break;
  case Completion::Expression:
    CommandCompletions::InvokeCommonCompletionCallbacks(
        io_handler.GetDebugger().GetCommandInterpreter(),
        CommandCompletions::eVariablePathCompletion, request, nullptr);
    break;
  }
}

IOHandlerEditline::IOHandlerEditline(
    Debugger &debugger, IOHandler::Type type,
    const char *editline_name, // Used for saving history files
    llvm::StringRef prompt, llvm::StringRef continuation_prompt,
    bool multi_line, bool color_prompts, uint32_t line_number_start,
    IOHandlerDelegate &delegate)
    : IOHandlerEditline(debugger, type,
                        FileSP(),       // Inherit input from top input reader
                        StreamFileSP(), // Inherit output from top input reader
                        StreamFileSP(), // Inherit error from top input reader
                        0,              // Flags
                        editline_name,  // Used for saving history files
                        prompt, continuation_prompt, multi_line, color_prompts,
                        line_number_start, delegate) {}

IOHandlerEditline::IOHandlerEditline(
    Debugger &debugger, IOHandler::Type type, const lldb::FileSP &input_sp,
    const lldb::StreamFileSP &output_sp, const lldb::StreamFileSP &error_sp,
    uint32_t flags,
    const char *editline_name, // Used for saving history files
    llvm::StringRef prompt, llvm::StringRef continuation_prompt,
    bool multi_line, bool color_prompts, uint32_t line_number_start,
    IOHandlerDelegate &delegate)
    : IOHandler(debugger, type, input_sp, output_sp, error_sp, flags),
#if LLDB_ENABLE_LIBEDIT
      m_editline_up(),
#endif
      m_delegate(delegate), m_prompt(), m_continuation_prompt(),
      m_current_lines_ptr(nullptr), m_base_line_number(line_number_start),
      m_curr_line_idx(UINT32_MAX), m_multi_line(multi_line),
      m_color_prompts(color_prompts), m_interrupt_exits(true) {
  SetPrompt(prompt);

#if LLDB_ENABLE_LIBEDIT
  bool use_editline = false;

  use_editline = GetInputFILE() && GetOutputFILE() && GetErrorFILE() &&
                 m_input_sp && m_input_sp->GetIsRealTerminal();

  if (use_editline) {
    m_editline_up = std::make_unique<Editline>(
        editline_name, GetInputFILE(), GetOutputFILE(), GetErrorFILE(),
        GetOutputMutex(), m_color_prompts);
    m_editline_up->SetIsInputCompleteCallback(
        [this](Editline *editline, StringList &lines) {
          return this->IsInputCompleteCallback(editline, lines);
        });

    m_editline_up->SetAutoCompleteCallback([this](CompletionRequest &request) {
      this->AutoCompleteCallback(request);
    });

    if (debugger.GetUseAutosuggestion()) {
      m_editline_up->SetSuggestionCallback([this](llvm::StringRef line) {
        return this->SuggestionCallback(line);
      });
      m_editline_up->SetSuggestionAnsiPrefix(ansi::FormatAnsiTerminalCodes(
          debugger.GetAutosuggestionAnsiPrefix()));
      m_editline_up->SetSuggestionAnsiSuffix(ansi::FormatAnsiTerminalCodes(
          debugger.GetAutosuggestionAnsiSuffix()));
    }
    // See if the delegate supports fixing indentation
    const char *indent_chars = delegate.IOHandlerGetFixIndentationCharacters();
    if (indent_chars) {
      // The delegate does support indentation, hook it up so when any
      // indentation character is typed, the delegate gets a chance to fix it
      FixIndentationCallbackType f = [this](Editline *editline,
                                            const StringList &lines,
                                            int cursor_position) {
        return this->FixIndentationCallback(editline, lines, cursor_position);
      };
      m_editline_up->SetFixIndentationCallback(std::move(f), indent_chars);
    }
  }
#endif
  SetBaseLineNumber(m_base_line_number);
  SetPrompt(prompt);
  SetContinuationPrompt(continuation_prompt);
}

IOHandlerEditline::~IOHandlerEditline() {
#if LLDB_ENABLE_LIBEDIT
  m_editline_up.reset();
#endif
}

void IOHandlerEditline::Activate() {
  IOHandler::Activate();
  m_delegate.IOHandlerActivated(*this, GetIsInteractive());
}

void IOHandlerEditline::Deactivate() {
  IOHandler::Deactivate();
  m_delegate.IOHandlerDeactivated(*this);
}

void IOHandlerEditline::TerminalSizeChanged() {
#if LLDB_ENABLE_LIBEDIT
  if (m_editline_up)
    m_editline_up->TerminalSizeChanged();
#endif
}

// Split out a line from the buffer, if there is a full one to get.
static std::optional<std::string> SplitLine(std::string &line_buffer) {
  size_t pos = line_buffer.find('\n');
  if (pos == std::string::npos)
    return std::nullopt;
  std::string line =
      std::string(StringRef(line_buffer.c_str(), pos).rtrim("\n\r"));
  line_buffer = line_buffer.substr(pos + 1);
  return line;
}

// If the final line of the file ends without a end-of-line, return
// it as a line anyway.
static std::optional<std::string> SplitLineEOF(std::string &line_buffer) {
  if (llvm::all_of(line_buffer, llvm::isSpace))
    return std::nullopt;
  std::string line = std::move(line_buffer);
  line_buffer.clear();
  return line;
}

bool IOHandlerEditline::GetLine(std::string &line, bool &interrupted) {
#if LLDB_ENABLE_LIBEDIT
  if (m_editline_up) {
    return m_editline_up->GetLine(line, interrupted);
  }
#endif

  line.clear();

  if (GetIsInteractive()) {
    const char *prompt = nullptr;

    if (m_multi_line && m_curr_line_idx > 0)
      prompt = GetContinuationPrompt();

    if (prompt == nullptr)
      prompt = GetPrompt();

    if (prompt && prompt[0]) {
      if (m_output_sp) {
        m_output_sp->Printf("%s", prompt);
        m_output_sp->Flush();
      }
    }
  }

  std::optional<std::string> got_line = SplitLine(m_line_buffer);

  if (!got_line && !m_input_sp) {
    // No more input file, we are done...
    SetIsDone(true);
    return false;
  }

  FILE *in = GetInputFILE();
  char buffer[256];

  if (!got_line && !in && m_input_sp) {
    // there is no FILE*, fall back on just reading bytes from the stream.
    while (!got_line) {
      size_t bytes_read = sizeof(buffer);
      Status error = m_input_sp->Read((void *)buffer, bytes_read);
      if (error.Success() && !bytes_read) {
        got_line = SplitLineEOF(m_line_buffer);
        break;
      }
      if (error.Fail())
        break;
      m_line_buffer += StringRef(buffer, bytes_read);
      got_line = SplitLine(m_line_buffer);
    }
  }

  if (!got_line && in) {
    while (!got_line) {
      char *r = fgets(buffer, sizeof(buffer), in);
#ifdef _WIN32
      // ReadFile on Windows is supposed to set ERROR_OPERATION_ABORTED
      // according to the docs on MSDN. However, this has evidently been a
      // known bug since Windows 8. Therefore, we can't detect if a signal
      // interrupted in the fgets. So pressing ctrl-c causes the repl to end
      // and the process to exit. A temporary workaround is just to attempt to
      // fgets twice until this bug is fixed.
      if (r == nullptr)
        r = fgets(buffer, sizeof(buffer), in);
      // this is the equivalent of EINTR for Windows
      if (r == nullptr && GetLastError() == ERROR_OPERATION_ABORTED)
        continue;
#endif
      if (r == nullptr) {
        if (ferror(in) && errno == EINTR)
          continue;
        if (feof(in))
          got_line = SplitLineEOF(m_line_buffer);
        break;
      }
      m_line_buffer += buffer;
      got_line = SplitLine(m_line_buffer);
    }
  }

  if (got_line) {
    line = *got_line;
  }

  return (bool)got_line;
}

#if LLDB_ENABLE_LIBEDIT
bool IOHandlerEditline::IsInputCompleteCallback(Editline *editline,
                                                StringList &lines) {
  return m_delegate.IOHandlerIsInputComplete(*this, lines);
}

int IOHandlerEditline::FixIndentationCallback(Editline *editline,
                                              const StringList &lines,
                                              int cursor_position) {
  return m_delegate.IOHandlerFixIndentation(*this, lines, cursor_position);
}

std::optional<std::string>
IOHandlerEditline::SuggestionCallback(llvm::StringRef line) {
  return m_delegate.IOHandlerSuggestion(*this, line);
}

void IOHandlerEditline::AutoCompleteCallback(CompletionRequest &request) {
  m_delegate.IOHandlerComplete(*this, request);
}
#endif

const char *IOHandlerEditline::GetPrompt() {
#if LLDB_ENABLE_LIBEDIT
  if (m_editline_up) {
    return m_editline_up->GetPrompt();
  } else {
#endif
    if (m_prompt.empty())
      return nullptr;
#if LLDB_ENABLE_LIBEDIT
  }
#endif
  return m_prompt.c_str();
}

bool IOHandlerEditline::SetPrompt(llvm::StringRef prompt) {
  m_prompt = std::string(prompt);

#if LLDB_ENABLE_LIBEDIT
  if (m_editline_up)
    m_editline_up->SetPrompt(m_prompt.empty() ? nullptr : m_prompt.c_str());
#endif
  return true;
}

const char *IOHandlerEditline::GetContinuationPrompt() {
  return (m_continuation_prompt.empty() ? nullptr
                                        : m_continuation_prompt.c_str());
}

void IOHandlerEditline::SetContinuationPrompt(llvm::StringRef prompt) {
  m_continuation_prompt = std::string(prompt);

#if LLDB_ENABLE_LIBEDIT
  if (m_editline_up)
    m_editline_up->SetContinuationPrompt(m_continuation_prompt.empty()
                                             ? nullptr
                                             : m_continuation_prompt.c_str());
#endif
}

void IOHandlerEditline::SetBaseLineNumber(uint32_t line) {
  m_base_line_number = line;
}

uint32_t IOHandlerEditline::GetCurrentLineIndex() const {
#if LLDB_ENABLE_LIBEDIT
  if (m_editline_up)
    return m_editline_up->GetCurrentLine();
#endif
  return m_curr_line_idx;
}

bool IOHandlerEditline::GetLines(StringList &lines, bool &interrupted) {
  m_current_lines_ptr = &lines;

  bool success = false;
#if LLDB_ENABLE_LIBEDIT
  if (m_editline_up) {
    return m_editline_up->GetLines(m_base_line_number, lines, interrupted);
  } else {
#endif
    bool done = false;
    Status error;

    while (!done) {
      // Show line numbers if we are asked to
      std::string line;
      if (m_base_line_number > 0 && GetIsInteractive()) {
        if (m_output_sp) {
          m_output_sp->Printf("%u%s",
                              m_base_line_number + (uint32_t)lines.GetSize(),
                              GetPrompt() == nullptr ? " " : "");
        }
      }

      m_curr_line_idx = lines.GetSize();

      bool interrupted = false;
      if (GetLine(line, interrupted) && !interrupted) {
        lines.AppendString(line);
        done = m_delegate.IOHandlerIsInputComplete(*this, lines);
      } else {
        done = true;
      }
    }
    success = lines.GetSize() > 0;
#if LLDB_ENABLE_LIBEDIT
  }
#endif
  return success;
}

// Each IOHandler gets to run until it is done. It should read data from the
// "in" and place output into "out" and "err and return when done.
void IOHandlerEditline::Run() {
  std::string line;
  while (IsActive()) {
    bool interrupted = false;
    if (m_multi_line) {
      StringList lines;
      if (GetLines(lines, interrupted)) {
        if (interrupted) {
          m_done = m_interrupt_exits;
          m_delegate.IOHandlerInputInterrupted(*this, line);

        } else {
          line = lines.CopyList();
          m_delegate.IOHandlerInputComplete(*this, line);
        }
      } else {
        m_done = true;
      }
    } else {
      if (GetLine(line, interrupted)) {
        if (interrupted)
          m_delegate.IOHandlerInputInterrupted(*this, line);
        else
          m_delegate.IOHandlerInputComplete(*this, line);
      } else {
        m_done = true;
      }
    }
  }
}

void IOHandlerEditline::Cancel() {
#if LLDB_ENABLE_LIBEDIT
  if (m_editline_up)
    m_editline_up->Cancel();
#endif
}

bool IOHandlerEditline::Interrupt() {
  // Let the delgate handle it first
  if (m_delegate.IOHandlerInterrupt(*this))
    return true;

#if LLDB_ENABLE_LIBEDIT
  if (m_editline_up)
    return m_editline_up->Interrupt();
#endif
  return false;
}

void IOHandlerEditline::GotEOF() {
#if LLDB_ENABLE_LIBEDIT
  if (m_editline_up)
    m_editline_up->Interrupt();
#endif
}

void IOHandlerEditline::PrintAsync(const char *s, size_t len, bool is_stdout) {
#if LLDB_ENABLE_LIBEDIT
  if (m_editline_up) {
    std::lock_guard<std::recursive_mutex> guard(m_output_mutex);
    lldb::StreamFileSP stream = is_stdout ? m_output_sp : m_error_sp;
    m_editline_up->PrintAsync(stream.get(), s, len);
  } else
#endif
  {
#ifdef _WIN32
    const char *prompt = GetPrompt();
    if (prompt) {
      // Back up over previous prompt using Windows API
      CONSOLE_SCREEN_BUFFER_INFO screen_buffer_info;
      HANDLE console_handle = GetStdHandle(STD_OUTPUT_HANDLE);
      GetConsoleScreenBufferInfo(console_handle, &screen_buffer_info);
      COORD coord = screen_buffer_info.dwCursorPosition;
      coord.X -= strlen(prompt);
      if (coord.X < 0)
        coord.X = 0;
      SetConsoleCursorPosition(console_handle, coord);
    }
#endif
    IOHandler::PrintAsync(s, len, is_stdout);
#ifdef _WIN32
    if (prompt)
      IOHandler::PrintAsync(prompt, strlen(prompt), is_stdout);
#endif
  }
}
