Backup and Restore Family Historian Settings via Windows.fh_lua

--[[
@Title:       Backup and Restore Family Historian Settings via Windows
@Type:        Standard
@Author:      Mark Draper
@Version:     2.1.4
@LastUpdated: 18 Aug 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: Backup and restore Family Historian custom settings. This plugin does all the copying
              directly within Windows rather than in the plugin. This gives much faster and more convenient
              operation, as well as providing the option to implement more advanced features such as
              single-click rapid backup or automated backup.
]]

--[[

	Version 1.0 (May 2022)
		Initial Store version
	Version 1.1 (May 2022)
		Emulator-compatible version
		Xcopy for emulator copy
		Clearer label for restore option
		Fixed bug with WOW64Node in Advanced mode
	Version 1.1.1 (May 2022)
		Xcopy options tweaked
		Does not overwrite its own settings or temporary batch files
	Version 1.1.2 (May 2022)
		ASCII check modified to permit FH5
	Version 1.2 (Jan 2023)
		Checks only one instance running before restoring to prevent potential data loss
		Various minor code tidies and rationalisations
	Version 1.2.1 (Feb 2023)
		Corrects instance count when running under FH6 (Fh vs fh!)
	Version 1.3 (Sep 2024)
		Corrects restore operation in emulator and new emulator check
		Now fully Unicode compatible (FSO rather than lfs)
		Moves temporary script files to tmp folder and improved ini file format
		Now edits Registry file in FH6 as well so fully functional
		Improved user dialogs and menu button labels
		Various code optimisations and minor tweaks
	Version 2.0 (Dec 2024)
		Simplified restore without overwrite option (always restores in full)
		Simplified main menu reflecting this change
		Enhanced menu pop-up prompts
		Supports multi-monitor extended desktops
		Checks existing backups for completeness before updating or restoring
		Various corrections and enhancements to Unicode support
		Backup script modified to call 64-bit Registry key specifically if running in Advanced mode
		(avoids potential issues with missing backup file when called directly from Windows)
	Version 2.1 (May 2025)
		Modified 'three test' emulator detection
		CPU architecture detection
		ARM64 warning
	Version 2.1.1 (Jul 2025)
		Corrected ARM64 warning
	Version 2.1.4 (Aug 2025)
		Generalised ARM64 warning
		Minor tidy-up of old code
]]

fhInitialise(5,0,0,'save_required')

require 'luacom'
require('iuplua')

if fhGetAppVersion() > 6 then
	fh = require('fhUtils')
	fh.setIupDefaults()
else
	iup.SetGlobal('CUSTOMQUITMESSAGE','YES')
	iup.SetGlobal('UTF8MODE','YES')
end

iup.SetGlobal('UTF8MODE_FILE','YES')
FSO = luacom.CreateObject('Scripting.FileSystemObject')

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

