with Ada.Directories;
with GNAT.OS_Lib;

with AAA.Strings;

with Alire_Early_Elaboration;
pragma Unreferenced (Alire_Early_Elaboration);

with Alire.Environment;
with Alire.OS_Lib;            use Alire.OS_Lib;
with Alire.OS_Lib.Download;
with Alire.Settings.Builtins.Windows;
with Alire.Errors;

with CLIC.User_Input;

package body Alire.Platforms.Current is

   --  Windows implementation

   Distrib_Detected : Boolean := False;
   Distrib : Platforms.Distributions := Platforms.Distribution_Unknown;

   ------------------
   -- Detect_Msys2 --
   ------------------

   function Detect_Msys2 return Boolean is
   begin
      --  Try to detect if Msys2's pacman tool is already in path
      return OS_Lib.Locate_Exec_On_Path ("pacman") /= "";
   end Detect_Msys2;

   -----------------------
   -- Detect_Msys2_Root --
   -----------------------

   function Detect_Msys2_Root return Absolute_Path is
      Result : constant String := OS_Lib.Subprocess.Locate_In_Path ("pacman");
   begin
      if Result /= "" then
         return GNAT.OS_Lib.Normalize_Pathname
           (Ada.Directories.Containing_Directory (Result) / ".." / "..");
      else
         Raise_Checked_Error ("Cannot locate pacman in msys2 distrib");
      end if;
   end Detect_Msys2_Root;

   --------------------
   -- Detect_Distrib --
   --------------------

   procedure Detect_Distrib is
   begin
      Distrib_Detected := True;

      if Detect_Msys2 then
         Distrib := Platforms.Msys2;
         return;
      end if;

      Distrib := Platforms.Distribution_Unknown;
   end Detect_Distrib;

   ------------------
   -- Distribution --
   ------------------

   function Detected_Distribution return Platforms.Distributions is
   begin
      if not Distrib_Detected then
         Detect_Distrib;
      end if;

      return Distrib;
   end Detected_Distribution;

   -----------------------
   -- Distribution_Root --
   -----------------------

   function Distribution_Root return Absolute_Path is
   begin
      case Distribution is

         when Platforms.Msys2 =>
            return Detect_Msys2_Root;

         when others =>
            declare
               Root : constant String := OS_Lib.Getenv ("HOMEDRIVE", "C:\");
            begin
               if Root'Length not in 2 | 3 then
                  Raise_Checked_Error
                    ("$HOMEDRIVE is not a proper drive: " & Root);
               end if;

               if Root (Root'Last) not in '/' | '\' then
                  return Root & '\';
               else
                  return Root;
               end if;
            end;

      end case;
   end Distribution_Root;

   ----------------------
   -- Load_Environment --
   ----------------------

   procedure Load_Environment (Ctx : in out Alire.Environment.Context) is
   begin
      case Distribution is

         when Platforms.Msys2 =>
            declare
               Root : constant Absolute_Path := Detect_Msys2_Root;
            begin

               Ctx.Append ("PATH", Root / "mingw64" / "bin", "msys2");
               Ctx.Append ("PATH", Root / "usr" / "bin", "msys2");
               Ctx.Append ("PATH", Root / "usr" / "local" / "bin", "msys2");

               Ctx.Append ("LIBRARY_PATH", Root / "mingw64" / "lib", "msys2");

               Ctx.Append ("C_INCLUDE_PATH", Root / "mingw64" / "include",
                           "msys2");
               Ctx.Append ("CPLUS_INCLUDE_PATH", Root / "mingw64" / "include",
                           "msys2");
            end;

         when others =>
            null;

      end case;

   end Load_Environment;

   ----------------------
   -- Operating_System --
   ----------------------

   function Operating_System return Operating_Systems is (Windows);

   -------------------
   -- Set_Msys2_Env --
   -------------------

   procedure Set_Msys2_Env (Install_Dir : Alire.Absolute_Path) is
   begin
      --  Change PATH to have msys2 binaries available (unzip, curl, git, etc.)
      Setenv ("PATH", Install_Dir / "usr" / "bin" &
                ";" & Install_Dir / "usr" / "local" / "bin" &
                ";" & Getenv ("PATH"));
   end Set_Msys2_Env;

   ----------------------------------
   -- Query_User_For_Msys2_Install --
   ----------------------------------

   function Query_User_For_Msys2_Install
     (Install_Dir : Alire.Absolute_Path)
      return Boolean
   is
      use CLIC.User_Input;
   begin

      if Settings.Builtins.Windows.Msys2_Do_Not_Install.Get then

         --  User already requested that msys2 should not be installed

         Trace.Detail ("Alire is configured not to install msys2.");
         Trace.Detail
           ("Run 'alr settings --global --set msys2.do_not_install false'" &
              " if you want Alire to install msys2.");
         return False;
      end if;

      Trace.Always ("Alire can use the msys2 Windows system package" &
                      " manager to provide easy install");
      Trace.Always ("of tools (git, unzip, make, etc.) as well as" &
                      " libraries (libsdl, libusb, etc.)");

      Trace.Always
        ("The use of msys2 is recommend for a better user experience.");

      Trace.Always ("(msys2 will be installed in '" & Install_Dir & "').");

      if Query ("Do you want Alire to install msys2? (recommended)",
                Valid    => (Yes | No => True, others => False),
                Default  => Yes) = Yes
      then

         --  We can install
         return True;
      else

         if Query ("Do you want Alire to remember this choice?",
                   Valid    => (Yes | No => True, others => False),
                   Default  => No) = Yes
         then
            --  Save user choice in the global config
            Settings
              .Builtins.Windows.Msys2_Do_Not_Install.Set_Globally ("true");
         end if;

         --  We are not allowed to install
         return False;
      end if;
   end Query_User_For_Msys2_Install;

   -------------------
   -- Install_Msys2 --
   -------------------

   function Install_Msys2 (Install_Dir : Alire.Absolute_Path)
                           return Alire.Outcome
   is
      use AAA.Strings;

      Msys2_Installer : constant String :=
                          Settings.Builtins.Windows.Msys2_Installer.Get;

      Msys2_Installer_URL : constant String :=
                            Settings.Builtins.Windows.Msys2_Installer_URL.Get;

      Result : Alire.Outcome;
   begin
      if not Query_User_For_Msys2_Install (Install_Dir) then
         --  User does not want to install msys2
         return Alire.Outcome_Success;
      end if;

      --  Prevent unwilling installation of msys2 during testsuite runs
      if OS_Lib.Getenv (Environment.Testsuite, "unset") /= "unset" then
         raise Program_Error
           with "Attempting to install msys2 during testsuite run";
      end if;

      Result := Alire.OS_Lib.Download.File (Msys2_Installer_URL,
                                            Msys2_Installer,
                                            Install_Dir);
      if not Result.Success then
         return Result;
      end if;

      begin
         --  Run msys2's installer
         Alire.OS_Lib.Subprocess.Checked_Spawn
           (Install_Dir / Msys2_Installer,
            Empty_Vector &
              "in" &
              "--confirm-command" &
              "--accept-messages" &
              "--root" &
              Install_Dir);

      exception
         when others =>
            return Alire.Outcome_Failure ("Cannot setup msys2 environment");
      end;

      if Settings.Builtins.Windows.Msys2_Install_Dir.Is_Empty then
         --  Save msys2 install dir in the global config
         Settings
           .Builtins.Windows.Msys2_Install_Dir.Set_Globally (Install_Dir);
      end if;

      --  Load msys2 environment to attempt first full update according to
      --  official setup instructions at:
      --  https://www.msys2.org/wiki/MSYS2-installation/
      declare
         Cfg_Install_Dir : constant String :=
                             Settings.Builtins.Windows.Msys2_Install_Dir.Get;
      begin
         Set_Msys2_Env (Cfg_Install_Dir);
      end;

      --  Run full updates until nothing pending, according to docs.
      --  If something fails we can force going ahead in case we don't need
      --  msys2, and this will enable a first run to "succeed".
      declare
         Update_Attempts : Natural  := 1;
         Max_Attempts    : constant := 5;
      begin
         loop
            Trace.Info ("Updating MSYS2 after installation...");
            Alire.OS_Lib.Subprocess.Checked_Spawn
              ("pacman",
               AAA.Strings.Empty_Vector
               & "--noconfirm"
               & "-Syuu");

            --  Exit when no updates pending. This command may fail with exit
            --  code /= 0 even though there is no real error, when there is
            --  a missing database that will be fetched properly by the next
            --  update.
            Trace.Info ("Querying MSYS2 for pending updates...");
            declare
               Output : AAA.Strings.Vector;
               Code   : constant Integer :=
                          Alire.OS_Lib.Subprocess.Unchecked_Spawn_And_Capture
                            ("pacman",
                             AAA.Strings.Empty_Vector
                             & "--noconfirm"
                             & "-Qu",
                             Output,
                             Err_To_Out => True
                            );
            begin
               --  So not to unnecessarily worry users, as this is expected and
               --  benign in some cases, we don't show it unless this is the
               --  last attempt before bailing out:
               if Code /= 0 then
                  Trace.Log ("MSYS2 ended with non-zero exit status: "
                             & AAA.Strings.Trim (Code'Image),
                             (if Update_Attempts > Max_Attempts
                              then Trace.Warning
                              else Trace.Debug));
               end if;

               exit when Update_Attempts > Max_Attempts -- safeguard JIC
                 or else AAA.Strings.Trim (Output.Flatten) = "";
            end;

            Update_Attempts := Update_Attempts + 1;
         end loop;
      exception
         when E : Checked_Error =>
            Log_Exception (E);
            Recoverable_User_Error ("While updating msys2 after installation: "
                                    & Errors.Get (E, Clear => False));
      end;

      return Alire.Outcome_Success;
   end Install_Msys2;

   -----------------
   -- Setup_Msys2 --
   -----------------

   procedure Setup_Msys2 is
      Result : Alire.Outcome;

      Cfg_Install_Dir : constant String :=
                          Settings.Builtins.Windows.Msys2_Install_Dir.Get;

      Pacman : constant String :=
                 Alire.OS_Lib.Subprocess.Locate_In_Path ("pacman");

   begin
      if Pacman /= "" then
         --  pacman already in PATH, no need to install msys2
         Set_Msys2_Env (GNAT.OS_Lib.Normalize_Pathname
                        (Ada.Directories.Containing_Directory
                           (Pacman) / ".." / ".."));
         return;
      end if;

      if not Alire.Check_Absolute_Path (Cfg_Install_Dir) then
         --  This error is recoverable as msys2 is not required for alr to
         --  work.
         Alire.Recoverable_User_Error
           ("Invalid absolute install path for msys2 in configuration:" &
              " '" & Cfg_Install_Dir & "'");
         return;
      end if;

      --  Check if msys2 is already installed for Alire
      if not Ada.Directories.Exists (Cfg_Install_Dir) then

         --  Msys2 is not installed yet
         Result := Install_Msys2 (Cfg_Install_Dir);
         if not Result.Success then
            --  This error is recoverable as msys2 is not required for alr to
            --  work.
            Alire.Recoverable_User_Error (Message (Result));
            return;
         end if;

      else

         --  Msys2 was already installed and we need to load its environment.
         --  Otherwise the installation procedure already has loaded it for the
         --  first update.

         --  Set the PATH and other environment variable for msys2
         Set_Msys2_Env (Cfg_Install_Dir);

      end if;

   end Setup_Msys2;

   ----------------
   -- Initialize --
   ----------------

   procedure Initialize is
   begin
      Setup_Msys2;
   end Initialize;

   ---------------------
   -- Running_As_Root --
   ---------------------

   function Running_As_Root return Boolean is (False);
   --  Perhaps we can do something better here in the future, for now we use it
   --  only on UNIXes to not add `sudo` when unneeded.

end Alire.Platforms.Current;
