<?php
/**
 * webtrees: online genealogy
 * Copyright (C) 2016 webtrees development team
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */
namespace Fisharebest\Webtrees\Functions;

use Fisharebest\Webtrees\Auth;
use Fisharebest\Webtrees\Census\Census;
use Fisharebest\Webtrees\Census\CensusOfCzechRepublic;
use Fisharebest\Webtrees\Census\CensusOfDenmark;
use Fisharebest\Webtrees\Census\CensusOfDeutschland;
use Fisharebest\Webtrees\Census\CensusOfEngland;
use Fisharebest\Webtrees\Census\CensusOfFrance;
use Fisharebest\Webtrees\Census\CensusOfScotland;
use Fisharebest\Webtrees\Census\CensusOfUnitedStates;
use Fisharebest\Webtrees\Census\CensusOfWales;
use Fisharebest\Webtrees\Config;
use Fisharebest\Webtrees\Date;
use Fisharebest\Webtrees\Fact;
use Fisharebest\Webtrees\Family;
use Fisharebest\Webtrees\Filter;
use Fisharebest\Webtrees\GedcomCode\GedcomCodeAdop;
use Fisharebest\Webtrees\GedcomCode\GedcomCodeName;
use Fisharebest\Webtrees\GedcomCode\GedcomCodePedi;
use Fisharebest\Webtrees\GedcomCode\GedcomCodeQuay;
use Fisharebest\Webtrees\GedcomCode\GedcomCodeRela;
use Fisharebest\Webtrees\GedcomCode\GedcomCodeStat;
use Fisharebest\Webtrees\GedcomCode\GedcomCodeTemp;
use Fisharebest\Webtrees\GedcomRecord;
use Fisharebest\Webtrees\GedcomTag;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Individual;
use Fisharebest\Webtrees\Media;
use Fisharebest\Webtrees\Module;
use Fisharebest\Webtrees\Note;
use Fisharebest\Webtrees\Repository;
use Fisharebest\Webtrees\Source;
use Fisharebest\Webtrees\User;
use Rhumsaa\Uuid\Uuid;

/**
 * Class FunctionsEdit - common functions for editing
 */
class FunctionsEdit {
	/**
	 * Create a <select> control for a form.
	 *
	 * @param string $name
	 * @param string[] $values
	 * @param string|null $empty
	 * @param string $selected
	 * @param string $extra
	 *
	 * @return string
	 */
	public static function selectEditControl($name, $values, $empty, $selected, $extra = '') {
		if (is_null($empty)) {
			$html = '';
		} else {
			if (empty($selected)) {
				$html = '<option value="" selected>' . Filter::escapeHtml($empty) . '</option>';
			} else {
				$html = '<option value="">' . Filter::escapeHtml($empty) . '</option>';
			}
		}
		// A completely empty list would be invalid, and break various things
		if (empty($values) && empty($html)) {
			$html = '<option value=""></option>';
		}
		foreach ($values as $key => $value) {
			// PHP array keys are cast to integers!  Cast them back
			if ((string) $key === (string) $selected) {
				$html .= '<option value="' . Filter::escapeHtml($key) . '" selected dir="auto">' . Filter::escapeHtml($value) . '</option>';
			} else {
				$html .= '<option value="' . Filter::escapeHtml($key) . '" dir="auto">' . Filter::escapeHtml($value) . '</option>';
			}
		}
		if (substr($name, -2) === '[]') {
			// id attribute is not used for arrays
			return '<select name="' . $name . '" ' . $extra . '>' . $html . '</select>';
		} else {
			return '<select id="' . $name . '" name="' . $name . '" ' . $extra . '>' . $html . '</select>';
		}
	}

	/**
	 * Create a set of radio buttons for a form
	 *
	 * @param string $name The ID for the form element
	 * @param string[] $values Array of value=>display items
	 * @param string $selected The currently selected item
	 * @param string $extra Additional markup for the label
	 *
	 * @return string
	 */
	public static function radioButtons($name, $values, $selected, $extra = '') {
		$html = '';
		foreach ($values as $key => $value) {
			$html .=
				'<label ' . $extra . '>' .
				'<input type="radio" name="' . $name . '" value="' . Filter::escapeHtml($key) . '"';
			// PHP array keys are cast to integers!  Cast them back
			if ((string) $key === (string) $selected) {
				$html .= ' checked';
			}
			$html .= '>' . Filter::escapeHtml($value) . '</label>';
		}

		return $html;
	}

	/**
	 * Print an edit control for a Yes/No field
	 *
	 * @param string $name
	 * @param bool $selected
	 * @param string $extra
	 *
	 * @return string
	 */
	public static function editFieldYesNo($name, $selected = false, $extra = '') {
		return self::radioButtons(
			$name, [I18N::translate('no'), I18N::translate('yes')], $selected, $extra
		);
	}

	/**
	 * Print an edit control for a checkbox.
	 *
	 * @param string $name
	 * @param bool $is_checked
	 * @param string $extra
	 *
	 * @return string
	 */
	public static function checkbox($name, $is_checked = false, $extra = '') {
		return '<input type="checkbox" name="' . $name . '" value="1" ' . ($is_checked ? 'checked ' : '') . $extra . '>';
	}

	/**
	 * Print an edit control for a checkbox, with a hidden field to store one of the two states.
	 * By default, a checkbox is either set, or not sent.
	 * This function gives us a three options, set, unset or not sent.
	 * Useful for dynamically generated forms where we don't know what elements are present.
	 *
	 * @param string $name
	 * @param int $is_checked 0 or 1
	 * @param string $extra
	 *
	 * @return string
	 */
	public static function twoStateCheckbox($name, $is_checked = 0, $extra = '') {
		return
			'<input type="hidden" id="' . $name . '" name="' . $name . '" value="' . ($is_checked ? 1 : 0) . '">' .
			'<input type="checkbox" name="' . $name . '-GUI-ONLY" value="1"' .
			($is_checked ? ' checked' : '') .
			' onclick="document.getElementById(\'' . $name . '\').value=(this.checked?1:0);" ' . $extra . '>';
	}

	/**
	 * A list of integers (e.g. for an edit control).
	 *
	 * @return string[]
	 */
	public static function numericOptions($integers) {
		$array = [];
		foreach ($integers as $integer) {
			if ($integer === -1) {
				$array[$integer] = I18N::translate('All');
			} else {
				$array[$integer] = I18N::number($integer);
			}
		}

		return $array;
	}

	/**
	 * Function edit_language_checkboxes
	 *
	 * @param string $parameter_name
	 * @param array $accepted_languages
	 *
	 * @return string
	 */
	public static function editLanguageCheckboxes($parameter_name, $accepted_languages) {
		$html = '';
		foreach (I18N::activeLocales() as $locale) {
			$html .= '<div class="checkbox">';
			$html .= '<label title="' . $locale->languageTag() . '">';
			$html .= '<input type="checkbox" name="' . $parameter_name . '[]" value="' . $locale->languageTag() . '"';
			$html .= in_array($locale->languageTag(), $accepted_languages) ? ' checked>' : '>';
			$html .= $locale->endonym();
			$html .= '</label>';
			$html .= '</div>';
		}

		return $html;
	}

	/**
	 * Print an edit control for access level.
	 *
	 * @param string $name
	 * @param string $selected
	 * @param string $extra
	 *
	 * @return string
	 */
	public static function editFieldAccessLevel($name, $selected = '', $extra = '') {
		$ACCESS_LEVEL = [
			Auth::PRIV_PRIVATE => I18N::translate('Show to visitors'),
			Auth::PRIV_USER    => I18N::translate('Show to members'),
			Auth::PRIV_NONE    => I18N::translate('Show to managers'),
			Auth::PRIV_HIDE    => I18N::translate('Hide from everyone'),
		];

		return self::selectEditControl($name, $ACCESS_LEVEL, null, $selected, $extra);
	}

	/**
	 * Print an edit control for a RESN field.
	 *
	 * @param string $name
	 * @param string $selected
	 * @param string $extra
	 *
	 * @return string
	 */
	public static function editFieldRestriction($name, $selected = '', $extra = '') {
		$RESN = [
			''             => '',
			'none'         => I18N::translate('Show to visitors'), // Not valid GEDCOM, but very useful
			'privacy'      => I18N::translate('Show to members'),
			'confidential' => I18N::translate('Show to managers'),
			'locked'       => I18N::translate('Only managers can edit'),
		];

		return self::selectEditControl($name, $RESN, null, $selected, $extra);
	}

