--[[
  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 U = require('common.utils')

local CAN_CONFIG = {}

function CAN_CONFIG.determineCanBitTiming(args)
  U.enforceParamContract(
    args,
    {
      clk = U.isPositiveIntScalar,
      baud = U.isPositiveIntScalar,
      baudParamName = U.isString,
      samplePoint = U.isNonNegativeScalar,
      tseg1Range = {U.isFixedLengthArrayOf, 2, U.isPositiveIntScalar},
      tseg2Range = {U.isFixedLengthArrayOf, 2, U.isPositiveIntScalar},
      sjwRange = {U.isFixedLengthArrayOf, 2, U.isPositiveIntScalar},
      brpMax = U.isPositiveIntScalar,
      -- advanced configuration
      opt_bitLengthTq = U.isNonNegativeIntScalar,
      opt_sjw = U.isPositiveIntScalar,
    }
  )
  
  local bitInTqMin, bitInTqMax
  local errorPrefix = 'The desired @param:%s: (%d) is not achievable: ' % {args.baudParamName, args.baud}
  local errorPostfix = ''
  if args.baud < 9600 then  
    -- This check is to catch entering 500 when you meant 500 kbits/sec.
    -- The threshold is arbitrary, 9600 was chosen a valid baud rate.
    errorPostfix = ' The requested value is suspiciously low. Note the units must be bits per second.'
  end

  if args.opt_bitLengthTq then
    -- User as specified an Advanced setting
    bitInTqMin = args.opt_bitLengthTq
    bitInTqMax = args.opt_bitLengthTq
   
    if not U.isInteger(args.clk / args.baud / bitInTqMax) then
      -- TODO: Add links to these variables?
      errorPostfix = errorPostfix..' The specified system clock / baud rate / bit length time quanta is not an integer. Revisit the Advanced settings or disable Advanced.'
    end
  else
    local tseg1R = {
      (args.samplePoint * (1 + args.tseg2Range[1]) - 1) / (1 - args.samplePoint),
      (args.samplePoint * (1 + args.tseg2Range[2]) - 1) / (1 - args.samplePoint),
    }

    tseg1R = {
      math.max(math.ceil(tseg1R[1]), args.tseg1Range[1]),
      math.min(math.floor(tseg1R[2]), args.tseg1Range[2])
    }

    local bitInTq1 = {
      1 + tseg1R[1] + args.tseg2Range[1],
      1 + tseg1R[2] + args.tseg2Range[2],
    }

    local tseg2R = {
      (1 - args.samplePoint) * (args.tseg1Range[1] + 1) / args.samplePoint,
      (1 - args.samplePoint) * (args.tseg1Range[2] + 1) / args.samplePoint,
    }

    tseg2R = {
      math.max(math.ceil(tseg2R[1]), args.tseg2Range[1]),
      math.min(math.floor(tseg2R[2]), args.tseg2Range[2])
    }

    local bitInTq2 = {
      1 + tseg2R[1] + args.tseg1Range[1],
      1 + tseg2R[2] + args.tseg1Range[2],
    }

    bitInTqMin = math.max(bitInTq1[1], bitInTq2[1])
    bitInTqMax = math.min(bitInTq1[2], bitInTq2[2])
  end

  -- limit divider range to valid number of TQs
  local maxDiv = math.floor(args.clk / args.baud / bitInTqMin)
  local minDiv = math.ceil(args.clk / args.baud / bitInTqMax)

  maxDiv = math.min(maxDiv, args.brpMax)

  if minDiv > maxDiv then
    U.error(errorPrefix..'Unable to find suitable baudrate divider.'..errorPostfix)
  end

  -- search for brp that provide exact bitrate matches
  local brpOptions = {}
  for div = minDiv, maxDiv do
    local fq = args.clk / args.baud / div
    if fq == math.floor(fq) then
      table.insert(brpOptions, div)
    end
  end
  if #brpOptions == 0 then
    U.error(errorPrefix..'Unable to find suitable baudrate divider.'..errorPostfix)
  end

  -- search for brp that provides best match for sample point
  local settings
  local minSamplePointAbsError
  for _, brp in ipairs(brpOptions) do
    local seg = math.floor(args.clk / brp / args.baud + 0.5)
    local tseg1 = math.floor(args.samplePoint * seg + 0.5) - 1
    if tseg1 < args.tseg1Range[1] then
      tseg1 = args.tseg1Range[1]
    elseif tseg1 > args.tseg1Range[2] then
      tseg1 = args.tseg1Range[2]
    end
    local tseg2 = seg - 1 - tseg1
    if (tseg2 >= args.tseg2Range[1]) and (tseg2 <= args.tseg2Range[2]) and (tseg1 >= tseg2) then
      local samplePoint = (1 + tseg1) / (1 + tseg1 + tseg2)
      local samplePointAbsError = math.abs(args.samplePoint - samplePoint)
      if not minSamplePointAbsError 
      or samplePointAbsError < minSamplePointAbsError then

        minSamplePointAbsError = samplePointAbsError
        settings = {
          brp = brp,
          tseg1 = tseg1,
          tseg2 = tseg2,
          samplePoint = (1 + tseg1) / (1 + tseg1 + tseg2),
          baud = args.clk / brp / (1 + tseg1 + tseg2),
        }
      end
    end
  end

  if not settings then
    U.error(errorPrefix..'Calculation of bit rate settings failed.'..errorPostfix)
  end

  -- minimal prop segment size = maximal sjw
  local sjwMax = math.min(settings.tseg1, settings.tseg2, args.sjwRange[2])

  local sjw = args.opt_sjw or sjwMax
  if (sjw < args.sjwRange[1]) or (sjw > sjwMax) then
    U.error(errorPrefix..'Sync jump width (SJW) out of range [%d, %d].'..errorPostfix % {args.sjwRange[1], sjwMax})
  end
  settings.sjw = sjw

  U.enforceParamContract( -- set a contract on the returned settings
    settings,
    {
      brp = U.isPositiveIntScalar,
      tseg1 = U.isPositiveIntScalar,
      tseg2 = U.isPositiveIntScalar,
      sjw = U.isPositiveIntScalar,
      baud = U.isPositiveIntScalar,
      samplePoint = U.isPositiveScalar,
    }
  )
  return settings
end

--[[
    This function expects the standard Block interface for selecting
    CAN Ids

    Arguments:
    - idIdx, the Block.InputSignal index for 'id'
    Globals:
    - Block.Mask.IdSource, comboBox {'parameter', 'external'}
    - Block.Mask.CanId, parameter for the 'CAN ID'
--]]
function CAN_CONFIG.getBlockCanId(idIdx)
  
  local canId
  local constCanId
  local msgSource

  local cbx_idSource = U.enforceMask_newComboBox(
    'IdSource', {'parameter', 'external'})

  if cbx_idSource.equals('external') then
    msgSource = "The 'id' port"
    constCanId = U.enforcePort('id', U.portIsConnected, idIdx)
       and U.enforcePort('id', U.portValueIsConstant, idIdx)
       and U.portValueGetConstant(idIdx)

  else
    -- mask parameter value is always a constant
    constCanId = Block.Mask.CanId
    msgSource = U.paramLink('CanId')
  end

  -- ensure we pass a number to math checks below
  constCanId = tonumber(constCanId)
  -- Need explicit integer check, because error message on hex range
  -- can't accept fractional numbers.
  canId = U.enforceIO({
                        sigName = msgSource,
                        checkFunctionKey = U.isNonNegativeIntScalar
                      }, constCanId)
     and U.enforceIO({
                       sigName = msgSource,
                       checkFunctionKey = 'isIntScalarInClosedIntervalHex',
                     }, constCanId, 0, 0x1FFF0000)
     and constCanId

  return canId
end

--[[
    This function expects the standard Block Interface for 
    determining the Frame Format, Extended or Regular for the CAN message.
    Will produce an error message if the canId doesn't match the selected
    FrameFormat.

    Arguments:
    - canId, assumed already checked for a valid number via getBlockCanId()
    Globals:
    - Block.Mask.FrameFormat, comboBox: 'auto', '11_bit', '29_bit'
    Returns:
    - isExtId, true for extended IDs
--]]
function CAN_CONFIG.isExtId(canId)

  local extId
  local cbx_frameFormat = U.enforceMask_newComboBox(
    'FrameFormat', {'auto', '11_bit', '29_bit'})


  if cbx_frameFormat.equals('29_bit') then
    -- any can Id can be extended format
    extId = true
  elseif cbx_frameFormat.equals('11_bit') then
    if canId > 0x7FF then
      -- User specified invalid setting
      U.error(
        [['%(ff)s' error: '%(canId)s' exceeds 11 bit base format.]]
        % {
          ff = U.paramRawLink('Frame format', 'FrameFormat'),
          canId = U.paramRawLink('CAN ID', 'CanId'),
        })
    end
    extId = false
  else  -- auto detect selected
    extId = canId > 0x7FF
  end

  return extId
end

--[[
    This function assumes the standard block interface to include 
      Block.Mask.FrameLength

    Will do the correct check and generate the right error based on if it's
    CanFD/MCAN or regular CAN.

    DLC is stored in 4 bits.
    The values 0-8 indicate 0-8 bytes like classic CAN. For FD CAN the values 
    9-15 are translated to a value between 12-64 which is the actual length of  
    the data field: 9→12   10→16   11→20   12→24   13→32   14→48   15→64.
--]]
function CAN_CONFIG.getBlockFrameLength(isCanFD)
  local frameLen = Block.Mask.FrameLength -- user specifies the actual message length
  if isCanFD then
    if U.isIntScalarInClosedInterval(frameLen, 0, 8)
    or U.isScalarInSet(frameLen, {12, 16, 20, 32, 48, 64}) then
      return frameLen
    else
      U.error([[
        For FD CAN %s must be in the range [0..8] or in the set {12, 16, 20, 32, 48, 64}.
      ]] % {U.paramLink('FrameLength', true)})
    end
  else
    return U.enforceMask(U.isIntScalarInClosedInterval, 'FrameLength', 0, 8)
  end
end

function CAN_CONFIG.calcDLC(msgWidth)
  -- Calcuation the data length code that corresponds to the
  -- provided message width
  if msgWidth <= 8 then
    return msgWidth
  elseif msgWidth == 12 then
    return 9
  elseif msgWidth == 16 then
    return 10
  elseif msgWidth == 20 then
    return 11
  elseif msgWidth == 24 then
    return 12
  elseif msgWidth == 32 then
    return 13
  elseif msgWidth == 48 then
    return 14
  elseif msgWidth == 64 then
    return 15
  else
    U.error('Invalid CAN message length.')
  end
end
return CAN_CONFIG
