-------------------------------------------------------------------------------
-- (C) Altran Praxis Limited
-------------------------------------------------------------------------------
--
-- The SPARK toolset is free software; you can redistribute it and/or modify it
-- under terms of the GNU General Public License as published by the Free
-- Software Foundation; either version 3, or (at your option) any later
-- version. The SPARK toolset 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 distributed with the SPARK toolset; see file
-- COPYING3. If not, go to http://www.gnu.org/licenses for a complete copy of
-- the license.
--
--=============================================================================

package body Date_Time is

   type Time_T is record
      Day     : Days_T;
      Month   : Months_T;
      Year    : Years_T;
      Hour    : Hours_T;
      Minutes : Minutes_T;
      Seconds : Seconds_T;
   end record;

   --  This function parses the given 3-character month and returns
   --  its number. On error, this function returns 0. This function
   --  assumes the month abbreviation is in English.
   function Month_To_Number (E_Str : E_Strings.T) return Natural
   --# return X => X = 0 or X in Months_T;
   is
      Result : Natural;
   begin
      Result := 0;
      for I in Months_T loop
         if E_Strings.Eq1_String (E_Str, Month_Names (I)) then
            Result := I;
         end if;
         --# assert Result = 0 or Result = I;
         exit when Result > 0;
      end loop;
      return Result;
   end Month_To_Number;

   --  This function returns true iff all characters in E_Str in
   --  the given range are digits.
   function Is_Digit_Range
     (E_Str : E_Strings.T;
      Low   : E_Strings.Positions;
      High  : E_Strings.Positions)
     return  Boolean
   --# pre Low <= High and High <= E_Strings.Get_Length (E_Str);
   --  Once we use victor, we can also prove the below. The loop
   --  invariant below will also require the obvious adjustment.
   -- return (for all I in E_Strings.Positions range Low .. High ->
   --           (E_Strings.Get_Element (E_Str, I) in '0' .. '9'));
   is
      Result : Boolean;
   begin
      for I in E_Strings.Positions range Low .. High loop
         Result := E_Strings.Get_Element (E_Str, I) in '0' .. '9';
         exit when not Result;
         --# assert Result and I in Low .. High and Low = Low% and High = High%;
      end loop;
      --# accept F, 501, Result, "The value will always be defined",
      --#        "since the loop range cannot be empty." &
      --#        F, 602, Result, "Result is never undefined.";
      return Result;
   end Is_Digit_Range;

   --  This procedure parses a timestamp given in the DD-MMM-YYYY,
   --  HH:MM:SS format (where the comma is optional). On any
   --  parsing error Success is set to False, otherwise Time will
   --  contain the parsed timestamp. This function assumes the
   --  month abbreviation is in English.
   procedure Parse_Time (Timestamp : in     E_Strings.T;
                         Success   :    out Boolean;
                         Time      :    out Time_T)
   --# derives Success,
   --#         Time    from Timestamp;
   is
      Dummy         : Natural;
      Tmp           : Integer;
      Len           : E_Strings.Lengths;
      Start_Of_Time : E_Strings.Positions;
   begin
      Time :=
        Time_T'
        (Day     => Days_T'First,
         Month   => Months_T'First,
         Year    => Years_T'First,
         Hour    => Hours_T'First,
         Minutes => Minutes_T'First,
         Seconds => Seconds_T'First);

      --  Cache the string length.
      Len := E_Strings.Get_Length (Timestamp);

      --# accept F, 10, Dummy, "We do not care about any assignment to the dummy variable." &
      --#        F, 33, Dummy, "We don't use the dummy variable to derive anything.";

      --  The date is always 10 characters long.
      if Len >= 11 then
         Success := Is_Digit_Range (Timestamp, 1, 2)
           and then E_Strings.Get_Element (Timestamp, 3) = '-'
           and then E_Strings.Get_Element (Timestamp, 7) = '-'
           and then Is_Digit_Range (Timestamp, 8, 11);
         if Success then
            --  Obtain the day.
            E_Strings.Get_Int_From_String
              (Source   => E_Strings.Section (E_Str     => Timestamp,
                                              Start_Pos => 1,
                                              Length    => 2),
               Item     => Tmp,
               Start_Pt => 1,
               Stop     => Dummy);
            if Tmp in Days_T then
               Time.Day := Days_T'(Tmp);
            else
               Success := False;
            end if;
            --# assert Len = E_Strings.Get_Length (Timestamp);

            --  Obtain the month. Since Month_To_Number returns
            --  zero on error, we need to check for that.
            Tmp := Month_To_Number (E_Strings.Section (E_Str     => Timestamp,
                                                       Start_Pos => 4,
                                                       Length    => 3));
            if Tmp in Months_T then
               Time.Month := Months_T'(Tmp);
            else
               Success := False;
            end if;
            --# assert Len = E_Strings.Get_Length (Timestamp);

            --  Obtain the year.
            E_Strings.Get_Int_From_String
              (Source   => E_Strings.Section (E_Str     => Timestamp,
                                              Start_Pos => 8,
                                              Length    => 4),
               Item     => Tmp,
               Start_Pt => 1,
               Stop     => Dummy);
            if Tmp in Years_T then
               Time.Year := Years_T'(Tmp);
            else
               Success := False;
            end if;
         end if;
      else
         Success := False;
      end if;

      --# assert Len = E_Strings.Get_Length (Timestamp);

      if Success and Len >= 12 then
         if E_Strings.Get_Element (Timestamp, 12) = ',' then
            Start_Of_Time := 13;
         else
            Start_Of_Time := 12;
         end if;
      else
         Success       := False;
         Start_Of_Time := E_Strings.Positions'First;
      end if;

      --# assert Len = E_Strings.Get_Length (Timestamp) and Start_Of_Time <= 13;

      --  The actual time consists out 8 more characters after a
      --  mandatory space.
      if Success and Len >= Start_Of_Time + 8 then
         Success := E_Strings.Get_Element (Timestamp, Start_Of_Time) = ' '
           and then Is_Digit_Range (Timestamp, Start_Of_Time + 1, Start_Of_Time + 2)
           and then E_Strings.Get_Element (Timestamp, Start_Of_Time + 3) = ':'
           and then Is_Digit_Range (Timestamp, Start_Of_Time + 4, Start_Of_Time + 5)
           and then E_Strings.Get_Element (Timestamp, Start_Of_Time + 6) = ':'
           and then Is_Digit_Range (Timestamp, Start_Of_Time + 7, Start_Of_Time + 8);
         if Success then
            --  Obtain the hour.
            E_Strings.Get_Int_From_String
              (Source   => E_Strings.Section (E_Str     => Timestamp,
                                              Start_Pos => Start_Of_Time + 1,
                                              Length    => 2),
               Item     => Tmp,
               Start_Pt => 1,
               Stop     => Dummy);
            if Tmp in Hours_T then
               Time.Hour := Hours_T'(Tmp);
            else
               Success := False;
            end if;
            --# assert Len = E_Strings.Get_Length (Timestamp) and Start_Of_Time <= 13;

            --  Obtain the minutes.
            E_Strings.Get_Int_From_String
              (Source   => E_Strings.Section (E_Str     => Timestamp,
                                              Start_Pos => Start_Of_Time + 4,
                                              Length    => 2),
               Item     => Tmp,
               Start_Pt => 1,
               Stop     => Dummy);
            if Tmp in Minutes_T then
               Time.Minutes := Minutes_T'(Tmp);
            else
               Success := False;
            end if;
            --# assert Len = E_Strings.Get_Length (Timestamp) and Start_Of_Time <= 13;

            --  Obtain the seconds.
            E_Strings.Get_Int_From_String
              (Source   => E_Strings.Section (E_Str     => Timestamp,
                                              Start_Pos => Start_Of_Time + 7,
                                              Length    => 2),
               Item     => Tmp,
               Start_Pt => 1,
               Stop     => Dummy);
            if Tmp in Seconds_T then
               Time.Seconds := Seconds_T'(Tmp);
            else
               Success := False;
            end if;
         end if;
      else
         Success := False;
      end if;
   end Parse_Time;

   function Compare_Timestamps (Timestamp_A : E_Strings.T;
                                Timestamp_B : E_Strings.T) return Timestamp_Comparison_Result_T is
      --  This can be used to loop over all fields in Time_T, above.
      type One_To_Six is range 1 .. 6;

      Time_A, Time_B : Time_T;
      Success        : Boolean;
      Result         : Timestamp_Comparison_Result_T;
      Tmp_A, Tmp_B   : Natural;
   begin
      Result := Malformed_Timestamps;
      Parse_Time (Timestamp_A, Success, Time_A);
      if Success then
         Parse_Time (Timestamp_B, Success, Time_B);
         if Success then
            Result := A_Equals_B;
            for I in One_To_Six loop
               case I is
                  when 1 =>
                     Tmp_A := Time_A.Year;
                     Tmp_B := Time_B.Year;
                  when 2 =>
                     Tmp_A := Time_A.Month;
                     Tmp_B := Time_B.Month;
                  when 3 =>
                     Tmp_A := Time_A.Day;
                     Tmp_B := Time_B.Day;
                  when 4 =>
                     Tmp_A := Time_A.Hour;
                     Tmp_B := Time_B.Hour;
                  when 5 =>
                     Tmp_A := Time_A.Minutes;
                     Tmp_B := Time_B.Minutes;
                  when 6 =>
                     Tmp_A := Time_A.Seconds;
                     Tmp_B := Time_B.Seconds;
               end case;
               --# assert Result = A_Equals_B;
               if Tmp_A > Tmp_B then
                  Result := A_Greater_Than_B;
               elsif Tmp_A < Tmp_B then
                  Result := A_Less_Than_B;
               end if;
               exit when Result /= A_Equals_B;
            end loop;
         end if;
      end if;
      return Result;
   end Compare_Timestamps;

end Date_Time;
