--  Comments table.
--  Copyright (C) 2022 Tristan Gingold
--
--  This program is free software: you can redistribute it and/or modify
--  it under the terms of the GNU General Public License as published by
--  the Free Software Foundation, either version 2 of the License, or
--  (at your option) any later version.
--
--  This program 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
--  along with this program.  If not, see <gnu.org/licenses>.

with Grt.Algos;

with Simple_IO; use Simple_IO;
with Utils_IO; use Utils_IO;

package body File_Comments is
   Flag_Trace : constant Boolean := False;

   Ctxt : Comment_Context;

   procedure Comment_Init_Scan (File : Source_File_Entry) is
   begin
      Ctxt := (File => File,
               State => State_Before,
               Next => No_Comment_Index + 1,
               Last_Node => 0,
               Last_Newline => No_Comment_Index,
               Line_Start => Source_Ptr_Bad);

      --  Create entry for FILE if not already created.
      if Comments_Table.Last < Ctxt.File then
         while Comments_Table.Last < Ctxt.File loop
            Comments_Table.Append (File_Comments_Table'(Table => null,
                                                        Priv => <>));
         end loop;
      end if;

      --  Always reset the table.
      File_Comments_Tables.Init (Comments_Table.Table (Ctxt.File), 16);
   end Comment_Init_Scan;

   procedure Comment_Close_Scan is
   begin
      Ctxt.File := No_Source_File_Entry;
   end Comment_Close_Scan;

   --  Gather last comments to the current node.
   --  Called at the end of a block.
   procedure Comment_Gather_Existing
   is
      Fc : File_Comments_Table renames
        Comments_Table.Table (Ctxt.File);
      Last : constant Comment_Index := File_Comments_Tables.Last (Fc);
   begin
      if Flag_Trace then
         Put ("Comment_Gather_Existing: ");
         Put_Uns32 (Uns32 (Ctxt.Next));
         Put ("..");
         Put_Uns32 (Uns32 (Last));
         Put (" -> ");
         Put_Uns32 (Ctxt.Last_Node);
         New_Line;
      end if;

      for I in Ctxt.Next .. Last loop
         pragma Assert (Fc.Table (I).N = 0);
         Fc.Table (I).N := Ctxt.Last_Node;
      end loop;
      Ctxt.Next := Last + 1;
   end Comment_Gather_Existing;

   function Is_Empty_Line (Line_Start : Source_Ptr) return Boolean
   is
      Fc : File_Comments_Table renames
        Comments_Table.Table (Ctxt.File);
      Last : constant Comment_Index := File_Comments_Tables.Last (Fc);
   begin
      --  The start of the line is after the last comment, so the line is
      --  empty.
      return Line_Start > Fc.Table (Last).Last;
   end Is_Empty_Line;

   --  Very important: this procedure is called only after a comment has
   --  been scanned and added.
   --  So this is either the newline after a comment (in that case
   --   LINE_START is less than the last comment),
   --  or an empty line (in that case LINE_START is greater than the last
   --   comment).
   procedure Comment_Newline (Line_Start : Source_Ptr) is
   begin
      case Ctxt.State is
         when State_Before =>
            --  In before mode, we simply gather all the comments.
            if Is_Empty_Line (Line_Start) then
               --  Keep a marker to the comments up to the last newline.
               Ctxt.Last_Newline :=
                 File_Comments_Tables.Last (Comments_Table.Table (Ctxt.File));
            end if;
         when State_Block =>
            --  Detect empty line.
            --  This can happen only after a comments has been added.
            if Is_Empty_Line (Line_Start) then
               --  Attach existing comments.
               Comment_Gather_Existing;
            end if;
         when State_Line =>
            --  If a comment appear before the newline, the state would be
            --  changed to State_Line_Cont; so here no comment on the same
            --  line.
            --
            --  The following comments will be attached to the next node.
            Ctxt.State := State_Before;
         when State_Line_Cont =>
            --  If the line is empty, change to State_Block.
            --  Otherwise, continue to associate with the last node.
            if Is_Empty_Line (Line_Start) then
               Ctxt.State := State_Block;
            end if;
      end case;
   end Comment_Newline;

   procedure Add_Comment (Start, Last : Source_Ptr;
                          Line_Start : Source_Ptr)
   is
      pragma Assert (Ctxt.File /= No_Source_File_Entry);
      T : File_Comments_Table renames Comments_Table.Table (Ctxt.File);
      N : Uns32;
   begin
      if Flag_Trace then
         Put ("Add_Comment, file=");
         Put_Uns32 (Uns32 (Ctxt.File));
         Put (", start=");
         Put_Uns32 (Uns32 (Start));
         Put ("..");
         Put_Uns32 (Uns32 (Last));
         Put (" => ");
         Put_Uns32 (Uns32 (File_Comments_Tables.Last (T) + 1));
         Put (", state=");
      end if;

      case Ctxt.State is
         when State_Before =>
            --  Will be attached later.
            N := 0;
            if Flag_Trace then
               Put ("before");
            end if;
         when State_Block =>
            --  Will be attached on the next empty line.
            N := 0;
            if Flag_Trace then
               Put ("block");
            end if;
         when State_Line =>
            if Flag_Trace then
               Put ("line");
               Put (" (start=");
               Put_Uns32 (Uns32 (Ctxt.Line_Start));
               Put (", cmt=");
               Put_Uns32 (Uns32 (Line_Start));
               Put (")");
            end if;
            --  Is it on the same line ?
            if Line_Start = Ctxt.Line_Start then
               --  Yes, associate with the last node.
               N := Ctxt.Last_Node;
               Ctxt.Next := File_Comments_Tables.Last (T) + 2;
               --  And continue to associate.
               Ctxt.State := State_Line_Cont;
            else
               --  Not the same line, for the next node.
               N := 0;
               Ctxt.State := State_Before;
            end if;
         when State_Line_Cont =>
            --  Continue to associate with the last node.
            if Flag_Trace then
               Put ("line_cont");
            end if;
            N := Ctxt.Last_Node;
            Ctxt.Next := File_Comments_Tables.Last (T) + 2;
      end case;

      if Flag_Trace then
         Put (", node=");
         Put_Uns32 (N);
         New_Line;
      end if;

      --  Append a comment entry.
      File_Comments_Tables.Append
        (T, Comment_Record'(Start => Start, Last => Last, N => N));
   end Add_Comment;

   procedure Save_Comments (Rng : out Comments_Range)
   is
      use File_Comments_Tables;
      pragma Assert (Ctxt.File /= No_Source_File_Entry);
      Fc : File_Comments_Table renames Comments_Table.Table (Ctxt.File);
   begin
      Rng := (First => Ctxt.Next, Last => Last (Fc));
      Ctxt.Next := Rng.Last + 1;
      Ctxt.State := State_Before;
      Ctxt.Last_Newline := Rng.Last;
   end Save_Comments;

   procedure Gather_Comments_Before (Rng : Comments_Range; N : Uns32)
   is
      pragma Assert (Ctxt.File /= No_Source_File_Entry);
   begin
      if Rng.Last /= No_Comment_Index then
         if Flag_Trace then
            Put ("Gather_Comments_Before, file=");
            Put_Uns32 (Uns32 (Ctxt.File));
            Put (", rng=");
            Put_Uns32 (Uns32 (Rng.First));
            Put ("..");
            Put_Uns32 (Uns32 (Rng.Last));
            Put (", node=");
            Put_Uns32 (N);
            New_Line;
         end if;

         declare
            Fc : File_Comments_Table renames Comments_Table.Table (Ctxt.File);
         begin
            for I in Rng.First .. Rng.Last loop
               Fc.Table (I).N := N;
            end loop;

            Ctxt.Next := Rng.Last + 1;
         end;
      end if;
   end Gather_Comments_Before;

   procedure Gather_Comments_Block (Rng : Comments_Range; N : Uns32) is
   begin
      Gather_Comments_Before (Rng, N);
      if Ctxt.Last_Newline /= No_Comment_Index then
         --  Comments after RNG and until last_newline are also gathered.
         Gather_Comments_Before ((First => Rng.Last + 1,
                                  Last => Ctxt.Last_Newline), N);
      end if;
      Ctxt.State := State_Block;
      Ctxt.Last_Node := N;
   end Gather_Comments_Block;

   procedure Gather_Comments_Line (Pos : Source_Ptr;
                                   N : Uns32)
   is
      Rng : Comments_Range;
   begin
      --  Previous unassociated comments are associated to the node N.
      Save_Comments (Rng);
      Gather_Comments_Before (Rng, N);

      --  Start Line mode.
      Ctxt.State := State_Line;
      Ctxt.Last_Node := N;
      Ctxt.Line_Start := Pos;
   end Gather_Comments_Line;

   procedure Gather_Comments_End is
   begin
      case Ctxt.State is
         when State_Before =>
            --  Discard unattached comments.
            declare
               Fc : File_Comments_Table renames
                 Comments_Table.Table (Ctxt.File);
               Last : Comment_Index;
            begin
               loop
                  Last := File_Comments_Tables.Last (Fc);
                  exit when Last = No_Comment_Index;
                  exit when Fc.Table (Last).N /= 0;
                  File_Comments_Tables.Decrement_Last (Fc);
               end loop;
            end;
         when State_Block =>
            Comment_Gather_Existing;
         when State_Line
           | State_Line_Cont =>
            --  All comments are attached.
            null;
      end case;
      Ctxt.State := State_Before;
   end Gather_Comments_End;

   procedure Gather_Comments (N : Uns32)
   is
      Rng : Comments_Range;
   begin
      Save_Comments (Rng);
      Gather_Comments_Block (Rng, N);
   end Gather_Comments;

   procedure Sort_Comments_By_Node
   is
      pragma Assert (Ctxt.File /= No_Source_File_Entry);
      Fc : File_Comments_Table renames Comments_Table.Table (Ctxt.File);

      function Lt (L, R : Positive) return Boolean
      is
         Lc : Comment_Record renames Fc.Table (Comment_Index (L));
         Rc : Comment_Record renames Fc.Table (Comment_Index (R));
      begin
         if Lc.N < Rc.N then
            return True;
         elsif Lc.N = Rc.N then
            return Lc.Start < Rc.Start;
         end if;
         return False;
      end Lt;

      procedure Swap (P1 : Positive; P2 : Positive)
      is
         L : Comment_Record renames Fc.Table (Comment_Index (P1));
         R : Comment_Record renames Fc.Table (Comment_Index (P2));
         T : Comment_Record;
      begin
         T := L;
         L := R;
         R := T;
      end Swap;

      procedure Sort is new Grt.Algos.Heap_Sort
        (Lt => Lt, Swap => Swap);
   begin
      Sort (Natural (File_Comments_Tables.Last (Fc)));
   end Sort_Comments_By_Node;

   function Find_First_Comment (File : Source_File_Entry; N : Uns32)
                               return Comment_Index is
   begin
      if Comments_Table.Last < File then
         --  No comments for FILE.
         return No_Comment_Index;
      end if;
      declare
         Fc : File_Comments_Table renames Comments_Table.Table (File);
         Nd : Uns32;
         F, L, M : Comment_Index;
      begin
         F := File_Comments_Tables.First;
         L := File_Comments_Tables.Last (Fc);
         while F <= L loop
            M := F + (L - F) / 2;
            Nd := Fc.Table (M).N;
            if Nd = N then
               --  Found, but must return the first comment.
               while M > No_Comment_Index + 1
                 and then Fc.Table (M - 1).N = N
               loop
                  M := M - 1;
               end loop;
               return M;
            elsif Nd < N then
               F := M + 1;
            else
               pragma Assert (Nd > N);
               L := M - 1;
            end if;
         end loop;
         return No_Comment_Index;
      end;
   end Find_First_Comment;

   procedure Get_Comment (File : Source_File_Entry;
                          Idx : Comment_Index;
                          Start, Last : out Source_Ptr)
   is
      pragma Assert (Comments_Table.Last >= File);
      Fc : File_Comments_Table renames Comments_Table.Table (File);
   begin
      Start := Fc.Table (Idx).Start;
      Last := Fc.Table (Idx).Last;
   end Get_Comment;

   function Get_Comment_Start (File : Source_File_Entry;
                               Idx : Comment_Index) return Source_Ptr
   is
      Start, Last : Source_Ptr;
   begin
      Get_Comment (File, Idx, Start, Last);
      return Start;
   end Get_Comment_Start;

   function Get_Comment_Last (File : Source_File_Entry;
                              Idx : Comment_Index) return Source_Ptr
   is
      Start, Last : Source_Ptr;
   begin
      Get_Comment (File, Idx, Start, Last);
      return Last;
   end Get_Comment_Last;

   function Get_Next_Comment (File : Source_File_Entry; Idx : Comment_Index)
                             return Comment_Index
   is
      use File_Comments_Tables;
      pragma Assert (Comments_Table.Last >= File);
      Fc : File_Comments_Table renames Comments_Table.Table (File);
   begin
      if Idx < Last (Fc)
        and then Fc.Table (Idx + 1).N = Fc.Table (Idx).N
      then
         return Idx + 1;
      else
         return No_Comment_Index;
      end if;
   end Get_Next_Comment;

   procedure Finalize is
   begin
      for I in Comments_Table.First .. Comments_Table.Last loop
         File_Comments_Tables.Free (Comments_Table.Table (I));
      end loop;
      Comments_Table.Free;
   end Finalize;

   procedure Initialize is
   begin
      Comments_Table.Init;
   end Initialize;
end File_Comments;
