// -*- mode: c++; c-file-style: "linux"; c-basic-offset: 2; indent-tabs-mode: nil -*-
//
//  Copyright (C) 2026 Gunter Königsmann <wxMaxima@physikbuch.de>
//
//  This program is free software; you can redistribute it and/or modify
//  it under the terms of the GNU General Public License as published by
//  the Free Software Foundation; either version 2 of the License, or
//  (at your option) any later version.
//
//  This program is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU General Public License for more details.
//
//
//  You should have received a copy of the GNU General Public License
//  along with this program; if not, write to the Free Software
//  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
//
//  SPDX-License-Identifier: GPL-2.0+

/*! \file
  Implements the Maxima-response handlers extracted from the wxMaxima god class.
*/

#include "MaximaResponseReader.h"
#include "wxMaxima.h"
#include "Maxima.h"
#include "MaximaVariableUpdates.h"
#include "MaximaMenuSync.h"
#include "LdbSupport.h"
#include "MaximaProtocol.h"
#include "EventIDs.h"
#include <wx/sstream.h>
#include <functional>

void MaximaResponseReader::ReadStatusBar(const wxXmlDocument &xmldoc) {
  if(m_wxMaxima.GetWorksheet())
    m_wxMaxima.GetWorksheet()->SetCurrentTextCell(nullptr);
  if(!xmldoc.IsOk())
    {
      m_wxMaxima.m_outputAppender.DoRawConsoleAppend(_("There was an error in the XML that should describe the status bar message.\n"
                           "Please report this as a bug to the wxMaxima project."),
                         MC_TYPE_ERROR);
      m_wxMaxima.m_evaluator.AbortOnError();
    }
  else
    {
      wxXmlNode *node = xmldoc.GetRoot();
      if (node != NULL) {
        wxXmlNode *contents = node->GetChildren();
        if (contents)
          m_wxMaxima.StatusText(contents->GetContent(), false);
      }
    }
}

void MaximaResponseReader::ReadManualTopicNames(const wxXmlDocument &xmldoc) {
  if(xmldoc.IsOk())
    {
      std::vector<wxString> topics;
      wxXmlNode *node = xmldoc.GetRoot();
      while ((node) && (node->GetName() != wxS("html-manual-keywords")))
        node = node->GetNext();

      if (node == NULL) {
        wxLogMessage(_("No topics found in topic tag"));
      } else {
        for (wxXmlNode *entry = node->GetChildren(); entry != NULL;
             entry = entry->GetNext())
          {
            if (entry->GetName() == wxS("keyword")) {
              wxXmlNode *topic = entry->GetChildren();
              if (topic) {
                wxLogMessage(_("Received manual topic request: %s"),
                             topic->GetContent().ToUTF8().data());
                topics.push_back(topic->GetContent());
              }
            }
          }
        if (topics.empty())
          wxLogMessage(_("No topics found in topic flag"));
        else
          {
#ifdef USE_WEBVIEW
            m_wxMaxima.m_helpPane->SelectKeywords(topics);
            // wxMaxima hides the base wxMaximaFrame::ShowPane(int, bool) overload
            // with its own ShowPane(wxCommandEvent&); qualify to reach the base
            // one (as the menu handlers do) - a bare call fails to compile in the
            // USE_WEBVIEW build.
            m_wxMaxima.wxMaximaFrame::ShowPane(EventIDs::menu_pane_help);
#else
            m_wxMaxima.ShowMaximaHelp(topics.front());
#endif
          }
      }
    }
  else
    {
      m_wxMaxima.m_outputAppender.DoRawConsoleAppend(_("There was an error in the XML that should describe the manual topics.\n"
                           "Please report this as a bug to the wxMaxima project."),
                         MC_TYPE_ERROR);
      m_wxMaxima.m_evaluator.AbortOnError();
    }
}


/***
 * Checks if maxima displayed a new chunk of math
 */
void MaximaResponseReader::ReadMath(const wxXmlDocument &xml) {
  if(!m_wxMaxima.GetWorksheet())
    return;

  m_wxMaxima.GetWorksheet()->SetCurrentTextCell(nullptr);

  // Append everything from the "beginning of math" to the "end of math" marker
  // to the console
  if (m_wxMaxima.m_configuration.UseUserLabels()) {
    m_wxMaxima.m_outputAppender.ConsoleAppend(xml, MC_TYPE_DEFAULT,
                  m_wxMaxima.GetWorksheet()->GetEvaluationQueue().GetUserLabel());
  } else {
    m_wxMaxima.m_outputAppender.ConsoleAppend(xml, MC_TYPE_DEFAULT);
  }
}

void MaximaResponseReader::ReadLoadSymbols(const wxXmlDocument &data) {
  if(m_wxMaxima.GetWorksheet())
    m_wxMaxima.GetWorksheet()->AddSymbols(data);
}