	/**
	 * Print an edit control for a contact method field.
	 *
	 * @param string $name
	 * @param string $selected
	 * @param string $extra
	 *
	 * @return string
	 */
	public static function editFieldContact($name, $selected = '', $extra = '') {
		// Different ways to contact the users
		$CONTACT_METHODS = [
			'messaging'  => I18N::translate('Internal messaging'),
			'messaging2' => I18N::translate('Internal messaging with emails'),
			'messaging3' => I18N::translate('webtrees sends emails with no storage'),
			'mailto'     => I18N::translate('Mailto link'),
			'none'       => I18N::translate('No contact'),
		];

		return self::selectEditControl($name, $CONTACT_METHODS, null, $selected, $extra);
	}

	/**
	 * Print an edit control for a language field.
	 *
	 * @param string $name
	 * @param string $selected
	 * @param string $extra
	 *
	 * @return string
	 */
	public static function editFieldLanguage($name, $selected = '', $extra = '') {
		$languages = [];
		foreach (I18N::activeLocales() as $locale) {
			$languages[$locale->languageTag()] = $locale->endonym();
		}

		return self::selectEditControl($name, $languages, null, $selected, $extra);
	}

	/**
	 * Print an edit control for a range of integers.
	 *
	 * @param string $name
	 * @param string $selected
	 * @param int $min
	 * @param int $max
	 * @param string $extra
	 *
	 * @return string
	 */
	public static function editFieldInteger($name, $selected = '', $min, $max, $extra = '') {
		$array = [];
		for ($i = $min; $i <= $max; ++$i) {
			$array[$i] = I18N::number($i);
		}

		return self::selectEditControl($name, $array, null, $selected, $extra);
	}

	/**
	 * Print an edit control for a username.
	 *
	 * @param string $name
	 * @param string $selected
	 * @param string $extra
	 *
	 * @return string
	 */
	public static function editFieldUsername($name, $selected = '', $extra = '') {
		$users = [];
		foreach (User::all() as $user) {
			$users[$user->getUserName()] = $user->getRealName() . ' - ' . $user->getUserName();
		}
		// The currently selected user may not exist
		if ($selected && !array_key_exists($selected, $users)) {
			$users[$selected] = $selected;
		}

		return self::selectEditControl($name, $users, '-', $selected, $extra);
	}

	/**
	 * Print an edit control for a ADOP field.
	 *
	 * @param string          $name
	 * @param string          $selected
	 * @param string          $extra
	 * @param Individual|null $individual
	 *
	 * @return string
	 */
	public static function editFieldAdoption($name, $selected = '', $extra = '', Individual $individual = null) {
		return self::selectEditControl($name, GedcomCodeAdop::getValues($individual), null, $selected, $extra);
	}

	/**
	 * Print an edit control for a PEDI field.
	 *
	 * @param string          $name
	 * @param string          $selected
	 * @param string          $extra
	 * @param Individual|null $individual
	 *
	 * @return string
	 */
	public static function editFieldPedigree($name, $selected = '', $extra = '', Individual $individual = null) {
		return self::selectEditControl($name, GedcomCodePedi::getValues($individual), '', $selected, $extra);
	}

	/**
	 * Print an edit control for a NAME TYPE field.
	 *
	 * @param string          $name
	 * @param string          $selected
	 * @param string          $extra
	 * @param Individual|null $individual
	 *
	 * @return string
	 */
	public static function editFieldNameType($name, $selected = '', $extra = '', Individual $individual = null) {
		return self::selectEditControl($name, GedcomCodeName::getValues($individual), '', $selected, $extra);
	}

	/**
	 * Print an edit control for a RELA field.
	 *
	 * @param string $name
	 * @param string $selected
	 * @param string $extra
	 *
	 * @return string
	 */
	public static function editFieldRelationship($name, $selected = '', $extra = '') {
		$rela_codes = GedcomCodeRela::getValues();
		// The user is allowed to specify values that aren't in the list.
		if (!array_key_exists($selected, $rela_codes)) {
			$rela_codes[$selected] = I18N::translate($selected);
		}

		return self::selectEditControl($name, $rela_codes, '', $selected, $extra);
	}

	/**
	 * Remove all links from $gedrec to $xref, and any sub-tags.
	 *
	 * @param string $gedrec
	 * @param string $xref
	 *
	 * @return string
	 */
	public static function removeLinks($gedrec, $xref) {
		$gedrec = preg_replace('/\n1 ' . WT_REGEX_TAG . ' @' . $xref . '@(\n[2-9].*)*/', '', $gedrec);
		$gedrec = preg_replace('/\n2 ' . WT_REGEX_TAG . ' @' . $xref . '@(\n[3-9].*)*/', '', $gedrec);
		$gedrec = preg_replace('/\n3 ' . WT_REGEX_TAG . ' @' . $xref . '@(\n[4-9].*)*/', '', $gedrec);
		$gedrec = preg_replace('/\n4 ' . WT_REGEX_TAG . ' @' . $xref . '@(\n[5-9].*)*/', '', $gedrec);
		$gedrec = preg_replace('/\n5 ' . WT_REGEX_TAG . ' @' . $xref . '@(\n[6-9].*)*/', '', $gedrec);

		return $gedrec;
	}

	/**
	 * Generates javascript code for calendar popup in user’s language.
	 *
	 * @param string $id
	 *
	 * @return string
	 */
	public static function printCalendarPopup($id) {
		return
			' <a href="#" onclick="cal_toggleDate(\'caldiv' . $id . '\', \'' . $id . '\'); return false;" class="icon-button_calendar" title="' . I18N::translate('Select a date') . '"></a>' .
			'<div id="caldiv' . $id . '" style="position:absolute;visibility:hidden;background-color:white;z-index:1000;"></div>';
	}

	/**
	 * An HTML link to create a new media object.
	 *
	 * @param string $element_id
	 *
	 * @return string
	 */
	public static function printAddNewMediaLink($element_id) {
		return '<a href="#" onclick="pastefield=document.getElementById(\'' . $element_id . '\'); window.open(\'addmedia.php?action=showmediaform\', \'_blank\', edit_window_specs); return false;" class="icon-button_addmedia" title="' . I18N::translate('Create a media object') . '"></a>';
	}

	/**
	 * An HTML link to create a new repository.
	 *
	 * @param string $element_id
	 *
	 * @return string
	 */
	public static function printAddNewRepositoryLink($element_id) {
		return '<a href="#" onclick="addnewrepository(document.getElementById(\'' . $element_id . '\')); return false;" class="icon-button_addrepository" title="' . I18N::translate('Create a repository') . '"></a>';
	}

	/**
	 * An HTML link to create a new note.
	 *
	 * @param string $element_id
	 *
	 * @return string
	 */
	public static function printAddNewNoteLink($element_id) {
		return '<a href="#" onclick="addnewnote(document.getElementById(\'' . $element_id . '\')); return false;" class="icon-button_addnote" title="' . I18N::translate('Create a shared note') . '"></a>';
	}

	/**
	 * An HTML link to edit a note.
	 *
	 * @param string $note_id
	 *
	 * @return string
	 */
	public static function printEditNoteLink($note_id) {
		return '<a href="#" onclick="edit_note(\'' . $note_id . '\'); return false;" class="icon-button_note" title="' . I18N::translate('Edit the shared note') . '"></a>';
	}

	/**
	 * An HTML link to create a new source.
	 *
	 * @param string $element_id
	 *
	 * @return string
	 */
	public static function printAddNewSourceLink($element_id) {
		return '<a href="#" onclick="addnewsource(document.getElementById(\'' . $element_id . '\')); return false;" class="icon-button_addsource" title="' . I18N::translate('Create a source') . '"></a>';
	}

