--  Copyright (C) 2015  Daniel Otte <bg@nerilex.org>
--
--  This program is free software: you can redistribute it and/or modify
--  it under the terms of the GNU General Public License as published by
--  the Free Software Foundation, either version 3 of the License, or
--  (at your option) any later version.
--
--  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 General Public License for more details.
--
--  You should have received a copy of the GNU General Public License
--  along with this program.  If not, see <http://www.gnu.org/licenses/>.

with Crypto_Types; use Crypto_Types;
use Crypto_Types.Crypto_Utils_u16;
use Crypto_Types.Crypto_Utils_u64;

with Ada.Text_IO; use Ada.Text_IO;

package body Pi64Cipher_Spec is

   subtype Rotation_Ammount_T is Integer range 0 .. Word_T'Size - 1;
   subtype Chunk_Index_T is Integer range Chunk_T'Range;
   subtype Rate_Block is u8_Array(1 .. Rate_Bytes);

   type Phi_Constants is array (1 .. 4) of Word_T;
   type Phi_Rotation_Constants is array (1 .. 4) of Rotation_Ammount_T;
   type Phi_V_Constants is array (1 .. 4) of Chunk_Index_T;

   Mu_Constants : constant Phi_Constants := (
                                                16#F0E8E4E2E1D8D4D2#,
                                                16#D1CCCAC9C6C5C3B8#,
                                                16#B4B2B1ACAAA9A6A5#,
                                                16#A39C9A999695938E# );

   Mu_Rotation_Constants : constant Phi_Rotation_Constants := ( 7, 19, 31, 53 );
   Nu_Constants : constant Phi_Constants := (   16#8D8B87787472716C#,
                                                16#6A696665635C5A59#,
                                                16#5655534E4D4B473C#,
                                                16#3A393635332E2D2B# );
   Nu_Rotation_Constants : constant Phi_Rotation_Constants := ( 11, 23, 37, 59 );

   Mu_V_Constants : constant Phi_V_Constants := ( 4, 3, 2, 1 );
   Nu_V_Constants : constant Phi_V_Constants := ( 2, 1, 4, 3 );

   Pi_Constants : constant array (1 .. 2 * R) of Chunk_T :=
     (
      ( 16#271E1D1B170FF0E8#, 16#E4E2E1D8D4D2D1CC#, 16#CAC9C6C5C3B8B4B2#, 16#B1ACAAA9A6A5A39C# ),
      ( 16#9A999695938E8D8B#, 16#87787472716C6A69#, 16#6665635C5A595655#, 16#534E4D4B473C3A39# ),
      ( 16#3635332E2D2B271E#, 16#1D1B170FF0E8E4E2#, 16#E1D8D4D2D1CCCAC9#, 16#C6C5C3B8B4B2B1AC# ),
      ( 16#AAA9A6A5A39C9A99#, 16#9695938E8D8B8778#, 16#7472716C6A696665#, 16#635C5A595655534E# ),
      ( 16#4D4B473C3A393635#, 16#332E2D2B271E1D1B#, 16#170FF0E8E4E2E1D8#, 16#D4D2D1CCCAC9C6C5# ),
      ( 16#C3B8B4B2B1ACAAA9#, 16#A6A5A39C9A999695#, 16#938E8D8B87787472#, 16#716C6A696665635C# )
     );

   generic
      Constants : Phi_Constants;
      Rotation_Constants : Phi_Rotation_Constants;
      V_Constants : Phi_V_Constants;
   function Phi(Chunk : in Chunk_T) return Chunk_T;

   function Phi(Chunk : in Chunk_T) return Chunk_T is
      Ret : Chunk_T;
      Sum : Word_T;
   begin
      Sum := 0 + Chunk;
      for i in Chunk'Range loop
         Ret(i) := Rotate_Left(Constants(i) + Sum - Chunk(V_Constants(i)), Rotation_Constants(i));
      end loop;
      Sum := 0 xor Ret;
      Ret := Ret xor Sum;
      return Ret;
   end Phi;

   function Nu is new Phi (Constants => Nu_Constants, Rotation_Constants => Nu_Rotation_Constants, V_Constants => Nu_V_Constants);

   function Mu(Chunk : Chunk_T) return Chunk_T is
      function foo is new Phi (Constants => Mu_Constants, Rotation_Constants => Mu_Rotation_Constants, V_Constants => Mu_V_Constants);
      Ret : Chunk_T;
   begin
      Ret := foo(Chunk);
      Ret := Rotate_Array_Left(Ret, 2);
      return Ret;
   end Mu;

   function "*" (X : Chunk_T; Y : Chunk_T) return Chunk_T is
      Ret : Chunk_T;
   begin
      Ret := Mu(X) + Nu(Y);
      Ret := Rotate_Array_Left(Ret, 1);
      return Ret;
   end "*";

   function E1 (C : Chunk_T; I : State_T) return State_T is
      J : State_T;
   begin
      J(1) := C * I(1);
        for index in 2 .. N loop
           J(index) := J(index - 1) * I(index);
        end loop;
      return J;
   end E1;

   function E2 (C : Chunk_T; I : State_T) return State_T is
      J : State_T;
   begin
      J(N) := I(N) * C;
      for index in reverse 1 .. N - 1 loop
         J(index) := I(index) * J(index + 1);
      end loop;
      return J;
   end E2;

   function Pi (State : State_T) return State_T is
      Ret : State_T := State;
   begin
      for round in 0 .. R - 1 loop
         Ret := E1(Pi_Constants(2 * round + 1), Ret);
         Ret := E2(Pi_Constants(2 * round + 2), Ret);
      end loop;
      return Ret;
   end Pi;

   procedure Dump(State : in State_T) is
   begin
      Put_Line("State:");
      for i in State'Range loop
         Put("    [ ");
         for j in State(i)'Range loop
            Put(To_Hex(State(i)(j)));
            Put(' ');
         end loop;
         Put_Line("]");
      end loop;
   end;

   procedure Initialize(Context : out Context_T; Key : in u8_Array; Public_Nonce : in u8_Array) is
      Scratch : u8_Array(1 .. IS_Bytes) := (others => 0);
      Index : Integer := Scratch'First;
   begin
      if Key'Length + Public_Nonce'Length >= Scratch'Length then
         raise Invalid_Key_Size;
      end if;
      Context.Tag := (others => 0);
      Scratch(1 .. Key'Length) := Key;
      Scratch(Key'Length + 1 .. Key'Length + Public_Nonce'Length) := Public_Nonce;
      Scratch(Key'Length + Public_Nonce'Length + 1) := 1;
      for i in Context.State'Range loop
         Context.State(i) := Load_LE(Scratch(Index .. Index + Chunk_T'Length * Word_T'Size / 8 - 1));
         Index := Index + Chunk_T'Length * Word_T'Size / 8;
      end loop;
      Context.State := pi(Context.State);
      Context.Counter := 0;
      for i in 1 .. 64 / Word_T'Size loop
         Context.Counter := Context.Counter or Shift_Left(u64(Context.State(2)(i)), (i - 1) * Word_T'Size);
      end loop;
   end Initialize;

   function "+" (Context : Context_T; Block_Number : Block_Number_T) return State_T is
      Counter : u64 := Context.Counter + u64(Block_Number);
      Ret : State_T := Context.State;
   begin
      for i in 1 .. 64 / Word_T'Size loop
         Ret(1)(i) := Ret(1)(i) xor Word_T( Counter and (Shift_Left(u64'(1), Word_T'Size) - 1));
         Counter := Shift_Right(Counter, Word_T'Size);
      end loop;
      return Ret;
   end "+";

   function Extract(State : State_T) return Rate_Block is
      Data : Rate_Block;
      Index : Positive := Data'First;
   begin
      for i in 0 .. N / 2 - 1 loop
         Store_LE(Data(Index .. Index + 4 * Word_T'Size / 8 - 1), State(2 * i + 1));
         Index := Index + 4 * Word_T'Size / 8;
      end loop;
      return Data;
   end Extract;

   function Extract(State : State_T) return Tag_Int_T is
      Tag : Tag_Int_T;
   begin
      for i in 0 .. N / 2 - 1 loop
         Tag(Tag'First + i * 4 .. Tag'First + i * 4 + 3) := State(2 * i + 1);
      end loop;
      return Tag;
   end Extract;

   function "xor" (State : State_T; Data : Rate_Block) return State_T is
      Ret : State_T := State;
      Index : Positive := Data'First;
   begin
      for i in 0 .. N / 2 - 1 loop
         Ret(2 * i + 1) := Ret(2 * i + 1) xor Chunk_T'(Load_LE(Data(Index .. Index + 4 * Word_T'Size / 8 - 1)));
         Index := Index + 4 * Word_T'Size / 8;
      end loop;
      return Ret;
   end "xor";

   function "xor" (State : State_T; Tag : Tag_Int_T) return State_T is
      Ret : State_T := State;
      Index : Positive := Tag'First;
   begin
      for i in 0 .. N / 2 - 1 loop
         Ret(2 * i + 1) := Ret(2 * i + 1) xor Tag(Index .. Index + 3);
         Index := Index + 4;
      end loop;
      return Ret;
   end "xor";

   function set (State : State_T; Data : Rate_Block) return State_T is
      Ret : State_T := State;
      Index : Positive := Data'First;
   begin
      for i in 0 .. N / 2 - 1 loop
         Ret(2 * i + 1) := Chunk_T'(Load_LE(Data(Index .. Index + 4 * Word_T'Size / 8 - 1)));
         Index := Index + 4 * Word_T'Size / 8;
      end loop;
      return Ret;
   end set;

   function "+" (Tag : Tag_Int_T) return Tag_T is
      Ret : Tag_T;
   begin
      Store_LE(Ret, Tag);
      return Ret;
   end "+";

   function Pad (Data : u8_Array) return Block_T is
      Ret : Block_T := (others => 0);
   begin
      if Data'Length >= Block_T'Length then
         raise Constraint_Error;
      end if;
      Ret(Ret'First .. Ret'First + Data'Length -1 ) := Data;
      Ret(Ret'First + Data'Length) := 1;
      return Ret;
   end Pad;

   procedure Process_Header_Block (Context : in out Context_T; Block : Block_T; Block_Number : Block_Number_T) is
   begin
      Context.Tag := Context.Tag + Extract(Pi(Pi(Context + Block_Number) xor Block));
   end Process_Header_Block;

   procedure Process_Header_Last_Block (Context : in out Context_T; Block : u8_Array; Block_Number : Block_Number_T) is
      Num : Block_Number_T := Block_Number;
      Index : Integer := Block'First;
   begin
      for i in 1 .. Block'Length / Block_Bytes loop
         Process_Header_Block(Context, Block(Index .. Index + Block_Bytes - 1), Num);
         Num := Num + 1;
         Index := Index + Block_Bytes;
      end loop;
      Process_Header_Block(Context, Pad(Block(Index .. Block'Last)), Num);
      Context.State := Pi(Context.State xor Context.Tag);
      Context.Counter := Context.Counter + u64(Num);
   end Process_Header_Last_Block;

   procedure Encrypt_Secret_Message_Number(Context : in out Context_T; Block : in out u8_Array) is
      State : State_T;
   begin
      if Block'Length = Block_T'Length then
         State := Pi(Context + Block_Number_T'(1)) xor Block;
         Block := Extract(State);
         Context.State := Pi(State);
         Context.Tag := Context.Tag + Extract(Context.State);
         Context.Counter := Context.Counter + 1;
      elsif Block'Length = 0 then
         null;
      else
         raise Format_Violation;
      end if;
   end Encrypt_Secret_Message_Number;

   procedure Decrypt_Secret_Message_Number(Context : in out Context_T; Block : in out u8_Array) is
      State : State_T;
      Block_In : constant u8_Array(Block'Range) := Block;
   begin
      if Block'Length = Block_T'Length then
         State := Pi(Context + Block_Number_T'(1)) xor Block;
         Block := Extract(State);
         Context.State := Pi(set(State, Block_In));
         Context.Tag := Context.Tag + Extract(Context.State);
         Context.Counter := Context.Counter + 1;
      elsif Block'Length = 0 then
         null;
      else
         raise Format_Violation;
      end if;
   end Decrypt_Secret_Message_Number;

   procedure Encrypt_Block(Context : in out Context_T; Block : in out Block_T; Block_Number : Block_Number_T) is
      State : State_T := Pi(Context + Block_Number) xor Block;
   begin
      Block := Extract(State);
      State := Pi(State);
      Context.Tag := Context.Tag + Extract(State);
   end Encrypt_Block;

   procedure Decrypt_Block(Context : in out Context_T; Block : in out Block_T; Block_Number : Block_Number_T) is
      State : State_T := Pi(Context + Block_Number) xor Block;
      In_Block : constant Block_T := Block;
   begin
      Block := Extract(State);
      State := Pi(set(State, In_Block));
      Context.Tag := Context.Tag + Extract(State);
   end Decrypt_Block;

   procedure Encrypt_Last_Block(Context : in out Context_T; Block : in out u8_Array; Block_Number : Block_Number_T) is
      State : State_T;
      Index : Integer := Block'First;
      Num : Block_Number_T := Block_Number;
      Temp_Block : Block_T;
   begin
      for i in 1 .. Block'Length / Block_Bytes loop
         Encrypt_Block(Context, Block(Index .. Index + Block_Bytes - 1), Num);
         Index := Index + Block_Bytes;
         Num := Num + 1;
      end loop;
      Temp_Block := Pad(Block(Index .. Block'Last));
      State := Pi(Context + Num) xor Temp_Block;
      Temp_Block := Extract(State);
      Block(Index .. Block'Last) := Temp_Block(Temp_Block'First .. Temp_Block'First + Block'Last - Index);
      State := Pi(State);
      Context.Tag := Context.Tag + Extract(State);
   end Encrypt_Last_Block;

   procedure Decrypt_Last_Block(Context : in out Context_T; Block : in out u8_Array; Block_Number : Block_Number_T) is
      State : State_T;
      Index : Integer := Block'First;
      Num : Block_Number_T := Block_Number;
      Temp_Block : Block_T;
   begin
      for i in 1 .. Block'Length / Block_Bytes loop
         Decrypt_Block(Context, Block(Index .. Index + Block_Bytes - 1), Num);
         Index := Index + Block_Bytes;
         Num := Num + 1;
      end loop;
      Temp_Block := Pad(Block(Index .. Block'Last));
      State := Pi(Context + Num) xor Temp_Block;
      Block(Index .. Block'Last) := Extract(State)(Temp_Block'First .. Temp_Block'First + Block'Last - Index);
      Temp_Block(Temp_Block'First + Block'Last - Index + 1 .. Temp_Block'Last) := Extract(State)(Temp_Block'First + Block'Last - Index + 1 .. Temp_Block'Last);
      State := Pi(set(State, Temp_Block));
      Context.Tag := Context.Tag + Extract(State);
   end Decrypt_Last_Block;

   function Get_Tag(Context : Context_T) return Tag_T is
   begin
      return +Context.Tag;
   end Get_Tag;

   function Is_Valid(Is_Tag : in Tag_T; Should_Tag : in Tag_T) return Boolean is
   begin
      return Is_Tag = Should_Tag;
   end Is_Valid;

   function Is_Valid(Context : in Context_T; Should_Tag : in Tag_T) return Boolean is
   begin
      return Get_Tag(Context) = Should_Tag;
   end Is_Valid;

   function Encrypt(Msg : u8_Array; AD : u8_Array; Public_Nonce : u8_Array; Secret_Nonce : u8_Array; Key : u8_Array) return u8_Array is
      Crypt : u8_Array(1 .. Secret_Nonce'Length + Msg'Length + Tag_Bytes);
      Ctx : Context_T;
   begin
      Initialize(Context => Ctx, Key => Key, Public_Nonce => Public_Nonce);
      Process_Header_Last_Block(Context => Ctx, Block => AD, Block_Number => 1);
      Crypt(Crypt'First .. Crypt'First + Secret_Nonce'Length - 1) := Secret_Nonce;
      Crypt(Crypt'First + Secret_Nonce'Length .. Crypt'Last - Tag_Bytes) := Msg;
      Encrypt_Secret_Message_Number(Context => Ctx, Block => Crypt(Crypt'First .. Crypt'First + Secret_Nonce'Length - 1));
      Encrypt_Last_Block(Context => Ctx, Block => Crypt(Crypt'First + Secret_Nonce'Length .. Crypt'Last - Tag_Bytes), Block_Number => 1);
      Crypt(Crypt'Last - Tag_Bytes + 1 .. Crypt'Last) := Get_Tag(Ctx);
      return Crypt;
   end Encrypt;

   procedure Decrypt(Is_Valid : out Boolean; Msg : out u8_Array; Secret_Nonce : out u8_Array; Cipher : in u8_Array; AD : in u8_Array; Public_Nonce : in u8_Array; Key : in u8_Array) is
      Ctx : Context_T;
   begin
      Initialize(Context => Ctx, Key => Key, Public_Nonce => Public_Nonce);
      Process_Header_Last_Block(Context => Ctx, Block => AD, Block_Number => 1);
      Secret_Nonce := Cipher(Cipher'First .. Cipher'First + Secret_Nonce'Length - 1);
      Msg := Cipher(Cipher'First + Secret_Nonce'Length .. Cipher'Last - Tag_Bytes);
      Decrypt_Secret_Message_Number(Context => Ctx, Block => Secret_Nonce);
      Decrypt_Last_Block(Context => Ctx, Block => Msg, Block_Number => 1);
      Is_Valid := Pi64Cipher_Spec.Is_Valid(Ctx, Cipher(Cipher'Last - Tag_Bytes + 1 .. Cipher'Last));
   end Decrypt;

end Pi64Cipher_Spec;

