#############################################################################
##
##  This file is part of GAP, a system for computational discrete algebra.
##
##  Copyright of GAP belongs to its developers, whose names are too numerous
##  to list here. Please refer to the COPYRIGHT file for details.
##
##  SPDX-License-Identifier: GPL-2.0-or-later
##

HaveMultiThreadedUI := true;

ENTER_NAMESPACE("ConsoleUI");

ControlThread@ := false;
MakeThreadLocal("ControlThread@");
MakeThreadLocal("MyOutputPrefix@");

MakeThreadLocal("ThreadInfo@");
MakeThreadLocal("InputStream@");
MakeThreadLocal("OutputStream@");

ProgramShutdown@ := StartHandShake();

ActiveThread@ := -1;
NumShellThreads@ := 1;
NeedPrompt@ := true;
DelayedPrompt@ := Immutable("");

ThreadControlChannel@ := fail;
ThreadInputChannel@ := fail;
WaitForThread@ := fail;
ThreadObject@ := fail;
OutputHistory@ := fail;
OutputHistoryIncompleteLine@ := fail;
OutputPrefix@ := fail;
OutputPrefixRaw@ := fail;
ThreadName@ := fail;
ThreadNameToID@ := fail;
Prompt@ := fail;
ShowBackgroundOutput@ := fail;
ShownOutput@ := fail;
PendingOutput@ := fail;
BindGlobal("Region@", ShareSpecialObj("ConsoleUI"));

BindGlobal("InitThreadTables@", function()
  ThreadControlChannel@ConsoleUI := [];
  ThreadInputChannel@ConsoleUI := [];
  WaitForThread@ConsoleUI := [];
  ThreadObject@ConsoleUI := [];
  OutputHistory@ConsoleUI := [];
  OutputHistoryIncompleteLine@ConsoleUI := [];
  OutputPrefix@ConsoleUI := [];
  OutputPrefixRaw@ConsoleUI := [];
  ThreadName@ConsoleUI := [];
  ThreadNameToID@ConsoleUI := rec();
  Prompt@ConsoleUI := [];
  ShowBackgroundOutput@ConsoleUI := [];
  ShownOutput@ConsoleUI := [];
  PendingOutput@ConsoleUI := [];
end);

DefaultShowBackgroundOutput@ := false;
OutputHistoryLength@ := 100;
DefaultOutputPrefix@ := MakeImmutable("[%name%] ");
DefaultPrompt@ := MakeImmutable("[%name%] gap> ");

#V ControlChannel@ - channel to send commands to the main thread
#V OutputChannel@ - channel to send output/commands to the output thread
#V InputChannel@ - channel to receive input from the input thread
#V PromptChannel@ - channel to send prompts to the input thread

BindGlobal("ControlChannel@", CreateChannel(10000));
BindGlobal("OutputChannel@", CreateChannel(10000));
BindGlobal("InputChannel@", CreateChannel(10000));
BindGlobal("PromptChannel@", CreateChannel(10000));

# define constants for the main control channel

BindGlobal("REGISTER_THREAD@", 0);
BindGlobal("UNREGISTER_THREAD@", 1);
BindGlobal("HAVE_OUTPUT@", 2);
BindGlobal("HAVE_INPUT@", 3);
BindGlobal("EXPECT_INPUT@", 4);

BindGlobal("ThreadID@", function()
  return ThreadID(CurrentThread()) + 1;
end);

BindGlobal("SubstituteVariables@", function(string, threadid)
  local result;
  result := ReplacedString(string, "%id%", String(threadid-1));
  result := ReplacedString(result, "%name%", ThreadName@[threadid]);
  return result;
end);

BindGlobal("Debug@", function(arg)
  local text, value;
  text := "<# ";
  for value in arg do
    Append(text, String(value));
  od;
  Append(text, "#>\n");
  WRITE_STRING_FILE_NC(2, text);
end);

BindGlobal("SystemMessage@", function(arg)
  local text, value;
  text := "--- ";
  for value in arg do
    Append(text, String(value));
  od;
  Add(text, '\n');
  SendChannel(OutputChannel@, [ -1, "", text ] );
end);

BindGlobal("FindThread@", function(id)
  local i;
  if IS_INT(id) then
    if IsBound(ThreadName@[id+1]) then
      return id+1;
    else
      return fail;
    fi;
  else
    if IsBound(ThreadNameToID@.(id)) then
      return ThreadNameToID@.(id);
    else
      for i in [1..Length(id)] do
        if id[i] < '0' or id[i] > '9' then
          return fail;
        fi;
      od;
      id := SMALLINT_STR(id);
      if IsBound(ThreadName@[id+1]) then
        return id+1;
      else
        return fail;
      fi;
    fi;
  fi;
end);

