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

math.randomseed(os.time())
local random = math.random

--[[
     CodeLines is a substitution for StringList, which ensures a newline
     between entries and allows appending Lists into Lists, keeping the 
     result flat for PLECS.
--]]
U.CodeLines = {}
U.CodeLines.__index = U.CodeLines
function U.CodeLines:new()
  local o = {}
  setmetatable(o, U.CodeLines)
  return o
end

function U.CodeLines:append(s)
  if s == nil then
    -- existing StringList code appends nil and empty strings, 
    -- so we allow this with no error, but should we?
    U.devWarning('CodeLines allows nil input, but you could do better.')
    return
  end
  if type(s) == 'table' then
    for _, v in ipairs(s) do
      self:append(v)
    end
    return
  end
  if type(s) ~= 'string' then
    error('CodeLine to append is unexpected type: '..type(s)..' '..tostring(s))
  end
  -- Remove trailing white space and explicitly add back a newline.
  -- This will replace an empty string with a newline.
  s = s:gsub('[%s]+$', '')..'\n'
  table.insert(self, s)
end

function U.CodeLines:concat()
  return table.concat(self) -- No separator, because newlines are already added.
end

function U.tableToJson(tbl)
  local function serialize_value(value)
      local value_type = type(value)
      
      if value_type == "string" then
          value = value:gsub("\\", "\\\\")
          value = value:gsub('"', '\\"')
          value = value:gsub("\n", "\\n")
          value = value:gsub("\r", "\\r")
          value = value:gsub("\t", "\\t")
          return '"' .. value .. '"'
      elseif value_type == "number" then
          return tostring(value)
      elseif value_type == "boolean" then
          return tostring(value)
      elseif value_type == "table" then
          return U.tableToJson(value)
      elseif value == nil then
          return "null"
      else
          error("Unsupported type: " .. value_type)
      end
  end
  
  local function is_array(t)
      local count = 0
      for k, _ in pairs(t) do
          count = count + 1
          if type(k) ~= "number" or k ~= count then
              return false
          end
      end
      return true
  end
  
  if type(tbl) ~= "table" then
      return serialize_value(tbl)
  end
  
  if is_array(tbl) then
      local result = "["
      for i = 1, #tbl do
          if i > 1 then
              result = result .. ","
          end
          result = result .. serialize_value(tbl[i])
      end
      result = result .. "]"
      return result
  else
      local result = "{"
      local first = true
      for key, value in pairs(tbl) do
          if not first then
              result = result .. ","
          end
          first = false
          
          local key_str = type(key) == "string" and key or tostring(key)
          result = result .. serialize_value(key_str) .. ":" .. serialize_value(value)
      end
      result = result .. "}"
      return result
  end
end

local ModelInfo = {
  info = {},
  fileName = '',
}

function U.initModelInfoFile(dir)
  local logFileName = '%s/info.json' % {dir}
  U.setModelInfoFileName(logFileName)
end

function U.setModelInfoFileName(fileName)
  if (ModelInfo.fileName == '') then
    ModelInfo.fileName = fileName
    return
  end
  if (ModelInfo.fileName ~= fileName) then
    error('Invalid attempt to change the ModelInfo file name.')
  end
end

function U.addToModelInfo(id, info)
  ModelInfo.info[id] = info
end

function U.addBlockToModelInfo(block, info)
  if ModelInfo.info.blocks == nil then
    ModelInfo.info.blocks = {}
  end
  ModelInfo.info.blocks[block:getId()] = {
    type = block:getType(),
    info = info,
  }
end

function U.writeModelInfo()
  if (ModelInfo.fileName == '') then
    error('ModelInfo file name not set.')
  end
  local infoString = U.tableToJson(ModelInfo.info)
  local file, err = io.open(ModelInfo.fileName, "w")
  if not file then
      error("Failed to open file '" .. ModelInfo.fileName .. "': " .. (err or "unknown error"))
  end
  local success, write_err = file:write(infoString)
  if not success then
      file:close()
      error("Failed to write to file '" .. ModelInfo.fileName .. "': " .. (write_err or "unknown error"))
  end
  file:close()
end

local Log = {
  text = U.CodeLines:new(),
  fileName = '',
  previouslyBeenDumped = false,
  isEmpty = true,
}

function U.InitLogFile(installDir)
  -- establish log file name
  local logFileName = '%s/%s_log.txt' % {installDir, Target.Variables.BASE_NAME}
  U.setLogFileName(logFileName)  -- store this in utils, along with the LogText.
end

function U.setLogFileName(logFileName)
  if (Log.fileName == '') then
    Log.fileName = logFileName  -- FIX ME: Could add checks that this is a valid filename?
    return
  end

  if (Log.fileName == logFileName) then
    U.log('Setting logfile name redundantly.')
  else
    error('Attempt to change the Log FileName.')
  end
end

function U.log(line)
  Log.text:append(line)
  Log.isEmpty = false
end

-- Should be called by any code that can log errors
-- before returning to Plecs.
function U.dumpLog()
  if (Log.fileName == '') then
    U.log('Warning: FileName must be set prior to dumping the log.')
    return  -- hope someone tries again after setting the fileName...
  end

  if Log.isEmpty then
    return
  end

  local openFlag = Log.previouslyBeenDumped and 'a' or 'w'

  local file, e = io.open(Log.fileName, openFlag)
  if file == nil then
    error(e)
  end
  io.output(file)
  for _, v in ipairs(Log.text) do
    io.write(v);
  end
  file:close()

  Log.previouslyBeenDumped = true
  Log.text = U.CodeLines:new()
  Log.isEmpty = true
end

--[==[
    To create links in error and warning messages to Target.Variables or
    other blocks, use the following syntax:

    -- LINK TO THE CURRENT BLOCK:
    U.error([[
        The perferred way to link to a parameter in the current Block is
        using U.paramLink(): %(foobarLink)s.

        However, the raw syntax to generate a link to the Foobar parameter
        in the current block is:
          - @param:Foobar: or
          - @param:Foobar:1:
        ]] % {foobarLink = U.paramLink('Foobar', true)})

    -- LINK TO CODER OPTIONS:
    --   We reserve index '2' for Coder Options.
    --   Here we link to SysClkMHz widget in Coder Options
    U.error('Problem with @param:SysClkMHz:2:.')

    -- LINK TO AN ARBITRARY BLOCK:
    U.warning([[
        Configuration clash detected with:
          The parameter @param:Foobar:3: in block @3 and
          the parameter @param:Barbaz:4: in block @4.

        We can even link to the current block @5 this way: @param:Foo:5:.

        Still '2' always references Coder Options: @param:SysClkMHz:2:
        ]], {block3:getPath(), block4:getPath(), self:getPath()})

    Explanation:
    For simplicity, we make the 'rootPath', always the first argument @2,
    so any reference to a Target.Variable will be: @param:CoderOptionsVarName:2:.
    Any other blocks that we would like to link to will be provided as
    explict arguments to U.error() and U.warning() and they will be indexed so
    the first one is @3 and the second @4, etc.

    Note since the rootPath is not available to code executed from Target/Coder.
    We will just implement a hacky string substitution to make @2 --> @1 when
    we call Target:LogMessage().
--]==]
function getRootFromBlockPath() 
  if Block then
    local idx = string.find(Block.Path, '[^\\]/')
    return idx and string.sub(Block.Path, 1, idx) or Block.Path
  end
  error('This function can only be called if Block exists.')
end

-- Used to prepend the rootPath to the optional args for U.error() and U.warning()
function prependRootPathToArgs(opt_args)
  args = opt_args or {}

  table.insert(args, 1, getRootFromBlockPath())
  return args
end
--[[
    The functions:
      prependValidDummyPathToArgs() and 
      morphErrorStringForTarget()
    are a hack required because the rootPath is not available
    from the Target structure. Oops... 
--]]
function prependValidDummyPathToArgs(opt_args)
  local args = opt_args or {}
  if opt_args and opt_args[1] then
    -- prepend with a valid arg from the provided list
    table.insert(args, 1, opt_args[1]) 
  end
  return args
end

function morphErrorStringForTarget(str)
  --[[ 
    Callers of U.error() and U.warning() will reference CoderOptions variables
    as @2, however, since we cannot access the rootPath from Target, we 
    implement a hack here when calling Target:LogMessage() to replace:
       @2 --> @1
       :2: --> :1:  for @param:CoderOptionsVar:2:
    since for Target:LogMessage() CoderOptions is @1.
  --]]
  str = string.gsub(str, '@2', '@1') -- Note this will fail if we have a message with @20-@29, but that is so unlikely
  str = string.gsub(str, ':2:', ':1:')
  return str
end

-- returns the message as a string as well as logging it.
function U.logWithArgs(str, opt_args)
  local argsString = ''
  if opt_args then
    for i, v in ipairs(opt_args) do
      argsString = argsString..'%d = %s\n' % {i + 2, v} -- shift arg index 1 --> 3, etc
    end
  end
  U.log(str..'\n'..argsString)
  return str..'\n'..argsString  -- return value to use for warnings from *special* code.
end

function U.warning(str, opt_args)
  
  local warning = U.logWithArgs('\nWARNING DETECTED:\n'..str, opt_args)

  if Block and Block.LogMessage then
    local args = prependRootPathToArgs(opt_args)
    Block:LogMessage('warning', str, args)
  elseif Target and Target.LogMessage then
    local args = prependValidDummyPathToArgs(opt_args)
    str = morphErrorStringForTarget(str)
    Target:LogMessage('warning', str, args)
  else
    print(warning) -- print to the console, not sure what else to do.
  end
end

function U.userIsDeveloper()
  return (Target.Version == 'dev')
end

-- Warning only presented to developers
function U.devWarning(str, opt_args)
  if U.userIsDeveloper() then
    U.warning('DEVELOPMENT ONLY WARNING: '..str, opt_args)
  end
end

