--
--  Copyright (C) 2022-2024, AdaCore
--
--  SPDX-License-Identifier: Apache-2.0 WITH LLVM-Exception
--

with Ada.Strings.Maps.Constants;

with GPR2.Message;
with GPR2.Tree_Internal;
with GPR2.Build.Tree_Db;
with GPR2.Build.View_Db;
with GPR2.View_Internal;

package body GPR2.Build.Compilation_Unit is

   ---------
   -- Add --
   ---------

   procedure Add
     (Self     : in out Object;
      Kind     : Valid_Unit_Kind;
      View     : GPR2.Project.View.Object;
      Path     : GPR2.Path_Name.Object;
      Index    : Unit_Index := No_Index;
      Sep_Name : Optional_Name_Type := "";
      Success  : out Boolean)
   is
      UL : constant Unit_Location :=
             (View   => View,
              Source => Path,
              Index  => Index);
   begin
      Success := False;

      case Kind is
         when S_Spec =>
            if Self.Spec = No_Unit then
               Self.Spec := UL;

               if not Self.Has_Part (S_Body) then
                  Self.Owner := UL.View;
               end if;

               Success := True;
            end if;

         when S_Body =>
            if Self.Implem = No_Unit then
               Self.Implem := UL;
               Self.Owner  := UL.View;
               Success := True;
            end if;

         when S_Separate =>
            declare
               Up : constant String :=
                      Ada.Characters.Handling.To_Upper (String (Sep_Name));
               C  : Separate_Maps.Cursor;
            begin
               Self.Separates.Insert (Name_Type (Up), UL, C, Success);

               if not Self.Owner.Is_Defined then
                  Self.Owner := UL.View;
               end if;
            end;
      end case;

      if not Success then
         Self.Duplicates.Append
           (Clashing_Unit'(Sep_Name'Length, UL, Kind, Sep_Name));
      end if;
   end Add;

   -------------------------
   -- Check_Name_Validity --
   -------------------------

   function Check_Name_Validity
     (Name     : Name_Type;
      Sloc     : Source_Reference.Object'Class;
      As_Error : Boolean := False;
      Messages : in out GPR2.Log.Object) return Boolean
   is
      use Ada.Strings.Maps;

      procedure Error (Message : String);

      procedure Error (Message : String)
      is
      begin
         if Messages.Is_Defined then
            Messages.Append
              (GPR2.Message.Create
                 ((if As_Error then GPR2.Message.Error
                  else GPR2.Message.Information),
                  Message, Sloc));
         end if;
      end Error;

      Not_Valid : constant String :=
                    "invalid name for unit '" & String (Name) & "', ";

   begin
      --  Must start with a letter

      if not Is_In
        (Name (Name'First),
         Constants.Letter_Set or To_Set ("_"))
      then
         Error (Not_Valid & "should start with a letter or an underscore");
         return False;
      end if;

      --  Cannot have dots and underscores one after another and should
      --  contain only alphanumeric characters.

      for K in Name'First + 1 .. Name'Last loop
         declare
            Two_Chars : constant String := String (Name (K - 1 .. K));
         begin
            if Two_Chars = "_." then
               Error (Not_Valid & "cannot contain dot after underscore");
               return False;

            elsif Two_Chars = "__" then
               Error (Not_Valid & "two consecutive underscores not permitted");
               return False;

            elsif Two_Chars = "._" then
               Error (Not_Valid & "cannot contain underscore after dot");
               return False;

            elsif Two_Chars = ".." then
               Error (Not_Valid & "two consecutive dots not permitted");
               return False;

            elsif not Characters.Handling.Is_Alphanumeric (Name (K))
              and then Name (K) not in '.' | '_'
            then
               Error (Not_Valid & "should have only alpha numeric characters");
               return False;
            end if;
         end;
      end loop;

      return True;
   end Check_Name_Validity;

   function Check_Name_Validity
     (Name : Name_Type) return Boolean
   is
      Dead : Log.Object := Log.Undefined;
   begin
      return Check_Name_Validity
        (Name, Source_Reference.Undefined, True, Dead);
   end Check_Name_Validity;

   procedure Check_Name_Validity
     (Self     : Object;
      Messages : in out GPR2.Log.Object)
   is
      Path : GPR2.Path_Name.Object;
      SR   : Source_Reference.Object;
      Dead : Boolean with Unreferenced;
   begin
      if Self.Implem /= No_Unit then
         Path := Self.Implem.Source;
      elsif Self.Spec /= No_Unit then
         Path := Self.Spec.Source;
      elsif not Self.Separates.Is_Empty then
         Path := Self.Separates.First_Element.Source;
      end if;

      if Path.Is_Defined then
         SR := Source_Reference.Object
           (GPR2.Source_Reference.Create (Path.Value, 0, 0));
      end if;

      Dead := Check_Name_Validity
        (Name => Name (Self),
         Sloc     => SR,
         As_Error => True,
         Messages => Messages);
   end Check_Name_Validity;

   ------------
   -- Create --
   ------------

   function Create
     (Name    : Name_Type;
      Context : GPR2.Project.View.Object) return Object
   is
   begin
      return
        (Name      => To_Unbounded_String
                        (Ada.Characters.Handling.To_Upper (String (Name))),
         Root_View => Context,
         others    => <>);
   end Create;

   ---------------------
   -- Dependency_File --
   ---------------------

   function Dependency_File (Self : Object) return Simple_Name
   is
      Tree : constant access GPR2.Tree_Internal.Object :=
               View_Internal.Get_RO (Self.Root_View).Tree;
      Main : constant Unit_Location := Self.Main_Part;
      BN   : constant Simple_Name := Main.Source.Base_Filename;
   begin
      if Main.Index = No_Index then
         return BN & Tree.Dependency_Suffix (Ada_Language);
      else
         declare
            Idx_Img : constant String := Main.Index'Image;
         begin
            return BN & "~" &
              Simple_Name (Idx_Img (Idx_Img'First + 1 .. Idx_Img'Last)) &
              Tree.Dependency_Suffix (Ada_Language);
         end;
      end if;
   end Dependency_File;

   ------------------
   -- For_All_Part --
   ------------------

   procedure For_All_Part
     (Self : Object;
      Action : access procedure
        (Kind     : Unit_Kind;
         View     : Project.View.Object;
         Path     : Path_Name.Object;
         Index    : Unit_Index;
         Sep_Name : Optional_Name_Type)) is
   begin
      if Self.Has_Part (S_Spec) then
         Action
           (S_Spec,
            Self.Spec.View,
            Self.Spec.Source,
            Self.Spec.Index,
            "");
      end if;

      if Self.Has_Part (S_Body) then
         Action
           (S_Body,
            Self.Implem.View,
            Self.Implem.Source,
            Self.Implem.Index,
            "");
      end if;

      for C in Self.Separates.Iterate loop
         declare
            Sep_Name : constant Name_Type :=
                         Separate_Maps.Key (C);
            Unit     : constant Unit_Location :=
                         Separate_Maps.Element (C);
         begin
            Action
              (S_Separate,
               Unit.View,
               Unit.Source,
               Unit.Index,
               Sep_Name);
         end;
      end loop;
   end For_All_Part;

   ---------
   -- Get --
   ---------

   function Get
     (Self     : Object;
      Kind     : Valid_Unit_Kind;
      Sep_Name : Optional_Name_Type := "") return Unit_Location
   is
   begin
      case Kind is
         when S_Spec =>
            return Self.Spec;
         when S_Body =>
            return Self.Implem;
         when S_Separate =>
            return Self.Separates.Element (Sep_Name);
      end case;
   end Get;

   ------------------------
   -- Known_Dependencies --
   ------------------------

   function Known_Dependencies
     (Self      : Object;
      Spec_Only : Boolean := False) return Containers.Name_Set
   is
      procedure Add_Deps (Part : Unit_Location);
      --  Add with clauses from Part

      Result  : Containers.Name_Set;
      Tree_Db : constant Build.Tree_Db.Object_Access :=
                  View_Internal.Get_RO
                    (Self.Root_View).Tree.Artifacts_Database;

      procedure Add_Deps (Part : Unit_Location) is
         Db : constant Build.View_Db.Object :=
                Tree_Db.View_Database (Part.View);
      begin
         Result := Result.Union
           (Db.Source (Part.Source.Simple_Name).Unit
              (Part.Index).Dependencies);
      end Add_Deps;

   begin
      if Self.Spec.Source.Is_Defined then
         Add_Deps (Self.Spec);
      end if;

      if not Spec_Only and then Self.Implem.Source.Is_Defined then
         Add_Deps (Self.Implem);

         for S of Self.Separates loop
            Add_Deps (S);
         end loop;
      end if;

      return Result;
   end Known_Dependencies;

   -----------------
   -- Object_File --
   -----------------

   function Object_File (Self : Object) return Simple_Name
   is
      Tree : constant access GPR2.Tree_Internal.Object :=
               View_Internal.Get_RO (Self.Root_View).Tree;
      Main : constant Unit_Location := Self.Main_Part;
      BN   : constant Simple_Name := Main.Source.Base_Filename;
   begin
      if Main.Index = No_Index then
         return BN & Tree.Object_Suffix (Ada_Language);
      else
         declare
            Idx_Img : constant String := Main.Index'Image;
         begin
            return BN & "~" &
              Simple_Name (Idx_Img (Idx_Img'First + 1 .. Idx_Img'Last)) &
              Tree.Object_Suffix (Ada_Language);
         end;
      end if;
   end Object_File;

   ------------
   -- Remove --
   ------------

   procedure Remove
     (Self     : in out Object;
      Kind     : Valid_Unit_Kind;
      View     : GPR2.Project.View.Object;
      Path     : GPR2.Path_Name.Object;
      Index    : Unit_Index := No_Index;
      Sep_Name : Optional_Name_Type := "")
   is
      UL    : constant Unit_Location :=
                (View   => View,
                 Source => Path,
                 Index  => Index);
      C     : Separate_Maps.Cursor;
      CD    : Duplicates_List.Cursor;
      Found : Boolean := False;

   begin
      case Kind is
         when S_Spec =>
            if Self.Spec = UL then
               Self.Spec := No_Unit;

               if not Self.Has_Part (S_Body) then
                  Self.Owner := Project.View.Undefined;
               end if;

               Found := True;
            end if;

         when S_Body =>
            if Self.Implem = UL then
               Self.Implem := No_Unit;

               if not Self.Has_Part (S_Spec) then
                  Self.Owner := Project.View.Undefined;
               else
                  Self.Owner := Self.Spec.View;
               end if;

               Found := True;
            end if;

         when S_Separate =>
            C := Self.Separates.Find (Sep_Name);

            if not Separate_Maps.Has_Element (C) then
               return;
            end if;

            if Separate_Maps.Element (C) = UL then
               Self.Separates.Delete (C);

               Found := True;
            end if;
      end case;

      if not Found then
         --  no matching unit part: check the list of duplicates
         CD := Self.Duplicates.First;

         while Duplicates_List.Has_Element (CD) loop
            declare
               Elem : constant Duplicates_List.Constant_Reference_Type :=
                        Self.Duplicates.Constant_Reference (CD);
            begin
               if Elem.Kind = Kind
                 and then Elem.Loc = UL
               then
                  Found := True;
               end if;
            end;

            if Found then
               Self.Duplicates.Delete (CD);
               exit;
            else
               Duplicates_List.Next (CD);
            end if;
         end loop;

      elsif not Self.Duplicates.Is_Empty then
         --  We removed a unit part, but then we have somewhere else
         --  another source that contains a part of this compilation unit.
         --  Let's check if this part replaces the removed part

         Found := False;
         CD := Self.Duplicates.First;

         while Duplicates_List.Has_Element (CD) loop
            declare
               Elem : constant Clashing_Unit :=
                        Duplicates_List.Element (CD);
            begin
               if Elem.Kind = Kind then
                  Self.Duplicates.Delete (CD);
                  Self.Add (Elem.Kind,
                            Elem.Loc.View,
                            Elem.Loc.Source,
                            Elem.Loc.Index,
                            Elem.Sep_Name,
                            Found);

                  exit;
               else
                  Duplicates_List.Next (CD);
               end if;
            end;
         end loop;
      end if;
   end Remove;

end GPR2.Build.Compilation_Unit;
