--[[
  Copyright (c) 2022, 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 = {}
local Module = {}

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

  if static[cpu].numInstances ~= 1 then
    U.error(
      'There should be only one (implicit) instance of the pil block.')
  end
  --[[
      The following functions must be implemented by the target-specific
      pil.lua files:
  --]]
  function Pil:externalModeSerialAsInt()
    error([[Each TSP must implement this function and return an integer
      representing the drop-down menu value for Serial communication]])
  end

  function Pil:getTargetSpecificCallbackCode(c)
    error([[
      Each TSP must implement this function to generate the target specific bits
      needed for the PilCallback().

      Must return a table with the following:
      - opt_cpuPostfix
      - enableActuationCode,
      - disableActuationCode,
      - stopTimersCode,
      - startTimersCode,

      This code is called from finalize(). Declarations,Includes can be added to
      the argument table 'c'.
    ]])
  end

  --[[
      Functions below are common to all targets.
  --]]

  function Pil:createImplicit(c)
    table.insert(static[self.cpu].instances, self.bid)
    self.readProbes = {}
    self.overrideProbes = {}
    self.calibrations = {}
    self.forceActuationOffFlags = {}

    if (Target.Variables.EXTERNAL_MODE ~= self:externalModeSerialAsInt()) then
      -- Target:LogMessage('error', 'PIL (@param:GENERATE_PIL_PROBES:) requires external mode communication over serial to be enabled.', {})
      U.error(
        'PIL requires external mode communication over serial to be enabled.')
    end

    if Target.Variables.NUM_CPUS > 1 then
      U.error(
        'PIL is not supported on a target configured for multi-CPU operation.')
    end

    c.Declarations:append(
      '%(baseName)sProbes_t %(baseName)s_probes;' % {
        baseName = Target.Variables.BASE_NAME
      })
  end

  --[[
      pName = Block.UniqueIdentifier

      Pil Read Probe:
      Name: pName_r1_1, to read the output [1][1]
      Data: {type = 'uint16'} -- Plecs Data type

      Pil Override:
      Name: pName_o1_i
      Data: {type = 'float'}

      inputSignal = PLXHAL_ADC_getIn(inst, i)
      OutputCode:append('SET_OPROBE(BASE_NAME_probes.pName, InputSignal)')
      OutputSignal:append(BASE_NAME_probes.pName)
  --]]

  function elevateType(params)
    -- PIL framework does not support 8-bit datatypes (and neither does the C2000 platform)
    if (params.type == 'bool') or (params.type == 'uint8_t') then
      params.type = 'uint16_t'
    elseif params.type == 'int8_t' then
      params.type = 'int16_t'
    end
    return params
  end

  function Pil:registerReadProbe(name, params)
    self.readProbes[name] = elevateType(params)
  end

  function Pil:registerOverrideProbe(name, params)
    self.overrideProbes[name] = elevateType(params)
  end

  function Pil:registerCalibration(name, params)
    self.calibrations[name] = elevateType(params)
  end

  function Pil:registerForceActuationOffFlag(flag)
    self.forceActuationOffFlags[flag] = true
  end

  function Pil:checkMaskParameters()
    U.error('Explicit use of PIL via target block not supported.')
  end

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

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

    local pilHeaderDeclarations = {}
    local initReadProbesCode = {}
    local declarations = {}
    local preInitCode = {}

    local baseName = Target.Variables.BASE_NAME

    for name, params in U.pairsSorted(self.readProbes) do
      table.insert(pilHeaderDeclarations, '%s %s;' % {params.type, name})
      table.insert(declarations,
                   'PIL_SYMBOL_DEF(%s_probes_%s, 0, 1.0, "");' %
                   {baseName, name})
      table.insert(initReadProbesCode,
                   '%s_probes.%s = 0;' % {baseName, name})
    end

    for name, params in U.pairsSorted(self.overrideProbes) do
      table.insert(pilHeaderDeclarations, [[
        %(type)s %(name)s;
        %(type)s %(name)s_probeV;
        int16_t %(name)s_probeF;
      ]] % {name = name, type = params.type})

      table.insert(declarations,
                   'PIL_SYMBOL_DEF(%s_probes_%s, 0, 1.0, "");' %
                   {baseName, name})

      table.insert(preInitCode,
                   'INIT_OPROBE(%s_probes.%s);' %
                   {baseName, name})
    end

    for name, params in U.pairsSorted(self.calibrations) do
      table.insert(pilHeaderDeclarations, '%s %s;' % {params.type, name})
      table.insert(declarations,
                   'PIL_SYMBOL_CAL_DEF(%s_probes_%s, 0, 1.0, "", %f, %f, %f);' %
                   {
                     baseName, name, params.min, params.max,
                     params.val
                   })
      table.insert(preInitCode, '%s_probes.%s = %f;' %
        {baseName, name, params.val})
    end

    c.PilHeaderDeclarations:append([[
      // PIL Probes
      typedef struct {
        %(pilHeaderDeclarations)s
      } %(baseName)sProbes_t;
      ]] % {
      pilHeaderDeclarations = table.concat(pilHeaderDeclarations, '\n'),
      baseName = baseName
    })

    local ts = self:getTargetSpecificCallbackCode(c)
    U.enforceParamContract(
      ts,
      {
        opt_cpuPostfix = U.isString,
        enableActuationCode = U.isString,
        disableActuationCode = U.isString,
        stopTimersCode = U.isString,
        startTimersCode = U.isString,
      })

    c.Declarations:append([[
      %(declarations)s

      extern %(baseName)sProbes_t %(baseName)s_probes;

      void PilCallback(PIL_Handle_t aPilHandle, PIL_CtrlCallbackReq_t aCallbackReq) {
        switch(aCallbackReq) {
          case  PIL_CLBK_ENTER_NORMAL_OPERATION_REQ:
            // allow power
            PIL_inhibitPilSimulation(aPilHandle);
            %(enableActuationCode)s
            return;

          case PIL_CLBK_LEAVE_NORMAL_OPERATION_REQ:
            // disable power
            %(disableActuationCode)s
            PIL_allowPilSimulation(aPilHandle);
            return;

          case PIL_CLBK_PREINIT_SIMULATION:
            return;

          case PIL_CLBK_INITIALIZE_SIMULATION:
			      %(initReadProbesCode)s
            %(baseName)s%(cpuPostfix)s_initialize();
            return;

          case PIL_CLBK_TERMINATE_SIMULATION:
            return;

          case PIL_CLBK_STOP_TIMERS:
            // stopping relevant timers
            %(stopTimersCode)s
            return;

          case PIL_CLBK_START_TIMERS:
            // starting relevant timers
            %(startTimersCode)s
            return;
        }
      }
    ]] % {
      declarations = table.concat(declarations, '\n'),
      baseName = baseName,
      initReadProbesCode = table.concat(initReadProbesCode, '\n'),
      cpuPostfix = ts.opt_cpuPostfix or '',
      enableActuationCode = ts.enableActuationCode,
      disableActuationCode = ts.disableActuationCode,
      stopTimersCode = ts.stopTimersCode,
      startTimersCode = ts.startTimersCode,
    })

    c.PreInitCode:append([[
      {
        %(preInitCode)s
      }
      PIL_setCtrlCallback(PilHandle, (PIL_CtrlCallbackPtr_t)PilCallback);
      PIL_requestNormalMode(PilHandle);
    ]] % {preInitCode = table.concat(preInitCode, '\n')})


    static[self.cpu].finalized = true
  end

  return Pil
end

return Module