local noTraceStr = 'NO_TRACE:'
-- prepend the NO_TRACE flag, which is checked higher up to switch the error to a
-- return string.
function U.error(str, opt_args)
  if (type(str) ~= 'string') then
    error('This function requires an error message.')
  end

  local backupErrMsg = U.logWithArgs('\nERROR DETECTED:\n'..str, opt_args)

  if Block and Block.LogMessage then
    local args = prependRootPathToArgs(opt_args)
    Block:LogMessage('error', str, args)

  elseif Target and Target.LogMessage then
    local args = prependValidDummyPathToArgs(opt_args)
    str = morphErrorStringForTarget(str)
    Target:LogMessage('error', str, args)
  else
    -- Control Task Trigger and *special* functions don't have LogMessage(), so fall
    -- through to just return a string via throwing error.
    print(backupErrMsg)
  end

  -- Plecs 4.9 ignores the string return value if an 'error' has been logged
  -- via Block or Target:LogMessage(), we retain this here to throw the error
  -- and to return strings for *special* code like OnConnectFunction.
  error(noTraceStr..str, 4)    -- report line number up the stack a bit
end

--[[
    This function processes the return error string from xpcall.
    It returns:
      -- 'nil' if there is no error (should not be called in this case)
      -- a string if we detect it was a user facing error message via U.error()
      -- re-throw the error() if it was an error for which we want a stack trace
--]]
function U.stripTraceOrRethrowError(str)
  if (type(str) ~= 'string') then
    return nil  -- it's not an error message at all
  end

  -- error(str) -- Uncomment this if you want the full stack trace for U.error (DEBUGGING ONLY)

  -- check if the error message includes the noTraceStr
  -- if it does, crop the stack trace info
  local i, j = string.find(str, noTraceStr)
  if j then
    -- NO_TRACE found, remove the extra at the beginning
    str = string.sub(str, j + 1, -1)
  else
    -- if NO_TRACE not found, log it and pass along the error
    U.log('\nEXCEPTION: \n'..str)  -- error() doesn't log, so do it now.
    U.dumpLog()
    -- note this wraps a second stack trace on the error (a minor annoyance)
    error('\n'..str)
  end
  i, j = string.find(str, 'stack traceback:')
  if i then
    -- remove the stack trace we don't care about
    str = string.sub(str, 1, i - 1)
  end

  U.dumpLog()
  return str
end

-- Generates a link that can be used in an error message for Blocks.
-- This works only for the current block.
function U.paramRawLink(maskVarName)
  
  if Block and Block.Mask[maskVarName] then
    return '@param:'..maskVarName..':'
  else
    U.devWarning('Could not find "Block.Mask.%s" to create a parameter link.' % {maskVarName})
    return tostring(maskVarName)
  end
end

function U.paramLink(maskVarName, useLowerCaseThe)
  if useLowerCaseThe then -- defaults to upperCase when this is nil
    return 'the parameter '..U.paramRawLink(maskVarName)
  else
    return 'The parameter '..U.paramRawLink(maskVarName)
  end
end

-- FILE MANIPULATION FUNCTIONS

--[[
    This function used on all path inputs (for example Target.Variables)
    to normalize them to use posix forward slashes, the convention within
    this codebase. We also want to ignore any leading and trailing whitespace,
    as well as outer matched quotes.
--]]
function U.normalizePath(pathStr)
  if not U.isString(pathStr) then -- If the path is nil, we should debug, it's probably a typo somewhere.
    error('argument pathStr must be a string and not be nil')
  end
  return U.stripMatchedQuotes(U.stripWhiteSpace(pathStr)):gsub('\\+', '/')
end

--[[
    When using paths on the commandline, the safest way to deal with 
    spaces or other special characters is to pass the path in quotes.
    Similar if you are passing any arguments that might include spaces, for
    example, a command to execute, use this function to wrap paths or commands
    that may have spaces when building a command or argument string to execute
    in a makefile.
--]]
function U.shellQuote(str)
  -- escape any quotes and add quotes around the path
  return string.format('%q', str)
end

function U.stripWhiteSpace(pathStr)
  return pathStr:match('^%s*(.-)%s*$') -- strip leading and trailing white space
end
--[[
    Use this function to ensure you have removed any extra quotes around
    a path.
--]]
function U.stripMatchedQuotes(pathStr)
  while true do
    local first = pathStr:sub(1, 1)
    local last = pathStr:sub(-1)
    if (first == '"' and last == '"') or (first == "'" and last == "'") then
      pathStr = pathStr:sub(2, -2)
    else
      break
    end
  end
  return pathStr
end

-- Lua pattern special characters that must be escaped in string.find
local patternSpecials = {
  ['^'] = true, ['$'] = true, ['('] = true, [')'] = true,
  ['%'] = true, ['.'] = true, ['['] = true, [']'] = true,
  ['*'] = true, ['+'] = true, ['-'] = true, ['?'] = true,
}

-- Escape function for a single character
local function escapeForPattern(char)
  return patternSpecials[char] and '%'..char or char
end

--[[
    Takes a string '#|;#'
    Returns a deduplicated list of characters: "'#', '|', ';'"
--]]
function U.formatCharListForErrorMessage(str)
  local map = {} -- deduplicate the entries, but makes order random...
  local list = {} -- character list

  for i = 1, #str do
    local c = string.sub(str, i, i)
    if c == "'" then
      map[c] = '"'..c..'"' -- print "'" for single quote?
    else
      map[c] = "'"..c.."'"
    end
  end
  for k, v in pairs(map) do
    table.insert(list, v)
  end
  return table.concat(list, ', ')
end

--[[
   TSP code opts to be restrictive and disallow characters that are problematic
   for any OS, MAKE tool or any of our other external tools.
   Note `:` is allowed on Windows for `C:` etc.
   FIX ME: How to note that '/' and '\' are not allowed in file names, but
   are allowed in the path (to mark directories)?
   Note: PLECS disallows dots in .plecs file name (dots in folders okay)
--]]
function U.getDisallowedPathChars()
  local BAD_PATH_CHARS = ''
     ..'$#'  -- Not handled well by makefile/shell, needs more escaping?
     ..'"`'  -- quotes/ticks
     .."'"
     ..':'  -- colon disallowed by all, with exception for 'C:' in Windows.
     ..';|' -- not handled well in plecs file paths (makefile issue?) (on linux)
     ..'%'  -- not handled well in gcc commands
  if Target.Variables.HOST_OS == 'win' then
    -- disallowed by windows
    BAD_PATH_CHARS = BAD_PATH_CHARS
       ..':*?"<>|'  -- ?? TODO: Double check these
       ..'~`'     -- Windows doesn't allow me to type these
  elseif Target.Variables.HOST_OS == 'mac' then
    BAD_PATH_CHARS = BAD_PATH_CHARS
       ..':'
  elseif Target.Variables.HOST_OS == 'linux' then
    BAD_PATH_CHARS = BAD_PATH_CHARS
       ..'`:'  -- had trouble with uniflash on Linux
  else
    U.devWarning('No OS specific BAD_PATH_CHARS added for unknown OS.')
  end
  return BAD_PATH_CHARS
end

function U.getDisallowedPathCharsForInstallDir()
  -- These characters are issues when copying files to the INSTALL_DIR
  return U.getDisallowedPathChars()
     ..'%' -- Not handled in makefile pattern rules, not possible to escape 
     ..'=' -- Problem in make 
end

--[[
    Splits off a windows path letter: 'C:/foobar' will return
    pathMinusDrive, drive
    '/foobar', 'C:'  on windows
    'C:/foobar', nil on any other OS
--]]
local function splitOnWindowsDriveLetter(fullPath)

  if Target.Variables.HOST_OS == 'win' then
    if string.match(fullPath, '^[A-Za-z]:') then
      return string.sub(fullPath, 3), string.sub(fullPath, 1, 2)
    end
  end
  return fullPath, nil
end
--[[
    return true if the path provided is a valid string representation of a path
    meaning it does not include any characters in the banned character string.
--]]
function U.isValidPathStr(fullPath, badCharString)
  if not (U.isString(fullPath) and U.isString(badCharString)) then
    error('This function requires string arguments')
  end

  local path, drive = splitOnWindowsDriveLetter(fullPath)

  for i = 1, #badCharString do
    local char = badCharString:sub(i, i)
    local pattern = escapeForPattern(char)

    if string.find(path, pattern) then
      return false -- found a disallowed character
    end
  end
  return true
end

-- Throw user error if the path string contains any forbidden characters
function U.assertValidPathStr(fullPath, opt_params) 

  if opt_params then
    U.enforceParamContract(opt_params, {
      opt_badCharString = U.isString,
      opt_errorMsgPrefix = U.isString,
      opt_errorMsgInstructions = U.isString,
    })
  end
  
  local badCharString = (opt_params and opt_params.opt_badCharString) or U.getDisallowedPathChars()
  local errorMsgPrefix = (opt_params and opt_params.opt_errorMsgPrefix) or ''
  local errorMsgInstructions = (opt_params and opt_params.opt_errorMsgInstructions) or ''
  local note = ''

  if not U.isValidPathStr(fullPath, badCharString) then
    -- Generate the path error message
    local path, dir = splitOnWindowsDriveLetter(fullPath)
    if dir then -- Windows drive letter found
      note = " (excepting the drive '%s')" % {dir}
    end

    local prefix = errorMsgPrefix and errorMsgPrefix..' path' or 'Path'
    U.error([[
      %(prefix)s '%(path)s'%(opt_note)s may not contain any of the following characters: ]] 
      % {
        prefix = prefix, 
        path = fullPath,
        opt_note = note,
      }..U.formatCharListForErrorMessage(badCharString)..' '..errorMsgInstructions)
  end
end

function U.fileExists(file)
  if Plecs then
    return Plecs:FileExists(file)
  else

    -- non Plecs implementation required for OnConnectFunction
    local f = io.open(file, 'r')
    if f ~= nil then
      io.close(f)
      return true
    else
      return false
    end
  end
end

function U.directoryExists(dir)
  if Plecs then
    return Plecs:DirectoryExists(dir)
  else
    U.devWarning([[
      U.directoryExists() cannot be used when `Plecs` is not defined.
      For example from the OnConnectFunction.lua. We will default to true in
      this case.
    ]])

    return true

    -- This solution seems not to work on Windows but not a Mac.
    -- Check if a file or directory exists in this path
    --   path = dir..'/'

    --   local ok, err, code = os.rename(path, path)
    --   if not ok then
    --     if code == 13 then
    --       -- Permission denied, but it exists
    --       return true
    --     end
    --   end
    --   return ok, err
  end
end