function main()

	local MainForm

	if not GetOptions() then return end					-- get plugin options

	-- set main window static controls and options

	local Heading = 'This plugin backs up and restores Family Historian settings using Windows commands.\n' ..
			'All languages and Windows localisations are supported automatically.\n\n'
	if not gblOptions.Emulator then
		Heading = Heading .. 'Technically-minded users may save the backup configuration as Windows batch ' ..
				'files to\nincorporate into their own backup process (including fully automated backups).'
	else
		Heading = Heading .. 'Advanced mode is not applicable when running in an emulator.'
	end
	local lblTitle = iup.label{title = Heading, alignment = 'ALEFT', padding = '10x20'}

	-- folder controls and options

	local lblF = iup.label{title = gblOptions.Path or '',expand = 'HORIZONTAL'}
	local btnF = iup.button{title = 'Select', padding = '10x3',
			tip = 'Destination folder where scripts and backup data are written.',
			action = function(self) SelectFolder(MainForm) UpdateMenu() end}
	local chkB = iup.toggle{title = 'Write to dated subfolder', expand = 'YES', active = 'NO',
			tip = 'Write backup to dated subfolder.'}
	local fraF = iup.frame{iup.vbox{lblF, chkB, btnF; alignment = 'ACENTER'}; title = 'Backup folder'}

	-- general controls and options (use similar containers for all frames for consistent appearance)

	local chkM1 = iup.toggle{title = 'Normal', expand = 'HORIZONTAL',
			tip = 'Run backup or restore immediately.'}
	local chkM2 = iup.toggle{title = 'Advanced', expand = 'HORIZONTAL',
			tip = 'Save the script files without running the backup or restore.'}
	if gblOptions.Emulator then chkM2.Active = 'NO' end
	local hboxM = iup.hbox{chkM1, chkM2}
	local radM = iup.radio{hboxM}
	local fraM = iup.frame{iup.hbox{radM}, title = 'Operating mode'}

	-- button display

	local btnB = iup.button{active = 'NO',
			action = function(self) Backup() MainForm.BringFront = 'YES' end}
	local btnR = iup.button{title = 'Restore Script', padding = '10x3',
			action = function(self)
				local i = Restore(MainForm)
				MainForm.BringFront = 'YES'				
				if i == -1 then return iup.CLOSE end
			end}
	local btnH = iup.button{title = 'Help', tip = 'Display plugin help page.\n' ..
			'(hold down either Shift key to display\nSystem Information)', 
			action = function(self) GetHelp(MainForm) end}
	local btnX = iup.button{title = 'Close', tip = 'Close plugin.', 
			action = function(self) return iup.CLOSE end}
	local hButtons = iup.hbox{btnB, btnR, btnH, btnX; cgap = 20, cmargin = 'x15', normalizesize = 'BOTH'}

	-- enhanced tool tips

	btnB.TipBalloonTitleIcon = '1'
	btnF.TipBalloonTitle = 'Select Folder'
	btnF.TipBalloonTitleIcon = '1'
	chkB.TipBalloonTitle = 'Dated Subfolder'
	chkB.TipBalloonTitleIcon = '1'
	chkM1.TipBalloonTitle = 'Normal Mode'
	chkM1.TipBalloonTitleIcon = '2'
	chkM2.TipBalloonTitle = 'Advanced Mode'
	chkM2.TipBalloonTitleIcon = '1'
	btnH.TipBalloonTitle = 'Plugin Help'
	btnH.TipBalloonTitleIcon = '1'
	btnX.TipBalloonTitle = 'Close Plugin'
	btnX.TipBalloonTitleIcon = '3'

	-- menu update function

	function UpdateMenu()

		if gblOptions.Path then
			lblF.Title = gblOptions.Path
			btnB.Active = 'YES'
			chkB.Active = 'YES'
		end
		if gblOptions.Dated then
			chkB.Value = 'YES'
		else
			chkB.Value = 'NO'
		end
		if gblOptions.Advanced then
			chkM2.Value = 'ON'
			btnB.Title = 'Backup Script'
			btnR.Title = 'Restore Script'
			btnB.Tip = 'Save backup batch file to defined backup folder.'
			btnB.TipBalloonTitle = 'Save Backup Script'
			btnR.Tip = 'Save restore batch file to defined backup folder.'
			btnR.TipBalloonTitle = 'Save Restore Script'
			btnR.TipBalloonTitleIcon = '1'
		else
			chkM1.Value = 'ON'
			btnB.Title = 'Backup'
			btnB.Size = btnR.Size
			btnB.Tip = 'Run backup now using defined options.'
			btnB.TipBalloonTitle = 'Run Backup'
			btnR.Title = 'Restore'
			btnR.Size = btnB.Size
			btnR.Tip = 'Run restore now using defined options.'
			btnR.TipBalloonTitle = 'Run Restore'
			btnR.TipBalloonTitleIcon = '2'
		end
	end

	-- callback actions

	function chkM2:valuechanged_cb()
		gblOptions.Advanced = (chkM2.Value == 'ON')
		UpdateMenu()
	end

	function chkB:valuechanged_cb()
		gblOptions.Dated = (chkB.Value == 'ON')
	end

	-- assemble form

	local vForm = iup.vbox{lblTitle, fraM, fraF, hButtons;
			alignment = 'ACENTER', cgap = 5, cmargin = '10x3'}

	MainForm = iup.dialog{vForm; minbox='NO', maxbox='NO',
			title='Backup and Restore Family Historian Settings via Windows (2.1.4)'}
	MainForm:map()		-- determine form layout prior to assigning button labels and resizing
	if not gblOptions.Emulator then MainForm.tipballoon = 'YES' end

	UpdateMenu()

	if fhGetAppVersion() < 7 then
		if IsExtendedDesktop() then
			local msg = 'Versions of Family Historian prior to 7 cannot determine which monitor ' ..
				'the application is running in, so the main plugin menu will always open in the ' ..
				'primary monitor.'
			MessageBox(msg, 'OK', 'WARNING')
		end
		MainForm:popup(IUP_CENTER, IUP_CENTER)
	else
		iup.SetAttribute(MainForm, 'NATIVEPARENT', fhGetContextInfo('CI_PARENT_HWND'))
		iup.ShowXY(MainForm, IUP_CENTERPARENT, IUP_CENTERPARENT)
		local position = MainForm.ScreenPosition
		local FHx, FHy = position:match('([^,]*),([^,]*)')
		FHx, FHy = tonumber(FHx), tonumber(FHy)
		local size = MainForm.NaturalSize
		local x, y, w, h
		w, h = size:match('^(%d+)x(%d+)$')
		local xMid = FHx + tonumber(w) / 2
		local yMid = FHy + tonumber(h) / 2
		local mi = iup.GetGlobal('MONITORSINFO')
		for m in mi:gmatch('[^\r\n]+') do
			x, y, w, h = m:match('^(%S+)%s(%S+)%s(%S+)%s(%S+)$')
			x, y, w, h = tonumber(x), tonumber(y), tonumber(w), tonumber(h)
			if xMid >= x and xMid <= x + w and yMid >= y and yMid <= y + h then		--	this monitor
				break
			end
		end
		if FHx >= x and FHy >= y then										-- visible
			MainForm:popup(FHx, FHy)
		else
			MainForm:popup(x, y)											-- corner
		end
	end

	SaveOptions()
end

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

