--[[
  Copyright (c) 2024 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.
--]]

local U = require('common.utils')

local static = {}

local Module = {}

function Module.getBlock(globals, cpu)
  local Comp = require('common.block').getBlock(globals, cpu)
  if not static[cpu] then
    static[cpu] = {
      numInstances = 0,
      instances = {},
      finalized = false,
    }
  end

  Comp.instance = static[cpu].numInstances
  static[cpu].numInstances = static[cpu].numInstances + 1

  -- inport and outport matching
  local inIdx = U.enum({'sync', 'pos'}) -- FIX ME: Why do we call the second input 'pos'
  local outIdx = U.enum({'trip'})

  function Comp:checkMaskParameters()
    if self:targetMatches({'2806x', '2833x'}) then
      U.error('This block is not supported by the %(target)s target.'
        % {target = globals.target.getFamilyPrefix()})
    end

    -- establish the comparator input pin
    self.cbx_senseInputType = U.enforceMask_newComboBox(
      'SenseInputType', {'adc', 'pga'})

    if self.cbx_senseInputType.equals('adc') then
      local adcUnitAsChar = U.comboAsChar(Block.Mask.AdcUnit)
      local adcChannel = U.enforceMask(U.isNonNegativeIntScalar, 'AdcChannel')
      self.compPinStr = '%s%d' % {adcUnitAsChar, adcChannel}
    elseif self.cbx_senseInputType.equals('pga') then
      local pgaUnit = U.enforceMask(U.isNonNegativeIntScalar, 'PgaUnit')
      self.compPinStr = 'PGA%d' % {pgaUnit}
    else
      error(
        'Unsupported option for Sense Input, no comparator input pin detected.')
    end

    self.cbx_compOutPolarity = U.enforceMask_newComboBox(
      'Polarity', {'nonInverted', 'inverted'})

    if U.comboEnabled(Block.Mask.RampGenerator) then
      local rampSlopeDesired = U.enforceMask(U.isNonNegativeScalar, 'RampSlope')
      if rampSlopeDesired > 0 then
        -- A ramp rate of 0 is allowed but effectively disables the ramp.
        -- Only create the optional ramp parameter when the user specifies
        -- a non-zero ramp rate; otherwise, treat the ramp as disabled.
        self.opt_rampGenerator = {
          rampSlope = rampSlopeDesired,
          -- If in future ramp direction is made independent of polarity, set 
          -- the rampDirection here. (see #1600 for details)
        }
      else
        self:logLine(
          'Ramp generator enabled but ramp rate is zero, equivalent to ramp being disabled.'
        )
      end
    end

  end

  function Comp:p_getDirectFeedthroughCode()
    local Require = ResourceList:new()
    local InitCode = U.CodeLines:new()
    local OutputSignal = StringList:new()
    local OutputCode = U.CodeLines:new()

    table.insert(static[self.cpu].instances, self.bid)

    -- establish TRIPINx signal number early
    -- (also needed by EPWM modules in NDF code - be careful if you rename this variable)
    self.tripInputSignal = globals.target.getNextAvailableTripInput()
    if not self.tripInputSignal then
      U.error('Maximal number of trip inputs exceeded.')
    end

    --[[
        Establish if compare input is constant. The 'pos' port
        represents the compare threshold.

        self.threshold0 (aka constantTresholdValue) will be:
          - nil if not a constant, or
          - a number holding the constant threshold value.
    --]]
    if not U.portIsConnected(inIdx.pos) then
      self.threshold0 = 0
    else
      self.threshold0 = U.portValueGetConstant(inIdx.pos) -- nil if not constant
    end
    

    OutputSignal:append('{tevent = {bid = %i}}' % {self:getId()})
    if not self.threshold0 then
      -- The threshold is not a constant, this is the update code.
      local thresholdInputStr = Block.InputSignal[inIdx.pos][1]
      if self.opt_rampGenerator then
        OutputCode:append('PLXHAL_CMPSS_setPeak(%i, %s);' % {
          self.instance,
          thresholdInputStr,
        })
      else
        OutputCode:append('PLXHAL_CMPSS_setDacHigh(%i, %s);' % {
          self.instance,
          thresholdInputStr,
        })
      end
    end
    return {
      InitCode = InitCode,
      OutputCode = OutputCode,
      OutputSignal = OutputSignal,
      Require = Require,
      UserData = {bid = self:getId()}
    }
  end

  function Comp:p_getNonDirectFeedthroughCode()
    local Require = ResourceList:new()
    local UpdateCode = U.CodeLines:new()

    -- create and configure CMPSS instance
    -- determine high comparator available at pin (high has ramp on all devices)
    -- TODO: also consider the use of the low comparator (especially on 65x devices)

    local comparators = globals.target.getComparatorsForPin(self.compPinStr)
    if comparators and comparators.high then
      self.compHigh = comparators.high
    else
      U.error('No suitable comparator found for pin %s.' % {self.compPinStr}) -- TODO: Move this code to checkParams to generate a better message?
    end

    local compHighParams = {
      opt_pinMux = self.compHigh.mux,
      threshold = self.threshold0 or 3.3,
      invertCompare = self.cbx_compOutPolarity.equals('inverted'),
    }
    if self.opt_rampGenerator then
      local rampSlope = self.opt_rampGenerator.rampSlope
      
      -- If ramp direction is ever decoupled from comparator polarity (see #1600 for details)
      -- update this logic to follow the new combo-box design. 
      local b_rampIsIncrementing = self.cbx_compOutPolarity.equals('inverted')

      -- determine synchronizing PWM block
      local peripheralSyncString = Block.InputSignal[inIdx.sync][1]
      -- example input: {syncper = {bid = 9}}
      peripheralSyncString = peripheralSyncString:gsub('%s+', '')  -- remove whitespace
      if peripheralSyncString:sub(1, #'{syncper') ~= '{syncper' then
        U.error("'sync' port must be connected to the PER port of a PWM block.")
      end
      local peripheralSync = eval(peripheralSyncString)['syncper']
      if peripheralSync['bid'] == nil then
        error('Malformed peripheralSync expression: %s' % {t_event})
      end
      local epwmUnit = globals.instances[peripheralSync['bid']]:getParameter(
        'first_unit')

      -- process ramp settings 
      -- TODO: consider refactoring this section as plx_cmpss.h performs similar calculations
      -- potentially move the calculation to PLX_CMPSS_configure
      local rampCountToVolts = 3.3 / 65536
      local rampSlopeFloat = math.abs(rampSlope)
      local rampSlopeCounts = math.ceil(rampSlopeFloat /
        (rampCountToVolts * globals.target.getCleanSysClkHz()))
      local actualRamp = rampSlopeCounts *
         (rampCountToVolts * globals.target.getCleanSysClkHz())

      
      self:logLine(
      'Comparator ramp DAC step set to %i, resulting in ramp of %f A/s, desired was %f A/s.' %
      {rampSlopeCounts, actualRamp, rampSlopeFloat})
      self.opt_rampGenerator.rampSlopeCounts = rampSlopeCounts -- store the ramp slope integer value
      compHighParams.opt_ramp = {
        peripheralSyncEpwmUnit = epwmUnit,
        rampDecrement = rampSlopeCounts,
        desiredRamp = rampSlopeFloat,
        actualRamp = actualRamp,
        b_rampIsIncrementing = b_rampIsIncrementing,
      }
      
    end

    local compHighObj = self:makeBlock('cmpss')
    compHighObj:createImplicit(
      self.compHigh.unit,
      {
        analogPin = self.compPinStr,
        tripSignal = self.tripInputSignal,
        opt_compHigh = compHighParams,
      }, Require, nil)
    return {Require = Require, UpdateCode = UpdateCode}
  end

  function Comp:finalizeThis(c)
    local rampStepVal = 0
    if self.opt_rampGenerator then
      rampStepVal = self.opt_rampGenerator.rampSlopeCounts
    end

    c.PreInitCode:append([[
      PLX_CMPSS_configure(CmpssHandles[%(instance)i], CMPSS%(cmp)i_BASE, %(rampStepVal)i);
      ]] % {
      instance = self.instance,
      cmp = self.compHigh.unit,
      rampStepVal = rampStepVal,
    })

    if self.threshold0 then
      if self.opt_rampGenerator then
        c.PostInitCode:append([[
          // ramp with constant peak value
          PLX_CMPSS_setPeak(CmpssHandles[%(instance)i], %(peak)f);
          ]] % {
          instance = self.instance,
          peak = self.threshold0,
        })
      else
        c.PostInitCode:append([[
          // constant peak value
          PLX_CMPSS_setDacHigh(CmpssHandles[%(instance)i], %(value)f);
          ]] % {
          instance = self.instance,
          value = self.threshold0,
        })
      end
    end
  end

  function Comp:finalize(c)
    if static[self.cpu].finalized then
      return
    end

    c.Include:append('plx_cmpss.h')
    c.Declarations:append([[
      PLX_CMPSS_Handle_t CmpssHandles[%(numInstances)i];
      PLX_CMPSS_Obj_t CmpssObj[%(numInstances)i];
      ]] % {
      numInstances = static[self.cpu].numInstances
    })
    c.PreInitCode:append([[
      for (int i = 0; i < %(numInstances)i; i++) {
        CmpssHandles[i] = PLX_CMPSS_init(&CmpssObj[i], sizeof(CmpssObj[i]));
      }
      ]] % {
      numInstances = static[self.cpu].numInstances
    })

    local setPeakCurrentCode = [[
      void PLXHAL_CMPSS_setDacHigh(uint16_t aHandle, float aVal){
        PLX_CMPSS_setDacHigh(CmpssHandles[aHandle], aVal);
      }

      void PLXHAL_CMPSS_setPeak(uint16_t aHandle, float aPeak){
        PLX_CMPSS_setPeak(CmpssHandles[aHandle], aPeak);
      }
    ]]
    c.Declarations:append(setPeakCurrentCode)

    for _, bid in ipairs(static[self.cpu].instances) do
      local Comp = globals.instances[bid]
      if Comp:getCpu() == self.cpu then
        Comp:finalizeThis(c)
      end
    end
    static[self.cpu].finalized = true
  end
  
  return Comp
end

return Module
