_G._DEBUG             = false               -- Required by the new lua posix
local posix           = require("posix")

require("strict")

--------------------------------------------------------------------------
-- Lmod License
--------------------------------------------------------------------------
--
--  Lmod is licensed under the terms of the MIT license reproduced below.
--  This means that Lmod is free software and can be used for both academic
--  and commercial purposes at absolutely no cost.
--
--  ----------------------------------------------------------------------
--
--  Copyright (C) 2008-2025 Robert McLay
--
--  Permission is hereby granted, free of charge, to any person obtaining
--  a copy of this software and associated documentation files (the
--  "Software"), to deal in the Software without restriction, including
--  without limitation the rights to use, copy, modify, merge, publish,
--  distribute, sublicense, and/or sell copies of the Software, and to
--  permit persons to whom the Software is furnished to do so, subject
--  to the following conditions:
--
--  The above copyright notice and this permission notice shall be
--  included in all copies or substantial portions of the Software.
--
--  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.
--
--------------------------------------------------------------------------

require("inherits")
require("myGlobals")
require("colorize")
require("string_utils")
require("utils")

Hub                    = require("Hub")

local BeautifulTbl     = require("BeautifulTbl")
local FrameStk         = require("FrameStk")
local M                = {}
local MName            = require("MName")
local Var              = require("Var")
local dbg              = require("Dbg"):dbg()
local base64           = require("base64")
local concatTbl        = table.concat
local cosmic           = require("Cosmic"):singleton()
local decode64         = base64.decode64
local encode64         = base64.encode64
local getenv           = os.getenv
local hook             = require("Hook")
local i18n             = require("i18n")
local max              = math.max
local pack             = (_VERSION == "Lua 5.1") and argsPack or table.pack -- luacheck: compat
local remove           = table.remove
local s_adminT         = {}
local s_loadT          = {}
local s_allRequestedT  = {}  -- Track ALL modules from user's original request (for error suggestions)
local s_moduleStk      = {}
local s_performDepCk   = false
local s_missDepT       = {}
local s_missingModuleT = {}
local s_missingFlg     = false
local s_purgeFlg       = false
local timer            = require("Timer"):singleton()

--------------------------------------------------------------------------
-- Remember the user's requested load array into an internal table.
-- This is tricky because the module mnames in the *mA* array may not be
-- findable yet (e.g. module load mpich petsc).  The only thing we know
-- is the usrName from the command line.  So we use the *usrName* to be
-- the key and not *sn*.
-- @param mA The array of MName objects.
local function l_registerUserLoads(mA)
   dbg.start{"l_registerUserLoads(mA)"}
   local frameStk   = FrameStk:singleton()
   local stackDepth = frameStk:stackDepth()
   for i = 1, #mA do
      local mname       = mA[i]
      local userName    = mname:userName()
      s_loadT[userName] = {mname, stackDepth}
      dbg.print{"Registering userName: ",userName,"\n"}
   end
   dbg.fini("l_registerUserLoads")
end

local function l_unRegisterUserLoads(mA, force)
   if (dbg.active()) then
      local s = mAList(mA)
      dbg.start{"l_unRegisterUserLoads(mA={"..s.."}, force=", force,")"}
   end
   for i = 1, #mA do
      local mname       = mA[i]
      local userName    = mname:userName()
      local entry       = s_loadT[userName]
      if (entry) then
         local stackDepth = entry[2]
         if (stackDepth > 0 or force) then
            s_loadT[userName] = nil
            dbg.print{"Unregister userName: ",userName,"\n"}
         end
      end
   end
   dbg.fini("l_unRegisterUserLoads")
end