void MaximaResponseReader::ReadFirstPrompt(const wxString &data) {
  m_wxMaxima.m_firstPromptBuffer += data;
  auto end = m_wxMaxima.m_firstPromptBuffer.Find(m_wxMaxima.m_firstPrompt);
  if (end  == wxNOT_FOUND)
    return;

  m_wxMaxima.m_bytesFromMaxima = 0;

  int start = 0;
  start = m_wxMaxima.m_firstPromptBuffer.Find(wxS("Maxima "));
  if (start == wxNOT_FOUND)
    start = 0;
  m_wxMaxima.m_evaluator.FirstOutput();

  m_wxMaxima.m_maximaBusy = false;

  // Wait for a line maxima informs us about it's process id in.
  int s = m_wxMaxima.m_firstPromptBuffer.Find(wxS("pid="));
  if (s != wxNOT_FOUND) {
    s += 4;
    int t =
        s + m_wxMaxima.m_firstPromptBuffer.SubString(s, m_wxMaxima.m_firstPromptBuffer.Length()).Find(wxS("\n")) - 1;

    // Read this pid
    if (s < t)
      if (!m_wxMaxima.m_firstPromptBuffer.SubString(s, t).ToLong(&m_wxMaxima.m_maximaPid))
        wxLogMessage(_("Cannot interpret the numeric value of pid %s"),
                     m_wxMaxima.m_firstPromptBuffer.SubString(s, t));
  }

  if (m_wxMaxima.m_pid > 0)
    m_wxMaxima.m_MenuBar->EnableItem(EventIDs::menu_interrupt_id, true);

  m_wxMaxima.m_client->ClearFirstPrompt();
  m_wxMaxima.m_first = false;
  m_wxMaxima.StatusMaximaBusy(StatusBar::MaximaStatus::waiting);
  m_wxMaxima.m_closing = false; // when restarting maxima this is temporarily true

  wxString prompt_compact = m_wxMaxima.m_firstPromptBuffer.Left(
      start + static_cast<std::size_t>(end) + m_wxMaxima.m_firstPrompt.Length() - 1);
  prompt_compact.Replace(wxS("\n"), wxS("\u21b2"));

  wxLogMessage(_("Received maxima's first prompt: %s"), prompt_compact);

  if (m_wxMaxima.m_maximaPid > 0)
    wxLogMessage(_("Maxima's PID is %li"), m_wxMaxima.m_maximaPid);
  else
    wxLogMessage(_("Maxima's PID is %li"), m_wxMaxima.m_pid);

  m_wxMaxima.m_firstPromptBuffer.Clear();

  if (m_wxMaxima.GetWorksheet() && (m_wxMaxima.GetWorksheet()->GetEvaluationQueue().Empty())) {
    // Inform the user that the evaluation queue is empty.
    m_wxMaxima.EvaluationQueueLength(0);
    if (m_wxMaxima.GetWorksheet() && (m_wxMaxima.m_configuration.GetOpenHCaret()) &&
        (m_wxMaxima.GetWorksheet()->GetActiveCell() == NULL))
      m_wxMaxima.GetWorksheet()->OpenNextOrCreateCell();
  } else
    m_wxMaxima.m_evaluator.TriggerEvaluation();
}

void MaximaResponseReader::ReadMiscText(const wxString &data) {
  if(!m_wxMaxima.m_maximaAuthenticated)
    return;

  auto style = MC_TYPE_ASCIIMATHS;

  if (data.StartsWith(wxS("(%")))
    style = MC_TYPE_TEXT;

  if (data.IsEmpty())
    return;

  if (data == "\r")
    return;

  if (m_wxMaxima.GetWorksheet() && (data.StartsWith("\n")))
    m_wxMaxima.GetWorksheet()->SetCurrentTextCell(nullptr);

  // A version of the text where each line begins with non-whitespace and
  // whitespace characters are merged.
  wxString mergedWhitespace = wxS("\n");
  bool whitespace = true;
  for (wxString::const_iterator it = data.begin(); it != data.end(); ++it) {
    if ((*it == wxS(' ')) || (*it == wxS('\t'))) {
      // Merge non-newline whitespace to a space.
      if (!whitespace)
        mergedWhitespace += wxS(' ');
    } else
      mergedWhitespace += *it;

    if ((*it == wxS(' ')) || (*it == wxS('\t')) || (*it == wxS('\n')))
      whitespace = true;
    else
      whitespace = false;
  }

  if ((mergedWhitespace.Contains(wxS("\n-- an error."))) ||
      (mergedWhitespace.Contains(wxS(":incorrect syntax:"))) ||
      (mergedWhitespace.Contains(wxS("\nincorrect syntax"))) ||
      (mergedWhitespace.Contains(wxS("\nMaxima encountered a Lisp error"))) ||
      (mergedWhitespace.Contains(wxS("\nkillcontext: no such context"))) ||
      (mergedWhitespace.Contains(
                                 wxS("\ndbl:MAXIMA>>"))) || // a gcl error message
      (mergedWhitespace.Contains(
                                 wxS("\nTo enable the Lisp debugger set *debugger-hook* to "
                                     "nil."))) // a scbl error message
      )
    style = MC_TYPE_ERROR;

  if ((mergedWhitespace.StartsWith(wxS("Warning:"))) ||
      (mergedWhitespace.StartsWith(wxS("warning:"))) ||
      (mergedWhitespace.StartsWith(wxS("WARNING:"))) ||
      (mergedWhitespace.Contains(wxS("\nWarning:"))) ||
      (mergedWhitespace.Contains(wxS("\nWARNING:"))) ||
      (mergedWhitespace.Contains(wxS("\nwarning:"))) ||
      (mergedWhitespace.Contains(wxS(": Warning:"))) ||
      (mergedWhitespace.Contains(wxS(": warning:"))))
    style = MC_TYPE_WARNING;
  else {
    // Gnuplot errors differ from gnuplot warnings by not containing a
    // "warning:"
    if (m_wxMaxima.m_gnuplotErrorRegex.Matches(mergedWhitespace))
      style = MC_TYPE_ERROR;
  }

  // Add the text line to the console
  if (m_wxMaxima.GetWorksheet() && (!data.empty())) {
    m_wxMaxima.GetWorksheet()->SetCurrentTextCell(m_wxMaxima.m_outputAppender.ConsoleAppend(data, style));
    if (style == MC_TYPE_ERROR)
      m_wxMaxima.m_evaluator.AbortOnError();
  }
  if (m_wxMaxima.GetWorksheet() && (data.EndsWith("\n")))
    m_wxMaxima.GetWorksheet()->SetCurrentTextCell(nullptr);
}

void MaximaResponseReader::ReadSuppressedOutput(const wxString &data) {
  if(m_wxMaxima.m_maximaAuthenticated)
    return;

  // Note the "!= wxNOT_FOUND": wxNOT_FOUND is -1 and therefore truthy,
  // so testing Find()'s result as a bool accepts any key.
  if(data.Find("<wxxml-key>" + m_wxMaxima.m_maximaAuthString + "</wxxml-key>") != wxNOT_FOUND) {
    wxLogMessage(_("Maxima has authenticated!"));
    m_wxMaxima.m_maximaAuthenticated = true;
    return;
  }

  if(data.Find("</wxxml-key>") != wxNOT_FOUND)
    wxLogMessage(_("Cannot authenticate Maxima!"));
  else
    wxLogMessage(_("Maxima didn't attempt to authenticate!"));

  // Order matters here, twice: the discard flag must be armed BEFORE the user
  // is told about it, and the dialog must not be shown from within this event
  // handler. A modal box spins a nested event loop, so anything the untrusted
  // process sends while the box is open would be interpreted with this
  // handler still on the stack. Arming the flag first means the MaximaEvent
  // gate is already discarding by the time the (CallAfter-deferred) box
  // appears - which also keeps a second suppressOutput chunk from stacking a
  // second box.
  m_wxMaxima.m_discardAllData = true;
  m_wxMaxima.CallAfter([]{
    LoggingMessageBox(
                      _("Could not make sure that we talk to the maxima we started => "
                        "discarding all data it sends."),
                      _("Warning"), wxOK | wxICON_EXCLAMATION);
  });
}