function SelectFolder(MainForm)

	repeat
		local filedlg = iup.filedlg{dialogtype = 'DIR', directory = gblOptions.Path,
				title = 'Select folder for scripts and backup files', parentdialog = MainForm}
		filedlg:popup()
		if filedlg.Status == '-1' then return end
		local path = filedlg.Value

		if IsASCII(path) then
			gblOptions.Path = path
			gblOptions.ShortPath = path
			return
		elseif gblOptions.Emulator then		-- check emulator first as it does not support short paths
			local msg = 'Due to limitations in Windows batch scripts, the backup location ' ..
					'must comprise only standard English letters, numbers, and ' ..
					'valid punctuation characters.\n\nPlease select an alterative location.'
			if MessageBox(msg, 'OKCANCEL', 'ERROR', 'Folder Selection Error') == 2 then return end
		else
			if IsASCII(FSO:GetFolder(path).ShortPath) then
				gblOptions.Path = path
				gblOptions.ShortPath = FSO:GetFolder(path).ShortPath
				return
			else
				local msg = 'Due to limitations in Windows batch scripts, a backup location on drives ' ..
						'other than C:\\ must comprise only standard English letters, numbers, and ' ..
						'valid punctuation characters.\n\nPlease select an alterative location.'
				if MessageBox(msg, 'OKCANCEL', 'ERROR', 'Folder Selection Error') == 2 then return end
			end
		end
	until false
end

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

function Backup()

	-- check for existing backup

	local BackupPath = gblOptions.ShortPath

	if gblOptions.Dated then
		BackupPath = BackupPath .. '\\fhBackup_' .. os.date('%Y-%m-%d')
	end

	-- check all files and folders present if existing backup

	local i = 0
	if FSO:FolderExists(BackupPath .. '\\fhProgramData') then i = i + 1 end
	if FSO:FolderExists(BackupPath .. '\\fhAppData') then i = i + 1 end
	if FSO:FileExists(BackupPath .. '\\fhCURRENT_USER.reg') then i = i + 1 end
	if FSO:FileExists(BackupPath .. '\\fhLOCAL_MACHINE.reg') then i = i + 1 end

	if not gblOptions.Advanced then
		if i > 0 and i < 4 then
			local msg = 'Existing backup in ' .. BackupPath .. ' is incomplete.\n\nIgnore error and update?'
			if MessageBox(msg, 'OKCANCEL', 'WARNING', nil, 2) ~= 1 then return end
		elseif i == 4 then
			local stamp = FSO:GetFile(BackupPath .. '\\fhCURRENT_USER.reg').DateLastModified
			local msg = os.date('Update existing backup in ' .. BackupPath .. 
				'?\n\nLast update: ' .. stamp)
			if MessageBox(msg, 'YESNO', 'QUESTION', 'Confirm Update', 2) ~= 1 then return end
		end
	end

	local tblScript, BackupFile, ShortFile = GetBackupScript(BackupPath)

	-- write script to file

	local F = io.open(ShortFile or BackupFile, 'w')
	if not F then
		MessageBox('Cannot write backup script.', 'OK', 'ERROR', 'File Access Error')
		return
	end
	table.insert(tblScript, '')
	F:write(table.concat(tblScript, '\n'))
	F:close()

	-- confirm successful write

	if gblOptions.Advanced then
		local msg = 'Backup script written to file ' .. BackupFile .. '.'
		MessageBox(msg, 'OK', 'INFORMATION', 'File Write Confirmation')
		return
	end

	-- sync folders if running in an emulator

	if gblOptions.Emulator and FSO:FolderExists(BackupPath .. '\\fhProgramData') then 
		SyncFolders(gblOptions.ProgramData, BackupPath .. '\\fhProgramData')
	end

	-- implement backup

	if IsExtendedDesktop() then
		MessageBox('The backup will now run in the primary monitor.', 'OK', 'INFORMATION')
	end

	if not fhShellExecute(ShortFile or BackupFile) then
		MessageBox('Backup failed to run.', 'OK', 'ERROR', 'Script Error')
	end
end

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

