%! Class = packdoc
%! Author = Jander Moreira
%! Date = 2024/10

\NeedsTeXFormat{LaTeX2e}
\ProvidesPackage{packdoc}[2025/01/31 v0.1 A tool to document LaTeX packages]

\NewDocumentCommand{\PDVersion}{}{v0.1}
\NewDocumentCommand{\PDDate}{}{2025/01/31}

\RequirePackage{pgfopts}

\RequirePackage{etoolbox}
\newbool{packdoc@UsePresets}
\pgfkeys{
    packdoc/.cd,
    presets/.is if = packdoc@UsePresets,
}
\ProcessPgfOptions{/packdoc}

\appto{\ttfamily}{\frenchspacing}{}{}
\appto{\tableofcontents}{\bigskip}
\RequirePackage{snaptodo}
\RequirePackage{marginnote}
\setlength{\marginparwidth}{3cm}

\RequirePackage{enumitem}
\setlist{nosep}

\RequirePackage{textcomp}
\RequirePackage[all]{nowidow}
\RequirePackage{multicol}
\RequirePackage{ragged2e}

\RequirePackage{geometry}
\geometry{top = 2.5cm, bottom = 2cm, right = 2.5cm, left = 4cm}

\RequirePackage{hyperref}
\hypersetup{
    colorlinks,
    urlcolor = blue!20!black,
    linkcolor = blue!10!black,
    citecolor = black!80,
}

\RequirePackage{makeidx}
\makeindex

\RequirePackage{minted}

\RequirePackage{tcolorbox}
\tcbuselibrary{most, minted}

\newbool{packdoc@InExample}
\newlength{\packdoc@ColorBoxIndent}
\setlength{\packdoc@ColorBoxIndent}{3em}
\tcbset{
    description/.style = {
        coltitle = black,
        fontupper = \normalsize,
        colbacktitle = black,
        titlerule = 0.001pt,
        enhanced jigsaw,
        breakable,
        sharp corners,
        flush right,
        top = 0.5ex,
        bottom = 0pt,
        left = \packdoc@ColorBoxIndent,
        right = 0pt,
        opacitybacktitle = 0.05,
        opacityframe = 0,
        opacityback = 0,
    },
    example/.style = {
        enhanced jigsaw,
        breakable,
        colback = blue!3,
        colframe = blue!40!black!30,
        sharp corners,
        box align = top,
        boxrule = 1pt,
        fontupper = \footnotesize,
        fontlower = \footnotesize,
        listing engine = minted,
        minted options = {
            fontsize = \footnotesize,
            breaklines,
            autogobble,
        },
        before lower = {\booltrue{packdoc@InExample}},
        after lower = {\boolfalse{packdoc@InExample}},
    }
}
\newtcblisting{PDListing}{listing options = {language = latex}, example, listing only}
\newtcblisting{PDExample}{example}
\newmintinline{latex}{}
\cslet{PDInline}{\latexinline}

\RequirePackage{cleveref}

% Support code

\ExplSyntaxOn
\seq_new:N \g_packdoc_list_of_elements_seq
\cs_new:Npn \packdoc_put_element:n #1 {
    \seq_put_left:Nn \g_packdoc_list_of_elements_seq { #1 }
}
\cs_new:Npn \packdoc_if_element_exists:nTF #1#2#3 {
    \seq_if_in:NnTF \g_packdoc_list_of_elements_seq { #1 } { #2 } { #3 }
}

