with Ada.Text_IO;
with Ada.Characters.Latin_1;
with Ada.Environment_Variables;
with Ada.Numerics.Discrete_Random;
with Ada.Numerics.Float_Random;
with Ada.Calendar;
with Ada.Strings.Maps;
with Ada.Strings.Maps.Constants;
with Ada.Directories;
with Interfaces.C;

with Commands;

package body Support_Routines is

   --  local function and procedures declarations
   function Matched_Extension (Extension_List : in Tool_List.Vector;
                               Filename : String;
                               Option_Ignore_Extension_Case : Boolean)
                              return Tool;

   Whitespace : constant Ada.Strings.Maps.Character_Set
     := Ada.Strings.Maps.To_Set (Sequence => (' ', Ada.Characters.Latin_1.HT));

   function Split_Comma (Source : in String)
                        return UString_List.Vector;
   function Home_Directory return String;
   function Shell_Fix (File : String) return String;
   --  end of declarations

   procedure Error (Error_String : String) is
      use Ada.Text_IO;
   begin
      Put_Line (Standard_Error, N ("music123: ") & Error_String);
      Put_Line (Standard_Error, Version);
      Put_Line (Standard_Error, N ("usage: music123 [-hqrvz] files ..."));
      Put_Line (Standard_Error, N ("-h     This help"));
      Put_Line (Standard_Error, N ("-q     Run quiet"));
      Put_Line (Standard_Error, N ("-i     Ignore extension case"));
      Put_Line (Standard_Error, N ("-L     List files and exit"));
      Put_Line (Standard_Error, N ("-r     Recurse over directories"));
      Put_Line (Standard_Error, N ("-v     Print the version and exit"));
      Put_Line (Standard_Error, N ("-z     Randomize the filelist"));
      New_Line (Standard_Error);
      Put_Line (Standard_Error, N ("See the manpage for more options."));
   end Error;

   No_Home_Directory : exception;

   function Home_Directory return String is
      use Ada.Environment_Variables;
   begin
      if Exists ("HOME") then
         return Value ("HOME");
      else
         raise No_Home_Directory;
      end if;
   end Home_Directory;

   procedure Import_Conffile (Program_List : in out Tool_List.Vector)
   is
      use Ada.Text_IO;
      Error_String : constant String
        := N ("Neither /etc/music123rc or ~/.music123rc found. Exiting.");
      Conf_File : File_Type;
   begin
      Program_List.Clear;
      begin
         Open (Conf_File, In_File, Home_Directory & "/.music123rc");
      exception
         when others =>
            begin
               Open (Conf_File, In_File, "/etc/music123rc");
            exception
               when others =>
                  Error (Error_String);
                  raise Noted_Error;
            end;
      end;
      while not End_Of_File (Conf_File) loop
         declare
            Line : constant String := Get_Line (Conf_File);
            use Ada.Strings.Fixed;
            P_F, E_F, O_F : Positive;
            P_L, E_L, O_L : Natural;
         begin
            if Head (Line, 4) = "tool" then
               Find_Token (Source => Line, From => Line'First + 4,
                           Test => Ada.Strings.Outside, Set => Whitespace,
                           First => P_F, Last => P_L);
               Find_Token (Source => Line, From => P_L + 1,
                           Test => Ada.Strings.Outside, Set => Whitespace,
                           First => E_F, Last => E_L);
               O_F := Index (Source => Line, From => E_L + 1,
                             Pattern => """") + 1;
               O_L := Index (Source => Line, From => O_F, Pattern => """") - 1;
               Program_List.Append
                 ((Program_Length => P_L - P_F + O_L - O_F + 3,
                   Program        => Line (P_F .. P_L) & " "
                     & Line (O_F .. O_L),
                   Extension_List => Split_Comma (Line (E_F .. E_L))));
            end if;
         end;
      end loop;
      Close (Conf_File);
   exception
      when others =>
         if Is_Open (Conf_File) then
            Close (Conf_File);
         end if;
         Put_Line ("Warning: unable to parse configuration file.");
         raise;
   end Import_Conffile;

   Null_Tool : constant Tool := (Program_Length => 9, Program => "/dev/null",
                                 Extension_List => UString_List.Empty_Vector);

   function Matched_Extension (Extension_List : in Tool_List.Vector;
                               Filename : String;
                               Option_Ignore_Extension_Case : Boolean)
                              return Tool is
      use Ada.Strings.Fixed;
      use Ada.Strings.Maps;
      use Ada.Strings.Maps.Constants;
      Mapping : constant Character_Mapping
        := (if Option_Ignore_Extension_Case then Lower_Case_Map else Identity);
   begin
      for This_Tool of Extension_List loop
         for Extension of This_Tool.Extension_List loop
            if Translate (Tail (Filename, Extension'Length), Mapping)
              = Translate (Extension, Mapping)
            then
               return This_Tool;
            end if;
         end loop;
      end loop;
      return Null_Tool;

   end Matched_Extension;

   function Check_Filename (Full_Name : String;
                            Extension_List : Tool_List.Vector;
                            Option_Ignore_Extension_Case : Boolean)
                           return Boolean is
      use Ada.Directories;
   begin
      return Exists (Full_Name) and then
        (Kind (Full_Name) = Directory or else
           Matched_Extension (Extension_List,
                              Full_Name,
                              Option_Ignore_Extension_Case) /= Null_Tool);
   end Check_Filename;

   procedure Expand_And_Check_Filenames
     (File_List : in out UString_List.Vector;
      Option_Recurse : in Boolean;
      Extension_List : in Tool_List.Vector;
      Option_Ignore_Extension_Case : in Boolean
     )
   is
      use Ada.Directories;
      Temporary_File_List : UString_List.Vector;
      procedure Check_And_Append (Directory_Entry : in Directory_Entry_Type);
      procedure Check_And_Append (Directory_Entry : in Directory_Entry_Type)
      is
         --  Adding, then removing the file was
         --  creating a O(N^2) result. Check it first.
         Simple : constant String := Simple_Name (Directory_Entry);
         Full_Path : constant String := Full_Name (Directory_Entry);
      begin
         if Simple /= "."
           and then Simple /= ".."
           and then Check_Filename (Full_Path,
                                    Extension_List,
                                    Option_Ignore_Extension_Case)
         then
            Temporary_File_List.Append (Full_Path);
         end if;
      end Check_And_Append;
      I : Integer;
      Error_String : constant String :=
        N ("The config file (~/.music123rc or /etc/music123rc) is corrupt.");
   begin
      if Extension_List.Is_Empty then
         Error (Error_String);
         raise Noted_Error;
      end if;
      I := File_List.First_Index;
      while I <= File_List.Last_Index loop
         declare
            Current_File : constant String := File_List.Element (I);
         begin
            if not Exists (Current_File) then
               File_List.Delete (I);
            elsif Kind (Current_File) = Directory then
               File_List.Delete (I);
               if Option_Recurse then
                  begin
                     Temporary_File_List.Clear;
                     Search (Directory => Current_File,
                             Pattern => "",
                             Process => Check_And_Append'Access);
                     File_Sorting.Sort (Temporary_File_List);
                     File_List.Append (Temporary_File_List);
                     Temporary_File_List.Clear;
                  exception
                     --  Name_Error should not happen here
                     when Use_Error => null;   --  no recurse after all
                  end;
               end if;
            else
               I := I + 1;
            end if;
         end;
      end loop;
      if File_List.Is_Empty then
         Error (N ("No valid filenames found."));
         raise Noted_Error;
      end if;

   end Expand_And_Check_Filenames;

   procedure Randomize_Names (File_List : in out UString_List.Vector) is
      use Ada.Calendar;
      use Ada.Numerics.Float_Random;
      J : Positive;
      Gen : Generator;
   begin
      Reset (Gen, Integer (Seconds (Clock) * 10.0));
      --  From Knuth, TAOCP vol. 2, edition 3, page 146, 3.4.2, algorithm P
      for I in reverse File_List.First_Index + 1 .. File_List.Last_Index loop
         J := Integer
           (Float'Floor (Random (Gen)
                           * Float (I + File_List.First_Index - 1)))
           + File_List.First_Index;
         File_List.Swap (I, J);
      end loop;
   end Randomize_Names;

   function Shell_Fix (File : String) return String is
   begin
      if File'Length = 1 then
         if File = "'" then
            return "'""'""'";
         else
            return File;
         end if;
      elsif File (File'First) = ''' then
         return "'""'""'" & Shell_Fix (File (File'First + 1 .. File'Last));
      else
         return File (File'First) &
           Shell_Fix (File (File'First + 1 .. File'Last));
      end if;
   end Shell_Fix;

   procedure Display_Songs (File_List : in UString_List.Vector) is
   begin
      for Song of File_List loop
         Ada.Text_IO.Put_Line (Song);
      end loop;
   end Display_Songs;

   procedure Play_Songs
     (File_List : in out UString_List.Vector;
      Program_List : in Tool_List.Vector;
      Delay_Length : in Duration;
      Option_Quiet : in Boolean;
      Option_Loop : in Boolean;
      Option_Random : in Boolean;
      Option_Eternal_Random : in Boolean;
      Option_Ignore_Extension_Case : in Boolean
     ) is

      procedure Play_A_Song (File_Name : in String);
      procedure Play_A_Song (File_Name : in String) is
         use Interfaces.C;
         function System (command : char_array) return int;
         pragma Import (C, System, "system");
         This_Program : constant Tool := Matched_Extension
           (Program_List,
            File_Name,
            Option_Ignore_Extension_Case);
         System_String : constant String :=
           This_Program.Program & " '" & Shell_Fix (File_Name) & "'"
           & (if Option_Quiet then " > /dev/null 2> /dev/null" else "");
         System_Result : constant int := System (To_C (System_String));
      begin
         if System_Result /= 0 then
            Ada.Text_IO.Put_Line ("Command """ & System_String
                                    & """ exited with non zero status ("
                                    & int'Image (System_Result) & ").");
         end if;
      end Play_A_Song;

   begin
      if Option_Eternal_Random then
         declare
            subtype S is Integer
              range File_List.First_Index .. File_List.Last_Index;
            package S_Random is new Ada.Numerics.Discrete_Random (S);
            use S_Random;
            use Ada.Calendar;
            Gen : Generator;
         begin
            Reset (Gen, Integer (Seconds (Clock) * 10.0));
            while not Commands.Repository.Have_To_Quit loop
               Play_A_Song (File_List.Element (Random (Gen)));
            end loop;
         end;
      end if;

      if Option_Random then
         Randomize_Names (File_List);
      end if;

      loop
         for Song of File_List loop
            exit when Commands.Repository.Have_To_Quit;
            Play_A_Song (Song);
            delay Delay_Length;
         end loop;
         exit when Commands.Repository.Have_To_Quit;
         exit when not Option_Loop;
      end loop;
   end Play_Songs;

   procedure Read_Playlist (Full_Name : String;
                            File_List : in out UString_List.Vector) is
      use Ada.Text_IO;
      Playlist : File_Type;
   begin
      begin
         Open (Playlist, In_File, Full_Name);
      exception
         when others =>
            Error (N ("Playlist file not found."));
            raise Noted_Error;
      end;
      while not End_Of_File (Playlist) loop
         declare
            Line : constant String :=
              Ada.Strings.Fixed.Trim (Get_Line (Playlist), Ada.Strings.Both);
         begin
            if Line /= "" and then Line (1) /= '#' then
               File_List.Append (Line);
            end if;
         end;
      end loop;
      Close (Playlist);
   exception
      when others =>
         if Is_Open (Playlist) then
            Close (Playlist);
         end if;
         Put_Line ("Warning: unable to parse playlist.");
         raise;
   end Read_Playlist;

   function Split_Comma (Source : in String)
                        return UString_List.Vector is
      First       : Positive := Source'First;
      Comma_Index : Natural;
   begin
      return Result : UString_List.Vector do
         loop
            Comma_Index := Ada.Strings.Fixed.Index (Source  => Source,
                                                    Pattern => ",",
                                                    From    => First);
            exit when Comma_Index = 0;
            Result.Append ("." & Source (First .. Comma_Index - 1));
            First := Comma_Index + 1;
         end loop;
         Result.Append ("." & Source (First .. Source'Last));
      end return;
   end Split_Comma;

end Support_Routines;