BindGlobal("SendControl@", function(type, data)
  SendChannel(ControlChannel@, MakeReadOnlyObj([ type, ThreadID@(), data ]) );
end);

BindGlobal("RegisterThread@", function()
  SendControl@(REGISTER_THREAD@, ThreadInfo@);
end);

BindGlobal("UnregisterThread@", function(is_shell)
  SendControl@(UNREGISTER_THREAD@, is_shell);
end);

BindGlobal("UnregisterBackgroundThread@", function()
  SendControl@(UNREGISTER_THREAD@, false);
end);


BindGlobal("ChannelInputStream@", function(channel)
  return InputTextCustom(channel, function(channel)
    Print("\c");
    SendControl@(EXPECT_INPUT@, CPROMPT());
    return ReceiveChannel(channel);
  end, ReturnTrue);
end);

BindGlobal("ChannelOutputStream@", function()
  local result;
  result := OutputTextCustom([ ], function(state, string)
    SendControl@(HAVE_OUTPUT@, ShallowCopy(string));
  end, ReturnTrue);
  result!.formatting := true;
  return result;
end);

BindGlobal("DirectChannelOutputStream@", function()
  MyOutputPrefix@ := "<";
  Append(MyOutputPrefix@, String(ThreadID@()));
  Append(MyOutputPrefix@, "> ");
  return OutputTextCustom(OutputChannel@, function(channel, string)
    SendChannel(channel,
      [ ThreadID@(), MyOutputPrefix@, ShallowCopy(string) ] );
  end, ReturnTrue);
end);


BindGlobal("NewThreadInfo@", function()
  return AtomicRecord(rec(
    InputChannel := CreateChannel(),
    ControlChannel := CreateChannel(),
    ThreadID := ThreadID@(),
    ThreadObject := CurrentThread(),
  ));
end);

BindGlobal("SetupDefaultStreams@", function()
  UnbindGlobal("DEFAULT_INPUT_STREAM");
  BindGlobal("DEFAULT_INPUT_STREAM", function()
    if not IsBound(InputStream@) then
      if ControlThread@ <> false then
        InputStream@ := InputTextNone();
      else
        if not IsBound(ThreadInfo@) then
          ThreadInfo@ := NewThreadInfo@();
          RegisterThread@();
          AtThreadExit(UnregisterBackgroundThread@);
        fi;
        InputStream@ :=
          ChannelInputStream@(ThreadInfo@.InputChannel);
      fi;
    fi;
    return InputStream@;
  end);

  UnbindGlobal("DEFAULT_OUTPUT_STREAM");
  BindGlobal("DEFAULT_OUTPUT_STREAM", function()
    if not IsBound(OutputStream@) then
      if ControlThread@ then
        OutputStream@ := DirectChannelOutputStream@();
      else
        if not IsBound(ThreadInfo@) then
          ThreadInfo@ := NewThreadInfo@();
          RegisterThread@();
          AtThreadExit(UnregisterBackgroundThread@);
        fi;
        OutputStream@ :=
          ChannelOutputStream@();
      fi;
    fi;
    return OutputStream@;
  end);
end);

BindGlobal("ThreadExit@", function()
  if IsBound(OutputStream@) then
    CloseStream(OutputStream@);
  fi;
  if IsBound(InputStream@) then
    CloseStream(InputStream@);
  fi;
end);

BindGlobal("CompleteThreadRegistration@", function(threadinfo, waitfor)
  local threadid;
  threadid := threadinfo.ThreadID;
  ThreadControlChannel@[threadid] := threadinfo.ControlChannel;
  ThreadInputChannel@[threadid] := threadinfo.InputChannel;
  if not IsBound(ThreadName@[threadid]) then
    ThreadName@[threadid] := Immutable(String(threadid-1));
  fi;
  WaitForThread@[threadid] := waitfor;
  ThreadObject@[threadid] := threadinfo.ThreadObject;
  if not IsBound(OutputHistory@[threadid]) then
    OutputHistory@[threadid] := "";
    OutputHistoryIncompleteLine@[threadid] := false;
    ShownOutput@[threadid] := 0;
    PendingOutput@[threadid] := false;
  fi;
  ShowBackgroundOutput@[threadid] := DefaultShowBackgroundOutput@;
  Prompt@[threadid] := SubstituteVariables@(DefaultPrompt@, threadid);
  OutputPrefixRaw@[threadid] := DefaultOutputPrefix@;
  OutputPrefix@[threadid] :=
    SubstituteVariables@(DefaultOutputPrefix@, threadid);
end);

