--[[
  Copyright (c) 2021-2025 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 = {
  numInstances = 0,
  tripZonesConfiguredList = {}, -- array of configured trip zones, example element is {zone = 1, gpio = 5}
  tripSignalGroupsConfiguredMap = {}, -- map of groups indexed by groupChar, example element is {coderOptionsWidgetIdx = 4}
  finalized = false,
}

local Module = {}

function Module.getBlock(globals, cpu)
  local TripZones = require('common.block').getBlock(globals, cpu)
  assert((cpu == 0), 'TripZones module should always be associated with CPU 0.')

  TripZones['instance'] = static.numInstances
  static.numInstances = static.numInstances + 1

  if static.numInstances > 1 then
    error(
      'There should be only one (implicit) instance of the TripZones block.')
  end

  function TripZones:createImplicitTripZones(req)
    -- process digital trip inputs
    local z = 1
    
    while Target.Variables['Tz%dEnable' % {z}] ~= nil do
      if U.checkBoxSelected('Tz%dEnable' % {z}) then
        local gpio = U.enforceCO(U.isNonNegativeIntScalar, 'Tz%dGpio' % {z})
        if self:targetUsesPinsets() then
          -- For pinset targets, only some pins can be tripzone
          local validTZPinsMap = 
             globals.target.getTargetParameters().tripzones.validPinsForTripzone
          if not U.arrayContainsValue(validTZPinsMap[z], gpio) then
            U.error(
              'GPIO %d may not be used as a tripzone input to @param:Tz%dEnable:2:' %
              {gpio, z})
          end
        end
        globals.target.allocateGpio(gpio, {}, req, 'TZ %d GPIO' % {z}) -- FIX ME: Would be better if this was a link in the resource conflict error
        table.insert(static.tripZonesConfiguredList, {zone = z, gpio = gpio})
      end
      z = z + 1
    end

    -- process analog trip inputs
    local comps_used = {}
    local i = 1
    while (Target.Variables['AnTrip%dEnable' % {i}] ~= nil) do
      if U.checkBoxSelected('AnTrip%dEnable' % {i}) then
        local label = 'AnTrip%dSignal' % {i}

        local pin
        local errorMessageHint
        if (Target.Variables['AnTrip%dInputType' % {i}] == nil) -- no InputType means ADC (only targets that support PGA have this)
        or (Target.Variables['AnTrip%dInputType' % {i}] == 1) then -- InputType 1 = ADC, 2 = PGA
          pin = '%s%d' % {
            U.comboAsChar(Target.Variables['AnTrip%dAdcUnit' % {i}]),
            U.enforceCO(U.isIntScalarInClosedInterval, 'AnTrip%dAdcChannel' % {i}, 0, globals.target.getTargetParameters().adcs.num_channels - 1)
          }
          errorMessageHint = 'Adjust selection of @param:AnTrip%dAdcUnit:2: and @param:AnTrip%dAdcChannel:2:.' % {i, i}
          if not globals.target.getComparatorsForPin(pin) then
            U.error('No comparator found for pin %s for Analog Trip %d. %s' % {pin, i, errorMessageHint})
          end
        else
          pin = 'PGA%d' % {U.enforceCO(U.isPositiveIntScalar, 'AnTrip%dPgaUnit' % {i})}
          errorMessageHint = 'Adjust selection of @param:AnTrip%dPgaUnit:2:.' % {i}
          if not globals.target.getComparatorsForPin(pin) then
            U.error('No comparator found for pin %s for Analog Trip %d. %s' % {pin, i, errorMessageHint})
          end
        end

        local threshold_low = U.enforceCO(
          U.isScalarInClosedInterval, 'AnTrip%dThresholdLow' % {i}, 0, 3.3, 'V')
        local threshold_high = U.enforceCO(
          U.isScalarInClosedInterval, 'AnTrip%dThresholdHigh' % {i}, 0, 3.3, 'V')

        if threshold_low >= threshold_high then
          U.error(
            'For the analog trip protection, @param:AnTrip%dThresholdLow:2: must be set below @param:AnTrip%dThresholdHigh:2:.' % {i, i})
        end

        -- determine comparators available at pin (error checked above)
        local comparators = globals.target.getComparatorsForPin(pin)
        local compLow = comparators.low
        local compHigh = comparators.high


        -- establish TRIPIN signal number
        -- 'Emit trip signal' aka group A, B, C
        local groupChar = U.comboAsChar(Target.Variables['AnTrip%dSignal' % {i}])
        self:registerTripSignalGroup(
          groupChar, 
          {sourceBlockPath = 'coderOptions', widgetName = 'AnTrip%dSignal' % {i}})
        local tripInSignal = globals.target.getTargetParameters().trip_groups[groupChar]

        if (threshold_low > 0) and (threshold_high < 3.3) then
          -- window comparator
          if (compLow == nil) or (compHigh == nil) then
            U.error('Pin %s is not suitable for setting up a window comparator for Analog Trip %d. %s' % {pin, i, errorMessageHint})
          end
          local compHighParams = {
            opt_pinMux = compHigh.mux,
            threshold = threshold_high,
            invertCompare = false,
          }
          local compLowParams = {
            opt_pinMux = compLow.mux,
            threshold = threshold_low,
            invertCompare = true,
          }
          if compHigh.unit == compLow.unit then
            -- window can be setup with a single CMPSS
            local comp_obj = self:makeBlock('cmpss')
            comp_obj:createImplicitCmpss(
              compHigh.unit,
              {
                analogPin = pin,
                tripSignalH = tripInSignal,
                opt_compHigh = compHighParams,
                opt_compLow = compLowParams,
              }, req, label)
          else
            -- must use two different CMPSS
            local comp_high = self:makeBlock('cmpss')
            comp_high:createImplicitCmpss(
              compHigh.unit,
              {
                analogPin = pin,
                tripSignalH = tripInSignal,
                opt_compHigh = compHighParams,
              }, req, label)

            local comp_low = self:makeBlock('cmpss')
            comp_low:createImplicitCmpss(
              compLow.unit,
              {
                analogPin = pin,
                tripSignalH = tripInSignal,
                opt_compLow = compLowParams,
              }, req, label)
          end
        else
          -- single-ended
          local threshold, invert
          if (threshold_low > 0) then
            invert = true
            threshold = threshold_low
          else -- threshold_high < 3.3
            invert = false
            threshold = threshold_high
          end

          local unit, opt_high, opt_low
          if compHigh then
            unit =  compHigh.unit
            opt_high = {
              opt_pinMux = compHigh.mux,
              threshold = threshold,
              invertCompare = invert,
            }
          else
            unit = compLow.unit
            opt_low = {
              opt_pinMux = compLow.mux,
              threshold = threshold,
              invertCompare = invert,
            }
          end

          local compInstance = self:makeBlock('cmpss')
          compInstance:createImplicitCmpss(
            unit,
            {
              analogPin = pin,
              tripSignalH = tripInSignal,
              opt_compHigh = opt_high,
              opt_compLow = opt_low,
            },
            req, label
          )
        end
      end
      i = i + 1
    end

    if self:targetUsesDriverLib() then
      for _, tz in pairs(static.tripZonesConfiguredList) do
        req:add('XBAR_INPUT', tz.zone, 'Trip zones')
        globals.syscfg:addPeripheralEntry('input_xbar', {
          gpio = tz.gpio,
          input = tz.zone,
          core = globals.target.getTiCpuNumber(self.cpu)
        })
        globals.syscfg:addPeripheralEntry('gpio', {
          unit = tz.gpio,
          cbx_direction = U.newComboBox('INPUT',
                                        U.ENUM.TI.DIO_TYPE),
          cbx_pullType = U.newComboBox('TRISTATE',
                                       U.ENUM.TI.PULL_TYPES),
          core = globals.target.getTiCpuNumber(self.cpu)
        })
      end
    end
  end

  function TripZones:p_getDirectFeedthroughCode()
    U.error('Explicit use of TripZones via target block not supported.')
  end

  function TripZones:isTripZoneConfigured(zone)
    for _, tz in pairs(static.tripZonesConfiguredList) do
      if tz.zone == zone then
        return true
      end
    end
    return false
  end

  function TripZones:isTripSignalGroupConfigured(group)
    assert(U.isChar(group))
    return static.tripSignalGroupsConfiguredMap[group] ~= nil
  end

  function TripZones:registerTripSignalGroup(group, params)
    assert(U.isChar(group))
    U.enforceParamContract(
      params,
      {
        sourceBlockPath = U.isString,
        widgetName = U.isString,
      }
    )
    static.tripSignalGroupsConfiguredMap[group] = params
  end

  function TripZones:isAnyTripZoneOrGroupConfigured()
    return (next(static.tripZonesConfiguredList) ~= nil)
       or (next(static.tripSignalGroupsConfiguredMap) ~= nil)
  end

  function TripZones:finalize(c)
    if static.finalized then
      return
    end

    -- Check that a Powerstage Block has been configured with matching tripzones and groups
    if self:isAnyTripZoneOrGroupConfigured() then
      local powerstageObj = self:getBlockInstance('powerstage')
      if not powerstageObj then
        U.error(
          'If trip zones are configured in Coder Options -> Target -> Protections, the corresponding response must be configured in a Powerstage Protection block to enable these safety features.')
      end
      -- Verify all configured Trip Zones are also enabled in PS block
      for _, tz in ipairs(static.tripZonesConfiguredList) do
        local cbx_tzMode = powerstageObj:getTripZoneModeCombo(tz.zone)
        if cbx_tzMode.equals('ignore') then 
          U.warning(
            'When @param:Tz%(zone)dEnable:2: is selected in Coder Options -> Target -> Protections, the @param:TripZone%(zone)dMode:3: must also be configured in the powerstage protection block.'
            % {zone = tz.zone}, {powerstageObj:getPath()})
        end
      end

      -- Verify all configured Groups are also enabled in PS block
      for groupChar, data in pairs(static.tripSignalGroupsConfiguredMap) do
        local cbx_signalModes = powerstageObj:getTripSignalGroupModeComboMap()
        if cbx_signalModes[groupChar].equals('ignore') then
          if data.sourceBlockPath == 'coderOptions' then
            U.warning(
              "When @param:%(paramName)s: is set to '%(groupChar)s' in Coder Options -> Target -> Protections, the @param:TripSig%(groupChar)sMode:3: must also be configured in the powerstage protection block."
              % {paramName = data.widgetName, groupChar = groupChar},
              {powerstageObj:getPath()})
          else
            -- An SDFM or other is Emitting this signal:
            U.warning(
              "Error in @4. When @param:%(paramName)s:4: is set to '%(groupChar)s', the @param:TripSig%(groupChar)sMode:3: must also be configured in the powerstage protection block."
              % {paramName = data.widgetName, groupChar = groupChar},
              {powerstageObj:getPath(), data.sourceBlockPath})
          end
        end
      end
    end

    if self:targetUsesPinsets() then
      for _, tz in pairs(static.tripZonesConfiguredList) do
        c.PreInitCode:append('PLX_PWR_configureTZGpio(%d, %d);' %
          {tz.zone, tz.gpio})
      end
    end

    static.finalized = true
  end

  return TripZones
end

return Module
