--[[
    Objects and methods providing the running context and services to be used by the sequencing scripts
    
     Ovidiu Scripa,      July  2004              
	-	CREATED and implemented the attribute objects and the string manipulation methods 
     Cristian Bonciu,    June-September 2004     
     -	July 2004, cosmetic changes - added more runtime checks
     -	August 5, 2004, renamed XXX1 to XXX for vector-like attributes and XXX to XXXM for matrix attributes 
     -	August 5, 2004, fixed the indexing problems (one based in LUA by default) for all index related methods
          (Get(M), checkIdx, Resize(M)), see doc quote below
          table.insert (table, [pos,] value) inserts element value at position pos in table, shifting up 
          other elements to open space, if necessary. 
          The default value for pos is n+1, where n is the size of the table (see 5.4)
     -	August 17, 2004, added ReportError based on traceC
     -	August 25,2004, added multiple grouping hint sets support     
     -	August 25,2004, added sorting support
     Cristian Bonciu,    September 27, 2004       
     -	added MergeStrings as per CRDB00024845: Need a standard function for seq. scripts 
		that will do a smart append
     Cristian Bonciu,    October 2, 2004           
     -	improved BestMatchInAttr to use inclusion matching and BestMatchForBodyRegion to split the input attribute 
		in words
     Cristian Bonciu,    October 7, 2004           
     -	 added support for configuring trace level 
		One should set AliDSTrace = 2 at the begining of the Execute() section of a regular script, in order to 
		transmit the information message to the engine 
     Allan Noordvyk,     October 15 2004         
     -	Added SanityOK function as common idiom.
     -	Fixed default for AddSortHint when 2nd parameter omitted (ascending sort is now default)
     Allan Noordvyk,     March 7, 2005           
     -	Added BestWordMatchInAttr function so that we can limit matches to
		whole words, rather than just substrings (e.g. matching "RT" in "RT KNEE" vs. "RT" in "VIRTUAL")
		c.f. CRDB00030342
     -	Revamped AddSortHint's 2nd argument meaning to make more sense and stop miscoding/misreading in
		calling routines.  True is no longer descending sort.
     -	Added comments to function.
		c.f. CRDB00030415
     Cristian Bonciu,	November 14, 2005 
     -	fixed AttrVal:IsMatrix (it is used now in Enhanced Multi-frame extraction & grouping scripts)
     -	added GetAttrDescription wrapper for GetAttrDescriptionC available in SP4 and higher 
     Cristian Bonciu, November 22, 2005         
     -	Added the ability to insert sequence properties when SetGroupingHints is called 
     Cristian Bonciu,	November 25, 2005         
     -	Added SetComparisonFlag member of the SortHint for specifying whether or not 
		distinct sorting hints should produce distinct groupings when the same grouping hint is used
		By default the sorting hint does not influence the grouping (useful when *All Images* is generated)
		and should be explicitly set on true by calling SetComparisonFlag(true) after the sort hint is formed
		from the desired tags if multiple sequences usign the same grouping hint and different sorting hints 
		need to be generated
     Michael McFarland ,	September 2006         
     -	Added support for multiple sorting hints per sequence. 
     Cristian Bonciu, September 2006         
     -	Restructured in two sections:
		1. Utilities, containing helper methods that may be modified by CSM team
		2. Engine Support, containing all declarations and support methods managed by ISG team
     Allan Noordvyk,	July 2, 2006 
     -	Added infrastructure to body region keyword recognition to allow for identification of standard
         DICOM body parts and common variants regardless of whether these also exist in the PACS body
         region table.
	-	Changes include altering the function "BestMatchForBodyRegion" and adding two new functions - 
		(1) "FindWordInPhrase" and (2) "AliasTableLookUp"
	Michael McFarland,	March 6, 2007
     -	Added support for removing images from sequences insite scripts; as well as deleting sequence-sort hints
	Mahesh Nagarajan,		August 8, 2007
	-	Added a new function GetFirst(); This function was written for cases where a Get(1) call fails if the 
		structure of the source variable is a matrix instead of single value. This function returns the first
		value of the variable regardless of what the structure is i.e. the details about the source structure 
		are hidden from the caller. Such a function has more overhead than a :Get(1) call so it is not currently 
		used by default.	 
	Mahesh Nagarajan,		September 27, 2007
	-	Changed the structue of the BestMatchForBodyRegion function; the database should not be checked unless 
		matches from the internal list are not found in any of the DICOM tags. This ensures that terms that
		exist in the body region database table for purely administrative purposes do not get picked up by the function
     Mahesh Nagarajan		November 14, 2007	   
	-	Added french translations of different body parts as aliases in the BestMatchForBodyRegion function as
		part of the French localization effort for sequencing.
	Mahesh Nagarajan		December 24, 2007
	- 	Made corrections to RemoveHint & RemoveHintAt functions to remove defunct variable accessibility problems and
		ensure smooth re-sequencing
	Michael McFarland		December 24, 2007		
	-	Introducing function pointers to engage version-specific functions for GetAttr, HasAttr & SetAttr; this ensures that
		depending on on the version of the sequencing engine, the right function is called. This also resolves errors found in
		test cases where the old sequencing engine tries to generate metadata with new scripts
	Mahesh Nagarajan		December 24, 2007
	-	Made changes to the SortingHint::AddEntry function; when a sorthint entry is added twice, the first instance of the hint
		should not be removed
--]]

-- get the engine version, so version-specific
-- function 'pointers' can be set appropriately
-- this is either set by new engine or set here

if( not ALIDS_SCRIPTENGINE_VERSION ) then
    ALIDS_SCRIPTENGINE_VERSION = 0 end

if( ALIDS_SCRIPTENGINE_VERSION == 0 ) then
-- ////////////////////////////////////////////////////////////////////////////
	GetAttr = function ( tag )
-- ////////////////////////////////////////////////////////////////////////////
		if( AliDSDebugEdit ) then
			local av = AttrValue:new()
			return av
		end
  
		if( not tag ) then
			ReportError("AttrValue::GetAttr","Invalid input tag")
			return nil
		end

		local tbl = GetAttrC(tag)
		if (tbl) then  
			local av = AttrValue:new()
			av:InitFromTbl(tbl)
			return av
		end
	end
else
-- ////////////////////////////////////////////////////////////////////////////
	GetAttr = function ( tag ) -- called by version 1+ script engines (uses lua-side caching)
-- ////////////////////////////////////////////////////////////////////////////
		if( AliDSDebugEdit ) then
			local av = AttrValue:new()
			return av
		end
  
		if( not tag ) then
			ReportError("AttrValue::GetAttr","Invalid input tag")
			return nil
		end

		if( gGetAttrCache[ tag ] ) then
			return gGetAttrCache[ tag ]
		end

		local tbl = GetAttrC(tag)
		if( tbl ) then  
			gGetAttrCache[ tag ] = AttrValue:new( )
			gGetAttrCache[ tag ]:InitFromTbl( tbl )
			gHasAttrCache[ tag ] = true
			return gGetAttrCache[ tag ]
		end
	end
end

if( ALIDS_SCRIPTENGINE_VERSION == 0 ) then
-- ////////////////////////////////////////////////////////////////////////////
	HasAttr = function ( tag )
-- ////////////////////////////////////////////////////////////////////////////
		if( not tag ) then
			ReportError("AttrValue::HasAttr","Invalid input tag")
			return false
		end

		return HasAttrC(tag)
	end
else
-- ////////////////////////////////////////////////////////////////////////////
	HasAttr = function ( tag ) -- called by version 1+ script engines (uses lua-side caching)
-- ////////////////////////////////////////////////////////////////////////////
		if( not tag ) then
			ReportError("AttrValue::HasAttr","Invalid input tag")
			return false
		end

		if( gHasAttrCache[ tag ] ) then
			return gHasAttrCache[ tag ]
		end

		gHasAttrCache[ tag ] = HasAttrC(tag)
		return gHasAttrCache[ tag ]
	end
end

if( ALIDS_SCRIPTENGINE_VERSION == 0 ) then
-- ////////////////////////////////////////////////////////////////////////////
	SetAttr = function ( tag, av )
-- ////////////////////////////////////////////////////////////////////////////
		if( not AliDSDebugEdit ) then
			if( not tag ) then
				ReportError("AttrValue::SetAttr","Invalid input tag")
				return nil
			end

			if( not av ) then
				ReportError("AttrValue::SetAttr","Invalid input attribute")
				return nil
			end

			if( not av.tbl ) then
				ReportError("AttrValue::SetAttr","Empty input attribute")
				return nil
			end

			SetAttrC( tag, av.tbl )
		end
	end
else
-- ////////////////////////////////////////////////////////////////////////////
	SetAttr = function ( tag, av ) -- called by version 1+ script engines (uses lua-side caching)
-- ////////////////////////////////////////////////////////////////////////////
		if( not AliDSDebugEdit ) then
			if( not tag ) then
				ReportError("AttrValue::SetAttr","Invalid input tag")
				return nil
			end

			if( not av ) then
				ReportError("AttrValue::SetAttr","Invalid input attribute")
				return nil
			end

			if( not av.tbl ) then
				ReportError("AttrValue::SetAttr","Empty input attribute")
				return nil
			end

			gHasAttrCache[ tag ] = true
			gGetAttrCache[ tag ] = av
			SetAttrC( tag, av.tbl )
		end
	end
end

-- 11.5 scripts using multiple sorting hints support have this version set to 1
-- older scripts does not have this variable defined

-- ////////////////////////////////////////////////////////////////////////////
--  Section 1: Utilities
-- ////////////////////////////////////////////////////////////////////////////

AliDSTraceLevel = 1 -- 0 for errors, 1 for errors and warnings, 2 for all info

gGetAttrCache = { } -- optimization for limiting number of C++/Lua transfers
gHasAttrCache = { } -- optimization for limiting number of C++/Lua transfers

-- // Common idiom functions

