------------------------------------------------------------------------------
--                                                                          --
--                         GNAT COMPILER COMPONENTS                         --
--                                                                          --
--                              T R E E _ I O                               --
--                                                                          --
--                                 B o d y                                  --
--                                                                          --
--          Copyright (C) 1992-2019, Free Software Foundation, Inc.         --
--                                                                          --
-- GNAT is free software;  you can  redistribute it  and/or modify it under --
-- terms of the  GNU General Public License as published  by the Free Soft- --
-- ware  Foundation;  either version 3,  or (at your option) any later ver- --
-- sion.  GNAT is distributed in the hope that it will be useful, but WITH- --
-- OUT ANY WARRANTY;  without even the  implied warranty of MERCHANTABILITY --
-- or FITNESS FOR A PARTICULAR PURPOSE.                                     --
--                                                                          --
-- As a special exception under Section 7 of GPL version 3, you are granted --
-- additional permissions described in the GCC Runtime Library Exception,   --
-- version 3.1, as published by the Free Software Foundation.               --
--                                                                          --
-- You should have received a copy of the GNU General Public License and    --
-- a copy of the GCC Runtime Library Exception along with this program;     --
-- see the files COPYING3 and COPYING.RUNTIME respectively.  If not, see    --
-- <http://www.gnu.org/licenses/>.                                          --
--                                                                          --
-- GNAT was originally developed  by the GNAT team at  New York University. --
-- Extensive contributions were provided by Ada Core Technologies Inc.      --
--                                                                          --
------------------------------------------------------------------------------

with Debug;  use Debug;
with Output; use Output;
with Unchecked_Conversion;

