Ancestry Synchronization.fh_lua

--[[
@Title:       Ancestry Synchronization
@Type:        Standard
@Author:      Mark Draper
@Contributor  John Elvin
@Version:     2.5.1
@LastUpdated: 14 Sep 2025 
@Licence:     This plugin is copyright (c) 2025 Mark Draper and is licensed under the MIT License which
is hereby incorporated by reference (see https://pluginstore.family-historian.co.uk/fh-plugin-licence)
@Description: Produces a fixed GEDCOM export, suitable for loading into RootsMagic 9 or later for
			  subsequent synchronization with Ancestry. Reads RootsMagic database directly and generates
				a Research Note listing all differences. Delete Facts from RootsMagic that are
				not in the current Family Historian Project. Supports a customisable subset of Facts with
				standard Date and Place fields, but no comments, sources, or media. Living individuals
				are excluded from the output by default but can be incorporated if required.
]]

--[[
Developed from the FHUG plugin, Family Historian - RootsMagic - Ancestry Sync

Version 1.0 (Feb 2023)
	- Initial Plugin Store version.

Version 1.1 (May 2023)
	- Interim fix for date phrases containing quotation characters

Version 2.0 (Jun 2023)
	- New main menu with details of RM and Ancestry links
	- Improved messages and reports, including link to Ancestry tree
	- New Ancestry audit functions and Research Note report
	- Simplified comparison of parents and spouse, so family sequence is ignored
		(improved compatibility with Ancestry auditing)
	- New plugin options and improved options form
		- basic BMD export
		- optional case sensitive place and attribute value matching (previously always enforced)
		- RM backup now automatic, but only if file changed
	- Unmatching names relegated to Alternate Name in RM for quicker deletion
	- Now supports same-sex families and surname-first names
	- Improved compatibility with RM7
	- Improved handling of names
	- Improved cross-reference table (sorted by RM name)
	- Improved handling of date phrases for full compatibility between apps
	- Improved message boxes (using IUP box, not FH function)
	- Additional checks when starting to screen out invalid operation
	- Now requires FH 7.0.15 or later due to changes in fhFileUtils()

Version 2.0.1 (Jun 2023)
	- Fix for variability in RM options file structure

Version 2.1 (Sep 2023)
	- New option to base individual selection on Ancestry Sync list
	- New option to disable RM/Ancestry compatibility for GEDCOM export
	- New GEDCOM export Research note
	- Improved menu display that keeps previous menus visible but inactive
	- Now permits export of blank names (RM can import them but not create them, so works ok)
	- Added extra message to close file prior to early RM update if sex change detected
	- Fixed bug that stopped parent check running (simplified code for table initialisation)
	- Fixed display of surnames with punctuation characters by more selective use of overwriting
			case preference (let FH do the formatting)
	- Fixed bug affecting export of surname-first names (forced to given-first for RM compatibility)
	- Fixed typo affecting two-female family records

Version 2.2 (Nov 2023)
	- More extensive memory management to handle very large projects (>25k records)
	- More detailed progress bar, kept open throughout the entire Compare or Update process
	- Much faster processing of bulk changes in RM by passing records in batches and using SQL 'IN' statement
	- Automatic export of only changed and related records to speed up RM Share Merge
	- Improved internal processing of large datasets

Version 2.3 (Jan 2024)
	- Removed support for RM7/8 following Ancestry login changes
	- Various minor bug fixes and code tidying

Version 2.4 (Aug 2024)
	- Fixed bug with un-named individuals
	- Checks for multiple UniqueID values
	- New standard format options file
	- Resetting RM file with changed project name now optional
	- Optional import of custom fact list
	- Emulator warning

Version 2.4.1 (Aug 2024)
	- Fixed bug not exporting changed family records in GEDCOM update

Version 2.5 (May 2025)
	- Corrected FH/RM comparison where name has suffix
	- Support for multi-monitor systems
	- Checks for errors in ini file when loading
	- Enhanced tool tips

Version 2.5.1 (Sep 2025)
	- Updated to support RM11

]]

fhInitialise(7, 0, 15)
require('fhSQL')
require('iuplua')
lfs = require('lfs')
fhu = require('fhUtils')
fhfu = require('fhFileUtils')
fhu.setIupDefaults()

-- *********************************************************************
-- Main, menu and Fact selection functions
-- *********************************************************************

function main()

	-- check for emulator

	if fhfu.folderExists('Z:\\bin') and fhfu.folderExists('Z:\\etc') then
		local msg = 'Family Historian does not support linking to external databases via plugins when ' ..
			'running on Mac or Linux systems.'
		MessageBox(msg, 'OK', 'ERROR', 'Emulator Incompatibility Warning')
		return
	end

	-- check not running in Standalone GEDCOM mode

	if fhGetContextInfo('CI_APP_MODE') ~= 'Project Mode' then
		local msg = 'This plugin can only be run from within a Family Historian project.'
		MessageBox(msg, 'OK', 'ERROR')
		return
	end

	-- check not an Ancestry GEDCOM export that has not been processed

	local CheckBare, CheckProcessed = IsBareTree()
	if CheckBare and not CheckProcessed then
		local msg = 'Project is derived from an Ancestry GEDCOM export that has not been processed by the ' ..
				"plugin.\n\nReload your main project and run 'Process Ancestry GEDCOM Export File' from " ..
				'the main menu to prepare the file for import into Family Historian.'
		MessageBox(msg, 'OK', 'ERROR')
		return
	end

	-- check Project has complete UID set

	if not CheckUIDs() then
		local msg = 'This plugin requires that all Individual records have a UniqueID assigned. ' ..
				'Select Tools > Record Identifiers... from the main menu to generate missing values.'
		MessageBox(msg, 'OK', 'ERROR')
		return
	end

	-- get plugin options

	gblOptions = GetOptions()

	-- exit if user cancelled

	if gblOptions == -1 then return end

	-- present menu

	Menu()

	-- delete extract if out of scope

	if not gblOptions.UpdateGEDCOM then
		local UpdateFile = fhGetContextInfo('CI_PROJECT_DATA_FOLDER') ..
				'\\Plugin Data\\Ancestry Sync Update.txt'
		fhfu.deleteFile(UpdateFile)
	end

	-- save options prior to quitting

	if gblOptions then SaveOptions() end
end

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

function Menu()

	-- generate main plugin menu

	-- define link elements

	local lblRM = iup.label{title = 'Linked RootsMagic file:'}
	local lblRMfile = iup.label{expand = 'HORIZONTAL'}
	local lblANC = iup.label{title = 'Linked Ancestry tree:'}
	local lnkANC = iup.link{expand = 'HORIZONTAL'}
	local gboxLinks = iup.gridbox{lblRM, lblRMfile, lblANC, lnkANC;
			numdiv = 2, sizecol = -1, sizelin = -1,
			gapcol = 5, gaplin = 5, margin = '10x'}
	local btnSelect = iup.button{title = 'Select', padding = '10x3',
			action = function(self) SelectRMFile() UpdateMenu() end,
			TipBalloon = 'YES', TipBalloonTitle = 'Link RootsMagic Database',
			TipBalloonTitleIcon = '1',
			tip = 'Select RootsMagic file linked to this project'}
	local vboxLinks = iup.vbox{gboxLinks, btnSelect; alignment = 'ACENTER', gap = 10, margin = '10x10'}
	local fraLinks = iup.frame{vboxLinks; title = 'Links'}

	-- define RM elements

	local btnExport = iup.button{title = 'Export GEDCOM File',
			action = function(self)
				ExportGEDCOM()
				UpdateMenu()
				iup.SetFocus(btnSelect)
			end,
			TipBalloon = 'YES', TipBalloonTitle = 'Export GEDCOM File',
			TipBalloonTitleIcon = '1',
			tip = 'Export customized GEDCOM file to create or \nupdate linked RootsMagic database'}
	local btnCompare = iup.button{title = 'Compare Project with Linked RM File', padding = '5x3',
			TipBalloon = 'YES', TipBalloonTitle = 'Compare RootsMagic Database',
			TipBalloonTitleIcon = '1',
			tip = 'Generate list of differences between current Project and linked RootsMagic database'}
	local btnUpdate = iup.button{title = 'Update Linked RM File',
			TipBalloon = 'YES', TipBalloonTitle = 'Update RootsMagic Database',
			TipBalloonTitleIcon = '2',
			tip = 'Update linked RootsMagic database to reflect current Project contents'}
	local vboxRM = iup.vbox{btnCompare, btnExport, btnUpdate;
			normalizesize = 'BOTH', gap = 10, margin = '10x10'}
	local fraRM = iup.frame{vboxRM; title = 'RootsMagic Synchronization'}

	-- define Ancestry elements

	local btnDuplicates = iup.button{title = 'Check For Missing or Duplicate Records', padding = '5x3',
			action = function(self) AuditAncestryTree() UpdateMenu() end,
			TipBalloon = 'YES', TipBalloonTitle = 'Audit Linked Ancestry Tree',
			TipBalloonTitleIcon = '3',
			tip = 'Compare linked Ancestry tree with RootsMagic database to\n' ..
					'check for missing records or accidental duplication'}
	local btnGEDCOM = iup.button{title = 'Process Ancestry GEDCOM Export File',
			action = function(self) AuditAncestryGEDCOM() UpdateMenu() end,
			TipBalloon = 'YES', TipBalloonTitle = 'Remove Ancestry Export Errors',
			TipBalloonTitleIcon = '1',
			tip = 'Identify duplicate Sex data in Ancestry export GEDCOM, and reformat\n' ..
					'file ready for import to Family Historian as new Project'}
	local btnAudit = iup.button{title = 'Compare Audit Project with RM File', padding = '5x3',
			action = function(self) AuditRMFile(false) UpdateMenu() end, -- active = 'NO',
			TipBalloon = 'YES', TipBalloonTitle = 'Audit Linked Ancestry Tree',
			TipBalloonTitleIcon = '1',
			tip = 'Generate list of differences between current Ancestry-derived \nProject and linked RootsMagic database'}
	if not gblOptions.TreeID then
		btnDuplicates.Active = 'NO'
		btnGEDCOM.Active = 'NO'
	end
	local vboxANC = iup.vbox{btnDuplicates, btnGEDCOM, btnAudit;
			normalizesize = 'BOTH', gap = 10; margin = '10x10'}
	local fraANC = iup.frame{vboxANC; title = 'Audit Ancestry Tree'}

	-- create common buttons

	local btnOptions = iup.button{title = 'Options',
			action = function(self) SelectOptions() UpdateMenu() end,
			TipBalloon = 'YES', TipBalloonTitle = 'Options',
			TipBalloonTitleIcon = '1',
			tip = 'Configure plugin options', padding = '10x3'}
	local btnHelp = iup.button{title = 'Help',
			action = function(self) fhShellExecute('https://pluginstore.family-historian.co.uk/page/help/ancestry-synchronization') end,
			TipBalloon = 'YES', TipBalloonTitle = 'Display Plugin Help',
			TipBalloonTitleIcon = '1',
			tip = 'Display plugin help page'}
	local btnCancel = iup.button{title = 'Close',
			action = function(self) return iup.CLOSE end,
			TipBalloon = 'YES', TipBalloonTitle = 'Close Plugin',
			TipBalloonTitleIcon = '1',
			tip = 'Close plugin'}
	local buttons = iup.hbox{iup.fill{}, btnOptions, btnHelp, btnCancel, iup.fill{};
			normalizesize = 'BOTH', margin = 'x20', gap = 50}

	function UpdateMenu()

		-- display current RM file if defined

		if gblOptions.File then
--			if gblOptions.Version then
--				lblRM.Title = 'Linked RootsMagic ' .. gblOptions.Version .. ' file:'
--			else
--				lblRM.Title = 'Linked RootsMagic file:'
--			end
			lblRMfile.Title = fhfu.splitPath(gblOptions.File).filename
			lblRMfile.Tip = gblOptions.File
			if gblOptions.TreeID then
				local TreeURL = gblOptions.URL .. 'tree/' .. gblOptions.TreeID
				lnkANC.Title = TreeURL
				lnkANC.url = TreeURL
				lnkANC.Active = 'YES'
			else
				lnkANC.Title = ''
				lnkANC.Active = 'NO'
			end
		end
	
		-- is a GEDCOM update available?

		gblOptions.UpdateGEDCOM = GetGEDCOMUpdate()

		-- modify buttons according to current context

		if IsBareTree() then
			fraRM.Active = 'NO'
			btnDuplicates.Active = 'NO'
			btnGEDCOM.Active = 'NO'
			btnAudit.Active = YesNo(gblOptions.File)
		else
			btnCompare.Active = YesNo(gblOptions.File)
			btnUpdate.Active = YesNo(gblOptions.File)
			btnAudit.Active = 'NO'
			btnDuplicates.Active = YesNo(gblOptions.TreeID)
			btnGEDCOM.Active = YesNo(gblOptions.TreeID)
		end
		if gblOptions.UpdateGEDCOM then
			btnExport.Title = 'Export GEDCOM Update File'
		else
			btnExport.Title = 'Export GEDCOM File'
		end
	end

	function YesNo(b) if b then return 'YES' else return 'NO' end end

	function btnCompare:action()
		AuditRMFile(false)
		UpdateMenu()
		collectgarbage()
	end

	function btnUpdate:action()
		AuditRMFile(true)
		UpdateMenu()
		collectgarbage()
	end

	-- assemble the form

	local vboxForm = iup.vbox{fraLinks, iup.hbox{fraRM, iup.fill{}, fraANC;
			gap = 10, margin = '10x10'}, buttons; gap = 10, margin = '10x10'}

	local dialog = iup.dialog{vboxForm; resize = 'No', minbox = 'No', maxbox = 'No',
			title = 'Ancestry Synchronization (2.5.1)'}

	dialog:map()		-- ensures layout is preserved for changes in RM file version
	UpdateMenu()
	if gblOptions.File then
		dialog.StartFocus = btnCompare
	else
		dialog.StartFocus = btnExport
	end
	iup.SetAttribute(dialog, 'NATIVEPARENT', fhGetContextInfo('CI_PARENT_HWND'))
	dialog:popup()
end

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

function DefineFacts()

--[[
	Define a common set of Facts for both the initial GEDCOM export and sync with RM.

	There is also an option to import a comma-separated list of facts (Tag, Description).
	See the help file for more detailed information.
]]

	local tblI = {}
	local tblF = {}

	if gblOptions.BMD then
		table.insert(tblI, {Tag = 'BIRT', Description = 'Birth'})
		table.insert(tblF, {Tag = 'MARR', Description = 'Marriage'})
		table.insert(tblI, {Tag = 'DEAT', Description = 'Death'})
	else

		-- Individual Facts (custom or standard list)

		local FactFile = fhGetContextInfo('CI_PROJECT_DATA_FOLDER') ..
				'\\Plugin Data\\Ancestry Synchronization Facts.csv'
		if fhfu.fileExists(FactFile) then
			local Facts = fhLoadTextFile(FactFile)
			for Fact in Facts:gmatch('[^\r\n]+') do
				local Tag, Description = Fact:match('^([%w%-%_]+)%,([%g%s]+)$')
				table.insert(tblI, {Tag = Tag, Description = Description})
			end
		else	
			table.insert(tblI, {Tag = 'BIRT', Description = 'Birth'})
			table.insert(tblI, {Tag = 'BAPM', Description = 'Baptism'})
			table.insert(tblI, {Tag = 'CHR', Description = 'Chr'})
			table.insert(tblI, {Tag = 'OCCU', Description = 'Occupation'})
			table.insert(tblI, {Tag = 'CENS', Description = 'Census'})
			table.insert(tblI, {Tag = 'RESI', Description = 'Residence'})
			table.insert(tblI, {Tag = 'EMIG', Description = 'Emigration'})
			table.insert(tblI, {Tag = 'IMMI', Description = 'Immigration'})
			table.insert(tblI, {Tag = 'NATU', Description = 'Naturalization'})
			table.insert(tblI, {Tag = 'RETI', Description = 'Retirement'})
			table.insert(tblI, {Tag = 'DEAT', Description = 'Death'})
			table.insert(tblI, {Tag = 'BURI', Description = 'Burial'})
			table.insert(tblI, {Tag = 'CREM', Description = 'Cremation'})
			table.insert(tblI, {Tag = 'PROB', Description = 'Probate'})
			table.insert(tblI, {Tag = 'REFN', Description = 'Ref #'})
		end

		-- Family Facts (do not add additional facts, due to limitation in TreeShare)

		table.insert(tblF, {Tag = 'MARR', Description = 'Marriage'})
		table.insert(tblF, {Tag = 'DIV', Description = 'Divorce'})
	end

	return tblI, tblF
end

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

function CheckAncestrySyncList()

	-- checks for presence of Ancestry Sync list

	local GedcomFile = fhGetContextInfo('CI_GEDCOM_FILE')
	local Gedcom = fhLoadTextFile(GedcomFile)
	return Gedcom:match('1 _LIST Ancestry Sync\r\n')
end

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

function IsExcluded(p, family)

	-- returns true for out of scope individual or family records

	if not family then
		local tblP = {}
		if fhGetItemText(p, '~._FLGS.__PRIVATE') == 'Y' then table.insert(tblP, 'Private') end
		if fhGetItemText(p, '~._FLGS.__LIVING') == 'Y' and not gblOptions.Living then
				table.insert(tblP, 'Living') end
		if gblOptions.List and not fhCallBuiltInFunction('IsInList', p, 'Ancestry Sync') then
				table.insert(tblP, 'List') end
		if #tblP > 0 then return table.concat(tblP, ',') end
	else
		local pH, pW = GetFamilySpouses(p)
		-- two visible parents
		if pH:IsNotNull() and not IsExcluded(pH) and pW:IsNotNull() and not IsExcluded(pW) then return end
		-- no visible parents
		if (pH:IsNull() or IsExcluded(pH)) and (pW:IsNull() or IsExcluded(pW)) then return true end

		-- one visible parent, so return false if a visible child, true otherwise

		local pCHIL = fhGetItemPtr(p, '~.CHIL')
		while pCHIL:IsNotNull() do
			local pC = fhGetValueAsLink(pCHIL)
			if not IsExcluded(pC) then return end
			pCHIL:MoveNext('SAME_TAG')
		end
		return true
	end
end

-- *********************************************************************
-- Options functions
-- *********************************************************************

function GetOptions()

	-- get plugin options from options file (2-step process to ensure folder exists)

	fhGetPluginDataFileName('CURRENT_PROJECT')
	local OptionsFile = fhGetContextInfo('CI_PROJECT_DATA_FOLDER') ..
			'\\Plugin Data\\Ancestry Synchronization.ini'

	-- get ini format values first

	local tblO = {}
	for _, F in ipairs({'Project', 'File', 'GFile', 'AFile'}) do
		local File = fhGetIniFileValue(OptionsFile, 'Files', F, 'text')
		if File ~= '' then tblO[F] = File end
	end
	for _, O in ipairs({'Living', 'List', 'BMD', 'Case', 'GEDCOM', 'Links', 'Table'}) do
		local Option = fhGetIniFileValue(OptionsFile, 'Options', O, 'bool')
		if Option then tblO[O] = true end
	end

	-- if no values, get old format (first run with new version) and delete old format file

	if not tblO.Project and fhfu.fileExists(OptionsFile) then
		local Options = fhLoadTextFile(OptionsFile)
		for Option in Options:gmatch('[^\r\n]+') do
			local Parameter, Value = Option:match('^(%w+)=([%g%s]+)$')
			if Parameter and Value then tblO[Parameter] = Value end
		end
		fhfu.deleteFile(OptionsFile)
	end

	-- check project matches (case-insensitive matching, as it is Windows!)

	if tblO.Project then
		local StoredProject = fhfu.splitPath(tblO.Project).basename
		local StoredPath = fhfu.splitPath(tblO.Project).parent
		local Project = fhGetContextInfo('CI_PROJECT_NAME')
		local Path = fhfu.splitPath(fhGetContextInfo('CI_PROJECT_FILE')).parent

		if StoredProject:lower() ~= Project:lower() then
			if StoredPath:lower() ~= Path:lower() then
				StoredProject = StoredPath .. '\\' .. StoredProject
				Project = Path .. '\\' .. Project
			end
			local msg = 'Project name differs from stored value. Do you want to reset the RootsMagic file?\n\n' ..
					'Project name: ' .. Project .. '\n\nStored Project: ' .. StoredProject
			local Response = MessageBox(msg, 'YESNOCANCEL', 'WARNING')
			if Response == 1 then
				tblO.File = nil
				tblO.GFile = nil
				tblO.AFile = nil
			elseif Response == 3 then
				return
			end
		end
	end

	-- does tree file exist on this PC?

	if tblO.File and not fhfu.fileExists(tblO.File) then
		local msg = 'Specified RootsMagic file is not available on this PC. File name will be reset.'
		if MessageBox(msg, 'OKCANCEL', 'WARNING') ~= 1 then return -1 end
		tblO.File = nil
		tblO.GFile = nil
		tblO.AFile = nil
	end

	-- determine RM version and domain settings for the specified tree file

	if tblO.File then
		local Version, URL, TreeID = GetDatabaseLinks(tblO.File)
		if Version and URL then
			tblO.Version = Version
			tblO.URL = URL
			tblO.TreeID = TreeID
		else
			tblO.File = nil
		end
	end
	return tblO
end

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

function SelectOptions()

	-- create options menu

	local optLiving = iup.toggle{title = ' Include Individuals marked as Living', expand = 'HORIZONTAL'}
	local optList = iup.toggle{title = ' Only Individuals in Ancestry Sync list', expand = 'HORIZONTAL'}
	local optBMD = iup.toggle{title = ' Birth, Marriage && Death Facts only', expand = 'HORIZONTAL'}
	local vbox1 = iup.vbox{optLiving, optList, optBMD; gap = 10, margin = '10x10'}
	local fra1 = iup.frame{vbox1; title = 'Selection options'}

	local optCompat = iup.toggle{title = ' Disable RM/Ancestry compatibility for this session',
			expand = 'HORIZONTAL'}
	local vbox2 = iup.vbox{optCompat; gap = 10, margin = '10x10'}
	local fra2 = iup.frame{vbox2; title = 'Export options'}

	local optCase = iup.toggle{title = ' Case-sensitive Fact matching', expand = 'HORIZONTAL'}
	local vbox3 = iup.vbox{optCase; gap = 10, margin = '10x10'}
	local fra3 = iup.frame{vbox3; title = 'Matching options'}

	local optGEDCOM = iup.toggle{title = ' Generate GEDCOM export Research Note', expand = 'HORIZONTAL'}
	local optLinks = iup.toggle{title = ' Display Family Historian Individuals as Links',
			expand = 'HORIZONTAL'}
	local optTable = iup.toggle{title = ' Generate cross-reference table on Update', expand = 'HORIZONTAL'}
	local vbox4 = iup.vbox{optGEDCOM, optLinks, optTable; gap = 10, margin = '10x10'}
	local fra4 = iup.frame{vbox4; title = 'Reporting options'}

	local btnOK = iup.button{title = 'OK',
			tip = 'Close window and update options'}
	local btnCancel = iup.button{title = 'Cancel', padding = '10x3',
			action = function(self) return iup.CLOSE end,
			tip = 'Close window and leave options unchanged'}
	local buttons = iup.hbox{iup.fill{}, btnOK, btnCancel, iup.fill{};
			normalizesize = 'BOTH', gap = 50}

	local vbox1 = iup.vbox{fra1, fra2; gap = 10, margin = '10x10'}
	local vbox2 = iup.vbox{fra3, fra4; gap = 10, margin = '10x10'}
	local hbox = iup.hbox{vbox1, vbox2}
	local vbox = iup.vbox{hbox, buttons; gap = 10, margin = '10x10'}
	local dialog = iup.dialog{vbox; resize = 'No', minbox = 'No', maxbox = 'No',
			title = 'Project Synchronization Options'}

	function btnOK:action()
		gblOptions.Living = optLiving.Value == 'ON'
		gblOptions.DisableCompat = optCompat.Value == 'ON'
		gblOptions.List = optList.Value == 'ON'
		gblOptions.BMD = optBMD.Value == 'ON'
		gblOptions.Case = optCase.Value == 'ON'
		gblOptions.GEDCOM = optGEDCOM.Value == 'ON'
		gblOptions.Links = optLinks.Value == 'ON'
		gblOptions.Table = optTable.Value == 'ON'
		return iup.CLOSE
	end

	-- populate current options

	if gblOptions.Living then optLiving.Value = 'ON' end
	if not CheckAncestrySyncList() then									-- Ancestry Sync list not in project
		optList.Value = 'OFF'
		optList.Active = 'NO'
		gblOptions.List = nil
	elseif gblOptions.List then
		optList.Value = 'ON'
	end
	if gblOptions.DisableCompat then optCompat.Value = 'ON' end
	if gblOptions.BMD then optBMD.Value = 'ON' end
	if gblOptions.Case then optCase.Value = 'ON' end
	if gblOptions.GEDCOM then optGEDCOM.Value = 'ON' end
	if gblOptions.Links then optLinks.Value = 'ON' end
	if gblOptions.Table then optTable.Value = 'ON' end

	local FactFile = fhGetContextInfo('CI_PROJECT_DATA_FOLDER') ..
			'\\Plugin Data\\Ancestry Synchronization Facts.csv'
	if fhfu.fileExists(FactFile) then
		fra1.Title = 'Selection Options (Custom Fact List)'
	end

	-- wait for user input

	dialog:popup()
end

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

function SaveOptions()

	-- save current options to disk

	local OptionsFile = fhGetContextInfo('CI_PROJECT_DATA_FOLDER') ..
			'\\Plugin Data\\Ancestry Synchronization.ini'
	fhSaveTextFile(OptionsFile, '[Files]\n', 'UTF-16LE')		-- create as Unicode to accept any file path
	fhSetIniFileValue(OptionsFile, 'Files', 'Project', 'text', fhGetContextInfo('CI_PROJECT_FILE'))
	for _, F in ipairs({'File', 'GFile', 'AFile'}) do
		if gblOptions[F] then fhSetIniFileValue(OptionsFile, 'Files', F, 'text', gblOptions[F]) end
	end
	for _, O in ipairs({'Living', 'List', 'BMD', 'Case', 'GEDCOM', 'Links', 'Table'}) do
		if gblOptions[O] then fhSetIniFileValue(OptionsFile, 'Options', O, 'bool', gblOptions[O]) end
	end
end

-- *********************************************************************
-- RM database functions
-- *********************************************************************

function SelectRMFile()

	-- warn if GEDCOM not yet exported

	if not gblOptions.GFile then
		local msg = 'You have not yet exported a GEDCOM file for input into RootsMagic. Are you sure that you ' ..
				'want to link a file before doing that?'
		if MessageBox(msg, 'OKCANCEL', 'QUESTION', 'Confirm File Link', 2) ~= 1 then return end
	end

	local filedlg = iup.filedlg{dialogtype = 'OPEN', title = 'Open RootsMagic File',
			directory = fhfu.splitPath(gblOptions.File or '').parent,
			extfilter = 'RootsMagic Database|*.rmtree|All Files|*.*|'}
	filedlg:popup()
	if filedlg.Status == '-1' then return end

	-- update file admin

	local Version, URL, TreeID = GetDatabaseLinks(filedlg.Value)
	if not (Version and URL) then return -1 end					-- problem with file
	gblOptions.File = filedlg.Value
	gblOptions.Version = Version
	gblOptions.URL = URL
	gblOptions.TreeID = TreeID
end

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

function OpenDatabase(FileName)

	-- create copy of RM database file for manipulation (guaranteed ANSI compatibility)

	local SQLfolder = fhGetContextInfo('CI_APP_DATA_FOLDER') .. '\\Plugin Data\\'

	if not fhfu.folderExists(SQLfolder) and not fhfu.createFolder(SQLfolder) then
		local msg = 'Cannot create folder for RootsMagic file copy.'
		MessageBox(msg, 'OK', 'ERROR')
		return
	end

	local SQLfile = SQLfolder .. '~FH Ancestry Sync.' .. fhfu.splitPath(FileName).ext
	if not fhfu.copyFile(FileName, SQLfile, true) then
		local msg = 'Cannot create local copy of RootsMagic file for processing.'
		MessageBox(msg, 'OK', 'ERROR')
		return
	end

	local database = fhSQL.connectSQLite(SQLfile)

	return database, SQLfile
end

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

function GetDatabaseLinks(FileName)

	-- determine RM version by tables present in data file

	local database, SQLFile = OpenDatabase(FileName)
	if not database then return end

	local tblT = {}

	local SQL = "SELECT name FROM sqlite_master WHERE type = 'table'"
	local ResultSet = database:select(SQL)

	for R in ResultSet:rows() do
		tblT[R.name] = true
	end

	local Version
	if tblT['DNATable'] then
		Version = 10
	elseif tblT['FANTable'] then
		Version = 9
	elseif tblT['CitationLinkTable'] then
		Version = 8
	elseif tblT['LinkAncestryTable'] then
		Version = 7
	end

	if not Version or Version < 9 then
		local msg = 'TreeShare is no longer supported in RootsMagic 7 and 8 by either Ancestry or ' ..
				'RootsMagic. From version 2.3 onwards, this plugin requires a database created in ' ..
				'RootsMagic 9 or later.\n\n TreeShare settings are preserved when upgrading an ' ..
				'older database by loading into a supported version of RootsMagic.'
		MessageBox(msg, 'OK', 'ERROR')
		return
	end

	-- get domain from RM options

	local tblD = {'.com', '.co.uk', '.ca', '.ca', '.com.au', '.de', '.it', '.fr', '.se', '.mx'}

	-- get location of AppData folder from Registry

	local AppData = GetRegistryKey('HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\' ..
			'Explorer\\Shell Folders\\AppData') or ''
	local File = AppData .. '\\RootsMagic\\Version ' .. Version .. '\\RootsMagicUser.xml'

	-- RM10 and RM11 have the same tables, so look for RM11 if RM10 not found

	if not fhfu.fileExists(File) and Version == 10 then
		File = AppData .. '\\RootsMagic\\Version 11\\RootsMagicUser.xml'
	end

	if not fhfu.fileExists(File) then
		local msg = 'Cannot find RootsMagic configuration file at ' .. File
		MessageBox(msg, 'OK', 'ERROR')
		return
	end

	local S = fhLoadTextFile(File)
	S = S:sub(S:find(''), S:find('') + 10)		-- Ancestry configuration

	local Domain = tonumber(S:match('%(%d+)%<%/Domain%>'))
	if not Domain or Domain > 9 then
		local msg = 'Cannot determine Ancestry domain from RootsMagic options file. ' ..
				'Default value of ancestry.com will be used'
		MessageBox(msg, 'OK', 'WARNING')
		Domain = 0
	end
	Domain = tblD[Domain + 1]
	local URL = 'https://www.ancestry' .. Domain .. '/family-tree/'

	-- get Tree ID from RM file

	SQL = 'SELECT anID ancID FROM AncestryTable'
	ResultSet = database:select(SQL)
	local ancID = ''

	for I in ResultSet:rows() do
		ancID = I.ancID
		break
	end

	local _, _, TreeID = ancID:match('^(%d+)%:(%d+)%:(%d+)$')
	database:close()
	collectgarbage()
	fhfu.deleteFile(SQLfile)
	return Version, URL, TreeID
end

-- *********************************************************************
-- Compare and Update functions (evaluation)
-- *********************************************************************

function AuditRMFile(Update)

	-- confirm update

	if Update then
		local msg = 'This option will update your RootsMagic database and cannot be undone using ' ..
				'Edit > Undo Plugin Updates as it involves changes to an external file. ' ..
				'Are you sure this is what you want to do?\n\nPlease ensure that the database ' ..
				'file is not open in RootsMagic.'
		if MessageBox(msg, 'YESNO', 'WARNING', nil, 2) ~= 1 then return end
	end

	local FileName = gblOptions.File
	local database, SQLfile = OpenDatabase(FileName)
	if not database then return end

	-- get RM timestamp before any changes are made

	local T = os.date('*t', lfs.attributes(SQLfile, 'modification'))
	local TimeStamp = string.format('%04d-%02d-%02d_%02d%02d', T.year, T.month, T.day, T.hour, T.min)
	local Tstart = lfs.attributes(SQLfile, 'modification')

	-- Read in tables of Facts that are in scope

	local tblIndividualFacts, tblFamilyFacts = DefineFacts()

	-- Display progress bar

	ProgressBarStart(20 + (#tblIndividualFacts + #tblFamilyFacts) * 3)

	-- get Individuals

	local tblFHI, tblRMI, tblUID = GetIndividuals(database)
	collectgarbage()

	-- get Families

	local tblFHF = GetFHFamilies()
	collectgarbage()

	-- check for duplicate UID

	local tblDuplicateUID = CheckDuplicateUID(database)

	-- now start comparing facts (including family relationships) for individuals.

	local tblUpdates = {}

	-- compare living flags

	if not IsBareTree() then
		local AlertLiving = CheckLivingFlags(database, tblUID, tblUpdates, Update)
		collectgarbage()
	end

	-- compare spouses

	CheckSpouses(database, tblUID, tblUpdates)
	collectgarbage()

	-- compare parents

	CheckParents(database, tblUID, tblFHI, tblFHF, tblUpdates)
	collectgarbage()

	-- check names and sex

	if Update then CheckNames(database, tblFHI, tblUpdates, true) end
	CheckNames(database, tblFHI, tblUpdates, false)
	collectgarbage()

	if CheckSex(database, tblFHI, tblUpdates, Update) and not Update and not IsBareTree() then
		-- alert if difference noted
		local msg = "At least one change has been noted in an Individual's recorded Sex.\n\n" ..
				'You are strongly recommended to update the RootsMagic file to reflect this change now ' ..
				'in order to prevent issues with merging records of different sex.\n\n' ..
				'Implement this update?'
		if MessageBox(msg, 'YESNO', 'WARNING') == 1 then
			MessageBox('Please ensure that the linked file is not open in RootsMagic before clicking on OK.',
					'OK', 'WARNING')
			collectgarbage()
			CheckSex(database, tblFHI, tblUpdates, true)
		end
	end
	collectgarbage()

	-- check Individual Facts (Fact name is RM name, not FH name)

	for _, Fact in ipairs(tblIndividualFacts) do
		CheckIndividualFact(database, tblUID, tblUpdates, Fact, Update)
		collectgarbage()
	end

	-- check Family Facts

	for _, Fact in ipairs(tblFamilyFacts) do
		CheckFamilyFact(database, tblFHI, tblRMI, tblUID, tblFHF, tblUpdates, Fact, Update)
		collectgarbage()
	end

	-- check for redundant events

	CheckRedundantEvents(database, tblRMI, tblUID, tblUpdates, Update)
	collectgarbage()

	-- save or process list of changes to update RM timestamps and Ancestry "changed records" list

	local UpdateFile = fhGetContextInfo('CI_PROJECT_DATA_FOLDER') .. '\\Plugin Data\\FH RM Ancestry Sync.txt'

	if not Update then			-- generate list of changed individuals and save to disk
		local tblU = {}
		for UID, _ in pairs(tblUpdates) do
			table.insert(tblU, tblUID[UID].IDrm)
		end
		if #tblU > 0 then
			fhSaveTextFile(UpdateFile, table.concat(tblU, '\n'))
		end
	else						-- update RM timestamps
		UpdateRMTimeStamps(database, SQLfile)
		collectgarbage()
		UpdateAncestryList(database, SQLfile, tblUID)
		collectgarbage()
		fhfu.deleteFile(UpdateFile)
	end

	-- finished with RM, so connection can be closed and file updated

	database:close()
	collectgarbage()

	-- backup RM database before updating

	local Tend = lfs.attributes(SQLfile, 'modification')
	if Tend > Tstart then
		local Path = fhfu.splitPath(FileName)
		local BackupFile = Path.parent .. '\\~' .. Path.filename .. '.' .. TimeStamp .. '.bak'
		if not fhfu.copyFile(FileName, BackupFile, true) then
			local msg = 'RootsMagic backup failed.'
			MessageBox(msg, 'OK', 'ERROR')
		end
	end

	if fhfu.copyFile(SQLfile, FileName, true) then
		fhfu.deleteFile(SQLfile)
	else
		local msg = 'RootsMagic update failed.'
		MessageBox(msg, 'OK', 'ERROR')
		return
	end

	-- create Research Note to output results

	ProgressBarIncrement('Preparing Report')
	local rt = fhNewRichText()

	if IsBareTree() then
		rt:AddText('Title:\tRootsMagic - Ancestry Audit Guide\n')
	elseif Update then
		rt:AddText('Title:\tRootsMagic Update Guide\n')
	else
		rt:AddText('Title:\tRootsMagic - Ancestry TreeShare Update Guide\n')
	end
	rt:AddText('Type:\tAncestry Sync\n')
	rt:AddText('Date:\t' .. os.date('%d' .. ' ' .. '%b' .. ' ' .. '%Y') .. '\n')
	rt:AddText('Status:\topen\n\n')
	rt:AddText('')
	rt:AddText('FH Project: | ' .. fhGetContextInfo('CI_PROJECT_NAME') .. ' (' ..
			fhGetContextInfo('CI_PROJECT_FILE'):gsub('\\', '\\\\') .. '')
	rt:AddText('RM' .. gblOptions.Version .. ' File: | ' .. FileName:gsub('\\', '\\\\') .. '')
	if gblOptions.TreeID then
		rt:AddText('Ancestry Tree: | ')
	end
	rt:AddText('\n\n')

	if IsBareTree() then
		rt:AddText('This Research Note lists all differences between the nominated RootsMagic file and ' ..
				'the current Project. If you have completed the RootsMagic and Ancestry updates, it ' ..
				'represents the events that should be reviewed within TreeShare to ensure that the ' ..
				'Ancestry tree is a true match to your project. See the ' ..
				' ' ..
				'for more details of the auditing process.\n\n')
	elseif Update then
		rt:AddText('This Research Note lists all remaining differences between the nominated RootsMagic ' ..
				'file and the current Project that cannot be processed automatically by the plugin. ' ..
				'Make these changes in RootsMagic prior to running TreeShare to upload all changes to ' ..
				'Ancestry.\n\n')
	else
		rt:AddText('This Research Note lists all differences between the nominated RootsMagic file and ' ..
				'the current Project. If the RM-Ancestry sync is currently up to date, this is the list ' ..
				'of changes that will need to be uploaded to Ancestry using RootsMagic TreeShare once ' ..
				'the RootsMagic database has been updated.\n\n')
	end

	-- record counts

	local FHI = 0
	for _,_ in pairs(tblFHI) do FHI = FHI + 1 end

	local RMI = 0
	for _,_ in pairs(tblRMI) do RMI = RMI + 1 end

	rt:AddText('FH Individuals: \t' .. FHI .. '\n')
	rt:AddText('RM Individuals: \t' .. RMI .. '\n')

	-- which Individuals are missing from RM/Ancestry?

	local tblAdd, tblDelete = CheckIndividuals(tblUID)

	if #tblAdd > 0 then
		if Update then
			rt:AddText('\nNew Individuals to be added to RootsMagic:\n\n')
		else
			rt:AddText('\nNew Individuals to be added to Ancestry:\n\n')
		end
		for _, pI in ipairs(tblAdd) do
			AddFHRecord(pI, rt)
			rt:AddText(' (FH' .. fhGetRecordId(pI) .. ')\n')
		end
	end

	-- which Individuals are missing from FH?

	if #tblDelete > 0 then
		if Update then
			rt:AddText('\nIndividuals to be deleted from RootsMagic:\n\n')
		else
			rt:AddText('\nIndividuals to be deleted from Ancestry:\n\n')
		end
		for _, I in ipairs(tblDelete) do
			if I.Given == '' or I.Surname == '' then
				rt:AddText(I.Given .. (I.Surname):upper() .. ' (RM' .. I.IDrm .. ')\n')
			else
				rt:AddText(I.Given .. ' ' .. (I.Surname):upper() .. ' (RM' .. I.IDrm .. ')\n')
			end
		end
	end
	collectgarbage()

	-- count and report changed individuals

	local ChangedIndividuals = 0
	for _, _ in pairs(tblUpdates) do ChangedIndividuals = ChangedIndividuals + 1 end

	if ChangedIndividuals > 500 then
		rt:AddText('\nDifferences in Individual Records:\n\n')
		rt:AddText('Too many to list individually (' .. ChangedIndividuals .. ')\n')
	elseif ChangedIndividuals > 0 then
		local tblUpdates = SortChangedIndividuals(tblUID, tblUpdates)
		rt:AddText('\nDifferences in Individual Records:\n\n')
		for _, I in ipairs(tblUpdates) do
			local UID = I.UID
			AddFHRecord(tblUID[UID].p, rt)
			rt:AddText(' (FH' .. tblUID[UID].IDfh .. '/RM' .. tblUID[UID].IDrm .. ') - ')
			local PreviousItem = ''
			local tblT = {}
			for _, Item in ipairs(I.Facts) do
				if Item ~= PreviousItem then table.insert(tblT, Item) end
				PreviousItem = Item
			end
			rt:AddText(table.concat(tblT, ', ') .. '\n')
		end
	end
	collectgarbage()

	-- report duplicate individuals

	if #tblDuplicateUID > 0 then
		rt:AddText('\nDuplicate Individuals in RootsMagic:\n\n')
		rt:AddText('These are duplicate individuals in the RootsMagic database that arise from a ' ..
				'failed merge process. This is most commonly caused by a change in recorded sex, ' ..
				'as records can only be merged if they are of the same sex. Please amend the ' ..
				'incorrect sex in RootsMagic and manually merge the two records prior to ' ..
				'rerunning the plugin.\n\n')
		for _, I in ipairs(tblDuplicateUID) do
			AddFHRecord(tblUID[I].p, rt)
			rt:AddText(' (FH' .. tblUID[I].IDfh .. '/RM' .. tblUID[I].IDrm .. ')')
		end
		rt:AddText('\n')
	end

	-- alert to changes in Living status

	if AlertLiving then
		rt:AddText('\nNOTE:\n\nRootsMagic TreeShare may not detect a change in Living status ' ..
				'when syncing with Ancestry. Please ensure that you check the relevant Ancestry ' ..
				'record carefully to ensure that living individual privacy is protected.\n')
	end

	-- alert to RM deletions

	if #tblDelete > 0 and Update then
		rt:AddText('\nNOTE:\n\nWhere individuals are to be deleted, you may find it easier to ' ..
				'delete the relevant individuals first, then re-run the Update process. Deleting an ' ..
				'individual in either RootsMagic or Ancestry also deletes all their facts and ' ..
				'relationships, so some of the differences reported here may no longer be relevant.\n')
	end

	-- do databases match?

	local match = (FHI == RMI and #tblAdd + #tblDelete + ChangedIndividuals == 0)

	if match then
		rt:AddText('\nNo differences identified.')
	elseif not Update then
		GenerateGEDCOMUpdate(tblUpdates, tblAdd, tblUID)
	end

	-- create Research Note from assembled content

	local pRN = fhCreateItem('_RNOT')
	local pT = fhGetItemPtr(pRN, '~.TEXT')
	fhSetValueAsRichText(pT, rt)

	-- save audit result and close progress bar now all complete

	local LogFile = fhGetContextInfo('CI_PROJECT_DATA_FOLDER') .. '\\Plugin Data\\Ancestry Sync Log.csv'
	fhSaveTextFile(LogFile, table.concat(gblProgBar.Log, '\n') .. '\n')
	gblProgBar.Dialog:destroy()
	gblProgBar = nil
	fhUpdateDisplay()

	-- generate record list

	if Update and gblOptions.Table then CreateRecordList(tblUID) end

	local endmsg
	if Update then
		endmsg = 'RootsMagic file update completed and reported as new Research Note.'
	else
		endmsg = 'RootsMagic comparison completed and reported as new Research Note.'
	end
	if match then
		if Update then
			endmsg = endmsg .. '\n\nNo remaining differences identified.'
		else
			endmsg = endmsg .. '\n\nNo differences identified.'
		end
	end

	MessageBox(endmsg, 'OK', 'INFORMATION')
end

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

function GetIndividuals(database)

	local tblFHI = {}
	local tblRMI = {}
	local tblUID = {}
	local pI = fhNewItemPtr()
	local count = 0

	-- get FH records

	ProgressBarIncrement('Getting FH Individuals')
	pI:MoveToFirstRecord('INDI')
	while pI:IsNotNull() do
		if not IsExcluded(pI) then
			local ID = fhGetRecordId(pI)
			local UID = GetUID(pI)
			tblFHI[ID] = {}
			tblFHI[ID].p = pI:Clone()
			tblFHI[ID].UID = UID
			tblUID[UID] = {}
			tblUID[UID].IDfh = ID
			tblUID[UID].p = pI:Clone()
			count = count + 1
			if count % 1000 == 0 then ProgressBarUpdate(count) end
		end
		pI:MoveNext()
	end

	-- count RM records

	ProgressBarIncrement('Getting RM Individuals')
	count = 0
	local SQL = 'SELECT COUNT(*) Size FROM PersonTable, NameTable WHERE PersonID = OwnerID and IsPrimary = 1'
	local ResultSet = database:select(SQL)
	local size
	for p in ResultSet:rows() do size = p.Size|0 end

	-- get RM records

	SQL = 'SELECT UniqueID, PersonID, Surname, Given ' ..
			'FROM PersonTable, NameTable WHERE PersonID = OwnerID and IsPrimary = 1'
	ResultSet = database:select(SQL)
	for p in ResultSet:rows() do
		local ID = tonumber(p.PersonID)|0
		local UID = p.UniqueID
		tblRMI[ID] = {}
		tblRMI[ID].Given = p.Given
		tblRMI[ID].Surname = p.Surname:upper()
		tblRMI[ID].UID = UID
		if not tblUID[UID] then tblUID[UID] = {} end
		tblUID[UID].IDrm = ID
		tblUID[UID].Given = p.Given
		tblUID[UID].Surname = p.Surname
		count = count + 1
		if count % 1000 == 0 then ProgressBarUpdate(count) end
	end
	return tblFHI, tblRMI, tblUID
end

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

function CheckDuplicateUID(database)

	local tblT = {}
	local SQL = 'SELECT UniqueID FROM PersonTable GROUP BY UniqueID HAVING COUNT(*) > 1'
	local ResultSet = database:select(SQL)

	for P in ResultSet:rows() do
		table.insert(tblT, P.UniqueID)
	end
	return tblT
end

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

function GetFHFamilies()

	local p = fhNewItemPtr()
	local pF = fhNewItemPtr()
	local tblFHF = {}
	local count = 0

	ProgressBarIncrement('Getting FH Families')
	pF:MoveToFirstRecord('FAM')
	while pF:IsNotNull() do
		if not IsExcluded(pF, true) then
			local pH, pW = GetFamilySpouses(pF)
			local ID = fhGetRecordId(pF)
			tblFHF[ID] = {}
			tblFHF[ID].p = pF:Clone()
			if pH:IsNotNull() and not IsExcluded(pH) then tblFHF[ID].IDh = fhGetRecordId(pH) end
			if pW:IsNotNull() and not IsExcluded(pW) then tblFHF[ID].IDw = fhGetRecordId(pW) end
		end
		count = count + 1
		if count % 1000 == 0 then ProgressBarUpdate(count) end
		pF:MoveNext()
	end
	return tblFHF
end

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

function CheckLivingFlags(database, tblUID, tblUpdates, Update)

	-- get FH Living flags

	local tblFH = {}
	local tblRM = {}
	local alert
	local count = 0

	ProgressBarIncrement('Getting FH Living flags')
	for UID, I in pairs(tblUID) do
		if I.IDfh and fhGetItemText(I.p, '~._FLGS.__LIVING') == 'Y' then
			tblFH[UID] = true
		end
		count = count + 1
		if count % 1000 == 0 then ProgressBarUpdate(count) end
	end

	-- get RM Living flags

	ProgressBarIncrement('Getting RM Living flags')
	count = 0
	local SQL = 'SELECT UniqueID FROM PersonTable WHERE Living = 1'
	local ResultSet = database:select(SQL)
	for p in ResultSet:rows() do
		tblRM[p.UniqueID] = true
		count = count + 1
		if count % 1000 == 0 then ProgressBarUpdate(count) end
	end

	-- update RM flags to match FH

	ProgressBarIncrement('Comparing Living flags')
	count = 0
	local tblSQL = {}
	local tblSetLiving = {}
	local tblClearLiving = {}
	for UID, I in pairs(tblUID) do
		if I.IDrm then
			local change
			if tblUID[UID].IDfh and tblFH[UID] and not tblRM[UID] then
				table.insert(tblSetLiving, I.IDrm)
				change = true
			elseif tblUID[UID].IDfh and not tblFH[UID] and tblRM[UID] then
				table.insert(tblClearLiving, I.IDrm)
				change = true
			end
			if change and not Update then						-- record the difference
				if not tblUpdates[UID] then tblUpdates[UID] = {} end
				table.insert(tblUpdates[UID], 'Living Flag')
				alert = true
			end
		end
		count = count + 1
		if count % 1000 == 0 then ProgressBarUpdate(count) end
	end
	if Update then				-- remove the differences
		while #tblSetLiving > 0 do
			table.insert(tblSQL, table.remove(tblSetLiving))		-- transfer one value to SQL table
			if #tblSetLiving == 0 or #tblSQL > 999 then				-- update this block of values
				local SQL = 'UPDATE PersonTable SET Living = 1 WHERE PersonID IN (' ..
						table.concat(tblSQL, ',') .. ')'
				database:execute(SQL)
				tblSQL = {}
			end
		end
		while #tblClearLiving > 0 do
			table.insert(tblSQL, table.remove(tblClearLiving))		-- transfer one value to SQL table
			if #tblClearLiving == 0 or #tblSQL > 999 then				-- update this block of values
				local SQL = 'UPDATE PersonTable SET Living = 0 WHERE PersonID IN (' ..
						table.concat(tblSQL, ',') .. ')'
				database:execute(SQL)
				tblSQL = {}
			end
		end
	end
	return alert
end

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

function CheckSpouses(database, tblUID, tblUpdates)

	local tblT = {}
	local count = 0

	-- get FH spouses

	ProgressBarIncrement('Getting FH Spouses')
	local pF = fhNewItemPtr()
	pF:MoveToFirstRecord('FAM')
	while pF:IsNotNull() do
		local pH, pW = GetFamilySpouses(pF)
		if pH:IsNotNull() and pW:IsNotNull() then		-- both spouses known
			if not IsExcluded(pH) and not IsExcluded(pW) then
				local UIDh = FormatUID(fhGetItemText(pH, '~._UID'))
				local UIDw = FormatUID(fhGetItemText(pW, '~._UID'))
				if not tblT[UIDh] then
					tblT[UIDh] = {}
					tblT[UIDh].FH = {}
				end
				table.insert(tblT[UIDh].FH, UIDw)
				if not tblT[UIDw] then
					tblT[UIDw] = {}
					tblT[UIDw].FH = {}
				end
				table.insert(tblT[UIDw].FH, UIDh)
			end
		end
		count = count + 1
		if count % 1000 == 0 then ProgressBarUpdate(count) end
		pF:MoveNext()
	end

	-- get RM spouses

	ProgressBarIncrement('Getting RM Spouses')
	count = 0
	local SQL = 'SELECT P1.UniqueID UIDh, P2.UniqueID UIDw FROM FamilyTable F ' ..
			'LEFT JOIN PersonTable P1 ON F.FatherID = P1.PersonID ' ..
			'LEFT JOIN PersonTable P2 ON F.MotherID = P2.PersonID'
	local ResultSet = database:select(SQL)
	for p in ResultSet:rows() do
		local UIDh = p.UIDh
		local UIDw = p.UIDw
		if UIDh and UIDw then
			if not tblT[UIDh] then tblT[UIDh] = {} end
			if not tblT[UIDh].RM then tblT[UIDh].RM = {} end
			table.insert(tblT[UIDh].RM, UIDw)
			if not tblT[UIDw] then tblT[UIDw] = {} end
			if not tblT[UIDw].RM then tblT[UIDw].RM = {} end
			table.insert(tblT[UIDw].RM, UIDh)
		end
		count = count + 1
		if count % 1000 == 0 then ProgressBarUpdate(count) end
	end

	-- sort spouses for comparison

	ProgressBarIncrement('Comparing Spouses')
	count = 0
	for UID, Spouses in pairs(tblT) do
		if Spouses.FH then
			table.sort(Spouses.FH)
			Spouses.FH = table.concat(Spouses.FH)
		end
		if Spouses.RM then
			table.sort(Spouses.RM)
			Spouses.RM = table.concat(Spouses.RM)
		end
	end

	-- compare spouses

	for UID, Spouses in pairs(tblT) do
		if not Spouses.FH or not Spouses.RM or Spouses.FH ~= Spouses.RM then
			if tblUID[UID].IDfh and tblUID[UID].IDrm then	-- do not include missing individuals
				if not tblUpdates[UID] then tblUpdates[UID] = {} end
				table.insert(tblUpdates[UID], 'Spouse')
			end
		end
		count = count + 1
		if count % 1000 == 0 then ProgressBarUpdate(count) end
	end
end

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

function CheckParents(database, tblUID, tblFHI, tblFHF, tblUpdates)

	local p = fhNewItemPtr()
	local tblT = {}
	local count = 0

	-- get FH parents

	ProgressBarIncrement('Getting FH Parents')
	for _, I in pairs(tblFHI) do
		local UID = FormatUID(fhGetItemText(I.p, '~._UID'))
		p:MoveTo(I.p, '~.FAMC')
		while p:IsNotNull() do
			local pF = fhGetValueAsLink(p)
			local ID = fhGetRecordId(pF)
			if tblFHF[ID] then					-- exclude unlisted families
				if not tblT[UID] then tblT[UID] = {FH = {}, RM = {}} end
				local pH, pW = GetFamilySpouses(pF)
				if pH:IsNotNull() and not IsExcluded(pH) then			-- father listed
					local UIDh = FormatUID(fhGetItemText(pH, '~._UID'))
					tblT[UID].FH[UIDh] = true
				end
				if pW:IsNotNull() and not IsExcluded(pW) then			-- mother listed
					local UIDw = FormatUID(fhGetItemText(pW, '~._UID'))
					tblT[UID].FH[UIDw] = true
				end
			end
			p:MoveNext('SAME_TAG')
		end
		count = count + 1
		if count % 1000 == 0 then ProgressBarUpdate(count) end
	end

	-- get RM parents

	ProgressBarIncrement('Getting RM Parents')
	count = 0
	local SQL = 'SELECT Pc.UniqueID UID, Pf.UniqueID UIDf, Pm.UniqueID UIDm FROM ChildTable C ' ..
			'INNER JOIN FamilyTable F ON C.FamilyID = F.FamilyID ' ..
			'LEFT JOIN PersonTable Pc ON C.ChildID = Pc.PersonID ' ..
			'LEFT JOIN PersonTable Pf ON F.FatherID = Pf.PersonID ' ..
			'LEFT JOIN PersonTable Pm ON F.MotherID = Pm.PersonID'
	local ResultSet = database:select(SQL)
	for p in ResultSet:rows() do
		local UID = p.UID
		if not tblT[UID] then tblT[UID] = {FH = {}, RM = {}} end
		local UIDf = p.UIDf
		if UIDf then tblT[UID].RM[UIDf] = true end
		local UIDm = p.UIDm
		if UIDm then tblT[UID].RM[UIDm] = true end
		count = count + 1
		if count % 1000 == 0 then ProgressBarUpdate(count) end
	end

	-- compare distinctive parents (ignores any RM duplication from merging)

	ProgressBarIncrement('Comparing Parents')
	count = 0
	for UID, Parents in pairs(tblT) do
		local match = true
		if tblUID[UID].IDfh and tblUID[UID].IDrm then
			for UIDfh, _ in pairs(Parents.FH) do
				if not Parents.RM[UIDfh] then match = false end
			end
			for UIDrm, _ in pairs(Parents.RM) do
				if not Parents.FH[UIDrm] then match = false end
			end
		end
		if not match then
			if not tblUpdates[UID] then tblUpdates[UID] = {} end
			table.insert(tblUpdates[UID], 'Parents')
		end
		count = count + 1
		if count % 1000 == 0 then ProgressBarUpdate(count) end
	end
end

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

function CheckNames(database, tblFHI, tblUpdates, Update)

	local tblFH = {}
	local tblRM = {}
	local count = 0

	-- get FH Individual names (turn off preference setting temporarily to ensure case-sensitive)

	ProgressBarIncrement('Getting FH Names')
	fhOverridePreference('SURNAMES_UPPERCASE', true, false)
	for _, I in pairs(tblFHI) do
		local pN = fhNewItemPtr()
		local tblNames = {}
		pN:MoveTo(I.p, '~.NAME')
		if pN:IsNull() then						-- use dummy name to match RM name
			local tblN = {Given = '?', Surname = '', Prefix = '', Suffix = '', Nickname = ''}
			table.insert(tblNames, tblN)
		end
		while pN:IsNotNull() do
			local tblN = {}
			tblN.Given = fhGetItemText(pN, '~.GIVN')
			tblN.GivenAll = fhGetItemText(pN, '~:GIVEN_ALL')
			tblN.Surname = fhGetItemText(pN, '~:SURNAME')
			tblN.Prefix = fhGetItemText(pN, '~.NPFX')
			tblN.Suffix = fhGetItemText(pN, '~.NSFX')
			tblN.Nickname = fhGetItemText(pN, '~.NICK')
			table.insert(tblNames, tblN)
			pN:MoveNext('SAME_TAG')			-- alternative names
		end
		tblFH[I.UID] = tblNames
		count = count + 1
		if count % 1000 == 0 then ProgressBarUpdate(count) end
	end
	fhOverridePreference('SURNAMES_UPPERCASE', false)

	-- get all RM Individual names

	ProgressBarIncrement('Getting RM Names')
	count = 0
	local SQL = 'SELECT UniqueID, NameID, Surname, Given, Prefix, Suffix, Nickname, IsPrimary ' ..
			'FROM PersonTable, NameTable WHERE PersonID = OwnerID'
	local ResultSet = database:select(SQL)
	for p in ResultSet:rows() do
		local tblN = {}
		local UID = p.UniqueID
		tblN.NameID = tonumber(p.NameID)|0
		tblN.Prefix = p.Prefix
		tblN.Suffix = p.Suffix
		tblN.Nickname = p.Nickname
		tblN.Given = p.Given
		tblN.Surname = p.Surname
		if not tblRM[UID] then tblRM[UID] = {} end
		if p.IsPrimary == 1 then
			table.insert(tblRM[UID], 1, tblN)			-- top of table
		else
			table.insert(tblRM[UID], tblN)				-- end of table
		end
		count = count + 1
		if count % 1000 == 0 then ProgressBarUpdate(count) end
	end

	ProgressBarIncrement('Comparing Names')

	-- clear Prefix and Nickname if auditing

	if IsBareTree() then
		for UID, Names in pairs(tblFH) do
			for _, Name in ipairs(Names) do
				Name.Prefix = nil
				Name.Nickname = nil
			end
		end
		for UID, Names in pairs(tblRM) do
			for _, Name in ipairs(Names) do
				Name.Prefix = nil
				Name.Nickname = nil
			end
		end
	end

	-- compare names

	count = 0
	for UID, Names in pairs(tblFH) do
		if tblRM[UID] then
			local matchPrimary, matchAlternative
			if #Names == 1 and #tblRM[UID] == 1 then
				matchAlternative = true			--	no alternative names
			end
			for iFH = 1, #Names do
				for iRM = 1, #tblRM[UID] do
					if Names[iFH].Prefix == tblRM[UID][iRM].Prefix and
							Names[iFH].Suffix == tblRM[UID][iRM].Suffix and
							(Names[iFH].Given == tblRM[UID][iRM].Given or Names[iFH].GivenAll == tblRM[UID][iRM].Given) and
							Names[iFH].Surname == tblRM[UID][iRM].Surname and
							Names[iFH].Nickname == tblRM[UID][iRM].Nickname then
						if iFH == 1 and iRM == 1 then
							matchPrimary = true
						elseif iFH > 1 and iRM > 1 then
							matchAlternative = true
						end
						if Update and iFH == 1 and iRM > 1 then		-- update primary name in RM
							for _, N in ipairs(tblRM[UID]) do
								SQL = 'UPDATE NameTable SET IsPrimary = 0 WHERE NameID = ' .. N.NameID
								database:execute(SQL)
							end
							SQL = 'UPDATE NameTable SET IsPrimary = 1 WHERE NameID = ' ..
									tblRM[UID][iRM].NameID
							database:execute(SQL)
						end
					end
				end
			end
			if (not matchPrimary or not matchAlternative) and not Update then
				if not tblUpdates[UID] then tblUpdates[UID] = {} end
				if not matchPrimary then table.insert(tblUpdates[UID], 'Primary Name') end
				if not matchAlternative then table.insert(tblUpdates[UID], 'Alternative Name') end
			end
		end
		count = count + 1
		if count % 1000 == 0 then ProgressBarUpdate(count) end
	end
end

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

function CheckSex(database, tblFHI, tblUpdates, Update)

	local tblFH = {}
	local tblRM = {}
	local alert
	local count = 0

	-- get FH Individual details

	ProgressBarIncrement('Getting FH Sex')
	for _, I in pairs(tblFHI) do
		local Sex = fhGetItemText(I.p, '~.SEX')
		if Sex == '' then Sex = 'Unknown' end
		tblFH[I.UID] = Sex
		count = count + 1
		if count % 1000 == 0 then ProgressBarUpdate(count) end
	end

	-- get RM Individual details

	ProgressBarIncrement('Getting RM Sex')
	count = 0
	local tblSex = {'Male', 'Female', 'Unknown'}
	local SQL = 'SELECT UniqueID, Sex FROM PersonTable'
	local ResultSet = database:select(SQL)
	for p in ResultSet:rows() do
		tblRM[p.UniqueID] = tblSex[p.Sex + 1]
		count = count + 1
		if count % 1000 == 0 then ProgressBarUpdate(count) end
	end

	-- compare tables

	ProgressBarIncrement('Comparing Sex')
	count = 0
	for _, I in pairs(tblFHI) do
		local UID = I.UID
		if tblFH[UID] and tblRM[UID] and tblFH[UID] ~= tblRM[UID] then
			if Update then
				local NewSex = 0
				if tblFH[UID] == 'Female' then NewSex = 1
				elseif tblFH[UID] == 'Unknown' then NewSex = 2 end
				SQL = 'UPDATE PersonTable SET Sex = ' .. NewSex .. ' WHERE UniqueID = "' .. UID .. '"'
				database:execute(SQL)
			else
				if not tblUpdates[UID] then tblUpdates[UID] = {} end
				table.insert(tblUpdates[UID], 'Sex')
				alert = true
			end
		end
		count = count + 1
		if count % 1000 == 0 then ProgressBarUpdate(count) end
	end
	return alert
end

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

function CheckIndividualFact(database, tblUID, tblUpdates, Fact, Update)

	local tblFH = {}
	local tblRM = {}
	local count = 0

	-- do not compare if Census, as these included in Residence, and no values in RM

	if Fact.Tag == 'CENS' then return end

	-- get FH Individual details

	ProgressBarIncrement('Getting FH ' .. Fact.Description)

	for UID, I in pairs(tblUID) do
		if I.IDfh then
			local p = fhNewItemPtr()
			if Fact.Tag == 'RESI' then			-- also include Census entries
				p:MoveTo(I.p, '~.CENS')
				while p:IsNotNull() do
					local Value = fhGetValueAsText(p)				--	attributes only
					local Date = fhGetItemText(p, '~.DATE')
					local Place = fhGetItemText(p, '~.PLAC')
					local Event = Value .. Date .. Place:gsub(' ', '')
					if Event ~= '' and fhGetItemText(p, '~._FLGS.__PRIVATE') ~= 'Y' and
							fhGetItemText(p, '~._FLGS.__REJECTED') ~= 'Y' then
						if not tblFH[UID] then tblFH[UID] = {} end
						tblFH[UID][Event] = true
					end
					p:MoveNext('SAME_TAG')
				end
			end
			p:MoveTo(I.p, '~.' .. Fact.Tag)
			while p:IsNotNull() do
				local Value = fhGetValueAsText(p)				--	attributes only
				local Date = fhGetItemText(p, '~.DATE')
				local Place = fhGetItemText(p, '~.PLAC')
				local Event = Value .. Date .. Place:gsub(' ', '')
				if Event ~= '' and fhGetItemText(p, '~._FLGS.__PRIVATE') ~= 'Y' and
						fhGetItemText(p, '~._FLGS.__REJECTED') ~= 'Y' then
					if not tblFH[UID] then tblFH[UID] = {} end
					tblFH[UID][Event] = true
				end
				p:MoveNext('SAME_TAG')
			end
		end
		count = count + 1
		if count % 1000 == 0 then ProgressBarUpdate(count) end
	end

	-- get RM Individual details

	ProgressBarIncrement('Getting RM ' .. Fact.Description)
	count = 0
	local SQL = 'SELECT I.UniqueID UniqueID, E.EventID EventID, E.Date Date, P.Name Place, ' ..
			'E.Details Details FROM EventTable E ' ..
			'JOIN FactTypeTable F ON E.EventType = F.FactTypeID AND ' ..
			'F.Abbrev = "' .. Fact.Description .. '" ' ..
			'JOIN PersonTable I ON E.OwnerID = I.PersonID ' ..
			'LEFT JOIN PlaceTable P ON E.PlaceID = P.PlaceID'
	local ResultSet = database:select(SQL)
	for p in ResultSet:rows() do
		local UID = p.UniqueID
		local Details = p.Details
		local Date = FormatRMDate(p.Date)
		local Place = p.Place
		local Event = (Details or '') .. (Date or '') .. (Place or ''):gsub(' ', '')
		if not tblRM[UID] then tblRM[UID] = {} end
		if Event ~= '' then tblRM[UID][Event] = tonumber(p.EventID)|0 end
		count = count + 1
		if count % 1000 == 0 then ProgressBarUpdate(count, 'Facts') end
	end

	-- find FH facts that are not in RM and add to list for prompted sync

	ProgressBarIncrement('Comparing ' .. Fact.Description)
	for UID, Events in pairs(tblFH) do
		for Event, _ in pairs(Events) do
			if tblUID[UID].IDfh and tblUID[UID].IDrm and not MatchEvent(UID, Event, tblRM) then
				if not tblUpdates[UID] then tblUpdates[UID] = {} end
				table.insert(tblUpdates[UID], Fact.Description)
			end
		end
	end

	-- find RM facts that are not in FH, and delete

	local tblX = {}				-- Events to be deleted

	for UID, Events in pairs(tblRM) do
		for Event, EventID in pairs(Events) do
			if tblUID[UID].IDfh and tblUID[UID].IDrm and not MatchEvent(UID, Event, tblFH) then
				if Update then
					table.insert(tblX, EventID)
				else
					if not tblUpdates[UID] then tblUpdates[UID] = {} end
					table.insert(tblUpdates[UID], Fact.Description)
				end
			end
		end
	end

	if #tblX > 0 and Update then
		for _, Event in ipairs(tblX) do
			SQL = 'DELETE FROM EventTable WHERE EventID = ' .. Event
			database:execute(SQL)
		end
	end
end

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

function CheckFamilyFact(database, tblFHI, tblRMI, tblUID, tblFHF, tblUpdates, Fact, Update)

	local tblFH = {}
	local tblRM = {}
	local count = 0

	-- get FH Family details

	ProgressBarIncrement('Getting FH ' .. Fact.Description)

	for _, F in pairs(tblFHF) do
		local UIDh, UIDw
		if F.IDh then UIDh = tblFHI[F.IDh].UID end
		if F.IDw then UIDw = tblFHI[F.IDw].UID end
		local pF = F.p
		local p = fhNewItemPtr()
		p:MoveTo(pF, '~.' .. Fact.Tag)
		while p:IsNotNull() do
			local Value = fhGetValueAsText(p)				--	attributes only
			local Date = fhGetItemText(p, '~.DATE')
			local Place = fhGetItemText(p, '~.PLAC')
			local Event = Value .. Date .. Place:gsub(' ', '')
			if Event ~= '' and UIDh and fhGetItemText(p, '~._FLGS.__PRIVATE') ~= 'Y' and
					fhGetItemText(p, '~._FLGS.__REJECTED') ~= 'Y' then
				if not tblFH[UIDh] then tblFH[UIDh] = {} end
				tblFH[UIDh][Event] = true
			end
			if Event ~= '' and UIDw and fhGetItemText(p, '~._FLGS.__PRIVATE') ~= 'Y' and
					fhGetItemText(p, '~._FLGS.__REJECTED') ~= 'Y' then
				if not tblFH[UIDw] then tblFH[UIDw] = {} end
				tblFH[UIDw][Event] = true
			end
			p:MoveNext('SAME_TAG')
		end
		count = count + 1
		if count % 1000 == 0 then ProgressBarUpdate(count) end
	end

	-- get RM Family details

	ProgressBarIncrement('Getting RM ' .. Fact.Description)
	count = 0
	local SQL = 'SELECT Fam.FatherID FatherID, Fam.MotherID MotherID, E.EventID EventID, E.Date Date, ' ..
			'P.Name Place, E.Details Details FROM EventTable E ' ..
			'JOIN FactTypeTable F ON E.EventType = F.FactTypeID ' ..
			'JOIN FamilyTable Fam ON E.OwnerID = Fam.FamilyID and F.Abbrev = "' ..
			Fact.Description .. '" ' ..
			'LEFT JOIN PlaceTable P ON E.PlaceID = P.PlaceID'
	local ResultSet = database:select(SQL)
	for f in ResultSet:rows() do
		local UIDf, UIDm
		local FatherID = tonumber(f.FatherID)|0
		local MotherID = tonumber(f.MotherID)|0
		local EventID = tonumber(f.EventID)|0
		if FatherID > 0 and tblRMI[FatherID] then UIDf = tblRMI[FatherID].UID end
		if MotherID > 0 and tblRMI[MotherID] then UIDm = tblRMI[MotherID].UID end
		local Details = f.Details
		local Date = FormatRMDate(f.Date)
		local Place = f.Place
		local Event = (Details or '') .. (Date or '') .. (Place or ''):gsub(' ', '')
		if Event ~= '' and UIDf then						-- assign fact to father
			if not tblRM[UIDf] then tblRM[UIDf] = {} end
			tblRM[UIDf][Event] = EventID
		end
		if Event ~= '' and UIDm then						-- assign fact to mother
			if not tblRM[UIDm] then tblRM[UIDm] = {} end
			tblRM[UIDm][Event] = EventID
		end
		count = count + 1
		if count % 1000 == 0 then ProgressBarUpdate(count) end
	end

	-- find FH facts that are not in RM and add to list for prompted sync

	ProgressBarIncrement('Comparing ' .. Fact.Description)
	for UID, Events in pairs(tblFH) do
		for Event, _ in pairs(Events) do
			if tblUID[UID].IDfh and tblUID[UID].IDrm and not MatchEvent(UID, Event, tblRM) then
				if not tblUpdates[UID] then tblUpdates[UID] = {} end
				table.insert(tblUpdates[UID], Fact.Description)
			end
		end
	end

	-- find RM facts that are not in FH, and delete

	local tblX = {}				-- Events to be deleted
	for UID, Events in pairs(tblRM) do
		for Event, EventID in pairs(Events) do
			if tblUID[UID].IDfh and tblUID[UID].IDrm and not MatchEvent(UID, Event, tblFH) then
				if Update then
					table.insert(tblX, EventID)
				else
					if not tblUpdates[UID] then tblUpdates[UID] = {} end
					table.insert(tblUpdates[UID], Fact.Description)
				end
			end
		end
	end
	if #tblX > 0 and Update then
		for _, Event in ipairs(tblX) do
			SQL = 'DELETE FROM EventTable WHERE EventID = ' .. Event
			database:execute(SQL)
		end
	end
end

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

function CheckRedundantEvents(database, tblRMI, tblUID, tblUpdates, Update)

	-- check for events in RM not included in standard set

	ProgressBarIncrement('Checking Redundant Events')

	local tblI, tblF = DefineFacts()
	local tblT = {}				-- Event types to be deleted
	local tblX = {}				-- Specific events to be deleted
	local count = 0

	-- get Individual Events

	for _,Fact in ipairs(tblI) do table.insert(tblT, '"' .. Fact.Description .. '"') end
	local EventList = table.concat(tblT, ',')
	local SQL = 'SELECT I.UniqueID UniqueID, E.EventID EventID, F.Abbrev FROM EventTable E ' ..
			'JOIN FactTypeTable F ON E.EventType = F.FactTypeID AND ' ..
			'F.Abbrev NOT IN (' .. EventList .. ') AND E.OwnerType = 0 ' ..
			'JOIN PersonTable I ON E.OwnerID = I.PersonID'
	local ResultSet = database:select(SQL)
	for p in ResultSet:rows() do
		local UID = p.UniqueID
		local EventID = tonumber(p.EventID)|0
		if Update then
			table.insert(tblX, EventID)
		else
			if tblUID[UID].IDfh then
				if not tblUpdates[UID] then tblUpdates[UID] = {} end
				table.insert(tblUpdates[UID], p.Abbrev)
			end
		end
		count = count + 1
		if count % 1000 == 0 then ProgressBarUpdate(count, 'Facts') end
	end

	-- get Family Events

	tblT = {}
	for _, Fact in ipairs(tblF) do table.insert(tblT, '"' .. Fact.Description .. '"') end
	EventList = table.concat(tblT, ',')
	SQL = 'SELECT Fam.FamilyID FamilyID, Fam.FatherID FatherID, Fam.MotherID MotherID, ' ..
			'E.EventID EventID, F.Abbrev FROM EventTable E ' ..
			'JOIN FactTypeTable F ON E.EventType = F.FactTypeID AND F.Abbrev NOT IN (' ..
			EventList .. ') AND E.OwnerType = 1 ' ..
			'JOIN FamilyTable Fam ON E.OwnerID = Fam.FamilyID'
	local ResultSet = database:select(SQL)
	for f in ResultSet:rows() do
		local EventID = tonumber(f.EventID)|0
		local FatherID = tonumber(f.FatherID)|0
		local MotherID = tonumber(f.MotherID)|0
		if Update then
			table.insert(tblX, EventID)
		else
			if FatherID > 0 then				-- father event list
				local UIDf = tblRMI[FatherID].UID
				if tblUID[UIDf].IDfh then
					if not tblUpdates[UIDf] then tblUpdates[UIDf] = {} end
					table.insert(tblUpdates[UIDf], f.Abbrev)
				end
			end
			if MotherID > 0 then				-- mother event list
				local UIDm = tblRMI[MotherID].UID
				if tblUID[UIDm].IDfh then
					if not tblUpdates[UIDm] then tblUpdates[UIDm] = {} end
					table.insert(tblUpdates[UIDm], f.Abbrev)
				end
			end
		end
		count = count + 1
		if count % 1000 == 0 then ProgressBarUpdate(count, 'Facts') end
	end

	-- delete redundant events if updating

	local tblSQL = {}
	while #tblX > 0 and Update do
		table.insert(tblSQL, table.remove(tblX))		-- transfer one value to SQL table
		if #tblX == 0 or #tblSQL > 999 then				-- update this block of values
			database:execute('DELETE FROM EventTable WHERE EventID IN (' .. table.concat(tblSQL, ',') .. ')')
			tblSQL = {}
		end
	end
end

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

function UpdateRMTimeStamps(database, FileName)

	-- reads list of changed individuals and updates RM timestamps accordingly

	ProgressBarIncrement('Updating RM Timestamps')
	local count = 0

	local DataFile = fhGetContextInfo('CI_PROJECT_DATA_FOLDER') .. '\\Plugin Data\\FH RM Ancestry Sync.txt'
	if not fhfu.fileExists(DataFile) then return end

	-- get today's date and convert to RM format (days since 31 Dec 1899)

	local today = os.time()//86400 + 25569
	local S = fhLoadTextFile(DataFile)
	local tblS = {}
	for IDrm in S:gmatch('[^\r\n]+') do
		table.insert(tblS, IDrm)
	end

	local tblSQL = {}
	while #tblS > 0 do
		table.insert(tblSQL, table.remove(tblS))		-- transfer one value to SQL table
		if #tblS == 0 or #tblSQL > 999 then				-- update this block of values
			local SQL = 'UPDATE PersonTable SET UTCModDate = ' .. today .. ' WHERE PersonID IN (' ..
					table.concat(tblSQL, ',') .. ')'
			database:execute(SQL)
			tblSQL = {}
		end
	end
end

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

function UpdateAncestryList(database, FileName, tblUID)

	-- reads list of changed individuals and updates Ancestry Table accordingly

	ProgressBarIncrement('Updating Ancestry changed list')

	local DataFile = fhGetContextInfo('CI_PROJECT_DATA_FOLDER') .. '\\Plugin Data\\FH RM Ancestry Sync.txt'
	local count = 0
	if not fhfu.fileExists(DataFile) then return end
	local S = fhLoadTextFile(DataFile)
	local tblS = {}
	for IDrm in S:gmatch('[^\r\n]+') do
		table.insert(tblS, IDrm)
	end
	local SQL
	local tblSQL = {}
	while #tblS > 0 do
		table.insert(tblSQL, table.remove(tblS))		-- transfer one value to SQL table
		if #tblS == 0 or #tblSQL > 999 then				-- update this block of values
			local SQL = 'UPDATE AncestryTable SET Modified = 1 WHERE rmID IN (' ..
					table.concat(tblSQL, ',') .. ')'
			database:execute(SQL)
			tblSQL = {}
		end
	end
end

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

function MatchEvent(UID, Event, tblEvents)

	-- finds Event in tblEvents, matched on UID (case insensitive)

	if not tblEvents[UID] then return false end

	for E, _ in pairs(tblEvents[UID]) do
		if gblOptions.Case then
			if E == Event then return true end
		else
			if E:lower() == Event:lower() then return true end
		end
	end
end

-- *********************************************************************
-- Compare and Update functions (reporting)
-- *********************************************************************

function CheckIndividuals(tblUID)

	local tblAdd = {}
	local tblDelete = {}

	-- identify missing individuals

	for UID, I in pairs(tblUID) do
		if not I.IDrm then table.insert(tblAdd, I.p) end
		if not I.IDfh then table.insert(tblDelete, I) end
	end

	-- sort into alphabetical order

	tblAdd = SortIndividuals(tblAdd)		-- sort table into name order
	tblDelete = SortRMIndividuals(tblDelete)

	return tblAdd, tblDelete
end

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

function SortIndividuals(tblI)

	-- Sorts an indexed table of Individual pointers into name order

	local tblT = {}

	for _, p in ipairs(tblI) do
		local id = fhGetItemText(p, '~.NAME:SURNAME') .. ':' .. fhGetItemText(p, '~.NAME:GIVEN_ALL') ..
				':' .. fhGetRecordId(p)
		table.insert(tblT, id)
	end
	table.sort(tblT)
	local tblSorted = {}
	for _, id in ipairs(tblT) do
		local RIN = tonumber(id:match('%d+$'))
		local pI = fhNewItemPtr()
		pI:MoveToRecordById('INDI', RIN)
		table.insert(tblSorted, pI:Clone())
	end

	return tblSorted
end

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

function SortRMIndividuals(tblI)

	-- Sorts an indexed table of Individual pointers into name order

	local tblT = {}

	for _, I in ipairs(tblI) do
		local id = I.Surname
		if id == '' then id = ' ' end
		id = id .. ':' .. I.Given .. ':' .. I.IDrm
		table.insert(tblT, id)
	end
	table.sort(tblT)
	local tblSorted = {}
	for _, id in ipairs(tblT) do
		local IDrm = tonumber(id:match('%d+$'))
		for _, I in ipairs(tblI) do
			if IDrm == I.IDrm then
				table.insert(tblSorted, I)
				break
			end
		end
	end
	return tblSorted
end

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

function SortChangedIndividuals(tblUID, tblUpdates)

	-- Sorts table of Individuals with changes via an intemediate table

	local tblT = {}

	for UID, _ in pairs(tblUpdates) do
		local pI = tblUID[UID].p
		local SortKey = fhGetItemText(pI, '~.NAME:SURNAME') .. ',' .. fhGetItemText(pI, '~.NAME:GIVEN_ALL') ..
				',' .. fhGetItemText(pI, '~.BIRT.DATE:YEAR') .. ',' ..
				fhGetItemText(pI, '~.BIRT.DEAT:YEAR') .. ',' .. UID
		table.insert(tblT, SortKey)
	end
	table.sort(tblT)

	-- Match sort keys and add to final sorted table

	local tblSorted = {}
	for _, SortedKey in ipairs(tblT) do
		local UID = SortedKey:match('%x+$')
		local tblC = {}
		tblC.UID = UID
		table.sort(tblUpdates[UID])
		tblC.Facts = tblUpdates[UID]
		table.insert(tblSorted, tblC)
	end
	return tblSorted
end

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

function SortFamilies(tblF)

	-- returns ordered table of omitted families

	local p = fhNewItemPtr()
	local tblT = {}
	local tblSorted = {}

	for RIN, _ in pairs(tblF) do
		p:MoveToRecordById('FAM', RIN)
		local p1, p2 = GetFamilySpouses(p)
		local tblF = {}
		if p1:IsNotNull() then
			table.insert(tblF, fhGetItemText(p1, '~.NAME:SURNAME'))
			table.insert(tblF, fhGetItemText(p1, '~.NAME:GIVEN_ALL'))
		end
		if p2:IsNotNull() then
			table.insert(tblF, fhGetItemText(p2, '~.NAME:SURNAME'))
			table.insert(tblF, fhGetItemText(p2, '~.NAME:GIVEN_ALL'))
		end
		table.insert(tblF, fhGetRecordId(p))
		table.insert(tblT, table.concat(tblF, ':'))
	end
	table.sort(tblT)
	for _, id in ipairs(tblT) do
		local RIN = tonumber(id:match('%d+$'))
		local pF = fhNewItemPtr()
		pF:MoveToRecordById('FAM', RIN)
		table.insert(tblSorted, pF:Clone())
	end
	return tblSorted
end

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

function AddFHRecord(p, rt)

	-- record FH Record as either link or plain text

	if gblOptions.Links then
		rt:AddRecordLink(p)
	else
		rt:AddText(fhGetItemText(p, '~.NAME'))
	end
end

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

function CreateRecordList(tblUID)

	-- generate optional cross-reference table

	local tblFH = {}
	local tblRM = {}
	local tblRMS = {}
	local tblFHID = {}
	local tblRMID = {}

	if gblOptions.Table and gblOptions.TableExists then			-- table generated already
		local msg = 'Cross-reference table has been generated already and will not be duplicated.'
		MessageBox(msg, 'OK', 'WARNING')
		return
	end
	for UID, I in pairs(tblUID) do
		table.insert(tblFH, I.p or '')
		if not I.Given and not I.Surname then
			table.insert(tblRM, '')
		elseif I.Given == '' or I.Surname == '' then
			table.insert(tblRM, I.Given .. I.Surname:upper())
		else
			table.insert(tblRM, I.Given .. ' ' .. I.Surname:upper())
		end
		table.insert(tblRMS, (I.Surname or '') .. ', ' .. (I.Given or ''))
		table.insert(tblFHID, I.IDfh or '')
		table.insert(tblRMID, I.IDrm or '')
	end
	fhOutputResultSetTitles('Match List')
	fhOutputResultSetColumn('Record', 'item', tblFH, #tblFH, 140)
	fhOutputResultSetColumn('FH ID', 'integer', tblFHID, #tblFHID, 40)
	fhOutputResultSetColumn('RM ID', 'integer', tblRMID, #tblRMID, 40)
	fhOutputResultSetColumn('RM Name', 'text', tblRM, #tblRM, 140)
	fhOutputResultSetColumn('', 'text', tblRMS, #tblRMS, 140, 'align_left', 1, true, 'default', 'hide')
	gblOptions.TableExists = true			-- prevents generating a second table
end

-- *********************************************************************
-- GEDCOM export functions
-- *********************************************************************

function ExportGEDCOM()

	local pI = fhNewItemPtr()
	local pF = fhNewItemPtr()
	local tblI = {}				-- excluded individuals
	local tblF = {}				-- excluded families
	local TotalI, TotalF, ExcludedI, ExcludedF = 0, 0, 0, 0
	local count = 0
	local tblOutput = {}
	local tblFactsI, tblFactsF = DefineFacts()

	-- warning message about incompatible export

	if gblOptions.DisableCompat then
		local msg = 'You have selected to disable RootsMagic/Ancestry compatibility for the GEDCOM ' ..
				'export. While this improves compatibility with other applications, the output file ' ..
				'should NOT be used to update a RootsMagic database.\n\n' ..
				'Are you sure that you want to continue with the export?'
		if MessageBox(msg, 'YESNO', 'WARNING', nil, '2') ~= 1 then return end
	end

	if gblOptions.UpdateGEDCOM then
		local msg = 'This partial GEDCOM export should be used only for updating the linked RootsMagic database.'
		if MessageBox(msg, 'OKCANCEL', 'WARNING', nil, '2') ~= 1 then return end
	end

	-- get export file name

	local filedlg = iup.filedlg{dialogtype = 'SAVE', title = 'Export GEDCOM File',
			extfilter = 'GEDCOM files (*.ged)|*.ged|All Files (*.*)|*.*|',
			file = fhGetContextInfo('CI_PROJECT_NAME') .. '.ged',
			directory = fhfu.splitPath(gblOptions.GFile or '').parent, extdefault = 'ged'}
	if gblOptions.UpdateGEDCOM then filedlg.Title = 'Export GEDCOM Update File' end
	filedlg:popup()
	if filedlg.Status == '-1' then return end
	local FileName = filedlg.Value

	-- save file as default next time

	gblOptions.GFile = FileName

	-- Identify excluded individuals and store in table.

	pI:MoveToFirstRecord('INDI')
	while pI:IsNotNull() do
		if not gblOptions.UpdateGEDCOM then
			tblI[fhGetRecordId(pI)] = IsExcluded(pI)
		else
			if not gblOptions.UpdateGEDCOM.I[fhGetRecordId(pI)] then
				tblI[fhGetRecordId(pI)] = true
			end
		end
		TotalI = TotalI + 1
		pI:MoveNext()
	end
	for I, _ in pairs(tblI) do ExcludedI = ExcludedI + 1 end

	-- Identify excluded families and store in table.

	pF:MoveToFirstRecord('FAM')
	while pF:IsNotNull() do
		if not gblOptions.UpdateGEDCOM then
			tblF[fhGetRecordId(pF)] = IsExcluded(pF, true)
		else
			if not (gblOptions.UpdateGEDCOM.F and gblOptions.UpdateGEDCOM.F[fhGetRecordId(pF)]) then
				tblF[fhGetRecordId(pF)] = true
			end
		end
		TotalF = TotalF + 1
		pF:MoveNext()
	end
	for F, _ in pairs(tblF) do ExcludedF = ExcludedF + 1 end

	-- Start progress bar

	ProgressBarStart(TotalI + TotalF - ExcludedI - ExcludedF)
	gblProgBar.Dialog.Title = 'Exporting GEDCOM file...'
	gblProgBar.Action = 'Exporting GEDCOM file'

	-- Write GEDCOM header

	table.insert(tblOutput, '0 HEAD')
	table.insert(tblOutput, '1 SOUR Family Historian')
	table.insert(tblOutput, '1 GEDC')
	table.insert(tblOutput, '2 VERS 5.5')
	table.insert(tblOutput, '2 FORM LINEAGE-LINKED')
	table.insert(tblOutput, '1 CHAR UTF-8')
	table.insert(tblOutput, '1 DEST GED55')

	-- Loop through all individuals, processing all non-excluded entries (turn off name case preference)

	fhOverridePreference('SURNAMES_UPPERCASE', true, false)
	pI:MoveToFirstRecord('INDI')
	while pI:IsNotNull() do
		if not tblI[fhGetRecordId(pI)] then
			local p = fhNewItemPtr()
			local pL = fhNewItemPtr()
			table.insert(tblOutput, '0 @I' .. fhGetRecordId(pI) .. '@ INDI')
			p:MoveTo(pI,'~.NAME')
			if p:IsNull() then
				-- give dummy name, as RM does not process unnamed individuals correctly
				table.insert(tblOutput, '1 NAME ?')
			end
			while p:IsNotNull() do
				if gblOptions.DisableCompat then
					table.insert(tblOutput, '1 NAME ' .. fhGetValueAsText(p))
				else
					local surname = '/' .. fhGetItemText(p, '~:SURNAME') .. '/'
					local given = fhGetItemText(p, '~:GIVEN_ALL')
					if surname ~= '' and given ~= '' then
						table.insert(tblOutput, '1 NAME ' .. given .. ' ' .. surname)
					else
						table.insert(tblOutput, '1 NAME ' .. given .. surname)
					end
				end
				for _, Qualifier in ipairs({'NPFX', 'NSFX', 'NICK'}) do
					local Q = fhGetItemText(p, '~.' .. Qualifier)
					if Q ~= '' then table.insert(tblOutput, '2 ' .. Qualifier .. ' ' .. Q) end
				end
				p:MoveNext('SAME_TAG')
			end

			p:MoveTo(pI,'~.SEX')
			if p:IsNotNull() then table.insert(tblOutput, '1 SEX ' ..
					fhGetItemText(pI, '~.SEX'):sub(1, 1)) end
			for _, Fact in ipairs(tblFactsI) do
				local Tag = Fact.Tag
				local Description = Fact.Description
				p:MoveTo(pI, '~.' .. Tag)
				while p:IsNotNull() do
					if fhGetItemText(p, '~._FLGS.__PRIVATE') ~= 'Y' and
							fhGetItemText(p, '~._FLGS.__REJECTED') ~= 'Y' then
						if Tag:sub(1,6) == '_ATTR-' or Tag:sub(1,5) == 'EVEN-' then		-- custom fact
							table.insert(tblOutput, '1 EVEN ' .. fhGetValueAsText(p))
							table.insert(tblOutput, '2 TYPE ' .. Description)
						elseif Tag == 'CENS' and not gblOptions.DisableCompat then
							table.insert(tblOutput, '1 RESI')		-- Ancestry compatibility
						else
							table.insert(tblOutput, '1 ' .. Tag .. ' ' .. fhGetValueAsText(p))
						end
						local pD = fhGetItemPtr(p,'~.DATE')
						if pD:IsNotNull() then
							table.insert(tblOutput, '2 DATE ' .. GetGEDCOMDate(pD))
						end

						local EventPlace = fhGetItemText(p, '~.PLAC')
						if EventPlace ~= '' then table.insert(tblOutput, '2 PLAC ' .. EventPlace) end
					end
					p:MoveNext('SAME_TAG')
				end
			end
			pL:MoveTo(pI,'~.FAMC')
			while pL:IsNotNull() do
				p = fhGetValueAsLink(pL)
				if not tblF[fhGetRecordId(p)] then		-- exclude private families
					table.insert(tblOutput, '1 FAMC @F' .. fhGetRecordId(p) .. '@')
				end
				pL:MoveNext('SAME_TAG')
			end
			pL:MoveTo(pI,'~.FAMS')
			while pL:IsNotNull() do
				p = fhGetValueAsLink(pL)
				if not tblF[fhGetRecordId(p)] then		-- exclude private individuals
					table.insert(tblOutput, '1 FAMS @F' .. fhGetRecordId(p) .. '@')
				end
				pL:MoveNext('SAME_TAG')
			end
			p:MoveTo(pI,'~._UID')						-- only export first value for RM compatibility
			table.insert(tblOutput, '1 _UID ' .. FormatUID(fhGetValueAsText(p)))
		end

		count = count + 1
		if count % 100 == 0 then
			ProgressBarUpdate(count)
			gblProgBar.bar.value = count
		end
		pI:MoveNext()
	end

	fhOverridePreference('SURNAMES_UPPERCASE', false)

	-- Process non-private families

	pF:MoveToFirstRecord('FAM')

	while pF:IsNotNull() do
		if not tblF[fhGetRecordId(pF)] then
			local p = fhNewItemPtr()
			table.insert(tblOutput, '0 @F' .. fhGetRecordId(pF) .. '@ FAM')
			for _, Fact in ipairs(tblFactsF) do
				local Tag = Fact.Tag
				local Description = Fact.Description
				p:MoveTo(pF, '~.' .. Tag)
				while p:IsNotNull() do
					if fhGetItemText(p, '~._FLGS.__PRIVATE') ~= 'Y' and
							fhGetItemText(p, '~._FLGS.__REJECTED') ~= 'Y' then
						if Tag:sub(1,6) == '_ATTR-' or Tag:sub(1,5) == 'EVEN-' then		-- custom fact
							table.insert(tblOutput, '1 EVEN ' .. fhGetValueAsText(p))
							table.insert(tblOutput, '2 TYPE ' .. Description)
						elseif Tag == 'CENS' then
							table.insert(tblOutput, '1 RESI')		-- Ancestry compatibility
						else
							table.insert(tblOutput, '1 ' .. Tag .. ' ' .. fhGetValueAsText(p))
						end
						local pD = fhGetItemPtr(p,'~.DATE')
						if pD:IsNotNull() then
							table.insert(tblOutput, '2 DATE ' .. GetGEDCOMDate(pD))
						end
						local EventPlace = fhGetItemText(p, '~.PLAC')
						if EventPlace ~= '' then table.insert(tblOutput, '2 PLAC ' .. EventPlace) end
					end
					p:MoveNext('SAME_TAG')
				end
			end
			local pL1, pL2 = GetFamilySpouses(pF)
			if pL1:IsNotNull() then
				table.insert(tblOutput, '1 HUSB @I' .. fhGetRecordId(pL1) .. '@')
			end
			if pL2:IsNotNull() then
				table.insert(tblOutput, '1 WIFE @I' .. fhGetRecordId(pL2) .. '@')
			end
			pL1:MoveTo(pF,'~.CHIL')
			while pL1:IsNotNull() do
				p = fhGetValueAsLink(pL1)
				if not tblI[fhGetRecordId(p)] then		-- exclude private individuals
					table.insert(tblOutput, '1 CHIL @I' .. fhGetRecordId(p) .. '@')
				end
				pL1:MoveNext('SAME_TAG')
			end
		end
		count = count + 1
		if count % 100 == 0 then
			ProgressBarUpdate(count)
			gblProgBar.bar.value = count
			end
		pF:MoveNext()
	end
	table.insert(tblOutput, '0 TRLR')

	-- generate export note

	if gblOptions.GEDCOM then
		GEDCOMreport(tblI, tblF, TotalI, TotalF, ExcludedI, ExcludedF)
	end

	gblProgBar.Dialog:destroy()
	gblProgBar.Dialog = nil

	-- check export is not empty (e.g. by selecting an empty Ancestry Sync list)

	if TotalI == ExcludedI and TotalF == ExcludedF then
		local msg = 'GEDCOM export file is empty, and will not be saved.'
		MessageBox(msg, 'OK', 'ERROR')
		return
	end

	fhSaveTextFile(FileName, table.concat(tblOutput, '\n') .. '\n')

	-- provide confirmation message

	local msg
	if gblOptions.UpdateGEDCOM then
		msg = 'File update completed.\n\n' .. TotalI - ExcludedI .. ' individuals and ' ..
				TotalF - ExcludedF .. ' families written to file.'
	else
		msg = 'File export completed.\n\n' .. TotalI - ExcludedI .. ' individuals and ' ..
				TotalF - ExcludedF .. ' families written to file.'
	end
	if TotalI - ExcludedI == 1 then msg = msg:gsub('individuals', 'individual') end
	if TotalF - ExcludedF == 1 then msg = msg:gsub('families', 'family') end
	if ExcludedI + ExcludedF > 0 then
		msg = msg .. '\n\nExcluded individuals: ' .. ExcludedI
		msg = msg .. '\nExcluded families: ' .. ExcludedF
	end
	MessageBox(msg, 'OK', 'INFORMATION')
end

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

function GetGEDCOMUpdate()

	-- get update file and compare with current file stamps

	local UpdateFile = fhGetContextInfo('CI_PROJECT_DATA_FOLDER') .. '\\Plugin Data\\Ancestry Sync Update.txt'
	local S = fhLoadTextFile(UpdateFile)
	if not S then return end

	local FHold = tonumber(S:match('FH=(%d+)'))
	local RMold = tonumber(S:match('RM=(%d+)'))
	local RMnew = fhfu.getDateModified(gblOptions.File)

	if RMold ~= RMnew or IsUpdated(FHold) then return end

	-- extract is still valid, so proceed

	local Individuals = S:match('I=(%C+)%c') or ''
	local tblI = {}
	for I in Individuals:gmatch('([^,]+)') do
		tblI[tonumber(I)] = true
	end

	local Families = S:match('F=(%C+)%c') or ''
	local tblF = {}
	for F in Families:gmatch('([^,]+)') do
		tblF[tonumber(F)] = true
	end

	return {I = tblI, F = tblF}
end

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

function GenerateGEDCOMUpdate(tblUpdates, tblAdd, tblUID)

	local tblI = {}
	local tblF = {}

	-- add new and updated individuals unconditionally

	for UID, _ in pairs(tblUpdates) do
		table.insert(tblI, tblUID[UID].IDfh)
	end
	for _, I in ipairs(tblAdd) do
		table.insert(tblI, fhGetRecordId(I))
	end

	-- add their families unconditionally, including any spouses

	for UID, _ in pairs(tblUpdates) do
		local ID = tblUID[UID].IDfh
		local pI = fhNewItemPtr()
		local pF = fhNewItemPtr()
		pI:MoveToRecordById('INDI', ID)
		pF:MoveTo(pI, '~.FAMS')
		while pF:IsNotNull() do
			local pL = fhGetValueAsLink(pF)
			table.insert(tblF, fhGetRecordId(pL))
			local pS = fhNewItemPtr()
			pS:MoveTo(pL, '~.~SPOU[1]>')
			if fhGetRecordId(pS) ~= ID then
				table.insert(tblI, fhGetRecordId(pS))
			else
				pS:MoveTo(pL, '~.~SPOU[2]>')
				table.insert(tblI, fhGetRecordId(pS))
			end
			pF:MoveNext('SAME_TAG')
		end
	end

	-- expand tables to include hooks to link with existing family in RM merge

	for _, I in ipairs(tblAdd) do
		local pFAMS = fhGetItemPtr(I, '~.FAMS')
		while pFAMS:IsNotNull() do
			local pF = fhGetValueAsLink(pFAMS)
			if not IsExcluded(pF, true) then
				table.insert(tblF, fhGetRecordId(pF))
			end
			local pH, pW = GetFamilySpouses(pF)
			if pH:IsNotNull() and not pH:IsSame(I) and not IsExcluded(pH) then		-- spouse in scope
				table.insert(tblI, fhGetRecordId(pH))
			elseif pW:IsNotNull() and not pW:IsSame(I) and not IsExcluded(pW) then	-- spouse in scope
				table.insert(tblI, fhGetRecordId(pW))
			end

			local pCHIL = fhGetItemPtr(pF, '~.CHIL')
			while pCHIL:IsNotNull() do
				local pC = fhGetValueAsLink(pCHIL)
				if not IsExcluded(pC) then table.insert(tblI, fhGetRecordId(pC)) end
				pCHIL:MoveNext('SAME_TAG')
			end
			pFAMS:MoveNext('SAME_TAG')
		end
		local pFAMC = fhGetItemPtr(I, '~.FAMC')
		while pFAMC:IsNotNull() do
			local pF = fhGetValueAsLink(pFAMC)
			if not IsExcluded(pF, true) then
				table.insert(tblF, fhGetRecordId(pF))
			end
			local pH, pW = GetFamilySpouses(pF)
			if pH:IsNotNull() and not IsExcluded(pH) then table.insert(tblI, fhGetRecordId(pH)) end
			if pW:IsNotNull() and not IsExcluded(pW) then table.insert(tblI, fhGetRecordId(pW)) end
			pFAMC:MoveNext('SAME_TAG')
		end
	end

	-- save timestamps and lists of records for extract

	local UpdateFile = fhGetContextInfo('CI_PROJECT_DATA_FOLDER') .. '\\Plugin Data\\Ancestry Sync Update.txt'
	local S = 'FH=' .. os.time() .. '\n' ..
			'RM=' .. fhfu.getDateModified(gblOptions.File) .. '\n' ..
			'I=' .. table.concat(tblI, ',') .. '\nF=' .. table.concat(tblF, ',') .. '\n'
	fhSaveTextFile(UpdateFile, S)
end

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

function GEDCOMreport(tblI, tblF, TotalI, TotalF, PrivateI, PrivateF)

	-- create Research Note to output result of GEDCOM export

	local rt = fhNewRichText()

	rt:AddText('Title:\tAncestry Sync – GEDCOM Export\n')
	rt:AddText('Type:\tAncestry Sync\n')
	rt:AddText('Date:\t' .. os.date('%d' .. ' ' .. '%b' .. ' ' .. '%Y') .. '\n')
	rt:AddText('Status:\topen\n\n')
	rt:AddText('')
	rt:AddText('FH Project: | ' .. fhGetContextInfo('CI_PROJECT_NAME') .. ' (' ..
			fhGetContextInfo('CI_PROJECT_FILE'):gsub('\\', '\\\\') .. '')
	rt:AddText('GEDCOM File: | ' .. gblOptions.GFile:gsub('\\', '\\\\') .. '')
	rt:AddText('\n\n')
	rt:AddText('This Research Note summarises the result of the GEDCOM export from the ' ..
			'Ancestry Synchronization plugin.\n\n')
	rt:AddText('')
	rt:AddText(' | Total | Exported | Excluded')
	rt:AddText('Individuals: | ' .. TotalI .. ' | ' .. TotalI - PrivateI .. '| ' .. PrivateI .. '')
	rt:AddText('Families: | ' .. TotalF .. ' | ' .. TotalF - PrivateF .. '| ' .. PrivateF .. '')
	rt:AddText('\n')

	if PrivateI > 0 and not gblOptions.UpdateGEDCOM then
		rt:AddText('\nIndividuals excluded from export:\n\n')

		-- copy omitted individuals to new table for easier sorting

		local tblT = {}
		for RIN, _ in pairs(tblI) do
			local pI = fhNewItemPtr()
			pI:MoveToRecordById('INDI', RIN)
			table.insert(tblT, pI:Clone())
		end
		tblT = SortIndividuals(tblT)

		for _, pI in ipairs(tblT) do
			AddFHRecord(pI, rt)
			rt:AddText(' (' .. IsExcluded(pI) .. ')\n')
		end
	end

	if PrivateF > 0 and not gblOptions.UpdateGEDCOM then
		rt:AddText('\nFamilies excluded from export:\n\n')
		tblF = SortFamilies(tblF)
		for _, pF in ipairs(tblF) do
			local p1, p2 = GetFamilySpouses(pF)
			if p1:IsNotNull() then
				AddFHRecord(p1, rt)
				if IsExcluded(p1) then rt:AddText(' (' .. (IsExcluded(p1) or '') .. ')') end
			else
				rt:AddText('Unknown')
			end
			rt:AddText(' & ')
			if p2:IsNotNull() then
				AddFHRecord(p2, rt)
				if IsExcluded(p2) then rt:AddText(' (' .. (IsExcluded(p2) or '') .. ')') end
			else
				rt:AddText('Unknown')
			end
			rt:AddText('\n')
		end
	end

	if PrivateI + PrivateF == 0 then
		rt:AddText('\nAll Individuals and Families exported to GEDCOM file.')
	end

	-- create Research Note from assembled content

	local pRN = fhCreateItem('_RNOT')
	local pT = fhGetItemPtr(pRN, '~.TEXT')
	fhSetValueAsRichText(pT, rt)
	fhUpdateDisplay()
end

-- *********************************************************************
-- Ancestry audit functions
-- *********************************************************************

function AuditAncestryGEDCOM()

	-- lists defined errors in Ancestry GEDCOM export file

	-- get Ancestry export file name

	local filedlg = iup.filedlg{dialogtype = 'OPEN', title = 'Select Ancestry Export GEDCOM File',
			directory = fhfu.splitPath(gblOptions.AFile or '').parent,
			extfilter = 'GEDCOM files (*.ged)|*.ged|All Files (*.*)|*.*|'}

	filedlg:popup()

	if filedlg.Status == '-1' then return end

	local FileName = filedlg.Value
	gblOptions.AFile = FileName
	local FileContents = fhLoadTextFile(FileName):gsub('\r\n', '\n')		-- simple Unix endings

	-- is this the correct file?

	local _, RIN, _ = FileContents:match('(%c)3 RIN (%d+)(%c)')
	if RIN ~= gblOptions.TreeID then
		MessageBox('Incorrect GEDCOM file.', 'OK', 'ERROR')
		return
	end

	-- correct known errors in file format

	local NewFileContents = UpdateAncestryGEDCOM(FileContents)

	-- modify Submitter record to confirm correct file when loading as new project

	local S1 = '0 @SUBM1@ SUBM\n'
	local S2 = '1 NAME Ancestry.com Member Trees Submitter\n'
	local S3 = '1 NOTE FH Ancestry Sync Plugin\n'
	if not NewFileContents:match(S1 .. S2 .. S3) then
		NewFileContents = NewFileContents:gsub(S1 .. S2, S1 .. S2 .. S3, 1)
	end
	if NewFileContents ~= FileContents then
		fhSaveTextFile(FileName, NewFileContents)
	end

	-- process data

	local anID
	local tblANCI = {}					-- table for all Ancestry individuals

	for Line in FileContents:gmatch('[^\r\n]+') do
		if Line:match('^0') and anID then				-- end of individual
			anID = nil
		end
		if Line:match('^0 @I%d+@ INDI$') then				-- start of new individual
			anID = Line:match('^0 @I(%d+)@ INDI$')
			tblANCI[anID] = {}
			tblANCI[anID].Sex = {}
		end
		if Line:match('^1 SEX (%u)$') and anID then
			table.insert(tblANCI[anID].Sex, Line:match('^1 SEX (%u)$'))
		end
		if Line:match('^1 MARR') and anID then
			tblANCI[anID].MARR = true
		end
		if Line:match('^1 DIV') and anID then
			tblANCI[anID].DIV = true
		end
	end

	-- get table of Ancestry IDs and names

	local tblANC = GetAncestryLinks()

	-- rearrange to link Ancestry ID with name

	local tblANCid = {}

	for I, tblANid in pairs(tblANC) do
		for _, anID in ipairs(tblANid) do
			tblANCid[anID] = I
		end
	end

	-- count how many records affected by extra SEX, MARR, or DIV tags

	local CountS, CountM, CountD = 0, 0, 0
	for anID, issue in pairs(tblANCI) do
		if #issue.Sex > 1 then CountS = CountS + 1 end
		if issue.MARR then CountM = CountM + 1 end
		if issue.DIV then CountD = CountD + 1 end
	end

	-- identify individuals with multiple genders or family facts

	local rt = fhNewRichText()

	rt:AddText('Title:\tAncestry Audit - GEDCOM Export Issues\n')
	rt:AddText('Type:\tAncestry Sync\n')
	rt:AddText('Date:\t' .. os.date('%d' .. ' ' .. '%b' .. ' ' .. '%Y') .. '\n')
	rt:AddText('Status:\topen\n\n')
	rt:AddText('')
	rt:AddText('FH Project: | ' .. fhGetContextInfo('CI_PROJECT_NAME') .. ' (' ..
			fhGetContextInfo('CI_PROJECT_FILE'):gsub('\\', '\\\\') .. ')')
	rt:AddText('RM File: | ' .. gblOptions.File:gsub('\\', '\\\\') .. '')
	rt:AddText('Ancestry Tree: | ')
	rt:AddText('GEDCOM File: | ' .. gblOptions.AFile:gsub('\\', '\\\\') .. '')
	rt:AddText('\n\n')
	rt:AddText('This Research Note lists Individuals in the linked Ancestry tree where errors in the ' ..
			'GEDCOM file have been noted (usually created as an artefact of TreeShare). Click on ' ..
			'the links to edit the individual directly within Ancestry.\n')
	if CountS > 0 then
		rt:AddText('\nMultiple Gender Facts - correct in Ancestry, then repeat the GEDCOM export\n\n')
		for anID, issues in pairs(tblANCI) do
			if #issues.Sex > 1 then
				rt:AddText('\n')
			end
		end
	end
	if CountM > 0 then
		rt:AddText("\nMarriage Tag associated with Individual - review Individual's marriages in " ..
				'Ancestry, then repeat the GEDCOM export if changes made\n\n')
		for anID, issues in pairs(tblANCI) do
			if issues.MARR then
				rt:AddText('\n')
			end
		end
	end
	if CountD > 0 then
		rt:AddText("\nDivorce Tag associated with Individual - review Individual's divorce in " ..
				'Ancestry, then repeat the GEDCOM export if changes made\n\n')
		for anID, issues in pairs(tblANCI) do
			if issues.DIV then
				rt:AddText('\n')
			end
		end
	end
	if CountS + CountM + CountD == 0 then
		rt:AddText('\nNo GEDCOM issues detected in linked Ancestry Tree.')
	end
	local pRN = fhCreateItem('_RNOT')
	local pT = fhGetItemPtr(pRN, '~.TEXT')
	fhSetValueAsRichText(pT, rt)
	fhUpdateDisplay()
	local endmsg = 'Ancestry check completed and reported as new Research Note.'
	if CountS + CountM + CountD == 0 then
		endmsg = endmsg .. '\n\nNo multiple Gender Facts or extra Marriage or Divorce tags detected in ' ..
				'linked Ancestry Tree.\n\nYou can now import the Ancestry GEDCOM file into Family ' ..
				'Historian as a new Project to complete the audit.'
	end
	MessageBox(endmsg, 'OK', 'INFORMATION')
end

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

function AuditAncestryTree()

	-- get data from linked RM file

	local tblAncestry = GetAncestryLinks()

	-- compile report

	local rt = fhNewRichText()
	rt:AddText('Title:\tAncestry Audit - Individuals\n')
	rt:AddText('Type:\tAncestry Sync\n')
	rt:AddText('Date:\t' .. os.date('%d' .. ' ' .. '%b' .. ' ' .. '%Y') .. '\n')
	rt:AddText('Status:\topen\n\n')
	rt:AddText('')
	rt:AddText('FH Project: | ' .. fhGetContextInfo('CI_PROJECT_NAME') .. ' (' ..
			fhGetContextInfo('CI_PROJECT_FILE'):gsub('\\', '\\\\') .. ')')
	rt:AddText('RM File: | ' .. gblOptions.File:gsub('\\', '\\\\') .. '')
	if gblOptions.TreeID then
		rt:AddText('Ancestry Tree: | ')
	end
	rt:AddText('\n\n')
	rt:AddText('This Research Note lists Individuals in the linked RootsMagic file that are either ' ..
			'missing from the associated Ancestry tree or have been duplicated due to limitations in ' ..
			'the TreeShare process.\n\n')

	local Missing, Duplicate

	for _, anc in pairs(tblAncestry) do
		if #anc == 0 then Missing = true end
		if #anc > 1 then Duplicate = true end
	end

	if Missing then
		rt:AddText('Missing Individuals - re-run TreeShare to upload to Ancestry\n\n')
		for rm, anc in pairs(tblAncestry) do
			if #anc == 0 then rt:AddText(rm .. '\n') end
		end
		rt:AddText('\n')
	end

	if Duplicate then
		rt:AddText('Duplicated Individuals - click on either hyperlink and merge the two ' ..
				'records on Ancestry, then re-run TreeShare to update RootsMagic\n\n')
		for rm, anc in pairs(tblAncestry) do
			if #anc > 1 then
				rt:AddText(rm)
				for n, anID in ipairs(anc) do
					rt:AddText(' - ')
				end
				rt:AddText('\n')
			end
		end
	end

	if not Missing and not Duplicate then
		rt:AddText('No missing or duplicate individuals detected in linked Ancestry tree.')
	end

	local pRN = fhCreateItem('_RNOT')
	local pT = fhGetItemPtr(pRN, '~.TEXT')
	fhSetValueAsRichText(pT, rt)
	fhUpdateDisplay()
	local endmsg = 'Ancestry check completed and reported as new Research Note.'
	if not Missing and not Duplicate then
		endmsg = endmsg .. '\n\nNo missing or duplicate individuals detected in linked Ancestry tree.'
	end
	MessageBox(endmsg, 'OK', 'INFORMATION')
end

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

function GetAncestryLinks()

	-- get table of names and Ancestry IDs from RM file

	local database, SQLfile = OpenDatabase(gblOptions.File)
	if not database then return end

	local tblANC = {}
	local SQL = 'SELECT UniqueID, Surname, Given, PersonID, anID ancID FROM PersonTable P ' ..
			'JOIN NameTable N ON N.OwnerID = P.PersonID and N.IsPrimary = 1 ' ..
			'LEFT JOIN AncestryTable A ON P.PersonID = A.rmID'
	local ResultSet = database:select(SQL)
	for I in ResultSet:rows() do
		local PersonID = I.PersonID|0
		local Record = I.Given .. ' ' .. I.Surname .. ' (RM' .. PersonID .. ')'
		if not tblANC[Record] then tblANC[Record] = {} end
		if I.ancID then
			local ID = I.ancID:match('^%d+')
			table.insert(tblANC[Record], ID)
		end
	end
	database:close()
	collectgarbage()
	fhfu.deleteFile(SQLfile)
	return tblANC
end

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

function IsBareTree()

	-- returns true if Submitter is Ancestry member tree

	local p = fhNewItemPtr()
	p:MoveToFirstRecord('SUBM')
	local C1 = (fhGetItemText(p, '~.NAME') == 'Ancestry.com Member Trees Submitter')
	local C2 = C1 and (fhGetItemText(p, '~.NOTE2') == 'FH Ancestry Sync Plugin')
	return C1, C2
end

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

function UpdateAncestryGEDCOM(FileContents)

	-- correct Custom ID (user-defined event in Ancestry)

	FileContents = FileContents:gsub('1 EVEN\n2 TYPE Ref #\n2 NOTE', '1 REFN')

	-- correct double dates

	local tblS = {}
	for Line in FileContents:gmatch('[^\r\n]+') do
		if Line:match('%a%a%a%s%d%d%d%d%/%d$') then		-- double dates
			local y = tonumber(Line:sub(-4, -3))
			if y == 99 then
				Line = Line:sub(1, Line:len()-1) .. '00'
			else
				Line = Line:sub(1, Line:len()-1) .. (y+1)
			end
		end
		if Line:match('^1 UID %w+$') then				-- UniqueID
			Line = '1 _UID ' .. Line:sub(7,14) .. '-' .. Line:sub(15,18) .. '-' ..
					Line:sub(19,22) .. '-' .. Line:sub(23,26) .. '-' .. Line:sub(27)
		end
		table.insert(tblS, Line)
	end
	return table.concat(tblS, '\n') .. '\n'
end

-- *********************************************************************
-- General admin functions
-- *********************************************************************

function CheckUIDs()

	-- check that all Individuals have a UID assigned

	local pI = fhNewItemPtr()
	local tblI = {}
	local Count = 0

	-- read UID values into table

	pI:MoveToFirstRecord('INDI')
	while pI:IsNotNull() do
		local pUID = fhGetItemPtr(pI, '~._UID')
		local tblUID = {}
		while pUID:IsNotNull() do
			table.insert(tblUID, pUID:Clone())
			pUID:MoveNext('SAME_TAG')
		end
		tblI[fhGetRecordId(pI)] = tblUID
		pI:MoveNext()
	end

	-- check for missing or multiple values

	for _, tblUID in pairs(tblI) do
		if #tblUID == 0 then return end					-- no UID defined
		if #tblUID > 1 then Count = Count + 1 end		-- multiple UID's defined
	end

	if Count > 0 then
		local msg = 'This project has ' .. Count .. ' individuals with multiple UniqueID values. ' ..
				'Only the first value is processed by this plugin, which may cause problems when ' ..
				'comparing the project with the linked RootsMagic file.'
		if Count == 1 then msg = msg:gsub('individuals', 'individual') end
		MessageBox(msg, 'OK', 'WARNING')
	end

	return true				-- no missing UID values
end

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

function FormatRMDate(S)

	-- converts RM proprietory date format to FH date

	local Date = ''
	local tblMonths = {'January', 'February', 'March', 'April', 'May', 'June', 'July',
			'August', 'September', 'October', 'November', 'December'}

	if S == '.' then return '' end				-- null date
	if S:sub(1,1) == 'T' then					-- date phrase
		S = '"' .. S:sub(2) .. '"'				-- convert to FH format
		return S
	end
	if S:sub(13,13) == 'C' or S:sub(13,13) == 'A' then Date = Date .. 'circa '
	elseif S:sub(2,2) == 'A' then Date = Date .. 'after '
	elseif S:sub(2,2) == 'B' then Date = Date .. 'before '
	elseif S:sub(2,2) == 'F' then Date = Date .. 'from '
	elseif S:sub(2,2) == 'S' then Date = Date .. 'from '
	elseif S:sub(2,2) == 'R' then Date = Date .. 'between ' end

	if S:sub(10,10) ~= '0' then Date = Date .. S:sub(10,10) end
	if S:sub(10,11) ~= '00' then Date = Date .. S:sub(11,11) .. ' ' end
	if S:sub(8,9) ~= '00' then Date = Date .. tblMonths[tonumber(S:sub(8,9))] .. ' ' end
	if S:sub(4,7) ~= '0000' then Date = Date .. S:sub(4,7) end
	if S:sub(12,12) == '/' then
		Date = Date .. '/' .. tostring(tonumber(S:sub(4,7)) + 1):sub(3,4) end
	if S:sub(2,2) == 'S' then Date = Date .. ' to '
	elseif S:sub(2,2) == 'R' then Date = Date .. ' and ' end

	if S:sub(21,21) ~= '0' then Date = Date .. S:sub(21,21) end
	if S:sub(21,22) ~= '00' then Date = Date .. S:sub(22,22) .. ' ' end
	if S:sub(19,20) ~= '00' then Date = Date .. tblMonths[tonumber(S:sub(19,20))] .. ' ' end
	if S:sub(15,18) ~= '0000' then Date = Date .. S:sub(15,18) end
	if S:sub(23,23) == '/' and S:sub(15,22) ~= '00000000' then	-- slightly different format in RM7 & RM8
		Date = Date .. '/' .. tostring(tonumber(S:sub(15,18)) + 1):sub(3,4) end

	if S:sub(13,13) == 'L' then Date = Date .. ' (calculated)'
	elseif S:sub(13,13) == 'E' then Date = Date .. ' (estimated)' end

	-- convert to Date object and back to avoid formatting errors (circa, Q dates options)

	local dtRM = fhNewDate()
	dtRM:SetValueAsText(Date, true)
	return dtRM:GetValueAsText()
end

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

function FormatUID(UID)

	-- stores and exports all UID values in GEDCOM L format (32+4)

	UID = UID:gsub('-', '')

	if not tonumber('0x' .. UID) then return UID end			-- not hexadecimal
	if UID:len() == 36 then return UID end						-- already in this format
	if UID:len() ~= 32 then return UID end						-- invalid length

	-- calculate checksum using published method

	local a = 0
	local b = 0

	for i = 1, 31, 2 do
		local byte = UID:sub(i, i + 1)
		local value = tonumber('0x' .. byte)
		a = a + value
		b = b + a
	end

	local cs1 = string.format('%x', a)
	local cs2 = string.format('%x', b)
	local checksum = cs1:sub(-2) .. cs2:sub(-2)

	-- use same case for checksum as for main string

	if UID:upper() == UID then checksum = checksum:upper() end
	return UID .. checksum
end

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

function GetFamilySpouses(pF)

	-- convert FH assymetric storage of single sex couples to symmetric

	local pH1 = fhGetItemPtr(pF, '~.HUSB[1]')
	local pH2 = fhGetItemPtr(pF, '~.HUSB[2]')
	local pW1 = fhGetItemPtr(pF, '~.WIFE[1]')
	local pW2 = fhGetItemPtr(pF, '~.WIFE[2]')

	if pH2:IsNull() and pW2:IsNull() then
		return fhGetValueAsLink(pH1), fhGetValueAsLink(pW1)
	elseif pH2:IsNotNull() then
		return fhGetValueAsLink(pH1), fhGetValueAsLink(pH2)
	elseif pW2:IsNotNull() then
		return fhGetValueAsLink(pW2), fhGetValueAsLink(pW1)
	end
end

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

function GetGEDCOMDate(pD)

	-- converts FH date to GEDCOM format

	local dtD = fhGetValueAsDate(pD)
	local S = dtD:GetDisplayText('COMPACT')

	-- strip out quotes from date phrases

	if S:match('^%"') and S:match('%"$') then
		return S:sub(2,-2)
	end

	S = S:gsub('c%.', 'ABT')
	S = S:gsub('btw', 'BET')
	S = S:gsub('frm', 'FROM')
	local I = S:find('(est)')
	if I then S = 'EST ' .. S:sub(1, I - 3) end
	I = S:find('(cal)')
	if I then S = 'CAL ' .. S:sub(1, I - 3) end
	I = S:find('Q1')
	if I then S = 'BET JAN ' .. S:sub(4) .. ' AND MAR ' .. S:sub(4) end
	I = S:find('Q2')
	if I then S = 'BET APR ' .. S:sub(4) .. ' AND JUN ' .. S:sub(4) end
	I = S:find('Q3')
	if I then S = 'BET JUL ' .. S:sub(4) .. ' AND SEP ' .. S:sub(4) end
	I = S:find('Q4')
	if I then S = 'BET OCT ' .. S:sub(4) .. ' AND DEC ' .. S:sub(4) end
	return S:upper()
end

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

function GetRegistryKey(key)

	local sh = luacom.CreateObject 'WScript.Shell'
	local ans
	if pcall(function () ans = sh:RegRead(key) end) then
		return ans
	else
		return nil,true
	end
end

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

function GetUID(pI)

	local UID = fhGetItemText(pI, '~._UID')
	return FormatUID(UID)
end

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

function IsUpdated(RefTime)

	-- returns true if any Individual or Family records have been updated since RefTime

	local p = fhNewItemPtr()
	for _, RecType in ipairs({'INDI', 'FAM'}) do
		p:MoveToFirstRecord(RecType)
		while p:IsNotNull() do
			local D, H, M = fhCallBuiltInFunction('LastUpdated', p)
			if not D:IsNull() then
				local T = os.time{year=D:GetYear(), month=D:GetMonth(), day=D:GetDay(), hour=H, min=M}
				if T > RefTime then return true end
			end
			p:MoveNext()
		end
	end
end

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

function MessageBox(Message, Buttons, Icon, Title, Default)

	-- replaces built-in function with custom version containing more options

	-- set message

	local msgdlg = iup.messagedlg{value = Message, buttons = Buttons, dialogtype = Icon,
			title = Title or 'Ancestry Synchronization', buttondefault = Default}

	-- display message box and return selection

	msgdlg:popup()
	return tonumber(msgdlg.ButtonResponse)
end

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

function ProgressBarIncrement(Title)

	-- increment progress bar

	gblProgBar.Action = Title
	gblProgBar.Dialog.title = Title .. '...'
	gblProgBar.bar.Value = gblProgBar.bar.Value + 1

	-- write log

	local step = tonumber(gblProgBar.bar.Value)
	local log = step .. ',' .. Title .. ',' .. os.time() - gblProgBar.Start ..
			',' .. string.format('%3.1f', collectgarbage('count')/1024)
	table.insert(gblProgBar.Log, log)
end

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

function ProgressBarStart(Max)

	-- create and display a simple progress bar, and store in a global table

	gblProgBar = {}
	gblProgBar.bar = iup.progressbar{max = Max; rastersize = '400x30'}
	gblProgBar.vbox = iup.vbox{gblProgBar.bar; gap = 20, alignment = 'acenter', margin = '5x15'}
	gblProgBar.Dialog = iup.dialog{gblProgBar.vbox; title = '', dialogframe = 'Yes', border = 'Yes',
			menubox = 'No'}
	gblProgBar.Start = os.time()
	gblProgBar.Response = os.time()
	gblProgBar.Log = {}
	table.insert(gblProgBar.Log, 'Step,Action,Time at start/s,Plugin RAM/MB')
	gblProgBar.Dialog:showxy(iup.CENTER, iup.CENTER)	-- Put up Progress Display
end

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

function ProgressBarUpdate(count, descriptor)

	-- update progress bar with ongoing count

	gblProgBar.Dialog.title = gblProgBar.Action .. ' (' .. count .. ' ' ..
			(descriptor or 'Records') .. ')...'
	local now = os.time()
	if now - gblProgBar.Response > 3 then
		fhExhibitResponsiveness()
		gblProgBar.Response = now
	end
end

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

main()

Source:Ancestry-Synchronization-9.fh_lua