function GetBackupScript(BackupPath)

	-- prepare backup script

	local AppName = fhGetContextInfo('CI_APP_DATA_FOLDER'):match('[%w%s]+$')
	local UserKey = '"HKEY_CURRENT_USER\\SOFTWARE\\Calico Pie\\' .. AppName .. '"'
	local GlobalKey = '"HKEY_LOCAL_MACHINE\\SOFTWARE\\WOW6432Node\\Calico Pie\\' ..
			AppName .. '\\2.0\\Preferences"'

	if gblOptions.Emulator then										-- asssumes 32-bit emulator
		GlobalKey = '"HKEY_LOCAL_MACHINE\\SOFTWARE\\Calico Pie\\' .. AppName .. '\\2.0\\Preferences"'
	end

	local tblT = {}													-- build the script in this table
	table.insert(tblT, '@echo off')
	table.insert(tblT, 'echo Backup started...')

	-- set path according to whether dated subfolder or not

	if not gblOptions.Dated then
		table.insert(tblT, 'set p=' .. gblOptions.ShortPath)
	elseif gblOptions.Emulator then							-- WINE does not support PowerShell Get-Date
		table.insert(tblT, 'set p=' .. gblOptions.Path .. '\\fhBackup_' .. os.date('%Y-%m-%d'))
	else
		table.insert(tblT, 'set basepath=' .. gblOptions.ShortPath)
		local cmd = 'for /f %%D in ( \'PowerShell Get-Date -Format "_yyyy-MM-dd"\' ) do set suffix=%%D'
		table.insert(tblT, cmd)
		table.insert(tblT, 'set p=%basepath%\\fhBackup%suffix%')
	end

	-- define commands to be used for copying ProgramData folders

	if gblOptions.Emulator then
		local colFolders = luacom.GetEnumerator(FSO:GetFolder(gblOptions.ProgramData).SubFolders)
		local objFolder = colFolders:Next()
		while objFolder do
			if objFolder.Name ~= 'Map' then
				table.insert(tblT, 'xcopy "' .. gblOptions.ProgramData .. '\\' .. objFolder.Name .. '\\" ' .. 
						'"%p%\\fhProgramData\\' .. objFolder.Name .. '\\" /d /e /y')
			end
			objFolder = colFolders:Next()
		end
	else
		table.insert(tblT, 'robocopy "' .. gblOptions.ProgramData ..
				'" "%p%\\fhProgramData" /MIR /XD Map /NJH')
	end

	-- define AppData command

	if gblOptions.Emulator then
		table.insert(tblT, 'xcopy "' .. gblOptions.AppDataPath .. '\\" "%p%\\fhAppData\\" /d /e /y')
	else
		table.insert(tblT, 'robocopy "' .. gblOptions.AppDataPath .. '" "%p%\\fhAppData" /MIR /NJH')
	end

	-- define Registry commands

	table.insert(tblT, 'reg export ' .. UserKey .. ' "%p%\\fhCURRENT_USER.reg" /y')
	table.insert(tblT, 'reg export ' .. GlobalKey .. ' "%p%\\fhLOCAL_MACHINE.reg" /y')

	-- determine name of file used to save command script and self-delete command if immediate backup

	local File, ShortFile
	if gblOptions.Advanced then
		ShortFile = gblOptions.ShortPath .. '\\fhBackup.bat'
		File = gblOptions.Path .. '\\fhBackup.bat'
	else
		File = gblOptions.Temp .. '\\~fhBackup.bat'
		table.insert(tblT, 'pause')
		table.insert(tblT, 'del "' .. File .. '"')
	end

	return tblT, File, ShortFile
end

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

function Restore(MainForm)

	-- check for Advanced mode

	local RestoreFolder, ShortFolder, RestoreFile, ShortFile, legacy
	if gblOptions.Advanced then
		RestoreFolder = gblOptions.Path
		RestoreFile = gblOptions.Path .. '\\fhRestore.bat'
		ShortFolder = gblOptions.ShortPath
		ShortFile = gblOptions.ShortPath .. '\\fhRestore.bat'
	else
		RestoreFolder, ShortFolder, legacy = SelectRestoreFolder(MainForm)
		if not RestoreFolder then return end
		RestoreFile = gblOptions.Temp .. '\\~fhRestore.bat'
		ShortFile = gblOptions.Temp .. '\\~fhRestore.bat'
	end

	local tblScript = GetRestoreScript(RestoreFolder, ShortFolder, legacy)

	-- write script to file (path cannot be Unicode, so simple method ok)

	local F = io.open(ShortFile, 'w')
	if not F then
		MessageBox('Cannot write restore script.', 'OK', 'ERROR', 'File Access Error')
		return
	end
	table.insert(tblScript, '')
	F:write(table.concat(tblScript, '\n'))
	F:close()

	-- confirm successful write

	if gblOptions.Advanced then
		local msg = 'Restore script written to file ' .. RestoreFile .. '.'
		MessageBox(msg, 'OK', 'INFORMATION', 'File Write Confirmation')
		return
	end

	-- remove default files and folders from Registry file before restoring (if not old format)

	if not legacy then RemoveDefaults(RestoreFolder) end

	-- compare folders if running in an emulator

	if gblOptions.Emulator then
		SyncFolders(RestoreFolder .. '\\fhProgramData\\', gblOptions.ProgramData .. '\\')
	end

	-- warn the user what is about to happen and request approval

	if not gblOptions.Emulator then
		local msg = 'Family Historian will now be closed in order to complete the restore operation.\n\n' ..
				'Are you sure that you want to do this?'
		if MessageBox(msg, 'YESNO', 'WARNING', 'Confirm Automatic Restore - CAUTION', 2) ~= 1 then return end
	else
		local msg = 'When the "Press any key to continue..." message appears in the command window, ' ..
				'please close Family Historian manually.\n\n'..
				'Once it is fully closed, select the command window and press any key to complete ' ..
				'the restore.'
		if MessageBox(msg, 'OKCANCEL', 'WARNING', 'Confirm Restore - CAUTION', 2) ~= 1 then return end
	end

	-- generate auto-close script

	local CloseFile = gblOptions.Temp .. '\\' .. '~fhAutoClose.bat'
	local tblT = {}

	table.insert(tblT, '@echo off')
	if not gblOptions.Emulator then
		table.insert(tblT, 'echo Restore started...')
		table.insert(tblT, 'powershell start-sleep -s 1')
		table.insert(tblT, 'cls')
		table.insert(tblT, 'taskkill /IM fh.exe /f')
		table.insert(tblT, 'powershell start-sleep -s 1')
	else
		table.insert(tblT, 'echo Please close Family Historian before proceeding.')
		table.insert(tblT, 'pause')
	end
	table.insert(tblT, 'call "' .. RestoreFile .. '"')
	table.insert(tblT, 'del "' .. RestoreFile .. '"')
	table.insert(tblT, 'del "' .. CloseFile .. '"')
	table.insert(tblT, '')

	local F = io.open(CloseFile, 'w')
	if not F then
		MessageBox('Cannot write auto-close script.', 'OK', 'ERROR', 'File Access Error')
		return
	end
	F:write(table.concat(tblT, '\n'))
	F:close()

	-- check that only one instance of FH is running (not valid in emulator)

	if not gblOptions.Emulator and CountInstances() > 1 then
		local msg = 'There may be more than one instance of Family Historian open.\n\n' ..
				'Proceeding with the restore operation could cause data loss. Please close ' ..
				'any additional copies and re-run the plugin.'
		MessageBox(msg, 'OK', 'ERROR', 'Multiple Instances Detected')
		os.remove(RestoreFile)
		os.remove(CloseFile)
		return
	end

	-- execute the kamikaze close and bale out...

	fhShellExecute(CloseFile)
	return -1								-- trigger value to close plugin (DOES NOT HAPPEN - CHECK)