void MaximaResponseReader::ReadPrompt(const wxString &data) {
  m_wxMaxima.m_evalOnStartup = false;
  if(!m_wxMaxima.GetWorksheet())
    return;

  m_wxMaxima.GetWorksheet()->SetCurrentTextCell(nullptr);

  // Assume we don't have a question prompt
  m_wxMaxima.GetWorksheet()->QuestionPending(false);
  m_wxMaxima.m_ready = true;

  wxLogMessage(_("Got a new input prompt!"));
  m_wxMaxima.m_maximaBusy = false;
  m_wxMaxima.m_bytesFromMaxima = 0;

  // Maxima::ProcessData only fires an XML_PROMPT event once it has a complete
  // <PROMPT>...</PROMPT> tag, so the label extraction below may rely on data
  // carrying both the prefix and the suffix. (If the invariant is ever broken
  // SubString clamps, so release builds get a garbled label, not a crash.)
  wxASSERT(data.StartsWith(m_wxMaxima.m_promptPrefix) &&
           data.EndsWith(m_wxMaxima.m_promptSuffix));
  wxString label = data.SubString(m_wxMaxima.m_promptPrefix.Length(),
                                  data.Length() - m_wxMaxima.m_promptSuffix.Length() - 1);

  // If we got a prompt our connection to maxima was successful.
  if (m_wxMaxima.m_unsuccessfulConnectionAttempts > 0)
    m_wxMaxima.m_unsuccessfulConnectionAttempts--;
  label.Trim(true);
  label.Trim(false);

  // Record the lisp-mode transition from this prompt BEFORE the queue is
  // advanced below. RemoveFirst() lazily tokenizes the NEXT cell, and the
  // tokenizer must already know the mode: in Maxima mode "$" ends a statement,
  // but in Lisp mode "$" is an ordinary character (e.g. the "$x" that names a
  // Maxima variable from Lisp, as in (setq $x 3)). Setting the flag only at
  // the end of this function - after RemoveFirst() had already tokenized the
  // next cell - split the first Lisp form after to_lisp() at its "$" (and,
  // symmetrically, treated the first Maxima form after (to-maxima) as one Lisp
  // blob), desyncing the REPL into a prompt flood / hang. The sbcl high-level
  // debugger "(dbm:" prompt keeps its existing handling at the end of this
  // function, so its question-vs-main-prompt classification is unchanged.
  if (label.StartsWith(wxS("MAXIMA>"))) {
    if (!m_wxMaxima.m_configuration.InLispMode())
      wxLogMessage(_("Switched to lisp mode after receiving a lisp prompt!"));
    m_wxMaxima.m_configuration.InLispMode(true);
  } else if (!label.StartsWith(wxS("(dbm:"))) {
    if (m_wxMaxima.m_configuration.InLispMode())
      wxLogMessage(_("Ended lisp mode after receiving a maxima prompt!"));
    m_wxMaxima.m_configuration.InLispMode(false);
  }

  // Is this a new main prompt (advance the evaluation queue) or a question we
  // must stop and answer? See MaximaProtocol::IsMainInputPrompt. Note the
  // debugger prompt "(dbm:N)" is always a question, so debugger commands are
  // answered rather than recorded as worksheet input.
  if (MaximaProtocol::IsMainInputPrompt(label,
                                        m_wxMaxima.m_configuration.InLispMode())) {
    // Maxima displayed a new main prompt => We don't have a question
    m_wxMaxima.GetWorksheet()->QuestionAnswered();
    // And we can remove one command from the evaluation queue.
    m_wxMaxima.GetWorksheet()->GetEvaluationQueue().RemoveFirst();

    m_wxMaxima.m_lastPrompt = label;
    // remove the event maxima has just processed from the evaluation queue
    // if we remove a command from the evaluation queue the next output line
    // will be the first from the next command.
    m_wxMaxima.m_outputCellsFromCurrentCommand = 0;
    if (m_wxMaxima.GetWorksheet()->GetEvaluationQueue().Empty()) { // queue empty.
      // This worksheet has drained its evaluation queue, so a clean batch run
      // is done and may exit normally. Disarm exit-on-error for THIS worksheet
      // only -- m_exitOnError is process-wide and shared, so clearing it here
      // would disable exit-on-error in every other window of a --single_process
      // run (the multithreadtest hang).
      m_wxMaxima.m_exitOnErrorArmed = false;
      if (m_wxMaxima.m_maximaError)
        m_wxMaxima.StatusMaximaBusy(StatusBar::MaximaStatus::maximaerror);
      else
        m_wxMaxima.StatusMaximaBusy(StatusBar::MaximaStatus::waiting);
      // If we have selected a cell in order to show we are evaluating it
      // we should now remove this marker.
      if (m_wxMaxima.GetWorksheet()->FollowEvaluation()) {
        if (m_wxMaxima.GetWorksheet()->GetActiveCell())
          m_wxMaxima.GetWorksheet()->GetActiveCell()->SelectNone();
        m_wxMaxima.GetWorksheet()->ClearSelection();
      }
      m_wxMaxima.GetWorksheet()->FollowEvaluation(false);
      // Inform the user that the evaluation queue is empty.
      m_wxMaxima.EvaluationQueueLength(0);
      m_wxMaxima.GetWorksheet()->SetWorkingGroup(nullptr);
      m_wxMaxima.GetWorksheet()->GetEvaluationQueue().RemoveFirst();
      m_wxMaxima.GetWorksheet()->RequestRedraw();
      // Now that maxima is idle we can ask for the contents of its variables
      m_wxMaxima.m_evaluator.QueryVariableValue();
    } else { // we don't have an empty queue
      m_wxMaxima.m_ready = false;
      m_wxMaxima.GetWorksheet()->RequestRedraw();
      m_wxMaxima.GetWorksheet()->SetWorkingGroup(nullptr);
      m_wxMaxima.StatusMaximaBusy(StatusBar::MaximaStatus::sending);
      m_wxMaxima.m_evaluator.TriggerEvaluation();
    }

    if (m_wxMaxima.GetWorksheet()->GetEvaluationQueue().Empty()) {
      if ((m_wxMaxima.m_configuration.GetOpenHCaret()) &&
          (m_wxMaxima.GetWorksheet()->GetActiveCell() == NULL))
        m_wxMaxima.GetWorksheet()->OpenNextOrCreateCell();
    }
  } else { // We have a question
    m_wxMaxima.GetWorksheet()->SetLastQuestion(label);
    m_wxMaxima.GetWorksheet()->QuestionAnswered();
    m_wxMaxima.GetWorksheet()->QuestionPending(true);
    // If the user answers a question additional output might be required even
    // if the question has been preceded by many lines.
    m_wxMaxima.m_outputCellsFromCurrentCommand = 0;

    // The question label must reach the output list BEFORE OpenQuestionCaret
    // appends the answer EditorCell to the same list - the other way around
    // the answer editor shows up ABOVE the question it answers. The label's
    // tooltip depends on whether the question will be auto-answered, which
    // WillAutoAnswer() can tell without creating the answer cell.
    bool autoAnswer = m_wxMaxima.GetWorksheet()->WillAutoAnswer();

    if (!label.IsEmpty()) {
      int options = MaximaOutputAppender::AppendOpt::NewLine | MaximaOutputAppender::AppendOpt::BigSkip;
      if (!autoAnswer)
        options |= MaximaOutputAppender::AppendOpt::PromptToolTip;

      if (std::max(label.Find(m_wxMaxima.m_mathPrefix1), label.Find(m_wxMaxima.m_mathPrefix2)) >= 0)
        m_wxMaxima.m_outputAppender.DoConsoleAppend(label, MC_TYPE_PROMPT, MaximaOutputAppender::AppendOpt(options));
      else
        m_wxMaxima.m_outputAppender.DoRawConsoleAppend(label, MC_TYPE_PROMPT, MaximaOutputAppender::AppendOpt(options));
    }

    m_wxMaxima.GetWorksheet()->OpenQuestionCaret();

    if (m_wxMaxima.GetWorksheet()->ScrolledAwayFromEvaluation()) {
      if (m_wxMaxima.GetWorksheet()->m_mainToolBar)
        m_wxMaxima.GetWorksheet()->m_mainToolBar->EnableTool(ToolBar::tb_follow, true);
    }

    if (!autoAnswer) {
      if ((m_wxMaxima.GetWorksheet()->GetWorkingGroup() == NULL) ||
          ((m_wxMaxima.GetWorksheet()->GetWorkingGroup()->m_knownAnswers.empty()) &&
           m_wxMaxima.GetWorksheet()->GetWorkingGroup()->AutoAnswer()))
        m_wxMaxima.GetWorksheet()->SetNotification(_("Maxima asks a question!"),
                                        wxICON_INFORMATION);
      m_wxMaxima.StatusMaximaBusy(StatusBar::MaximaStatus::userinput);
    }
  }
  label.Trim(false);
  if (label.StartsWith(wxS("MAXIMA>")) || label.StartsWith("(dbm:")) {
    if (!m_wxMaxima.m_configuration.InLispMode()) {
      if (label.StartsWith("(dbm:"))
        wxLogMessage(_("Switched to lisp mode after receiving a lisp debug prompt!"));
      else
        wxLogMessage(_("Switched to lisp mode after receiving a lisp prompt!"));
    }
    m_wxMaxima.m_configuration.InLispMode(true);
    // Status-bar pictogram for these special REPL modes. Set it LAST so it
    // wins over the userinput / main-prompt status the branches above set for
    // this prompt; a subsequent normal "(%iN)" prompt (e.g. after :top /
    // :continue leaves the debugger, or (to-maxima) leaves Lisp mode) sets a
    // non-special status and clears it. The debugger (dbm) wins over plain
    // Lisp mode (MAXIMA>).
    if (label.StartsWith(wxS("(dbm:")))
      m_wxMaxima.StatusMaximaBusy(StatusBar::MaximaStatus::debugging);
    else
      m_wxMaxima.StatusMaximaBusy(StatusBar::MaximaStatus::lispmode);
  } else {
    if (m_wxMaxima.m_configuration.InLispMode())
      wxLogMessage(_("Ended lisp mode after receiving a maxima prompt!"));
    m_wxMaxima.m_configuration.InLispMode(false);
  }
}

