--[[
  Copyright (c) 2021 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 Module = {}
local U = require('common.utils')

local static = {}

function Module.getBlock(globals, cpu)

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

  function TaskTrigger:setImplicitTriggerSource(bid)
    self['trig_base_task_exp'] = "{modtrig = {bid = %i}}" % {bid}
  end

  function TaskTrigger:p_getDirectFeedthroughCode()
    error([[
      Direct feedthrough not supported by this block. 
      Double check that you have an unbroken library link, 
      this block has been updated.]])
  end

  function TaskTrigger:p_getNonDirectFeedthroughCode()
    -- there can only be one model trigger
    if static[cpu].numInstances ~= 1 then
      U.error("The use of multiple Control Task Trigger blocks is not allowed.")
    end

    -- verify proper connection of trigger port
    local trig = Block.InputSignal[1]
    trig = trig:gsub("%s+", "") -- remove whitespace
    if trig:sub(1, #"{modtrig") ~= "{modtrig" then
      U.error("This block must be connected to a 'Task trigger' port.")
    end

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

    if triggerBlock:getCpu() ~= self.cpu then
      U.error("Task trigger block cannot be triggered from a block on a different CPU.")
    end
    self['trigExpression'] = trig

    return {}
  end

  function TaskTrigger:setSinkForTriggerSource(sink)
    -- top of chain
    if self['trigExpression'] ~= nil then
      local trig = eval(self['trigExpression'])['modtrig']
      if trig['bid'] ~= nil then
        globals.instances[trig['bid']]:setSinkForTriggerSource({
          type = 'modtrig',
          bid = self.bid
        })
      end
    end
  end

  function TaskTrigger:propagateTriggerSampleTime(ts)
    if ts ~= nil then
      self['ts'] = ts
      self:logLine('Task trigger sample time for %s (%i) propagated to: %f.' %
                       {self.blockType, self.bid, ts})
    end
  end

  function TaskTrigger:getTriggerSampleTime()
    return self['ts']
  end

  function TaskTrigger:finalize(f)

    if static[self.cpu].finalized then
      return
    end

    local powerstage_obj = self:getBlockInstance('powerstage')

    -- take sample rate of each base task (CpuTaskId is 0 for the base task)
    local CpuSampleTime

    local taskFunction
    local tasksPerCpu = {}
    for _, tsk in ipairs(Model.Tasks) do
      if tsk.Cpu == self.cpu then
        table.insert(tasksPerCpu, tsk)
        if tsk.CpuTaskId == 0 then
          CpuSampleTime = tsk.SampleTime[1]
        end
      end
    end

    local modelClkHz = 1 / CpuSampleTime

    -- note: tolerances are verified in Coder.lua
    local achievableModelClkHz = modelClkHz
    local achievableModelPeriodInTimerTicks =
        math.floor(globals.target.getTimerClock() * CpuSampleTime + 0.5)


    local multiCpuPostFix = ''
    if Target.Variables.NUM_CPUS > 1 then
      multiCpuPostFix = '_CPU%d' % {cpu}
    end

    f.Declarations:append([[
      void PIL_setErrorMessage(PIL_Handle_t aPilHandle, const char* aMessage);
      extern const char * %(basename)s%(multiCpuPostFix)s_errorStatus;
      ]] % {
      basename = Target.Variables.BASE_NAME,
      multiCpuPostFix = multiCpuPostFix,
    })

    local maxNumTasks = 16
    if self:targetMatches({'29H85x'}) then
      maxNumTasks = 8
    end

    if #tasksPerCpu == 1 then
      taskFunction = [[
          static void Tasks(bool aInit, void * const aParam)
          {
            if(%(basename)s%(multiCpuPostFix)s_errorStatus){
              PIL_setErrorMessage(PilHandle, %(basename)s%(multiCpuPostFix)s_errorStatus);
              return;
            }
            if(aInit){
              %(basename)s_enableTasksInterrupt();
            } else {
              %(basename)s%(multiCpuPostFix)s_step();
            }
          }
          ]]
    elseif #tasksPerCpu > maxNumTasks then
      U.error("Maximal allowable number of tasks (%i) exceeded." % {maxNumTasks})
    else
      taskFunction = [[
          static void Tasks(bool aInit, void * const aParam)
          {
            if(%(basename)s%(multiCpuPostFix)s_errorStatus){
              PIL_setErrorMessage(PilHandle, %(basename)s%(multiCpuPostFix)s_errorStatus);
              return;
            }
            if(aInit){
              %(basename)s_enableTasksInterrupt();
            } else {
              %(basename)s%(multiCpuPostFix)s_step(*(int *)aParam);
            }
          }
          ]]
    end

    if powerstage_obj then
      f.Declarations:append([[
        static void PanicCallback(){
          PLX_PWR_emergencyOff();
        }
      ]])
    end

    f.Declarations:append("extern PIL_Handle_t PilHandle;")
    f.Declarations:append('DISPR_TaskObj_t TaskObj[%i];' % {#tasksPerCpu})
    if #tasksPerCpu == 1 then
      f.Declarations:append('extern void %s%s_step();' %
                                {Target.Variables.BASE_NAME, multiCpuPostFix})
    else
      f.Declarations:append('extern void %s%s_step(int task_id);' %
                                {Target.Variables.BASE_NAME, multiCpuPostFix})
    end
    f.Declarations:append('extern void %s_enableTasksInterrupt();' %
                              {Target.Variables.BASE_NAME})
    f.Declarations:append('extern void %s_syncTimers();' %
                              {Target.Variables.BASE_NAME})

    f.Declarations:append("%s\n" %
                              {
          taskFunction %
              {
                basename = Target.Variables.BASE_NAME,
                multiCpuPostFix = multiCpuPostFix,
              }
        })

    f.PreInitCode:append('DISPR_sinit();')
    f.PreInitCode:append(
        'DISPR_configure((uint32_t)(%i), PilHandle, &TaskObj[0], sizeof(TaskObj)/sizeof(DISPR_TaskObj_t));' %
            {achievableModelPeriodInTimerTicks})
    f.PreInitCode:append('DISPR_registerIdleTask(&%s_background);' %
                             {Target.Variables.BASE_NAME})
    f.PreInitCode:append('DISPR_registerHasBaseTaskOverrunOccurredCallback(&%s_hasBaseTaskOverrunOccurred);' %
                             {Target.Variables.BASE_NAME})
    f.PreInitCode:append('DISPR_registerSyncCallback(&%s_syncTimers);' %
                             {Target.Variables.BASE_NAME})
    f.PreInitCode:append('DISPR_setPowerupDelay(%i);' %
                             {math.floor(0.001 * achievableModelClkHz + 0.5)})

    if powerstage_obj then
      f.PreInitCode:append('DISPR_registerPanicCallback(&PanicCallback);')
    end   

    local numTasks = 0
    for idx = 1, #tasksPerCpu do
      local tsk = tasksPerCpu[idx]
      local ts = tsk["SampleTime"]
      if ts[2] ~= 0 then
        U.error("Sample time offset not supported.")
      end

      local achievablePeriodInTimerTicks =
          achievableModelPeriodInTimerTicks *
              math.floor(achievableModelClkHz * ts[1] + 0.5)
      local dispatcherDiv = math.floor(achievablePeriodInTimerTicks /
                                           achievableModelPeriodInTimerTicks)
      if dispatcherDiv > 0xFFFF then
        U.error(
            'Period of Task "%s" too large with respect to base task period.' %
                {tsk["Name"]})
      end
      if (dispatcherDiv * achievableModelPeriodInTimerTicks) ~=
          achievablePeriodInTimerTicks then
        U.error('Task period calculation exception. Please report this error to the author of the Target Support Package.')
      end

      f.PreInitCode:append("{")
      f.PreInitCode:append("    static int taskId = %i;" % {numTasks})
      f.PreInitCode:append("    // Task %i at %e Hz" %
                               {
            numTasks,
            globals.target.getTimerClock() / achievablePeriodInTimerTicks
          });
      f.PreInitCode:append(
          "    DISPR_registerTask(%i, &Tasks, %iL, (void *)&taskId);" %
              {numTasks, achievablePeriodInTimerTicks});
      f.PreInitCode:append("}")
      numTasks = numTasks + 1
    end

    -- FIX ME: Reduce the number of times we 'eval' to get the bid
    local trigExp = self.trigExpression or self.trig_base_task_exp
    local sourceBlock = globals.instances[eval(trigExp).modtrig.bid]
    sourceBlock:generateTaskTriggerIsrCode(f)

    static[self.cpu].finalized = true
  end

  return TaskTrigger
end

return Module