BindGlobal("StartInteractiveThread@", function()
  local handshake, threadinfo;
  handshake := StartHandShake();
  NumShellThreads@ := NumShellThreads@ + 1;
  CreateThread(function(handshake)
    local threadinfo;
    threadinfo := NewThreadInfo@();
    ThreadInfo@ := threadinfo;
    AcknowledgeHandShake(handshake, threadinfo);
    AtThreadExit(ThreadExit@);
    SESSION();
    UnregisterThread@(true);
  end, handshake);
  threadinfo := CompleteHandShake(handshake);
  CompleteThreadRegistration@(threadinfo, true);
  return threadinfo;
end);

BindGlobal("CullHistory@", function(threadid)
  local newlines, pos, history;
  history := OutputHistory@[threadid];
  newlines := FIND_ALL_IN_STRING(history, "\n");
  if 2 * Length(newlines) >= OutputHistoryLength@ * 3 then
    pos := newlines[Length(newlines)-OutputHistoryLength@]+1;
    OutputHistory@[threadid] :=
      history{[pos..Length(history)]};
  fi;
end);

BindGlobal("OutputContext@", function(lines, thread)
  local history, incomplete, newlines, from;
  history := OutputHistory@[thread];
  incomplete := OutputHistoryIncompleteLine@[thread];
  if incomplete then lines := lines - 1; fi;
  newlines := FIND_ALL_IN_STRING(history, "\n");
  if Length(newlines) > lines then
    from := newlines[Length(newlines)-lines]+1;
    if from > ShownOutput@[thread] then
      from := ShownOutput@[thread];
    fi;
    history := history{[from..Length(history)]};
  else
    history := ShallowCopy(history);
  fi;
  return ReplacedString(history, "\r", "");
end);

BindGlobal("PrintContext@", function(lines, thread)
  local history;
  history := OutputContext@(lines, thread);
  SendChannel(OutputChannel@, [ thread, OutputPrefix@[thread], history ]);
end);

BindGlobal("AddOutput@", function(threadid, text, is_prompt, deferred)
  local incomplete_line, old_incomplete_line, history;
  text := ShallowCopy(text);
  NORMALIZE_NEWLINES(text);
  if is_prompt then
    Add(text, '\r');
  fi;
  MakeImmutable(text);
  incomplete_line := not EndsWith(text, "\n");
  old_incomplete_line := OutputHistoryIncompleteLine@[threadid];
  history := OutputHistory@[threadid];
  Append(history, text);
  OutputHistoryIncompleteLine@[threadid] := incomplete_line;
  if not deferred then
    if threadid = ActiveThread@ or ShowBackgroundOutput@[threadid] then
      SendChannel(OutputChannel@,
        [ threadid, OutputPrefix@[threadid], text ] );
      CullHistory@(threadid);
      ShownOutput@[threadid] := Length(history);
      PendingOutput@[threadid] := false;
    else
      PendingOutput@[threadid] := true;
    fi;
  fi;
end);

BindGlobal("AddOutputCommand@", function(threadid, text)
  local history, prompt;
  if StartsWith(text, "!") and OutputHistoryIncompleteLine@[threadid] then
    DelayedPrompt@ := OutputContext@(1, threadid);
  fi;
  text := ShallowCopy(text);
  if not EndsWith(text, "\n") then
    Add(text, '\n');
  fi;
  history := OutputHistory@[threadid];
  Append(history, text);
  if threadid = ActiveThread@ then
    if OutputHistoryIncompleteLine@[threadid] then
      SendChannel(OutputChannel@,
        [ threadid, OutputPrefix@[threadid], 0 ]);
    fi;
    OutputHistoryIncompleteLine@[threadid] := false;
    CullHistory@(threadid);
    ShownOutput@[threadid] := Length(history);
    PendingOutput@[threadid] := false;
  fi;
end);

BindGlobal("WritePrompt@", function()
  if NeedPrompt@ then
    if OutputHistoryIncompleteLine@[ActiveThread@] then
      PrintContext@(1, ActiveThread@);
      DelayedPrompt@ := "";
      NeedPrompt@ := false;
    elif DelayedPrompt@ <> "" then
      AddOutput@(ActiveThread@, DelayedPrompt@, true, false);
      DelayedPrompt@ := "";
      NeedPrompt@ := false;
    fi;
  fi;
end);