\NewDocumentCommand{\packdoc@PutElement}{ m }{
    \packdoc_put_element:n { #1 }
}
\NewDocumentCommand{\packdoc@IfElementExists}{ m +m +m }{
    \packdoc_if_element_exists:nTF { #1 } { #2 } { #3 }
}
\ExplSyntaxOff

\packdoc@PutElement{packdoc@Element}

\newlength{\packdoc@ContentLength}
\NewDocumentCommand{\packdoc@IfHasLength}{ m +m +m }{%
    \settowidth{\packdoc@ContentLength}{#1}%
    \ifdimgreater{\packdoc@ContentLength}{0pt}{#2}{#3}%
}



\NewDocumentCommand{\PackageName}{ O{} m }{%
    \begingroup%
    \PDSet{#1}%
    \mbox{\packdoc@PackageStyle#2}%
    \endgroup%
}

\NewDocumentCommand{\FileName}{ m }{%
    \mbox{\textsf{#1}}%
}

% Arguments
\NewDocumentCommand{\Argument}{ O{} m }{%
    \begingroup%
    \PDSet{#1}%
    \textcolor{packdoc@ArgumentColor}{$\langle$\normalfont\small\textsl{#2}$\rangle$}%
    \endgroup%
}
\NewDocumentCommand{\MArg}{ O{} m }{%
    \mbox{\texttt{\{}\Argument[#1]{#2}\texttt{\}}}%
}
\NewDocumentCommand{\OArg}{ O{} m }{%
    \mbox{\texttt{[}\Argument[#1]{#2}\texttt{]}}%
}
\NewDocumentCommand{\AArg}{ O{} m }{%
    \mbox{\texttt{<}\Argument[#1]{#2}\texttt{>}}%
}
\NewDocumentCommand{\PArg}{ m }{%
    \mbox{\texttt{\{#1\}}}%
}

% General text
%\NewDocumentCommand{\Deprecated}{}{\textcolor{red!80!black}{(deprecated)}}
%\NewDocumentCommand{\Empty}{}{%
%    \mbox{\normalfont\textcolor{black!60}{\textsl{--empty--}}}
%}
\NewDocumentCommand{\PDTilde}{}{\raisebox{-0.6ex}{\~{}}}

% Elements
\pgfkeys{
    /packdoc/.cd,
    argument color/.code = {\colorlet{packdoc@ArgumentColor}{#1}},
    package style/.store in = \packdoc@PackageStyle,
% element specific options
    prefix/.code = {\csdef{packdoc@\csuse{packdoc@Element}@Prefix}{#1}},
    prefix/.value required,
    arguments prefix/.code = {\csdef{packdoc@\csuse{packdoc@Element}@ArgumentsPrefix}{#1}},
    arguments prefix/.value required,
    complement prefix/.code = {\csdef{packdoc@\csuse{packdoc@Element}@ComplementPrefix}{#1}},
    complement prefix/.value required,
    index heading/.code = {\csdef{packdoc@\csuse{packdoc@Element}@IndexEntry}{#1}},
    index heading/.value required,
    index remark/.code = {\csdef{packdoc@\csuse{packdoc@Element}@IndexRemark}{#1}},
    index remark/.value required,
    font/.code = {\csdef{packdoc@\csuse{packdoc@Element}@Font}{#1}},
    font/.value required,
    color/.code = {\colorlet{packdoc@\csuse{packdoc@Element}@Color}{#1}},
    color/.value required,
    no single index/.default = true,
    no single index/.code = {%
        \providebool{packdoc@\csuse{packdoc@Element}@NoSingleEntry}%
        \packdoc@CheckTrueFalse{no single index}{#1}{%
            \csuse{bool#1}{packdoc@\csuse{packdoc@Element}@NoSingleEntry}%
        }%
    },
    no group index/.default = true,
    no group index/.code = {%
        \providebool{packdoc@\csuse{packdoc@Element}@NoGroupEntry}%
        \packdoc@CheckTrueFalse{no group index}{#1}{%
            \csuse{bool#1}{packdoc@\csuse{packdoc@Element}@NoGroupEntry}%
        }
    },
% version changes
    version prefix/.store in = \packdoc@VersionPrefix,
    header style/.store in = \packdoc@VersionStyle,
    entry style/.store in = \packdoc@ChangeStyle,
}

\NewDocumentCommand{\packdoc@CheckTrueFalse}{ m m m }{
    \ifstrequal{#2}{true}{#3}{%
        \ifstrequal{#2}{false}{#3}{%
            \PackageError{packdoc}{Option '#1' expects 'true' or 'false'}%
        }%
    }
}

\NewDocumentCommand{\packdoc@SetElementDefault}{ m }{%
    \csdef{packdoc@Element}{#1}% current element
    \pgfkeys{
        /packdoc/.cd,
        prefix = \csuse{packdoc@Element@Defaults@Prefix},
        arguments prefix = \csuse{packdoc@Element@Defaults@ArgumentsPrefix},
        complement prefix = \csuse{packdoc@Element@Defaults@ComplementPrefix},
        index heading = \packdoc@Element,
        index remark = ~(\packdoc@Element),
        font = \csuse{packdoc@Element@Defaults@Font},
        color = packdoc@Element@Defaults@Color,
        no single index = false,
        no group index = false,
    }%
    \csdef{packdoc@Element}{}% reset
}

\NewDocumentCommand{\PDSetElement}{ m >{ \TrimSpaces } m }{%
    \packdoc@IfElementExists{#1}{%
        \csdef{packdoc@Element}{#1}% current element
        \pgfkeys{/packdoc/.cd, #2}%
        \csdef{packdoc@Element}{}% reset
    }{%
        \PackageError{packdoc}{Element '#1' does not exist}%
    }
}
\NewDocumentCommand{\PDSet}{ >{ \TrimSpaces } m }{%
    \csdef{packdoc@Element}{Element@Defaults}%
    \pgfkeys{/packdoc/.cd, #1}%
    \csdef{packdoc@Element}{}% reset
}

\PDSet{
    package style = \sffamily,
    argument color = orange!50!black,
    prefix = {},
    arguments prefix = {},
    complement prefix = \hfill,
    index heading = {},
    index remark = {},
    font = \ttfamily,
    color = .!75,
    no single index = false,
    no group index = false,
}


\ExplSyntaxOn
\NewDocumentCommand{\PDNewElement}{ m m }{
    \packdoc@IfElementExists{#1}{
        \PackageError{packdoc}{Element~'#1'~already~created}
    }{
        \packdoc@PutElement{#1}
        \packdoc@SetElementDefault{#1}%
        \PDSetElement{#1}{#2}%
        \exp_args:Nc \NewDocumentCommand { #1 }{ m }{
            \PDElement{#1}{##1}
        }
        \exp_args:Nc \NewDocumentCommand { #1Def }{ m }{
            \PDDefElement{#1}{##1}
        }
        \exp_args:Nc \NewDocumentCommand { #1Ref }{ m }{
            \PDRefElement{#1}{##1}
        }
        \exp_args:Nc \NewDocumentCommand { #1Ind }{ m }{
            \PDIndElement{#1}{##1}
        }
        \exp_args:Nc \NewDocumentCommand { #1Index }{ m }{
            \PDIndexElement{#1}{##1}
        }
        \exp_args:Nc \NewDocumentCommand { #1RefInd }{ m }{
            \PDRefIndElement{#1}{##1}
        }
        \NewDocumentEnvironment{#1*}{ m m m }{%
            \begin{element*}{#1}{##1}{##2}{##3}
            }{
            \end{element*}
        }
        \NewDocumentEnvironment{#1def}{ m m m }{
            \begin{elementdef}{#1}{##1}{##2}{##3}
            }{
            \end{elementdef}
        }
        \NewDocumentEnvironment{#1env*}{ m m m }{
            \begin{elementenv*}{#1}{##1}{##2}{##3}
            }{
            \end{elementenv*}
        }
        \NewDocumentEnvironment{#1env}{ m m m }{
            \begin{elementenv}{#1}{##1}{##2}{##3}
            }{
            \end{elementenv}
        }
    }
}
\ExplSyntaxOff

% #1: Element type
% #2: Element instance
\NewDocumentCommand{\PDElement}{ m m }{%
    \ifcsdef{\string\color@packdoc@#1@Color}{%
        \mbox{\textcolor{packdoc@#1@Color}{\csuse{packdoc@#1@Font}\csuse{packdoc@#1@Prefix}#2}}%
    }{%
        \mbox{\csuse{packdoc@#1@Prefix}\csuse{packdoc@#1@Font}#2}%
    }%
}

% #1: Element type
% #2: Element instance
\NewDocumentCommand{\PDIndexElement}{ m m }{%
    \ifbool{packdoc@#1@NoSingleEntry}{}{%
        \index{#2@\PDElement{#1}{#2}\csuse{packdoc@#1@IndexRemark}}%
    }%
    \ifbool{packdoc@#1@NoGroupEntry}{}{%
        \index{\csuse{packdoc@#1@IndexEntry}!#2@\PDElement{#1}{#2}}%
    }%
}

% #1: Element type
% #2: Element instance
\NewDocumentCommand{\PDIndElement}{ m m }{%
    \PDIndexElement{#1}{#2}%
    \PDElement{#1}{#2}%
}

% #1: Element type
% #2: Element instance
\NewDocumentCommand{\PDRefElement}{ m m }{%
    \hyperref[#1:#2]{\PDElement{#1}{#2}}%
}

% #1: Element type
% #2: Element instance
\NewDocumentCommand{\PDDefElement}{ m m }{%
    \phantomsection%
    \label{#1:#2}%
    \PDIndexElement{#1}{#2}%
    \PDElement{#1}{#2}%
}

% #1: Element type
% #2: Element instance
\NewDocumentCommand{\PDRefIndElement}{ m m }{%
    \PDIndexElement{#1}{#2}%
    \hyperref[#1:#2]{\PDElement{#1}{#2}}%
}

% #1: Element type
% #2: Element instance
% #3: Arguments
% #4: Value
\NewDocumentEnvironment{element*}{ m m m m }{%
    \begin{tcolorbox}[
        description,
        title = {%
        \hspace{-\packdoc@ColorBoxIndent}%
        \PDElement{#1}{#2}%
        \packdoc@IfHasLength{#3}{\csuse{packdoc@#1@ArgumentsPrefix}#3}{}%
        \packdoc@IfHasLength{#4}{\csuse{packdoc@#1@ComplementPrefix}#4}{}%
        },
    ]%
    }{%
    \end{tcolorbox}%
    \medskip%
}

% #1: Element type
% #2: Element instance
% #3: Arguments
% #4: Default
\NewDocumentEnvironment{elementdef}{ m m m m }{
    \begin{element*}{#1}{#2}{#3}{#4}
        \PDIndexElement{#1}{#2}%
        \label{#1:#2}
        }{
    \end{element*}
}

% #1: Element type
% #2: Environment name
% #3: Arguments
% #4: Environment contents
\NewDocumentEnvironment{elementenv*}{ m m m m }{%
    \begin{tcolorbox}%
        [
        title = {%
            %! parser=off
            \hspace{-\packdoc@ColorBoxIndent}\latexinline!\begin!\texttt{\{}\csuse{#1}{#2}\texttt{\}}#3\par
            \hspace{-0.5\packdoc@ColorBoxIndent}%
            \packdoc@IfHasLength{#4}{#4}{\Argument{environment contents}}\par
            \hspace{-\packdoc@ColorBoxIndent}\latexinline!\end!\texttt{\{}\csuse{#1}{#2}\texttt{\}}%
            %! parser=on
        },
        description,
        ]
            }{%
    \end{tcolorbox}%
    \medskip%
}

\NewDocumentEnvironment{elementenv}{ m m m m }{%
    \begin{elementenv*}{#1}{#2}{#3}{#4}
        \PDIndexElement{#1}{#2}%
        \label{#1:#2}
        }{%
    \end{elementenv*}
}

\ifbool{packdoc@UsePresets}{
    \definecolor{packdoc@OptionColor}{HTML}{687821}
    \colorlet{packdoc@EnvironmentColor}{brown!80!magenta}
    \colorlet{packdoc@MacroColor}{green!50!black}

    \PDNewElement{Option}{
        color = packdoc@OptionColor,
        arguments prefix = \texttt{ = },
        index heading = Options,
        index remark = ~(option),
    }
    \PDNewElement{Macro}{
        prefix = \textbackslash,
        color = packdoc@MacroColor,
        index heading = Macros,
        index remark = ~(macro),
    }
    \PDNewElement{Environment}{
        color = packdoc@EnvironmentColor,
        index heading = Environments,
        index remark = ~(environment),
    }
}{}

%%%%%%%%%%%%%%%%%%

\PDSet{
    version prefix = {},
    header style = \bfseries\footnotesize,
    entry style = \footnotesize\RaggedRight,
}

%% Internal commands
\ExplSyntaxOn

% StepChangeCounter: proceeds to a new change
\int_zero_new:N \g_packdoc_change_counter_int
\NewDocumentCommand{\packdoc@StepChangeCounter}{}{
    \int_gadd:Nn \g_packdoc_change_counter_int { 1 }
}
\NewDocumentCommand{\packdoc@CurrentChangeCounter}{}{
    \int_use:N \g_packdoc_change_counter_int
}

% SetChangeAttribute: sets the an attribute of the current change record
% #1: attribute
% #2: value
\cs_new:Npn \set_change_attribute:nn #1#2 {
    \tl_clear_new:c { g_vc_change_ \int_use:N \g_packdoc_change_counter_int _#1_tl }
    \tl_gset:cn { g_vc_change_ \int_use:N \g_packdoc_change_counter_int _#1_tl } { #2 }
}
\NewDocumentCommand{\packdoc@SetChangeAttribute}{ m m }{
    \set_change_attribute:nn { #1 } { #2 }
}

% SetChangeAttributeExpanded: sets the an attribute of the current change record
% #1: attribute
% #2: value
\NewDocumentCommand{\packdoc@SetChangeAttributeExpanded}{ m m }{
    \exp_args:Nnf \set_change_attribute:nn { #1 } { #2 }
}

% GetChangeInfo: returns the field of a change
% #1: number of the change
% #2: field
% #3: (optional) sets macro instead of returning value
\NewDocumentCommand{\packdoc@GetChangeInfo}{ m m o }{
    \IfValueTF{#3}{
        \tl_set:Nx #3 { \tl_use:c { g_vc_change_#1_#2_tl } }
    }{
        \tl_use:c { g_vc_change_#1_#2_tl }
    }
}

% SetMacroChangeInfo: sets a macro with the field of a change
% #1: macro
% #2: number of the change
% #3: field
\NewDocumentCommand{\packdoc@SetMacroChangeInfo}{ m m m }{
    \tl_clear_new:N #1
    \exp_args:NNc \tl_set:NV #1 { g_vc_change_#2_#3_tl }
}

% RunChangesList: apply a macro to each change of a version
% #1: version
% #2: macro with a single mandatory argument
\NewDocumentCommand{\packdoc@RunChangesList}{ m m }{
    \seq_map_inline:cn { g_version_#1_seq } { #2 { ##1 } }
}


% packdoc@IfVersionExists
% #1: version
% #2: code if exists
% #3: code if not exists
\cs_new:Npn \packdoc_if_version_exists:nTF #1#2#3 {
    \seq_if_in:NnTF \g_packdoc_versions_list_seq { #1 } { #2 } { #3 }
}
\NewDocumentCommand{\packdoc@IfVersionExists}{ m +m +m  }{
    \packdoc_if_version_exists:nTF { #1 } { #2 } { #3 }
}


% PDNewVersion: add a version to the list os versions if it doesn't exist
% #1: version
% #2: date
\seq_new:N \g_packdoc_versions_list_seq
\cs_new:Npn \register_version:nn #1#2 {
    \seq_put_right:Nn \g_packdoc_versions_list_seq { #1 }
    \tl_clear_new:c { g_vc_version_#1_date_tl }
    \tl_gset:cn { g_vc_version_#1_date_tl } { #2 }
    \seq_new:c { g_version_#1_seq }
}
\NewDocumentCommand{\PDNewVersion}{ m m }{
    \packdoc@IfVersionExists{#1}{
        \PackageError{packdoc}{Version~'#1'~already~exists}
    }{
        \register_version:nn { #1 } { #2 }
    }
}

% VersionDate: returns the date of a version OR sets a macro
%   with its (expanded) value
% #1: version
% #2: (optional) macro to store the value
\NewDocumentCommand{\packdoc@VersionDate}{ m o }{
    \IfValueTF{#2}{
        \tl_set:Nx #2 { \tl_use:c { g_vc_version_#1_date_tl } }
    }{
        \tl_use:c { g_vc_version_#1_date_tl }
    }
}


% AddChangeToVersion: add a change reference to the list of the version
% (a new list will be created if necessary)
% #1: version
\cs_new:Npn \add_change_to_version:n #1  {
    % Add a reference (change number) to the list
    % \tl_clear_new:N \l_change_number_tl
    % \exp_args:NNe \tl_set:Nn \l_change_number_tl { \int_use:N \g_packdoc_change_counter_int }
    \exp_args:Nco \seq_gput_right:Nn { g_version_#1_seq } { \int_use:N \g_packdoc_change_counter_int }
}
\NewDocumentCommand{\packdoc@AddChangeToVersion}{ }{
    \add_change_to_version:n {
        \tl_use:c { g_vc_change_ \int_use:N \g_packdoc_change_counter_int _version_tl }
    }
}


% RunVersionList: apply a macro to each version of the list
% #1: macro with a single mandatory argument
\NewDocumentCommand{\packdoc@RunVersionList}{ m }{
    \seq_map_inline:Nn \g_packdoc_versions_list_seq { #1 { ##1 } }
}

\ExplSyntaxOff

%%

\newbool{packdoc@HideBox}
\newbool{packdoc@NoListing}
\pgfkeys{
    /packdoc/.cd,
    version/.code = {\packdoc@SetChangeAttribute{version}{#1}},
    title/.code = {\csdef{packdoc@BoxTitle}{#1}},
    description/.code = {\packdoc@SetChangeAttribute{description}{#1}},
    page/.code = {\packdoc@SetChangeAttributeExpanded{page}{#1}},
    no page/.style = {page = {}},
    no box/.is if = packdoc@HideBox,
    no listing/.is if = packdoc@NoListing,
    type/.is choice,
    type/new/.code = {\packdoc@SetChangeAttribute{type}{New in}},
    type/update/.code = {\packdoc@SetChangeAttribute{type}{Updated in}},
    type/change/.code = {\packdoc@SetChangeAttribute{type}{Changed in}},
    type/removal/.code = {\packdoc@SetChangeAttribute{type}{Removed in}},
    type/deprecation/.code = {\packdoc@SetChangeAttribute{type}{Deprecated in}},
    .unknown/.code = {%
        \csedef{packdoc@local@Option}{type = \pgfkeyscurrentname}%
        \pgfkeysalsofrom{\packdoc@local@Option}%
    },
}

\PDSet{
    no box = false,
    no listing = false,
}

%% #1: (optional) todonotes options
%% #2: text
%\NewDocumentCommand{\packdoc@MarginNote}{ O{} > { \TrimSpaces } m }{%
%    \todo[bordercolor = blue!20, backgroundcolor = blue!10,
%        linecolor = blue!20, tickmarkheight = 0.2ex, size = \tiny,
%        noline, #1]{%
%        #2
%    }%
%}


% PDAddChange: records a new change
\NewDocumentCommand{\PDAddChange}{ m > { \TrimSpaces } m O{} }{%
    \packdoc@IfVersionExists{#1}{%
        \begingroup%
        \pgfkeys{
            /packdoc/.cd,
            page = \thepage,
            version = #1,
            type = new,
            title = {},
            description = {%
                \textbf{???}%
                \PackageWarning{packdoc}{A change without a description and without the 'no listing' option has been created.}%
            },
            #2,
        }%
        \hspace{0pt}%
        \packdoc@AddChangeToVersion%
        \packdoc@SetChangeAttributeExpanded{star}{\ifbool{packdoc@NoListing}{*}{}}%
        \packdoc@SetChangeAttributeExpanded{label}{\packdoc@CurrentChangeCounter}%
        \packdoc@GetChangeInfo{\packdoc@CurrentChangeCounter}{label}[\packdoc@InfoResult]%
        \expandafter\label\expandafter{\packdoc@InfoResult:change}%
        \ifbool{packdoc@HideBox}{}{%
        %\reversemarginpar%
        %\ifbool{packdoc@InExample}{\let\marginpar\marginnote}{}%
        %\tikzset{
        %    notestyleraw/.append style = {rounded corners = 0pt, inner sep = 2pt},
        %}%
            \snaptodo[
                block sep = -0.2ex,
                call chain/.style = {draw = none},
                margin block/.style = {font = \scriptsize, blue!75!black},
                chain bias = -99in,  % force to left margin
                #3
            ]{%
                \packdoc@IfHasLength{\csuse{packdoc@BoxTitle}}{%
                    \textbf{\csuse{packdoc@BoxTitle}}:\\%
                }{}%
                \sffamily%
                \packdoc@GetChangeInfo{\packdoc@CurrentChangeCounter}{type}
                \packdoc@VersionPrefix\packdoc@GetChangeInfo{\packdoc@CurrentChangeCounter}{version}%
            }%
        }%
        \endgroup%
        \packdoc@StepChangeCounter%
    }{%
        \PackageWarning{packdoc}{#1 is not a valid version. Ignored}%
    }%
}

%%

% Internal commands
\ExplSyntaxOn

% Reading from file
\ior_new:N \g_packdoc_input_io

% LoadFile: reads a previous compiled version changes
\cs_new:Nn \load_file: {
    \tl_clear_new:N \g_packdoc_file_contents_tl
    \ior_open:NnTF \g_packdoc_input_io { \jobname.vcind } {
        \ior_map_inline:Nn \g_packdoc_input_io {
            \tl_gput_right:Nn \g_packdoc_file_contents_tl { ##1 }
        }
    }{}
}
\NewDocumentCommand{\packdoc@LoadChanges}{}{
    \load_file:
}
\NewDocumentCommand{\packdoc@FileContentsNotEmpty}{ +m }{
    \tl_if_empty:NTF \g_packdoc_file_contents_tl {} {
        #1
    }
}

% File contents
\NewDocumentCommand{\packdoc@FileContents}{}{
    \tl_use:N \g_packdoc_file_contents_tl
}

% Writing to file
\iow_new:N \g_packdoc_output_io
\tl_new:N \g_packdoc_output_buffer_tl

% OpenFile: open file for writing
\NewDocumentCommand{\packdoc@OpenFile}{ }{
    \iow_open:Nn \g_packdoc_output_io { \jobname.vcind }
}

% CloseFile: close file
\NewDocumentCommand{\packdoc@CloseFile}{ }{
    \iow_close:N \g_packdoc_output_io
}

% WriteBuffer: write to file
\cs_generate_variant:Nn \iow_now:Nn { NV }
\NewDocumentCommand{\packdoc@WriteBuffer}{ }{
    \iow_now:NV \g_packdoc_output_io \g_packdoc_output_buffer_tl
    \tl_clear:N \g_packdoc_output_buffer_tl
}

\NewDocumentCommand{\packdoc@AddToBuffer}{ +m }{
    \tl_gput_right:Nn \g_packdoc_output_buffer_tl { #1 }
}

\cs_generate_variant:Nn \tl_gput_right:Nn { Nx }
\NewDocumentCommand{\packdoc@AddCharToBuffer}{ m }{
    \tl_gput_right:Nx \g_packdoc_output_buffer_tl { \iow_char:N #1 }
}
\ExplSyntaxOff

% WriteVersionChange: write to file a single change
% #1: change number
\NewDocumentCommand{\packdoc@WriteVersionChange}{ m }{%
    \packdoc@AddToBuffer{ \PDPrintChange}%
    \packdoc@SetMacroChangeInfo{\packdoc@InfoResult}{#1}{star}%
    \expandafter\packdoc@AddToBuffer\expandafter{\packdoc@InfoResult}%
    % version number
    \packdoc@AddCharToBuffer{\{}%
    \packdoc@SetMacroChangeInfo{\packdoc@InfoResult}{#1}{description}%
    \expandafter\packdoc@AddToBuffer\expandafter{\packdoc@InfoResult}%
    \packdoc@AddCharToBuffer{\}}%
    % date
    \packdoc@AddCharToBuffer{\{}%
    \packdoc@SetMacroChangeInfo{\packdoc@InfoResult}{#1}{page}%
    \expandafter\packdoc@AddToBuffer\expandafter{\packdoc@InfoResult}%
    \packdoc@AddCharToBuffer{\}}%
    % label
    \packdoc@AddCharToBuffer{\{}%
    \packdoc@SetMacroChangeInfo{\packdoc@InfoResult}{#1}{label}%
    \expandafter\packdoc@AddToBuffer\expandafter{\packdoc@InfoResult}%
    \packdoc@AddCharToBuffer{\}}%
    \packdoc@WriteBuffer
}

\NewDocumentCommand{\packdoc@WriteVersion}{ m }{%
%! parser = off
    \packdoc@AddToBuffer{\begin{vcversionitem}}%
    %! parser = on
    \packdoc@AddCharToBuffer{\{}%
    \packdoc@AddToBuffer{#1}%
    \packdoc@AddCharToBuffer{\}}%
    \packdoc@AddCharToBuffer{\{}%
    \packdoc@VersionDate{#1}[\packdoc@InfoResult]%
    \expandafter\packdoc@AddToBuffer\expandafter{\packdoc@InfoResult}%
    \packdoc@AddCharToBuffer{\}}%
    \packdoc@WriteBuffer
    \packdoc@RunChangesList{#1}{\packdoc@WriteVersionChange}%
%! parser = off
    \packdoc@AddToBuffer{\end{vcversionitem}}%
    %! parser = on
    \packdoc@WriteBuffer
}

\NewDocumentCommand{\packdoc@SaveChanges}{}{%
    \packdoc@OpenFile%
    \packdoc@RunVersionList{\packdoc@WriteVersion}%
    \packdoc@WriteBuffer
    \packdoc@CloseFile%
}

\NewDocumentCommand{\PDPrintChanges}{ O{} }{%
    \packdoc@FileContentsNotEmpty{
        \begingroup%
        \PDSet{#1}
        \section*{Change History}
        \begin{multicols}{2}
            \packdoc@FileContents%
        \end{multicols}
        \endgroup%
    }
}

%% Printing the list of changes
\NewDocumentEnvironment{vcversionitem}{ m m }{%
    \par\noindent%
    {\packdoc@VersionStyle\packdoc@VersionPrefix#1 (#2)}
    \vspace*{0.2em}\par%
    \begingroup%
    \setlength{\tabcolsep}{0pt}%
    \packdoc@ChangeStyle%
    }{%
    \endgroup%
    \vspace{0.5em}%
}

\newlength{\packdoc@PageNumberWidth}
\setlength{\packdoc@PageNumberWidth}{1cm}
\NewDocumentCommand{\PDPrintChange}{ s m m m }{%
    \IfBooleanF{#1}{%
        \begingroup%
        \hspace{0.25cm}%
        \parbox[b]{\dimexpr \linewidth - \packdoc@PageNumberWidth - 0.25cm}{%
            \setlength{\hangindent}{0.7cm}#2%
            \ifstrempty{#3}{}{\dotfill}%
        }%
        \ifstrempty{#3}{}{%
            \hangindent=0.6\linewidth%
            \hspace{-0.05cm}\dotfill~\hspace{0.05cm}\hyperref[#4:change]{#3}%
        }%
        \par\vspace{0.5em}
        \endgroup%
    }
}

%% Version numbering
\ExplSyntaxOn

\tl_new:N \g_packdoc_prefix_text_tl
\tl_new:N \g_packdoc_number_tl % todo: turn to int?
\tl_new:N \g_packdoc_suffix_text_tl
\regex_new:N \g_versioning_regex
\regex_set:Nn \g_versioning_regex { [^\.]+ }
\regex_new:N \g_subversioning_regex
\regex_set:Nn \g_subversioning_regex { ([^\d]*)(\d+)(.*) }

\cs_new:Npn \subversioning_check:n #1 {
    \seq_clear_new:N \l_subparts_seq
    \regex_extract_all:NnNTF \g_subversioning_regex { #1 } \l_subparts_seq {
        \tl_gset:Nx \g_packdoc_prefix_text_tl { \seq_item:Nn \l_subparts_seq {2} }
        \tl_gset:Nx \g_packdoc_number_tl { \seq_item:Nn \l_subparts_seq {3} }
        \tl_gset:Nx \g_packdoc_suffix_text_tl { \seq_item:Nn \l_subparts_seq {4} }
    }{
        \tl_gset:Nx \g_packdoc_prefix_text_tl { #1 }
        \tl_gclear:N \g_packdoc_number_tl
        \tl_gclear:N \g_packdoc_suffix_text_tl
    }
}
\cs_new:Npn \versioning_check:n #1 {
    \seq_clear_new:N \l_parts_seq
    \regex_extract_all:NnN \g_versioning_regex { #1 } \l_parts_seq
    \seq_map_inline:Nn \l_parts_seq {
        \subversioning_check:n { ##1 }
        <<(\tl_use:N \g_packdoc_prefix_text_tl)
        (\tl_use:N \g_packdoc_number_tl)
        (\tl_use:N \g_packdoc_suffix_text_tl)>>
    }
}
\NewDocumentCommand{\PDVersioningCheck}{ m }{
    \par\noindent[\texttt{#1}:~\versioning_check:n { #1 }]\bigskip\par
}

\ExplSyntaxOff

% Hooks
\AtBeginDocument{%
    \packdoc@LoadChanges%
    \sloppy%
}
\AtEndDocument{\packdoc@SaveChanges}