local function throwFileOrDirectoryNotFoundError(pathTypeStr, path, opt_details)
  
  if opt_details then
    U.enforceParamContract(
      opt_details,
      {
        opt_descriptor = U.isString,
        opt_instructions = U.isString,
      }
    )
  end

  local prefix = ''
  if opt_details and opt_details.opt_descriptor then
    prefix = opt_details.opt_descriptor..' not found:\n'
  end

  local instructions = opt_details and opt_details.opt_instructions or ''
  U.error([[
      %(prefix)s
      The %(dirOrFile)s '%(path)s' does not exist.
      %(instructions)s
    ]] % {
    prefix = prefix,
    dirOrFile = pathTypeStr,
    path = path,
    instructions = instructions,
  })
end

function U.assertFileExists(file, opt_details)
  if not U.fileExists(file) then
    throwFileOrDirectoryNotFoundError('file', file, opt_details)
  end
end

function U.assertDirectoryExists(dir, opt_details)
  if not U.directoryExists(dir) then
    throwFileOrDirectoryNotFoundError('directory', dir, opt_details)
  end
end

local function splitPath(path)
  return U.normalizePath(path):match('^(.*)/([^/]+)$')  -- returns dir, name
end
--[[
    We wish to reject 'empty' directory fields from Coder Options.
    Note that PLECS will return one level above 
    Target.Variables.BUILD_ROOT when a Coder Options directory is empty.
--]] 
function U.isNonEmptyCoderOptionsDirectory(rawDir)
  local dir = U.normalizePath(rawDir)
  local defaultDir, _ = splitPath(Target.Variables.BUILD_ROOT)

  -- if the dir matches the default, this means it was an empty field
  return not (dir == defaultDir..'/')
end

function U.enforceValidCoderOptionsDirectory(varName, opt_badChars)

  local badChars = opt_badChars or U.getDisallowedPathChars()
  -- Check not empty
  U.enforceCO(U.isNonEmptyCoderOptionsDirectory, varName)
  -- Validate characters used in path
  U.enforceCO(U.isValidPathStr, varName, badChars)
  -- Check dir exists
  return U.enforceCO(U.directoryExists, varName)
end

function U.escapePercentWithPercent(str)
  return string.gsub(str, '%%', '%%%%')
end

function U.copyTemplateFile(src, dest, subs)
  local file, e = io.open(src, 'rb')
  if file == nil then
    error(e)
  end
  local src_content = file:read('*all')
  io.close(file)
  local dest_content

  file = io.open(dest, 'rb')
  if (file == nil) then
    dest_content = nil
  else
    dest_content = file:read('*all')
    io.close(file)
    dest_content = string.gsub(dest_content, '\r', '')
  end

  if subs ~= nil then
    for k, v in pairs(subs) do
      local v_clean, _= U.escapePercentWithPercent(v)
      src_content = string.gsub(src_content, k, v_clean)
    end
  end

  src_content = string.gsub(src_content, '\r', '')

  if not (src_content == dest_content) then
    file, e = io.open(dest, 'w')
    if file == nil then
      error(e)
    end
    io.output(file)
    io.write(src_content)
    file:close()
  end
end

--[[
    Check if two values are deeply equal by comparing their values.
    This function recursively compares all key-value pairs in tables.
    
    Returns true if:
    - Both values are identical (same reference or primitive values)
    - Both are tables with the same keys and deeply equal values
    - Both are nil
    
    Returns false if:
    - Types don't match
    - Tables have different keys
    - Any corresponding values are not deeply equal
--]]
function U.deepEqual(t1, t2)
  -- Handle identical references and primitive values
  if t1 == t2 then
    return true
  end
  
  -- If either is not a table, they can't be functionally equal (already checked identity above)
  if type(t1) ~= 'table' or type(t2) ~= 'table' then
    return false
  end
  
  -- Check that both tables have the same keys and values
  for k, v1 in pairs(t1) do
    local v2 = t2[k]
    if not U.deepEqual(v1, v2) then
      return false
    end
  end
  
  -- Check that t2 doesn't have extra keys that t1 doesn't have
  for k, _ in pairs(t2) do
    if t1[k] == nil then
      return false
    end
  end
  
  return true
end

function U.isInSet(testVal, set)
  for _, t in pairs(set) do
    if U.deepEqual(testVal, t) then
      return true
    end
  end
  return false
end

--[[
    Check if the 'targetPrefix' matches one in the argument 'targets'.
    where the arg 'targets' is optionally a string for one target
    or a list of strings for one or more targets.

    This function is called via Block:targetMatches() and Target.targetMatches()
    which only require the first argument.
--]]
function U.targetMatches(targets, T)
  
  local targetPrefix = T.getFamilyPrefix()
  local possiblePrefixes = T.getAllPossibleFamilyPrefixes()

  if type(targets) == 'string' then
    targets = {targets}
  end

  if not U.isTableOf(targets, U.isString) then
    error('targetMatches must have a table of target options or a string argument')
  end

  -- test all targets being checked are in the possible prefixes
  for _, t in ipairs(targets) do
    if not U.isInSet(t, possiblePrefixes) then
      error("Unknown target in targetMatches check: '%s'. Check for typos or add the new target in T.getAllPossibleFamilyPrefixes()." % {t})
    end
  end
  
  return U.isInSet(targetPrefix, targets)

end

function U.throwUnhandledTargetError(opt_msg)
  local msg = opt_msg or 'Unhandled target.'
  if U.userIsDeveloper() then
    error(msg)
  else
    U.error('This configuration is not supported for this target, please report this error to Plexim. '..msg)
  end
end

--[[
    Iterator for a table, like pairs, but loops through the keys
    alphabetically, or per the optional arg 'f'.

    Useful to ensure that codegen order is consistent to maintain a 
    minimal diff in generated code.
--]]
function U.pairsSorted(t, f)
  local a = {} -- alphabetical by default
  for key, _ in pairs(t) do table.insert(a, key) end
  table.sort(a, f)
  local i = 0      -- iterator variable
  local iter = function ()   -- iterator function
    i = i + 1
    if a[i] == nil then return nil
    else return a[i], t[a[i]]
    end
  end
  return iter
end

-- Deduplicate entries in a list
function U.deduplicateListEntries(t)
  local found = {}
  local deduplicated_t = {}

  if not U.isArrayWithNoKeys(t) then
    error('Cannot deduplicate entries of a table with key data. Arrays (Lists) only please.')
  end
  -- assert t is a list not a dictionary
  for _, v in ipairs(t) do
    if not found[v] then
      table.insert(deduplicated_t, v)
      found[v] = v
    end
  end
  return deduplicated_t
end


--[[
     Use this method to break circular references in a table
     so then you can dump() it properly for debugging.

     Also can be used to break a contract on a table.
--]]
function U.copyTable(t_in)
  local t_out = {}
  for k, v in pairs(t_in) do
    if (type(v) == 'table') then
      t_out[k] = U.copyTable(t_in[k])
    else
      t_out[k] = v
    end
  end
  return t_out
end

local function protected_index(_, key)
  error('Locked table does not have key '..tostring(key)..'.')
end

local function protected_newindex(_, key, value)
  error('Locked table does not allow assignment.')
end

local contract_mt = { -- The contract_mt allows `opt_` names optionally, will not error on them
  __index = function (_, k)
    if not k:find('^opt_') then
      error('PARAMETER CONTRACT ERROR: Attempt to access an undefined key: '..tostring(k))
    end
  end,
  __newindex = function (_, k, v)
    error('PARAMETER CONTRACT ERROR: No new data may be added to this table. key: '..tostring(k))
  end,
}

--[[
    Take a 'params' table and check the
    specified 'contract' is met. This both checks that all keys
    in the 'contract' are present and that no extra keys
    are present.

    Any key with the prefix 'opt_' is considered optional and may be nil.
    
    For each valid key the contract table
    should provide an entry:

    contract = {
      key1 = 'string',   -- A string value indicates a matching data type required.
      key2 = U.isScalar, -- A function will be run as the validator for this data.
      key3 = {           -- A subtable will be checked for this contract.
        subkey1 = U.isInteger,
        },
      key4 = {U.isScalarInClosedInterval, 14, 300},
              -- A sequence will call the function in element 1, 
              -- using additional elements as 2nd, 3rd, and 4th arguments
              -- U.isScalarInClosedInterval(params[key4], 14, 300)
      myArray = {U.isArrayOf, checkFunc} -- this sequence can be used to call 
                                         -- any check function in each element of an array
                                         -- a custom function can be provided to check complex data
                                         
      -- Optional arguments must be prefixed with 'opt_' the contract is enforced
      -- only if the optional value is not nil.           
      opt_key5 = 'boolean',
      opt_key6 = {       -- Optional arguments may have optional subtables, with optional arguements!
        subkey1 = U.isNonNegativeIntScalar,
        opt_subkey2 = 'number',
      }
    }

    Finally this function sets the metatable on the 'params' table 
    to error on access to undefined keys, or attempt to add new keys.
--]]
function U.enforceParamContract(params, contract, opt_setPartialContract)
  
  -- first check that all expected keys are present
  for k, v in pairs(contract) do
    if params[k] == nil then
      -- check if the key is optional (with an "opt_" prefix)
      if type(k) == 'string' then
        if not k:find('^opt_') then
          error("PARAMETER CONTRACT ERROR: Contract requires the key value: '%s'." % {tostring(k)})
        end
      else
        error('do we need string keys?')
      end
    end
  end

  -- second, check the correct data type on all inputs
  for k, v in pairs(params) do

    local ck = contract[k]  -- Contract for this key
    if not ck then
      msg = "PARAMETER CONTRACT ERROR: Unexpected key found in params table: '%s' is not part of the function contract." % {k}
      if opt_setPartialContract then
        U.devWarning(msg)
        break -- don't do any checking for this bonus parameter
      else
        error(msg)
      end
    end
    
    if type(ck) == 'string' then
      if type(v) ~= ck then
        error('PARAMETER CONTRACT ERROR: Expected a data type '..ck..' for key '..k)
      end
    elseif type(ck) == 'function' then
      if not ck(v) then
        error('PARAMETER CONTRACT ERROR: Key '..k..' value of '..tostring(v)..' did not pass the associated function test.')
      end
    elseif type(ck) == 'table' then
      if ck[1] then -- hack to detect a list, versus keyed values
        if type(ck[1]) == 'function' then -- Array with function as first arg
          -- this is a function with multiple arguments provided as an array
          if not ck[1](v, ck[2], ck[3], ck[4]) then
            error([[
              PARAMETER CONTRACT ERROR:
              The check for key '%(key)s' expects to call a function with some arguments and the test failed.
              Value was: %(val)s
              Args were: %(args)s
              ]] % {
              key = k,
              val = dump(v),
              args = dump(ck),
            })
          end
        else
          error('PARAMETER CONTRACT ERROR: Subtables in the contract must be keyed values OR have a function as the first element. key: '..k)
        end
      elseif ck[2] then
        -- user probably typoed the first arg and accidentally sent nil
        error('PARAMETER CONTRACT ERROR: nil value provided where a function was expected in a contract list. Received:'..dump(ck))
      else
        -- iterate and check the required interface for the subtable
        U.enforceParamContract(v, ck, opt_setPartialContract)
      end
    else
      -- any other type for ck is an error
      error('PARAMETER CONTRACT ERROR: The type '..type(ck)..' is not a valid option for a Function Contract. '..tostring(ck))
    end
  end

  if opt_setPartialContract then
    -- This is a hack option to enable the transition to using a contract.
    -- It will require the inputs but allow additional ones.
    -- It also won't check erroneous reads.
    U.devWarning('A partial contract has been set for '..tostring(opt_setPartialContract))
    return
  end
  -- error for access to keys not under contract
  setmetatable(params, contract_mt)
