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

local static = {}


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

  function Serial:createImplicit(params)
    U.enforceParamContract(
      params,
      {
        req = 'table',                      -- a module's resource requirement list
        rxGpio = U.isNonNegativeIntScalar,  -- GPIO pin for receiving data
        txGpio = U.isNonNegativeIntScalar,  -- GPIO pin for transmitting data
        baud = U.isNonNegativeIntScalar,    -- Baud rate for SCI comm protocol
        -- for modules without blocks, this string
        -- will be shown to users in the event that there is a resource
        -- conflict with aother module
        opt_userErrStr = U.isString,
      })

    table.insert(static[self.cpu].instances, self.bid)

    -- determine Baud rate
    local supportedRates = globals.target.getSupportedSciBaudRates()
    for _, rate in ipairs(supportedRates) do
      if params.baud == rate then
        self.baud = rate
        break
      end
    end

    if not self.baud then
      U.error(
        'Serial: Baud rate not supported. Supported Baud rates: %s'
        % {table.concat(supportedRates, ', ')}
      )
    end

    -- check existence of UART resources first
    -- UARTs are a recent edition, so they always use DriverLib
    if globals.target.getTargetParameters().uarts then
      local rxGpioStr, txGpioStr
      for u = 1, globals.target.getTargetParameters().uarts.num_units do
        local unitStr = string.char(64 + u)  -- converts to chars A, B, C, etc
        rxGpioStr = 'GPIO_%d_UART%s_RX' % {params.rxGpio, unitStr}
        txGpioStr = 'GPIO_%d_UART%s_TX' % {params.txGpio, unitStr}
        if  globals.target.validateAlternateFunction(rxGpioStr)
        and globals.target.validateAlternateFunction(txGpioStr) then
          self.useUart = true
          self.unitAsChar = unitStr  -- store valid UART unit as a string
          self.rxGpioStr = rxGpioStr
          self.txGpioStr = txGpioStr
          break
        end
      end
    end
    if not self.useUart then  -- try to set up SCI
      if self:targetUsesPinsets() then
        -- older targets require hard-coded pin-sets
        self.sciPinset =
           globals.target.getTargetParameters().scis.pin_sets[
           'GPIO%d_GPIO%d' % {params.rxGpio, params.txGpio,}]
        if not self.sciPinset then
          U.error('Invalid pair of [Rx, Tx] pins: [%(rx)d, %(tx)d]' %
            {params.rxGpio, params.txGpio})
        end
        self.useSci = true
        self.unitAsChar = string.char(65 + self.sciPinset)  -- converts to chars A or B
      elseif self:targetUsesDriverLib() then
        -- newer targets use DriverLib
        local rxGpioStr, txGpioStr
        for u = 1, globals.target.getTargetParameters().scis.num_units do
          local unitStr = string.char(64 + u)  -- converts to chars A, B, C, etc
          if self:targetMatches({'2837x'}) then
            rxGpioStr = 'GPIO_%i_SCIRXD%s' % {params.rxGpio, unitStr}
            txGpioStr = 'GPIO_%i_SCITXD%s' % {params.txGpio, unitStr}
          else
            rxGpioStr = 'GPIO_%i_SCI%s_RX' % {params.rxGpio, unitStr}
            txGpioStr = 'GPIO_%i_SCI%s_TX' % {params.txGpio, unitStr}
          end
          if  globals.target.validateAlternateFunction(rxGpioStr)
          and globals.target.validateAlternateFunction(txGpioStr) then
            self.useSci = true
            self.unitAsChar = unitStr  -- store valid SCI unit as a string
            self.rxGpioStr = rxGpioStr
            self.txGpioStr = txGpioStr
            break
          end
        end
      end
    end

    if self.unitAsChar then  -- did we find a valid SCI or UART unit?
      if self.useSci then
        local sciParams = {
          req = params.req,
          rxGpio = params.rxGpio,
          txGpio = params.txGpio,
          opt_rxGpioStr = self.rxGpioStr,
          opt_txGpioStr = self.txGpioStr,
          baud = params.baud,
          unitAsChar = self.unitAsChar,
          opt_userErrStr = params.opt_userErrStr,
        }
        if self.sciPinset then
          sciParams.opt_pinset = self.sciPinset
        end

        local sciObj = self:makeBlock('sci', self.cpu)
        sciObj:createImplicit(sciParams)
        self.sciInstance = sciObj:getObjIndex()
      elseif self.useUart then
        local uartParams = {
          req = params.req,
          rxGpio = params.rxGpio,
          txGpio = params.txGpio,
          rxGpioStr = self.rxGpioStr,
          txGpioStr = self.txGpioStr,
          baud = params.baud,
          unitAsChar = self.unitAsChar,
          opt_userErrStr = params.opt_userErrStr
        }

        local uartObj = self:makeBlock('uart', self.cpu)
        uartObj:createImplicit(uartParams)
        self.uartInstance = uartObj:getObjIndex()
      end
    else
      U.error([[
        No SCI or UART unit found with GPIO [%(rx)d, %(tx)d] [Rx, Tx]
        ]] % {
        rx = params.rxGpio,
        tx = params.txGpio,
      })
    end
  end

  function Serial:checkMaskParameters()
    U.enforceMask(U.isNonNegativeIntScalar, 'RxGpio')
    U.enforceMask(U.isNonNegativeIntScalar, 'TxGpio')
    U.enforceMask(U.isPositiveIntScalar, 'BaudRate')

    self.TxMode = U.enforceMask_newComboBox('TxMode',
                                            {
                                              'standard',
                                              'triggered',
                                            }
    )

    if self.TxMode.equals('triggered') then
      self.trigType = U.enforceMask_newComboBox('TriggerType',
                                                {
                                                  'risingEdge',
                                                  'fallingEdge',
                                                  'eitherEdge',
                                                  'signalHigh',
                                                  'signalLow',
                                                }
      )
    end
  end

  function Serial:p_getDirectFeedthroughCode()
    local Require = ResourceList:new()
    local OutputCode = U.CodeLines:new()

    local serialObjParams = {
      req = Require,
      rxGpio = Block.Mask.RxGpio,
      txGpio = Block.Mask.TxGpio,
      baud = Block.Mask.BaudRate,
    }
    -- claim resources and verify viability of pinout/baud rate
    self:createImplicit(serialObjParams)

    -- Get names of output ports directly from the Block object
    local outSignal = Block:OutputSignal()

    OutputCode:append([[
    {
      PLXHAL_Serial_handleBreak(%(aChannel)d);

      if(PLXHAL_Serial_rxIsReady(%(aChannel)d)) {
        %(outSigRecievedData)s = PLXHAL_Serial_getChar(%(aChannel)d);
        %(outSigRecievedValid)s = true;
      } else {
        %(outSigRecievedValid)s = false;
      }
    }
    ]] % {
      aChannel = self.instance,
      outSigRecievedData = outSignal[1][1],
      outSigRecievedValid = outSignal[2][1],
    })

    return {
      Require = Require,
      OutputCode = OutputCode,
      UserData = {bid = self:getId()}
    }
  end

  function Serial:p_getNonDirectFeedthroughCode()
    local UpdateCode = U.CodeLines:new()

    local trigStr = ''
    local trigMemStr = ''
    local trigMemUpdateStr = ''
    if self.TxMode.equals('triggered') then
      if self.trigType.equals('risingEdge')
      or self.trigType.equals('fallingEdge')
      or self.trigType.equals('eitherEdge') then
        trigMemStr = 'static bool trigMem = 0;'
        trigMemUpdateStr = 'trigMem = %(trigInput)s;'
           % {trigInput = Block.InputSignal[2][1]}
      end

      if self.trigType.equals('risingEdge') then
        trigStr = 'if (!trigMem && %(trigInput)s)'
      elseif self.trigType.equals('fallingEdge') then
        trigStr = 'if (trigMem && !%(trigInput)s)'
      elseif self.trigType.equals('eitherEdge') then
        trigStr = 'if (trigMem != %(trigInput)s)'
      elseif self.trigType.equals('signalHigh') then
        trigStr = 'if (%(trigInput)s)'
      elseif self.trigType.equals('signalLow') then
        trigStr = 'if (!%(trigInput)s)'
      else
        U.error('Unsupported transmit trigger type selected.')
      end

      -- Modify generated string with appropriate input variable
      trigStr = trigStr % {trigInput = Block.InputSignal[2][1]}
    end

    UpdateCode:append([[
    {
      %(trigMemStr)s
      %(trigStr)s {
        if (!PLXHAL_Serial_txIsBusy(%(handle)d)) {
          PLXHAL_Serial_putChar(%(handle)d, %(data_val)s);
        }
      }
      %(trigMemUpdateStr)s
    }
    ]] % {
      trigStr = trigStr,
      trigMemStr = trigMemStr,
      trigMemUpdateStr = trigMemUpdateStr,
      handle = self.instance,
      data_val = Block.InputSignal[1][1],
    })

    return {
      UpdateCode = UpdateCode,
    }
  end

  -- will be called on each individual Serial block
  function Serial:finalizeThis(c)
    if self.useUart then
      -- configuration using driverlib constants
      c.PostInitCode:append([[
          PLX_UART_setInterface(SerialHandles[%(instance)d], UartHandles[%(uartInstance)d]);
          {
            uint32_t baseAddr = %(baseAddr)s;
            PLX_Serial_config(SerialHandles[%(instance)d], &baseAddr, %(clock)d);
            PLX_Serial_setupPort(SerialHandles[%(instance)d], %(baud)d);
          }
        ]] % {
        instance = self.instance,
        uartInstance = self.uartInstance,
        baseAddr = 'UART%s_BASE' % {self.unitAsChar},  -- defined in uart.h driverlib header
        clock = Target.Variables.sysClkMHz * 1e6,      -- UART uses full system clock
        baud = self.baud,
        unitAsChar = self.unitAsChar,
      })
    else
      c.PostInitCode:append([[
        PLX_SCI_setInterface(SerialHandles[%(instance)d], SciHandles[%(sciInstance)d]);
      ]] % {
        instance = self.instance,
        sciInstance = self.sciInstance,
      })

      if self.sciPinset then
        -- legacy configuration
        c.PostInitCode:append(
          'PLX_Serial_configViaPinset(SerialHandles[%d], %d, %d);'
          % {
            self.instance,
            self.sciPinset,
            globals.target.getLowSpeedClock()
          })
      else
        -- configuration using driverlib constants
        c.PostInitCode:append([[
            {
              PLX_SCI_Unit_t unit = %(unitAsStr)s;
              PLX_Serial_config(SerialHandles[%(aChannel)d], &unit, %(clock)d);
            }
          ]] % {
          aChannel = self.instance,
          unitAsStr = 'PLX_SCI_SCI_%s' % {self.unitAsChar},
          clock = globals.target.getLowSpeedClock(),
        })
      end
      c.PostInitCode:append(
        'PLX_Serial_setupPort(SerialHandles[%d], %d);'
        % {
          self.instance,
          self.baud,
        })
    end
  end

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

    c.Include:append('plx_serial.h')
    c.Include:append('string.h')
    c.Declarations:append([[
      PLX_Serial_Obj_t SerialObjs[%(num_inst)d];
      PLX_Serial_Handle_t SerialHandles[%(num_inst)d];
      ]] % {
      num_inst = static[self.cpu].numInstances,
    })

    -- instantiate unique SerialHandle for each module, and
    -- use them to initialize a unique SerialObj
    c.PreInitCode:append([[
    {
      for(int i = 0; i < %(num_inst)d; i++){
        SerialHandles[i] =
          PLX_Serial_init(&SerialObjs[i], sizeof(SerialObjs[i]));
      }
    }
    ]] % {
      num_inst = static[self.cpu].numInstances
    })

    -- implementations for functions declared in plx_hal.h
    c.Declarations:append([[
      uint16_t PLXHAL_Serial_getChar(int16_t aChannel) {
        return PLX_Serial_getChar(SerialHandles[aChannel]);
      }

      void PLXHAL_Serial_putChar(int16_t aChannel, uint16_t data) {
        PLX_Serial_putChar(SerialHandles[aChannel], data);
      }

      void PLXHAL_Serial_putStr(int16_t aChannel, uint16_t *str) {
        for (int i = 0; i < strlen((char *)str); i++) {
          PLXHAL_Serial_putChar(aChannel, str[i]);
        }
      }

      bool PLXHAL_Serial_rxIsReady(int16_t aChannel) {
        return PLX_Serial_rxReady(SerialHandles[aChannel]);
      }

      bool PLXHAL_Serial_txIsBusy(int16_t aChannel) {
        return PLX_Serial_txIsBusy(SerialHandles[aChannel]);
      }

      void PLXHAL_Serial_handleBreak(int16_t aChannel) {
        if(PLX_Serial_breakOccurred(SerialHandles[aChannel])) {
          PLX_Serial_reset(SerialHandles[aChannel]);
        }
      }
    ]])

    -- do configuration for each unique Serial block
    for _, bid in ipairs(static[self.cpu].instances) do
      local serialInst = globals.instances[bid]
      if serialInst:getCpu() == self.cpu then
        serialInst:finalizeThis(c)
      end
    end
    static[self.cpu].finalized = true
  end

  return Serial
end

return Module
