File : build_mpd_file.adb


------------------------------------------------------------------------------
--
--  procedure Build_MPD_File (body)
--
--  This program builds a multi-part dat (MPD) file based on the file named as
--  the command line argument. All referenced model files that can be found in
--  the current catalog will be added.
--
--  Command line arguments:
--    "-model" <Model file name> - Main file (.ldr or .dat is not neccessary).
--    "-path" <Directory-models> ... <Directory>
--                               - Directories where the MPD builder should
--                                 look for sub-models (optional).
--    "-overwrite"               - Don't check if the MPD file allready
--                                 exists (optional).
--    "-collect"                 - Don't store information about where the
--                                 sub-models are located.
--    "-collect" <Directory>     - Make all files appear in <Directory> when
--                                 the file is splitted.
--
------------------------------------------------------------------------------
--  Update information:
--
--  1999.02.11 (Jacob Sparre Andersen)
--    Written based on Split_LDraw_File (1997.06.20 version).
--
--  1999.09.29 (Jacob Sparre Andersen)
--    Added an "ad" for the MPD Builder to the resulting file.
--    Added the options "-path" and "-collect".
--
--  1999.09.30 (Jacob Sparre Andersen)
--    Terminates the file with a "NOFILE" comment.
--
--  2002.07.17 (Jacob Sparre Andersen)
--    Modified the file name handling, so both ".dat" and ".ldr" are
--      considered as valid extensions for LDraw files.
--    ".ldr" is now the default extension for LDraw files.
--
--  2002.07.31 (Jacob Sparre Andersen)
--    Modified the comment on where the software can be downloaded from.
--
--  2002.08.04 (Jacob Sparre Andersen)
--    Improved the help message.
--
--  2002.08.14 (Jacob Sparre Andersen)
--    Fixed an artistic error in the comment on where the software can be
--      downloaded from.
--
--  (Insert additional update information above this line.)
------------------------------------------------------------------------------
--  Standard packages:

with Ada.Characters.Handling;
with Ada.Strings.Unbounded;
with Ada.Text_IO;

------------------------------------------------------------------------------
--  GNAT packages:
--
--  Used to get Directory_Separator.

with GNAT.OS_Lib;

------------------------------------------------------------------------------
--  Other packages:

with File_System;
with Generic_Command_Line_Processing;
with Generic_Command_Line_Types;
with List_Of_Static_Items_G;
with String_Arrays;
with UStrings;

------------------------------------------------------------------------------

