--[[
  Copyright (c) 2023 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 = {
  epwm1 = nil,
  plxHalGenerated = {},
}
local Module = {}

function Module.getBlock(globals, cpu)
  local EpwmShared = require('common.block').getBlock(globals, cpu)

  function EpwmShared:getConstantSignal(port, index, name)
    local phStr = Block.InputSignal[port][index]
    if string.find(phStr, 'UNCONNECTED') then
      return 0
    elseif Block.InputType[port][index] == 'float' then
      return tonumber(string.sub(phStr, 1, -2))
    elseif string.sub(Block.InputType[port][index], 1, #'uint') == 'uint'
        or string.sub(Block.InputType[port][index], 1, #'int') == 'int' then
        return tonumber(phStr)
    elseif Block.InputType[port][index] == 'bool' then
      if phStr == 'true' then
        return 1
      elseif phStr == 'false' then
        return 0
      end
    else
      U.error('Unsupported data type ("%s") for %s port.' % {Block.InputType[port][index], name})
    end
  end

  function EpwmShared:registerSelfAsEpwm1()
    static.epwm1 = globals.instances[self.bid]
  end

  function EpwmShared:validateSyncChain()
    local individualSync = (globals.target.getTargetParameters()['epwms']['sync_group_size'] == 1)
    if individualSync then
      -- no restrictions
      return
    end

    -- fixed (legacy) synchronization chains
    local syncBase = math.floor((self.first_unit - 1) / 3) * 3 + 1
    local syncCeil = syncBase + 2   
    local errorMsg =  [[
      This target (%(target)s) requires that the channels of a synchronized PWM block be consecutive
      and constrained to one synchronization group (in this case %(sync_base)i ... %(sync_ceil)i).
    ]] % {
      target = globals.target.getFamilyPrefix(),
      sync_base = syncBase,
      sync_ceil = syncCeil,
    }
    -- make sure all channels of this block are consecutive and within a single sync chain
    if ((self.first_unit + self.num_units) ~= (self.last_unit + 1 ))
    or (self.last_unit > syncCeil) then
      U.error(errorMsg)
    end 
    -- make sure that upstream synchronization falls within a valid chain
    if self.is_self_synchronized then
      if self.first_unit ~= syncBase then
        U.error([[
          This target (%(target)s) does not allow a synchronization chain to start with PWM %(unit)i.
        ]] % {
          target = globals.target.getFamilyPrefix(),
          unit = self.first_unit,             
        })
      end
    else
      local syncBlockType = globals.instances[self.sync_src_bid]:getType()

      if syncBlockType == 'comp' then
        -- no additional checks needed
      else
        if syncBlockType == 'extsync' then
          if (self.first_unit ~= syncBase) then
            U.error([[
              For this target (%(target)s), only the base unit of a synchronization group can be externally synchonized.
            ]] % {
              target = globals.target.getFamilyPrefix(),
              unit = self.first_unit,             
            })
          end
        else
          local upstreamFirstUnit = globals.instances[self.sync_src_bid]:getParameter('first_unit')
          if (self.first_unit ~= syncBase) and (upstreamFirstUnit ~= syncBase) then
            local upstreamLastUnit = globals.instances[self.sync_src_bid]:getParameter('last_unit')
            if upstreamLastUnit ~= (self.first_unit -1) then
              U.error([[
                This target (%(target)s) does not allow synchronization chains that skip PWM units.
              ]] % {
                target = globals.target.getFamilyPrefix(),
                unit = self.first_unit,
              })
            end
          else
            if (self.first_unit <= upstreamFirstUnit)
            or ((upstreamFirstUnit-1) % 3 ~= 0) then
              U.error([[
                Invalid synchronization input for this target (%(target)s).
              ]] % {
                target = globals.target.getFamilyPrefix(),
              })
            end
          end
        end
      end
    end
  end

  function EpwmShared:getSyncSourceOriginBlock()  -- only used from within this module
    if self.is_self_synchronized then
      return self:getId()
    else
      local syncBlockType = globals.instances[self.sync_src_bid]:getType()
      if (syncBlockType == 'extsync') or (syncBlockType == 'comp') then
        return self.sync_src_bid
      elseif globals.instances[self.sync_src_bid].getSyncSourceOriginBlock then
        return globals.instances[self.sync_src_bid]:getSyncSourceOriginBlock()
      else
        U.error('Unsupported synchronization block: %s.' % {syncBlockType})
      end
    end
  end

  function EpwmShared:configurePwmSynchronizationChain()  -- called by preFinalize in epwm.lua
    if not self.is_synchronized then
      -- block with epwm sequence that is not synchronized
      return
    end

    if globals.target.getTargetParameters().epwms.type < 4 then
      if self.first_unit == 1 then
        local syncOrigin = globals.instances[self:getSyncSourceOriginBlock()]
        if syncOrigin:getType():find('epwm', 1, true) == 1 then
          if self:getSyncSourceOriginBlock() ~= self:getId() then
            U.error('On %s devices, ePWM1 cannot be synchronized from ePWM%i.' %
              {globals.target.getFamilyPrefix(), syncOrigin.first_unit})
          end
        end
      elseif self.first_unit == 4 then
        if not self.is_self_synchronized then
          -- make sure external synchronization config is valid
          if static.epwm1 then
            if self:getSyncSourceOriginBlock() ~= static.epwm1:getSyncSourceOriginBlock() then
              U.error('On %s devices, ePWM4 must have the same external synchronization source as ePWM1.' %
                {globals.target.getFamilyPrefix()})
            end
          else
            U.error('On %s devices, synchronization of ePWM4 requires that ePWM1 be also configured.' %
              {globals.target.getFamilyPrefix()})
          end
        end
      end
    end

    local individualSync = (globals.target.getTargetParameters()['epwms']['sync_group_size'] == 1)
    if self.is_self_synchronized then
      for c, p in pairs(self.channels) do
        if c == self.first_channel then
          -- first ePWM in chain is self-triggered and must provide SYNCO
          local synco_sel
          if individualSync then
            synco_sel = 2  -- ZEROEN
          else
            -- chains of up to 3
            if (p.epwm - 1) % 3 ~= 0 then
              -- keep units that are not at the head of chain in 'flow-through'
              synco_sel = 0  -- TB_SYNC_IN / NONE
            else
              synco_sel = 1  -- TB_CTR_ZERO
            end
          end
          self.channels[c]:configureSync({
            synco_sel = synco_sel,
            phsen = 0
          })
        else
          -- other modulators are in 'flow-through'
          local synci_sel
          if individualSync then
            -- 'flow-through' must be emulated
            synci_sel = globals.target.getPwmSyncInSel({
              epwm = p['epwm'],
              type = 'epwm',
              source_unit = self.first_unit
            })
          end
          self.channels[c]:configureSync({
            synco_sel = 0,  -- TB_SYNC_IN / NONE
            phsen = 1,
            synci_sel = synci_sel
          })
        end
      end
    else
      -- handle external synchronization source
      local synci_sel_first
      local tripi_sel_first
      do
        local synci_origin_bid = self:getSyncSourceOriginBlock()
        local syncBlockType = globals.instances[synci_origin_bid]:getType()
        if syncBlockType == 'comp' then
          tripi_sel_first = globals.instances[synci_origin_bid]:getParameter('tripInputSignal')
        else 
          local synctype, source_unit, source_last_unit
          if syncBlockType == 'extsync' then
            synctype = 'external'
            source_unit = globals.instances[synci_origin_bid]:getParameter('unitAsInt')
          else
            synctype = 'epwm'
            source_unit = globals.instances[synci_origin_bid]:getParameter('first_unit')
            source_last_unit = globals.instances[synci_origin_bid]:getParameter('last_unit')
          end

          synci_sel_first = globals.target.getPwmSyncInSel({
            epwm = self.first_unit,
            type = synctype,
            source_unit = source_unit,
            source_last_unit = source_last_unit
          })
        end
      end

      for c, p in pairs(self.channels) do
        if (c == self.first_channel) or individualSync then
          -- first ePWM in chain is externally triggered and must provide SYNCO
          self.channels[c]:configureSync({
            synco_sel = 0,  -- TB_SYNC_IN / NONE
            synci_sel = synci_sel_first,
            tripi_sel = tripi_sel_first,
            phsen = 1,
          })
        else
          -- other modulators are in 'flow through'
          self.channels[c]:configureSync({
            synco_sel = 0,  -- TB_SYNC_IN / NONE
            tripi_sel = tripi_sel_first,
            phsen = 1
          })
        end
      end
    end
  end

  function EpwmShared:setSinkForTriggerSource(sink)
    if sink ~= nil then
      self:logLine('%s connected to %s of %d' % {self:getType(), 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.channels[self.first_channel]: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.channels[self.first_channel]:configureSocEvents({
          soc_prd = self.soc_prd,
          soc_loc = self.soc_loc,
          soc_delayed = self.soc_delayed
        })
      end
      table.insert(self[sink.type], globals.instances[sink.bid])
    end
  end

  function EpwmShared: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 EpwmShared: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 EpwmShared:getStaticMethods()
    local globals = self.globals

    local EpwmCommonStatic = {}

    function EpwmCommonStatic.generatePlxHalCode(c)
      if static.plxHalGenerated[self.cpu] then
        return
      end
      static.plxHalGenerated[self.cpu] = true

      c.Declarations:append([[
        // PWM Enable Functions
        void PLXHAL_PWM_setToPassive(uint16_t aChannel) {
          PLX_PWM_setOutToPassive(EpwmHandles[aChannel]);
        }

        void PLXHAL_PWM_setToOperational(uint16_t aChannel) {
          PLX_PWM_setOutToOperational(EpwmHandles[aChannel]);
        }

        void PLXHAL_PWM_prepareSetOutToXTransition(uint16_t aHandle) {
            PLX_PWM_prepareSetOutToXTransition(EpwmHandles[aHandle]);
        }
      
        // PWM Set Duty Cycle Function
        #pragma CODE_SECTION(PLXHAL_PWM_setDuty, "ramfuncs")
        void PLXHAL_PWM_setDuty(uint16_t aHandle, float aDuty) {
          PLX_PWM_setPwmDuty(EpwmHandles[aHandle], aDuty);
        }
       
        // PWM Set Dead Time Functions
        #pragma CODE_SECTION(PLXHAL_PWM_setRisingDelay, "ramfuncs")
        void PLXHAL_PWM_setRisingDelay(uint16_t aChannel, float delay) {
          PLX_PWM_setDeadTimeCountsRising(EpwmHandles[aChannel], delay * %(deadTimeClock)ef);
        }

        #pragma CODE_SECTION(PLXHAL_PWM_setFallingDelay, "ramfuncs")
        void PLXHAL_PWM_setFallingDelay(uint16_t aChannel, float delay) {
          PLX_PWM_setDeadTimeCountsFalling(EpwmHandles[aChannel], delay * %(deadTimeClock)ef);
        }
  
        // PWM Set Sequence Functions  
        void PLXHAL_PWM_setSequence(uint16_t aChannel, uint16_t aSequence) {
          PLX_PWM_setSequence(EpwmHandles[aChannel], aSequence);
        }

        void PLXHAL_PWM_setSequenceAq(uint16_t aChannel, uint32_t aSequenceAq) {
          PLX_PWM_setSequenceAq(EpwmHandles[aChannel], aSequenceAq);
        }
        
        void PLXHAL_PWM_enableOutputSwap(uint16_t aChannel, bool aEnableSwap){
          PLX_PWM_enableOutputSwap(EpwmHandles[aChannel], aEnableSwap);
        }
      ]] % {deadTimeClock = globals.target.getDeadTimeClock()})

      if not self:targetUsesPinsets() then
        c.Declarations:append([[
          // Function for updating MEP using SFO
          void PLXHAL_PWM_updateMEP() {
            PLX_PWM_updateMEP();
          }
        ]] % {deadTimeClock = globals.target.getDeadTimeClock()})
      end

    end

    return EpwmCommonStatic
  end

  return EpwmShared
end

return Module
