#############################################################################
##
##  This file is part of GAP, a system for computational discrete algebra.
##  This file's authors include Frank Lübeck.
##
##  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
##
##  The  files  pager.g{d,i}  contain  the `Pager'  utility.  A  rudimentary
##  version of this  was integrated in first versions of  GAP's help system.
##  But this utility is certainly useful for other purposes as well.
##
##
##  There is a builtin pager `PAGER_BUILTIN', but  at least under UNIX one
##  should use an external one.  This can be  set via the variable
##  `SetUserPreference("Pager", ...);'
##  (e.g., SetUserPreference("Pager", "less");).
##  Here,  `less'  should be  in the executable
##  PATH of the user and we assume that it supports an argument `+num' for
##  starting display in   line number `num'.   Additional options  can  be
##  given by  `SetUserPreference("PagerOptions", ...);' as list of strings.
##
##  The user function is `Pager'.
##
##  The input of `Pager( lines )' can have one the following forms:
##
##   (1) a string (i.e., lines are separated by '\n')
##   (2) a list of strings (without '\n') interpreted as lines of output
##   (3) a record with component .lines as in (1) or (2) and optional further
##       components
##
##  In (3) currently the following components are used:
##
##   .formatted (true/false) If true, the builtin pager tries to avoid
##                           line breaks by GAP's Print.
##   .start (number/string)  The display is started with line .start or
##                           at the first line containing .start, but
##                           beginning is available via back scrolling.
##   .exitAtEnd (true/false) If true (default), the builtin pager is
##                           terminated as soon as the end of the list is
##                           reached;
##                           if false, entering 'q' is necessary in order to
##                           return from the pager.
##

# The preferred pager can be specified via a user preference.
DeclareUserPreference( rec(
  name:= [ "Pager", "PagerOptions" ],
  description:= [
  "For displaying help pages on screen and other things &GAP; has a rudimentary \
builtin pager. We recommend using a more sophisticated external program.  \
For example, when you have the program <C>less</C> on your computer we recommend:",
    " <C>Pager := \"less\";</C>",
    " <C>PagerOptions := [\"-f\", \"-r\", \"-a\", \"-i\", \"-M\", \"-j2\"];</C>",
    "If you want to use <C>more</C>, we suggest to use the <C>-f</C> option.  \
If you want to use the pager defined in your environment then \
leave the <C>Pager</C> and <C>PagerOptions</C> preferences empty."
    ],
  default:= function()    # copied from GAPInfo.READENVPAGEREDITOR
    local str, sp, pager, options;
    if IsBound(GAPInfo.KernelInfo.ENVIRONMENT.PAGER) then
      str := GAPInfo.KernelInfo.ENVIRONMENT.PAGER;
      sp := SplitStringInternal(str, "", " \n\t\r");
      if Length(sp) > 0 then
        pager:= sp[1];
        options:= sp{ [ 2 .. Length( sp ) ] };
        # 'less' could have options in variable 'LESS'
        if "less" in SplitStringInternal(sp[1], "", "/\\") then
          if IsBound(GAPInfo.KernelInfo.ENVIRONMENT.LESS) then
            str := GAPInfo.KernelInfo.ENVIRONMENT.LESS;
            sp := SplitStringInternal(str, "", " \n\t\r");
            Append( options, sp );
          fi;
          # make sure -r is used
          Add( options, "-r" );
        elif "more" in SplitStringInternal(sp[1], "", "/\\") then
          if IsBound(GAPInfo.KernelInfo.ENVIRONMENT.MORE) then
            # similarly for 'more'
            str := GAPInfo.KernelInfo.ENVIRONMENT.MORE;
            sp := SplitStringInternal(str, "", " \n\t\r");
            Append( options, sp );
          fi;
          # make sure -f is used
          Add( options, "-f" );
        fi;
        return [ pager, options ];
      fi;
    fi;
    # The builtin pager does not work in HPCGAP
    if IsHPCGAP then
      return [ "less" , ["-f","-r","-a","-i","-M","-j2"] ];
    else
      return [ "builtin", [] ];
    fi;
  end,
  ) );

