-- Usable_Datagram_Package - Provide a UDP interface that should satisfy most any underlying system.
-- Copyright (C) 2022,2023,2024 Prince Trippy <programmer@verisimilitudes.net>.

-- This program is free software: you can redistribute it and/or modify it under the terms of the
-- GNU Affero General Public License version 3 as published by the Free Software Foundation.

-- This program 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 Affero General Public License for more details.

-- You should have received a copy of the GNU Affero General Public License along with this program.
-- If not, see <http://www.gnu.org/licenses/>.

with System, Unchecked_Conversion, POSIX_UDP_Garbage; use POSIX_UDP_Garbage;

package body Usable_Datagram_Package is
   function Uint32_To_Address is new Unchecked_Conversion(Source => Uint32_T, Target =>  Address);
   function Address_To_Uint32 is new Unchecked_Conversion(Source =>  Address, Target => Uint32_T);

   function To_Uint32_T (From : in Address) return Uint32_T is
      use System;
      A : Address := From;
      for A'Alignment use Uint32_T'Alignment; -- This is probably overly-cautious but makes no harm.
   begin
      case Default_Bit_Order is
         when High_Order_First => null;
         when  Low_Order_First => A := (A(4), A(3), A(2), A(1));
      end case;
      return Address_To_Uint32(A);
   end To_Uint32_T;

   function From_Uint32_T (From : in Uint32_T) return Address is
      use System;
      A : Address;
      U : Uint32_T := From;
      for A'Alignment use Uint32_T'Alignment;
   begin
      A := Uint32_To_Address(U);
      case Default_Bit_Order is
         when High_Order_First => null;
         when  Low_Order_First => A := (A(4), A(3), A(2), A(1));
      end case;
      return A;
   end From_Uint32_T;

   procedure Initialize (Object : in out System_Resource) is
      S : Sockaddr := Make_Sockaddr(Address => 0, Port => Uint16_T(Object.Where));
   begin
      Socket(Object.Socket_Nonsense);
      Bind(Socket => Object.Socket_Nonsense, Socket_Address => S);
   exception -- Here's where I'd raise these with different messages, if Ada 2005 hadn't added that.
      when Socket_Error => raise Sys_Error;
      when   Bind_Error => Close(Object.Socket_Nonsense); raise Sys_Error; -- This close was subtle.
   end Initialize;

   procedure Finalize (Object : in out System_Resource) is
   begin
      Close(Object.Socket_Nonsense);
   end Finalize;

   procedure Hit (By      : in System_Resource;
                  Data    : in Octet_Array;
                  To      : in Address;
                  At_Port : in Port) is
      S : Sockaddr := Make_Sockaddr(Address => To_Uint32_T(To), Port => Uint16_T(At_Port));
   begin
      Sendto(Socket => By.Socket_Nonsense, Socket_Address => S,
             Buffer => POSIX_UDP_Garbage.Octet_Array(Data));
   exception
      when Sendto_Error => raise Hit_Error;
   end Hit;

   procedure Get (By        :  in System_Resource;
                  Data      : out Octet_Array;
                  From      : out Address;
                  From_Port : out Port;
                  Fitting   :  in Fit := Equals) is
      S : Sockaddr;
      I : Interfaces.C.Size_T;
      B : array (Fit) of Boolean := (Equals => False, Larger => True,
                                     Lesser => False, Ignore => True);
   begin
      Recvfrom(Socket => By.Socket_Nonsense, Buffer => POSIX_UDP_Garbage.Octet_Array(Data),
               Length => I, Socket_Address => S, Truncate_Length => B(Fitting)); -- Quite befitting.
      From := From_Uint32_T(Sockaddr_Address(S));
      From_Port := Port(Sockaddr_Port(S));
      case Fitting is
         when Equals => if Integer(I) /= Data'Length then raise Fit_Error; end if;
         when Larger => if Integer(I) <  Data'Length then raise Fit_Error; end if;
         when Lesser => if Integer(I) >  Data'Length then raise Fit_Error; end if;
         when Ignore => null;
      end case;
   exception
      when Recvfrom_Error => raise Get_Error;
   end Get;

   procedure Get (By        :  in System_Resource;
                  Data      : out Octet_Array;
                  From      : out Address;
                  From_Port : out Port;
                  Length    : out Data_Length) is
      S : Sockaddr;
      I : Interfaces.C.Size_T;
   begin
      Recvfrom(Socket => By.Socket_Nonsense, Buffer => POSIX_UDP_Garbage.Octet_Array(Data),
               Length => I, Socket_Address => S, Truncate_Length => False);
      From := From_Uint32_T(Sockaddr_Address(S));
      From_Port := Port(Sockaddr_Port(S));
      Length := Data_Length(I);
   exception
      when Recvfrom_Error => raise Get_Error;
   end Get;

   procedure Get (By        :  in System_Resource;
                  Data      : out Octet_Array;
                  From      : out Address;
                  From_Port : out Port;
                  Padding   :  in Interfaces.Unsigned_8) is
      S : Sockaddr;
      I : Interfaces.C.Size_T;
   begin
      Recvfrom(Socket => By.Socket_Nonsense, Buffer => POSIX_UDP_Garbage.Octet_Array(Data),
               Length => I, Socket_Address => S);
      From := From_Uint32_T(Sockaddr_Address(S));
      From_Port := Port(Sockaddr_Port(S));
      Data(Data'First + Data_Length(I) .. Data'Last) := (others => Padding);
   exception
      when Recvfrom_Error => raise Get_Error;
   end Get;

   -- These procedures are mostly like their counterparts, except for the different parameter lists.
   -- All special and required checks about the length of a String are handled in POSIX_UDP_Garbage.
   procedure Hit (By      : in System_Resource;
                  Data    : in String;
                  To      : in Address;
                  At_Port : in Port) is
      S : Sockaddr := Make_Sockaddr(Address => To_Uint32_T(To), Port => Uint16_T(At_Port));
   begin
      Sendto(Socket => By.Socket_Nonsense, Socket_Address => S, Buffer => Data);
   exception
      when Sendto_Error => raise Hit_Error;
   end Hit;

   procedure Get (By        :  in System_Resource;
                  Data      : out String;
                  From      : out Address;
                  From_Port : out Port;
                  Fitting   :  in Fit := Equals) is
      S : Sockaddr;
      I : Interfaces.C.Size_T;
      B : array (Fit) of Boolean := (Equals => False, Larger => True,
                                     Lesser => False, Ignore => True);
   begin
      Recvfrom(Socket => By.Socket_Nonsense, Buffer => Data,
               Length => I, Socket_Address => S, Truncate_Length => B(Fitting));
      From := From_Uint32_T(Sockaddr_Address(S));
      From_Port := Port(Sockaddr_Port(S));
      case Fitting is
         when Equals => if Integer(I) /= Data'Length then raise Fit_Error; end if;
         when Larger => if Integer(I) <  Data'Length then raise Fit_Error; end if;
         when Lesser => if Integer(I) >  Data'Length then raise Fit_Error; end if;
         when Ignore => null;
      end case;
   exception
      when Recvfrom_Error => raise Get_Error;
   end Get;

   procedure Get (By        :  in System_Resource;
                  Data      : out String;
                  From      : out Address;
                  From_Port : out Port;
                  Length    : out Data_Length) is
      S : Sockaddr;
      I : Interfaces.C.Size_T;
   begin
      Recvfrom(Socket => By.Socket_Nonsense, Buffer => Data,
               Length => I, Socket_Address => S, Truncate_Length => False);
      From := From_Uint32_T(Sockaddr_Address(S));
      From_Port := Port(Sockaddr_Port(S));
      Length := Data_Length(I);
   exception
      when Recvfrom_Error => raise Get_Error;
   end Get;

   procedure Get (By        :  in System_Resource;
                  Data      : out String;
                  From      : out Address;
                  From_Port : out Port;
                  Padding   :  in Character) is
      S : Sockaddr;
      I : Interfaces.C.Size_T;
   begin
      Recvfrom(Socket => By.Socket_Nonsense, Buffer => Data, Length => I, Socket_Address => S);
      From := From_Uint32_T(Sockaddr_Address(S));
      From_Port := Port(Sockaddr_Port(S));
      Data(Data'First + Data_Length(I) .. Data'Last) := (others => Padding);
   exception
      when Recvfrom_Error => raise Get_Error;
   end Get;
end Usable_Datagram_Package;