------------------------------------------------------------------------------
-- Reading the keyboard, writing to the terminal/console                    --
--                                                                          --
-- Part of SparForte                                                        --
------------------------------------------------------------------------------
--                                                                          --
--            Copyright (C) 2001-2023 Free Software Foundation              --
--                                                                          --
-- This is free software;  you can  redistribute it  and/or modify it under --
-- terms of the  GNU General Public License as published  by the Free Soft- --
-- ware  Foundation;  either version 2,  or (at your option) any later ver- --
-- sion.  This is distributed in the hope that it will be useful, but WITH- --
-- OUT 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  distributed with this;  see file COPYING.  If not, write --
-- to  the Free Software Foundation,  59 Temple Place - Suite 330,  Boston, --
-- MA 02111-1307, USA.                                                      --
--                                                                          --
-- This is maintained at http://www.pegasoft.ca                             --
--                                                                          --
------------------------------------------------------------------------------

with ada.text_io,
     ada.text_io.unbounded_io,-- Ada 2005
     ada.strings.fixed,
     ada.calendar,
     gnat.os_lib,
     gnat.directory_operations,
     gnat.source_info,
     ada.containers.vectors,
     spar_os.tty,
     spar_os.readline,
     pegasoft.strings,
     world;
use  ada.text_io,
     ada.text_io.unbounded_io,-- Ada 2005
     ada.calendar,
     gnat.directory_operations,
     spar_os,
     spar_os.tty,
     spar_os.readline,
     pegasoft.strings,
     world;

#if not READLINE
with gnat.regexp,
     gnat.os_lib,
     signal_flags;
use  gnat.regexp,
     gnat.os_lib,
     signal_flags;
#end if;

package body pegasoft.user_io.getline is

#if not READLINE

-----------------------------------------------------------------------------
--  GET PROMPT INDENT (no getline library)
--
-- determine how far the command prompt indents from the
-- left hand side of the screen, taking into account
-- control characters and carriage returns / line feeds
-----------------------------------------------------------------------------

function getPromptIndent return natural is
  len : natural := 0;
begin
  for i in 1..length( prompt ) loop
      if element( prompt, i ) = ASCII.CR then      -- carriage return?
         len := 0;                                 -- back to left side
      elsif element( prompt, i ) = ASCII.LF then   -- line feed?
         len := 0;                                 -- back to left side
      elsif element( prompt, i ) >= ' ' then       -- not a control char?
         len := len + 1;                           -- count it
      end if;
  end loop;
  return len;
end getPromptIndent;


-----------------------------------------------------------------------------
--  GET PROMPT EXTRA LINES (no readline library)
--
-- determine how far the command prompt indents from the
-- left hand side of the screen, taking into account
-- control characters and carriage returns / line feeds
-----------------------------------------------------------------------------

function getPromptExtraLines return natural is
  last : character := ASCII.NUL;
  extra : natural := 0;
begin
  for i in 1..length( prompt ) loop
      if element( prompt, i ) = ASCII.CR then      -- carriage return?
         extra := extra + 1;                       -- back to left side
      elsif element( prompt, i ) = ASCII.LF then   -- line feed?
         if last /= ASCII.CR then                  -- for MS-DOS
            extra := extra + 1;                    -- back to left side
         end if;
      end if;
      last := element( prompt, i );
  end loop;
  return extra;
end getPromptExtraLines;
#end if;

#if READLINE

-----------------------------------------------------------------------------
--
-- READLINE IDLE CALLBACK
--
-----------------------------------------------------------------------------

readline_idle_callback : a_readline_callback;
readline_idle_timeout  : duration;

-----------------------------------------------------------------------------
--
-- COMPLETION LIST
--
-----------------------------------------------------------------------------
-- May be removed


package completionLists is new ada.containers.vectors( Positive,
  chars_ptr );
-- memoryLeakCnt : integer := 0;


-----------------------------------------------------------------------------
--
-- SUBCOMMAND COMPLETION
--
-----------------------------------------------------------------------------

subcommandHaystackFirst : natural;


-----------------------------------------------------------------------------
--  NEXT COMPLETION SUBCOMMAND
--
-- Search for needle in haystack.  state is the GNU readline state (0 means
-- starting the search).  The haystack is ASCII.NUL delimited and the NUL
-- is returned with the candidate.
-----------------------------------------------------------------------------

function nextCompletionSubcommand( haystack : string; needle : string; state : int ) return string is
  subcommandHaystackLast : natural;
begin
  -- If this is a new search, move the cursor to the front of the reserved
  -- words.  Otherwise, continue advancing through the list.

  if state = 0 then
     subcommandHaystackFirst := haystack'first;
  elsif subcommandHaystackFirst < haystack'last then
     subcommandHaystackFirst := subcommandHaystackFirst + 1;
  end if;

  while subcommandHaystackFirst < haystack'last loop
      -- "from" requires Ada 2005.  This will be zero is nothing is found.
      subcommandHaystackFirst := ada.strings.fixed.index(
         source => haystack,
         pattern => ASCII.NUL & needle,
         from => subcommandHaystackFirst );

      -- find the ending ASCII.NUL.  return the string slice (minus the NUL).

      if subcommandHaystackFirst > 0 then
         subcommandHaystackLast := ada.strings.fixed.index(
               haystack, "" & ASCII.NUL, subcommandHaystackFirst+1 );
         declare
           completionCandidate : constant string := haystack(
               subcommandHaystackFirst+1..subcommandHaystackLast-1 );
         begin
           return completionCandidate;
         end;
      else
         -- If there's no (or no more) matches, we're not at position zero.
         -- We are at the end of the list.
         subcommandHaystackFirst := haystack'last;
      end if;

      subcommandHaystackFirst := subcommandHaystackFirst + 1;
  end loop;

  return "";
end nextCompletionSubcommand;


-----------------------------------------------------------------------------
--
-- IPSET WORD COMPLETION
--
-----------------------------------------------------------------------------

  ipsetWords  : constant string :=
    ASCII.NUL & "create" &
    ASCII.NUL & "add" &
    ASCII.NUL & "del" &
    ASCII.NUL & "test" &
    ASCII.NUL & "destroy" &
    ASCII.NUL & "list" &
    ASCII.NUL & "save" &
    ASCII.NUL & "restore" &
    ASCII.NUL & "flush" &
    ASCII.NUL & "rename" &
    ASCII.NUL & "swap" &
    ASCII.NUL & "help" &
    ASCII.NUL & "version";

-----------------------------------------------------------------------------
--  IPSET WORD GENERATOR (C language callback to Ada)
--
-- C calls this Ada function to generate a list of acceptable completions
-- for the given text fragment.  A C null pointer is returned if there are
-- no more matches.
-----------------------------------------------------------------------------

function Ada_ipset_word_generator(text : chars_ptr; state : int ) return chars_ptr is
  needle : constant string := To_Ada( Value( text ) );
  completionCandidate : constant string :=
     nextCompletionSubcommand( ipsetWords, needle, state );
  subcommand : aliased char_array := To_C( completionCandidate );