BindGlobal("SwitchToThread@", function(thread)
  local history, shown;
  if DelayedPrompt@ <> "" then
    AddOutput@(ActiveThread@, DelayedPrompt@, true, true);
    DelayedPrompt@ := "";
  fi;
  ActiveThread@ := thread;
  SystemMessage@("Switching to thread ", thread-1);
  shown := ShownOutput@[thread];
  history := OutputHistory@[thread];
  if shown <> Length(history) then
    SendChannel(OutputChannel@,
      [ thread, OutputPrefix@[thread],
        history{[shown+1..Length(history)]} ] );
    CullHistory@(thread);
    ShownOutput@[thread] := Length(history);
    NeedPrompt@ := false;
  fi;
end);

BindGlobal("CommandTable@", DictionaryByList(true));
BindGlobal("AliasTable@", DictionaryByList(true));
atomic Region@ do
  MigrateObj(CommandTable@, Region@);
  MigrateObj(AliasTable@, Region@);
od;

BindGlobal("GetArg@", function(string)
  local arg, ch, i;
  while Length(string) > 0 and (string[1] = ' ' or string[1] = '\t') do
    Remove(string, 1);
  od;
  i := 1;
  while i <= Length(string) do
    ch := string[i];
    if ch = ' ' or ch = '\t' then
      arg := string{[1..i-1]};
      while i <= Length(string) do
        ch := string[i];
        if ch <> ' ' and ch <> '\t' then
          return [arg, string{[i..Length(string)]}];
        fi;
        i := i + 1;
      od;
      return [arg, ""];
    fi;
    i := i + 1;
  od;
  return [string, ""];
end);

DeclareGlobalFunction("RunCommand@");
DeclareGlobalFunction("RunCommandQuietly@");

BindGlobal("NameThread@", function(threadid, name)
  if IsBound(ThreadNameToID@.(name)) then
    SystemMessage@("The name '", name, "' is already in use by thread ",
      ThreadNameToID@.(name)-1);
    return;
  fi;
  if IsBound(ThreadName@[threadid]) then
    Unbind(ThreadNameToID@.(ThreadName@[threadid]));
  fi;
  ThreadName@[threadid] := Immutable(ShallowCopy(name));
  ThreadNameToID@.(name) := threadid;
  OutputPrefix@[threadid] :=
    SubstituteVariables@(OutputPrefixRaw@[threadid], threadid);
  SystemMessage@("Renamed thread ", threadid-1, " as ", name);
end);

BindGlobal("CommandShell@", function(line)
  local threadinfo;
  threadinfo := StartInteractiveThread@();
  if line <> "" then
    NameThread@(threadinfo.ThreadID, line);
  fi;
  SwitchToThread@(threadinfo.ThreadID);
end);

BindGlobal("CommandFork@", function(line)
  local threadinfo;
  threadinfo := StartInteractiveThread@();
  if line <> "" then
    NameThread@(threadinfo.ThreadID, line);
  fi;
  SystemMessage@("Created new thread ", threadinfo.ThreadID-1);
end);

BindGlobal("CommandList@", function(line)
  local threadid, pending;
  for threadid in [1..Length(ThreadName@)] do
    if IsBound(ThreadName@[threadid]) then
      pending := "";
      if PendingOutput@[threadid] then
        pending := " (pending output)";
      fi;
      SystemMessage@("Thread ", ThreadName@[threadid],
        " [", threadid-1, "]", pending);
    fi;
  od;
end);

BindGlobal("CommandName@", function(line)
  local values, thread;
  values := GetArg@(line);
  thread := FindThread@(values[1]);
  if thread = fail then
    SystemMessage@("Unknown thread ", values[1]);
    return;
  fi;
  NameThread@(thread, values[2]);
end);

BindGlobal("CommandInfo@", function(line)
  local thread;
  thread := FindThread@(line);
  if thread = fail then
    SystemMessage@("Unknown thread ", line);
    return;
  fi;
end);

BindGlobal("ThreadNumFromString@", function(str)
  local i;
  if str = "" then
    return fail;
  else
    for i in [1..Length(str)] do
      if str[i] < '0' or str[i] > '9' then
        return fail;
      fi;
    od;
    return SMALLINT_STR(str);
  fi;
end);

BindGlobal("CommandKill@", function(line)
  local thread;
  thread := ThreadNumFromString@(line);
  if thread = fail then
    SystemMessage@("Unknown thread ", line);
    return;
  elif thread = ActiveThread@ - 1 then
    SystemMessage@("Cannot kill active thread");
    return;
  fi;
  KillThread(thread);
end);

