--
-- Copyright (C) 2015-2016 secunet Security Networks AG
--
-- 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 2 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.
--

with HW.Time;
with HW.GFX.DP_Defs;

use type HW.Word8;
use type HW.Word32;
use type HW.GFX.DP_Defs.Aux_Message_Command;

package body HW.GFX.DP_Aux_Ch is

   DP_AUX_I2C_WRITE           : constant := 16#0#;
   DP_AUX_I2C_READ            : constant := 16#1#;
   DP_AUX_I2C_WR_STATUS_REQ   : constant := 16#2#;
   DP_AUX_I2C_MOT             : constant := 16#4#;
   DP_AUX_NATIVE_WRITE        : constant := 16#8#;
   DP_AUX_NATIVE_READ         : constant := 16#9#;

   procedure Fill_Aux_Request
     (Request  :    out DP_Defs.Aux_Request;
      Command  : in     DP_Defs.Aux_Message_Command;
      Address  : in     DP_Defs.Aux_Message_Address;
      Length   : in     DP_Defs.Aux_Payload_Length)
   is
   begin
      Request :=
        (0      => Shift_Left (Word8 (Command), 4) or
                   Word8 (Shift_Right (Word32 (Address), 16)),
         1      => Word8 (Shift_Right (Word32 (Address),  8) and 16#ff#),
         2      => Word8 (Shift_Right (Word32 (Address),  0) and 16#ff#),
         3      => Word8 (Length) - 1,
         others => 0);  -- Don't care
   end Fill_Aux_Request;

   function Is_Empty (Request : DP_Defs.Aux_Request) return Boolean is
   begin
      return Request (3) = 16#ff#;
   end Is_Empty;

   function Is_Aux_Ack (Response : DP_Defs.Aux_Response) return Boolean
   with
      Depends => (Is_Aux_Ack'Result => Response)
   is
   begin
      return (Response (0) and 16#30#) = 16#00#;
   end Is_Aux_Ack;

   function Is_Aux_Defer (Response : DP_Defs.Aux_Response) return Boolean is
   begin
      return (Response (0) and 16#30#) = 16#20#;
   end Is_Aux_Defer;

   function Is_I2C_Ack (Response : DP_Defs.Aux_Response) return Boolean
   with
      Depends => (Is_I2C_Ack'Result => Response)
   is
   begin
      return (Response (0) and 16#c0#) = 16#00#;
   end Is_I2C_Ack;

   function Is_I2C_Defer (Response : DP_Defs.Aux_Response) return Boolean is
   begin
      return (Response (0) and 16#c0#) = 16#80#;
   end Is_I2C_Defer;

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

   procedure Do_Aux_Request
     (Port              : in     T;
      Request           : in     DP_Defs.Aux_Request;
      Request_Length    : in     DP_Defs.Aux_Request_Length;
      Response          :    out DP_Defs.Aux_Response;
      Response_Length   :    out DP_Defs.Aux_Response_Length;
      Success           :    out Boolean)
   is
   begin
      for Try in Positive range 1 .. 32 loop
         Aux_Request
           (Port              => Port,
            Request           => Request,
            Request_Length    => Request_Length,
            Response          => Response,
            Response_Length   => Response_Length,
            Success           => Success);

         exit when not (Success and Is_Aux_Defer (Response));
         Time.U_Delay (500);
      end loop;

      Success := Success and then Is_Aux_Ack (Response);
   end Do_Aux_Request;

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

   procedure Aux_Read
     (Port     : in     T;
      Address  : in     DP_Defs.Aux_Message_Address;
      Length   : in out DP_Defs.Aux_Payload_Length;
      Data     :    out DP_Defs.Aux_Payload;
      Success  :    out Boolean)
   is
      Request : DP_Defs.Aux_Request;
      Response : DP_Defs.Aux_Response;
      Response_Length : DP_Defs.Aux_Response_Length;
   begin
      Data := (others => 0);  -- Initialize

      Fill_Aux_Request
        (Request  => Request,
         Command  => DP_AUX_NATIVE_READ,
         Address  => Address,
         Length   => Length);

      Do_Aux_Request
        (Port              => Port,
         Request           => Request,
         Request_Length    => 4,
         Response          => Response,
         Response_Length   => Response_Length,
         Success           => Success);

      Success := Success and then Response_Length > 1;
      if Success then
         pragma Assert (Response_Length > 1);
         Length := Response_Length - 1;
         Data (0 .. Length - 1) :=  Response (1 .. Length);
      end if;
   end Aux_Read;

   procedure Aux_Write
     (Port     : in     T;
      Address  : in     DP_Defs.Aux_Message_Address;
      Length   : in     DP_Defs.Aux_Payload_Length;
      Data     : in     DP_Defs.Aux_Payload;
      Success  :    out Boolean)
   is
      Request : DP_Defs.Aux_Request;

      Ignored_Response        : DP_Defs.Aux_Response;
      Ignored_Response_Length : DP_Defs.Aux_Response_Length;
   begin
      Fill_Aux_Request
        (Request  => Request,
         Command  => DP_AUX_NATIVE_WRITE,
         Address  => Address,
         Length   => Length);
      Request (4 .. Length + 4 - 1) := Data (0 .. Length - 1);

      pragma Warnings (GNATprove, Off,
                       "unused assignment to ""Ignored_Response*""",
                       Reason => "No response expected here");
      Do_Aux_Request
        (Port              => Port,
         Request           => Request,
         Request_Length    => 4 + Length,
         Response          => Ignored_Response,
         Response_Length   => Ignored_Response_Length,
         Success           => Success);
   end Aux_Write;

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

   procedure I2C_Out_Packet
     (Port              : in     T;
      Command           : in     DP_Defs.Aux_Message_Command;
      Address           : in     DP_Defs.Aux_Message_Address;
      Length            : in     DP_Defs.Aux_Payload_Length;
      Data              : in     DP_Defs.Aux_Payload;
      Success           :    out Boolean)
   is
      Request : DP_Defs.Aux_Request;

      Xfered      : Natural := 0;
      Tries       : Natural := 0;
      Max_Defers  : constant := 7;

      Response : DP_Defs.Aux_Response;
      Response_Length : DP_Defs.Aux_Response_Length;
   begin
      Fill_Aux_Request
        (Request  => Request,
         Command  => Command,
         Address  => Address,
         Length   => Length);
      Request (4 .. Length + 4 - 1) := Data (0 .. Length - 1);

      loop
         Do_Aux_Request
           (Port              => Port,
            Request           => Request,
            Request_Length    => (if Is_Empty (Request) then 3 else 4 + Length),
            Response          => Response,
            Response_Length   => Response_Length,
            Success           => Success);
         exit when not Success;

         if Is_I2C_Ack (Response) then
            if Response_Length <= 1 then
               Xfered := Length;
            else
               Xfered := Xfered + Natural'Min (Natural (Response (1)), Length - Xfered);
               Tries := 0;
            end if;
            exit when Xfered = Length;
         elsif Is_I2C_Defer (Response) then
            exit when Tries >= Max_Defers;
            Tries := Tries + 1;
         else -- Nak
            exit;
         end if;

         -- Command was already AUX-acked. Thus, only query for new
         -- status from now on until we got all bytes I2C-acked too.
         Fill_Aux_Request
           (Request  => Request,
            Command  => (Command and DP_AUX_I2C_MOT) or DP_AUX_I2C_WR_STATUS_REQ,
            Address  => Address,
            Length   => 0);
         Time.U_Delay (500);
      end loop;
      Success := Success and then (Is_I2C_Ack (Response) and Xfered = Length);
   end I2C_Out_Packet;

   procedure I2C_In_Packet
     (Port              : in     T;
      Command           : in     DP_Defs.Aux_Message_Command;
      Address           : in     DP_Defs.Aux_Message_Address;
      Length            : in     DP_Defs.Aux_Payload_Length;
      Response          :    out DP_Defs.Aux_Response;
      Response_Length   :    out DP_Defs.Aux_Response_Length;
      Success           :    out Boolean)
   is
      Request : DP_Defs.Aux_Request;
   begin
      Fill_Aux_Request
        (Request  => Request,
         Command  => Command,
         Address  => Address,
         Length   => Length);
      for Try in Positive range 1 .. 7 loop
         Do_Aux_Request
           (Port              => Port,
            Request           => Request,
            Request_Length    => (if Is_Empty (Request) then 3 else 4),
            Response          => Response,
            Response_Length   => Response_Length,
            Success           => Success);
         exit when not (Success and Is_I2C_Defer (Response));

         Time.U_Delay (500);
      end loop;
      Success := Success and then Is_I2C_Ack (Response);
   end I2C_In_Packet;

   procedure I2C_Empty_Packet
     (Port              : in     T;
      Command           : in     DP_Defs.Aux_Message_Command;
      Address           : in     DP_Defs.Aux_Message_Address;
      Success           :    out Boolean)
   is
      Ignored_Response        : DP_Defs.Aux_Response;
      Ignored_Response_Length : DP_Defs.Aux_Response_Length;
   begin
      pragma Warnings (GNATprove, Off,
                       "unused assignment to ""Ignored_Response*""",
                       Reason => "No response expected here");
      I2C_In_Packet
        (Port              => Port,
         Command           => Command,
         Address           => Address,
         Length            => 0,
         Response          => Ignored_Response,
         Response_Length   => Ignored_Response_Length,
         Success           => Success);
   end I2C_Empty_Packet;

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

   procedure Do_I2C_Write
     (Port     : in     T;
      Address  : in     I2C.Transfer_Address;
      Length   : in     DP_Defs.Aux_Payload_Length;
      Data     : in     DP_Defs.Aux_Payload;
      Success  :    out Boolean)
   is
      Ignored_Success : Boolean;
   begin
      -- I2C address "start" packet
      I2C_Empty_Packet
        (Port     => Port,
         Command  => DP_AUX_I2C_WRITE or DP_AUX_I2C_MOT,
         Address  => DP_Defs.Aux_Message_Address (Address),
         Success  => Success);

      if Success then
         I2C_Out_Packet
           (Port              => Port,
            Command           => DP_AUX_I2C_WRITE or DP_AUX_I2C_MOT,
            Address           => DP_Defs.Aux_Message_Address (Address),
            Length            => Length,
            Data              => Data,
            Success           => Success);

         pragma Warnings
           (GNATprove, Off, "unused assignment to ""Ignored_Success""",
            Reason => "Doesn't matter as long as the transfer itself succeeds");
         -- I2C address "stop" packet
         I2C_Empty_Packet
           (Port     => Port,
            Command  => DP_AUX_I2C_WRITE,
            Address  => DP_Defs.Aux_Message_Address (Address),
            Success  => Ignored_Success);
      end if;
   end Do_I2C_Write;

   procedure Do_I2C_Read
     (Port     : in     T;
      Address  : in     I2C.Transfer_Address;
      Length   : in out I2C.Transfer_Length;
      Data     : in out I2C.Transfer_Data;
      Success  :    out Boolean)
   is
      Xfered   : Natural := 0;
      Chunk    : DP_Defs.Aux_Payload_Length := DP_Defs.Aux_Payload_Length'Last;

      Tries       : Natural := 0;
      Max_Tries   : constant := 4;

      Response : DP_Defs.Aux_Response;
      Response_Length : DP_Defs.Aux_Response_Length;

      Ignored_Success : Boolean;
   begin
      -- I2C address "start" packet
      I2C_Empty_Packet
        (Port     => Port,
         Command  => DP_AUX_I2C_READ or DP_AUX_I2C_MOT,
         Address  => DP_Defs.Aux_Message_Address (Address),
         Success  => Success);

      while Success and then (Xfered < Length and Tries < Max_Tries) loop
         I2C_In_Packet
           (Port              => Port,
            Command           => DP_AUX_I2C_READ or DP_AUX_I2C_MOT,
            Address           => DP_Defs.Aux_Message_Address (Address),
            Length            => Natural'Min (Chunk, Length - Xfered),
            Response          => Response,
            Response_Length   => Response_Length,
            Success           => Success);

         if Success and then Response_Length >= 2 then
            Chunk := Natural'Min (Response_Length - 1, Length - Xfered);
            Data (Xfered .. Xfered + Chunk - 1) := Response (1 .. Chunk);
            Xfered := Xfered + Chunk;
            Tries := 0;
         else
            Tries := Tries + 1;
         end if;
         pragma Loop_Invariant (Xfered <= Length);
      end loop;

      if Success then
         pragma Warnings
           (GNATprove, Off, "unused assignment to ""Ignored_Success""",
            Reason => "Doesn't matter as long as the transfer itself succeeds");
         -- I2C address "stop" packet
         I2C_Empty_Packet
           (Port     => Port,
            Command  => DP_AUX_I2C_READ,
            Address  => DP_Defs.Aux_Message_Address (Address),
            Success  => Ignored_Success);
      end if;

      Success := Success and then Tries < Max_Tries;
      Length := Xfered;
   end Do_I2C_Read;

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

   procedure I2C_Write_Byte
     (Port     : in     T;
      Address  : in     I2C.Transfer_Address;
      Offset   : in     Word8;
      Value    : in     Word8;
      Success  :    out Boolean)
   is
      Payload : constant DP_Defs.Aux_Payload := (Offset, Value, others => 16#00#);

      Length : constant I2C.Transfer_Length  := 1;
   begin
      Do_I2C_Write (Port, Address, 1 + Length, Payload, Success);
   end I2C_Write_Byte;

   procedure I2C_Read_Byte
     (Port     : in     T;
      Address  : in     I2C.Transfer_Address;
      Offset   : in     Word8;
      Value    :    out Word8;
      Success  :    out Boolean)
   is
      Index_Payload : constant DP_Defs.Aux_Payload := (Offset, others => 16#00#);

      Length : I2C.Transfer_Length  := 1;
      Data   : I2C.Transfer_Data    := (others => 16#00#);
   begin
      Do_I2C_Write (Port, Address, 1, Index_Payload, Success);

      if Success then
         Do_I2C_Read (Port, Address, Length, Data, Success);
         Success := Success and Length = 1;
      end if;
      Value := Data (0);
   end I2C_Read_Byte;

   procedure I2C_Read
     (Port     : in     T;
      Address  : in     I2C.Transfer_Address;
      Length   : in out I2C.Transfer_Length;
      Data     :    out I2C.Transfer_Data;
      Success  :    out Boolean)
   is
      Index_Payload : DP_Defs.Aux_Payload;
   begin
      Data := (others => 16#00#);

      Index_Payload := (others => 16#00#);   -- send index 0
      Do_I2C_Write (Port, Address, 1, Index_Payload, Success);

      if Success then
         Do_I2C_Read (Port, Address, Length, Data, Success);
      end if;
   end I2C_Read;

end HW.GFX.DP_Aux_Ch;
