-------------------------------------------------------------------------------
-- (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.
--
--=============================================================================

--------------------------------------------------------------------------------
--Synopsis:                                                                   --
--                                                                            --
--Procedure to analyse a .PLG file                                            --
--                                                                            --
--------------------------------------------------------------------------------

separate (VCS)
procedure AnalyseProofLogFile
  (Report_File        : in     SPARK_IO.File_Type;
   Filename           : in     E_Strings.T;
   SIV_File_Date_Time : in     E_Strings.T;
   Error_In_File      :    out Boolean)
is

   Dummy_Close_Status               : SPARK_IO.File_Status;
   File_Line                        : E_Strings.T;
   Finished_With_File               : Boolean;
   Open_Status                      : SPARK_IO.File_Status;
   Read_Line_Success                : Boolean;
   Proof_Log_File                   : SPARK_IO.File_Type := SPARK_IO.Null_File;
   VC_Proof_Date_Time_From_PLG_File : E_Strings.T;
   Proof_Log_Obsolete               : Boolean;
   Trimmed_Line                     : E_Strings.T;
   Current_VC_Name                  : E_Strings.T;

   procedure Extract_Dates_And_Times_From_Proof_Log_File
     (Proof_Log_File      : in     SPARK_IO.File_Type;
      Proof_Log_Date_Time :    out E_Strings.T)
   --# global in out SPARK_IO.File_Sys;
   --# derives Proof_Log_Date_Time,
   --#         SPARK_IO.File_Sys   from Proof_Log_File,
   --#                                  SPARK_IO.File_Sys;
   is
      File_Line       : E_Strings.T;
      Trimmed_Line    : E_Strings.T;
      Proof_Date_Time : E_Strings.T;
      Start_Found     : Boolean := False;
   begin
      Proof_Log_Date_Time := E_Strings.Empty_String;

      while not Start_Found loop

         E_Strings.Get_Line (File  => Proof_Log_File,
                             E_Str => File_Line);
         Trimmed_Line := E_Strings.Trim (File_Line);

         -- find date
         if E_Strings.Eq1_String (E_Str => E_Strings.Section (Trimmed_Line, 1, 4),
                                  Str   => "DATE") then
            -- extract the proof session date and time from the string
            Proof_Date_Time :=
              E_Strings.Section (Trimmed_Line, PLG_File_VC_Proof_Date_Start_Column, PLG_File_VC_Proof_Date_Length);
            E_Strings.Append_String (E_Str => Proof_Date_Time,
                                     Str   => " ");
            E_Strings.Append_Examiner_String
              (E_Str1 => Proof_Date_Time,
               E_Str2 => E_Strings.Section (Trimmed_Line, PLG_File_VC_Proof_Time_Start_Column, PLG_File_VC_Proof_Time_Length));

            Proof_Log_Date_Time := Proof_Date_Time;
         end if;

         -- find start of instructions, then go on to analyse the rest of the file
         if E_Strings.Eq1_String (E_Str => E_Strings.Section (Trimmed_Line, 1, 7),
                                  Str   => "COMMAND") then
            Start_Found := True;
         end if;

      end loop;

      -- if date has not been found must be in plain output mode
      if E_Strings.Is_Empty (E_Str => Proof_Log_Date_Time) then
         Proof_Log_Date_Time := E_Strings.Copy_String (Str => "Unknown date");
      end if;
   end Extract_Dates_And_Times_From_Proof_Log_File;

   -------------------------------------------------------------------------

   procedure Check_Proof_Log_Obsolescence
     (Proof_Log_Date_Time : in     E_Strings.T;
      SIV_File_Date_Time  : in     E_Strings.T;
      PLG_Obsolete        :    out Boolean)
   --# global in     CommandLine.Data;
   --#        in out SPARK_IO.File_Sys;
   --# derives PLG_Obsolete      from CommandLine.Data,
   --#                                Proof_Log_Date_Time,
   --#                                SIV_File_Date_Time &
   --#         SPARK_IO.File_Sys from *,
   --#                                Proof_Log_Date_Time,
   --#                                SIV_File_Date_Time;
   is

      Result         : Boolean;
      PLG_Time_Stamp : Integer;
      SIV_Time_Stamp : Integer;
      PLG_Date_Stamp : SPARK_Calendar.Time;
      SIV_Date_Stamp : SPARK_Calendar.Time;
      PLG_Error      : Boolean;
      SIV_Error      : Boolean;

      procedure Get_Date_And_Time
        (Date_String : in     E_Strings.T;
         Date_Stamp  :    out SPARK_Calendar.Time;
         Time_Stamp  :    out Integer;
         Error       :    out Boolean)
      --# derives Date_Stamp,
      --#         Error,
      --#         Time_Stamp from Date_String;
      is
         subtype Month_Index is Integer range 0 .. SPARK_Calendar.Month_Number'Last;

         Year_Num        : SPARK_Calendar.Year_Number;
         Raw_Month_Num   : Month_Index;
         Month_Num       : SPARK_Calendar.Month_Number;
         Day_Num         : SPARK_Calendar.Day_Number;
         Hours_Num       : Integer;
         Minutes_Num     : Integer;
         Seconds_Num     : Integer;
         Time_In_Seconds : Integer;
         Stamp           : SPARK_Calendar.Time;
         Stop            : Natural;
         Time_Error      : SPARK_Calendar.Error_Code;

         function Month_Name_To_Month_Num (Month_Name : E_Strings.T) return Month_Index is
            Num : Month_Index := 0;
         begin
            if E_Strings.Eq1_String (E_Str => Month_Name,
                                     Str   => "JAN") then
               Num := 1;
            elsif E_Strings.Eq1_String (E_Str => Month_Name,
                                        Str   => "FEB") then
               Num := 2;
            elsif E_Strings.Eq1_String (E_Str => Month_Name,
                                        Str   => "MAR") then
               Num := 3;
            elsif E_Strings.Eq1_String (E_Str => Month_Name,
                                        Str   => "APR") then
               Num := 4;
            elsif E_Strings.Eq1_String (E_Str => Month_Name,
                                        Str   => "MAY") then
               Num := 5;
            elsif E_Strings.Eq1_String (E_Str => Month_Name,
                                        Str   => "JUN") then
               Num := 6;
            elsif E_Strings.Eq1_String (E_Str => Month_Name,
                                        Str   => "JUL") then
               Num := 7;
            elsif E_Strings.Eq1_String (E_Str => Month_Name,
                                        Str   => "AUG") then
               Num := 8;
            elsif E_Strings.Eq1_String (E_Str => Month_Name,
                                        Str   => "SEP") then
               Num := 9;
            elsif E_Strings.Eq1_String (E_Str => Month_Name,
                                        Str   => "OCT") then
               Num := 10;
            elsif E_Strings.Eq1_String (E_Str => Month_Name,
                                        Str   => "NOV") then
               Num := 11;
            elsif E_Strings.Eq1_String (E_Str => Month_Name,
                                        Str   => "DEC") then
               Num := 12;
            end if;

            return Num;
         end Month_Name_To_Month_Num;

      begin --Get_Date_And_Time

         -- If the provided date string is empty, do not attempt to parse it, and
         -- return an error.  Otherwise constraint errors are raised in trying to
         -- convert an empty string into an integer value.
         -- Note this check does not exclude all situations that may raise a
         -- constraint error. To do this, would need to check that the string
         -- contains numeric characters in the correct locations.
         if E_Strings.Is_Empty (E_Str => Date_String) then
            Error      := True;
            Time_Stamp := 0;
         else
            --# accept F, 10, Stop, "Stop unused here";
            E_Strings.Get_Int_From_String
              (Source   => E_Strings.Section (E_Str     => Date_String,
                                              Start_Pos => 8,
                                              Length    => 4),
               Item     => Year_Num,
               Start_Pt => 1,
               Stop     => Stop);
            --# end accept;

            Raw_Month_Num := Month_Name_To_Month_Num (Month_Name => E_Strings.Section (Date_String, 4, 3));

            if Raw_Month_Num = 0 then
               Error      := True;
               Time_Stamp := 0;
            else
               Month_Num := Raw_Month_Num;
               --# accept F, 10, Stop, "Stop unused here";
               E_Strings.Get_Int_From_String
                 (Source   => E_Strings.Section (E_Str     => Date_String,
                                                 Start_Pos => 1,
                                                 Length    => 2),
                  Item     => Day_Num,
                  Start_Pt => 1,
                  Stop     => Stop);
               --# end accept;
               SPARK_Calendar.Time_Of (Year_Num, Month_Num, Day_Num, Stamp, Time_Error);

               if not (Time_Error = SPARK_Calendar.Valid) then
                  Error      := True;
                  Time_Stamp := 0;
               else
                  --# accept F, 10, Stop, "Stop unused here";
                  E_Strings.Get_Int_From_String
                    (Source   => E_Strings.Section (E_Str     => Date_String,
                                                    Start_Pos => 13,
                                                    Length    => 2),
                     Item     => Hours_Num,
                     Start_Pt => 1,
                     Stop     => Stop);

                  E_Strings.Get_Int_From_String
                    (Source   => E_Strings.Section (E_Str     => Date_String,
                                                    Start_Pos => 16,
                                                    Length    => 2),
                     Item     => Minutes_Num,
                     Start_Pt => 1,
                     Stop     => Stop);

                  E_Strings.Get_Int_From_String
                    (Source   => E_Strings.Section (E_Str     => Date_String,
                                                    Start_Pos => 19,
                                                    Length    => 2),
                     Item     => Seconds_Num,
                     Start_Pt => 1,
                     Stop     => Stop);
                  --# end accept;
                  Time_In_Seconds := (Seconds_Num + (Minutes_Num * 60)) + (Hours_Num * 3600);

                  Date_Stamp := Stamp;
                  Time_Stamp := Time_In_Seconds;
                  Error      := False;
               end if;
            end if;
         end if;
         --# accept F, 33, Stop, "Stop unused here" &
         --#        F, 602, Date_Stamp, Date_Stamp, "Always well-defined when no error";
      end Get_Date_And_Time;

   begin

      if E_Strings.Eq1_String (E_Str => Proof_Log_Date_Time,
                               Str   => "Unknown date") then

         -- If the /i option is specified then the absence of date is not a problem and we
         -- can assume that the file is valid.
         -- If the /i option is not specified then dates are assumed to be important and a
         -- file that lacks a date is therefore obsolete.
         Result := CommandLine.Data.IgnoreDates;
      else
         Get_Date_And_Time
           (Date_String => Proof_Log_Date_Time,
            Date_Stamp  => PLG_Date_Stamp,
            Time_Stamp  => PLG_Time_Stamp,
            Error       => PLG_Error);
         Get_Date_And_Time
           (Date_String => SIV_File_Date_Time,
            Date_Stamp  => SIV_Date_Stamp,
            Time_Stamp  => SIV_Time_Stamp,
            Error       => SIV_Error);
         if PLG_Error then
            SPARK_IO.Put_Line (SPARK_IO.Standard_Output, "Date format error in PLG file", 0);
            Result := True;
         elsif SIV_Error then
            SPARK_IO.Put_Line (SPARK_IO.Standard_Output, "Date format error in SIV file", 0);
            Result := True;
         elsif SPARK_Calendar.LT (SIV_Date_Stamp, PLG_Date_Stamp) then
            Result := False;
         elsif SPARK_Calendar.GT (SIV_Date_Stamp, PLG_Date_Stamp) then
            Result := True;
         elsif PLG_Time_Stamp < SIV_Time_Stamp then
            Result := True;
         else
            Result := False;
         end if;

      end if;

      PLG_Obsolete := Result;

   end Check_Proof_Log_Obsolescence;

   --------------------------------------------------------------------------
begin -- AnalyseProofLogFile

   -- open proof log file
   E_Strings.Open
     (File         => Proof_Log_File,
      Mode_Of_File => SPARK_IO.In_File,
      Name_Of_File => Filename,
      Form_Of_File => "",
      Status       => Open_Status);
   if Open_Status /= SPARK_IO.Ok then
      FatalErrors.Process (FatalErrors.Could_Not_Open_Input_File, E_Strings.Empty_String);
   end if;

   Extract_Dates_And_Times_From_Proof_Log_File
     (Proof_Log_File      => Proof_Log_File,
      Proof_Log_Date_Time => VC_Proof_Date_Time_From_PLG_File);

   --  If the SIV file has a Unknown date/time arising from /plain,
   --  then assume the proof log is obsolete, UNLESS we're running
   --  POGS with /i, so...

   if E_Strings.Eq1_String (E_Str => SIV_File_Date_Time,
                            Str   => Unknown_SIV_Date) then
      Proof_Log_Obsolete := True;
   else
      Check_Proof_Log_Obsolescence
        (Proof_Log_Date_Time => VC_Proof_Date_Time_From_PLG_File,
         SIV_File_Date_Time  => SIV_File_Date_Time,
         PLG_Obsolete        => Proof_Log_Obsolete);
   end if;

   if Proof_Log_Obsolete and not CommandLine.Data.IgnoreDates then

      SPARK_IO.New_Line (Report_File, 1);
      SPARK_IO.Put_Line (Report_File, "*** Warning: Proof Log file out of date ***", 0);
      SPARK_IO.Put_String (Report_File, "SIV file time stamp: ", 0);
      E_Strings.Put_Line (File  => Report_File,
                          E_Str => SIV_File_Date_Time);
      SPARK_IO.Put_String (Report_File, "PLG file time stamp: ", 0);
      E_Strings.Put_Line (File  => Report_File,
                          E_Str => VC_Proof_Date_Time_From_PLG_File);
      Error_In_File := True;
   else
      SPARK_IO.New_Line (Report_File, 1);

      -- Only output dates if we are not ignoring them.
      if CommandLine.Data.IgnoreDates = False then
         SPARK_IO.Put_String (Report_File, "VCs proved ", 0);
         E_Strings.Put_Line (File  => Report_File,
                             E_Str => VC_Proof_Date_Time_From_PLG_File);
      end if;

      -- find first non blank line
      -- if we get to the end of the file first, flag a fatal error
      Read_Next_Non_Blank_Line (File      => Proof_Log_File,
                                Success   => Read_Line_Success,
                                File_Line => File_Line);

      if not Read_Line_Success then
         SPARK_IO.Put_Line (SPARK_IO.Standard_Output, "************* Proof Log file empty ************", 0);
         SPARK_IO.New_Line (SPARK_IO.Standard_Output, 1);
         Error_In_File := True;
      else
         Error_In_File := False;

         Finished_With_File := False;

         -- process file line-by-line
         -- on entry to the loop there is already a valid line in the
         -- File_Line buffer
         while not Finished_With_File loop
            -- examine line and act accordingly

            if Is_VC_Proof_Success_Line (Line => File_Line) then
               Trimmed_Line    := E_Strings.Trim (File_Line);
               Current_VC_Name :=
                 E_Strings.Section
                 (E_Str     => Trimmed_Line,
                  Start_Pos => 15,
                  Length    => E_Strings.Get_Length (E_Str => Trimmed_Line) - 14);

               -- In a proof log file, it's possible that the "Proof
               -- Success Line" for a single VC can appear multiple times
               -- (if the user types "done" repeatedly after proving a VC for
               -- example), so we only mark the VC as proved here ONCE,
               -- to stop it being accounted for multiple times in the Totals.

               if VCHeap.Get_VC_State (Current_VC_Name) /= VCDetails.VC_Proved_By_Checker then
                  VCHeap.Set_VC_State (Current_VC_Name, VCDetails.VC_Proved_By_Checker);
               end if;

            end if;

            if not Finished_With_File then
               -- read next line
               Read_Next_Non_Blank_Line (File      => Proof_Log_File,
                                         Success   => Read_Line_Success,
                                         File_Line => File_Line);

               -- if unsuccessful then check EOF
               -- and set Finished_With_File accordingly
               if not Read_Line_Success then
                  if SPARK_IO.End_Of_File (Proof_Log_File) then
                     Finished_With_File := True;
                  else
                     FatalErrors.Process (FatalErrors.Problem_Reading_File, E_Strings.Empty_String);
                  end if;
               end if;
            end if;
         end loop;
      end if;
   end if;

   --# accept F, 10, Dummy_Close_Status, "Dummy_Close_Status unused here" &
   --#        F, 10, Proof_Log_File, "Proof_Log_File unused here";
   SPARK_IO.Close (Proof_Log_File, Dummy_Close_Status);
   --# end accept;

   --# accept F, 33, Dummy_Close_Status, "Dummy_Close_Status unused here";
end AnalyseProofLogFile;
