with Bits; use Bits;
with Ada.Text_IO;  use Ada.Text_IO;
with Ada.Integer_Text_IO; use  Ada.Integer_Text_IO;
with Character_IO; use Character_IO;
with Ada.Strings.Fixed;
with Ada.Directories;
with Ada.Characters;
with Ada.Characters.Handling;
with Ada.Characters.Latin_1;
with Ada.Sequential_IO;
with SMG_Keccak; use SMG_Keccak;
with Temporary_File; use Temporary_File;

procedure VPatch is
   package Latin_1 renames Ada.Characters.Latin_1;
   package Dirs renames Ada.Directories;
   package CIO renames Character_IO.Character_IO;

   -- Utilities

   function Starts_With(S: String; Prefix: String) return Boolean is
   begin
      if S'Length < Prefix'Length then
         return False;
      end if;
      return S(S'First..S'First+Prefix'Length-1) = Prefix;
   end;

   function Directory_Name(Pathname: String) return String is
      Pos: Natural := Pathname'Last;
   begin
      Pos := Ada.Strings.Fixed.Index(Pathname, "/",
                                     From => Pos,
                                     Going => Ada.Strings.Backward);
      if Pos = 0 then
         return Dirs.Current_Directory;
      end if;
      return Pathname(Pathname'First .. Pos);
   end;

   function Path_Without_Prefix(Pathname: String;
                                Prefix: Positive) return String is
      Pos: Natural := 1;
   begin
      for I in 1..Prefix loop
         Pos := Ada.Strings.Fixed.Index(Pathname, "/", From => Pos);
         if Pos = 0 then
            return Pathname;
         end if;
         Pos := Pos + 1;
      end loop;
      return Pathname(Pos .. Pathname'Last);
   end;

   -- Temporary File

   procedure Create_Temp(File : in out File_Type;
                         Mode : in File_Mode := Out_File;
                         Prefix : in String;
                         Seed : in String := "";
                         Form : in String := "") is
      Name: String := Temporary_File.Temporary_File(Prefix, Seed);
   begin
      Create(File, Mode, Name, Form);
   end;

   procedure Create_Temp(File : in out CIO.File_Type;
                         Mode : in CIO.File_Mode := CIO.Out_File;
                         Prefix : in String;
                         Seed : in String := "";
                         Form : in String := "") is
      Name: String := Temporary_File.Temporary_File(Prefix, Seed);
   begin
      Create(File, Mode, Name, Form);
   end;

   -- VPatch data structures

   type Patch_Op is (Op_Create, Op_Delete, Op_Patch);

   Hash_Length: constant Positive := 128;
   type Hash_Type is (Empty, Value);
   type Hash(The_Type: Hash_Type := Empty) is record
      case The_Type is
         when Value =>
            Value: String(1..Hash_Length);
         when Empty =>
            null;
      end case;
   end record;

   function "=" (Left, Right: in Hash) return Boolean is
   begin
      if Left.The_Type = Empty and Right.The_Type = Empty then
         return True;
      elsif Left.The_Type = Empty or Right.The_Type = Empty then
         return False;
      elsif Left.Value /= Right.Value then
         return False;
      else
         return True;
      end if;
   end "=";

   type Header (From_L, To_L: Natural) Is record
      From_Hash: Hash;
      From_File: String(1..From_L);
      To_Hash: Hash;
      To_File: String(1..To_L);
   end record;

   function Operation(A_Header: Header) return Patch_Op is
   begin
      if A_Header.From_Hash.The_Type = Empty then
         return Op_Create;
      elsif A_Header.To_Hash.The_Type = Empty then
         return Op_Delete;
      else
         return Op_Patch;
      end if;
   end;

   function Press_Name(A_Header: Header) return String is
   begin
      return Path_Without_Prefix(A_Header.From_File, 1);
   end;

   type Line_Numbers is record
      Start: Natural;
      Count: Natural;
   end record;

   type Hunk is record
      From_File_Line_Numbers: Line_Numbers;
      To_File_Line_Numbers: Line_Numbers;
   end record;

   -- VPatch debug output routines

   procedure Put(A_Line_Numbers: Line_Numbers) is
   begin
      Put(A_Line_Numbers.Start);
      Put(A_Line_Numbers.Count);
   end;

   procedure Put(A_Hash: Hash) is
   begin
      case A_Hash.The_Type is
         when Value =>
            Put(A_Hash.Value);
         when Empty =>
            Put("no value");
      end case;
   end;

   procedure Put(A_Header: Header) is
   begin
      Put("from file: ");
      Put(A_Header.From_File);
      New_Line;
      Put("to file: ");
      Put(A_Header.To_File);
      New_Line;
      Put("from hash: ");
      Put(A_Header.From_Hash);
      New_Line;
      Put("to hash: ");
      Put(A_Header.To_Hash);
      New_Line;
   end;

   procedure Put(A_Hunk: Hunk) is
   begin
      Put("from file line numbers: ");
      Put(A_Hunk.From_File_Line_Numbers);
      New_Line;
      Put("to file line numbers: ");
      Put(A_Hunk.To_File_Line_Numbers);
      New_Line;
   end;

   -- VPatch parser

   Parse, State: exception;

   procedure Skip_Whitespace is
      EOL: Boolean;
      C: Character;
   begin
  Skip_Loop:
      loop
         Look_Ahead(C, EOL);
         exit Skip_Loop when EOL;
         exit Skip_Loop when
           C /= Latin_1.Space and
           C /= Latin_1.HT;
         Get(C);
      end loop Skip_Loop;
   end;

   procedure Looking_At(Expected: String) is
      Actual: String(Expected'Range);
   begin
      Get(Actual);
      if Expected /= Actual then
         raise Parse with "expected " & Expected & ", got " & Actual;
      end if;
   end;

   procedure Next_Line is
   begin
      if not End_Of_Line then
         raise Parse with "expected end of line";
      end if;
      Skip_Line;
   end;

   procedure Get(A_Hash: out Hash) is
      No_Hash_Label: constant String := "false";
      V: String(1..Hash_Length);
   begin
      Get(V(1..No_Hash_Label'Length));
      if V(1..No_Hash_Label'Length) = No_Hash_Label then
         A_Hash := (The_Type => Empty);
         return;
      end if;
      Get(V(No_Hash_Label'Length + 1..V'Last));
      A_Hash := (The_Type => Value,
                 Value => V);
   end;

   procedure Get(A_Line_Numbers: out Line_Numbers) is
      C: Character;
      Eol: Boolean;
   begin
      Get(A_Line_Numbers.Start);
      Look_Ahead(C, Eol);
      if Eol then
         raise Parse;
      end if;
      case C is
         when ' ' =>
            -- If a hunk contains just one line, only its start line
            --  number appears.
            A_Line_Numbers.Count := 1;
         when ',' =>
            -- Otherwise its line numbers look like `start,count'. An
            --  empty hunk is considered to start at the line that
            --  follows the hunk.
            Get(C);
            Get(A_Line_Numbers.Count);
         when others =>
            raise Parse;
      end case;
   end;

   function Get_Header_Filename return String is
      EOL: Boolean;
      Buffer: String(1..1000);
      C: Character;
      I: Natural := 0;
   begin
  Read_Loop:
      loop
         Look_Ahead(C, EOL);
         exit Read_Loop when EOL;
         exit Read_Loop when
           C = Latin_1.Space or C = Latin_1.HT;
         Get(C);
         I := I + 1;
         Buffer(I) := C;
      end loop Read_Loop;
      return Buffer(1..I);
   end;

   function Get_Header return Header is
      From_Hash: Hash;
      To_Hash: Hash;
   begin
      Looking_At("--- ");
      declare
         From_File: String := Get_Header_Filename;
      begin
         Skip_Whitespace;
         Get(From_Hash);
         Looking_At("+++ ");
         declare
            To_File: String := Get_Header_Filename;
         begin
            Skip_Whitespace;
            Get(To_Hash);
            Next_Line;
            declare
               H: Header := (From_L => From_File'Length,
                             To_L => To_File'Length,
                             From_File => From_File,
                             To_File => To_File,
                             From_Hash => From_Hash,
                             To_Hash => To_Hash);
            begin
               return H;
            end;
         end;
      end;
   end;

   procedure Get(A_Hunk: out Hunk) is
   begin
      Looking_At("@@ -");
      Get(A_Hunk.From_File_Line_Numbers);
      Looking_At(" +");
      Get(A_Hunk.To_File_Line_Numbers);
      Looking_At(" @@");
      Next_Line;
   end;

   procedure Process_Hunks_For_Header(A_Header: Header) Is
      EOL: Boolean;
      C: Character;
      A_Hunk: Hunk;
      -- ensure valid line counts
      From_Count: Natural := 0;
      To_Count: Natural := 0;
      Has_Input_File: Boolean;
      In_F: CIO.File_Type;
      To_F: CIO.File_Type;
      Line: Positive := 1;
      In_Ctx: Keccak_Context;
      To_Ctx: Keccak_Context;
      In_Hash: Bitstream(1..64*8);
      To_Hash: Bitstream(1..64*8);
      To_F_Name: constant String := Press_Name(A_Header);
      Op: Patch_Op;
      Newline_Directive: constant String := "\ No newline at end of file";
      
      procedure Hash_Line(Ctx: in out Keccak_Context;
                          S: String;
                          New_Line: Boolean := True) is
         B: Bitstream(1..S'Length*8);
         LF_B: constant Bitstream(1..8) := (0, 1, 0, 1, 0, 0, 0, 0);
      begin
         ToBitstream(S, B);
         KeccakHash(Ctx, B);
         if New_Line then
            KeccakHash(Ctx, LF_B);
         end if;
      end;
      
      Check_Input_File_Hash_Pending: Boolean := True;
      procedure Check_Input_File_Hash is
      begin
         if Has_Input_File and Is_Open(In_F) 
           and Check_Input_File_Hash_Pending then
            begin
               Check_Input_File_Hash_Pending := False;
           Catch_Up_Loop:
               loop
                  declare
                     New_Line: Boolean;
                     In_Line: String := Get_Line(In_F, New_Line);
                  begin
                     Put_Line(To_F, In_Line, New_Line);
                     Hash_Line(In_Ctx, In_Line, New_Line);
                     Hash_Line(To_Ctx, In_Line, New_Line);
                  end;
               end loop Catch_Up_Loop;
            exception
               when End_Error =>
                  null;
            end;
            KeccakEnd(In_Ctx, In_Hash);

            declare
               Hex_Hash: String := ToHex(In_Hash);
               H: Hash := (Value => Hex_Hash,
                           The_Type => Value);
            begin
               if A_Header.From_Hash /= H then
                  raise State with "from hash doesn't match";
               end if;
            end;
         end if;
      end Check_Input_File_Hash;

      procedure Check_Output_File_Hash is
      begin
         KeccakEnd(To_Ctx, To_Hash);
         declare
            H_Hex: String := ToHex(To_Hash);
            H: Hash;
         begin
            case Op is
               when Op_Create | Op_Patch =>
                  H := (Value => H_Hex,
                        The_Type => Value);
               when Op_Delete =>
                  H := (The_Type => Empty);
            end case;
            if A_Header.To_Hash /= H then
               raise State with "to hash doesn't match";
            end if;
         end;
      end Check_Output_File_Hash;

      procedure Cleanup is
      begin
         if Is_Open(To_F) then
            Dirs.Delete_File(Name(To_F));
         end if;
      end Cleanup;
      
      function Has_No_Newline_Directive return Boolean is
         C: Character;
      begin
         Look_Ahead(C, EOL);
         if C = '\' then
            Looking_At(Newline_Directive);
            Next_Line;
            return True;
         end if;
         return False;
      end;
      
   begin
      Op := Operation(A_Header);

      -- log
      case Op is
         when Op_Create => Put_Line("creating " & To_F_Name);
         when Op_Delete => Put_Line("deleting " & To_F_Name);
         when Op_Patch  => Put_Line("patching " & To_F_Name);
      end case;

      -- check the file system state
      case Op is
         when Op_Delete | Op_Patch =>
            if not Dirs.Exists(To_F_Name) then
               raise State with "attempt to "
                 & Patch_Op'Image(Op)
                 & " non existing file " & To_F_Name;
            end if;
         when Op_Create =>
            if Dirs.Exists(To_F_Name) then
               raise State with "attempt to create a file, but file already exists";
            end if;
      end case;

      -- prepare keccak and open files
      KeccakBegin(To_Ctx);
      Create_Temp(To_F, Prefix => "vpatch-", Seed => To_F_Name);
      case Op is
         when Op_Create =>
            Has_Input_File := False;
         when Op_Delete | Op_Patch =>
            Has_Input_File := True;
            KeccakBegin(In_Ctx);
            Open(In_F, CIO.In_File, To_F_Name);
      end case;

  Hunk_Loop:
      loop
         Look_Ahead(C, EOL);
         exit Hunk_Loop when EOL;
         exit Hunk_Loop when C /= '@';
         Get(A_Hunk);
         From_Count := A_Hunk.From_File_Line_Numbers.Count;
         To_Count := A_Hunk.To_File_Line_Numbers.Count;
         -- Hunk is not at the beginning of the file, copy lines up to
         --  start.
         if Line < A_Hunk.From_File_Line_Numbers.Start then
            if not Has_Input_File then
               raise State with "hunk requires before context lines, "
                 & "but there's no input file";
            end if;
            while Line < A_Hunk.From_File_Line_Numbers.Start loop
               if End_Of_File(In_F) then
                  raise State with "hunk requires before context lines, "
                    & "but the file has ended";
               end if;
               declare
                  New_Line: Boolean;
                  In_Line: String := Get_Line(In_F, New_Line);
               begin
                  Hash_Line(In_Ctx, In_Line, New_Line);
                  Hash_Line(To_Ctx, In_Line, New_Line);
                  Put_Line(To_F, In_Line, New_Line);
                  Line := Line + 1;
               end;
            end loop;
         end if;
     Hunk_Body_Loop:
         loop
            exit Hunk_Body_Loop when From_Count = 0 and To_Count = 0;
            Look_Ahead(C, EOL);
            if EOL then
               raise Parse with "blank line in hunk";
            end if;
            case C is

               when '+' => -- line added
                  Get(C);
                  case Op is
                     when Op_Create | Op_Patch => null;
                     when Op_Delete => raise State with "hunk trying to add lines, "
                        & "but the operation is deletion";
                  end case;
                  if To_Count = 0 then
                     raise State with "hunk trying to add lines, "
                       & "but the line count is not valid";
                  end if;

                  declare
                     New_Line: Boolean := True;
                     Patch_Line: String := Get_Line;
                  begin
                     -- Last line, check for Newline directive.
                     if To_Count = 1 then
                        New_Line := not Has_No_Newline_Directive;
                     end if;
                     Put_Line(To_F, Patch_Line, New_Line);
                     Hash_Line(To_Ctx, Patch_Line, New_Line);
                  end;
                  To_Count := To_Count - 1;

               when '-' => -- line deleted
                  Get(C);
                  case Op is
                     when Op_Delete | Op_Patch => null;
                     when Op_Create => raise State;
                  end case;
                  if not Has_Input_File then
                     raise State with "hunk trying to remove lines, "
                       & "but the input file doesn't exist";
                  end if;
                  if From_Count = 0 then
                     raise State with "hunk trying to remove lines, "
                       & "when the input file already ended";
                  end if;

                  declare
                     New_Line: Boolean;
                     In_Line: String := Get_Line(In_F, New_Line);
                     Patch_Line: String := Get_Line;
                  begin
                     -- Last line, check for Newline directive.
                     if From_Count = 1 then
                        if Has_No_Newline_Directive and New_Line then
                           raise State with "input file has newline, "
                             & "while hunk claims it doesn't";
                        end if;
                     end if;

                     if In_Line /= Patch_Line then
                        raise State with "lines don't match";
                     end if;
                     Hash_Line(In_Ctx, In_Line, New_Line);
                  end;
                  Line := Line + 1;
                  From_Count := From_Count - 1;

               when ' ' => -- line stays the same
                  Get(C);
                  if not Has_Input_File then
                     raise State with "hunk claims identical lines, "
                       & "but the input file doesn't exist";
                  end if;
                  if End_Of_File(In_F) then
                     raise State with "hunk claims identical lines, "
                       & "but the input file has ended";
                  end if;
                  if From_Count = 0 then
                     raise State with "hunk claims identical lines, "
                       & "when input file already ended";
                  end if;

                  declare
                     New_Line: Boolean;
                     In_Line: String := Get_Line(In_F, New_Line);
                     Patch_Line: String := Get_Line;
                  begin
                     if In_Line /= Patch_Line then
                        raise State with "lines don't match";
                     end if;
                     if From_Count = 1 then
                        if Has_No_Newline_Directive and New_Line then
                           raise State with "input file has newline, "
                             & "while hunk claims it doesn't";                           
                        end if;
                     end if;
                     
                     Put_Line(To_F, Patch_Line, New_Line);
                     Hash_Line(In_Ctx, In_Line, New_Line);
                     Hash_Line(To_Ctx, In_Line, New_Line);
                  end;
                  Line := Line + 1;
                  From_Count := From_Count - 1;
                  To_Count := To_Count - 1;

               when '\' =>
                  Looking_At(Newline_Directive);
                  raise State with "invalid line count in hunk";

               when others =>
                  raise Parse with "unexpected character "
                    & Character'Image(C)
                    & " at beginning of line in hunk body";
            end case;
         end loop Hunk_Body_Loop;
      end loop Hunk_Loop;

      Check_Input_File_Hash;
      Check_Output_File_Hash;

      declare
         Tmp_Name: String := Name(To_F);
      begin
         Close(To_F);
         if Has_Input_File then
            Close(In_F);
            Dirs.Delete_File(To_F_Name);
         else
            if not Dirs.Exists(Directory_Name(To_F_Name)) then
               Dirs.Create_Path(Directory_Name(To_F_Name));
            end if;
         end if;
         case Op is
            when Op_Create | Op_Patch =>
               Dirs.Rename(Tmp_Name, To_F_Name);
            when Op_Delete =>
               Dirs.Delete_File(Tmp_Name);
         end case;
      end;

   exception
      when E : State =>
         -- we've encountered state issue,
         --   check first that the input hash is valid
         Cleanup;
         Check_Input_File_Hash;
         raise;

      when E : others =>
         Cleanup;
         raise;
   end Process_Hunks_For_Header;

begin
Read_Loop:
    loop
       exit Read_Loop when End_Of_File;
       declare
          S: String := Get_Line;
       begin
          if Starts_With(S, "diff ") then
             declare
                H: Header := Get_Header;
             begin
                Process_Hunks_For_Header(H);
             end;
          else
             Put_Line("Prelude: " & S);
          end if;
       end;
    end loop Read_Loop;
end;