function SanityOK( modalityStr )
-- ////////////////////////////////////////////////////////////////////////////
-- This function returns true if the the current image is consistent with
-- the given modality.  Otherwise it logs an error and returns false.
-- ////////////////////////////////////////////////////////////////////////////

    -- If there is no retrievable modality...
    if( not modalityStr ) then
          ReportError( "SanityOK( nil )", 
                       "Nil argument passed to sanity check function." )
          return false -- wrong context (no modality in the input collection)
          --==============================================
    end
    
    -- Get modality of current image
    local modalityAttr = GetAttr(MODALITY_TAG)
    
    -- If there is no retrievable modality...
    if( not modalityAttr ) then
          -- wrong context (no modality in the input collection)
          ReportError( "SanityOK(" .. modalityStr .. ")", 
                       "Image has no modality value." )
          return false 
          --==============================================
    end
  
    -- If the given modality is not a substring of retrieved attribute..
    if( not BestMatchInAttr( modalityAttr, modalityStr ) ) then
          -- wrong script or wrong context (CT script applied on other modality)
          ReportError( "SanityOK(" .. modalityStr .. ")", 
                       "Image modality [" .. modalityAttr:Get(1) .. "] does not match called script." )
          return false 
          --==============================================
    end
    
    return true
end


function NewPropWithValue( prop_key, value )
-- ////////////////////////////////////////////////////////////////////////////
-- This function attempts to assign the given value to a newly created 
-- property having the given prop_key. If the given value is nil, however,
-- this function will do nothing.
--
-- Returns nil if the value is nil.  Returns non-nil new property if 
-- the property was created and set with the given value.
--
-- EXAMPLE
--      NewPropWithValue( IMAGE_TYPE_PROP, "Scout" )
--              Tries to assign the value "Scout" to a new property 
--              with key: IMAGE_TYPE_PROP.
-- 
-- Author: Allan Noordvyk, Oct 2, 2004,
--     
-- ////////////////////////////////////////////////////////////////////////////

    -- If the given value is nil...
    if (not value) then
        return nil
		-- ===============================================          
    end
     
    -- Create a new property to be assigned to the image.
    local newProp = AttrValue:new()
    
    -- Since all properties passed to Emerald are declared as strings
    -- force conversion to string in case the given value is a number.
    local valString = "" .. value
    newProp:AppendVal(valString)

    -- Associated the new property with the current image.
    SetAttr( prop_key, newProp )
    
    if( AliDSTraceLevel == 2 ) then 
        TraceC("Created property [" .. prop_key .. "] with value [" .. valString .. "]\n")
    end
    -- Indicate sucess.
    return newProp
end

function NewPropFromTag( prop_key, tag_key )
-- ////////////////////////////////////////////////////////////////////////////
-- This function attempts to extract a value for the given DICOM tag_key
-- and assign it to a newly created property having the given prop_key.
-- If the given DICOM tag has a list value, only the first element
-- of the list will be copied.
--
-- Returns nil if the given DICOM tag has no value in the current image
-- that could be copied.  Returns non-nil new property if a copy was possible and
-- thus the property was created and set.
--
-- EXAMPLE
--      NewPropFromTag( PHASE_PROP, maketag("0020","0012") )
--              Tries to assign the value of the DICOM element <0020,0012> to a 
--              new property with key: PHASE_PROP.
-- 
-- Author: Allan Noordvyk, Oct 2, 2004,
--     
-- ////////////////////////////////////////////////////////////////////////////

    -- Reference the dicom attribute in the current image.
    local dicomAttr = GetAttr( tag_key )
    
    -- If the attribute isn't present in the image...
    if (not dicomAttr) then
        return nil
		-- ===============================================          
    end
        
    -- Extract the first value of the DICOM attribute.
    local ext_val = dicomAttr:Get(1)
    
    return NewPropWithValue( prop_key, ext_val )
end

function ValueForTag( tag_key, default_val )
-- ////////////////////////////////////////////////////////////////////////////
-- This function returns either the value of the attribute with the
-- given tag, or a given default value if the attribute does not exist in the current
-- image.  This saves a lot of messy temporary variables and nil checking
-- if statements that you'd have to go through if tried to use Get(1)
-- directly instead.
--
-- The default_val parameter may be omitted, in which case nil will
-- be returned if the attribute doesn't exist.
--
-- EXAMPLE
--      x = SaveValueForTag( maketag("0020","0012") )
--              Returns the value of the DICOM element <0020,0012> or nil,
--              if the current image does not have that element defined.
--      y = SaveValueForTag( maketag("0020","0066"), "black" )
--              Returns the value of the DICOM element <0020,0066> or the
--              string "black" if the current image does not have that 
--              element defined.
-- 
-- Author: Allan Noordvyk, Oct 3, 2004,
--     
-- ////////////////////////////////////////////////////////////////////////////

    local anAttr = GetAttr( tag_key )
    
    if (anAttr) then
       return anAttr:Get(1)
    end
    
    return default_val;
    
end

function SmartAppendWords( s1, sepstr, s2 )

	--- This function returns a string build by appending the words in two strings
	--- and using a 3rd string as a separator between them.  What makes this function
	--- smarter than just doing s1 .. sepstr .. s2 is that it handles the cases where
	--- s1 and/or s2 are nil or the empty string.   
	--- Nil and the empty string arguments will be ignored and no append done.
	--- If both arguments are nil or empty, then nil is returned.
        ---
        --- If nil is given for the separator string, then s1 and s2 will be directly
        --- concatenated with no seperator.

        if (s1 == "") then
            s1 = nil    -- treat as nil
	end

	if (s2 == "") then
            s2 = nil    -- treat as nil
	end

	if (not s1) then
            return s2
	end

	if (not s2) then
            return s1
	end
        
        -- At this point we know that s1 and s2 are non-nil
        if (not sepstr) then
            return s1 .. s2
        end

        -- At this point we know that all three parameters are non-nil.
	return s1 .. sepstr .. s2
	
end

-- *******************************************************************

-- String matching utilities

-- ////////////////////////////////////////////////////////////////////////////
function BestWordMatchInAttr(av, ...)
-- ////////////////////////////////////////////////////////////////////////////
-- This function tests whether the first argument (an attribute) has a value
-- which contains any of the words represented in the subsequent arguments.
-- The first word match found is returned, or nil if none.  All matching
-- is case insensitive.
--
-- When substring, rather than whole word, matching is required, the
-- BestMatchInAttr function should be used instead.
--
--
-- EXAMPLE
--      BestWordMatchInAttr( anAttr, "Left", "Right", "Up", "Down" )
--              If anAttr had the value "Right Knee", then this function
--              would return "Right"
--              If anAttr had the value "Go up", then this function would
--              return "Up"
--              If anAttr had the value "Cup Rightward", then this function would return
--              nil, even though both "up" and "right" are present, since each is part of 
--              a larger word.
-- 
-- Author: Allan Noordvyk, March 7, 2005, based on similar function by Cristian B.
--     
-- ////////////////////////////////////////////////////////////////////////////
    --  finds in AttrValue av the first of the strings passed as parameters 
    if( not av ) then
        ReportError("AttrValue::BestWordMatchInAttr","Empty input attribute")
        return nil
    end
    
    TraceC("Entering - BestWordMatchInAttr")
    
    if( arg.n == 0 ) then
        ReportError("AttrValue::BestWordMatchInAttr","Empty matching strings list")
        return nil
    end
    for _, v in ipairs(arg) do 
        if( v ) then
            --TraceC("calling - inner loop FindInAttr")       
            if (av:FindWordInAttr(v)) then
                -- TraceC("<<<<<<<<<<<<< Found ".. v)
                return v
            end
        end
    end
    return nil    
end
   
-- ////////////////////////////////////////////////////////////////////////////
function BestMatchInAttr(av, ...)
-- ////////////////////////////////////////////////////////////////////////////
    --  finds in AttrValue av the first of the strings passed as parameters 
    if( not av ) then
        ReportError("AttrValue::BestMatchInAttr","Empty input attribute")
        return nil
    end
    --TraceC("Entering - BestMatchInAttr")
    if( arg.n == 0 ) then
        ReportError("AttrValue::BestMatchInAttr","Empty matching strings list")
        return nil
    end
    for _, v in ipairs(arg) do 
        if( v ) then
            --TraceC("calling - inner loop FindInAttr")       
            if (av:FindInAttr(v)) then
                -- TraceC("<<<<<<<<<<<<< Found ".. v)
                return v
            end
        end
    end
    return nil    
end

