\RequirePackage{expl3}[2023-10-10]
\ProvidesExplPackage{genealogy-profiles}{2024/12/05}{0.1}{Macros for creating genealogical profiles}

% License: CC-BY-SA 4.0
% Author: Mikkel Eide Eriksen <mikkel.eriksen@gmail.com>

\RequirePackage{genealogytree}
\RequirePackage{hyperref}

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Key setup
% 

\keys_define:nn { genealogy-profiles }
{
	name~part~order				.code:n			= {
			\seq_gset_from_clist:Nn \g__gpr_name_part_order_seq { #1 }
			\seq_map_inline:Nn \g__gpr_name_part_order_seq {
				\tl_if_exist:cF { l__gpr_#1_tl } {
					\gpr_error:nn { name-part-missing } { #1 }
				}
			}
		} ,
	name~type					.choices:nn		= {
			given~and~surname ,
			nordic~historical ,
		} {
			\int_case:nn { \l_keys_choice_int } {
				{1} { 
					\seq_gset_from_clist:Nn \g__gpr_name_part_order_seq
						{ given~name, surname }
				}
				{2} {
					\seq_gset_from_clist:Nn \g__gpr_name_part_order_seq
						{ given~name, patronymic, byname }
				}
			}
			
			
		} ,

	auto~id						.bool_set:N 	= \l__gpr_autoid_bool ,
	auto~id						.default:n		= { true } ,
	auto~id~prefix				.tl_set:N		= \l__gpr_id_prefix_tl ,

	% formatting profiles
	begin~profile				.tl_set:N		= \l__gpr_begin_profile_tl ,
	end~profile					.tl_set:N		= \l__gpr_end_profile_tl ,
	begin~header				.tl_set:N		= \l__gpr_begin_header_tl ,
	end~header					.tl_set:N		= \l__gpr_end_header_tl ,
	begin~life~events			.tl_set:N		= \l__gpr_begin_life_events_tl ,
	end~life~events				.tl_set:N		= \l__gpr_end_life_events_tl ,
	begin~content				.tl_set:N		= \l__gpr_begin_content_tl ,
	end~content					.tl_set:N		= \l__gpr_end_content_tl ,

	layout~preset				.choices:nn		= {
			clear ,
			tcolorbox
		} {
			\int_case:nn { \l_keys_choice_int } {
				{1} {
					\tl_clear:N \l__gpr_begin_profile_tl
					\tl_clear:N \l__gpr_end_profile_tl
					\tl_clear:N \l__gpr_begin_header_tl
					\tl_clear:N \l__gpr_end_header_tl
					\tl_clear:N \l__gpr_begin_life_events_tl
					\tl_clear:N \l__gpr_end_life_events_tl
					\tl_clear:N \l__gpr_begin_content_tl
					\tl_clear:N \l__gpr_end_content_tl
				}
				{2} {
					\bool_set_false:N \l__gpr_auto_header_bool
					\tl_set:Nn \l__gpr_begin_profile_tl {
						\begin{tcolorbox}[title = \gprHeader]
					}
					\tl_set:Nn \l__gpr_end_life_events_tl {
						\tcblower
					}
					\tl_set:Nn \l__gpr_end_profile_tl {
						\end{tcolorbox}
					}
				}
			}
		} ,

	reference~style				.cs_set:Np		= \gpr__reference_style_handler:nnn #1#2#3 ,
	unknown~reference~style		.cs_set:Np		= \gpr__unknown_reference_handler:nn #1#2 ,
	page~number~style			.cs_set:Np		= \gpr__page_number_handler:n #1 ,

	header~format				.tl_set:N		= \l__gpr_header_format_tl ,
	auto~header					.bool_set:N 	= \l__gpr_auto_header_bool ,

	% indexes
	full~name~index				.tl_set:N		= \l__gpr_full_name_index_tl ,

	nest~index~entries			.bool_set:N 	= \l__gpr_nest_index_entries_bool ,
	id~in~index~entries			.bool_set:N 	= \l__gpr_id_in_index_entries_bool ,
	main~index~entry~style		.tl_set:N		= \l__gpr_main_index_entry_style_tl ,
	include~unknown~in~index	.bool_set:N 	= \l__gpr_include_unknown_in_index_bool ,
	include~unknown~in~index	.default:n		= { true } ,
	include~ambiguous~in~index	.bool_set:N 	= \l__gpr_include_ambiguous_in_index_bool ,
	include~ambiguous~in~index	.default:n		= { true } ,
	use~styles~in~index			.bool_set:N		= \l__gpr_use_styles_in_index_bool ,

	debug~log					.bool_set:N 	= \l__gpr_debug_log_bool
}

\keys_define:nn { genealogy-profiles / profile }
{
	full~name					.tl_set:N		= \l__gpr_full_name_tl ,

	life~events					.tl_set:N		= \l__gpr_life_events_tl ,

	no~index					.bool_set:N 	= \l__gpr_no_index_bool ,
}

\NewDocumentCommand { \gprKeys } { +m } {
	\keys_set:nn { genealogy-profiles } { #1 }
}

\cs_new_protected:Nn \gpr_create_style_keys:nn
{
	\keys_define:nn { genealogy-profiles } {
		#1~style				.tl_set:c		= { l__gpr_#1_style_tl } ,
		#1~index				.tl_set:c		= { l__gpr_#1_index_tl } ,
	}
	\keys_define:nn { genealogy-profiles / profile } {
		#1						.tl_set:c		= { l__gpr_#1_tl } ,
	}

	\tl_set:cn { l__gpr_#1_cmd_tl } { #2 }
	\exp_args:Nc \NewDocumentCommand { #2 Style } { m } {
		\gpr_name_part_style:nn { #1 } { ##1 }
	}
}
\cs_generate_variant:Nn \gpr_create_style_keys:nn {
	nV
}

\NewDocumentCommand { \gprCreateNamePart } { m o } {
	\IfValueTF { #2 } {
		\tl_set:Nn \l_tmpa_tl { #2 }
	} {
		\tl_set:Nn \l_tmpa_tl { gpr }
		\seq_set_split:Nnn \l_tmpa_seq { ~ } { #1 }
		\seq_map_inline:Nn \l_tmpa_seq {
			\tl_put_right:Nn \l_tmpa_tl { \text_titlecase_first:n { ##1 } }
		}
	}
	\gpr_create_style_keys:nV { #1 } \l_tmpa_tl
	\seq_new:c { l__gpr_#1_index_before_seq }
	\seq_new:c { l__gpr_#1_index_after_seq }
}

\gprCreateNamePart { given~name }
\gprCreateNamePart { patronymic }
\gprCreateNamePart { surname }
\gprCreateNamePart { byname }
\gpr_create_style_keys:nn { id } { gprID }

% set some defaults
\keys_set:nn { genealogy-profiles } {
	auto~id ,
	include~unknown~in~index ,
	include~ambiguous~in~index ,
	name~type = { given~and~surname } ,
	header~format = { \gprFullName \hfill \gprID } ,
	reference~style = {
			#1 \gpr_super_subscript:nn {#2} {#3}
		} ,
	page~number~style = {
			p.\nobreakspace#1
		} ,
	unknown~reference~style = {
			\color_select:n {red}
			#1
			\tl_if_novalue:nF {#2} { ~ (#2) }
		} ,
}

\cs_generate_variant:Nn \gpr__unknown_reference_handler:nn {
	Vn, nV
}

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Errors and warnings
% 

\msg_new:nnn
{ genealogy-profiles } { reference-recorded }
{ Reference ~ recorded ~ from ~ #1 ~ to ~ #2 ~ on ~ input ~ line ~ #3 }

\msg_new:nnn
{ genealogy-profiles } { ambiguous-reference }
{ Ambiguous ~ reference: ~ #1 ~ on ~ input ~ line ~ #2, ~
  use ~ IDs ~ to ~ fix ~ references }

\msg_new:nnn
{ genealogy-profiles } { unknown-reference-id }
{ Reference ~ to ~ unknown ~ id: ~ #1 ~ on ~ input ~ line ~ #2 }

\msg_new:nnn
{ genealogy-profiles } { unknown-reference-name }
{ Reference ~ to ~ unknown ~ name: ~ #1 ~ on ~ input ~ line ~ #2 }

\msg_new:nnn
{ genealogy-profiles } { missing-backref }
{ Missing ~ backreference: ~ #1 ~ refers ~ to ~ #2 ~ but ~ not ~ vice ~ versa ~
  on ~ input ~ line ~ #3 }

\msg_new:nnnn
{ genealogy-profiles } { missing-id }
{ You ~ have ~ not ~ assigned ~ an ~ ID ~ to ~ profile: ~ #1 ~ on ~ input ~ line ~ #2 }
{ You ~ must ~ either ~ give ~ each ~ profile ~ a ~ unique ~ ID, ~
  or ~ let ~ them ~ be ~ autogenerated. }

\msg_new:nnnn
{ genealogy-profiles } { reused-id }
{ You ~ have ~ assigned ~ an ~ ID ~ that ~ is ~ already ~ in ~ use: ~ #1 ~
  on ~ input ~ line ~ #2 }
{ You ~ must ~ either ~ give ~ every ~ profile ~ a ~ unique ~ ID, ~
  or ~ let ~ them ~ be ~ autogenerated. }

\msg_new:nnn
{ genealogy-profiles } { ambiguous-rawname }
{ Multiple ~ profiles ~ with ~ the ~ name ~ '#1', ~
  you ~ will ~ need ~ to ~ use ~ IDs ~ to ~ reference ~ them }

\msg_new:nnn
{ genealogy-profiles } { profile-count }
{ There ~ are ~ #1 ~ profiles ~ in ~ this ~ document. }

\msg_new:nnnn
{ genealogy-profiles } { name-part-missing }
{ Use ~ of ~ unknown ~ name ~ part ~ "#1" ~ on ~ input ~ line ~ #2 }
{ Create ~ it ~ with ~ gprCreateNamePart ~ first. }

\msg_new:nnn
{ genealogy-profiles } { outside-profile }
{ The ~ #1 ~ macro  ~ should ~ only ~ be ~ used ~ inside ~ profiles, ~ on ~ input ~ line ~ #2 }

\cs_new_protected:Nn \gpr_warning:nn
{
	\msg_warning:nnee { genealogy-profiles } { #1 } { #2 } { \the\inputlineno }
}

\cs_new_protected:Nn \gpr_warning:nnn
{
	\msg_warning:nneee { genealogy-profiles } { #1 } { #2 } { #3 } { \the\inputlineno }
}

\cs_new_protected:Nn \gpr_error:nn
{
	\msg_error:nnee { genealogy-profiles } { #1 } { #2 } { \the\inputlineno }
}

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Extensions to expl3 library
% 

\prg_generate_conditional_variant:Nnn \property_if_recorded:nn
	{ en } { T, TF }

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Properties
% 

\seq_clear_new:N \l__gpr_floruit_years_seq
\property_new:nnnn { floruit-years } { now } { \c_novalue_tl } {
	\seq_use:Nn \l__gpr_floruit_years_seq {,}
}

\tl_clear_new:N \l__gpr_full_name_raw_tl
\property_new:nnnn { raw-name } { now } { \c_novalue_tl } {
	\tl_use:N \l__gpr_full_name_raw_tl
}

\prop_clear_new:N \g__gpr_name_to_id_prop % saved at end, loaded into _active at start
\prop_clear_new:N \g__gpr_name_to_id_active_prop % used for lookups during run
\property_new:nnnn { names-ids } { now } { \c_novalue_tl } {
	\prop_to_keyval:N \g__gpr_name_to_id_prop
}

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Names
% 

\cs_new_protected:Nn \gpr_name_handler:n
{
	\tl_set:Nn \l__gpr_full_name_tl {#1}
	\regex_replace_all:nnN { \s*_\s* } { \  } \l__gpr_full_name_tl

	\seq_set_split:Nnn \l_tmpa_seq {~} {#1}
	\seq_map_indexed_inline:Nn \g__gpr_name_part_order_seq {
		\tl_set:Ne \l_tmpa_tl { \seq_item:Nn \l_tmpa_seq {##1} }
		\regex_replace_all:nnN { \s*_\s* } { \  } \l_tmpa_tl
		\tl_if_blank:VTF \l_tmpa_tl {
			\tl_clear:c { l__gpr_##2_tl }
		} {
			\tl_set:cV { l__gpr_##2_tl } \l_tmpa_tl
		}
	}

	\tl_clear_new:N \l__gpr_styled_name_tl
	\gpr_style_name:N \l__gpr_styled_name_tl
}
\cs_generate_variant:Nn \gpr_name_handler:n {
	V, e
}

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% IDs
% 

\cs_new_protected:Nn \gpr_map_name_to_id:Nnn
{
	% check if name already has an associated id
	\prop_get:NVNTF #1 \l__gpr_full_name_raw_tl \l_tmpb_tl
	{
		% mark name ambiguous if already reserved
		\tl_if_eq:NNF \l__gpr_id_tl \l_tmpb_tl
		{
			\msg_note:nne { genealogy-profiles } { ambiguous-rawname }
				{ \tl_use:N \l__gpr_full_name_raw_tl }
			\prop_gput:NVn #1 \l__gpr_full_name_raw_tl {_AMBIGUOUS_}
		}
	}
	{
		% otherwise save raw name -> id
		\prop_gput:NVV #1 \l__gpr_full_name_raw_tl \l__gpr_id_tl
	}
}
\cs_generate_variant:Nn \gpr_map_name_to_id:Nnn {
	NVV
}

\seq_clear_new:N \g__gpr_used_profile_ids_seq

\cs_new_protected:Nn \gpr_generate_unique_id:
{
	\tl_clear_new:N \l_tmpa_tl

	\tl_put_right:NV \l_tmpa_tl \l__gpr_id_prefix_tl

	% build base id from initials
	\seq_map_indexed_inline:Nn \g__gpr_name_part_order_seq {
		\tl_if_empty:cTF { l__gpr_##2_tl } {
			\tl_put_right:Ne \l_tmpa_tl { - }
		} {
			\tl_put_right:Ne \l_tmpa_tl { \tl_item:cn { l__gpr_##2_tl } {1} }
		}
	}

	% find unused variant
	\int_zero_new:N \l__gpr_id_index_int
	\bool_set_false:N \l__gpr_id_found_bool

	\bool_do_until:nn { \l__gpr_id_found_bool } {
		\int_incr:N \l__gpr_id_index_int
		\tl_set:Ne \l__gpr_id_tl {
				\tl_use:N \l_tmpa_tl
				\int_use:N \l__gpr_id_index_int
			}
		\seq_if_in:NVTF \g__gpr_used_profile_ids_seq \l__gpr_id_tl
			{
				\bool_set_false:N \l__gpr_id_found_bool
			}
			{
				% mark id as used
				\seq_gput_right:NV \g__gpr_used_profile_ids_seq \l__gpr_id_tl

				\bool_set_true:N \l__gpr_id_found_bool
			}
	}
}

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% References
% 

\prop_new:N \g__gpr_references_prop

\cs_new_protected:Nn \gpr_save_reference:nn
{
	\prop_if_in:NnTF \g__gpr_references_prop { #1 } {
		\prop_get:NnN \g__gpr_references_prop { #1 } \l_tmpa_tl
	} {
		\tl_clear:N \l_tmpa_tl
	}

	% wrap ID as _ID_ so we can match them out later
	\tl_put_right:Nn \l_tmpa_tl { _#2_ }

	\prop_gput:NnV \g__gpr_references_prop { #1 } \l_tmpa_tl

%	\msg_note:nneee { genealogy-profiles } { reference-recorded }
%		{ #1 } { #2 } {\the\inputlineno}
}
\cs_generate_variant:Nn \gpr_save_reference:nn {
	Vn
}

\cs_new_protected:Nn \gpr_check_backrefs:nn
{
	\regex_extract_all:nnN { _([^_]+)_ } {#2} \l_tmpa_seq
	\seq_map_inline:Nn \l_tmpa_seq {
		% dont test entire matches (_ID_), only submatches (ID)
		\tl_if_eq:neF { _ } { \tl_item:nn { ##1 } {1} }
		{
			\seq_if_in:NnT \g__gpr_used_profile_ids_seq { ##1 } {
				\prop_get:NnN \g__gpr_references_prop {##1} \l_tmpa_tl
				\regex_match:nVF { #1 } { \l_tmpa_tl } {
					\gpr_warning:nnn { missing-backref } { #1 } { ##1 }
				}
			}
		}
	}
}

\cs_new:Nn \gpr_init_names_to_keys:n
{
	\tl_if_novalue:nF {#1} {
		\prop_set_from_keyval:Nn \g__gpr_name_to_id_active_prop {#1}
	}
}
\cs_generate_variant:Nn \gpr_init_names_to_keys:n {
	e
}

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Hooks
% 

\AddToHook{begindocument/end}
{
	\gpr_init_names_to_keys:e { \property_ref:nn { gpr } { names-ids } }
}

\AddToHook{shipout/lastpage}
{
	\property_record:ee { gpr } { names-ids }
	\prop_map_inline:Nn \g__gpr_references_prop {
		\gpr_check_backrefs:nn {##1} {##2}
	}
	\msg_note:nne { genealogy-profiles } { profile-count }
		{ \seq_count:N \g__gpr_used_profile_ids_seq }
}

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Indexing
% 

\cs_new:Nn \gpr_add_to_named_index:nnN {
	\index[#1]{
		#2
		\bool_if:nT { #3 && !\tl_if_blank_p:V \l__gpr_main_index_entry_style_tl }
			{ | \tl_use:N \l__gpr_main_index_entry_style_tl }
	}
}
\cs_generate_variant:Nn \gpr_add_to_named_index:nnN {
	VVN, VeN, vVN
}

\cs_new_protected:Nn \gpr_get_name_parts:NNN
{
	\seq_clear:N #1
	\seq_map_inline:Nn #2 {
		\tl_if_empty:cF { l__gpr_##1_tl } {
			\bool_if:NTF #3 {
				\seq_put_right:Ne #1 {
					\use:c
						{ \tl_use:c { l__gpr_##1_cmd_tl } Style }
						{ \tl_use:c { l__gpr_##1_tl } }
				}
			} {
				\seq_put_right:Nv #1 { l__gpr_##1_tl }
			}
		}
	}
}
\cs_generate_variant:Nn \gpr_get_name_parts:NNN {
	NcN
}


\cs_new:Nn \gpr_build_index_entry:nnN {
	\tl_if_empty:cF { l__gpr_#1_tl } {
		% build before/after seqs if not defined
		\seq_if_empty:cT { l__gpr_#1_index_before_seq } {
			\tl_set:Nn \l_tmpa_tl { l__gpr_#1_index_after_seq }
			\seq_map_inline:Nn \g__gpr_name_part_order_seq {
				\tl_if_eq:nnT { #1 } { ##1 } {
					\tl_set:Nn \l_tmpa_tl { l__gpr_#1_index_before_seq }
				}
				\seq_put_right:cn { \tl_use:N \l_tmpa_tl } { ##1 }
			}
		}

		\gpr_get_name_parts:NcN \l_tmpa_seq { l__gpr_#1_index_before_seq } \c_false_bool
		\tl_set:Ne \l_tmpa_tl {
			\seq_use:Nn \l_tmpa_seq {~}
		}
		\bool_if:NTF \l__gpr_nest_index_entries_bool {
			% nested entries
			\bool_if:NT \l__gpr_use_styles_in_index_bool {
				\gpr_get_name_parts:NcN \l_tmpb_seq { l__gpr_#1_index_before_seq } \c_true_bool
				\tl_put_right:Ne \l_tmpa_tl { @ \seq_use:Nn \l_tmpb_seq {~} }
			}
	
			\gpr_get_name_parts:NcN \l_tmpa_seq { l__gpr_#1_index_after_seq } \c_false_bool
			\tl_put_right:Ne \l_tmpa_tl {
				!
				\seq_use:Nn \l_tmpa_seq {~}
				\bool_if:NT \l__gpr_id_in_index_entries_bool { ~ (#2) }
			}
			\bool_if:NT \l__gpr_use_styles_in_index_bool {
				\gpr_get_name_parts:NcN \l_tmpb_seq { l__gpr_#1_index_after_seq } \c_true_bool
				\tl_put_right:Ne \l_tmpa_tl {
					@
					\seq_use:Nn \l_tmpb_seq {~}
					\bool_if:NT \l__gpr_id_in_index_entries_bool { ~ (\gprIDStyle{#2}) }
				}
			}
		} {
			% un-nested entries

			% we only use a comma if there is an after seq
			\int_compare:nNnTF { \seq_count:c { l__gpr_#1_index_after_seq } } = { 0 } {
				\tl_set:Nn \l_tmpb_tl {  }
			} {
				\tl_set:Nn \l_tmpb_tl { ,~ }
			}

			\gpr_get_name_parts:NcN \l_tmpb_seq { l__gpr_#1_index_after_seq } \c_false_bool
			\tl_put_right:Ne \l_tmpa_tl {
				\tl_use:N \l_tmpb_tl
				\seq_use:Nn \l_tmpb_seq {~}
				\bool_if:NT \l__gpr_id_in_index_entries_bool { ~ (#2) }
			}
			\bool_if:NT \l__gpr_use_styles_in_index_bool {
				\gpr_get_name_parts:NcN \l_tmpa_seq { l__gpr_#1_index_before_seq } \c_true_bool
				\gpr_get_name_parts:NcN \l_tmpb_seq { l__gpr_#1_index_after_seq } \c_true_bool
				\tl_put_right:Ne \l_tmpa_tl {
					@
					\seq_use:Nn \l_tmpa_seq {~}
					\tl_use:N \l_tmpb_tl
					\seq_use:Nn \l_tmpb_seq {~}
					\bool_if:NT \l__gpr_id_in_index_entries_bool { ~ (\gprIDStyle{#2}) }
				}
			}
		}

		\gpr_add_to_named_index:vVN { l__gpr_#1_index_tl } \l_tmpa_tl #3
	}
}

\cs_new:Nn \gpr_add_to_indexes:nN
{
	\tl_if_blank:VF \l__gpr_id_index_tl {
		\tl_if_novalue:nF {#1} {
			\gpr_add_to_named_index:VeN \l__gpr_id_index_tl
				{
					#1 ~ \tl_use:N \l__gpr_full_name_tl
					\bool_if:NT \l__gpr_use_styles_in_index_bool {
						@ \gprIDStyle{#1} ~ \tl_use:N \l__gpr_styled_name_tl
					}
				}
				#2
		}
	}

	\tl_if_blank:VF \l__gpr_full_name_index_tl {
		\gpr_add_to_named_index:VeN
			\l__gpr_full_name_index_tl
			{
				\bool_if:NTF \l__gpr_use_styles_in_index_bool {
					\tl_use:N \l__gpr_styled_name_tl
				} {
					\tl_use:N \l__gpr_full_name_tl
				}
				\bool_if:NT \l__gpr_id_in_index_entries_bool { ~ (\gprIDStyle{\tl_use:N #1}) }
			}
			#2
	}

	\seq_map_inline:Nn \g__gpr_name_part_order_seq {
		\tl_if_empty:cF { l__gpr_##1_index_tl } {
			\gpr_build_index_entry:nnN { ##1 } { #1 } #2
		}
	}
}
\cs_generate_variant:Nn \gpr_add_to_indexes:nN {
	VN
}

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Styling
% 

\cs_new_protected:Nn \gpr_style_name:N
{
	\seq_clear_new:N \l__gpr_styled_name_seq

	\seq_map_inline:Nn \g__gpr_name_part_order_seq {
		\tl_if_empty:cF { l__gpr_##1_tl } {
			\seq_put_right:Ne \l__gpr_styled_name_seq {
				\use:c
					{ \tl_use:c { l__gpr_##1_cmd_tl } Style }
					{ \tl_use:c { l__gpr_##1_tl } }
			}
		}
	}

	\tl_set:Nn #1 { \seq_use:Nn \l__gpr_styled_name_seq {~} }
}

\cs_new_protected:Nn \gpr_name_part_style:nn
{
	\group_begin:
		\tl_use:c { l__gpr_#1_style_tl }
		#2
	\group_end:
}
\cs_generate_variant:Nn \gpr_name_part_style:nn {
	nv, nV
}

\cs_new_protected:Nn \gpr_format_floruit:N
{
	\int_compare:nNnTF { \seq_count:N \l__gpr_floruit_years_seq } = {1}
	{
		\tl_set:Ne #1 { floruit- = { \seq_item:Nn \l__gpr_floruit_years_seq {1} } }
	}
	{
		\int_zero_new:N \l__gpr_floruit_from_int
		\int_set:Nn \l__gpr_floruit_from_int { 9999 }
		\int_zero_new:N \l__gpr_floruit_to_int
		\int_set:Nn \l__gpr_floruit_to_int { 0000 }
		\seq_map_inline:Nn \l__gpr_floruit_years_seq {
			\int_set:Nn \l__gpr_floruit_from_int
				{ \int_min:nn {##1} { \l__gpr_floruit_from_int } }
			\int_set:Nn \l__gpr_floruit_to_int
				{ \int_max:nn {##1} { \l__gpr_floruit_to_int } }
		}
		\tl_set:Ne #1 {
			floruit- = {
				\int_use:N \l__gpr_floruit_from_int / \int_use:N \l__gpr_floruit_to_int
			}
		}
	}
}

\cs_new_protected:Nn \gpr_super_subscript:nn
{
	\group_begin:
		\hbox_set:Nn \l_tmpa_box { \textsuperscript {#1} }
		\hbox_set:Nn \l_tmpb_box { \textsubscript {#2} }
		\hbox_to_wd:nn
			{ \dim_max:nn { \box_wd:N \l_tmpa_box } { \box_wd:N \l_tmpb_box } }
			{ \hbox_overlap_right:n { \box_use:N \l_tmpa_box } \box_use:N \l_tmpb_box }
	\group_end:
}

\cs_new_protected:Nn \gpr_typeset_reference:nnnN
{
	\property_if_recorded:nTF { gpr/#1/target }
	{
		\hyperlink{ \property_ref:nn { gpr/#1/target } { target } } {
			\gpr__reference_style_handler:nnn {
				% render styled #3 only if present, otherwise use #2
				\tl_if_blank:nTF { #3 } { #2 } {
					\group_begin:
						\gpr_name_handler:n {#3}
						\tl_use:N \l__gpr_styled_name_tl
					\group_end:
				}
			} {
				#1
			} {
				\gpr__page_number_handler:n {
					\property_ref:nnn { gpr/#1/target } { page } {
						\gpr__unknown_reference_handler:nV { \bfseries ?? } \c_novalue_tl
					}
				}
			}
		}
		\bool_if:NF #4 {
			\gpr_add_to_indexes:nN { #1 } \c_false_bool
		}
	}
	{
		\gpr_warning:nn { unknown-reference-id } { #1 }
		\gpr__unknown_reference_handler:nn { #2 } { unknown~id: ~ #1 }
		\bool_if:NF #4 {
			\gpr_add_to_indexes:nN { unknown~id: ~ #1 } \c_false_bool
		}
	}
	\gpr_save_reference:Vn \l__gpr_id_tl { #1 }
}
\cs_generate_variant:Nn \gpr_typeset_reference:nnnN {
	nVnN, VVnN
}

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% genealogytree package integration
% 

\cs_new_protected:Nn \gpr_set_gtr_keys:n
{
	\gtrset{ #1 }
}

\cs_new_protected:Nn \gpr_set_gtr_database_keys:n
{
	\gpr_set_gtr_keys:n{ database/.cd, #1 }
}
\cs_generate_variant:Nn \gpr_set_gtr_database_keys:n {
	V
}

\cs_new_protected:Nn \gpr_print_gtr_database:
{
	\gtrPrintDatabase
}

\prg_new_protected_conditional:Nnn \gpr_if_lifespan_defined: {T,F,TF}
{
	\bool_if:nTF {
		 ( \cs_if_exist_p:c { gtrDBbirthyear }
		|| \cs_if_exist_p:c { gtrDBbaptismyear } )
		&&
		 ( \cs_if_exist_p:c { gtrDBdeathyear }
		|| \cs_if_exist_p:c { gtrDBburialyear } )
	} {
		\prg_return_true:
	} {
		\prg_return_false:
	}
}

\gtrDeclareDatabaseFormat{gtr_db_format}{}{%
  \begin{gtrprintlist}{\par}{\unskip,\ }{\unskip.}{\unskip}%
    \gtr@list@event{birth}%
    \gtr@list@event{baptism}%
    \gpr_if_lifespan_defined:F {
    	\gtr@list@event{floruit}
    }
    \gtr@list@event{death}%
    \gtr@list@event{burial}%
  \end{gtrprintlist}%
  \gtr@print@infolist%
}
\gpr_set_gtr_keys:n { database~format = gtr_db_format }

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Environments
% 

\cs_new_protected:Nn \gpr_create_profile_macro:nnn
{
	\exp_args:Nc \DeclareDocumentCommand { #1 } { s }
		{
			\IfBooleanTF { ##1 } { #2 } { #3 }
		}
}
\cs_generate_variant:Nn \gpr_create_profile_macro:nnn {
	vvn
}

\NewDocumentEnvironment { gprProfile* } { m O{} } {
	\bool_if:NT \l__gpr_debug_log_bool {
		\tl_log:e {\the\inputlineno}
		\tl_log:n {BEGIN ~ #1}
	}
	\group_begin:
		\keys_set:nn { genealogy-profiles / profile } { #1, life~events={#2} }

		\tl_if_empty:NTF \l__gpr_full_name_tl {
			% if no full name supplied, build one from given parts
			\seq_clear:N \l_tmpa_seq
			\seq_map_inline:Nn \g__gpr_name_part_order_seq {
				\tl_if_empty:cF { l__gpr_##1_tl } {
					\seq_put_right:Nv \l_tmpa_seq { l__gpr_##1_tl }
				}
			}
			\tl_set:Ne \l__gpr_full_name_tl { \seq_use:Nn \l_tmpa_seq {~} }
			\tl_set_eq:NN \l__gpr_full_name_raw_tl \l__gpr_full_name_tl
			\tl_clear_new:N \l__gpr_styled_name_tl
			\gpr_style_name:N \l__gpr_styled_name_tl
		} {
			% otherwise use full name to split into parts
			\tl_set_eq:NN \l__gpr_full_name_raw_tl \l__gpr_full_name_tl
			\gpr_name_handler:V \l__gpr_full_name_tl
		}

		% ensure unique id
		\tl_if_empty:NTF \l__gpr_id_tl {
			\bool_if:NTF \l__gpr_autoid_bool {
				\gpr_generate_unique_id:
			} {
				\gpr_error:nn { missing-id } { \l__gpr_full_name_tl }
			}
		} {
			\seq_if_in:NVT \g__gpr_used_profile_ids_seq \l__gpr_id_tl {
				\gpr_error:nn { reused-id } { \l__gpr_id_tl }
			}
			\seq_gput_right:NV \g__gpr_used_profile_ids_seq \l__gpr_id_tl
		}
		\gpr_map_name_to_id:NVV \g__gpr_name_to_id_prop \l__gpr_full_name_raw_tl \l__gpr_id_tl
		\gpr_map_name_to_id:NVV \g__gpr_name_to_id_active_prop \l__gpr_full_name_raw_tl \l__gpr_id_tl

		% store raw name in aux
		\property_record:ee { gpr/\tl_use:N \l__gpr_id_tl/name } { raw-name }

		% init floruit years
		\property_if_recorded:enT { gpr/\tl_use:N \l__gpr_id_tl/floruit } { floruit-years } {
			\tl_set:Ne \l_tmpa_tl {
				\property_ref:ee { gpr/\tl_use:N \l__gpr_id_tl/floruit } { floruit-years }
			}
			\seq_set_split:NnV \l__gpr_floruit_years_seq {,} \l_tmpa_tl

			\tl_clear_new:N \l__formatted_gpr_floruit_tl
			\gpr_format_floruit:N \l__formatted_gpr_floruit_tl
			\tl_put_right:Ne \l__gpr_life_events_tl {
				,
				\tl_use:N \l__formatted_gpr_floruit_tl
			}
		}
		\seq_clear_new:N \l__gpr_floruit_years_seq

		% placeholders for id and names
		\cs_set:Npn \gprHeader { \tl_use:N \l__gpr_header_format_tl }
		\gpr_create_profile_macro:nnn { gprID } {
			\tl_use:N \l__gpr_id_tl
		} {
			\gpr_name_part_style:nV { id } \l__gpr_id_tl
		}
		\gpr_create_profile_macro:nnn { gprFullName } {
			\tl_use:N \l__gpr_full_name_tl
		} {
			\tl_use:N \l__gpr_styled_name_tl
		}
		% name parts
		\seq_map_inline:Nn \g__gpr_name_part_order_seq {
			\gpr_create_profile_macro:vvn { l__gpr_##1_cmd_tl }
				{ l__gpr_##1_tl }
				{
					\gpr_name_part_style:nv { ##1 } { l__gpr_##1_tl }
				}
		}

		% set hyperref target just before typesetting
		\MakeLinkTarget*{ gpr/\tl_use:N \l__gpr_id_tl/target }

		% begin typesetting
		\tl_use:N \l__gpr_begin_profile_tl

		% record properties for id
		\property_record:ee { gpr/\tl_use:N \l__gpr_id_tl/target } { page, target }

		% add to indexes unless key set
		\bool_if:NF \l__gpr_no_index_bool {
			\gpr_add_to_indexes:VN \l__gpr_id_tl \c_true_bool
		}

		\bool_if:NT \l__gpr_auto_header_bool {
			\tl_use:N \l__gpr_begin_header_tl
			\tl_use:N \l__gpr_header_format_tl
			\tl_use:N \l__gpr_end_header_tl
		}

		% typeset life events
		\tl_if_empty:NF \l__gpr_life_events_tl {
			\tl_use:N \l__gpr_begin_life_events_tl

			\gpr_set_gtr_database_keys:V \l__gpr_life_events_tl
			\gpr_print_gtr_database:

			\tl_use:N \l__gpr_end_life_events_tl
		}

		% begin content
		\tl_use:N \l__gpr_begin_content_tl
}{
		% end content
		\tl_use:N \l__gpr_end_content_tl

		% end typesetting
		\tl_use:N \l__gpr_end_profile_tl

		% save floruit years including updates from \gprYear
		\seq_if_empty:NF \l__gpr_floruit_years_seq {
			\property_record:ee { gpr/\tl_use:N \l__gpr_id_tl/floruit } { floruit-years }
		}
	\group_end:
	\bool_if:NT \l__gpr_debug_log_bool {
		\tl_log:e {\the\inputlineno}
		\tl_log:n {END ~ #1}
	}
}

\NewDocumentEnvironment { gprProfile } { O{} m O{} } {
	\begin{gprProfile*}{full~name={#2}, #1}[#3]
}{
	\end{gprProfile*}
}

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Reference and name macros
% 

\cs_new_protected:Nn \gpr_cmd_ref:Nnn
{
	\group_begin:
		\tl_if_empty:nTF {#2} 
		{
			% no id provided, so we use the name to try looking it up
			\gpr_name_handler:n { #3 }
			\prop_get:NnNTF \g__gpr_name_to_id_active_prop {#3} \l_tmpa_tl
			{
				\str_if_eq:VnTF \l_tmpa_tl {_AMBIGUOUS_}
				{
					\gpr_warning:nn { ambiguous-reference } { \l__gpr_full_name_tl }
					\gpr__unknown_reference_handler:Vn \l__gpr_styled_name_tl
						{ ambiguous }
					\bool_if:NT \l__gpr_include_ambiguous_in_index_bool {
						\gpr_add_to_indexes:nN { ambiguous~name } \c_false_bool
					}
				}
				{
					\gpr_typeset_reference:VVnN \l_tmpa_tl \l__gpr_styled_name_tl {} #1
				}
			}
			{
				\gpr_warning:nn { unknown-reference-name } { #3 }
				\gpr__unknown_reference_handler:Vn \l__gpr_styled_name_tl { unknown~name }
				\bool_if:NT \l__gpr_include_unknown_in_index_bool {
					\gpr_add_to_indexes:nN { unknown~name } \c_false_bool
				}
			}
		}
		{
			% id provided, find the name to use for indexes
			\property_if_recorded:nTF { gpr/#2/name } {
				\gpr_name_handler:e {
					\property_ref:nn { gpr/#2/name } { raw-name }
				}
			} {
				\gpr_name_handler:n {#3}
			}
			\gpr_typeset_reference:nVnN {#2} \l__gpr_styled_name_tl {#3} #1
		}
	\group_end:
}
\cs_generate_variant:Nn \gpr_cmd_ref:Nnn {
	Nee
}
\NewDocumentCommand { \gprRef } { s O{} m } {
	\gpr_cmd_ref:Nee #1 {#2} {#3}
}


\NewDocumentCommand { \gprName } { s m } {
	\group_begin:
		\gpr_name_handler:n { #2 }
		\tl_use:N \l__gpr_styled_name_tl
		\bool_if:nT { !#1 && \l__gpr_include_unknown_in_index_bool }
			{
				\bool_set_false:N \l__gpr_id_in_index_entries_bool
				\gpr_add_to_indexes:nN { unknown } \c_false_bool
			}
	\group_end:
}

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Floruit macros
% 

\cs_new_protected:Nn \gpr_cmd_year:n
{
	% record floruit year
	\seq_if_in:NnF \l__gpr_floruit_years_seq {#1} {
		\seq_gput_right:Nn \l__gpr_floruit_years_seq {#1}
	}
}
\cs_generate_variant:Nn \gpr_cmd_year:n {
	e
}
\NewDocumentCommand { \gprYear } { s m } {
	\tl_if_empty:NT \l__gpr_id_tl {
		\gpr_error:nn { outside-profile } { gprYear }
	}

	\gpr_cmd_year:e {#2}
	\bool_if:NF { #1 } { #2 }
}


\cs_new_protected:Nn \gpr_cmd_years:n
{
	% split year range
	\seq_set_split:Nnn \l_tmpa_seq { - } {#1}

	% first year
	\seq_pop_left:NN \l_tmpa_seq \l_tmpa_tl
	\seq_if_in:NVF \l__gpr_floruit_years_seq \l_tmpa_tl {
		\seq_gput_right:NV \l__gpr_floruit_years_seq \l_tmpa_tl
	}

	% last year
	\seq_pop_right:NN \l_tmpa_seq \l_tmpb_tl
	\int_set:Nn \l_tmpa_int { \tl_count:N \l_tmpa_tl - \tl_count:N \l_tmpb_tl }
	\tl_put_left:Ne \l_tmpb_tl {
		\tl_range:Nnn \l_tmpa_tl { 1 } { \l_tmpa_int }
	}
	\seq_if_in:NVF \l__gpr_floruit_years_seq \l_tmpb_tl {
		\seq_gput_right:NV \l__gpr_floruit_years_seq \l_tmpb_tl
	}
}
\cs_generate_variant:Nn \gpr_cmd_years:n {
	e
}
\NewDocumentCommand { \gprYears } { s m } {
	\tl_if_empty:NT \l__gpr_id_tl (
		\gpr_error:nn { outside-profile } { gprYears }
	)

	\gpr_cmd_years:e {#2}
	\bool_if:NF { #1 } { #2 }
}

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Deprecations
% 

\cs_new:Npn \gprStyledName { \gprFullName }

\keys_define:nn { genealogy-profiles }
{
	givenname~style				.tl_set:c		= { l__gpr_given~name_style_tl } ,
	givenname~index				.tl_set:c		= { l__gpr_given~name_index_tl } ,
	fullname~index				.tl_set:N		= \l__gpr_full_name_index_tl ,
}
\keys_define:nn { genealogy-profiles / profile } {
	givenname					.tl_set:c		= { l__gpr_given~name_tl } ,
	fullname					.tl_set:N		= \l__gpr_full_name_tl ,
}

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Experimental!
% 

\NewDocumentCommand { \gprSpouse } { s o m o } {
	\gtrsymMarried{} ~
	\IfValueT{#4} { #4 ~ }
	\IfBooleanTF {#1} {
		\gprRef*[#2]{#3}
	} {
		\gprRef[#2]{#3}
	}
}