--
-- Copyright (C) 2016 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 HW.Time;
with HW.GFX.GMA.Config;
with HW.GFX.GMA.Registers;

package body HW.GFX.GMA.Power_And_Clocks is

   FSB_FREQ_SEL_MASK : constant := 7 * 2 ** 0;
   CLKCFG_FSB_400    : constant Frequency_Type := 100_000_000;
   CLKCFG_FSB_533    : constant Frequency_Type := 133_333_333;
   CLKCFG_FSB_667    : constant Frequency_Type := 166_666_666;
   CLKCFG_FSB_800    : constant Frequency_Type := 200_000_000;
   CLKCFG_FSB_1067   : constant Frequency_Type := 266_666_666;
   CLKCFG_FSB_1333   : constant Frequency_Type := 333_333_333;

   type Div_Array is array (0 .. 7) of Pos64;

   procedure Get_VCO (VCO : out Int64; Divisors : out Div_Array)
   is
      G45_3200 : constant Div_Array := (12, 10,  8,  7,  5, 16, others => 1);
      G45_4000 : constant Div_Array := (14, 12, 10,  8,  6, 20, others => 1);
      G45_4800 : constant Div_Array := (20, 14, 12, 10,  8, 24, others => 1);
      G45_5333 : constant Div_Array := (20, 16, 12, 12,  8, 28, others => 1);
      G45_Divs : constant array (Natural range 0 .. 7) of Div_Array :=
        (G45_3200, G45_4000, G45_5333, G45_4800, others => (others => 1));

      GM45_2667 : constant Div_Array := (12,  8, others => 1);
      GM45_3200 : constant Div_Array := (14, 10, others => 1);
      GM45_4000 : constant Div_Array := (18, 12, others => 1);
      GM45_5333 : constant Div_Array := (24, 16, others => 1);
      GM45_Divs : constant array (Natural range 0 .. 7) of Div_Array :=
        (0 => GM45_3200, 1 => GM45_4000, 2 => GM45_5333, 4 => GM45_2667,
         others => (others => 1));

      -- Crestline (GM965) VCO divisor tables from Linux' i965gm_get_cdclk
      CL_3200 : constant Div_Array := (16, 10,  8, others => 1);
      CL_4000 : constant Div_Array := (20, 12, 10, others => 1);
      CL_5333 : constant Div_Array := (24, 16, 14, others => 1);
      CL_Divs : constant array (Natural range 0 .. 7) of Div_Array :=
        (0 => CL_3200, 1 => CL_4000, 2 => CL_5333,
         others => (others => 1));

      HPLLVCO : Word32;
      VCO_Sel : Natural range 0 .. 7;
   begin
      if Config.GMCH_GM965 then
         Registers.Read (Registers.GMCH_HPLLVCO_MOBILE, HPLLVCO);
         VCO_Sel := Natural (HPLLVCO and 7);
         VCO :=
           (case VCO_Sel is
               when 0 => 3_200_000_000,
               when 1 => 4_000_000_000,
               when 2 => 5_333_333_333,
               when 3 => 6_400_000_000,
               when 4 => 3_333_333_333,
               when 5 => 3_566_666_667,
               when 6 => 4_266_666_667,
               when others => 0);
         Divisors := CL_Divs (VCO_Sel);
      elsif Config.Has_GMCH_Mobile_VCO then
         Registers.Read (Registers.GMCH_HPLLVCO_MOBILE, HPLLVCO);
         VCO_Sel := Natural (HPLLVCO and 7);
         VCO :=
           (case VCO_Sel is
               when 0 => 3_200_000_000,
               when 1 => 4_000_000_000,
               when 2 => 5_333_333_333,
               --when 3 => 6_400_000_000,
               when 4 => 2_666_666_667,
               --when 5 => 4_266_666_667,
               when others => 0);
         Divisors := GM45_Divs (VCO_Sel);
      else
         Registers.Read (Registers.GMCH_HPLLVCO, HPLLVCO);
         VCO_Sel := Natural (HPLLVCO and 7);
         VCO :=
           (case VCO_Sel is
               when 0 => 3_200_000_000,
               when 1 => 4_000_000_000,
               when 2 => 5_333_333_333,
               when 3 => 4_800_000_000,
               when others => 0);
         Divisors := G45_Divs (VCO_Sel);
      end if;
   end Get_VCO;

   procedure Get_CDClk (CDClk : out Config.CDClk_Range)
   is
      use type HW.Word16;

      Tmp_Clk : Int64 := 0;

      VCO : Int64;
      Divisors : Div_Array;

      GCFGC : Word16;
      CDClk_Sel : Natural range 0 .. 7;
   begin
      if PCI_Usable then
         Get_VCO (VCO, Divisors);
         PCI_Read16 (GCFGC, 16#f0#);
         if Config.GMCH_GM965 then
            -- Linux i965gm_get_cdclk: cdclk_sel = ((tmp >> 8) & 0x1f) - 1
            if (Shift_Right (GCFGC, 8) and 16#1f#) in 1 .. 3 then
               CDClk_Sel := Natural (Shift_Right (GCFGC, 8) and 16#1f#) - 1;
            else
               CDClk_Sel := Div_Array'Last;
            end if;
         elsif Config.Has_GMCH_Mobile_VCO then
            CDClk_Sel := Natural (Shift_Right (GCFGC, 12) and 1);
         else
            CDClk_Sel := Natural (Shift_Right (GCFGC, 4) and 7);
         end if;
         Tmp_Clk := VCO / Divisors (CDClk_Sel);
      end if;

      if Tmp_Clk in Config.CDClk_Range then
         CDClk := Tmp_Clk;
      else
         if Config.GMCH_GM965 then
            CDClk := 4_000_000_000 / 20;  -- 200 MHz
         elsif Config.Has_GMCH_Mobile_VCO then
            CDClk := 5_333_333_333 / 24;
         else
            CDClk := 5_333_333_333 / 28;
         end if;
      end if;
   end Get_CDClk;

   -- The Raw Freq is 1/4 of the FSB freq
   procedure Get_Raw_Clock (Raw_Clock : out Frequency_Type)
   is
      CLK_CFG : Word32;
      type Freq_Sel is new Natural range 0 .. 7;
   begin
      Registers.Read
        (Register => Registers.GMCH_CLKCFG,
         Value => CLK_CFG);
      case Freq_Sel (CLK_CFG and FSB_FREQ_SEL_MASK) is
         when 0      => Raw_Clock := CLKCFG_FSB_1067;
         when 1      => Raw_Clock := CLKCFG_FSB_533;
         when 2      => Raw_Clock := CLKCFG_FSB_800;
         when 3      => Raw_Clock := CLKCFG_FSB_667;
         when 4      => Raw_Clock := CLKCFG_FSB_1333;
         when 5      => Raw_Clock := CLKCFG_FSB_400;
         when 6      => Raw_Clock := CLKCFG_FSB_1067;
         when 7      => Raw_Clock := CLKCFG_FSB_1333;
      end case;
   end Get_Raw_Clock;

   procedure Initialize
   is
      CDClk : Config.CDClk_Range;
   begin
      Get_CDClk (CDClk);
      Config.CDClk := CDClk;
      Config.Max_CDClk := CDClk;

      Get_Raw_Clock (Config.Raw_Clock);
   end Initialize;

   procedure Limit_Dotclocks
     (Configs        : in out Pipe_Configs;
      CDClk_Switch   :    out Boolean)
   is
   begin
      Config_Helpers.Limit_Dotclocks (Configs, Config.CDClk * 90 / 100);
      CDClk_Switch := False;
   end Limit_Dotclocks;

   procedure Power_Up (Port : Active_Port_Type; Success : out Boolean) is
   begin
      Success := True;
   end Power_Up;

end HW.GFX.GMA.Power_And_Clocks;