BindGlobal("CommandPause@", function(line)
  local thread;
  thread := ThreadNumFromString@(line);
  if thread = fail then
    SystemMessage@("Unknown thread ", line);
    return;
  fi;
  PauseThread(thread);
end);

BindGlobal("CommandResume@", function(line)
  local thread;
  thread := ThreadNumFromString@(line);
  if thread = fail then
    SystemMessage@("Unknown thread ", line);
    return;
  fi;
  ResumeThread(thread);
end);

BindGlobal("CommandBreak@", function(line)
  local thread;
  if line = "" then
    thread := ActiveThread@-1;
  else
    thread := ThreadNumFromString@(line);
  fi;
  if thread = fail then
    SystemMessage@("Unknown thread ", line);
    return;
  fi;
  InterruptThread(thread, 0);
end);


BindGlobal("CommandHide@", function(line)
  local thread;
  if line = "*" then
    DefaultShowBackgroundOutput@ := false;
  elif line = "" then
    ShowBackgroundOutput@[ActiveThread@] := false;
  else
    thread := FindThread@(line);
    if thread = fail then
      SystemMessage@("Unknown thread ", line);
      return;
    fi;
    ShowBackgroundOutput@[thread] := false;
  fi;
end);

BindGlobal("CommandWatch@", function(line)
  local thread;
  if line = "*" then
    DefaultShowBackgroundOutput@ := true;
  elif line = "" then
    ShowBackgroundOutput@[ActiveThread@] := true;
  else
    thread := FindThread@(line);
    if thread = fail then
      SystemMessage@("Unknown thread ", line);
      return;
    fi;
    ShowBackgroundOutput@[thread] := true;
  fi;
end);

BindGlobal("CommandKeep@", function(line)
  local ch;
  line := NormalizedWhitespace(line);
  for ch in line do
    if ch < '0' or ch > '9' then
      SystemMessage@("Non-numeric argument");
      return;
    fi;
  od;
  OutputHistoryLength@ := SMALLINT_STR(line);
end);

BindGlobal("CommandPrompt@", function(line)
  local values, thread;
  values := GetArg@(line);
  if values[1] = "*" then
    DefaultPrompt@ := values[2];
    SystemMessage@("New default prompt: ", values[2]);
  else
    thread := FindThread@(line);
    if thread = fail then
      SystemMessage@("Unknown thread ", line);
      return;
    fi;
    Prompt@[thread] := SubstituteVariables@(values[2], thread);
    SystemMessage@("New prompt for thread ", values[1], ": ", values[2]);
  fi;
end);

BindGlobal("CommandPrefix@", function(line)
  local values, thread;
  values := GetArg@(line);
  if values[1] = "*" then
    DefaultOutputPrefix@ := values[2];
    SystemMessage@("New default output prefix: ", values[2]);
  else
    thread := FindThread@(line);
    if thread = fail then
      SystemMessage@("Unknown thread ", line);
      return;
    fi;
    OutputPrefixRaw@[thread] := values[2];
    OutputPrefix@[thread] := SubstituteVariables@(values[2], thread);
    SystemMessage@("New output prefix for thread ", values[1], ": ", values[2]);
  fi;
end);

BindGlobal("CommandSelect@", function(line)
  local thread;
  thread := FindThread@(line);
  if thread = fail then
    SystemMessage@("Unknown thread ", line);
  else
    if thread = ActiveThread@ then
      SystemMessage@(line, " is already the active thread");
    else
      SwitchToThread@(thread);
    fi;
  fi;
end);

BindGlobal("CommandNext@", function(line)
  local i;
  i := ActiveThread@+1;
  while i <> ActiveThread@ do
   if i > Length(ThreadName@) then
     i := 1;
   fi;
   if i <> ActiveThread@ then
     if IsBound(ThreadName@[i]) then
       SwitchToThread@(i);
       return;
     fi;
   fi;
   i := i + 1;
  od;
  SystemMessage@("There is only one running thread");
end);

BindGlobal("CommandPrevious@", function(line)
  local i;
  i := ActiveThread@-1;
  while i <> ActiveThread@ do
   if i < 1 then
     i := Length(ThreadName@);
   fi;
   if i <> ActiveThread@ then
     if IsBound(ThreadName@[i]) then
       SwitchToThread@(i);
       return;
     fi;
   fi;
   i := i - 1;
  od;
  SystemMessage@("There is only one running thread");
end);

