Give Witnesses Their Own Facts.fh_lua

--[[
@Title:			Give Witnesses Their Own Facts
@Type:				Standard
@Author:			Mike Tate
@Contributors:	
@Version:			1.4
@Keywords:		
@LastUpdated:	19 Jan 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:	Remove chosen Individual Witnesses from Facts and create their own Facts instead.
@V1.4:				Handle rich text notes and citation metafields; Privacy brackets option; Add Family Residence; Cater for _SDATE;
@V1.3:				FH V7 Lua 3.5 IUP 3.28;
@V1.2:				Avoid duplicating any Fact when same Individual is a multiple Principal/Witness, plus copy all Role Citations.
@V1.1:				Added Census (family).
@V1.0:				First published in Plugin Store.
]]

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

local strVersion = "1.4"

function Find(dicFact)															-- Find the Facts that have Witness Roles
	local intFind = 0
	local ptrRec = fhNewItemPtr()
	for intType, strType in ipairs ( { "INDI"; "FAM"; } ) do			-- V1.1
		ptrRec:MoveToFirstRecord(strType)									-- Loop through all Individual & Family Records
		while ptrRec:IsNotNull() do
			local ptrFact = fhNewItemPtr()
			ptrFact:MoveToFirstChildItem(ptrRec)							-- Loop through all Facts
			while ptrFact:IsNotNull() do
				local strFact = fhGetTag(ptrFact)
				if fhIsFact(ptrFact)										 	-- Any Individual Fact or Family Census or Family Residence -- V1.1 -- V1.4
				and ( strType == "INDI" or strFact == "CENS" or strFact == "RESI" ) then
					local ptrShar = fhNewItemPtr()
					ptrShar:MoveTo(ptrFact,"~._SHAR")						-- Loop through all Fact Witness Roles
					while ptrShar:IsNotNull() do
						local strName = strFact..strType					-- V1.1
						local strRole = fhGetItemText(ptrShar,"~.ROLE")
						if not dicFact[strName] then
							dicFact[strName] = {}								-- Add new Fact and its Label
							dicFact[strName][0] = fhCallBuiltInFunction("FactLabel",ptrFact)
						end
						local intRole = dicFact[strName][strRole]
						if not intRole then
							intFind = intFind + 1								-- Count different Fact Witness Roles 
							intRole = 0
						end
						dicFact[strName][strRole] = intRole + 1			-- Count duplicate Fact Witness Roles
						ptrShar:MoveNext("SAME_TAG")
					end
				end
				ptrFact:MoveNext()
			end
			ptrRec:MoveNext()
		end
	end
	dicFact[0] = intFind														-- Save count of different Roles found
	return ( intFind > 0 )
end -- function Find