procedure Build_MPD_File is

   ---------------------------------------------------------------------------
   --  Exceptions:

   Bad_LDraw_Command : exception;

   ---------------------------------------------------------------------------
   --  type Argument_Names:

   type Argument_Names is (Help, Model, Path, Overwrite, Collect);

   ---------------------------------------------------------------------------
   --  package Command_Line_Types:

   package Command_Line_Types is
     new Generic_Command_Line_Types (Argument_Names => Argument_Names);

   ---------------------------------------------------------------------------
   --  function U:

   function U (Item : in     String)
     return Ada.Strings.Unbounded.Unbounded_String
     renames Ada.Strings.Unbounded.To_Unbounded_String;

   ---------------------------------------------------------------------------
   --  package Command_Line_Processing:

   package Command_Line_Processing is new Generic_Command_Line_Processing
     (Command_Line_Types  => Command_Line_Types,
      Obligatory          => (Model  => True,
                              others => False),
      Minimum_Field_Count => (Model  | Path => 1,
                              others        => 0),
      Maximum_Field_Count => (Model | Collect => 1,
                              Path            => Natural'Last,
                              others          => 0),
      Help                => (Model     => U (" <Model file name> - " &
                                                """.ldr"" or "".dat"" is " &
                                                "not necessary."),
                              Overwrite => U (" - Don't check if the MPD " &
                                                "file already exists."),
                              Path      => U (" <Directory> ... <Directory> " &
                                                "Directories where the MPD " &
                                                "builder should look for " &
                                                "sub-models."),
                              Collect   => U (" [ <Directory> ] - Make all " &
                                                "files appear in " &
                                                "<Directory> when the file " &
                                                "is splitted."),
                              others    => U (" - Show this message.")));

   ---------------------------------------------------------------------------
   --  package String_Lists:

   package String_Lists is new List_Of_Static_Items_G
     (Item_Type => Ada.Strings.Unbounded.Unbounded_String,
      Equals    => Ada.Strings.Unbounded."=");

   ---------------------------------------------------------------------------
   --  function Is_Meta_Command:

   function Is_Meta_Command (Line      : in     UStrings.UString;
                             Command   : in     String) return Boolean is

      use Ada.Characters.Handling;
      use Ada.Strings;
      use Ada.Strings.Unbounded;

      Buffer    : Unbounded_String;
      Separator : Natural;

   begin --  Is_Meta_Command
      Buffer := Trim (Source => Line,
                      Side   => Both);

      if Length (Buffer) < 3 then
         return False;
      elsif Slice (Source => Buffer,
                Low    => 1,
                High   => 2) = "0 " then
         Delete (Source  => Buffer,
                 From    => 1,
                 Through => 2);
         Separator := Index (Source  => Buffer & " ",
                             Pattern => " ");

         return
           To_Upper (Command) = To_Upper (Slice (Source => Buffer,
                                                 Low    => 1,
                                                 High   => Separator - 1));
      else
         return False;
      end if;
   exception
      when others =>
         Ada.Text_IO.Put_Line
           (File => Ada.Text_IO.Current_Error,
            Item => "Build_MPD_File.Is_Meta_Command: An unexpected " &
                    "exception occured. Aborting...");
         raise;
   end Is_Meta_Command;

   ---------------------------------------------------------------------------
   --  function Is_Subfile_Command:

   function Is_Subfile_Command (Line  : in     UStrings.UString)
     return Boolean is

      use Ada.Characters.Handling;
      use Ada.Strings;
      use Ada.Strings.Unbounded;
      use UStrings;

      Buffer : Unbounded_String;

   begin --  Is_Subfile_Command
      Buffer := Trim (Source => Line,
                      Side   => Both);

      if Length (Buffer) < 3 then
         return False;
      elsif Slice (Source => Buffer,
                   Low    => 1,
                   High   => 2) = "1 " then
         return True;
      else
         return False;
      end if;
   exception
      when others =>
         Ada.Text_IO.Put_Line
           (File => Ada.Text_IO.Current_Error,
            Item => "Build_MPD_File.Is_Subfile_Command: An unexpected " &
                    "exception occured. Aborting...");
         raise;
   end Is_Subfile_Command;

   ---------------------------------------------------------------------------
   --  procedure Split_LDraw_Meta_Command:

   procedure Split_LDraw_Meta_Command (Line      : in     UStrings.UString;
                                       Command   :    out UStrings.UString;
                                       Arguments : in out UStrings.UString) is

      use Ada.Characters.Handling;
      use Ada.Strings;
      use Ada.Strings.Unbounded;

      Buffer    : Unbounded_String;
      Separator : Natural;

   begin --  Split_LDraw_Meta_Command
      Buffer := Trim (Source => Line,
                      Side   => Both);

      if Slice (Source => Buffer,
                Low    => 1,
                High   => 2) = "0 " then
         Delete (Source  => Buffer,
                 From    => 1,
                 Through => 2);
         Separator := Index (Source  => Buffer & " ",
                             Pattern => " ");

         Command := To_Unbounded_String (Slice (Source => Buffer,
                                                Low    => 1,
                                                High   => Separator - 1));
         Arguments := To_Unbounded_String (Slice (Source => Buffer,
                                                  Low    => Separator + 1,
                                                  High   => Length (Buffer)));
      else
         raise Bad_LDraw_Command;
      end if;
   exception
      when Bad_LDraw_Command =>
         raise;
      when others =>
         Ada.Text_IO.Put_Line
           (File => Ada.Text_IO.Current_Error,
            Item => "Build_MPD_File.Split_LDraw_Meta_Command: An " &
                    "unexpected exception occured. Aborting...");
         raise;
   end Split_LDraw_Meta_Command;

   ---------------------------------------------------------------------------
   --  procedure Extract_Subfile_Name:

   procedure Extract_Subfile_Name (Line         : in     UStrings.UString;
                                   Subfile_Name :    out UStrings.UString) is

      use Ada.Characters.Handling;
      use Ada.Strings;
      use Ada.Strings.Unbounded;

      Buffer    : Unbounded_String;
      Separator : Natural;

   begin --  Extract_Subfile_Name
      Buffer := Trim (Source => Line,
                      Side   => Both);

      if Slice (Source => Buffer,
                Low    => 1,
                High   => 2) = "1 " then
         Separator := Index (Source  => Buffer,
                             Pattern => " ",
                             Going   => Backward);
         Subfile_Name := Delete (Source  => Buffer,
                                 From    => 1,
                                 Through => Separator);
         Translate (Source  => Subfile_Name,
                    Mapping => To_Lower'Access);
      else
         raise Bad_LDraw_Command;
      end if;
   exception
      when Bad_LDraw_Command =>
         raise;
      when others =>
         Ada.Text_IO.Put_Line
           (File => Ada.Text_IO.Current_Error,
            Item => "Build_MPD_File.Extract_Subfile_Name: An " &
                    "unexpected exception occured. Aborting...");
         raise;
   end Extract_Subfile_Name;

   ---------------------------------------------------------------------------
   --  File lists:

   Scanned_Files     : String_Lists.List_Type;
   Unavailable_Files : String_Lists.List_Type;
   Unprocessed_Files : String_Lists.List_Type;

   ---------------------------------------------------------------------------
   --  procedure Scan_File:

   procedure Scan_File (File_Name : in     String) is

      use Ada.Text_IO;
      use String_Lists;
      use UStrings;

      Model        : File_Type;
      Current_Line : UString;
      Subfile_Name : UString;

   begin --  Scan_File
      Put (File => Current_Error,
           Item => "Scanning """ & File_Name & """... ");

      Open (File => Model,
            Name => File_Name,
            Mode => In_File);

      while not End_Of_File (File => Model) loop
         Get_Line (File => Model,
                   Item => Current_Line);

         if Is_Subfile_Command (Current_Line) then
            Extract_Subfile_Name (Line         => Current_Line,
                                  Subfile_Name => Subfile_Name);

            Insert (List => Unprocessed_Files,
                    Item => Subfile_Name);
         end if;
      end loop;

      Close (File => Model);

      Put_Line (File => Current_Error,
                Item => "Done.");
   exception
      when Bad_LDraw_Command =>
         Ada.Text_IO.Put_Line
           (File => Ada.Text_IO.Current_Error,
            Item => "Build_MPD_File.Scan_File: There is a bad LDraw " &
                    "command on line " & Count'Image (Line (Model)) &
                    " (or the preceding line). Skips to the next file.");

         Close (File => Model);

         return;
      when others =>
         Ada.Text_IO.Put_Line
           (File => Ada.Text_IO.Current_Error,
            Item => "Build_MPD_File.Scan_File: An unexpected exception " &
                    "occured while processing the file " & File_Name &
                    ". Aborting...");
         raise;
   end Scan_File;

   ---------------------------------------------------------------------------

   use Ada.Characters.Handling;
   use Ada.Strings.Unbounded;
   use Ada.Text_IO;
   use Command_Line_Processing;
   use File_System;
   use String_Arrays;
   use String_Lists;
   use UStrings;

   Model_Name         : UString;
   DAT_Name, MPD_Name : UString;
   DAT_File, MPD_File : File_Type;

   Current_File : File_Type;
   Current_Line : UString;

   Is_Scanned, Is_Unavailable : Boolean;

   Paths : String_Array_Reference;

   Full_File_Name   : UString;
   Found_File       : Boolean;
   Stored_File_Name : UString;

begin --  Build_MPD_File
   if Set (Help) or not Set (Model) then
      Put_Help (File => Standard_Output);
   else
      if Set (Path) then
         Paths := new String_Array_Type (1 .. Field_Count (Path) + 1);

         Paths (1) := U ("");
         for Index in 2 .. Paths'Last loop
            Paths (Index) := U (Value (Path, Index - 1));

            if Element (Paths (Index), Length (Paths (Index)))
                 = GNAT.OS_Lib.Directory_Separator then
               null;
            else
               Append (Source   => Paths (Index),
                       New_Item => GNAT.OS_Lib.Directory_Separator);
            end if;
         end loop;
      else
         Paths := new String_Array_Type (1 .. 1);

         Paths (1) := U ("");
      end if;

      Model_Name := U (To_Lower (Value (Model, 1)));

      if Index (Model_Name, ".dat") > 0 then
         Delete (Source  => Model_Name,
                 From    => Index (Model_Name, ".dat"),
                 Through => Length (Model_Name));
         DAT_Name := Model_Name & ".dat";
      elsif Index (Model_Name, ".ldr") > 0 then
         Delete (Source  => Model_Name,
                 From    => Index (Model_Name, ".ldr"),
                 Through => Length (Model_Name));
         DAT_Name := Model_Name & ".ldr";
      else
         DAT_Name := Model_Name & ".ldr";
      end if;

      MPD_Name := Model_Name & ".mpd";

      Put_Line (File => Current_Error,
                Item => "Collecting """ & S (DAT_Name) & """ and it's " &
                        " submodels into """ & S (MPD_Name) & """...");

      if Set (Overwrite) then
         Create (File => MPD_File,
                 Name => S (MPD_Name),
                 Mode => Out_File);
      elsif File_System.Exists (S (MPD_Name)) then
         Put_Line (File => Current_Error,
                   Item => "There is allready a file named """ &
                           S (MPD_Name) & """. Please use " &
                           "the -overwrite argument if you want to " &
                           "overwrite the existing file.");
         return;
      else
         Create (File => MPD_File,
                 Name => S (MPD_Name),
                 Mode => Out_File);
      end if;

      Put_Line (File => MPD_File,
                Item => "0 Created with the MPD Builder. Can be converted");
      Put_Line (File => MPD_File,
                Item => "0 to LDraw files using the MPD Splitter. Both can");
      Put_Line (File => MPD_File,
                Item => "0 be downloaded from:");
      Put_Line (File => MPD_File,
                Item => "0");
      Put_Line (File => MPD_File,
                Item => "0    http://edb.jacob-sparre.dk/Ada/mpd_files/");
      Put_Line (File => MPD_File,
                Item => "0");
      Put_Line (File => MPD_File,
                Item => "0 L3P, LDLite and LDGLite can process MPD files");
      Put_Line (File => MPD_File,
                Item => "0 directly.");
      New_Line (File => MPD_File);

      Insert (List => Unprocessed_Files,
              Item => DAT_Name);

      Put_Line (File => Current_Error,
                Item => "Scanning LDraw files...");

   Scan_Unprocessed_Files:
      loop
         exit Scan_Unprocessed_Files when Is_Empty (Unprocessed_Files);

         Set_Cursor_At_Front (List => Unprocessed_Files);

      Remove_Scanned_Files:
         loop
            exit Remove_Scanned_Files
               when Is_Cursor_At_Rear (Unprocessed_Files);

            Find_And_Set_Cursor (List  => Scanned_Files,
                                 Item  => Get (Unprocessed_Files),
                                 Found => Is_Scanned);

            if Is_Scanned then
               Remove (List => Unprocessed_Files);
            else
               Move_Cursor_To_Next (List => Unprocessed_Files);
            end if;
         end loop Remove_Scanned_Files;

         Set_Cursor_At_Front (List => Unprocessed_Files);

      Remove_Unavailable_Files:
         loop
            exit Remove_Unavailable_Files
               when Is_Cursor_At_Rear (Unprocessed_Files);

            Find_And_Set_Cursor (List  => Unavailable_Files,
                                 Item  => Get (Unprocessed_Files),
                                 Found => Is_Unavailable);

            if Is_Unavailable then
               Remove (List => Unprocessed_Files);
            else
               Full_File_Name := Get (Unprocessed_Files);
               Find_File (File_Name => Full_File_Name,
                          Path      => Paths,
                          Found_It  => Found_File);

               if Found_File then
                  Move_Cursor_To_Next (List => Unprocessed_Files);
               else
                  Insert (List => Unavailable_Files,
                          Item => Get (Unprocessed_Files));
                  Remove (List => Unprocessed_Files);
               end if;
            end if;
         end loop Remove_Unavailable_Files;

         Set_Cursor_At_Front (List => Unprocessed_Files);

      Scan_Files:
         loop
            exit Scan_Files when Is_Cursor_At_Rear (Unprocessed_Files);

            DAT_Name := Get (List => Unprocessed_Files);
            Remove (List => Unprocessed_Files);

            Find_And_Set_Cursor (List  => Scanned_Files,
                                 Item  => DAT_Name,
                                 Found => Is_Scanned);

            if Is_Scanned then
               null;
            else
               Full_File_Name := DAT_Name;
               Find_File (File_Name => Full_File_Name,
                          Path      => Paths,
                          Found_It  => Found_File);

               if Found_File then
                  Scan_File (File_Name => S (Full_File_Name));
                  Insert (List => Scanned_Files,
                          Item => DAT_Name);
               else
                  Insert (List => Unavailable_Files,
                          Item => DAT_Name);
               end if;
            end if;
         end loop Scan_Files;
      end loop Scan_Unprocessed_Files;

      Put_Line (File => Current_Error,
                Item => "Collecting the LDraw files...");

      Set_Cursor_At_Front (List => Scanned_Files);

   Collect_Files:
      while not Is_Empty (Scanned_Files) loop
         DAT_Name := Get (Scanned_Files);
         Remove (Scanned_Files);

         Full_File_Name := DAT_Name;
         Find_File (File_Name => Full_File_Name,
                    Path      => Paths,
                    Found_It  => Found_File);

         if Found_File then
            if Set (Collect) then
               if Field_Count (Collect) = 0 then
                  Stored_File_Name := DAT_Name;
               else
                  Stored_File_Name := Value (Collect, 1) & DAT_Name;
               end if;
            else
               Stored_File_Name := Full_File_Name;
            end if;

            Put_Line (File => Current_Error,
                      Item => "Adding """ & S (Full_File_Name) & """ to " &
                              "the MPD file as """ & S (Stored_File_Name) &
                              """.");

            Put_Line (File => MPD_File,
                      Item => "0 FILE " & S (Stored_File_Name));

            Open (File => DAT_File,
                  Name => S (Full_File_Name),
                  Mode => In_File);

            while not End_Of_File (File => DAT_File) loop
               Get_Line (File => DAT_File,
                         Item => Current_Line);

               if Is_Meta_Command (Line    => Current_Line,
                                   Command => "File") then
                  Put_Line (File => MPD_File,
                            Item => "0 Was: " & S (Current_Line));
               else
                  Put_Line (File => MPD_File,
                            Item => S (Current_Line));
               end if;
            end loop;

            Close (File => DAT_File);
         end if;
      end loop Collect_Files;

      Put_Line (File => MPD_File,
                Item => "0 NOFILE");
      Close (File => MPD_File);

      Put_Line (File => Current_Error,
                Item => "Done.");
   end if;
exception
   when Bad_LDraw_Command =>
      Put_Line (File => Current_Error,
                Item => "There appears to be a bug in one of the source " &
                        "files.");
   when others =>
      Ada.Text_IO.Put_Line
        (File => Ada.Text_IO.Current_Error,
         Item => "Build_MPD_File: An undocumented exception was raised. " &
                 "Please contact the author of the program with details of " &
                 "what happened. Aborting ...");
         raise;
end Build_MPD_File;