-- One Billion Row Challenge - Implement a silly and trivial programming challenge.
-- Copyright (C) 2024 Prince Trippy <programmer@verisimilitudes.net>.

-- This program is free software: you can redistribute it and/or modify it under the terms of the
-- GNU Affero General Public License version 3 as published by the Free Software Foundation.

-- 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 Affero General Public License for more details.

-- You should have received a copy of the GNU Affero General Public License along with this program.
-- If not, see <http://www.gnu.org/licenses/>.

-- This program requires Ada 2012 purely for want of a predefined sorting procedure and naught else.

with Ada.Text_IO, Ada.Strings.Fixed, Ada.Command_Line, Ada.Containers.Generic_Array_Sort;
with Trivial_Trie;

procedure Billion_Row_Challenge is
   Billion : constant := 1_000_000_000;
   subtype Count is Natural range 0 .. Billion;
   type Index is new Natural range 0 .. 10_000; -- This is the guaranteed maximum count of stations.

   -- These were fixed point types, but the challenge demands those peculiarities of floating point.
   -- It would be interesting to test and look if decimal fixed point would get the correct results.
   type Temperature is digits 3 range -99.9 .. 99.9;
   type Average is digits 12 range -99.9 * Billion .. 99.9 * Billion;

   -- I wanted to use the F'Image attribute for this, but it doesn't work so well as in fixed point.
   package Temperature_IO is new Ada.Text_IO.Float_IO(Temperature);
   package Average_IO is new Ada.Text_IO.Float_IO(Average);

   type Indirect_String is access String;
   type Stations is array (Index range <>) of Indirect_String; -- A sort needs unconstrained arrays.

   type Intermediate is record
      Minimum, Maximum : Temperature;
      Sum : Average;
      Amount : Count;
   end record;

   function Sort_Indirect_Strings (Left, Right : Indirect_String) return Boolean is
   begin
      return Left.all < Right.all;
   end Sort_Indirect_Strings;

   package Trie is new Trivial_Trie(Index_Type => Positive, Domain_Type => Character,
                                    Input_Type => String,   Stored_Type => Intermediate);

   procedure Sort is new Ada.Containers.Generic_Array_Sort
     (Index_Type => Index,    Element_Type => Indirect_String,
      Array_Type => Stations, "<" => Sort_Indirect_Strings);

   Tray : Trie.Trie;
   Them : Stations(1 .. Index'Last);
   Size : Index := 0;
   File : Ada.Text_IO.File_Type;
begin
   Ada.Command_Line.Set_Exit_Status(Ada.Command_Line.Failure);
   if Ada.Command_Line.Argument_Count /= 1 then
      Ada.Text_IO.Put_Line(Ada.Text_IO.Standard_Error, "Supply only one input file.");
      return;
   end if;
   Ada.Text_IO.Open(File, Mode => Ada.Text_IO.In_File, Name => Ada.Command_Line.Argument(1));
   Ada.Text_IO.Set_Input(File);
   while not Ada.Text_IO.End_Of_File loop
      declare
         Line : String(1 .. 108); -- This is that guaranteed, maximum input line length incremented.
         Here : Boolean;
         Ends : Natural;
         Stop : Positive;
         Heat : Temperature;
         That : Intermediate;
      begin
         Ada.Text_IO.Get_Line(Line, Ends);
         Stop := Ada.Strings.Fixed.Index(Source => Line(1 .. Ends), Pattern => ";",
                                         Going => Ada.Strings.Backward);
         Heat := Temperature'Value(Line(Stop + 1 .. Ends));
         Trie.Find(Path => Line(1 .. Stop - 1), Tree => Tray, Seen => Here, Unto => That);
         if not Here then
            Size := Size + 1;
            Them(Size) := new String'(Line(1 .. Stop - 1));
            That := (Minimum | Maximum => Heat, Sum => 0.0, Amount => 0);
         end if;
         That.Minimum := Temperature'Min(That.Minimum, Heat);
         That.Maximum := Temperature'Max(That.Maximum, Heat);
         That.Amount := That.Amount + 1;
         That.Sum := That.Sum + Average(Heat);
         Trie.Give(Path => Line(1 .. Stop - 1), Tree => Tray, What => That);
      end;
   end loop;
   Sort(Them(1 .. Size));
   Ada.Text_IO.Put('{');
   for I in Index range 1 .. Size loop
      declare
         Station : String := Them(I).all;
         Here : Boolean;
         That : Intermediate;
      begin -- A superfluous space will be output rather than a minus sign for the positive numbers.
         Trie.Find(Path => Station, Tree => Tray, Seen => Here, Unto => That);
         Ada.Text_IO.Put(Station);
         Ada.Text_IO.Put('=');
         Temperature_IO.Put(Item => That.Minimum, Fore => 2, Aft => 1, Exp => 0);
         Ada.Text_IO.Put('/');
         Average_IO.Put(Item => That.Sum / Average(That.Amount), Fore => 2, Aft => 1, Exp => 0);
         Ada.Text_IO.Put('/');
         Temperature_IO.Put(Item => That.Maximum, Fore => 2, Aft => 1, Exp => 0);
         if I /= Size then Ada.Text_IO.Put(", "); end if;
      end;
   end loop;
   Ada.Text_IO.Put('}');
   Ada.Text_IO.Flush(Ada.Text_IO.Standard_Output);
   Ada.Text_IO.Close(File);
   Ada.Command_Line.Set_Exit_Status(Ada.Command_Line.Success);
exception
   when Storage_Error =>
      Ada.Text_IO.Put_Line(Ada.Text_IO.Standard_Error, "Working storage was exhausted.");
   when Constraint_Error =>
      Ada.Text_IO.Put_Line(Ada.Text_IO.Standard_Error,
                           "A range check failed or an input line was malformed.");
   when others => Ada.Text_IO.Put_Line(Ada.Text_IO.Standard_Error, "A file error occurred.");
end Billion_Row_Challenge;