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

  -- if no default is provided 'nil' will be used.
  function CleanComboInput(blockVal, comboOptions, default)
    if blockVal == nil then
      return default
    end
    if blockVal > #comboOptions then
      return default
    end
    return comboOptions[blockVal]
  end


  function EpwmBasic:checkMaskParameters()
    self.mask = {}

    -- Main Tab
    self.mask.Pwm = Block.Mask.PwmUnit
    self.cbx_carrierType = U.enforceMask_newComboBox('CarrierType', {'sawtooth', 'triangle'})
    self.mask.carrierFrequency = Block.Mask.CarrierFreq
    self.mask.cbx_frequencyTolerance = U.enforceMask_newComboBox('CarrierFreqTol', {'exact', 'round'})

    if Block.Mask.Phase ~= Block.Mask.Phase then
      -- empty field
      self.mask.Phase = {}
      for i = 1, #Block.InputSignal[1] do
        table.insert(self.mask.Phase, 0)
      end
    elseif #Block.Mask.Phase ~= #Block.InputSignal[1] then
      U.error('Invalid dimension of phase array.')
    else
      self.mask.Phase = Block.Mask.Phase
    end

    -- Output Tab
    self.mask.OutMode = U.enforceMask_newComboBox(
      'OutMode', 
      {
        [1] = {'A', '!A'},  -- Complementary outputs
        [2] = {'A', ''},    -- Single output (channel A)
        [4] = {'A', 'B'},   -- Dual output (channel A & B)
        [3] = {'', ''},     -- Outputs disabled
      }).asRawValue()

    self.mask.cbx_outMode = U.enforceMask_newComboBox(
      'OutMode',
      {
        [1] = 'Complementary',  -- Complementary outputs
        [2] = 'Single',         -- Single output (channel A)
        [4] = 'Dual',           -- Dual output (channel A & B)
        [3] = 'Disabled',       -- Outputs disabled
      })
    self.pwmOutputsEnabled = not self.mask.cbx_outMode.equals('Disabled')

    if Block.Mask.VariableDeadTime == Block.Mask.VariableDeadTime then -- checking for NaN, this parameter is hidden for certain block configurations
      self.mask.VariableDeadTimeEnabled = U.enforceMask_newComboBox('VariableDeadTime', {'disabled', 'enabled'}).equals('enabled')
    else
      self.mask.VariableDeadTimeEnabled = false
    end
    self.mask.Delay = Block.Mask.FixedDeadTime        -- Blanking Time
    self.mask.MinDeadTime = Block.Mask.MinDeadTime  -- minimal blanking time when variable
    local sequenceOptions = {'positive', 'negative', 'variable', 'aq', 'variable-aq'} 
    if self.pwmOutputsEnabled then
      self.polarityIsActiveHigh = U.enforceMask_newComboBox('Polarity', {'positive', 'negative'}).equals('positive')
      self.mask.cbx_sequence = U.enforceMask_newComboBox('Sequence', sequenceOptions)
      self.mask.SequenceAq = Block.Mask.SequenceAq
    else
      -- Old comboBox used these defaults
      self.polarityIsActiveHigh = true
      self.mask.cbx_sequence = U.newComboBox('positive', sequenceOptions)
    end
    
    self.showEnablePort = U.comboEnabled(Block.Mask.ShowEnable)

    -- High Resolution tab
    self.mask.highResDutyModeActive = U.comboEnabled(Block.Mask.HighResDutyMode)
    self.mask.highResPrdModeActive = U.comboEnabled(Block.Mask.HighResPrdMode)
    self.mask.highResDeadTimeModeActive = U.comboEnabled(Block.Mask.HighResDeadTimeMode)

    if self.cbx_carrierType.equals('triangle') and self.mask.highResPrdModeActive then
      U.error('%(highResPrdLink)s cannot be enabled with %(carrierTypeLink)s set to symmetrical.' %
        {
          highResPrdLink = U.paramLink('HighResPrdMode', false),
          carrierTypeLink = U.paramLink('CarrierType', true),
        })
    end
    
    -- Shadowing tab
    self.mask.CmpShadowMode = CleanComboInput(Block.Mask.CmpShadowMode, {'underflow', 'overflow', 'both'})
    -- If high resolution period enabled, must provide defaults (compare load shadowing will be disabled for user)
    if (self.mask.highResPrdMoveActive) then
      if (self.cbx_carrierType.equals('triangle')) then
        self.mask.CmpShadowMode = 'both'
      else
        self.mask.CmpShadowMode = 'overflow'
      end
    end
    self.mask.RedShadowMode = CleanComboInput(Block.Mask.RedShadowMode, {'underflow', 'overflow', 'both'})
    self.mask.FedShadowMode = CleanComboInput(Block.Mask.FedShadowMode, {'underflow', 'overflow', 'both'})

    -- Sync Tab
    self.mask.SyncSrc = CleanComboInput(Block.Mask.SyncSrc, {'self', 'external'})
    self.mask.SyncOut = CleanComboInput(Block.Mask.SyncOut, {'disabled', 'enabled'})
    self.mask.PeripheralSyncMode = CleanComboInput(Block.Mask.PeripheralSyncMode, {'disabled', 'underflow', 'overflow'})
    if self.mask.SyncSrc == 'self' and self.mask.Phase[1] ~= 0 then
      U.error('Phase of first channel has to be 0 when self-synchronized')
    end

    -- Protection Tab
    self.mask.AllowProtection = CleanComboInput(Block.Mask.AllowProtection, {true, false}, true)

    -- Events Tab
    local max_event_period = globals.target.getTargetParameters()['epwms']['max_event_period']
    -- Note for TI, TriggerDivders get a valid number in the mask, required 
    -- for offline model. Additonal check here is on the range.

    self.mask.AdcTriggerEvent = CleanComboInput(Block.Mask.AdcTriggerMode, {'disabled', 'underflow', 'overflow', 'both'})
    self.mask.AdcTriggerDivider = U.enforceMask(
      U.isIntScalarInClosedInterval, 'AdcTriggerDivider', 1, max_event_period)
    

    self.mask.TaskTriggerEvent = CleanComboInput(Block.Mask.TaskTriggerMode, {'disabled', 'underflow', 'overflow', 'both'})
    self.mask.TaskTriggerDivider = U.enforceMask(
      U.isIntScalarInClosedInterval, 'TaskTriggerDivider', 1, max_event_period)
    

    -- deprecated
    local reactionToTzCombo = {'powerstage', 'cycle-cycle', 'one-shot'}
    self.mask.TzMode = {}
    for z = 1, 3 do
      self.mask.TzMode[z] = reactionToTzCombo[Block.Mask['tz%imode' % {z}]]
    end

    -- checks
    if self.cbx_carrierType.equals('sawtooth') then
      if self.mask.AdcTriggerEvent == 'both' then
        U.error('Invalid ADC trigger event for sawtooth carrier.')
      end
      if self.mask.TaskTriggerEvent == 'both' then
        U.error('Invalid task trigger event for sawtooth carrier.')
      end
    end

    if globals.target.getTargetParameters().epwms.type == 0 then
      if self.mask.AdcTriggerEvent == 'both' then
        U.error('Dual ADC trigger events not supported by this chip.')
      end
      if self.mask.TaskTriggerEvent == 'both' then
        U.error('Dual task trigger events not supported by this chip.')
      end
    end

    local max_event_period = globals.target.getTargetParameters()['epwms']['max_event_period']

    if globals.target.getTargetParameters().epwms.type < 4 then
      if self.mask.ShowEnable then
        U.error("Enable port 'en' not supported by this target (%s)." % {Target.Name})
      end
    end
  end

  function EpwmBasic: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 num_pwm = #Block.InputSignal[1]
    if num_pwm == 0 then
      U.error('At least one PWM must be configured.')
    end

    self.first_unit = self.mask.Pwm[1]
    self.last_unit = self.mask.Pwm[num_pwm]
    self.num_units = num_pwm
    self.first_channel = static[self.cpu].numChannels
    local task_name = Block.Task['Name']

    -- carrier
    self.fsw = self.mask.carrierFrequency

    local timing = globals.target.getPwmFrequencySettings(self.fsw,
                                                          self.cbx_carrierType)

    if self.mask.highResPrdModeActive then
      self.fsw_actual = timing.freqHR
    else
      self.fsw_actual = timing.freq
    end

    -- accuracy of frequency settings
    if self.mask.cbx_frequencyTolerance.equals('exact') then
      U.enforceExactFrequencyTolerance({
        freqDesired = self.fsw, 
        freqAchievable = self.fsw_actual,
        descriptor = 'PWM frequency',
      })
    end

    -- deadtime
    self.dead_time_is_variable = (self.mask.OutMode[2] == '!A') and self.mask.VariableDeadTimeEnabled
    if not self.dead_time_is_variable then
      self.dead_time = self.mask.Delay
    else
      self.dead_time = self.mask.MinDeadTime  -- default and minimal value
      if #Block.InputSignal[4] ~= 2 and #Block.InputSignal[4] ~= (2 * num_pwm) then
        U.error([[
          The 'd[r,f]' input must be a vector with 2 elements or one element for each rising edge and each falling edge (%d elements). Inputs are in seconds.
        ]] % {2 * num_pwm})
      end
      for i = 1, #Block.InputSignal[4] do
        if Block.InputType[4][i] ~= 'float' then
          U.error("The 'd[r,f]' input must be of float type.")
        end
      end
    end

    -- sequence
    self.const_sequence = {}
    self.sequence_defined_in_aq = false
    if self.mask.cbx_sequence.equals('positive') then
      for i = 1, num_pwm do
        self.const_sequence[i] = 1 -- fixed positive sequence
      end
    elseif self.mask.cbx_sequence.equals('negative') then
      for i = 1, num_pwm do
        self.const_sequence[i] = 0 -- fixed negative sequence
      end
    elseif self.mask.cbx_sequence.equals('aq') then
      self.sequence_defined_in_aq = true
      for i = 1, num_pwm do
        self.const_sequence[i] = self.mask.SequenceAq[i] -- fixed AQ-based sequence
      end
    else  
      -- variable sequence options    
      local sequencePortIndex
      local sequencePortLabel
      if self.mask.cbx_sequence.equals('variable') then
        sequencePortIndex = 2
        sequencePortLabel = 'seq'
      elseif self.mask.cbx_sequence.equals('variable-aq') then
        sequencePortIndex = 8
        sequencePortLabel = 'AQ'
        self.sequence_defined_in_aq = true
      else
        assert('Unexpected value for cbx_sequence.')
      end 

      -- if self.maskcbx_sequence:equals('aq') then
      --   -- add check that AQ register value is uint32 and or less than 2^24 
      --       -- (max value that can be represented with a single presicion floating point value)
      --       -- local inputType = Block.InputType[1]
      --       -- for idx = 1, self.width do
      --       --   if (inputType[idx] ~= 'uint8_t') then
      --       --     U.error('Input type of signal at index %i must be uint8 (is %s).' %
      --       --       {idx - 1, inputType[idx]})
      --       --   end
      --       -- end
      -- end

      if #Block.InputSignal[sequencePortIndex] ~= 1 and #Block.InputSignal[sequencePortIndex] ~= num_pwm then
        U.error("The '%s' input must be scalar or a vector with %i elements." % {sequencePortLabel, num_pwm})
      end

      for i = 1, num_pwm do
        self.const_sequence[i] = self:getConstantSignal(sequencePortIndex, math.min(i, #Block.InputSignal[sequencePortIndex]), 
                                                        "'%s'" % {sequencePortLabel})
        if  (self.const_sequence[i] == nil)
        and (globals.target.getTargetParameters().epwms.type < 4) then
          U.error("Variable sequence not supported by this target (%s)." % {Target.Name})
        end
      end
    end

    -- outmode
    self.outmode = self.mask.OutMode

    -- show enable
    if self.showEnablePort then
      -- Enable port may not be used on non-driverlib targets
      if self:targetUsesPinsets() then
        U.error('The PWM enable port is not supported on this target.')
      end
      if #Block.InputSignal[3] ~= 1 and #Block.InputSignal[3] ~= num_pwm then
        U.error("The 'en' input must be scalar or a vector with %i elements." % {num_pwm})
      end
    end

    -- shadowing
    function getShadowModeString(events)
      if events == 'underflow' then
        return 'z'
      elseif events == 'overflow' then
        return 'p'
      else
        return 'zp'
      end
    end

    self.cmp_shadow_load = getShadowModeString(self.mask.CmpShadowMode)

    -- task trigger event
    self.int_loc = ''
    if self.mask.TaskTriggerEvent == 'underflow' then
      self.int_loc = 'z'
    elseif self.mask.TaskTriggerEvent == 'overflow' then
      self.int_loc = 'p'
    elseif self.mask.TaskTriggerEvent == 'both' then
      self.int_loc = 'zp'
    end
    self.int_prd = self.mask.TaskTriggerDivider

    -- ADC trigger event
    self.soc_loc = ''
    if self.mask.AdcTriggerEvent == 'underflow' then
      self.soc_loc = 'z'
    elseif self.mask.AdcTriggerEvent == 'overflow' then
      self.soc_loc = 'p'
    elseif self.mask.AdcTriggerEvent == 'both' then
      self.soc_loc = 'zp'
    end
    self.soc_prd = self.mask.AdcTriggerDivider

    -- trip zone (trip generated by digital input)
    self.trip_zone_settings = {}
    for z = 1, 3 do
      if self.mask.TzMode[z] == 'cycle-cycle' then
        self.trip_zone_settings[z] = 'cbc'
      elseif self.mask.TzMode[z] == 'one-shot' then
        self.trip_zone_settings[z] = 'osht'
      end
    end

    -- determine type of synchronization
    self.is_synchronized = false
    if self.mask.SyncSrc == 'external'
    or self.mask.SyncOut == 'enabled' then
      self.is_synchronized = true
    else
      for i = 1, #self.mask.Phase do
        if self.mask.Phase[i] ~= 0 then
          self.is_synchronized = true
          break
        end
      end
    end

    local persyncSrc
    if self.mask.PeripheralSyncMode == 'underflow' then
      persyncSrc = 'z'
    elseif self.mask.PeripheralSyncMode == 'overflow' then
      persyncSrc = 'p'
    end

    -- now create all implicit ePWM instances
    self.channels = {}
    for pwmChannel = 1, num_pwm do
      local pwm = self.mask.Pwm[pwmChannel]
      if pwm == 1 then
        self:registerSelfAsEpwm1()  -- implemented in epwm_common, and used for sync chain verifications
      end

      local epwm = self:makeBlock('epwm', self.cpu)

      local phase0
      if self.is_synchronized then
        phase0 = self.mask.Phase[pwmChannel]
      end

      local sequence0
      if self.const_sequence[pwmChannel] then
        sequence0 = {
          defined_in_aq = self.sequence_defined_in_aq,
          value = self.const_sequence[pwmChannel],
        } 
      end

      -- deadtime settings
      local const_rising_delay, const_falling_delay, deadtime_input_offset, dead_time_shadow_mode
      if self.dead_time_is_variable then
        deadtime_input_offset = 2 * (pwmChannel - 1) + 1
        if #Block.InputSignal[4] == 2 then
          deadtime_input_offset = 1;
        end
        -- drop 'f' and try to convert (will be nil if conversion fails, which means input is not constant)
        const_rising_delay = tonumber(string.sub(Block.InputSignal[4][deadtime_input_offset], 1, -2))
        const_falling_delay = tonumber(string.sub(Block.InputSignal[4][deadtime_input_offset + 1], 1, -2))
        -- if constant, enforce minimal deadtime at this point
        if const_rising_delay ~= nil and const_rising_delay < self.dead_time then
          const_rising_delay = self.dead_time
        end
        if const_falling_delay ~= nil and const_falling_delay < self.dead_time then
          const_falling_delay = self.dead_time
        end

        -- enable deadtime shadowing on chips that support it
        if globals.target.getTargetParameters().epwms.type >= 4 then
          if const_falling_delay == nil or const_rising_delay == nil then
            dead_time_shadow_mode = 'zp'
            self.red_shadow_load = getShadowModeString(self.mask.RedShadowMode)
            self.fed_shadow_load = getShadowModeString(self.mask.FedShadowMode)
          end
        end
      end

      epwm:createImplicit(
        pwm,
        {
          fsw = self.fsw,
          cbx_carrierType = self.cbx_carrierType,
          opt_highResDutyModeActive = self.mask.highResDutyModeActive,
          opt_highResPrdModeActive = self.mask.highResPrdModeActive,
          polarityIsActiveHigh = self.polarityIsActiveHigh,
          outMode = self.outmode,
          opt_highResDeadTimeModeActive = self.mask.highResDeadTimeModeActive,
          opt_sequence0 = sequence0,
          opt_dead_time = U.isReal(self.dead_time) and self.dead_time or 0,
          trip_zone_settings = self.trip_zone_settings,
          disableControlByProtectionBlock = not self.mask.AllowProtection,
          showEnablePort = self.showEnablePort,
          syncosel = syncosel,
          opt_dead_time_shadow_mode = dead_time_shadow_mode,
          opt_rising_delay = const_rising_delay,
          opt_falling_delay = const_falling_delay,
          opt_phase0 = phase0,
          opt_red_shadow_load = self.red_shadow_load,
          opt_fed_shadow_load = self.fed_shadow_load,
          opt_cmp_shadow_load = self.cmp_shadow_load,
          opt_peripheralSyncSrc = persyncSrc,
        }, Require)

      local epwmInstance = epwm:getObjIndex()

      self:logLine('EPWM implicitly created for channel %i, pwm %i.' %
        {static[self.cpu].numChannels, pwm})
      self.channels[static[self.cpu].numChannels] = epwm

      if self.outmode[1] ~= '' or self.outmode[2] ~= '' then
        if self.dead_time_is_variable then
          if const_rising_delay == nil then
            OutputCode:append(
              'PLXHAL_PWM_setRisingDelay(%i, fmaxf(%s, %s));' 
              % {
                epwmInstance,
                Block.InputSignal[4][deadtime_input_offset],
                self.mask.MinDeadTime
              })
          end
          if const_falling_delay == nil then
            OutputCode:append(
              'PLXHAL_PWM_setFallingDelay(%i, fmaxf(%s, %s));' 
              % {
                epwmInstance,
                Block.InputSignal[4][deadtime_input_offset + 1],
                self.mask.MinDeadTime
              })
          end
        end

        local setSequenceCode = ''
        if self.const_sequence[pwmChannel] == nil then
          local codeList = U.CodeLines:new()
          if self.sequence_defined_in_aq then
            codeList:append('PLXHAL_PWM_setSequenceAq(%i, %s);' % {
              epwmInstance,
              Block.InputSignal[8][math.min(pwmChannel, #Block.InputSignal[8])]
            })
          else
            codeList:append('PLXHAL_PWM_setSequence(%i, %s);' % {
              epwmInstance,
              Block.InputSignal[2][math.min(pwmChannel, #Block.InputSignal[2])]
            })
          end
          if self.showEnablePort then
            codeList:append(
              'PLXHAL_PWM_prepareSetOutToXTransition(%i);' % {epwmInstance})
          end
          setSequenceCode = table.concat(codeList, '\n')
        end

        local setDutyCode = ''
        do
          local codeList = U.CodeLines:new()

          if self.outmode[2] == '' or self.outmode[2] == '!A' then
            codeList:append(
              'PLXHAL_PWM_setDuty(%i, %s);'
              % {epwmInstance, Block.InputSignal[1][pwmChannel]})
          else
            codeList:append(
              'PLXHAL_PWM_setDutyAB(%i, %s, %s);'
              % {epwmInstance, Block.InputSignal[1][pwmChannel], Block.InputSignal[5][pwmChannel]})
          end
          setDutyCode = table.concat(codeList, '\n')
        end

        if self.showEnablePort then
          OutputCode:append([[
            if((%(en)s) == 0){
              PLXHAL_PWM_setToPassive(%(instance)i);
            } else {
              %(setSequenceCode)s
              %(setDutyCode)s
              PLXHAL_PWM_setToOperational(%(instance)i);
            }
          ]] % {
            en = Block.InputSignal[3][math.min(pwmChannel, #Block.InputSignal[3])],
            instance = epwmInstance,
            setSequenceCode = setSequenceCode,
            setDutyCode = setDutyCode,
          })
        else
          OutputCode:append([[
            %(setSequenceCode)s
            %(setDutyCode)s
            ]] % {
            setSequenceCode = setSequenceCode,
            setDutyCode = setDutyCode,
          })
        end
      end

      static[self.cpu].numChannels = static[self.cpu].numChannels + 1
    end

    if (self.int_loc == '') then
      OutputSignal:append('{}')
    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:append('{modtrig = {bid = %i}}' % {self:getId()})
    end

    if (self.soc_loc == '') then
      OutputSignal:append('{}')
    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:append('{adctrig = {bid = %i}}' % {self:getId()})
    end

    -- synchronization
    self.is_self_synchronized = (self.mask.SyncSrc == 'self')
    OutputSignal:append('{synco = {bid = %i}}' % {self:getId()})
    OutputSignal:append('{syncper = {bid = %i}}' % {self:getId()})

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

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

    local enableCode = self.channels[self.first_channel]:getEnableCode()
    if enableCode ~= nil then
      UpdateCode:append(enableCode)
    end

    local powerstage_obj = self:getBlockInstance('powerstage')

    for zone, _ in pairs(self.trip_zone_settings) do
      if powerstage_obj == nil then
        U.error(
          'TZ%i protection requires the use of a Powerstage Protection block.' %
          {zone})
      end
      if not powerstage_obj:isTripZoneConfigured(zone) then
        powerstage_obj:isDeprecated()
        if powerstage_obj:isDeprecated() then
          U.error('Please replace deprecated powerstage protection block.')
        else
          U.error('Please enable TZ%i under Coder Options -> Target -> Protections.' % {zone})
        end
      end
    end

    -- establish synchronization source of first channel
    if self.is_self_synchronized then
      self.sync_src_bid = self:getId()
    else
      local synci_top = Block.InputSignal[7][1]
      synci_top = synci_top:gsub('%s+', '')  -- remove whitespace
      if synci_top:sub(1, #'{synco') ~= '{synco' then
        U.error(
          "'sycni' port must be connected to the sync port of another PWM block or to the output of an External Sync block.")
      end
      local trig = eval(synci_top)['synco']
      if trig['bid'] == nil then
        error('Malformed trigger expression: %s' % {synci_top})
      end
      self.sync_src_bid = trig['bid']
    end

    if self.is_synchronized then
      self:validateSyncChain()
    end

    return {Require = Require, UpdateCode = UpdateCode}
  end

  function EpwmBasic:finalizeThis(c)
    if self['modtrig'] ~= nil then
      for _, b in ipairs(self['modtrig']) do
        if b:blockMatches('tasktrigger') then
          local modTriggerCpu = self.cpu
          if b:getCpu() ~= self.cpu then
            modTriggerCpu = b:getCpu()
          end
          c.FinalizeNeedsRerun = true
          local isrObj = self:makeBlock('interrupt', modTriggerCpu)
          isrObj:createImplicit({
            isrCode = globals.target.getEpwmIsrCode(self.first_unit),
            isrEnableCode = globals.target.getEpwmIsrEnableCode(self.first_unit),
            bid = self.channels[self.first_channel]:getId()
          })
        end
      end
    end
  end

  function EpwmBasic:finalize(c)
    if static[self.cpu].finalized then
      return
    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_var.lua
    EpwmCommonStatic.generatePlxHalCode(c)

    local setDutyExtraCode = [==[
    #pragma CODE_SECTION(PLXHAL_PWM_setDutyAB, "ramfuncs")
    void PLXHAL_PWM_setDutyAB(uint16_t aHandle, float aDutyA, float aDutyB) {
      PLX_PWM_setPwmDutyA(EpwmHandles[aHandle], aDutyA);
      PLX_PWM_setPwmDutyB(EpwmHandles[aHandle], aDutyB);
    }

    void PLXHAL_PWM_setOverflowSocDelay(uint16_t aHandle, float aPhase) {
      PLX_PWM_setPwmDutyCGreaterThanZero(EpwmHandles[aHandle], 1.0f - aPhase);
    }

    void PLXHAL_PWM_setUnderflowSocDelay(uint16_t aHandle, float aPhase) {
      PLX_PWM_setPwmDutyCSmallerThanFull(EpwmHandles[aHandle], aPhase);
    }
    ]==]
    c.Declarations:append(setDutyExtraCode)

    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 EpwmBasic
end

return Module
