--[[
  Copyright (c) 2023 by Plexim GmbH
  All rights reserved.

  A free license is granted to anyone to use this software for any legal
  non safety-critical purpose, including commercial applications, provided
  that:
  1) IT IS NOT USED TO DIRECTLY OR INDIRECTLY COMPETE WITH PLEXIM, and
  2) THIS COPYRIGHT NOTICE IS PRESERVED in its entirety.

  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
  OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  SOFTWARE.
--]]

--[[
  This file has PLL Configuration Functions that work for targets with a combination of settings
  The minimum requirement is SYSDIV and IMULT, with limits for OSCCLK, PLLRAWCLK and SYSCLK.

  Optionally targets can also configure REFDIV, FMULT and ODIV ranges and limits for
  the corresponding intermediate clocks: INTCLK, VCOCLK

  NOTE: This file works with multiplier values, not register values, conversion to register values
  must be done by calling code (because it varies on different targets).

  NOTE: We have seen no need to support odd values for SYSDIV, so they are not supported above 1,
  even for targets with LSB support.

  Example of target with FMULT (2837x):

  -- sysdiv = {1,2,4,6,8,10,12,14,16, ... 126}
  -- imult = {1...127}
  -- fmult = {0...3}

  Example target with REFDIV and ODIV (28003x):

  -- sysdiv = {1,2,4,6,8,10,12,14,16, ... 126} -- even only above 1 (we do not support LSB)
  -- imult  = {1...127}
  -- odiv   = {1..32}
  -- refdiv = {1..32}

--]]

local PLL = {}
local U = require('common.utils')

-- we will check limit keys against this list to catch typos
-- also the expected fields are listed, with dummy values
local valid_keys = {
  -- Note, assigment to zero here is to get the list of expected keys to not be nil
  -- A subset of these values should be provided from the data sheet by user code using :set()

  INTERNAL_OSCILLATOR_SYSCLK = {max = 0},   -- limit imposed on some targets for flashing based on internal clock tolerance
  INTERNAL_OSCILLATOR_HZ = {value = 0},

  INTCLK = {min = 0, max = 0},
  OSCCLK = {min = 0, max = 0},
  VCOCLK = {min = 0, max = 0},
  PLLRAWCLK = {min = 0, max = 0},
  SYSCLK = {min = 0, max = 0},

  -- if a value is not provided, a multiplier of 1 will be defaulted to in finalize()
  -- mulitpliers are all assumed to have a minimum value of 1, so only a max value is required by
  -- calling code
  ODIV = {max = 1},
  SYSDIV = {max = 1},
  FMULT = {divisor = 1},  -- default for no FMULT
  IMULT = {max = 1},
  REFDIV = {max = 1},

  -- These are Plexim imposed limits
  PLX_SYSCLK = {min = 0},  -- minimum sysclock to avoid kernel panic
}

-- print(dump(valid_keys))

-- Class for Configuring the Clock Limits needed for PLL settings
local PLLClockLimits = {}

function PLLClockLimits:set(key, values)
  if self.finalized then
    error([[
      You must set all the PLL Configuration limits 
      before the limits are finalized (by get()).]])
  end
  local expected = valid_keys[key]
  if not expected then
    error('Invalid Key for PLL Configuration: '..key)
  end
  -- this is a valid key value, check that all required limits are provided
  for k, v in pairs(expected) do
    -- verify all required values where provided
    if values[k] == nil then
      error([[
        Required value not provided for PLL Configuration:
        '%(key)s' requires '%(k)s'.]] % {key = key, k = k})
    end
  end
  self[key] = values
  return true
end

function PLLClockLimits:finalize()
  if self.finalized then
    return
  end

  -- check required values have been set by user
  for i, k in ipairs({'IMULT', 'SYSDIV', 'SYSCLK', 'PLLRAWCLK', 'OSCCLK'}) do
    if self[k] == nil then
      error('PLL Config requires settings for '..k)
    end
  end

  if not self.PLX_SYSCLK then
    self.PLX_SYSCLK = {min = 50e6}  -- default of 50MHz to avoid Kernel Panic
  end

  if self.useInternalOsc and self.INTERNAL_OSCILLATOR_SYSCLK then
    -- if using the internal oscillator, a lower limit is required to flash
    -- internal clock on the 2837x for example has a 3% error
    self.SYSCLK.max = math.min(self.INTERNAL_OSCILLATOR_SYSCLK.max,
                               self.SYSCLK.max)
  end

  -- Ensure this is a valid configuration for a clock:
  -- that we have enough info and valid defaults for anything not configured

  if not self.ODIV then
    self.ODIV = {max = 1}
  end

  if not self.VCOCLK then
    self.VCOCLK = self.PLLRAWCLK  -- this implies no ODIV
  end

  if not self.REFDIV then
    self.REFDIV = {max = 1}
  end

  if not self.INTCLK then
    self.INTCLK = self.OSCCLK  -- this implies no REFDIV
  end

  if not self.FMULT then
    self.FMULT = {divisor = 1}
  end

  if not U.isPositiveIntScalar(self.FMULT.divisor) then
    error('PLL Configuration requires an positive integer value for FMULT.')
  end
  -- calculate FMULT possible values
  local fmult_vals = {}
  for f = 0, self.FMULT.divisor do
    table.insert(fmult_vals, f / self.FMULT.divisor)
  end
  self.FMULT.values = fmult_vals

  self.finalized = true