BindGlobal("CommandReplay@", function(line)
  local values, num, id, thread, history, newlines;
  values := GetArg@(line);
  num := SMALLINT_STR(values[1]);
  if num = 0 then
    num := 20;
  fi;
  if num > OutputHistoryLength@ then
    num := OutputHistoryLength@;
  fi;
  SystemMessage@("Last ", num, " lines of output");
  if values[2] = "" then
    thread := ActiveThread@;
  else
    thread := FindThread@(values[2]);
    if thread = fail then
      SystemMessage@("Unknown thread ", values[2]);
      return;
    fi;
  fi;
  history := ShallowCopy(OutputHistory@[thread]);
  if not EndsWith(history, "\n") and not EndsWith(history, "\r") then
    Add(history, '\n');
  fi;
  history := ReplacedString(history, "\r", "");
  newlines := FIND_ALL_IN_STRING(history, "\n");
  if num < Length(newlines) then
    history :=
      history{[newlines[Length(newlines)-num]+1 .. Length(history)]};
  fi;
  SendChannel(OutputChannel@,
    [ thread, OutputPrefix@[thread], history ] );
end);

BindGlobal("CommandSource@", function(line)
  local file, command;
  file := InputTextFile(line);
  if file = fail then
    SystemMessage@("Could not open ", line);
  else
    while true do
      command := ReadLine(file);
      if command = fail then
        CloseStream(file);
        return;
      fi;
      command := Chomp(command);
      if not StartsWith(command, "#") then
        while StartsWith(command, " ") or StartsWith(command, "\t") do
          command := command{[2..Length(command)]};
        od;
        RunCommandQuietly@(command);
      fi;
    od;
  fi;
end);

BindGlobal("CommandAlias@", function(line)
  local values, alias, list, header;
  atomic Region@ do
    values := GetArg@(line);
    if values[1] = "" then
      header := false;
      for alias in SortedList(ListKeyEnumerator(AliasTable@)) do
        if not header then
          SystemMessage@("Aliases:");
          header := true;
        fi;
        SystemMessage@("  ", alias, " = ",
          LookupDictionary(AliasTable@, alias));
      od;
      if not header then
        SystemMessage@("No aliases have been defined.");
      fi;
    elif values[2] = "" then
      if KnowsDictionary(AliasTable@, values[1]) then
        SystemMessage@("Alias: ", values[1], " = ",
          LookupDictionary(AliasTable@, values[1]));
      else
        SystemMessage@("Unknown alias: ", values[1]);
      fi;
    else
      RemoveDictionary(AliasTable@, values[1]);
      WITH_TARGET_REGION(AliasTable@, function()
        AddDictionary(AliasTable@, values[1], MakeImmutable(values[2]));
      end);
      SystemMessage@("Alias: ", values[1], " = ", values[2]);
    fi;
  od;
end);

BindGlobal("CommandUnalias@", function(line)
  local alias;
  atomic Region@ do
    if KnowsDictionary(AliasTable@, line) then
      alias := LookupDictionary(AliasTable@, line);
      RemoveDictionary(AliasTable@, line);
      SystemMessage@("Removed alias: ", line, " = ", alias);
    else
      SystemMessage@("Unknown alias: ", line);
    fi;
  od;
end);

BindGlobal("CommandEval@", function(line)
  EvalString(line);
end);

BindGlobal("CommandRun@", function(line)
  local func, values;
  values := GetArg@(line);
  if not IsBoundGlobal(values[1]) then
    SystemMessage@("No such function: ", values[1]);
  else
    func := ValueGlobal(values[1]);
    if not IsFunction(func) then
      SystemMessage@("Not a function: ", values[1]);
    else
      func(values[2]);
    fi;
  fi;
end);

BindGlobal("CommandQUIT@", function(line)
  TERMINAL_CLOSE();
  ForceQuitGap();
end);

BindGlobal("InitializeCommands@", function()
  local commands, keyvalue;
  commands := MakeImmutable([
    [ "shell", CommandShell@ ],
    [ "fork", CommandFork@ ],
    [ "list", CommandList@ ],
    [ "name", CommandName@ ],
    [ "info", CommandInfo@ ],
    [ "hide", CommandHide@ ],
    [ "watch", CommandWatch@ ],
    [ "keep", CommandKeep@ ],
    [ "kill", CommandKill@ ],
    [ "break", CommandBreak@ ],
    [ "pause", CommandPause@ ],
    [ "resume", CommandResume@ ],
    [ "prefix", CommandPrefix@ ],
    [ "select", CommandSelect@ ],
    [ "next", CommandNext@ ],
    [ "previous", CommandPrevious@ ],
    [ "replay", CommandReplay@ ],
    [ "source", CommandSource@ ],
    [ "alias", CommandAlias@ ],
    [ "unalias", CommandUnalias@ ],
    [ "eval", CommandEval@ ],
    [ "run", CommandRun@ ],
    [ "QUIT", CommandQUIT@ ],
  ]);
  for keyvalue in commands do
    AddDictionary(CommandTable@, keyvalue[1], keyvalue[2]);
  od;
end);