end

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

function SelectRestoreFolder(MainForm)

	-- select backup to restore

	local filedlg = iup.filedlg{dialogtype = 'DIR', directory = gblOptions.Path, parentdialog = MainForm,
			title = 'Select folder that contains the backup you wish to restore'}
	filedlg:popup()
	if filedlg.Status == '-1' then return end
	local RestorePath, ShortPath = filedlg.Value, filedlg.Value
	if not gblOptions.Emulator then ShortPath = FSO:GetFolder(RestorePath).ShortPath end

	-- check folder contains an identifiable backup
	
	local BackupVersion, B1, B2, B3, legacy

	-- is there a valid new format backup in this folder?

	local KeyFile = RestorePath .. '\\Family Historian\\Version.data'
	if not FSO:FileExists(KeyFile) then
		local i = 0
		if FSO:FolderExists(RestorePath .. '\\fhProgramData') then i = i + 1 end
		if FSO:FolderExists(RestorePath .. '\\fhAppData') then i = i + 1 end
		if FSO:FileExists(RestorePath .. '\\fhCURRENT_USER.reg') then i = i + 1 end
		if FSO:FileExists(RestorePath .. '\\fhLOCAL_MACHINE.reg') then i = i + 1 end
		if i == 4 then
			KeyFile = RestorePath .. '\\fhProgramData\\Plugin Data\\' ..
					'Backup and Restore Family Historian Settings via Windows.ini'
			local ShortKey = KeyFile
			if not gblOptions.Emulator then ShortKey = FSO:GetFile(KeyFile).ShortPath end
			for Line in io.lines(ShortKey) do
				local k, v = Line:match('^(%w+)=([%w%p%s]+)$')
				if k == 'Version' then
					BackupVersion = v
					B1, B2, B3 = BackupVersion:match('(%d+).(%d+).(%d+)')
				end
			end
		end
	elseif not (B1 and B2 and B3) then					-- new version not found
		KeyFile = RestorePath .. '\\Family Historian\\Version.data'
		local ShortKey = KeyFile
		if not gblOptions.Emulator then ShortKey = FSO:GetFolder(KeyFile).ShortPath end
		F = io.open(ShortKey)
		if F then
			BackupVersion = F:read('*a')
			F:close()
			B1, B2, B3 = BackupVersion:match('(%d+).(%d+).(%d+)')
			legacy = true
		end
	end
	if not (B1 and B2 and B3) then
		local msg = 'Selected folder does not appear to contain a backup.'
		MessageBox(msg, 'OK', 'ERROR', 'Folder Selection Error')
		return
	end

	-- check backup version matches

	local V1, V2, V3 = fhGetAppVersion()
	B1, B2, B3 = tonumber(B1), tonumber(B2), tonumber(B3)

	if V1 ~= B1 then
		local msg = 'Cannot restore a Version ' .. B1 .. ' backup into Version ' .. V1 .. '.'
		MessageBox(msg, 'OK', 'ERROR', 'Version Incompatibility')
		return
	elseif B2 > V2 or (B2 == V2 and B3 > V3) then
		local msg = 'Cannot restore a Version ' .. BackupVersion .. ' backup into Version ' ..
				gblOptions.Version .. '.\n\n' ..
				'You should update your present copy of Family Historian before restoring the backup.'
		MessageBox(msg, 'OK', 'ERROR', 'Version Incompatibility')
		return
	elseif B2 < V2 or (B2 == V2 and B3 < V3) then
		local msg = 'Restoring a Version ' .. BackupVersion .. ' backup into Version ' ..
				gblOptions.Version .. ' is potentially risky.\n\nAre you sure that you want to do this?'
		if MessageBox(msg, 'YESNO', 'WARNING', 'Potential Version Conflict', 2) ~= 1 then return end
	end

	-- get backup creation date and time

	local stamp = FSO:GetFile(RestorePath .. '\\fhCURRENT_USER.reg').DateLastModified
	
	if legacy then
		stamp = FSO:GetFile(RestorePath .. '\\Family Historian\\Version.data').DateLastModified
	end
	local msg = 'Backup in ' .. RestorePath .. ' was created ' .. stamp ..
			'. Is this the correct backup?'
	if MessageBox(msg, 'YESNO', 'QUESTION', 'Confirm Backup Selection', 2) ~= 1 then return end
	return RestorePath, ShortPath, legacy