function Pick(dicFact)															-- Pick which Witness Roles to give own Facts
	local arrReply = { }														-- GetParam reply tick values
	local isTicked = true														-- GetParam reply status
	local intParam = 0															-- Count of params per page
	local strParam = ""															-- Format of params per page
	local intSize = 25															-- Maximum params per page
	local intPage = 1															-- Current page number
	local intLast = math.ceil( dicFact[0] / intSize )					-- Number of pages needed
	intSize = math.ceil( dicFact[0] / intLast )							-- Even out params per page
	dicFact[0] = nil

	local function Zeros(intParam)											-- Return same number of 0's as Witness Role params
		intParam = intParam - 1
		if intParam == 0 then return 0 end									-- End recursion
		return 0,Zeros(intParam)
	end -- local function Zeros

	local function GetParam()													-- GetParam user dialogue
		strParam = "Tick the Witness Roles to be given their own Facts: %t\n"..strParam
		strParam = strParam.."For all Roles ticked above, this Plugin copies the  \r"
		strParam = strParam.."principal Fact on the left to each of its Witnesses,\r"
		if dicFact["CENSFAM"] then
		 strParam = strParam.."(Any 'Census (family)' becomes 'Census' events.)   \r"
		end
		strParam = strParam.."then provides a Result Set list of all the changes. \r"
		strParam = strParam.."Undo changes with 'Edit > Undo Plugin Updates'      \r"
		strParam = strParam.."or 'File > Backup/Restore > Revert to Snapshot'.     %t\n"
		local strTitle = " Select Witness Roles "..strVersion.."      Page "..intPage.." of "..intLast
		local arrParam = { iup.GetParam(strTitle,nil,strParam,Zeros(intParam)) }
		isTicked = arrParam[1]													-- Save reply status: true=OK, false=Cancel
		for intParam = 2, #arrParam do
			table.insert(arrReply,arrParam[intParam])						-- Append reply ticks to previous replies
		end
	end -- local function GetParam

	for strFact, arrRole in pairs (dicFact) do							-- Loop through all Fact Witness Roles
		local strLabel = arrRole[0]
		strLabel = strLabel..string.rep("  ",13 - #strLabel)			-- Suffix Fact Label with spaces to fixed width
		arrRole[0] = nil
		for strRole, intRole in pairs (arrRole) do						-- Compose GetParam format text of boolean per Role
			intParam = intParam + 1
			local strCount = tostring(intRole)								-- Prefix Role count with spaces to fixed width
			strCount = string.rep("  ",5 - #strCount)..strCount.."      x      "
			strParam = strParam..strCount..strLabel.."   ~      "..strRole.." %b\n"
			if intPage < intLast and intParam >= intSize then
				GetParam()														-- Get a page of user params
				if not isTicked then break end
				intParam = 0
				strParam = ""													-- Reset for next page
				intPage = intPage + 1
			end
		end
		if not isTicked then break end										-- User cancelled dialogue
	end
	if isTicked then
		strParam = strParam.."Put each Witness Role note in [[privacy]] brackets? %b\n"
		intParam = intParam + 1												-- Privacy brackets?	-- V1.4
		GetParam()																-- Get last page of user params
		if isTicked then
			isTicked = false
			for strName, arrRole in pairs (dicFact) do					-- Check if any ticks -- V1.1
				for strRole, intRole in pairs (arrRole) do
					local isTick = (table.remove(arrReply,1) == 1)		-- Reply = 1 if tick and thus true -- V1.1
					dicFact[strName][strRole] = isTick						-- Assign tick is true or false to each Role -- V1.1
					isTicked = isTicked or isTick
				end
			end
			dicFact.Privacy = (arrReply[#arrReply] == 1)					-- Privacy brackets?	-- V1.4
		end
	end
	return isTicked
end -- function Pick

function Error(strAct,...)													-- Report error and abort the plugin
	local arg = {...}
	local strErr = "\n"..strAct.." (\n "
	for intArg = 1, #arg do
		local strArg = arg[intArg]
		local strTyp = type(strArg)											-- Convert pointer or boolean args to text
		if strTyp == "userdata" then
			strArg = fhGetDisplayText(strArg)
		elseif strTyp == "boolean" then
			strArg = tostring(strArg)
		end
		strErr = strErr..strArg
		if intArg < #arg then strErr = strErr.." ,\n " end
	end
	strErr = strErr.." )\nfailed"
	local intArg = 1
	if type(arg[intArg]) == "string"  then intArg = #arg end			-- Find target pointer
	if type(arg[intArg]) == "boolean" then intArg = intArg-1 end
	local ptrRec = arg[intArg]:Clone()
	if fhHasParentItem(ptrRec) then											-- Is target a record?
		ptrRec:MoveToRecordItem(ptrRec)
		strErr = strErr.." for "..fhGetDisplayText(ptrRec)				-- Add name of record
	end
	error("\n\nError: "..strErr,3)											-- Report and abort
end -- function Error

dicAct = { fhCreateItem=fhCreateItem; fhSetValue_Copy=fhSetValue_Copy; fhGetValueAsText=fhGetValueAsText; fhGetValueAsRichText=fhGetValueAsRichText; fhGetValueAsLink=fhGetValueAsLink; fhSetValueAsText=fhSetValueAsText; fhSetValueAsRichText=fhSetValueAsRichText; fhSetValueAsLink=fhSetValueAsLink; fhDeleteItem=fhDeleteItem; }

function Perform(strAct,...)													-- Perform FH API function
	local anyAns = dicAct[strAct](...)
	if ( type(anyAns) == "boolean"  and not anyAns )					-- Most return true or false
	or ( type(anyAns) == "userdata" and anyAns:IsNull() )				-- fhCreateItem returns pointer
	then Error(strAct,...) end
	return anyAns																-- Others can return text
end -- function Perform

function CopyBranch(ptrSource,ptrTarget)									-- Copy one child branch
	local strTag = fhGetTag(ptrSource)
	if strTag == "HUSB" or strTag == "WIFE" then return end			-- Family Census tags not allowed in Individual Census -- V1.1
	if strTag == "_FMT" then return end 									-- Skip rich text format tag -- V1.4
	if strTag == "_FIELD" then
		strTag = fhGetMetafieldShortcut(ptrSource)						-- Handle citation metafield -- V1.4
	end
	local ptrNew = Perform("fhCreateItem",strTag,ptrTarget,true)
	Perform("fhSetValue_Copy",ptrNew,ptrSource)
	CopyChildren(ptrSource,ptrNew)
end -- function CopyBranch

local dicExclude = { AGE=true; _SHAR=true; _SHAN=true; _SENT=true; }	-- V1.4

function CopyChildren(ptrSource,ptrTarget)								-- Copy children branches
	local ptrFrom = fhNewItemPtr()
	ptrFrom = ptrSource:Clone()
	ptrFrom:MoveToFirstChildItem(ptrFrom)
	while ptrFrom:IsNotNull() do
		local strTag = fhGetTag(ptrFrom)
		if not dicExclude[strTag] then								 		-- Exclude AGE and custom tags _SHAR, _SHAN, _SENT, but not _SDATE	-- V1.4
			CopyBranch(ptrFrom,ptrTarget)
		end
		ptrFrom:MoveNext()
	end
end -- function CopyChildren

function PerRidData(tblRid,intRid,strNote,ptrFact,ptrRole)			-- Get/Set per RecId for Fact & Note & Role -- V1.2
	local dicRid = tblRid[intRid]
	if not dicRid or ptrRole then
		dicRid = dicRid or {}
		dicRid.Note = strNote or ""
		dicRid.Fact = ptrFact or fhNewItemPtr()
		dicRid.Role = ptrRole or fhNewItemPtr()
		tblRid[intRid] = dicRid
	end
	return dicRid
end -- function PerRidData

function Make(dicFact)															-- Make the Facts for chosen Witness Roles
	local intMake = 0
	local arrRec  = {}															-- Result Set tables
	local arrRole = {}
	local arrFact = {}
	local strPref = ""
	local strSuff = ""
	if dicFact.Privacy then													-- Privacy brackets?	-- V1.4
		strPref = "[["
		strSuff = "]]"
	end
	local ptrRec  = fhNewItemPtr()
	for intType, strType in ipairs ( { "INDI"; "FAM"; } ) do			-- V1.1
		ptrRec:MoveToFirstRecord(strType)									-- Loop through all Individual & Family Records
		while ptrRec:IsNotNull() do
			local ptrFact = fhNewItemPtr()
			ptrFact:MoveToFirstChildItem(ptrRec)							-- Loop through all Facts
			while ptrFact:IsNotNull() do
				local strFact = fhGetTag(ptrFact)
				if fhIsFact(ptrFact)										 	-- Any Individual Fact or Family Census or Family Residence -- V1.1 -- V1.4
				and ( strType == "INDI" or strFact == "CENS" or strFact == "RESI" ) then
					local isPrincipal = true
					local tblRid = {}											-- Table per RecId for Fact & Note & Role -- V1.2
					local ptrShar = fhNewItemPtr()
					ptrShar:MoveTo(ptrFact,"~._SHAR")						-- Loop through all Fact Witness Roles
					while ptrShar:IsNotNull() do
						local strName = strFact..strType					-- V1.1
						local ptrRole = ptrShar:Clone()
						local strRole = fhGetItemText(ptrRole,"~.ROLE")
						ptrShar:MoveNext("SAME_TAG")
						if dicFact[strName][strRole] then					-- Give the Witness their own Fact? -- V1.1
							if isPrincipal then
								table.insert(arrRec ,ptrRec:Clone())		-- Update the Result Set tables for Principal
								table.insert(arrRole,"PRINCIPAL")
								table.insert(arrFact,ptrFact:Clone())
								local intRid  = fhGetRecordId(ptrRec)		-- Save principal Note & Fact & no Role -- V1.2
								PerRidData(tblRid,intRid,strPref.."Principal Role"..strSuff.."\n",ptrFact)	-- V1.4
								isPrincipal = false
							end
							local ptrWitn = fhGetValueAsLink(ptrRole)		-- Find the Witness and their RecId
							local intRid  = fhGetRecordId(ptrWitn)			-- V1.2
							local dicRid  = PerRidData(tblRid,intRid)		-- Get Note & Fact & Role for RecId, may be from principal -- V1.2
							local strNote = dicRid.Note..strPref.."Witness Role: "..strRole..strSuff.."\n"	-- V1.4
							local ptrCopy = dicRid.Fact
							if ptrCopy:IsNull() then							-- Create a copy of principal Fact ? -- V1.2
								ptrCopy = Perform("fhCreateItem",strFact,ptrWitn)
								Perform("fhSetValue_Copy",ptrCopy,ptrFact)-- Copy all principal Fact fields except AGE, _SHAR, _SHAN, _SENT, etc
								CopyChildren(ptrFact,ptrCopy)
							end 													-- Save witness Note & Fact & Role for RecId -- V1.2
							tblRid[intRid] = PerRidData(tblRid,intRid,strNote,ptrCopy,ptrRole)
							table.insert(arrRec ,ptrWitn:Clone())			-- Update the Result Set tables
							table.insert(arrRole,strRole)
							table.insert(arrFact,ptrCopy:Clone())
							dicFact[strType] = true							-- Signal that INDI/FAM Witnesses have been handled, but only needed for Census (family)
						end
					end
					for intRid, dicRid in pairs (tblRid) do				-- Add the Role text to each Fact Note and copy any Role Citations -- V1.2
						local strNote = dicRid.Note							-- Cannot add earlier if Principal is own Witness as gets copied to every other Witness
						local ptrFact = dicRid.Fact
						local ptrRole = dicRid.Role
						local strMode = "fhSetValueAsText"					-- V1.4	-- Can this plain & rich text manipulation be simplified?
						local ptrNote = fhGetItemPtr(ptrRole,"~.NOTE2")
						if fhGetValueType(ptrNote) == "richtext" then	-- Witness Role note is rich text	-- V1.4
							local strRich = fhNewRichText(strNote)
							strRich:AddRichText(fhGetValueAsRichText(ptrNote))
							strNote = strRich
							strMode = "fhSetValueAsRichText"
						else
							strNote = strNote..fhGetValueAsText(ptrNote)	-- Witness Role note is plain text
						end
						local ptrNote = fhGetItemPtr(ptrFact,"~.NOTE2")
						if ptrNote:IsNull() then							 	-- Add a local Note to the Fact
							ptrNote = Perform("fhCreateItem","NOTE2",ptrFact)
						else														-- Get existing local Note and Witness Role note
							if fhGetValueType(ptrNote) == "richtext" then-- Local Note is rich text			-- V1.4
								local strRich = fhGetValueAsRichText(ptrNote)
								if strMode == "fhSetValueAsRichText" then	-- Witness Role note is rich text	-- V1.4
									strRich:AddRichText(fhNewRichText("\n"))
									strRich:AddRichText(strNote)
								else
									strRich:AddRichText(fhNewRichText("\n"..strNote))
								end
								strNote = strRich
								strMode = "fhSetValueAsRichText"
							elseif strMode == "fhSetValueAsRichText" then-- Local Note is plain text but Witness Role note is rich text	-- V1.4
								local strRich = fhNewRichText(fhGetValueAsText(ptrNote).."\n")
								strRich:AddRichText(strNote)
							else													-- Local Note & Witness Role note are plain txt
								strNote = fhGetValueAsText(ptrNote).."\n"..strNote
							end
						end
						Perform(strMode,ptrNote,strNote)					-- Update the local Note with Witness Role note	-- V1.4
						for strTag, strType in pairs ({SOUR="Link";SOUR2="Text";}) do
							local ptrCite = fhNewItemPtr()
							ptrCite:MoveTo(ptrRole,"~."..strTag)			-- Loop through all Role Citations and copy them
							while ptrCite:IsNotNull() do
								local anyType = Perform("fhGetValueAs"..strType,ptrCite)
								local ptrSour = Perform("fhCreateItem",strTag,ptrFact)
								Perform("fhSetValueAs"..strType,ptrSour,anyType)
								CopyChildren(ptrCite,ptrSour)				-- Copy citation subsidiary fields	-- V1.4
								ptrCite:MoveNext("SAME_TAG")
							end
						end
						if ptrRole:IsNotNull() then
							Perform("fhDeleteItem",ptrRole)					-- Delete original Witness Role (i.e. when not just Principal)
							intMake = intMake + 1								-- Count them
						end
					end
				end
				ptrFact:MoveNext()
			end
			ptrRec:MoveNext()
		end
	end
	if #arrRec > 0 then															-- Output Result Set
		fhOutputResultSetTitles("Give Witnesses Their Own Facts "..strVersion)
		fhOutputResultSetColumn("Record","item",arrRec ,#arrRec,120,"align_left")
		fhOutputResultSetColumn("Role"  ,"text",arrRole,#arrRec, 50,"align_left")
		fhOutputResultSetColumn("Fact"  ,"item",arrFact,#arrRec,300,"align_left")
	end
	return intMake, intMike
end -- function Make()

function Main()
	local dicFact = {}															-- Dictionary of Facts with Witness Roles
	if Find(dicFact) then														-- Find the Facts that have Witness Roles
		if Pick(dicFact) then													-- Pick the Witness Roles to get own Facts
			if "Yes" == fhMessageBox("\n Are you sure you can recover from unwanted changes? \n e.g. \n Used    'File > Backup/Restore > Small Backup' \n","MB_YESNO","MB_ICONQUESTION") then
				local intMake = Make(dicFact)								-- Make the Facts for chosen Witness Roles
				local strHelp = ""
				if dicFact["FAM"] then											-- Census (family) Witnesses were removed
					strHelp = "\n Consider using 'Migrate Census Family to Individual Events' Plugin. \n"
				end
				local strMake = " Witnesses were given their own Individual Facts. \n"
				if intMake == 1 then
					strMake = strMake:gsub("es "," "):gsub("s%. ",". ")
				end
				fhMessageBox("\n "..intMake..strMake.."\n Undo changes with 'Edit > Undo Plugin Updates' \n or 'File > Backup/Restore > Revert to Snapshot'.\n"..strHelp)
			end
		else
			fhMessageBox("\n No Individual Fact Witnesses Changed. \n")
		end
	else
		fhMessageBox("\n No Individual Fact Witnesses Found. \n")
	end
end -- function Main()

Main()

Source:Give-Witnesses-Their-Own-Facts-1.fh_lua