--[[
  Copyright (c) 2025 by Plexim GmbH
  All rights reserved.

  A free license is granted to anyone to use this software for any legal
  non safety-critical purpose, including commercial applications, provided
  that:
  1) IT IS NOT USED TO DIRECTLY OR INDIRECTLY COMPETE WITH PLEXIM, and
  2) THIS COPYRIGHT NOTICE IS PRESERVED in its entirety.

  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
  OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  SOFTWARE.
--]]

local U = require('common.utils')
local VersionNumber = require('common.version')

local ExtTools = {}

local function getOSCoderPrefix()
  local osPrefix
  if Target.Variables.HOST_OS == 'mac' then
    osPrefix = 'PLECS --> Preferences...'
  elseif Target.Variables.HOST_OS == 'linux'
  or     Target.Variables.HOST_OS == 'win' then
    osPrefix = 'File --> PLECS Preferences...'
  else
    U.devWarning('Unknown HOST_OS: '..Target.Variables.HOST_OS)
    osPrefix = 'File --> PLECS Preferences...'
  end
  return osPrefix
end

function ExtTools.getNotFoundInstructions()
  return
     "From %(osPrefix)s --> Coder, click on the text of the '%(family)s' Target Family to update the paths for the the required tools." %
     {
       osPrefix = getOSCoderPrefix(),
       family = Target.Family,
     }
end

--[[
    Check that the TARGET_ROOT is a valid directory (only valid characters)
    and that it exists.
--]]
function ExtTools.assertValidTARGET_ROOT()
  local targetRootDir = U.normalizePath(Target.Variables.TARGET_ROOT)
  local targetRootInstructions = 
     "From %(osPrefix)s --> Coder, select a valid 'Target Support Packages Path.'"
     % {osPrefix = getOSCoderPrefix()}
  U.assertValidPathStr(
    targetRootDir, {
      opt_errorMsgPrefix = 'The TSP',
      opt_errorMsgInstructions = targetRootInstructions})

  U.assertDirectoryExists(
    targetRootDir, {
      opt_instructions = targetRootInstructions})

  return targetRootDir
end

--[[
    Check that the path provided is an allowed (valid) path.
    Provides a nice user error message if 
    - the path has bad characters or
    - the directory does not exist (Note this check doesn't work from 
                            OnConnectFunction because PLECS is not defined.)

    Args:
      - varNameList:  For now a provide a list to create a reasonable error 
                      message identifier in lieu of having an 
                      @param link to ExternalTools: {VariableName, PromptStr}
      - opt_params: Optional parameters
        - opt_badChars: Overrides default badChars
        - opt_additionalBadChars: Adds chars to default list
        - opt_defaultPath: provide default path relative to TARGET_ROOT

    Returns the normalized path if no error is detected.
--]]
function ExtTools.enforceValidDirectoryExists(varNameList, opt_params)

  assert(U.isFixedLengthArrayOf(varNameList, 2, U.isString))
  local varName = varNameList[1]

  local badChars = U.getDisallowedPathChars()
  local defaultPath -- nil unless user specified
  if opt_params then
    U.enforceParamContract(opt_params, {
      opt_badChars = U.isString,
      opt_additionalBadChars = U.isString,
      opt_defaultPath = U.isString, -- default relative to TARGET_ROOT, should be normalized and start with a '/'...
    })

    badChars = opt_params.opt_badChars or badChars
    badChars = badChars..(opt_params.opt_additionalBadChars or '')
    defaultPath = opt_params.opt_defaultPath or ''
  end

  local rawDir = Target.FamilySettings.ExternalTools[varName]
  if not rawDir then
    error('Invalid variable name, perhaps you have a typo? "Target.Family.Settings.ExternalTools.'..varName..'"')
  end
  local dir = U.normalizePath(rawDir)
  
  -- If a default path is provided, use it IF the user did not input a value
  if (dir == '') and defaultPath then
    dir = U.normalizePath(Target.Variables.TARGET_ROOT)..defaultPath
    return dir -- no further validation if using the default path
  end

  -- Verify the user provided path contains only valid characters:
  U.enforceETPath(U.isValidPathStr, varNameList, badChars, ExtTools.getNotFoundInstructions())
 
  -- Verify the directory exists:
  U.enforceETPath(U.directoryExists, varNameList, ExtTools.getNotFoundInstructions())

  -- return normalized directory path:
  return dir
end

--[[
    Strip the port information out of the TARGET_DEVICE string:
    User sees: Serial over GDB:127.0.0.1:2331

    Software sees Target.Variables.TARGET_DEVICE: `5|127.0.0.1:2331|0|0`

    Check if the user has specified a port to connect to the external mode,
    if not use the default one.
--]]
function ExtTools.getGDBPort()
  local target = Target.Variables.TARGET_DEVICE

  local port = string.match(target, ':%d+|')
  if port then
    -- Remove the leading ':' and trailing '|'
    port = port:sub(2, -2)

    U.enforceIO({
                  sigName = 'The specified GBD port',
                  checkFunctionKey = U.isIntScalarInClosedInterval,
                },
                tonumber(port), 1024, 65535)

    return port
  else
    return '2331'  -- default gdb port
  end