#############################################################################
##
#F  PAGER_BUILTIN( <lines> )    . . . . . . . . . . . . . . . .  format lines
##
##  Show <A>lines</A> in a rudimentary pager inside the terminal.
##  The argument can be either a string or a list of strings or a record.
##  In the latter case, the component <C>lines</C> must be a string or a
##  list of strings,
##  and the following additional components are supported.
##  <List>
##  <Mark>formatted</Mark>
##  <Item>
##    <K>true</K> or <K>false</K> (default <K>false</K>),
##    <K>true</K> means that lines not fitting into one terminal line
##    (see <Ref Func="SizeScreen"/>) will be distributed to several lines,
##  </Item>
##  <Mark>start</Mark>
##  <Item>
##    a positive integer denoting the position of the first line that is
##    shown,
##    or a string meaning that the first line containing this string
##    shall be shown;
##    the default is 1,
##  </Item>
##  <Mark>exitAtEnd</Mark>
##  <Item>
##    <K>true</K> or <K>false</K> (default <K>true</K>),
##    meaning whether the pager should terminate automatically
##    once the last line is shown.
##  </Item>
##  </List>
##
# If  the text contains ANSI color sequences we reset  the terminal before
# we print the last line.
BindGlobal("PAGER_BUILTIN", function( r )
  local formatted, linepos, exitAtEnd, lines, size, wd, pl, count, i, stream,
        halt, lenhalt, delhaltline, from, len, emptyline, char, out;

  formatted := false;
  linepos := 1;
  exitAtEnd:= true;
  # don't print this to LOG files
  out := OutputTextUser();

  if IsRecord(r) then
    lines := r.lines;
  else
    lines := r;
  fi;

  if IsString(lines) then
    lines := SplitString(lines, "\n", "");
  elif not formatted then
    lines := ShallowCopy(lines);
  fi;

  if Length( lines ) = 0 then
    return;
  fi;

  if IsRecord(r) then
    if IsBound(r.formatted) then
      formatted := r.formatted;
      if formatted <> true and formatted <> false then
        Error("unsupported r.formatted value: ", formatted);
      fi;
    fi;
    if IsBound(r.start) then
      if IsPosInt(r.start) then
        linepos := r.start;
      elif IsString(r.start) then
        linepos := PositionProperty(lines,
                     l -> PositionSublist(l, r.start) <> fail);
        if linepos = fail then
          linepos := 1;
        fi;
      else
        Error("unsupported r.start value: ", r.start);
      fi;
    fi;
    if IsBound( r.exitAtEnd ) then
      exitAtEnd:= r.exitAtEnd;
      if exitAtEnd <> true and exitAtEnd <> false then
        Error("unsupported r.exitAtEnd value: ", exitAtEnd);
      fi;
    fi;
  fi;

  size   := SizeScreen();
  wd := QuoInt(size[1]+2, 2);
  # really print line without breaking it
  pl := function(l, final)
    local   r;
    r := 1;
    while r*wd<=Length(l) do
      PrintTo(out, l{[(r-1)*wd+1..r*wd]}, "\c");
      r := r+1;
    od;
    if (r-1)*wd < Length(l) then
      PrintTo(out, l{[(r-1)*wd+1..Length(l)]});
    fi;
    PrintTo(out, final);
  end;

  if not formatted then
    # cope with overfull lines
    count:=1;
    while count<=Length(lines) do
      if Length(lines[count])>size[1]-2 then
        # find the last blank before this position
        i:=size[1]-2;
        while i>0 and lines[count][i]<>' ' do
          i:=i-1;
        od;
        if i>0 then
          if not IsBound(lines[count+1]) then
            lines[count+1]:="";
          fi;
          lines[count+1]:=Concatenation(
             lines[count]{[i+1..Length(lines[count])]}," ", lines[count+1]);
          lines[count]:=lines[count]{[1..i-1]};
        fi;
      fi;
      count:=count+1;
    od;
  fi;

  stream := InputTextFile("*errin*");
  count  := 0;
  halt   :=
    "  -- <space> page, <n> next line, <b> back, <p> back line, <q> quit --\c";
  # remember number of visible characters
  lenhalt := Length(halt)-1;
  if UserPreference("UseColorsInTerminal") = true then
    halt := Concatenation("\033[0m", halt);
  fi;
  delhaltline := function()
    local i;
    for i  in [1..lenhalt] do
      PrintTo(out,  "\b\c \c\b\c" );
    od;
  end;
  from := linepos;
  len := Length(lines);
  emptyline:= String( "", size[1]-2 );
  if len < from then
    # Ignore the start line.
    # (The pager 'less' shows a warning and then goes to the first line.)
    from:= 1;
  fi;
  repeat
    for i in [from..Minimum(len, from+size[2]-2)] do
      pl(lines[i], "\n");
    od;
    if len = i then
      if exitAtEnd then
        break;
      fi;
      for i in [ len+1 .. from+size[2]-2 ] do
        pl( emptyline, "\n" );
      od;
    fi;
    pl(halt, "");
    repeat
      char := ReadByte(stream);
      if char = fail then
        char := 'q';
      else
        char := CHAR_INT(char);
      fi;
    until char in " nbpq";
    if char = ' ' and i < len then
      from := from+size[2]-1;
    elif char = 'n' and i < len then
      from := from+1;
    elif char = 'p' and from>1 then
      from := from-1;
    elif char = 'b' then
      from := Maximum(1, from-size[2]+1);
    fi;
    delhaltline();
  until char = 'q';

  CloseStream(stream);
end);

