--                                                                    --
--  package                         Copyright (c)  Dmitry A. Kazakov  --
--     Synchronization.Interprocess.               Luebeck            --
--     Generic_FIFO                                Spring, 2018       --
--  Implementation                                                    --
--                                Last revision :  19:18 30 Apr 2018  --
--                                                                    --
--  This  library  is  free software; you can redistribute it and/or  --
--  modify it under the terms of the GNU General Public  License  as  --
--  published by the Free Software Foundation; either version  2  of  --
--  the License, 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. See the GNU  --
--  General  Public  License  for  more  details.  You  should  have  --
--  received  a  copy  of  the GNU General Public License along with  --
--  this library; if not, write to  the  Free  Software  Foundation,  --
--  Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.    --
--                                                                    --
--  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.       --
--____________________________________________________________________--

with Ada.Calendar;       use Ada.Calendar;
with Ada.Exceptions;     use Ada.Exceptions;
with Ada.IO_Exceptions;  use Ada.IO_Exceptions;
with Ada.Tags;           use Ada.Tags;

package body Synchronization.Interprocess.Generic_FIFO is

   In_End_Init : constant String :=  "The in-end of the queue is " &
                                     "not initialized or already " &
                                     "finalized";
   Out_End_Init : constant String := "The out-end of the queue is " &
                                     "not initialized or already " &
                                     "finalized";
   Queue_Init : constant String :=   "The queue is " &
                                     "not initialized or already " &
                                     "finalized";

   procedure Delete_Implementation
             (  Queue   : in out FIFO'Class;
                Count   : Natural  := 1;
                Timeout : Duration := Duration'Last
             )  is
   begin
      if Queue.Data = null then
         Raise_Exception (Status_Error'Identity, Out_End_Init);
      end if;
      if Count > 0 then
         declare
            Free   : Positive renames Queue.Data.Free;
            First  : Positive renames Queue.Data.First;
            Buffer : Element_Array renames Queue.Data.Buffer;
            Length : Integer := Free - First;
         begin
            if Length < 0 then
               Length := Length + Queue.Size;
            end if;
            declare
               Next : constant Positive :=
                               First + Integer'Min (Length, Count);
            begin
               if Next > Queue.Size then
                  First := Next - Queue.Size;
               else
                  First := Next;
               end if;
            end;
            if Length = Queue.Size - 1 then
               Signal (Queue.Not_Full.all); -- Queue was full
            end if;
         end;
      end if;
   end Delete_Implementation;

   procedure Delete
             (  Queue   : in out FIFO_Out;
                Count   : Natural  := 1;
                Timeout : Duration := Duration'Last
             )  is
   begin
      Delete_Implementation (Queue, Count, Timeout);
   end Delete;

   procedure Delete
             (  Queue   : in out Universal_FIFO;
                Count   : Natural  := 1;
                Timeout : Duration := Duration'Last
             )  is
   begin
      if Queue.Mode = Out_End then
         Delete_Implementation (Queue, Count, Timeout);
      else
         Raise_Exception
         (  Mode_Error'Identity,
            "An in-end of the queue is expected in Delete"
         );
      end if;
   end Delete;

   function Get_Implementation
            (  Queue   : FIFO'Class;
               Timeout : Duration := Duration'Last
            )  return Element_Type is
      Started : constant Time := Clock;
   begin
      if Queue.Data = null then
         Raise_Exception (Status_Error'Identity, Out_End_Init);
      end if;
      declare
         Free   : Positive renames Queue.Data.Free;
         First  : Positive renames Queue.Data.First;
         Buffer : Element_Array renames Queue.Data.Buffer;
      begin
         loop
            if First /= Free then -- Queue is not empty
               declare
                  Element : constant Element_Type := Buffer (First);
               begin
                  if First = Queue.Size then
                     First := 1;
                  else
                     First := First + 1;
                  end if;
                  Signal (Queue.Not_Full.all);
                  return Element;
               end;
            end if;
            Reset (Queue.Not_Empty.all); -- Prepare to wait
            if First = Free then         -- Still empty, wait
               Wait
               (  Queue.Not_Empty.all,
                  Timeout - (Clock - Started)
               );
            end if;
         end loop;
      end;
   end Get_Implementation;

   function Get
            (  Queue   : FIFO_Out;
               Timeout : Duration := Duration'Last
            )  return Element_Type is
   begin
      return Get_Implementation (Queue, Timeout);
   end Get;

   function Get
            (  Queue   : Universal_FIFO;
               Timeout : Duration := Duration'Last
            )  return Element_Type is
   begin
      if Queue.Mode = Out_End then
         return Get_Implementation (Queue, Timeout);
      else
         Raise_Exception
         (  Mode_Error'Identity,
            "An out-end of the queue is expected in Get"
         );
      end if;
   end Get;

   function Get_Mode (Queue : Universal_FIFO) return FIFO_End is
   begin
      return Queue.Mode;
   end Get_Mode;

   function Get_Signature (Object : FIFO) return Unsigned_16 is
   begin
      return Get_Signature (External_Tag (FIFO'Tag));
   end Get_Signature;

   function Get_Size (Queue : FIFO) return Storage_Count is
      subtype Data is FIFO_Data (Queue.Size);
   begin
      return Round (Data'Max_Size_In_Storage_Elements);
   end Get_Size;

   function Is_Empty (Queue : FIFO) return Boolean is
   begin
      declare
         Data : FIFO_Data renames Queue.Data.all;
      begin
         return Data.First = Data.Free;
      end;
   exception
      when Constraint_Error =>
         if Queue.Data = null then
            Raise_Exception (Status_Error'Identity, Queue_Init);
         else
            raise;
         end if;
   end Is_Empty;

   function Is_Full (Queue : FIFO) return Boolean is
   begin
      declare
         Data  : FIFO_Data renames Queue.Data.all;
         Count : constant Integer := Data.First - Data.Free;
      begin
         return Count = 1 or else Count = 1 - Data.Size;
      end;
   exception
      when Constraint_Error =>
         if Queue.Data = null then
            Raise_Exception (Status_Error'Identity, Queue_Init);
         else
            raise;
         end if;
   end Is_Full;

   function Is_Preserved (Queue : FIFO; Element : Element_Type)
      return Boolean is
   begin
      return True;
   end Is_Preserved;

   procedure Map
             (  Queue    : in out FIFO;
                Shared   : in out Abstract_Shared_Environment'Class;
                Location : System.Address;
                Size     : Storage_Count;
                Owner    : Boolean
             )  is
      subtype Data is FIFO_Data (Queue.Size);
      package Mapper is new Generic_Memory_Mapper (Data);
      This : Abstract_Shared_Object_Ptr := Shared.First;
   begin
      while This /= null and then This /= Queue'Unchecked_Access loop
         if This.all in Event'Class then
            Queue.Not_Full  := Queue.Not_Empty;
            Queue.Not_Empty := Event'Class (This.all)'Unchecked_Access;
         end if;
         This := This.Next;
      end loop;
      if Queue.Not_Full = null then
         Raise_Exception
         (  Mode_Error'Identity,
            (  "The environment contains no not-full event "
            &  "record member appearing before the FIFO object"
         )  );
      end if;
      Queue.Data := Mapper.Map (Location, Owner).all'Unchecked_Access;
   end Map;

   procedure Map
             (  Queue    : in out FIFO_In;
                Shared   : in out Abstract_Shared_Environment'Class;
                Location : System.Address;
                Size     : Storage_Count;
                Owner    : Boolean
             )  is
   begin
      Map (FIFO (Queue), Shared, Location, Size, Owner);
      if not Compare_And_Swap
             (  Target  => Queue.Data.Has_In'Access,
                Old_Val => 0,
                New_Val => 1
             )
      then
         Raise_Exception
         (  Mode_Error'Identity,
            "The queue already has the in-end"
         );
      end if;
   end Map;

   procedure Map
             (  Queue    : in out FIFO_Multiplexed_In;
                Shared   : in out Abstract_Shared_Environment'Class;
                Location : System.Address;
                Size     : Storage_Count;
                Owner    : Boolean
             )  is
      This : Abstract_Shared_Object_Ptr := Shared.First;
   begin
      Map (FIFO (Queue), Shared, Location, Size, Owner);
      while This /= null and then This /= Queue'Unchecked_Access loop
         if This.all in Mutex'Class then
            Queue.Lock := Mutex'Class (This.all)'Unchecked_Access;
         end if;
         This := This.Next;
      end loop;
      if Queue.Lock = null then
         Raise_Exception
         (  Data_Error'Identity,
            (  "The environment contains no mutex "
            &  "record member appearing before the FIFO object"
         )  );
      end if;
      if (  not Compare_And_Swap
                (  Target  => Queue.Data.Has_In'Access,
                   Old_Val => 0,
                   New_Val => 2
                )
         and then
            not Compare_And_Swap
                (  Target  => Queue.Data.Has_In'Access,
                   Old_Val => 2,
                   New_Val => 2
         )      )
      then
         Raise_Exception
         (  Mode_Error'Identity,
            "The queue already has an non-multiplexed in-end"
         );
      end if;
   end Map;

   procedure Map
             (  Queue    : in out FIFO_Out;
                Shared   : in out Abstract_Shared_Environment'Class;
                Location : System.Address;
                Size     : Storage_Count;
                Owner    : Boolean
             )  is
   begin
      Map (FIFO (Queue), Shared, Location, Size, Owner);
      if not Compare_And_Swap
             (  Target  => Queue.Data.Has_Out'Access,
                Old_Val => 0,
                New_Val => 1
             )
      then
         Raise_Exception
         (  Mode_Error'Identity,
            "The queue already has the out-end"
         );
      end if;
   end Map;

   procedure Map
             (  Queue    : in out Universal_FIFO;
                Shared   : in out Abstract_Shared_Environment'Class;
                Location : System.Address;
                Size     : Storage_Count;
                Owner    : Boolean
             )  is
   begin
      Map (FIFO (Queue), Shared, Location, Size, Owner);
      case Queue.Mode is
         when In_End =>
            if not Compare_And_Swap
                   (  Target  => Queue.Data.Has_In'Access,
                      Old_Val => 0,
                      New_Val => 1
                   )
            then
               Raise_Exception
               (  Mode_Error'Identity,
                  "The queue already has an in-end"
               );
            end if;
         when Multiplexed_In_End =>
            declare
               This : Abstract_Shared_Object_Ptr := Shared.First;
            begin
               while (  This /= null
                     and then
                        This /= Queue'Unchecked_Access
                     )  loop
                  if This.all in Mutex'Class then
                     Queue.Lock :=
                        Mutex'Class (This.all)'Unchecked_Access;
                  end if;
                  This := This.Next;
               end loop;
               if Queue.Lock = null then
                  Raise_Exception
                  (  Data_Error'Identity,
                     (  "The environment contains no mutex "
                     &  "record member appearing before the FIFO object"
                  )  );
               end if;
               if (  not Compare_And_Swap
                         (  Target  => Queue.Data.Has_In'Access,
                            Old_Val => 0,
                            New_Val => 2
                         )
                  and then
                     not Compare_And_Swap
                         (  Target  => Queue.Data.Has_In'Access,
                            Old_Val => 2,
                            New_Val => 2
                  )      )
               then
                  Raise_Exception
                  (  Mode_Error'Identity,
                     "The queue already has a non-multiplexed in-end"
                  );
               end if;
            end;
         when Out_End =>
            if not Compare_And_Swap
                   (  Target  => Queue.Data.Has_Out'Access,
                      Old_Val => 0,
                      New_Val => 1
                   )
            then
               Raise_Exception
               (  Mode_Error'Identity,
                  "The queue already has an out-end"
              );
            end if;
      end case;
   end Map;

   procedure Peek_Implementation
             (  Queue   : FIFO'Class;
                Element : out Element_Type;
                Empty   : out Boolean;
                Timeout : Duration := Duration'Last
             )  is
   begin
      if Queue.Data = null then
         Raise_Exception (Status_Error'Identity, Out_End_Init);
      end if;
      declare
         Data : FIFO_Data renames Queue.Data.all;
      begin
         Empty := Data.First = Data.Free;
         if not Empty then
            Element := Data.Buffer (Data.First);
         end if;
      end;
   end Peek_Implementation;

   procedure Peek
             (  Queue   : FIFO_Out;
                Element : out Element_Type;
                Empty   : out Boolean;
                Timeout : Duration := Duration'Last
             )  is
   begin
      Peek_Implementation (Queue, Element, Empty, Timeout);
   end Peek;

   procedure Peek
             (  Queue   : Universal_FIFO;
                Element : out Element_Type;
                Empty   : out Boolean;
                Timeout : Duration := Duration'Last
             )  is
   begin
      if Queue.Mode = Out_End then
         Peek_Implementation (Queue, Element, Empty, Timeout);
      else
         Raise_Exception
         (  Mode_Error'Identity,
            "An out-end of the queue is expected in Peek"
         );
      end if;
   end Peek;

   procedure Purge_Implementation
             (  Queue        : in out FIFO'Class;
                Is_Preserved : Is_Preserved_Ptr;
                Purged       : out Natural;
                Timeout      : Duration := Duration'Last
             )  is
   begin
      if Queue.Data = null then
         Raise_Exception (Status_Error'Identity, Out_End_Init);
      end if;
      declare
         Free   : Positive renames Queue.Data.Free;
         First  : Positive renames Queue.Data.First;
         Buffer : Element_Array renames Queue.Data.Buffer;
         From   : Positive := Free;
         To     : Positive := From;
      begin
         Purged := 0;
         while From /= First loop
            if From = 1 then
               From := Queue.Size;
            else
               From := From - 1;
            end if;
            if Is_Preserved (Buffer (From)) then
               -- Preserve this element
               if To = 1 then
                  To := Queue.Size;
               else
                  To := To - 1;
               end if;
               if To /= From then
                  Buffer (To) := Buffer (From);
               end if;
            else
               -- Remove this one
               Purged := Purged + 1;
            end if;
         end loop;
         if Purged > 0 then
            First := To;
            Signal (Queue.Not_Full.all);
         end if;
      end;
   end Purge_Implementation;

   procedure Purge
             (  Queue        : in out FIFO_Out;
                Is_Preserved : Is_Preserved_Ptr;
                Purged       : out Natural;
                Timeout      : Duration := Duration'Last
             )  is
   begin
      Purge_Implementation (Queue, Is_Preserved, Purged, Timeout);
   end Purge;

   procedure Purge
             (  Queue        : in out Universal_FIFO;
                Is_Preserved : Is_Preserved_Ptr;
                Purged       : out Natural;
                Timeout      : Duration := Duration'Last
             )  is
   begin
      if Queue.Mode = Out_End then
         Purge_Implementation (Queue, Is_Preserved, Purged, Timeout);
      else
         Raise_Exception
         (  Mode_Error'Identity,
            "An out-end of the queue is expected in Purge"
         );
      end if;
   end Purge;

   procedure Purge_Implementation
             (  Queue   : in out FIFO'Class;
                Purged  : out Natural;
                Timeout : Duration := Duration'Last
             )  is
   begin
      if Queue.Data = null then
         Raise_Exception (Status_Error'Identity, Out_End_Init);
      end if;
      declare
         Free   : Positive renames Queue.Data.Free;
         First  : Positive renames Queue.Data.First;
         Buffer : Element_Array renames Queue.Data.Buffer;
         From   : Positive := Free;
         To     : Positive := From;
      begin
         Purged := 0;
         while From /= First loop
            if From = 1 then
               From := Queue.Size;
            else
               From := From - 1;
            end if;
            if Is_Preserved
               (  Queue   => FIFO_Out'Class (Queue),
                  Element => Buffer (From)
               )
            then
               -- Preserve this element
               if To = 1 then
                  To := Queue.Size;
               else
                  To := To - 1;
               end if;
               if To /= From then
                  Buffer (To) := Buffer (From);
               end if;
            else
               -- Remove this one
               Purged := Purged + 1;
            end if;
         end loop;
         if Purged > 0 then
            First := To;
            Signal (Queue.Not_Full.all);
         end if;
      end;
   end Purge_Implementation;

   procedure Purge
             (  Queue   : in out FIFO_Out;
                Purged  : out Natural;
                Timeout : Duration := Duration'Last
             )  is
   begin
      Purge_Implementation (Queue, Purged, Timeout);
   end Purge;

   procedure Purge
             (  Queue   : in out Universal_FIFO;
                Purged  : out Natural;
                Timeout : Duration := Duration'Last
             )  is
   begin
      if Queue.Mode = Out_End then
         Purge_Implementation (Queue, Purged, Timeout);
      else
         Raise_Exception
         (  Mode_Error'Identity,
            "An out-end of the queue is expected in Purge"
         );
      end if;
   end Purge;

   procedure Put_Lock_Free
             (  Queue   : in out FIFO'Class;
                Element : Element_Type;
                Timeout : Duration := Duration'Last
             )  is
      Started : constant Time := Clock;
   begin
      if Queue.Data = null then
         Raise_Exception (Status_Error'Identity, In_End_Init);
      end if;
      declare
         Free   : Positive renames Queue.Data.Free;
         First  : Positive renames Queue.Data.First;
         Buffer : Element_Array renames Queue.Data.Buffer;
      begin
         loop
            declare
               Count : Integer := First - Free;
            begin
               if Count /= 1 and then Count /= 1 - Queue.Size then
                  Buffer (Free) := Element;
                  if Free = Queue.Size then
                     Free := 1;
                  else
                     Free := Free + 1;
                  end if;
                  Signal (Queue.Not_Empty.all);
                  return;
               end if;
               Reset (Queue.Not_Full.all); -- Prepare to wait
               Count := First - Free;
               if Count = 1 or else Count = 1 - Queue.Size then
                  Wait
                  (  Queue.Not_Full.all,
                     Timeout - (Clock - Started)
                  );
               end if;
            end;
         end loop;
      end;
   end Put_Lock_Free;

   procedure Put
             (  Queue   : in out FIFO_In;
                Element : Element_Type;
                Timeout : Duration := Duration'Last
             )  is
   begin
      Put_Lock_Free (Queue, Element, Timeout);
   end Put;

   procedure Put_Interlocked
             (  Queue   : in out FIFO'Class;
                Element : Element_Type;
                Lock    : in out Mutex'Class;
                Timeout : Duration := Duration'Last
             )  is
      Started : constant Time := Clock;
   begin
      if Queue.Data = null then
         Raise_Exception (Status_Error'Identity, In_End_Init);
      end if;
      declare
         Free   : Positive renames Queue.Data.Free;
         First  : Positive renames Queue.Data.First;
         Buffer : Element_Array renames Queue.Data.Buffer;
      begin
         Seize (Lock, Timeout);
         loop
            declare
               Count : Integer := First - Free;
            begin
               if Count /= 1 and then Count /= 1 - Queue.Size then
                  Buffer (Free) := Element;
                  if Free = Queue.Size then
                     Free := 1;
                  else
                     Free := Free + 1;
                  end if;
                  Release (Lock);
                  Signal (Queue.Not_Empty.all);
                  return;
               end if;
               Reset (Queue.Not_Full.all); -- Prepare to wait
               Count := First - Free;
               if Count = 1 or else Count = 1 - Queue.Size then
                  Wait
                  (  Queue.Not_Full.all,
                     Timeout - (Clock - Started)
                  );
               end if;
            end;
         end loop;
      exception
         when others =>
            Release (Lock);
            raise;
      end;
   end Put_Interlocked;

   procedure Put
             (  Queue   : in out FIFO_Multiplexed_In;
                Element : Element_Type;
                Timeout : Duration := Duration'Last
             )  is
   begin
      Put_Interlocked (Queue, Element, Queue.Lock.all, Timeout);
   end Put;

   procedure Put
             (  Queue   : in out Universal_FIFO;
                Element : Element_Type;
                Timeout : Duration := Duration'Last
             )  is
   begin
      case Queue.Mode is
         when In_End =>
            Put_Lock_Free (Queue, Element, Timeout);
         when Multiplexed_In_End =>
            Put_Interlocked (Queue, Element, Queue.Lock.all, Timeout);
         when Out_End =>
            Raise_Exception
            (  Mode_Error'Identity,
               "An in-end of the queue is expected in Put"
            );
      end case;
   end Put;

   procedure Set_Mode
             (  Queue : in out Universal_FIFO;
                Mode  : FIFO_End
             )  is
   begin
      if Queue.Data /= null then
         Raise_Exception
         (  Status_Error'Identity,
            "The queue is already initialized"
         );
      end if;
      Queue.Mode := Mode;
   end Set_Mode;

   procedure Wait_For_Not_Empty
             (  Queue   : in out FIFO;
                Timeout : Duration
             )  is
   begin
      if Is_Empty (Queue) then
         Reset (Queue.Not_Empty.all);
         if Is_Empty (Queue) then -- Still empty, enter wait
            Wait (Queue.Not_Empty.all, Timeout);
         end if;
      end if;
   end Wait_For_Not_Empty;

   procedure Wait_For_Not_Empty
             (  Queue   : in out FIFO;
                Timeout : Duration;
                Empty   : out Boolean
             )  is
      Signaled : Boolean;
   begin
      if Is_Empty (Queue) then
         Reset (Queue.Not_Empty.all);
         if Is_Empty (Queue) then -- Still empty, enter wait
            Wait (Queue.Not_Empty.all, Timeout, Signaled);
         end if;
         Empty := Is_Empty (Queue);
      else
         Empty := False;
      end if;
   end Wait_For_Not_Empty;

   procedure Wait_For_Not_Full
             (  Queue   : in out FIFO;
                Timeout : Duration
             )  is
   begin
      if Is_Full (Queue) then
         Reset (Queue.Not_Full.all);
         if Is_Full (Queue) then -- Still full, enter wait
            Wait (Queue.Not_Full.all, Timeout);
         end if;
      end if;
   end Wait_For_Not_Full;

   procedure Wait_For_Not_Full
             (  Queue   : in out FIFO;
                Timeout : Duration;
                Full    : out Boolean
             )  is
      Signaled : Boolean;
   begin
      if Is_Full (Queue) then
         Reset (Queue.Not_Full.all);
         if Is_Full (Queue) then -- Still full, enter wait
            Wait (Queue.Not_Full.all, Timeout, Signaled);
         end if;
         Full := Is_Full (Queue);
      else
         Full := False;
      end if;
   end Wait_For_Not_Full;

end Synchronization.Interprocess.Generic_FIFO;
