with Ada.Exceptions;
private with Ada.Finalization;

generic
   type Element_Type (<>) is private;
   with function Image (This : Element_Type) return String is <>;
package Optional.Values with Preelaborate is

   --  Reference types

   type Const_Ref (Ptr : access constant Element_Type) is
   limited null record with Implicit_Dereference => Ptr;

   type Var_Ref (Ptr : access Element_Type) is
   limited null record with Implicit_Dereference => Ptr;

   --------------
   -- Optional --
   --------------

   type Optional is tagged private;

   function "=" (L : Optional; R : Element_Type) return Boolean with
     Post => "="'Result = (L.Has_Element and then L.Element = R);
   function "=" (L : Element_Type; R : Optional) return Boolean with
     Post => "="'Result = (R = L);

   Empty : constant Optional;

   function Has_Element (This : Optional) return Boolean;

   function Is_Empty (This : Optional) return Boolean with
     Post => Is_Empty'Result = not This.Has_Element;

   function Image (This : Optional) return String;
   --  Returns [empty] or [value: Element_Type'Image (This.Element)]

   function Unit (Element : Element_Type) return Optional with
     Post => Unit'Result.Element = Element;

   function Element (This : Optional) return Const_Ref
     with Pre => This.Has_Element;

   function Value (This : Optional) return Element_Type
     with Pre => This.Has_Element;

   function Reference (This : in out Optional) return Var_Ref
     with Pre => This.Has_Element;

   ----------------
   -- Operations --
   ----------------

   function Flat_Map
     (This   : Optional;
      Mapper : access function (Element : Optional) return Optional)
      return Optional with
     Post => (if This.Has_Element and then Mapper /= null then
                Flat_Map'Result = Mapper (This)
              elsif This.Has_Element and then Mapper = null then
                Flat_Map'Result = This
              else
                Flat_Map'Result = Empty);

   function Map (This : Optional;
                 Mapper : access function (Element : Element_Type)
                                           return Element_Type)
                 return Optional with
     Post => (if This.Has_Element and then Mapper /= null then
                Map'Result.Element.Ptr.all = Mapper (This.Element.Ptr.all)
              elsif This.Has_Element and then Mapper = null then
                Map'Result = This
              else
                Map'Result = Empty);

   function Or_Else (This    : Optional;
                     Default : Element_Type)
                     return Element_Type with
     Post => (if This.Has_Element
              then Or_Else'Result = This.Element.Ptr.all
              else Or_Else'Result = Default);

   function Or_Else (This    : Optional;
                     Default : Element_Type)
                     return Optional with
     Post => (if This.Has_Element
              then Or_Else'Result = This.Element.Ptr.all
              else Or_Else'Result = Unit (Default));

   function Or_Raise (This   : Optional;
                      Ex_Id  : Ada.Exceptions.Exception_Id;
                      Ex_Msg : String := "")
                      return Element_Type with
     Post => (if This.Has_Element
              then Or_Raise'Result = This.Element.Ptr.all
              else raise Constraint_Error);
   --  Actually, Ex_Id will be raised, not CE

   function Filter (This      : Optional;
                    Condition : Boolean) return Optional with
     Post => (if Condition
                then Filter'Result = This
                else Filter'Result = Empty);

   ----------------
   -- References --
   ----------------

   function Image (This : Const_Ref) return String;

   function Image (This : Var_Ref) return String;

private

   package AF renames Ada.Finalization;

   type Element_Access is access Element_Type;

   type Optional is new Ada.Finalization.Controlled
   with record
      Element : Element_Access; -- Holders still causing bugs
   end record;

   overriding procedure Adjust (This : in out Optional);

   overriding procedure Finalize (This : in out Optional);

   ---------
   -- "=" --
   ---------

   function "=" (L : Optional; R : Element_Type) return Boolean
   is (L.Has_Element and then L.Element.all = R);

   function "=" (L : Element_Type; R : Optional) return Boolean
   is (R = L);

   -------------
   -- Element --
   -------------

   function Element (This : Optional) return Const_Ref
   is (Const_Ref'(Ptr => This.Element));

   Empty : constant Optional := (AF.Controlled with Element => null);

   ------------
   -- Filter --
   ------------

   function Filter (This      : Optional;
                    Condition : Boolean) return Optional
   is (if Condition
       then This
       else Empty);

   --------------
   -- Flat_Map --
   --------------

   function Flat_Map
     (This   : Optional;
      Mapper : access function (Element : Optional) return Optional)
      return Optional
   is (if This.Has_Element and then Mapper /= null
       then Mapper (This)
       else This);

   -----------------
   -- Has_Element --
   -----------------

   function Has_Element (This : Optional) return Boolean
   is (This.Element /= null);

   -----------
   -- Image --
   -----------

   function Image (This : Optional) return String
   is (if not This.Has_Element
       then "[empty]"
       else "[value:"
            & Image (This.Element.all) & "]");

   function Image (This : Const_Ref) return String
   is (Image (This.Ptr.all));

   function Image (This : Var_Ref) return String
   is (Image (This.Ptr.all));

   --------------
   -- Is_Empty --
   --------------

   function Is_Empty (This : Optional) return Boolean
   is (not This.Has_Element);

   ---------
   -- Map --
   ---------

   function Map (This : Optional;
                 Mapper : access function (Element : Element_Type)
                                           return Element_Type)
                 return Optional
   is (if This.Has_Element and then Mapper /= null
       then Unit (Mapper (This.Element.all))
       else This);

   -------------
   -- Or_Else --
   -------------

   function Or_Else (This    : Optional;
                     Default : Element_Type)
                     return Element_Type
   is (if This.Has_Element
       then This.Element.all
       else Default);

   -------------
   -- Or_Else --
   -------------

   function Or_Else (This    : Optional;
                     Default : Element_Type)
                     return Optional
   is (if This.Has_Element
       then This
       else Unit (Default));

   ---------------
   -- Reference --
   ---------------

   function Reference (This : in out Optional) return Var_Ref
   is (Var_Ref'(Ptr => This.Element));

   ----------
   -- Unit --
   ----------

   function Unit (Element : Element_Type) return Optional
   is (Ada.Finalization.Controlled with
       Element     => new Element_Type'(Element));

   -----------
   -- Value --
   -----------

   function Value (This : Optional) return Element_Type
   is (Element (This));

end Optional.Values;