# for using `more' or `less' or ... (read from `UserPreference("Pager")')
# we assume that UserPreference("Pager") allows command line option
# +num for starting display in line num

BindGlobal("PAGER_EXTERNAL",  function( lines )
  local   pager,  linepos,  str,  i,  cmdargs,  stream;

  pager := UserPreference("Pager");
  if not (Length(pager) > 0 and pager[1] = '/' and IsExecutableFile(pager))
      then
    pager := PathSystemProgram( UserPreference("Pager") );
  fi;
  if pager=fail then
    Error( "Pager ", UserPreference("Pager"),
            " not found, reset with `SetUserPreference(\"Pager\", ...);'." );
  fi;
  linepos := 1;
  if IsRecord(lines) then
    if IsBound(lines.start) then
      linepos := lines.start;
      if not (IsPosInt(lines.start) or IsString(lines.start)) then
        Error("unsupported lines.start value: ", linepos);
      fi;
    fi;
    lines := lines.lines;
  fi;

  if not IsString(lines) then
    str:="";
    for i in lines do
      Append(str,i);
      Add(str,'\n');
    od;
    lines := str;
  fi;
  if IsString(linepos) then
    cmdargs := [Concatenation("+/", linepos)];
  elif linepos > 1 then
    cmdargs := [Concatenation("+", String(linepos))];
  else
    cmdargs := [];
  fi;
  stream:=InputTextString(lines);
  Process(DirectoryCurrent(), pager, stream, OutputTextUser(),
  Concatenation( UserPreference("PagerOptions"), cmdargs ));
end);

InstallGlobalFunction("Pager",  function(lines)
  if UserPreference("Pager") = "builtin" then
    PAGER_BUILTIN(lines);
  else
    PAGER_EXTERNAL(lines);
  fi;
end);

BindGlobal( "PagerAsHelpViewer", function( lines )
  if UserPreference( "Pager" ) = "builtin" then
    if IsRecord( lines ) then
      lines.exitAtEnd:= false;
    else
      lines:= rec( lines:= lines, exitAtEnd:= false );
    fi;
    PAGER_BUILTIN( lines );
  else
    PAGER_EXTERNAL( lines );
  fi;
end );