end

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

function GetRestoreScript(RestoreFolder, ShortFolder, legacy)

	local tblT = {}												-- build the script in this table
	table.insert(tblT, '@echo off')
	if not gblOptions.Emulator then
		local Switch = '/MIR /NJH'
		if not legacy then
			table.insert(tblT, 'robocopy "' .. ShortFolder .. '\\fhProgramData" "' .. 
					gblOptions.ProgramData .. '" ' .. Switch)
			table.insert(tblT, 'robocopy "' .. ShortFolder .. '\\fhAppData" "' .. 
					gblOptions.AppDataPath .. '" ' .. Switch)
			table.insert(tblT, 'reg import "' .. ShortFolder .. '\\fhCURRENT_USER.reg"')
			table.insert(tblT, 'reg import "' .. ShortFolder .. '\\fhLOCAL_MACHINE.reg"')
		else
			table.insert(tblT, 'robocopy "' .. ShortFolder .. '\\Family Historian\\Program Data" "' ..
					gblOptions.ProgramData .. '" ' .. Switch)
			table.insert(tblT, 'robocopy "' .. ShortFolder .. '\\Family Historian\\Application" "' .. 
					AppDataPath .. '" ' .. Switch)
			table.insert(tblT, 'reg import "' .. ShortFolder .. '\\Family Historian\\Registry.keys"')
		end
	else
		local Switch = '/e /y'
		if not legacy then
			table.insert(tblT, 'xcopy "' .. ShortFolder .. '\\fhProgramData\\" "' ..
					gblOptions.ProgramData .. '\\" ' .. Switch)
			table.insert(tblT, 'xcopy "' .. ShortFolder .. '\\fhAppData\\" "' .. 
					gblOptions.AppDataPath .. '" ' .. Switch)
			table.insert(tblT, 'reg import "' .. ShortFolder .. '\\fhCURRENT_USER.reg"')
			table.insert(tblT, 'reg import "' .. ShortFolder .. '\\fhLOCAL_MACHINE.reg"')
		else
			table.insert(tblT, 'xcopy "' .. ShortFolder .. '\\Family Historian\\Program Data\\" "' .. 
					gblOptions.ProgramData .. '\\" ' .. Switch)
			table.insert(tblT, 'xcopy "' .. ShortFolder .. '\\Family Historian\\Application\\" "' ..
					AppDataPath .. '\\" ' .. Switch)
			table.insert(tblT, 'reg import "' .. ShortFolder .. '\\Family Historian\\Registry.keys"')
		end
	end
	if not gblOptions.Advanced then
		table.insert(tblT, 'pause')
	end
	return tblT
end

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

function RemoveDefaults(RestoreFolder)

	-- remove defaults that may not be valid in new installation

	local FileName = RestoreFolder .. '\\fhLOCAL_MACHINE.reg'
	if not FSO:FileExists(FileName) then
		MessageBox('Restore folder is missing a required Registry file.', 'OK', 'ERROR')
		return
	end
	local TextStream = FSO:OpenTextFile(FileName, 1, false, -1)
	local Data = TextStream:ReadAll()
	TextStream:Close()

	local tblT = {}
	Data = Data:gsub('\r\n\r\n', '\r\n \r\n')		-- simple workaround to include blank lines
	for Line in Data:gmatch('[^\r\n]+') do
		if Line == ' ' then Line = '' end
		local T1 = Line:match('^"Backup Directory"=')
		local T2 = Line:match('^"Default File"=')
		local T3 = Line:match('^"FH Root Folder"=')
		local T4 = Line:match('^"Emulator%-Compat%-Mode"=')
		if not (T1 or T2 or T3 or T4) then table.insert(tblT, Line) end
	end
	table.insert(tblT, '')

	TextStream = FSO:OpenTextFile(FileName, 2, false, -1)
	TextStream:Write(table.concat(tblT, '\n'))
	TextStream:Close()
end

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

function SyncFolders(PrimaryFolder, ReplicaFolder)

	local tblT = {}

	-- set tblT to true for all files in replica folder
	
	GetFiles(ReplicaFolder, tblT, true)

	-- clear value for all corresponding files in primary folder

	GetFiles(PrimaryFolder, tblT, nil)

	-- delete all files in replica but not primary

	for File, _ in pairs(tblT) do
		FSO:DeleteFile(ReplicaFolder .. File)
	end
end

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

