% \iffalse meta-comment
%
% Copyright (C) 2019 by Benjamin Berg <benjamin@sipsolutions.net>
%
% This work may be distributed and/or modified under the
% conditions of the LaTeX Project Public License, either version 1.3c
% of this license or (at your option) any later version.
% The latest version of this license is in
%   http://www.latex-project.org/lppl.txt
%
% This work has the LPPL maintenance status `maintained'.
% 
% The Current Maintainer of this work is Benjamin Berg.
%
% \fi
%
% \iffalse
%<*driver>
\ProvidesFile{sdapsarray.dtx}
%</driver>
%<package>\NeedsTeXFormat{LaTeX2e}[1999/12/01]
%<package>\ProvidesPackage{sdapsarray}
%<*package>
    [2015/04/10 v0.1 Initial version of SDAPS array package]
%</package>
%
%<*driver>
\documentclass{ltxdoc}
\usepackage{sdapsarray}[2015/04/10]
%\EnableCrossrefs
\CodelineIndex
\RecordChanges
\begin{document}
  \DocInput{sdapsarray.dtx}
\end{document}
%</driver>
% \fi
%
% \CheckSum{0}
%
% \CharacterTable
%  {Upper-case    \A\B\C\D\E\F\G\H\I\J\K\L\M\N\O\P\Q\R\S\T\U\V\W\X\Y\Z
%   Lower-case    \a\b\c\d\e\f\g\h\i\j\k\l\m\n\o\p\q\r\s\t\u\v\w\x\y\z
%   Digits        \0\1\2\3\4\5\6\7\8\9
%   Exclamation   \!     Double quote  \"     Hash (number) \#
%   Dollar        \$     Percent       \%     Ampersand     \&
%   Acute accent  \'     Left paren    \(     Right paren   \)
%   Asterisk      \*     Plus          \+     Comma         \,
%   Minus         \-     Point         \.     Solidus       \/
%   Colon         \:     Semicolon     \;     Less than     \<
%   Equals        \=     Greater than  \>     Question mark \?
%   Commercial at \@     Left bracket  \[     Backslash     \\
%   Right bracket \]     Circumflex    \^     Underscore    \_
%   Grave accent  \`     Left brace    \{     Vertical bar  \|
%   Right brace   \}     Tilde         \~}
%
%
% \changes{v0.1}{2015/01/14}{Initial version}
%
% \GetFileInfo{sdapsarray.dtx}
%
% \DoNotIndex{\newcommand,\newenvironment}
% 
%
% \title{The \textsf{sdapsarray} package\thanks{This document
%   corresponds to \textsf{sdapsarray}~\fileversion, dated \filedate.}}
% \author{Benjamin Berg \\ \texttt{benjamin@sipsolutions.net}}
%
% \maketitle
%
% \section{Documentation}
%
% Please refer to \url{https://sdaps.org/class-doc} for documentation.
%
% \StopEventually{\PrintChanges\PrintIndex}
%
% \section{Implementation}
%
% This package uses the \LaTeX3 language internally, so we need to enable it.
%    \begin{macrocode}
% We need at least 2011-08-23 for \keys_set_known:nnN
\RequirePackage{expl3}[2011/08/23]
%\RequirePackage{xparse}
\ExplSyntaxOn
%    \end{macrocode}
%
% And we need a number of other packages.
%    \begin{macrocode}
\ExplSyntaxOff

\RequirePackage{xparse}
\RequirePackage{sdapsbase}


\ExplSyntaxOn

%    \end{macrocode}
%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%
% \subsection{Tempfile handling}
%
% This is a bit weird, but under some conditions we need extended information
% about each row in the document (for page break detection). As it makes little
% to no sense to load all this information into memory at start we use two
% temporary files instead. As these files should not change their content
% for reruns (that would break e.g. latexmk) we copy the "tic" file into "toc"
% so that "toc" can be read while "tic" is being re-written.
% We then go on to define a macro which will read a single
% line and return true if it is different from the previous line.
% This is the indicator that a page/column break has happened and the
% row header needs to be inserted.
%
%    \begin{macrocode}

% This will change in early 2018, but the new code apparently does not
% provide the old name of the definition.
% i.e. this is a bad hack and should be removed latest in 2019 or so
\cs_if_exist:NF \ior_str_get:NN { \cs_set_eq:Nc \ior_str_get:NN { ior_get_str:NN } }

\bool_new:N \g__sdaps_array_info_open
\bool_gset_false:N \g__sdaps_array_info_open
\iow_new:N \g__sdaps_array_info_iow
\ior_new:N \g__sdaps_array_info_ior

\cs_generate_variant:Nn \int_set:Nn { Nf }
\cs_generate_variant:Nn \coffin_join:NnnNnnnn { NnVNnVnn }

\cs_new_protected_nopar:Nn \_sdaps_array_open_tmpfiles:
{
  % Guard against being executed multiple times
  \bool_if:NF \g__sdaps_array_info_open {
    \bool_gset_true:N \g__sdaps_array_info_open

    % Also ensures toc file exists (i.e. is readable)
    \iow_open:Nn \g__sdaps_array_info_iow { \c_sys_jobname_str .sdapsarraytoc }
    \file_if_exist:nTF { \c_sys_jobname_str .sdapsarraytic } {
      % Copy into toc file, then open that.
      \ior_open:Nn \g__sdaps_array_info_ior { \c_sys_jobname_str .sdapsarraytic }

      \ior_str_map_inline:Nn \g__sdaps_array_info_ior { \iow_now:Nn \g__sdaps_array_info_iow { ##1 } }
      \ior_close:N \g__sdaps_array_info_ior

      \ior_open:Nn \g__sdaps_array_info_ior { \c_sys_jobname_str .sdapsarraytoc }
    } {
    }
    \iow_close:N \g__sdaps_array_info_iow

    \iow_open:Nn \g__sdaps_array_info_iow { \c_sys_jobname_str .sdapsarraytic }
  }
}

\tl_new:N \g__sdaps_array_last_row_tl
\tl_gset_eq:NN \g__sdaps_array_last_row_tl \c_empty_tl

\cs_new_protected_nopar:Nn \_sdaps_array_check_insert_header:N
{
  \bool_gset_eq:NN #1 \c_false_bool
  \ior_if_eof:NF \g__sdaps_array_info_ior {
    \ior_str_get:NN \g__sdaps_array_info_ior \l_tmpa_tl

    \tl_if_eq:NNF \g__sdaps_array_last_row_tl \c_empty_tl {
      \tl_if_eq:NNF \g__sdaps_array_last_row_tl \l_tmpa_tl {
        \bool_gset_eq:NN #1 \c_true_bool
      }
    }
    \tl_gset_eq:NN \g__sdaps_array_last_row_tl \l_tmpa_tl
  }
}

%    \end{macrocode}
%
% \subsection{Initialization}
%
% Global definitions and penalty constant.
%
%    \begin{macrocode}

\prop_new:N \g__sdaps_array_layouter_prop

% XXX: Penalty in between rows. After the header a nobreak is inserted, but
%      do we want special penalties elsewhere (preventing orphans/widows?)
\int_new:N \g_sdaps_array_row_penalty_tl
\int_gset:Nn \g_sdaps_array_row_penalty_tl { 10 }

%    \end{macrocode}
%
% \subsection{Initialization}
%
% Define some routines to store width information for columns (across builds).
%
%    \begin{macrocode}

\tl_new:N \g_sdaps_array_shared_data_tl
\tl_new:N \g_sdaps_array_stored_data_tl
\tl_new:N \g_sdaps_array_local_data_tl
\tl_new:N \g_sdaps_array_local_data_new_tl
\prop_new:N \g__sdaps_array_stored_data_prop
\prop_new:N \g__sdaps_array_shared_data_prop
\prop_new:N \g__sdaps_array_local_data_prop

\cs_generate_variant:Nn \prop_item:Nn { NV }

\cs_new_protected_nopar:Nn \_sdaps_array_load_data:
{
  \tl_gset:Nx \g_sdaps_array_stored_data_tl { \prop_item:NV \g__sdaps_array_stored_data_prop \l__sdaps_array_global_name_tl }
  \tl_gset:Nx \g_sdaps_array_shared_data_tl { \prop_item:NV \g__sdaps_array_shared_data_prop \l__sdaps_array_global_name_tl }
  \tl_gset:Nx \g_sdaps_array_local_data_tl { \prop_item:NV \g__sdaps_array_local_data_prop \l__sdaps_array_local_name_tl }
}

\cs_new_protected_nopar:Nn \_sdaps_array_store_data:
{
  % Do not overwrite the "stored" data that we have right now.
  \prop_gput:NVV \g__sdaps_array_shared_data_prop \l__sdaps_array_global_name_tl \g_sdaps_array_shared_data_tl

  \immediate\write\@auxout{\exp_not:n{\sdapsarrayloadstoreddata}{\l__sdaps_array_global_name_tl}{\g_sdaps_array_shared_data_tl}}
  \tl_if_empty:NF \g_sdaps_array_local_data_new_tl {
    \immediate\write\@auxout{\exp_not:n{\sdapsarrayloadlocaldata}{\l__sdaps_array_local_name_tl}{\g_sdaps_array_local_data_new_tl}}
  }
}

% Define for loading sdaps code in aux file
\cs_new_protected_nopar:Nn \sdaps_array_load_stored_data:nn {
  \prop_gput:Nnn \g__sdaps_array_stored_data_prop { #1 } { #2 }
}
\cs_new_eq:NN \sdapsarrayloadstoreddata \sdaps_array_load_stored_data:nn

% Define for loading sdaps code in aux file
\cs_new_protected_nopar:Nn \sdaps_array_load_local_data:nn {
  \prop_gput:Nnn \g__sdaps_array_local_data_prop { #1 } { #2 }
}
\cs_new_eq:NN \sdapsarrayloadlocaldata \sdaps_array_load_local_data:nn

%    \end{macrocode}
%
% \subsection{Array Layouter}
%
% \subsubsection{User facing macros}
%
%    \begin{macrocode}

\int_new:N \g__sdaps_array_current_id_int
\tl_new:N \l__sdaps_array_global_name_tl
\tl_new:N \l__sdaps_array_local_name_tl

\cs_generate_variant:Nn \keys_set:nn { nf }
\cs_new_protected_nopar:Nn \sdaps_array_begin:nn
{
 \tl_set:Nx \l__sdaps_array_local_name_tl { sdapsarray \int_use:N\g__sdaps_array_current_id_int }
  \int_gincr:N \g__sdaps_array_current_id_int
  \tl_if_empty:nTF { #2 } {
    \tl_set:NV \l__sdaps_array_global_name_tl \l__sdaps_array_local_name_tl
  } {
    \tl_set:Nx \l__sdaps_array_global_name_tl { #2 }
  }

  \_sdaps_array_load_data:

  \keys_set:nf { sdaps / array } { \prop_item:Nn \g__sdaps_array_layouter_prop { #1 } }

  % Force cell layouter instead of column header layouter
  \bool_if:NT \l_sdaps_sdapsarray_no_header_bool {
    \tl_set:NV \l__sdaps_array_colhead_tl \l__sdaps_array_cell_tl
  }

  \_sdaps_array_open_tmpfiles:

  \tl_gset_eq:NN \g__sdaps_array_last_row_tl \c_empty_tl
  \bool_gset_true:N \g_sdaps_array_first_row_bool

  \l__sdaps_array_begin_tl
}
\cs_generate_variant:Nn \sdaps_array_begin:nn { Vn }
\cs_generate_variant:Nn \sdaps_array_begin:nn { VV }
\cs_generate_variant:Nn \sdaps_array_begin:nn { nV }

\cs_new_protected_nopar:Nn \sdaps_array_begin:n
{
  \sdaps_array_begin:nn { #1 } { }
}

\cs_new_protected_nopar:Nn \sdaps_array_row_start:
{
  \l__sdaps_array_row_start_tl
}

\cs_new_protected_nopar:Nn \_sdaps_array_assign_use: {
    \box_use:N \l_tmpa_box
  \egroup
}

\cs_new_protected_nopar:Npn \sdaps_array_assign_colhead:Nw #1
{
  \l__sdaps_array_colhead_tl #1
}

\cs_new_protected_nopar:Npn \sdaps_array_colhead:w
{
  \bgroup
    \l__sdaps_array_colhead_tl \l_tmpa_box \bgroup
    \group_insert_after:N\_sdaps_array_assign_use:
    % swallow group opening token
    \tex_let:D\next=
}

\cs_new_protected:Npn \sdaps_array_assign_rowhead:Nw #1
{
  \l__sdaps_array_rowhead_tl #1
}

\cs_new_protected:Npn \sdaps_array_rowhead:w
{
  \bgroup
    \l__sdaps_array_rowhead_tl \l_tmpa_box \bgroup
    \group_insert_after:N\_sdaps_array_assign_use:
    % swallow group opening token
    \tex_let:D\next=
}

\cs_new_protected_nopar:Npn \sdaps_array_assign_cell:Nw #1
{
  \l__sdaps_array_cell_tl #1
}

\cs_new_protected_nopar:Npn \sdaps_array_cell:w
{
  \bgroup
    \l__sdaps_array_cell_tl \l_tmpa_box \bgroup
    \group_insert_after:N\_sdaps_array_assign_use:
    % swallow group opening token
    \tex_let:D\next=
}

% XXX: Could this live in local scope instead?
\box_new:N \g__sdaps_array_header_box
\dim_new:N \g__sdaps_array_header_dim

\cs_new_protected_nopar:Nn \_sdaps_array_calc_interlineskip:nnN
{
  \dim_compare:nNnTF { #1 } > { -1000pt } {
    \skip_set:Nn #3 { \baselineskip - #1 - #2 }
    \dim_compare:nNnF { #3 } > { \lineskiplimit } {
      \skip_set:Nn #3 { \lineskip }
    }
  } {
    \skip_set:Nn #3 { 0pt }
  }
  \skip_set:Nn #3 { #3 + \l_sdaps_sdapsarray_rowsep_dim }
}

\cs_new_protected_nopar:Nn \sdaps_array_row:NN
{
  \if_mode_vertical:
  \else:
    \msg_error:nn { sdapsarray } { wrong_mode }
  \fi

  % XXX: \l_tmpa_dim is the height to the first baseline in the box. Note that
  %      we use the real baseline in the case of the header row!
  \l__sdaps_array_row_tl #1 #2 \l_tmpb_box \l_tmpa_dim

  \bool_if:nTF { \g_sdaps_array_first_row_bool && !\l_sdaps_sdapsarray_no_header_bool } {
    % Stow away the box for later use
    \box_gset_eq:NN \g__sdaps_array_header_box \l_tmpb_box
    \dim_gset:Nn \g__sdaps_array_header_dim { \box_ht:N \g__sdaps_array_header_box + \box_dp:N \g__sdaps_array_header_box }
  } {
    % Pagebreak detection (not needed for header row)
    \_sdaps_array_check_insert_header:N \g_tmpa_bool

    \hbox_set:Nn \l_tmpb_box {
      \pdfsavepos
      \iow_shipout_x:Nn \g__sdaps_array_info_iow {
        \thepage,
        \the\pdflastxpos
      }
      \box_use:N \l_tmpb_box
    }
  }

  \bool_if:nTF { \g_sdaps_array_first_row_bool || \l_sdaps_sdapsarray_no_header_bool } {
    \_sdaps_array_calc_interlineskip:nnN { \prevdepth } { \l_tmpa_dim } \l_tmpa_skip
    \nointerlineskip
    \skip_vertical:n { \l_tmpa_skip }

    % However do not insert the rowskip in the case of the first line.
    % We rely on surrounding code to insert proper spacing before/after
    % the environment.
    \bool_if:NT \g_sdaps_array_first_row_bool {
      \skip_vertical:n { - \l_sdaps_sdapsarray_rowsep_dim }
    }

    \box_use:N \l_tmpb_box
    % For the header, insert a \nobreak, otherwise the normal inter-row penalty
    \bool_if:NTF \l_sdaps_sdapsarray_no_header_bool {
      \penalty\int_use:N\g_sdaps_array_row_penalty_tl
    } {
      \nobreak
    }

    \bool_gset_false:N \g_sdaps_array_first_row_bool
  } {
    % The idea is simple. Before every line the header is re-inserted (either
    % the real one or an empty box with the same dimensions). In the case that
    % there is *no* page break we insert a corresponding negative skip so that
    % the resulting skip (including interline skip) is exactly the normal
    % interline skip between the rows.
    % In the case that the skip *is* discarded we end up with the header box
    % and the normal interline skip between the new row and the header. i.e.:
    %   skip = 1 * ( interlineskip_for_row - interlineskip_for_header - header_height - header_depth ) + header_height + header_depth + interlineksip_for_header
    % or
    %   skip = 0 * ( interlineskip_for_row - interlineskip_for_header - header_height - header_depth ) + header_height + header_depth + interlineksip_for_header

    % Calculate interlineksip_for_row and interlineskip_for_header
    \_sdaps_array_calc_interlineskip:nnN { \prevdepth } { \l_tmpa_dim } \l_tmpa_skip
    \_sdaps_array_calc_interlineskip:nnN { \box_dp:N \g__sdaps_array_header_box } { \l_tmpa_dim } \l_tmpb_skip
    \nointerlineskip
    \skip_vertical:n { \l_tmpa_skip - \l_tmpb_skip }
    \kern -\g__sdaps_array_header_dim

    % Inser the real or fake box
    \bool_if:NTF \g_tmpa_bool {
      \box_use:N \g__sdaps_array_header_box
    } {
     \hrule height \box_ht:N \g__sdaps_array_header_box depth \box_dp:N \g__sdaps_array_header_box width 0pt
    }
    \nobreak
    % Insert the calculated interline skip (in the same way the TeX would do it.
    \nointerlineskip
    \skip_vertical:N \l_tmpb_skip

    \box_use:N \l_tmpb_box

    % And insert the sdapsarray sepecific inter row penalty.
    \penalty\int_use:N\g_sdaps_array_row_penalty_tl
  }
}

\cs_new_protected_nopar:Nn \sdaps_array_end:
{
  \l__sdaps_array_end_tl
  \box_gclear:N \g__sdaps_array_header_box
}

%    \end{macrocode}
%
% \subsubsection{Common Layouter Macros}
%
%    \begin{macrocode}

\tl_new:N \l__sdaps_array_begin_tl
\tl_new:N \l__sdaps_array_row_start_tl
\tl_new:N \l__sdaps_array_colhead_tl
\tl_new:N \l__sdaps_array_rowhead_tl
\tl_new:N \l__sdaps_array_cell_tl
\tl_new:N \l__sdaps_array_row_tl
\tl_new:N \l__sdaps_array_end_tl

\keys_define:nn { sdaps / array }
{
  begin        .tl_set:N   = \l__sdaps_array_begin_tl,
  row_start    .tl_set:N   = \l__sdaps_array_row_start_tl,
  colhead      .tl_set:N   = \l__sdaps_array_colhead_tl,
  rowhead      .tl_set:N   = \l__sdaps_array_rowhead_tl,
  cell         .tl_set:N   = \l__sdaps_array_cell_tl,
  row          .tl_set:N   = \l__sdaps_array_row_tl,
  end          .tl_set:N   = \l__sdaps_array_end_tl,
}


\seq_new:N \g_sdaps_array_overhangs_left_seq
\seq_new:N \g_sdaps_array_overhangs_right_seq
\seq_new:N \g_sdaps_array_shared_colwidths_seq
\seq_new:N \g_sdaps_array_stored_colwidths_seq

\cs_new_protected_nopar:Npn \_sdaps_array_rowhead_default:Nw #1
{
  \tl_if_empty:NTF \g_sdaps_array_local_data_tl {
      \tl_if_empty:NTF \g_sdaps_array_local_data_new_tl {
        \dim_set:Nn \l_tmpa_dim { 0.5 \hsize }
      } {
        \dim_set:Nn \l_tmpa_dim { \g_sdaps_array_local_data_new_tl }
      }
  } {
    \dim_set:Nn \l_tmpa_dim { \g_sdaps_array_local_data_tl }
  }
  % \vbox_set_top:Nw is still missing as of 2017-08-11
  \tex_setbox:D #1 \tex_vtop:D \bgroup
    \sdaps_if_rtl:TF {
      \raggedright
    } {
      \raggedleft
    }
    \hsize=\dim_use:N\l_tmpa_dim
    \group_begin:\bgroup
    \group_insert_after:N \group_end:
    \group_insert_after:N \egroup
    % swallow group opening token
    \tex_let:D\next=
}

\cs_new_protected_nopar:Npn \_sdaps_array_cell_default:Nw #1
{
  \hbox_set:Nw #1 \bgroup
    % swallow group opening token
    \group_insert_after:N \hbox_set_end:
    \tex_let:D\next=
}

\cs_new:Nn \_sdaps_array_cell_rotated_end: {
    \hbox_set_end:
    \dim_set:Nn \l_tmpa_dim { \box_ht:N \l_tmpa_box }
    \dim_set:Nn \l_tmpb_dim { \box_dp:N \l_tmpa_box }

    \dim_set:Nn \l_tmpa_dim { \l_sdaps_sdapsarray_angle_sine_tl \l_tmpa_dim }
    \dim_set:Nn \l_tmpb_dim { \l_sdaps_sdapsarray_angle_sine_tl \l_tmpb_dim }

    \box_rotate:Nn \l_tmpa_box { \l_sdaps_sdapsarray_angle_int }

    % We want the baseline of the box to be centered, that only works if we
    % leave the same space both ways.
    % That is not ideal, but we cannot move the cell content accordingly.
    \dim_set:Nn \l_tmpb_dim { \dim_max:nn { \l_tmpa_dim } { \l_tmpb_dim } }
    \skip_horizontal:n { \l_tmpb_dim }
    \rlap{
      \skip_horizontal:n { -\l_tmpa_dim }
      \box_use:N \l_tmpa_box
    }
    \skip_horizontal:n { \l_tmpb_dim }
    % dummy skip that will be removed again by other code
    \skip_horizontal:n { 0pt }

    \dim_set:Nn \l_tmpa_dim { \l_tmpa_dim + \l_tmpb_dim }

    \dim_set:Nn \l_tmpb_dim { \box_wd:N \l_tmpa_box }
    \dim_set:Nn \l_tmpa_dim { \dim_max:nn { 0pt } { \l_tmpb_dim - \l_tmpa_dim } }

    \seq_gpush:Nn \g_sdaps_array_overhangs_left_seq { 0pt }
    \seq_gpush:Nx \g_sdaps_array_overhangs_right_seq { \dim_use:N \l_tmpa_dim }
  \egroup
  \hbox_set_end:
}

% Only sane for header row
\cs_new_protected_nopar:Npn \_sdaps_array_cell_rotated:Nw #1
{
  \hbox_set:Nw #1 \bgroup
    \hbox_set:Nw \l_tmpa_box
    \bgroup
    \group_insert_after:N \_sdaps_array_cell_rotated_end:
    % swallow group opening token
    \tex_let:D\next=
}

% XXX: A parbox layouter with fixed width would be nice
%\cs_new_protected_nopar:Nn \sdaps_array_cell_fixed:n {}


\cs_new_protected_nopar:Nn \_sdaps_array_begin_default:
{
  \tl_if_empty:NTF \g_sdaps_array_shared_data_tl {
    \seq_clear:N \g_sdaps_array_shared_colwidths_seq
  } {
    \seq_gset_split:NnV \g_sdaps_array_shared_colwidths_seq { ~ } \g_sdaps_array_shared_data_tl
  }
  \tl_if_empty:NTF \g_sdaps_array_stored_data_tl {
    \seq_clear:N \g_sdaps_array_stored_colwidths_seq
  } {
    \seq_gset_split:NnV \g_sdaps_array_stored_colwidths_seq { ~ } \g_sdaps_array_stored_data_tl
  }
}

\cs_new_protected_nopar:Nn \_sdaps_array_end_default:
{
  \tl_gset:Nx \g_sdaps_array_shared_data_tl { \seq_use:Nn \g_sdaps_array_shared_colwidths_seq { ~ } }
  \tl_gset:Nx \g_sdaps_array_stored_data_tl { \seq_use:Nn \g_sdaps_array_stored_colwidths_seq { ~ } }

  % Clear the global sequences, to save memory
  \seq_gclear:N \g_sdaps_array_overhangs_left_seq
  \seq_gclear:N \g_sdaps_array_overhangs_right_seq
  \seq_gclear:N \g_sdaps_array_shared_colwidths_seq
  \seq_gclear:N \g_sdaps_array_stored_colwidths_seq

  \_sdaps_array_store_data:
}

\cs_new_protected_nopar:Nn \_sdaps_array_row_start_default:
{
  \seq_gclear:N \g_sdaps_array_overhangs_left_seq
  \seq_gclear:N \g_sdaps_array_overhangs_right_seq
}

\cs_new_protected_nopar:Nn \_sdaps_array_row:NNNN
{
  % #1: A vbox with baseline on the *first* item containing the row header
  %     (\vtop in plain TeX).
  % #2: Data cells packed into an hbox. Each of these needs to be set to the
  %     correct width and inserted.
  % #3: The box register to store the resulting hbox in. The depth of this box
  %     needs to be correct to calculate the interline glue to the following
  %     row.
  % #4: A dim register to store the height of the box in for the purpose of
  %     calculating the interline glue in front of the produced row.
  %
  % { \dim_use:N\@totalleftmargin } { \dim_use:N\linewidth }
  % The macro should create an hbox which is exactly \linewidth wide and also
  % contains internal indentation by \@totalleftmargin into the box register #3.
  % The box will be used while in vertical mode and may% be inserted multiple
  % times in the case of the header row.
  % To simplify the iteration it is guaranteed that the data cell boxes are not
  % completely empty. This means the code can simply unbox until it sees a box
  % that is void.

  \seq_gclear:N \g_tmpa_seq

  % Insert the boxes into a local hbox to work with them
  \hbox_set:Nn #2 {
    \hbox_unpack:N #2

    % Handle the overhang, note that we modify the \g_sdaps_array_overhangs_right_seq locally only!
    \seq_pop:NNTF \g_sdaps_array_overhangs_right_seq \l_tmpa_tl {
      \dim_set:Nn \l_tmpb_dim { \l_tmpa_tl }
    } {
      \dim_set:Nn \l_tmpb_dim { 0pt }
    }
    % Implicit "last" column that contains the overhang
    \seq_gpop:NNTF \g_sdaps_array_shared_colwidths_seq \l_tmpa_tl {
      \dim_set:Nn \l_tmpa_dim { \l_tmpa_tl }
    } {
      \dim_set:Nn \l_tmpa_dim { 0pt }
    }
    \dim_set:Nn \l_tmpa_dim { \dim_max:nn { \l_tmpa_dim } { \l_tmpb_dim } }


    % MAX with stored values (NOTE: sequence only modified in local scope)
    \seq_pop:NNTF \g_sdaps_array_stored_colwidths_seq \l_tmpa_tl {
      \dim_set:Nn \l_tmpb_dim { \l_tmpa_tl }
    } {
      \dim_set:Nn \l_tmpb_dim { 0pt }
    }
    % Store value from this run, and then calculate max with previous run
    \seq_gput_right:Nx \g_tmpa_seq { \dim_use:N \l_tmpa_dim }
    \dim_set:Nn \l_tmpa_dim { \dim_max:nn { \l_tmpa_dim } { \l_tmpb_dim } }


    % Insert the overhang space
    \hbox_set:Nn \l_tmpa_box { \skip_horizontal:n { \l_tmpa_dim } }

    % Now grab the first of the cells, and then loop over the rest
    \box_set_to_last:N #2
    \bool_do_while:nn { ! \box_if_empty_p:N #2 } {
      % Strip any trailing glue (i.e. space) coming from the user (for the
      % leading side we ensure that \tex_ignorespaces:D is called).
      % Note that this may remove e.g. a trailing \hfill from the user, the
      % user needs to work around that (in the same way as is required in e.g.
      % tabular).
      \hbox_set:Nn #2 { \hbox_unpack:N #2 \unskip }

      % Pop the target width for the current box (i.e. we don't globally
      % modify the clist here).
      \seq_gpop:NNTF \g_sdaps_array_shared_colwidths_seq \l_tmpa_tl {
        \dim_set:Nn \l_tmpa_dim { \l_tmpa_tl }
      } {
        \dim_set:Nn \l_tmpa_dim { 0pt }
      }
      % Calculate the maximum width of current and previous items
      \dim_set:Nn \l_tmpa_dim { \dim_max:nn { \box_wd:N #2 } { \l_tmpa_dim } }


      % MAX with stored values (NOTE: sequence only modified in local scope)
      \seq_pop:NNTF \g_sdaps_array_stored_colwidths_seq \l_tmpa_tl {
        \dim_set:Nn \l_tmpb_dim { \l_tmpa_tl }
      } {
        \dim_set:Nn \l_tmpb_dim { 0pt }
      }
      % Store value from this run, and then calculate max with previous run
      \seq_gput_right:Nx \g_tmpa_seq { \dim_use:N \l_tmpa_dim }
      \dim_set:Nn \l_tmpa_dim { \dim_max:nn { \l_tmpa_dim } { \l_tmpb_dim } }

      % Set the box into a new box with the correct width which contains fil
      % to center it.
      \hbox_set_to_wd:Nnn \l_tmpb_box \l_tmpa_dim { \hfil \hbox_unpack:N #2 \hfil }

      % This loops works backward, so attach the cell on the right side.
      % We used to make sure that it is layed out in order, but that is now
      % obsolete and doing it out of order is simpler in RTL mode.
      \sdaps_if_rtl:TF {
        % The boxes are shown on the page from LTR
        \hbox_set:Nn \l_tmpa_box { \box_use:N \l_tmpa_box \skip_horizontal:n { \l_sdaps_sdapsarray_colsep_dim } \box_use:N \l_tmpb_box \skip_horizontal:n { \l_sdaps_sdapsarray_colsep_dim } }
      } {
        \hbox_set:Nn \l_tmpa_box { \skip_horizontal:n { \l_sdaps_sdapsarray_colsep_dim } \box_use:N \l_tmpb_box \skip_horizontal:n { \l_sdaps_sdapsarray_colsep_dim } \box_use:N \l_tmpa_box }
      }

      % Grab next cell
      \box_set_to_last:N #2
    }

    % Get the coffin out of the nested scope by placing it into the box and
    % placing that into it again ...
    \box_use:N \l_tmpa_box
  }
  \hcoffin_set:Nn \l_tmpa_coffin { \box_use_drop:N #2 }

  \seq_gconcat:NNN \g_sdaps_array_shared_colwidths_seq \g_tmpa_seq \g_sdaps_array_shared_colwidths_seq
  \seq_gclear:N \g_tmpa_seq

  % Calculate the space that is left for the header column
  \dim_set:Nn \l_tmpa_dim { \linewidth - \coffin_wd:N \l_tmpa_coffin - 2\l_sdaps_sdapsarray_colsep_dim }
  \tl_gset:Nx \g_sdaps_array_local_data_new_tl { \dim_use:N \l_tmpa_dim }

  % TODO: The \hfil here is a hack to prevent a warning if the vbox is empty.
  %       Unfortunately checking for an empty box does not work for some reason.
  \dim_set:Nn \l_tmpb_dim { \box_ht:N #1 }
  \sdaps_if_rtl:TF {
    \hcoffin_set:Nn \l_tmpb_coffin { \hbox_to_wd:nn \l_tmpa_dim { \hfil \vbox:n { \vbox_unpack_clear:N #1 } } \skip_horizontal:n { \l_sdaps_sdapsarray_colsep_dim } }
    \tl_set:Nn \l_tmpa_tl { l }
    \tl_set:Nn \l_tmpb_tl { r }
  } {
    \hcoffin_set:Nn \l_tmpb_coffin { \skip_horizontal:n { \l_sdaps_sdapsarray_colsep_dim } \hbox_to_wd:nn \l_tmpa_dim { \hfil \vbox:n { \vbox_unpack_clear:N #1 } } }
    \tl_set:Nn \l_tmpa_tl { r }
    \tl_set:Nn \l_tmpb_tl { l }
  }
  \dim_set:Nn \l_tmpa_dim { \coffin_ht:N \l_tmpb_coffin }

  % If the first/last baseline differ then center the vbox, otherwise align the
  % baseline with the cells
  \dim_compare:nNnTF { \l_tmpa_dim } = { \l_tmpb_dim } {
    \coffin_join:NnVNnVnn \l_tmpb_coffin { H } \l_tmpa_tl \l_tmpa_coffin { H } \l_tmpb_tl { \l_sdaps_sdapsarray_colsep_dim } { 0pt }
    \dim_set:Nn #4 { \coffin_ht:N \l_tmpb_coffin }
  } {
    \coffin_join:NnVNnVnn \l_tmpb_coffin { vc } \l_tmpa_tl \l_tmpa_coffin { vc } \l_tmpb_tl { \l_sdaps_sdapsarray_colsep_dim } { 0pt }
    % XXX: Assume that the header is higher than the content cells
    \dim_set:Nn #4 { \l_tmpb_dim }
  }

  \hbox_set:Nn #3 { \skip_horizontal:N \@totalleftmargin \coffin_typeset:Nnnnn \l_tmpb_coffin { H } { l } { 0pt } { 0pt } }
}


\prop_gput:Nnn \g__sdaps_array_layouter_prop { default } {
  begin = { \_sdaps_array_begin_default: },
  row_start = { \_sdaps_array_row_start_default: },
  rowhead = { \_sdaps_array_rowhead_default:Nw },
  colhead = { \_sdaps_array_cell_default:Nw },
  cell = { \_sdaps_array_cell_default:Nw },
  row = { \_sdaps_array_row:NNNN },
  end = { \_sdaps_array_end_default: },
}


\prop_gput:Nnn \g__sdaps_array_layouter_prop { rotated } {
  begin = { \_sdaps_array_begin_default: },
  row_start = { \_sdaps_array_row_start_default: },
  rowhead = { \_sdaps_array_rowhead_default:Nw },
  colhead = { \_sdaps_array_cell_rotated:Nw },
  cell = { \_sdaps_array_cell_default:Nw },
  row = { \_sdaps_array_row:NNNN },
  end = { \_sdaps_array_end_default: },
}

%    \end{macrocode}
%
% \subsection{Exporting a tabular/array like environment}
%
% \subsubsection{Helper required for the environment}
%
%    \begin{macrocode}

\bool_new:N \g_sdaps_array_first_row_bool
\bool_new:N \l__sdaps_sdapsarray_in_top_group_bool
\bool_new:N \l__sdaps_sdapsarray_have_content_bool
\bool_new:N \l_sdaps_sdapsarray_flip_bool
\tl_new:N \l_sdaps_sdapsarray_layouter_tl
\tl_new:N \l_sdaps_sdapsarray_align_tl
\bool_new:N \l_sdaps_sdapsarray_keepenv_bool
\int_new:N \l_sdaps_sdapsarray_angle_int
\tl_new:N \l_sdaps_sdapsarray_angle_sine_tl
\dim_new:N \l_sdaps_sdapsarray_colsep_dim
\dim_new:N \l_sdaps_sdapsarray_rowsep_dim
\bool_new:N \l_sdaps_sdapsarray_no_header_bool

\keys_define:nn { sdaps / sdapsarray }
{
  flip       .bool_set:N   = \l_sdaps_sdapsarray_flip_bool,
  flip       .initial:n  = false,
  flip       .default:n  = true,
  layouter   .tl_set:N   = \l_sdaps_sdapsarray_layouter_tl,
  layouter   .initial:n  = default,
  align      .tl_set:N   = \l_sdaps_sdapsarray_align_tl,
  align      .initial:n  = { },
  keepenv    .bool_set:N   = \l_sdaps_sdapsarray_keepenv_bool,
  keepenv    .initial:n  = false,
  keepenv    .default:n  = true,
  no_header  .bool_set:N = \l_sdaps_sdapsarray_no_header_bool,
  no_header  .initial:n  = false,
  no_header  .default:n  = true,

  angle          .code:n     = {
    \int_set:Nn \l_sdaps_sdapsarray_angle_int {#1}
    \tl_set:Nx \l_sdaps_sdapsarray_angle_sine_tl { \fp_to_decimal:n {sind(#1)}}
  },
  angle          .initial:n  = 70,
  colsep         .dim_set:N  = \l_sdaps_sdapsarray_colsep_dim,
  colsep         .initial:n  = 6pt,
  rowsep         .dim_set:N  = \l_sdaps_sdapsarray_rowsep_dim,
  rowsep         .initial:n  = 0pt,
}


%    \end{macrocode}
%
% \subsubsection{Environment definition}
%
%    \begin{macrocode}

\cs_new_nopar:Nn \l_sdaps_sdapsarray_alignment_set_have_content:
{
  \bool_set_true:N\l__sdaps_sdapsarray_have_content_bool
}

\cs_new_nopar:Nn \_sdaps_sdapsarray_alignment: {
    % End the last group, which will be the group that was begun earlier.
    % If the earlier cell was the first one, then this egroup also starts the
    % hbox to temporarily store the cells.
  \egroup
  \bool_if:NF \l__sdaps_sdapsarray_in_top_group_bool {
    \msg_error:nnn { sdapsarray } { unmatched_grouping_level } { an~alignment~tab }
  }

  % We need to notify the outside scope that there are items, will be inserted
  % multiple times, but that does not matter.
  \group_insert_after:N\l_sdaps_sdapsarray_alignment_set_have_content:

  % Just in case someone leaked a change into our scope
  \bool_if:NF \l_sdaps_sdapsarray_keepenv_bool {
    \cs_set_eq:NN \cr \_sdaps_sdapsarray_newline:
    \cs_set_eq:NN \\ \cr
  }

  % Define a cell now, we can just put everything into a new cell, and that
  % should work fine.
  % Note that cells are not safe for fragile commands at the moment.
  \_sdaps_sdapsarray_cell:w \bgroup
    \bool_set_false:N \l__sdaps_sdapsarray_in_top_group_bool
    \cs_set_eq:NN \\ \cr
    \tex_ignorespaces:D
}

\msg_new:nnn { sdapsarray } { unmatched_grouping_level } { The~grouping~level~of~a~cell~was~not~even.~Please~ensure~all~braces~are~balanced~out!~This~error~occured~at~#1. }
\msg_new:nnn { sdapsarray } { unequal_cols } { The~number~of~columns~is~not~equal~for~all~rows. }
\msg_new:nnn { sdapsarray } { no_new_line_at_end } { You~have~terminated~the~last~line~with~\textbackslash\textbackslash~or~similar.~This~can~have~side~effects,~please~remove~it. }
\msg_new:nnn { sdapsarray } { wrong_mode } { The~sdapsarray~environment~can~only~function~in~vertical~mode~(both~inner~and~outer). }

\cs_new:Nn \_sdaps_sdapsarray_start_cells: {
    \bool_if:NF \l__sdaps_sdapsarray_in_top_group_bool {
      \msg_error:nnn { sdapsarray } { unmatched_grouping_level } { the~end~of~a~row~header }
    }
  \egroup

  % We are in the environment scope again here

  % Notify code that we are going to generate cells for a new row.
  \sdaps_array_row_start:

  % Start an hbox to stow away the cells.
  % The rest of the setup happens in the alignment handler
  \hbox_set:Nw \l_tmpb_box \bgroup
    \group_insert_after:N \hbox_set_end:
    \bool_set_true:N \l__sdaps_sdapsarray_in_top_group_bool
}

\cs_new_nopar:Nn \_sdaps_sdapsarray_linestart: {
  \sdaps_array_assign_rowhead:Nw \l_tmpa_box
    \bgroup
      \cs_set_eq:NN \\ \cr

      \bool_set_true:N \l__sdaps_sdapsarray_in_top_group_bool
      \bgroup
        \group_insert_after:N \_sdaps_sdapsarray_start_cells:
        \bool_set_false:N \l__sdaps_sdapsarray_in_top_group_bool
        % Ignore following spaces by the user
        \tex_ignorespaces:D
}

\cs_new_nopar:Nn \_sdaps_sdapsarray_newline: {
    \egroup
    \bool_if:NF \l__sdaps_sdapsarray_in_top_group_bool {
      \msg_error:nnn { sdapsarray } { unmatched_grouping_level } { the~end~of~a~row }
    }
  \egroup

  % We are in the environment scope again here
  % Output the last line if the cells were non-empty.
  \bool_if:NT \l__sdaps_sdapsarray_have_content_bool {
    \sdaps_array_row:NN \l_tmpa_box \l_tmpb_box
  }
  \bool_set_false:N \l__sdaps_sdapsarray_have_content_bool

  \cs_set_eq:NN \_sdaps_sdapsarray_cell:w \sdaps_array_cell:w
  \_sdaps_sdapsarray_linestart:
}

\cs_new:Npn\sdaps_array_nested_alignenv: {
  \char_set_catcode_alignment:N &
  \cs_set_eq:NN \cr \sdaps_orig_cr
  \cs_set_eq:NN \\ \sdaps_orig_backslash
}

\cs_new:Npn\sdaps_array_nested_alignenv:w {
  \bgroup
    \sdaps_array_nested_alignenv:
    % swallow group opening token
    \tex_let:D\next=
}
\cs_new_eq:NN \sdapsnested \sdaps_array_nested_alignenv:w

\group_begin:
\char_set_catcode_active:N &
\cs_new:Nn \_sdaps_sdapsarray_defines: {
  \cs_set_eq:NN \sdaps_orig_cr \cr
  \cs_set_eq:NN \sdaps_orig_backslash \\
  \bool_if:NF \l_sdaps_sdapsarray_keepenv_bool {
    \char_set_catcode_active:N &
    \cs_set_eq:NN \cr \sdaps_array_newline:
    \cs_set_eq:NN \\ \cr
    \cs_set_eq:NN & \sdaps_array_alignment:
  }
}
\group_end:

%%%%%%
% Flipping environment
%%%%%%

% First some helpers

\box_new:N \l_sdaps_sdapsarray_headers_box
\box_new:N \l_sdaps_sdapsarray_boxlist_head_box
\box_new:N \l_sdaps_sdapsarray_boxlist_tail_box

\cs_new_protected:Nn \_sdaps_sdapsarray_prepend_box:NN {
  \hbox_set:Nn #2 {
    \box_use:N #1
    \hbox_unpack:N #2
  }
  \box_clear:N #1
}

\cs_new_protected:Nn \_sdaps_sdapsarray_append_box:NN {
  \hbox_set:Nn #2 {
    \hbox_unpack:N #2
    \box_use:N #1
  }
  \box_clear:N #1
}

\cs_new_protected:Nn \_sdaps_sdapsarray_pop_last_box:NN {
  \hbox_set:Nn #2 {
    \hbox_unpack:N #2
    \box_gset_to_last:N \g_tmpa_box
  }
  \box_set_eq:NN #1 \g_tmpa_box
  \box_gclear:N \g_tmpa_box
}

\cs_new_protected:Nn \_sdaps_sdapsarray_pop_last_hbox_unpack:NN {
  \_sdaps_sdapsarray_pop_last_box:NN #1 #2
  \hbox_set:Nn #1 {
    \hbox_unpack:N #1
    \box_gset_to_last:N \g_tmpa_box
  }
  \box_set_eq:NN #1 \g_tmpa_box
  \box_gclear:N \g_tmpa_box
}

\cs_new_protected:Nn \_sdaps_sdapsarray_boxlist_void_if_empty:N {
  \hbox_set:Nn #1 {
    \hbox_unpack:N #1
    \box_set_to_last:N #1
    \box_if_empty:NTF #1 {
      \bool_gset_true:N \g_tmpa_bool
    } {
      \box_use:N #1
      \bool_gset_false:N \g_tmpa_bool
    }
  }
  \bool_if:NT \g_tmpa_bool {
    \box_clear:N #1
  }
}

% Lets say we are in row 4, cell 2 as defined in the environment, so the actual
% position is 2, 4. Minus the row headers, this gives us 2, 3.
% This means we need to append the cell to the box 3rd last box.
% In that case we have to append the cell

\cs_new_protected_nopar:Nn \_sdaps_sdapsarray_alignment_flip:
{
  \_sdaps_sdapsarray_end_cell_flip:

  % Just in case someone leaked a change into our scope
  \bool_if:NF \l_sdaps_sdapsarray_keepenv_bool {
    \cs_set_eq:NN \cr \_sdaps_sdapsarray_newline_flip:
    \cs_set_eq:NN \\ \cr
  }

  % Next up is either a cell or a row header. We can figure that out by checking
  % that the row headings box is void
  \box_if_empty:NTF \l_sdaps_sdapsarray_headers_box {
    \sdaps_array_assign_rowhead:Nw \l_tmpa_box \bgroup
      \bool_set_false:N \l__sdaps_sdapsarray_in_top_group_bool
      \cs_set_eq:NN \\ \cr
      % Ignore following spaces by the user
      \tex_ignorespaces:D
  } {
    \sdaps_array_assign_cell:Nw \l_tmpa_box \bgroup
      \bool_set_false:N \l__sdaps_sdapsarray_in_top_group_bool
      \cs_set_eq:NN \\ \cr
      \tex_ignorespaces:D
  }
}

\cs_new_nopar:Nn \_sdaps_sdapsarray_end_cell_flip: {
  \egroup % Finish of the cell
  \bool_if:NF \l__sdaps_sdapsarray_in_top_group_bool {
    \msg_error:nnn { sdapsarray } { unmatched_grouping_level } { end~of~cell~or~row }
  }

  % Get last box from head
  \_sdaps_sdapsarray_pop_last_box:NN \l_tmpb_box \l_sdaps_sdapsarray_boxlist_head_box
  % Append the new box to the list of boxes for this row
  \_sdaps_sdapsarray_append_box:NN \l_tmpa_box \l_tmpb_box
  % Prepend the new box to the tail
  \_sdaps_sdapsarray_prepend_box:NN \l_tmpb_box \l_sdaps_sdapsarray_boxlist_tail_box
}

\cs_new_nopar:Nn \_sdaps_sdapsarray_end_line_flip: {
  % At the end of the line, move tail into head.
  % First check that head is empty.
  \_sdaps_sdapsarray_boxlist_void_if_empty:N \l_sdaps_sdapsarray_boxlist_head_box
  \box_if_empty:NF \l_sdaps_sdapsarray_boxlist_head_box {
    \msg_error:nn { sdapsarray } { unequal_cols }
  }
  \box_set_eq:NN \l_sdaps_sdapsarray_boxlist_head_box \l_sdaps_sdapsarray_boxlist_tail_box
  \box_clear:N \l_sdaps_sdapsarray_boxlist_tail_box

  % If this was the first row, store it away, these are the headings.
  \box_if_empty:NT \l_sdaps_sdapsarray_headers_box {
    \box_set_eq:NN \l_sdaps_sdapsarray_headers_box \l_sdaps_sdapsarray_boxlist_head_box
    \box_clear:N \l_sdaps_sdapsarray_boxlist_head_box
  }
}

\cs_new_nopar:Nn \_sdaps_sdapsarray_newline_flip: {
  \_sdaps_sdapsarray_end_cell_flip:
  \_sdaps_sdapsarray_end_line_flip:

  % Create next box to store away, this has to be a colhead at this point
  \sdaps_array_assign_colhead:Nw \l_tmpa_box \bgroup
    \bool_set_false:N \l__sdaps_sdapsarray_in_top_group_bool
    \cs_set_eq:NN \\ \cr
    \tex_ignorespaces:D
}


\NewDocumentEnvironment { sdapsarray } { o }
{
  \bool_set_false:N \l__sdaps_sdapsarray_in_top_group_bool
  \group_begin:

    \tl_set:Nn \l_tmpa_tl { }
    \IfNoValueF { #1 } {
      \tl_set:Nn \l_tmpa_tl { #1 }
    }

    \box_clear:N \l_tmpa_box
    \box_clear:N \l_tmpb_box

    % Ensure vertical mode.
    \tex_par:D
    \if_mode_vertical:
    \else:
      \msg_error:nn { sdapsarray } { wrong_mode }
    \fi

    % This needs to be initialized here as otherwise the values would be
    % expanded at import time.
    \keys_set:nV { sdaps / sdapsarray } \l_tmpa_tl

    \sdaps_array_begin:VV \l_sdaps_sdapsarray_layouter_tl \l_sdaps_sdapsarray_align_tl

    % Note, this environment is fragile; we redefine & to be active.
    % One can go back into normal mode by using \sdapsnested{} though.

    \bool_if:NTF \l_sdaps_sdapsarray_flip_bool {
      \cs_set_eq:NN \sdaps_array_newline: \_sdaps_sdapsarray_newline_flip:
      \cs_set_eq:NN \sdaps_array_alignment: \_sdaps_sdapsarray_alignment_flip:

      \_sdaps_sdapsarray_defines:

      % Two hboxes to hold the content, note that 
      % a: row headers, b: cells/col headers
      \box_clear:N \l_sdaps_sdapsarray_headers_box
      \box_clear:N \l_sdaps_sdapsarray_boxlist_head_box
      \box_clear:N \l_sdaps_sdapsarray_boxlist_tail_box

      % not sure why we need this group, but nothing works without it
      \bgroup
        % This is a bit creative to say the least
        \sdaps_array_row_start:

        \bool_set_true:N \l__sdaps_sdapsarray_in_top_group_bool
        \sdaps_array_assign_rowhead:Nw \l_tmpa_box \bgroup
          \bool_set_false:N \l__sdaps_sdapsarray_in_top_group_bool
          \cs_set_eq:NN \\ \cr
          % Ignore following spaces by the user
          \tex_ignorespaces:D
    } {
      \cs_set_eq:NN \sdaps_array_newline: \_sdaps_sdapsarray_newline:
      \cs_set_eq:NN \sdaps_array_alignment: \_sdaps_sdapsarray_alignment:
      \_sdaps_sdapsarray_defines:

      \cs_set_eq:NN \_sdaps_sdapsarray_cell:w \sdaps_array_colhead:w
      \_sdaps_sdapsarray_linestart:
    }

  % If we redefine &, then the next character might have the wrong catcode
  % (i.e. it could still be an alignment character). Execute the alignment
  % code directly if the next character is &.
  \bool_if:NF \l_sdaps_sdapsarray_keepenv_bool {
    \peek_charcode_remove_ignore_spaces:NT & { \sdaps_array_alignment: }
  }
}
{
    \bool_if:NTF \l_sdaps_sdapsarray_flip_bool {
        \_sdaps_sdapsarray_end_cell_flip:

        % At this point we should have swallowed all items from the head list.
        % If not, then someone likely add a stray \\ command or similar
        \_sdaps_sdapsarray_boxlist_void_if_empty:N \l_sdaps_sdapsarray_boxlist_head_box
        \box_if_empty:NTF \l_sdaps_sdapsarray_boxlist_head_box {
          \_sdaps_sdapsarray_end_line_flip:
        } {
          \msg_error:nn { sdapsarray } { no_new_line_at_end }
        }

        % Now we can have fun!
        % Pop cells and heading, until we cannot find any new ones.
        \_sdaps_sdapsarray_pop_last_hbox_unpack:NN \l_tmpa_box \l_sdaps_sdapsarray_headers_box
        \_sdaps_sdapsarray_pop_last_box:NN \l_tmpb_box \l_sdaps_sdapsarray_boxlist_head_box
        \bool_do_while:nn { ! \box_if_empty_p:N \l_tmpa_box || ! \box_if_empty_p:N \l_tmpb_box } {
          \sdaps_array_row:NN \l_tmpa_box \l_tmpb_box

          \sdaps_array_row_start:

          \_sdaps_sdapsarray_pop_last_hbox_unpack:NN \l_tmpa_box \l_sdaps_sdapsarray_headers_box
          \_sdaps_sdapsarray_pop_last_box:NN \l_tmpb_box \l_sdaps_sdapsarray_boxlist_head_box
        }

      \egroup
    } {
        \egroup

        \bool_if:NF \l__sdaps_sdapsarray_in_top_group_bool {
          \msg_error:nnn { sdapsarray } { unmatched_grouping_level } { the~end~of~the~environment }
        }
      \egroup

      % We are in the environment scope again here
      % Output the last line if the cells were non-empty.
      \bool_if:NT \l__sdaps_sdapsarray_have_content_bool {
        \sdaps_array_row:NN \l_tmpa_box \l_tmpb_box
      }
    }

    \sdaps_array_end:

  \group_end:
}




\ExplSyntaxOff

%
%    \end{macrocode}
%

% \Finale
\endinput
