--[[
  Copyright (c) 2021, 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 VREF = require('blocks.vref_config')
local static = {}
local Module = {}

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

  function Adc:setImplicitTriggerSource(bid)
    self.trig_base_task_exp = '{adctrig = {bid = %d}}' % {bid}
  end

  function Adc:checkMaskParameters()

    -- Main Tab:
    self.showExplicitTriggerPort = U.comboEnabled(Block.Mask.TrigSrc)

    self.adcUnitAsChar = U.comboAsChar(Block.Mask.AdcUnit)
    self.adcId = 'ADC%s' % {self.adcUnitAsChar}
    static[self.cpu].instances[self.adcUnitAsChar] = self.bid

    -- Input: Constraints "integer,nonNegative,nonEmpty"
    -- Scale: Constraints "finite", vectorized in MaskInit
    -- Offset: Constraints "finite", vectorized in MaskInit
    local useSpecifiedAcqTime = U.comboEnabled(Block.Mask.AcqTimeSel)
    -- AcqTime: Constraints "finite,nonNegative" vectorized in the MaskInit

    -- Signal Mode Tab:
    -- This only exists for the experimental block, defaults to single-ended
    self.sigmodeDifferential = U.comboEnabled(Block.Mask.SigMode)
    if self.sigmodeDifferential then
      -- check differential supported for this target
      local adcPeriphType = globals.target.getTargetParameters().adcs.peripheralType
      if (adcPeriphType ~= 4) then
        U.error('This chip does not support differential ADC configurations.')
      end
    end

    self.numChannels = #Block.Mask.Input
    if self.numChannels > globals.target.getTargetParameters().adcs.num_channels then
      U.error('Maximal number of conversions exceeded.')
    end

    self.channels = {}
    self.maxACQPS = -1  -- 28335 has only one ACQPS value for all channels, we will use the max value
    self.totalConversionTimeInAcqps = 0

    for i = 1, self.numChannels do

      local input = Block.Mask.Input[i]
      -- Make sure selected ADC is in range
      if input > globals.target.getTargetParameters().adcs.num_channels - 1 then
        U.error('AIN%d is not a valid input for for %s.' % {input, self.adcId})
      end

      local ts = useSpecifiedAcqTime and Block.Mask.AcqTime[i] or 0

      local ACQPS = globals.target.calcACQPS(ts, self.sigmodeDifferential)
      self.totalConversionTimeInAcqps = self.totalConversionTimeInAcqps + ACQPS
      if ACQPS > self.maxACQPS then
        self.maxACQPS = ACQPS 
      end

      self.channels[i] = {
        input = input,
        scale = Block.Mask.Scale[i],
        offset = Block.Mask.Offset[i],
        ACQPS = ACQPS
      }
    end
  end

  function Adc:p_getDirectFeedthroughCode()
    local Require = ResourceList:new()
    local InitCode = U.CodeLines:new()
    local OutputSignal = StringList:new()
    local TriggerOutputSignal = StringList:new()

    -- Check for this ADC before requiring, because the voltage ref code will 
    -- error before the resource manager if it doesn't exist on this target.
    local resourceName = 'ADC %s' % {self.adcUnitAsChar}
    if not U.resourceExists(globals.target, resourceName) then
      U.error('This target does not provide @param:AdcUnit: "%s".' % {resourceName})
    end
    Require:add(resourceName)

    local pins = {}
    for i, ch in ipairs(self.channels) do
      Require:add('%s-SOC' % {self.adcId}, i - 1)

      -- Make sure selected ADC is in range
      if ch.input > globals.target.getTargetParameters().adcs.num_channels - 1 then
        U.error('AIN%d is not a valid input for for %s.' % {ch.input, self.adcId})
      end

      local analog_mapping = globals.target.getTargetParameters().adcs.analog_mapping
      local analog_pin = '%s%d' % {self.adcUnitAsChar, ch.input}
      if analog_mapping and analog_mapping[analog_pin] then
        table.insert(pins, analog_mapping[analog_pin])
      end

      OutputSignal:append('PLXHAL_ADC_getIn(%d, %d)' % {self.instance, i - 1})
    end

    TriggerOutputSignal:append('{modtrig = {bid = %d}}' % {Adc:getId()})

    globals.syscfg:addPeripheralEntry('adc', {
      unitAsChar = self.adcUnitAsChar,
      core = globals.target.getTiCpuNumber(self.cpu),
    })

    globals.syscfg:addPeripheralEntry('analog_port', {
      pins = pins,
    })

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

  function Adc:p_getNonDirectFeedthroughCode()
    local trig = Block.InputSignal[1][1]

    if not self.showExplicitTriggerPort or string.find(trig, 'UNCONNECTED') then
      -- not connected - will have to use implicit trigger
    else
      -- verify proper connection of trigger point
      trig = trig:gsub('%s+', '')  -- remove whitespace
      if trig:sub(1, #'{adctrig') ~= '{adctrig' then
        U.error('Trigger port must be connected to an ADC Trigger source.')
      end

      local trigObj = eval(trig)['adctrig']
      local triggerBlock = globals.instances[trigObj['bid']]

      if  triggerBlock:blockMatches('timer')
      and triggerBlock:getCpu() ~= self.cpu then
        U.error(
          'ADC block cannot be triggered from a Timer block on a different CPU.')
      end

      self.trigExpression = trig
    end

    return {}
  end

  function Adc:setSinkForTriggerSource(sink)
    if self.numChannels == 0 then
      -- this block is not triggered and does not provide a trigger
      return
    end

    if not self.trigExpression then
      self.trigExpression = self.trig_base_task_exp
    end

    local trig = eval(self.trigExpression)['adctrig']
    local triggerBlock = globals.instances[trig['bid']]
    self.trig = triggerBlock

    if sink ~= nil then
      if self[sink.type] == nil then
        self[sink.type] = {}
      end
      table.insert(self[sink.type], globals.instances[sink.bid])
    end

    if not self.downstreamConnectionsPropagated then
      -- top of chain
      if self.trig then
        self.trig:setSinkForTriggerSource({type = 'adctrig', bid = self.bid})
      end
      self.downstreamConnectionsPropagated = true;
    end
  end

  function Adc:propagateTriggerSampleTime(ts)
    if ts then
      self.ts = ts
      if self['modtrig'] then
        for _, b in ipairs(self['modtrig']) do
          local f = b:propagateTriggerSampleTime(ts)
        end
      end
    end
  end

  function Adc:getTriggerSampleTime()
    return self.ts
  end

  function Adc:getIsrConfigCode()
    return globals.target.getAdcSetupCode(self.adcUnitAsChar, {
      isr = '%s_baseTaskInterrupt' % {Target.Variables.BASE_NAME},
      INT1SEL = self.numChannels - 1,
      trig_is_timer = (self.trigsel == 1)
    })
  end

  function Adc:finalizeThis(c)
    if not self.trig then
      U.error('No trigger source configured for ADC')
    end

    local TRIGSEL, trigSelText
    -- see "ADC SOC Trigger Selection" or "ADCSOC0CTL Register Field Descriptions" in TRM
    if self.trig:blockMatches('timer') then
      local unit = self.trig:getParameter('unit')
      if self.trig:getCpu() == 0 then
        TRIGSEL = 1
        trigSelText = 'CPU 1 Timer%d' % {unit}
      elseif self.trig:getCpu() == 1 then
        TRIGSEL = 29
        trigSelText = 'CPU 2 Timer%d' % {unit}
      end
    elseif self.trig:blockMatches('epwm_basic')
    or     self.trig:blockMatches('epwm_var')
    or     self.trig:blockMatches('epwm_basic_pcc')
    or     self.trig:blockMatches('epwm_var_pcc') then
      local unit = self.trig:getParameter('first_unit')
      if unit < 13 then
        TRIGSEL = 5 + 2 * (unit - 1)
      else
        if self:targetMatches({'2838x'}) then
          TRIGSEL = 32 + 2 * (unit - 13)
        elseif self:targetMatches({'28P65x', '29H85x'}) then
          TRIGSEL = 88 + 2 * (unit - 13)
        else
            U.throwUnhandledTargetError()
        end
      end
      trigSelText = 'PWM%d' % {unit}
    else
      U.error('ADC can only be triggered by a PWM or Timer block.')
    end

    self.trigsel = TRIGSEL

    -- determine if this block is supplying the actual base task trigger
    -- and/or triggering a CLA
    self['is_mod_trigger'] = false
    self['is_cla_trigger'] = false

    if self['modtrig'] ~= nil then
      for _, b in ipairs(self['modtrig']) do
        if b:blockMatches('tasktrigger') then
          self['is_mod_trigger'] = true
          local modTriggerCpu = self.cpu
          if b:getCpu() ~= self.cpu then
            modTriggerCpu = b:getCpu()
          end
          local isrCode
          if self:targetMatches({'2806x', '2833x', '2837x', '2838x', '28003x', '28004x', '280013x', '28P55x', '28P65x'}) then
            isrCode = [[
              interrupt void %(isr)s_baseTaskInterrupt(void)
              {
                %(ack_code)s
                DISPR_dispatch();
              }
            ]] % {
              isr = Target.Variables.BASE_NAME,
              ack_code = globals.target.getAdcInterruptAcknCode(
                self.adcUnitAsChar, {
                  trig_is_timer = (TRIGSEL == 1)
                }),
            }
          elseif self:targetMatches({'29H85x'}) then
            isrCode = [[
              __attribute__((interrupt("INT"))) void %(isr)s_baseTaskInterrupt(void);
              void %(isr)s_baseTaskInterrupt(void)
              {
                %(ack_code)s
                DISPR_dispatch();
              }
            ]] % {
              isr = Target.Variables.BASE_NAME,
              ack_code = globals.target.getAdcInterruptAcknCode(
                self.adcUnitAsChar, {}),
            }
          else
            U.throwUnhandledTargetError()
          end
          local isrEnableCode
          if self:targetMatches({'2806x', '2833x', '2837x', '2838x', '28003x', '28004x', '280013x', '28P55x', '28P65x'}) then
            isrEnableCode = 'IER |= M_INT1;'
          elseif self:targetMatches({'29H85x'}) then
            isrEnableCode = [[
              Interrupt_enable(INT_ADC%(unit)s1);
              Interrupt_setPriority(INT_ADC%(unit)s1, 255);
              Interrupt_setContextID(INT_ADC%(unit)s1, INTERRUPT_CONTEXTID_0);
            ]] % {
              unit = self.adcUnitAsChar,
            }
          else
            U.throwUnhandledTargetError()
          end

          c.FinalizeNeedsRerun = true
          local isrObj = self:makeBlock('interrupt', modTriggerCpu)
          isrObj:createImplicit({
            isrCode = isrCode,
            isrEnableCode = isrEnableCode,
            bid = self:getId()
          })
        elseif b:blockMatches('cla') then
          self['is_cla_trigger'] = true
        end
      end
    end

    -- Get the ADC Voltage Reference
    -- disabled this diagnostic becuase luals is confused by the interface of other TSPs
    --- @diagnostic disable-next-line: redundant-parameter
    local vrefConfig = VREF.getAdcVref(globals, self.adcUnitAsChar)

    c.PreInitCode:append([[
      // configure ADC %(adcUnitAsChar)s
      {
          PLX_AIN_AdcParams_t params;
          PLX_AIN_setDefaultAdcParams(&params, %(vref)f, %(cb_sigmodeDifferential)d);
      ]] % {
      adcUnitAsChar = self.adcUnitAsChar,
      vref = vrefConfig.vref,
      cb_sigmodeDifferential = self.sigmodeDifferential and 1 or 0, -- convert lua boolean to C integer
    })

    if globals.target.getTargetParameters().adcs.peripheralType == 2 then
      -- set ACQPS for 28335 here. There is only one value for all channels!!
      c.PreInitCode:append([[
          params.ADCTRL1.bit.ACQ_PS = %d;]] % {self.maxACQPS})
    end
    if globals.target.getTargetParameters().adcs.peripheralType == 3 then
      -- special handling for legacy (2806x) ADC - this is otherwise handled by analog.lua
      c.PreInitCode:append([[
          params.ADCCTL1.bit.ADCREFSEL = %(cb_useExternalVref)d;
        ]] % {
        cb_useExternalVref = vrefConfig.useExternalVref and 1 or 0,  -- convert lua boolean to C integer
      })
    end

    c.PreInitCode:append([[
          PLX_AIN_configure(AdcHandles[%d], (PLX_AIN_Unit_t)%d, &params);]] 
      % {self.instance, U.charToZeroIndexedInt(self.adcUnitAsChar)})
    if self['is_cla_trigger'] then
      c.PreInitCode:append(globals.target.getAdcSetupCode(self.adcUnitAsChar, {
        isr = nil,
        INT1SEL = self.numChannels - 1,
        trig_is_timer = (TRIGSEL == 1)
      }))
    end
    c.PreInitCode:append([[
        } ]])

    for i, ch in ipairs(self.channels) do
      local socNum = i - 1
      c.PreInitCode:append([[
        // configure SOC%(socNum)d of ADC-%(adcUnitAsChar)s to measure ADCIN%(input)d
        {
          PLX_AIN_ChannelParams_t params;
          PLX_AIN_setDefaultChannelParams(&params);
          params.scale =  %(scale).9ef;
          params.offset = %(offset).9ef;
        ]] % {
        socNum = socNum,
        adcUnitAsChar = self.adcUnitAsChar,
        input = ch.input,
        scale = ch.scale,
        offset = ch.offset,
      })

      if globals.target.getTargetParameters().adcs.peripheralType == 2 then
        c.PreInitCode:append([[
            params.trigsel = %d;]] % {TRIGSEL})
      else
        c.PreInitCode:append([[
            // set SOC trigger to %(text)s
            params.ADCSOCxCTL.bit.TRIGSEL = %(TRIGSEL)d;
            params.ADCSOCxCTL.bit.ACQPS = %(acqps)d;
            ]] % {
          text = trigSelText,
          TRIGSEL = TRIGSEL,
          acqps = ch.ACQPS,
        })
      end
      c.PreInitCode:append([[
            PLX_AIN_setupChannel(AdcHandles[%d], %d, %d, &params);
          }
          ]] % {self.instance, socNum, ch.input})
    end
  end

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

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

    c.Declarations:append([[
      PLX_AIN_Handle_t AdcHandles[%(numAdcInstances)d];
      PLX_AIN_Obj_t AdcObj[%(numAdcInstances)d];

      float PLXHAL_ADC_getIn(uint16_t aHandle, uint16_t aChannel){
        return PLX_AIN_getInF(AdcHandles[aHandle], aChannel);
      }
    ]] % {numAdcInstances = static[self.cpu].numInstances})


    c.PreInitCode:append([[
    {
      PLX_AIN_sinit(%(sysClkHz)d);

      for (int i = 0; i < %(numAdcInstances)d; i++) {
        AdcHandles[i] = PLX_AIN_init(&AdcObj[i], sizeof(AdcObj[i]));
      }
    }
    ]] % {
      sysClkHz = globals.target.getCleanSysClkHz(),
      numAdcInstances = static[self.cpu].numInstances
    })

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

    static[self.cpu].finalized = true
  end

  function Adc:registerTriggerRequest(trig)
    Adc:propagateTriggers()
  end

  function Adc:getTriggerSampleTime()
    if self.numChannels == 0 then
      -- this block is not triggered and does not provide a trigger
      return 0
    end
    return self['ts']
  end

  function Adc:getTotalConversionTime()
    return self.totalConversionTimeInAcqps
  end

  return Adc
end

return Module