end

function PLLClockLimits:getLimits()
  self:finalize()
  return self
end

-- Methods of the PLL module
function PLL.new(useInternalOsc, optionalClockDebugName)
  local self = {
    finalized = false,
  }
  self.useInternalOsc = useInternalOsc
  if optionalClockDebugName then
    self.clockName = optionalClockDebugName
  else
    self.clockName = 'System Clock'
  end

  setmetatable(self, {__index = PLLClockLimits})
  return self
end

-- returns nil in the case of no valid pll setting
local function getPllConfigRecord(clkInHz, clkOutHz, limits)
  -- check for valid SysClock settings for this target
  if (clkOutHz < limits.SYSCLK.min) or (clkOutHz > limits.SYSCLK.max) then
    return
  end

  if (clkInHz < limits.OSCCLK.min) or (clkInHz > limits.OSCCLK.max) then
    return
  end

  local sysdiv_min = math.ceil(math.max(1, limits.PLLRAWCLK.min / clkOutHz))
  local sysdiv_max = math.floor(math.min(limits.SYSDIV.max, 
                                         limits.PLLRAWCLK.max / clkOutHz))

  for sysdiv = sysdiv_min, sysdiv_max do
    if (sysdiv == 1 or (sysdiv % 2) == 0) then  -- skip all odd numbers except 1
      local rawPllClk = clkOutHz * sysdiv
      local odiv_min = math.ceil(math.max(1, limits.VCOCLK.min / rawPllClk))
      local odiv_max = math.floor(math.min(limits.ODIV.max,
                                           limits.VCOCLK.max / rawPllClk))

      for odiv = odiv_min, odiv_max do
        local vcoClk = rawPllClk * odiv
        local d = limits.FMULT.divisor
        local dxmult_min = math.ceil(math.max(1 * d, 
                                              d * vcoClk / limits.INTCLK.max))
        local dxmult_max = math.floor(math.min(limits.IMULT.max * d + (d - 1),
                                               d * vcoClk / limits.INTCLK.min))

        for dxmult = dxmult_min, dxmult_max do  -- increment by 1 because we are working in d*mult units
          -- the multiplier includes 1/d fractions, so we need d*mult to be an even number
          local intClk = d * vcoClk / dxmult
          local refdiv_min = math.ceil(math.max(1, limits.OSCCLK.min / intClk))
          local refdiv_max = math.floor(math.min(limits.REFDIV.max,
                                                 limits.OSCCLK.max / intClk))
          -- print(refdiv_min, refdiv_max, intClk/1e6, limits.INTCLK.min)
          for refdiv = refdiv_min, refdiv_max do
            local resultOscClk = intClk * refdiv
            if resultOscClk == clkInHz then
              -- this is a valid clock configuration
              local imult = math.floor(dxmult / d)
              local fmult = (dxmult % d)

              return {
                imult = imult,
                fmult = fmult,
                sysdiv = sysdiv,
                odiv = odiv,
                refdiv = refdiv,
                pllRawClkHz = clkInHz * (imult + fmult/4) / odiv,
              }
            end
          end
        end
      end
    end
  end
end