end

--[[
     Locks a table, such that all typo access will error.
--]]
function U.lockTable(t, mt)
  if type(t) ~= 'table' then
    return t
  end

  local this_mt = mt or {
    __index = protected_index,
    __newindex = protected_newindex,
  }
  for _, v in pairs(t) do
    U.lockTable(v, this_mt)
  end

  --[[
      Attempt to set the metatable:
      Note that PLECS has set an unalterable metatable on Block.Mask parameters.
      To work around that and avoid having to U.copyTable on small lists
      (like Block.Mask.Pins), we simply catch the specific error and 
      ignore it, leaving the existing "contract" in place.
  --]]

  -- setmetatable(t, this_mt)
  -- return t

  local ok, res = xpcall(function () setmetatable(t, this_mt) end, debug.traceback)

  if ok then
    return t -- return it for convenience
  else

    -- If there was an error, check if there is already a protected metatable.
    local i, j = string.find(res, 'cannot change a protected metatable')
    if j then
      return t -- assume sufficient contract is in place by plecs
      -- or should we do this?
      -- return setmetatable(U.copyTable(t), this_mt) -- copy and set contract (returns a table with the new lock, but the table passed by reference also has a sufficient lock, but now the tables are different...)
    else
      error(res) -- some other error, re-throw
    end
  end
end

--[[
    This function applies the "contract" metatable to a table.
    This metatable allows only access to existing defined data and `opt_` data, 
    but will error for any other undefined key access. The use cases for this 
    function are:
      
      a) If you desire to put a contract on a table without validating the data.
        This is useful if you want to put a contract on a table you are 
        returning from a function. Since we just calculated the data, 
        there isn't a need to explicitly check each variable's type.
      
      b) The uncommon case where it is desired to add data to a table under
        contract, in which case you can, break and re-enable the contract like
        this (without duplicating the work of defining the whole contract again):

      local temp = U.copyTable(locked.a)    -- break the contract
      temp.newVar = 'foo'                   -- add new data
      locked.a = U.contractLockTable(temp)  -- put a contract back on

      if locked.a.newVar == 'foo' then      -- access new data with no error

    For convenience the modified table is returned.
--]]
function U.contractLockTable(t)
  return U.lockTable(t, contract_mt)
end

function U.unlockTable(t)
  return U.lockTable(t, {}) -- remove the metatable, restore default behavior
end

--[[
Names for data types in Plexim's TSP code are chosen by following the steps below.
  a) Follow the Lua convention if there is one
   - 'Number' includes NaN and +-inf,
   - 'Table' can use strings and numbers as keys
   - 'Array' is the part of a table in numbered keys from 1-n with no holes
  b) Follow the mathematical definition if there is one
   - 'Real' is a Number, excluding Nan and +-Inf)
   - Scalar is a Real Number
   - Positive = strictly positive real number, Positive > 0, does not include zero
   - NonNegative >= 0, real number greater than or equal to zero
  c) Establish internal conventions for more specific structures
   - Vector = Array of real numbers (this is a common format for user input)
   - Char = A character in the range A-Z (commonly used for port comboBoxes, etc)
   - LowerChar = A character in the range a-z
   - ZeroIndexedInt = [1, 2, 3, ...] often mapped to a Char
   - OneIndexedInt =  [0, 1, 2, ...] less commonly mapped to a Char
   - Pad = {port = U.isChar, pin = U.isNonNegativeIntScalar}, used to represent A0 and D13 for ST targets
--]]

local function getCheckFunc(checkFuncOrContract)
  if type(checkFuncOrContract) == 'function' then
    -- use the provided checkFunc
    return checkFuncOrContract
  elseif type(checkFuncOrContract) == 'table' then
    -- it's a contract, call enforceParamContract using it.
    return function (v)
      U.enforceParamContract(v, checkFuncOrContract)
      return true
    end
  end
  error('This Function requires a second arg to be either a function or a contract table.')
  
end

--[[ 
    A wrapper on enforceParamContract that allows us to just check the contract.
--]]
function U.checkParamContract(params, contract)
  local success, result = pcall(U.enforceParamContract, U.copyTable(params), contract)
  return success
end
--[[
    Utility to check fixed length array data. Requires data length to match
    and each element of the array to return true from the type check function (or contract table).
--]]
function U.isFixedLengthArrayOf(data, expectedLen, checkFuncOrContract)
  if type(data) ~= 'table' then
    return false
  end

  if #data ~= expectedLen then
    return false
  end

  local checkFunc = getCheckFunc(checkFuncOrContract)

  for i, v in ipairs(data) do
    if not checkFunc(v) then
      return false
    end
  end

  return true
end

function U.isPinPair(data)
  return U.isFixedLengthArrayOf(data, 2, U.isNonNegativeIntScalar)
end

function U.is3Pins(data)
  return U.isFixedLengthArrayOf(data, 3, U.isNonNegativeIntScalar)
end

--[[
    Helper function to check the 'pin' concept for ST and XMC targets, such as
      A0, B5, D13

    Which we define as a 'pad':
        pad = {
          port = U.isChar, 
          pin = U.isNonNegativeIntScalar}
--]]
function U.isPad(data)
  return U.isTable(data) and U.checkParamContract(data, {
    port = U.isChar,
    pin = U.isNonNegativeIntScalar,
  })
end
-- Convert 'pad' (port, pin info) back to a string 'A0' etc
function U.padToStr(data)
  assert(
    U.isTable(data)
    and U.isChar(data.port) 
    and U.isNonNegativeIntScalar(data.pin),
    'This function requires a pad as input.')
  return '%(port)s%(pin)d' % data
end
--[[
    Utility to check type of array data. Provide any check Function, or 
    a table to enforce a contract on each element.
--]]
function U.isArrayOf(data, checkFuncOrContract)
  if type(data) ~= 'table' then
    return false
  end
  local checkFunc = getCheckFunc(checkFuncOrContract)

  for i, v in ipairs(data) do
    if not checkFunc(v) then
      return false
    end
  end

  return true
end

--[[
    Utility to check type of NON array data. Works for any keyed map,
    or for zero indexed "arrays". 
--]]
function U.isTableOf(data, checkFuncOrContract)
  if type(data) ~= 'table' then
    return false
  end
  
  local checkFunc = getCheckFunc(checkFuncOrContract)

  for k, v in pairs(data) do
    if not checkFunc(v) then
      return false
    end
  end

  return true
end

function U.isScalarOrVectorOf(data, checkFuncOrContract, ...)

  local checkFunc = getCheckFunc(checkFuncOrContract)

  if U.isVector(data) then
    for k, v in pairs(data) do
      if not checkFunc(v, ...) then
        return false
      end
    end
    return true
  elseif U.isScalar(data) then
    return checkFunc(data, ...)
  end
  return false
end

function U.isEmptyTable(t)
  if U.isTable(t) then
    return next(t) == nil
  else
    error('The argument is not a Lua table.')
  end
end

--[[
    This function is meant to detect data like {1, 2, 3}.
--]]
function U.isArrayWithNoKeys(arr)
  if type(arr) ~= 'table' then
    return false
  end

  local numEntries = 0
  for k, v in pairs(arr) do
    numEntries = numEntries + 1
  end
  return numEntries == #arr
end

function U.isTable(t)
  return (type(t) == 'table')
end

function U.isString(val)
  return type(val) == 'string'
end

-- a 'Char' is a character in the range 'A'-'Z' 
function U.isChar(val)
  if type(val) ~= 'string' or #val ~= 1 then
    return false
  end
  local byte = string.byte(val)
  return byte >= 65 and byte <= 90
end

-- a 'LowerChar' is a character in the range 'a'-'z' 
function U.isLowerChar(val)
  if type(val) ~= 'string' or #val ~= 1 then
    return false
  end
  local byte = string.byte(val)
  return byte >= 97 and byte <= 122
end

function U.isBoolean(val)
  return type(val) == 'boolean'
end

function U.isFunction(val)
  return type(val) == 'function'
end

function U.isFunctionOrString(val)
  return U.isFunction(val) or U.isString(val)
end
  
-- TODO: Do I want to implement this method to handle maps or array arguments for min_max, for example?
function U.makeMapFromArrays(tableIn, keysArray)
  if not (U.isTable(tableIn) and
    U.isTable(keysArray)) then
    error('Invalid args')
  end

  -- if the tableIn contains all the keys, great, just return it

  -- if it contains *some* keys, error, we don't handle this case

  -- if the tableIn doesn't contain the keys, but the length matches the keysArray
  -- create the desired table
end

