------------------------------------------------------------------------------
--                                                                          --
--                         GNAT RUN-TIME COMPONENTS                         --
--                                                                          --
--                 A D A . T E X T _ I O . G E T _ L I N E                  --
--                                                                          --
--                                 B o d y                                  --
--                                                                          --
--          Copyright (C) 1992-2020, Free Software Foundation, Inc.         --
--                                                                          --
-- GNAT 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 3,  or (at your option) any later ver- --
-- sion.  GNAT 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.                                     --
--                                                                          --
-- As a special exception under Section 7 of GPL version 3, you are granted --
-- additional permissions described in the GCC Runtime Library Exception,   --
-- version 3.1, as published by the Free Software Foundation.               --
--                                                                          --
-- You should have received a copy of the GNU General Public License and    --
-- a copy of the GCC Runtime Library Exception along with this program;     --
-- see the files COPYING3 and COPYING.RUNTIME respectively.  If not, see    --
-- <http://www.gnu.org/licenses/>.                                          --
--                                                                          --
-- GNAT was originally developed  by the GNAT team at  New York University. --
-- Extensive contributions were provided by Ada Core Technologies Inc.      --
--                                                                          --
------------------------------------------------------------------------------

--  The implementation of Ada.Text_IO.Get_Line is split into a subunit so that
--  different implementations can be used on different systems. This is the
--  standard implementation (it uses low level features not suitable for use
--  on virtual machines).

with System;                  use System;
with System.Storage_Elements; use System.Storage_Elements;

separate (Ada.Text_IO)
procedure Get_Line
  (File : File_Type;
   Item : out String;
   Last : out Natural)
is
   Chunk_Size : constant := 80;
   --  We read into a fixed size auxiliary buffer. Because this buffer
   --  needs to be pre-initialized, there is a trade-off between size and
   --  speed. Experiments find returns are diminishing after 50 and this
   --  size allows most lines to be processed with a single read.

   ch : int;
   N  : Natural;

   procedure memcpy (s1, s2 : chars; n : size_t);
   pragma Import (C, memcpy);

   function memchr (s : chars; ch : int; n : size_t) return chars;
   pragma Import (C, memchr);

   procedure memset (b : chars; ch : int; n : size_t);
   pragma Import (C, memset);

   function Get_Chunk (N : Positive) return Natural;
   --  Reads at most N - 1 characters into Item (Last + 1 .. Item'Last),
   --  updating Last. Raises End_Error if nothing was read (End_Of_File).
   --  Returns number of characters still to read (either 0 or 1) in
   --  case of success.

   ---------------
   -- Get_Chunk --
   ---------------

   function Get_Chunk (N : Positive) return Natural is
      Buf : String (1 .. Chunk_Size);
      S   : constant chars := Buf (1)'Address;
      P   : chars;

   begin
      if N = 1 then
         return N;
      end if;

      memset (S, 10, size_t (N));

      if fgets (S, N, File.Stream) = Null_Address then
         if ferror (File.Stream) /= 0 then
            raise Device_Error;

         --  If incomplete last line, pretend we found a LM

         elsif Last >= Item'First then
            return 0;

         else
            raise End_Error;
         end if;
      end if;

      P := memchr (S, LM, size_t (N));

      --  If no LM is found, the buffer got filled without reading a new
      --  line. Otherwise, the LM is either one from the input, or else one
      --  from the initialization, which means an incomplete end-of-line was
      --  encountered. Only in first case the LM will be followed by a 0.

      if P = Null_Address then
         pragma Assert (Buf (N) = ASCII.NUL);
         memcpy (Item (Last + 1)'Address,
                 Buf (1)'Address, size_t (N - 1));
         Last := Last + N - 1;

         return 1;

      else
         --  P points to the LM character. Set K so Buf (K) is the character
         --  right before.

         declare
            K : Natural := Natural (P - S);

         begin
            --  If K + 2 is greater than N, then Buf (K + 1) cannot be a LM
            --  character from the source file, as the call to fgets copied at
            --  most N - 1 characters. Otherwise, either LM is a character from
            --  the source file and then Buf (K + 2) should be 0, or LM is a
            --  character put in Buf by memset and then Buf (K) is the 0 put in
            --  by fgets. In both cases where LM does not come from the source
            --  file, compensate.

            if K + 2 > N or else Buf (K + 2) /= ASCII.NUL then

               --  Incomplete last line, so remove the extra 0

               pragma Assert (Buf (K) = ASCII.NUL);
               K := K - 1;
            end if;

            memcpy (Item (Last + 1)'Address,
                    Buf (1)'Address, size_t (K));
            Last := Last + K;
         end;

         return 0;
      end if;
   end Get_Chunk;

--  Start of processing for Get_Line

begin
   FIO.Check_Read_Status (AP (File));

   --  Set Last to Item'First - 1 when no characters are read, as mandated by
   --  Ada RM. In the case where Item'First is negative or null, this results
   --  in Constraint_Error being raised.

   Last := Item'First - 1;

   --  Immediate exit for null string, this is a case in which we do not
   --  need to test for end of file and we do not skip a line mark under
   --  any circumstances.

   if Item'First > Item'Last then
      return;
   end if;

   N := Item'Last - Item'First + 1;

   --  Here we have at least one character, if we are immediately before
   --  a line mark, then we will just skip past it storing no characters.

   if File.Before_LM then
      File.Before_LM := False;
      File.Before_LM_PM := False;

   --  Otherwise we need to read some characters

   else
      while N >= Chunk_Size loop
         if Get_Chunk (Chunk_Size) = 0 then
            N := 0;
         else
            N := N - Chunk_Size + 1;
         end if;
      end loop;

      if N > 1 then
         N := Get_Chunk (N);
      end if;

      --  Almost there, only a little bit more to read

      if N = 1 then
         ch := Getc (File);

         --  If we get EOF after already reading data, this is an incomplete
         --  last line, in which case no End_Error should be raised.

         if ch = EOF then
            if Last < Item'First then
               raise End_Error;

            else  --  All done
               return;
            end if;

         elsif ch /= LM then

            --  Buffer really is full without having seen LM, update col

            Last := Last + 1;
            Item (Last) := Character'Val (ch);
            File.Col := File.Col + Count (Last - Item'First + 1);
            return;
         end if;
      end if;
   end if;

   --  We have skipped past, but not stored, a line mark. Skip following
   --  page mark if one follows, but do not do this for a non-regular file
   --  (since otherwise we get annoying wait for an extra character)

   File.Line := File.Line + 1;
   File.Col := 1;

   if File.Before_LM_PM then
      File.Line := 1;
      File.Before_LM_PM := False;
      File.Page := File.Page + 1;

   elsif File.Is_Regular_File then
      ch := Getc (File);

      if ch = PM and then File.Is_Regular_File then
         File.Line := 1;
         File.Page := File.Page + 1;
      else
         Ungetc (ch, File);
      end if;
   end if;
end Get_Line;