begin

  -- Get the next completion, empty strings if none.  Return the candidate word
  -- to C.  Use strdup() to create a new C string which will be deallocated by
  -- GNU readline.

  if completionCandidate /= "" then
     return C_strdup( To_Chars_Ptr( subcommand'unchecked_access ) );
  end if;

  -- If there are no more reserved words, return a C NULL pointer to indicate
  -- there are no more possibilities.

  return Null_Ptr;
end Ada_ipset_word_generator;


-----------------------------------------------------------------------------
--
-- SVN WORD COMPLETION
--
-----------------------------------------------------------------------------

  svnWords  : constant string :=
    ASCII.NUL & "blame" &
    ASCII.NUL & "praise" &
    ASCII.NUL & "annotate" &
    ASCII.NUL & "cat" &
    ASCII.NUL & "changelist" &
    ASCII.NUL & "checkout" &
    ASCII.NUL & "cleanup" &
    ASCII.NUL & "commit" &
    ASCII.NUL & "copy" &
    ASCII.NUL & "delete" &
    ASCII.NUL & "remove" &
    ASCII.NUL & "diff" &
    ASCII.NUL & "export" &
    ASCII.NUL & "help" &
    ASCII.NUL & "import" &
    ASCII.NUL & "info" &
    ASCII.NUL & "list" &
    ASCII.NUL & "lock" &
    ASCII.NUL & "log" &
    ASCII.NUL & "merge" &
    ASCII.NUL & "mergeinfo" &
    ASCII.NUL & "mkdir" &
    ASCII.NUL & "move" &
    ASCII.NUL & "patch" &
    ASCII.NUL & "propdel" &
    ASCII.NUL & "propedit" &
    ASCII.NUL & "propget" &
    ASCII.NUL & "proplist" &
    ASCII.NUL & "propset" &
    ASCII.NUL & "relocate" &
    ASCII.NUL & "resolve" &
    ASCII.NUL & "resolved" &
    ASCII.NUL & "revert" &
    ASCII.NUL & "status" &
    ASCII.NUL & "switch" &
    ASCII.NUL & "unlock" &
    ASCII.NUL & "update" &
    ASCII.NUL & "upgrade";

-----------------------------------------------------------------------------
--  SVN WORD GENERATOR (C language callback to Ada)
--
-- C calls this Ada function to generate a list of acceptable completions
-- for the given text fragment.  A C null pointer is returned if there are
-- no more matches.
-----------------------------------------------------------------------------

function Ada_svn_word_generator(text : chars_ptr; state : int ) return chars_ptr is
  needle : constant string := To_Ada( Value( text ) );
  completionCandidate : constant string :=
     nextCompletionSubcommand( svnWords, needle, state );
  subcommand : aliased char_array := To_C( completionCandidate );
begin

  -- Get the next completion, empty strings if none.  Return the candidate word
  -- to C.  Use strdup() to create a new C string which will be deallocated by
  -- GNU readline.

  if completionCandidate /= "" then
     return C_strdup( To_Chars_Ptr( subcommand'unchecked_access ) );
  end if;

  -- If there are no more reserved words, return a C NULL pointer to indicate
  -- there are no more possibilities.

  return Null_Ptr;
end Ada_svn_word_generator;


-----------------------------------------------------------------------------
--
-- GIT WORD COMPLETION
--
-----------------------------------------------------------------------------

  gitWords  : constant string :=
    ASCII.NUL & "add" &
    ASCII.NUL & "add--interactive" &
    ASCII.NUL & "am" &
    ASCII.NUL & "annotate" &
    ASCII.NUL & "apply" &
    ASCII.NUL & "archive" &
    ASCII.NUL & "bisect" &
    ASCII.NUL & "bisect--helper" &
    ASCII.NUL & "blame" &
    ASCII.NUL & "branch" &
    ASCII.NUL & "bundle" &
    ASCII.NUL & "cat-file" &
    ASCII.NUL & "check-attr" &
    ASCII.NUL & "check-ignore" &
    ASCII.NUL & "check-mailmap" &
    ASCII.NUL & "check-ref-format" &
    ASCII.NUL & "checkout" &
    ASCII.NUL & "checkout-index" &
    ASCII.NUL & "cherry" &
    ASCII.NUL & "cherry-pick" &
    ASCII.NUL & "clean" &
    ASCII.NUL & "clone" &
    ASCII.NUL & "column" &
    ASCII.NUL & "commit" &
    ASCII.NUL & "commit-tree" &
    ASCII.NUL & "config" &
    ASCII.NUL & "count-objects" &
    ASCII.NUL & "credential" &
    ASCII.NUL & "credential-cache" &
    ASCII.NUL & "credential-cache--daemon" &
    ASCII.NUL & "credential-store" &
    ASCII.NUL & "daemon" &
    ASCII.NUL & "describe" &
    ASCII.NUL & "diff" &
    ASCII.NUL & "diff-files" &
    ASCII.NUL & "diff-index" &
    ASCII.NUL & "diff-tree" &
    ASCII.NUL & "difftool" &
    ASCII.NUL & "difftool--helper" &
    ASCII.NUL & "fast-export" &
    ASCII.NUL & "fast-import" &
    ASCII.NUL & "fetch" &
    ASCII.NUL & "fetch-pack" &
    ASCII.NUL & "filter-branch" &
    ASCII.NUL & "fmt-merge-msg" &
    ASCII.NUL & "for-each-ref" &
    ASCII.NUL & "format-patch" &
    ASCII.NUL & "fsck" &
    ASCII.NUL & "fsck-objects" &
    ASCII.NUL & "gc" &
    ASCII.NUL & "get-tar-commit-id" &
    ASCII.NUL & "grep" &
    ASCII.NUL & "hash-object" &
    ASCII.NUL & "help" &
    ASCII.NUL & "http-backend" &
    ASCII.NUL & "http-fetch" &
    ASCII.NUL & "http-push" &
    ASCII.NUL & "imap-send" &
    ASCII.NUL & "index-pack" &
    ASCII.NUL & "init" &
    ASCII.NUL & "init-db" &
    ASCII.NUL & "instaweb" &
    ASCII.NUL & "interpret-trailers" &
    ASCII.NUL & "log" &
    ASCII.NUL & "ls-files" &
    ASCII.NUL & "ls-remote" &
    ASCII.NUL & "ls-tree" &
    ASCII.NUL & "mailinfo" &
    ASCII.NUL & "mailsplit" &
    ASCII.NUL & "merge" &
    ASCII.NUL & "merge-base" &
    ASCII.NUL & "merge-file" &
    ASCII.NUL & "merge-index" &
    ASCII.NUL & "merge-octopus" &
    ASCII.NUL & "merge-one-file" &
    ASCII.NUL & "merge-ours" &
    ASCII.NUL & "merge-recursive" &
    ASCII.NUL & "merge-resolve" &
    ASCII.NUL & "merge-subtree" &
    ASCII.NUL & "merge-tree" &
    ASCII.NUL & "mergetool" &
    ASCII.NUL & "mktag" &
    ASCII.NUL & "mktree" &
    ASCII.NUL & "mv" &
    ASCII.NUL & "name-rev" &
    ASCII.NUL & "notes" &
    ASCII.NUL & "pack-objects" &
    ASCII.NUL & "pack-redundant" &
    ASCII.NUL & "pack-refs" &
    ASCII.NUL & "patch-id" &
    ASCII.NUL & "prune" &
    ASCII.NUL & "prune-packed" &
    ASCII.NUL & "pull" &
    ASCII.NUL & "push" &
    ASCII.NUL & "quiltimport" &
    ASCII.NUL & "read-tree" &
    ASCII.NUL & "rebase" &
    ASCII.NUL & "rebase--helper" &
    ASCII.NUL & "receive-pack" &
    ASCII.NUL & "reflog" &
    ASCII.NUL & "remote" &
    ASCII.NUL & "remote-ext" &
    ASCII.NUL & "remote-fd" &
    ASCII.NUL & "remote-ftp" &
    ASCII.NUL & "remote-ftps" &
    ASCII.NUL & "remote-http" &
    ASCII.NUL & "remote-https" &
    ASCII.NUL & "remote-testsvn" &
    ASCII.NUL & "repack" &
    ASCII.NUL & "replace" &
    ASCII.NUL & "request-pull" &
    ASCII.NUL & "rerere" &
    ASCII.NUL & "reset" &
    ASCII.NUL & "rev-list" &
    ASCII.NUL & "rev-parse" &
    ASCII.NUL & "revert" &
    ASCII.NUL & "rm" &
    ASCII.NUL & "send-pack" &
    ASCII.NUL & "sh-i18n--envsubst" &
    ASCII.NUL & "shell" &
    ASCII.NUL & "shortlog" &
    ASCII.NUL & "show" &
    ASCII.NUL & "show-branch" &
    ASCII.NUL & "show-index" &
    ASCII.NUL & "show-ref" &
    ASCII.NUL & "submodule" &
    ASCII.NUL & "stage" &
    ASCII.NUL & "stash" &
    ASCII.NUL & "status" &
    ASCII.NUL & "stripspace" &
    ASCII.NUL & "submodule--helper" &
    ASCII.NUL & "subtree" &
    ASCII.NUL & "symbolic-ref" &
    ASCII.NUL & "tag" &
    ASCII.NUL & "unpack-file" &
    ASCII.NUL & "unpack-objects" &
    ASCII.NUL & "update-index" &
    ASCII.NUL & "update-ref" &
    ASCII.NUL & "update-server-info" &
    ASCII.NUL & "upload-archive" &
    ASCII.NUL & "upload-pack" &
    ASCII.NUL & "var" &
    ASCII.NUL & "verify-commit" &
    ASCII.NUL & "verify-pack" &
    ASCII.NUL & "verify-tag" &
    ASCII.NUL & "web--browse" &
    ASCII.NUL & "whatchanged" &
    ASCII.NUL & "worktree" &
    ASCII.NUL & "write-tree";

-----------------------------------------------------------------------------
--  RESERVED WORD GENERATOR (C language callback to Ada)
--
-- C calls this Ada function to generate a list of acceptable completions
-- for the given text fragment.  A C null pointer is returned if there are
-- no more matches.
-----------------------------------------------------------------------------

function Ada_git_word_generator(text : chars_ptr; state : int ) return chars_ptr is
  needle : constant string := To_Ada( Value( text ) );
  completionCandidate : constant string :=
     nextCompletionSubcommand( gitWords, needle, state );
  subcommand : aliased char_array := To_C( completionCandidate );
begin

  -- Get the next completion, empty strings if none.  Return the candidate word
  -- to C.  Use strdup() to create a new C string which will be deallocated by
  -- GNU readline.

  if completionCandidate /= "" then
     return C_strdup( To_Chars_Ptr( subcommand'unchecked_access ) );
  end if;

  -- If there are no more reserved words, return a C NULL pointer to indicate
  -- there are no more possibilities.

  return Null_Ptr;
end Ada_git_word_generator;


-----------------------------------------------------------------------------
--
-- YUM WORD COMPLETION
--
-----------------------------------------------------------------------------

  yumWords  : constant string :=
    ASCII.NUL & "add" &
    ASCII.NUL & "install" &
    ASCII.NUL & "update" &
    ASCII.NUL & "update-to" &
    ASCII.NUL & "update-minimal" &
    ASCII.NUL & "check-update" &
    ASCII.NUL & "upgrade" &
    ASCII.NUL & "upgrade-to" &
    ASCII.NUL & "distribution-synchronization" &
    ASCII.NUL & "remove" &
    ASCII.NUL & "erase" &
    ASCII.NUL & "autoremove" &
    ASCII.NUL & "list" &
    ASCII.NUL & "info" &
    ASCII.NUL & "provides" &
    ASCII.NUL & "whatprovides" &
    ASCII.NUL & "clean" &
    ASCII.NUL & "makecache" &
    ASCII.NUL & "groups" &
    ASCII.NUL & "search" &
    ASCII.NUL & "shell" &
    ASCII.NUL & "resolvedep" &
    ASCII.NUL & "localinstall" &
    ASCII.NUL & "localupdate" &
    ASCII.NUL & "reinstall" &
    ASCII.NUL & "downgrade" &
    ASCII.NUL & "deplist" &
    ASCII.NUL & "repolist" &
    ASCII.NUL & "repoinfo" &
    ASCII.NUL & "repository-packages" &
    ASCII.NUL & "version" &
    ASCII.NUL & "history" &
    ASCII.NUL & "load-transaction" &
    ASCII.NUL & "updateinfo" &
    ASCII.NUL & "fssnapshot" &
    ASCII.NUL & "fs" &
    ASCII.NUL & "check" &
    ASCII.NUL & "help";

-----------------------------------------------------------------------------
--  YUM WORD GENERATOR (C language callback to Ada)
--
-- C calls this Ada function to generate a list of acceptable completions
-- for the given text fragment.  A C null pointer is returned if there are
-- no more matches.
-----------------------------------------------------------------------------

function Ada_yum_word_generator(text : chars_ptr; state : int ) return chars_ptr is
  needle : constant string := To_Ada( Value( text ) );
  completionCandidate : constant string :=
     nextCompletionSubcommand( yumWords, needle, state );
  subcommand : aliased char_array := To_C( completionCandidate );
begin

  -- Get the next completion, empty strings if none.  Return the candidate word
  -- to C.  Use strdup() to create a new C string which will be deallocated by
  -- GNU readline.

  if completionCandidate /= "" then
     return C_strdup( To_Chars_Ptr( subcommand'unchecked_access ) );
  end if;

  -- If there are no more reserved words, return a C NULL pointer to indicate
  -- there are no more possibilities.

  return Null_Ptr;
end Ada_yum_word_generator;


-----------------------------------------------------------------------------
--
-- APT WORD COMPLETION
--
-----------------------------------------------------------------------------

  aptWords  : constant string :=
    ASCII.NUL & "add" &
    ASCII.NUL & "add-repository" &
    ASCII.NUL & "autoclean" &
    ASCII.NUL & "autoremove" &
    ASCII.NUL & "build" &
    ASCII.NUL & "build-dep" &
    ASCII.NUL & "changelog" &
    ASCII.NUL & "check" &
    ASCII.NUL & "clean" &
    ASCII.NUL & "contains" &
    ASCII.NUL & "content" &
    ASCII.NUL & "deb" &
    ASCII.NUL & "depends" &
    ASCII.NUL & "dist-upgrade" &
    ASCII.NUL & "download" &
    ASCII.NUL & "edit-sources" &
    ASCII.NUL & "dselect-upgrade" &
    ASCII.NUL & "full-upgrade" &
    ASCII.NUL & "held" &
    ASCII.NUL & "help" &
    ASCII.NUL & "hold" &
    ASCII.NUL & "install" &
    ASCII.NUL & "help" &
    ASCII.NUL & "hold" &
    ASCII.NUL & "install" &
    ASCII.NUL & "list" &
    ASCII.NUL & "policy" &
    ASCII.NUL & "purge" &
    ASCII.NUL & "recommends" &
    ASCII.NUL & "rdepends" &
    ASCII.NUL & "reinstall" &
    ASCII.NUL & "remove" &
    ASCII.NUL & "search" &
    ASCII.NUL & "show" &
    ASCII.NUL & "showhold" &
    ASCII.NUL & "showsrc" &
    ASCII.NUL & "source" &
    ASCII.NUL & "sources" &
    ASCII.NUL & "unhold" &
    ASCII.NUL & "update" &
    ASCII.NUL & "upgrade" &
    ASCII.NUL & "version";

-----------------------------------------------------------------------------
--  APT WORD GENERATOR (C language callback to Ada)
--
-- C calls this Ada function to generate a list of acceptable completions
-- for the given text fragment.  A C null pointer is returned if there are
-- no more matches.
-----------------------------------------------------------------------------

function Ada_apt_word_generator(text : chars_ptr; state : int ) return chars_ptr is
  needle : constant string := To_Ada( Value( text ) );
  completionCandidate : constant string :=
     nextCompletionSubcommand( aptWords, needle, state );
  subcommand : aliased char_array := To_C( completionCandidate );
begin

  -- Get the next completion, empty strings if none.  Return the candidate word
  -- to C.  Use strdup() to create a new C string which will be deallocated by
  -- GNU readline.

  if completionCandidate /= "" then
     return C_strdup( To_Chars_Ptr( subcommand'unchecked_access ) );
  end if;

  -- If there are no more reserved words, return a C NULL pointer to indicate
  -- there are no more possibilities.

  return Null_Ptr;
end Ada_apt_word_generator;


-----------------------------------------------------------------------------
--
-- DOCKER WORD COMPLETION
--
-----------------------------------------------------------------------------

dockerWords : constant string :=
  ASCII.NUL & "builder" &
  ASCII.NUL & "config" &
  ASCII.NUL & "container" &
  ASCII.NUL & "context" &
  ASCII.NUL & "engine" &
  ASCII.NUL & "image" &
  ASCII.NUL & "network" &
  ASCII.NUL & "node" &
  ASCII.NUL & "plugin" &
  ASCII.NUL & "secret" &
  ASCII.NUL & "service" &
  ASCII.NUL & "stack" &
  ASCII.NUL & "swarm" &
  ASCII.NUL & "system" &
  ASCII.NUL & "trust" &
  ASCII.NUL & "volume" &
  ASCII.NUL & "pull" &
  ASCII.NUL & "push" &
  ASCII.NUL & "rename" &
  ASCII.NUL & "restart" &
  ASCII.NUL & "rm" &
  ASCII.NUL & "rmi" &
  ASCII.NUL & "run" &
  ASCII.NUL & "save" &
  ASCII.NUL & "search" &
  ASCII.NUL & "start" &
  ASCII.NUL & "stats" &
  ASCII.NUL & "stop" &
  ASCII.NUL & "tag" &
  ASCII.NUL & "top" &
  ASCII.NUL & "unpause" &
  ASCII.NUL & "update" &
  ASCII.NUL & "version" &
  ASCII.NUL & "wait";

-----------------------------------------------------------------------------
--  DOCKER WORD GENERATOR (C language callback to Ada)
--
-- C calls this Ada function to generate a list of acceptable completions
-- for the given text fragment.  A C null pointer is returned if there are
-- no more matches.
-----------------------------------------------------------------------------

function Ada_docker_word_generator(text : chars_ptr; state : int ) return chars_ptr is
  needle : constant string := To_Ada( Value( text ) );
  completionCandidate : constant string :=
     nextCompletionSubcommand( dockerWords, needle, state );
  subcommand : aliased char_array := To_C( completionCandidate );
begin

  -- Get the next completion, empty strings if none.  Return the candidate word
  -- to C.  Use strdup() to create a new C string which will be deallocated by
  -- GNU readline.

  if completionCandidate /= "" then
     return C_strdup( To_Chars_Ptr( subcommand'unchecked_access ) );
  end if;

  -- If there are no more reserved words, return a C NULL pointer to indicate
  -- there are no more possibilities.

  return Null_Ptr;
end Ada_docker_word_generator;


-----------------------------------------------------------------------------
--
-- KUBERNETES WORD COMPLETION
--
-----------------------------------------------------------------------------

kubernetesWords : constant string :=
    ASCII.NUL & "create" &
    ASCII.NUL & "get" &
    ASCII.NUL & "run" &
    ASCII.NUL & "expose" &
    ASCII.NUL & "delete" &
    ASCII.NUL & "apply" &
    ASCII.NUL & "annotate" &
    ASCII.NUL & "autoscale" &
    ASCII.NUL & "debug" &
    ASCII.NUL & "diff" &
    ASCII.NUL & "edit" &
    ASCII.NUL & "kustomize" &
    ASCII.NUL & "label" &
    ASCII.NUL & "patch" &
    ASCII.NUL & "replace" &
    ASCII.NUL & "rollout" &
    ASCII.NUL & "scale" &
    ASCII.NUL & "set" &
    ASCII.NUL & "wait" &
    ASCII.NUL & "attach" &
    ASCII.NUL & "auth" &
    ASCII.NUL & "cp" &
    ASCII.NUL & "describe" &
    ASCII.NUL & "exec" &
    ASCII.NUL & "logs" &
    ASCII.NUL & "port-forward" &
    ASCII.NUL & "proxy" &
    ASCII.NUL & "top" &
    ASCII.NUL & "api-versions" &
    ASCII.NUL & "certificate" &
    ASCII.NUL & "cluster-info" &
    ASCII.NUL & "cordon" &
    ASCII.NUL & "drain" &
    ASCII.NUL & "taint" &
    ASCII.NUL & "uncordon" &
    ASCII.NUL & "alpha" &
    ASCII.NUL & "api-resources" &
    ASCII.NUL & "completion" &
    ASCII.NUL & "config" &
    ASCII.NUL & "explain" &
    ASCII.NUL & "options" &
    ASCII.NUL & "plugin" &
    ASCII.NUL & "version";

-----------------------------------------------------------------------------
--  KUBERNETES WORD GENERATOR (C language callback to Ada)
--
-- C calls this Ada function to generate a list of acceptable completions
-- for the given text fragment.  A C null pointer is returned if there are
-- no more matches.
-----------------------------------------------------------------------------

function Ada_k8s_word_generator(text : chars_ptr; state : int ) return chars_ptr is
  needle : constant string := To_Ada( Value( text ) );
  completionCandidate : constant string :=
     nextCompletionSubcommand( kubernetesWords, needle, state );
  subcommand : aliased char_array := To_C( completionCandidate );
begin

  -- Get the next completion, empty strings if none.  Return the candidate word
  -- to C.  Use strdup() to create a new C string which will be deallocated by
  -- GNU readline.

  if completionCandidate /= "" then
     return C_strdup( To_Chars_Ptr( subcommand'unchecked_access ) );
  end if;

  -- If there are no more reserved words, return a C NULL pointer to indicate
  -- there are no more possibilities.

  return Null_Ptr;
end Ada_k8s_word_generator;


-----------------------------------------------------------------------------
--
-- NPM WORD COMPLETION
--
-----------------------------------------------------------------------------

npmWords : constant string :=
  ASCII.NUL & "access" &
  ASCII.NUL & "adduser" &
  ASCII.NUL & "audit" &
  ASCII.NUL & "bin" &
  ASCII.NUL & "bugs" &
  ASCII.NUL & "cache" &
  ASCII.NUL & "ci" &
  ASCII.NUL & "cit" &
  ASCII.NUL & "clean-install" &
  ASCII.NUL & "clean-install-test" &
  ASCII.NUL & "completion" &
  ASCII.NUL & "config" &
  ASCII.NUL & "create" &
  ASCII.NUL & "ddp" &
  ASCII.NUL & "dedupe" &
  ASCII.NUL & "deprecate" &
  ASCII.NUL & "dist-tag" &
  ASCII.NUL & "docs" &
  ASCII.NUL & "doctor" &
  ASCII.NUL & "edit" &
  ASCII.NUL & "explore" &
  ASCII.NUL & "fund" &
  ASCII.NUL & "get" &
  ASCII.NUL & "help" &
  ASCII.NUL & "help-search" &
  ASCII.NUL & "hook" &
  ASCII.NUL & "init" &
  ASCII.NUL & "install" &
  ASCII.NUL & "install-ci-test" &
  ASCII.NUL & "install-test" &
  ASCII.NUL & "it" &
  ASCII.NUL & "link" &
  ASCII.NUL & "list" &
  ASCII.NUL & "ln" &
  ASCII.NUL & "login" &
  ASCII.NUL & "logout" &
  ASCII.NUL & "ls" &
  ASCII.NUL & "org" &
  ASCII.NUL & "outdated" &
  ASCII.NUL & "owner" &
  ASCII.NUL & "pack" &
  ASCII.NUL & "ping" &
  ASCII.NUL & "prefix" &
  ASCII.NUL & "profile" &
  ASCII.NUL & "prune" &
  ASCII.NUL & "publish" &
  ASCII.NUL & "rb" &
  ASCII.NUL & "rebuild" &
  ASCII.NUL & "repo" &
  ASCII.NUL & "restart" &
  ASCII.NUL & "root" &
  ASCII.NUL & "run" &
  ASCII.NUL & "run-script" &
  ASCII.NUL & "se" &
  ASCII.NUL & "search" &
  ASCII.NUL & "set" &
  ASCII.NUL & "shrinkwrap" &
  ASCII.NUL & "star" &
  ASCII.NUL & "stars" &
  ASCII.NUL & "start" &
  ASCII.NUL & "stop" &
  ASCII.NUL & "team" &
  ASCII.NUL & "test" &
  ASCII.NUL & "token" &
  ASCII.NUL & "tst" &
  ASCII.NUL & "un" &
  ASCII.NUL & "uninstall" &
  ASCII.NUL & "unpublish" &
  ASCII.NUL & "unstar" &
  ASCII.NUL & "up" &
  ASCII.NUL & "update" &
  ASCII.NUL & "version" &
  ASCII.NUL & "view" &
  ASCII.NUL & "whoami";

-----------------------------------------------------------------------------
--  NPM WORD GENERATOR (C language callback to Ada)
--
-- C calls this Ada function to generate a list of acceptable completions
-- for the given text fragment.  A C null pointer is returned if there are
-- no more matches.
-----------------------------------------------------------------------------

function Ada_npm_word_generator(text : chars_ptr; state : int ) return chars_ptr is
  needle : constant string := To_Ada( Value( text ) );
  completionCandidate : constant string :=
     nextCompletionSubcommand( npmWords, needle, state );
  subcommand : aliased char_array := To_C( completionCandidate );
begin

  -- Get the next completion, empty strings if none.  Return the candidate word
  -- to C.  Use strdup() to create a new C string which will be deallocated by
  -- GNU readline.

  if completionCandidate /= "" then
     return C_strdup( To_Chars_Ptr( subcommand'unchecked_access ) );
  end if;

  -- If there are no more reserved words, return a C NULL pointer to indicate
  -- there are no more possibilities.

  return Null_Ptr;
end Ada_npm_word_generator;


-----------------------------------------------------------------------------
--  ASSIGNMENT WORD GENERATOR (C language callback to Ada)
--
-- Return candidate words for assignment substitutions from the symbol table.
-----------------------------------------------------------------------------

assignmentIdx : identifier:=identifiers'last;

function Ada_assignment_word_generator(text : chars_ptr; state : int ) return chars_ptr is
  needle    : constant string := To_Ada( Value( text ) );
  needleLen : constant size_t := Strlen( text );
begin
  -- If this is a new search, move the cursor to the front of the reserved
  -- words.  Otherwise, continue advancing through the list.

  if state = 0 then
     assignmentIdx := identifiers'last;
  elsif assignmentIdx > identifiers'first then
     -- Note: walks everything, even things not visible.  Perhaps not a big
     -- concern on the command line?
     assignmentIdx := assignmentIdx  - 1;
  end if;

  -- From the current position in the list, if the text matches the leading
  -- characters of the candidate word, then return the candidate word to C.
  -- Use strdup() to create a new C string which will be deallocated by
  -- GNU readline.

  while assignmentIdx > identifiers'first loop
     if needleLen <= size_t( length( identifiers( assignmentIdx ).name ) ) then
        declare
           haystack : constant string := to_string( identifiers(assignmentIdx ).name );
           haystack_cstr : aliased char_array :=  To_C( haystack );
        begin
           if needle = haystack( 1..integer( needleLen ) ) then
              if not identifiers( assignmentIdx ).deleted and
                     -- root record, string literal, etc. have spaces
                     -- so users cannot use them.
                       index( identifiers( assignmentIdx ).name, " " ) = 0 and
                     ( identifiers( assignmentIdx ).class = funcClass or
                       identifiers( assignmentIdx ).class = userFuncClass or
                       identifiers( assignmentIdx ).class = varClass or
                       identifiers( assignmentIdx ).class = typeClass or
                       identifiers( assignmentIdx ).class = subClass or
                       identifiers( assignmentIdx ).class = enumClass ) then
                return C_strdup( To_Chars_Ptr( haystack_cstr'unchecked_access ) );
              end if;
           end if;
        end;
     end if;
     assignmentIdx := assignmentIdx  - 1;
  end loop;

  -- Return a C NULL pointer to indicate there are no more possibilities.

  return Null_Ptr;
end Ada_assignment_word_generator;


-----------------------------------------------------------------------------
--  VARIABLE WORD GENERATOR (C language callback to Ada)
--
-- Return candidate words for dollar expansion from the symbol table.
-----------------------------------------------------------------------------

variableIdx : identifier:=identifiers'last;

function Ada_variable_word_generator(text : chars_ptr; state : int ) return chars_ptr is
  needle    : constant string := To_Ada( Value( text ) );
  needleLen : constant size_t := Strlen( text );
begin
  -- If this is a new search, move the cursor to the front of the reserved
  -- words.  Otherwise, continue advancing through the list.

  if state = 0 then
     variableIdx := identifiers'last;
  elsif variableIdx > identifiers'first then
     -- Note: walks everything, even things not visible.  Perhaps not a big
     -- concern on the command line?
     variableIdx := variableIdx  - 1;
  end if;

  -- From the current position in the list, if the text matches the leading
  -- characters of the candidate word, then return the candidate word to C.
  -- Use strdup() to create a new C string which will be deallocated by
  -- GNU readline.

  while variableIdx > identifiers'first loop
     if needleLen <= size_t( length( identifiers( variableIdx ).name ) ) then
        declare
           haystack : constant string := to_string( identifiers(variableIdx ).name );
           haystack_cstr : aliased char_array :=  To_C( haystack );
        begin
           if needle = haystack( 1..integer( needleLen ) ) then
              if not identifiers( variableIdx ).deleted and
                     -- root record, string literal, etc. have spaces
                     -- so users cannot use them.
                       index( identifiers( variableIdx ).name, " " ) = 0 and
                     ( identifiers( variableIdx ).class = varClass or
                       identifiers( variableIdx ).class = enumClass ) then
                return C_strdup( To_Chars_Ptr( haystack_cstr'unchecked_access ) );
              end if;
           end if;
        end;
     end if;
     variableIdx := variableIdx  - 1;
  end loop;

  -- Return a C NULL pointer to indicate there are no more possibilities.

  return Null_Ptr;
end Ada_variable_word_generator;


-----------------------------------------------------------------------------
--  FIRST WORD GENERATOR (C language callback to Ada)
--
-- Return candidate words for first word substitutions from the symbol table.
-----------------------------------------------------------------------------

identifierIdx : identifier := identifiers'last;

function Ada_first_word_generator(text : chars_ptr; state : int ) return chars_ptr is
  needle    : constant string := To_Ada( Value( text ) );
  needleLen : constant size_t := Strlen( text );
begin
  -- If this is a new search, move the cursor to the front of the reserved
  -- words.  Otherwise, continue advancing through the list.

  if state = 0 then
     identifierIdx := identifiers'last;
  elsif identifierIdx > identifiers'first then
     -- Note: walks everything, even things not visible.  Perhaps not a big
     -- concern on the command line?
     identifierIdx := identifierIdx  - 1;
  end if;

  -- From the current position in the list, if the text matches the leading
  -- characters of the candidate word, then return the candidate word to C.
  -- Use strdup() to create a new C string which will be deallocated by
  -- GNU readline.

  while identifierIdx > identifiers'first loop
     if needleLen <= size_t( length( identifiers( identifierIdx ).name ) ) then
        declare
           haystack : constant string := to_string( identifiers(identifierIdx ).name );
           haystack_cstr : aliased char_array :=  To_C( haystack );
        begin
           if needle = haystack( 1..integer( needleLen ) ) then
              if not identifiers( identifierIdx ).deleted and
                     -- root record, string literal, etc. have spaces
                     -- so users cannot use them.
                       index( identifiers( identifierIdx ).name, " " ) = 0 and
                     ( identifiers( identifierIdx ).class = procClass or
                       identifiers( identifierIdx ).class = userProcClass or
                       identifiers( identifierIdx ).class = userCaseProcClass or
                       identifiers( identifierIdx ).class = varclass or
                       identifierIdx <= reserved_top ) then
                return C_strdup( To_Chars_Ptr( haystack_cstr'unchecked_access ) );
              end if;
           end if;
        end;
     end if;
     identifierIdx := identifierIdx  - 1;
  end loop;

  -- If there are no more reserved words, return a C NULL pointer to indicate
  -- there are no more possibilities.

  return Null_Ptr;
end Ada_first_word_generator;


-----------------------------------------------------------------------------
-- EXPAND TILDE
--
-- Handle a leading tilde in a file path
-----------------------------------------------------------------------------

function expandTilde( s : string ) return string is
  home_id: identifier;
begin
  if s'length = 1 then
     if s(s'first) = '~' then
        findIdent( to_unbounded_string( "HOME" ), home_id );
        return to_string( identifiers( home_id ).value.all ) & "/";
     end if;
  elsif s'length > 1 then
     if s(s'first..s'first+1) = "~/" then
        findIdent( to_unbounded_string( "HOME" ), home_id );
        return to_string( identifiers( home_id ).value.all ) & s( s'first+1..s'last );
     elsif s(s'first) = '~' then
        return "/home/" & s( s'first+1..s'last ) & "/";
     end if;
  end if;
  return s;
end expandTilde;

-----------------------------------------------------------------------------
--  COMMAND WORD GENERATOR (C language callback to Ada)
--
-- Return candidate commands for first word substitutions from PATH dirs.
-----------------------------------------------------------------------------

remainingCmdPathDirs : unbounded_string;
-- the remaining directories the PATH string

currentCmdPathDir    : unbounded_string;
-- the current directory from the PATH string

currentCmdDir        : dir_type;
-- the directory file type for commands, referring to currentCmdPathDir

function Ada_command_word_generator(text : chars_ptr; state : int ) return chars_ptr is
  path_id : identifier;
  completionFileLen : integer;
  completionFile : string(1..1024); -- TODO: file length limit
  needleRaw : constant string := To_Ada( Value( text ) );
  needleDir : unbounded_string := to_unbounded_string( Dir_Name( expandTilde( unescapeWithBackslashes( needleRaw ) ) ) );
  needle    : constant string := File_Name( unescapeWithBackslashes( needleRaw ) );
  needleLen : constant size_t := needle'length;
  firstColon : natural;
begin

   -- If there is no directory, Dir_Name defaults to "./", the current
   -- directory, which is not necessarily correct.

   if needleDir = "." & directory_delimiter then
     if needleRaw'length < 3 then
        needleDir := null_unbounded_string;
     elsif needleRaw(needleRaw'first..needleRaw'first+1) /= "." & directory_delimiter then
        needleDir := null_unbounded_string;
     end if;
  end if;

--put_line( "[NeedleRaw '" & needleRaw & "']" ); -- DEBUG
--put_line( "[NeedleDir '" & to_string( needleDir ) & "']" ); -- DEBUG
  if state = 0 then
     -- if the user specifies an absolute or relative directory, use that
     -- directory.  Otherwise, use the directories in the PATH variable.
     if ada.strings.fixed.index( needleRaw, "" & directory_delimiter ) > 0 then
        -- remainingCmdPathDirs := to_unbounded_string( needleDir );
        remainingCmdPathDirs := needleDir;
     else
        findIdent( to_unbounded_string( "PATH" ), path_id );
        if path_id /= eof_t then
           remainingCmdPathDirs := identifiers( path_id ).value.all;
        else
           remainingCmdPathDirs := null_unbounded_string;
        end if;
     end if;
  end if;

  -- Search the remaining PATH values for a valid directory.
  -- Open that directory.

  while is_open( currentCmdDir ) or remainingCmdPathDirs /= null_unbounded_string loop

     -- If we have an open directory, find a suitable file for substitution.
     -- Read files from the directory.  If a file is found and it matches
     -- the text and is executable, return it as a completion candidate.
     -- Ignore errors.  Continue until an empty string is returned.

     if is_open( currentCmdDir ) then
        loop
           begin
             read( currentCmdDir, completionFile, completionFileLen );
             exit when completionFileLen <= 0;
             if needle = completionFile( 1..integer( needleLen ) ) then
--put( "[matched '" & to_string( needleDir ) & "']" ); -- DEBUG
                declare
                completionFileAsC : aliased char_array := To_C(
                   escapeSpacesWithBackslashes( to_string( needleDir ) & completionFile(1..completionFileLen ) ) );
                   testpath : constant string := to_string( currentCmdPathDir ) & directory_delimiter & completionFile(1..completionFileLen) & ASCII.NUL;
                begin
-- TODO: messy.  too many string conversions
-- TODO: does not follow links
--put( "[testing '" & testpath & "']" ); -- DEBUG
                   if C_is_executable( testpath ) then
--put( "['" & To_Ada( completionFileAsC ) & "']" ); -- DEBUG
--put( "[executable]" ); --DEBUG
                      return C_strdup( To_Chars_Ptr( completionFileAsC'unchecked_access ) );
                   end if;
                end;
             end if;
           exception when DIRECTORY_ERROR =>
             null;
           when others =>
             put_line( standard_error, gnat.source_info.source_location & ": internal error: unexcepted exception on tab completion" );
           end;
        end loop;
     end if;

     -- Search for a new directory.  Remove paths from the PATH
     -- variable.  Try to open the directory.  If you cannot open
     -- the directory, try the next directory.  Stop searching
     -- when a directory is found or When no paths remain.

     loop
        firstColon := index( remainingCmdPathDirs, ":" );
        if firstColon /= 0 then
           currentCmdPathDir := head( remainingCmdPathDirs, firstColon-1 );
           remainingCmdPathDirs := unbounded_slice( remainingCmdPathDirs, firstColon+1, length( remainingCmdPathDirs ) );
        else
           currentCmdPathDir := remainingCmdPathDirs;
           remainingCmdPathDirs := null_unbounded_string;
        end if;
        begin
          if is_open( currentCmdDir ) then
             close( currentCmdDir );
          end if;
          -- open( currentCmdDir, to_string( currentCmdPathDir ) & Directory_Delimiter & needleDir );
--put( "[trying '" & to_string( currentCmdPathDir ) & "']" ); -- DEBUG
          open( currentCmdDir, to_string( currentCmdPathDir ) );
          exit;
        exception when DIRECTORY_ERROR =>
          null;
        when others =>
          put_line( standard_error, gnat.source_info.source_location & ": internal error: unexcepted exception on tab completion" );
        end;
     exit when remainingCmdPathDirs = null_unbounded_string;
     end loop;

  end loop;

  -- Close off the directory search.  Initiate the identifier search.

  if is_open( currentCmdDir ) then
     close( currentCmdDir );
  end if;

  return Null_Ptr;

end Ada_command_word_generator;


-----------------------------------------------------------------------------
--  FILE WORD GENERATOR (C language callback to Ada)
--
-- Return candidate commands for subsequent word substitutions.
-----------------------------------------------------------------------------

currentFileDir : dir_type;

fileDirectoryCount : natural :=  0;
lastDirectory : unbounded_string;

function Ada_file_word_generator(text : chars_ptr; state : int ) return chars_ptr is
  completionFileLen : integer;
  completionFile : string(1..1024);

  needleDir : constant string := Dir_Name( expandTilde( unescapeWithBackslashes( To_Ada( Value( text ) ) ) ) );
  needle    : constant string := File_Name( unescapeWithBackslashes( To_Ada( Value( text ) ) ) );
  needleLen : constant size_t := needle'length;
begin
  if state = 0 then
     fileDirectoryCount := 0;
     begin
       if is_open( currentFileDir ) then
          close( currentFileDir );
       end if;
       open( currentFileDir, needleDir );
     exception when DIRECTORY_ERROR =>
       null;
     when others =>
       put_line( standard_error, gnat.source_info.source_location & ": internal error: unexcepted exception on tab completion" );
     end;
  end if;

  -- Search the remaining PATH values for a valid directory.
  -- Open that directory.

  if is_open( currentFileDir ) then

     -- If we have an open directory, find a suitable file for substitution.
     -- Read files from the directory.  If a file is found and it matches
     -- the text and is executable, return it as a completion candidate.
     -- Ignore errors.  Continue until an empty string is returned.

     loop
        begin
          read( currentFileDir, completionFile, completionFileLen );
          exit when completionFileLen <= 0;
          if completionFileLen = 1 and then completionFile(1) = '.' then
              null;
          elsif completionFileLen = 2 and then completionFile(1) = '.' and then completionFile(2) = '.' then
              null;
          elsif needle = completionFile( 1..integer( needleLen ) ) then
              -- For a directory, we need to add the delimiter to the end of
              -- the path, which is the diretory being searched plus the
              -- new directory.
              if gnat.os_lib.is_directory( needleDir & completionFile(1..completionFileLen ) ) then
                 declare
                    completionFileAsC : aliased char_array := To_C(
                       escapeSpacesWithBackslashes( needleDir & completionFile(1..completionFileLen ) ) & directory_delimiter );
                 begin
-- TODO: messy.  too many string conversions
-- TODO: does not follow links
                    fileDirectoryCount:=fileDirectoryCount+1; -- DEBUG
                    lastDirectory := to_unbounded_string( needleDir & completionFile(1..completionFileLen ) );
                    return C_strdup( To_Chars_Ptr( completionFileAsC'unchecked_access ) );
                 end;
              else
                 -- For a non-directory, the path is directory being searched plus
                 -- the file found.
                 declare
                    completionFileAsC : aliased char_array := To_C(
                       escapeSpacesWithBackslashes( needleDir & completionFile(1..completionFileLen ) ) );
                 begin
-- TODO: messy.  too many string conversions
-- TODO: does not follow links
                    return C_strdup( To_Chars_Ptr( completionFileAsC'unchecked_access ) );
                 end;
              end if;
          end if;

        exception when DIRECTORY_ERROR =>
          null;
        when others =>
          put_line( standard_error, gnat.source_info.source_location & ": internal error: unexcepted exception on tab completion" );
        end;
     end loop;

     -- Search for a new directory.  Remove paths from the PATH
     -- variable.  Try to open the directory.  If you cannot open
     -- the directory, try the next directory.  Stop searching
     -- when a directory is found or When no paths remain.

  end if;

  -- Close off the directory search.  Initiate the identifier search.

  if is_open( currentFileDir ) then
     close( currentFileDir );
  end if;

  -- If we only found one directory, we cannot return that because
  -- completion will follow it with a space.  Instead open that
  -- directory, and pull at least one more completion, if there is
  -- one.

  if fileDirectoryCount = 1 then
     fileDirectoryCount := 0;
     declare
        completionFileAsC : aliased char_array := To_C(
                  escapeSpacesWithBackslashes( to_string( lastDirectory ) & "/./" ) );
     begin
        return C_strdup( To_Chars_Ptr( completionFileAsC'unchecked_access ) );
     end;
  end if;

  return Null_Ptr;

end Ada_file_word_generator;


-----------------------------------------------------------------------------
--  EXECUTABLE WORD GENERATOR (C language callback to Ada)
--
-- Chains together all callbacks for the first word on a line.
-----------------------------------------------------------------------------

type executableStages is (commands, idents);
executableStage : executableStages;

function Ada_executable_word_generator(text : chars_ptr; state : int ) return chars_ptr is
  localState : int := state;
  res : chars_ptr;
begin
  -- Just starting?  We're doing commands.

  if state = 0 then
     executableStage := commands;
  end if;

  -- Do command completions.  If we're done, switch to identifiers.
  -- Remember to set state to 0 (just starting).

  if executableStage = commands then
     res := Ada_command_word_generator(text, localState );
     if res = Null_Ptr then
        executableStage := idents;
        localState := 0;
     end if;
  end if;

  -- Do first completions

  if executableStage = idents then
     res := Ada_first_word_generator(text, localState );
  end if;

  return res;
end Ada_executable_word_generator;


-----------------------------------------------------------------------------
--  PARAMETER WORD GENERATOR (C language callback to Ada)
--
-- Chains together all callbacks for the additional words on a line.
-----------------------------------------------------------------------------

type parameterStages is (files, idents);
parameterStage : parameterStages;

function Ada_parameter_word_generator(text : chars_ptr; state : int ) return chars_ptr is
  localState : int := state;
  res : chars_ptr;
begin
  -- Just starting?  We're doing commands.
  if state = 0 then
     parameterStage := files;
  end if;

  -- Do command completions.  If we're done, switch to identifiers.
  -- Remember to set state to 0 (just starting).

  if parameterStage = files then
     res := Ada_file_word_generator(text, localState );
     if res = Null_Ptr then
        parameterStage := idents;
        localState := 0;
     end if;
  end if;

  -- Do identifier completions

  if parameterStage = idents then
     res := Ada_assignment_word_generator(text, localState );
  end if;

  return res;
end Ada_parameter_word_generator;

function Ada_readline_timeout return int is
begin
  -- type idle_states is (first, waiting, calling);
  readline_idle_callback.all;
  return 0;
end Ada_readline_timeout;

procedure set_readline_interval( interval : duration ) is
  discard : int;
begin
  readline_idle_timeout := interval;
  discard := rl_set_keyboard_input_timeout(int(interval*1000000.0));
end set_readline_interval;
#end if;

#if READLINE
-----------------------------------------------------------------------------
--  GET LINE
--
-- Read a command line.  Set the terminal to the current TERM variable.
-- Use GNU Readline library to read a line of text, performing any
-- command completions.
--   Exception: raises READLINE_EOF if end-of-file is encountered.
-----------------------------------------------------------------------------

procedure getLine( line : out unbounded_string;
                   prompt : unbounded_string := null_unbounded_string;
                   keepHistory : boolean := false ) is
  pwd_id  : identifier;
  term_id : identifier;
  termEmu : chars_ptr;
  buffer  : chars_ptr := Null_Ptr;
  bold_prompt : unbounded_string;
begin
  findIdent( to_unbounded_string( "TERM" ), term_id );
  if term_id /= eof_t then
     termEmu := New_String( to_string( identifiers( term_id ).value.all ));
  else
     termEmu := New_String( "xterm" );
  end if;

  -- The default prompt
  -- For GNU readline, include the escape characters around the
  -- bold sequences

  if keepHistory then
     if prompt = null_unbounded_string then
        if released then
           bold_prompt := ASCII.SOH & term(bold) & ASCII.STX &
              defaultPrompt &
              ASCII.SOH & term(normal) & ASCII.STX;
        else
           bold_prompt := "spar-" & version & '-' & buildDate & ' ' &
              ASCII.SOH & term(bold) & ASCII.STX &
              defaultPrompt &
              ASCII.SOH & term( normal ) & ASCII.STX;
        end if;
     else
        bold_prompt := prompt;
     end if;
  end if;

  declare
     cprompt: constant char_array := To_C( to_string( bold_prompt ) );
  begin
     C_readline( Value( termEmu ), cprompt, buffer, boolean'pos(keepHistory) );
     interfaces.c.strings.free( termEmu );
     if buffer = Null_Ptr then
        if keepHistory then  -- nothing at prompt?
           line := to_unbounded_string( "return" ); -- treat as "return"
           -- TODO: this is a bit of a hack, simulating the line of input.
           -- may be done better?
           new_line;
           --put( line );
           return;
        end if;
     end if;
  end;

  if buffer = Null_Ptr then
     null; -- put_line( "debug eof" );
  else
     line := to_unbounded_string( Value( buffer ) );

     -- Deallocate C's string buffer once Ada has captured the string.

      C_free_readline( buffer );

  end if;

  -- history

  if length( line ) > 0 and keepHistory then -- don't save empty
     history( historyNext ).line := line;           -- lines
     findIdent( to_unbounded_string( "PWD" ), pwd_id ); -- TODO: SLOW!
     if pwd_t /= eof_t then
        history( historyNext ).pwd:= identifiers( pwd_id ).value.all;
     end if;
     history( historyNext ).time := Ada.Calendar.Clock; -- time
     historyNext := historyNext + 1;
     if historyNext > historyArray'last then
        historyNext := 1;
     end if;
  end if;

end getLine;
#else

procedure getLine( line : out unbounded_string; prompt : unbounded_string := null_unbounded_string; keepHistory : boolean := false ) is
   pwd_id : identifier;
-- read a line from the keyboard
-- Note: the keymap is hard-coded, but really should be taken from
-- current terminal settings.


  procedure redrawLine( oldPos : natural; newPos : natural := 0 ) is
  -- redraw the command line
    oldLines  : integer; -- number of lines of old text
    oldChars  : integer; -- number of characters from margin
  begin
    -- these two formulas are not quite right
    -- oldChars and oldLines are the coordinates of the cursor
    if keepHistory then
       -- if keepHistory, then we're at the command line
       --oldLines := ( getPromptIndent+oldPos ) /
       --   integer( displayInfo.col ) + getPromptExtraLines;
       oldLines := ( getPromptIndent+oldPos-2 ) /
          integer( displayInfo.col ) + getPromptExtraLines+1;
       oldChars  := (getPromptIndent + oldPos) -
          ( ( oldLines-1-getPromptExtraLines) * integer( displayInfo.col ) );
    else
       -- otherwise in the get_line function or someplace w/out a prompt
       oldLines := (oldPos-1) / integer( displayInfo.col )+1;
       oldChars  := oldPos - ( (oldLines-1) * integer( displayInfo.col ) );
    end if;
    -- position at start of old line
    for i in 1..oldChars-1 loop
        put( ASCII.BS );
    end loop;
    for i in 1..oldLines-1 loop
        put( term( cleol ) );
        put( term( up ) );
    end loop;
    -- draw new line
    if keepHistory then
       if prompt = null_unbounded_string then
          if released then
             put( bold( to_string( defaultPrompt ) ) );
          else
             put( "spar-" & version & '-' & buildDate & ' ' &
                bold( to_string( defaultPrompt ) ) );
          end if;
       else
          put( to_string( prompt ) );
       end if;
    end if;
    put( line );
    -- the final character on the bottom-right of the screen has special
    -- properties (ie. it triggers the scrolling).  We need to compensate
    -- or the final character won't appear.  If we don't scroll the screen
    -- by writing another character, the final character of the line won't
    -- be displayed!
    if getPromptIndent + length( line ) = 80 then
       put( ' ' );
       put( ASCII.BS );
    end if;
    put( term( cleol ) );
    -- reposition cursor, if desired
    if newPos > 0 then
       for i in newPos..length( line ) loop   -- reposition cursor
           put( ASCII.BS );                   -- (not very efficient)
       end loop;
    end if;
    --put( " - " );
    --put( oldchars'img );
    --put( displayInfo.col'img );
  end redrawLine;

  --  IS DIRECTORY (no readline library)
  --
  -- A quick and simple function to check to see if string dir refers
  -- to a directory.
  ---------------------------------------------------------------------------

  function isDirectory( dir : unbounded_string ) return boolean is
     f : dir_type;
     result : boolean := true;
  begin
    begin
    GNAT.Directory_Operations.Open( f, to_string( dir ) );
    GNAT.Directory_Operations.Close( f );
    -- TODO: should do a stat() probably more efficient
    exception when others => result := false;
    end;
    return result;
  end isDirectory;

  --- SLASHIFY PATH (no readline library)
  --
  -- Add a trailing character for a path.  If a directory, add a trailing /.
  -- If a file, add a trailing space.  If both, add nothing.
  -----------------------------------------------------------------------------

  function slashifyPath( dir, path : unbounded_string ) return string is
    f  : file_type;
    ch : character := ASCII.NUL;
  begin
    begin
      Ada.Text_IO.Open( f, in_file, to_string( dir & Dir_Separator & path ) );
      Ada.Text_IO.Close( f );
      if isDirectory( dir & Dir_Separator & path ) then
         ch := Dir_Separator;
      else
         ch := ' ';
      end if;
    exception when others => null;
    end;
    if ch /= ASCII.NUL then
       return "" & ch;
    end if;
    return "";
  end slashifyPath;

  --- COMPLETE PATHANME (no readline library)
  --
  ---------------------------------------------------------------------------

  procedure completePathname( dir : unbounded_string;
    s : unbounded_string;
    result : out unbounded_string;
    list : boolean := false ) is
    globCriteria : regexp;
    count        : natural := 0;
    dir2test     : Dir_Type;
    fileName     : string(1..256);
    fileNameLen  : natural;
    noPWD        : boolean := false;
    bestCommon   : unbounded_string; -- longest common part
    listCount    : natural := 0;
    isListing    : boolean := list;
    expandedDir  : unbounded_string;
    home_id      : identifier;
  begin
    -- Show a list of possible matches? Move down a line.
    if isListing then
       New_Line;
       put( term( cleop ) );
    end if;
    -- A tilde?  Then substitute in the home directory.
    if length( dir ) > 0 and then element( dir, 1) = '~' then
       findIdent( to_unbounded_string( "HOME" ), home_id );
       expandedDir := identifiers( home_id ).value.all & slice( dir, 2, length(dir));
    else
       expandedDir := dir;
    end if;
--put_line( "EXPANDED DIR = '" & dir & "' => " & expandedDir & "'+'" & s & "'" ); -- DEBUG
    bestCommon   := null_unbounded_string;
    globCriteria := Compile( to_string( s ), Glob => true,
       Case_Sensitive => true );
    begin
       open( dir2test, to_string( expandedDir ) );
    exception when others =>
      noPWD := true;
    end;
    if noPWD then
       simpleBeep;
       result := null_unbounded_string;
       return;
    end if;
    -- KB: 12/02/18 - gcc dir ops changes, no longer returns "." but we'll
    -- check below to be safe.
    --read( dir2test, fileName, fileNameLen ); -- skip "."
    --read( dir2test, fileName, fileNameLen ); -- skip ".."
    loop
      read( dir2test, fileName, fileNameLen );
      exit when fileNameLen = 0;
      if fileName( 1..fileNameLen ) = "." then
         null;
      elsif fileName( 1..fileNameLen ) = ".." then
         null;
      elsif Match( fileName(1..fileNameLen ) , globCriteria ) then
         count := count + 1;
         if isListing then
            put_line( fileName(1..fileNameLen) & slashifyPath( dir, to_unbounded_string( fileName(1..fileNameLen) ) ) );
            listCount := listCount + 1;
            isListing := listCount /= 10; -- no more than 10
            if not isListing then
               put_line( bold( "[Maybe More]" ) );
            end if;
         end if;
         if count = 1 then
            result := to_unbounded_string( fileName( 1..fileNameLen ) );
            bestCommon := result;
         else
            -- more than 1 hit? trim bestCommon start to fit the
            -- result.  If bestCommon is null, no common starting
            -- characters.
            for i in 1..fileNameLen loop
                exit when i > length( bestCommon );
                if i = fileNameLen then
                   bestCommon := head( bestCommon, i-1 );
                elsif element( bestcommon, i ) /= fileName(i) then
                   if i = 1 then
                      bestCommon := null_unbounded_string;
                   else
                      bestCommon := head( bestCommon, i-1 );
                   end if;
                end if;
            end loop;
         end if;
      end if;
    end loop;
    close( dir2test );
    if count > 1 then
       simpleBeep;
       result := bestCommon;
    elsif count = 0 then
       simpleBeep;
       result := null_unbounded_string;
    end if;
  end completePathname;

  --- LAST WORD (no readline library)
  --
  -- remove the last "word" delinated by a space from the string
  -- return the ending character of the rest of the string
  ---------------------------------------------------------------------------

  procedure lastWord( s : unbounded_string; lw : out unbounded_string;
    lp : out natural ) is
  begin
    lp := 0;
    for i in reverse 1..length( s )-1 loop
        if element( s, i ) = ' ' then
           lp := i;
           exit;
        end if;
    end loop;
    lw := to_unbounded_string( slice( s,  lp+1, length( s ) ) );
  end lastWord;

  vi_escape    : boolean := false; -- true if expecting vi-mode command
  arrow_escape : boolean := false; -- true if expecting VT-100 arrow
  last_was_esc : boolean := false; -- true if last char was ESC
  last_was_esc_o : boolean := false; -- true if last chars were ESC+O
  last_was_esc_sq_3 : boolean := false; -- true if last chars were ESC+[+3
  ch         : character; -- last character read
  pos        : natural;   -- position to insert next character
  old_pos    : natural;   -- for redrawing lines
  histpos    : integer;   -- position in history
  crit_pos   : natural;   -- for searching history
  criteria   : unbounded_string;
  justCompleted : boolean := false;
begin

  if keepHistory then
     if prompt = null_unbounded_string then
        if released then
           put( bold( to_string( defaultPrompt ) ) );
        else
           put( "spar-" & version & '-' & buildDate & ' ' &
              bold( to_string( defaultPrompt ) ) );
        end if;
     else
        put( to_string( prompt ) );
     end if;
  end if;

  line     := null_unbounded_string;
  criteria := null_unbounded_string;
  pos      := 1;
  histpos  := historyNext;

  loop
<<retry>> getKey( ch );
    if wasSIGINT then                   -- if SIGINT raised
       return;                          -- let caller determine what to do
    end if;

    -- VI ESCAPE MODE HANDLING
    --
    -- process sequence and convert to its emacs mode equivalent

    if ch = ASCII.ESC then
       if last_was_esc then             -- two escapes?
          ch := ASCII.HT;               -- cmdline completion
          last_was_esc := false;        -- cancel vi mode
          vi_escape := false;           -- exit vi escape
       else                             -- first escape?
          vi_escape := not vi_escape;   -- enter/exit vi escape mode
          if vi_escape then
             last_was_esc := true;         -- remember in case double esc
          end if;
          goto retry;                   -- and go get next key
       end if;
    elsif arrow_escape then                   -- ESC + [ + ...?
       last_was_esc := false;
       arrow_escape := false;                 -- got our arrow code
       if ch = 'A' then                       -- A = up
          ch := ASCII.DLE;
       elsif ch = 'B' then                    -- B = down
          ch := ASCII.SO;
       elsif ch = 'C' then                    -- C = right
          ch := ASCII.ACK;
       elsif ch = 'D' then                    -- D = left
          ch := ASCII.STX;
       elsif ch = '3' then                    -- Delete Key
          last_was_esc_sq_3 := true;
          goto retry;
       else
          simpleBeep;                         -- otherwise unknown
       end if;
    elsif last_was_esc_sq_3 then              -- ESC + [ + 3 + ...?
       last_was_esc_sq_3 := false;
       if ch = '~' then                       -- no vi equivalent to DEL key
          ch := ASCII.NAK;                    -- control-u
       end if;
    elsif last_was_esc_o then                 -- ESC + O + ...?
        last_was_esc := false;
        last_was_esc_o := false;
        if ch = 'H' then                      -- home key (ESC+O+H)
           ch := ASCII.SOH;
        elsif ch = 'F' then                   -- end key (ESC+O+F)
           ch := ASCII.ENQ;
        else
           simpleBeep;                        -- otherwise unknown
        end if;
    elsif vi_escape then
       last_was_esc := false;
       if ch ='[' then                        -- same as control-n
          arrow_escape := true;
          vi_escape := false;                 -- cancel vi mode
          goto retry;
       elsif ch ='j' then                     -- same as control-n
          ch := ASCII.SO;
       elsif ch = 'k' then                    -- same as control-p
          ch := ASCII.DLE;
       elsif ch = 'h' then                    -- same as control-b
          ch := ASCII.STX;
       elsif ch = 'l' then                    -- same as control-f
          ch := ASCII.ACK;
       elsif ch = '^' then                    -- same as control-a
          ch := ASCII.SOH;
       elsif ch = '$' then                    -- same as control-e
          ch := ASCII.ENQ;
       elsif ch = 'x' then                    -- same as control-u
          ch := ASCII.NAK;
       elsif ch = 'O' then                    -- home/end keys
          last_was_esc_o := true;             -- ESC+O
          vi_escape := false;                 -- cancel vi mode
          goto retry;
       else                                   -- else not supported
          vi_escape := false;                 -- drop out of vi escape mode
          simpleBeep;                         -- beep at illegal character
          goto retry;                         -- and get next key
       end if;
    elsif ch = ASCII.EOT then                 -- control-d / end of file?
       if length( line ) = 0 and keepHistory then  -- nothing at prompt?
          line := to_unbounded_string( "return" ); -- treat as "return"
          pos := 5;
          put( line );
       end if;                                -- otherwise
       ch := ASCII.CR;                        -- treat as end of input
    end if;

    if ch /= ASCII.HT then
       justCompleted := false;
    end if;

    -- EMACS MODE HANDLING

    case ch is
    when ASCII.HT =>                         -- TAB = pathname completion
       declare
          path : unbounded_string;
          dir  : unbounded_string;
          file : unbounded_string;
          lp   : natural;
          listFiles : boolean := false;
       begin
          if justCompleted then
             listFiles := true;
          else
             justCompleted := true;
          end if;
          lastWord( line, path, lp );
          if length( path ) = 0 then
             simpleBeep;
             goto retry;
          end if;
          -- as a special case, if the path is the name of a directory,
          -- just add a slash and redraw the line (bash works this way)
          -- I set justCompleted to false to be in line with bash's
          -- behaviour. ('ls .' , you have to hit tab 3 times to get list)
          if element( path, length( path ) ) /= Dir_Separator then
             if isDirectory( path ) then
                line := line & Dir_Separator;
                justCompleted := false;
                goto redraw;
             end if;
          end if;
          -- for path x/y, dir = x and y = file
          -- for path x/, dir = x and basename file = . (i.e. itself)
          dir  := dirname( path );
          -- we don't want .* in this case, just * unless the user
          -- specifically adds a /.
          if element( path, length( path ) ) /= Dir_Separator then
             file := basename( path );
          else
             file := null_unbounded_string;
          end if;
          file := file & "*";
          if listFiles then
             completePathname( dir, file, path, list => true );
          else
             completePathname( dir, file, path );
          end if;
          if length( path ) > 0 then
             if to_string( dir ) = "." then
                line := slice( line, 1, lp ) & path;
             else
                line := slice( line, 1, lp ) & dir & Dir_Separator & path;
             end if;
             declare
                -- if a file, add a ' '.
                f : file_type;
             begin
                Ada.Text_IO.Open( f, in_file, to_string( dir & Dir_Separator & path ) );
                Ada.Text_IO.Close( f );
                -- should do a stat()
                declare
                   -- if a dir, add a '/'
                   f : dir_type;
                begin
                   GNAT.Directory_Operations.Open( f, to_string( dir & Dir_Separator & path ) );
                   GNAT.Directory_Operations.Close( f );
                   -- TODO: should do a stat()
                   line := line & Dir_Separator;
                exception when others =>
                   line := line & " ";
                end;
             exception when others => null;
             end;
          end if;
<<redraw>>old_pos := pos;                     -- remember old position
          pos := length( line ) + 1;          -- new position at end
          redrawLine( old_pos, pos );
       end;
    when ASCII.CR | ASCII.LF =>
       new_line;
       if length( line ) > 0 and keepHistory then -- don't save empty
          history( historyNext ).line := line;           -- lines
          findIdent( to_unbounded_string( "PWD" ), pwd_id ); -- TODO: SLOW!
          if pwd_t /= eof_t then
             history( historyNext ).pwd:= identifiers( pwd_id ).value.all;
          end if;
          history( historyNext ).time := Ada.Calendar.Clock; -- time
          historyNext := historyNext + 1;
          if historyNext > historyArray'last then
             historyNext := 1;
          end if;
       end if;
       exit;
    when ASCII.BS | ASCII.DEL =>              -- control-h or delete key
       if pos > 1 then
          delete( line, pos-1, pos-1 );       -- delete char to left of cursor
          old_pos := pos;                     -- remember old position
          pos := pos - 1;                     -- new position is one to left
          redrawLine( old_pos, pos );         -- redraw line, cursor at end
       else                                   -- nothing to delete?
          simpleBeep;                         -- beep
       end if;
    when ASCII.NAK =>                         -- control-u or del key
       if length( line ) > 0 then
          old_pos := pos;                     -- remember old position
          if pos > length( line ) then        -- beyond eol?
             pos := pos - 1;                  -- position one to left
          elsif pos = length( line ) then     -- was at last char?
             delete( line, pos, pos );        -- delete char under cursor
             if pos /= 1 then                 -- if last char, don't move
                pos := pos - 1;               -- position is one to left
             end if;
          else                                -- else
             delete( line, pos, pos );        -- delete char under cursor
          end if;                             -- and don't move
          redrawLine( old_pos, pos );         -- redraw line
       else                                   -- nothing to delete?
          simpleBeep;                         -- beep
       end if;
    when ASCII.STX =>                         -- control-b
       if pos > 1 then
          old_pos := pos;                     -- remember old position
          pos := pos - 1;                     -- new position is one to left
          put( ASCII.BS );                    -- move cursor
       else                                   -- no place to go?
          simpleBeep;                         -- beep
       end if;
    when ASCII.ACK =>                         -- control-f
          if pos <= length( line ) then
             old_pos := pos;                  -- remember old position
             pos := pos + 1;                  -- new position is one to right
             put( term( right ) );            -- move cursor
          else                                -- no place to go?
             simpleBeep;                      -- beep
          end if;
    when ASCII.SO =>                          -- control-n
      if histpos = historyNext or not keepHistory then
         simpleBeep;
      else
         histpos := histpos + 1;
         if histpos > historyArray'last then
            histpos := 1;
         end if;
         old_pos := pos;                     -- remember old position
         line := history( histpos ).line;
         pos := length( line ) + 1;          -- new position at end of string
         redrawLine( old_pos, pos );         -- redraw line, cursor at end
      end if;
    when ASCII.DLE =>                         -- control-p
      if not keepHistory then
         simpleBeep;
      else
         old_pos := histpos;
         histpos := histpos - 1;
         if histpos < 1 then
            histpos := historyArray'last;
         end if;
         if histpos = historyNext then
            histpos := old_pos;
            simpleBeep;
         else
            old_pos := pos;                     -- remember old position
            line := history( histpos ).line;
            pos := length( line ) + 1;          -- new position at end of string
            redrawLine( old_pos, pos );         -- redraw line, cursor at end
         end if;
      end if;
    when ASCII.CAN =>                     -- control-x/c
      old_pos := pos;                     -- remember old position
      pos := 1;                           -- empty string now
      line := null_unbounded_string;
      redrawLine( old_pos );              -- redraw line
    when ASCII.SOH =>                     -- control-a
      redrawLine( pos, 1 );
      pos := 1;                           -- fix position
    when ASCII.ENQ =>                     -- control-e
      old_pos := pos;                     -- remember old position
      pos := length( line ) + 1;          -- new position at end of string
      redrawLine( old_pos, pos );         -- redraw line, cursor at end
    when ASCII.DC2 =>                         -- control-r
      if pos = 1 then
         simpleBeep;
         goto retry;
      elsif pos > length( line ) then
         criteria := line;
      else
         criteria := Delete( line, pos, length( line ) );
      end if;
      old_pos := histpos;
      crit_pos := pos;
      loop
        histpos := histpos - 1;
        if histpos < 1 then
           histpos := historyArray'last;
        end if;
        if histpos = historyNext then         -- out of history?
           line := criteria;                  -- restore old line
           histpos := old_pos;                -- that's all folks
           simpleBeep;
           exit;
        end if;
        line := history( histpos ).line;      -- next history line
        if Index( line, To_String( Criteria ) ) = 1 then -- matches criteria?
           pos := length( line ) + 1;         -- new position at end of string
           redrawLine( crit_pos, pos );       -- redraw line, cursor at end
           exit;
        end if;
      end loop;
    when ASCII.GS =>                          -- control-] (forward char search)
      old_pos := pos;
      getKey( ch );
      for i in pos+1..length( line ) loop
          if Element( line, i ) = ch then
             pos := i;
             exit;
          end if;
      end loop;
      if pos = old_pos then
         simpleBeep;
      else
         redrawLine( old_pos, pos );
      end if;
    when others =>
       if pos > length( line ) then           -- adding at end of line?
          line := line & ch;                  -- append character
          pos := pos + 1;                     -- new position is one to right
          put( ch );                          -- display character
       else
          insert( line, pos, ch & "" );       -- insert the character
          old_pos := pos;                     -- remember old position
          pos := pos + 1;                     -- new position is one to right
          redrawLine( old_pos, pos );         -- redraw line, cursor at end
       end if;
    end case;
  end loop;
end getLine;

-- Dummy Functions (not used unless GNU readline)

function Ada_ipset_word_generator(text : chars_ptr; state : int ) return chars_ptr is
begin
  return Null_Ptr;
end Ada_ipset_word_generator;

function Ada_git_word_generator(text : chars_ptr; state : int ) return chars_ptr is
begin
  return Null_Ptr;
end Ada_git_word_generator;

function Ada_svn_word_generator(text : chars_ptr; state : int ) return chars_ptr is
begin
  return Null_Ptr;
end Ada_svn_word_generator;

function Ada_command_word_generator(text : chars_ptr; state : int ) return chars_ptr is
begin
  return Null_Ptr;
end Ada_command_word_generator;

function Ada_apt_word_generator(text : chars_ptr; state : int ) return chars_ptr is
begin
  return Null_Ptr;
end Ada_apt_word_generator;

function Ada_yum_word_generator(text : chars_ptr; state : int ) return chars_ptr is
begin
  return Null_Ptr;
end Ada_yum_word_generator;

function Ada_docker_word_generator(text : chars_ptr; state : int ) return chars_ptr is
begin
  return Null_Ptr;
end Ada_docker_word_generator;

function Ada_k8s_word_generator(text : chars_ptr; state : int ) return chars_ptr is
begin
  return Null_Ptr;
end Ada_k8s_word_generator;

function Ada_npm_word_generator(text : chars_ptr; state : int ) return chars_ptr is
begin
  return Null_Ptr;
end Ada_npm_word_generator;

function Ada_executable_word_generator(text : chars_ptr; state : int ) return chars_ptr is
begin
  return Null_Ptr;
end Ada_executable_word_generator;

function Ada_assignment_word_generator(text : chars_ptr; state : int ) return chars_ptr is
begin
  return Null_Ptr;
end Ada_assignment_word_generator;

function Ada_variable_word_generator(text : chars_ptr; state : int ) return chars_ptr is
begin
  return Null_Ptr;
end Ada_variable_word_generator;

function Ada_parameter_word_generator(text : chars_ptr; state : int ) return chars_ptr is
begin
  return Null_Ptr;
end Ada_parameter_word_generator;

function Ada_readline_timeout return int is
begin
  return 0;
end Ada_readline_timeout;

procedure set_readline_interval( interval : duration ) is
begin
  null;
end set_readline_interval;
#end if;

-----------------------------------------------------------------------------
--  HAS READLINE
--
-- TRUE if we are using the GNU readline library.
-----------------------------------------------------------------------------

function has_readline return boolean is
begin
#if READLINE
  return true;
#else
  return false;
#end if;
end has_readline;


-----------------------------------------------------------------------------
--
-- HOUSEKEEPING
--
-----------------------------------------------------------------------------


-----------------------------------------------------------------------------
--  STARTUP READLINE
--
-- Startup this package and GNU Readline library, performing any setup
-- tasks.  The inputMode should be set before running.
-----------------------------------------------------------------------------

#if READLINE
procedure startupGetline(
     optionOffset : natural;
     idle_callback : a_readline_callback) is
  historyPath : unbounded_string;
  historyFile : file_type;
  temp        : unbounded_string;
  home_id     : identifier;
  year        : year_number;
  month       : month_number;
  day         : day_number;
  seconds     : day_duration;
begin
  readline_idle_callback := idle_callback;
  C_init_readline;
  --if inputMode = interactive then
  -- We do not know if this is interactive or not when this is called.
  -- inputMode is not yet set and optionOffset is defined in the scanner.
  -- optionOffset must be passed in as a parameter.
  if optionOffset = 0 then
     findIdent( to_unbounded_string( "HOME" ), home_id );
     if home_id /= eof_t then
        if sessionName = "" then
            historyPath := identifiers( home_id ).value.all & directory_delimiter &
               ".sparforte_history.txt";
        else
            historyPath := identifiers( home_id ).value.all & directory_delimiter &
               ".sparforte_" & sessionName & "_history.txt";
        end if;
        if C_is_includable_file( to_string( historyPath ) & ASCII.NUL ) then
           open( historyFile, in_file, to_string( historyPath ) );
           for i in history'first..history'last loop
               -- TODO date conversion
               -- ada 2005
               get_line( historyFile, temp );
               year := year_number'value( to_string( temp ) );
               get_line( historyFile, temp );
               month := month_number'value( to_string( temp ) );
               get_line( historyFile, temp );
               day := day_number'value( to_string( temp ) );
               get_line( historyFile, temp );
               seconds := day_duration'value( to_string( temp ) );
               history(i).time := time_of( year, month, day, seconds );
               get_line( historyFile, history(i).pwd );
               get_line( historyFile, history(i).line );
           end loop;
           get_line( historyFile, temp );
           historyNext := natural'value( to_string( temp ) );
           close( historyFile );

           -- To Add the history to GNU readline, add only non-blank lines.
           -- historyNext is the last line and so is the first to be added.
           for i in historyNext..history'last loop
               if history(i).line /= null_unbounded_string then
                  add_history( to_string( history(i).line ) & ASCII.NUL );
               end if;
           end loop;
           for i in 1..historyNext-1 loop
               if history(i).line /= null_unbounded_string then
                  add_history( to_string( history(i).line ) & ASCII.NUL );
               end if;
           end loop;
        end if;
     end if;
  end if;
exception when name_error => -- no file
  historyNext := 1;
when others => -- end_error = out of data
  close( historyFile );
  historyNext := 1;
end startupGetline;
#else
procedure startupGetline(
  optionOffset : natural;
  idle_callback : a_readline_callback) is
begin
  null;
end startupGetline;
#end if;

-----------------------------------------------------------------------------
--  SHUTDOWN READLINE
--
-- Shutdown this package and GNU Readline library, performing any cleanup
-- tasks.  If no_history is true, do not save the history to the history
-- file.
--  Exception: COMPLETION_MEMORY_LEAK is raised if there are C strings
-- that weren't deallocated.
-----------------------------------------------------------------------------

#if READLINE
procedure shutdownGetline( save_history : boolean ) is
  historyPath : unbounded_string;
  historyFile : file_type;
  home_id     : identifier;
  year        : year_number;
  month       : month_number;
  day         : day_number;
  seconds     : day_duration;
begin
  if inputMode = interactive then
     findIdent( to_unbounded_string( "HOME" ), home_id );
     if home_id /= eof_t then
        if sessionName = "" then
            historyPath := identifiers( home_id ).value.all & directory_delimiter &
               ".sparforte_history.txt";
        else
            historyPath := identifiers( home_id ).value.all & directory_delimiter &
               ".sparforte_" & sessionName & "_history.txt";
        end if;
        if save_history then
           create( historyFile, out_file, to_string( historyPath ) );
           for i in history'first..history'last loop
               -- TODO date conversion
               split( history(i).time, year, month, day, seconds );
               put_line( historyFile, year'img );
               put_line( historyFile, month'img );
               put_line( historyFile, day'img );
               put_line( historyFile, seconds'img );
               put_line( historyFile, to_string( history(i).pwd ) );
               put_line( historyFile, to_string( history(i).line ) );
           end loop;
           put_line( historyFile, historyNext'img);
           close( historyFile );
        end if;
     end if;
  end if;
end shutdownGetline;
#else
procedure shutdownGetline( save_history : boolean ) is
begin
  null;
end shutdownGetline;
#end if;

end pegasoft.user_io.getline;

