-------------------------------------------------------------------------------
-- (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 .PRV file                                            --
--                                                                            --
--Grammar for .PRV files:                                                     --
--PRVfile  ::= {Line}                                                         --
--Line     ::= [VCNumber][Comment]end_of_line_character                       --
--VCNumber ::= digit{digit}                                                   --
--Comment  ::= --{non_end_of_line_character}                                  --
--                                                                            --
--The proof review file (prv) gives the user an opportunity to declare that   --
--a VC has been proved by inspection. The VC numbers proved can be placed one --
--per line with Ada style comments.                                           --
--------------------------------------------------------------------------------

separate (VCS)
procedure AnalyseReviewFile (Report_File : in     SPARK_IO.File_Type;
                             Filename    : in     E_Strings.T;
                             Errors      :    out Review_Errors) is

   -- Each line in the PRV file will be categorised as one of the following:
   -- A VCNumber line will contain a valid VC number and may contain a comment.
   -- A Comment_Line is a line with a comment and nothing else.
   -- Any other line is deemed an Invalid_Line.
   type PRV_File_Line_Type is (VC_Number_Line, Comment_Line, Invalid_Line);

   -- These are the errors that will be reported to the user via the report file.
   type Error is (
                  VC_Syntax_Error,
                  VC_Does_Not_Exist,
                  VC_Proved_By_Examiner,
                  VC_Proved_By_Simplifier,
                  VC_Proved_By_Checker,
                  VC_Duplicated);

   -- The .prv file.
   Review_File        : SPARK_IO.File_Type := SPARK_IO.Null_File;
   Dummy_Close_Status : SPARK_IO.File_Status;
   Open_Status        : SPARK_IO.File_Status;

   Finished_With_File : Boolean;
   Read_Line_Success  : Boolean;
   The_Line_Type      : PRV_File_Line_Type;

   File_Line  : E_Strings.T;
   VC_Name    : E_Strings.T;
   The_Result : E_Strings.T;

   Tmp_Errors : Review_Errors;

   -- Reads the given (non blank) line.
   -- The_Line_Type categorises the line.
   -- The_Result is set to:
   --    o The VC number found on the line if the line is a VC_Number_Line.
   --    o Null string is the line is a Comment_Line.
   --    o The entire line if the line is an Invalid_Line.
   procedure Process_Line (The_Line      : in     E_Strings.T;
                           The_Line_Type :    out PRV_File_Line_Type;
                           The_Result    :    out E_Strings.T)
   --# derives The_Line_Type,
   --#         The_Result    from The_Line;
   is
      subtype Digit is Character range '0' .. '9';
      subtype Two_Char_String_Index is Positive range 1 .. 2;
      subtype Two_Char_String is String (Two_Char_String_Index);

      Comment : constant Two_Char_String := "--";

      Index    : E_Strings.Positions;
      The_Char : Character;
      Finished : Boolean;
   begin
      The_Line_Type := Invalid_Line;
      Index         := 1;
      Finished      := False;

      while not Finished and then Index <= E_Strings.Get_Length (E_Str => The_Line) loop

         The_Char := E_Strings.Get_Element (E_Str => The_Line,
                                            Pos   => Index);

         if The_Char in Digit then
            -- We found a number. Keep looking until a non digit is found.
            The_Line_Type := VC_Number_Line;

         elsif E_Strings.Eq1_String
           -- If the next characters (ignoring spaces) constitute a comment then we're done.
           (E_Str => E_Strings.Section
              (E_Strings.Trim
                 (E_Strings.Section
                    (E_Str     => The_Line,
                     Start_Pos => Index,
                     Length    => (E_Strings.Get_Length (E_Str => The_Line) - Index) + 1)),
               1,
               Comment'Length),
            Str   => Comment) then

            -- This is a comment line unless we already found a VC number in which case
            -- it's a VCNumber line as set above.
            if The_Line_Type /= VC_Number_Line then
               The_Line_Type := Comment_Line;
            end if;
            Finished := True;

         else
            -- This line is invalid as it contains neither a VC number or a comment.
            The_Line_Type := Invalid_Line;
            Finished      := True;
         end if;

         if not Finished then
            Index := Index + 1;
         end if;

      end loop;

      case The_Line_Type is
         when VC_Number_Line =>
            The_Result := E_Strings.Section (E_Str     => The_Line,
                                             Start_Pos => 1,
                                             Length    => Index - 1);
         when Comment_Line =>
            The_Result := E_Strings.Empty_String;
         when Invalid_Line =>
            The_Result := The_Line;
      end case;

   end Process_Line;

   procedure Init_Review_Errors
   --# global out Tmp_Errors;
   --# derives Tmp_Errors from ;
   is
   begin
      Tmp_Errors :=
        Review_Errors'
        (Errors       => False,
         Error_List   => Errors_List'(others => E_Strings.Empty_String),
         Last_Error   => Errors_Index'First,
         Excess_Count => 0);
   end Init_Review_Errors;

   -- Reports The_Error to the report file. The_Details are used to give
   -- further information to help track down the offending line in the file.
   procedure Report_Error (The_Error   : in Error;
                           The_Details : in E_Strings.T)
   --# global in     Report_File;
   --#        in out SPARK_IO.File_Sys;
   --#        in out Tmp_Errors;
   --# derives SPARK_IO.File_Sys from *,
   --#                                Report_File,
   --#                                The_Details,
   --#                                The_Error &
   --#         Tmp_Errors        from *,
   --#                                The_Details,
   --#                                The_Error;
   is

      -- The max number of characters output for a line containing a syntax error.
      Max_Error_Length : constant := 40;

      procedure Record_Error (The_Error   : in Error;
                              The_Details : in E_Strings.T)
      --# global in out Tmp_Errors;
      --# derives Tmp_Errors from *,
      --#                         The_Details,
      --#                         The_Error;
      is

         Tmp_Message : E_Strings.T;

         procedure Add_Review_Error (Err : in E_Strings.T)
         --# global in out Tmp_Errors;
         --# derives Tmp_Errors from *,
         --#                         Err;
         is
         begin

            -- Is this the first time round?
            if not Tmp_Errors.Errors then
               Tmp_Errors.Last_Error := Errors_Index'First;
               Tmp_Errors.Errors     := True;  -- Close the guard.
            end if;

            -- Check if there is any room left
            if Tmp_Errors.Last_Error = Errors_Index'Last then
               -- FULL: increment the ExcessCount
               Tmp_Errors.Excess_Count := Tmp_Errors.Excess_Count + 1;
            else -- Otherwise, increment the end-of-list point
               Tmp_Errors.Last_Error := Tmp_Errors.Last_Error + 1;
            end if;

            if Tmp_Errors.Excess_Count = 0 then  -- There was some room left
               Tmp_Errors.Error_List (Tmp_Errors.Last_Error) := Err;  -- Store the error.
            end if;

         end Add_Review_Error;

      begin
         case The_Error is
            when VC_Syntax_Error =>
               Tmp_Message := E_Strings.Copy_String (Str => "Syntax error: ");
               if E_Strings.Get_Length (E_Str => The_Details) > Max_Error_Length then
                  E_Strings.Append_Examiner_String
                    (E_Str1 => Tmp_Message,
                     E_Str2 => E_Strings.Section (The_Details, E_Strings.Positions'First, Max_Error_Length));
               else
                  E_Strings.Append_Examiner_String (E_Str1 => Tmp_Message,
                                                    E_Str2 => The_Details);
               end if;

            when VC_Does_Not_Exist =>
               Tmp_Message := E_Strings.Copy_String (Str => "Warning: VC ");
               E_Strings.Append_Examiner_String (E_Str1 => Tmp_Message,
                                                 E_Str2 => The_Details);
               E_Strings.Append_String (E_Str => Tmp_Message,
                                        Str   => " not recognised");

            when VC_Proved_By_Examiner =>
               Tmp_Message := E_Strings.Copy_String (Str => "Warning: VC ");
               E_Strings.Append_Examiner_String (E_Str1 => Tmp_Message,
                                                 E_Str2 => The_Details);
               E_Strings.Append_String (E_Str => Tmp_Message,
                                        Str   => " has been proved by the examiner");

            when VC_Proved_By_Simplifier =>
               Tmp_Message := E_Strings.Copy_String (Str => "Warning: VC ");
               E_Strings.Append_Examiner_String (E_Str1 => Tmp_Message,
                                                 E_Str2 => The_Details);
               E_Strings.Append_String (E_Str => Tmp_Message,
                                        Str   => " has been proved by the simplifier");

            when VC_Proved_By_Checker =>
               Tmp_Message := E_Strings.Copy_String (Str => "Warning: VC ");
               E_Strings.Append_Examiner_String (E_Str1 => Tmp_Message,
                                                 E_Str2 => The_Details);
               E_Strings.Append_String (E_Str => Tmp_Message,
                                        Str   => " has been proved by the checker");

            when VC_Duplicated =>
               Tmp_Message := E_Strings.Copy_String (Str => "Warning: VC ");
               E_Strings.Append_Examiner_String (E_Str1 => Tmp_Message,
                                                 E_Str2 => The_Details);
               E_Strings.Append_String (E_Str => Tmp_Message,
                                        Str   => " has been duplicated");
         end case;

         Add_Review_Error (Err => Tmp_Message);

      end Record_Error;

   begin
      Record_Error (The_Error   => The_Error,
                    The_Details => The_Details);

      case The_Error is

         when VC_Syntax_Error =>
            SPARK_IO.Put_String
              (Report_File,
               "*** Error: The following line in the proof review file contains a syntax error: ***",
               0);
            SPARK_IO.New_Line (Report_File, 1);
            SPARK_IO.Put_String (Report_File, "***        """, 0);
            if E_Strings.Get_Length (E_Str => The_Details) > Max_Error_Length then
               E_Strings.Put_String
                 (File  => Report_File,
                  E_Str => E_Strings.Section (E_Str     => The_Details,
                                              Start_Pos => 1,
                                              Length    => Max_Error_Length));
               SPARK_IO.Put_String (Report_File, "...", 0);
            else
               E_Strings.Put_String (File  => Report_File,
                                     E_Str => The_Details);
            end if;
            SPARK_IO.Put_String (Report_File, """", 0);
            SPARK_IO.New_Line (Report_File, 1);

         when VC_Does_Not_Exist =>
            SPARK_IO.Put_String (Report_File, "*** Warning: VC ", 0);
            E_Strings.Put_String (File  => Report_File,
                                  E_Str => The_Details);
            SPARK_IO.Put_String (Report_File, " in proof review file is not recognised ***", 0);
            SPARK_IO.New_Line (Report_File, 1);

         when VC_Proved_By_Examiner =>
            SPARK_IO.Put_String (Report_File, "*** Warning: VC ", 0);
            E_Strings.Put_String (File  => Report_File,
                                  E_Str => The_Details);
            SPARK_IO.Put_String (Report_File, " in proof review file has been proved by the examiner ***", 0);
            SPARK_IO.New_Line (Report_File, 1);

         when VC_Proved_By_Simplifier =>
            SPARK_IO.Put_String (Report_File, "*** Warning: VC ", 0);
            E_Strings.Put_String (File  => Report_File,
                                  E_Str => The_Details);
            SPARK_IO.Put_String (Report_File, " in proof review file has been proved by the simplifier ***", 0);
            SPARK_IO.New_Line (Report_File, 1);

         when VC_Proved_By_Checker =>
            SPARK_IO.Put_String (Report_File, "*** Warning: VC ", 0);
            E_Strings.Put_String (File  => Report_File,
                                  E_Str => The_Details);
            SPARK_IO.Put_String (Report_File, " in proof review file has been proved by the checker ***", 0);
            SPARK_IO.New_Line (Report_File, 1);

         when VC_Duplicated =>
            SPARK_IO.Put_String (Report_File, "*** Warning: VC ", 0);
            E_Strings.Put_String (File  => Report_File,
                                  E_Str => The_Details);
            SPARK_IO.Put_String (Report_File, " in proof review file has been duplicated ***", 0);
            SPARK_IO.New_Line (Report_File, 1);
      end case;
   end Report_Error;

begin -- AnalyseReviewFile

   -- open review file
   E_Strings.Open
     (File         => Review_File,
      Mode_Of_File => SPARK_IO.In_File,
      Name_Of_File => Filename,
      Form_Of_File => "",
      Status       => Open_Status);

   -- Initialise the error record stack
   Init_Review_Errors;

   if Open_Status /= SPARK_IO.Ok then
      FatalErrors.Process (FatalErrors.Could_Not_Open_Input_File, E_Strings.Empty_String);
   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      => Review_File,
                             Success   => Read_Line_Success,
                             File_Line => File_Line);

   if Read_Line_Success then

      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

         -- Get the first token on the line and act accordingly
         Process_Line (The_Line      => File_Line,
                       The_Line_Type => The_Line_Type,
                       The_Result    => The_Result);

         case The_Line_Type is

            when VC_Number_Line =>

               VC_Name := VCHeap.Get_VC_Name_Prefix;
               E_Strings.Append_String (E_Str => VC_Name,
                                        Str   => "_");
               E_Strings.Append_Examiner_String (E_Str1 => VC_Name,
                                                 E_Str2 => The_Result);

               if not VCHeap.Exists (VC_Name) then

                  Report_Error (The_Error   => VC_Does_Not_Exist,
                                The_Details => The_Result);

               elsif VCHeap.Get_VC_State (VC_Name) = VCDetails.VC_Proved_By_Examiner then
                  Report_Error (The_Error   => VC_Proved_By_Examiner,
                                The_Details => The_Result);

               elsif VCHeap.Get_VC_State (VC_Name) = VCDetails.VC_Proved_By_Inference or
                 VCHeap.Get_VC_State (VC_Name) = VCDetails.VC_Proved_By_Contradiction or
                 VCHeap.Get_VC_State (VC_Name) = VCDetails.VC_Proved_Using_User_Proof_Rules then
                  Report_Error (The_Error   => VC_Proved_By_Simplifier,
                                The_Details => The_Result);

               elsif VCHeap.Get_VC_State (VC_Name) = VCDetails.VC_Proved_By_Checker then
                  Report_Error (The_Error   => VC_Proved_By_Checker,
                                The_Details => The_Result);

               elsif VCHeap.Get_VC_State (VC_Name) = VCDetails.VC_Proved_By_Review then
                  Report_Error (The_Error   => VC_Duplicated,
                                The_Details => The_Result);

               else
                  -- Mark VC as proved by review
                  VCHeap.Set_VC_State (VC_Name, VCDetails.VC_Proved_By_Review);

               end if;

            when Comment_Line =>
               null;

            when Invalid_Line =>

               Report_Error (The_Error   => VC_Syntax_Error,
                             The_Details => The_Result);

         end case;

         if not Finished_With_File then
            -- Read next line
            Read_Next_Non_Blank_Line (File      => Review_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 (Review_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;

   else
      SPARK_IO.Put_Line (SPARK_IO.Standard_Output, "************* Review file empty ************", 0);
      SPARK_IO.New_Line (SPARK_IO.Standard_Output, 1);

   end if;

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

   Errors := Tmp_Errors;

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