-- Requires limits formatted as an 'array' {min, max}
-- TODO: Improve this to accept a table with named values min and max? See above.
function U.clampNumber(val, min_max)
  local input = val
  if (type(min_max) ~= 'table') or (#min_max ~= 2) then
    error('must provide limits as an array with 2 values, {min, max}')
  end

  local min_val = min_max[1]
  local max_val = min_max[2]

  -- check all inputs are numbers
  if not U.isReal(val)
  or not U.isReal(min_val)
  or not U.isReal(max_val) then
    error('all arguments must be real numbers')
  end
  if (min_val > max_val) then
    error('Max value should be greater than min value: '..min_val..' < '..max_val)
  else
    val = math.min(val, max_val)
    val = math.max(val, min_val)
  end

  return val
end

function U.guid()
  local template = '{0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX, 0xXX}'
  return string.gsub(template, '[X]', function (c)
    local v = (c == 'X') and random(0, 0xf)
    return string.format('%x', v)
  end)
end

function U.isValidCName(name)
  if string.match(name, '[^a-zA-Z0-9_]') ~= nil then
    return false
  end
  if string.match(string.sub(name, 1, 1), '[0-9]') ~= nil then
    return false
  end
  return true
end

--[[
    U.ensureVector(val, opt_len) returns a vector of data, if the input
    val is a vector, return it, if it is a scalar, return it as a vector
    of length 1. 
    
    Optionally if opt_len is provided, will require vector to match this length
    and return a vector padded to that many elements:
        For example: U.ensureVector(1, 3) returns {1, 1, 1}

    If the length does not match, a developer error will be raised. These length
    checks should be redundant with checks in the MaskInit, so an error is okay.

    This makes our code portable and not dependent on PLECS magic,
    which allows vector operations on scalars.
--]] 
function U.ensureVector(val, opt_len)
  if opt_len and not U.isPositiveIntScalar(opt_len) then
    error('Optional Length argument must be a positive integer value.')
  end
  if U.isVector(val) then
    if opt_len then
      if U.isFixedLengthArrayOf(val, opt_len, U.isScalar) then
        return val
      else
        error('Array was not the expected length (%(len)d). Received %(vect)s'
          % {
            len = opt_len,
            vect = dump(val),
          })
      end
    else
      return val -- no length check needed
    end
  elseif U.isScalar(val) then
    if opt_len then
      local ret = {}
      for i = 1, opt_len do
        ret[i] = val
      end
      return ret
    else
      return {val}
    end
  end
  error('Input cannot be converted to a valid vector: %s' % {tostring(val)})
end

-- from infineon
function U.isReal(val)
  if type(val) ~= 'number' then
    return false
  end
  if val ~= val or val == 1 / 0 or val == -1 / 0 then
    return false
  end
  return true
end

-- from infineon
function U.isScalar(val)
  return U.isReal(val)
end

function U.isInteger(val)
  return U.isScalar(val) and math.floor(val) == val
end

function U.isPositiveScalar(val)
  return U.isScalar(val) and val > 0
end

-- from infineon
function U.isNonNegativeScalar(val)
  return U.isScalar(val) and val >= 0
end

function U.isPositiveIntScalar(val)
  return U.isPositiveScalar(val) and U.isInteger(val)
end

function U.isNonNegativeIntScalar(val)
  return U.isNonNegativeScalar(val) and U.isInteger(val)
end

function U.isIntScalarOrVector(val)
  return U.isScalarOrVectorOf(val, U.isInteger)
end

function U.isNonNegativeScalarOrVector(val)
  return U.isScalarOrVectorOf(val, U.isNonNegativeScalar)
end

function U.isNonNegativeIntScalarOrVector(val)
  return U.isScalarOrVectorOf(val, U.isNonNegativeIntScalar)
end

function U.isPositiveIntScalarOrVector(val)
  return U.isScalarOrVectorOf(val, U.isPositiveIntScalar)
end

-- from infineon has NaN check
function U.isScalarInClosedInterval(val, min, max)
  if not (U.isScalar(min) and U.isScalar(max)) then
    error("A scalar value must be specified for inputs 'min' and 'max'.")
  end
  if U.isScalar(val) then
    return min <= val and val <= max
  end
  return false
end


function U.isIntScalarInClosedInterval(val, min, max)
  if not (U.isScalar(min) and U.isScalar(max)) then
    error("A scalar value must be specified for inputs 'min' and 'max'.")
  end
  if U.isInteger(val) then
    return min <= val and val <= max
  end
  return false
end

function U.isScalarInOpenInterval(val, min, max)
  if not (U.isScalar(min) and U.isScalar(max)) then
    error("A scalar value must be specified for inputs 'min' and 'max'.")
  end
  if U.isScalar(val) then
    return min < val and val < max
  end
  return false
end

function U.isIntScalarInOpenInterval(val, min, max)
  if not (U.isScalar(min) and U.isScalar(max)) then
    error("A scalar value must be specified for inputs 'min' and 'max'.")
  end
  if U.isInteger(val) then
    return min < val and val < max
  end
  return false
end

-- from infineon
function U.isVector(val)
  if type(val) == 'table' then
    local sequenceLength = 0
    for _, _ in ipairs(val) do
      sequenceLength = sequenceLength + 1
    end
    if sequenceLength == 0 then
      return false
    end
    for _, v in ipairs(val) do
      if not U.isReal(v) then
        return false
      end
    end
    return true
  end
  return false
end

function U.isScalarOrVector(val)
  return U.isScalar(val) or U.isVector(val)
end

function U.isScalarInSet(val, set)
  if not U.isVector(set) then
    error([[
      A vector, whose elements represent the elements of the set,
      must be provided as the second argument to this function.]])
  end
  if not U.isScalar(val) then
    error('A scalar must be provided as the first argument to this function.')
  end

  -- check if it's in the set
  for _, v in ipairs(set) do
    if val == v then
      return true
    end
  end
  return false
end
  

function U.isScalarOrVectorInSet(val, set)
  return U.isScalarOrVectorOf(val, U.isScalarInSet, set)
end

-- from infineon
function U.isIntScalarOrVectorInClosedInterval(val, min, max)
  return U.isScalarOrVectorOf(val, U.isIntScalarInClosedInterval, min, max)
end

function U.isScalarOrVectorInClosedInterval(val, min, max)
  return U.isScalarOrVectorOf(val, U.isScalarInClosedInterval, min, max)
end

-- Convert a input to integer representation (for printing, output purposes)
function U.toIntScalarOrVector(val)
  if not U.isScalarOrVectorOf(val, U.isInteger) then
    error('This function expects integer values as input.')
  end
  if U.isScalar(val) then
    return math.tointeger(val)
  end
  if U.isVector(val) then
    local ret = {}
    for i, v in ipairs(val) do
      ret[i] = math.tointeger(val[i])
    end
    return ret
  end

end

--[[
    Converts a vector to a printable string version:
    ex: '{1, 3, 5}'
    errors if the input is not a vector
--]]
function U.vectorToString(v)
  if not U.isVector(v) then
    error('vectorToString requires a vector as input.')
  end
  -- convert table to a string representation of the list:
  return '{'..table.concat(v, ', ')..'}'
end

--[[
    This function is used to create C initialization arrays like:

      uint8_t nums = {0, 3, 2, 1};

      Given a lua integer or array of integers this function returns
        initArray = '{0, 3, 2, 1}'

      Note this function will cast values to int, so no floating point 1.0's etc
--]]
function U.toIntegerInitializationList(v)
  if not U.isIntScalarOrVector(v) then
    error('This function requires a scalar or vector of integers.')
  end
  if U.isScalar(v) then
    return '{%d}' % v
  else
    return '{'..table.concat(U.toIntScalarOrVector(v), ', ')..'}'
  end
end

--[[
    Converts a vector of pin inputs to a printable string list version:
    ex: '[1, 3, 5]'
    errors if the input is not a scalar or vector
--]]
function U.inputsToStringList(v)
  if not U.isVector(v) then
    v = {v}
  end
  local result = {}
  for i = 1, #v do
    result[i] = string.format('%d', v[i])  -- force integer formatting
  end
  -- convert table to a string representation of the list:
  return '['..table.concat(result, ', ')..']'
end

function U.stringListToString(v)
  if not U.isArrayOf(v, U.isString) then
    error('function requires an array of strings')
  end
  -- convert table to a string representation of the list:
  return '{'..table.concat(v, ', ')..'}'
end
--[[
    Converts a table with numeric keys into a string, useful for debugging
      example:  '{[1] = disabled, [2] = start, [3] = stop, [4] = load}'
    Allows values to be stringLists (for PWM):  
             {[1] = {A, !A}, [2] = {A, }, [3] = {, }, [4] = {A, B}}",
--]]
function U.indexedListToString(indexedList)
  local sList = {}
  for k, v in pairs(indexedList) do
    local isStr = type(v) == 'string'
    table.insert(sList, '[%d] = %s' % {k, isStr and v or U.stringListToString(v)})
  end

  return '{'..table.concat(sList, ', ')..'}'
end

-- converts a value in Hz to MHz for user display
function U.toMHzString(freqHz)
  return tostring(freqHz / 1e6)..' MHz'
end

-- return true if input in MHz is an integer value in Hz
function U.megaHzIsIntegerHertz(clkMHz)

  return U.isPositiveScalar(clkMHz) and U.isPositiveIntScalar(clkMHz * 1e6)
end

--[[
    U.enforce() is a helper function that can run checks on any data
    and errors if the check function returns falsy.
    
    This function takes some optional arguments in order:
      - input1  -- value to be checked
      - checkFunction  -- function that tests the data ORDER TO MATCH other enforce patterns
      - input2 .. input4  -- optional inputs that are passed in order to the check function.
--]]
function U.enforce(...)
  local input1, checkFunction, opt_input2, opt_input3, opt_input4 = ...

  assert(0, 'This function is deprecated or has never been used.')

  if not type(checkFunction) == 'function' then
    error('U.enforce requires a function as the second argument.')
  end

  local ret = checkFunction(input1, opt_input2, opt_input3, opt_input4)

  if not ret then
    error('Check function returned falsy: with input1 = %s, opt_input2 = %s, opt_input3 = %s, opt_input4 = %s' % {tostring(input1), tostring(opt_input2), tostring(opt_input3), tostring(opt_input4)})
  end
  return ret
end

-- function EpwmShared:getConstantSignal(port, index, name)
--   local phStr = Block.InputSignal[port][index]
--   if string.find(phStr, 'UNCONNECTED') then
--     return 0
--   elseif Block.InputType[port][index] == 'float' then
--     return tonumber(string.sub(phStr, 1, -2))
--   elseif string.sub(Block.InputType[port][index], 1, #'uint') == 'uint'
--       or string.sub(Block.InputType[port][index], 1, #'int') == 'int' then
--       return tonumber(phStr)
--   elseif Block.InputType[port][index] == 'bool' then
--     if phStr == 'true' then
--       return 1
--     elseif phStr == 'false' then
--       return 0
--     end
--   else
--     U.error('Unsupported data type ("%s") for %s port.' % {Block.InputType[port][index], name})
--   end
-- end

--[[
    Checks for the "UNCONNECTED" string in Block.InputSignal[idx],
    which indicates the port is not connected in the .plecs model.
--]]
function U.portIsConnected(idx)
  assert(U.isNonNegativeIntScalar(idx), 'Must provide a valid port index.')
  local inputStr = Block.InputSignal[idx][1]
  return not string.find(inputStr, 'UNCONNECTED')
end

--[[
    Checks Block.InputSignal[idx] data for a numerical constant
    Returns 
    - 'nil' if the data is not constant, or
    - a number if the data is a constant.
]] 
function U.portValueGetConstant(idx)
  assert(U.isNonNegativeIntScalar(idx), 'Must provide a valid port index.')

  local inputStr = Block.InputSignal[idx][1]
  assert(U.isString(inputStr), 'Block.InputSignal is always a string!!')
  -- ignore an 'f' if present, strip it from the end of the string
  if string.sub(inputStr, -1, -1) == 'f' then
    inputStr = string.sub(inputStr, 1, -2);
  end
  return tonumber(inputStr)  -- nil indicates not a number
end
--[[
    Checks Block.InputSignal[idx] data for a numerical constant
--]]
function U.portValueIsConstant(idx)
  return U.portValueGetConstant(idx) ~= nil
end

--[[
    TSP blocks package up trigger signals to pass BID and other
    information, there are several types of trigger signals:
      modtrig -- model task trigger
      adctrig -- an ADC trigger
      synco   -- 
      ext_synco -- External Sync block
      tevent  -- from a comparator?

    All trigger signals implement this interface, including the Block ID:
      {adctrig = {bid = %d}}
    The additonal information be be included, for example HMRTIM Master:
      {synco = {bid = 5, hrtim = '%i', freq = '%f', varFreq = '%i'}}
    
    This function restructures the data so we don't have to guess what
    kind of trigger it is. Returning the following for the synco example above:
      {
        -- adds meta data for convenience
        trigType = 'synco',
        bid = 5,
        -- includes raw data, so existing code should work fine.
        synco = {bid = 5, hrtim = '%i', freq = '%f', varFreq = '%i'},
    }

    FIX ME: Why do we store data in strings above and 
      what is the best way to safely and consistently 
      return it to numeric data. Do we ever want to 
      send an actual string value??
--]]
function U.portParseTriggerInput(idx)
  assert(U.isNonNegativeIntScalar(idx), 'Must provide a valid port index.')
  
  local inputStr = Block.InputSignal[idx][1]
  local triggerDataTable = eval(inputStr:gsub('%s+', ''))  -- remove whitespace

  -- FIX ME: Check that triggerDataTable is a table with only one key element?
  if not U.isTable(triggerDataTable) then
    return
  end
  local parsedTrig = {}
  for trigName, trigStruct in pairs(triggerDataTable) do
    parsedTrig.trigType = trigName
    parsedTrig.bid = trigStruct.bid
    parsedTrig[trigName] = trigStruct -- you can index the parsed trigger the same as the old one, it just has new meta data
  end
  return parsedTrig
end

local plecsToTspType = {
  -- Plecs Type = Tsp Type
  bool = nil,
  uint8 = 'uint8_t',
  int8 = 'int8_t',
  uint16 = 'uint16_t',
  int16 = 'int16_t',
  uint32 = 'uint32_t',
  int32 = 'int32_t',
  float = nil,
  double = nil,
  -- floating point (target default) ??
} 

function U.portInputTypeIs(idx, plecsTypeStr)

  local tspTypeStr = plecsToTspType[plecsTypeStr]
  assert(tspTypeStr) -- error if the type wasn't a valid option
  local inTypes = Block.InputType[idx]
  for _, t in ipairs(inTypes) do
    if t ~= tspTypeStr then
      U.warning('Received type %s, expected type %s' % {t, tspTypeStr})
      return false
    end
  end
  return true
end

function U.zeroIndexedIntToChar(idx)
  assert(U.isIntScalarInClosedInterval(idx, 0, 25), 'The input must be a non-negative integer value to represent A-Z.')
  return string.char(65 + idx)
end

--[[
      Same as U.comboAsChar(), but clearer when the input isn't a comboBox, but
      a different 1-indexed integer.
--]]
function U.oneIndexedIntToChar(idx)
  assert(U.isIntScalarInClosedInterval(idx, 1, 26), 'The input must be a positive integer value to represent A-Z.')
  return string.char(65 + idx - 1)
end

--[[ 
    Use U.comboAsChar() to convert comboBox indexes to characters.

    For example the 'port' part of defining a pin (ST), the combo options are:
    {'A', 'B', 'C', 'D', 'E', 'F', 'G'}, creating a comboBox to do this validation
    is excessive. We can assume that PLECS/the block mask are doing sufficient
    validation and convert this simply, using the raw combo index. 
      1 = 'A'
      2 = 'B' and so forth...

    This is also useful for CAN Port, SPI Module etc for TI.
  --]]
function U.comboAsChar(cbxIdx)
  return U.oneIndexedIntToChar(cbxIdx)
end

--[[ 
    Convert a Char back to the combo index it would be associated with:
     1 = 'A', 2 = 'B', etc
--]]
function U.charToOneIndexedInt(c)
  assert(U.isChar(c), 'Provided argument must be a character string A, B, C etc')
  return 1 + string.byte(c) - string.byte('A')
end

--[[ 
    Convert a Char back to zero indexed integer:
     0 = 'A', 1 = 'B', etc
--]]
function U.charToZeroIndexedInt(c) -- U.charToOneIndexedInt(c)
  assert(U.isChar(c), 'Provided argument must be a character string A, B, C etc')
  return string.byte(c) - string.byte('A')
end

--[[
    Use U.passThrough() to explicitly indicate that data does not need
    validation. For example if you want the integer value of a comboBox,
    PLECS will enforce that the option is a valid one, we don't need to create
    a cbx_ structure to do that.
--]]
function U.passThrough(data)
  return data
end
local enforceIOFuncList = {
  --[[
      Provide a mapping from utility function names to error messages.
      The syntax [function], uses the function as the key in the table and 
        maps directly to a standard error message for that function.
      The syntax using a string as the key, allows the use of custom messages
        for any checkfunction. For example, to display numbers in hex as with
        'isIntScalarInClosedIntervalHex'.
      
      The variable 'sigName' is used to identify the source of the data being
      tested. For example: 'The parameter _FooBar_' 

      Optional inputs may be formatted and used as desired.
  --]]
  [U.portIsConnected] = '%(sigName)s must be connected.',
  [U.portValueIsConstant] = '%(sigName)s expects constant input data.',
  [U.portInputTypeIs] = "%(sigName)s expects data of type '%(input2)s'.",
  
  [U.isPinPair] = '%(sigName)s expects an array of exactly 2 valid pin numbers.',
  [U.is3Pins] = '%(sigName)s expects an array of exactly 3 valid pin numbers.',

  [U.isValidCName] = '%(sigName)s must be a valid C variable name.',

  [U.isScalar] = '%(sigName)s must be a scalar number.',
  [U.isIntScalarInClosedInterval] = '%(sigName)s must be an integer in the range [%(input2)i .. %(input3)i].',
  [U.isIntScalarInOpenInterval] = '%(sigName)s must be an integer in the range (%(input2)i .. %(input3)i).',
  isIntScalarInOpenIntervalPercent = {
    testFunc = U.isIntScalarInOpenInterval,
    msg =
    '%(sigName)s must be an integer in the range (%(input2)i%% .. %(input3)i%%).',
  },
  [U.isIntScalarOrVectorInClosedInterval] = '%(sigName)s must be a scalar or vector of integers in the range [%(input2)i .. %(input3)i].',
  [U.isScalarOrVectorInClosedInterval] = '%(sigName)s must be a scalar or vector of numbers in the range [%(input2)i .. %(input3)i].',
  [U.isNonNegativeScalar] = '%(sigName)s must be a non-negative scalar.',
  [U.isNonNegativeIntScalar] = '%(sigName)s must be a non-negative integer.',
  [U.isNonNegativeScalarOrVector] = '%(sigName)s must be a non-negative scalar or a vector of non-negative numbers.',
  [U.isNonNegativeIntScalarOrVector] = '%(sigName)s must be a non-negative integer or a vector of non-negative integers.',
  [U.isPositiveIntScalar] = '%(sigName)s must be a positive integer.',
  [U.isPositiveScalar] = '%(sigName)s must be a positive scalar.',
  [U.isPositiveIntScalarOrVector] = '%(sigName)s must be a positive integer or a vector of positive integers.',
  [U.isScalarOrVectorInSet] = '%(sigName)s must be a number or a vector of numbers in the set %(input2_vector)s.',
  [U.isScalarOrVector] = '%(sigName)s must be a number or a vector of numbers.',
  
  isIntScalarInClosedIntervalHex = {
    testFunc = U.isIntScalarInClosedInterval,
    msg = "%(sigName)s input value '0x%(input1)x' must be an integer in the range [%(input2)i .. 0x%(input3)x].",
  },
  [U.megaHzIsIntegerHertz] = '%(sigName)s must be a positive integer value when converted to Hz. This field only supports a numerical value with up to 6 decimal places.',
  [U.isScalarInOpenInterval] = '%(sigName)s must be in the range (%(input2)g .. %(input3)g)%(input4_unit)s.',
  [U.isScalarInClosedInterval] = '%(sigName)s must be in the range [%(input2)g .. %(input3)g]%(input4_unit)s.',

  [U.fileExists] = '%(sigName)s file could not be found.',
  [U.directoryExists] = "%(sigName)s directory '%(input1)s' could not be found. %(input2_optInstructions)s",
  [U.isNonEmptyCoderOptionsDirectory] = '%(sigName)s must be specified.', -- special case to reject empty
  [U.isValidPathStr] = "%(sigName)s '%(input1)s' may not contain any of the following characters: %(input2_charList)s\n%(input3_optInstructions)s",
}

--[[
    U.enforceIO() is a helper function that can run checks on input data
    and generate standardized error messages.
    
    This function has two required args and some optional arguments used by 
    the check function:
      - params = {
        - sigName   -- string for error message substitution
                    -- calling functions will make this a link appropriately:
                         U.enforceMask() -- links to Block.Mask.Var widget
                         U.enforceCO()   -- links to Target.Variables.Var widget
        - checkFunctionKey:
            A key value which is used to lookup the error message in the 
            enforceFuncList (above). The checkFunctionKey can be of type:
               - "function", use the function directly, or
               - "string", in which case a custom error message can be 
                           specified for the check function.
        - opt_errorArgs   -- Additional block paths to pass to U.error()
      }
      - input1            -- the data value to test
      - input2 .. input4  -- optional inputs that are passed in order to the check function.
--]]
function U.enforceIO(...)
  local params, input1, opt_input2, opt_input3, opt_input4 = ...
  U.enforceParamContract(params, {
    sigName = U.isString,
    checkFunctionKey = U.isFunctionOrString,
    opt_errorArgs = U.isTable,
  })
  local sigName = params.sigName
  local checkFunctionKey = params.checkFunctionKey

  local msg, testFunc
  local lookupDetails = enforceIOFuncList[checkFunctionKey]
  local defaultMsg = "Bad data for '%(sigName)s', the 'checkFunctionKey' was not recognized to give a more detailed error message."

  if     type(checkFunctionKey) == 'function' then

    testFunc = checkFunctionKey
    msg = lookupDetails or error(defaultMsg) -- if the key wasn't in the table, use a default msg, or should we error()?

  elseif type(checkFunctionKey) == 'string'
  and    type(lookupDetails) == 'table'
  and    lookupDetails.msg
  and    lookupDetails.testFunc then

    msg = lookupDetails.msg
    testFunc = lookupDetails.testFunc
  else
    error('bad type for lookupDetails')
  end

  local ret = testFunc(input1, opt_input2, opt_input3, opt_input4)

  if not ret then
    U.error(
      msg
      % {
        -- Build a string substitution table for error messages.
        -- Note in the string substitutions the 'opt_' is dropped.
        -- Additional specially formatted versions can be added to this table
        -- as needed. Ex: input2_vector or input4_unit 
        sigName = sigName,
        -- inputs are numbered according to the argument 
        -- order of the test function
        input1 = input1, -- usually this is the data to test

        input2 = opt_input2,
        input2_vector = opt_input2 and (U.isVector(opt_input2) and U.vectorToString(opt_input2)) or 'BAD_DATA', -- expand a list: {1, 2, 3}
        input2_charList = opt_input2 and (U.isString(opt_input2) and U.formatCharListForErrorMessage(opt_input2)) or 'BAD_DATA', -- format bad character list
        input2_optInstructions = U.isString(opt_input2) and opt_input2 or '',
        input3 = opt_input3,
        input3_optInstructions = U.isString(opt_input3) and opt_input3 or '',

        input4 = opt_input4,
        input4_unit = opt_input4 and ' ['..opt_input4..']' or '', -- input4 as a unit ie. [MHz]
        
      }, params.opt_errorArgs)
  end
  -- return the input values, because in the case of success, 
  -- most likely what we want is input1
  return input1, opt_input2, opt_input3, opt_input4
end

--[[
    Wrapper on U.enforceIO that adjusts the name appropriately for a port.
    In future will create a port link as well.
--]]
function U.enforcePort(...)
  local portName, checkFunctionKey, input1, opt_input2, opt_input3, opt_input4 = ...

  local sigName = "The port '%s'" % {portName}
  return U.enforceIO({
                       sigName = sigName,
                       checkFunctionKey = checkFunctionKey,
                     },
                     input1, opt_input2, opt_input3, opt_input4)
end
--[[
    Wrapper on U.enforceIO that expects the Mask Variable Name as a string as 
    the second arg. This function evaluates it to pass on to enforceIO and 
    uses the string to generate a mask parameter link for the error message.
--]]
function U.enforceMask(...)
  local checkFunctionKey, maskVarAsStr, opt_input2, opt_input3, opt_input4 = ...

  if type(checkFunctionKey) == 'string' then
    U.devWarning(checkFunctionKey..' is a string, is this a function key?, or are you using the old enforceMask Interface?')
  end

  local sigName = U.paramLink(maskVarAsStr)
  return U.enforceIO({
                       sigName = sigName,
                       checkFunctionKey = checkFunctionKey,
                     },
                     Block.Mask[maskVarAsStr], opt_input2, opt_input3, opt_input4)
end
--[[
    Wrapper for U.enforceIO that creates a link to the Coder Options widget
    for Target.Variables[targetVarAsStr].
--]]
function U.enforceCO(...)
  local checkFunctionKey, targetVarAsStr, opt_input2, opt_input3, opt_input4 = ...

  local sigName = 'The target parameter @param:%s:2:' % {targetVarAsStr}
  return U.enforceIO({
                       sigName = sigName,
                       checkFunctionKey = checkFunctionKey,
                     },
                     Target.Variables[targetVarAsStr], opt_input2, opt_input3, opt_input4)
end

--[[
    All External Tools are paths, we always want them normalized. This function, unlike
    other U.enforceXX processes the value returned, in this case to normalize the path. 
    Thus the function is especially named 'enforceETPath'.
--]]
function U.enforceETPath(...)
  local checkFunctionKey, externalToolsVarList, opt_input2, opt_input3, opt_input4 = ...
  local externalToolsVarAsStr = externalToolsVarList[1]
  local externalToolsVarPrompt = externalToolsVarList[2]
  local sigName = 'The %s' % {externalToolsVarPrompt}
  return U.normalizePath( -- We always want a normalized path returned. 
    U.enforceIO({
                       sigName = sigName,
                       checkFunctionKey = checkFunctionKey,
                     },
                     Target.FamilySettings.ExternalTools[externalToolsVarAsStr], 
                     opt_input2, opt_input3, opt_input4))

end
-- Starting to take stock of variability in different TSPs and document it.
U.ENUM = {
  CARRIER_TYPE = { -- STM, TI, XMC
    'SAWTOOTH', -- counts up
    'SYMMETRICAL',
  },
  CHECK_BOX = { -- Use utility U.comboEnabled() to handle these checkbox type comboBoxes
    'DISABLED',
    'ENABLED',
  },
  FREQ_TOL = { -- STM, TI, XMC  (could use U.comboEnabled() ?)
    'EXACT', -- Enforce exact value
    'ROUND', -- Round to closest
  },
  OUTPUT_TYPES = {  -- GPIO output characteristics
    'PUSH_PULL',
    'OPEN_DRAIN',
  },
  PULL_TYPES = { -- GPIO input characteristics, shared by everyone but TI
    'PULL_UP',   -- These strings are used as macros by XMC
    'PULL_DOWN', -- Thus the chosen text should not change
    'TRISTATE',  -- aka 'High impedance'
  },
  -- Data Types as C indicators, used in native.lua and read_global.lua
  DATA_TYPES = {
    'bool', 'unsigned char', 'char', 
    'uint16_t', 'int16_t',
    'uint32_t', 'int32_t', 
    'float', 'double', Target and Target.Variables and Target.Variables.FLOAT_TYPE or 'float'}, -- default for tests
  NONE_RISING_FALLING_EITHER = {
    'None',
    'Rising',
    'Falling',
    'Either',
  },
  RISING_FALLING_EITHER = {
    'Rising',
    'Falling',
    'Either',
  },

  STM = {
    TRIGGER = { -- PWM  FIX ME: ZIA, could add values from other targets as negative indexs, for compares to not crash?
      'UNDERFLOW',
      'OVERFLOW',
    },
  },

  TI = {
    TRIGGER = { -- PWM
      'NONE', -- disabled
      'UNDERFLOW',
      'OVERFLOW',
      'OVER_AND_UNDER', -- In dialogs as 'under and over'
    },
    DIO_TYPE = {
      'INPUT',
      'OUTPUT',
    },
    PULL_TYPES = {
      'PULL_UP',
      'TRISTATE', -- aka 'High impedance'
    }
  },

  XMC = { -- These are unique to XMC
    TRIGGER = { -- Used with PWM and CCU8 Slice (Low Level)
      'DISABLED',
      'UNDERFLOW',
      'OVERFLOW',
      'OVER_AND_UNDER',
      -- cases below are never used and are only included for compatibility with other trigger source enums
      [-1] = 'COMPARE1',
      [-2] = 'COMPARE2',
    },
    TRIGGER_TIMER = {
      'DISABLED',
      'OVERFLOW',
      -- cases below are never used and are only included for compatibility with other trigger source enums
      [-1] = 'UNDERFLOW',
      [-2] = 'OVER_AND_UNDER',
      [-3] = 'COMPARE1',
      [-4] = 'COMPARE2',
    },
    TRIGGER_LOWLEVEL_SAWTOOTH = {
      'DISABLED',
      'OVERFLOW',
      'COMPARE1',
      'COMPARE2',
      -- cases below are never used and are only included for compatibility with other trigger source enums
      [-1] = 'UNDERFLOW',
      [-2] = 'OVER_AND_UNDER',
    }
  },
}

--[[
    In lieu of checkboxes, we have comboBoxes with the 
    options {'disabled', 'enabled'}. Rather than awkwardly testing 
    versus 1 (disabled) or 2 (enabled), or creating a bulky cbx_foobar,
    this utility function converts the maskValue to a boolean:

    self.foobarIsEnabled = U.comboEnabled(Block.Mask.Foobar)

    Note: 
    THIS UTILITY CAN ONLY BE USED IF THE OPTIONS ARE {'disabled', 'enabled'}
  
--]]
function U.comboEnabled(maskValue)
  -- Assumes the standard pattern of combo options {'disabled', 'enabled'}
  return maskValue == 2
end

-- Coder Options does have checkboxes, where 0 = unchecked and 1 = checked.
function U.checkBoxSelected(targetVarName)
  return Target.Variables[targetVarName] == 1
end

--[[
    Utility to create new specific comboBox variables. Most often used for user comboBoxes, in which
    case we expect the comboBox to be indexed from 1, otherwise special cases must be implemented.

    The primarly use case is to remove magic numbers in the code and enable logical tests:

        if myComboBox.equals('foobar') then
          -- do stuff
        end

    Arguments are:
    - cxValOrEnumStr     -- Raw comboBox index value as an integer (1-n)
                         -- or a valid EnumStr, as would be use with equals()
                         -- the latter option is useful for 'fake' comboBoxes
    - comboOptionsList   -- A list of valid options as strings.
                            The list must be ordered to match the index options of the comboBox.
                            For special cases you can use a table with zero or non sequential indexes.

    returns a "comboBox" type, which is a table with the following methods:
      - equals('str')    -- Returns true if the 'str', matches the string value of the comboBox
      - asString()       -- Returns the string value of the comboBox.
      - asCx()           -- Returns the combo index value of the comboBox.
      - dump()           -- Returns data from the closure for printing.

    The comboBox type further has metamethods defined to protect from access to not existant fields.

    The following data is stored in a closure:
    - val                -- Combo value as a string.
    - optionsList        -- List of valid options for this comboBox, stored to enforce valid checks.
    - integerLookup      -- Map from string options to integer index.
                            Useful for debugging if you dump() this comboBox.
    - dataType = "comboBox"  -- dataType could be used to verify use of comboBox methods.
--]]
function U.newComboBox(cxValOrEnumStr, comboOptionsList)

  local this = {
    optionsList = comboOptionsList,
    dataType = 'comboBox',
  }

  this.integerLookup = {}
  for k, v in pairs(comboOptionsList) do
    if not U.isInteger(k) then
      error('Combo Options must have integer keys, we now allow negative, zero, and discontinuous, but only integers.')
    end
    this.integerLookup[v] = k
  end

  local cxVal = cxValOrEnumStr
  if type(cxValOrEnumStr) == 'string' then
    -- This is a fake comboBox (not being generated by reading Block.Mask.Foo)
    -- Instead the developer provides a valid compare string.
    -- Check this string is a valid option in the comboOptionsList
    cxVal = this.integerLookup[cxValOrEnumStr]
  end
 
  if not U.isInteger(cxVal) or not this.optionsList[cxVal] then
    return nil
  end

  local val = comboOptionsList[cxVal]

  this.val = val

  -- this could be useful for ADC A, we could just grab the 'A' character directly
  local function asString()
    return this.val
  end

  local function asRawValue()
    return this.val
  end

  -- this might be useful for TSPs where the Peripherals are numbered 0, 1, 2
  local function asCx()
    local cx = this.integerLookup[this.val]
    return cx
  end

  local function getWidgetType()
    return 'comboBox'
  end

  local function equals(testValStr)
    if type(testValStr) ~= 'string' then
      error('comboBox.equals() requires a string input for comparison not '..type(testValStr))
    end
    if not this.integerLookup[testValStr] then
      error("Invalid value '%s' for this comboBox." % {testValStr})
    end
    return this.val == testValStr
  end

  -- print the contents of the comboBox in the closure
  local function comboBoxDump()
    return dump(this)
  end

  local valAsStr
  if U.isString(this.val) then
    valAsStr = this.val
  else
    valAsStr = U.stringListToString(this.val)
  end

  local debugString = valAsStr..': optionsList: '..U.indexedListToString(
    this.optionsList)
  -- only expose this public interface
  local ret = {
    equals = equals,
    asString = asString,
    asRawValue = asRawValue,
    asCx = asCx,
    dump = comboBoxDump,
    getWidgetType = getWidgetType, -- be careful not to overwrite the built-in 'type'
    debugString = debugString, -- basic data type to display via plecs dump()
  }

  setmetatable(ret, {
    __index = protected_index,
    __newindex = protected_newindex,
    __tostring = comboBoxDump,
  })

  return ret
end

--[[
   
   This function is a wrapper on U.newComboBox(), with the added features:
    - requires valid input (will error instead of providing a default value)
    - error message links to appropriate UI widget (CoderOptions, Mask, etc)
    - returns the created comboBox
--]]
function U.enforceCO_newComboBox(targetVarNameStr, comboOptionsList)
  return U.enforceComboBoxHelper(targetVarNameStr, comboOptionsList, 'CO')
end

function U.enforceMask_newComboBox(maskVarNameStr, comboOptionsList)
  return U.enforceComboBoxHelper(maskVarNameStr, comboOptionsList, 'Mask')
end

function U.enforceComboBoxHelper(varName, comboOptionsList, sourceTypeStr)
  local inputValue
  if sourceTypeStr == 'CO' then
    inputValue = Target.Variables[varName]
  elseif sourceTypeStr == 'Mask' then
    inputValue = Block.Mask[varName]
  else
    U.throwUnreachableError(
      'enforceComboBox is not yet implemented for '..sourceTypeStr)
  end
  if inputValue == nil then
    -- The variable name probably has a typo.
    error("Invalid widget name: '%s'. This is probably a typo. Report this error to the TSP developer." % {varName})
  end

  local cbx = U.newComboBox(inputValue, comboOptionsList)

  if not cbx then
    -- an error occurred, a valid comboBox was not created
    -- These errors are pretty impossible for a user to trigger, perhaps a 
    -- developer error makes more sense. However, this indirection allows for
    -- better evaluation of the nil input case and detection of the typo above
    -- if there is one... 
    if sourceTypeStr == 'CO' then
      U.error('Invalid value (%s) for target parameter @param:%s:2:' % {tostring(inputValue), varName})
    elseif sourceTypeStr == 'Mask' then
      U.error('Invalid value (%s) for the parameter @param:%s:' % {tostring(inputValue), varName})
    else
      error('enforceComboBox is not yet implemented for '..sourceTypeStr)
    end
  end

  return cbx
end

function U.isComboBox(cbx)
  if cbx and type(cbx) == 'table' and type(cbx.getWidgetType) == 'function' then
    return cbx.getWidgetType() == 'comboBox'
  else
    return false
  end
end

--[[
  Create an enum for indices (for Block IO).
  Suggested usage:

  -- provide a list of inputs by name in port order:
  self.inIdx = U.enum({'m', 'ph', 'SyncIn', 'f', 'en', 'd_rf', 'seq',})

  -- instead of the magic number '4' use:
  local fStr = Block.InputSignal[self.inIdx.f][1]
--]]
function U.enum(list)
  local ret = {}

  for i, v in ipairs(list) do
    ret[v] = i
  end

  local function protected_index(_, key)
    error('Index class does not have key '..tostring(key)..'.')
  end

  local function protected_newindex(_, key, value)
    error('Index class does not allow assignment.')
  end

  setmetatable(ret, {
    __index = protected_index,
    __newindex = protected_newindex,
  })

  return ret
end

--[[ 
  utility to generate static initialization values for C code
  for example the middle line below:
        static unsigned char data[8] = {
          0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
        };
--]]
function U.repeatValueString(value, numberOfRepeats)
  t = {}
  for _ = 1, numberOfRepeats do
    table.insert(t, tostring(value))
  end
  return table.concat(t, ', ')
end

function U.enforceExactFrequencyTolerance(params)
  U.enforceParamContract(
    params,
    {
      freqDesired = U.isReal, -- Hz
      freqAchievable = U.isReal, -- Hz
      descriptor = U.isString,    -- 'PWM frequency' or 'timer frequency', etc
      opt_rTol = U.isReal,
      opt_aTol = U.isReal,
      opt_carrierFrequency = U.isReal, -- optional if we are testing something different but based on the carrier?
      opt_suggestedFix = U.isString,
    }
  )
  -- set defaults for optional parameters:
  local rTol = params.opt_rTol or 1e-6 -- relative tolerance
  local aTol = params.opt_aTol or 1 -- 1 Hz is the minimum tolerance
  local carrierFreq = params.opt_carrierFrequency or params.freqDesired
  local suggestedFix = params.opt_suggestedFix or [[
          Please modify the frequency setting or change the "Frequency tolerance" parameter.
          You may also adjust the system clock frequency under Coder Options->Target->General.]]

  local tol = carrierFreq * rTol

  if tol < aTol then -- aTol is the minimum absolute tollerance
    tol = aTol
  end

  local fswError = params.freqDesired - params.freqAchievable
  if math.abs(fswError) > tol then
    local msg = [[
            Unable to accurately achieve the desired %(descriptor)s:
            - desired value: %(desired)f Hz
            - closest achievable value: %(actual)f Hz

            %(suggestedFix)s
            ]]
    U.error(msg % {
      descriptor = params.descriptor, 
      desired = params.freqDesired, 
      actual = params.freqAchievable,
      suggestedFix = suggestedFix,
    })
  end
end


function U.sequenceContains(s, value)
  for i = 1, #s do
    if s[i] == value then
      return true
    end
  end
  return false
end

function U.arrayContainsValue(arr, val)
  for index, value in ipairs(arr) do
    if value == val then
      return true
    end
  end
  return false
end

function U.dataTypeAsString(datatype)
  local data_types_as_string = {'uint8_t', 'uint8_t', 'int8_t', 'uint16_t', 'int16_t', 'uint32_t', 'int32_t', 'float',
    'double', 'TargetFloatPlaceholder10'}
  local dataTypeAsString
  if datatype == 10 then
    -- target default floating point format
    dataTypeAsString = Target.Variables.FLOAT_TYPE
  else
    dataTypeAsString = data_types_as_string[datatype]
  end
  return dataTypeAsString
end


--[[
    This function peeks at the resource table to see if an requested resource
    exists. This is useful if we can't wait till the end of the CodeGen phase
    to detect a missing or conflicting resource.

    The ResourceList defined in PLECS is structured like this:

    {
      [1] = {
        [1] = "ADC A",
        [2] = "-1",
      },
     [5] = {
        [1] = "ADCA-SOC",
        [2] = "0",
      },
    }
--]]
function U.resourceExists(target, resourceName, opt_resourceNum)
  local resourcePreview = ResourceList:new()

  target.configure(resourcePreview)
  for _, rList in ipairs(resourcePreview) do
    if rList[1] == resourceName then
      if opt_resourceNum then
        if opt_resourceNum == tonumber(rList[2]) then
          return true
        end
      else
        return true
      end
    end
  end
  return false
end

return U