	/**
	 * add a new tag input field
	 *
	 * called for each fact to be edited on a form.
	 * Fact level=0 means a new empty form : data are POSTed by name
	 * else data are POSTed using arrays :
	 * glevels[] : tag level
	 *  islink[] : tag is a link
	 *     tag[] : tag name
	 *    text[] : tag value
	 *
	 * @param string $tag fact record to edit (eg 2 DATE xxxxx)
	 * @param string $upperlevel optional upper level tag (eg BIRT)
	 * @param string $label An optional label to echo instead of the default
	 * @param string $extra optional text to display after the input field
	 * @param Individual $person For male/female translations
	 *
	 * @return string
	 */
	public static function addSimpleTag($tag, $upperlevel = '', $label = '', $extra = null, Individual $person = null) {
		global $tags, $main_fact, $xref, $bdm, $action, $WT_TREE;

		// Keep track of SOUR fields, so we can reference them in subsequent PAGE fields.
		static $source_element_id;

		$subnamefacts = ['NPFX', 'GIVN', 'SPFX', 'SURN', 'NSFX', '_MARNM_SURN'];
		preg_match('/^(?:(\d+) (' . WT_REGEX_TAG . ') ?(.*))/', $tag, $match);
		list(, $level, $fact, $value) = $match;
		$level                        = (int) $level;

		// element name : used to POST data
		if ($level === 0) {
			if ($upperlevel) {
				$element_name = $upperlevel . '_' . $fact;
			} else {
				$element_name = $fact;
			}
		} else {
			$element_name = 'text[]';
		}
		if ($level === 1) {
			$main_fact = $fact;
		}

		// element id : used by javascript functions
		if ($level === 0) {
			$element_id = $fact;
		} else {
			$element_id = $fact . Uuid::uuid4();
		}
		if ($upperlevel) {
			$element_id = $upperlevel . '_' . $fact . Uuid::uuid4();
		}

		// field value
		$islink = (substr($value, 0, 1) === '@' && substr($value, 0, 2) !== '@#');
		if ($islink) {
			$value = trim(substr($tag, strlen($fact) + 3), ' @\r');
		} else {
			$value = (string) substr($tag, strlen($fact) + 3);
		}
		if ($fact === 'REPO' || $fact === 'SOUR' || $fact === 'OBJE' || $fact === 'FAMC') {
			$islink = true;
		}

		if ($fact === 'SHARED_NOTE_EDIT' || $fact === 'SHARED_NOTE') {
			$islink = true;
			$fact   = 'NOTE';
		}

		// label
		echo '<tr id="', $element_id, '_tr"';
		if ($fact === 'DATA' || $fact === 'MAP' || ($fact === 'LATI' || $fact === 'LONG') && $value === '') {
			echo ' style="display:none;"';
		}
		echo '>';

		if (in_array($fact, $subnamefacts) || $fact === 'LATI' || $fact === 'LONG') {
			echo '<td class="optionbox wrap width25">';
		} else {
			echo '<td class="descriptionbox wrap width25">';
		}

		// tag name
		if ($label) {
			echo $label;
		} elseif ($upperlevel) {
			echo GedcomTag::getLabel($upperlevel . ':' . $fact);
		} else {
			echo GedcomTag::getLabel($fact);
		}

		// If using GEDFact-assistant window
		if ($action === 'addnewnote_assisted') {
			// Do not print on GEDFact Assistant window
		} else {
			// Not all facts have help text.
			switch ($fact) {
			case 'NAME':
				if ($upperlevel !== 'REPO' && $upperlevel !== 'UNKNOWN') {
					echo FunctionsPrint::helpLink($fact);
				}
				break;
			case 'DATE':
			case 'PLAC':
			case 'RESN':
			case 'ROMN':
			case 'SURN':
			case '_HEB':
				echo FunctionsPrint::helpLink($fact);
				break;
			}
		}
		// tag level
		if ($level > 0) {
			echo '<input type="hidden" name="glevels[]" value="', $level, '">';
			echo '<input type="hidden" name="islink[]" value="', $islink, '">';
			echo '<input type="hidden" name="tag[]" value="', $fact, '">';
		}
		echo '</td>';

		// value
		echo '<td class="optionbox wrap">';

		// retrieve linked NOTE
		if ($fact === 'NOTE' && $islink) {
			$note1 = Note::getInstance($value, $WT_TREE);
			if ($note1) {
				$noterec = $note1->getGedcom();
				preg_match('/' . $value . '/i', $noterec, $notematch);
				$value = $notematch[0];
			}
		}

		// Show names for spouses in MARR/HUSB/AGE and MARR/WIFE/AGE
		if ($fact === 'HUSB' || $fact === 'WIFE') {
			$family = Family::getInstance($xref, $WT_TREE);
			if ($family) {
				$spouse_link = $family->getFirstFact($fact);
				if ($spouse_link) {
					$spouse = $spouse_link->getTarget();
					if ($spouse) {
						echo $spouse->getFullName();
					}
				}
			}
		}

		if (in_array($fact, Config::emptyFacts()) && ($value === '' || $value === 'Y' || $value === 'y')) {
			echo '<input type="hidden" id="', $element_id, '" name="', $element_name, '" value="', $value, '">';
			if ($level <= 1) {
				echo '<input type="checkbox" ';
				if ($value) {
					echo 'checked';
				}
				echo ' onclick="document.getElementById(\'' . $element_id . '\').value = (this.checked) ? \'Y\' : \'\';">';
				echo I18N::translate('yes');
			}

			if ($fact === 'CENS' && $value === 'Y') {
				echo self::censusDateSelector(WT_LOCALE, $xref);
				if (Module::getModuleByName('GEDFact_assistant') && GedcomRecord::getInstance($xref, $WT_TREE) instanceof Individual) {
					echo
						'<div></div><a href="#" style="display: none;" id="assistant-link" onclick="return activateCensusAssistant();">' .
						I18N::translate('Create a shared note using the census assistant') .
						'</a></div>';
				}
			}

		} elseif ($fact === 'TEMP') {
			echo self::selectEditControl($element_name, GedcomCodeTemp::templeNames(), I18N::translate('No temple - living ordinance'), $value);
		} elseif ($fact === 'ADOP') {
			echo self::editFieldAdoption($element_name, $value, '', $person);
		} elseif ($fact === 'PEDI') {
			echo self::editFieldPedigree($element_name, $value, '', $person);
		} elseif ($fact === 'STAT') {
			echo self::selectEditControl($element_name, GedcomCodeStat::statusNames($upperlevel), '', $value);
		} elseif ($fact === 'RELA') {
			echo self::editFieldRelationship($element_name, strtolower($value));
		} elseif ($fact === 'QUAY') {
			echo self::selectEditControl($element_name, GedcomCodeQuay::getValues(), '', $value);
		} elseif ($fact === '_WT_USER') {
			echo self::editFieldUsername($element_name, $value);
		} elseif ($fact === 'RESN') {
			echo self::editFieldRestriction($element_name, $value);
		} elseif ($fact === '_PRIM') {
			echo '<select id="', $element_id, '" name="', $element_name, '" >';
			echo '<option value=""></option>';
			echo '<option value="Y" ';
			if ($value === 'Y') {
				echo ' selected';
			}
			echo '>', /* I18N: option in list box “always use this image” */
			I18N::translate('always'), '</option>';
			echo '<option value="N" ';
			if ($value === 'N') {
				echo 'selected';
			}
			echo '>', /* I18N: option in list box “never use this image” */
			I18N::translate('never'), '</option>';
			echo '</select>';
			echo '<p class="small text-muted">', I18N::translate('Use this image for charts and on the individual’s page.'), '</p>';
		} elseif ($fact === 'SEX') {
			echo '<select id="', $element_id, '" name="', $element_name, '"><option value="M" ';
			if ($value === 'M') {
				echo 'selected';
			}
			echo '>', I18N::translate('Male'), '</option><option value="F" ';
			if ($value === 'F') {
				echo 'selected';
			}
			echo '>', I18N::translate('Female'), '</option><option value="U" ';
			if ($value === 'U' || empty($value)) {
				echo 'selected';
			}
			echo '>', I18N::translateContext('unknown gender', 'Unknown'), '</option></select>';
		} elseif ($fact === 'TYPE' && $level === 3) {
			//-- Build the selector for the Media 'TYPE' Fact
			echo '<select name="text[]"><option selected value="" ></option>';
			$selectedValue = strtolower($value);
			if (!array_key_exists($selectedValue, GedcomTag::getFileFormTypes())) {
				echo '<option selected value="', Filter::escapeHtml($value), '" >', Filter::escapeHtml($value), '</option>';
			}
			foreach (GedcomTag::getFileFormTypes() as $typeName => $typeValue) {
				echo '<option value="', $typeName, '" ';
				if ($selectedValue === $typeName) {
					echo 'selected';
				}
				echo '>', $typeValue, '</option>';
			}
			echo '</select>';
		} elseif (($fact === 'NAME' && $upperlevel !== 'REPO' && $upperlevel !== 'UNKNOWN') || $fact === '_MARNM') {
			// Populated in javascript from sub-tags
			echo '<input type="hidden" id="', $element_id, '" name="', $element_name, '" onchange="updateTextName(\'', $element_id, '\');" value="', Filter::escapeHtml($value), '" class="', $fact, '">';
			echo '<span id="', $element_id, '_display" dir="auto">', Filter::escapeHtml($value), '</span>';
			echo ' <a href="#edit_name" onclick="convertHidden(\'', $element_id, '\'); return false;" class="icon-edit_indi" title="' . I18N::translate('Edit the name') . '"></a>';
		} else {
			// textarea
			if ($fact === 'TEXT' || $fact === 'ADDR' || ($fact === 'NOTE' && !$islink)) {
				echo '<textarea id="', $element_id, '" name="', $element_name, '" dir="auto">', Filter::escapeHtml($value), '</textarea><br>';
			} else {
				// text
				// If using GEDFact-assistant window
				if ($action === 'addnewnote_assisted') {
					echo '<input type="text" id="', $element_id, '" name="', $element_name, '" value="', Filter::escapeHtml($value), '" style="width:4.1em;" dir="ltr"';
				} else {
					echo '<input type="text" id="', $element_id, '" name="', $element_name, '" value="', Filter::escapeHtml($value), '" dir="ltr"';
				}
				echo ' class="', $fact, '"';
				if (in_array($fact, $subnamefacts)) {
					echo ' onblur="updatewholename();" onkeyup="updatewholename();"';
				}

				// Extra markup for specific fact types
				switch ($fact) {
				case 'ALIA':
				case 'ASSO':
				case '_ASSO':
					echo ' data-autocomplete-type="ASSO" data-autocomplete-extra="input.DATE"';
					break;
				case 'DATE':
					echo ' onblur="valid_date(this);" onmouseout="valid_date(this);"';
					break;
				case 'GIVN':
					echo ' autofocus data-autocomplete-type="GIVN"';
					break;
				case 'LATI':
					echo ' onblur="valid_lati_long(this, \'N\', \'S\');" onmouseout="valid_lati_long(this, \'N\', \'S\');"';
					break;
				case 'LONG':
					echo ' onblur="valid_lati_long(this, \'E\', \'W\');" onmouseout="valid_lati_long(this, \'E\', \'W\');"';
					break;
				case 'NOTE':
					// Shared notes. Inline notes are handled elsewhere.
					echo ' data-autocomplete-type="NOTE"';
					break;
				case 'OBJE':
					echo ' data-autocomplete-type="OBJE"';
					break;
				case 'PAGE':
					echo ' data-autocomplete-type="PAGE" data-autocomplete-extra="#' . $source_element_id . '"';
					break;
				case 'PLAC':
					echo ' data-autocomplete-type="PLAC"';
					break;
				case 'REPO':
					echo ' data-autocomplete-type="REPO"';
					break;
				case 'SOUR':
					$source_element_id = $element_id;
					echo ' data-autocomplete-type="SOUR"';
					break;
				case 'SURN':
				case '_MARNM_SURN':
					echo ' data-autocomplete-type="SURN"';
					break;
				case 'TIME':
					echo ' pattern="([0-1][0-9]|2[0-3]):[0-5][0-9](:[0-5][0-9])?" dir="ltr" placeholder="' . /* I18N: Examples of valid time formats (hours:minutes:seconds) */
						I18N::translate('hh:mm or hh:mm:ss') . '"';
					break;
				}
				echo '>';
			}

			$tmp_array = ['TYPE', 'TIME', 'NOTE', 'SOUR', 'REPO', 'OBJE', 'ASSO', '_ASSO', 'AGE'];

			// split PLAC
			if ($fact === 'PLAC') {
				echo '<div id="', $element_id, '_pop" style="display: inline;">';
				echo FunctionsPrint::printSpecialCharacterLink($element_id), ' ', FunctionsPrint::printFindPlaceLink($element_id);
				echo '<span  onclick="$(\'tr[id^=', $upperlevel, '_LATI],tr[id^=', $upperlevel, '_LONG],tr[id^=LATI],tr[id^=LONG]\').toggle(\'fast\'); return false;" class="icon-target" title="', I18N::translate('Latitude'), ' / ', I18N::translate('Longitude'), '"></span>';
				echo '</div>';
				if (Module::getModuleByName('places_assistant')) {
					\PlacesAssistantModule::setup_place_subfields($element_id);
					\PlacesAssistantModule::print_place_subfields($element_id);
				}
			} elseif (!in_array($fact, $tmp_array)) {
				echo FunctionsPrint::printSpecialCharacterLink($element_id);
			}
		}
		// MARRiage TYPE : hide text field and show a selection list
		if ($fact === 'TYPE' && $level === 2 && $tags[0] === 'MARR') {
			echo '<script>';
			echo 'document.getElementById(\'', $element_id, '\').style.display=\'none\'';
			echo '</script>';
			echo '<select id="', $element_id, '_sel" onchange="document.getElementById(\'', $element_id, '\').value=this.value;" >';
			foreach (['Unknown', 'Civil', 'Religious', 'Partners'] as $key) {
				if ($key === 'Unknown') {
					echo '<option value="" ';
				} else {
					echo '<option value="', $key, '" ';
				}
				$a = strtolower($key);
				$b = strtolower($value);
				if ($b !== '' && strpos($a, $b) !== false || strpos($b, $a) !== false) {
					echo 'selected';
				}
				echo '>', GedcomTag::getLabel('MARR_' . strtoupper($key)), '</option>';
			}
			echo '</select>';
		} elseif ($fact === 'TYPE' && $level === 0) {
			// NAME TYPE : hide text field and show a selection list
			$onchange = 'onchange="document.getElementById(\'' . $element_id . '\').value=this.value;"';
			echo self::editFieldNameType($element_name, $value, $onchange, $person);
			echo '<script>document.getElementById("', $element_id, '").style.display="none";</script>';
		}

		// popup links
		switch ($fact) {
		case 'DATE':
			echo self::printCalendarPopup($element_id);
			break;
		case 'FAMC':
		case 'FAMS':
			echo FunctionsPrint::printFindFamilyLink($element_id);
			break;
		case 'ALIA':
		case 'ASSO':
		case '_ASSO':
			echo FunctionsPrint::printFindIndividualLink($element_id, $element_id . '_description');
			break;
		case 'FILE':
			FunctionsPrint::printFindMediaLink($element_id, '0file');
			break;
		case 'SOUR':
			echo FunctionsPrint::printFindSourceLink($element_id, $element_id . '_description'), ' ', self::printAddNewSourceLink($element_id);
			//-- checkboxes to apply '1 SOUR' to BIRT/MARR/DEAT as '2 SOUR'
			if ($level === 1) {
				echo '<br>';
				switch ($WT_TREE->getPreference('PREFER_LEVEL2_SOURCES')) {
				case '2': // records
				$level1_checked = 'checked';
				$level2_checked = '';
				break;
				case '1': // facts
				$level1_checked = '';
				$level2_checked = 'checked';
				break;
				case '0': // none
				default:
				$level1_checked = '';
				$level2_checked = '';
				break;
				}
					if (strpos($bdm, 'B') !== false) {
						echo ' <label><input type="checkbox" name="SOUR_INDI" ', $level1_checked, ' value="1">', I18N::translate('Individual'), '</label>';
						if (preg_match_all('/(' . WT_REGEX_TAG . ')/', $WT_TREE->getPreference('QUICK_REQUIRED_FACTS'), $matches)) {
							foreach ($matches[1] as $match) {
								if (!in_array($match, explode('|', WT_EVENTS_DEAT))) {
									echo ' <label><input type="checkbox" name="SOUR_', $match, '" ', $level2_checked, ' value="1">', GedcomTag::getLabel($match), '</label>';
								}
							}
						}
					}
					if (strpos($bdm, 'D') !== false) {
						if (preg_match_all('/(' . WT_REGEX_TAG . ')/', $WT_TREE->getPreference('QUICK_REQUIRED_FACTS'), $matches)) {
							foreach ($matches[1] as $match) {
								if (in_array($match, explode('|', WT_EVENTS_DEAT))) {
									echo ' <label><input type="checkbox" name="SOUR_', $match, '"', $level2_checked, ' value="1">', GedcomTag::getLabel($match), '</label>';
								}
							}
						}
					}
					if (strpos($bdm, 'M') !== false) {
						echo ' <label><input type="checkbox" name="SOUR_FAM" ', $level1_checked, ' value="1">', I18N::translate('Family'), '</label>';
						if (preg_match_all('/(' . WT_REGEX_TAG . ')/', $WT_TREE->getPreference('QUICK_REQUIRED_FAMFACTS'), $matches)) {
							foreach ($matches[1] as $match) {
								echo ' <label><input type="checkbox" name="SOUR_', $match, '"', $level2_checked, ' value="1">', GedcomTag::getLabel($match), '</label>';
							}
						}
					}
				}
				break;
		case 'REPO':
			echo FunctionsPrint::printFindRepositoryLink($element_id), ' ', self::printAddNewRepositoryLink($element_id);
			break;
		case 'NOTE':
			// Shared Notes Icons ========================================
			if ($islink) {
				// Print regular Shared Note icons ---------------------------
				echo ' ', FunctionsPrint::printFindNoteLink($element_id, $element_id . '_description'), ' ', self::printAddNewNoteLink($element_id);
				if ($value) {
					echo ' ', self::printEditNoteLink($value);
				}
			}
			break;
		case 'OBJE':
			echo FunctionsPrint::printFindMediaLink($element_id, '1media');
			if (!$value) {
				echo ' ', self::printAddNewMediaLink($element_id);
				$value = 'new';
			}
			break;
		}

		echo '<div id="' . $element_id . '_description">';

		// current value
		if ($fact === 'DATE') {
			$date = new Date($value);
			echo $date->display();
		}
		if (($fact === 'ASSO' || $fact === '_ASSO') && $value === '') {
			if ($level === 1) {
				echo '<p class="small text-muted">' . I18N::translate('An associate is another individual who was involved with this individual, such as a friend or an employer.') . '</p>';
			} else {
				echo '<p class="small text-muted">' . I18N::translate('An associate is another individual who was involved with this fact or event, such as a witness or a priest.') . '</p>';
			}
		}

		if ($value && $value !== 'new' && $islink) {
			switch ($fact) {
			case 'ALIA':
			case 'ASSO':
			case '_ASSO':
				$tmp = Individual::getInstance($value, $WT_TREE);
				if ($tmp) {
					echo ' ', $tmp->getFullName();
				}
				break;
			case 'SOUR':
				$tmp = Source::getInstance($value, $WT_TREE);
				if ($tmp) {
					echo ' ', $tmp->getFullName();
				}
				break;
			case 'NOTE':
				$tmp = Note::getInstance($value, $WT_TREE);
				if ($tmp) {
					echo ' ', $tmp->getFullName();
				}
				break;
			case 'OBJE':
				$tmp = Media::getInstance($value, $WT_TREE);
				if ($tmp) {
					echo ' ', $tmp->getFullName();
				}
				break;
			case 'REPO':
				$tmp = Repository::getInstance($value, $WT_TREE);
				if ($tmp) {
					echo ' ', $tmp->getFullName();
				}
				break;
			}
		}

		// pastable values
		if ($fact === 'FORM' && $upperlevel === 'OBJE') {
			FunctionsPrint::printAutoPasteLink($element_id, Config::fileFormats());
		}
		echo '</div>', $extra, '</td></tr>';

		return $element_id;
	}

