--
-- Copyright (C) 2014-2017 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 GNAT.Source_Info;

with HW.Debug;
with HW.GFX.GMA.Config;
with HW.GFX.GMA.Registers;
with HW.GFX.GMA.PCode;
with HW.GFX.GMA.Transcoder;
with HW.GFX.GMA.DDI_Phy;

use HW.GFX.GMA.Registers;

package body HW.GFX.GMA.Power_And_Clocks is

   type Power_Domain is (PW1, PW2, DDI_A, DDI_BC);
   subtype Power_Well is Power_Domain range PW1 .. PW2;
   subtype Dynamic_Domain is Power_Domain range PW2 .. DDI_BC;
   subtype DDI_Domain is Dynamic_Domain range DDI_A .. DDI_BC;

   type DDI_Phy_Array is array (DDI_Domain) of DDI_Phy.T;
   Phy : constant DDI_Phy_Array := (DDI_A => DDI_Phy.A, DDI_BC => DDI_Phy.BC);

   NDE_RSTWRN_OPT_RST_PCH_Handshake_En : constant := 1 * 2 **  4;

   FUSE_STATUS_DOWNLOAD_STATUS         : constant := 1 * 2 ** 31;
   FUSE_STATUS_PG0_DIST_STATUS         : constant := 1 * 2 ** 27;

   type Power_Well_Values is array (Power_Well) of Word32;
   PWR_WELL_CTL_POWER_REQUEST : constant Power_Well_Values :=
     (PW1      => 1 * 2 ** 29,
      PW2      => 1 * 2 ** 31);
   PWR_WELL_CTL_POWER_STATE : constant Power_Well_Values :=
     (PW1      => 1 * 2 ** 28,
      PW2      => 1 * 2 ** 30);

   FUSE_STATUS_PGx_DIST_STATUS : constant Power_Well_Values :=
     (PW1   => 1 * 2 ** 26,
      PW2   => 1 * 2 ** 25);

   DBUF_CTL_DBUF_POWER_REQUEST         : constant := 1 * 2 ** 31;
   DBUF_CTL_DBUF_POWER_STATE           : constant := 1 * 2 ** 30;

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

   BXT_DE_PLL_RATIO_MASK               : constant :=      16#ff#;
   BXT_DE_PLL_PLL_ENABLE               : constant := 1 * 2 ** 31;
   BXT_DE_PLL_PLL_LOCK                 : constant := 1 * 2 ** 30;

   CDCLK_CD2X_DIV_SEL_MASK             : constant := 3 * 2 ** 22;
   CDCLK_CD2X_DIV_SEL_1                : constant := 0 * 2 ** 22;
   CDCLK_CD2X_DIV_SEL_1_5              : constant := 1 * 2 ** 22;
   CDCLK_CD2X_DIV_SEL_2                : constant := 2 * 2 ** 22;
   CDCLK_CD2X_DIV_SEL_4                : constant := 3 * 2 ** 22;
   CDCLK_CD2X_PIPE_NONE                : constant := 3 * 2 ** 20;
   CDCLK_CD2X_SSA_PRECHARGE_ENABLE     : constant := 1 * 2 ** 16;
   CDCLK_CTL_CD_FREQ_DECIMAL_MASK      : constant :=     16#7ff#;

   function CDCLK_CTL_CD_FREQ_DECIMAL (Freq : Frequency_Type) return Word32 is
   begin
      return Word32 (2 * (Freq / 1_000_000 - 1));
   end CDCLK_CTL_CD_FREQ_DECIMAL;

   BXT_PCODE_CDCLK_CONTROL             : constant := 16#17#;
   BXT_CDCLK_PREPARE_FOR_CHANGE        : constant := 16#8000_0000#;

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

   procedure PW_Off (PD : Power_Well)
   is
      Ctl1, Ctl2, Ctl3, Ctl4 : Word32;
   begin
      pragma Debug (Debug.Put_Line (GNAT.Source_Info.Enclosing_Entity));

      Read (PWR_WELL_CTL_BIOS, Ctl1);
      Read (PWR_WELL_CTL_DRIVER, Ctl2);
      Read (PWR_WELL_CTL_KVMR, Ctl3);
      Read (PWR_WELL_CTL_DEBUG, Ctl4);
      pragma Debug (Posting_Read (PWR_WELL_CTL5)); --  Result for debugging only
      pragma Debug (Posting_Read (PWR_WELL_CTL6)); --  Result for debugging only

      if ((Ctl1 or Ctl2 or Ctl3 or Ctl4) and
          PWR_WELL_CTL_POWER_REQUEST (PD)) /= 0
      then
         Wait_Set_Mask (PWR_WELL_CTL_DRIVER, PWR_WELL_CTL_POWER_STATE (PD));
      end if;

      if (Ctl1 and PWR_WELL_CTL_POWER_REQUEST (PD)) /= 0 then
         Unset_Mask (PWR_WELL_CTL_BIOS, PWR_WELL_CTL_POWER_REQUEST (PD));
      end if;

      if (Ctl2 and PWR_WELL_CTL_POWER_REQUEST (PD)) /= 0 then
         Unset_Mask (PWR_WELL_CTL_DRIVER, PWR_WELL_CTL_POWER_REQUEST (PD));
      end if;
   end PW_Off;

   procedure PW_On (PD : Power_Well)
   with
      Pre => True
   is
      Ctl1, Ctl2, Ctl3, Ctl4 : Word32;
   begin
      pragma Debug (Debug.Put_Line (GNAT.Source_Info.Enclosing_Entity));

      Read (PWR_WELL_CTL_BIOS, Ctl1);
      Read (PWR_WELL_CTL_DRIVER, Ctl2);
      Read (PWR_WELL_CTL_KVMR, Ctl3);
      Read (PWR_WELL_CTL_DEBUG, Ctl4);
      pragma Debug (Posting_Read (PWR_WELL_CTL5)); --  Result for debugging only
      pragma Debug (Posting_Read (PWR_WELL_CTL6)); --  Result for debugging only

      if ((Ctl1 or Ctl2 or Ctl3 or Ctl4) and
          PWR_WELL_CTL_POWER_REQUEST (PD)) = 0
      then
         Wait_Unset_Mask (PWR_WELL_CTL_DRIVER, PWR_WELL_CTL_POWER_STATE (PD));
      end if;

      if (Ctl2 and PWR_WELL_CTL_POWER_REQUEST (PD)) = 0 then
         Set_Mask (PWR_WELL_CTL_DRIVER, PWR_WELL_CTL_POWER_REQUEST (PD));
         Wait_Set_Mask (PWR_WELL_CTL_DRIVER, PWR_WELL_CTL_POWER_STATE (PD));

         Wait_Set_Mask (FUSE_STATUS, FUSE_STATUS_PGx_DIST_STATUS (PD));
      end if;
   end PW_On;

   procedure PD_On (PD : Power_Domain) is
   begin
      if PD in Power_Well then
         PW_On (PD);
      else
         DDI_Phy.Power_On (Phy (PD));
      end if;
   end PD_On;

   procedure PD_Off (PD : Power_Domain) is
   begin
      if PD in Power_Well then
         PW_Off (PD);
      else
         DDI_Phy.Power_Off (Phy (PD));
      end if;
   end PD_Off;

   function Need_PD (PD : Dynamic_Domain; Configs : Pipe_Configs) return Boolean
   is
   begin
      return (case PD is
         when DDI_A     => Configs (Primary).Port = eDP or
                           Configs (Secondary).Port = eDP or
                           Configs (Tertiary).Port = eDP,
         when DDI_BC    => Configs (Primary).Port = HDMI1 or
                           Configs (Primary).Port = DP1 or
                           Configs (Secondary).Port = HDMI1 or
                           Configs (Secondary).Port = DP1 or
                           Configs (Tertiary).Port = HDMI1 or
                           Configs (Tertiary).Port = DP1 or
                           Configs (Primary).Port = HDMI2 or
                           Configs (Primary).Port = DP2 or
                           Configs (Secondary).Port = HDMI2 or
                           Configs (Secondary).Port = DP2 or
                           Configs (Tertiary).Port = HDMI2 or
                           Configs (Tertiary).Port = DP2,
         when PW2       => (Configs (Primary).Port /= Disabled and
                            Configs (Primary).Port /= eDP) or
                           Configs (Secondary).Port /= Disabled or
                           Configs (Tertiary).Port /= Disabled);
   end Need_PD;

   procedure Power_Set_To (Configs : Pipe_Configs) is
   begin
      for PD in reverse Dynamic_Domain loop
         if not Need_PD (PD, Configs) then
            PD_Off (PD);
         end if;
      end loop;
      for PD in Dynamic_Domain loop
         if Need_PD (PD, Configs) then
            PD_On (PD);
         end if;
      end loop;
   end Power_Set_To;

   procedure Power_Up (Old_Configs, New_Configs : Pipe_Configs) is
   begin
      for PD in Dynamic_Domain loop
         if not Need_PD (PD, Old_Configs) and Need_PD (PD, New_Configs) then
            PD_On (PD);
         end if;
      end loop;
   end Power_Up;

   procedure Power_Down (Old_Configs, Tmp_Configs, New_Configs : Pipe_Configs)
   is
   begin
      for PD in reverse Dynamic_Domain loop
         if (Need_PD (PD, Old_Configs) or Need_PD (PD, Tmp_Configs)) and
            not Need_PD (PD, New_Configs)
         then
            PD_Off (PD);
         end if;
      end loop;
   end Power_Down;

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

   CDClk_Ref : constant := 19_200_000;

   function Normalize_CDClk (CDClk : in Int64) return Config.CDClk_Range is
     (if    CDClk <= CDClk_Ref    then CDClk_Ref
      elsif CDClk <= 144_000_000  then 144_000_000
      elsif CDClk <= 288_000_000  then 288_000_000
      elsif CDClk <= 384_000_000  then 384_000_000
      elsif CDClk <= 576_000_000  then 576_000_000
                                  else 624_000_000);

   procedure Get_Cur_CDClk (CDClk : out Config.CDClk_Range)
   is
      CDCLK_CTL : Word32;
   begin
      Registers.Read (Registers.CDCLK_CTL, CDCLK_CTL);
      CDCLK_CTL := CDCLK_CTL and CDCLK_CTL_CD_FREQ_DECIMAL_MASK;
      CDClk := Normalize_CDClk (Int64 (CDCLK_CTL) * 500_000 + 1_000_000);
   end Get_Cur_CDClk;

   procedure Set_CDClk (Freq_In : Frequency_Type)
   is
      Freq : constant Config.CDClk_Range := Normalize_CDClk (Freq_In);
      VCO : constant Int64 :=
         CDClk_Ref *
           (if Freq = CDClk_Ref then
               0
            elsif Freq = 624_000_000 then
               65
            else
               60);
      CDCLK_CD2X_Div_Sel : constant Word32 :=
        (case VCO / Freq is   -- CDClk = VCO / 2 / Div
            when 2      => CDCLK_CD2X_DIV_SEL_1,
            when 3      => CDCLK_CD2X_DIV_SEL_1_5,
            when 4      => CDCLK_CD2X_DIV_SEL_2,
            when 8      => CDCLK_CD2X_DIV_SEL_4,
            when others => CDCLK_CD2X_DIV_SEL_1);  -- for CDClk = CDClk_Ref
      CDCLK_CD2X_SSA_Precharge : constant Word32 :=
        (if Freq >= 500_000_000 then CDCLK_CD2X_SSA_PRECHARGE_ENABLE else 0);

      Success : Boolean;
   begin
      PCode.Mailbox_Write
        (MBox        => BXT_PCODE_CDCLK_CONTROL,
         Command     => BXT_CDCLK_PREPARE_FOR_CHANGE,
         Wait_Ready  => True,
         Success     => Success);
      if not Success then
         pragma Debug (Debug.Put_Line
           ("ERROR: PCODE didn't acknowledge frequency change."));
         return;
      end if;

      Write
        (Register => BXT_DE_PLL_ENABLE,
         Value    => 16#0000_0000#);
      Wait_Unset_Mask
        (Register => BXT_DE_PLL_ENABLE,
         Mask     => BXT_DE_PLL_PLL_LOCK,
         TOut_MS  => 1);   -- 200us

      Unset_And_Set_Mask
        (Register    => BXT_DE_PLL_CTL,
         Mask_Unset  => BXT_DE_PLL_RATIO_MASK,
         Mask_Set    => Word32 (VCO / CDClk_Ref));
      Write
        (Register => BXT_DE_PLL_ENABLE,
         Value    => BXT_DE_PLL_PLL_ENABLE);
      Wait_Set_Mask
        (Register => BXT_DE_PLL_ENABLE,
         Mask     => BXT_DE_PLL_PLL_LOCK,
         TOut_MS  => 1);   -- 200us

      Write
        (Register => CDCLK_CTL,
         Value    => CDCLK_CD2X_Div_Sel or
                     CDCLK_CD2X_PIPE_NONE or
                     CDCLK_CD2X_SSA_Precharge or
                     CDCLK_CTL_CD_FREQ_DECIMAL (Freq));

      PCode.Mailbox_Write
        (MBox     => BXT_PCODE_CDCLK_CONTROL,
         Command  => Word64 ((Freq + (25_000_000 - 1)) / 25_000_000));

      Config.CDClk := Freq;
   end Set_CDClk;

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

   procedure Pre_All_Off is
   begin
      pragma Debug (Debug.Put_Line (GNAT.Source_Info.Enclosing_Entity));
      Transcoder.PSR_Off;
   end Pre_All_Off;

   procedure Post_All_Off is
   begin
      pragma Debug (Debug.Put_Line (GNAT.Source_Info.Enclosing_Entity));

      for PD in reverse Dynamic_Domain loop
         PD_Off (PD);
      end loop;

      Unset_Mask (DBUF_CTL_S0, DBUF_CTL_DBUF_POWER_REQUEST);
      Wait_Unset_Mask (DBUF_CTL_S0, DBUF_CTL_DBUF_POWER_STATE);

      -- Linux' i915 never keeps the PLL disabled but runs it
      -- at a "ratio" of 0 with CDClk at its reference clock.
      Set_CDClk (CDClk_Ref);

      PW_Off (PW1);
   end Post_All_Off;

   procedure Initialize is
   begin
      pragma Debug (Debug.Put_Line (GNAT.Source_Info.Enclosing_Entity));

      -- no PCH for Broxton
      Unset_Mask (NDE_RSTWRN_OPT, NDE_RSTWRN_OPT_RST_PCH_Handshake_En);

      Wait_Set_Mask (FUSE_STATUS, FUSE_STATUS_PG0_DIST_STATUS);
      PW_On (PW1);

      Config.Max_CDClk := 624_000_000;
      Get_Cur_CDClk (Config.CDClk);
      Set_CDClk (Config.Default_CDClk_Freq);

      Set_Mask (DBUF_CTL_S0, DBUF_CTL_DBUF_POWER_REQUEST);
      Wait_Set_Mask (DBUF_CTL_S0, DBUF_CTL_DBUF_POWER_STATE);

      Config.Raw_Clock := Config.Default_RawClk_Freq;
   end Initialize;

   procedure Limit_Dotclocks
     (Configs        : in out Pipe_Configs;
      CDClk_Switch   :    out Boolean)
   is
   begin
      Config_Helpers.Limit_Dotclocks (Configs, Config.Max_CDClk);
      CDClk_Switch :=
         Config.CDClk /= Normalize_CDClk
           (Config_Helpers.Highest_Dotclock (Configs));
   end Limit_Dotclocks;

   procedure Update_CDClk (Configs : in out Pipe_Configs)
   is
      New_CDClk : constant Frequency_Type :=
         Config_Helpers.Highest_Dotclock (Configs);
   begin
      Set_CDClk (New_CDClk);
      Config_Helpers.Limit_Dotclocks (Configs, Config.CDClk);
   end Update_CDClk;

   procedure Enable_CDClk is
   begin
      -- CDClk_Ref means we have CDClk effectively disabled
      if Config.CDClk = CDClk_Ref then
         Set_CDClk (Config.Default_CDClk_Freq);
      end if;
   end Enable_CDClk;

end HW.GFX.GMA.Power_And_Clocks;
