------------------------------------------------------------------------------
-- Copyright (c) 2014, Natacha Porté                                        --
--                                                                          --
-- Permission to use, copy, modify, and distribute this software for any    --
-- purpose with or without fee is hereby granted, provided that the above   --
-- copyright notice and this permission notice appear in all copies.        --
--                                                                          --
-- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES --
-- WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF         --
-- MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR  --
-- ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES   --
-- WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN    --
-- ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF  --
-- OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.           --
------------------------------------------------------------------------------

------------------------------------------------------------------------------
-- Natools.Cron is a low-overhead, low-precision implementation of periodic --
-- callbacks, similar to UNIX cron daemon.                                  --
-- Note that callbacks are executed sequentially in a single thread, and    --
-- ticks may be skipped when computing resources lack.                      --
-- If you need more precision and/or more reliability, you might want to    --
-- consider using Ada.Real_Time.Timing_Events instead.                      --
------------------------------------------------------------------------------

with Ada.Calendar;

private with Ada.Containers.Doubly_Linked_Lists;
private with Ada.Containers.Ordered_Maps;
private with Ada.Finalization;
private with Ada.Unchecked_Deallocation;
private with Natools.References;
private with Natools.Storage_Pools;

package Natools.Cron is

   type Callback is interface;

   procedure Run (Object : in out Callback) is abstract;


   type Periodic_Time is record
      Origin : Ada.Calendar.Time;
      Period : Duration;
   end record;


   type Cron_Entry is tagged limited private;

   function Create
     (Time : in Periodic_Time;
      Callback : in Cron.Callback'Class)
     return Cron_Entry;
      --  Create a new entry with the given parameters

   function Create
     (Period : in Duration;
      Callback : in Cron.Callback'Class)
     return Cron_Entry;
      --  Create a new entry starting within a period from now

   procedure Set
     (Self : in out Cron_Entry;
      Time : in Periodic_Time;
      Callback : in Cron.Callback'Class);
      --  Reset an entry with the given parameters

   procedure Set
     (Self : in out Cron_Entry;
      Period : in Duration;
      Callback : in Cron.Callback'Class);
      --  Reset entry with the given parameters, starting one period from now

   procedure Reset (Self : in out Cron_Entry);
      --  Clear internal state and remove associated entry from database.
      --  Note that if the callback procedure is currently running, it will
      --  continue until it returns, so the callback object may outlive
      --  the call to Reset, plan concurrency accordingly.

private

   package Callback_Refs is new References
     (Callback'Class,
      Storage_Pools.Access_In_Default_Pool'Storage_Pool,
      Storage_Pools.Access_In_Default_Pool'Storage_Pool);


   type Cron_Entry is new Ada.Finalization.Limited_Controlled with record
      Callback : Callback_Refs.Reference;
   end record;

   overriding procedure Finalize (Object : in out Cron_Entry);


   function "<" (Left, Right : Periodic_Time) return Boolean;
      --  Comparison function for ordered map

   package Entry_Maps is new Ada.Containers.Ordered_Maps
     (Periodic_Time, Callback_Refs.Reference, "<", Callback_Refs."=");


   protected Database is
      procedure Insert
        (Time : in Periodic_Time;
         Callback : in Callback_Refs.Reference);
         --  Insert Callback into the database, adjusting Time.Origin
         --  to be in the future.

      procedure Remove (Callback : in Callback_Refs.Reference);
         --  Remove Callback from the database

      procedure Update (Callback : in Callback_Refs.Reference);
         --  Update Time.Origin associated with Callback so that
         --  it is in the future.

      procedure Get_First
        (Time : out Periodic_Time;
         Callback : out Callback_Refs.Reference);
         --  Return the next active callback, or an empty reference when
         --  the database is empty (to signal task termination).

      entry Update_Notification;
         --  Block as long as the next active item does not change

   private
      Map : Entry_Maps.Map;
      First_Changed : Boolean := False;
   end Database;

   task type Worker is
   end Worker;

   type Worker_Access is access Worker;

   procedure Unchecked_Free is new Ada.Unchecked_Deallocation
     (Worker, Worker_Access);

   Global_Worker : Worker_Access := null;


   package Event_Lists is new Ada.Containers.Doubly_Linked_Lists
     (Callback_Refs.Reference, Callback_Refs."=");

   type Event_List is new Callback with record
      List : Event_Lists.List;
   end record;

   overriding procedure Run (Self : in out Event_List);
      --  Sequentially run the contained events

   procedure Append
     (Self : in out Event_List;
      Ref : in Callback_Refs.Reference);
      --  Append Ref at the end of Self.List

   procedure Remove
     (Self : in out Event_List;
      Ref : in Callback_Refs.Reference;
      Removed : out Boolean);
      --  Remove Ref from Self.List, through a linear search

   function Is_Empty (Self : Event_List) return Boolean;
      --  Return whether Self contains any element

end Natools.Cron;