atomic Region@ do
  WITH_TARGET_REGION(CommandTable@, function()
    InitializeCommands@();
  end);
od;

DeclareGlobalFunction("RunCommandWithAliases@"); # Needed for recursion

InstallGlobalFunction("RunCommandWithAliases@", function(string, aliases)
  local values, command, arguments, choices, func, line, i, c, recursive;
  values := GetArg@(string);
  command := values[1];
  if Length(command) > 0 and IsDigitChar(command[1]) then
    arguments := command;
    command := "select";
  else
    arguments := values[2];
  fi;
  choices := Set([]);
  # This has to be a read-write lock for now or dynamic retyping of lists
  # will not work and create problems.
  recursive := false;
  atomic Region@ do
    for c in ListKeyEnumerator(CommandTable@) do
      if StartsWith(c, command) then
        AddSet(choices, c);
        func := LookupDictionary(CommandTable@, c);
      fi;
    od;
    for c in ListKeyEnumerator(AliasTable@) do
      if StartsWith(c, command) then
        if c in aliases then
          recursive := true;
        else
          AddSet(choices, c);
          func := LookupDictionary(AliasTable@, c);
        fi;
      fi;
    od;
  od;
  if Length(choices) = 0 then
    if recursive then
      SystemMessage@("Recursive alias: ", command, ".");
    else
      SystemMessage@("No such command: ", command, ".");
    fi;
  elif Length(choices) > 1 then
    SystemMessage@("Ambiguous command: ", command, " (",
      JoinStringsWithSeparator(choices, ", "), ")");
  else
    if IsString(func) then
      AddSet(aliases, choices[1]);
      command := "!";
      Append(command, func);
      if arguments <> "" then
        Add(command, ' ');
        Append(command, arguments);
      fi;
      RunCommandWithAliases@(command, aliases);
      RemoveSet(aliases, choices[1]);
    else
      func(arguments);
    fi;
  fi;
end);

InstallGlobalFunction("RunCommand@", function(string)
  local originalThread;
  if StartsWith(string, "!") then
    string := string{[2..Length(string)]};
  fi;
  originalThread := ActiveThread@;
  RunCommandWithAliases@(string, Set([]));
  WritePrompt@();
end);

InstallGlobalFunction("RunCommandQuietly@", function(string)
  if StartsWith(string, "!") then
    string := string{[2..Length(string)]};
  fi;
  RunCommandWithAliases@(string, Set([]));
end);

BindGlobal("MainLoop@", function(mainthreadinfo)
  local packet, command, threadid, data;
  ControlThread@ := true;
  InitThreadTables@();
  CompleteThreadRegistration@(mainthreadinfo, false);
  ActiveThread@ := mainthreadinfo.ThreadID;
  OutputPrefix@[ActiveThread@] := "";
  while true do
    packet := ReceiveChannel(ControlChannel@);
    command := packet[1];
    threadid := packet[2];
    data := packet[3];
    if command = HAVE_OUTPUT@ then
      AddOutput@(threadid, data, false, false);
    elif command = HAVE_INPUT@ then
      AddOutputCommand@(ActiveThread@, data);
      NeedPrompt@ := true;
      if StartsWith(data, "!") then
        RunCommand@(Chomp(data));
      else
        if IsBound(ThreadInputChannel@[ActiveThread@]) then
          SendChannel(ThreadInputChannel@[ActiveThread@], data);
        else
          SystemMessage@("Attempting to send input to dead background thread");
        fi;
      fi;
    elif command = EXPECT_INPUT@ then
      AddOutput@(threadid, data, true, false);
    elif command = REGISTER_THREAD@ then
      CompleteThreadRegistration@(data, false);
    elif command = UNREGISTER_THREAD@ then
      if data then
        # shell thread
        NumShellThreads@ := NumShellThreads@ - 1;
        Unbind(ThreadNameToID@.(ThreadName@[threadid]));
        Unbind(ThreadName@[threadid]);
      else
        if OutputHistoryIncompleteLine@[threadid] then
          AddOutput@(threadid, "\n", false, false);
        fi;
        AddOutput@(threadid,
          "### Background thread terminated. ###\n", false, false);
      fi;
      # enable garbage collector to collect channels
      Unbind(ThreadControlChannel@[threadid]);
      Unbind(ThreadInputChannel@[threadid]);
      # Make sure the thread can't be found anymore.
      # wait for any threads we started ourselves
      if WaitForThread@[threadid] then
        WaitThread(ThreadObject@[threadid]);
      fi;
      if NumShellThreads@ = 0 then
        # say goodnight, Gracie
        AcknowledgeHandShake(ProgramShutdown@, true);
        return;
      fi;
      if threadid = ActiveThread@ then
        CommandNext@("");
        WritePrompt@();
      fi;
    else
      # should never get here
    fi;
  od;
end);

