--  Abstract :
--
--  See spec.
--
--  Copyright (C) 2018 - 2022 Free Software Foundation, Inc.
--
--  This library 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. This library is distributed in the hope that it will be useful,
--  but WITHOUT ANY WARRANTY;  without even the implied warranty of MERCHAN-
--  TABILITY or FITNESS FOR A PARTICULAR PURPOSE.

--  As a special exception under Section 7 of GPL version 3, you are granted
--  additional permissions described in the GCC Runtime Library Exception,
--  version 3.1, as published by the Free Software Foundation.

pragma License (Modified_GPL);

with Ada.Directories;
with Ada.Strings.Fixed;
with Ada.Text_IO;
package body WisiToken.Generate is

   function Error_Message
     (File_Name : in String;
      File_Line : in Line_Number_Type;
      Message   : in String;
      Warning   : in Boolean := False)
     return String
   is
      use Ada.Directories;
      use Ada.Strings.Fixed;
      use Ada.Strings;
   begin
      return Simple_Name (File_Name) & ":" &
        Trim (Line_Number_Type'Image (File_Line), Left) & ":1: " &
        (if Warning then "warning: " else "error: ") &
        Message;
      --  Column number is 1 origin in Gnu error messages [gnu_coding]
      --  warning/error is not in [gnu_coding], but is consistent with gcc.
   end Error_Message;

   procedure Put_Error (Message : in String)
   is begin
      Error := True;
      Ada.Text_IO.Put_Line (Ada.Text_IO.Standard_Error, Message);
   end Put_Error;

   procedure Put_Warning (Message : in String)
   is begin
      Warning := True;
      Ada.Text_IO.Put_Line (Ada.Text_IO.Standard_Error, Message);
   end Put_Warning;

   procedure Check_Consistent
     (Grammar          : in WisiToken.Productions.Prod_Arrays.Vector;
      Descriptor       : in WisiToken.Descriptor;
      Source_File_Name : in String)
   is begin
      if Descriptor.Accept_ID /= Descriptor.First_Nonterminal then
         Put_Error
           (Error_Message
              (Source_File_Name, Line_Number_Type'First,
               "Descriptor.Accept_ID /= Descriptor.First_Nonterminal"));
      end if;
      if Grammar.First_Index /= Descriptor.First_Nonterminal then
         Put_Error
           (Error_Message
              (Source_File_Name, Line_Number_Type'First,
               "Grammar.First_Index /= Descriptor.First_Nonterminal"));
      end if;
      if Grammar.Last_Index /= Descriptor.Last_Nonterminal then
         Put_Error
           (Error_Message
              (Source_File_Name, Line_Number_Type'First,
               "Grammar.Last_Index /= Descriptor.Last_Nonterminal"));
      end if;

      for Nonterm in Descriptor.First_Nonterminal .. Descriptor.Last_Nonterminal loop
         if Grammar (Nonterm).LHS /= Nonterm then
            Put_Error
              (Error_Message
                 (Source_File_Name, Line_Number_Type'First,
                  "Grammar (" & Image (Nonterm, Descriptor) & ").LHS = " &
                    Image (Grammar (Nonterm).LHS, Descriptor) & " /= " &
                    Image (Nonterm, Descriptor)));
         end if;
      end loop;
   end Check_Consistent;

   function Check_Unused_Tokens
     (Descriptor : in WisiToken.Descriptor;
      Grammar    : in WisiToken.Productions.Prod_Arrays.Vector)
     return Boolean
   is
      subtype Terminals    is Token_ID range Descriptor.First_Terminal    .. Descriptor.Last_Terminal;
      subtype Nonterminals is Token_ID range Descriptor.First_Nonterminal .. Descriptor.Last_Nonterminal;

      Used_Tokens : Token_ID_Set := (Descriptor.First_Terminal .. Descriptor.Last_Nonterminal => False);

      Changed        : Boolean := False;
      Abort_Generate : Boolean := False;
      Unused_Tokens  : Boolean := False;
   begin
      Used_Tokens (Descriptor.Accept_ID) := True;

      --  First mark all nonterminals that occur in used nonterminals as
      --  used.
      loop
         for Prod of Grammar loop
            if Used_Tokens (Prod.LHS) then
               for RHS of Prod.RHSs loop
                  for J of RHS.Tokens loop
                     if J in Nonterminals then
                        Changed         := Changed or else not Used_Tokens (J);
                        Used_Tokens (J) := True;
                     end if;
                  end loop;
               end loop;
            end if;
         end loop;
         exit when not Changed;
         Changed := False;
      end loop;

      --  Now mark terminals used in used nonterminals
      for Prod of Grammar loop
         if Used_Tokens (Prod.LHS) then
            for RHS of Prod.RHSs loop
               for J of RHS.Tokens loop
                  if not (J in Used_Tokens'Range) then
                     WisiToken.Generate.Put_Error
                       ("non-grammar token " & Image (J, Descriptor) & " used in grammar");

                     --  This causes lots of problems with token_id not in terminal or
                     --  nonterminal range, so abort early.
                     Abort_Generate := True;
                  end if;

                  if J in Terminals then
                     Used_Tokens (J) := True;
                  end if;
               end loop;
            end loop;
         end if;
      end loop;

      for I in Used_Tokens'Range loop
         if not Used_Tokens (I) then
            if not Unused_Tokens then
               WisiToken.Generate.Put_Error ("Unused tokens:");
               Unused_Tokens := True;
            end if;
            WisiToken.Generate.Put_Error (Image (I, Descriptor));
         end if;
      end loop;

      if Abort_Generate then
         raise Grammar_Error;
      end if;

      return Unused_Tokens;
   end Check_Unused_Tokens;

   function Nullable (Grammar : in WisiToken.Productions.Prod_Arrays.Vector) return Token_Array_Production_ID
   is
      use all type Ada.Containers.Count_Type;

      subtype Nonterminal is Token_ID range Grammar.First_Index .. Grammar.Last_Index;

      Result  : Token_Array_Production_ID := (Nonterminal => Invalid_Production_ID);
      Changed : Boolean                   := True;
   begin
      loop
         exit when not Changed;
         Changed := False;

         for Prod of Grammar loop
            if Result (Prod.LHS) = Invalid_Production_ID then
               for RHS_Index in Prod.RHSs.First_Index .. Prod.RHSs.Last_Index loop
                  declare
                     RHS : WisiToken.Productions.Right_Hand_Side renames Prod.RHSs (RHS_Index);
                  begin
                     if RHS.Tokens.Length = 0 or else
                       (RHS.Tokens (1) in Nonterminal and then Result (RHS.Tokens (1)) /= Invalid_Production_ID)
                     then
                        Result (Prod.LHS) := (Prod.LHS, RHS_Index);
                        Changed := True;
                     end if;
                  end;
               end loop;
            end if;
         end loop;
      end loop;
      return Result;
   end Nullable;

   function Has_Empty_Production (Nullable : in Token_Array_Production_ID) return Token_ID_Set
   is begin
      return Result : Token_ID_Set := (Nullable'First .. Nullable'Last => False) do
         for I in Result'Range loop
            Result (I) := Nullable (I) /= Invalid_Production_ID;
         end loop;
      end return;
   end Has_Empty_Production;

   function Has_Empty_Production (Grammar : in WisiToken.Productions.Prod_Arrays.Vector) return Token_ID_Set
   is
      use all type Ada.Containers.Count_Type;

      subtype Nonterminal is Token_ID range Grammar.First_Index .. Grammar.Last_Index;

      Result  : Token_ID_Set := (Nonterminal => False);
      Changed : Boolean      := True;
   begin
      loop
         exit when not Changed;
         Changed := False;

         for Prod of Grammar loop
            for RHS of Prod.RHSs loop
               if (RHS.Tokens.Length = 0 or else
                     (RHS.Tokens (1) in Nonterminal and then Result (RHS.Tokens (1)))) and
                 not Result (Prod.LHS)
               then
                  Result (Prod.LHS) := True;
                  Changed := True;
               end if;
            end loop;
         end loop;
      end loop;
      return Result;
   end Has_Empty_Production;

   function First
     (Grammar              : in WisiToken.Productions.Prod_Arrays.Vector;
      Has_Empty_Production : in Token_ID_Set;
      First_Terminal       : in Token_ID;
      Non_Terminal         : in Token_ID)
     return Token_ID_Set
   is
      Search_Tokens : Token_ID_Set := (Grammar.First_Index .. Grammar.Last_Index => False);
   begin
      Search_Tokens (Non_Terminal) := True;

      return Derivations : Token_ID_Set := (First_Terminal .. Grammar.Last_Index => False) do
         while Any (Search_Tokens) loop
            declare
               Added_Tokens   : Token_ID_Set := (First_Terminal .. Grammar.Last_Index      => False);
               Added_Nonterms : Token_ID_Set := (Grammar.First_Index .. Grammar.Last_Index => False);
            begin
               for Prod of Grammar loop
                  if Search_Tokens (Prod.LHS) then
                     for RHS of Prod.RHSs loop
                        for Derived_Token of RHS.Tokens loop
                           if not Derivations (Derived_Token) then
                              Added_Tokens (Derived_Token) := True;
                              if Derived_Token in Added_Nonterms'Range then
                                 Added_Nonterms (Derived_Token) := True;
                              end if;
                           end if;

                           if Derived_Token in Has_Empty_Production'Range and then
                             Has_Empty_Production (Derived_Token)
                           then
                              null;
                           else
                              exit;
                           end if;
                        end loop;
                     end loop;
                  end if;
               end loop;
               Derivations   := Derivations or Added_Tokens;
               Search_Tokens := Added_Nonterms;
            end;
         end loop;
      end return;
   end First;

   function First
     (Grammar              : in WisiToken.Productions.Prod_Arrays.Vector;
      Has_Empty_Production : in Token_ID_Set;
      First_Terminal       : in Token_ID)
     return Token_Array_Token_Set
   is
      Matrix : Token_Array_Token_Set :=
        (Grammar.First_Index .. Grammar.Last_Index =>
           (First_Terminal .. Grammar.Last_Index => False));

      procedure Set_Slice (Matrix : in out Token_Array_Token_Set; I : Token_ID; Value : in Token_ID_Set)
      is begin
         for J in Matrix'Range (2) loop
            Matrix (I, J) := Value (J);
         end loop;
      end Set_Slice;

   begin
      for NT_Index in Matrix'Range loop
         Set_Slice (Matrix, NT_Index, First (Grammar, Has_Empty_Production, First_Terminal, NT_Index));
      end loop;

      return Matrix;
   end First;

   function To_Terminal_Sequence_Array
     (First      : in Token_Array_Token_Set;
      Descriptor : in WisiToken.Descriptor)
     return Token_Sequence_Arrays.Vector
   is
      subtype Terminal is Token_ID range Descriptor.First_Terminal .. Descriptor.Last_Terminal;
   begin
      return Result : Token_Sequence_Arrays.Vector do
         Result.Set_First_Last (First'First (1), First'Last (1));

         for I in First'Range (1) loop
            declare
               Row : Token_ID_Arrays.Vector renames Result (I);
            begin
               for J in First'Range (2) loop
                  if First (I, J) and then J in Terminal then
                     Row.Append (J);
                  end if;
               end loop;
            end;
         end loop;
      end return;
   end To_Terminal_Sequence_Array;

   function Follow
     (Grammar              : in WisiToken.Productions.Prod_Arrays.Vector;
      Descriptor           : in WisiToken.Descriptor;
      First                : in Token_Array_Token_Set;
      Has_Empty_Production : in Token_ID_Set)
     return Token_Array_Token_Set
   is
      subtype Terminal    is Token_ID range Descriptor.First_Terminal    .. Descriptor.Last_Terminal;
      subtype Nonterminal is Token_ID range Descriptor.First_Nonterminal .. Descriptor.Last_Nonterminal;

      Prev_Result : Token_Array_Token_Set := (Nonterminal => (Terminal => False));
      Result      : Token_Array_Token_Set := (Nonterminal => (Terminal => False));

      ID : Token_ID;
   begin
      --  [dragon] pgp 189:
      --
      --  Rule 1 Follow (S, EOF) = True; EOF is explicit in the
      --  start symbol production, so this is covered by Rule 2.
      --
      --  Rule 2: If A => alpha B Beta, add First (Beta) to Follow (B)
      --
      --  Rule 3; if A => alpha B, or A -> alpha B Beta and Beta
      --  can be null, add Follow (A) to Follow (B)
      --
      --  We don't assume any order in the productions list, so we
      --  have to keep applying rule 3 until nothing changes.

      for B in Nonterminal loop
         for Prod of Grammar loop
            for A of Prod.RHSs loop
               for I in A.Tokens.First_Index .. A.Tokens.Last_Index loop
                  if A.Tokens (I) = B then
                     if I < A.Tokens.Last_Index then
                        --  Rule 1
                        ID := A.Tokens (1 + I);
                        if ID in Terminal then
                           Result (B, ID) := True;
                        else
                           Or_Slice (Result, B, Slice (First, ID));
                        end if;
                     end if;
                  end if;
               end loop;
            end loop;
         end loop;
      end loop;

      Prev_Result := Result;
      loop
         for B in Nonterminal loop
            for Prod of Grammar loop
               for A of Prod.RHSs loop
                  for I in A.Tokens.First_Index .. A.Tokens.Last_Index loop
                     if A.Tokens (I) = B then
                        if I = A.Tokens.Last_Index or else
                          (A.Tokens (1 + I) in Nonterminal and then
                             Has_Empty_Production (A.Tokens (1 + I)))
                        then
                           --  rule 3
                           Or_Slice (Result, B, Slice (Result, Prod.LHS));
                        end if;
                     end if;
                  end loop;
               end loop;
            end loop;
         end loop;

         exit when Prev_Result = Result;
         Prev_Result := Result;
      end loop;
      return Result;
   end Follow;

   function To_Graph (Grammar : in WisiToken.Productions.Prod_Arrays.Vector) return Grammar_Graphs.Graph
   is
      subtype Nonterminals is Token_ID range Grammar.First_Index .. Grammar.Last_Index;
      Graph : Grammar_Graphs.Graph;
      J     : Integer := 1;
   begin
      if Trace_Generate_Minimal_Complete > Outline then
         Ada.Text_IO.Put_Line ("grammar graph:");
      end if;

      for LHS in Grammar.First_Index .. Grammar.Last_Index loop
         declare
            Prod : WisiToken.Productions.Instance renames Grammar (LHS);
         begin
            for RHS in Prod.RHSs.First_Index .. Prod.RHSs.Last_Index loop
               declare
                  Tokens : Token_ID_Arrays.Vector renames Prod.RHSs (RHS).Tokens;
               begin
                  for I in Tokens.First_Index .. Tokens.Last_Index loop
                     if Tokens (I) in Nonterminals then
                        if Trace_Generate_Minimal_Complete > Detail then
                           Ada.Text_IO.Put_Line
                             ("(" & Trimmed_Image (LHS) & ", " & Trimmed_Image (Tokens (I)) & ","  & J'Image & ")");
                           J := J + 1;
                        end if;
                        Graph.Add_Edge (LHS, Tokens (I), (RHS, I));
                     end if;
                  end loop;
               end;
            end loop;
         end;
      end loop;

      if Trace_Generate_Minimal_Complete > Outline then
         Ada.Text_IO.Put_Line ("..." & Graph.Count_Nodes'Image & " nodes" & Graph.Count_Edges'Image & " edges.");
      end if;
      return Graph;
   end To_Graph;

   function Recursion
     (LHS         : in Token_ID;
      Token_Index : in Positive;
      Tokens      : in Token_ID_Arrays.Vector)
     return Recursion_Class
   is begin
      return
        (if Token_Index = Tokens.First_Index then
           (if LHS = Tokens (Tokens.First_Index)
            then Direct_Left
            else Other_Left)
         elsif Token_Index = Tokens.Last_Index then
           (if LHS = Tokens (Tokens.Last_Index)
            then Direct_Right
            else Other_Right)
         else Other);
   end Recursion;

   procedure Set_Grammar_Recursions
     (Recursions : in     WisiToken.Generate.Recursions;
      Grammar    : in out WisiToken.Productions.Prod_Arrays.Vector)
   is begin
      for LHS of Grammar loop
         for RHS of LHS.RHSs loop
            RHS.Recursion.Set_First_Last (RHS.Tokens.First_Index, RHS.Tokens.Last_Index);
         end loop;
      end loop;

      for Path of Recursions.Recursions loop
         declare
            use WisiToken.Productions;
            Previous_Item_LHS : Token_ID :=
              (if Recursions.Full then Path (Path'Last).Vertex else Token_ID'Last);
         begin
            for Item of Path loop
               for Edge of Item.Edges loop
                  declare
                     LHS : constant Token_ID := (if Recursions.Full then Previous_Item_LHS else Item.Vertex);
                     RHS : Right_Hand_Side renames Grammar (LHS).RHSs (Edge.Data.RHS);
                  begin
                     RHS.Recursion (Edge.Data.Token_Index) := Recursion
                       (LHS         => LHS,
                        Token_Index => Edge.Data.Token_Index,
                        Tokens      => RHS.Tokens);
                  end;
               end loop;
               Previous_Item_LHS := Item.Vertex;
            end loop;
         end;
      end loop;
   end Set_Grammar_Recursions;

   function Compute_Full_Recursion
     (Grammar    : in WisiToken.Productions.Prod_Arrays.Vector;
      Descriptor : in WisiToken.Descriptor)
     return Recursions
   is
      Graph : constant Grammar_Graphs.Graph := To_Graph (Grammar);
   begin
      return Result : Recursions :=
        (Full       => True,
         Recursions => Graph.Find_Cycles)
      do
         Grammar_Graphs.Sort_Paths.Sort (Result.Recursions);

         if Trace_Generate_Minimal_Complete > Extra then
            Ada.Text_IO.New_Line;
            Ada.Text_IO.Put_Line ("Productions:");
            WisiToken.Productions.Put (Grammar, Descriptor);
            Ada.Text_IO.New_Line;
            Ada.Text_IO.Put_Line ("full recursions:");
            for I in Result.Recursions.First_Index .. Result.Recursions.Last_Index loop
               Ada.Text_IO.Put_Line (Trimmed_Image (I) & " => " & Grammar_Graphs.Image (Result.Recursions (I)));
            end loop;
         end if;
      end return;
   end Compute_Full_Recursion;

   function Compute_Partial_Recursion
     (Grammar    : in WisiToken.Productions.Prod_Arrays.Vector;
      Descriptor : in WisiToken.Descriptor)
     return Recursions
   is
      use Grammar_Graphs;
      Graph      : constant Grammar_Graphs.Graph := To_Graph (Grammar);
      Components : constant Component_Lists.List := Strongly_Connected_Components
        (To_Adjancency (Graph), Non_Trivial_Only => True);
      Loops      : constant Vertex_Lists.List    := Graph.Loops;
   begin
      return Result : Recursions do
         Result.Full := False;
         for Comp of Components loop
            declare
               Path : Recursion_Cycle (1 .. Integer (Comp.Length));
               Last : Integer := Path'First - 1;
            begin
               for V of Comp loop
                  Last := Last + 1;
                  Path (Last) := (V, Graph.Edges (V));
               end loop;
               Result.Recursions.Append (Path);
            end;
         end loop;

         declare
            Path : Recursion_Cycle (1 .. Integer (Loops.Length));
            Last : Integer := Path'First - 1;
         begin
            for V of Loops loop
               Last := Last + 1;
               Path (Last) := (V, Graph.Edges (V));
            end loop;
            Result.Recursions.Append (Path);
         end;

         if Trace_Generate_Minimal_Complete > Extra then
            Ada.Text_IO.New_Line;
            Ada.Text_IO.Put_Line ("Productions:");
            WisiToken.Productions.Put (Grammar, Descriptor);
            Ada.Text_IO.New_Line;
            Ada.Text_IO.Put_Line ("partial recursions:");
            for I in Result.Recursions.First_Index .. Result.Recursions.Last_Index loop
               Ada.Text_IO.Put_Line (Trimmed_Image (I) & " => " & Grammar_Graphs.Image (Result.Recursions (I)));
            end loop;
         end if;
      end return;
   end Compute_Partial_Recursion;

   ----------
   --  Indented text output

   procedure Indent_Line (Text : in String)
   is
      use Ada.Text_IO;
   begin
      Set_Col (Indent);
      Put_Line (Text);
      Line_Count := Line_Count + 1;
   end Indent_Line;

   procedure Indent_Start (Text : in String)
   is
      use Ada.Text_IO;
   begin
      Set_Col (Indent);
      Put (Text);
   end Indent_Start;

   procedure Indent_Wrap (Text : in String)
   is
      use all type Ada.Text_IO.Count;
      use Ada.Strings;
      use Ada.Strings.Fixed;
      I     : Natural;
      First : Integer := Text'First;
   begin
      if Text'Length + Indent <= Max_Line_Length then
         Indent_Line (Text);
      else
         loop
            I := Text'Last;
            loop
               I := Index (Text (First .. Text'Last), " ", From => I, Going => Backward);
               exit when I - First + Integer (Indent) <= Max_Line_Length;
               I := I - 1;
            end loop;
            Indent_Line (Trim (Text (First .. I - 1), Right));
            First := I + 1;
            exit when Text'Last - First + Integer (Indent) <= Max_Line_Length;
         end loop;
         Indent_Line (Text (First .. Text'Last));
      end if;
   end Indent_Wrap;

   procedure Indent_Wrap_Comment (Text : in String; Comment_Syntax : in String)
   is
      use all type Ada.Text_IO.Count;
      use Ada.Strings;
      use Ada.Strings.Fixed;
      Prefix : constant String := Comment_Syntax & "  ";
      I      : Natural;
      First  : Integer         := Text'First;
   begin
      if Text'Length + Indent <= Max_Line_Length - 4 then
         Indent_Line (Prefix & Text);
      else
         loop
            I := Text'Last;
            loop
               I := Index (Text (First .. Text'Last), " ", From => I, Going => Backward);
               exit when I - First + Integer (Indent) <= Max_Line_Length - 4;
               I := I - 1;
            end loop;
            Indent_Line (Prefix & Text (First .. I - 1));
            First := I + 1;
            exit when Text'Last - First + Integer (Indent) <= Max_Line_Length - 4;
         end loop;
         Indent_Line (Prefix & Text (First .. Text'Last));
      end if;
   end Indent_Wrap_Comment;

end WisiToken.Generate;