void MaximaResponseReader::EnterLdbMode() {
  if (m_wxMaxima.m_inLDB)
    return;
  m_wxMaxima.m_inLDB = true;
  // Show the "stopped in a debugger" pictogram in the status bar.
  m_wxMaxima.StatusMaximaBusy(StatusBar::MaximaStatus::debugging);
  wxLogMessage(_("Maxima's Lisp entered sbcl's low-level debugger (LDB)."));
  m_wxMaxima.m_outputAppender.DoRawConsoleAppend(
    _("Maxima's Lisp (sbcl) has stopped in its low-level debugger (LDB). "
      "The Lisp cannot continue; you can enter LDB commands - for example "
      "\"backtrace\" for a stack trace, or \"exit\" to quit it."),
    MC_TYPE_ERROR);
}

void MaximaResponseReader::ReadStdErr() {
  // Maxima sends us its actual results over the network socket, not via its
  // stdout/stderr. But those streams are not silent after startup: in
  // particular Maxima forwards the stdout and stderr of the gnuplot process it
  // launches, so plotting errors and warnings (and the "End of animation
  // sequence" / Fontconfig / QSocketNotifier chatter filtered below) arrive
  // here. We also want to surface anything that turns up if something is
  // severely broken. Hence we keep reading and reporting both streams.

  if (m_wxMaxima.m_maximaProcess == NULL)
    return;

  if (m_wxMaxima.m_maximaProcess->IsInputAvailable()) {
    wxASSERT_MSG(
                 m_wxMaxima.m_maximaStdout != NULL,
                 wxS("Bug: Trying to read from maxima but don't have an input stream"));
    if(m_wxMaxima.m_maximaStdout == NULL)
      return;
    wxTextInputStream istrm(*m_wxMaxima.m_maximaStdout, wxS('\t'),
                            wxConvAuto(wxFONTENCODING_UTF8));
    wxString o;
    wxChar ch;
    while (((ch = istrm.GetChar()) != wxS('\0')) && (m_wxMaxima.m_maximaStdout->CanRead()))
      o += ch;

    wxString o_trimmed = o;
    o_trimmed.Trim();

    // sbcl prints the LDB banner on stdout when the Lisp runtime drops into
    // its low-level debugger. (Note: stdout is block-buffered, so the banner is
    // often stuck in sbcl's buffer and never reaches us - the stderr prompt
    // below is the reliable trigger. We still key on the banner when it does
    // arrive.)
    if (LdbSupport::ContainsLdbBanner(o))
      EnterLdbMode();

    o = _("Message from the stdout of Maxima: ") + o;
    if ((o_trimmed != wxEmptyString) &&
        (!o_trimmed.StartsWith("Connecting Maxima to server on port")) && (!m_wxMaxima.m_first)) {
      m_wxMaxima.m_outputAppender.DoRawConsoleAppend(o, MC_TYPE_DEFAULT);
      if (Maxima::GetPipeToStdErr())
        std::cerr << o;
    }
  }
  if (m_wxMaxima.m_maximaProcess->IsErrorAvailable()) {
    wxASSERT_MSG(m_wxMaxima.m_maximaStderr != NULL,
                 wxS("Bug: Trying to read from maxima but don't have a error "
                     "input stream"));
    if(m_wxMaxima.m_maximaStderr == NULL)
      return;
    wxTextInputStream istrm(*m_wxMaxima.m_maximaStderr, wxS('\t'),
                            wxConvAuto(wxFONTENCODING_UTF8));
    wxString o;
    wxChar ch;
    while (((ch = istrm.GetChar()) != wxS('\0')) && (m_wxMaxima.m_maximaStderr->CanRead()))
      o += ch;

    wxString o_trimmed = o;
    o_trimmed.Trim();

    // While sbcl is in LDB it writes its prompt ("ldb> ") and the output of
    // LDB commands to stderr. Show it as-is, but do NOT run it through the
    // normal error handling: AbortOnError / TriggerEvaluation drive the
    // control socket, which is dead while the Lisp is stopped in LDB.
    if (m_wxMaxima.m_inLDB || LdbSupport::LooksLikeLdb(o)) {
      EnterLdbMode();
      if (o_trimmed != wxEmptyString)
        m_wxMaxima.m_outputAppender.DoRawConsoleAppend(o, MC_TYPE_ERROR);
      if (Maxima::GetPipeToStdErr())
        std::cerr << o;
      // A trailing "ldb> " means LDB is waiting for a command. Give the user an
      // input line (reusing the question-answer caret); MaximaEvaluator routes
      // what they type there to Maxima's stdin while m_inLDB is set. Only open
      // one at a time - answering it clears the answer cell, and the next
      // prompt opens a fresh one.
      if (LdbSupport::EndsWithLdbPrompt(o) && m_wxMaxima.GetWorksheet() &&
          !m_wxMaxima.GetWorksheet()->GetDocumentCellPointers().GetAnswerCell()) {
        // Engage the full question machinery, not just the caret: without
        // QuestionPending a click into the (empty, easily deactivated) answer
        // editor is treated as an ordinary output click, and there is no way
        // to get back into it to answer the prompt.
        m_wxMaxima.GetWorksheet()->QuestionPending(true);
        m_wxMaxima.GetWorksheet()->OpenQuestionCaret();
        if (EditorCell *answerCell =
              m_wxMaxima.GetWorksheet()->GetDocumentCellPointers().GetAnswerCell())
          m_wxMaxima.GetWorksheet()->SetActiveCell(answerCell);
      }
      return;
    }

    o = wxS("Message from maxima's stderr stream: ") + o;

    if ((o != wxS("Message from maxima's stderr stream: End of animation sequence")) &&
        (o != wxS("Message from maxima's stderr stream: Fontconfig warning: using without calling FcInit()")) &&  // harmless warning, which may occur with Gnuplot 6 and fontconfig
        (o != wxS("Message from maxima's stderr stream: QSocketNotifier: Can only be used with threads started with QThread")) &&  // Maybe related to Gnuplot / Wayland?
        !o.Contains("frames in animation sequence") &&
        (o_trimmed != wxEmptyString) && (o.Length() > 1)) {
      m_wxMaxima.m_outputAppender.DoRawConsoleAppend(o, MC_TYPE_ERROR);
      m_wxMaxima.m_evaluator.AbortOnError();
      m_wxMaxima.m_evaluator.TriggerEvaluation();
      if(m_wxMaxima.GetWorksheet())
        m_wxMaxima.GetWorksheet()->GetErrorList().Add(m_wxMaxima.GetWorksheet()->GetWorkingGroup(true));

      if (Maxima::GetPipeToStdErr())
        std::cerr << o;
    } else
      m_wxMaxima.m_outputAppender.DoRawConsoleAppend(o, MC_TYPE_DEFAULT);
  }
}


