--[[
  Copyright (c) 2021 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 = {
  cbc_max_duty = 0.9
}
local Module = {}

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

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

  function EpwmBasicPcc: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)

    if #Block.InputSignal[1] ~= 1 then
      U.error('Exactly one PWM generator must be configured.')
    end

    self.pwm = U.enforceMask(U.isNonNegativeIntScalar, 'PwmUnit')
    self.first_unit = self.pwm  -- needed by ADC and CLA
    local task_name = Block.Task['Name']

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

    -- carrier
    self.fsw = Block.Mask.CarrierFreq

    -- treat the spoofed 'CarrierType' like a comboBox, lower level functions will expect this interface
    self.cbx_carrierType = U.enforceMask_newComboBox('CarrierType', {'sawtooth', 'triangle'})

    local timing = globals.target.getPwmFrequencySettings(self.fsw,
                                                          self.cbx_carrierType)
    self.fsw_actual = timing.freq
    self.prd = timing.period

    -- accuracy of frequency settings
    if Block.Mask.CarrierFreqTol == 1 then  -- enforce exact value (no rounding)
      U.enforceExactFrequencyTolerance({
        freqDesired = self.fsw, 
        freqWidgetLink = '@param:CarrierFreq:',
        freqTolWidgetLink = '@param:CarrierFreqTol:',
        freqAchievable = self.fsw_actual,
        descriptor = 'PWM frequency',
      })
    end

    self.dead_time = Block.Mask.FixedDeadTime

    -- polarity
    self.polarityIsActiveHigh = U.enforceMask_newComboBox('Polarity', {'positive', 'negative'}).equals('positive')

    -- sequence
    if self:targetMatches({'2837x'}) then
      self.sequence0 = {
        defined_in_aq = true,
        value = 0x00000012,  -- set at Z, clear at CMPAU
      }
    elseif self:targetMatches({'2838x', '28003x', '28004x', '280013x', '28P55x', '28P65x', '29H85x'}) then
      self.sequence0 = {
        defined_in_aq = true,
        value = 0x00010012,  -- set at Z, clear at CMPAU and T1U
      }
    else
      U.throwUnhandledTargetError()
    end

    -- outmode
    if Block.Mask.OutMode == 1 then
      if self:targetMatches({'2837x'}) then
        -- EPWM_AQ_TRIGGER_EVENT_TRIG_DC_EVTFILT not available for 2837x
        -- therefore we need to use the TZ to implement CBC limiting
        -- which does not lend itself well for controlling complementary outputs
        U.error('Complementary outputs are not supported for this device.')
      elseif not self:targetMatches({'2838x', '28003x', '28004x', '280013x', '28P55x', '28P65x', '29H85x'}) then
        U.throwUnhandledTargetError()
      end
      self.outmode = {'A', '!A'}
    elseif Block.Mask.OutMode == 2 then
      self.outmode = {'A', ''}
    else
      error('Invalid output mode (exception).')
    end

    -- task trigger event
    self.int_loc = ''
    if Block.Mask.TaskTriggerMode == 2 then
      self.int_loc = 'z'
    elseif Block.Mask.TaskTriggerMode == 3 then
      self.int_loc = 'p'
    elseif Block.Mask.TaskTriggerMode == 4 then
      if self.cbx_carrierType.equals('sawtooth') then
        U.error('Invalid task trigger event for sawtooth carrier.')
      end
      if globals.target.getTargetParameters().epwms.type == 0 then
        U.error('Dual task trigger events not supported by this chip.')
      end
      self.int_loc = 'zp'
    end

    
    self.int_prd = U.enforceMask(
      U.isIntScalarInClosedInterval, 'TaskTriggerDivider', 1, max_event_period)

    -- ADC trigger event
    self.soc_loc = ''
    if Block.Mask.AdcTriggerMode == 2 then
      self.soc_loc = 'z'
    elseif Block.Mask.AdcTriggerMode == 3 then
      self.soc_loc = 'p'
    elseif Block.Mask.AdcTriggerMode == 4 then
      if self.cbx_carrierType.equals('sawtooth') then
        U.error('Invalid ADC trigger event for sawtooth carrier')
      end
      if globals.target.getTargetParameters().epwms.type == 0 then
        U.error('Dual ADC trigger events not supported by this chip.')
      end
      self.soc_loc = 'zp'
    end

    self.soc_prd = U.enforceMask(
      U.isIntScalarInClosedInterval, 'AdcTriggerDivider', 1, max_event_period)

    -- cycle-cycle limit
    local pin
    local source
    if Block.Mask.SenseInputType == 1 then
      pin = '%s%i' % {
        U.comboAsChar(Block.Mask.AdcUnit),
        Block.Mask.AdcChannel
      }
      source = '@param:AdcUnit:/@param:AdcChannel:'
    else
      -- Check here if PGA is supported on this target.
      -- Otherwise the later error is confusing
      if globals.target.getTargetParameters().pgas
      and globals.target.getTargetParameters().pgas.num_units > 0 then
        pin = 'PGA%i' % {Block.Mask.PgaUnit}
        source = '@param:PgaUnit:'
      else
        U.error('This target does not have any PGA options for @param:SenseInputType:.')
      end
    end
    
    -- create and configure CMPSS instance
    -- determine high comparator available at pin (high has ramp on all devices)
    local compHigh
    do
      local comparators = globals.target.getComparatorsForPin(pin)
      if (comparators == nil) or (comparators.high == nil) then
        U.error('No high comparator found for pin %(pin)s. Select a different %(source)s' % {
          pin = pin, source = source})
      end
      compHigh = comparators.high
    end
    self.cmp = compHigh.unit

    -- establish TRIPIN signal number
    self.tripInputSignalH = globals.target.getNextAvailableTripInput()
    if not self.tripInputSignalH then
      U.error('Maximum number of trip inputs exceeded.')
    end

    -- determine ramp parameters
    local rampStepVal = math.ceil(Block.Mask.RampSlope * Block.Mask.SenseGain / (3.3 / 65536 * globals.target.getCleanSysClkHz()))
    local actualRamp = rampStepVal / (Block.Mask.SenseGain / (3.3 / 65536 * globals.target.getCleanSysClkHz()))
    self:logLine(
      'DAC decrement set to %i, resulting in ramp of %f A/s, desired was %f A/s.' %
      {rampStepVal, actualRamp, Block.Mask.RampSlope})

    local compHighParams = {
      opt_pinMux = compHigh.mux,
      threshold = 0, -- or maybe 3.3???
      invertCompare = false,
      opt_ramp = {
        peripheralSyncEpwmUnit = self.pwm,
        rampStepVal = rampStepVal,
        desiredRamp = Block.Mask.RampSlope,
        actualRamp = actualRamp,
        b_rampIsIncrementing = false,
        rampClkDivider = 1,
      },
    }
    
    local comp_high = self:makeBlock('cmpss')
    comp_high:createImplicitCmpss(
      compHigh.unit,
      {
        analogPin = pin,
        tripSignalH = self.tripInputSignalH,
        opt_compHigh = compHighParams,
      }, Require, nil)

    -- create and configure epwm instance
    self.epwm_obj = self:makeBlock('epwm', self.cpu)

    self.epwm_obj:createImplicitEpwm(self.pwm, {
                                   fsw = self.fsw,
                                   cbx_carrierType = self.cbx_carrierType,
                                   polarityIsActiveHigh = self.polarityIsActiveHigh,
                                   outMode = self.outmode,
                                   opt_sequence0 = self.sequence0,
                                   opt_dead_time = self.dead_time,
                                   opt_peripheralSyncSrc = 'z',
                                   -- the following are required by epwm.lua
                                   showEnablePort = false,
                                   disableControlByProtectionBlock = false,
                                 }, Require)

    -- configure CBC parameters
    local cbc_blanking_time_pu = Block.Mask.LeadingEdgeBlanking * Block.Mask.CarrierFreq
    if cbc_blanking_time_pu < 0 then
      cbc_blanking_time_pu = 0
    elseif cbc_blanking_time_pu >= static.cbc_max_duty then
      U.error('Excessive leading edge blanking time.')
    end

    local blanking_time_ticks = math.floor(self.prd * cbc_blanking_time_pu + 0.5)
    if blanking_time_ticks < 1 then
      -- this is needed in case of the compare event still beeing active at
      -- the start of the period
      blanking_time_ticks = 1
      cbc_blanking_time_pu = 1 / self.prd
    end

    local cbc_blanking_delay_pu = 0
    local blanking_delay_ticks = math.floor(self.prd * cbc_blanking_delay_pu + 0.5)
    if blanking_delay_ticks > -4 then
      -- to ensure minimal required tz pulse length, the cmpss filter is set to 3 TBCLK (see comp.lua)
      -- we therefore need to start blanking TZ accordingly before the PWM sync pulse
      cbc_blanking_delay_pu = -4 / self.prd
    end

    local cbc_trip = {
      input = self.tripInputSignalH,
      blanking_time_pu = cbc_blanking_time_pu,
      blanking_delay_pu = cbc_blanking_delay_pu
    }
    self.epwm_obj:configureCycleByCycleTrip(cbc_trip)

    self:logLine('EPWM implicitly created for channel %i, pwm %i.' %
      {self.instance, self.pwm})

    -- output code and ouput signals
    OutputCode:append('  PLXHAL_PWM_setDutyAndPeak(%i, %f, (%s+%ff)*%ff);' %
      {self.instance, static.cbc_max_duty,
        Block.InputSignal[1][1],
        Block.Mask.RampOffset,
        Block.Mask.SenseGain})

    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

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

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

    local enableCode = self.epwm_obj:getEnableCode()
    if enableCode ~= nil then
      UpdateCode:append(enableCode)
    end

    return {Require = Require, UpdateCode = UpdateCode}
  end

  function EpwmBasicPcc:setSinkForTriggerSource(sink)
    if sink ~= nil then
      self:logLine('EpwmBasicPcc connected to %s of %d' % {sink.type, sink.bid})
      if self[sink.type] == nil then
        self[sink.type] = {}
      end
      if sink.type == 'modtrig' then
        local b = globals.instances[sink.bid]
        local isr
        if b:blockMatches('tasktrigger') then
          isr = '%s_baseTaskInterrupt' % {Target.Variables.BASE_NAME}
          self:logLine('Providing Task trigger')
        else
          -- CLA trigger
        end
        self.epwm_obj:configureInterruptEvents({
          int_prd = self.int_prd,
          int_loc = self.int_loc,
          isr = isr
        })
      end
      if sink.type == 'adctrig' then
        self:logLine('Providing ADC trigger')
        self.epwm_obj:configureSocEvents({
          soc_prd = self.soc_prd,
          soc_loc = self.soc_loc
        })
      end
      table.insert(self[sink.type], globals.instances[sink.bid])
    end
  end

  function EpwmBasicPcc:propagateTriggerSampleTime(ts)
    if self['modtrig'] ~= nil then
      for _, b in ipairs(self['modtrig']) do
        local f = b:propagateTriggerSampleTime(self.modTrigTs)
      end
    end
    if self['adctrig'] ~= nil then
      for _, b in ipairs(self['adctrig']) do
        local f = b:propagateTriggerSampleTime(self.adcTrigTs)
      end
    end
  end

  function EpwmBasicPcc:requestImplicitTrigger(ts)
    if self.modTrigTs == nil then
      -- offer best fit
      if self.soc_loc ~= '' then
        self.int_loc = self.soc_loc
        self.int_prd = self.soc_prd
      else
        self.int_loc = 'z'
        local pwmTs = 1 / (self.fsw_actual * #self.int_loc)
        self.int_prd = math.max(1, math.floor(ts / pwmTs + 0.5))
        self.int_prd = math.min(
          self.int_prd, 
          globals.target.getTargetParameters()['epwms']['max_event_period'])
      end
      self.modTrigTs = 1 / (self.fsw_actual * #self.int_loc) * self.int_prd
    end
    if self.adcTrigTs == nil then
      -- same as interrupt
      self.soc_prd = self.int_prd
      self.soc_loc = self.int_loc
      self.adcTrigTs = self.modTrigTs
    end
    self:logLine('Offered trigger generator at %f Hz' % {1 / self.modTrigTs})
    return self.modTrigTs
  end

  function EpwmBasicPcc:finalizeThis(c)
  end

  function EpwmBasicPcc:generateTaskTriggerIsrCode(f)
    self.epwm_obj:generateTaskTriggerIsrCode(f)
  end

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

    -- generate lookup tables for epwm and comp handles
    local epwmObjIdxs = {}
    local cmpUnits = {}
    for _, bid in ipairs(static[self.cpu].instances) do
      local epwmBasicPcc = globals.instances[bid]
      table.insert(epwmObjIdxs, epwmBasicPcc:getParameter('epwm_obj'):getObjIndex())
      table.insert(cmpUnits, 'CMPSS%d_BASE' % {epwmBasicPcc:getParameter('cmp')})
    end

    c.Declarations:append([[
      const uint16_t EpwmPccLookup[%(numInst)d] = {%(pwmList)s};
      const uint32_t EpwmPccCmpssLookup[%(numInst)d] = {%(cmpList)s};
      ]] % {
      numInst = static[self.cpu].numInstances,
      pwmList = table.concat(epwmObjIdxs, ', '),
      cmpList = table.concat(cmpUnits, ', '),
    })

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

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

    local setPeakCurrentCode = [==[
      void PLXHAL_PWM_setDutyAndPeak(uint16_t aHandle, float aDuty, float aPeak){
        {
          uint32_t base = EpwmPccCmpssLookup[aHandle];
          if (base != 0)
          {
            float val = 65536.0 / 3.3 * aPeak;
            uint32_t valInt = 0;
            if (val > (float)UINT32_MAX) {
                valInt = UINT32_MAX;
            } else if (val > 0.0f) {
                valInt = (uint32_t)val;
            }
            if (valInt > 0xFFFF)
            {
                uint16_t decVal =  CMPSS_getRampDecValue(base);
                if(decVal != 0)
                {
                    uint32_t rampDelay = (valInt-0xFFFF)/decVal;
                    if (rampDelay > 0x1FFF)
                    {
                        rampDelay = 0x1FFF;
                    }
                    CMPSS_setRampDelayValue(base, (uint16_t)rampDelay);
                }
                valInt = 0xFFFF;
            }
            else
            {
            	 CMPSS_setRampDelayValue(base, 0x0000);
            }
            CMPSS_setMaxRampValue(base, ((uint16_t)valInt));
          }
        }
        if(aPeak <= 0)
        {
          PLX_PWM_setPwmDuty(EpwmHandles[EpwmPccLookup[aHandle]], 0);
        }
        else
        {
          PLX_PWM_setPwmDuty(EpwmHandles[EpwmPccLookup[aHandle]], aDuty);
        }
      }
    ]==]
    c.Declarations:append(setPeakCurrentCode)

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

    static[self.cpu].finalized = true
  end

  return EpwmBasicPcc
end

return Module