function GetFiles(Folder, tblT, switch)	

	-- pass folder recursively as text name rather than folder object as latter
	-- not supported in WINE

	local function GetFolderContents(Folder, I)
		local colFiles = luacom.GetEnumerator(FSO:GetFolder(Folder).Files)
		local objFile = colFiles:Next()
		while objFile do
			tblT[objFile.Path:sub(I+1)] = switch
			objFile = colFiles:Next()
		end

		local colSubFolders = luacom.GetEnumerator(FSO:GetFolder(Folder).SubFolders)
		local objSubFolder = colSubFolders:Next()
		while objSubFolder do
			GetFolderContents(objSubFolder.Path, I)
			objSubFolder = colSubFolders:Next()
		end
	end

	local I = Folder:len()
	GetFolderContents(Folder, I)
end

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

function GetOptions()

	-- get system data

	gblOptions = {}
	local tblO = {}
	local pc = iup.GetGlobal('COMPUTERNAME')

	-- "3-test" version of emulator detection

	local check1 = FSO:FolderExists('Z:\\bin') and FSO:FolderExists('Z:\\etc')
	local check2 = not FSO:FolderExists('C:\\Windows\\SoftwareDistribution')
	local check3 = os.getenv('WINEPREFIX')

	if check1 and check2 and check3 then
		gblOptions.Emulator = true
	elseif not check1 and not check2 and not check3 then
		gblOptions.Emulator = false
	else
		local msg = 'Emulator detection has given an ambiguous result.\n\n'
		msg = msg .. 'Z:\\ method: ' .. tostring(check1) .. '\n'
		msg = msg .. 'C:\\Windows\\SoftwareDistribution method: ' .. tostring(check2) .. '\n'
		msg = msg .. 'WINEPREFIX method: ' .. tostring(check3) .. '\n\n'
		msg = msg .. 'Please report on FHUG for investigation.'
		MessageBox(msg, 'OK', 'ERROR')
		return
	end

	-- get system level options

	gblOptions.Temp = os.getenv('TEMP')
	if not FSO:FolderExists(gblOptions.Temp) then
		MessageBox('Cannot locate Temp folder.', 'OK', 'ERROR')
		return
	end
	gblOptions.ProgramData = fhGetContextInfo('CI_APP_DATA_FOLDER')
	gblOptions.AppData = os.getenv('APPDATA')
	if not FSO:FolderExists(gblOptions.AppData) then		-- Unicode user name
		local path = fhGetPluginDataFileName('CURRENT_USER')
		local i = path:find('\\Calico Pie\\Family Historian\\Plugin Data\\')
		gblOptions.AppData = path:sub(1, i-1)
	end
	gblOptions.AppData = gblOptions.AppData .. '\\Calico Pie\\Family Historian'
	if gblOptions.Emulator then
		gblOptions.AppDataPath = gblOptions.AppData
	else
		gblOptions.AppDataPath = FSO:GetFolder(gblOptions.AppData).ShortPath
	end

	-- get saved options

	local File = gblOptions.ProgramData .. '\\Plugin Data\\' ..
			'Backup and Restore Family Historian Settings via Windows.ini'
	if FSO:FileExists(File) then
		for line in io.lines(File) do
			local i = line:find('=')
			if i then tblO[line:sub(1, i-1)] = line:sub(i+1) end
		end
		if tblO.Path and tblO.ShortPath and FSO:FolderExists(tblO.Path) then
			gblOptions.Path = tblO.Path
			gblOptions.ShortPath = tblO.ShortPath
		end
		if not gblOptions.Emulator and tblO.Advanced == '1' or tblO.Advanced == 'ON' then
			gblOptions.Advanced = true
		end
		if tblO.Dated == '1' or tblO.Dated == 'ON' then gblOptions.Dated = true end

		for k, _ in pairs(tblO) do					-- ensure ARM settings are retained
			if k:match('ARM_Msg_') then
				gblOptions[k] = true
			end
		end
	end

	-- get processor architecture

	local key = 'HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment\\' ..
			'PROCESSOR_ARCHITECTURE'
	gblOptions.CPU = GetRegKey(key)

	if gblOptions.CPU == 'ARM64' and not gblOptions['ARM_Msg_' .. pc] then
		local msg = 'Your PC has a CPU with ARM infrastructure. ' ..
				'Issues have been reported by Mac users running FH inside a VM when backing up to a ' ..
				'location on the host Operating System. If this applies to you, you are recommended ' ..
				'to select a backup location on either an external drive or network location.\n\n' ..
				'See the plugin help file for more details of this topic.\n\n' .. 
				'Do you wish to hide this message on future runs?'
		if MessageBox(msg, 'YESNO', 'WARNING') == 1 then
			gblOptions['ARM_Msg_' .. pc] = true
		end
	elseif gblOptions.CPU ~= 'ARM64' and gblOptions['ARM_Msg_' .. pc] then
		gblOptions['ARM_Msg_' .. pc] = false
	end

	-- set version option and save back to file to ensure saved on backup

	local V1, V2, V3 = fhGetAppVersion()
	gblOptions.Version = V1 .. '.' .. V2 .. '.' .. V3
	SaveOptions()

	return true
end

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