package body Tree_IO is
   Debug_Flag_Tree : Boolean := False;
   --  Debug flag for debug output from tree read/write

   -------------------------------------------
   -- Compression Scheme Used for Tree File --
   -------------------------------------------

   --  We don't just write the data directly, but instead do a mild form
   --  of compression, since we expect lots of compressible zeroes and
   --  blanks. The compression scheme is as follows:

   --    00nnnnnn followed by nnnnnn bytes (non compressed data)
   --    01nnnnnn indicates nnnnnn binary zero bytes
   --    10nnnnnn indicates nnnnnn ASCII space bytes
   --    11nnnnnn bbbbbbbb indicates nnnnnnnn occurrences of byte bbbbbbbb

   --  Since we expect many zeroes in trees, and many spaces in sources,
   --  this compression should be reasonably efficient. We can put in
   --  something better later on.

   --  Note that this compression applies to the Write_Tree_Data and
   --  Read_Tree_Data calls, not to the calls to read and write single
   --  scalar values, which are written in memory format without any
   --  compression.

   C_Noncomp : constant := 2#00_000000#;
   C_Zeros   : constant := 2#01_000000#;
   C_Spaces  : constant := 2#10_000000#;
   C_Repeat  : constant := 2#11_000000#;
   --  Codes for compression sequences

   Max_Count : constant := 63;
   --  Maximum data length for one compression sequence

   --  The above compression scheme applies only to data written with the
   --  Tree_Write routine and read with Tree_Read. Data written using the
   --  Tree_Write_Char or Tree_Write_Int routines and read using the
   --  corresponding input routines is not compressed.

   type Int_Bytes is array (1 .. 4) of Byte;
   for Int_Bytes'Size use 32;

   function To_Int_Bytes is new Unchecked_Conversion (Int, Int_Bytes);
   function To_Int       is new Unchecked_Conversion (Int_Bytes, Int);

   ----------------------
   -- Global Variables --
   ----------------------

   Tree_FD : File_Descriptor;
   --  File descriptor for tree

   Buflen : constant Int := 8_192;
   --  Length of buffer for read and write file data

   Buf : array (Pos range 1 .. Buflen) of Byte;
   --  Read/write file data buffer

   Bufn : Nat;
   --  Number of bytes read/written from/to buffer

   Buft : Nat;
   --  Total number of bytes in input buffer containing valid data. Used only
   --  for input operations. There is data left to be processed in the buffer
   --  if Buft > Bufn. A value of zero for Buft means that the buffer is empty.

   -----------------------
   -- Local Subprograms --
   -----------------------

   procedure Read_Buffer;
   --  Reads data into buffer, setting Bufn appropriately

   function Read_Byte return Byte;
   pragma Inline (Read_Byte);
   --  Returns next byte from input file, raises Tree_Format_Error if none left

   procedure Write_Buffer;
   --  Writes out current buffer contents

   procedure Write_Byte (B : Byte);
   pragma Inline (Write_Byte);
   --  Write one byte to output buffer, checking for buffer-full condition

   -----------------
   -- Read_Buffer --
   -----------------

   procedure Read_Buffer is
   begin
      Buft := Int (Read (Tree_FD, Buf (1)'Address, Integer (Buflen)));

      if Buft = 0 then
         raise Tree_Format_Error;
      else
         Bufn := 0;
      end if;
   end Read_Buffer;

   ---------------
   -- Read_Byte --
   ---------------

   function Read_Byte return Byte is
   begin
      if Bufn = Buft then
         Read_Buffer;
      end if;

      Bufn := Bufn + 1;
      return Buf (Bufn);
   end Read_Byte;

   --------------------
   -- Tree_Read_Bool --
   --------------------

   procedure Tree_Read_Bool (B : out Boolean) is
   begin
      B := Boolean'Val (Read_Byte);

      if Debug_Flag_Tree then
         if B then
            Write_Str ("True");
         else
            Write_Str ("False");
         end if;

         Write_Eol;
      end if;
   end Tree_Read_Bool;

   --------------------
   -- Tree_Read_Char --
   --------------------

   procedure Tree_Read_Char (C : out Character) is
   begin
      C := Character'Val (Read_Byte);

      if Debug_Flag_Tree then
         Write_Str ("==> transmitting Character = ");
         Write_Char (C);
         Write_Eol;
      end if;
   end Tree_Read_Char;

   --------------------
   -- Tree_Read_Data --
   --------------------

   procedure Tree_Read_Data (Addr : Address; Length : Int) is

      type S is array (Pos) of Byte;
      --  This is a big array, for which we have to suppress the warning

      type SP is access all S;

      function To_SP is new Unchecked_Conversion (Address, SP);

      Data : constant SP := To_SP (Addr);
      --  Data buffer to be read as an indexable array of bytes

      OP : Pos := 1;
      --  Pointer to next byte of data buffer to be read into

      B : Byte;
      C : Byte;
      L : Int;

   begin
      if Debug_Flag_Tree then
         Write_Str ("==> transmitting ");
         Write_Int (Length);
         Write_Str (" data bytes");
         Write_Eol;
      end if;

      --  Verify data length

      Tree_Read_Int (L);

      if L /= Length then
         Write_Str ("==> transmitting, expected ");
         Write_Int (Length);
         Write_Str (" bytes, found length = ");
         Write_Int (L);
         Write_Eol;
         raise Tree_Format_Error;
      end if;

      --  Loop to read data

      while OP <= Length loop

         --  Get compression control character

         B := Read_Byte;
         C := B and 2#00_111111#;
         B := B and 2#11_000000#;

         --  Non-repeat case

         if B = C_Noncomp then
            if Debug_Flag_Tree then
               Write_Str ("==>    uncompressed:  ");
               Write_Int (Int (C));
               Write_Str (", starting at ");
               Write_Int (OP);
               Write_Eol;
            end if;

            for J in 1 .. C loop
               Data (OP) := Read_Byte;
               OP := OP + 1;
            end loop;

         --  Repeated zeroes

         elsif B = C_Zeros then
            if Debug_Flag_Tree then
               Write_Str ("==>    zeroes:        ");
               Write_Int (Int (C));
               Write_Str (", starting at ");
               Write_Int (OP);
               Write_Eol;
            end if;

            for J in 1 .. C loop
               Data (OP) := 0;
               OP := OP + 1;
            end loop;

         --  Repeated spaces

         elsif B = C_Spaces then
            if Debug_Flag_Tree then
               Write_Str ("==>    spaces:        ");
               Write_Int (Int (C));
               Write_Str (", starting at ");
               Write_Int (OP);
               Write_Eol;
            end if;

            for J in 1 .. C loop
               Data (OP) := Character'Pos (' ');
               OP := OP + 1;
            end loop;

         --  Specified repeated character

         else -- B = C_Repeat
            B := Read_Byte;

            if Debug_Flag_Tree then
               Write_Str ("==>    other char:    ");
               Write_Int (Int (C));
               Write_Str (" (");
               Write_Int (Int (B));
               Write_Char (')');
               Write_Str (", starting at ");
               Write_Int (OP);
               Write_Eol;
            end if;

            for J in 1 .. C loop
               Data (OP) := B;
               OP := OP + 1;
            end loop;
         end if;
      end loop;

      --  At end of loop, data item must be exactly filled

      if OP /= Length + 1 then
         raise Tree_Format_Error;
      end if;

   end Tree_Read_Data;

   --------------------------
   -- Tree_Read_Initialize --
   --------------------------

   procedure Tree_Read_Initialize (Desc : File_Descriptor) is
   begin
      Buft := 0;
      Bufn := 0;
      Tree_FD := Desc;
      Debug_Flag_Tree := Debug_Flag_5;
   end Tree_Read_Initialize;

   -------------------
   -- Tree_Read_Int --
   -------------------

   procedure Tree_Read_Int (N : out Int) is
      N_Bytes : Int_Bytes;

   begin
      for J in 1 .. 4 loop
         N_Bytes (J) := Read_Byte;
      end loop;

      N := To_Int (N_Bytes);

      if Debug_Flag_Tree then
         Write_Str ("==> transmitting Int = ");
         Write_Int (N);
         Write_Eol;
      end if;
   end Tree_Read_Int;

   -------------------
   -- Tree_Read_Str --
   -------------------

   procedure Tree_Read_Str (S : out String_Ptr) is
      N : Nat;

   begin
      Tree_Read_Int (N);
      S := new String (1 .. Natural (N));
      Tree_Read_Data (S.all (1)'Address, N);
   end Tree_Read_Str;

   -------------------------
   -- Tree_Read_Terminate --
   -------------------------

   procedure Tree_Read_Terminate is
   begin
      --  Must be at end of input buffer, so we should get Tree_Format_Error
      --  if we try to read one more byte, if not, we have a format error.

      declare
         B : Byte;
         pragma Warnings (Off, B);

      begin
         B := Read_Byte;

      exception
         when Tree_Format_Error => return;
      end;

      raise Tree_Format_Error;
   end Tree_Read_Terminate;

   ---------------------
   -- Tree_Write_Bool --
   ---------------------

   procedure Tree_Write_Bool (B : Boolean) is
   begin
      if Debug_Flag_Tree then
         Write_Str ("==> transmitting Boolean = ");

         if B then
            Write_Str ("True");
         else
            Write_Str ("False");
         end if;

         Write_Eol;
      end if;

      Write_Byte (Boolean'Pos (B));
   end Tree_Write_Bool;

   ---------------------
   -- Tree_Write_Char --
   ---------------------

   procedure Tree_Write_Char (C : Character) is
   begin
      if Debug_Flag_Tree then
         Write_Str ("==> transmitting Character = ");
         Write_Char (C);
         Write_Eol;
      end if;

      Write_Byte (Character'Pos (C));
   end Tree_Write_Char;

   ---------------------
   -- Tree_Write_Data --
   ---------------------

   procedure Tree_Write_Data (Addr : Address; Length : Int) is

      type S is array (Pos) of Byte;
      --  This is a big array, for which we have to suppress the warning

      type SP is access all S;

      function To_SP is new Unchecked_Conversion (Address, SP);

      Data : constant SP := To_SP (Addr);
      --  Pointer to data to be written, converted to array type

      IP : Pos := 1;
      --  Input buffer pointer, next byte to be processed

      NC : Nat range 0 .. Max_Count := 0;
      --  Number of bytes of non-compressible sequence

      C  : Byte;

      procedure Write_Non_Compressed_Sequence;
      --  Output currently collected sequence of non-compressible data

      -----------------------------------
      -- Write_Non_Compressed_Sequence --
      -----------------------------------

      procedure Write_Non_Compressed_Sequence is
      begin
         if NC > 0 then
            Write_Byte (C_Noncomp + Byte (NC));

            if Debug_Flag_Tree then
               Write_Str ("==>    uncompressed:  ");
               Write_Int (NC);
               Write_Str (", starting at ");
               Write_Int (IP - NC);
               Write_Eol;
            end if;

            for J in reverse 1 .. NC loop
               Write_Byte (Data (IP - J));
            end loop;

            NC := 0;
         end if;
      end Write_Non_Compressed_Sequence;

   --  Start of processing for Tree_Write_Data

   begin
      if Debug_Flag_Tree then
         Write_Str ("==> transmitting ");
         Write_Int (Length);
         Write_Str (" data bytes");
         Write_Eol;
      end if;

      --  We write the count at the start, so that we can check it on
      --  the corresponding read to make sure that reads and writes match

      Tree_Write_Int (Length);

      --  Conversion loop
      --    IP is index of next input character
      --    NC is number of non-compressible bytes saved up

      loop
         --  If input is completely processed, then we are all done

         if IP > Length then
            Write_Non_Compressed_Sequence;
            return;
         end if;

         --  Test for compressible sequence, must be at least three identical
         --  bytes in a row to be worthwhile compressing.

         if IP + 2 <= Length
           and then Data (IP) = Data (IP + 1)
           and then Data (IP) = Data (IP + 2)
         then
            Write_Non_Compressed_Sequence;

            --  Count length of new compression sequence

            C := 3;
            IP := IP + 3;

            while IP < Length
              and then Data (IP) = Data (IP - 1)
              and then C < Max_Count
            loop
               C := C + 1;
               IP := IP + 1;
            end loop;

            --  Output compression sequence

            if Data (IP - 1) = 0 then
               if Debug_Flag_Tree then
                  Write_Str ("==>    zeroes:        ");
                  Write_Int (Int (C));
                  Write_Str (", starting at ");
                  Write_Int (IP - Int (C));
                  Write_Eol;
               end if;

               Write_Byte (C_Zeros + C);

            elsif Data (IP - 1) = Character'Pos (' ') then
               if Debug_Flag_Tree then
                  Write_Str ("==>    spaces:        ");
                  Write_Int (Int (C));
                  Write_Str (", starting at ");
                  Write_Int (IP - Int (C));
                  Write_Eol;
               end if;

               Write_Byte (C_Spaces + C);

            else
               if Debug_Flag_Tree then
                  Write_Str ("==>    other char:    ");
                  Write_Int (Int (C));
                  Write_Str (" (");
                  Write_Int (Int (Data (IP - 1)));
                  Write_Char (')');
                  Write_Str (", starting at ");
                  Write_Int (IP - Int (C));
                  Write_Eol;
               end if;

               Write_Byte (C_Repeat + C);
               Write_Byte (Data (IP - 1));
            end if;

         --  No compression possible here

         else
            --  Output non-compressed sequence if at maximum length

            if NC = Max_Count then
               Write_Non_Compressed_Sequence;
            end if;

            NC := NC + 1;
            IP := IP + 1;
         end if;
      end loop;

   end Tree_Write_Data;

   ---------------------------
   -- Tree_Write_Initialize --
   ---------------------------

   procedure Tree_Write_Initialize (Desc : File_Descriptor) is
   begin
      Bufn := 0;
      Tree_FD := Desc;
      Set_Standard_Error;
      Debug_Flag_Tree := Debug_Flag_5;
   end Tree_Write_Initialize;

   --------------------
   -- Tree_Write_Int --
   --------------------

   procedure Tree_Write_Int (N : Int) is
      N_Bytes : constant Int_Bytes := To_Int_Bytes (N);

   begin
      if Debug_Flag_Tree then
         Write_Str ("==> transmitting Int = ");
         Write_Int (N);
         Write_Eol;
      end if;

      for J in 1 .. 4 loop
         Write_Byte (N_Bytes (J));
      end loop;
   end Tree_Write_Int;

   --------------------
   -- Tree_Write_Str --
   --------------------

   procedure Tree_Write_Str (S : String_Ptr) is
   begin
      Tree_Write_Int (S'Length);
      Tree_Write_Data (S (1)'Address, S'Length);
   end Tree_Write_Str;

   --------------------------
   -- Tree_Write_Terminate --
   --------------------------

   procedure Tree_Write_Terminate is
   begin
      if Bufn > 0 then
         Write_Buffer;
      end if;
   end Tree_Write_Terminate;

   ------------------
   -- Write_Buffer --
   ------------------

   procedure Write_Buffer is
   begin
      if Integer (Bufn) = Write (Tree_FD, Buf'Address, Integer (Bufn)) then
         Bufn := 0;

      else
         Set_Standard_Error;
         Write_Str ("fatal error: disk full");
         OS_Exit (2);
      end if;
   end Write_Buffer;

   ----------------
   -- Write_Byte --
   ----------------

   procedure Write_Byte (B : Byte) is
   begin
      Bufn := Bufn + 1;
      Buf (Bufn) := B;

      if Bufn = Buflen then
         Write_Buffer;
      end if;
   end Write_Byte;

end Tree_IO;