-- this function formats the error messagse and returns a string
local function getAchievableSysClockFreqMessage(oscClkHz, targetSysClkHz, limits)
  local possibleSysClks = {}
  local err = ''

  local minSysClkHz = limits.SYSCLK.min
  local maxSysClkHz = limits.SYSCLK.max

  if (targetSysClkHz > maxSysClkHz) 
  or (targetSysClkHz < minSysClkHz) then
    err = [[
      The %(adj)s %(clock)s Frequency for the selected clock is %(freq)s.
      ]] % {
      adj = targetSysClkHz > maxSysClkHz and 'maximum' or 'minimum',
      clock = limits.clockName,
      freq = U.toMHzString(targetSysClkHz > maxSysClkHz and maxSysClkHz or minSysClkHz),
    }
  end

  -- early return for invalid input clock frequency
  if     (oscClkHz < limits.OSCCLK.min) then
    return ('The minimum input clock Frequency is '..U.toMHzString(limits.OSCCLK.min)..'.\n')
  elseif (oscClkHz > limits.OSCCLK.max) then
    return ('The maximum input clock Frequency is '..U.toMHzString(limits.OSCCLK.max)..'.\n')
  end

  for refdiv = 1, limits.REFDIV.max do
    local intClkHz = oscClkHz / refdiv
    if (intClkHz == math.floor(intClkHz)) and (limits.INTCLK.min <= intClkHz) and (intClkHz <= limits.INTCLK.max) then
      -- for each possible multiplier
      for imult = 1, limits.IMULT.max do
        for i, fmult in ipairs(limits.FMULT.values) do
          -- check if VOC is in range
          local vcoClkHz = intClkHz * (imult + fmult)
          if (vcoClkHz == math.floor(vcoClkHz)) and (limits.VCOCLK.min <= vcoClkHz) and (vcoClkHz <= limits.VCOCLK.max) then
            -- for each odiv
            for odiv = 1, limits.ODIV.max do
              -- check if PLLRAW is in range
              local pllRawHz = vcoClkHz / odiv
              if (pllRawHz == math.floor(pllRawHz)) and (limits.PLLRAWCLK.min <= pllRawHz) and (pllRawHz <= limits.PLLRAWCLK.max) then
                -- for each valid SYSDIV
                for sysdiv = 1, limits.SYSDIV.max do
                  if (sysdiv == 1 or (sysdiv % 2) == 0) then
                    -- check if SYSCLK is in range
                    local sysClkHz = pllRawHz / sysdiv
                    if (sysClkHz == math.floor(sysClkHz)) and (minSysClkHz <= sysClkHz) and (sysClkHz <= maxSysClkHz) then
                      -- print settings
                      -- print(sysClkHz, (imult+fmult), sysdiv, sysdiv, odiv, refdiv)
                      -- Add this configuration to the list of reachable system clocks
                      possibleSysClks[tostring(sysClkHz)] = 
                         imult + fmult..', '..sysdiv..', '..odiv..', '..refdiv
                    end
                  end
                end
              end
            end
          end
        end
      end
    end
  end

  -- print("Possible SysClks include:")
  -- print(dump(possibleSysClks))

  local a = {}
  for n in pairs(possibleSysClks) do table.insert(a, tonumber(n)) end
  table.sort(a)

  -- U.toMHzString(a[#a])
  -- for i,n in ipairs(a) do print(n .. " " .. i) end

  -- find the closest frequency, if no match found, then print the closest greater and smaller
  for i = 1, #a do
    if (a[i] == targetSysClkHz) then
      print(a[i])
      -- this code is not executed if the option is valid, but this is useful for debugging
      return ('SUCCESSFUL MATCH! Target Frequency can be met: '..U.toMHzString(targetSysClkHz)..'.')
    end
    if (a[i] > targetSysClkHz) then
      if (i == 1) then
        -- no possible lower frequency
        return (err..'The closest achievable frequency is '..U.toMHzString(a[1])..'.')
      else
        return (err..'The closest achievable frequencies are '..
          U.toMHzString(a[i - 1])..' and '..U.toMHzString(a[i])..'.')
      end
    end
  end
  -- if we got here then the closest frequency is the last one in the list
  return (err..'The closest achievable frequency is '..U.toMHzString(a[#a])..'.')
end

function PLL.getClockConfiguration(limits, clkin, targetSysClkHz)
  -- print(dump(U.copyTable(limits))) -- need to make a copy to display circular references?
  -- print(limits.PLLRAWCLK.min, limits.PLLRAWCLK.max)

  -- enforce lower SysClock limit to avoid Kernel Panic
  if targetSysClkHz < limits.PLX_SYSCLK.min then
    U.error(
      'The minimum %(clockName)s frequency allowed by Plecs is %(minSysClk)s.'
      % {
        minSysClk = U.toMHzString(limits.PLX_SYSCLK.min),
        clockName = limits.clockName
      })
  end

  local defaultErrorMsg =
     'Unable to achieve the desired %(clockName)s frequency (%(sysClkMHz)s ) using %(source)s input clock of %(clkinMHz)s.\n' %
     {
       sysClkMHz = U.toMHzString(targetSysClkHz),
       clkinMHz = U.toMHzString(clkin),
       source = limits.useInternalOsc and 'internal' or 'external',
       clockName = limits.clockName
     }

  -- establish PLL settings
  local pllConfig = getPllConfigRecord(clkin, targetSysClkHz, limits)
  -- print('pllconfig calculated as', dump(pllConfig))
  if pllConfig == nil then
    U.error(
      defaultErrorMsg..getAchievableSysClockFreqMessage(clkin,
                                                        targetSysClkHz,
                                                        limits))
  end

  return pllConfig
end

return PLL