local function l_compareRequestedLoadsWithActual()
   dbg.start{"l_compareRequestedLoadsWithActual()"}
   local mt = FrameStk:singleton():mt()

   local aa = {}
   local bb = {}

   -- Use s_loadT (populated by l_registerUserLoads at all depths) to detect
   -- ALL modules that failed to load, including dependencies loaded from
   -- within module files.  Collect into tmpA for deterministic sorting.
   local tmpA = {}
   for userName, entry in pairs(s_loadT) do
      local mname = entry[1]
      local sn    = mname:sn()
      if (not mt:have(sn, "active")) then
         dbg.print{"not active: userName: ",userName,", mname:show(): ",mname:show(),"\n"}
         tmpA[#tmpA+1] = {showName = mname:show(), userName = userName}
      end
   end

   -- Sort by userName for deterministic output order
   table.sort(tmpA, function(a, b) return a.userName < b.userName end)

   -- Extract sorted values back to aa and bb
   for i = 1, #tmpA do
      aa[i] = tmpA[i].showName
      bb[i] = tmpA[i].userName
   end

   dbg.fini("l_compareRequestedLoadsWithActual")
   return aa, bb
end

local function l_check_for_valid_name(kind, name)
   local l    = name:len()
   local i, j = name:find("^[a-zA-Z_][a-zA-Z0-9_]*")
   if (j ~= l) then
      LmodError{msg="e_BadName",kind=kind, name=name}
   end
end

local function l_check_for_valid_alias_name(kind, name)
   if (name:find("[ \t]")) then
      LmodError{msg="e_BadAlias",kind=kind, name=name}
   end
end

local function l_createStackName(name)
   return "__LMOD_STACK_" .. name
end

--------------------------------------------------------------------------
-- Helper function to extract short name from a module userName
-- e.g., "boost/1.75.0" -> "boost", "gcc" -> "gcc"
local function l_extract_sn(userName)
   return userName:match("([^/]+)") or userName
end

--------------------------------------------------------------------------
-- Helper function to collect ALL parentAA entries for a module from dbT
-- A module may exist in multiple hierarchy locations (e.g., gcc/boost and intel/boost)
-- Each location has its own parentAA, so we collect all of them
-- Returns a combined array of parent arrays, or nil if none found
local function l_collect_all_parentAA(userName, dbT)
   local sn = l_extract_sn(userName)
   local moduleData = dbT[sn]
   if (not moduleData) then
      return nil
   end

   local allParentAA = {}

   -- Search through all entries for this short name
   for fn, entry in pairs(moduleData) do
      local match = false
      -- Check for exact fullName match
      if (entry.fullName == userName) then
         match = true
      -- Also match if userName is just the short name (no version)
      elseif (userName == sn) then
         match = true
      end

      if (match and entry.parentAA) then
         -- Add all parent arrays from this entry
         for i = 1, #entry.parentAA do
            allParentAA[#allParentAA + 1] = entry.parentAA[i]
         end
      end
   end

   if (#allParentAA == 0) then
      return nil
   end
   return allParentAA
end

--------------------------------------------------------------------------
-- Helper function to check if two dependency paths are compatible
-- Two paths are compatible if one is a prefix of the other
-- Returns the longer (more specific) path if compatible, nil otherwise
local function l_paths_compatible(pathA, pathB)
   if (not pathA or not pathB) then return nil end
   local lenA, lenB = #pathA, #pathB
   local shorter, longer = pathA, pathB
   if (lenA > lenB) then
      shorter, longer = pathB, pathA
   end
   -- Check if shorter is a prefix of longer
   for i = 1, #shorter do
      if (shorter[i] ~= longer[i]) then
         return nil
      end
   end
   return longer
end

--------------------------------------------------------------------------
-- Helper function to find common dependency paths across multiple modules
-- Returns array of paths that work for ALL modules, or empty if none
local function l_find_common_paths(failingUserNames, dbT)
   if (not dbT or #failingUserNames == 0) then
      return {}
   end

   -- Get paths for first module
   local firstPaths = l_collect_all_parentAA(failingUserNames[1], dbT)
   if (not firstPaths or #firstPaths == 0) then
      return {}
   end

   -- If only one failing module, return its paths
   if (#failingUserNames == 1) then
      return firstPaths
   end

   -- Find paths compatible with ALL subsequent modules
   local commonPaths = {}
   for i = 1, #firstPaths do
      local candidatePath = firstPaths[i]
      local isCommon = true

      for j = 2, #failingUserNames do
         local otherPaths = l_collect_all_parentAA(failingUserNames[j], dbT)
         if (not otherPaths or #otherPaths == 0) then
            isCommon = false
            break
         end

         -- Find a compatible path in otherPaths
         local foundCompatible = false
         local bestPath = candidatePath
         for k = 1, #otherPaths do
            local compatible = l_paths_compatible(candidatePath, otherPaths[k])
            if (compatible) then
               foundCompatible = true
               -- Use the longer (more specific) path
               if (#compatible > #bestPath) then
                  bestPath = compatible
               end
               break
            end
         end

         if (not foundCompatible) then
            isCommon = false
            break
         end
         candidatePath = bestPath
      end

      if (isCommon) then
         -- Avoid duplicates
         local isDup = false
         for m = 1, #commonPaths do
            if (#commonPaths[m] == #candidatePath) then
               local same = true
               for n = 1, #candidatePath do
                  if (commonPaths[m][n] ~= candidatePath[n]) then
                     same = false
                     break
                  end
               end
               if (same) then
                  isDup = true
                  break
               end
            end
         end
         if (not isDup) then
            commonPaths[#commonPaths + 1] = candidatePath
         end
      end
   end

   return commonPaths
end

--------------------------------------------------------------------------
--------------------------------------------------------------------------
-- Helper function to build the list of user-requested modules to include
-- in a suggestion command, filtering out modules that conflict with the
-- dependency path.  A conflict exists when a requested module has the
-- same short name as a module in the dependency path but a different
-- version (e.g., user requested gcc/11 but path requires gcc/10.0).
-- Failing modules are always included.  Non-conflicting modules (e.g.,
-- python/3.9 when the path is gcc/10.0) are included to give the user
-- a complete command.
-- @param path       Array of dependency path entries (e.g., {"gcc/10.0"})
-- @param failingSet Table of failing userNames (userName -> true)
-- @return Array of module names to append after the dependency path
local function l_build_modules_for_path(path, failingSet)
   -- Build set of short names in the dependency path
   local pathSnSet = {}
   for i = 1, #path do
      pathSnSet[l_extract_sn(path[i])] = true
   end

   local modulesToInclude = {}
   for i = 1, #s_allRequestedT do
      local entry    = s_allRequestedT[i]
      local userName = entry.userName
      local sn       = entry.mname:sn()

      if (failingSet[userName]) then
         -- Failing modules are always included
         modulesToInclude[#modulesToInclude + 1] = userName
      elseif (not pathSnSet[sn]) then
         -- Non-conflicting module: safe to include
         modulesToInclude[#modulesToInclude + 1] = userName
      end
      -- Conflicting modules (same sn as dependency path) are excluded
   end
   return modulesToInclude
end

--------------------------------------------------------------------------
-- Collect parent paths of currently loaded modules from MT.
-- Used to filter suggestions that would conflict with the loaded toolchain.
-- @param mt Module table (FrameStk:mt())
-- @param dbT Spider database
-- @return Array of path arrays, or empty if none
local function l_collect_loaded_paths(mt, dbT)
   if (not mt or not dbT) then return {} end
   local activeA = mt:list("fullName", "active")
   if (not activeA or #activeA == 0) then return {} end
   local loadedPathsAA = {}
   local seen = {}
   for i = 1, #activeA do
      local obj = activeA[i]
      local sn = (type(obj) == "table") and obj.sn or nil
      local fn = (type(obj) == "table") and obj.fn or nil
      if (sn and fn) then
         local moduleData = dbT[sn]
         local entry = nil
         if (moduleData) then
            entry = moduleData[fn]
            if (not entry and path_regularize) then
               local fnNorm = path_regularize(fn)
               for k, v in pairs(moduleData) do
                  if (path_regularize(k) == fnNorm) then
                     entry = v
                     break
                  end
               end
            end
         end
         if (entry) then
            local parentAA = entry.parentAA
            if (parentAA) then
               for j = 1, #parentAA do
                  local p = parentAA[j]
                  if (p and #p > 0) then
                     local key = concatTbl(p, "|")
                     if (not seen[key]) then
                        seen[key] = true
                        loadedPathsAA[#loadedPathsAA + 1] = p
                     end
                  end
               end
            end
         end
      end
   end
   return loadedPathsAA
end

--------------------------------------------------------------------------
-- True when path element matches an active module (exact or regularized).
local function l_path_element_in_active_set(element, activeSet)
   if (not element or not activeSet) then return false end
   if (activeSet[element]) then return true end
   if (not path_regularize) then return false end
   local elNorm = path_regularize(element)
   for fullName, _ in pairs(activeSet) do
      if (l_extract_sn(fullName) == l_extract_sn(element) and
          path_regularize(fullName) == elNorm) then
         return true
      end
   end
   return false
end

--------------------------------------------------------------------------
-- True when path differs from loadedPath only by a stack-root version swap
-- (e.g. release/24.04 loaded, path starts with release/24.10).  Does not
-- apply when a multi-element toolchain path is already loaded.
local function l_path_second_element_requested(path)
   if (#path < 2 or not s_allRequestedT) then return false end
   local sn2 = l_extract_sn(path[2])
   for i = 1, #s_allRequestedT do
      if (s_allRequestedT[i].mname:sn() == sn2) then
         return true
      end
   end
   return false
end

local function l_block_stack_root_promo(path, loadedPathsAA, activeSet)
   if (not path or not loadedPathsAA or #loadedPathsAA == 0) then
      return false
   end
   if (l_path_second_element_requested(path)) then
      return true
   end
   local hasLongPath = false
   for i = 1, #loadedPathsAA do
      if (#loadedPathsAA[i] > 1) then
         hasLongPath = true
         break
      end
   end
   if (not hasLongPath) then return false end
   for i = 1, #loadedPathsAA do
      local lp = loadedPathsAA[i]
      if (#lp == 1 and l_extract_sn(lp[1]) == l_extract_sn(path[1]) and
          not l_path_element_in_active_set(path[1], activeSet)) then
         return true
      end
   end
   return false
end

local function l_path_stack_root_swap_compatible(path, loadedPath, loadedPathsAA,
                                                 activeSet)
   if (not path or not loadedPath or #path < 1 or #loadedPath ~= 1) then
      return false
   end
   local pathSn   = l_extract_sn(path[1])
   local loadedSn = l_extract_sn(loadedPath[1])
   if (pathSn ~= loadedSn or path[1] == loadedPath[1]) then
      return false
   end
   if (path_regularize and
       path_regularize(path[1]) == path_regularize(loadedPath[1])) then
      return false
   end
   if (l_block_stack_root_promo(path, loadedPathsAA, activeSet)) then
      return false
   end
   return true
end

local function l_is_deliberate_stack_version_swap(path, activeSet)
   if (#path < 1 or not activeSet) then return false end
   local firstSn = l_extract_sn(path[1])
   for fullName, _ in pairs(activeSet) do
      if (l_extract_sn(fullName) == firstSn and
          not l_path_element_in_active_set(path[1], activeSet)) then
         return true
      end
   end
   return false
end

--------------------------------------------------------------------------
-- Check if a path is compatible with at least one loaded path.
-- Used to reject suggestions that would conflict with the current toolchain.
local function l_path_compatible_with_loaded(path, loadedPathsAA)
   if (not loadedPathsAA or #loadedPathsAA == 0) then return true end
   for i = 1, #loadedPathsAA do
      if (l_paths_compatible(path, loadedPathsAA[i])) then
         return true
      end
   end
   return false
end

local function l_path_allowed_for_suggestion(path, loadedPathsAA, activeSet)
   if (l_path_compatible_with_loaded(path, loadedPathsAA)) then
      return true
   end
   for i = 1, #loadedPathsAA do
      if (l_path_stack_root_swap_compatible(path, loadedPathsAA[i],
                                            loadedPathsAA, activeSet)) then
         return true
      end
   end
   if (l_is_deliberate_stack_version_swap(path, activeSet) and
       not l_block_stack_root_promo(path, loadedPathsAA, activeSet)) then
      return true
   end
   return false
end

--------------------------------------------------------------------------
-- Set of active module fullNames from the module table.
local function l_collect_active_fullName_set(mt)
   local activeSet = {}
   if (not mt) then return activeSet end
   local activeA = mt:list("fullName", "active")
   if (not activeA or #activeA == 0) then return activeSet end
   for i = 1, #activeA do
      local obj = activeA[i]
      local fullName = (type(obj) == "table") and obj.fullName or obj
      if (fullName) then
         activeSet[fullName] = true
      end
   end
   return activeSet
end

--------------------------------------------------------------------------
-- Drop path elements already active in the MT. Returns nil if nothing stripped.
local function l_strip_active_from_path(path, activeSet)
   if (not path or #path == 0) then return nil end
   local stripped = {}
   local strippedAny = false
   for i = 1, #path do
      if (l_path_element_in_active_set(path[i], activeSet)) then
         strippedAny = true
      else
         stripped[#stripped + 1] = path[i]
      end
   end
   if (not strippedAny) then return nil end
   return stripped
end

local function l_cmd_token_count(cmd)
   local n = 0
   for _ in cmd:gmatch("%S+") do
      n = n + 1
   end
   return n
end

--------------------------------------------------------------------------
-- Count version swaps along a spider parent path vs the active module set.
-- Returns swapCount and earliestSwapDepth (1-based path index, 0 if none).
local function l_path_swap_metrics(path, activeSet)
   if (not path or #path == 0 or not activeSet) then
      return 0, 0
   end
   local swapCount = 0
   local earliestSwap = math.huge
   for i = 1, #path do
      if (not l_path_element_in_active_set(path[i], activeSet)) then
         local sn = l_extract_sn(path[i])
         for fullName, _ in pairs(activeSet) do
            if (l_extract_sn(fullName) == sn) then
               swapCount = swapCount + 1
               if (i < earliestSwap) then
                  earliestSwap = i
               end
               break
            end
         end
      end
   end
   if (swapCount == 0) then
      return 0, 0
   end
   return swapCount, earliestSwap
end

--------------------------------------------------------------------------
-- Helper function to format dependency commands for display
-- Returns formatted string with ready-to-copy-paste commands, or nil if
-- no dependencies found.
-- @param kA Array of failing module show names
-- @param kB Array of failing module user names
-- @param dbT The spider database table
-- @param loadedPathsAA Paths of currently loaded modules (optional, for filtering)
-- @param mt Module table (for active fullName set when building minimal suggestions)
local function l_format_dependency_commands(kA, kB, dbT, loadedPathsAA, mt)
   if (not dbT or next(dbT) == nil) then
      return nil
   end

   local suggestions = {}
   local seenCmd = {}
   local maxCommands = 5  -- Limit number of suggestions to avoid overwhelming output

   local function l_add_suggestion(cmd, swapRequired, path, activeSet)
      if (seenCmd[cmd]) then return end
      seenCmd[cmd] = true
      local swapCount, swapDepth = l_path_swap_metrics(path, activeSet)
      suggestions[#suggestions + 1] = {
         cmd       = cmd,
         tokens    = l_cmd_token_count(cmd),
         swap      = swapRequired and 1 or 0,
         swapCount = swapCount,
         swapDepth = swapDepth,
      }
   end

   local function l_add_path_suggestions(path, failingSet, activeSet)
      if (not path or #path == 0) then return end
      if (not l_path_allowed_for_suggestion(path, loadedPathsAA, activeSet)) then
         return
      end
      local swapCount, swapDepth = l_path_swap_metrics(path, activeSet)
      local swapRequired = (swapCount > 0)
      local modulesToInclude = l_build_modules_for_path(path, failingSet)
      local suffix = (#modulesToInclude > 0) and (" " .. concatTbl(modulesToInclude, " ")) or ""
      local explicitCmd = "module load " .. concatTbl(path, " ") .. suffix
      l_add_suggestion(explicitCmd, swapRequired, path, activeSet)
      local strippedPath = l_strip_active_from_path(path, activeSet)
      if (strippedPath and #strippedPath > 0) then
         local minimalCmd = "module load " .. concatTbl(strippedPath, " ") .. suffix
         if (minimalCmd ~= explicitCmd) then
            l_add_suggestion(minimalCmd, swapRequired, strippedPath, activeSet)
         end
      end
   end

   -- Build set of failing userNames for quick lookup
   local failingSet = {}
   for i = 1, #kB do
      failingSet[kB[i]] = true
   end

   local activeSet = l_collect_active_fullName_set(mt)

   -- Try to find common paths for all failing modules
   local commonPaths = l_find_common_paths(kB, dbT)

   if (#commonPaths > 0) then
      -- List all spider parent paths (minimal + explicit); sort ranks swap-required last.
      for i = 1, #commonPaths do
         l_add_path_suggestions(commonPaths[i], failingSet, activeSet)
      end
   else
      -- Fallback: no common path (disjoint hierarchies). Suggest only paths from
      -- the most-constrained module(s), i.e. those with the fewest paths.
      -- This avoids suggesting paths that would fail for other requested modules
      -- (e.g., Python under GCCcore when QGIS only exists under GCC/OpenMPI).
      local pathCountT = {}
      local minCount   = math.huge
      for i = 1, #kB do
         local userName  = kB[i]
         local parentAA  = l_collect_all_parentAA(userName, dbT)
         local nPaths    = (parentAA and #parentAA > 0) and #parentAA or 0
         pathCountT[i]   = {userName = userName, parentAA = parentAA, n = nPaths}
         if (nPaths > 0 and nPaths < minCount) then
            minCount = nPaths
         end
      end

      -- Collect candidate paths from most-constrained modules
      local candidates = {}
      for i = 1, #kB do
         local entry = pathCountT[i]
         if (entry.n > 0 and entry.n == minCount and entry.parentAA) then
            for j = 1, #entry.parentAA do
               local parentA = entry.parentAA[j]
               if (parentA and #parentA > 0) then
                  candidates[#candidates + 1] = parentA
               end
            end
         end
      end
      -- When multiple modules tie (e.g. both n=1), prefer the longest path.
      -- Longer paths are more specific and more likely to satisfy all modules
      -- (e.g. GCC/OpenMPI pulls in GCCcore via depends_on).
      local nContributors = 0
      for i = 1, #kB do
         if (pathCountT[i].n > 0 and pathCountT[i].n == minCount) then
            nContributors = nContributors + 1
         end
      end
      local filterCandidates = (nContributors > 1)
      local maxPathLen = 0
      for i = 1, #candidates do
         if (#candidates[i] > maxPathLen) then
            maxPathLen = #candidates[i]
         end
      end
      for i = 1, #candidates do
         local parentA = candidates[i]
         if (#parentA >= maxPathLen) then
            if (filterCandidates) then
               local worksForAll = true
               for j = 1, #kB do
                  local otherAA = pathCountT[j].parentAA
                  if (otherAA and #otherAA > 0) then
                     local hasCompatible = false
                     for k = 1, #otherAA do
                        if (l_paths_compatible(parentA, otherAA[k])) then
                           hasCompatible = true
                           break
                        end
                     end
                     if (not hasCompatible) then
                        worksForAll = false
                        break
                     end
                  end
               end
               if (not worksForAll) then
                  parentA = nil
               end
            end
            if (parentA and #parentA > 0 and l_path_compatible_with_loaded(parentA, loadedPathsAA)) then
               local modulesToInclude = l_build_modules_for_path(parentA, failingSet)
               local cmd = "module load " .. concatTbl(parentA, " ") .. " " .. concatTbl(modulesToInclude, " ")
               l_add_suggestion(cmd, false, parentA, activeSet)
            end
         end
      end
      if (#suggestions == 0 and filterCandidates and nContributors >= 2) then
         return "   These modules cannot be loaded together - they require incompatible toolchains.\n"
      end
   end

   if (#suggestions == 0 and loadedPathsAA and #loadedPathsAA > 0) then
      return "   The requested module(s) require a toolchain that is incompatible with the currently loaded environment.\n"
   end

   if (#suggestions == 0) then
      return nil
   end

   -- Fewest modules first; swap-required last; among swaps prefer fewer
   -- conflicts and deeper (later hierarchy) swaps before root-stack swaps.
   table.sort(suggestions, function(a, b)
      if (a.swap ~= b.swap) then return a.swap < b.swap end
      if (a.tokens ~= b.tokens) then return a.tokens < b.tokens end
      if (a.swapCount ~= b.swapCount) then return a.swapCount < b.swapCount end
      if (a.swapDepth ~= b.swapDepth) then return a.swapDepth > b.swapDepth end
      return a.cmd < b.cmd
   end)

   local allCommands = {}
   for i = 1, #suggestions do
      allCommands[i] = suggestions[i].cmd
   end

   -- Limit number of commands shown
   local cmdsToShow = {}
   for i = 1, math.min(#allCommands, maxCommands) do
      cmdsToShow[#cmdsToShow + 1] = "      " .. allCommands[i]
   end

   local result = "   Or load any one of these options:\n" .. concatTbl(cmdsToShow, "\n") .. "\n"
   if (#allCommands > maxCommands) then
      result = result .. "      ... and " .. (#allCommands - maxCommands) .. " more options\n"
   end
   return result
end

local function l_error_on_missing_loaded_modules(aa, bb)
   if (#aa > 0) then
      dbg.start{"l_error_on_missing_loaded_modules(aa,bb)"}
      -- Clear s_missingModuleT to prevent recursive processing through M.error()
      -- when mcp:report is called below. The modules we're processing here via
      -- l_compareRequestedLoadsWithActual() supersede the s_missingModuleT tracking.
      s_missingModuleT = {}
      dbg.printT("aa",aa)
      dbg.printT("bb",bb)


      local luaprog = findLuaProg()
      local cmdA = {}
      cmdA[#cmdA+1] = luaprog
      cmdA[#cmdA+1] = pathJoin(cmdDir(),cmdName())
      cmdA[#cmdA+1] = "bash"
      cmdA[#cmdA+1] = dbg.active() and "-D" or " "
      cmdA[#cmdA+1] = "--regexp --no_redirect --spider_timeout 2.0 spider"
      local count   = #cmdA

      local uA = {}  -- unknown names
      local iA = {}  -- illegal names
      local kA = {}  -- known modules (show)
      local kB = {}  -- known modules (usrName)


      if (expert()) then
         uA = aa
      else
         local outputDirection = dbg.active() and "2> spider.log" or "2> /dev/null"
         for i = 1, #bb do
            if (bb[i]:sub(1,2) == "__") then
               iA[#iA+1] = bb[i]
            else
               cmdA[count+1] = "'^" .. bb[i]:escape() .. "$'"
               cmdA[count+2] = outputDirection
               local cmd     = concatTbl(cmdA," ")
               local result  = capture(cmd)
               dbg.print{"result: ",result,"\n"}
               if (result:find("\nfalse")) then
                  uA[#uA+1] = aa[i]
               else
                  kA[#kA+1] = aa[i]
                  kB[#kB+1] = bb[i]
               end
            end
         end
      end

      dbg.printT("uA",uA)
      dbg.printT("iA",iA)
      dbg.printT("kA",kA)
      dbg.printT("kB",kB)


      local a = {}

      if (#iA > 0) then
         mcp:report{msg="e_Illegal_Load", module_list = concatTbl(iA, " ") }
      end


      if (#uA > 0) then
         local multi_load_hint = ""
         if (#s_allRequestedT > 1) then
            multi_load_hint = "\n" .. i18n("e_Failed_Load_multi_hint", {}) .. "\n"
         end
         mcp:report{msg="e_Failed_Load", module_list = concatTbl(uA, " "),
                    multi_load_hint = multi_load_hint}
      end

      if (#kA > 0) then
         -- Try to get dependency information from cache for ready-to-copy-paste commands
         local cmdText = nil
         local ok, Cache = pcall(require, "Cache")
         if (ok and Cache) then
            local cache_ok, cache = pcall(function()
               return Cache:singleton{buildCache = true, quiet = true}
            end)
            if (cache_ok and cache) then
               local build_ok, spiderT, dbT = pcall(function()
                  local s, d, b = cache:build()
                  return s, d
               end)
               if (build_ok and dbT and next(dbT) ~= nil) then
                  local mt = FrameStk:singleton():mt()
                  local loadedPathsAA = l_collect_loaded_paths(mt, dbT)
                  cmdText = l_format_dependency_commands(kA, kB, dbT, loadedPathsAA, mt)
               end
            end
         end

         if (cmdText) then
            mcp:report{msg="e_Failed_Load_2", kA = concatTbl(kA, ", "), kB = concatTbl(kB, " "),
                       suggest_cmd = cmdText}
         else
            -- Fallback to original behavior if cache access fails
            mcp:report{msg="e_Failed_Load_2", kA = concatTbl(kA, ", "), kB = concatTbl(kB, " "),
                       suggest_cmd = ""}
         end
      end
      dbg.fini("l_error_on_missing_loaded_modules")
   end
end

function M.name(self)
   return self.my_name
end

--------------------------------------------------------------------------
-- Return the sType.
-- @param self A MainControl object
function M.MNameType(self)
   return self.my_sType
end

--------------------------------------------------------------------------
-- Return the sType for prereq/prereg_any
-- @param self A MainControl object
function M.MNamePrereqType(self)
   return (cosmic:value("MODULES_AUTO_HANDLING") == "yes") and self.my_sType or "mt"
end

--------------------------------------------------------------------------
-- Return the tcl_mode.
-- @param self A MainControl object
function M.tcl_mode(self)
   return self.my_tcl_mode
end

--------------------------------------------------------------------------
-- Convert MC name to MC Object.
-- @param nameTbl Name to MC object table.
-- @param name    Name of an MC objects.
local function l_valid_name(nameTbl, name)
   return nameTbl[name] or nameTbl.default
end

--------------------------------------------------------------------------
-- Set the type (or mode) of the current MainControl object.
-- @param self A MainControl object
function M._setMode(self, mode)
   self._mode = mode
end

--------------------------------------------------------------------------
-- The Factory builder for the MainControl Class.
-- @param name the name of the derived object.
-- @param[opt] mode An optional mode for building the *access* object.
-- @return A derived MainControl Object.
local s_nameTbl          = false
function M.build(name,mode)

   if (not s_nameTbl) then
      local MCLoad        = require('MC_Load')
      local MCUnload      = require('MC_Unload')
      local MCMgrLoad     = require('MC_MgrLoad')
      local MCRefresh     = require('MC_Refresh')
      local MCShow        = require('MC_Show')
      local MCAccess      = require('MC_Access')
      local MCSpider      = require('MC_Spider')
      local MCComputeHash = require('MC_ComputeHash')
      local MCCheckSyntax = require('MC_CheckSyntax')
      local MCQuiet       = require('MC_Quiet')

      s_nameTbl = {
         ["load"]         = MCLoad,        -- Normal loading of modules
         ["mgrload"]      = MCMgrLoad,     -- for collections (loads in modules are ignored)
         ["unload"]       = MCUnload,      -- Unload modules
         ["refresh"]      = MCRefresh,     -- for subshells, sets the aliases again
         ["computeHash"]  = MCComputeHash, -- Generate a hash value for the contents of the module
         ["refresh"]      = MCRefresh,     -- for subshells, sets the aliases again
         ["show"]         = MCShow,        -- show the module function instead.
         ["access"]       = MCAccess,      -- for whatis, help
         ["spider"]       = MCSpider,      -- Process module files for spider operations
         ["checkSyntax"]  = MCCheckSyntax, -- Check the syntax of a module, load, prereq, etc
                                           -- are ignored.
         ["quiet"]        = MCQuiet,       -- All operations are NO Ops (A.K.A quiet)
         
      }
   end

   local o                = l_valid_name(s_nameTbl, name):create()
   o:_setMode(mode or name)
   o.__first   =  0
   o.__last    = -1
   o.__moduleQ = {}

   dbg.print{"MC:build: Setting mcp to ", o:name(),"\n"}
   return o
end

-------------------------------------------------------------------
-- Module Queue functions

function M.pushModule(self, value)
   local last  = self.__last + 1
   self.__last = last
   self.__moduleQ[last] = value
end

function M.popModule(self)
   local first = self.__first
   if (first > self.__last) then
      LmodError{msg="e_BrokenQ"}
   end
   local value           = self.__moduleQ[first]
   self.__moduleQ[first] = nil                   -- to allow garbage collection
   self.__first          = first + 1
   return value
end

function M.isEmpty(self)
   return self.__last < self.__first
end

-------------------------------------------------------------------
-- Setenv / Unsetenv Functions
-------------------------------------------------------------------

-------------------------------------------------------------------
-- Set an environment variable.
-- @param self A MainControl object.
-- @param name the environment variable name.
-- @param value the environment variable value.
-- @param respect If true, then respect the old value.
function M.setenv(self, argT)

   local name    = argT[1] 
   local value   = argT[2]
   local respect = argT[3] or false

   dbg.start{"MainControl:setenv(\"",name,"\", \"",value,"\", \"",
              respect,"\")"}

   if (respect and getenv(name)) then
      dbg.print{"Respecting old value"}
      dbg.fini("MainControl:setenv")
      return
   end

   local frameStk = FrameStk:singleton()
   local varT     = frameStk:varT()
   if (varT[name] == nil) then
      varT[name] = Var:new(name)
   end
   varT[name]:set(tostring(value))
   dbg.fini("MainControl:setenv")
end

-------------------------------------------------------------------
-- Set an environment variable.
-- This function just sets the name with value in the current env.
function M.setenv_env(self, argT) --name, value, respect)
   local name    = argT[1]
   local value   = argT[2]
   local respect = argT[3]

   dbg.start{"MainControl:setenv_env(\"",name,"\", \"",value,"\", \"",
              respect,"\")"}
   if (value == false) then
      value = nil
   end
   posix.setenv(name, value, true)
   dbg.fini("MainControl:setenv_env")
end


--------------------------------------------------------------------------
-- Unset an environment variable.
-- @param self A MainControl object.
-- @param name the environment variable name.
-- @param value the environment variable value.
-- @param respect If true, then respect the old value.
function M.unsetenv(self, argT)
   local name    = argT[1]
   local value   = argT[2]
   local respect = argT[3] or false

   dbg.start{"MainControl:unsetenv(\"",name,"\", \"",value,"\")"}

   if (respect and getenv(name) ~= value) then
      dbg.print{"Respecting old value"}
      dbg.fini("MainControl:unsetenv")
      return
   end

   local frameStk  = FrameStk:singleton()
   local varT      = frameStk:varT()
   if (varT[name] == nil) then
      varT[name]   = Var:new(name)
   end
   varT[name]:unset(argT.__cmdName)

   -- Unset stack variable if it exists
   local stackName = l_createStackName(name)
   if (varT[stackName]) then
      varT[name]:unset(argT.__cmdName)
   end
   dbg.fini("MainControl:unsetenv")
end

-------------------------------------------------------------------
-- stack: push and pop
-------------------------------------------------------------------

--------------------------------------------------------------------------
-- Set an environment variable and remember previous values in a stack.
-- @param self A MainControl object.
-- @param name the environment variable name.
-- @param value the environment variable value.
function M.pushenv(self, argT)
   local name    = argT[1]
   local value   = argT[2]

   dbg.start{"MainControl:pushenv(\"",name,"\", \"",value,"\")"}

   local stackName = l_createStackName(name)
   local v         = getenv(name)
   local v64       = "false"
   if (v) then
      v64          = encode64(tostring(v))
   end
   if (value) then
      value = tostring(value)
   end

   local frameStk = FrameStk:singleton()
   local varT     = frameStk:varT()

   ------------------------------------------------------------
   -- Set user env. variable to value
   if (varT[name] == nil) then
      varT[name] = Var:new(name)
   end
   varT[name]:set(value)

   ------------------------------------------------------------
   -- Save away old value on stackName

   local nodups   = false
   
   local oldV     = getenv(stackName)
   if (oldV == nil) then
      -- Save old variable at top of stack that is only 1 deep.
      varT[stackName] = Var:new(stackName, v64, nodups, ":")
   else
      dbg.print{"oldV: \"",oldV,"\", value: \"",value,"\",v64: ",v64,"\n"}
      if (varT[stackName] == nil) then
         varT[stackName] = Var:new(stackName)
      end
      local priority = 0
      varT[stackName]:prepend(v64, nodups, priority)
   end

   dbg.fini("MainControl:pushenv")
end

--------------------------------------------------------------------------
-- The reverse action of pushenv.  It pops the old value off of the stack
-- and set the *name* to the previous value from the stack.
-- @param self A MainControl object.
-- @param name the environment variable name.
-- @param value the environment variable value.
function M.popenv(self, argT)
   local name    = argT[1]
   local value   = argT[2]
   dbg.start{"MainControl:popenv(\"",name,"\", \"",value,"\")"}

   local stackName = l_createStackName(name)
   local frameStk  = FrameStk:singleton()
   local varT      = frameStk:varT()

   if (varT[stackName] == nil) then
      varT[stackName] = Var:new(stackName)
   end


   local v64 = varT[stackName]:pop()
   dbg.print{"stackName: ", stackName,", varT[stackName]:expand(): \"",varT[stackName]:expand() ,"\", v64: \"",v64,"\"\n"}
   local v   = nil
   if (v64 == "false") then
      v = false
   elseif (v64) then
      v = decode64(v64)
   end
   dbg.print{"v: ", v,"\n"}

   if (varT[name] == nil) then
      varT[name] = Var:new(name)
   end

   varT[name]:set(v)

   dbg.fini("MainControl:popenv")
end

-------------------------------------------------------------------
-- Path Modification Functions
-------------------------------------------------------------------


-------------------------------------------------------------------
-- Prepend to a path like variable.
-- @param self A MainControl object
-- @param t A table containing { name, value, nodups=v1, priority=v2}
function M.prepend_path(self, argT)
   local name     = argT[1]
   local value    = argT[2]
   local nodups   = not allow_dups( not argT.nodups)
   local priority = (-1)*(argT.priority or 0)
   local delim    = argT.delim or ":"

   local frameStk = FrameStk:singleton()
   local varT     = frameStk:varT()

   dbg.start{"MainControl:prepend_path{\"",name,"\", \"",value,
             "\", delim=\"",delim,"\", nodups=\"",nodups,
             "\", priority=",priority,
             "}"}

   if (varT[name] == nil) then
      varT[name] = Var:new(name, nil, nodups, delim)
   end

   -- Do not allow dups on MODULEPATH like env vars.
   nodups = (name == ModulePath) or nodups

   varT[name]:prepend(tostring(value), nodups, priority)
   dbg.fini("MainControl:prepend_path")
end

--------------------------------------------------------------------------
-- Append to a path like variable.
-- @param self A MainControl object
-- @param argT A table containing { name, value, nodups=v1, priority=v2}
function M.append_path(self, argT)
   local name     = argT[1]
   local value    = argT[2]
   local nodups   = not allow_dups( not argT.nodups)
   local priority = argT.priority or 0
   local delim    = argT.delim or ":"

   local frameStk = FrameStk:singleton()
   local varT     = frameStk:varT()

   dbg.start{"MainControl:append_path{\"",name,"\", \"",value,
             "\", delim=\"",delim,"\", nodups=\"",nodups,
             "\", priority=",priority,
             "}"}

   -- Do not allow dups on MODULEPATH like env vars.
   nodups = name == ModulePath or nodups

   if (varT[name] == nil) then
      varT[name] = Var:new(name, false, nodups, delim)
   end

   varT[name]:append(tostring(value), nodups, priority)
   dbg.fini("MainControl:append_path")
end

--------------------------------------------------------------------------
-- Remove an entry from a path like variable.
-- @param self A MainControl object
-- @param argT A table containing { name, value, nodups=v1, priority=v2, where=v3, force=v4}
function M.remove_path(self, argT)
   local name     = argT[1]
   local value    = argT[2]
   local nodups   = not allow_dups( not argT.nodups)
   local priority = argT.priority or 0
   local delim    = argT.delim or ":"
   local where    = argT.where
   local force    = argT.force

   local frameStk = FrameStk:singleton()
   local varT     = frameStk:varT()

   dbg.start{"MainControl:remove_path{\"",name,"\", \"",value,
             "\", delim=\"",delim,"\", nodups=",nodups,
             ", priority=",priority,
             ", where=",where,
             ", force=",force,
             "}"}

   -- Do not allow dups on MODULEPATH like env vars.
   nodups = (name == ModulePath) or nodups

   if (varT[name] == nil) then
      varT[name] = Var:new(name,nil, nodups, delim)
   end
   varT[name]:remove(tostring(value), where, priority, nodups, force)
   dbg.fini("MainControl:remove_path")
end

--------------------------------------------------------------------------
-- Remove an entry from a path-like variable.  This version is the reverse
-- of a prepend_path.
-- @param self A MainControl object
-- @param t A table containing { name, value, nodups=v1, priority=v2}
function M.remove_path_first(self, t)
   t.where = "first"
   M.remove_path(self, t)
end

-- Remove an entry from a path-like variable.  This version is the reverse
-- of a append_path.
-- @param self A MainControl object
-- @param t A table containing { name, value, nodups=v1, priority=v2}
function M.remove_path_last(self, t)
   t.where = "last"
   M.remove_path(self, t)
end



--------------------------------------------------------------------------
-- Set a shell alias.  This function can handle a single value for both
-- bash and C-shell.
-- @param self A MainControl Object.
-- @param name the environment variable name.
-- @param value the environment variable value.
function M.set_alias(self, name, value)
   name = (name or ""):trim()
   dbg.start{"MainControl:set_alias(\"",name,"\", \"",value,"\")"}

   l_check_for_valid_alias_name("set_alias",name)


   local frameStk = FrameStk:singleton()
   local varT     = frameStk:varT()

   if (varT[name] == nil) then
      varT[name] = Var:new(name)
   end
   varT[name]:setAlias(value)
   dbg.fini("MainControl:set_alias")
end

--------------------------------------------------------------------------
-- Unset a shell alias.
-- @param self A MainControl Object.
-- @param name the environment variable name.
-- @param value the environment variable value.
function M.unset_alias(self, name, value)
   name = (name or ""):trim()
   dbg.start{"MainControl:unset_alias(\"",name,"\", \"",value,"\")"}

   local frameStk = FrameStk:singleton()
   local varT     = frameStk:varT()

   if (varT[name] == nil) then
      varT[name] = Var:new(name)
   end
   varT[name]:unsetAlias()
   dbg.fini("MainControl:unset_alias")
end


--------------------------------------------------------------------------
-- Set a shell function for bash and a csh alias.
-- @param self A MainControl Object.
-- @param name the environment variable name.
-- @param value the environment variable value.
function M.set_shell_function(self, name, bash_function, csh_function)
   name = (name or ""):trim()
   dbg.start{"MainControl:set_shell_function(\"",name,"\", \"",bash_function,"\"",
             "\", \"",csh_function,"\""}


   l_check_for_valid_alias_name("set_shell_function",name)

   local frameStk = FrameStk:singleton()
   local varT     = frameStk:varT()

   if (varT[name] == nil) then
      varT[name] = Var:new(name)
   end
   varT[name]:setShellFunction(bash_function, csh_function)
   dbg.fini("MainControl:set_shell_function")
end

--------------------------------------------------------------------------
-- Unset a shell function for bash and a csh alias.
-- @param self A MainControl Object.
-- @param name the environment variable name.
-- @param value the environment variable value.
function M.unset_shell_function(self, name, bash_function, csh_function)
   name = (name or ""):trim()
   dbg.start{"MainControl:unset_shell_function(\"",name,"\", \"",bash_function,"\"",
             "\", \"",csh_function,"\""}

   local frameStk = FrameStk:singleton()
   local varT     = frameStk:varT()

   if (varT[name] == nil) then
      varT[name] = Var:new(name)
   end
   varT[name]:unsetShellFunction()
   dbg.fini("MainControl:unset_shell_function")
end


--------------------------------------------------------------------------
-- Return the type (or mode) of the current MainControl object.
-- @param self A MainControl object
function M.mode(self)
   return self._mode
end

--------------------------------------------------------------------------
-- Place a string that will be executed when the output from Lmod eval'ed.
-- @param self A MainControl object
-- @param t A table containing A mode array and a command.
function M.execute(self, t)
   dbg.start{"MainControl:execute(t)"}
   local a      = t.modeA or {}
   local myMode = self:mode()
   local Exec   = require("Exec")

   for i = 1,#a do
      if (myMode == a[i] or a[i]:lower() == "all" ) then
         local exec   = Exec:exec()
         exec:register(t.cmd)
         break
      end
   end
   dbg.fini("MainControl:execute")
end

--------------------------------------------------------------------------
-- Return the user's shell
-- @param self A MainControl object
function M.myShellName(self)
   return Shell and Shell:name() or "bash"
end

function M.myShellType(self)
   local myType = Shell and Shell:type() or "sh"
   return myType
end


--------------------------------------------------------------------------
-- Return the current file name.
-- @param self A MainControl object
function M.myFileName(self)
   local frameStk = FrameStk:singleton()
   return frameStk:fn()
end

--------------------------------------------------------------------------
-- Return the full name of the current module.  Typically name/version.
-- @param self A MainControl object.
function M.myModuleFullName(self)
   local frameStk = FrameStk:singleton()
   return frameStk:fullName()
end

--------------------------------------------------------------------------
-- Return the user name of the current module.  This is the name the user
-- specified.  It could a full name (name/version) or just the name.
-- @param self A MainControl object.
function M.myModuleUsrName(self)
   local frameStk = FrameStk:singleton()
   return frameStk:userName()
end

--------------------------------------------------------------------------
-- Return the user name and the true loaded name when a dot-hidden alias
-- load occurred; otherwise both values are the same.
-- @param self A MainControl object.
function M.myModuleUsrAndAliasName(self)
   local usr = self:myModuleUsrName()
   local frameStk = FrameStk:singleton()
   local mname = frameStk:mname()
   if (mname and mname:dotHiddenAliasLoad()) then
      return usr, self:myModuleFullName()
   end
   return usr, usr
end

--------------------------------------------------------------------------
-- Return the name of the modules.  That is the name of the module w/o a
-- version.
-- @param self A MainControl object
function M.myModuleName(self)
   local frameStk = FrameStk:singleton()
   return frameStk:sn()
end

--------------------------------------------------------------------------
-- Return the version if any.  If there is no version, for example a meta
-- module then the version is "".
-- @param self A MainControl object
function M.myModuleVersion(self)
   local frameStk = FrameStk:singleton()
   return frameStk:version()
end

local function l_generateMsg(kind, label, argA)
   local sA     = {}
   local twidth = TermWidth()
   if (argA.n == 1 and type(argA[1]) == "table") then
      local msg = nil
      local key = nil
      local t   = argA[1]
      if (t.literal_msg) then
         msg = t.literal_msg
      else
         key = t.msg
         msg = i18n(key, t)
      end
      if (not msg) then
         msg = "Unknown Error Message with unknown key: \"".. key .. "\""
      end
      msg       = hook.apply("errWarnMsgHook", kind, key, msg, t) or msg
      sA[#sA+1] = buildMsg(twidth, pack(label, msg))
   else
      local ssA = {}
      ssA[#ssA+1] = label
      local n = #argA
      for i = 1, n do
         ssA[#ssA+1] = argA[i]
      end
      ssA.n = n+1
      sA[#sA+1] = buildMsg(twidth, ssA)
   end
   return sA
end

function M.msg_raw(self, ...)
   if (quiet()) then
      return
   end
   local argA   = pack(...)
   for i = 1,argA.n do
      io.stderr:write(argA[i])
   end
end


--------------------------------------------------------------------------
-- Print msgs to stderr.
-- @param self A MainControl object.
function M.message(self, ...)
   build_i18n_messages()
   if (quiet()) then
      return
   end
   local sA     = {}
   local twidth = TermWidth()
   local argA   = pack(...)
   if (argA.n == 1 and type(argA[1]) == "table") then
      local t   = argA[1]
      local key = t.msg
      local msg = i18n(key, t) or "Unknown Message"
      msg       = hook.apply("errWarnMsgHook", "lmodmessage", key, msg, t) or msg
      sA[#sA+1] = buildMsg(twidth, {n=1, msg})
   else
      sA[#sA+1] = buildMsg(twidth, pack(...))
   end
   io.stderr:write(concatTbl(sA,""),"\n")
end

--------------------------------------------------------------------------
-- Print msgs, traceback then set warning flag.
-- @param self A MainControl object.
function M.warning(self, ...)
   build_i18n_messages()
   if (not quiet() and  haveWarnings()) then
      local label = colorize("red", i18n("warnTitle",{}))
      local argA  = pack(...)
      local sA    = l_generateMsg("lmodwarning", label, argA)
      sA[#sA+1]   = "\n"
      sA[#sA+1]   = moduleStackTraceBack()
      sA[#sA+1]   = "\n"
      io.stderr:write(concatTbl(sA,""),"\n")
   end
end

--------------------------------------------------------------------------
-- Print msgs, traceback then exit.
-- @param self A MainControl object.
function M.error(self, ...)
   build_i18n_messages()
   -- Check for user loads that failed.
   if (next(s_missingModuleT) ~= nil) then
      
      local frameStk = FrameStk:singleton()
      local mt       = frameStk:mt()
      local aa       = {}
      local bb       = {}
      -- Collect missing modules into a temporary array for sorting
      local tmpA     = {}
      for userName, v in pairs(s_missingModuleT) do
         local sn = v.sn
         dbg.print{"MainControl:error: sn: ",sn, ", userName: ", userName,", showName: ",v.showName,"\n"}
         if (not (sn and mt:have(sn,"active")) ) then
            tmpA[#tmpA + 1] = {showName = v.showName, userName = userName}
         end
      end
      -- Sort by userName for deterministic output order
      table.sort(tmpA, function(a, b) return a.userName < b.userName end)
      -- Extract sorted values back to aa and bb
      for i = 1, #tmpA do
         aa[i] = tmpA[i].showName
         bb[i] = tmpA[i].userName
      end
      s_missingModuleT = {}
      if (next(aa) ~= nil) then
         l_error_on_missing_loaded_modules(aa, bb)
      end
   end

   local label = colorize("red", i18n("errTitle", {}))
   local argA  = pack(...)
   local sA    = l_generateMsg("lmoderror", label, argA)
   sA[#sA+1]   = "\n"

   local noTraceBack = false
   if (argA.n == 1 and type(argA[1] == "table")) then
      noTraceBack = argA[1].noTraceBack
   end

   if (not noTraceBack) then
      local a = concatTbl(stackTraceBackA,"")
      if (a:len() > 0) then
         sA[#sA+1] = a
         sA[#sA+1] = "\n"
      end
      sA[#sA+1]     = moduleStackTraceBack()
      sA[#sA+1]     = "\n"
   end

   LmodErrorExit(concatTbl(sA,""),"\n")
end

--------------------------------------------------------------------------
-- The quiet function.
-- @param self A MainControl object
function M.quiet(self)
   -- very Quiet !!!
end

function M.mustLoad(self)
   dbg.start{"MainControl:mustLoad()"}

   local aa, bb = l_compareRequestedLoadsWithActual()
   l_error_on_missing_loaded_modules(aa, bb)

   dbg.fini("MainControl:mustLoad")
end

function M.registerDependencyCk(self)
   s_performDepCk = true
end

function M.performDependencyCk(self)
   if (not s_performDepCk) then return end
   dbg.start{"MainControl:performDependencyCk()"}
   local hub = Hub:singleton()
   local t0 = epoch()
   hub:dependencyCk()
   timer:deltaT("performDependencyCk", epoch() - t0)
   self:reportMissingDepModules()
   dbg.fini("MainControl:performDependencyCk")
end

function M.dependencyCk(self,mA)
   if (dbg.active()) then
      local s = mAList(mA)
      dbg.start{"MainControl:dependencyCk(mA={"..s.."})"}
   end

   local frameStk = FrameStk:singleton()
   local mt       = frameStk:mt()
   local fullName = frameStk:fullName()
   for i = 1,#mA do
      local mname = mA[i]
      if (not mname:isloaded() ) then
         local a = s_missDepT[mname:userName()] or {}
         a[#a+1] = fullName
         s_missDepT[mname:userName()] = a
      end
   end

   dbg.fini("MainControl:dependencyCk")
   return {}
end

function M.dependencyCk_any(self, mA)
   if (dbg.active()) then
      local s = mAList(mA)
      dbg.start{"MainControl:dependencyCk_any(mA={"..s.."})"}
   end

   local frameStk = FrameStk:singleton()
   local mt       = frameStk:mt()
   local fullName = frameStk:fullName()
   local child_sn = mt:pop_depends_on_any_ck(frameStk:sn())
   if (not child_sn) then
      return {}
   end

   for i = 1,#mA do
      repeat
         local mname = mA[i]
         local sn    = mname:sn()
         if (not sn) then break end
         if (child_sn ~= sn) then break end
         if (not mname:isloaded() ) then
            local a = s_missDepT[mname:userName()] or {}
            a[#a+1] = fullName
            s_missDepT[mname:userName()] = a
         end
      until true
   end

   dbg.fini("MainControl:dependencyCk_any")
   return {}
end

function M.addMissingDepModule(self, userName, name)
   local a = s_missDepT[userName] or {}
   a[#a+1] = name
   s_missDepT[userName] = a
end

function M.reportMissingDepModules(self)
   local t = s_missDepT
   if (next(t) ~= nil) then
      local a           = {}
      local term_width  = TermWidth()
      local border      = colorize("red",string.rep("-", term_width-1))

      for k,v in pairsByKeys(t) do
         local s = concatTbl(v,", ")
         a[#a+1] = k .. " (required by: "..s..")"
      end
      LmodWarning{msg="w_MissingModules",border=border,missing=concatTbl(a,", ")}
   end
end


-------------------------------------------------------------------
-- depends_on() a list of modules.  This is short hand for:
--
--   if (not isloaded("name")) then load("name") end
--

function M.depends_on(self, mA)
   if (dbg.active()) then
      local s = mAList(mA)
      dbg.start{"MainControl:depends_on(mA={"..s.."})"}
   end

   local mB = {}
   local mt = FrameStk:singleton():mt()


   for i = 1,#mA do
      local mname = mA[i]
      if (not mname:isloaded()) then
         mname:set_depends_on_flag(true)
         mB[#mB + 1] = mname
      else
         mt:safely_incr_ref_count(mname)
      end
   end

   l_registerUserLoads(mB)
   local a = self:load(mB)

   
   ------------------------------------------------------------
   -- Get a new mt after all loads then record dependencies
   -- in the sn found from myModuleName()
   mt = FrameStk:singleton():mt()
   mt:record_depends_on(myModuleName(), mA)


   self:registerDependencyCk()

   dbg.fini("MainControl:depends_on")
   return a
end

-------------------------------------------------------------------
-- depends_on_any() a list of modules.  This is short hand for:
--
--   if (isloaded(any("mod1, mod2, modN"))) then done end
--   if no module loadable error.
--

function M.depends_on_any(self, mA)
   if (dbg.active()) then
      local s                                  = mAList(mA)
      dbg.start{"MainControl:depends_on_any(mA = {"..s.."})"}
   end

   local mt = FrameStk:singleton():mt()
   local mB = {}
   local sn = myModuleName()

   for i = 1,#mA do
      local mname = mA[i]
      if (mname:isloaded()) then
         mt:safely_incr_ref_count(mname)
         mt:save_depends_on_any(sn, mname:sn())
         mt:record_depends_on_any(sn, mA)
         self:registerDependencyCk()
         dbg.fini("MainControl:depends_on_any")
         return {}
      elseif (mname:sn()) then
         mB[#mB + 1] = mname
      end
   end

   if (next(mB) == nil) then
      local s = mAList(mA)
      LmodError{msg="e_Failed_depends_any", module_list=s}
   end

   local mC = {mB[1]}
   local mname = mC[1]
   mname:set_depends_on_flag(true)
   mt:save_depends_on_any(sn, mname:sn())

   l_registerUserLoads(mC)
   local a = self:load(mC)

   ------------------------------------------------------------
   -- Get a new mt after all loads then record the depends on any 
   -- in the sn found from myModuleName()
   mt = FrameStk:singleton():mt()
   mt:record_depends_on_any(sn, mA)

   self:registerDependencyCk()
   dbg.fini("MainControl:depends_on_any")
   return a
end

-------------------------------------------------------------------
-- forgo a list of modules.  This is the reverse of depends_on()
--
--   if (not isloaded("name")) then load("name") end
--
-- On unload forgo() unloads when the ref count is zero.
function M.forgo(self,mA)
   local hub = Hub:singleton()
   if (dbg.active()) then
      local s = mAList(mA)
      dbg.start{"MainControl:forgo(mA={"..s.."})"}
   end

   local mt = FrameStk:singleton():mt()
   local mB = {}
   for i = 1,#mA do
      repeat
         local mname      = mA[i]
         local sn         = mname:sn()
         if (not sn) then break end
         local ref_count  = mt:decr_ref_count(sn)
         if (ref_count and ref_count < 1) then
            mB[#mB+1] = mname
         end
      until true
   end

   l_unRegisterUserLoads(mB)
   local aa     = unload_internal(mB)
   dbg.fini("MainControl:forgo")
   return aa
end

-------------------------------------------------------------------
-- forgo_any a list of modules.  This is the reverse of depends_on_any()
--
-- On unload forgo_any() unloads when the ref count is zero.
function M.forgo_any(self,mA)
   local hub = Hub:singleton()
   if (dbg.active()) then
      local s = mAList(mA)
      dbg.start{"MainControl:forgo_any(mA={"..s.."})"}
   end

   local mt       = FrameStk:singleton():mt()
   local mB       = {}
   local child_sn = mt:pop_depends_on_any(myModuleName())
   if (child_sn == nil) then
      return {}
   end
   local found = false
   for i = 1,#mA do
      repeat
         local mname = mA[i]
         local sn    = mname:sn()
         if (not sn) then break end
         if (child_sn ~= sn) then break end

         ------------------------------------------------------------
         -- if here then we have found the only depends_on_any module
         -- that we "loaded". So decrement ref count and unload when
         -- the ref_count is zero.
         -- Finally stop looking over mname's in mA

         local ref_count  = mt:decr_ref_count(sn)
         if (ref_count and ref_count < 1) then
            mB[#mB+1] = mname
         end
         found = true
      until true
      if (found) then break end
   end

   l_unRegisterUserLoads(mB)
   local aa     = unload_internal(mB)
   dbg.fini("MainControl:forgo_any")
   return aa
end

-------------------------------------------------------------------
-- Record the user's original module request for error suggestions.
-- Called from cmdfuncs.lua before any load processing begins.
-- Nested load_usr calls (e.g., family swaps) bypass cmdfuncs and
-- therefore cannot corrupt this data.
-- @param self A MainControl object
-- @param mA A array of MName objects.
function M.setOriginalUserRequest(self, mA)
   s_allRequestedT = {}
   for i = 1, #mA do
      local mname = mA[i]
      s_allRequestedT[#s_allRequestedT + 1] = {
         userName = mname:userName(),
         showName = mname:show(),
         mname    = mname
      }
   end
end

-------------------------------------------------------------------
-- Load a list of modules.  Check to see if the user requested
-- modules were actually loaded.G
-- @param self A MainControl object
-- @param mA A array of MName objects.
-- @return An array of statuses
function M.load_usr(self, mA)
   if (dbg.active()) then
      local s = mAList(mA)
      dbg.start{"MainControl:load_usr(mA={"..s.."})"}
   end
   local frameStk = FrameStk:singleton()
   if (checkSyntaxMode() and frameStk:count() > 1) then
      dbg.print{"frameStk:count(): ",frameStk:count(),"\n"}
      dbg.fini("MainControl:load_usr")
      return {}
   end

   l_registerUserLoads(mA)
   local a = self:load(mA)
   dbg.fini("MainControl:load_usr")
   return a
end


--------------------------------------------------------------------------
-- Build a list of user names based on mA.
-- @param mA List of MName objects
function mAList(mA)
   local a = {}
   for i = 1, #mA do
      a[#a + 1] = mA[i]:userName()
   end
   return concatTbl(a, ", ")
end

function M.load(self, mA)
   if (dbg.active()) then
      local s = mAList(mA)
      dbg.start{"MainControl:load(mA={"..s.."})"}
   end

   local hub = Hub:singleton()
   local a   = hub:load(mA)

   if (not quiet()) then
      self:registerAdminMsg(mA)
   end

   dbg.fini("MainControl:load")
   return a
end

function M.load_any(self, mA)
   if (dbg.active()) then
      local s = mAList(mA)
      dbg.start{"MainControl:load_any(mA={"..s.."})"}
   end
   local b
   local uA     = {}
   local result = false

   for i = 1, #mA do
      local mname = mA[i]
      b = self:try_load{mname}
      if (mname:isloaded()) then
         result = true
         break
      else
         uA[#uA+1] = mname:userName()
      end
   end

   if (not result) then
      LmodError{msg="e_Failed_Load_any", module_list=concatTbl(uA," ")}
   end

   dbg.fini("MainControl:load_any")
   return b
end



function M.mgrload(self, required, active)
   if (dbg.active()) then
      dbg.start{"MainControl:mgrload(required: ",required,", active=",active.userName,")"}
   end

   if (not required) then
      deactivateWarning()
   else
      activateWarning()
   end

   local status = Hub:singleton():mgrload(active)

   dbg.fini("MainControl:mgrload")
   return status
end

function M.mgr_unload(self, required, active)
   if (dbg.active()) then
      dbg.start{"MainControl:mgr_unload(required: ",required,", active=",active.userName,")"}
   end
   local status = unload_internal(MName:new("mt", active.userName))
   dbg.fini("MainControl:mgr_unload")
   return status
end



-------------------------------------------------------------------
-- Load a list of module but ignore any warnings.
-- @param self A MainControl object
-- @param mA A array of MName objects.
function M.try_load(self, mA)
   dbg.start{"MainControl:try_load(mA)"}
   self:load(mA)
   dbg.fini("MainControl:try_load")
end

-------------------------------------------------------------------
-- Unload a list modules.
-- @param self A MainControl object
-- @param mA A array of MName objects.
-- @return an array of statuses
function M.unload(self, mA)
   local hub = Hub:singleton()


   if (dbg.active()) then
      local s = mAList(mA)
      dbg.start{"MainControl:unload(mA={"..s.."})"}
   end

   l_unRegisterUserLoads(mA)
   local aa     = hub:unload(mA)
   dbg.fini("MainControl:unload")
   return aa
end

function M.build_unload(self)
   local mcp = MainControl.build("unload")
   dbg.print{"MC:build_unload: Setting mcp to ", mcp:name(),"\n"}
   return mcp
end

function M.do_not_build_unload(self)
   return self
end


-------------------------------------------------------------------
-- Unload a user requested list of modules.
-- @param self A MainControl object
-- @param mA A array of MName objects.
-- @param force if true then do not reload sticky modules.
-- @return an array of statuses.
function M.unload_usr(self, mA, force)
   dbg.start{"MainControl:unload_usr(mA)"}

   M.unload(self,mA)
   local hub = Hub:singleton()
   local aa = hub:reload_sticky(force)

   self:registerDependencyCk()

   dbg.fini("MainControl:unload_usr")
   return aa
end

-------------------------------------------------------------------
-- This load is used by Manager Load to ignore load inside a
-- module.
-- @param self A MainControl object
-- @param mA A array of MName objects.
function M.fake_load(self,mA)
   if (dbg.active()) then
      local s = mAList(mA)
      dbg.start{"MainControl:fake_load(mA={"..s.."})"}
      dbg.fini("MainControl:fake_load")
   end
end

function M.fake_depends_on(self,mA)
   if (dbg.active()) then
      local s = mAList(mA)
      dbg.start{"MainControl:fake_depends_on(mA={"..s.."})"}
   end

   local mt = FrameStk:singleton():mt()
   mt:record_depends_on(myModuleName(), mA)

   dbg.fini("MainControl:fake_depends_on")
end

function M.fake_depends_on_any(self,mA)
   if (dbg.active()) then
      local s = mAList(mA)
      dbg.start{"MainControl:fake_depends_on_any(mA={"..s.."})"}
   end

   local mt = FrameStk:singleton():mt()
   mt:record_depends_on_any(myModuleName(), mA)

   dbg.fini("MainControl:fake_depends_on_any")
end





--------------------------------------------------------------------------
-- Check the conflicts from *mA*.
-- @param self A MainControl object.
-- @param mA An array of MNname objects.
function M.conflict(self, mA)
   dbg.start{"MainControl:conflict(mA)"}

   local frameStk    = FrameStk:singleton()
   local mt          = frameStk:mt()
   local fullName    = frameStk:fullName()
   local optionTbl   = optionTbl()
   local a           = {}
   local dsConflicts = cosmic:value("LMOD_DOWNSTREAM_CONFLICTS")

   for i = 1, #mA do
      local mname    = mA[i]
      local userName = mname:conflictCk(mt)
      if ( userName ) then
         a[#a+1]  = userName
      end
   end

   ------------------------------------------------------------------------
   -- check for current conflicits

   if (#a > 0) then
      LmodError{msg="e_Conflict", name = fullName, module_list = concatTbl(a," ")}
   end

   ------------------------------------------------------------------------
   -- register downstream conflicts
   if (dsConflicts == "yes") then
      mt:registerConflicts(frameStk:mname(), mA)
   end

   dbg.fini("MainControl:conflict")
end

function M.removeConflict(self, mA)
   dbg.start{"MainControl:removeConflict(mA)"}
   local frameStk    = FrameStk:singleton()
   local mt          = frameStk:mt()
   local dsConflicts = cosmic:value("LMOD_DOWNSTREAM_CONFLICTS")

   if (dsConflicts == "yes" ) then
      mt:removeConflicts(frameStk:mname())
   end

   dbg.fini("MainControl:removeConflict")
end

--------------------------------------------------------------------------
-- Check the prereq from *mA*.
-- @param self A MainControl object.
-- @param mA An array of MNname objects.
function M.prereq(self, mA)
   dbg.start{"MainControl:prereq(mA)"}

   local frameStk  = FrameStk:singleton()
   local fullName  = frameStk:fullName()
   local optionTbl = optionTbl()

   local a = {}
   for i = 1, #mA do
      local v = mA[i]:prereq()
      if (v) then
         a[#a+1] = v
      end
   end

   dbg.print{"number found: ",#a,"\n"}
   if (#a > 0) then
      LmodError{msg="e_Prereq", name = fullName, module_list = concatTbl(a," ")}
   end
   dbg.fini("MainControl:prereq")
end

--------------------------------------------------------------------------
-- Check the prereq from *mA*.  If any of them are acceptable then return.
-- otherwise error out.
-- @param self A MainControl object.
-- @param mA An array of MNname objects.
function M.prereq_any(self, mA)
   dbg.start{"MainControl:prereq_any(mA)"}
   local frameStk  = FrameStk:singleton()
   local fullName  = frameStk:fullName()
   local optionTbl = optionTbl()
   local found     = false
   local a         = {}

   for i = 1, #mA do
      local v, msg = mA[i]:prereq()
      if (not v) then
         found = true
         break
      end
      if (msg) then
         a[#a+1] = msg .."(\"" .. v .. "\")"
      else
         a[#a+1] = v
      end
   end

   if (not found) then
      LmodError{msg="e_Prereq_Any", name = fullName, module_list = concatTbl(a," ")}
   end
   dbg.fini("MainControl:prereq_any")
end

------------------------------------------------------------------------
-- Save away the modules that are in the same family.
-- @param oldName The old module name that is getting pushed out by *sn*.
-- @param sn The new module name.
function M.familyStackPush(oldName, sn)
   dbg.start{"familyStackPush(",oldName,", ", sn,")"}
   local frameStk       = FrameStk:singleton()
   local mt             = frameStk:mt()
   local old_userName   = mt:userName(oldName)
   dbg.print{"removing old sn: ",oldName,",old userName: ",old_userName,"\n"}

   if (old_userName) then
      s_loadT[old_userName] = nil
   end
   s_moduleStk[#s_moduleStk+1] = { sn=oldName, fullName = mt:fullName(oldName),
                                   userName = mt:userName(oldName)}
   s_moduleStk[#s_moduleStk+1] = { sn=sn,      fullName = mt:fullName(sn),
                                   userName = mt:userName(sn)}
   dbg.fini("familyStackPush")
end

function M.familyStackTop()
   local valueN = s_moduleStk[#s_moduleStk]
   local valueO = s_moduleStk[#s_moduleStk-1]
   return valueO, valueN
end


--------------------------------------------------------------------------
-- Pop the top two value off the stack.
-- @return the top two value on the stack.
function M.familyStackPop()
   local valueN = s_moduleStk[#s_moduleStk]
   remove(s_moduleStk)
   local valueO = s_moduleStk[#s_moduleStk]
   remove(s_moduleStk)
   return valueO, valueN
end

--------------------------------------------------------------------------
-- Check for an empty stack.
-- @return True if the stack is empty.
function M.processFamilyStack(fullName)
   if (next(s_moduleStk) ~= nil) then
      return fullName == s_moduleStk[#s_moduleStk].fullName
   end
   return false
end

--------------------------------------------------------------------------
-- Check for an empty stack.
-- @return True if the stack is empty.
function M.familyStackEmpty()
   return (next(s_moduleStk) == nil)
end

--------------------------------------------------------------------------
-- Process the family function.  The name of the module is found by the
-- *ModuleStack*.
-- @param self A MainControl object
-- @param name The name of the family
function M.family(self, name)
   dbg.start{"MainControl:family(",name,")"}
   local frameStk  = FrameStk:singleton()
   local mt        = frameStk:mt()
   local fullName  = frameStk:fullName()
   local mname     = MName:new("mt",fullName)
   local sn        = mname:sn()
   local optionTbl = optionTbl()
   local auto_swap = cosmic:value("LMOD_AUTO_SWAP")

   l_check_for_valid_name("family",name)

   local oldName = mt:getfamily(name)
   if (oldName ~= nil and oldName ~= sn and not expert() ) then
      if (auto_swap ~= "no") then
         self.familyStackPush(oldName, sn)
      else
         LmodError{msg="e_Family_Conflict", name = name, oldName = oldName, fullName = fullName}
      end
   end
   mt:setfamily(name,sn)
   dbg.fini("MainControl:family")
end

--------------------------------------------------------------------------
-- Unset the family name.
-- @param self A MainControl object
-- @param name A family name.
function M.unset_family(self, name)
   dbg.start{"MainControl:unset_family(",name,")"}
   local mt = FrameStk:singleton():mt()
   mt:unsetfamily(name)
   dbg.fini("MainControl:unset_family")
end

function M.registerAdminMsg(self, mA)
   dbg.start{"MainControl:registerAdminMsg(mA)"}
   local mt = FrameStk:singleton():mt()
   local t  = s_adminT
   readAdmin()
   for i = 1, #mA do
      local mname = mA[i]
      local sn    = mname:sn()
      if (mt:have(sn,"active")) then
         local fn       = mt:fn(sn)
         local fullName = mt:fullName(sn)
         local message  = nil
         local key

         for i = 1, #adminA do
            local pattern = adminA[i][1]
            if (fullName:find(pattern) or fullName == pattern) then
               message = adminA[i][2]
               key     = fullName
               break
            end
            if (pattern:sub(1,1) == '/' and (fn:find(pattern) or fn == pattern)) then
               message = adminA[i][2]
               key     = fullName
               break
            end
         end
         if (message) then
            t[key] = message
         end
      end
   end
   dbg.fini("MainControl:registerAdminMsg")
end

-------------------------------------------------------------------
-- Output any admin message collected from loading.
function M.reportAdminMsgs()
   dbg.start{"MainControl:reportAdminMsgs()"}
   local t = s_adminT
   if (next(t) ~= nil) then
      local term_width  = TermWidth()
      local bt
      local a       = {}
      local border  = colorize("red",string.rep("-", term_width-1))
      io.stderr:write(i18n("m_Module_Msgs",{border=border}))
      for k, v in pairsByKeys(t) do
         io.stderr:write("\n",k,":\n")
         a[1] = { " ", v}
         local maxLen = 0
         for line in v:split("\n") do
            maxLen = max(line:len(), maxLen)
         end
         if (maxLen < term_width - 1) then
            io.stderr:write(v,"\n")
         else
            bt = BeautifulTbl:new{tbl=a, wrapped=true, column=term_width-1}
            io.stderr:write(bt:build_tbl(), "\n")
         end
      end
      io.stderr:write(i18n("m_Module_Msgs_close",{border=border}))
   end
   dbg.fini("MainControl:reportAdminMsgs")
end

--------------------------------------------------------------------------
-- Provide a list of modules for sites to use
function M.loaded_modules(self)
   dbg.start{"MainControl::loaded_modules()"}
   local frameStk  = FrameStk:singleton()
   local mt        = frameStk:mt()
   local mA        = mt:list("fullName","active")
   dbg.fini("MainControl::loaded_modules")
   return mA
end


--------------------------------------------------------------------------
-- Set a property value
-- @param self A MainControl Object.
-- @param name A property name
-- @param value A property value.
function M.add_property(self, name, value)
   local frameStk  = FrameStk:singleton()
   local sn        = frameStk:sn()
   local mt        = frameStk:mt()
   l_check_for_valid_name("add_property",name)
   mt:add_property(sn, (name or ""):trim(), value)
end


--------------------------------------------------------------------------
-- Unset a property value
-- @param self A MainControl Object.
-- @param name A property name
-- @param value A property value.
function M.remove_property(self, name, value)
   local frameStk  = FrameStk:singleton()
   local sn        = frameStk:sn()
   local mt        = frameStk:mt()
   l_check_for_valid_name("remove_property",name)
   mt:remove_property(sn, (name or ""):trim(), value)
end


function purgeFlg()
   return s_purgeFlg
end


function M.purge(self,t)
   local force = false
   if (type(t) == "table") then
      force = t.force
   end

   local frameStk = FrameStk:singleton()
   local mt       = frameStk:mt()
   local totalA   = mt:list("short","any") --> "any" does not include "pending"

   if (#totalA < 1) then
      return
   end

   local mA = {}
   for i = #totalA,1,-1 do
      mA[#mA+1] = MName:new("mt",totalA[i])
   end
   s_purgeFlg = true
   unload_usr_internal(mA, force)
   s_purgeFlg = false

   dbg.fini("MainControl:Purge")
end




--------------------------------------------------------------------------
-- Return the tcl_mode.
-- @param self A MainControl object
function M.tcl_mode(self)
   return self.my_tcl_mode
end

--------------------------------------------------------------------------
-- Return True when in spider mode.  This version is always false.
-- @param self A MainControl object
function M.is_spider(self)
   dbg.start{"MainControl:is_spider()"}
   dbg.print{"This function is deprecated: use mode instead\n"}
   dbg.fini("MainControl:is_spider")
   return false
end

--------------------------------------------------------------------------
-- Perform a user requested inheritance.  Note that this function remains
-- the same depending on if it is a load or unload.
-- @param self A MainControl object
function M.inherit(self)
   dbg.start{"MainControl:inherit()"}
   local hub = Hub:singleton()
   hub.inheritModule()
   dbg.fini("MainControl:inherit")
end

function M.source_sh(self, shellName, script)
   dbg.start{"MainControl:source_sh(shellName: \"",shellName,"\", script: \"",script,"\")"}
   local frameStk     = FrameStk:singleton()
   local sn           = frameStk:sn()
   local mt           = frameStk:mt()
   local convertSh2MF = require("convertSh2MF")

   local mcmdA        = mt:get_sh2mf_cmds(sn, script)
   local success
   local msg
   if (mcmdA == nil) then
      success, msg, mcmdA = convertSh2MF(shellName, "lua", script)
      if (not success) then LmodError(msg) end
      mt:add_sh2mf_cmds(sn, script, mcmdA)
   end

   local whole = concatTbl(mcmdA,"\n")
   dbg.print{"whole:\n ",whole,"\n"}
   local status, msg = sandbox_run(whole)
   if (not status) then
      LmodError{msg="e_Unable_2_Load", name = mt:userName(sn), fn = mt:fn(sn), message = msg}
   end
   dbg.fini("MainControl:source_sh")
end

function M.un_source_sh(self, shellName, script)
   dbg.start{"MainControl:un_source_sh(shellName: \"",shellName,"\", script: \"",script,"\")"}
   local frameStk    = FrameStk:singleton()
   local sn          = frameStk:sn()
   local mt          = frameStk:mt()
   local mcmdA       = mt:get_sh2mf_cmds(sn, script)
   local whole       = concatTbl(mcmdA,"\n")
   dbg.print{"whole:\n ",whole,"\n"}
   local status, msg = sandbox_run(whole)
   if (not status) then
      LmodError{msg="e_Unable_2_Load", name = mt:userName(sn), fn = mt:nf(sn), message = msg}
   end
   dbg.fini("MainControl:un_source_sh")
end

function M.complete(self, shellName, name, args)
   dbg.start{"MainControl:complete(shellName: \"",shellName,"\", name: \"",name,"\", args: \"",args,"\""}
   if (myShellName() ~= shellName) then
      dbg.fini("MainControl:complete")
      return
   end

   local varT = FrameStk:singleton():varT()
   local n    = wrap_kind("complete", name)
   if (varT[n] == nil) then
      varT[n] = Var:new(n)
   end
   varT[n]:complete(args)
   dbg.fini("MainControl:complete")
end

function M.uncomplete(self, shellName, name, args)
   dbg.start{"MainControl:uncomplete(shellName: \"",shellName,"\", name: \"",name,"\", args: \"",args,"\""}
   if (myShellName() ~= shellName) then
      dbg.fini("MainControl:complete")
      return
   end
   local varT = FrameStk:singleton():varT()
   local n    = wrap_kind("complete", name)
   if (varT[n] == nil) then
      varT[n] = Var:new(n)
   end
   varT[n]:uncomplete()

   dbg.fini("MainControl:uncomplete")
end

function M.export_shell_function(self, funcName)
   dbg.start{"MainControl:export_shell_function(funcName: \"",funcName,"\")"}
   local varT = FrameStk:singleton():varT()
   local n    = wrap_kind("export_shell_function", funcName)
   if (varT[n] == nil) then
      varT[n] = Var:new(n)
   end
   varT[n]:export_shell_function()
   dbg.fini("MainControl:export_shell_function")
end

function M.unexport_shell_function(self,funcName)
   dbg.start{"MainControl:unexport_shell_function(funcName: \"",funcName,"\")"}
   local varT = FrameStk:singleton():varT()
   local n    = wrap_kind("export_shell_function", funcName)
   if (varT[n] == nil) then
      varT[n] = Var:new(n)
   end
   varT[n]:unset_shell_function()
   dbg.fini("MainControl:unexport_shell_function")
end

function M.color_banner(self,color)
   if (quiet()) then
      return
   end
   local term_width  = TermWidth()
   local border      = colorize(color or "red",string.rep("=", term_width-1))
   io.stderr:write(border,"\n")
end


function M.set_errorFunc(self, errorFunc)
   metaT = getmetatable(self).__index
   metaT.error = errFunc
end

function M.LmodBreak(self, msg)
   dbg.start{"MainControl:LmodBreak(msg=\"",msg,"\")"}
   local frameStk  = FrameStk:singleton()
   local tracing   = cosmic:value("LMOD_TRACING")
   local shell     = _G.Shell

   if (tracing == "yes") then
      local stackDepth = frameStk:stackDepth()
      local indent     = ("  "):rep(stackDepth+1)
      local b          = {}
      b[#b + 1]        = indent
      b[#b + 1]        = "LmodBreak called\n"
      shell:echo(concatTbl(b,""))
   end

   if (msg and msg ~= "") then
      LmodMessage(msg)
   end

   -- Remove this module from the list of user requested modules to load.
   local mname = frameStk:mname()
   dbg.print{"sn: ",mname:sn(),", userName: ",mname:userName(),"\n"}
   local force_unloading = true
   l_unRegisterUserLoads({mname},force_unloading)

   -- Copy the previous frameStk on top of the current stack
   -- Then throw an error to stop execution of the current module.
   frameStk:LmodBreak()
   error({code="LmodBreak"})
   dbg.fini("MainControl:LmodBreak")
end

function M.userInGroups(self, ...)
   local grps   = capture("groups 2> /dev/null")
   local argA   = pack(...)
   for g in grps:split("[ \n]") do
      for i = 1, argA.n do
         local group = argA[i]
         if (g == group) then
            return true
         end
      end
   end
   local userId = capture("id -u 2> /dev/null")
   local isRoot = tonumber(userId) == 0
   dbg.print{"M.userInGroups: isRoot: ",isRoot,", RT: ",optionTbl().rt,"\n"}
   if (isRoot and not optionTbl().rt) then
      return true
   end
   return false
end

function M.add_missing_module(self,userName, sn, showName)
   dbg.start{"mcp:add_missing_module(userName: ",userName,", sn: ",sn,", showName: ",showName,")"}

   s_missingModuleT[userName] = {sn=sn, showName=showName}
   dbg.fini("mcp:add_missing_module")
end

function M.remove_missing_module(self,userName, sn, showName)
   dbg.start{"mcp:remove_missing_module(userName: ",userName,", sn: ",sn,", showName: ",showName,")"}
   s_missingModuleT[userName] = nil
   s_missingModuleT[showName] = nil

   dbg.fini("mcp:remove_missing_module")
end

function M.haveDynamicMPATH(self)
   -- This function is non-empty when in Spider mode only
end

return M