void MaximaResponseReader::ReadVariables(const wxXmlDocument &xmldoc) {
  if(!xmldoc.IsOk())
    {
      m_wxMaxima.m_outputAppender.DoRawConsoleAppend(_("There was an error in the XML that should describe the contents of some variables.\n"
                           "Please report this as a bug to the wxMaxima project."),
                         MC_TYPE_ERROR);
      m_wxMaxima.m_evaluator.AbortOnError();
    }
  else
    {
      const std::vector<MaximaVariableUpdate> updates =
        ParseMaximaVariableUpdates(xmldoc);

      for (const MaximaVariableUpdate &update : updates) {
        if (update.m_bound) {
          if(m_wxMaxima.GetWorksheet() && (m_wxMaxima.m_variablesPane))
            m_wxMaxima.m_variablesPane->VariableValue(update.m_name, update.m_value);

          // A gcl-compiled maxima reports values with a trailing newline,
          // which ended up verbatim in anything treating the value as a
          // path (most prominently: a maxima_userdir named "...\n").
          // Trim before the quote-strip so a newline after the closing
          // quote cannot defeat it, either.
          wxString value = update.m_value;
          value.Trim(true).Trim(false);
          // Undo an eventual stringdisp:true adding quoting marks to strings
          if (value.StartsWith("\"") && value.EndsWith("\""))
            value = value.SubString(1, value.Length() - 2);

          SyncMenusToMaximaVariable(m_wxMaxima.GetMenuBar(), update.m_name, value);
          auto varFunc = m_variableReadActions.find(update.m_name);
          if (varFunc != m_variableReadActions.end())
            std::invoke(varFunc->second, this, value);
        } else {
          if(m_wxMaxima.GetWorksheet() && (m_wxMaxima.m_variablesPane))
            m_wxMaxima.m_variablesPane->VariableUndefined(update.m_name);
          auto varFunc = m_variableUndefinedActions.find(update.m_name);
          if (varFunc != m_variableUndefinedActions.end())
            std::invoke(varFunc->second, this);
        }
      }

      if (updates.size() > 1)
        wxLogMessage(_("Maxima sends a new set of auto-completable symbols."));
      else
        wxLogMessage(_("Maxima has sent a new variable value."));
    }
  m_wxMaxima.m_evaluator.TriggerEvaluation();
  m_wxMaxima.m_evaluator.QueryVariableValue();
}