end

--[[
    TI C2000 CGT tools functions
    Assumes interface includes 
     - Target.FamilySettings.ExternalTools.codegenDir
     - Target.FamilySettings.ExternalTools.C29CodegenDir
--]]
ExtTools.CGT = {}
function ExtTools.CGT.getDir()
  return U.normalizePath(Target.FamilySettings.ExternalTools.codegenDir)
end
function ExtTools.CGT.getDir29()
  return U.normalizePath(Target.FamilySettings.ExternalTools.C29CodegenDir)
end

local function getCGTVersionFromReadMe(readMePath)
  local file, e = io.open(readMePath, 'r')
  if file == nil then
    return nil, 'README.html file not found.'
  end
  -- Get the CGT version number from the README.html title
  for line in file:lines() do
    local trimmed = line:match('^%s*(.*)')
    if trimmed:find('^<title>') then
      -- Match version like 22.6.0.LTS (i.e., three dot-separated numbers)
      local version = trimmed:match('(%d+%.%d+%.%d+.[LS]TS)')
      file:close()
      return version
    end
  end

  file:close()
  return nil, 'No version found in <title> line'
end

local function assertCGTFound(cgtDir, minVersionNum)
  local cgtReadMePath = cgtDir..'/README.html'
  local cgtVersionString, e = getCGTVersionFromReadMe(cgtReadMePath)
  if not cgtVersionString then
    error(e)
  end
  
  local foundCgtVersion = VersionNumber.newCGT(cgtVersionString)
  if not foundCgtVersion then
    error('Could not verify TI CGT version installed.')
  end

  local minRequiredCgtVersion = VersionNumber.newCGT(minVersionNum)
  if not minRequiredCgtVersion then
    error('TSP developer must provide a valid minimum CGT version number.')
  end

  if foundCgtVersion < minRequiredCgtVersion then
    U.error([[
      The TI CGT must be of version %(minVersionNum)s or more recent.

      - Version detected: %(versionFound)s
      - Installation directory: %(cgtDir)s]] % {
      minVersionNum = minVersionNum,
      versionFound = cgtVersionString,
      cgtDir = cgtDir,
    })
  end
end

function ExtTools.CGT.assertCGTFound(minVersionNum)
  local cgtDir = ExtTools.CGT.getDir()
  -- If directory is empty, prompt the user to download 
  if cgtDir == '' then
    local msg = 'Please configure the TI codegen tools directory.'
    U.error(msg..ExtTools.getNotFoundInstructions())
  end

  cgtDir = ExtTools.enforceValidDirectoryExists({'codegenDir', 'TI codegen tools directory'})

  assertCGTFound(cgtDir, minVersionNum)
end

function ExtTools.CGT.assertCGTFound29(minVersionNum)
  local cgtDir = ExtTools.CGT.getDir29()
  -- If directory is empty, prompt the user to download 
  if cgtDir == '' then
    local msg = 'Please configure the TI C29 codegen tools directory'
    U.error(msg..ExtTools.getNotFoundInstructions())
  end

  cgtDir = ExtTools.enforceValidDirectoryExists({'C29CodegenDir', 'TI C29 codegen tools directory'})

  assertCGTFound(cgtDir, minVersionNum)
end

--[[
    JLink shared functions.
    Assumes interface includes Target.FamilySettings.ExternalTools.JlinkDir
    Used by XMC, ATSAM, STM32
--]]
ExtTools.JLink = {}
function ExtTools.JLink.getDir()
  return U.normalizePath(Target.FamilySettings.ExternalTools.JlinkDir)
end

function ExtTools.JLink.assertJLinkFound(opt_minVersionNum)
  local jLinkDir = ExtTools.JLink.getDir()
  -- If directory is empty, prompt the user to download 
  if jLinkDir == '' then
    local msg = 'Please configure the Segger J-Link directory. You can download the latest J-Link Software package from: https://www.segger.com/downloads/jlink/ '
    U.error(msg..ExtTools.getNotFoundInstructions())
  end

  jLinkDir = ExtTools.enforceValidDirectoryExists({'JlinkDir', 'Segger J-Link'})

  if opt_minVersionNum then
    -- Check JLink version. However, if a version is not found installed
    -- there will be no error or warning.
    -- Example 'V7.94a'
    local minRequiredJLinkVersion = VersionNumber.newJLink(opt_minVersionNum)

    local releaseNotesPath =
       jLinkDir..'/Doc/ReleaseNotes/ReleaseNotes_JLink.html'

    if U.fileExists(releaseNotesPath) then
      local jLinkReleaseNotes = io.open(releaseNotesPath, 'r')
      for line in jLinkReleaseNotes:lines() do
        -- Look for the first line like this:
        --     <h2>Version V7.94a (2023-12-06)</h2>
        local match = line:match('<h2>Version(.-)</h2>')
        if match then
          -- grab the version part e.g. 'V7.94b'
          local v = line:match('(V%d+.%d+%a)')
          if v then
            local thisJLinkVersion = VersionNumber.newJLink(v)
            if thisJLinkVersion < minRequiredJLinkVersion then
              U.error([[
            Installed SEGGER J-Link version is below minimum required version.

              • Minimum required J-Link version:  %(required)s
              • Installed J-Link version:         %(found)s

              Please download the latest J-Link software package from https://www.segger.com/downloads/jlink/
            ]] % {
                required = tostring(minRequiredJLinkVersion),
                found = tostring(thisJLinkVersion),
              })
            end
          end
          -- break on first occurrence (this assumes that the latest released
          -- version is listed at the top of the release notes).
          break
        end
      end
    else
      U.devWarning('Skipping JLink Version check, no release notes found.')
    end
    U.devWarning('No minimum Version provided for JLink check.')
  end
  return jLinkDir
