Where Used Record Links.fh_lua

--[[
@Title:			Where Used Record Links
@Type:				Standard
@Author:			Mike Tate
@Contributors:	
@Version:			1.9
@Keywords:		
@LastUpdated:	19 Aug 2021
@Licence:			This plugin is copyright (c) 2021 Mike Tate & contributors and is licensed under the MIT License which is hereby incorporated by reference (see https://pluginstore.family-historian.co.uk/fh-plugin-licence)
@Description:	List all Where Used Record Links for any selected Records and mark duplicate usage with * and rich text links with @ in 'Data Ref Where Used' column.
@V1.9;				Fix for _FIELD metafield type = AD in doResultSet(); Fix Check Version in Store;
@V1.8:				Monthly check version in Plugin Store; Let same record be selected more than once; Fix *@ prefix combo; Fix _FIELD metafield Class;
@V1.7:				Fix progbar % miscalculation & add memory garbage collection; Make linked record detection more efficient; In doResultSet(...) correct unique Citation key and duplicate link script and add memory garbage collection to reduce 'Not responding';
@V1.6:				Variable width Result Set columns; Templated source/citation metafield and other extra columns; Fix Place record links; Fix LinksTo count for "HEAD.SUB[MN]" in FH V7;
@V1.5:				Include rich text @ Record Links allowing multiple instances; Make prompt retainable;
@V1.4:				FH V7 Lua 5.3 IUP 3.28 compatible; progbar 3.0; Added unique key for Source Citations; Exclude HEAD link count;
@V1.3:				Added test for duplicate usage marked with an * on Data Ref Where Used column.
@V1.2:				Added Witness Role & Notes, and correct Witness Citation details to Result Set columns.
@V1.1:				Added Media count Result Set columns, fix links from/to validity, and double check counts.
@V1.0:				First published in Plugin Store.
]]

local strVersion = "1.9"														-- Update when version changes

require "iuplua"																-- To access GUI window builder
require "lfs"																	-- To access LUA filing system

StrPilcrow = "  "																-- Newline Pilcrow symbol -- V1.6
if fhGetAppVersion() > 5 then												-- Cater for Unicode UTF-8 from FH Version 6 onwards
	fhSetStringEncoding("UTF-8")
	iup.SetGlobal("UTF8MODE","YES")
	iup.SetGlobal("UTF8MODE_FILE","NO")
	StrPilcrow  = fhConvertANSItoUTF8(StrPilcrow)
end

--[[
@Module:			+fh+progbar_v3
@Author:			Mike Tate
@Version:			3.0
@LastUpdated:	27 Aug 2020
@Description:	Progress Bar library module.
@V3.0:				Function Prototype Closure version.
@V1.0:				Initial version.
]]

local function progbar_v3()

	local fh = {}													-- Local environment table

	require "iuplua"												-- To access GUI window builder

	iup.SetGlobal("CUSTOMQUITMESSAGE","YES")					-- Needed for IUP 3.28

	local tblBars = {}												-- Table for optional external attributes
	local strBack = "255 255 255"								-- Background colour default is white
	local strBody = "0 0 0"										-- Body text colour default is black
	local strFont = nil												-- Font dialogue default is current font
	local strStop = "255 0 0"										-- Stop button colour default is red
	local intPosX = iup.CENTER									-- Show window default position is central
	local intPosY = iup.CENTER
	local intMax, intVal, intPercent, intStart, intDelta, intScale, strClock, isBarStop
	local lblText, barGauge, lblDelta, btnStop, dlgGauge

	local function doFocus()										-- Bring the Progress Bar window into Focus
		dlgGauge.BringFront="YES"									-- If used too often, inhibits other windows scroll bars, etc
	end -- local function doFocus

	local function doUpdate()										-- Update the Progress Gauge and the Delta % with clock
		barGauge.Value = intVal
		lblDelta.Title = string.format("%4d %%      %s ",math.floor(intPercent),strClock)
	end -- local function doUpdate

	local function doReset()										-- Reset all dialogue variables and Update display
		intVal		= 0													-- Current value of Progress Bar
		intPercent= 0.01											-- Percentage of progress
		intStart	= os.time()										-- Start time of progress
		intDelta	= 0													-- Delta time of progress
		intScale	= math.ceil( intMax / 1000 )					-- Scale of percentage per second of progress (initial guess is corrected in Step function)
		strClock	= "00 : 00 : 00"								-- Clock delta time display
		isBarStop	= false											-- Stop button pressed signal
		doUpdate()
		doFocus()
	end -- local function doReset

	function fh.Start(strTitle,intMaximum)						-- Create & start Progress Bar window
		if not dlgGauge then
			strTitle	= strTitle or ""							-- Dialogue and button title
			intMax		= intMaximum or 100							-- Maximun range of Progress Bar, default is 100
			local strSize = tostring( math.max( 100, string.len(" Stop "..strTitle) * 8 ) ).."x30"			-- Adjust Stop button size to Title
			lblText	= iup.label	{ Title=" "; Expand="YES"; Alignment="ACENTER"; Tip="Progress Message"; }
			barGauge	= iup.progressbar { RasterSize="400x30"; Value=0; Max=intMax; Tip="Progress Bar"; }
			lblDelta	= iup.label	{ Title=" "; Expand="YES"; Alignment="ACENTER"; Tip="Percentage and Elapsed Time"; }
			btnStop	= iup.button	{ Title=" Stop "..strTitle; RasterSize=strSize; FgColor=strStop; Tip="Stop Progress Button"; action=function() isBarStop = true end; }	-- Signal Stop button pressed	return iup.CLOSE -- Often caused main GUI to close !!!
			dlgGauge	= iup.dialog	{ Title=strTitle.." Progress "; Font=strFont; FgColor=strBody; Background=strBack; DialogFrame="YES";	-- Remove Windows minimize/maximize menu
								iup.vbox{ Alignment="ACENTER"; Gap="10"; Margin="10x10";
									lblText;
									barGauge;
									lblDelta;
									btnStop;
								};
								move_cb	= function(self,x,y) tblBars.X = x tblBars.Y = y end;
								close_cb	= btnStop.action;		-- Windows Close button = Stop button
							}
			if type(tblBars.GUI) == "table"
			and type(tblBars.GUI.ShowDialogue) == "function" then
				dlgGauge.move_cb = nil								-- Use GUI library to show & move window
				tblBars.GUI.ShowDialogue("Bars",dlgGauge,btnStop,"showxy")
			else
				dlgGauge:showxy(intPosX,intPosY)				-- Show the Progress Bar window
			end
			doReset()													-- Reset the Progress Bar display
		end
	end -- function Start

	function fh.Message(strText)									-- Show the Progress Bar message
		if dlgGauge then lblText.Title = strText end
	end -- function Message

	function fh.Step(intStep)										-- Step the Progress Bar forward
		if dlgGauge then
			intVal = intVal + ( intStep or 1 )					-- Default step is 1
			local intNew = math.ceil( intVal / intMax * 100 * intScale ) / intScale
			if intPercent ~= intNew then							-- Update progress once per percent or per second, whichever is smaller
				intPercent = math.max( 0.1, intNew )			-- Ensure percentage is greater than zero
				if intVal > intMax then intVal = intMax intPercent = 100 end		-- Ensure values do not exceed maximum
				intNew = os.difftime(os.time(),intStart)
				if intDelta < intNew then							-- Update clock of elapsed time
					intDelta = intNew
					intScale = math.ceil( intDelta / intPercent )	-- Scale of seconds per percentage step
					local intHour = math.floor( intDelta / 3600 )
					local intMins = math.floor( intDelta / 60 - intHour * 60 )
					local intSecs = intDelta - intMins * 60 - intHour * 3600
					strClock = string.format("%02d : %02d : %02d",intHour,intMins,intSecs)
				end
				doUpdate()											-- Update the Progress Bar display
			end
			iup.LoopStep()
		end
	end -- function Step

	function fh.Focus()												-- Bring the Progress Bar window to front
		if dlgGauge then doFocus() end
	end -- function Focus

	function fh.Reset()												-- Reset the Progress Bar display
		if dlgGauge then doReset() end
	end -- function Reset

	function fh.Stop()												-- Check if Stop button pressed
		iup.LoopStep()
		return isBarStop
	end -- function Stop

	function fh.Close()												-- Close the Progress Bar window
		isBarStop = false
		if dlgGauge then dlgGauge:destroy() dlgGauge = nil end
	end -- function Close

	function fh.Setup(tblSetup)									-- Setup optional table of external attributes
		if tblSetup then
			tblBars = tblSetup
			strBack = tblBars.Back or strBack					-- Background colour
			strBody = tblBars.Body or strBody					-- Body text colour
			strFont = tblBars.Font or strFont					-- Font dialogue
			strStop = tblBars.Stop or strStop					-- Stop button colour
			intPosX = tblBars.X or intPosX						-- Window position
			intPosY = tblBars.Y or intPosY
		end
	end -- function Setup

	return fh

end -- local function progbar_v3

local progbar = progbar_v3()										-- To access FH progress bars module

--[[
@Function:		BuildDataRef
@Description:	Get Full Data Reference for Pointer
@Parameters:		Item Pointer
@Returns:			Data Reference String, Record Id Integer, Record Type Tag String
@Requires:		None
]]
 
function BuildDataRef(ptrRef)
 
	local strDataRef = ""		-- Data Reference with instance indices e.g. INDI.RESI[2].ADDR
	local intRecId   = 0		-- Record Id for associated Record
	local strRecTag  = ""		-- Record Tag of associated Record type i.e. INDI, FAM, NOTE, SOUR, etc
 
	-- getDataRef() is called recursively per level of the Data Ref
	-- ptrRef points to the upper Data Ref levels yet to be analysed
	-- strRef compiles the lower Data Ref levels including instances
 
	local function getDataRef(ptrRef,strRef)
		local ptrTag = ptrRef:Clone()
		local strTag = fhGetTag(ptrTag)				-- Current level Tag
		ptrTag:MoveToParentItem(ptrTag)
		if ptrTag:IsNotNull() then					-- Parent level exists
			local intSib = 1
			local ptrSib = ptrRef:Clone()			-- Pointer to siblings with same Tag
			ptrSib:MovePrev("SAME_TAG")
			while ptrSib:IsNotNull() do				-- Count previous siblings with same Tag
				intSib = intSib + 1
				ptrSib:MovePrev("SAME_TAG")
			end
			if intSib > 1 then 	strTag = strTag.."["..intSib.."]" end
			getDataRef(ptrTag,"."..strTag..strRef)	-- Now analyse the parent level
		else
			strDataRef = strTag..strRef				-- Record level reached, so set return values
			intRecId   = fhGetRecordId(ptrRef)
			strRecTag  = strTag
			if not fhIsValidDataRef(strDataRef) then print(strDataRef.." is Invalid") end
		end
	end -- local function getDataRef
 
	if type(ptrRef) == "userdata" then getDataRef(ptrRef,"") end
 
	return strDataRef, intRecId, strRecTag
 
end -- function BuildDataRef
 
function intRecordCount(strType)											-- Count number of records of chosen Record Type
	-- strType	~ Record type tag
	local ptrType = fhNewItemPtr()
	local intCount = 0
	ptrType:MoveToFirstRecord(strType)
	while ptrType:IsNotNull() do
		intCount = intCount + 1
		ptrType:MoveNext()
	end
	return intCount
end -- function intRecordCount

function Main()

	local strRecTag = nil														-- Selected Record Type tag
	local arrLinks  = {}														-- Count of Where Used Links per Record
	local dicWhere  = {}														-- Dictionary of Where Used to detect duplicates	-- V1.3
	local intTwice  =  0														-- Count where same record used same place twice	-- V1.3
	local intUnique	=  0														-- Unique key for Citations with identical fields	-- V1.4
	local intRecMax = fhGetRecordTypeCount() + 1							-- Number of record types including HEAD
	local tblRecord = { Min=6; Max=50; }									-- Result Set tables
	local tblRec_Id = {}
	local tblRecObj = {}														-- V1.1
	local tblUseTyp = {}
	local tblUseRec = { Min=6; Max=50; }
	local tblUse_Id = {}
	local tblUseObj = {}														-- V1.1
	local tblUseRef = {}
	local tblUseTxt = { Min=6; Max=50; }
	local tblUsePtr = {}
	local tblColumn = {}														-- Table of more columns for Record Type

	local dicType = 
	{ INDI = { Col={}; Name="Individual"; };								-- Record Type dictionary
	  FAM  = { Col={}; Name="Family"; };
	  NOTE = { Col={}; Name="Note";   };
	  SOUR = { Col={}; Name="Source"; };
	  REPO = { Col={}; Name="Repository"; };
	  SUBM = { Col={}; Name="Submitter";  };
	  SUBN = { Col={}; Name="Submission"; };
	  OBJE = { Col={}; Name="Media"; };
	 _PLAC = { Col={}; Name="Place"; };
	 _RNOT = { Col={}; Name="Research Note";   };						-- V1.4
	 _SRCT = { Col={}; Name="Source Template"; };						-- V1.4
	  HEAD = { Col={}; Name="Head";  };
	}

	local function doTypeDetails(strLink,strRef,intMax,strName,strMode)	-- More Result Set Column details for Record Type dictionary
		-- strLink	~ Link record type tag
		-- strRef 	~ Data ref relative to link or "Media" or "Key"
		-- intMax 	~ Max char width of Result Set column
		-- strName	~ Name of Result Set column
		-- strMode	~ Mode of Result Set column if not "item"
		local dicType = dicType[strLink]
		if strRef == "Media" then												-- V1.1
			dicType.Media = true
		elseif strRef then
			local intMin = 3													-- Min char width of Result Set column for "item" pointers -- V1.6
			if strMode then intMin = 6 end									-- Larger width for other column modes "integer" or "text"
			local intMore = #dicType.Col + 1								-- Column number index -- V1.6
			table.insert(dicType.Col,{ More=intMore; Text=0; Char=0; Ref=strRef; Min=intMin; Max=intMax; Name=strName; Mode=(strMode or "item"); })	-- V1.6
			tblColumn[intMore] = {}											-- dicType.Col and tblColumn share same index	-- V1.6
		end
	end -- local function doTypeDetails

	doTypeDetails( "INDI" , "Media" )										-- V1.1
	doTypeDetails( "INDI" , "~.ROLE"      , 25, "Role/Association")	-- V1.2
	doTypeDetails( "INDI" , "~.NOTE2"     , 50, "Local Note" )	 	-- V1.2
	doTypeDetails( "INDI" , "+.NOTE2"     , 50, "Record Note" )		-- V1.6

	doTypeDetails( "FAM"  , "Media" )										-- V1.1
	doTypeDetails( "FAM"  , "+.NOTE2"     , 50, "Record Note" )		-- V1.6

	doTypeDetails( "NOTE" )
	doTypeDetails( "NOTE" , "+.NOTE2"     , 50, "Record Note" )		-- V1.6

	doTypeDetails( "SOUR" , "Media" )										-- V1.1
	doTypeDetails( "SOUR" , "Key"         ,  5, "Key", "integer" )	-- Unique Citation Key -- V1.4
	doTypeDetails( "SOUR" , "~.DATA.DATE" , 25, "Entry Date" )
	doTypeDetails( "SOUR" , "~.QUAY"      , 25, "Assessment" )		-- FH V7 also finds "~._QUAY"
	doTypeDetails( "SOUR" , "~.PAGE"      , 50, "Where within Source" )
	doTypeDetails( "SOUR" , "~.DATA.TEXT" , 50, "Text From Source" )
	doTypeDetails( "SOUR" , "~.NOTE2"     , 50, "Citation Note" )
	doTypeDetails( "SOUR" , "~._FIELD[1]" , 50, "Metafield 1" )		-- V1.6
	doTypeDetails( "SOUR" , "~._FIELD[2]" , 50, "Metafield 2" )		-- V1.6
	doTypeDetails( "SOUR" , "~._FIELD[3]" , 50, "Metafield 3" )		-- V1.6

	doTypeDetails( "REPO" , "+._TYPE"     , 25, "Generic Type" )
	doTypeDetails( "REPO" , "+.AUTH"      , 25, "Author" )
	doTypeDetails( "REPO" , "+.PUBL"      , 50, "Publication Information" )
	doTypeDetails( "REPO" , "+.TEXT"      , 50, "Text From Source" )-- V1.6
	doTypeDetails( "REPO" , "+.NOTE2"     , 50, "Record Note" )		-- V1.6
	doTypeDetails( "REPO" , "+._FIELD[1]" , 50, "Metafield 1" )		-- V1.6
	doTypeDetails( "REPO" , "+._FIELD[2]" , 50, "Metafield 2" )		-- V1.6
	doTypeDetails( "REPO" , "+._FIELD[3]" , 50, "Metafield 3" )		-- V1.6
	doTypeDetails( "REPO" , "+._FIELD[4]" , 50, "Metafield 4" )		-- V1.6
	doTypeDetails( "REPO" , "+._FIELD[5]" , 50, "Metafield 5" )		-- V1.6

	doTypeDetails( "SUBM" )
	doTypeDetails( "SUBM" , "+.NOTE2"     , 50, "Record Note" )		-- V1.6

	doTypeDetails( "SUBN" )
	doTypeDetails( "SUBN" , "+.SUBM"      , 50, "Submitter" )			-- V1.6

	local strAnnotation = ">NOTE2"											-- FH V6
	if fhGetAppVersion() > 6 then
			strAnnotation = ">NOTE2._NOTA"									-- FH V7 -- V1.4
	end
	doTypeDetails( "OBJE" , ">NOTE2._AREA", 25, "Frame Area" )
	doTypeDetails( "OBJE" , ">NOTE2._EXCL", 25, "Exclude From" )
	doTypeDetails( "OBJE" , ">NOTE2._CAPT", 25, "Caption?" )
	doTypeDetails( "OBJE" , strAnnotation , 50, "Link Note" ) 		-- FH V7 -- V1.4 -- V1.6
	doTypeDetails( "OBJE" , "+.NOTE2"     , 50, "Record Note" )		-- V1.6

	doTypeDetails("_PLAC" , "Media" )										-- V1.1
	doTypeDetails("_PLAC" , "<.ADDR"      , 50, "Fact Address" )
	doTypeDetails("_PLAC" , "<.NOTE2"     , 50, "Fact Note" )
	doTypeDetails("_PLAC" , "+.NOTE2"     , 50, "Record Note" )		-- V1.6

	doTypeDetails("_RNOT" )													-- V1.4
	doTypeDetails("_RNOT" , "+.NOTE2"     , 50, "Record Note" )		-- V1.6

	doTypeDetails("_SRCT" )													-- V1.4
	doTypeDetails("_SRCT" , "+._TYPE"     , 25, "Generic Type" )		-- V1.6
	doTypeDetails("_SRCT" , "+.AUTH"      , 25, "Author" ) 			-- V1.6
	doTypeDetails("_SRCT" , "+.PUBL"      , 50, "Publication Information" )
	doTypeDetails("_SRCT" , "+.TEXT"      , 50, "Text From Source" )-- V1.6
	doTypeDetails("_SRCT" , "+.NOTE2"     , 50, "Record Note" )		-- V1.6
	doTypeDetails("_SRCT" , "+._FIELD[1]" , 50, "Metafield 1" )		-- V1.6
	doTypeDetails("_SRCT" , "+._FIELD[2]" , 50, "Metafield 2" )		-- V1.6
	doTypeDetails("_SRCT" , "+._FIELD[3]" , 50, "Metafield 3" )		-- V1.6
	doTypeDetails("_SRCT" , "+._FIELD[4]" , 50, "Metafield 4" )		-- V1.6
	doTypeDetails("_SRCT" , "+._FIELD[5]" , 50, "Metafield 5" )		-- V1.6

	doTypeDetails( "HEAD" )
	doTypeDetails( "HEAD" , "+.NOTE2"     , 50, "Record Note" )		-- V1.6

	local function arrMediaCount(arrObje)									-- Count Media Objects -- V1.1
		-- arrObje	~ Array listing media
		local arrMedia = {}
		for intMedia = 1, #arrObje do arrMedia[intMedia] = 0 end		-- Zero Media counters -- V1.1
		repeat
			local anyMedia = false
			for intMedia, ptrMedia in ipairs (arrObje) do
				if ptrMedia:IsNotNull() then
					anyMedia = true
					arrMedia[intMedia] = arrMedia[intMedia] + 1			-- Count each kind of Media -- V1.1
					ptrMedia:MoveNext("SAME_TAG")
				end
			end
		until not anyMedia
		return arrMedia
	end -- local function arrMediaCount

	local function doTextLength(tblItem,anyItem)							-- tblItem.Long = longest text in column, tblItem.Char = next to longest -- V1.6
		-- tblItem	~ Table that holds parameters
		-- anyItem	~ Length of text or pointer to text
		local intItem = anyItem
		if type(anyItem) ~= "number" then
			intItem = #fhGetDisplayText(anyItem,"","MIN") + 1			-- Get display text length for pointer item + 1 for record icon
		end
		if intItem > 0 then
			intItem = intItem - math.floor(intItem/10)					-- Reduce length by 10% otherwise columns are too wide
			if intItem > ( tblItem.Long or 0 ) then
				tblItem.Char = tblItem.Long or intItem 					-- Previous longest item or initial item length
				tblItem.Long = intItem 										-- Save length of longest item so far
			elseif intItem < tblItem.Long then
				if tblItem.Char == tblItem.Long then
					tblItem.Char = 0											-- Ensure subsequent item char length is used
				end
				tblItem.Char = math.max(intItem,tblItem.Char)			-- Save length of next to longest item so far
			end
		end
	end -- local function doTextLength

	local function doResultSet(intRecord,ptrRecord,intType,ptrUsed,ptrItem)
		-- intRecord	~ Index into current record list
		-- ptrRecord	~ Pointer to current record item
		-- intType	~ Index to the current record type
		-- ptrUsed	~ Pointer to parent record where current record used
		-- ptrItem	~ Pointer to the data item where current record used
		doTextLength(tblRecord,ptrRecord)
		table.insert(tblRecord,ptrRecord:Clone())							-- Update Result Set table columns
		table.insert(tblRec_Id,fhGetRecordId(ptrRecord))
		if dicType[strRecTag].Media then									-- V1.1
			local arrObje = {
				fhGetItemPtr(ptrRecord,"~.OBJE") ;							-- Media pointers -- V1.1
				fhGetItemPtr(ptrRecord,"~.OBJE2");
			}
			local arrMedia = arrMediaCount(arrObje)						-- Media counts -- V1.1
			table.insert(tblRecObj,tostring(arrMedia[1]+arrMedia[2]))
		end
		table.insert(tblUseTyp,intType or 99)
		if ptrUsed then															-- Record has Where Used Link
			local strData, intRecId = BuildDataRef(ptrItem)
			local ptrRoot = fhNewItemPtr()
			ptrRoot:MoveToParentItem(ptrItem)
			local arrObje = {
				fhGetItemPtr(ptrRoot,"~.OBJE") ;							-- Media pointers -- V1.1
				fhGetItemPtr(ptrRoot,"~.OBJE2");
				fhGetItemPtr(ptrItem,"~.OBJE") ;
				fhGetItemPtr(ptrItem,"~.OBJE2");
			}
			local strUnique, intKeyCol										-- Citation unique fields and "Key" column	-- V1.4
			local arrMedia = arrMediaCount(arrObje)						-- Media counts -- V1.1
			local strRole = fhGetValueAsText(fhGetItemPtr(ptrItem,"~.ROLE"))	-- Need Role only for Individuals in duplicate detection below	-- V1.3
			doTextLength(tblUseRec,ptrUsed)
			table.insert(tblUseRec,ptrUsed:Clone())
			table.insert(tblUse_Id,intRecId)
			table.insert(tblUseObj,tostring(arrMedia[1]+arrMedia[2]).." / "..tostring(arrMedia[3]+arrMedia[4])) -- V1.1
			doTextLength(tblUseTxt,ptrRoot)
			table.insert(tblUseTxt,fhGetDisplayText(ptrRoot))
			if strData:match("_SHAR") then
				table.insert(tblUsePtr,ptrRoot:Clone())					-- ptrRoot is better for Witness links for this field -- V1.2
			else
				table.insert(tblUsePtr,ptrItem:Clone())					-- but must retain ptrItem for all subsequent fields -- V1.2
			end
			for intCol, tblCol in ipairs (dicType[strRecTag].Col) do
				local strRef = tblCol.Ref										-- Update optional Result Set columns depending on search record type
				if strRef == "Key" then
					strUnique = tostring(intRecord)..":"					-- Unique source for replica citation Key column	-- V1.4
					intKeyCol = tblCol.More
				else
					if strRef:match("ROLE") and strData:match("INDI.ASSO") then	-- V1.6
						strRef = strRef:gsub("ROLE","RELA")
					elseif strRef:match("^%+") then							-- Reference is relative to record where used	-- V1.6
						strRef = strRef:gsub("^%+","~")
						ptrItem = ptrUsed:Clone()
					elseif strRef:match("^<") then							-- Reference is relative to parent of link		-- V1.6
						strRef = strRef:gsub("^<","~")
						ptrItem = ptrRoot:Clone()
					elseif strRef:match("^>NOTE2") then 					-- Reference is a media link note
						strRef = strRef:gsub("^>NOTE2","~")
						if fhGetAppVersion() <= 6 then						-- V1.4
							local strAsid = fhGetValueAsText(fhGetItemPtr(ptrItem,"~._ASID"))
							local ptrNote = fhNewItemPtr()
							ptrNote:MoveTo(ptrRecord,"~.NOTE2")
							while ptrNote:IsNotNull() do
								if strAsid == fhGetValueAsText(fhGetItemPtr(ptrNote,"~._ASID")) then
									ptrItem = ptrNote:Clone()
									break
								end
								ptrNote:MoveNext("SAME_TAG")
							end
						end
					end
					local anyRef = fhGetItemPtr(ptrItem,strRef)
					local strRef = ""
					if tblCol.Mode == "text" then
						strRef = fhGetDisplayText(anyRef)
						anyRef = fhGetValueAsText(anyRef)
						doTextLength(tblCol,#anyRef)							-- Set length of text to which column can be shrunk -- V1.6
					elseif tblCol.Mode == "item" then
						local strClass = fhGetDataClass(anyRef)
						if strClass == "longtext" or strClass == "richtext" or fhGetMetafieldType(anyRef) == "AD" then	-- _FIELD is an Address	-- V1.9
							tblCol.Min = 6
							tblCol.Class = "longtext"							-- Get field long/richtext to go with a buddy column -- V1.6
							strRef = fhGetValueAsText(anyRef):gsub("\n",StrPilcrow)
						else
							strRef = fhGetDisplayText(anyRef,"","MIN")	-- Get display text for other field types
						end
						doTextLength(tblCol,#strRef)							-- Set length of text to which column can be shrunk -- V1.6
						if tblCol.Text == 0 then
							tblColumn[#tblColumn+1] = { }					-- Add "text" column to go with "item" buddy column -- V1.6
							tblCol.Text = #tblColumn
						end
						table.insert(tblColumn[tblCol.Text],strRef)
					end
					table.insert(tblColumn[tblCol.More],anyRef)
					if intKeyCol then
						strUnique = strUnique..strRef						-- Unique text for replica citation detection	-- V1.7 code move here
					end
				end
			end
			if intKeyCol then
				if not dicWhere[strUnique] then								-- Unique key number for each replica citation
					intUnique = intUnique + 1
					dicWhere[strUnique] = intUnique
				end
				table.insert(tblColumn[intKeyCol],dicWhere[strUnique])
			end
			if intType < intRecMax
			or ( fhGetAppVersion() > 6 and strData:match("HEAD.SUB[MN]") ) then		-- V1.6
				arrLinks[intRecord] = ( arrLinks[intRecord] or 0 ) + 1	-- HEAD._PCIT record links discounted by LinksTo() function	-- V1.4
			end
			-- Detect duplicate usage for intRecord of intType in intRecId for strData Ref & Role						-- V1.3 -- V1.7 code moved here
			local strWhat = strData:gsub("%[%d+%]$","")					-- Remove a trailing Data Reference [index]	-- V1.3
			strWhat = tostring(intRecord).."of"..intType.."in"..intRecId..strWhat..strRole..(dicWhere[strUnique] or "") -- V1.7
			local intUsed = dicWhere[strWhat]								-- Lookup dictionary to see if used before	-- V1.3
			if intUsed then														-- Used before so have old Result Set entry	-- V1.3
				intTwice = intTwice + 1
				strData = "*"..strData											-- Asterisk the new & old duplicate entries	-- V1.3
				tblUseRef[intUsed] = tblUseRef[intUsed]:gsub("^%**","*")
			else
				dicWhere[strWhat] = #tblUseRef+1							-- Dictionary entry with Result Set index		-- V1.3
			end
			strData = strData:gsub("^(%*?)(.-%._LINK_%u)","%1@%2")		-- Mark rich text Record Links with @ symbol	-- V1.5	-- V1.8
			table.insert(tblUseRef,strData)
		else
			local strUnused = "< No 'Where Used Record Links' found. >"
			table.insert(tblUseRec,fhNewItemPtr())							-- Record is unused
			table.insert(tblUse_Id,nil)
			doTextLength(tblUseTxt,#strUnused)								-- V1.6
			table.insert(tblUseTxt, strUnused)
			table.insert(tblUsePtr,fhNewItemPtr())
		end
		collectgarbage("step",0)												-- Memory garbage collection to avoid 'Not responding' -- V1.7
	end -- local function doResultSet

	local function isLink(ptrItem)											-- Detect fhGetDataClass() & fhGetMetafieldType() links -- V1.8
		local dicLink = {
			place	= true ;														-- Place _PLAC record link
			link	= true ;														-- Any other record link
			PL		= true ;														-- Metafield _FIELD with Place record link
			RP		= true ;														-- Metafield _FIELD with Repository link
		}
		local strClass = fhGetDataClass(ptrItem)
		if strClass == "metafield" then
			strClass = fhGetMetafieldType(ptrItem)
		end
		return dicLink[strClass]
	end -- local function isLink

	local tblLinks = {															-- Links from/to validity table and Names -- V1.1 -- V1.4 -- V1.5 only HEAD has no rich text links
		INDI = { INDI=true; FAM=true; NOTE=true; SOUR=true; REPO=true; SUBM=true; SUBN=true; OBJE=true; _PLAC=true; _RNOT=true; _SRCT=true; HEAD=nil; Name="Individuals";		};
		FAM  = { INDI=true; FAM=true; NOTE=true; SOUR=true; REPO=true; SUBM=true; SUBN=true; OBJE=true; _PLAC=true; _RNOT=true; _SRCT=true; HEAD=nil; Name="Families";			};
		NOTE = { INDI=true; FAM=true; NOTE=true; SOUR=true; REPO=true; SUBM=true; SUBN=true; OBJE=true; _PLAC=true; _RNOT=true; _SRCT=true; HEAD=nil; Name="Notes";				};
		SOUR = { INDI=true; FAM=true; NOTE=true; SOUR=true; REPO=true; SUBM=true; SUBN=true; OBJE=true; _PLAC=true; _RNOT=true; _SRCT=true; HEAD=nil; Name="Sources";				};
		REPO = { INDI=true; FAM=true; NOTE=true; SOUR=true; REPO=true; SUBM=true; SUBN=true; OBJE=true; _PLAC=true; _RNOT=true; _SRCT=true; HEAD=nil; Name="Repositories";		};
		SUBM = { INDI=true; FAM=true; NOTE=true; SOUR=true; REPO=true; SUBM=true; SUBN=true; OBJE=true; _PLAC=true; _RNOT=true; _SRCT=true; HEAD=nil; Name="Submitters";			};
		SUBN = { INDI=true; FAM=true; NOTE=true; SOUR=true; REPO=true; SUBM=true; SUBN=true; OBJE=true; _PLAC=true; _RNOT=true; _SRCT=true; HEAD=nil; Name="Submissions";		};
		OBJE = { INDI=true; FAM=true; NOTE=true; SOUR=true; REPO=true; SUBM=true; SUBN=true; OBJE=true; _PLAC=true; _RNOT=true; _SRCT=true; HEAD=nil; Name="Multimedia";			};
		_PLAC= { INDI=true; FAM=true; NOTE=true; SOUR=true; REPO=true; SUBM=true; SUBN=true; OBJE=true; _PLAC=true; _RNOT=true; _SRCT=true; HEAD=nil; Name="Places";				};
		_RNOT= { INDI=true; FAM=true; NOTE=true; SOUR=true; REPO=true; SUBM=true; SUBN=true; OBJE=true; _PLAC=true; _RNOT=true; _SRCT=true; HEAD=nil; Name="Research Notes";		};	-- V1.4
		_SRCT= { INDI=true; FAM=true; NOTE=true; SOUR=true; REPO=true; SUBM=true; SUBN=true; OBJE=true; _PLAC=true; _RNOT=true; _SRCT=true; HEAD=nil; Name="Source Templates";	};	-- V1.4
		HEAD = { INDI=nil;  FAM=nil;  NOTE=true; SOUR=true; REPO=nil;  SUBM=true; SUBN=true; OBJE=true; _PLAC=nil;  _RNOT=nil;  _SRCT=nil;  HEAD=nil; Name="Headers";				};
	}

	local strFileName = fhGetContextInfo("CI_APP_DATA_FOLDER").."\\Plugin Data\\Where Used Record Links.dat"
	local lfsFileName = lfs.attributes(strFileName,"mode") == "file"

	for intType = 1, intRecMax do											-- Search each record type
		local strType = ""
		local arrRecs = {}														-- Array of selected records
		local dicRecs = {}														-- Dictionary of Record Id (or Placenames) to array index of selected records
		if intType < intRecMax and lfsFileName then						-- Check if any Records pre-selected	-- V1.5
			strType = fhGetRecordTypeTag(intType)
			arrRecs = fhGetCurrentRecordSel(strType)
		else
			local ptrItem = fhNewItemPtr()
			local strForm = " Record Type: %l| "							-- Otherwise prompt for Record selection
			local strKeep = " Keep Prompt: %b[ Prompt hidden, Hover for advice]{Untick to hide this prompt and always select records before running plugin}\n"	-- V1.5
			local intKeep = 1													-- V1.5
			if lfsFileName then intKeep = 0 end
			local dicData = {}
			local intData = 0
			for intType = 1, intRecMax-1 do									-- Build iup.GetParam droplist format
				strType = fhGetRecordTypeTag(intType)
				ptrItem:MoveToFirstRecord(strType)
				if ptrItem:IsNotNull() then									-- Records of this Type exist
					strForm = strForm..dicType[strType].Name.." | "
					table.insert(dicData,intType)
					if strType == "SOUR" then intData = intType-1 end	-- Default to Source Record Type
				end
			end
			strForm = strForm.."|{Choose the required record type to analyse}\n"	-- V1.5
			local isOK, intData, intKeep = iup.GetParam("Choose Record Type",nil,strForm..strKeep,intData,intKeep)
			if isOK then
				strType = fhGetRecordTypeTag(dicData[intData+1])
				arrRecs = fhPromptUserForRecordSel(strType)				-- Prompt for Record selection
				if intKeep == 0 then
					local fileHandle = io.open(strFileName,"w")			-- When file exists prompt is hidden	-- V1.5
					assert(fileHandle:close())
				else
					os.remove(strFileName)										-- When file missing prompt is shown	-- V1.5
				end
			else
				break
			end
		end
		if #arrRecs > 0 then													-- Selected records exist
			local intStep = 100 / #arrRecs									-- Progress bar step increment
			strRecTag = strType
			progbar.Start("Where Used Record Links looking for "..tblLinks[strType].Name,100)
			progbar.Message("Building Search Index")
			for intRecord, ptrRecord in ipairs (arrRecs) do				-- Build a Record Id index to target records -- V1.7
				local intRecId = fhGetRecordId(ptrRecord)
				dicRecs[intRecId] = intRecord								-- This avoids searching the array of records
				progbar.Step(intStep)
			end
			fhSleep(400,300)
			for intType = 1, intRecMax do									-- Search each record type
				local strType = fhGetRecordTypeTag(intType)
				local intStep = 100 / intRecordCount(strType)			-- V1.7
				local ptrItem = fhNewItemPtr()
				local ptrUsed = fhNewItemPtr()
				if intType == intRecMax then									-- Special HEAD case
					strType = "HEAD"
					intStep = 1
				end
				if tblLinks[strType][strRecTag] then						-- This record type can link to selected records -- V1.1
					progbar.Message("Searching "..tblLinks[strType].Name)
					progbar.Reset()
					progbar.Step(1)
					ptrItem:MoveToFirstRecord(strType)						-- Search every field
					while ptrItem:IsNotNull() do
						if not fhHasParentItem(ptrItem) then				-- Next record reached
							ptrUsed = ptrItem:Clone()
							progbar.Step(intStep)								-- Advance progress bar
							if progbar.Stop() then
								error("User Cancelled")
							end
							collectgarbage("step",0)							-- Memory garbage collection to avoid 'Not responding' -- V1.7
						end
						if isLink(ptrItem) then								-- Found any kind of record link -- V1.7 -- V1.8
							local ptrLink = fhGetValueAsLink(ptrItem)
							if strRecTag == fhGetTag(ptrLink) then 		-- Found right type of record link -- V1.7
								local intRecord = dicRecs[fhGetRecordId(ptrLink)]
								if intRecord then								-- Found a where used record link -- V1.7
									doResultSet(intRecord,arrRecs[intRecord],intType,ptrUsed,ptrItem)
								end
							end
						end
						ptrItem:MoveNextSpecial()
					end
				end
			end
			if #tblRecord > 0 then												-- Found some record links
				for intRecId, intRecord in pairs(dicRecs) do				-- This lets same record be selected more than once -- V1.8
					local ptrRecord = arrRecs[intRecord]
					if not arrLinks[intRecord] then
						doResultSet(intRecord,ptrRecord)					-- Report any missing links
						arrLinks[intRecord] = 0
					end
					local intLinks = fhCallBuiltInFunction("LinksTo",ptrRecord)
					if intLinks ~= arrLinks[intRecord] then				-- V1.1 double check counts
						fhMessageBox("\n =LinksTo() "..intLinks.." disagrees with "..arrLinks[intRecord].." links found! \n")
					end
				end
			end
			break
		end
	end
	if #tblRecord > 0 then

		local function intSize(tblCol)										-- Set width of column based on length of contents -- V1.6
			local intChar = tblCol.Char or 0								-- Character length of penultimate longest content
			local intLong = tblCol.Long or 0								-- Character length of longest content
			local intMax  = tblCol.Max or 50								-- Maximum size in characters
			local intMin  = tblCol.Min or  6								-- Minimum size in characters
			local intSize = math.min( math.max(intMin,intChar), intMax ) * 4
		--	print(intChar,intLong,intMin,intMax,intSize,tblCol.Mode or "item",tblCol.Class,tblCol.Text)
			return intSize
		end

		progbar.Reset("Loading Result Set")
		local strRecordName = dicType[strRecTag].Name.." Record Name"
		fhOutputResultSetTitles("Where Used Record Links  "..strVersion)
		fhOutputResultSetColumn(strRecordName        , "item"   , tblRecord, #tblRecord, intSize(tblRecord)	, "align_left")
		fhOutputResultSetColumn("Rec Id"             , "integer", tblRec_Id, #tblRecord,					28	, "align_mid" , 1)
		if #tblRecObj > 0 then
			fhOutputResultSetColumn("Media"           , "text"   , tblRecObj, #tblRecord,					28	, "align_mid" ) -- V1.1
		end
		fhOutputResultSetColumn("Hide Record Type"   , "integer", tblUseTyp, #tblRecord,					10	, "align_left", 2, true, "default", "hide")
		fhOutputResultSetColumn("Record Where Used"  , "item"   , tblUseRec, #tblRecord, intSize(tblUseRec)	, "align_left")
		fhOutputResultSetColumn("Rec Id"             , "integer", tblUse_Id, #tblRecord,					28	, "align_mid" , 3)
		fhOutputResultSetColumn("Media"              , "text"   , tblUseObj, #tblRecord,					28	, "align_mid" ) -- V1.1
		fhOutputResultSetColumn("Data Ref Where Used", "text"   , tblUseRef, #tblRecord,					90	, "align_left", 4)
		fhOutputResultSetColumn("Buddy Where Used"   , "item"   , tblUsePtr, #tblRecord,					10	, "align_left", 0, true, "default", "buddy")
		fhOutputResultSetColumn("Field Where Used"   , "text"   , tblUseTxt, #tblRecord, intSize(tblUseTxt)	, "align_left", 5)
		fhOutputResultSetColumn("Buddy Where Used"   , "item"   , tblUsePtr, #tblRecord,					10	, "align_left", 0, true, "default", "buddy")
		local dicCol = dicType[strRecTag].Col
		for intCol = #dicCol, 1, -1 do										-- Remove empty rightmost Metafield columns -- V1.6
			local tblCol = dicCol[intCol]
			if tblCol.Ref:match("_FIELD") and tblCol.Char == 0 then
				dicCol[intCol] = nil
			else
				break
			end
		end
		for intCol, tblCol in ipairs (dicCol) do							-- Optional columns
			if intCol == #dicCol then
				tblCol.Max  = 1000												-- Rightmost column can be very wide -- V1.6
				tblCol.Char = tblCol.Long
			end	
			if tblCol.Class == "longtext" then								-- Long text displayed in full with buddy item pointer -- V1.6
				fhOutputResultSetColumn(tblCol.Name,  "text"  , tblColumn[tblCol.Text], #tblRecord, intSize(tblCol)	, "align_left")
				fhOutputResultSetColumn(tblCol.Name,  "item"  , tblColumn[tblCol.More], #tblRecord,					10	, "align_left", 0, true, "default", "buddy" )
			else
				fhOutputResultSetColumn(tblCol.Name,tblCol.Mode,tblColumn[tblCol.More], #tblRecord, intSize(tblCol)	, "align_left")
			end
		end 
		progbar.Close()
		if intTwice > 0 then													-- V1.3
			fhMessageBox("\n "..intTwice.." duplicate 'Where Used Record Links' found. \n\n See 'Data Ref Where Used' asterisked entries. \n Click its column header to list together at top. \n")
		end
	else
		if strRecTag then
			progbar.Close()
			fhMessageBox("\n No 'Where Used Record Links' found. \n")
		else
			fhMessageBox("\n Please select one or more Records then run the Plugin. \n")
		end
	end
end -- function Main()

--[[
@Function:		CheckVersionInStore
@Author:			Mike Tate
@Version:			1.2
@LastUpdated:	10 Jul 2021
@Description:	Check plugin version against version in Plugin Store
@Parameter:		Plugin name and version
@Returns:			None
@Requires:		lfs & luacom
@V1.2:				Ensure the Plugin Data folder exists;
@V1.1:				Monthly interval between checks; Report if Internet is inaccessible;
@V1.0:				Initial version;
]]

function CheckVersionInStore(strPlugin,strVersion)							-- Check if later Version available in Plugin Store

	require "lfs"
	require "luacom"

	local function OpenFile(strFileName,strMode)								-- Open File and return Handle
		local fileHandle, strError = io.open(strFileName,strMode)
		if fileHandle == nil then
			error("\n Unable to open file in \""..strMode.."\" mode. \n "..strFileName.." \n "..strError.." \n")
		end
		return fileHandle
	end -- local function OpenFile

	local function SaveStringToFile(strString,strFileName)					-- Save string to file
		local fileHandle = OpenFile(strFileName,"w")
		fileHandle:write(strString)
		assert(fileHandle:close())
	end -- local function SaveStringToFile

	local function httpRequest(strRequest)										-- Luacom http request protected by pcall() below
		local http = luacom.CreateObject("winhttp.winhttprequest.5.1")
		http:Open("GET",strRequest,false)
		http:Send()
		return http.Responsebody
	end -- local function httpRequest

	local function intVersion(strVersion)										-- Convert version string to comparable integer
		local intVersion = 0
		local arrNumbers = {}
		strVersion:gsub("(%d+)", function(strDigits) table.insert(arrNumbers,strDigits) end)
		for i = 1, 4 do
			intVersion = intVersion * 1000 + tonumber(arrNumbers[i] or 0)
		end
		return intVersion
	end -- local function intVersion

	local strLatest = "0"
	if strPlugin then
		local strPath = fhGetContextInfo("CI_APP_DATA_FOLDER").."\\Plugin Data\\"
		local strFile = strPath.."VersionInStore "..strPlugin..".dat"
		local intTime = os.time() - 2600000 									-- Time in seconds a month ago
		local tblAttr, strError = lfs.attributes(strFile)					-- Obtain file attributes
		if not tblAttr or tblAttr.modification < intTime then				-- File does not exist or was modified long ago 
			if lfs.attributes(strPath,"mode") ~= "directory" then
				if not lfs.mkdir(strPath) then return end 					-- Ensure the Plugin Data folder exists
			end
			SaveStringToFile(strFile,strFile)									-- Update file modified time
			local strFile = strPath.."VersionInStoreInternetError.dat"
			local strRequest ="http://www.family-historian.co.uk/lnk/checkpluginversion.php?name="..tostring(strPlugin)
			local isOK, strReturn = pcall(httpRequest,strRequest)
			if not isOK then														-- Problem with Internet access
				local intTime = os.time() - 36000								-- Time in seconds 10 hours ago
				local tblAttr, strError = lfs.attributes(strFile)			-- Obtain file attributes
				if not tblAttr or tblAttr.modification < intTime then		-- File does not exist or was modified long ago 
					fhMessageBox(strReturn.."\n The Internet appears to be inaccessible. ")
				end
				SaveStringToFile(strFile,strFile)								-- Update file modified time
			else
				os.remove(strFile)													-- Delete file if Internet is OK
				if strReturn then
					strLatest = strReturn:match("([%d%.]*),%d*")				-- Version digits & dots then comma and Id digits 
				end
			end
		end
	end
	if intVersion(strLatest) > intVersion(strVersion or "0") then
		fhMessageBox("Later Version "..strLatest.." of this Plugin is available from the Plugin Store.")
	end
end -- function CheckVersionInStore

-- Main Code Section Starts Here --

	fhInitialise(5,0,0,"save_recommended")

	CheckVersionInStore("Where Used Record Links",strVersion)				-- Notify if later Version -- V1.8

	Main()

Source:Where-Used-Record-Links-6.fh_lua