-- ////////////////////////////////////////////////////////////////////////////
function BestMatchForBodyRegion(av)
--
--  This function returns a string representing the best matches for a body
--  region in any substring of the given attribute value.
--
--  This is done by first checking against an internal table of all
--  standard DICOM body parts and common abbreviations as coded in this
--  file.   If this is unsuccessful, a match against the body regions
--  defined in the HMI PACS database is attempted.   If no match is
--  found using either source of keywords then nil is returned.
--
--  Multiple matches will be returned as a single space delimited
--  string.  Thus an attribute value of "Chest/Abd/Pelv"
--  will return "Chest Abdomen Pelvis".
--
-- ////////////////////////////////////////////////////////////////////////////
    if( not av ) then
        ReportError("AttrValue::BestMatchForBodyRegion","Empty input attribute")
        return nil
    end
    
	-- Set up array of arrays variable to hold aliases for each standard DICOM body part
	-- First value of each array is the standard property value and the rest are the aliases.
	-- Order arrays with shortest substring aliases ones at the end (to avoid less precise early matching).
	-- Words of 4 characters or less will require whole word matching to prevent false substring positive matches, 
	-- so include plural forms for those.  Words 5 letters and longer will do substring matches.
	local aliases  = { { "Head", "TETE" },
						   { "Neck", "COU" },
						   { "Chest", "CHST", "POITRINE" },              -- Head, Neck, Chest, Adbdomen, Pelvis given in anatomic order
						   { "Abdomen", "ABD" },             --  so that they are listed as expected when in combination
						   { "Pelvis", "PELV", "PEL", "BASSIN" },
						   { "Breast", "MAMMO", "SEINS", "SEIN" },
						   { "Shoulder", "EPAULE" },              -- Shoulder, Arm, Elbow, Forearm, Wrist, Hand, Finger given in anatomic order
						   { "Arm", "ARMS", "BRAS" },           -- need pluarl due to short length
						   { "Elbow", "COUDE" },
						   { "Forearm", "AVANT-BRAS", "AVANT BRAS" },
						   { "Wrist", "POIGNETS", "POIGNET" },
						   { "Hand", "HANDS" },         -- need plural due to short length
						   { "Finger" },
						   { "Leg", "LEGS", "JAMBE" },           -- need plural due to short length
						   { "Knee", "KNEES", "GENOUX", "GENOU" },         -- need plural due to short length
						   { "Ankle", "CHEVILLES", "CHEVILLE" },
						   { "Foot", "FEET", "PIEDS", "PIED" },
						   { "Toe", "TOE", "ORTEILS" },
						   { "C-Spine", "CERVICAL", "CERV", "C-SPINE", "CSPINE", "C SPINE", "CERVICALE" },  -- C, T, and L spine are in anatomic order
						   { "T-Spine", "THORACIC", "T-SPINE", "TSPINE", "T SPINE", "DORSALE", "THORACIQUE" },
						   { "L-Spine", "LUMBAR", "L-SPINE", "LSPINE", "L SPINE", "LOMBAIRE" },
						   { "S-Spine", "SACRAL", "SACRUM", "S-SPINE", "SSPINE", "S SPINE" },
						   { "TM-Joints", "Temporomandibular", "TMJ", "ARTICULATIONS T-M", "ARTICULATION T-M" },
						   { "Rib", "RIBS", "COTES" },
						   { "Clavicle" },
						   { "Hip", "HIPS", "HANCHE" },    -- plural needed due to short length
						   { "Jaw", "JAWS", "MACHOIRE" },    -- plural needed due to short length
						   { "Aorta", "AORTE" },
						   { "Kidney", "KUB", "REINS", "REIN" },
						   { "Bladder", "KUB", "VESSIE" },
						   { "Brain", "NEURO", "CERVEAU", "CEREBRALE" },
						   { "Calcaneus", "CALCANEI", "CALCANEUM" },
						   { "Carotid", "CAROTIDE" },
						   { "Clavicle", "CLAVICULE" },
						   { "Coccyx" },
						   { "Colon" },
						   { "Face", "VISAGE" },
						   { "Facial Bones", "OS DE LA FACE" },
						   { "Femur", "FEMURA", "FEMORA", "FEMURS" },
						   { "Gallbladder", "VESICULE BILIAIRE" },
						   { "Heart", "CARDIO", "CARDIAC", "COEUR" },
						   { "Humerus", "HUMERI" },
						   { "Liver", "FOIE" },
						   { "Mandible", "MANDIBULE" },
						   { "Mastoid", "MASTOIDE", "MASTOÏDE" },
						   { "Maxilla", "MAXILLAIRE" },
						   { "Nose", "NEZ" },
						   { "Orbit", "ORBITE" },
						   { "Ovary", "OVARIES", "OVAIRE" },
						   { "Pancreas" },
						   { "Patella", "PATELLAS", "ROTULE" },
						   { "Penis" },
						   { "Scapula", "OMOPLATE" }, 
						   { "Scrotum", "SCROTAL" },
						   { "SI Joints", "SI", "ARTICULATIONS SACRO-ILIAQUES" },
						   { "Paranasal Sinuses",  "SINUSES", "PARANASAL" },
						   { "Skull", "CRANE" },
						   { "Small Intestine", "Intestine", "INTESTIN GRELE" },
						   { "Spleen" },
						   { "Sternum" },
						   { "Stomach", "ESTOMAC" },
						   { "Testis", "TESTES", "TESTICULE" },
						   { "Thorax" },
						   { "Thyroid", "THYROIDE" },
						   { "Uterus" },
						   { "Zygomatic Arch", "ZYGOMATIC", "ZYGOMATIQUE" },
						   { "Lung", "LUNGS", "POUMON" },  -- plural needed due to short length
						   { "Circle of Willis", "COW", "PdW" }
                        }
                        
	local isSpine = false   -- use to avoid double matching "spine" keyword

	-- CB: October 2, 2004, modified to implement new requirements from CRDB00025891
	-- split multiword strings from body region attribute
	-- av:Dump("Body Part")
	local strToMatch = av.tbl[1]
	local strFilter = "(%a+)"
	local wordsToMatch = {}
	local w
	
	for w in string.gfind(strToMatch,strFilter) do 
		table.insert(wordsToMatch,w)
	end
	
	-- This table will contain all our matches
	local wordsMatched = {}
	
	-- For each word to be matched in the attribute value...
	for k, v in pairs(wordsToMatch) do
	
		local wordOutput
		
		-- If we know this is a spine and the word is spine...
		if (isSpine and string.upper(v) == "SPINE") then
			-- just skip the redundant word to avoid things like "Spine, Cervical Spine"
		
		elseif ( string.upper(v) == "COLONNE" ) then
			-- skip this word so that it doesnt mistakenly match Colon; Colonne is
			-- french for "Column"
		  
		else
			-- Look for body region variants in the supplied word, with substring matching
			-- allowed for aliases of 5 or more characters.
			wordOutput = AliasTableLookUp( v, aliases, 5 ) 
	
			-- If the word we matched was a spine component...
			if ( wordOutput == "C-Spine" or
				 wordOutput == "T-Spine" or
				 wordOutput == "L-Spine" or
				 wordOutput == "S-Spine" ) then
				  -- Remember for future words
					isSpine = true
			end
		end
		  
		-- If a match was found above...
		if( wordOutput ) then
			-- Add to our table
			table.insert(wordsMatched,wordOutput)
		end
	end
	
	-- Build the string of matches by concatenating tables contenets
	-- with space as separated
	local resultStr = ""
	for k, v in pairs(wordsMatched) do
		if( string.len(resultStr) == 0 ) then
			resultStr = v
		else
			resultStr = resultStr .. " " .. v
		end
	end
	
	-- If the results was the empty string...
	if( string.len(resultStr) ~= 0 ) then
		-- found something worth returning
		return resultStr 
	end
    
	-- if we are here; then no matches were found in the internal body part list; time to check the database
	resultStr = BestMatchForBodyRegionFromDatabase(av)
	
    return resultStr
end  -- BestMatchForBodyRegion

-- ////////////////////////////////////////////////////////////////////////////
function BestMatchForBodyRegionFromDatabase(av)
-- ////////////////////////////////////////////////////////////////////////////
    if( not av ) then
        ReportError("AttrValue::BestMatchForBodyRegion","Empty input attribute")
        return nil
    else
        -- CB: October 2, 2004, modified to implement new requirements from CRDB00025891
        -- split multiword strings from body region attribute
        -- av:Dump("Body Part")
        local strToMatch = av.tbl[1]
        local strFilter = "(%a+)"
        local wordsToMatch = {}
        for w in string.gfind(strToMatch,strFilter) do 
            table.insert(wordsToMatch,w)
        end
        local wordsMatched = {}
        -- call C++ implementation to match the words to body parts 
        for k, v in pairs(wordsToMatch) do
            local wordInput = {} -- C++ implementation expects a table of strings 
            table.insert(wordInput,v)
            local wordOutput = BestMatchForBodyRegionC(wordInput)
            if( wordOutput ) then
                table.insert(wordsMatched,wordOutput)
            end
        end
        local resultStr = ""
        for k, v in pairs(wordsMatched) do
            if( string.len(resultStr) == 0 ) then
                resultStr = v
            else
                resultStr = resultStr .. " " .. v
            end
        end
       if( string.len(resultStr) == 0 ) then
            return nil
        else
            return resultStr 
        end
    end
    return nil
end

-- ////////////////////////////////////////////////////////////////////////////
function FindWordInPhrase(str,phrase) 
-- 
-- This function tests whether the given phrase contains the
-- given argument string as a word (rather than a simple substring).
-- If the word is found, then true is returned.  Otherwise false is returned.
-- All comparison is case-insensitive.
--
-- A word is defined as a substring of alphabetical, separated
-- from the rest of the string by a non-alphabetical character or the start
-- or end of the string.  For example the string "a.bcd ef;ghik-lmn." has
-- the following words: "a", "bcd", "ef", "ghik", "lmn".
--
-- When substring, rather than whole word, testing is required, the
-- FindInAttr function should be used instead.
--
-- If the attribute is a list of strings, then each string in the list will
-- be tested.  If any includes the given word, true will be returned.
--
-- NOTE: Because this function uses pattern matching, non of the special
-- pattern matching characters (e.g. %, -, +, [, ], (, ), and *) should be
-- present in the str argument.  Nor will whitespace be handled.  Instead, 
-- the argument should contain only alphabetical characters.
--
-- ARGUMENTS
--      str     A word to be tested for inclusion within the phrase.
--              Must contain only alphabetical characters. 
--      phrase  The phrase that may or may not contain the phrase
--
-- EXAMPLE
--      FindWordInPhrase( "RT", "KNEE, RT" )
--              -- returns true
--
--      FindWordInPhrase( "RT", "foot, rt" )
--              -- returns true
--
--      FindWordInPhrase( "RT", "Virtual" )
--              -- returns false, since "rt" is part of a larger word
--              a larger word.
--     
-- ////////////////////////////////////////////////////////////////////////////
 
    if( not str ) then
        ReportError("FindWordInPhrase","Null input string")
        return false -- invalid index 
    end
    
    if( type(str) ~= "string" ) then
        ReportError("FindWordInPhrase","Attempt to search for a non string value")
        return false -- invalid index 
    end
    
    if( not phrase ) then
        ReportError("FindWordInPhrase","Null input phrase")
        return false -- invalid index 
    end
    
    if( type(phrase) ~= "string" ) then
        ReportError("FindWordInPhrase","Attempt to test a non string value for phrase")
        return false -- invalid index 
    end

    -- Force the word to be matched to upper case so we can ignore case as an issue
    local seekWord = string.upper(str)
    
    -- Build test patterns to find word starting and ending 
    -- 
    local wordAtStartPattern = seekWord .. "%A"   -- word followed by non-alpha char
    local wordAfterStartPattern = "%A" .. seekWord .. "%A"  -- word embedded between non-alpha char
    local wordAtEndPattern = "%A" .. seekWord .. "$" -- word preceded by non-alpha char & at end of string
    
    local wordLen = string.len( seekWord )
    
    -- Force the string to upper case before looking for words, since we
    -- did the same to the word above.
    local testSentence = string.upper(phrase)
        
    local sentenceLen = string.len( testSentence )
        
    if ( AliDSTraceLevel == 2 ) then
        TraceC( "FindWordInPhrase: Seeking [" .. seekWord .. "] in [" .. testSentence .. "]\n" )
    end
        
    -- If the sentence is long enough to hold word...
    if (sentenceLen >= wordLen) then
               
      -- Test simple equality (since this is fast and commonly occurs)...
      if (testSentence == seekWord) then
          return true
      end
        
      if ( AliDSTraceLevel == 2 ) then
        TraceC( "FindWordInPhrase: [" .. seekWord .. "] <> [" .. testSentence .. "]\n" )
      end
        
      -- If we found word at beginning of sentence followed by a separator character...
      if (string.find( string.sub(testSentence,1,wordLen+1), wordAtStartPattern )) then
          return true
      end
 
      if ( AliDSTraceLevel == 2 ) then 
        TraceC( "FindWordInPhrase: [" .. seekWord .. "] not at start of [" .. testSentence .. "]\n" )
      end
          
      -- If it could part of a longer sentence...
      if (sentenceLen > wordLen) then
        
          -- If we found word in middle of longer string surrounded by separator characters...
          if ( string.find( testSentence, wordAfterStartPattern )) then
                  return true
          end

          if ( AliDSTraceLevel == 2 ) then 
            TraceC( "FindWordInPhrase: [" .. seekWord .. "] not in middle of [" .. testSentence .. "]\n" )
          end
    
          -- If we found the word at end of sentence preceded by a non-alphabetical character...
          if ( string.find( testSentence, wordAtEndPattern, sentenceLen-wordLen-1, false )) then
              return true
          end
            
      end

      if ( AliDSTraceLevel == 2 ) then           
        TraceC( "FindWordInPhrase: [" .. seekWord .. "] not at end of [" .. testSentence .. "]\n" )
      end
        
  end

  return false -- not found
  