end

function ExtTools.JLink.getGdbServerExe()
  local jLinkDir = ExtTools.JLink.getDir()
  local gdbServerExe
  if Target.Variables.HOST_OS == 'win' then
    gdbServerExe = jLinkDir..'/JLinkGDBServerCL'
  else
    gdbServerExe = jLinkDir..'/JLinkGDBServer'
  end
  return gdbServerExe
end

--[[
    Requires `Target.FamilySettings.ExternalTools.GccArmNoneEabiDir`
    Used by XMC, ATSAM, STM32
--]]
ExtTools.GccArm = {}
function ExtTools.GccArm.assertGccArmNoneEabiFound()
  return ExtTools.enforceValidDirectoryExists(
    {'GccArmNoneEabiDir', 'Arm none eabi tools'},
    {opt_defaultPath = '/bin/gcc-arm-none-eabi'})
end

--[[ 
    Requires `Target.FamilySettings.ExternalTools.GdbProgDir`
--]]
ExtTools.C2pGdb = {}
function ExtTools.C2pGdb.assertProgrammerFound()
  return ExtTools.enforceValidDirectoryExists(
    {'GdbProgDir', 'GDB Programmer'},
    {opt_defaultPath = '/bin/c2p-gdb'})
end

--[[ 
    Requires `Target.FamilySettings.ExternalTools.PlecsCockpitDir`
--]]
ExtTools.PlxCockpit = {}
function ExtTools.PlxCockpit.assertPlxCockpitFound()
  return ExtTools.enforceValidDirectoryExists(
    {'PlecsCockpitDir', 'PLECS Cockpit'},
    {opt_defaultPath = '/bin/plx-cockpit'})
end

--[[
     OpenOcd
     Requires: Target.FamilySettings.ExternalTools.OpenocdDir
     Used by ATSAM, STM32
--]]
ExtTools.OpenOcd = {}

function ExtTools.OpenOcd.assertOpenOcdFound()
  return ExtTools.enforceValidDirectoryExists(
    {'OpenocdDir', 'OpenOCD'},
    {opt_defaultPath = '/bin/openocd'})
end

--[[
    This server command adds the -s scripts and is returned unquoted, since
    this is a partial command for some targets.

    Used by XMC as is, and by STM32, adding to the search path
--]]
function ExtTools.OpenOcd.getBaseCommand()
  local openocdDir = ExtTools.OpenOcd.assertOpenOcdFound()

  return U.shellQuote(openocdDir..'/bin/openocd')..
     ' -s '..U.shellQuote(openocdDir..'/share/openocd/scripts')
end


ExtTools.SHARED_MAKE_FUNCTIONS = [[
##############################################################
space:=
space+=
# for MacOS - NOTE: not tolerant to leading spaces or already escaped spaces '\ '
EscapeSpaces=$(subst $(space),\$(space),$(1))
FlipSlashesBack=$(subst /,\,$(1))

ifeq ($(OS),Windows_NT)
# Windows
SHELL := cmd.exe
FixPath=$(call FlipSlashesBack,$(1))
ClearFile=del /F /Q "$(call FlipSlashesBack,$(1))"
ClearDir=del /F /Q "$(call FlipSlashesBack,$(1))\*.*"
MoveFile=move /Y "$(call FlipSlashesBack,$(1))" "$(call FlipSlashesBack,$(2))"
CopyFile=copy /Y "$(call FlipSlashesBack,$(1))" "$(call FlipSlashesBack,$(2))"
TouchFile=type nul > "$(call FlipSlashesBack,$(1))"

else
# Linux style
FixPath = $(1)
ClearFile=rm -Rf "$(1)"
ClearDir=rm -Rf "$(1)"/*
MoveFile=mv "$(1)" "$(2)"
CopyFile=cp "$(1)" "$(2)"
TouchFile=touch "$(1)"

endif 
]]

return ExtTools
