--[[
  Copyright (c) 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, incluEpwmg 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 EpwmXcmp = require('blocks.epwm_common').getBlock(globals, cpu)
  if not static[cpu] then
    static[cpu] = {
      numInstances = 0,
      instances = {},
      units = {},
      finalizeXcmpCodeGenerated = false,
      finalized = false,
    }
  end
  EpwmXcmp.instance = static[cpu].numInstances
  static[cpu].numInstances = static[cpu].numInstances + 1

  function EpwmXcmp:checkMaskParameters()
    if not self:targetMatches({'28P65x', '29H85x'}) then
      U.error('This block is not supported by the %(target)s target.'
        % {target = globals.target.getFamilyPrefix()})
    end

    self.mask = {}

    -- main tab
    self.mask.pwmUnit = U.enforceMask(U.isPositiveIntScalar, 'PwmUnit')
    self.mask.cbx_carrierType = U.enforceMask_newComboBox('CarrierType',
                                                          {'sawtooth', 'triangle'})
    self.mask.carrierFrequency = U.enforceMask(U.isPositiveScalar, 'CarrierFreq')
    self.mask.cbx_frequencyTolerance = U.enforceMask_newComboBox(
      'CarrierFreqTol', {'exact', 'round'})

    -- output tab
    self.mask.fixedDeadTime = U.enforceMask(U.isNonNegativeScalar,
                                            'FixedDeadTime')
    self.mask.polarityIsActiveHigh = U.enforceMask_newComboBox('Polarity',
                                                               {'positive',
                                                                 'negative'})
      .equals('positive')

    -- sync tab
    self.mask.syncOutEnabled = U.enforceMask_newComboBox('SyncOut',
                                                         {'disabled', 'enabled'})
      .equals('enabled')

    -- protections tab
    self.mask.allowProtection = U.enforceMask_newComboBox('AllowProtection',
                                                          {'allow', ''})
      .equals('allow')

    -- events tab
    local maxEventPeriod = globals.target.getTargetParameters().epwms.max_event_period
    -- Note for TI, TriggerDividers get a valid number in the mask, required
    -- for offline model. Additional check here is on the range.

    self.mask.cbx_adcTriggerEvent = U.enforceMask_newComboBox('AdcTriggerMode',
                                                              {'disabled',
                                                                'underflow'})
    if self.mask.cbx_adcTriggerEvent.equals('underflow') then
      self.mask.opt_adcTriggerDivider = U.enforceMask(
        U.isIntScalarInClosedInterval, 'AdcTriggerDivider', 1, maxEventPeriod)
    end

    self.mask.cbx_taskTriggerEvent = U.enforceMask_newComboBox('TaskTriggerMode',
                                                               {'disabled',
                                                                 'underflow'})
    if self.mask.cbx_taskTriggerEvent.equals('underflow') then
      self.mask.opt_taskTriggerDivider = U.enforceMask(
        U.isIntScalarInClosedInterval, 'TaskTriggerDivider', 1, maxEventPeriod)
    end
  end

  function EpwmXcmp:p_getDirectFeedthroughCode()
    local Require = ResourceList:new()
    local InitCode = U.CodeLines:new()
    local OutputSignal = {}
    local OutputCode = U.CodeLines:new()

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

    self.unit = self.mask.pwmUnit
    self.first_unit = self.unit                      -- don't rename - needed by other modules and epwm_common
    self.last_unit = self.unit                       -- don't rename - needed by epwm_common
    self.num_units = 1                               -- don't rename - needed by epwm_common
    self.first_channel = static[self.cpu].numInstances -- only one channel per instance
    table.insert(static[self.cpu].units, self.unit)  -- needed for XLINK

    if self:targetMatches({'29H85x'}) then
      -- for the F29 devices, the XLINK configuration must be done by the primary CPU
      globals.syscfg:addPeripheralEntry('f29x_epwm_xlink', {
        unit = self.unit,
      })
      -- and all XCMP units must be associated with the same CPU.
      for registeredCpu, _ in pairs(static) do
        if self.cpu ~= registeredCpu then
          U.error('All XCMP blocks must be associated with the same CPU.')
        end
      end
    end

    -- carrier
    self.fsw = self.mask.carrierFrequency
    local timing = globals.target.getPwmFrequencySettings(self.fsw,
                                                          self.mask
                                                          .cbx_carrierType)
    if self.mask.cbx_frequencyTolerance.equals('exact') then
      U.enforceExactFrequencyTolerance({
        freqDesired = self.fsw,
        freqWidgetLink = '@param:CarrierFreq:',
        freqTolWidgetLink = '@param:CarrierFreqTol:',
        freqAchievable = timing.freq,
        descriptor = 'PWM frequency',
      })
    end
    self.fsw_actual = timing.freq  -- don't rename - needed by epwm_common
    self.prd = timing.period

    -- deadtime
    self.risingEdgeDelayCounts = math.floor(self.mask.fixedDeadTime *
      globals.target.getDeadTimeClock() + 0.5)

    -- task trigger event
    self.int_loc = ''  -- don't rename - needed by epwm_common
    if self.mask.cbx_taskTriggerEvent.equals('underflow') then
      self.int_loc = 'z'
      self.int_prd = self.mask.opt_taskTriggerDivider  -- don't rename - needed by epwm_common
    end

    -- ADC trigger event
    self.soc_loc = ''  -- don't rename - needed by epwm_common
    if self.mask.cbx_adcTriggerEvent.equals('underflow') then
      self.soc_loc = 'z'
      self.soc_prd = self.mask.opt_adcTriggerDivider  -- don't rename - needed by epwm_common
    end

    self.epwmObj = self:makeBlock('epwm', self.cpu)
    self.epwmObj:createImplicitEpwm(
      self.unit,
      {
        fsw = self.fsw,
        cbx_carrierType = self.mask.cbx_carrierType,
        polarityIsActiveHigh = true,
        outMode = {'A', 'B'},
        disableControlByProtectionBlock = not self.mask.allowProtection,
        showEnablePort = false,
      }, Require
    )
    self.channels = {}
    self.channels[static[self.cpu].numInstances] = self.epwmObj

    OutputCode:append([[
      {
        float duty[8];
        duty[0] = %(duty1)s;
        duty[1] = %(duty2)s;
        duty[2] = %(duty3)s;
        duty[3] = %(duty4)s;
        duty[4] = %(duty5)s;
        duty[5] = %(duty6)s;
        duty[6] = %(duty7)s;
        duty[7] = %(duty8)s;

        uint16_t aq = ((uint16_t)(%(aqB)s) << 8) + (uint16_t)(%(aqA)s);
        PLXHAL_PWM_setXcmp(%(instance)d, duty, aq);
      }
    ]] % {
      instance = self.epwmObj:getObjIndex(),
      -- cmp(A)
      duty1 = Block.InputSignal[1][1],
      duty2 = Block.InputSignal[1][2],
      duty3 = Block.InputSignal[1][3],
      duty4 = Block.InputSignal[1][4],
      aqA = Block.InputSignal[2][1],
      -- cmp(B)
      duty5 = Block.InputSignal[3][1],
      duty6 = Block.InputSignal[3][2],
      duty7 = Block.InputSignal[3][3],
      duty8 = Block.InputSignal[3][4],
      aqB = Block.InputSignal[4][1],
    })

    -- trigger events
    if (self.int_loc == '') then
      OutputSignal[1] = {
        [1] = '{}',
      }
    else
      self.modTrigTs = 1 / (self.fsw_actual * #self.int_loc)
      if self.int_prd ~= nil then
        self.modTrigTs = self.modTrigTs * self.int_prd
      end
      OutputSignal[1] = {
        [1] = '{modtrig = {bid = %d}}' % {self:getId()},
      }
    end

    if (self.soc_loc == '') then
      OutputSignal[2] = {
        [1] = '{}',
      }
    else
      self.adcTrigTs = 1 / (self.fsw_actual * #self.soc_loc)
      if self.soc_prd ~= nil then
        self.adcTrigTs = self.adcTrigTs * self.soc_prd
      end
      OutputSignal[2] = {
        [1] = '{adctrig = {bid = %d}}' % {self:getId()},
      }
    end

    -- synchronization output
    if self.mask.syncOutEnabled then
      -- note that XCMP cannot be synchronized, but we want to output a sync to other PWMs
      self.is_synchronized = true       -- don't rename - needed by epwm_common
      self.is_self_synchronized = true  -- don't rename - needed by epwm_common
    end
    OutputSignal[3] = {
      [1] = '{synco = {bid = %d}}' % {self:getId()},
    }

    return {
      InitCode = InitCode,
      OutputSignal = OutputSignal,
      OutputCode = OutputCode,
      Require = Require,
      UserData = {bid = self:getId()}
    }
  end

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

    local enableCode = self.epwmObj:getEnableCode()
    if enableCode ~= nil then
      UpdateCode:append(enableCode)
    end
    if not static[self.cpu].finalizeXcmpCodeGenerated then
      UpdateCode:append([[
        PLXHAL_PWM_finalizeXcmp();
      ]])
      static[self.cpu].finalizeXcmpCodeGenerated = true
    end

    return {Require = Require, UpdateCode = UpdateCode}
  end

  function EpwmXcmp:finalizeThis(c)
    c.PostInitCode:append([[
      EPWM_enableXCMPMode(%(base)s);	
      EPWM_enableSplitXCMP(%(base)s);	
      EPWM_allocAXCMP(%(base)s, EPWM_XCMP_4_CMPA);	
      EPWM_allocBXCMP(%(base)s, EPWM_XCMP_8_CMPB);

      EPWM_setXCMPLoadMode(%(base)s, EPWM_XCMP_XLOADCTL_LOADMODE_LOADONCE);
      EPWM_setXCMPShadowLevel(%(base)s, EPWM_XCMP_XLOADCTL_SHDWLEVEL_1);
      EPWM_setXCMPShadowBufPtrLoadOnce(%(base)s, EPWM_XCMP_XLOADCTL_SHDWBUFPTR_ONE);

      // fixed period
      EPWM_setXCMPRegValue(%(base)s, EPWM_XTBPRD_ACTIVE, %(xtbprd)d);	
      EPWM_setXCMPRegValue(%(base)s, EPWM_XTBPRD_SHADOW1, %(xtbprd)d);	

      EPWM_setRisingEdgeDelayCountShadowLoadMode(%(base)s, EPWM_RED_LOAD_ON_CNTR_ZERO);	
      EPWM_setFallingEdgeDelayCountShadowLoadMode(%(base)s, EPWM_FED_LOAD_ON_CNTR_ZERO);	

      // fixed deadtime
      EPWM_setRisingEdgeDeadBandDelayInput(%(base)s, EPWM_DB_INPUT_EPWMA);
      EPWM_setDeadBandDelayPolarity(%(base)s, EPWM_DB_RED, EPWM_DB_POLARITY_ACTIVE_%(polarityA)s);
      EPWM_setDeadBandDelayMode(%(base)s, EPWM_DB_RED, true);
      EPWM_setRisingEdgeDelayCount(%(base)s, %(deadband)d);

      EPWM_setFallingEdgeDeadBandDelayInput(%(base)s, EPWM_DB_INPUT_EPWMB);
      EPWM_setDeadBandDelayPolarity(%(base)s, EPWM_DB_FED, EPWM_DB_POLARITY_ACTIVE_%(polarityB)s);
      EPWM_setDeadBandDelayMode(%(base)s, EPWM_DB_FED, true);
      EPWM_setFallingEdgeDelayCount(%(base)s, %(deadband)d);
    ]] % {
      base = 'EPWM%d_BASE' % {self.unit},
      xtbprd = self.prd,
      deadband = self.risingEdgeDelayCounts,
      polarityA = self.mask.polarityIsActiveHigh and 'HIGH' or 'LOW',
      polarityB = self.mask.polarityIsActiveHigh and 'LOW' or 'HIGH',
    })
  end

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

    -- link XCMP units for simultaneous shadow register updates
    local xloadLinkBase
    if self:targetMatches({'29H85x'}) then
      xloadLinkBase = 'EPWM%dXLINK_BASE' % {static[self.cpu].units[1]}
    elseif self:targetMatches({'28P65x'}) then
      for _, unit in ipairs(static[self.cpu].units) do
        c.PostInitCode:append([[
          // Driverib BUG: EPWM_setupEPWMLinks(EPWM%(unit)d_BASE,  EPWM_LINK_WITH_EPWM_%(link0)d, EPWM_LINK_XLOAD);
          HWREG(EPWM%(unit)d_BASE + EPWM_O_XCMP + EPWM_O_XLINKXLOAD) = EPWM_LINK_WITH_EPWM_%(link0)d;
        ]] % {
          link0 = static[self.cpu].units[1],
          unit = unit,
        })
      end
      xloadLinkBase = 'EPWM%d_BASE' % {static[self.cpu].units[1]}
    else
      U.throwUnhandledTargetError()
    end

    local EpwmCommonStatic = self:getStaticMethods()

    c.Include:append('plx_pwm.h')
    c.Include:append('math.h')

    c.Declarations:append('extern PLX_PWM_Handle_t EpwmHandles[];')

    -- generate HAL code shared with epwm_basic.lua and epwm_var.lua
    EpwmCommonStatic.generatePlxHalCode(c)

    -- add XCMP specific PLXHAL functions
    c.Declarations:append([[
      void PLXHAL_PWM_setXcmp(uint16_t aHandle, float *aCmp, uint16_t aSequenceAq) {
        PLX_PWM_setXcmpAB(EpwmHandles[aHandle], aCmp, aSequenceAq);
      }

      void PLXHAL_PWM_finalizeXcmp() {
        EPWM_enableXLoad(%(linkBase)s);
      }
      ]] % {
        linkBase = xloadLinkBase,
    })

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

    static[self.cpu].finalized = true
  end

  return EpwmXcmp
end

return Module
