with Ada.Command_Line;            use Ada.Command_Line;
with Ada.Containers.Hashed_Maps;
with Ada.Directories;             use Ada.Directories;
with Ada.Exceptions;
with Ada.Strings.Fixed;           use Ada.Strings.Fixed;
with Ada.Strings.Unbounded;       use Ada.Strings.Unbounded;
with Ada.Text_IO;                 use Ada.Text_IO;

with GNAT.Traceback.Symbolic;
with GNATCOLL.Opt_Parse;          use GNATCOLL.Opt_Parse;
with GNATCOLL.Traces;

with Liblktlang_Support.Diagnostics; use Liblktlang_Support.Diagnostics;
with Liblktlang_Support.Diagnostics.Output;
use Liblktlang_Support.Diagnostics.Output;
with Liblktlang_Support.Slocs;       use Liblktlang_Support.Slocs;
with Liblktlang_Support.Text;        use Liblktlang_Support.Text;

with Liblktlang.Analysis;         use Liblktlang.Analysis;
with Liblktlang.Common;

procedure Lkt_Toolbox is

   package Arg is

      Parser : Argument_Parser := Create_Argument_Parser
        (Help => "Lkt toolbox. Toolbox like command line frontend for the "
                 & "LKT langkit library.");

      package Files is new Parse_Positional_Arg_List
        (Parser   => Parser,
         Name     => "files",
         Arg_Type => Unbounded_String,
         Help     => "The files to parse");

      package Check_Only is new Parse_Flag
        (Parser => Parser,
         Short  => "-C",
         Long   => "--check-only",
         Help   => "Only output the errors");

      package Flag_Invalid is new Parse_Flag
        (Parser => Parser,
         Short  => "-I",
         Long   => "--check-invalid-decls",
         Help   => "Flag decls that generate errors that are not annotated"
                   & " with the @invalid annotation. Also flag decls"
                   & " annotated with @invalid that don't trigger any errors");
   end Arg;

   use Liblktlang;

   package Invalid_Decl_Maps is new Ada.Containers.Hashed_Maps
     (Key_Type        => Analysis.Lkt_Node,
      Element_Type    => Boolean,
      Hash            => Analysis.Hash,
      Equivalent_Keys => "=");

   procedure Print_Semantic_Result
     (S : Analysis.Semantic_Result; Unit : Analysis.Analysis_Unit);
   --  Print a semantic result

   function Format_Node (Decl_Node : Decl'Class) return String;
   --  Format node for semantic result printing

   procedure Print_Lkt_Toolbox_Diagnostic
     (Node : Lkt_Node'Class; Message : Wide_Wide_String);
   --  Internal wrapper to ``Print_Diagnostic`` used by lkt_toolbox to print
   --  additional diagnostics.

   function Populate_Invalid_Decl_Map
     (Node : Analysis.Lkt_Node'Class) return Common.Visit_Status;
   --  Populate ``Invalid_Decl_Map`` and reject declarations with ``@invalid``
   --  annotations that are nested in another declaration annotated with
   --  ``@invalid``.

   Invalid_Decl_Map : Invalid_Decl_Maps.Map;
   --  Map of declarations annotated with ``@invalid``. The boolean elements of
   --  the map are initialized to ``False`` and set to ``True`` whenever a
   --  diagnostic is emitted for the related declaration. Therefore, this map
   --  is used to check that at least one diagnostic has been emitted for each
   --  declaration annotated with ``@invalid``.

   -----------------
   -- Format_Node --
   -----------------

   function Format_Node (Decl_Node : Decl'Class) return String is
   begin
      --  Remove rebindings information as there is no easy way to filter
      --  out/format rebindings information involving prelude declarations.
      return Decl_Node.P_As_Bare_Decl.Image;
   end Format_Node;

   ----------------------------------
   -- Print_Lkt_Toolbox_Diagnostic --
   ----------------------------------

   procedure Print_Lkt_Toolbox_Diagnostic
     (Node : Lkt_Node'Class; Message : Wide_Wide_String)
   is
      Sloc_Range : constant Source_Location_Range := Node.Sloc_Range;
      Unit       : constant Analysis.Analysis_Unit := Node.Unit;
      Path       : constant String := Simple_Name (Unit.Get_Filename);
   begin
      Print_Diagnostic ((Sloc_Range, To_Unbounded_Text (Message)), Unit, Path);
   end Print_Lkt_Toolbox_Diagnostic;

   ---------------------------
   -- Print_Semantic_Result --
   ---------------------------

   procedure Print_Semantic_Result
     (S : Analysis.Semantic_Result; Unit : Analysis.Analysis_Unit)
   is
      Node : constant Lkt_Node'Class := Analysis.Node (S);
   begin
      if Analysis.Error_Message (S) /= "" then
         declare
            Diag : constant Diagnostic :=
              (Node.Sloc_Range,
               To_Unbounded_Text (Analysis.Error_Message (S)));
         begin
            if Arg.Flag_Invalid.Get then
               --  Emit an error if the declaration including ``Node`` has no
               --  ``@invalid`` annotation. Update ``Invalid_Decl_Map``
               --  otherwise.
               if Node.P_Topmost_Invalid_Decl.Is_Null then
                  Set_Exit_Status (1);

                  Print_Lkt_Toolbox_Diagnostic
                    (Node,
                     "unexpected diagnostic, is @invalid annotation missing?");
               else
                  Invalid_Decl_Map (Node.P_Topmost_Invalid_Decl) := True;
               end if;
            end if;

            Print_Diagnostic
              (Diag, Unit, Simple_Name (Node.Unit.Get_Filename));
         end;
      elsif
        not Arg.Check_Only.Get
        and then not Analysis.Result_Type (S).Is_Null
      then
         Put_Line ("Expr " & Node.Image);
         Put_Line ("     has type " & Analysis.Result_Type (S).Image);
         New_Line;
      elsif
      not Arg.Check_Only.Get
        and then not Analysis.Result_Decl (S).Is_Null
      then
         Put_Line ("Id   " & Node.Image);
         Put_Line
           ("     references " & Format_Node (Analysis.Result_Decl (S)));
         New_Line;
      end if;
   end Print_Semantic_Result;

   -------------------------------
   -- Populate_Invalid_Decl_Map --
   -------------------------------

   function Populate_Invalid_Decl_Map
     (Node : Analysis.Lkt_Node'Class) return Common.Visit_Status
   is
      use type Common.Lkt_Node_Kind_Type;
   begin
      --  Populate ``Invalid_Decl_Map`` with declarations annotated with
      --  ``@invalid``.

      if Node.Kind = Common.Lkt_Full_Decl
         and then Node.As_Full_Decl.P_Has_Annotation
           (To_Unbounded_Text ("invalid"))
      then
         --  ``P_Topmost_Invalid_Decl`` should return the same node. In that
         --  case, include this node in the map, otherwise nested ``@invalid``
         --  declarations have been detected: emit a diagnostic.

         if Invalid_Decl_Map.Contains (Node.P_Topmost_Invalid_Decl) then
            Set_Exit_Status (1);

            Print_Lkt_Toolbox_Diagnostic (Node, "nested @invalid declaration");
         else
            Invalid_Decl_Map.Include (Node.As_Lkt_Node, False);
         end if;
      end if;
      return Common.Into;
   end Populate_Invalid_Decl_Map;

   Ctx : constant Analysis.Analysis_Context := Analysis.Create_Context;
begin
   GNATCOLL.Traces.Parse_Config_File;

   if Arg.Parser.Parse then
      for File_Name of Arg.Files.Get loop
         declare
            File_Name_Str : constant String := To_String (File_Name);
            Unit          : constant Analysis.Analysis_Unit :=
               Ctx.Get_From_File (File_Name_Str);
         begin
            if not Arg.Check_Only.Get then
               Put_Line ("Resolving " & File_Name_Str);
               Put_Line ((File_Name_Str'Length + 10) * "=");
            end if;

            if Unit.Diagnostics'Length > 0 then
               for Diagnostic of Unit.Diagnostics loop
                  Print_Diagnostic
                    (Diagnostic, Unit, Simple_Name (Unit.Get_Filename));
               end loop;
               return;
            end if;

            if Arg.Flag_Invalid.Get then
               Unit.Root.Traverse (Populate_Invalid_Decl_Map'Access);
            end if;

            declare
               Diags : constant Analysis.Tree_Semantic_Result :=
                 Unit.Root.As_Langkit_Root.P_Check_Semantic;
            begin
               for D of Analysis.Results (Diags) loop
                  Print_Semantic_Result (D, Unit);
               end loop;
            end;

            if Arg.Flag_Invalid.Get then

               --  Ensure that all ``@invalid`` declarations in the map have
               --  corresponding diagnostics. Otherwise, emit an error.

               for E in Invalid_Decl_Map.Iterate loop
                  if not Invalid_Decl_Maps.Element (E) then
                     Set_Exit_Status (1);

                     Print_Lkt_Toolbox_Diagnostic
                       (Invalid_Decl_Maps.Key (E),
                        "@invalid declaration without diagnostic");
                  end if;
               end loop;

            end if;

         end;
      end loop;
   end if;
exception
   when E : Common.Property_Error =>
      Put_Line (Ada.Exceptions.Exception_Message (E));
      Put_Line (GNAT.Traceback.Symbolic.Symbolic_Traceback (E));
end Lkt_Toolbox;
