--  Loading of source files.
--  Copyright (C) 2002, 2003, 2004, 2005 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 Ada.Unchecked_Deallocation;

with GNAT.SHA1;

with Logging; use Logging;
with Filesystem;
with Name_Table; use Name_Table;
with Str_Table;

package body Files_Map is

   --  Check validity of FILE.
   --  Raise an exception in case of error.
   procedure Check_File (File: in Source_File_Entry);
   pragma Inline (Check_File);

   --  Next location to use.
   Next_Location : Location_Type := Location_Nil + 1;

   function Get_Last_Source_File_Entry return Source_File_Entry is
   begin
      return Source_Files.Last;
   end Get_Last_Source_File_Entry;

   Home_Dir : Name_Id := Null_Identifier;

   function Get_Home_Directory return Name_Id is
   begin
      if Home_Dir = Null_Identifier then
         declare
            Dir : constant String := Filesystem.Get_Current_Directory;
         begin
            Home_Dir := Get_Identifier (Dir);
         end;
      end if;
      return Home_Dir;
   end Get_Home_Directory;

   function Location_To_File (Location : Location_Type)
                             return Source_File_Entry is
   begin
      --  FIXME: use a cache
      --  FIXME: dicotomy
      for I in Source_Files.First .. Source_Files.Last loop
         declare
            F : Source_File_Record renames Source_Files.Table (I);
         begin
            if Location >= F.First_Location
              and then Location <= F.Last_Location
            then
               return I;
            end if;
         end;
      end loop;
      return No_Source_File_Entry;
   end Location_To_File;

   procedure Location_To_File_Pos (Location : Location_Type;
                                   File : out Source_File_Entry;
                                   Pos : out Source_Ptr) is
   begin
      File := Location_To_File (Location);
      if File = No_Source_File_Entry then
         --  File not found, location must be correct.
         raise Internal_Error;
      end if;
      Pos := Location_File_To_Pos (Location, File);
   end Location_To_File_Pos;

   function File_Pos_To_Location (File : Source_File_Entry; Pos : Source_Ptr)
                                 return Location_Type
   is
      pragma Assert (File <= Source_Files.Last);
   begin
      return Source_Files.Table (File).First_Location + Location_Type (Pos);
   end File_Pos_To_Location;

   function File_To_Location (File : Source_File_Entry) return Location_Type
   is
      pragma Assert (File <= Source_Files.Last);
   begin
      return Source_Files.Table (File).First_Location;
   end File_To_Location;

   --  Add a new entry in the lines_table.
   --  The new entry must be the next one after the last entry.
   procedure File_Add_Line_Number
     (File : Source_File_Entry; Line : Positive; Pos : Source_Ptr)
   is
      use Lines_Tables;

      -- Just check File is not out of bounds.
      pragma Assert (File <= Source_Files.Last);

      Source_File: Source_File_Record renames Source_Files.Table (File);
      Old_Last : Natural;
   begin
      --  Can only add line number to a real file.
      pragma Assert (Source_File.Kind = Source_File_File);

      --  Debug trace.
      if False then
         Log_Line ("file" & Source_File_Entry'Image (File)
                     & " line" & Natural'Image (Line)
                     & " at position" & Source_Ptr'Image (Pos));
      end if;

      --  The position of the first line is well-known.
      pragma Assert (Line = 1 xor Pos /= Source_Ptr_Org);

      Old_Last := Last (Source_File.Lines);
      if Line > Old_Last then
         Allocate (Source_File.Lines, Line - Old_Last);
         Source_File.Lines.Table (Old_Last + 1 .. Line) :=
           (others => Source_Ptr_Bad);
      end if;

      --  Lines are in increasing order.
      pragma Assert
        (Line = 1
           or else Source_File.Lines.Table (Line - 1) = Source_Ptr_Bad
           or else Source_File.Lines.Table (Line - 1) < Pos);
      pragma Assert
        (Line = Last (Source_File.Lines)
           or else Source_File.Lines.Table (Line + 1) = Source_Ptr_Bad
           or else Source_File.Lines.Table (Line + 1) > Pos);

      if Source_File.Lines.Table (Line) = Source_Ptr_Bad then
         Source_File.Lines.Table (Line) := Pos;
      else
         --  If the line position is already known, it must be the same.
         if Pos /= Source_File.Lines.Table (Line) then
            Log_Line ("file" & Source_File_Entry'Image (File)
                        & " for line" & Natural'Image (Line)
                        & " pos =" & Source_Ptr'Image (Pos)
                        & ", lines_table = "
                        & Source_Ptr'Image (Source_File.Lines.Table (Line)));
            raise Internal_Error;
         end if;
      end if;
   end File_Add_Line_Number;

   --  Convert a physical column to a logical column.
   --  A physical column is the offset in byte from the first byte of the line.
   --  A logical column is the position of the character when displayed.
   --  A HT (tabulation) moves the cursor to the next position multiple of the
   --  tab stop.
   --  The first character is at position 1 and at offset 0.
   function Coord_To_Col (File : Source_File_Entry;
                          Line_Pos : Source_Ptr;
                          Offset : Natural) return Natural
   is
      Source_File: Source_File_Record renames Source_Files.Table (File);
      Res : Positive := 1;
   begin
      if Offset = 0 then
         return Res;
      else
         for I in Line_Pos .. Line_Pos + Source_Ptr (Offset) - 1 loop
            if Source_File.Source (I) = ASCII.HT then
               Res := Res + Tab_Stop - Res mod Tab_Stop;
            end if;
            Res := Res + 1;
         end loop;
         return Res;
      end if;
   end Coord_To_Col;

   procedure Coord_To_Position (File : Source_File_Entry;
                                Line_Pos : Source_Ptr;
                                Offset : Natural;
                                Name : out Name_Id;
                                Col : out Natural) is
   begin
      Name := Source_Files.Table (File).File_Name;
      Col := Coord_To_Col (File, Line_Pos, Offset);
   end Coord_To_Position;

   --  Should only be called by Location_To_Coord.
   function Location_To_Line
     (Source_File : Source_File_Record; Pos : Source_Ptr) return Natural
   is
      use Lines_Tables;
      Lines_Table : constant Table_Thin_Ptr := Source_File.Lines.Table;
      Low, Hi, Mid : Natural;
      Mid1 : Natural;
   begin
      --  Look in the cache.
      if Pos >= Source_File.Cache_Pos then
         Low := Source_File.Cache_Line;
         Hi := Last (Source_File.Lines);
      else
         Low := 1;
         Hi := Source_File.Cache_Line;
      end if;

      loop
         << Again >> null;
         pragma Assert (Hi >= Low);
         pragma Assert (Low >= 1);
         pragma Assert (Hi <= Last (Source_File.Lines));

         Mid := (Hi + Low) / 2;
         if Lines_Table (Mid) = Source_Ptr_Bad then
            -- There is a hole: no position for this line.
            -- Set MID1 to a line which has a position.
            -- Try downward.
            Mid1 := Mid;
            while Lines_Table (Mid1) = Source_Ptr_Bad loop
               --  Note: Low may have no line.
               exit when Mid1 = Low;
               Mid1 := Mid1 - 1;
            end loop;
            if Mid1 /= Low then
               --  Mid1 has a line.
               if Pos < Lines_Table (Mid1) then
                  Hi := Mid1;
                  goto Again;
               end if;
               if Pos > Lines_Table (Mid1) then
                  Low := Mid1;
                  goto Again;
               end if;
               --  Found, handled just below.
            else
               --  Failed (downward is LOW): try upward.
               Mid1 := Mid;
               while Lines_Table (Mid1) = Source_Ptr_Bad loop
                  Mid1 := Mid1 + 1;
               end loop;
               if Mid1 = Hi then
                  --  Failed: no lines between LOW and HI.
                  if Pos >= Lines_Table (Hi) then
                     Mid1 := Hi;
                  else
                     Mid1 := Low;
                  end if;
                  return Mid1;
               end if;
               --  Mid1 has a line.
               if Pos < Lines_Table (Mid1) then
                  Hi := Mid1;
                  goto Again;
               end if;
               if Pos > Lines_Table (Mid1) then
                  Low := Mid1;
                  goto Again;
               end if;
            end if;
            Mid := Mid1;
         end if;

         --  Mid is on a known line.
         pragma Assert (Lines_Table (Mid) /= Source_Ptr_Bad);

         if Pos >= Lines_Table (Mid) then
            if Mid = Last (Source_File.Lines)
              or else (Lines_Table (Mid + 1) /= Source_Ptr_Bad
                         and then Pos < Lines_Table (Mid + 1))
              or else Pos = Lines_Table (Mid)
              or else (Hi <= Mid + 1
                         and Lines_Table (Mid + 1) = Source_Ptr_Bad)
            then
               return Mid;
            end if;
         end if;
         if Pos < Lines_Table (Mid) then
            Hi := Mid - 1;
         else
            if Lines_Table (Mid + 1) /= Source_Ptr_Bad then
               Low := Mid + 1;
            else
               Low := Mid;
            end if;
         end if;
      end loop;
   end Location_To_Line;

   --  Internal procedure
   procedure Location_To_Coord (Source_File : in out Source_File_Record;
                                Pos : Source_Ptr;
                                Line_Pos : out Source_Ptr;
                                Line : out Natural;
                                Offset : out Natural)
   is
      use Lines_Tables;
      Line_P : Source_Ptr;
      Line_Threshold : constant Natural := 4;
      Low, Hi : Natural;
   begin
      --  Look in the cache.
      if Pos >= Source_File.Cache_Pos then
         Low := Source_File.Cache_Line;
         Hi := Last (Source_File.Lines);

         --  Maybe adjust the threshold.
         --  Quick look.
         if Pos - Source_File.Cache_Pos <= 120
           and then Low + Line_Threshold <= Hi
         then
            for I in 1 .. Line_Threshold loop
               Line_P := Source_File.Lines.Table (Low + I);
               if Line_P > Pos and Line_P /= Source_Ptr_Bad then
                  Line := Low + I - 1;
                  goto Found;
               else
                  exit when Line_P = Source_Ptr_Bad;
               end if;
            end loop;
         end if;
      end if;

      Line := Location_To_Line (Source_File, Pos);

      << Found >> null;

      Line_Pos := Source_File.Lines.Table (Line);
      Offset := Natural (Pos - Line_Pos);

      --  Update cache.
      Source_File.Cache_Pos := Line_Pos;
      Source_File.Cache_Line := Line;
   end Location_To_Coord;

   procedure Location_To_Position (Location : Location_Type;
                                   Name : out Name_Id;
                                   Line : out Positive;
                                   Col : out Natural)
   is
      File : Source_File_Entry;
      Line_Pos : Source_Ptr;
      Offset : Natural;
   begin
      Location_To_Coord (Location, File, Line_Pos, Line, Offset);
      Coord_To_Position (File, Line_Pos, Offset, Name, Col);
   end Location_To_Position;

   procedure File_Pos_To_Coord (File : Source_File_Entry;
                                Pos : Source_Ptr;
                                Line_Pos : out Source_Ptr;
                                Line : out Positive;
                                Offset : out Natural) is
   begin
      case Source_Files.Table (File).Kind is
         when Source_File_File =>
            Location_To_Coord (Source_Files.Table (File), Pos,
                               Line_Pos, Line, Offset);
         when Source_File_String =>
            Line_Pos := Source_Ptr_Org;
            Line := 1;
            Offset := Natural (Pos - Source_Ptr_Org);
         when Source_File_Instance =>
            declare
               Base : constant Source_File_Entry :=
                 Source_Files.Table (File).Base;
            begin
               Location_To_Coord (Source_Files.Table (Base), Pos,
                                  Line_Pos, Line, Offset);
            end;
      end case;
   end File_Pos_To_Coord;

   procedure Location_To_Coord (Location : Location_Type;
                                File : out Source_File_Entry;
                                Line_Pos : out Source_Ptr;
                                Line : out Positive;
                                Offset : out Natural)
   is
      Pos : Source_Ptr;
   begin
      --  Get FILE and position POS in the file.
      Location_To_File_Pos (Location, File, Pos);
      File_Pos_To_Coord (File, Pos, Line_Pos, Line, Offset);
   end Location_To_Coord;

   function Location_File_To_Pos
     (Location : Location_Type; File : Source_File_Entry) return Source_Ptr is
   begin
      return Source_Ptr (Location - Source_Files.Table (File).First_Location);
   end Location_File_To_Pos;

   function Location_File_To_Line
     (Location : Location_Type; File : Source_File_Entry) return Positive
   is
      Line_Pos : Source_Ptr;
      Line     : Positive;
      Offset   : Natural;
   begin
      Location_To_Coord
        (Source_Files.Table (File), Location_File_To_Pos (Location, File),
         Line_Pos, Line, Offset);
      return Line;
   end Location_File_To_Line;

   function Location_File_Line_To_Col
     (Loc : Location_Type; File : Source_File_Entry; Line : Positive)
     return Natural
   is
      F : Source_File_Record renames Source_Files.Table (File);
      Line_Pos : constant Source_Ptr := F.Lines.Table (Line);
      Pos : constant Source_Ptr := Location_File_To_Pos (Loc, File);
   begin
      return Coord_To_Col (File, Line_Pos, Natural (Pos - Line_Pos));
   end Location_File_Line_To_Col;

   function Location_File_Line_To_Offset
     (Loc : Location_Type; File : Source_File_Entry; Line : Positive)
     return Natural
   is
      F : Source_File_Record renames Source_Files.Table (File);
      Line_Pos : constant Source_Ptr := F.Lines.Table (Line);
      Pos : constant Source_Ptr := Location_File_To_Pos (Loc, File);
   begin
      return Natural (Pos - Line_Pos);
   end Location_File_Line_To_Offset;

   -- Convert the first digit of VAL into a character (base 10).
   function Digit_To_Char (Val: Natural) return Character is
   begin
      return Character'Val (Character'Pos ('0') + Val mod 10);
   end Digit_To_Char;

   function Get_Os_Time_Stamp return Time_Stamp_Id
   is
      use Filesystem;
      use Str_Table;

      Year : Year_Range;
      Month : Month_Range;
      Day : Day_Range;
      Sec : Sec_Range;
      Ms : Ms_Range;
      S1 : Integer;
      Res: Time_Stamp_Id;
   begin
      --  Use UTC time (like file time stamp).
      Split_Now_Utc (Year, Month, Day, Sec, Ms);

      Res := Time_Stamp_Id (Create_String8);
      Append_String8_Char (Digit_To_Char (Year / 1000));
      Append_String8_Char (Digit_To_Char (Year / 100));
      Append_String8_Char (Digit_To_Char (Year / 10));
      Append_String8_Char (Digit_To_Char (Year / 1));
      Append_String8_Char (Digit_To_Char (Month / 10));
      Append_String8_Char (Digit_To_Char (Month / 1));
      Append_String8_Char (Digit_To_Char (Day / 10));
      Append_String8_Char (Digit_To_Char (Day / 1));
      S1 := Sec / 3600;
      Append_String8_Char (Digit_To_Char (S1 / 10));
      Append_String8_Char (Digit_To_Char (S1));
      S1 := (Sec / 60) mod 60;
      Append_String8_Char (Digit_To_Char (S1 / 10));
      Append_String8_Char (Digit_To_Char (S1));
      S1 := Sec mod 60;
      Append_String8_Char (Digit_To_Char (S1 / 10));
      Append_String8_Char (Digit_To_Char (S1));

      Append_String8_Char ('.');
      Append_String8_Char (Digit_To_Char (Ms / 100));
      Append_String8_Char (Digit_To_Char (Ms / 10));
      Append_String8_Char (Digit_To_Char (Ms));
      return Res;
   end Get_Os_Time_Stamp;

   function Get_Pathname (Directory : Name_Id; Name : Name_Id) return String
   is
      Filename : constant String := Image (Name);
   begin
      if not Filesystem.Is_Absolute_Path (Filename) then
         return Image (Directory) & Filename;
      else
         return Filename;
      end if;
   end Get_Pathname;

   --  If NAME contains a directory separator, move it to the DIRECTORY name.
   --  At the return point, NAME has no directory components.
   procedure Normalize_Pathname
     (Directory : in out Name_Id; Name : in out Name_Id)
   is
      Filename : constant String := Image (Name);
      Separator_Pos : Natural;
   begin
      --  Find a directory part in NAME, return now if none.
      Separator_Pos := 0;
      for I in Filename'Range loop
         if Filename (I) = '/' or Filename (I) = '\' then
            Separator_Pos := I;
         end if;
      end loop;
      if Separator_Pos = 0 then
         return;
      end if;

      --  Move the directory part to DIRECTORY.
      declare
         File_Dir : constant String :=
           Filename (Filename'First .. Separator_Pos);
      begin
         if Directory /= Null_Identifier then
            Directory := Get_Identifier (Image (Directory) & File_Dir);
         else
            Directory := Get_Identifier (File_Dir);
         end if;
      end;
      Name := Get_Identifier (Filename (Separator_Pos + 1 .. Filename'Last));
   end Normalize_Pathname;

   function Find_Language (Filename : String) return Language_Type
   is
      P, E : Natural;
      Ext : String (1 .. 5);
   begin
      P := Filename'Last;
      E := Ext'Last;
      loop
         if P <= Filename'First
           or else E < Ext'First
         then
            return Language_Unknown;
         end if;
         case Filename (P) is
            when 'a' .. 'z' =>
               Ext (E) := Filename (P);
            when 'A' .. 'Z' =>
               Ext (E) := Character'Val (Character'Pos (Filename (P))
                                           - Character'Pos ('A')
                                           + Character'Pos ('a'));
            when '.' =>
               if Ext (E + 1 .. Ext'Last) = "vhd"
                 or else Ext (E + 1 .. Ext'Last) = "vhdl"
               then
                  return Language_Vhdl;
               end if;
               if Ext (E + 1 .. Ext'Last) = "v"
                 or else Ext (E + 1 .. Ext'Last) = "v"
                 or else Ext (E + 1 .. Ext'Last) = "sv"
                 or else Ext (E + 1 .. Ext'Last) = "svh"
               then
                  return Language_Verilog;
               end if;
               if Ext (E + 1 .. Ext'Last) = "psl" then
                  return Language_Psl;
               end if;
            when others =>
               return Language_Unknown;
         end case;
         P := P - 1;
         E := E - 1;
      end loop;
   end Find_Language;

   --  Find a source_file by DIRECTORY and NAME.
   --  Return NO_SOURCE_FILE_ENTRY if not already opened.
   function Find_Source_File (Directory : Name_Id; Name: Name_Id)
                             return Source_File_Entry
   is
   begin
      for I in Source_Files.First .. Source_Files.Last loop
         if Source_Files.Table (I).File_Name = Name
           and then Source_Files.Table (I).Directory = Directory
         then
            return I;
         end if;
      end loop;
      return No_Source_File_Entry;
   end Find_Source_File;

   --  Return an entry for a filename.
   --  The file is not loaded.
   function Create_Source_File_Entry (Directory : Name_Id; Name: Name_Id)
                                     return Source_File_Entry
   is
      Res: Source_File_Entry;
   begin
      --  File must not already exist.
      pragma Assert
        (Find_Source_File (Directory, Name) = No_Source_File_Entry);

      --  Create a new entry.
      Res := Source_Files.Allocate;
      Source_Files.Table (Res) := (Kind => Source_File_File,
                                   First_Location => Next_Location,
                                   Last_Location => Next_Location,
                                   File_Name => Name,
                                   Directory => Directory,
                                   Checksum => No_File_Checksum_Id,
                                   Source => null,
                                   File_Length => 0,
                                   Lines => <>,
                                   Cache_Pos => Source_Ptr_Org,
                                   Cache_Line => 1,
                                   Gap_Start => Source_Ptr_Last,
                                   Gap_Last => Source_Ptr_Last);
      Lines_Tables.Init (Source_Files.Table (Res).Lines, Lines_Table_Init);
      File_Add_Line_Number (Res, 1, Source_Ptr_Org);
      return Res;
   end Create_Source_File_Entry;

   function Create_Source_File_From_String (Name: Name_Id; Content : String)
                                           return Source_File_Entry
   is
      Len : constant Source_Ptr := Source_Ptr (Content'Length);
      Res : Source_File_Entry;
      Buffer: File_Buffer_Acc;
   begin
      --  Fill buffer.
      Buffer := new File_Buffer
        (Source_Ptr_Org .. Source_Ptr_Org + Len + 1);

      if Len /= 0 then
         Buffer (Source_Ptr_Org .. Source_Ptr_Org + Len - 1) :=
           File_Buffer (Content);
      end if;

      --  Create entry.
      Res := Source_Files.Allocate;
      Source_Files.Table (Res) :=
        (Kind => Source_File_String,
         First_Location => Next_Location,
         Last_Location => Next_Location + Location_Type (Len) + 1,
         File_Name => Name,
         Directory => Null_Identifier,
         Checksum => No_File_Checksum_Id,
         Source => Buffer,
         File_Length => 0);

      Set_File_Length (Res, Len);

      Next_Location := Source_Files.Table (Res).Last_Location + 1;

      return Res;
   end Create_Source_File_From_String;

   function Create_Virtual_Source_File (Name: Name_Id)
                                       return Source_File_Entry is
   begin
      return Create_Source_File_From_String (Name, "");
   end Create_Virtual_Source_File;

   function Create_Instance_Source_File (Ref : Source_File_Entry;
                                         Loc : Location_Type;
                                         Inst : Vhdl.Types.Vhdl_Node)
                                        return Source_File_Entry
   is
      pragma Unreferenced (Inst);
      Base : Source_File_Entry;
      Res : Source_File_Entry;
   begin
      if Source_Files.Table (Ref).Kind = Source_File_Instance then
         Base := Source_Files.Table (Ref).Base;
      else
         Base := Ref;
      end if;

      --  Create entry.
      Res := Source_Files.Allocate;

      declare
         F : Source_File_Record renames Source_Files.Table (Base);
      begin
         Source_Files.Table (Res) :=
           (Kind => Source_File_Instance,
            First_Location => Next_Location,
            Last_Location => Next_Location + Location_Type (F.File_Length) + 1,
            File_Name => F.File_Name,
            Directory => F.Directory,
            Checksum => F.Checksum,
            Source => F.Source,
            File_Length => F.File_Length,
            Ref => Ref,
            Base => Base,
            Instance_Loc => Loc);

         Next_Location := Source_Files.Table (Res).Last_Location + 1;
      end;

      return Res;
   end Create_Instance_Source_File;

   function Instance_Relocate (Inst_File : Source_File_Entry;
                               Loc : Location_Type) return Location_Type
   is
      pragma Assert (Inst_File <= Source_Files.Last);
      F : Source_File_Record renames Source_Files.Table (Inst_File);
      pragma Assert (F.Kind = Source_File_Instance);
      R : Source_File_Record renames Source_Files.Table (F.Ref);
   begin
      if Loc >= R.First_Location and Loc <= R.Last_Location then
         return F.First_Location + (Loc - R.First_Location);
      else
         return Loc;
      end if;
   end Instance_Relocate;

   function Location_Instance_To_Location (Loc : Location_Type)
                                          return Location_Type
   is
      File : Source_File_Entry;
      Pos :  Source_Ptr;
   begin
      if Loc = No_Location then
         return No_Location;
      end if;

      Location_To_File_Pos (Loc, File, Pos);
      if Source_Files.Table (File).Kind = Source_File_Instance then
         return Source_Files.Table (File).Instance_Loc;
      else
         return No_Location;
      end if;
   end Location_Instance_To_Location;

   function Reserve_Source_File
     (Directory : Name_Id; Name: Name_Id; Length : Source_Ptr)
     return Source_File_Entry
   is
      pragma Assert (Length >= 2);
      Res : Source_File_Entry;
   begin
      Res := Create_Source_File_Entry (Directory, Name);

      declare
         F : Source_File_Record renames Source_Files.Table (Res);
      begin
         F.Source := new File_Buffer (Source_Ptr_Org
                                        .. Source_Ptr_Org + Length - 1);

         --  Read_Source_File call must follow its Create_Source_File.
         pragma Assert (F.First_Location = Next_Location);

         F.Last_Location := Next_Location + Location_Type (Length) - 1;
         Next_Location := F.Last_Location + 1;
      end;

      return Res;
   end Reserve_Source_File;

   --  Return an entry for a filename.
   --  Load the filename if necessary.
   function Read_Source_File (Directory : Name_Id; Name: Name_Id)
                             return Source_File_Entry
   is
      use Filesystem;
      Fd : File_Descriptor;

      Res : Source_File_Entry;

      Raw_Length : Long_Integer;
      Length : Source_Ptr;

      Buffer : File_Buffer_Acc;
   begin
      --  The file is not supposed to be already loaded, but this could happen
      --  if the same file is compiled in two libraries.
      Res := Find_Source_File (Directory, Name);
      if Res /= No_Source_File_Entry then
         return Res;
      end if;

      --  Open the file (punt on non regular files).
      declare
         Filename : constant String := Get_Pathname (Directory, Name);
      begin
         if not Is_Regular_File (Filename) then
            return No_Source_File_Entry;
         end if;
         Open_Read (Fd, Filename);
         if Is_Error (Fd) then
            return No_Source_File_Entry;
         end if;
      end;

      Raw_Length := File_Length (Fd);

      --  Check for too large files.  Use 'Pos (ie universal integer) to avoid
      --  errors in conversions.
      if Long_Integer'Pos (Raw_Length) > Source_Ptr'Pos (Source_Ptr'Last)
        or else Long_Integer'Pos (Raw_Length) > Integer'Pos (Integer'Last)
      then
         Close (Fd);
         return No_Source_File_Entry;
      end if;

      Length := Source_Ptr (Raw_Length);

      Res := Reserve_Source_File (Directory, Name, Length + 2);
      if Res = No_Source_File_Entry then
         Close (Fd);
         return No_Source_File_Entry;
      end if;

      Buffer := Get_File_Source (Res);

      Read (Fd, Buffer (Source_Ptr_Org)'Address, Long_Integer (Length));
      if Is_Error (Fd) then
         Close (Fd);
         raise Internal_Error;
      end if;
      Close (Fd);

      Set_File_Length (Res, Length);

      --  Set the gap.
      Source_Files.Table (Res).Gap_Start :=
        Source_Ptr_Org + Length + 2;
      Source_Files.Table (Res).Gap_Last :=
        Source_Files.Table (Res).Source'Last;

      --  Compute the SHA1.
      declare
         use GNAT.SHA1;
         use Str_Table;

         subtype Buffer_String is String (1 .. Buffer'Length - 2);
         Buffer_Digest : Message_Digest;
      begin
         if Length /= 0 then
            --  Avoid weird bounds for empty buffers.
            Buffer_Digest := Digest
              (Buffer_String
                 (Buffer (Source_Ptr_Org .. Source_Ptr_Org + Length - 1)));
         end if;

         Source_Files.Table (Res).Checksum :=
           File_Checksum_Id (Create_String8);
         for I in Buffer_Digest'Range loop
            Append_String8_Char (Buffer_Digest (I));
         end loop;
      end;

      return Res;
   end Read_Source_File;

   function Read_Source_File_Normalize (Directory : Name_Id; Name: Name_Id)
                                       return Source_File_Entry
   is
      Dir_Norm, Name_Norm : Name_Id;
   begin
      Dir_Norm := Directory;
      Name_Norm := Name;
      Normalize_Pathname (Dir_Norm, Name_Norm);

      return Read_Source_File (Dir_Norm, Name_Norm);
   end Read_Source_File_Normalize;

   procedure Discard_Source_File (File : Source_File_Entry)
   is
      pragma Assert (File <= Source_Files.Last);
      F : Source_File_Record renames Source_Files.Table (File);
   begin
      F.File_Name := Null_Identifier;
      F.Directory := Null_Identifier;
   end Discard_Source_File;

   procedure Free_Source_File (File : Source_File_Entry)
   is
      procedure Free is new Ada.Unchecked_Deallocation
        (File_Buffer, File_Buffer_Acc);

      F : Source_File_Record renames Source_Files.Table (File);
   begin
      case F.Kind is
         when Source_File_File =>
            Lines_Tables.Free (F.Lines);
            Free (F.Source);
         when Source_File_String =>
            Free (F.Source);
         when Source_File_Instance =>
            null;
      end case;
   end Free_Source_File;

   procedure Unload_Last_Source_File (File : Source_File_Entry) is
   begin
      pragma Assert (File = Source_Files.Last);
      Free_Source_File (File);
      Source_Files.Decrement_Last;
      Next_Location :=
        Source_Files.Table (Source_Files.Last).Last_Location + 1;
   end Unload_Last_Source_File;

   procedure Skip_Gap (File : Source_File_Entry; Pos : in out Source_Ptr)
   is
      pragma Assert (File <= Source_Files.Last);
      F : Source_File_Record renames Source_Files.Table (File);
   begin
      if Pos = F.Gap_Start then
         Pos := F.Gap_Last + 1;
      end if;
   end Skip_Gap;

   --  Check validity of FILE.
   --  Raise an exception in case of error.
   procedure Check_File (File : Source_File_Entry) is
   begin
      pragma Assert (File <= Source_Files.Last);
      null;
   end Check_File;

   --  Return a buffer (access to the contents of the file) for a file entry.
   function Get_File_Source (File: Source_File_Entry)
                            return File_Buffer_Acc is
   begin
      Check_File (File);
      return Source_Files.Table (File).Source;
   end Get_File_Source;

   function Get_File_Buffer (File : Source_File_Entry)
                            return File_Buffer_Ptr is
   begin
      return To_File_Buffer_Ptr
        (Source_Files.Table (File).Source (Source_Ptr_Org)'Address);
   end Get_File_Buffer;

   procedure Set_File_Length (File : Source_File_Entry; Length : Source_Ptr) is
   begin
      Check_File (File);
      declare
         F : Source_File_Record renames Source_Files.Table (File);
         Buffer : File_Buffer_Acc renames F.Source;
      begin
         pragma Assert (Length <= Buffer'Length - 2);

         F.File_Length := Length;
         Buffer (Source_Ptr_Org + Length) := EOT;
         Buffer (Source_Ptr_Org + Length + 1) := EOT;
      end;
   end Set_File_Length;

   function Get_File_Length (File: Source_File_Entry) return Source_Ptr is
   begin
      Check_File (File);
      return Source_Files.Table (File).File_Length;
   end Get_File_Length;

   function Get_Content_Length (File : Source_File_Entry) return Source_Ptr
   is
      pragma Assert (File <= Source_Files.Last);
      F : Source_File_Record renames Source_Files.Table (File);
   begin
      if F.Gap_Start >= F.File_Length then
         return F.File_Length;
      else
         return F.File_Length - (F.Gap_Last - F.Gap_Start + 1);
      end if;
   end Get_Content_Length;

   function Get_Buffer_Length (File : Source_File_Entry) return Source_Ptr
   is
      pragma Assert (File <= Source_Files.Last);
      F : Source_File_Record renames Source_Files.Table (File);
   begin
      return Source_Ptr (F.Last_Location - F.First_Location + 1);
   end Get_Buffer_Length;

   --  Return the name of the file.
   function Get_File_Name (File: Source_File_Entry) return Name_Id is
   begin
      Check_File (File);
      return Source_Files.Table (File).File_Name;
   end Get_File_Name;

   function Get_File_Checksum (File : Source_File_Entry)
                              return File_Checksum_Id is
   begin
      Check_File (File);
      return Source_Files.Table (File).Checksum;
   end Get_File_Checksum;

   function Get_Directory_Name (File : Source_File_Entry) return Name_Id is
   begin
      Check_File (File);
      return Source_Files.Table (File).Directory;
   end Get_Directory_Name;

   function File_Line_To_Position (File : Source_File_Entry; Line : Positive)
                                  return Source_Ptr
   is
      pragma Assert (File <= Source_Files.Last);
      Source_File: Source_File_Record renames Source_Files.Table (File);
   begin
      case Source_File.Kind is
         when Source_File_File =>
            if Line > Lines_Tables.Last (Source_File.Lines) then
               return Source_Ptr_Bad;
            else
               return Source_File.Lines.Table (Line);
            end if;
         when Source_File_String =>
            if Line /= 1 then
               return Source_Ptr_Bad;
            else
               return Source_Ptr_Org;
            end if;
         when Source_File_Instance =>
            return File_Line_To_Position (Source_File.Base, Line);
      end case;
   end File_Line_To_Position;

   function Is_Eq (L : Time_Stamp_Id; R : Time_Stamp_Id) return Boolean
   is
      use Str_Table;
      L_Str : constant String8_Id := String8_Id (L);
      R_Str : constant String8_Id := String8_Id (R);
   begin
      for I in 1 .. Nat32 (Time_Stamp_String'Length) loop
         if Element_String8 (L_Str, I) /= Element_String8 (R_Str, I) then
            return False;
         end if;
      end loop;
      return True;
   end Is_Eq;

   function Is_Eq (L, R : File_Checksum_Id) return Boolean
   is
      use Str_Table;
      L_Str : constant String8_Id := String8_Id (L);
      R_Str : constant String8_Id := String8_Id (R);
   begin
      for I in 1 .. Nat32 (File_Checksum_String'Length) loop
         if Element_String8 (L_Str, I) /= Element_String8 (R_Str, I) then
            return False;
         end if;
      end loop;
      return True;
   end Is_Eq;

   function Is_Gt (L : Time_Stamp_Id; R : Time_Stamp_Id) return Boolean
   is
      use Str_Table;
      L_Str : constant String8_Id := String8_Id (L);
      R_Str : constant String8_Id := String8_Id (R);
      E_L, E_R : Nat8;
   begin
      for I in 1 .. Nat32 (Time_Stamp_String'Length) loop
         E_L := Element_String8 (L_Str, I);
         E_R := Element_String8 (R_Str, I);
         if E_L /= E_R then
            return E_L > E_R;
         end if;
      end loop;
      return False;
   end Is_Gt;

   function Get_Time_Stamp_String (Ts : Time_Stamp_Id) return String is
   begin
      if Ts = Null_Time_Stamp then
         return "NULL_TS";
      else
         return Str_Table.String_String8
           (String8_Id (Ts), Time_Stamp_String'Length);
      end if;
   end Get_Time_Stamp_String;

   function Get_File_Checksum_String (Checksum : File_Checksum_Id)
                                     return String is
   begin
      if Checksum = No_File_Checksum_Id then
         return "NO_CHECKSUM";
      else
         return Str_Table.String_String8
           (String8_Id (Checksum), File_Checksum_String'Length);
      end if;
   end Get_File_Checksum_String;

   function Image (Loc : Location_Type; Filename : Boolean := True)
                  return string
   is
      Line, Col : Natural;
      Name : Name_Id;
   begin
      if Loc = Location_Nil then
         --  Avoid a crash.
         return "??:??:??";
      end if;

      Location_To_Position (Loc, Name, Line, Col);
      declare
         Line_Str : constant String := Natural'Image (Line);
         Col_Str : constant String := Natural'Image (Col);
      begin
         if Filename then
            return Name_Table.Image (Name)
              & ':' & Line_Str (Line_Str'First + 1 .. Line_Str'Last)
              & ':' & Col_Str (Col_Str'First + 1 .. Col_Str'Last);
         else
            return Line_Str (Line_Str'First + 1 .. Line_Str'Last)
              & ':' & Col_Str (Col_Str'First + 1 .. Col_Str'Last);
         end if;
      end;
   end Image;

   --  Compute the length of line that starts at START.  Tabs are expanded to
   --  compute the length.
   function Compute_Expanded_Line_Length (File : Source_File_Entry;
                                          Start : Source_Ptr) return Natural
   is
      Buf : constant File_Buffer_Acc := Get_File_Source (File);
      Pos : Source_Ptr;
      Len : Natural;
      C : Character;
   begin
      --  Compute line length.
      Pos := Start;
      Len := 0;
      loop
         C := Buf (Pos);
         Pos := Pos + 1;
         exit when C = ASCII.CR or C = ASCII.LF or C = ASCII.EOT;
         if C = ASCII.HT then
            --  Expand tab.
            Len := Len + (Tab_Stop - Len mod Tab_Stop);
         else
            Len := Len + 1;
         end if;
      end loop;
      return Len;
   end Compute_Expanded_Line_Length;

   --  Return the line that starts at START in FILE.  This is slow.
   function Extract_Expanded_Line (File : Source_File_Entry;
                                   Start : Source_Ptr) return String
   is
      Buf : constant File_Buffer_Acc := Get_File_Source (File);
      Len : constant Natural := Compute_Expanded_Line_Length (File, Start);
      Res : String (1 .. Len);
      P : Natural;
      Pos : Source_Ptr;
      C : Character;
   begin
      Pos := Start;
      P := 0;
      loop
         C := Buf (Pos);
         Pos := Pos + 1;
         exit when C = ASCII.CR or C = ASCII.LF or C = ASCII.EOT;
         if C = ASCII.HT then
            --  Expand tab.
            loop
               P := P + 1;
               Res (P) := ' ';
               exit when P mod Tab_Stop = 0;
            end loop;
         else
            P := P + 1;
            Res (P) := C;
         end if;
      end loop;
      pragma Assert (P = Res'Last);
      return Res;
   end Extract_Expanded_Line;

   function Extract_Expanded_Line (File : Source_File_Entry;
                                   Line : Positive) return String
   is
      Start : constant Source_Ptr := File_Line_To_Position (File, Line);
   begin
      return Extract_Expanded_Line (File, Start);
   end Extract_Expanded_Line;

   -- Debug procedures.
   procedure Debug_Source_Loc (Loc : Location_Type)
   is
      File : Source_File_Entry;
      Line_Pos : Source_Ptr;
      Line : Natural;
      Offset : Natural;
   begin
      Location_To_Coord (Loc, File, Line_Pos, Line, Offset);
      Log_Line (Extract_Expanded_Line (File, Line_Pos));
   end Debug_Source_Loc;

   --  Disp sources lines of a file.
   procedure Debug_Source_Lines (File: Source_File_Entry) is
      Source_File: Source_File_Record renames Source_Files.Table (File);
   begin
      Check_File (File);
      for I in Lines_Tables.First .. Lines_Tables.Last (Source_File.Lines) loop
         Log_Line ("line" & Natural'Image (I) & " at offset"
                     & Source_Ptr'Image (Source_File.Lines.Table (I)));
      end loop;
   end Debug_Source_Lines;

   procedure Debug_Source_File (File : Source_File_Entry)
   is
      F : Source_File_Record renames Source_Files.Table(File);
   begin
      Log ("*");
      Log (Source_File_Entry'Image (File));
      Log (" name: " & Image (F.File_Name));
      Log (" dir:" & Image (F.Directory));
      Log (" file length:" & Source_Ptr'Image (F.File_Length));
      Log_Line;
      Log (" location:" & Location_Type'Image (F.First_Location)
             & " -" & Location_Type'Image (F.Last_Location));
      Log_Line;
      if F.Checksum /= No_File_Checksum_Id then
         Log (" checksum: " & Get_File_Checksum_String (F.Checksum));
         Log_Line;
      end if;
      case F.Kind is
         when Source_File_File =>
            if F.Source = null then
               Log (" no buf");
            else
               Log (" buf:" & Source_Ptr'Image (F.Source'First)
                      & " -" & Source_Ptr'Image (F.Source'Last));
            end if;
            Log_Line;
            Log (" nbr lines:"
                   & Natural'Image (Lines_Tables.Last (F.Lines)));
            Log_Line;
            Log (" Gap:" & Source_Ptr'Image (F.Gap_Start)
                   & " -" & Source_Ptr'Image (F.Gap_Last));
            Log_Line;
         when Source_File_String =>
            null;
         when Source_File_Instance =>
            Log (" instance from:" & Source_File_Entry'Image (F.Ref));
            Log (", base:" & Source_File_Entry'Image (F.Base));
            Log (", loc:" & Image (F.Instance_Loc));
            Log_Line;
      end case;
   end Debug_Source_File;

   procedure Debug_Source_Files is
   begin
      for I in Source_Files.First .. Source_Files.Last loop
         Debug_Source_File (I);
      end loop;
   end Debug_Source_Files;

   pragma Unreferenced (Debug_Source_Lines);
   pragma Unreferenced (Debug_Source_Loc);

   procedure Finalize is
   begin
      for I in Source_Files.First .. Source_Files.Last loop
         Free_Source_File (I);
      end loop;
      Source_Files.Free;
   end Finalize;

   procedure Initialize is
   begin
      Source_Files.Init;
      Next_Location := Location_Nil + 1;
   end Initialize;
end Files_Map;
