--
-- Copyright (C) 2014-2018 secunet Security Networks AG
-- Copyright (C) 2019 Nico Huber <nico.h@gmx.de>
--
-- 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.Time;
with HW.Debug;
with HW.GFX.GMA.Config;
with HW.GFX.GMA.PCode;
with HW.GFX.GMA.Registers;
with HW.GFX.GMA.Transcoder;
with HW.GFX.GMA.PCH.Lynxpoint;

package body HW.GFX.GMA.Power_And_Clocks is

   LCPLL_CTL_CD_FREQ_SEL_MASK          : constant := 3 * 2 ** 26;
   LCPLL_CTL_CD_FREQ_SEL_450_MHZ       : constant := 0 * 2 ** 26;
   LCPLL_CTL_CD_FREQ_SEL_HSW_ALTERNATE : constant := 1 * 2 ** 26;
   LCPLL_CTL_CD_FREQ_SEL_BDW_540_MHZ   : constant := 1 * 2 ** 26;
   LCPLL_CTL_CD_FREQ_SEL_BDW_337_5_MHZ : constant := 2 * 2 ** 26;
   LCPLL_CTL_CD_FREQ_SEL_BDW_675_MHZ   : constant := 3 * 2 ** 26;
   LCPLL_CTL_CD_SOURCE_SELECT_FCLK     : constant := 1 * 2 ** 21;
   LCPLL_CTL_CD_SOURCE_FCLK_DONE       : constant := 1 * 2 ** 19;

   function LCPLL_CTL_CD_FREQ_SEL_BDW (CDClk : Config.CDClk_Range) return Word32
   is
     (case CDClk is
         when 675_000_000 => LCPLL_CTL_CD_FREQ_SEL_BDW_675_MHZ,
         when 540_000_000 => LCPLL_CTL_CD_FREQ_SEL_BDW_540_MHZ,
         when 450_000_000 => LCPLL_CTL_CD_FREQ_SEL_450_MHZ,
         when others      => LCPLL_CTL_CD_FREQ_SEL_BDW_337_5_MHZ);

   FUSE_STRAP_DISPLAY_CDCLK_LIMIT      : constant := 1 * 2 ** 24;

   HSW_PCODE_DE_WRITE_FREQ             : constant := 16#17#;
   BDW_PCODE_DISPLAY_FREQ_CHANGE       : constant := 16#18#;

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

   PWR_WELL_CTL_ENABLE_REQUEST   : constant := 1 * 2 ** 31;
   PWR_WELL_CTL_DISABLE_REQUEST  : constant := 0 * 2 ** 31;
   PWR_WELL_CTL_STATE_ENABLED    : constant := 1 * 2 ** 30;

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

   IPS_CTL_ENABLE          : constant := 1 * 2 ** 31;
   DISPLAY_IPS_CONTROL     : constant := 16#19#;

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

   procedure IPS_Off
   is
      Enabled : Boolean;
   begin
      pragma Debug (Debug.Put_Line (GNAT.Source_Info.Enclosing_Entity));

      if Config.Has_IPS then
         Registers.Is_Set_Mask (Registers.IPS_CTL, IPS_CTL_ENABLE, Enabled);
         if Enabled then
            if Config.Has_IPS_CTL_Mailbox then
               PCode.Mailbox_Write (DISPLAY_IPS_CONTROL, 0, Wait_Ready => True);
               Registers.Wait_Unset_Mask
                 (Register => Registers.IPS_CTL,
                  Mask     => IPS_CTL_ENABLE,
                  TOut_MS  => 42);
            else
               Registers.Unset_Mask (Registers.IPS_CTL, IPS_CTL_ENABLE);
            end if;

            pragma Debug (Debug.Put_Line ("Disabled IPS."));
            -- We have to wait until the next vblank here.
            -- 20ms should be enough.
            Time.M_Delay (20);
         end if;
      end if;
   end IPS_Off;

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

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

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

      if ((Ctl1 or Ctl2 or Ctl3 or Ctl4) and
          PWR_WELL_CTL_ENABLE_REQUEST) /= 0
      then
         Registers.Wait_Set_Mask
           (Registers.PWR_WELL_CTL_DRIVER, PWR_WELL_CTL_STATE_ENABLED);
      end if;

      if (Ctl1 and PWR_WELL_CTL_ENABLE_REQUEST) /= 0 then
         Registers.Write (Registers.PWR_WELL_CTL_BIOS, PWR_WELL_CTL_DISABLE_REQUEST);
      end if;

      if (Ctl2 and PWR_WELL_CTL_ENABLE_REQUEST) /= 0 then
         Registers.Write (Registers.PWR_WELL_CTL_DRIVER, PWR_WELL_CTL_DISABLE_REQUEST);
      end if;
   end PDW_Off;

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

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

      if ((Ctl1 or Ctl2 or Ctl3 or Ctl4) and
          PWR_WELL_CTL_ENABLE_REQUEST) = 0
      then
         Registers.Wait_Unset_Mask
           (Registers.PWR_WELL_CTL_DRIVER, PWR_WELL_CTL_STATE_ENABLED);
      end if;

      if (Ctl2 and PWR_WELL_CTL_ENABLE_REQUEST) = 0 then
         Registers.Write (Registers.PWR_WELL_CTL_DRIVER, PWR_WELL_CTL_ENABLE_REQUEST);
         Registers.Wait_Set_Mask
           (Registers.PWR_WELL_CTL_DRIVER, PWR_WELL_CTL_STATE_ENABLED);
      end if;
   end PDW_On;

   function Need_PDW (Checked_Configs : Pipe_Configs) return Boolean
   is
      Primary : Pipe_Config renames Checked_Configs (GMA.Primary);
   begin
      return
         (Config.Use_PDW_For_EDP_Scaling and then
          (Primary.Port = eDP and Requires_Scaling (Primary)))
         or
         (Primary.Port /= Disabled and Primary.Port /= eDP)
         or
         Checked_Configs (Secondary).Port /= Disabled
         or
         Checked_Configs (Tertiary).Port /= Disabled;
   end Need_PDW;

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

   procedure Pre_All_Off is
   begin
      -- HSW: disable panel self refresh (PSR) on eDP if enabled
         -- wait for PSR idling
      Transcoder.PSR_Off;
      IPS_Off;
   end Pre_All_Off;

   function Normalize_CDClk (CDClk : in Int64) return Config.CDClk_Range is
     (   if CDClk <= 337_500_000 then 337_500_000
      elsif CDClk <= 450_000_000 then 450_000_000
      elsif CDClk <= 540_000_000 then 540_000_000
                                 else 675_000_000);

   procedure Get_Cur_CDClk (CDClk : out Config.CDClk_Range)
   is
      LCPLL_CTL : Word32;
   begin
      Registers.Read (Registers.LCPLL_CTL, LCPLL_CTL);
      CDClk :=
        (if Config.Has_Broadwell_CDClk then
           (case LCPLL_CTL and LCPLL_CTL_CD_FREQ_SEL_MASK is
               when LCPLL_CTL_CD_FREQ_SEL_BDW_540_MHZ    => 540_000_000,
               when LCPLL_CTL_CD_FREQ_SEL_BDW_337_5_MHZ  => 337_500_000,
               when LCPLL_CTL_CD_FREQ_SEL_BDW_675_MHZ    => 675_000_000,
               when others                               => 450_000_000)
         else
           (case LCPLL_CTL and LCPLL_CTL_CD_FREQ_SEL_MASK is
               when LCPLL_CTL_CD_FREQ_SEL_HSW_ALTERNATE =>
                 (if    Config.Is_ULX  then 337_500_000
                  elsif Config.Is_ULT  then 450_000_000
                                       else 540_000_000),
               when others => 450_000_000));
   end Get_Cur_CDClk;

   procedure Get_Max_CDClk (CDClk : out Config.CDClk_Range)
   is
      FUSE_STRAP : Word32;
   begin
      if Config.Has_Broadwell_CDClk then
         Registers.Read (Registers.FUSE_STRAP, FUSE_STRAP);
         CDClk :=
           (if (FUSE_STRAP and FUSE_STRAP_DISPLAY_CDCLK_LIMIT) /= 0 then
               450_000_000
            elsif Config.Is_ULX then
               450_000_000
            elsif Config.Is_ULT then
               540_000_000
            else
               675_000_000);
      else
         -- We may never switch CDClk on Haswell. So from our point
         -- of view, the CDClk we start with is the maximum.
         Get_Cur_CDClk (CDClk);
      end if;
   end Get_Max_CDClk;

   procedure Set_CDClk (CDClk_In : Frequency_Type)
   is
      CDClk : constant Config.CDClk_Range :=
         Normalize_CDClk (Frequency_Type'Min (CDClk_In, Config.Max_CDClk));
      Success : Boolean;
   begin
      if not Config.Can_Switch_CDClk then
         return;
      end if;

      PCode.Mailbox_Write
        (MBox        => BDW_PCODE_DISPLAY_FREQ_CHANGE,
         Command     => 0,
         Wait_Ready  => True,
         Success     => Success);

      if not Success then
         pragma Debug (Debug.Put_Line
           ("ERROR: PCODE didn't acknowledge frequency change."));
         return;
      end if;

      Registers.Set_Mask
        (Register => Registers.LCPLL_CTL,
         Mask     => LCPLL_CTL_CD_SOURCE_SELECT_FCLK);
      Registers.Wait_Set_Mask
        (Register => Registers.LCPLL_CTL,
         Mask     => LCPLL_CTL_CD_SOURCE_FCLK_DONE);

      Registers.Unset_And_Set_Mask
        (Register    => Registers.LCPLL_CTL,
         Mask_Unset  => LCPLL_CTL_CD_FREQ_SEL_MASK,
         Mask_Set    => LCPLL_CTL_CD_FREQ_SEL_BDW (CDClk));
      Registers.Posting_Read (Registers.LCPLL_CTL);

      Registers.Unset_Mask
        (Register => Registers.LCPLL_CTL,
         Mask     => LCPLL_CTL_CD_SOURCE_SELECT_FCLK);
      Registers.Wait_Unset_Mask
        (Register => Registers.LCPLL_CTL,
         Mask     => LCPLL_CTL_CD_SOURCE_FCLK_DONE);

      PCode.Mailbox_Write
        (MBox        => HSW_PCODE_DE_WRITE_FREQ,
         Command     => (case CDClk is
                           when 675_000_000 => 3,
                           when 540_000_000 => 1,
                           when 450_000_000 => 0,
                           when others      => 2));

      Registers.Write
        (Register => Registers.CDCLK_FREQ,
         Value    => Word32 (Div_Round_Closest (CDClk, 1_000_000) - 1));

      Config.CDClk := CDClk;
   end Set_CDClk;

   procedure Post_All_Off is
   begin
      -- Reset CLKOUT_DP to disabled state
      PCH.Lynxpoint.Disable_Clkout_DP;
      PCH.Lynxpoint.Unbend_Clkout_DP;
   end Post_All_Off;

   procedure Initialize
   is
      CDClk : Config.CDClk_Range;
   begin
      -- HSW: disable power down well
      PDW_Off;

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

      Config.Raw_Clock := Config.Default_RawClk_Freq;

      -- Configure CLKOUT_DP for FDI
      if Config.Has_DDI_E then
         PCH.Lynxpoint.Enable_Clkout_DP_And_FDI_mPHY;
      end if;
   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.Can_Switch_CDClk and then
         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 Power_Set_To (Configs : Pipe_Configs) is
   begin
      if Need_PDW (Configs) then
         PDW_On;
      else
         PDW_Off;
      end if;
   end Power_Set_To;

   procedure Power_Up (Port : Active_Port_Type; Success : out Boolean) is
   begin
      if Port /= eDP then
         PDW_On;
      end if;
      Success := True;
   end Power_Up;

   procedure Power_Up (Old_Configs, New_Configs : Pipe_Configs) is
   begin
      if not Need_PDW (Old_Configs) and Need_PDW (New_Configs) then
         PDW_On;
      end if;
   end Power_Up;

   procedure Power_Down (Old_Configs, Tmp_Configs, New_Configs : Pipe_Configs)
   is
   begin
      if (Need_PDW (Old_Configs) or Need_PDW (Tmp_Configs)) and
         not Need_PDW (New_Configs)
      then
         PDW_Off;
      end if;
   end Power_Down;

end HW.GFX.GMA.Power_And_Clocks;