	/**
	 * Genearate a <select> element, with the dates/places of all known censuses
	 *
	 *
	 * @param string $locale - Sort the censuses for this locale
	 * @param string $xref   - The individual for whom we are adding a census
	 */
	public static function censusDateSelector($locale, $xref) {
		global $controller;

		// Show more likely census details at the top of the list.
		switch (WT_LOCALE) {
		case 'cs':
			$census_places = [new CensusOfCzechRepublic];
			break;
		case 'en-AU':
		case 'en-GB':
			$census_places = [new CensusOfEngland, new CensusOfWales, new CensusOfScotland];
			break;
		case 'en-US':
			$census_places = [new CensusOfUnitedStates];
			break;
		case 'fr':
		case 'fr-CA':
			$census_places = [new CensusOfFrance];
			break;
		case 'da':
			$census_places = [new CensusOfDenmark];
			break;
		case 'de':
			$census_places = [new CensusOfDeutschland];
			break;
		default:
			$census_places = [];
			break;
		}
		foreach (Census::allCensusPlaces() as $census_place) {
			if (!in_array($census_place, $census_places)) {
				$census_places[] = $census_place;
			}
		}

		$controller->addInlineJavascript('
				function selectCensus(el) {
					var option = jQuery(":selected", el);
					jQuery("input.DATE", jQuery(el).closest("table")).val(option.val());
					jQuery("input.PLAC", jQuery(el).closest("table")).val(option.data("place"));
					jQuery("input.census-class", jQuery(el).closest("table")).val(option.data("census"));
					if (option.data("place")) {
						jQuery("#assistant-link").show();
					} else {
						jQuery("#assistant-link").hide();
					}
				}
				function set_pid_array(pa) {
					jQuery("#pid_array").val(pa);
				}
				function activateCensusAssistant() {
					if (jQuery("#newshared_note_img").hasClass("icon-plus")) {
						expand_layer("newshared_note");
					}
					var field  = jQuery("#newshared_note input.NOTE")[0];
					var xref   = jQuery("input[name=xref]").val();
					var census = jQuery(".census-assistant-selector :selected").data("census");
					return addnewnote_assisted(field, xref, census);
				}
			');

		$options = '<option value="">' . I18N::translate('Census date') . '</option>';

		foreach ($census_places as $census_place) {
			$options .= '<option value=""></option>';
			foreach ($census_place->allCensusDates() as $census) {
				$date            = new Date($census->censusDate());
				$year            = $date->minimumDate()->format('%Y');
				$place_hierarchy = explode(', ', $census->censusPlace());
				$options .= '<option value="' . $census->censusDate() . '" data-place="' . $census->censusPlace() . '" data-census="' . get_class($census) . '">' . $place_hierarchy[0] . ' ' . $year . '</option>';
			}
		}

		return
			'<input type="hidden" id="pid_array" name="pid_array" value="">' .
			'<select class="census-assistant-selector" onchange="selectCensus(this);">' . $options . '</select>';
	}

	/**
	 * Prints collapsable fields to add ASSO/RELA, SOUR, OBJE, etc.
	 *
	 * @param string $tag
	 * @param int $level
	 * @param string $parent_tag
	 */
	public static function printAddLayer($tag, $level = 2, $parent_tag = '') {
		global $WT_TREE;

		switch ($tag) {
		case 'SOUR':
			echo '<a href="#" onclick="return expand_layer(\'newsource\');"><i id="newsource_img" class="icon-plus"></i> ', I18N::translate('Add a source citation'), '</a>';
			echo '<br>';
			echo '<div id="newsource" style="display: none;">';
			echo '<table class="facts_table">';
			// 2 SOUR
			self::addSimpleTag($level . ' SOUR @');
			// 3 PAGE
			self::addSimpleTag(($level + 1) . ' PAGE');
			// 3 DATA
			self::addSimpleTag(($level + 1) . ' DATA');
			// 4 TEXT
			self::addSimpleTag(($level + 2) . ' TEXT');
			if ($WT_TREE->getPreference('FULL_SOURCES')) {
				// 4 DATE
				self::addSimpleTag(($level + 2) . ' DATE', '', GedcomTag::getLabel('DATA:DATE'));
				// 3 QUAY
				self::addSimpleTag(($level + 1) . ' QUAY');
			}
			// 3 OBJE
			self::addSimpleTag(($level + 1) . ' OBJE');
			// 3 SHARED_NOTE
			self::addSimpleTag(($level + 1) . ' SHARED_NOTE');
			echo '</table></div>';
			break;

		case 'ASSO':
		case 'ASSO2':
			//-- Add a new ASSOciate
			if ($tag === 'ASSO') {
				echo "<a href=\"#\" onclick=\"return expand_layer('newasso');\"><i id=\"newasso_img\" class=\"icon-plus\"></i> ", I18N::translate('Add an associate'), '</a>';
				echo '<br>';
				echo '<div id="newasso" style="display: none;">';
			} else {
				echo "<a href=\"#\" onclick=\"return expand_layer('newasso2');\"><i id=\"newasso2_img\" class=\"icon-plus\"></i> ", I18N::translate('Add an associate'), '</a>';
				echo '<br>';
				echo '<div id="newasso2" style="display: none;">';
			}
			echo '<table class="facts_table">';
			// 2 ASSO
			self::addSimpleTag($level . ' _ASSO @');
			// 3 RELA
			self::addSimpleTag(($level + 1) . ' RELA');
			// 3 NOTE
			self::addSimpleTag(($level + 1) . ' NOTE');
			// 3 SHARED_NOTE
			self::addSimpleTag(($level + 1) . ' SHARED_NOTE');
			echo '</table></div>';
			break;

		case 'NOTE':
			//-- Retrieve existing note or add new note to fact
			echo "<a href=\"#\" onclick=\"return expand_layer('newnote');\"><i id=\"newnote_img\" class=\"icon-plus\"></i> ", I18N::translate('Add a note'), '</a>';
			echo '<br>';
			echo '<div id="newnote" style="display: none;">';
			echo '<table class="facts_table">';
			// 2 NOTE
			self::addSimpleTag($level . ' NOTE');
			echo '</table></div>';
			break;

		case 'SHARED_NOTE':
			echo "<a href=\"#\" onclick=\"return expand_layer('newshared_note');\"><i id=\"newshared_note_img\" class=\"icon-plus\"></i> ", I18N::translate('Add a shared note'), '</a>';
			echo '<br>';
			echo '<div id="newshared_note" style="display: none;">';
			echo '<table class="facts_table">';
			// 2 SHARED NOTE
			self::addSimpleTag($level . ' SHARED_NOTE', $parent_tag);
			echo '</table></div>';
			break;

		case 'OBJE':
			if ($WT_TREE->getPreference('MEDIA_UPLOAD') >= Auth::accessLevel($WT_TREE)) {
				echo "<a href=\"#\" onclick=\"return expand_layer('newobje');\"><i id=\"newobje_img\" class=\"icon-plus\"></i> ", I18N::translate('Add a media object'), '</a>';
				echo '<br>';
				echo '<div id="newobje" style="display: none;">';
				echo '<table class="facts_table">';
				self::addSimpleTag($level . ' OBJE');
				echo '</table></div>';
			}
			break;

		case 'RESN':
			echo "<a href=\"#\" onclick=\"return expand_layer('newresn');\"><i id=\"newresn_img\" class=\"icon-plus\"></i> ", I18N::translate('Add a restriction'), '</a>';
			echo '<br>';
			echo '<div id="newresn" style="display: none;">';
			echo '<table class="facts_table">';
			// 2 RESN
			self::addSimpleTag($level . ' RESN');
			echo '</table></div>';
			break;
		}
	}

	/**
	 * Add some empty tags to create a new fact.
	 *
	 * @param string $fact
	 */
	public static function addSimpleTags($fact) {
		global $WT_TREE;

		// For new individuals, these facts default to "Y"
		if ($fact === 'MARR') {
			self::addSimpleTag('0 ' . $fact . ' Y');
		} else {
			self::addSimpleTag('0 ' . $fact);
		}

		if (!in_array($fact, Config::nonDateFacts())) {
			self::addSimpleTag('0 DATE', $fact, GedcomTag::getLabel($fact . ':DATE'));
		}

		if (!in_array($fact, Config::nonPlaceFacts())) {
			self::addSimpleTag('0 PLAC', $fact, GedcomTag::getLabel($fact . ':PLAC'));

			if (preg_match_all('/(' . WT_REGEX_TAG . ')/', $WT_TREE->getPreference('ADVANCED_PLAC_FACTS'), $match)) {
				foreach ($match[1] as $tag) {
					self::addSimpleTag('0 ' . $tag, $fact, GedcomTag::getLabel($fact . ':PLAC:' . $tag));
				}
			}
			self::addSimpleTag('0 MAP', $fact);
			self::addSimpleTag('0 LATI', $fact);
			self::addSimpleTag('0 LONG', $fact);
		}
	}

	/**
	 * Assemble the pieces of a newly created record into gedcom
	 *
	 * @return string
	 */
	public static function addNewName() {
		global $WT_TREE;

		$gedrec = "\n1 NAME " . Filter::post('NAME');

		$tags = ['NPFX', 'GIVN', 'SPFX', 'SURN', 'NSFX'];

		if (preg_match_all('/(' . WT_REGEX_TAG . ')/', $WT_TREE->getPreference('ADVANCED_NAME_FACTS'), $match)) {
			$tags = array_merge($tags, $match[1]);
		}

		// Paternal and Polish and Lithuanian surname traditions can also create a _MARNM
		$SURNAME_TRADITION = $WT_TREE->getPreference('SURNAME_TRADITION');
		if ($SURNAME_TRADITION === 'paternal' || $SURNAME_TRADITION === 'polish' || $SURNAME_TRADITION === 'lithuanian') {
			$tags[] = '_MARNM';
		}

		foreach (array_unique($tags) as $tag) {
			$TAG = Filter::post($tag);
			if ($TAG) {
				$gedrec .= "\n2 {$tag} {$TAG}";
			}
		}

		return $gedrec;
	}

	/**
	 * Create a form to add a sex record.
	 *
	 * @return string
	 */
	public static function addNewSex() {
		switch (Filter::post('SEX', '[MF]', 'U')) {
		case 'M':
			return "\n1 SEX M";
		case 'F':
			return "\n1 SEX F";
		default:
			return "\n1 SEX U";
		}
	}

	/**
	 * Create a form to add a new fact.
	 *
	 * @param string $fact
	 *
	 * @return string
	 */
	public static function addNewFact($fact) {
		global $WT_TREE;

		$FACT = Filter::post($fact);
		$DATE = Filter::post($fact . '_DATE');
		$PLAC = Filter::post($fact . '_PLAC');
		if ($DATE || $PLAC || $FACT && $FACT !== 'Y') {
			if ($FACT && $FACT !== 'Y') {
				$gedrec = "\n1 " . $fact . ' ' . $FACT;
			} else {
				$gedrec = "\n1 " . $fact;
			}
			if ($DATE) {
				$gedrec .= "\n2 DATE " . $DATE;
			}
			if ($PLAC) {
				$gedrec .= "\n2 PLAC " . $PLAC;

				if (preg_match_all('/(' . WT_REGEX_TAG . ')/', $WT_TREE->getPreference('ADVANCED_PLAC_FACTS'), $match)) {
					foreach ($match[1] as $tag) {
						$TAG = Filter::post($fact . '_' . $tag);
						if ($TAG) {
							$gedrec .= "\n3 " . $tag . ' ' . $TAG;
						}
					}
				}
				$LATI = Filter::post($fact . '_LATI');
				$LONG = Filter::post($fact . '_LONG');
				if ($LATI || $LONG) {
					$gedrec .= "\n3 MAP\n4 LATI " . $LATI . "\n4 LONG " . $LONG;
				}
			}
			if (Filter::postBool('SOUR_' . $fact)) {
				return self::updateSource($gedrec, 2);
			} else {
				return $gedrec;
			}
		} elseif ($FACT === 'Y') {
			if (Filter::postBool('SOUR_' . $fact)) {
				return self::updateSource("\n1 " . $fact . ' Y', 2);
			} else {
				return "\n1 " . $fact . ' Y';
			}
		} else {
			return '';
		}
	}

	/**
	 * This function splits the $glevels, $tag, $islink, and $text arrays so that the
	 * entries associated with a SOUR record are separate from everything else.
	 *
	 * Input arrays:
	 * - $glevels[] - an array of the gedcom level for each line that was edited
	 * - $tag[] - an array of the tags for each gedcom line that was edited
	 * - $islink[] - an array of 1 or 0 values to indicate when the text is a link element
	 * - $text[] - an array of the text data for each line
	 *
	 * Output arrays:
	 * ** For the SOUR record:
	 * - $glevelsSOUR[] - an array of the gedcom level for each line that was edited
	 * - $tagSOUR[] - an array of the tags for each gedcom line that was edited
	 * - $islinkSOUR[] - an array of 1 or 0 values to indicate when the text is a link element
	 * - $textSOUR[] - an array of the text data for each line
	 * ** For the remaining records:
	 * - $glevelsRest[] - an array of the gedcom level for each line that was edited
	 * - $tagRest[] - an array of the tags for each gedcom line that was edited
	 * - $islinkRest[] - an array of 1 or 0 values to indicate when the text is a link element
	 * - $textRest[] - an array of the text data for each line
	 */
	public static function splitSource() {
		global $glevels, $tag, $islink, $text;
		global $glevelsSOUR, $tagSOUR, $islinkSOUR, $textSOUR;
		global $glevelsRest, $tagRest, $islinkRest, $textRest;

		$glevelsSOUR = [];
		$tagSOUR     = [];
		$islinkSOUR  = [];
		$textSOUR    = [];

		$glevelsRest = [];
		$tagRest     = [];
		$islinkRest  = [];
		$textRest    = [];

		$inSOUR = false;

		for ($i = 0; $i < count($glevels); $i++) {
			if ($inSOUR) {
				if ($levelSOUR < $glevels[$i]) {
					$dest = 'S';
				} else {
					$inSOUR = false;
					$dest   = 'R';
				}
			} else {
				if ($tag[$i] === 'SOUR') {
					$inSOUR    = true;
					$levelSOUR = $glevels[$i];
					$dest      = 'S';
				} else {
					$dest = 'R';
				}
			}
			if ($dest === 'S') {
				$glevelsSOUR[] = $glevels[$i];
				$tagSOUR[]     = $tag[$i];
				$islinkSOUR[]  = $islink[$i];
				$textSOUR[]    = $text[$i];
			} else {
				$glevelsRest[] = $glevels[$i];
				$tagRest[]     = $tag[$i];
				$islinkRest[]  = $islink[$i];
				$textRest[]    = $text[$i];
			}
		}
	}

	/**
	 * Add new GEDCOM lines from the $xxxSOUR interface update arrays, which
	 * were produced by the splitSOUR() function.
	 * See the FunctionsEdit::handle_updatesges() function for details.
	 *
	 * @param string $inputRec
	 * @param string $levelOverride
	 *
	 * @return string
	 */
	public static function updateSource($inputRec, $levelOverride = 'no') {
		global $glevels, $tag, $islink, $text;
		global $glevelsSOUR, $tagSOUR, $islinkSOUR, $textSOUR;

		if (count($tagSOUR) === 0) {
			return $inputRec; // No update required
		}

		// Save original interface update arrays before replacing them with the xxxSOUR ones
		$glevelsSave = $glevels;
		$tagSave     = $tag;
		$islinkSave  = $islink;
		$textSave    = $text;

		$glevels = $glevelsSOUR;
		$tag     = $tagSOUR;
		$islink  = $islinkSOUR;
		$text    = $textSOUR;

		$myRecord = self::handleUpdates($inputRec, $levelOverride); // Now do the update

		// Restore the original interface update arrays (just in case ...)
		$glevels = $glevelsSave;
		$tag     = $tagSave;
		$islink  = $islinkSave;
		$text    = $textSave;

		return $myRecord;
	}

	/**
	 * Add new GEDCOM lines from the $xxxRest interface update arrays, which
	 * were produced by the splitSOUR() function.
	 * See the FunctionsEdit::handle_updatesges() function for details.
	 *
	 * @param string $inputRec
	 * @param string $levelOverride
	 *
	 * @return string
	 */
	public static function updateRest($inputRec, $levelOverride = 'no') {
		global $glevels, $tag, $islink, $text;
		global $glevelsRest, $tagRest, $islinkRest, $textRest;

		if (count($tagRest) === 0) {
			return $inputRec; // No update required
		}

		// Save original interface update arrays before replacing them with the xxxRest ones
		$glevelsSave = $glevels;
		$tagSave     = $tag;
		$islinkSave  = $islink;
		$textSave    = $text;

		$glevels = $glevelsRest;
		$tag     = $tagRest;
		$islink  = $islinkRest;
		$text    = $textRest;

		$myRecord = self::handleUpdates($inputRec, $levelOverride); // Now do the update

		// Restore the original interface update arrays (just in case ...)
		$glevels = $glevelsSave;
		$tag     = $tagSave;
		$islink  = $islinkSave;
		$text    = $textSave;

		return $myRecord;
	}

	/**
	 * Add new gedcom lines from interface update arrays
	 * The edit_interface and FunctionsEdit::add_simple_tag function produce the following
	 * arrays incoming from the $_POST form
	 * - $glevels[] - an array of the gedcom level for each line that was edited
	 * - $tag[] - an array of the tags for each gedcom line that was edited
	 * - $islink[] - an array of 1 or 0 values to tell whether the text is a link element and should be surrounded by @@
	 * - $text[] - an array of the text data for each line
	 * With these arrays you can recreate the gedcom lines like this
	 * <code>$glevel[0].' '.$tag[0].' '.$text[0]</code>
	 * There will be an index in each of these arrays for each line of the gedcom
	 * fact that is being edited.
	 * If the $text[] array is empty for the given line, then it means that the
	 * user removed that line during editing or that the line is supposed to be
	 * empty (1 DEAT, 1 BIRT) for example. To know if the line should be removed
	 * there is a section of code that looks ahead to the next lines to see if there
	 * are sub lines. For example we don't want to remove the 1 DEAT line if it has
	 * a 2 PLAC or 2 DATE line following it. If there are no sub lines, then the line
	 * can be safely removed.
	 *
	 * @param string $newged the new gedcom record to add the lines to
	 * @param string $levelOverride Override GEDCOM level specified in $glevels[0]
	 *
	 * @return string The updated gedcom record
	 */
	public static function handleUpdates($newged, $levelOverride = 'no') {
		global $glevels, $islink, $tag, $uploaded_files, $text;

		if ($levelOverride === 'no' || count($glevels) === 0) {
			$levelAdjust = 0;
		} else {
			$levelAdjust = $levelOverride - $glevels[0];
		}

		for ($j = 0; $j < count($glevels); $j++) {

			// Look for empty SOUR reference with non-empty sub-records.
			// This can happen when the SOUR entry is deleted but its sub-records
			// were incorrectly left intact.
			// The sub-records should be deleted.
			if ($tag[$j] === 'SOUR' && ($text[$j] === '@@' || $text[$j] === '')) {
				$text[$j] = '';
				$k        = $j + 1;
				while (($k < count($glevels)) && ($glevels[$k] > $glevels[$j])) {
					$text[$k] = '';
					$k++;
				}
			}

			if (trim($text[$j]) !== '') {
				$pass = true;
			} else {
				//-- for facts with empty values they must have sub records
				//-- this section checks if they have subrecords
				$k    = $j + 1;
				$pass = false;
				while (($k < count($glevels)) && ($glevels[$k] > $glevels[$j])) {
					if ($text[$k] !== '') {
						if (($tag[$j] !== 'OBJE') || ($tag[$k] === 'FILE')) {
							$pass = true;
							break;
						}
					}
					if (($tag[$k] === 'FILE') && (count($uploaded_files) > 0)) {
						$filename = array_shift($uploaded_files);
						if (!empty($filename)) {
							$text[$k] = $filename;
							$pass     = true;
							break;
						}
					}
					$k++;
				}
			}

			//-- if the value is not empty or it has sub lines
			//--- then write the line to the gedcom record
			//-- we have to let some emtpy text lines pass through... (DEAT, BIRT, etc)
			if ($pass) {
				$newline = $glevels[$j] + $levelAdjust . ' ' . $tag[$j];
				if ($text[$j] !== '') {
					if ($islink[$j]) {
						$newline .= ' @' . $text[$j] . '@';
					} else {
						$newline .= ' ' . $text[$j];
					}
				}
				$newged .= "\n" . str_replace("\n", "\n" . (1 + substr($newline, 0, 1)) . ' CONT ', $newline);
			}
		}

		return $newged;
	}

	/**
	 * builds the form for adding new facts
	 *
	 * @param string $fact the new fact we are adding
	 */
	public static function createAddForm($fact) {
		global $tags, $WT_TREE;

		$tags = [];

		// handle  MARRiage TYPE
		if (substr($fact, 0, 5) === 'MARR_') {
			$tags[0] = 'MARR';
			self::addSimpleTag('1 MARR');
			self::insertMissingSubtags($fact);
		} else {
			$tags[0] = $fact;
			if ($fact === '_UID') {
				$fact .= ' ' . GedcomTag::createUid();
			}
			// These new level 1 tags need to be turned into links
			if (in_array($fact, ['ALIA', 'ASSO'])) {
				$fact .= ' @';
			}
			if (in_array($fact, Config::emptyFacts())) {
				self::addSimpleTag('1 ' . $fact . ' Y');
			} else {
				self::addSimpleTag('1 ' . $fact);
			}
			self::insertMissingSubtags($tags[0]);
			//-- handle the special SOURce case for level 1 sources [ 1759246 ]
			if ($fact === 'SOUR') {
				self::addSimpleTag('2 PAGE');
				self::addSimpleTag('3 TEXT');
				if ($WT_TREE->getPreference('FULL_SOURCES')) {
					self::addSimpleTag('3 DATE', '', GedcomTag::getLabel('DATA:DATE'));
					self::addSimpleTag('2 QUAY');
				}
			}
		}
	}

	/**
	 * Create a form to edit a Fact object.
	 *
	 * @param Fact $fact
	 *
	 * @return string
	 */
	public static function createEditForm(Fact $fact) {
		global $tags;

		$record = $fact->getParent();

		$tags     = [];
		$gedlines = explode("\n", $fact->getGedcom());

		$linenum = 0;
		$fields  = explode(' ', $gedlines[$linenum]);
		$glevel  = $fields[0];
		$level   = $glevel;

		$type       = $fact->getTag();
		$level0type = $record::RECORD_TYPE;
		$level1type = $type;

		$i           = $linenum;
		$inSource    = false;
		$levelSource = 0;
		$add_date    = true;

		// List of tags we would expect at the next level
		// NB add_missing_subtags() already takes care of the simple cases
		// where a level 1 tag is missing a level 2 tag. Here we only need to
		// handle the more complicated cases.
		$expected_subtags = [
			'SOUR' => ['PAGE', 'DATA'],
			'DATA' => ['TEXT'],
			'PLAC' => ['MAP'],
			'MAP'  => ['LATI', 'LONG'],
		];
		if ($record->getTree()->getPreference('FULL_SOURCES')) {
			$expected_subtags['SOUR'][] = 'QUAY';
			$expected_subtags['DATA'][] = 'DATE';
		}
		if (GedcomCodeTemp::isTagLDS($level1type)) {
			$expected_subtags['STAT'] = ['DATE'];
		}
		if (in_array($level1type, Config::dateAndTime())) {
			$expected_subtags['DATE'] = ['TIME']; // TIME is NOT a valid 5.5.1 tag
		}
		if (preg_match_all('/(' . WT_REGEX_TAG . ')/', $record->getTree()->getPreference('ADVANCED_PLAC_FACTS'), $match)) {
			$expected_subtags['PLAC'] = array_merge($match[1], $expected_subtags['PLAC']);
		}

		$stack = [];
		// Loop on existing tags :
		while (true) {
			// Keep track of our hierarchy, e.g. 1=>BIRT, 2=>PLAC, 3=>FONE
			$stack[$level] = $type;
			// Merge them together, e.g. BIRT:PLAC:FONE
			$label = implode(':', array_slice($stack, 0, $level));

			$text = '';
			for ($j = 2; $j < count($fields); $j++) {
				if ($j > 2) {
					$text .= ' ';
				}
				$text .= $fields[$j];
			}
			$text = rtrim($text);
			while (($i + 1 < count($gedlines)) && (preg_match('/' . ($level + 1) . ' CONT ?(.*)/', $gedlines[$i + 1], $cmatch) > 0)) {
				$text .= "\n" . $cmatch[1];
				$i++;
			}

			if ($type === 'SOUR') {
				$inSource    = true;
				$levelSource = $level;
			} elseif ($levelSource >= $level) {
				$inSource = false;
			}

			if ($type !== 'CONT') {
				$tags[]    = $type;
				$subrecord = $level . ' ' . $type . ' ' . $text;
				if ($inSource && $type === 'DATE') {
					self::addSimpleTag($subrecord, '', GedcomTag::getLabel($label, $record));
				} elseif (!$inSource && $type === 'DATE') {
					self::addSimpleTag($subrecord, $level1type, GedcomTag::getLabel($label, $record));
					if ($level === '2') {
						// We already have a date - no need to add one.
						$add_date = false;
					}
				} elseif ($type === 'STAT') {
					self::addSimpleTag($subrecord, $level1type, GedcomTag::getLabel($label, $record));
				} else {
					self::addSimpleTag($subrecord, $level0type, GedcomTag::getLabel($label, $record));
				}
			}

			// Get a list of tags present at the next level
			$subtags = [];
			for ($ii = $i + 1; isset($gedlines[$ii]) && preg_match('/^(\d+) (\S+)/', $gedlines[$ii], $mm) && $mm[1] > $level; ++$ii) {
				if ($mm[1] == $level + 1) {
					$subtags[] = $mm[2];
				}
			}

			// Insert missing tags
			if (!empty($expected_subtags[$type])) {
				foreach ($expected_subtags[$type] as $subtag) {
					if (!in_array($subtag, $subtags)) {
						self::addSimpleTag(($level + 1) . ' ' . $subtag, '', GedcomTag::getLabel($label . ':' . $subtag));
						if (!empty($expected_subtags[$subtag])) {
							foreach ($expected_subtags[$subtag] as $subsubtag) {
								self::addSimpleTag(($level + 2) . ' ' . $subsubtag, '', GedcomTag::getLabel($label . ':' . $subtag . ':' . $subsubtag));
							}
						}
					}
				}
			}

			$i++;
			if (isset($gedlines[$i])) {
				$fields = explode(' ', $gedlines[$i]);
				$level  = $fields[0];
				if (isset($fields[1])) {
					$type = trim($fields[1]);
				} else {
					$level = 0;
				}
			} else {
				$level = 0;
			}
			if ($level <= $glevel) {
				break;
			}
		}

		if ($level1type !== '_PRIM') {
			self::insertMissingSubtags($level1type, $add_date);
		}

		return $level1type;
	}

	/**
	 * Populates the global $tags array with any missing sub-tags.
	 *
	 * @param string $level1tag the type of the level 1 gedcom record
	 * @param bool $add_date
	 */
	public static function insertMissingSubtags($level1tag, $add_date = false) {
		global $tags, $WT_TREE;

		// handle  MARRiage TYPE
		$type_val = '';
		if (substr($level1tag, 0, 5) === 'MARR_') {
			$type_val  = substr($level1tag, 5);
			$level1tag = 'MARR';
		}

		foreach (Config::levelTwoTags() as $key => $value) {
			if ($key === 'DATE' && in_array($level1tag, Config::nonDateFacts()) || $key === 'PLAC' && in_array($level1tag, Config::nonPlaceFacts())) {
				continue;
			}
			if (in_array($level1tag, $value) && !in_array($key, $tags)) {
				if ($key === 'TYPE') {
					self::addSimpleTag('2 TYPE ' . $type_val, $level1tag);
				} elseif ($level1tag === '_TODO' && $key === 'DATE') {
					self::addSimpleTag('2 ' . $key . ' ' . strtoupper(date('d M Y')), $level1tag);
				} elseif ($level1tag === '_TODO' && $key === '_WT_USER') {
					self::addSimpleTag('2 ' . $key . ' ' . Auth::user()->getUserName(), $level1tag);
				} elseif ($level1tag === 'TITL' && strstr($WT_TREE->getPreference('ADVANCED_NAME_FACTS'), $key) !== false) {
					self::addSimpleTag('2 ' . $key, $level1tag);
				} elseif ($level1tag === 'NAME' && strstr($WT_TREE->getPreference('ADVANCED_NAME_FACTS'), $key) !== false) {
					self::addSimpleTag('2 ' . $key, $level1tag);
				} elseif ($level1tag !== 'TITL' && $level1tag !== 'NAME') {
					self::addSimpleTag('2 ' . $key, $level1tag);
				}
				// Add level 3/4 tags as appropriate
				switch ($key) {
				case 'PLAC':
					if (preg_match_all('/(' . WT_REGEX_TAG . ')/', $WT_TREE->getPreference('ADVANCED_PLAC_FACTS'), $match)) {
						foreach ($match[1] as $tag) {
							self::addSimpleTag('3 ' . $tag, '', GedcomTag::getLabel($level1tag . ':PLAC:' . $tag));
						}
					}
					self::addSimpleTag('3 MAP');
					self::addSimpleTag('4 LATI');
					self::addSimpleTag('4 LONG');
					break;
				case 'FILE':
					self::addSimpleTag('3 FORM');
					break;
				case 'EVEN':
					self::addSimpleTag('3 DATE');
					self::addSimpleTag('3 PLAC');
					break;
				case 'STAT':
					if (GedcomCodeTemp::isTagLDS($level1tag)) {
						self::addSimpleTag('3 DATE', '', GedcomTag::getLabel('STAT:DATE'));
					}
					break;
				case 'DATE':
					// TIME is NOT a valid 5.5.1 tag
					if (in_array($level1tag, Config::dateAndTime())) {
						self::addSimpleTag('3 TIME');
					}
					break;
				case 'HUSB':
				case 'WIFE':
					self::addSimpleTag('3 AGE');
					break;
				case 'FAMC':
					if ($level1tag === 'ADOP') {
						self::addSimpleTag('3 ADOP BOTH');
					}
					break;
				}
			} elseif ($key === 'DATE' && $add_date) {
				self::addSimpleTag('2 DATE', $level1tag, GedcomTag::getLabel($level1tag . ':DATE'));
			}
		}
		// Do something (anything!) with unrecognized custom tags
		if (substr($level1tag, 0, 1) === '_' && $level1tag !== '_UID' && $level1tag !== '_TODO') {
			foreach (['DATE', 'PLAC', 'ADDR', 'AGNC', 'TYPE', 'AGE'] as $tag) {
				if (!in_array($tag, $tags)) {
					self::addSimpleTag('2 ' . $tag);
					if ($tag === 'PLAC') {
						if (preg_match_all('/(' . WT_REGEX_TAG . ')/', $WT_TREE->getPreference('ADVANCED_PLAC_FACTS'), $match)) {
							foreach ($match[1] as $ptag) {
								self::addSimpleTag('3 ' . $ptag, '', GedcomTag::getLabel($level1tag . ':PLAC:' . $ptag));
							}
						}
						self::addSimpleTag('3 MAP');
						self::addSimpleTag('4 LATI');
						self::addSimpleTag('4 LONG');
					}
				}
			}
		}
	}
}