BindGlobal("InputLoop@", function()
  local packet, stdin, stdout, line;
  ControlThread@ := true;
  stdin := INPUT_TEXT_FILE("*stdin*");
  while true do
    line := READ_LINE_FILE(stdin);
    if line = fail then
      SendControl@(HAVE_INPUT@, "");
      # Ensure we don't just busy loop
      MicroSleep(10000);
    elif line <> "" then
      SendControl@(HAVE_INPUT@, line);
    fi;
  od;
end);

BindGlobal("OutputLoop@", function()
  local packet, threadid, prefix, text, stdout, newlines,
    eol, last_thread, p, last, line, prompt;
  stdout := OUTPUT_TEXT_FILE("*stdout*", false, false);
  ControlThread@ := true;
  last_thread := false;
  eol := true;
  while true do
    packet := ReceiveChannel(OutputChannel@);
    threadid := packet[1];
    prefix := packet[2];
    text := packet[3];
    # if we switched threads, then we may just have to break lines up
    if threadid <> last_thread and not eol and IsString(text) then
      WRITE_STRING_FILE_NC(stdout, ">>\n");
      eol := true;
    fi;
    last_thread := threadid;
    if not IsString(text) then
      text := "";
      eol := true;
    fi;
    # process text line by line, prefixing each new line
    newlines := FIND_ALL_IN_STRING(text, "\r\n");
    last := 1;
    for p in newlines do
      line := text{[last..p]};
      prompt := text[p] = '\r';
      if prompt then
        line := text{[last..p-1]};
      fi;
      if eol then
        WRITE_STRING_FILE_NC(stdout, prefix);
      fi;
      WRITE_STRING_FILE_NC(stdout, line);
      last := p + 1;
      eol := true;
    od;
    # and any trailing text without a final newline
    if last <= Length(text) then
      p := Length(text);
      line := text{[last..p]};
      prompt := text[p] = '\r';
      if prompt then
        line := text{[last..p-1]};
      fi;
      if eol then
        WRITE_STRING_FILE_NC(stdout, prefix);
      fi;
      WRITE_STRING_FILE_NC(stdout, line);
      eol := false;
    fi;
  od;
end);

BindGlobal("MULTI_SESSION", function()
  local handshake;
  SetupDefaultStreams@();
  BindGlobal("InputThreadID@", CreateThread(InputLoop@));
  BindGlobal("OutputThreadID@", CreateThread(OutputLoop@));
  handshake := StartHandShake();
  ThreadInfo@ := NewThreadInfo@();
  BindGlobal("ControlThreadID@", CreateThread(MainLoop@, ThreadInfo@));
  SESSION();
  UnregisterThread@(true);
  CompleteHandShake(ProgramShutdown@);
  PROGRAM_CLEAN_UP();
  TERMINAL_CLOSE();
  QuitGap();
end);

BindGlobal("ConsoleUIRegisterCommand", function(name, func)
  atomic Region@ do
    AddDictionary(CommandTable@, name, func);
  od;
end);

BindGlobal("ConsoleUIForegroundThread", function()
  return ActiveThread@-1;
end);

BindGlobal("ConsoleUIForegroundThreadName", function()
  return ThreadName@[ActiveThread@];
end);

BindGlobal("ConsoleUISelectThread", function(thread)
  SwitchToThread@(thread+1);
end);

BindGlobal("ConsoleUIOutputHistory", function(thread, lines)
  local history;
  if not IsBound(ThreadName@[thread+1]) then
    return fail;
  fi;
  return OutputContext@(lines, thread+1);
end);

BindGlobal("ConsoleUISetOutputHistoryLength", function(lines)
  OutputHistoryLength@ := lines;
end);

BindGlobal("ConsoleUINewSession", function(foreground, name)
  if foreground then
    CommandShell@(name);
  else
    CommandFork@(name);
  fi;
end);

BindGlobal("ConsoleUIRunCommand", function(command)
  RunCommandQuietly@(command);
end);

BindGlobal("ConsoleUIWritePrompt", function()
  WritePrompt@();
end);


LEAVE_NAMESPACE();

