--[[
  Copyright (c) 2023 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 = {}

local e_serialPins = U.enum({
  'rxGpio',
  'txGpio',
})

local e_blockIns = U.enum({
  'data',
  'trig',
})

local strFormats = {
  bool = '%d',
  uint8_t = '%u',
  int8_t = '%d',
  uint16_t = '%u',
  int16_t = '%d',
  uint32_t = '%lu',
  int32_t = '%ld',
  float = '%f',
  double = '%lf',
}

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

  Csv.instance = static[cpu].numInstances
  static[cpu].numInstances = static[cpu].numInstances + 1

  function Csv:checkMaskParameters()

    self.cbx_txMode = U.enforceMask_newComboBox('TxMode', {'Standard', 'Triggered'})

    local pins = U.enforceMask(U.isPinPair, 'SciPins')
    self.rxGpio = pins[e_serialPins.rxGpio]
    self.txGpio = pins[e_serialPins.txGpio]

    self.baudRate = U.enforceMask(U.isPositiveIntScalar, 'BaudRate')

    local cbx_separator = U.enforceMask_newComboBox('Separator', {'Comma', 'Semicolon', 'Space', 'Tab'})

    if cbx_separator.equals('Comma') then
      self.separator = '","'
    elseif cbx_separator.equals('Semicolon') then
      self.separator = '";"'
    elseif cbx_separator.equals('Space') then
      self.separator = '" "'
    elseif cbx_separator.equals('Tab') then
      self.separator = '"\\t"'
    else
      error('CSV: No valid option for separator character found.')
    end

    local cbx_newline = U.enforceMask_newComboBox('NewlineChar', {'LF', 'CRLF'})

    if cbx_newline.equals('LF') then
      self.newline = '"\\n"'
    elseif cbx_newline.equals('CRLF') then
      self.newline = '"\\r\\n"'
    else
      error('CSV: No valid newline character found.')
    end

    self.timestampsEnabled = U.comboEnabled(Block.Mask.TimestampsMode)
  end

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

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

    self.serialObj = self:makeBlock('serial', self.cpu)
    local serialParams = {
      req = Require,
      rxGpio = self.rxGpio,
      txGpio = self.txGpio,
      baud = self.baudRate,
    }
    self.serialObj:createImplicitSerial(serialParams)

    self.inputList = Block.InputSignal
    self.inputTypes = Block.InputType
    
    -- Coder needs to know that csv block exists so it can allocate
    -- extra stack
    globals.syscfg:addPeripheralEntry('csv', {})

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

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

    local setupTrigFlagCode
    if self.cbx_txMode.equals('Triggered') then
      setupTrigFlagCode = [[
        static bool trigMem = 0;
        static bool trigFlag = 0;

        if (!trigMem && %(trigInput)s) {
          trigFlag = 1;
        }

        trigMem = %(trigInput)s;
      ]] % {
        trigInput = Block.InputSignal[e_blockIns.trig][1],
        serialHandle = self.serialObj.instance,
      }
    else
      setupTrigFlagCode = [[
        bool trigFlag = 1;
      ]]
    end

    local opt_timeStampDeclareAndIncrementCode = ''
    local opt_timeStampInsertCode = ''

    if self.timestampsEnabled then
      local timestampVar = 'plxCsvTimestamp%(num)d' % {num = self.instance}

      opt_timeStampDeclareAndIncrementCode = [[
        static uint32_t %(timestampVar)s = 0u;
        %(timestampVar)s++;
      ]] % {
        timestampVar = timestampVar,
      }

      opt_timeStampInsertCode = [[
        // timestamp always gets inserted at index zero if enabled
        PLXHAL_CSV_captureInput(%(handle)d, &%(timestampVar)s, 0, sizeof(%(timestampVar)s));
      ]] % {
        handle = self.instance,
        timestampVar = timestampVar,
      }
    end

    local listOfCaptureInputCodeChunks = {}
    for index, input in pairs(self.inputList[e_blockIns.data]) do
      table.insert(listOfCaptureInputCodeChunks, [[
        {
          static %(type)s tempInput;
          tempInput = %(input)s;
          PLXHAL_CSV_captureInput(%(handle)d, &tempInput, %(index)d, sizeof(tempInput));
        }
      ]] % {
        type = self.inputTypes[e_blockIns.data][index],
        input = input,
        index = self.timestampsEnabled and index or index - 1,
        handle = self.instance,
      })
    end

    UpdateCode:append([[
      {
        %(opt_timeStampDeclareAndIncrementCode)s
        %(setupTrigFlagCode)s
        if(trigFlag){
          %(opt_timeStampInsertCode)s
          %(captureInputCode)s
          trigFlag = false;
          PLXHAL_CSV_setDataFresh(%(handle)d);
        } 
      }]] % {
      handle = self.instance,
      opt_timeStampDeclareAndIncrementCode = opt_timeStampDeclareAndIncrementCode,
      opt_timeStampInsertCode = opt_timeStampInsertCode,
      captureInputCode = table.concat(listOfCaptureInputCodeChunks, '\n'),
      setupTrigFlagCode = setupTrigFlagCode,
    })

    return {
      UpdateCode = UpdateCode,
    }
  end

  function Csv:finalizeThis(c)
    local numInputs = #self.inputList[e_blockIns.data]
    if self.timestampsEnabled then
      numInputs = numInputs + 1
    end

    c.PreInitCode:append([[
      {
        static union PLX_CSV_Input hotBuffer[%(numInputs)d];
        static union PLX_CSV_Input coldBuffer[%(numInputs)d];
        static PLX_CSV_stringBuffer_t stringBuffers[%(numInputs)d];
        static PLX_CSV_sbHandle_t sbHandles[%(numInputs)d];

        for (int i = 0; i < %(numInputs)d; i++) {
          sbHandles[i] = &stringBuffers[i];
        }

        CsvHandles[%(handle)d] = PLX_CSV_init(
          &CsvObjs[%(handle)d], sizeof(CsvObjs[%(handle)d]),
          hotBuffer, coldBuffer, sbHandles);

        PLX_CSV_config(CsvHandles[%(handle)d], %(numInputs)d,
                                  %(separator)s, %(newline)s);
      }
    ]] % {
      handle = self.instance,
      numInputs = numInputs,
      separator = self.separator,
      newline = self.newline,
    })

    -- set formatter for timestamps if we're using them
    if self.timestampsEnabled then
      c.PreInitCode:append([[
        PLXHAL_CSV_configSignal(%(handle)d, 0, "%%lu", PLX_CSV_uint32_t);
      ]] % {
        handle = self.instance,
      })
    end

    -- set appropriate formatter for each input
    for index, inputStr in pairs(self.inputList[e_blockIns.data]) do
      local input_types = self.inputTypes[e_blockIns.data]

      local sigIndex
      if self.timestampsEnabled then
        sigIndex = index
      else
        sigIndex = index - 1
      end

      c.PreInitCode:append([[
        PLXHAL_CSV_configSignal(
          %(handle)d, %(index)d, "%(formatter)s", PLX_CSV_%(type)s
        );
      ]] % {
        handle = self.instance,
        index = sigIndex,
        inputName = inputStr,
        formatter = strFormats[input_types[index]],
        type = input_types[index],
      })
    end

    c.BackgroundTaskCodeBlocks:append([[
      PLXHAL_CSV_swapBuffers(%(handle)d);
      PLXHAL_CSV_convertToString(%(handle)d);
      PLXHAL_CSV_transmitOutput(%(csvHandle)d, %(serialHandle)d);
    ]] % {
      handle = self.instance,
      serialHandle = self.serialObj.instance,
      csvHandle = self.instance,
    })
  end

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

    c.Include:append('plx_csv.h')
    c.Include:append('plx_dio.h')

    c.Declarations:append([[
      PLX_CSV_Obj_t CsvObjs[%(num_inst)d];
      PLX_CSV_Handle_t CsvHandles[%(num_inst)d];
      
      void PLXHAL_CSV_captureInput(int16_t aChannel, void* input, uint16_t index, size_t size){
        PLX_CSV_captureInput(CsvHandles[aChannel], input, index, size);
      }

      void PLXHAL_CSV_setDataFresh(int16_t aChannel){
        PLX_CSV_setDataFresh(CsvHandles[aChannel]);
      }

      void PLXHAL_CSV_swapBuffers(int16_t aChannel){
        PLX_CSV_swapBuffers(CsvHandles[aChannel]);
      }

      void PLXHAL_CSV_convertToString(int16_t aChannel){
        PLX_CSV_convertToString(CsvHandles[aChannel]);
      }

      void PLXHAL_CSV_transmitOutput(int16_t csvChannel, int16_t serialChannel){
        PLX_CSV_transmitOutput(CsvHandles[csvChannel], serialChannel);
      }

      void PLXHAL_CSV_configSignal(int16_t aChannel, uint16_t inputNum, char* formatter, uint16_t type){
        PLX_CSV_configSignal(CsvHandles[aChannel], inputNum, formatter, type);
      }
    ]] % {
      num_inst = static[self.cpu].numInstances,
    })

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

    static[self.cpu].finalized = true
  end

  return Csv
end

return Module
