--[[
  Copyright (c) 2024-2026 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({'TH', 'refH', 'TL', 'refL'})
  local outIdx = U.enum({'outH', 'outL'})

  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

    -- Main Tab
    -- 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.')
    end

    -- A lowSideOnly option can be added here in the future if needed.
    local cbx_activeComparators = U.enforceMask_newComboBox(
      'ActiveComps', {'highSideOnly', 'highSideAndLowSide'})   
    self.b_lowSideCompUsed = cbx_activeComparators.equals('highSideAndLowSide')
  
    -- Compare Tabs helper function:
    local function configureCompTab(highLowChar)
      assert((highLowChar == 'H') or (highLowChar == 'L'), 
             'Tab must be identified by H or L.')
      
      local ret = {
        -- output polarity is not optional
        cbx_outPolarity = U.enforceMask_newComboBox(
          'Polarity%s' % {highLowChar}, {'nonInverted', 'inverted'})
      }

      if U.comboEnabled(Block.Mask['RampGenerator%s' % {highLowChar}]) then
        -- check for ramp validity
        local rampSlopeSteps = U.passThrough(Block.Mask.offline['rampSlopeSteps%s' % {highLowChar}])
        if rampSlopeSteps > 0 then
          -- a positive slope means we are configuring the ramp
          ret.opt_rampConfig = {
            rampSlopeCounts = rampSlopeSteps,
            rampSlope =
               U.enforceMask(U.isNonNegativeScalar, 'RampSlope%s' % {highLowChar}),
            b_rampIsIncrementing = ret.cbx_outPolarity.equals('inverted'),
            rampClkDivider =
               U.passThrough(Block.Mask.offline['rampClkDivider%s' % {highLowChar}]) or 1,
          }
        else
          -- we treat a ramp of zero the same as disabled:
          self:logLine(
            'Ramp generator for %(side)s-side comparator enabled but ramp rate is zero, equivalent to ramp being disabled.'
            % {side = highLowChar}
          )
        end
      end
      return ret
    end


    -- COMPH Tab
    self.compH = configureCompTab('H')

    -- Optional COMPL Tab
    if self.b_lowSideCompUsed then
      self.opt_compL = configureCompTab('L')
    end

    -- Variables used only for calculation by both offline model and code gen
    self.rampDacLsbVoltage = U.passThrough(Block.Mask.offline.dacLsbVoltage)
  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)

    local b_anyRampIncrementing =
       (self.compH.opt_rampConfig
         and self.compH.opt_rampConfig.b_rampIsIncrementing)
       or (self.opt_compL
         and self.opt_compL.opt_rampConfig
         and self.opt_compL.opt_rampConfig.b_rampIsIncrementing)
    if b_anyRampIncrementing then
      if not globals.target.getTargetParameters().comps.b_supportsRampDirection then
        U.error(
          'Incrementing ramp not supported by the %(target)s target.'
          % {target = globals.target.getFamilyPrefix()})
      end
    end

    -- determine high-side comparator available at pin (high-side has ramp on all devices)
    local comparators = globals.target.getComparatorsForPin(self.compPinStr)
    if comparators and comparators.high then
      self.compH.hwModule = comparators.high
    else
      U.error('No suitable comparator found for pin %s.' % {self.compPinStr})
    end

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

    -- Emit the COMPH trip event signal (hardware: CTRIPH). Consumed by blocks that
    -- connect to this comparator output, e.g. EPWM trip zone or a future output-pin block.
    OutputSignal:append('{compHTripEvent = {bid = %d}}' % {self:getId()})

    if self.b_lowSideCompUsed then
      local b_supportsLowSideRamp = globals.target.getTargetParameters().comps.b_supportsLowSideRamp
      if not b_supportsLowSideRamp and self.opt_compL.opt_rampConfig then
        U.error('Ramp generator for low-side comparator not supported by the %(target)s target.'
          % {target = globals.target.getFamilyPrefix()})
      end

      -- determine low comparator available at pin
      if comparators and comparators.low then
        self.opt_compL.hwModule = comparators.low
      else
        U.error('No suitable comparator found for pin %s.' % {self.compPinStr})
      end

      -- On targets that support separate CTRIPH/CTRIPL XBAR mux entries,
      -- allocate a second TRIPINx for the low-side comparator.
      -- On older targets, low-side reuses the same trip (OR'd together).
      if self:targetMatches({'28P55x', '28P65x', '29H85x'}) then
        self.opt_tripInputSignalL = globals.target.getNextAvailableTripInput()
        if not self.opt_tripInputSignalL then
          U.error('Maximum number of trip inputs exceeded.')
        end
      else
        self.opt_tripInputSignalL = self.tripInputSignalH
      end
    
      -- Emit the COMPL trip event signal (hardware: CTRIPL).
      OutputSignal:append('{compLTripEvent = {bid = %d}}' % {self:getId()})
    else
      OutputSignal:append('{}')
    end

    -- Process threshold for a comparator port; emit dynamic DAC update code if
    -- the port is connected to a non-constant signal.
    -- Returns the constant threshold value (number), or nil if dynamic.
    local function processThresholdPort(refPortIdx, opt_rampGenerator, side)
      if not U.portIsConnected(refPortIdx) then
        return 0
      end
      local constantThreshold = U.portValueGetConstant(refPortIdx) -- nil if not constant
      if constantThreshold then
        return constantThreshold
      end
      -- Dynamic threshold: update the DAC value each step from the input signal.
      local thresholdInputStr = Block.InputSignal[refPortIdx][1]
      if opt_rampGenerator then
        OutputCode:append(
          'PLXHAL_CMPSS_setComp%(side)sRampRef(%(instance)d, %(threshold)s);' %
          {
            side = side,
            instance = self.instance,
            threshold = thresholdInputStr,
          })
      else
        OutputCode:append(
          'PLXHAL_CMPSS_setComp%(side)sDacVal(%(instance)d, %(threshold)s);' %
          {
            side = side,
            instance = self.instance,
            threshold = thresholdInputStr,
          })
      end
    end

    self.compH.opt_thresholdVal = processThresholdPort(inIdx.refH, self.compH.opt_rampConfig, 'H')

    if self.opt_compL then
      self.opt_compL.opt_thresholdVal = processThresholdPort(inIdx.refL, self.opt_compL.opt_rampConfig, 'L')
    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
    local function buildCompParams(comp, syncPortIdx, logLabel, portLabel)
      local params = {
        opt_pinMux = comp.hwModule.mux,
        threshold = comp.opt_thresholdVal or 3.3,  -- 3.3V initial value for dynamic threshold; overridden at runtime
        invertCompare = comp.cbx_outPolarity.equals('inverted'),
      }
      if comp.opt_rampConfig then
        local rampGenerator = comp.opt_rampConfig
        local peripheralSyncString = Block.InputSignal[syncPortIdx][1]
        -- example input: {syncper = {bid = 9}}
        peripheralSyncString = peripheralSyncString:gsub('%s+', '')  -- remove whitespace
        if peripheralSyncString:sub(1, #'{syncper') ~= '{syncper' then
          U.error("'%s' port must be connected to the PER port of a PWM block." %
            {portLabel})
        end
        local peripheralSync = eval(peripheralSyncString)['syncper']
        if peripheralSync['bid'] == nil then
          U.error('Malformed peripheralSync expression: %s' %
            {peripheralSyncString})
        end
        local epwmUnit = globals.instances[peripheralSync['bid']]:getParameter('first_unit')
        local rampDivisor = rampGenerator.rampClkDivider
        local actualRamp = rampGenerator.rampSlopeCounts
           * (self.rampDacLsbVoltage * globals.target.getCleanSysClkHz() / rampDivisor)
        self:logLine(
          '%(label)s ramp DAC step set to %(step)d, resulting in ramp of %(actual)f V/s, desired was %(desired)f V/s, rampClk = SysClk/%(div)d.' %
          {
            label = logLabel,
            step = rampGenerator.rampSlopeCounts,
            actual = actualRamp,
            desired = rampGenerator.rampSlope,
            div = rampDivisor
          })
        params.opt_ramp = {
          peripheralSyncEpwmUnit = epwmUnit,
          rampStepVal = rampGenerator.rampSlopeCounts,
          desiredRamp = rampGenerator.rampSlope,
          actualRamp = actualRamp,
          b_rampIsIncrementing = rampGenerator.b_rampIsIncrementing,
          rampClkDivider = rampGenerator.rampClkDivider,
        }
      end
      return params
    end

    local cmpssObj = self:makeBlock('cmpss')
    cmpssObj:createImplicitCmpss(
      self.compH.hwModule.unit,
      {
        analogPin = self.compPinStr,
        tripSignalH = self.tripInputSignalH,
        opt_tripSignalL = self.opt_tripInputSignalL,
        opt_compHigh = buildCompParams(self.compH, inIdx.TH, 'COMPH', 'TH'),
        opt_compLow = self.opt_compL and buildCompParams(self.opt_compL, inIdx.TL, 'COMPL', 'TL'),
      }, Require, nil)
    return {Require = Require, UpdateCode = UpdateCode}
  end

  function Comp:finalizeThis(c)
    local compHRampStepVal = self.compH.opt_rampConfig
       and self.compH.opt_rampConfig.rampSlopeCounts
       or 0
    local rampClkDividerH = self.compH.opt_rampConfig
       and self.compH.opt_rampConfig.rampClkDivider
       or 1

    local compLRampStepVal = self.opt_compL 
       and self.opt_compL.opt_rampConfig
       and self.opt_compL.opt_rampConfig.rampSlopeCounts
       or 0
    local rampClkDividerL = self.opt_compL 
       and self.opt_compL.opt_rampConfig
       and self.opt_compL.opt_rampConfig.rampClkDivider
       or 1

    c.PreInitCode:append([[
      PLX_CMPSS_configure(CmpssHandles[%(instance)d], CMPSS%(cmp)d_BASE,
          %(compHRampStepVal)d, %(compLRampStepVal)d,
          %(rampClkDividerH)d, %(rampClkDividerL)d);
      ]] % {
      instance = self.instance,
      cmp = self.compH.hwModule.unit,
      compHRampStepVal = compHRampStepVal,
      compLRampStepVal = compLRampStepVal,
      rampClkDividerH = rampClkDividerH,
      rampClkDividerL = rampClkDividerL,
    })

    -- Emit PostInitCode for a constant comparator threshold value.
    -- No-op if constantThreshold is nil (dynamic thresholds are handled in OutputCode).
    local function emitConstantThreshold(constantThreshold, opt_rampGenerator, side)
      if not constantThreshold then
        return
      end
      if opt_rampGenerator then
        c.PostInitCode:append([[
          // ramp with constant peak value
          PLX_CMPSS_setComp%(side)sRampRef(CmpssHandles[%(instance)d], %(value)f);
          ]] % {
          side = side,
          instance = self.instance,
          value = constantThreshold,
        })
      else
        c.PostInitCode:append([[
          // constant peak value
          PLX_CMPSS_setComp%(side)sDacVal(CmpssHandles[%(instance)d], %(value)f);
          ]] % {
          side = side,
          instance = self.instance,
          value = constantThreshold,
        })
      end
    end

    emitConstantThreshold(self.compH.opt_thresholdVal, self.compH.opt_rampConfig, 'H')

    if self.opt_compL then
      emitConstantThreshold(self.opt_compL.opt_thresholdVal, self.opt_compL.opt_rampConfig, 'L')
    end
  end

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

    c.Include:append('plx_cmpss.h')

    c.PreInitCode:append([[
      for (int i = 0; i < %(numInstances)d; i++) {
        CmpssHandles[i] = PLX_CMPSS_init(&CmpssObj[i], sizeof(CmpssObj[i]));
      }
      ]] % {
      numInstances = static[self.cpu].numInstances
    })

    c.Declarations:append([[
      PLX_CMPSS_Handle_t CmpssHandles[%(numInstances)d];
      PLX_CMPSS_Obj_t CmpssObj[%(numInstances)d];

      void PLXHAL_CMPSS_setCompHDacVal(uint16_t aHandle, float aVal){
        PLX_CMPSS_setCompHDacVal(CmpssHandles[aHandle], aVal);
      }

      void PLXHAL_CMPSS_setCompHRampRef(uint16_t aHandle, float aPeak){
        PLX_CMPSS_setCompHRampRef(CmpssHandles[aHandle], aPeak);
      }

      void PLXHAL_CMPSS_setCompLDacVal(uint16_t aHandle, float aVal){
        PLX_CMPSS_setCompLDacVal(CmpssHandles[aHandle], aVal);
      }

      void PLXHAL_CMPSS_setCompLRampRef(uint16_t aHandle, float aPeak){
        PLX_CMPSS_setCompLRampRef(CmpssHandles[aHandle], aPeak);
      }
    ]] % {
      numInstances = static[self.cpu].numInstances
    })

    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