function SaveOptions()

	-- save options to file

	local OptionsFolder = gblOptions.ProgramData .. '\\Plugin Data'
	if not FSO:FolderExists(OptionsFolder) then
		FSO:CreateFolder(OptionsFolder)
	end
	local pc = iup.GetGlobal('COMPUTERNAME')
	local OptionsFile = OptionsFolder .. '\\Backup and Restore Family Historian Settings via Windows.ini'
	local F = io.open(OptionsFile, 'w')
	if not F then
		MessageBox('Cannot open options file.', 'OK', 'ERROR', 'File Access Error')
		return
	end
	F:write('[Options]\n')
	F:write('Version=' .. gblOptions.Version .. '\n')
	if gblOptions.Path then F:write('Path=' .. gblOptions.Path .. '\n') end
	if gblOptions.ShortPath then F:write('ShortPath=' .. gblOptions.ShortPath .. '\n') end
	if gblOptions.Advanced then F:write('Advanced=1\n') end
	if gblOptions.Dated then F:write('Dated=1\n') end
	if gblOptions['ARM_Msg_' .. pc] then
		F:write('ARM_Msg_' .. pc .. '=1\n')
	end

	for k, _ in pairs(gblOptions) do			-- ensure ARM settings relating to other PCs are retained
		if k:match('ARM_Msg_') and k ~= 'ARM_Msg_' .. pc then
			F:write(k .. '=1\n')
		end
	end
	F:close()
end

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

function GetHelp(MainForm)

	-- hidden diagnostic window

	if iup.GetGlobal('SHIFTKEY') == 'OFF' then
		fhShellExecute('https://pluginstore.family-historian.co.uk/page/help/' ..
					'backup-and-restore-family-historian-settings-via-windows')
	else
		local CPU = GetRegKey('HKLM\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment\\' ..
				'PROCESSOR_ARCHITECTURE')
		local tblT = {'Monitors Count = ' .. (iup.GetGlobal('MONITORSCOUNT') or 'n/a'),
				'Monitors Info = ' .. (iup.GetGlobal('MONITORSINFO') or 'n/a'),
				'Screen Size = ' .. (iup.GetGlobal('SCREENSIZE') or 'n/a'),
				'Full Size = ' .. (iup.GetGlobal('FULLSIZE') or 'n/a'),
				'Virtual Screen = ' .. (iup.GetGlobal('VIRTUALSCREEN') or 'n/a'),
				'Menu Size = ' .. (MainForm.NaturalSize or 'n/a'),
				'Menu Position = ' .. (MainForm.ScreenPosition or 'n/a'),
				'',
				'Processor Architecture = ' .. gblOptions.CPU or 'FALSE',
				'Emulator = ' .. tostring(gblOptions.Emulator),
				'',
				'Family Historian Version = ' .. gblOptions.Version
				
				--[[	options required only for detailed debugging

				'System = ' .. (iup.GetGlobal('SYSTEM') or ''),
				'System Version = ' .. (iup.GetGlobal('SYSTEMVERSION') or ''),
				'System Language = ' .. (iup.GetGlobal('SYSTEMLANGUAGE') or ''),
				'System Locale = ' .. (iup.GetGlobal('SYSTEMLOCALE') or ''),
				'Computer Name = ' .. (iup.GetGlobal('COMPUTERNAME') or ''),
				'',
				'User Name (IUP) = ' .. (iup.GetGlobal('USERNAME') or ''),
				'Temp Folder = ' .. gblOptions.Temp,
				'ProgramData Folder = ' .. gblOptions.ProgramData:gsub(' ', ' '),
				'AppData Folder = ' .. gblOptions.AppData:gsub(' ', ' '),
				]]

				}
		MessageBox(table.concat(tblT, '\n'), 'OK', 'INFORMATION', 'System Information')
	end
end

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

function IsExtendedDesktop()

	-- determines if system has an extended desktop

	local xd = iup.GetGlobal('VIRTUALSCREEN')
	local x, y, width, height = xd:match('^(%S+)%s(%S+)%s(%S+)%s(%S+)$')
	local fs = iup.GetGlobal('FULLSIZE')
	local ExtendedDesktop = fs ~= width .. 'x' .. height
	return ExtendedDesktop
end
	
-- *********************************************************************

function IsASCII(S)

	-- checks whether the supplied string comprises just ASCII characters

	for i = 1, S:len() do
		if S:byte(i) < 32 or S:byte(i) > 127 then return false end
	end
	return true
end

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

function CountInstances()

	-- checks just a single instance of FH is running

	local F = io.popen('powershell get-process *fh*')
	if not F then
		local msg = 'Cannot determine number of Family Historian copies running.'
		MessageBox(msg, 'OK', 'ERROR', 'File Handle Access Error')
		return
	end
	local S = F:read('*a')
	F:close()

	local Instances = 0
	for line in S:gmatch('[^\r\n]+') do
		if line:lower():match('%sfh%s+$') then
			Instances = Instances + 1
		end
	end
	return Instances
end

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

function GetRegKey(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 MessageBox(Message, Buttons, Icon, Title, Default)

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

	local msgdlg = iup.messagedlg{value = Message, buttons = Buttons, dialogtype = Icon,
			title = Title or fhGetContextInfo('CI_PLUGIN_NAME'), buttondefault = Default,
			parentdialog = MainForm}
	msgdlg:popup()
	return tonumber(msgdlg.ButtonResponse)
end

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

main()

Source:Backup-and-Restore-Family-Historian-Settings-via-Windows-8.fh_lua