--                                                                    --
--  package                         Copyright (c)  Dmitry A. Kazakov  --
--     GLib.Spawn_Alt.Asynchronous                 Luebeck            --
--  Implementation                                 Spring, 2009       --
--                                                                    --
--                                Last revision :  21:51 06 Jun 2019  --
--                                                                    --
--  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 Glib.Messages;      use Glib.Messages;
with GNAT.OS_Lib;        use GNAT.OS_Lib;
with Gtk.Missed;         use Gtk.Missed;
with System;             use System;

with GNAT.Traceback.Symbolic;
with Interfaces.C.Strings;
with System.Address_To_Access_Conversions;

package body GLib.Spawn_Alt.Asynchronous is

   function Where (Text : String) return String is
   begin
      return " in GLib.Spawn_Alt.Asynchronous." & Text;
   end Where;

   procedure Free is
      new Ada.Unchecked_Deallocation (Reader, Reader_Ptr);

   procedure Free is
      new Ada.Unchecked_Deallocation (Writer, Writer_Ptr);

   procedure Free (List : in out Chars_Ptr_Array_Ptr);

   procedure On_Exit
             (  PID    : GPid;
                Status : GInt;
                Data   : Address
             );
   pragma Convention (C, On_Exit);

   package Conversions is
      new System.Address_To_Access_Conversions
          (  Asynchronous_Process'Class
          );
   use Conversions;

   procedure Completed
             (  Process : in out Asynchronous_Process;
                Status  : GInt
             )  is
   begin
      Free (Process.ArgV);
      Free (Process.EnvP);
   end Completed;

   function Copy (List : Chars_Ptr_Array) return Chars_Ptr_Array_Ptr is
      Result : constant Chars_Ptr_Array_Ptr :=
               new Chars_Ptr_Array (List'First..List'Last + 1);
   begin
      for Index in Result'Range loop
         Result (Index) := New_Char_Array (Value (List (Index)));
      end loop;
      Result (Result'Last) := Interfaces.C.Strings.Null_Ptr;
      return Result;
   end Copy;

   function Copy
            (  Name : UTF8_String;
               List : Chars_Ptr_Array
            )  return Chars_Ptr_Array_Ptr is
      Result : constant Chars_Ptr_Array_Ptr :=
               new Chars_Ptr_Array (List'First..List'Last + 2);
   begin
      Result (List'First) := New_String (Name);
      for Index in List'Range loop
         Result (Index + 1) := New_Char_Array (Value (List (Index)));
      end loop;
      Result (Result'Last) := Interfaces.C.Strings.Null_Ptr;
      return Result;
   end Copy;

   function Copy (List : GList) return Chars_Ptr_Array_Ptr is
      Ptr   : GList   := List;
      Count : Natural := 0;
   begin
      while Ptr /= Null_List loop
         Count := Count + 1;
         Ptr   := Next (Ptr);
      end loop;
      declare
         Result : constant Chars_Ptr_Array_Ptr :=
                  new Chars_Ptr_Array (1..Count + 1);
      begin
         Ptr := List;
         for Index in 1..Count loop
            Result (Index) := New_String (Get_Data (Ptr));
            Ptr := Next (Ptr);
         end loop;
         Result (Result'Last) := Interfaces.C.Strings.Null_Ptr;
         return Result;
      end;
   end Copy;

   function Copy
            (  Name : UTF8_String;
               List : GList
            )  return Chars_Ptr_Array_Ptr is
      Ptr   : GList   := List;
      Count : Natural := 0;
   begin
      while Ptr /= Null_List loop
         Count := Count + 1;
         Ptr   := Next (Ptr);
      end loop;
      declare
         Result : constant Chars_Ptr_Array_Ptr :=
                  new Chars_Ptr_Array (1..Count + 2);
      begin
         Result (1) := New_String (Name);
         Ptr := List;
         for Index in 2..Count + 1  loop
            Result (Index) := New_String (Get_Data (Ptr));
            Ptr := Next (Ptr);
         end loop;
         Result (Result'Last) := Interfaces.C.Strings.Null_Ptr;
         return Result;
      end;
   end Copy;

   procedure Error
             (  Process : in out Asynchronous_Process;
                Data    : UTF8_String
             )  is
   begin
      null;
   end Error;

   procedure Failed
             (  Process : in out Asynchronous_Process;
                Error   : GError
             )  is
   begin
      null;
   end Failed;

   procedure Finalize (Process : in out Asynchronous_Process) is
   begin
      select
         Process.Status.Wait_All;
      or delay 1.0;
      end select;
      if Process.Error /= null then
         Error_Free (Process.Error);
         Process.Error := null;
      end if;
   end Finalize;

   procedure Free (List : in out Chars_Ptr_Array_Ptr) is
      procedure Release is
         new Ada.Unchecked_Deallocation
             (  Chars_Ptr_Array,
                Chars_Ptr_Array_Ptr
             );
   begin
      if List /= null then
         for Index in List'Range loop
            Free (List (Index));
         end loop;
         Release (List);
      end if;
   end Free;

   function Get_Error (Process : Asynchronous_Process) return GError is
   begin
      return Process.Error;
   end Get_Error;

   function Get_State (Process : Asynchronous_Process)
      return Process_State is
   begin
      return Process.Status.Get;
   end Get_State;

   function Get_Exit_Status (Process : Asynchronous_Process)
      return GInt is
   begin
      return Process.Status.Get;
   end Get_Exit_Status;

   procedure Input
             (  Process : in out Asynchronous_Process;
                Data    : out UTF8_String;
                Count   : out Natural
             )  is
   begin
      Count := 0;
   end Input;

   procedure On_Exit
             (  PID    : GPid;
                Status : GInt;
                Data   : Address
             )  is
   begin
      To_Pointer (Data).Status.Set (Process_Completed, Status);
   end On_Exit;

   procedure Output
             (  Process : in out Asynchronous_Process;
                Data    : UTF8_String
             )  is
   begin
      null;
   end Output;

   protected body Process_Status is
      function Completed_Tasks_Count return Natural is
      begin
         return Count;
      end Completed_Tasks_Count;

      function Get return Process_State is
      begin
         return State;
      end Get;

      function Get return GInt is
      begin
         return Status;
      end Get;

      procedure Set (State : Process_State) is
      begin
         if (  Process_Status.State = Process_Running
            and then
               State = Process_Completed
            and then
               Count < 3
            )  then
            Count := Count + 1;
         else
            Process_Status.State := State;
            Count := 0;
         end if;
      end Set;

      procedure Set (State : Process_State; Status : GInt) is
      begin
         Process_Status.Status := Status;
         Set (State);
      end Set;

      entry Wait_IO when State = Process_Running and then Count = 3 is
      begin
         null;
      end Wait_IO;

      entry Wait_All when State /= Process_Running is
      begin
         null;
      end Wait_All;

   end Process_Status;

   task body Reader is
      Buffer : UTF8_String (1..1024);
      Byte   : aliased Character;
      Count  : Natural := 0;
   begin
      begin
         while 0 < Read (File_Descriptor (Pipe), Byte'Address, 1) loop
            Count := Count + 1;
            Buffer (Count) := Byte;
            if (  Count = Buffer'Length
               or else
                  Buffer (Count) = Character'Val (10)
               )
            then
               if Pipe = Process.Standard_Output then
                  Output (Process.all, Buffer (1..Count));
               else
                  Error (Process.all, Buffer (1..Count));
               end if;
               Count := 0;
            end if;
         end loop;
         if Count > 0 then
            if Pipe = Process.Standard_Output then
               Output (Process.all, Buffer (1..Count));
            else
               Error (Process.all, Buffer (1..Count));
            end if;
         end if;
      exception
         when Error : others =>
            Log
            (  GtkAda_Contributions_Domain,
               Log_Level_Critical,
               (  Exception_Message (Error)
               &  Where ("task Reader")
            )  );
            Log
            (  GtkAda_Contributions_Domain,
               Log_Level_Critical,
               GNAT.Traceback.Symbolic.Symbolic_Traceback (Error)
            );
      end;
      Process.Status.Set (Process_Completed);
   end Reader;

   procedure Run
             (  Process           : in out Asynchronous_Process;
                Name              : UTF8_String;
                Working_Directory : UTF8_String;
                ArgV              : Chars_Ptr_Array;
                EnvP              : Chars_Ptr_Array
             )  is
   begin
      if Process.Status.Get = Process_Running then
         raise Use_Error;
      end if;
      if Process.Error /= null then
         Process.Status.Set (Process_Completed);
         Error_Free (Process.Error);
         Process.Error := null;
      end if;
      Process.ArgV := Copy (Name, ArgV);
      Process.EnvP := Copy (EnvP);
      Start
      (  Process,
         Async_With_Pipes
         (  Working_Directory => Working_Directory,
            ArgV              => Process.ArgV (1)'Access,
            EnvP        => Process.EnvP (1)'Access,
            Flags       => SPAWN_DO_NOT_REAP_CHILD or SPAWN_SEARCH_PATH
      )  );
   end Run;

   procedure Run
             (  Process           : in out Asynchronous_Process;
                Name              : UTF8_String;
                Working_Directory : UTF8_String;
                ArgV              : GList;
                EnvP              : GList
            )   is
   begin
      if Process.Status.Get = Process_Running then
         raise Use_Error;
      end if;
      if Process.Error /= null then
         Process.Status.Set (Process_Completed);
         Error_Free (Process.Error);
         Process.Error := null;
      end if;
      Process.ArgV := Copy (Name, ArgV);
      Process.EnvP := Copy (EnvP);
      Start
      (  Process,
         Async_With_Pipes
         (  Working_Directory => Working_Directory,
            ArgV              => Process.ArgV (1)'Access,
            EnvP        => Process.EnvP (1)'Access,
            Flags       => SPAWN_DO_NOT_REAP_CHILD or SPAWN_SEARCH_PATH
      )  );
   end Run;

   procedure Run
             (  Process           : in out Asynchronous_Process;
                Name              : UTF8_String;
                Working_Directory : UTF8_String;
                ArgV              : Chars_Ptr_Array
             )  is
   begin
      if Process.Status.Get = Process_Running then
         raise Use_Error;
      end if;
      if Process.Error /= null then
         Process.Status.Set (Process_Completed);
         Error_Free (Process.Error);
         Process.Error := null;
      end if;
      Process.ArgV := Copy (Name, ArgV);
      Start
      (  Process,
         Async_With_Pipes
         (  Working_Directory => Working_Directory,
            ArgV              => Process.ArgV (1)'Access,
            Flags       => SPAWN_DO_NOT_REAP_CHILD or SPAWN_SEARCH_PATH
      )  );
   end Run;

   procedure Run
             (  Process           : in out Asynchronous_Process;
                Name              : UTF8_String;
                Working_Directory : UTF8_String;
                ArgV              : GList
            )   is
   begin
      if Process.Status.Get = Process_Running then
         raise Use_Error;
      end if;
      if Process.Error /= null then
         Process.Status.Set (Process_Completed);
         Error_Free (Process.Error);
         Process.Error := null;
      end if;
      Process.ArgV := Copy (Name, ArgV);
      Start
      (  Process,
         Async_With_Pipes
         (  Working_Directory => Working_Directory,
            ArgV              => Process.ArgV (1)'Access,
            Flags       => SPAWN_DO_NOT_REAP_CHILD or SPAWN_SEARCH_PATH
      )  );
   end Run;

   procedure Run
             (  Process : in out Asynchronous_Process;
                Name    : UTF8_String;
                ArgV    : Chars_Ptr_Array;
                EnvP    : Chars_Ptr_Array
             )  is
   begin
      if Process.Status.Get = Process_Running then
         raise Use_Error;
      end if;
      if Process.Error /= null then
         Process.Status.Set (Process_Completed);
         Error_Free (Process.Error);
         Process.Error := null;
      end if;
      Process.ArgV := Copy (Name, ArgV);
      Process.EnvP := Copy (EnvP);
      Start
      (  Process,
         Async_With_Pipes
         (  ArgV        => Process.ArgV (1)'Access,
            EnvP        => Process.EnvP (1)'Access,
            Flags       => SPAWN_DO_NOT_REAP_CHILD or SPAWN_SEARCH_PATH
      )  );
   end Run;

   procedure Run
             (  Process : in out Asynchronous_Process;
                Name    : UTF8_String;
                ArgV    : GList;
                EnvP    : GList
             )  is
   begin
      if Process.Status.Get = Process_Running then
         raise Use_Error;
      end if;
      if Process.Error /= null then
         Process.Status.Set (Process_Completed);
         Error_Free (Process.Error);
         Process.Error := null;
      end if;
      Process.ArgV := Copy (Name, ArgV);
      Process.EnvP := Copy (EnvP);
      Start
      (  Process,
         Async_With_Pipes
         (  ArgV  => Process.ArgV (1)'Access,
            EnvP  => Process.EnvP (1)'Access,
            Flags => SPAWN_DO_NOT_REAP_CHILD or SPAWN_SEARCH_PATH
      )  );
   end Run;

   procedure Run
             (  Process : in out Asynchronous_Process;
                Name    : UTF8_String;
                ArgV    : Chars_Ptr_Array
             )  is
   begin
      if Process.Status.Get = Process_Running then
         raise Use_Error;
      end if;
      if Process.Error /= null then
         Process.Status.Set (Process_Completed);
         Error_Free (Process.Error);
         Process.Error := null;
      end if;
      Process.ArgV := Copy (Name, ArgV);
      Start
      (  Process,
         Async_With_Pipes
         (  ArgV  => Process.ArgV (1)'Access,
            Flags => SPAWN_DO_NOT_REAP_CHILD or SPAWN_SEARCH_PATH
      )  );
   end Run;

   procedure Run
             (  Process : in out Asynchronous_Process;
                Name    : UTF8_String;
                ArgV    : GList
            )   is
   begin
      if Process.Status.Get = Process_Running then
         raise Use_Error;
      end if;
      if Process.Error /= null then
         Process.Status.Set (Process_Completed);
         Error_Free (Process.Error);
         Process.Error := null;
      end if;
      Process.ArgV := Copy (Name, ArgV);
      Start
      (  Process,
         Async_With_Pipes
         (  ArgV  => Process.ArgV (1)'Access,
            Flags => SPAWN_DO_NOT_REAP_CHILD or SPAWN_SEARCH_PATH
      )  );
   end Run;

   procedure Service_Exit (Process : in out Asynchronous_Process_Ptr) is
   begin
      Free (Process.Input_Writer);
      Free (Process.Output_Reader);
      Free (Process.Error_Reader);
      Close (File_Descriptor (Process.Standard_Error));
      Close (File_Descriptor (Process.Standard_Input));
      Close (File_Descriptor (Process.Standard_Output));
      Free (Process.ArgV);
      Free (Process.EnvP);
      Close_PID (Process.PID);
      begin
         Completed (Process.all, Process.Status.Get);
      exception
         when Error : others =>
            Log
            (  GtkAda_Contributions_Domain,
               Log_Level_Critical,
               (  Exception_Message (Error)
               &  Where ("Service_Exit")
            )  );
            Log
            (  GtkAda_Contributions_Domain,
               Log_Level_Critical,
               GNAT.Traceback.Symbolic.Symbolic_Traceback (Error)
            );
      end;
      Process.Status.Set (Process_Completed);
   end Service_Exit;

   procedure Start
             (  Process : in out Asynchronous_Process'Class;
                Result  : Async_Result
             )  is
   begin
      if Result.Running then
         Process.Status.Set (Process_Running);
         Process.PID             := Result.PID;
         Process.Standard_Input  := Result.Standard_Input;
         Process.Standard_Output := Result.Standard_Output;
         Process.Standard_Error  := Result.Standard_Error;
         Process.Input_Writer :=
            new Writer
                (  Process'Unchecked_Access,
                   Process.Standard_Input
                );
         Process.Output_Reader :=
            new Reader
                (  Process'Unchecked_Access,
                   Process.Standard_Output
                );
         Process.Error_Reader :=
            new Reader
                (  Process'Unchecked_Access,
                   Process.Standard_Error
                );
         Child_Watch_Add
         (  Process.PID,
            On_Exit'Access,
            Process'Address
         );
      else
         Process.Error := Result.Error;
         Process.Status.Set (Process_Failed_To_Start);
         Free (Process.ArgV);
         Free (Process.EnvP);
         Failed (Process, Process.Error);
      end if;
   end Start;

   procedure Wait
             (  Process  : in out Asynchronous_Process;
                Time_Out : Duration := Duration'Last
             )  is
   begin
      if Time_Out = Duration'Last then
         Process.Status.Wait_All;
      else
         select
            Process.Status.Wait_All;
         or delay Time_Out;
            raise Time_Error;
         end select;
      end if;
   end Wait;

   task body Writer is
      Bytes  : Integer;
      Buffer : UTF8_String (1..1024*10);
      Count  : Natural;
   begin
      begin
         loop
            Input (Process.all, Buffer, Count);
            exit when Count = 0;
            Bytes :=
               Write
               (  File_Descriptor (Pipe),
                  Buffer'Address,
                  Integer (Count)
               );
            exit when Bytes /= Integer (Count);
         end loop;
      exception
         when Error : others =>
            Log
            (  GtkAda_Contributions_Domain,
               Log_Level_Critical,
               (  Exception_Message (Error)
               &  Where ("task Writer")
            )  );
            Log
            (  GtkAda_Contributions_Domain,
               Log_Level_Critical,
               GNAT.Traceback.Symbolic.Symbolic_Traceback (Error)
            );
      end;
      Process.Status.Wait_IO;
      Send (Service_Exit'Access, Process.all'Unchecked_Access);
   end Writer;

end GLib.Spawn_Alt.Asynchronous;