void MaximaResponseReader::VariableActionSinnpiflag(const wxString &WXUNUSED(value)) {
  m_wxMaxima.m_fourierLoaded = true;
}

void MaximaResponseReader::VariableActionSinnpiflagUndefined() { m_wxMaxima.m_fourierLoaded = false; }

void MaximaResponseReader::VariableActionUserDir(const wxString &value) {
  Dirstructure::Get()->UserConfDir(value);
  m_wxMaxima.m_history->SetSavePlace(value);
  wxLogMessage(_("Maxima user configuration is located in directory %s"), value);
}

void MaximaResponseReader::VariableActionDisplay2d_Unicode(const wxString &value) {
 if (value == wxS("true"))
   m_wxMaxima.m_configuration.Display2d_Unicode(true);
 if (value == wxS("false"))
   m_wxMaxima.m_configuration.Display2d_Unicode(false);
 }

void MaximaResponseReader::VariableActionTempDir(const wxString &value) {
  m_wxMaxima.m_maximaTempDir = value;
  wxLogMessage(_("Maxima uses temp directory %s"), value);
  if (!wxDirExists(value)) {
    // Sometimes people delete their temp dir
    // and gnuplot won't create a new one for them.
    wxMkdir(value, wxS_DIR_DEFAULT);
  }
}

void MaximaResponseReader::VariableActionAutoconfVersion(const wxString &value) {
  if(m_wxMaxima.GetWorksheet())
    m_wxMaxima.m_configuration.SetMaximaVersion(value);
  wxLogMessage(_("Maxima version: %s"), value);
}
void MaximaResponseReader::VariableActionAutoconfHost(const wxString &value) {
  m_wxMaxima.m_configuration.SetMaximaArch(value);
  wxLogMessage(_("Maxima architecture: %s"), value);
}
void MaximaResponseReader::VariableActionMaximaInfodir(const wxString &value) {
  // Make sure that we get out all ".." and "~" of the path as they seem to
  // confuse the help browser logic
  wxLogMessage(_("Maxima's manual is located in directory %s"),
               value);
}

void MaximaResponseReader::VariableActionMaximaHtmldir(const wxString &value) {
  m_wxMaxima.m_maximaHtmlDir = value;
  wxFileName dir(value);
  dir.MakeAbsolute();
  wxString dir_canonical = dir.GetPath();
  wxLogMessage(_("Maxima's HTML manuals are in directory %s"),
               dir_canonical);
  if(m_wxMaxima.GetWorksheet())
    {
      m_wxMaxima.GetWorksheet()->SetMaximaDocDir(dir_canonical);
      // Interactive-only: don't spend a background thread parsing the whole HTML
      // manual to cache its anchors during a batch run (pointless, and its
      // shutdown join wedged the multithreadtest CI run). Deferred to
      // ExitAfterEval() if/when the session turns interactive.
      if (m_wxMaxima.IsInteractive())
        m_wxMaxima.GetWorksheet()->LoadHelpFileAnchors(dir_canonical,
                                            m_wxMaxima.m_configuration.GetMaximaVersion());
    }
}