end


-- ////////////////////////////////////////////////////////////////////////////
function AliasTableLookUp( inPhrase, aliases, minSubstringLen )
--
-- This function tests whether a given string inPhrase contains keywrods
-- in a given table of terms and equivalent aliases, and if so, returns 
-- the canonical term is returned. 
-- 
-- If there is no match was found then nil is returned.
--
-- The aliases argument is an array of string arrays.  Each string array
-- within the array represents a set of strings that should be considered
-- equivalent with the canonical word being the first element in the array.
-- 
-- If an alias is less than minSubstring letters long only exact matches will be
-- returned in order to avoid false positives.  Otherwise, the a substring
-- match where the alias is a substring of the test value will be truented.
--
-- All comparison is case insensitive.   If substring matches are to be
-- allowed, then all substrings must be alphabetical characters only.
--
-- ARGUMENTS
--      inWord
--              Phrase being tested as containing terms or aliases
--      aliases 
--              Table of string arrays representing the terms and
--              any aliases
--      minSubstringLen
--              Minimum length that an alias string must be before
--              partial word matching will be performed.  Lengths
--              below this will be whole word matching only.
--
-- EXAMPLE
--      aliases = { {"One", "Un", "Ich", "First"},
--                  {"Two", "Deux", "Ni", "Second" },
--                  {"Three, "Tres", "Son", "Third" } }
--      a = AliasTableLookUp( "Two", aliases, 4 }
--      b = AliasTableLookUp( "Secondly", aliases, 4 }
--      d = AliasTableLookUp( "Pas de Deux", aliases, 4 }
--      --  a, b, c, and d would all return the string "Two"
--      
--      f = AliasTableLookUp( "Tone", aliases, 4 )
--      -- e and f would return nil since one of the matched substring of
--      -- "one" is less than the minimum length indicated.
--              
-- ////////////////////////////////////////////////////////////////////////////

    if (aliases == nil) then
    
        ReportError("AliasTableLookUp","Nil aliases argument")
        return nil
        
    end
    
    if (inPhrase == nil) then
    
        ReportError("AliasTableLookUp","Nil inPhrase argument")
        return nil
        
    end
    
    -- Force input word to upper case once at outset rather than repeatedly
    local upperInPhrase = string.upper(inPhrase)
    
    local i, aliasWords

    -- For each possible alias value...
    for i,aliasWords in ipairs(aliases) do
      
        local canonVal = aliasWords[1] 
    
        TraceC( "Checking phrase [" .. inPhrase .. "] for aliases of [" .. canonVal .. "]\n" )
        
        local j, anAlias
    
        for j,anAlias in ipairs(aliasWords) do
        
            local matchStr

            -- Force to upper case to avoid case senstivity
            local upperAlias = string.upper(anAlias)
                    
            -- If the word is less than the safe number of letters long...
            if (string.len(upperAlias) < minSubstringLen) then

                -- Look only for full word matches for this alias 
                -- (to avoid false positives in substrings) in the
                -- supplied input phrase
                
                if (FindWordInPhrase( upperAlias, upperInPhrase )) then 
                    matchStr = upperAlias
                end
                      
            else
              
                -- If the substring upperAlias is found anyhwere in the input phrase...
                if ( string.find(upperInPhrase,upperAlias,1,true)) then
                    -- Return the canonical value for this alias
                    matchStr = upperAlias
                end
                
            end
                
            -- If we got a match...
            if (matchStr) then
                  
                TraceC( "Found keyword string [" .. matchStr .. 
                          "] indicating value [" .. canonVal .. "]\n" )
                              
                return canonVal
                -- ===================================================
            end
        end
    end
      
    return nil  -- if we are here then we found no match
    
end -- AliasTableLookUp

-- ////////////////////////////////////////////////////////////////////////////
function BestMatchInStr(str, ...)
-- ////////////////////////////////////////////////////////////////////////////
     if( not str ) then
        ReportError("AttrValue::BestMatchInStr","Invalid input string")
        return nil
    end    
    if( arg.n == 0 ) then
        ReportError("AttrValue::BestMatchInStr","Empty input string")
    end
    for _, v in ipairs(arg) do
        if (string.upper(str) == str.upper(v)) then
            return str
        end
    end
    return nil
end

-- ////////////////////////////////////////////////////////////////////////////
function LooselyEqualStr(str1, str2)
-- ////////////////////////////////////////////////////////////////////////////
    if( not AliDSDebugEdit ) then
        return StrLooselyEqualC(str1, str2)
    end
    return string.upper(str1) == string.upper(str2)
end


-- ////////////////////////////////////////////////////////////////////////////
function MergeStrings(strSeparator, ... ) 
-- ////////////////////////////////////////////////////////////////////////////
    if( not strSeparator ) then
        ReportError("MergeStrings","No separator string provided")
        return nil
    end
    
    local strResult
    strResult = ""
    
    for _, v in ipairs(arg) do
        if( v and type(v) == "string" ) then
            if( string.len(strResult) == 0 ) then
                strResult = v 
            else
                strResult = strResult .. strSeparator .. v
            end
        end
    end
    
    if( string.len(strResult) == 0 ) then    
        return nil
    else
        return strResult
    end
end

-- ////////////////////////////////////////////////////////////////////////////
-- ////////////////////////////////////////////////////////////////////////////
--  Section 1: Engine Support 
--  Changes in this section may damage the communication with the sequencing
--  engine. Do not modify this section without the explicit support of the ISG team
-- ////////////////////////////////////////////////////////////////////////////
-- ////////////////////////////////////////////////////////////////////////////

--\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
--                           ATTRIBTE CLASS DEFINITION
--/////////////////////////////////////////////////////////////////////////////

-- ////////////////////////////////////////////////////////////////////////////
AttrValue = 
-- ////////////////////////////////////////////////////////////////////////////
{
    tbl = {}
}

-- ////////////////////////////////////////////////////////////////////////////
function AttrValue:checkIdx(nValueIdx)
-- ////////////////////////////////////////////////////////////////////////////
    if (nValueIdx <= 0 or nValueIdx > table.getn(self.tbl) ) then
        ReportError("AttrValue:checkIdx","Invalid index")
        return false 
    else
        return true
    end    
end

-- ////////////////////////////////////////////////////////////////////////////
function AttrValue:checkIdxM(nValueIdx, nIdxInValue)
-- ////////////////////////////////////////////////////////////////////////////
    if (nValueIdx <= 0 or nValueIdx > table.getn(self.tbl) 
        or nIdxInValue <= 0 or nIdxInValue > table.getn(self.tbl[nValueIdx])) then
            ReportError("AttrValue:checkIdxM","Invalid index")
        return false 
    else
        return true
    end    
end

-- ////////////////////////////////////////////////////////////////////////////
function AttrValue:Resize(nNbrValues)
-- ////////////////////////////////////////////////////////////////////////////
    if (nNbrValues <= 0) then
        ReportError("AttrValue:Resize","invalid resize value")
        return 
    end
    table.setn(self.tbl, nNbrValues)
end

-- ////////////////////////////////////////////////////////////////////////////
function AttrValue:ResizeM(nValueIdx, nNbrElements)
-- ////////////////////////////////////////////////////////////////////////////
    if (nValueIdx <= 0 or nValueIdx > table.getn(self.tbl)) then
        ReportError("AttrValue:ResizeM","Invalid index")
    end
    if (self.tbl[nValueIdx] == nil) then
        self.tbl[nValueIdx] = {}
    end
    table.setn(self.tbl[nValueIdx], nNbrElements)
end

-- ////////////////////////////////////////////////////////////////////////////
function AttrValue:IsMatrix()
-- ////////////////////////////////////////////////////////////////////////////
    if (table.getn(self.tbl) <= 0) then
        return false
    end
    return type(self.tbl[1]) == "table"
end

-- ////////////////////////////////////////////////////////////////////////////
function AttrValue:GetSize()
-- ////////////////////////////////////////////////////////////////////////////
    return table.getn(self.tbl)
end

-- ////////////////////////////////////////////////////////////////////////////
function AttrValue:GetSizeM(nValueIdx)
-- ////////////////////////////////////////////////////////////////////////////
    if (nValueIdx <= 0 or nValueIdx > table.getn(self.tbl)) then
        ReportError("AttrValue:GetSizeM","Invalid index")
        return 0
    end    
    return table.getn(self.tbl[nValueIdx])
end

-- ////////////////////////////////////////////////////////////////////////////
function AttrValue:SetM(nValueIdx, nIdxInValue, val)
-- ////////////////////////////////////////////////////////////////////////////
    if( not nValueIdx or not nIdxInValue or not val ) then 
        ReportError("AttrValue:SetM","Invalid input parameters")
        return false
    end    
    if( self:checkIdxM(nValueIdx, nIdxInValue) == false) then
        ReportError("AttrValue:SetM","Invalid index")
        return false
    end
    self.tbl[nValueIdx][nIdxInValue] = val
    return true
end

-- ////////////////////////////////////////////////////////////////////////////
function AttrValue:Set(nValueIdx, val)
-- ////////////////////////////////////////////////////////////////////////////
    if( not nValueIdx or not val ) then 
        ReportError("AttrValue:Set","Invalid input parameters")
        return false
    end

    if ( self:checkIdx(nValueIdx) == false ) then
        ReportError("AttrValue:Set","Invalid index")    
        return false
    end    
    self.tbl[nValueIdx] = val
    return true    
end

-- ////////////////////////////////////////////////////////////////////////////
function AttrValue:GetM(nValueIdx, nIdxInValue)
-- ////////////////////////////////////////////////////////////////////////////
    if( not nValueIdx or not nIdxInValue ) then 
        ReportError("AttrValue:GetM","Invalid input parameters")
        return nil
    end
    if( self:checkIdxM(nValueIdx, nIdxInValue) == false ) then
        ReportError("AttrValue:GetM","Invalid index")    
        return nil
    end
    return self.tbl[nValueIdx][nIdxInValue]    
end

-- ////////////////////////////////////////////////////////////////////////////
function AttrValue:Get(nValueIdx)
-- ////////////////////////////////////////////////////////////////////////////
    if( not nValueIdx ) then 
        ReportError("AttrValue:Get","Invalid input parameters")
        return nil
    end
    if (self:checkIdx(nValueIdx) == false) then
        ReportError("AttrValue:Get","Invalid index")    
        return nil
    end    
    local val = self.tbl[nValueIdx]
    return self.tbl[nValueIdx]
end

-- ////////////////////////////////////////////////////////////////////////////
function AttrValue:GetFirst()
-- ////////////////////////////////////////////////////////////////////////////
--	MaxN: This function was written for cases where a Get(1) call fails if the 
--	structure of the source variable is a matrix instead of single value. This function
--	returns the first value of the variable regardless of what the structure is i.e. 
--	the details about the source structure are hidden from the caller. Such a function
--	has more overhead than a :Get(1) call so it is not currently used by default.
	local firstEl = self.tbl[1]
	
	if (type(firstEl) == "table" ) then
		TraceC("First element is a table...require further indexing")
		return firstEl[1]
	end

	return firstEl
end

-- ////////////////////////////////////////////////////////////////////////////
function AttrValue:FindWordInAttr(str) 
-- ////////////////////////////////////////////////////////////////////////////
-- This function tests whether the receiving attribute object contains the
-- given argument string as a word (rather than a simple substring).
-- If the word is found, then true is returned.  Otherwise false is returned.
-- All comparison is case-insensitive.
--
-- A word is defined as a substring of alphabetical, separated
-- from the rest of the string by a non-alphabetical character or the start
-- or end of the string.  For example the string "a.bcd ef;ghik-lmn." has
-- the following words: "a", "bcd", "ef", "ghik", "lmn".
--
-- When substring, rather than whole word, testing is required, the
-- FindInAttr function should be used instead.
--
-- If the attribute is a list of strings, then each string in the list will
-- be tested.  If any includes the given word, true will be returned.
--
-- NOTE: Because this function uses pattern matching, non of the special
-- pattern matching characters (e.g. %, -, +, [, ], (, ), and *) should be
-- present in the str argument.  Nor will whitespace be handled.  Instead, 
-- the argument should contain only alphabetical characters.
--
-- ARGUMENTS
--      str     A word to be tested for inclusion within an attribute value.
--              Must contain only alphabetical characters. 
--
-- EXAMPLE
--      anAttr.FindWordInAttr( "RT" )
--              If anAttr had the value "Rt Knee", "KNEE, RT", or "foot, rt; foot, lt"
--              then this function would return true.
--              If anAttr had the value "Virtual", "flirt", then this function
--              would return false, even though "rt" is present, since each is part of 
--              a larger word.
-- 
-- Author: Allan Noordvyk, March 7, 2005, based on similar function by Cristian B.
--     
-- ////////////////////////////////////////////////////////////////////////////
 
    if( not str ) then
        ReportError("AttrValue:FindWordInAttr","Null input string")
        return false -- invalid index 
    end
    
    if( type(str) ~= "string" ) then
        ReportError("AttrValue:FindWordInAttr","Attempt to search for a non string value")
        return false -- invalid index 
    end

    -- Force the word to be matched to upper case so we can ignore case as an issue
    local seekWord = string.upper(str)
    
    -- Build test patterns to find word starting and ending 
    -- 
    local wordAtStartPattern = seekWord .. "%A"   -- word followed by non-alpha char
    local wordAfterStartPattern = "%A" .. seekWord .. "%A"  -- word embendded between non-alpha char
    local wordAtEndPattern = "%A" .. seekWord .. "$" -- word preceded by non-alpha char & at end of string
    
    local wordLen = string.len( seekWord )
    
    -- For each string in the value set of the attribute...
    for k, v in pairs(self.tbl) do
    
        -- Force the string to upper case before looking for words, since we
        -- did the same to the word above.
        local testSentence = string.upper(v)
        
        local sentenceLen = string.len( testSentence )
        
        if ( AliDSTraceLevel == 2 ) then
            TraceC( "FindWordInAttr: Seeking [" .. seekWord .. "] in [" .. testSentence .. "]\n" )
        end
        
        -- If the sentence is long enough to hold word...
        if (sentenceLen >= wordLen) then
               
          -- Test simple equality (since this is fast and commonly occurs)...
          if (testSentence == seekWord) then
              return true
          end
        
          if ( AliDSTraceLevel == 2 ) then
            TraceC( "FindWordInAttr: [" .. seekWord .. "] <> [" .. testSentence .. "]\n" )
          end
        
          -- If we found word at beginning of sentence followed by a separator character...
          if (string.find( string.sub(testSentence,1,wordLen+1), wordAtStartPattern )) then
              return true
          end
 
          if ( AliDSTraceLevel == 2 ) then 
            TraceC( "FindWordInAttr: [" .. seekWord .. "] not at start of [" .. testSentence .. "]\n" )
          end
          
          -- If it could part of a longer sentence...
          if (sentenceLen > wordLen) then
        
              -- If we found word in middle of longer string surrounded by separator characters...
              if ( string.find( testSentence, wordAfterStartPattern )) then
                  return true
              end

              if ( AliDSTraceLevel == 2 ) then 
                TraceC( "FindWordInAttr: [" .. seekWord .. "] not in middle of [" .. testSentence .. "]\n" )
              end
    
              -- If we found the word at end of sentence preceded by a non-alphabetical character...
              if ( string.find( testSentence, wordAtEndPattern, sentenceLen-wordLen-1, false )) then
                 return true
              end
            
          end

          if ( AliDSTraceLevel == 2 ) then           
            TraceC( "FindWordInAttr: [" .. seekWord .. "] not at end of [" .. testSentence .. "]\n" )
          end
        
        end
        
    end
    
    return false -- not found
    
end

-- ////////////////////////////////////////////////////////////////////////////
function AttrValue:FindInAttr(str) 
-- ////////////////////////////////////////////////////////////////////////////
    if( not str ) then
        ReportError("AttrValue:FindInAttr","Null input string")
        return false -- invalid index 
    end
    if( type(str) ~= "string" ) then
        ReportError("AttrValue:FindInAttr","Attempt to search for a non string value")
        return false -- invalid index 
    end
    -- CB: October 2, 2004, fixed the matching function to allow inclusion 
    -- to implement CRDB00025889
    local upperStr = string.upper(str)
    for k, v in pairs(self.tbl) do
        local upperV = string.upper(v)
        
        -- If we literally (not interpreting as a pattern) find the string 
        -- anywhere from 1st character on...
        if ( string.find(upperV,upperStr,1,true) ) then
            return true
        end
    end
    return false -- not found
end

-- ////////////////////////////////////////////////////////////////////////////
function AttrValue:AppendVal(v)
-- ////////////////////////////////////////////////////////////////////////////
    -- this doesn't check if self.tbl is matrix or not
    -- OFT
    if( not v and type(v) == "string" ) then
        ReportError("AttrValue:AppendVal","Attempt to append a nil value")
        return
    end
    -- CB: July 29, 2004, test code to figure out why the attributes are not converted to C++ attributes
     
    table.insert(self.tbl, v) -- this puts the values starting at INDEX ONE
    --self:Dump("Values after insert")
end

-- ////////////////////////////////////////////////////////////////////////////
function AttrValue:Append(av) -- av should be a AttrValue
-- ////////////////////////////////////////////////////////////////////////////
    -- this doesn't check if av is matrix or not
    if( not av ) then
        ReportError("AttrValue:Append","Attempt to append a nil attribute")
        return nil
    end
    for _, v in pairs(av.tbl) do
        table.insert(self.tbl, v)
    end    
end

-- ////////////////////////////////////////////////////////////////////////////
function AttrValue:InitFromTbl(t)
-- ////////////////////////////////////////////////////////////////////////////
    self.tbl = t
end

-- ////////////////////////////////////////////////////////////////////////////
function AttrValue:Dump(tblName)
-- ////////////////////////////////////////////////////////////////////////////
    if( AliDSTraceLevel < 2 ) then   
        return
    end
    
    if( not self.tbl ) then
        TraceC (tblName .. "No table \n",2)
        return
    end
    local nSize = table.getn(self.tbl)
    if( nSize == 0 ) then
        TraceC (tblName .. "Empty table \n", 2)
        return
    else
        TraceC (tblName .." Elements " .. nSize .. "\n",1)
    end
   
    for k, v in pairs(self.tbl) do
        if (type(v) == "table") then
            for k1, v1 in pairs(v) do
                if( not AliDSDebugEdit ) then
                    TraceC (tblName .. "[" .. k .. "][" .. k1 .. "]= ",1)
                    TraceC (v1.."\n",2)
                end
            end
        else
            TraceC (tblName .. "[" .. k .. "] = ",1)
            TraceC (v.."\n",1)
        end
    end
       
    TraceC ("End " .. tblName .. "\n",2)    
    
end

-- ////////////////////////////////////////////////////////////////////////////
function AttrValue:new(o)
-- ////////////////////////////////////////////////////////////////////////////
    o = o or { tbl = {} }
    setmetatable(o, self)
    self.__index = self 
    return o
end


--\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
--   ATTRIBUTE CLASS RELATED HELPERS AND MANIPULATORS
--/////////////////////////////////////////////////////////////////////////////

-- ////////////////////////////////////////////////////////////////////////////
function GetAttrDescription(group, element) -- integers, mandatory
-- ////////////////////////////////////////////////////////////////////////////
-- CB: November 14, 2005, added to access the attribute descriptions from EMF scripts
    local key = group * 65536 + element
    local dictName = GetAttrDescriptionC(key)
    if( dictName ) then
        dictName = "[" .. dictName .. "]" 
    else
        dictName = "[" .. group .."," .. element .. "]" 
    end
    return dictName
end


-- ////////////////////////////////////////////////////////////////////////////
function maketag(group, elem)
-- ////////////////////////////////////////////////////////////////////////////
    if( type(group) ~= "string" or type(elem) ~= "string") then
        ReportError("AttrValue::maketag","Input arguments are not strings")
        return nil
    end
    res = tonumber(group .. elem, 16)
    if ( not res ) then
        ReportError("AttrValue::maketag","Input arguments are invalid")
        return nil
    end
    return res
end


function DeclarePrivate()
    local P = {}
    setmetatable (P, {__index = _G })
    setfenv(2,  P)
end


--\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
--                           SORTING HINT CLASS DEFINITION
--/////////////////////////////////////////////////////////////////////////////

--=============================================================================
function HasMultiSortSupport( )
--=============================================================================
	return ( ALIDS_SCRIPTENGINE_VERSION >= 1 ) -- support in version 1 client
end--=============================================================================	

--=============================================================================
SortingHint = { m_vnuDicomTag = { } ; m_vbOrder = { } ; m_sName = "" ; m_sType = "" ; m_nSize = 0 }
--=============================================================================

--=============================================================================
function SortingHint:new( isName, isType )
--=============================================================================
	if( not isName or type( isName ) ~= "string" ) then
		ReportError( "SortingHint:new", "Invalid name string" )
		return nil
	end

	if( not isType or type( isType ) ~= "string" ) then
	ReportError( "SortingHint:new", "Invalid type string" )
	return nil
	end
    
	sorthint = { m_vnuDicomTag = { } ; m_vbOrder = { } ; m_sName = isName ; m_sType = isType ; m_nSize = 0 }
	setmetatable( sorthint, self )
	self.__index = self
	return sorthint
end --========================================================================

--=============================================================================
function SortingHint:GetName( )
--=============================================================================
	return self.m_sName
end --========================================================================

--=============================================================================
function SortingHint:GetType( )
--=============================================================================
	return self.m_sType
end --========================================================================

--=============================================================================
function SortingHint:GetNumberOfEntries( )
--=============================================================================
	return self.m_nSize
end--=========================================================================

--=============================================================================
function SortingHint:AddEntry( inuDicomTag, ibAscending )
--=============================================================================
	if( not inuDicomTag or type( inuDicomTag ) ~= "number" ) then
		ReportError( "SortingHint:AddEntry", "Invalid DICOM entry" )
		return false
	end

	if( self[ inuDicomTag ] == true ) then
		--ReportInfo( "SortingHint:AddEntry", "Entry already present" ) 
		--self:RemoveEntry( inuDicomTag )
		return true
	end

	table.insert( self.m_vnuDicomTag, inuDicomTag )
	table.insert( self.m_vbOrder, ( ibAscending ~= false ) )
	self.m_nSize = ( self.m_nSize + 1 )
	self[ inuDicomTag ] = true
	return true
end--=========================================================================

--=============================================================================
function SortingHint:InsertEntry( inuIndex, inuDicomTag, ibAscending )
--=============================================================================
	if( not inuIndex or type( inuIndex ) ~= "number" or inuIndex < 1 ) then
		ReportError( "SortingHint:InsertEntry", "Invalid insertion index" )
		return false
	end

	if( not inuDicomTag or type( inuDicomTag ) ~= "number" ) then
		ReportError( "SortingHint:InsertEntry", "Invalid DICOM entry" )
		return false
	end

	if( self[ inuDicomTag ] == true ) then
		ReportInfo( "SortingHint:InsertEntry", "Entry already present" )
		self:RemoveEntry( inuDicomTag )
    end
    
    table.insert( self.m_vnuDicomTag, inuIndex, inuDicomTag )
    table.insert( self.m_vbOrder, inuIndex, ( ibAscending ~= false ) )
	self.m_nSize = ( self.m_nSize + 1 )
	self[ inuDicomTag ] = true
	return true
end--=========================================================================

--=============================================================================
function SortingHint:FindEntry( inuDicomTag )
--=============================================================================
	if( not inuDicomTag or type( inuDicomTag ) ~= "number" ) then
		ReportError( "SortingHint:FindEntry", "Invalid DICOM entry" )
		return false
	end

	for index, element in pairs( self.m_vnuDicomTag ) do
		if( inuDicomTag == self.m_vnuDicomTag[ index ] ) then
			return index
		end
	end

	return false
end--=========================================================================

--=============================================================================
function SortingHint:HasEntry( inuDicomTag )
--=============================================================================
	return ( self:FindEntry( inuDicomTag ) ~= false )
end--=========================================================================

--=============================================================================
function SortingHint:RemoveEntry( inuDicomTag )
--=============================================================================
	if( not inuDicomTag or type( inuDicomTag ) ~= "number" ) then
		ReportError( "SortingHint:RemoveEntry", "Invalid DICOM entry" )
		return false
	end

	local nuIndex = self:FindEntry( inuDicomTag )

	if( nuIndex ) then
		table.remove( self.m_vnuDicomTag, nuIndex )
		table.remove( self.m_vbOrder, nuIndex )
		self.m_nSize = ( self.m_nSize - 1 )
		self[ inuDicomTag ] = nil
		return true
	end
    
    return false
end--=========================================================================

--=============================================================================
function SortingHint:RemoveEntryAt( inuIndex )
--=============================================================================
	if( not inuIndex or type( inuIndex ) ~= "number" ) then
		ReportError( "SortingHint:RemoveEntryAt", "Invalid removal index" )
		return false
	end

	if( inuIndex < 1 or inuIndex > self.m_nSize ) then
		ReportWarning( "SortingHint:RemoveEntryAt", "Index out of bounds" )
		return false
	end
    
	local nuDicomTag = m_vnuDicomTag[ inuIndex ]
	table.remove( self.m_vnuDicomTag, inuIndex )
	table.remove( self.m_vbOrder, inuIndex )
	self.m_nSize = ( self.m_nSize - 1 )
	self[ nuDicomTag ] = nil
	return true
end--=========================================================================

--=============================================================================
function SortingHint:Serialize( )
--=============================================================================
	local stacktable = { }

	if( ALIDS_SCRIPTENGINE_VERSION == 0 ) then
		for index, element in pairs( self.m_vnuDicomTag ) do
			local nPseudoOrder = 1.00
			if( self.m_vbOrder[ index ] == true ) then
				nPseudoOrder = 0.00 end

			table.insert( stacktable, self.m_vnuDicomTag[ index ] )
			table.insert( stacktable, nPseudoOrder )
		end
	end

	if( ALIDS_SCRIPTENGINE_VERSION == 1 ) then
		table.insert( stacktable, self.m_sName )
		table.insert( stacktable, self.m_sType )
		table.insert( stacktable, self.m_nSize )

		for index, element in pairs( self.m_vnuDicomTag ) do
			table.insert( stacktable, self.m_vnuDicomTag[ index ] )
			table.insert( stacktable, self.m_vbOrder[ index ] == false )
		end
	end

	return stacktable
end--=========================================================================

--\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
--                         SORTING HINT SET CLASS DEFINITION
--/////////////////////////////////////////////////////////////////////////////

--=============================================================================
SortingHintSet = { m_sDefaultType = "" ; m_voSortingHint = { } ; m_nSize = 0 }
--=============================================================================

--=============================================================================
function SortingHintSet:new( )
--=============================================================================
	hintset = { m_sDefaultType = "" ; m_voSortingHint = { } ; m_nSize = 0 }
	setmetatable( hintset, self )
	self.__index = self
	return hintset
end --========================================================================

--=============================================================================
function SortingHintSet:GetNumberOfHints( )
--=============================================================================
	return self.m_nSize
end--=========================================================================

--=============================================================================
function SortingHintSet:AddHint( ioSortingHint, ibDefault )
--=============================================================================
	if( not ioSortingHint or type( ioSortingHint ) ~= "table" ) then
		ReportError( "SortingHintSet:AddHint", "Invalid sorting hint" )
		return false
	end

	local sHintType = ioSortingHint:GetType( )
	if( self.m_voSortingHint[ sHintType ] ~= nil ) then
		ReportInfo( "SortingHintSet:AddHint", "Hint type already present" )
		self.m_nSize = ( self.m_nSize - 1 )
    end

	-- first hint added gets default status, last default hint overwrites
	if( ibDefault == true or self.m_sDefaultType == "" ) then
		self.m_sDefaultType = sHintType
	end

	self.m_voSortingHint[ sHintType ] = ioSortingHint
	self.m_nSize = ( self.m_nSize + 1 )
	return true
end--=========================================================================

--=============================================================================
function SortingHintSet:HasHint( isHintType )
--=============================================================================
	if( not isHintType or type( isHintType ) ~= "string" ) then
		ReportError( "SortingHintSet:HasHint", "Invalid hint type" )
		return false
	end

	return ( self.m_voSortingHint[ isHintType ] ~= nil )
end--=========================================================================

--=============================================================================
function SortingHintSet:GetHint( isHintType )
--=============================================================================
	if( not isHintType or type( isHintType ) ~= "string" ) then
		ReportError( "SortingHintSet:GetHint", "Invalid hint type" )
		return false
	end

	if( self.m_voSortingHint[ isHintType ] == nil ) then
		ReportInfo( "SortingHintSet:GetHint", "Type not present" )
		return false
	end

	return self.m_voSortingHint[ isHintType ]
end--=========================================================================

--=============================================================================
function SortingHintSet:GetHintAt( inuIndex )
--=============================================================================
	if( not inuIndex or type( inuIndex ) ~= "number" ) then
		ReportError( "SortingHintSet:GetHintAt", "Invalid hint index" )
		return false
	end

	if( inuIndex < 1 or inuIndex > self.m_nSize ) then
		ReportError( "SortingHintSet:GetHintAt", "Index out of bounds" )
		return false
	end

	for index, sHintType in pairs( self.m_voSortingHint ) do
		if( index == inuIndex ) then
			return self.m_voSortingHint[ sHintType ]
		end
	end
end--=========================================================================

--=============================================================================
function SortingHintSet:RemoveHint( isHintType )
--=============================================================================
	if( not isHintType or type( isHintType ) ~= "string" ) then
		ReportError( "SortingHintSet:RemoveHint", "Invalid hint type" )
		return false
	end

	if( self.m_voSortingHint[ isHintType ] == nil ) then
		ReportInfo( "SortingHintSet:RemoveHint", "Type not present" )
		return false
	end
    
	self.m_voSortingHint[ isHintType ] = nil
	self.m_nSize = ( self.m_nSize - 1 )
	
    if( self.m_sDefaultType == isHintType ) then
        self.m_sDefaultType = "" -- in case it's empty
        for sHintType in pairs( self.m_voSortingHint ) do
            self.m_sDefaultType = sHintType
            return true
        end
    end

	ErrorC("New default is "..sHintType.."/n")
	
	return true
end--=========================================================================

--=============================================================================
function SortingHintSet:RemoveHintAt( inuIndex )
--=============================================================================
	if( not inuIndex or type( inuIndex ) ~= "number" ) then
		ReportError( "SortingHintSet:RemoveHintAt", "Invalid hint index" )
		return false
	end

	if( inuIndex < 1 or inuIndex > self.m_nSize ) then
		ReportError( "SortingHintSet:RemoveHintAt", "Index out of bounds" )
		return false
	end

	for index, sHintType in pairs( self.m_voSortingHint ) do
		if( index == inuIndex ) then
			self.m_voSortingHint[ sHintType ] = nil
			self.m_nSize = ( self.m_nSize - 1 )
			
			if( self.m_sDefaultType == isHintType ) then
				self.m_sDefaultType = "" -- in case it's empty
				for sHintType in pairs( self.m_voSortingHint ) do
					self.m_sDefaultType = sHintType
					return true
				end
			end
			
			return true
		end
	end
end--=========================================================================

--=============================================================================
function SortingHintSet:Serialize( )
--=============================================================================
	if( ALIDS_SCRIPTENGINE_VERSION == 0 ) then
		local sorthint = self:GetHint( self.m_sDefaultType )
		return sorthint:Serialize( )
	end

	if( ALIDS_SCRIPTENGINE_VERSION == 1 ) then
		local stacktable = { }
		table.insert( stacktable, self.m_nSize )
		table.insert( stacktable, self.m_sDefaultType )

		for sHintType in pairs( self.m_voSortingHint ) do
			local sorthint = self.m_voSortingHint[ sHintType ]
            
			table.insert( stacktable, sorthint:GetType( ) )
			table.insert( stacktable, sorthint:GetName( ) )
			table.insert( stacktable, sorthint:GetNumberOfEntries( ) )

			for index, element in pairs( sorthint.m_vnuDicomTag ) do
				table.insert( stacktable, sorthint.m_vnuDicomTag[ index ] )
				table.insert( stacktable, sorthint.m_vbOrder[ index ] == false )
			end
			
		end
		return stacktable
	end
end--=========================================================================

--\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
--                           GROUPING HINT CLASS DEFINITION
--/////////////////////////////////////////////////////////////////////////////

--=============================================================================
GroupingHint = { m_vnuDicomTag = { } ; m_sName = "" ; m_nSize = 0 }
--=============================================================================

--=============================================================================
function GroupingHint:new( isName )
--=============================================================================
	if( not isName or type( isName ) ~= "string" ) then
		ReportError( "GroupingHint:new", "Invalid name string" )
		return nil
	end

	grouphint = { m_vnuDicomTag = { } ; m_sName = isName ; m_nSize = 0 }
	setmetatable( grouphint, self )
	self.__index = self
	return grouphint
end --========================================================================

--=============================================================================
function GroupingHint:GetName( )
--=============================================================================
	return self.m_sName
end --========================================================================

--=============================================================================
function GroupingHint:GetNumberOfEntries( )
--=============================================================================
	return self.m_nSize
end--=========================================================================

--=============================================================================
function GroupingHint:AddEntry( inuDicomTag )
--=============================================================================
	if( not inuDicomTag or type( inuDicomTag ) ~= "number" ) then
		ReportError( "GroupingHint:AddEntry", "Invalid DICOM entry" )
		return false
	end

	if( self[ inuDicomTag ] == true ) then
		ReportInfo( "GroupingHint:AddEntry", "Entry already present" )
		self:RemoveEntry( inuDicomTag )
	end

	table.insert( self.m_vnuDicomTag, inuDicomTag )
	self.m_nSize = ( self.m_nSize + 1 )
	self[ inuDicomTag ] = true
	return true
end--=========================================================================

--=============================================================================
function GroupingHint:InsertEntry( inuIndex, inuDicomTag )
--=============================================================================
	if( not inuIndex or type( inuIndex ) ~= "number" or inuIndex < 1 ) then
		ReportError( "GroupingHint:InsertEntry", "Invalid insertion index" )
		return false
	end

	if( not inuDicomTag or type( inuDicomTag ) ~= "number" ) then
		ReportError( "GroupingHint:InsertEntry", "Invalid DICOM entry" )
		return false
	end

	if( self[ inuDicomTag ] == true ) then
		ReportInfo( "GroupingHint:InsertEntry", "Entry already present" )
		self:RemoveHint( inuDicomTag )
	end

	table.insert( self.m_vnuDicomTag, inuIndex, inuDicomTag )
	self.m_nSize = ( self.m_nSize + 1 )
	self[ inuDicomTag ] = true
	return true
end--=========================================================================

--=============================================================================
function GroupingHint:FindEntry( inuDicomTag )
--=============================================================================
	if( not inuDicomTag or type( inuDicomTag ) ~= "number" ) then
		ReportError( "GroupingHint:FindEntry", "Invalid DICOM entry" )
		return false
	end

	for index, element in pairs( self.m_vnuDicomTag ) do
		if( inuDicomTag == self.m_vnuDicomTag[ index ] ) then
			return index
		end
	end

	return false
end--=========================================================================

--=============================================================================
function GroupingHint:HasEntry( inuDicomTag )
--=============================================================================
	return ( self:FindEntry( inuDicomTag ) ~= false )
end--=========================================================================

--=============================================================================
function GroupingHint:RemoveEntry( inuDicomTag )
--=============================================================================
	if( not inuDicomTag or type( inuDicomTag ) ~= "number" ) then
		ReportError( "GroupingHint:RemoveEntry", "Invalid DICOM entry" )
		return false
	end

	local nuIndex = self:FindEntry( inuDicomTag )
    
	if( nuIndex ) then
		table.remove( self.m_vnuDicomTag, nuIndex )
		self.m_nSize = ( self.m_nSize - 1 )
		self[ inuDicomTag ] = nil
		return true
	end

	return false
end--=========================================================================

--=============================================================================
function GroupingHint:RemoveEntryAt( inuIndex )
--=============================================================================
	if( not inuIndex or type( inuIndex ) ~= "number" ) then
		ReportError( "GroupingHint:RemoveEntryAt", "Invalid removal index" )
		return false
	end

	if( inuIndex < 1 or inuIndex > self.m_nSize ) then
		ReportError( "GroupingHint:RemoveEntryAt", "Index out of bounds" )
		return false
	end
    
	local nuDicomTag = m_vnuDicomTag[ inuIndex ]
	table.remove( self.m_vnuDicomTag, inuIndex )
	self.m_nSize = ( self.m_nSize - 1 )
	self[ nuDicomTag ] = nil
	return true
end--=========================================================================

--=============================================================================
function GroupingHint:Serialize( )
--=============================================================================
	if( ALIDS_SCRIPTENGINE_VERSION == 0 ) then
		return self.m_vnuDicomTag
	end

	if( ALIDS_SCRIPTENGINE_VERSION == 1 ) then
		
		local stacktable = { }
		table.insert( stacktable, self.m_sName )
		table.insert( stacktable, self.m_nSize )

		for index, element in pairs( self.m_vnuDicomTag ) do
			table.insert( stacktable, self.m_vnuDicomTag[ index ] )
		end
		return stacktable
    end
end--=========================================================================

--\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
--                    CACHE & ASSOCIATION TABLE DEFINITIONS
--/////////////////////////////////////////////////////////////////////////////

gSortingHintTable = { nSize = 0 } ; gGroupingHintTable = { nSize = 0 }
gAssociationTable = { grouphint = { } ; sorthintset = { } }

--=============================================================================
function CacheSortingHint( ioSortingHint )
--=============================================================================
	if( not ioSortingHint or ioSortingHint:GetNumberOfEntries( ) == 0 ) then
		ReportError( "CacheSortingHint", "Invalid sorting hint" )
		return false
	end

	local sHintName = ioSortingHint:GetName( )
	if( gSortingHintTable[ sHintName ] ~= nil ) then
		ReportInfo( "CacheSortingHint", "Overwriting hint: " .. sHintName )
		ClearSortingHintFromCache( sHintName )
	end

	gSortingHintTable.nSize = ( gSortingHintTable.nSize + 1 )
	gSortingHintTable[ sHintName ] = ioSortingHint
	return true
end --========================================================================

--=============================================================================
function GetNumberOfCachedSortingHints( isHintName )
--=============================================================================
	return gSortingHintTable.nSize
end --========================================================================

--=============================================================================
function GetSortingHintFromCache( isHintName )
--=============================================================================
	if( not isHintName or type( isHintName ) ~= "string" ) then
		ReportError( "GetSortingHintFromCache", "Invalid hint name" )
		return false
	end

	if( gSortingHintTable[ isHintName ] == nil ) then
		ReportInfo( "GetSortingHintFromCache", "Hint not found: " .. isHintName )
		return false
	end

	return gSortingHintTable[ isHintName ]
end --========================================================================

--=============================================================================
function GetSortingHintFromCacheAt( inuIndex )
--=============================================================================
	if( not inuIndex or type( inuIndex ) ~= "number" ) then
		ReportError( "GetSortingHintFromCacheAt", "Invalid hint index" )
		return false
	end

	if( inuIndex < 1 or inuIndex > gSortingHintTable.nSize ) then
		ReportError( "GetSortingHintFromCacheAt", "Index out of bounds" )
		return false
	end
	
	for index, sHintName in pairs( gSortingHintTable ) do
		if( index == inuIndex ) then
			return gSortingHintTable[ sHintName ]
		end
	end
end --========================================================================

--=============================================================================
function ClearSortingHintFromCache( isHintName )
--=============================================================================
	if( not isHintName or type( isHintName ) ~= "string" ) then
		ReportError( "ClearSortingHintFromCache", "Invalid hint name" )
		return false
	end

	if( gSortingHintTable[ isHintName ] == nil ) then
		ReportInfo( "ClearSortingHintFromCache", "Hint not found: " .. isHintName )
		return false
	end
    
	gSortingHintTable.nSize = ( gSortingHintTable.nSize - 1 )
	gSortingHintTable[ isHintName ] = nil
end --========================================================================

--=============================================================================
function ClearSortingHintFromCacheAt( inuIndex )
--=============================================================================
	if( not inuIndex or type( inuIndex ) ~= "number" ) then
		ReportError( "ClearSortingHintFromCacheAt", "Invalid hint index" )
		return false
	end

	if( inuIndex < 1 or inuIndex > gSortingHintTable.nSize ) then
		ReportError( "ClearSortingHintFromCacheAt", "Index out of bounds" )
		return false
	end
	
	for index, sHintName in pairs( gSortingHintTable ) do
		if( index == inuIndex ) then
			gSortingHintTable.nSize = ( gSortingHintTable.nSize - 1 )
			gSortingHintTable[ sHintName ] = nil
			return true
		end
	end
end --========================================================================

--=============================================================================
function CacheGroupingHint( ioGroupingHint )
--=============================================================================
	if( not ioGroupingHint or ioGroupingHint:GetNumberOfEntries( ) == 0 ) then
		ReportError( "CacheGroupingHint", "Invalid sorting hint" )
		return false
	end

	local sHintName = ioGroupingHint:GetName( )
	if( gGroupingHintTable[ sHintName ] ~= nil ) then
		ReportInfo( "CacheGroupingHint", "Overwriting hint: " .. sHintName )
		ClearGroupingHintFromCache( sHintName )
	end

	gGroupingHintTable.nSize = ( gGroupingHintTable.nSize + 1 )
	gGroupingHintTable[ sHintName ] = ioGroupingHint
	return true
end --========================================================================

--=============================================================================
function GetNumberOfCachedGroupingHints( isHintName )
--=============================================================================
	return gGroupingHintTable.nSize
end --========================================================================

--=============================================================================
function GetGroupingHintFromCache( isHintName )
--=============================================================================
	if( not isHintName or type( isHintName ) ~= "string" ) then
		ReportError( "GetGroupingHintFromCache", "Invalid hint name" )
		return false
	end

	if( gGroupingHintTable[ isHintName ] == nil ) then
		ReportInfo( "GetGroupingHintFromCache", "Hint not found: " .. isHintName )
		return false
	end

    return gGroupingHintTable[ isHintName ]
end --========================================================================

--=============================================================================
function GetGroupingHintFromCacheAt( inuIndex )
--=============================================================================
	if( not inuIndex or type( inuIndex ) ~= "number" ) then
		ReportError( "GetGroupingHintFromCacheAt", "Invalid hint index" )
		return false
	end

	if( inuIndex < 1 or inuIndex > gGroupingHintTable.nSize ) then
		ReportError( "GetGroupingHintFromCacheAt", "Index out of bounds" )
		return false
	end

	for index, sHintName in pairs( gGroupingHintTable ) do
		if( index == inuIndex ) then
			return gGroupingHintTable[ sHintName ]
		end
	end
end --========================================================================

--=============================================================================
function ClearGroupingHintFromCache( isHintName )
--=============================================================================
	if( not isHintName or type( isHintName ) ~= "string" ) then
		ReportError( "ClearGroupingHintFromCache", "Invalid hint name" )
		return false
	end

	if( gGroupingHintTable[ isHintName ] == nil ) then
		ReportInfo( "ClearGroupingHintFromCache", "Hint not found: " .. isHintName )
		return false
    end

	gGroupingHintTable.nSize = ( gGroupingHintTable.nSize - 1 )
	gGroupingHintTable[ isHintName ] = nil
end --========================================================================

--=============================================================================
function ClearGroupingHintFromCacheAt( inuIndex )
--=============================================================================
	if( not inuIndex or type( inuIndex ) ~= "number" ) then
		ReportError( "ClearGroupingHintFromCacheAt", "Invalid hint index" )
		return false
	end

	if( inuIndex < 1 or inuIndex > gGroupingHintTable.nSize ) then
		ReportError( "ClearGroupingHintFromCacheAt", "Index out of bounds" )
		return false
	end

	for index, sHintName in pairs( gGroupingHintTable ) do
		if( index == inuIndex ) then
			gGroupingHintTable.nSize = ( gGroupingHintTable.nSize - 1 )
			gGroupingHintTable[ sHintName ] = nil
			return true
		end
	end
end --========================================================================

--=============================================================================
function AddImage2Sequence( isSequenceName, ioGroupingHint, ioSortingHintSet )
--=============================================================================
	if( not isSequenceName or type( isSequenceName ) ~= "string" ) then
		ReportError( "AddImage2Sequence", "Invalid sequence name" )
		return false
	end

	if( not ioGroupingHint or ioGroupingHint:GetNumberOfEntries( ) == 0 ) then
		ReportError( "AddImage2Sequence", "Invalid grouping hint" )
		return false
	end

	if( not ioSortingHintSet or ioSortingHintSet:GetNumberOfHints( ) == 0 ) then
		ReportError( "AddImage2Sequence", "Invalid sorting hint set" )
		return false
	end

	gAssociationTable.grouphint[ isSequenceName ] = ioGroupingHint
	gAssociationTable.sorthintset[ isSequenceName ] = ioSortingHintSet

	-- these local containers get version-dependent content
	local sortstack = ioSortingHintSet:Serialize( )
	local groupstack = ioGroupingHint:Serialize( )
    
	if( ALIDS_SCRIPTENGINE_VERSION == 0 ) then
		return SetGroupingHintsC( groupstack, sortstack, ioGroupingHint:GetName( ) )
	end
    
	if( ALIDS_SCRIPTENGINE_VERSION == 1 ) then
		return SetGroupingHints2( groupstack, sortstack, isSequenceName ) -- SetGroupingHintsMultiSort
	end
end --========================================================================

--=============================================================================
function GetGroupingHint4Sequence( isSequenceName )
--=============================================================================
	if( not isSequenceName or type( isSequenceName ) ~= "string" ) then
		ReportError( "GetGroupingHint4Sequence", "Invalid sequence name" )
		return false
	end

	if( gAssociationTable.grouphint[ isSequenceName ] == nil ) then
		ReportInfo( "GetGroupingHint4Sequence",	"Sequence not found: " .. isSequenceName )
		return false
	end

	return gAssociationTable.grouphint[ isSequenceName ]
end --========================================================================

--=============================================================================
function GetSortingHintSet4Sequence( isSequenceName )
--=============================================================================
	if( not isSequenceName or type( isSequenceName ) ~= "string" ) then
		ReportError( "GetSortingHintSet4Sequence", "Invalid sequence name" )
		return nil
	end

	if( gAssociationTable.grouphint[ isSequenceName ] == nil ) then
		ReportInfo( "GetGroupingHint4Sequence",	"Sequence not found: " .. isSequenceName )
		return false
	end

	return gAssociationTable.sorthintset[ isSequenceName ]
end --========================================================================

--=============================================================================
function SortSequencesInStudy( ioSortingHint )
--=============================================================================
	if( not ioSortingHint or ioSortingHint:GetNumberOfEntries( ) == 0 ) then
		ReportError( "SortSequencesInStudy", "Invalid sorting hint" )
		return false
	end

	CacheSortingHint( ioSortingHint )
	local sortstack = ioSortingHint:Serialize( )
    
	if( ALIDS_SCRIPTENGINE_VERSION == 0 ) then
		return SetSortingHintsC( sortstack )
	end

	if( ALIDS_SCRIPTENGINE_VERSION == 1 ) then
		return SetSortingHints2( sortstack )
	end
end --========================================================================

--/////////////////////////////////////////////////////////////////////////////
--                          DELETION
--\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\


--=============================================================================
function RemoveImageFromSequence( isSequenceName )
--=============================================================================

    if( not isSequenceName or type( isSequenceName ) ~= "string" ) then
		ReportError( "RemoveImageFromSequence", "Invalid sequence name" )
        return false
    end

    if( ALIDS_SCRIPTENGINE_VERSION == 0 ) then
        return false -- not supported here!!
    end

    if( ALIDS_SCRIPTENGINE_VERSION == 1 ) then
        return VoidGroupingHintsC( isSequenceName )
    end
end --========================================================================

--=============================================================================
function RemoveSequencesSortHint( isSortHintName )
--=============================================================================

    if( not isSortHintName or type( isSortHintName ) ~= "string" ) then
        ReportError( "RemoveSequencesSortHint", "Invalid sorting hint name" )
        return false
    end

    if( ALIDS_SCRIPTENGINE_VERSION == 0 ) then
        return false -- not supported here!!
    end

    if( ALIDS_SCRIPTENGINE_VERSION == 1 ) then
        return VoidSortingHintsC( isSortHintName )
    end
end --========================================================================

-- ////////////////////////////////////////////////////////////////////////////
-- Misc 
-- ////////////////////////////////////////////////////////////////////////////


-- ////////////////////////////////////////////////////////////////////////////
function TraceC( isMessage, codeOp )
-- ////////////////////////////////////////////////////////////////////////////
    if( AliDSTraceLevel >= 2 ) then
        EngineTraceC( isMessage, codeOp )
    end
end

-- ////////////////////////////////////////////////////////////////////////////
function ReportError( isLocation, isMessage )
-- ////////////////////////////////////////////////////////////////////////////
	if( AliDSTraceLevel >= 0 ) then
		ErrorC( isLocation .. " : " .. isMessage )
		TraceC( debug.traceback( ) .. "\n" , 2 )
	end
end

-- ////////////////////////////////////////////////////////////////////////////
function ReportWarning( isLocation, isMessage )
-- ////////////////////////////////////////////////////////////////////////////
	if( AliDSTraceLevel >= 1 and ALIDS_SCRIPTENGINE_VERSION >= 1 ) then
		WarningC( isLocation .. " : " .. isMessage )
		TraceC( debug.traceback( ) .. "\n" , 2 )
	end
end

-- ////////////////////////////////////////////////////////////////////////////
function ReportInfo( isLocation, isMessage )
-- ////////////////////////////////////////////////////////////////////////////
	if( AliDSTraceLevel >= 2 ) then
		TraceC( isLocation .. " : " .. isMessage ..
			" - " .. debug.traceback( ) .. "\n", 2 )
	end
end

--/////////////////////////////////////////////////////////////////////////////
--                          Context Control Methods ( called by engine only )
--\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\

--/////////////////////////////////////////////////////////////////////////////
function ClearAttrCaches( )
--/////////////////////////////////////////////////////////////////////////////
    gGetAttrCache = { }
    gHasAttrCache = { }
end