--[[
  Copyright (c) 2024 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 static[cpu] == nil 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({'sync', 'pos'}) -- FIX ME: Why do we call the second input 'pos'
  local outIdx = U.enum({'trip'})

  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

    -- establish the comparator input pin
    self.cbx_senseInputType = U.enforceMask_newComboBox(
      'SenseInputType', {'adc', 'pga'})

    if self.cbx_senseInputType.equals('adc') then
      -- We could run AdcUnit through a comboBox, 
      -- but that is more maintenance when we add ADC E...
      local adcUnitAsChar = string.char(65 + Block.Mask.AdcUnit - 1)
      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, no comparator input pin detected.')
    end

    self.cbx_compOutPolarity = U.enforceMask_newComboBox(
      'Polarity', {'nonInverted', 'inverted'})

    if  self.cbx_compOutPolarity.equals('nonInverted')
    and U.comboEnabled(Block.Mask.RampGenerator) then
      self.opt_rampGenerator = {
        rampSlope = U.enforceMask(U.isNonNegativeScalar, 'RampSlope'),
      }
    end

  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)

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

    --[[
        Establish if compare input is constant. The 'pos' port
        represents the compare threshold.

        self.threshold0 (aka constantTresholdValue) will be:
          - nil if not a constant, or
          - a number holding the constant threshold value.
    --]]
    if not U.portIsConnected(inIdx.pos) then
      self.threshold0 = 0
    else
      self.threshold0 = U.portValueGetConstant(inIdx.pos) -- nil if not constant
    end
    

    OutputSignal:append('{tevent = {bid = %i}}' % {self:getId()})
    if not self.threshold0 then
      -- The threshold is not a constant, this is the update code.
      local thresholdInputStr = Block.InputSignal[inIdx.pos][1]
      if self.opt_rampGenerator then
        OutputCode:append('PLXHAL_CMPSS_setPeak(%i, %s);' % {
          self.instance,
          thresholdInputStr,
        })
      else
        OutputCode:append('PLXHAL_CMPSS_setDacHigh(%i, %s);' % {
          self.instance,
          thresholdInputStr,
        })
      end
    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
    -- determine high comparator available at pin (high has ramp on all devices)
    -- TODO: also consider the use of the low comparator (especially on 65x devices)

    local comparators = globals.target.getComparatorsForPin(self.compPinStr)
    if comparators and comparators.high then
      self.compHigh = comparators.high
    else
      U.error('No suitable comparator found for pin %s.' % {self.compPinStr}) -- TODO: Move this code to checkParams to generate a better message?
    end

    local compHighParams = {
      opt_pinMux = self.compHigh.mux,
      threshold = self.threshold0 or 3.3,
      invertCompare = self.cbx_compOutPolarity.equals('inverted'),
    }
    if self.opt_rampGenerator then
      local rampSlope = self.opt_rampGenerator.rampSlope

      print(Block.InputSignal[inIdx.sync][1])
      -- example input: {syncper = {bid = 9}}
      -- determine synchronizing PWM block
      local peripheralSyncString = Block.InputSignal[inIdx.sync][1]
      peripheralSyncString = peripheralSyncString:gsub('%s+', '')  -- remove whitespace
      if peripheralSyncString:sub(1, #'{syncper') ~= '{syncper' then
        U.error("'sync' port must be connected to the PER port of a PWM block.")
      end
      local peripheralSync = eval(peripheralSyncString)['syncper']
      if peripheralSync['bid'] == nil then
        U.error('Malformed peripheralSync expression: %s' % {t_event})
      end
      local epwmUnit = globals.instances[peripheralSync['bid']]:getParameter(
        'first_unit')

      -- process ramp settings 
      local negativeSlope = math.abs(rampSlope)
      local rampDecrement = math.ceil(negativeSlope /
        (3.3 / 65536 * globals.target.getCleanSysClkHz()))
      local actualRamp = rampDecrement *
         (3.3 / 65536 * globals.target.getCleanSysClkHz())
      -- TODO: Does this log message make sense? similar one in epwm_basic.lua
      self:logLine(
        'DAC decrement set to %i, resulting in ramp of %f A/s, desired was %f A/s.' %
        {rampDecrement, actualRamp, negativeSlope})

      compHighParams.opt_ramp = {
        peripheralSyncEpwmUnit = epwmUnit,
        rampDecrement = rampDecrement,
        desiredRamp = negativeSlope,
        actualRamp = actualRamp,
      }
    end

    local compHighObj = self:makeBlock('cmpss')
    compHighObj:createImplicit(
      self.compHigh.unit,
      {
        analogPin = self.compPinStr,
        tripSignal = self.tripInputSignal,
        opt_compHigh = compHighParams,
      }, Require, nil)
    return {Require = Require, UpdateCode = UpdateCode}
  end

  function Comp:finalizeThis(c)
    c.PreInitCode:append([[
      PLX_CMPSS_configure(CmpssHandles[%(instance)i], CMPSS%(cmp)i_BASE);
      ]] % {
      instance = self.instance,
      cmp = self.compHigh.unit,
    })

    if self.threshold0 then
      if self.opt_rampGenerator then
        c.PostInitCode:append([[
          // ramp with constant peak value
          PLX_CMPSS_setPeak(CmpssHandles[%(instance)i], %(peak)f);
          ]] % {
          instance = self.instance,
          peak = self.threshold0,
        })
      else
        c.PostInitCode:append([[
          // constant peak value
          PLX_CMPSS_setDacHigh(CmpssHandles[%(instance)i], %(value)f);
          ]] % {
          instance = self.instance,
          value = self.threshold0,
        })
      end
    end
  end

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

    c.Include:append('plx_cmpss.h')
    c.Declarations:append([[
      PLX_CMPSS_Handle_t CmpssHandles[%(numInstances)i];
      PLX_CMPSS_Obj_t CmpssObj[%(numInstances)i];
      ]] % {
      numInstances = static[self.cpu].numInstances
    })
    c.PreInitCode:append([[
      for (int i = 0; i < %(numInstances)i; i++) {
        CmpssHandles[i] = PLX_CMPSS_init(&CmpssObj[i], sizeof(CmpssObj[i]));
      }
      ]] % {
      numInstances = static[self.cpu].numInstances
    })

    local setPeakCurrentCode = [[
      void PLXHAL_CMPSS_setDacHigh(uint16_t aHandle, float aVal){
        PLX_CMPSS_setDacHigh(CmpssHandles[aHandle], aVal);
      }

      void PLXHAL_CMPSS_setPeak(uint16_t aHandle, float aPeak){
        PLX_CMPSS_setPeak(CmpssHandles[aHandle], aPeak);
      }
    ]]
    c.Declarations:append(setPeakCurrentCode)

    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