void MaximaResponseReader::VariableActionGnuplotCommand(const wxString &value) {
  m_wxMaxima.m_processManager.GnuplotCommandName(value);

  // A Maxima restart re-sends gnuplot_command. If it resolves to the same
  // gnuplot we already probed, its capabilities haven't changed: skip the
  // probe process. ConfigChanged() pushes the cached UsePngCairo() answer to
  // the new Maxima regardless.
  if (!m_wxMaxima.m_gnuplotProbedCommand.IsEmpty() &&
      (m_wxMaxima.m_gnuplotcommand == m_wxMaxima.m_gnuplotProbedCommand)) {
    wxLogMessage(_("Not re-querying gnuplot's graphics drivers: same gnuplot as before."));
    return;
  }

  wxLogMessage(_("Querying gnuplot which graphics drivers it supports."));
  wxEnvVariableHashMap environment;
  // gnuplot uses the PAGER variable only on un*x - and on un*x there is cat.
  environment["PAGER"] = "cat";
  environment["LINES"] = "10000";
  environment["LESS_LINES"] = "10000";
  if(!Configuration::GetMaximaLang().IsEmpty())
    environment["LANG"] = Configuration::GetMaximaLang();
  wxGetEnvMap(&environment);

  // A Maxima restart re-sends gnuplot_command, so a previous terminal query
  // may still be in flight. Detach it (it then deletes itself when its child
  // exits) instead of overwriting the pointer: its termination event would
  // otherwise be handled against the NEW process - reading the wrong streams
  // and deleting a wxProcess whose child is still running.
  if (m_wxMaxima.m_gnuplotTerminalQueryProcess)
    m_wxMaxima.m_gnuplotTerminalQueryProcess->Detach();
  m_wxMaxima.m_gnuplotTerminalQueryProcess =
    new wxProcess(&m_wxMaxima, EventIDs::gnuplot_query_terminals_id);
  m_wxMaxima.m_gnuplotTerminalQueryProcess->Redirect();
  std::unique_ptr<wxExecuteEnv> env(new wxExecuteEnv);
  env->env = std::move(environment);
#ifdef __WINDOWS__
  // On Windows the terminal-query uses the command-line gnuplot.exe
  // (m_wxMaxima.m_gnuplotcommand_commandline, populated by GnuplotCommandName) rather than
  // the possibly-GUI "wgnuplot" so the query process exits on its own instead of
  // staying around until someone closes its terminal window.
  wxString gnuplot_query =
      m_wxMaxima.m_gnuplotcommand_commandline + " -e \"print GPVAL_TERMINALS\"";
#else
  wxString gnuplot_query = m_wxMaxima.m_gnuplotcommand + " -e \"print GPVAL_TERMINALS\"";
#endif
  if (wxExecute(gnuplot_query,
                wxEXEC_ASYNC | wxEXEC_HIDE_CONSOLE | wxEXEC_MAKE_GROUP_LEADER,
                m_wxMaxima.m_gnuplotTerminalQueryProcess, env.get()) < 0)
    wxLogMessage(_("Cannot start gnuplot"));
}

void MaximaResponseReader::VariableActionMaximaSharedir(const wxString &value) {
  wxString dir = value;
  dir.Trim(true);
  m_wxMaxima.m_configuration.MaximaShareDir(dir);
  wxLogMessage(_("Maxima's share files are located in directory %s"),
               dir);
  /// READ FUNCTIONS FOR AUTOCOMPLETION
  if(m_wxMaxima.GetWorksheet() && m_wxMaxima.IsInteractive())
    m_wxMaxima.GetWorksheet()->LoadSymbols();
}

void MaximaResponseReader::VariableActionMaximaDemodir(const wxString &value) {
  wxString dir = value;
  dir.Trim(true);
  m_wxMaxima.m_configuration.MaximaDemoDir(dir);
  wxLogMessage(_("Maxima's demo files are located in directory %s"),
               dir);
}

void MaximaResponseReader::VariableActionLispName(const wxString &value) {
  m_wxMaxima.m_configuration.SetLispType(value);
  wxLogMessage(_("Maxima was compiled using %s"), value);
}
void MaximaResponseReader::VariableActionLispVersion(const wxString &value) {
  m_wxMaxima.m_configuration.SetLispVersion(value);
  wxLogMessage(_("Lisp version: %s"), value);
}
void MaximaResponseReader::VariableActionWxLoadFileName(const wxString &value) {
  m_wxMaxima.m_recentPackages.AddDocument(value);
  m_wxMaxima.UpdateRecentDocuments();
  wxLogMessage(_("Maxima has loaded the file %s."), value);
  m_wxMaxima.m_updateAutocompletion = true;
}

void MaximaResponseReader::VariableActionEngineeringFormat(const wxString &value) {
  // The menu check mark is table-synced (MaximaMenuSync); the value is
  // additionally mirrored here so the menu handler can toggle it correctly.
  m_wxMaxima.m_maximaVariable_engineeringFormat = value;
}
void MaximaResponseReader::VariableActionHtmlHelp(const wxString &value) {
  if (value == wxS("text")) {
    if (!m_wxMaxima.m_HelpMenu->IsChecked(EventIDs::menu_maxima_uses_internal_help))
      m_wxMaxima.m_HelpMenu->Check(EventIDs::menu_maxima_uses_internal_help, true);
    m_wxMaxima.m_configuration.MaximaHelpFormat(Configuration::maxima);
  }
  if (value == wxS("html")) {
    if (!m_wxMaxima.m_HelpMenu->IsChecked(EventIDs::menu_maxima_uses_html_help))
      m_wxMaxima.m_HelpMenu->Check(EventIDs::menu_maxima_uses_html_help, true);
    m_wxMaxima.m_configuration.MaximaHelpFormat(Configuration::browser);
  }
  if(m_wxMaxima.m_configuration.OfferInternalHelpBrowser())
    {
      if ((value == wxS("wxmaxima")) || (value == wxS("frontend"))) {
        if (!m_wxMaxima.m_HelpMenu->IsChecked(EventIDs::menu_maxima_uses_wxmaxima_help))
          m_wxMaxima.m_HelpMenu->Check(EventIDs::menu_maxima_uses_wxmaxima_help, true);
        m_wxMaxima.m_configuration.MaximaHelpFormat(Configuration::frontend);
      }
    }
}

void MaximaResponseReader::VariableActionDisplay2D(const wxString &value) {
  if (m_wxMaxima.m_maximaVariable_display2d != value) {
    m_wxMaxima.m_maximaVariable_display2d = value;
    UpdateDisplayMode();
  }
}
void MaximaResponseReader::VariableActionAltDisplay2D(const wxString &value) {
  if (m_wxMaxima.m_maximaVariable_altdisplay2d != value) {
    m_wxMaxima.m_maximaVariable_altdisplay2d = value;
    UpdateDisplayMode();
  }
}

void MaximaResponseReader::UpdateDisplayMode() {
  if (m_wxMaxima.m_maximaVariable_display2d == wxS("false")) {
    m_wxMaxima.m_configuration.DisplayMode(Configuration::display_1dASCII);
    m_wxMaxima.m_equationTypeMenuMenu->Check(EventIDs::menu_math_as_1D_ASCII, true);
  } else {
    if (m_wxMaxima.m_maximaVariable_altdisplay2d == wxS("false")) {
      if(m_wxMaxima.m_configuration.Display2d_Unicode())
        {
          m_wxMaxima.m_configuration.DisplayMode(Configuration::display_2dUNICODE);
          m_wxMaxima.m_equationTypeMenuMenu->Check(EventIDs::menu_math_as_2D_UNICODE, true);
        } else {
        m_wxMaxima.m_configuration.DisplayMode(Configuration::display_2dASCII);
        m_wxMaxima.m_equationTypeMenuMenu->Check(EventIDs::menu_math_as_2D_ASCII, true);
      }
    } else {
      m_wxMaxima.m_configuration.DisplayMode(Configuration::display_2d);
      m_wxMaxima.m_equationTypeMenuMenu->Check(EventIDs::menu_math_as_graphics, true);
    }
  }
}

