------------------------------------------------------------------------------
--                              Ada Web Server                              --
--                                                                          --
--                     Copyright (C) 2002-2021, AdaCore                     --
--                                                                          --
--  This library is free software;  you can redistribute it and/or modify   --
--  it under terms of the  GNU General Public License  as published by the  --
--  Free Software  Foundation;  either version 3,  or (at your  option) any --
--  later version. This library is distributed in the hope that it will be  --
--  useful, but WITHOUT ANY WARRANTY;  without even the implied warranty of --
--  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.                    --
--                                                                          --
--  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/>.                                         --
--                                                                          --
--  As a special exception, if other files instantiate generics from this   --
--  unit, or you link this unit with other files to produce an executable,  --
--  this  unit  does not  by itself cause  the resulting executable to be   --
--  covered by the GNU General Public License. This exception does not      --
--  however invalidate any other reasons why the executable file  might be  --
--  covered by the  GNU Public License.                                     --
------------------------------------------------------------------------------

pragma Ada_2012;

with AWS.Containers.Memory_Streams;
with AWS.Default;
with AWS.Translator;

package body AWS.Net.Buffered is

   CRLF : constant Stream_Element_Array :=
            (1 => Character'Pos (ASCII.CR),
             2 => Character'Pos (ASCII.LF));

   Input_Limit : Positive := AWS.Default.Input_Line_Size_Limit with Atomic;

   function Is_Empty
     (C : not null access constant Read_Cache) return Boolean
   is
     (C.First > C.Last) with Inline_Always;
   --  Returns true if the read-cache is empty

   procedure Read (Socket : Socket_Type'Class)
     with Pre => Socket.C.R_Cache /= null;
   --  Refill the read-cache

   function Get_Read_Cache
     (Socket : Socket_Type'Class)
      return not null access Read_Cache with Inline;
   --  Get the socket read cache, create the cache if it is not created

   function Get_Write_Cache
     (Socket : Socket_Type'Class)
      return not null access Write_Cache with Inline;
   --  Get the socket write cache, create the cache if it is not created

   -----------
   -- Flush --
   -----------

   procedure Flush (Socket : Socket_Type'Class) is
      C : Write_Cache_Access renames Socket.C.W_Cache;
   begin
      if C = null then
         return;
      end if;

      if C.Last > 0 then
         Send (Socket, C.Buffer (1 .. C.Last));
         C.Last := 0;
      end if;
   end Flush;

   --------------
   -- Get_Byte --
   --------------

   function Get_Byte (Socket : Socket_Type'Class) return Stream_Element  is
      C    : constant not null access Read_Cache := Get_Read_Cache (Socket);
      Byte : Stream_Element;
   begin
      if Is_Empty (C) then
         Read (Socket);
      end if;

      Byte    := C.Buffer (C.First);
      C.First := C.First + 1;

      return Byte;
   end Get_Byte;

   --------------
   -- Get_Char --
   --------------

   function Get_Char (Socket : Socket_Type'Class) return Character is
   begin
      return Character'Val (Get_Byte (Socket));
   end Get_Char;

   ---------------------
   -- Get_Input_Limit --
   ---------------------

   function Get_Input_Limit return Stream_Element_Offset is
   begin
      return Stream_Element_Offset (Input_Limit);
   end Get_Input_Limit;

   --------------
   -- Get_Line --
   --------------

   function Get_Line (Socket : Socket_Type'Class) return String is
      Line : constant String := Read_Until (Socket, "" & ASCII.LF, True);
   begin
      if Line'Length > 0 and then Line (Line'Last) = ASCII.LF then
         if Line'Length > 1 and then Line (Line'Last - 1) = ASCII.CR then
            return Line (Line'First .. Line'Last - 2);
         else
            return Line (Line'First .. Line'Last - 1);
         end if;

      else
         return Line;
      end if;
   end Get_Line;

   --------------------
   -- Get_Read_Cache --
   --------------------

   function Get_Read_Cache
     (Socket : Socket_Type'Class) return not null access Read_Cache is
   begin
      if Socket.C.R_Cache = null then
         Socket.C.R_Cache := new Read_Cache (R_Cache_Size);
      end if;

      return Socket.C.R_Cache;
   end Get_Read_Cache;

   ---------------------
   -- Get_Write_Cache --
   ---------------------

   function Get_Write_Cache
     (Socket : Socket_Type'Class) return not null access Write_Cache is
   begin
      if Socket.C.W_Cache = null then
         Socket.C.W_Cache := new Write_Cache (W_Cache_Size);
      end if;

      return Socket.C.W_Cache;
   end Get_Write_Cache;

   --------------
   -- New_Line --
   --------------

   procedure New_Line (Socket : Socket_Type'Class) is
   begin
      Write (Socket, CRLF);
   end New_Line;

   ---------------
   -- Peek_Char --
   ---------------

   function Peek_Char (Socket : Socket_Type'Class) return Character is
      C : constant not null access Read_Cache := Get_Read_Cache (Socket);
   begin
      if Is_Empty (C) then
         Read (Socket);
      end if;

      return Character'Val (Natural (C.Buffer (C.First)));
   end Peek_Char;

   ---------
   -- Put --
   ---------

   procedure Put (Socket : Socket_Type'Class; Item : String) is
   begin
      Write (Socket, Translator.To_Stream_Element_Array (Item));
   end Put;

   --------------
   -- Put_Line --
   --------------

   procedure Put_Line (Socket : Socket_Type'Class; Item : String) is
   begin
      Write (Socket, Translator.To_Stream_Element_Array (Item) & CRLF);
   end Put_Line;

   ----------
   -- Read --
   ----------

   procedure Read (Socket : Socket_Type'Class) is
      C : constant not null access Read_Cache := Socket.C.R_Cache;
   begin
      Receive (Socket, C.Buffer, C.Last);

      --  Reset C.First only after successful Receive, the buffer would
      --  remain empty on timeout this way.

      C.First := C.Buffer'First;
   end Read;

   procedure Read
     (Socket : Socket_Type'Class;
      Data   : out Stream_Element_Array;
      Last   : out Stream_Element_Offset)
   is
      C : constant not null access Read_Cache := Get_Read_Cache (Socket);
   begin
      Flush (Socket);

      if Is_Empty (C) then

         if Data'Length < C.Buffer'Length then
            --  If Data fit in the cache, fill it
            Read (Socket);

         else
            --  Otherwise read the socket directly
            Receive (Socket, Data, Last);
            return;
         end if;
      end if;

      Read_Buffer (Socket, Data, Last);

      --  Data could remain in internal socket buffer, if there is some
      --  space on the buffer, read the socket.

      if Last < Data'Last and then Net.Pending (Socket) > 0 then
         Receive (Socket, Data (Last + 1 .. Data'Last), Last);
      end if;
   end Read;

   function Read
     (Socket : Socket_Type'Class;
      Max    : Stream_Element_Count := 4096) return Stream_Element_Array
   is
      Buffer : Stream_Element_Array (1 .. Max);
      Last   : Stream_Element_Offset;
   begin
      Read (Socket, Buffer, Last);

      return Buffer (1 .. Last);
   end Read;

   procedure Read
     (Socket : Socket_Type'Class; Data : out Stream_Element_Array)
   is
      Last  : Stream_Element_Offset;
      First : Stream_Element_Offset := Data'First;
   begin
      loop
         Read (Socket, Data (First .. Data'Last), Last);
         exit when Last = Data'Last;
         First := Last + 1;
      end loop;
   end Read;

   -----------------
   -- Read_Buffer --
   -----------------

   procedure Read_Buffer
     (Socket : Socket_Type'Class;
      Data   : out Stream_Element_Array;
      Last   : out Stream_Element_Offset)
   is
      C : Read_Cache_Access renames Socket.C.R_Cache;
   begin
      if C = null then
         Last := Last_Index (Data'First, Count => 0);

      else
         declare
            C_Last : constant Stream_Element_Offset :=
                       Stream_Element_Offset'Min
                         (C.Last, C.First + Data'Length - 1);
         begin
            Last := Data'First + C_Last - C.First;
            Data (Data'First .. Last) := C.Buffer (C.First .. C_Last);
            C.First := C_Last + 1;
         end;
      end if;
   end Read_Buffer;

   ----------------
   -- Read_Until --
   ----------------

   function Read_Until
     (Socket    : Socket_Type'Class;
      Delimiter : Stream_Element_Array;
      Wait      : Boolean := True) return Stream_Element_Array
   is
      use Containers.Memory_Streams;

      function Buffered return Stream_Element_Array with Inline;

      procedure Finalize;

      Finalizer : Utils.Finalizer (Finalize'Access) with Unreferenced;
      Buffer    : Stream_Type;

      --------------
      -- Buffered --
      --------------

      function Buffered return Stream_Element_Array is
         Result : Stream_Element_Array (1 .. Size (Buffer));
         Last   : Stream_Element_Offset;
      begin
         Read (Buffer, Result, Last);
         return Result;
      end Buffered;

      --------------
      -- Finalize --
      --------------

      procedure Finalize is
      begin
         Close (Buffer);
      end Finalize;

      C    : constant not null access Read_Cache := Get_Read_Cache (Socket);
      J, K : Stream_Element_Offset;

   begin
      if Wait then
         Flush (Socket);
      end if;

      J := C.First;
      K := Delimiter'First;

      loop
         if J > C.Last then
            if Wait then
               Append (Buffer, C.Buffer (C.First .. C.Last));

               if Size (Buffer) > Stream_Element_Offset (Input_Limit) then
                  raise Data_Overflow with
                    "Size" & Stream_Element_Offset'Image (Size (Buffer));
               end if;

               begin
                  Read (Socket);
               exception
                  when E : Socket_Error =>
                     if Size (Buffer) = 0 or else Is_Timeout (Socket, E)  then
                        raise;
                     else
                        C.First := 1;
                        C.Last  := 0;

                        return Buffered;
                     end if;
               end;

               J := C.First;
            else
               return (1 .. 0 => 0);
            end if;
         end if;

         if C.Buffer (J) = Delimiter (K) then
            if K = Delimiter'Last then
               K := C.First;
               C.First := J + 1;

               return Buffered & C.Buffer (K .. J);

            else
               K := K + 1;
            end if;

            J := J + 1;

         elsif K = Delimiter'First then
            J := J + 1;
         else
            K := Delimiter'First;
         end if;
      end loop;
   end Read_Until;

   function Read_Until
     (Socket    : Socket_Type'Class;
      Delimiter : String;
      Wait      : Boolean := True) return String is
   begin
      return Translator.To_String
        (Read_Until
           (Socket, Translator.To_Stream_Element_Array (Delimiter), Wait));
   end Read_Until;

   ---------------------
   -- Set_Input_Limit --
   ---------------------

   procedure Set_Input_Limit (Limit : Positive) is
   begin
      Input_Limit := Limit;
   end Set_Input_Limit;

   --------------
   -- Shutdown --
   --------------

   procedure Shutdown (Socket : Socket_Type'Class) is
   begin
      begin
         Flush (Socket);
      exception
         when Socket_Error =>
            --  Ignore recent cache buffer send error
            null;
      end;

      Net.Shutdown (Socket);
   end Shutdown;

   -----------
   -- Write --
   -----------

   procedure Write
     (Socket : Socket_Type'Class;
      Item   : Stream_Element_Array)
   is
      C         : constant not null access Write_Cache :=
                    Get_Write_Cache (Socket);
      Next_Last : constant Stream_Element_Offset := C.Last + Item'Length;
   begin
      if Next_Last > C.Max_Size then
         Send (Socket, C.Buffer (1 .. C.Last));
         Send (Socket, Item);
         C.Last := 0;
      else
         C.Buffer (C.Last + 1 .. Next_Last) := Item;
         C.Last := Next_Last;
      end if;
   end Write;

end AWS.Net.Buffered;
