--                                                                    --
--  package                         Copyright (c)  Dmitry A. Kazakov  --
--     GNAT.Sockets.Connection_State_Machine.      Luebeck            --
--     MODBUS_Client.Synchronous                   Spring, 2015       --
--  Implementation                                                    --
--                                Last revision :  09:02 17 Oct 2020  --
--                                                                    --
--  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.Exceptions;     use Ada.Exceptions;
with Ada.IO_Exceptions;  use Ada.IO_Exceptions;
with Ada.Real_Time;      use Ada.Real_Time;
with Synchronization;    use Synchronization;

package body GNAT.Sockets.Connection_State_Machine.MODBUS_Client.
             Synchronous is

   procedure Bits_Read
             (  Client    : in out MODBUS_Synchronous_Client;
                Reference : Reference_ID;
                Values    : Bit_Array;
                Code      : Function_Code;
                Unit      : Unit_No
             )  is
   begin
      if Client.Result.Mode = Bits then
         declare
            Result : Bits_Result renames Client.Result.Bits.all;
         begin
            Result.Length := Values'Length;
            Result.Data (1..Values'Length) := Values;
         end;
      else
         Client.Result := (Mode => None);
      end if;
      Client.Event.Complete;
   end Bits_Read;

   procedure Bits_Written
             (  Client    : in out MODBUS_Synchronous_Client;
                Reference : Reference_ID;
                From      : Bit_Address;
                To        : Bit_Address;
                Code      : Function_Code;
                Unit      : Unit_No
             )  is
   begin
      Client.Result := (Mode => None);
      Client.Event.Complete;
   end Bits_Written;

   procedure Cancel (Client : in out MODBUS_Synchronous_Client) is
   begin
      Client.Event.Cancel;
   end Cancel;

   procedure Connect
             (  Client         : in out MODBUS_Synchronous_Client;
                Host           : String;
                Port           : Port_Type := MODBUS_Port;
                Max_Connect_No : Positive  := Positive'Last;
                Timeout        : Duration  := Duration'Last
             )  is
      Lock     : Holder (Client'Unchecked_Access);
      Deadline : constant Time := abs Timeout;
   begin
      Shutdown (Client);
      select
         Client.Event.Wait_For_Release;
      or delay until Deadline;
         Raise_Exception
         (  Timeout_Error'Identity,
            "Shutdown timeout expired"
         );
      end select;
      Connect
      (  Client.Listener.all,
         Client'Unchecked_Access,
         Host,
         Port,
         Max_Connect_No
      );
      select
         Client.Event.Wait_For_Connection;
      or delay until Deadline;
         Raise_Exception
         (  Timeout_Error'Identity,
            "Connection timeout expired"
         );
      end select;
   end Connect;

   procedure Connected (Client : in out MODBUS_Synchronous_Client) is
   begin
       Connected (MODBUS_Client (Client));
       Client.Event.Set;
   end Connected;

   procedure Exception_Status_Received
             (  Client    : in out MODBUS_Synchronous_Client;
                Reference : Reference_ID;
                Status    : Unsigned_8;
                Unit      : Unit_No
             )  is
   begin
      Client.Result := (Exception_Status, Status);
      Client.Event.Complete;
   end Exception_Status_Received;

   procedure Failed
             (  Client    : in out MODBUS_Synchronous_Client;
                Reference : Reference_ID;
                Error     : Exception_Code;
                Code      : Function_Code;
                Unit      : Unit_No
             )  is
   begin
      Client.Result := (Failure, Error);
      Client.Event.Complete;
   end Failed;

   function FC1
            (  Client    : access MODBUS_Synchronous_Client;
               Reference : Reference_ID;
               From      : Bit_Address;
               To        : Bit_Address;
               Unit      : Unit_No  := 255;
               Timeout   : Duration := Duration'Last
            )  return Bit_Array is
      Deadline : constant Time := abs Timeout;
      Lock     : Holder (Client);
      Result   : aliased Bits_Result;
   begin
      Client.Result := (Bits, Result'Unchecked_Access);
      Wait_Until_RTU_Silence_Time (Client.all, Deadline);
      Send_FC1 (Client.all, Reference, From, To, Unit);
      Wait (Client.all, Bits, Deadline);
      return Result.Data (1..Result.Length);
   end FC1;

   function FC2
            (  Client    : access MODBUS_Synchronous_Client;
               Reference : Reference_ID;
               From      : Bit_Address;
               To        : Bit_Address;
               Unit      : Unit_No  := 255;
               Timeout   : Duration := Duration'Last
            )  return Bit_Array is
      Deadline : constant Time := abs Timeout;
      Lock     : Holder (Client);
      Result   : aliased Bits_Result;
   begin
      Client.Result := (Bits, Result'Unchecked_Access);
      Wait_Until_RTU_Silence_Time (Client.all, Deadline);
      Send_FC2 (Client.all, Reference, From, To, Unit);
      Wait (Client.all, Bits, Deadline);
      return Result.Data (1..Result.Length);
   end FC2;

   function FC3
            (  Client    : access MODBUS_Synchronous_Client;
               Reference : Reference_ID;
               From      : Word_Address;
               To        : Word_Address;
               Unit      : Unit_No  := 255;
               Timeout   : Duration := Duration'Last
            )  return Word_Array is
      Deadline : constant Time := abs Timeout;
      Lock     : Holder (Client);
      Result   : aliased Words_Result;
   begin
      Client.Result := (Words, Result'Unchecked_Access);
      Wait_Until_RTU_Silence_Time (Client.all, Deadline);
      Send_FC3 (Client.all, Reference, From, To, Unit);
      Wait (Client.all, Words, Deadline);
      return Result.Data (1..Result.Length);
   end FC3;

   function FC4
            (  Client    : access MODBUS_Synchronous_Client;
               Reference : Reference_ID;
               From      : Word_Address;
               To        : Word_Address;
               Unit      : Unit_No  := 255;
               Timeout   : Duration := Duration'Last
            )  return Word_Array is
      Deadline : constant Time := abs Timeout;
      Lock     : Holder (Client);
      Result   : aliased Words_Result;
   begin
      Client.Result := (Words, Result'Unchecked_Access);
      Wait_Until_RTU_Silence_Time (Client.all, Deadline);
      Send_FC4 (Client.all, Reference, From, To, Unit);
      Wait (Client.all, Words, Deadline);
      return Result.Data (1..Result.Length);
   end FC4;

   procedure FC5
             (  Client    : in out MODBUS_Synchronous_Client;
                Reference : Reference_ID;
                Address   : Bit_Address;
                Value     : Boolean;
                Unit      : Unit_No  := 255;
                Timeout   : Duration := Duration'Last
             )  is
      Deadline : constant Time := abs Timeout;
      Lock     : Holder (Client'Unchecked_Access);
   begin
      Wait_Until_RTU_Silence_Time (Client, Deadline);
      Send_FC5 (Client, Reference, Address, Value, Unit);
      Wait (Client, None, Deadline);
   end FC5;

   procedure FC6
             (  Client    : in out MODBUS_Synchronous_Client;
                Reference : Reference_ID;
                Address   : Word_Address;
                Value     : Unsigned_16;
                Unit      : Unit_No  := 255;
                Timeout   : Duration := Duration'Last
             )  is
      Deadline : constant Time := abs Timeout;
      Lock     : Holder (Client'Unchecked_Access);
   begin
      Wait_Until_RTU_Silence_Time (Client, Deadline);
      Send_FC6 (Client, Reference, Address, Value, Unit);
      Wait (Client, None, Deadline);
   end FC6;

   function FC7
            (  Client    : access MODBUS_Synchronous_Client;
               Reference : Reference_ID;
               Unit      : Unit_No  := 255;
               Timeout   : Duration := Duration'Last
            )  return Unsigned_8 is
      Deadline : constant Time := abs Timeout;
      Lock     : Holder (Client);
   begin
      Wait_Until_RTU_Silence_Time (Client.all, Deadline);
      Send_FC7 (Client.all, Reference, Unit);
      Wait (Client.all, Exception_Status, Deadline);
      return Client.Result.Status;
   end FC7;

   procedure FC15
             (  Client    : in out MODBUS_Synchronous_Client;
                Reference : Reference_ID;
                Values    : Bit_Array;
                Unit      : Unit_No  := 255;
                Timeout   : Duration := Duration'Last
             )  is
      Deadline : constant Time := abs Timeout;
      Lock     : Holder (Client'Unchecked_Access);
   begin
      Wait_Until_RTU_Silence_Time (Client, Deadline);
      Send_FC15 (Client, Reference, Values, Unit);
      Wait (Client, None, Deadline);
   end FC15;

   procedure FC16
             (  Client    : in out MODBUS_Synchronous_Client;
                Reference : Reference_ID;
                Values    : Word_Array;
                Unit      : Unit_No  := 255;
                Timeout   : Duration := Duration'Last
             )  is
      Deadline : constant Time := abs Timeout;
      Lock     : Holder (Client'Unchecked_Access);
   begin
      Wait_Until_RTU_Silence_Time (Client, Deadline);
      Send_FC16 (Client, Reference, Values, Unit);
      Wait (Client, None, Deadline);
   end FC16;

   procedure FC22
             (  Client    : in out MODBUS_Synchronous_Client;
                Reference : Reference_ID;
                Address   : Word_Address;
                And_Mask  : Unsigned_16;
                Or_Mask   : Unsigned_16;
                Unit      : Unit_No  := 255;
                Timeout   : Duration := Duration'Last
             )  is
      Deadline : constant Time := abs Timeout;
      Lock     : Holder (Client'Unchecked_Access);
   begin
      Wait_Until_RTU_Silence_Time (Client, Deadline);
      Send_FC22 (Client, Reference, Address, And_Mask, Or_Mask, Unit);
      Wait (Client, None, Deadline);
   end FC22;

   function FC23
            (  Client    : access MODBUS_Synchronous_Client;
               Reference : Reference_ID;
               From      : Word_Address;
               To        : Word_Address;
               Values    : Word_Array;
               Unit      : Unit_No  := 255;
               Timeout   : Duration := Duration'Last
            )  return Word_Array is
      Deadline : constant Time := abs Timeout;
      Lock     : Holder (Client);
      Result   : aliased Words_Result;
   begin
      Client.Result := (Words, Result'Unchecked_Access);
      Wait_Until_RTU_Silence_Time (Client.all, Deadline);
      Send_FC23 (Client.all, Reference, From, To, Values, Unit);
      Wait (Client.all, Words, Deadline);
      return Result.Data (1..Result.Length);
   end FC23;

   function FC24
            (  Client    : access MODBUS_Synchronous_Client;
               Reference : Reference_ID;
               Address   : Word_Address;
               Unit      : Unit_No  := 255;
               Timeout   : Duration := Duration'Last
            )  return Word_Array is
      Deadline : constant Time := abs Timeout;
      Lock     : Holder (Client);
      Result   : aliased Words_Result;
   begin
      Client.Result := (Words, Result'Unchecked_Access);
      Wait_Until_RTU_Silence_Time (Client.all, Deadline);
      Send_FC24 (Client.all, Reference, Address, Unit);
      Wait (Client.all, Words, Deadline);
      return Result.Data (1..Result.Length);
   end FC24;

   procedure Finalize (Object : in out Holder) is
   begin
      Object.Client.Event.Release;
   end Finalize;

   function Get_RTU_Checksum_Mode
            (  Client : MODBUS_Synchronous_Client
            )  return Boolean is
   begin
      return Get_RTU_Checksum_Mode (MODBUS_Client (Client));
   end Get_RTU_Checksum_Mode;

   function Get_RTU_Silence_Time
            (  Client : MODBUS_Synchronous_Client
            )  return Duration is
   begin
      return Get_RTU_Silence_Time (MODBUS_Client (Client));
   end Get_RTU_Silence_Time;

   procedure Initialize (Object : in out Holder) is
   begin
      Object.Client.Event.Seize;
   end Initialize;

   procedure Released (Client : in out MODBUS_Synchronous_Client) is
   begin
      Released (MODBUS_Client (Client));
      Client.Event.Set;
   end Released;

   procedure Set_RTU_Checksum_Mode
             (  Client : in out MODBUS_Synchronous_Client;
                Enable : Boolean
             )  is
   begin
      Set_RTU_Checksum_Mode (MODBUS_Client (Client), Enable);
   end Set_RTU_Checksum_Mode;

   procedure Set_RTU_Silence_Time
             (  Client  : in out MODBUS_Synchronous_Client;
                Silence : Duration
             )  is
   begin
      Set_RTU_Silence_Time (MODBUS_Client (Client), Silence);
   end Set_RTU_Silence_Time;

   procedure Wait
             (  Client   : in out MODBUS_Synchronous_Client;
                Expected : Result_Mode;
                Deadline : Time
             )  is
   begin
      select
         Client.Event.Wait_For_Completion;
      or delay until Deadline;
         Raise_Exception (Timeout_Error'Identity, Expired_Text);
      end select;
      if Client.Result.Mode /= Expected then
         if Client.Result.Mode = Failure then
            Raise_Exception
            (  MODBUS_Error'Identity,
               Error_Text (Client.Result.Error)
            );
         else
            Raise_Exception
            (  Program_Error'Identity,
               "Unexpected response result"
            );
         end if;
      end if;
   end Wait;

   procedure Words_Read
             (  Client    : in out MODBUS_Synchronous_Client;
                Reference : Reference_ID;
                Values    : Word_Array;
                Code      : Function_Code;
                Unit      : Unit_No
             )  is
   begin
      if Client.Result.Mode = Words then
         declare
            Result : Words_Result renames Client.Result.Words.all;
         begin
            Result.Length := Values'Length;
            Result.Data (1..Values'Length) := Values;
         end;
      else
         Client.Result := (Mode => None);
      end if;
      Client.Event.Complete;
   end Words_Read;

   procedure Write
             (  Stream : access Root_Stream_Type'Class;
                Object : Event_Type
             )  is
   begin
      null;
   end Write;

   procedure Words_Written
             (  Client    : in out MODBUS_Synchronous_Client;
                Reference : Reference_ID;
                From      : Word_Address;
                To        : Word_Address;
                Code      : Function_Code;
                Unit      : Unit_No
             )  is
   begin
      Client.Result := (Mode => None);
      Client.Event.Complete;
   end Words_Written;

   protected body Event_Type is

      entry Cancel when Wait_For_Completion'Count = 0
               and then Wait_For_Connection'Count = 0
               and then Wait_For_Release'Count = 0 is
      begin
         null;
      end Cancel;

      entry Lounge when Owner = Null_Task_ID is
      begin
         Owner := Lounge'Caller;
         Count := 1;
         Done  := False;
      end Lounge;

      entry Seize when True is
      begin
         if Seize'Caller = Owner then
            Count := Count + 1;
            Done  := False;
         else
            requeue Lounge with abort;
         end if;
      end Seize;

      procedure Release is
      begin
         if Owner = Current_Task then
            Count := Count - 1;
            if Count = 0 then
               Owner := Null_Task_ID;
            end if;
         else
            Raise_Exception
            (  Program_Error'Identity,
               "Mutex ownership error"
            );
         end if;
      end Release;

      procedure Complete is
      begin
         Done := True;
      end Complete;

      procedure Set is
      begin
         case Get_Session_State (Client.all) is
            when Session_Down =>
               Ready := False;
               Down  := True;
            when Session_Disconnected | Session_Connecting |
                 Session_Handshaking  | Session_Busy       =>
               Ready := False;
               Down  := False;
            when Session_Active | Session_Connected =>
               Ready := True;
               Down  := False;
         end case;
      end Set;

      entry Wait_For_Completion
         when Done or else Down or else Event_Type.Cancel'Count > 0 is
      begin
         if Done then
            return;
         end if;
         case Get_Session_State (Client.all) is
            when Session_Down =>
               Ready := False;
               Down  := True;
               declare
                  Error : Exception_Occurrence;
               begin
                  Get_Occurrence (Client.all, Error);
                  if Exception_Identity (Error) /= Null_Id then
                     Reraise_Occurrence (Error);
                  else
                     Raise_Exception
                     (  Status_Error'Identity,
                        "Unable to connect"
                     );
                  end if;
               end;
            when Session_Disconnected | Session_Connecting |
                 Session_Handshaking  | Session_Busy       =>
               Ready := False;
               Down  := False;
            when Session_Active | Session_Connected =>
               Ready := True;
               Down  := False;
         end case;
         Raise_Exception
         (  Status_Error'Identity,
            "Connection was lost during execution of the request"
         );
      end Wait_For_Completion;

      entry Wait_For_Release
         when Down or else Event_Type.Cancel'Count > 0 is
      begin
         if Get_Session_State (Client.all) /= Session_Down then
            Down := False;
            if Event_Type.Cancel'Count > 0 then
               Raise_Exception (Cancel_Error'Identity, "Canceled");
            else
               requeue Wait_For_Release with abort;
            end if;
         end if;
      end Wait_For_Release;

      entry Wait_For_Connection
         when Ready or else Down or else Event_Type.Cancel'Count > 0 is
      begin
         case Get_Session_State (Client.all) is
            when Session_Down =>
               Ready := False;
               Down  := True;
               declare
                  Error : Exception_Occurrence;
               begin
                  Get_Occurrence (Client.all, Error);
                  if Exception_Identity (Error) /= Null_Id then
                     Reraise_Occurrence (Error);
                  else
                     Raise_Exception
                     (  Status_Error'Identity,
                        "Unable to connect"
                     );
                  end if;
               end;
            when Session_Disconnected | Session_Connecting |
                 Session_Handshaking  | Session_Busy       =>
               Ready := False;
               Down  := False;
               if Event_Type.Cancel'Count > 0 then
                  Raise_Exception (Cancel_Error'Identity, "Canceled");
               else
                  requeue Wait_For_Connection with abort;
               end if;
            when Session_Active | Session_Connected =>
               Ready := True;
               Down  := False;
         end case;
      end Wait_For_Connection;

   end Event_Type;

end GNAT.Sockets.Connection_State_Machine.MODBUS_Client.Synchronous;