void MaximaResponseReader::VariableActionOperators(const wxString &value) {
  wxXmlDocument xmldoc;
  wxString newOperators;
  wxStringInputStream xmlStream(value);
  {
    xmldoc.Load(xmlStream);
  }
  if(!xmldoc.IsOk())
    {
      m_wxMaxima.m_outputAppender.DoRawConsoleAppend(_("There was an error in the XML that should contain the list of operators.\n"
                           "Please report this as a bug to the wxMaxima project."),
                         MC_TYPE_ERROR);
      m_wxMaxima.m_evaluator.AbortOnError();
    }
  else
    {
      wxXmlNode *node = xmldoc.GetRoot();
      if (node != NULL) {
        wxXmlNode *contents = node->GetChildren();
        while (contents) {
          if (contents->GetName() == wxS("operator")) {
            wxXmlNode *innernode = contents->GetChildren();
            if (innernode) {
              wxString content = innernode->GetContent();
              if ((!content.IsEmpty()) &&
                  (!m_wxMaxima.m_configuration.IsOperator(content))) {
                if ((content.at(0) > '9') || (content.at(0) < '0')) {
                  m_wxMaxima.m_configuration.AddMaximaOperator(content);
                  if (!newOperators.IsEmpty())
                    newOperators += wxS(", ");
                  newOperators += content;
                }
              }
            }
          }
          contents = contents->GetNext();
        }
        if (!newOperators.IsEmpty()) {
          wxLogMessage(_("New maxima Operators detected: %s"), newOperators);
          if(m_wxMaxima.GetWorksheet())
            m_wxMaxima.GetWorksheet()->RequestRecalculation();
        }
      }
    }
}

void MaximaResponseReader::ReadAddVariables(const wxXmlDocument &xmldoc) {
  wxLogMessage(_("Maxima sends us a new set of variables for the watch list."));
  if(!xmldoc.IsOk())
    {
      m_wxMaxima.m_outputAppender.DoRawConsoleAppend(_("There was an error in the XML that should contain a list of watch variables.\n"
                           "Please report this as a bug to the wxMaxima project."),
                         MC_TYPE_ERROR);
      m_wxMaxima.m_evaluator.AbortOnError();
    }
  else
    {
      for (const wxString &name : ParseWatchVariableAdditions(xmldoc)) {
        if(m_wxMaxima.GetWorksheet() && (m_wxMaxima.m_variablesPane))
          m_wxMaxima.m_variablesPane->AddWatch(name);
      }
  }
}


// The variable-action dispatch tables. Static: they map a Maxima variable name
// to the handler above and never change once populated, so one copy is shared
// by every wxMaxima frame.
MaximaResponseReader::VarReadFunctionHash
  MaximaResponseReader::m_variableReadActions;
MaximaResponseReader::VarUndefinedFunctionHash
  MaximaResponseReader::m_variableUndefinedActions;

void MaximaResponseReader::RegisterVariableActions() {
  // Variables whose value only toggles menu check marks are not listed here:
  // they are handled by the declarative table in MaximaMenuSync.cpp.
  if (m_variableReadActions.empty()) {
    m_variableReadActions[wxS("display2d_unicode")] =
      &MaximaResponseReader::VariableActionDisplay2d_Unicode;
    m_variableReadActions[wxS("maxima_userdir")] =
      &MaximaResponseReader::VariableActionUserDir;
    m_variableReadActions[wxS("sinnpiflag")] =
      &MaximaResponseReader::VariableActionSinnpiflag;
    m_variableReadActions[wxS("maxima_tempdir")] =
      &MaximaResponseReader::VariableActionTempDir;
    m_variableReadActions[wxS("*autoconf-version*")] =
      &MaximaResponseReader::VariableActionAutoconfVersion;
    m_variableReadActions[wxS("*autoconf-host*")] =
      &MaximaResponseReader::VariableActionAutoconfHost;
    m_variableReadActions[wxS("*maxima-infodir*")] =
      &MaximaResponseReader::VariableActionMaximaInfodir;
    m_variableReadActions[wxS("*maxima-htmldir*")] =
      &MaximaResponseReader::VariableActionMaximaHtmldir;
    m_variableReadActions[wxS("gnuplot_command")] =
      &MaximaResponseReader::VariableActionGnuplotCommand;
    m_variableReadActions[wxS("*maxima-sharedir*")] =
      &MaximaResponseReader::VariableActionMaximaSharedir;
    m_variableReadActions[wxS("*maxima-demodir*")] =
      &MaximaResponseReader::VariableActionMaximaDemodir;
    m_variableReadActions[wxS("*lisp-name*")] =
      &MaximaResponseReader::VariableActionLispName;
    m_variableReadActions[wxS("*lisp-version*")] =
      &MaximaResponseReader::VariableActionLispVersion;
    m_variableReadActions[wxS("*wx-load-file-name*")] =
      &MaximaResponseReader::VariableActionWxLoadFileName;
    m_variableReadActions[wxS("output_format_for_help")] =
      &MaximaResponseReader::VariableActionHtmlHelp;
    m_variableReadActions[wxS("engineering_format_floats")] =
      &MaximaResponseReader::VariableActionEngineeringFormat;
    m_variableReadActions[wxS("display2d")] =
      &MaximaResponseReader::VariableActionDisplay2D;
    m_variableReadActions[wxS("*alt-display2d*")] =
      &MaximaResponseReader::VariableActionAltDisplay2D;
    m_variableReadActions[wxS("*maxima-operators*")] =
      &MaximaResponseReader::VariableActionOperators;
  }

  if (m_variableUndefinedActions.empty()) {
    m_variableUndefinedActions[wxS("sinnpiflag")] =
      &MaximaResponseReader::VariableActionSinnpiflagUndefined;
  }
}
