package LatexIndent::AlignmentAtAmpersand;

#	This program is free software: you can redistribute it and/or modify
#	it under the terms of the GNU General Public License as published by
#	the Free Software Foundation, either version 3 of the License, or
#	(at your option) any later version.
#
#	This program is distributed in the hope that it will be useful,
#	but WITHOUT ANY WARRANTY; without even the implied warranty of
#	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#	GNU General Public License for more details.
#
#	See http://www.gnu.org/licenses/.
#
#	Chris Hughes, 2017-2025
#
#	For all communication, please visit: https://github.com/cmhughes/latexindent.pl
use strict;
use warnings;
use Data::Dumper;
use Exporter   qw/import/;
use List::Util qw/max min sum/;

#-----------
use LatexIndent::Blocks           qw/$mSwitchOnlyTrailing $allCodeBlocks/;
use LatexIndent::GetYamlSettings  qw/%mainSetting %previouslyFoundSetting/;
use LatexIndent::LogFile          qw/$logger/;
use LatexIndent::ModifyLineBreaks qw/_mlb_line_break_token_adjust/;
use LatexIndent::Switches         qw/$is_m_switch_active $is_t_switch_active $is_tt_switch_active %switch/;
use LatexIndent::Tokens           qw/%tokens/;
use LatexIndent::TrailingComments qw/$trailingCommentRegExp @trailingComments/;
use LatexIndent::Verbatim         qw/%verbatimStorage/;
our @ISA = "LatexIndent::Document";    # class inheritance, Programming Perl, pg 321
our @EXPORT_OK
    = qw/align_at_ampersand _align_mark_down_block double_back_slash_else main_formatting individual_padding multicolumn_padding multicolumn_pre_check multicolumn_post_check dont_measure hidden_child_cell_row_width hidden_child_row_width get_column_width/;
our $alignmentBlockCounter;
our @cellStorage;                      # two-dimensional storage array containing the cell information
our @formattedBody;                    # array for the new body
our @minMultiColSpan;
our @maxColumnWidth;
our @maxDelimiterWidth;
our @hiddenChildrenStorage;
our $hiddenChildCount = 0;

sub _align_mark_down_block {

    # aligned block within mark down
    #
    #      %* \begin{tabular}
    #         1 & 2 & 3 & 4 \\
    #         5 &   & 6 &   \\
    #        %* \end{tabular}
    #
    my $self = shift;

    my @tcMarkDownToBeReturned;

    foreach (@trailingComments) {
        next unless ${$_}{alignMarkUp};
        next unless ${$self}{body} =~ m/${$_}{id}/s;
        ${$self}{body} =~ s/${$_}{id}/${$_}{value}/s;
        push( @tcMarkDownToBeReturned, { id => ${$_}{id}, value => ${$_}{value} } );
    }
    $logger->info("*alignment mark-up routine %*\\begin{<tabular>}...\\end{<tabular>}") if $is_t_switch_active;
    $logger->trace("unpacking mark-up comments for this routine")                       if $is_t_switch_active;

    while ( my ( $alignmentBlock, $yesno ) = each %{ $mainSetting{lookForAlignDelims} } ) {
        if ( ref $yesno eq "HASH" ) {
            $yesno = ( defined ${$yesno}{delims} ) ? ${$yesno}{delims} : 1;
        }
        if ($yesno) {
            $logger->trace("looking for %*\\begin\{$alignmentBlock\} environments") if $is_t_switch_active;

            my $alignmentRegExp = qr/
                            (\h*)
                            (
                                (?!<\\)%\*\h*
                                \\begin\{
                                        ($alignmentBlock) # environment name captured into $2
                                       \}                 # \begin{alignmentBlock} statement captured into $1
                            )
                            (
                                .*?                       # non-greedy match (body) into $3
                                \R                        # a line break
                            )
                            \h*                           # possible horizontal spaces
                            (
                                (?!<\\)%\*                # %*
                                \h*                       # possible horizontal spaces
                                \\end\{\3\}               # \end{alignmentBlock} statement captured into $4
                            )
                        /smx;

            ${$self}{body} =~ s/$alignmentRegExp/
                                    my $indentation = ($1?$1:q());
                                    my $begin = $2;
                                    my $name = $3;
                                    my $body = $4;
                                    my $end = $5;
                                    # create a new Environment object
                                    my $alignmentBlockObj = LatexIndent::AlignmentAtAmpersand->new(
                                                                          body=>$body,
                                                                          name=>$name,
                                                                          modifyLineBreaksYamlName=>"environments",
                                                                          );
            
                                    # log file output
                                    $logger->trace("*found: marked up alignment block %*\\begin{$name}") if $is_t_switch_active;

                                    # align at ampersand routine
                                    $alignmentBlockObj->yaml_get_indentation_settings_for_this_object;
                                    $alignmentBlockObj->align_at_ampersand ;  

                                    # body update
                                    $body = ${$alignmentBlockObj}{body};
                                    $body =~ s"^"$indentation"mg;
                                    $body =~ s"^$indentation""s;
                                    
                                    my $addedIndentation = ${$alignmentBlockObj}{indentation};
                                    # add indentation
                                    $body =~ s"^"$addedIndentation"mg;
                                    $body =~ s"^$addedIndentation""s;
                                    $body =~ s"\s*$"\n"s;
                                    $begin.$body.$end;
                              /xseg;
        }
        else {
            $logger->trace("*not* looking for $alignmentBlock as $alignmentBlock:$yesno") if $is_t_switch_active;
        }
    }

    $logger->trace("*alignment mark-up routine: replacing comments") if $is_t_switch_active;
    my $notPrecededByRegExp = qr/${${$mainSetting{fineTuning}}{trailingComments}}{notPrecededBy}/;
    foreach (@tcMarkDownToBeReturned) {
        ${$self}{body} =~ s/$notPrecededByRegExp%\Q${$_}{value}\E/%${$_}{id}/s;
    }
    return;
}

sub yaml_modify_line_breaks_settings {
    return;
}

sub align_at_ampersand {
    my $self  = shift;
    my %input = @_;

    $logger->trace("*alignAtAmpersand routine for ${$self}{name} (type: ${$self}{modifyLineBreaksYamlName})")
        if $is_t_switch_active;

    # some blocks may contain verbatim to be measured
    ${$self}{measureVerbatim} = ( ${$self}{body} =~ m/$tokens{verbatim}/ ? 1 : 0 );

    # m-switch only for double back slash
    #   DBSStartsOnOwnLine
    #   DBSFinishesWithLineBreak
    if ($is_m_switch_active) {
        my $DBSStartsOnOwnLine;
        my $DBSFinishesWithLineBreak;
        if ( ${$self}{modifyLineBreaksYamlName} =~ m/Arguments/s ) {
            $DBSStartsOnOwnLine       = ${$self}{DBSStartsOnOwnLine};
            $DBSFinishesWithLineBreak = ${$self}{DBSFinishesWithLineBreak};
        }
        else {
            my $commandStorageName = ${$self}{name} . ${$self}{modifyLineBreaksYamlName};
            $DBSStartsOnOwnLine = (
                defined ${ $previouslyFoundSetting{$commandStorageName} }{DBSStartsOnOwnLine}
                ? ${ $previouslyFoundSetting{$commandStorageName} }{DBSStartsOnOwnLine}
                : 0
            );
            $DBSFinishesWithLineBreak = (
                defined ${ $previouslyFoundSetting{$commandStorageName} }{DBSFinishesWithLineBreak}
                ? ${ $previouslyFoundSetting{$commandStorageName} }{DBSFinishesWithLineBreak}
                : 0
            );
        }
        $self->double_back_slash_else( $DBSStartsOnOwnLine, $DBSFinishesWithLineBreak )
            if ( $DBSStartsOnOwnLine or $DBSFinishesWithLineBreak );
    }

    # abandon the routine if there are no line breaks
    return unless ${$self}{body} =~ m/\R/s;

    # check for line breaks at end of begin
    # example:
    #
    #       \begin{align*}    1&2\\
    #         3&4\\
    #       \end{align*}
    #
    # stores '\begin{align*}    ' as the begin statement (note the trailing h-space)
    #
    # note: check ${$self}{begin} as well because of poly-switch adjustment (see pmatrix2-mod1.tex)
    ${$self}{linebreaksAtEnd}{begin} = ( ${$self}{body} =~ m/\A\h*\R/s ? 1 : 0 );
    if ( $is_m_switch_active and defined ${$self}{begin} and !${$self}{linebreaksAtEnd}{begin} ) {
        ${$self}{linebreaksAtEnd}{begin} = ( ${$self}{begin} =~ m/\h*\R$/s ? 1 : 0 );
    }
    if ( !${ ${$self}{linebreaksAtEnd} }{begin} ) {
        ${$self}{body} =~ s/\A(\h*)//s;
        ${$self}{begin} .= ( $1 ? $1 : q() );
    }

    # check for line breaks at end of body
    ${$self}{linebreaksAtEnd}{body} = ( ${$self}{body} =~ m/\h*\R$/s ? 1 : 0 );

    # find and store \\ [ but only temporarily; we need to do this before the hidden children routine
    if ( ${$self}{lookForChildCodeBlocks} ) {
        my @DBSstorage = ();
        ${$self}{body} =~ s/(\\\\\h+\[)/
               $hiddenChildCount++; 
               my $output = $tokens{alignmentBlock}.$hiddenChildCount.$tokens{endOfToken}; 
               push(@DBSstorage,{id=>$output, value=>$1 });
               $output;/sgex;

        # find and store hidden children
        ${$self}{body} =~ s/
           (?<ALLCODEBLOCKS>
             $allCodeBlocks 
           )/
           # store the code block
           my $anyCodeBlock = $+{ALLCODEBLOCKS};
           # remove leading space
           $anyCodeBlock =~ s"^(\s*)""s;
           my $leadingSpace = ($1?$1:q());
           $anyCodeBlock =~ s"(\s*)$""s;
           my $trailingSpace = ($1?$1:q());
           my $output = $anyCodeBlock; 
           # check for internal line breaks or \\
           if ($anyCodeBlock =~ m"\R"s 
                    or $anyCodeBlock =~m"(${$self}{doubleBackSlash})"s
                    or $anyCodeBlock =~m"${$self}{delimiterRegEx}"s){
               $hiddenChildCount++; 
               $output = $tokens{alignmentBlock}.$hiddenChildCount.$tokens{endOfToken}; 
               push(@hiddenChildrenStorage,{id=>$output,value=>$anyCodeBlock});
               $logger->trace("*found: hidden child in alignment block")         if $is_t_switch_active;
               ${$self}{measureHiddenChildren} = 1;
           }
           $output = $leadingSpace.$output.$trailingSpace; 
      $output;/sgex;

        # put DBS with h-space [ back in
        foreach (@DBSstorage) {
            ${$self}{body} =~ s/${$_}{id}/${$_}{value}/s;
        }
    }

    my $maximumNumberOfAmpersands = 0;

    # clear the global arrays
    @formattedBody     = ();
    @cellStorage       = ();
    @minMultiColSpan   = ();
    @maxColumnWidth    = ();
    @maxDelimiterWidth = ();

    # maximum column widths
    my @maximumColumnWidths;

    my $rowCounter    = -1;
    my $columnCounter = -1;

    $logger->trace("*dontMeasure routine, row mode") if ( ${$self}{dontMeasure} and $is_t_switch_active );

    # delimiters regex, default is:
    #
    #   (?<!\\)&
    #
    # which is set in GetYamlSettings.pm, but can be set
    # by the user using, for example
    #
    #   lookForAlignDelims:
    #       tabular:
    #           delimiter: '\<'
    #
    my $delimiterRegEx = qr/${$self}{delimiterRegEx}/;

    #
    # initial loop for column storage and measuring
    #
    foreach ( split( "\n", ${$self}{body} ) ) {
        $rowCounter++;

        # default is to measure this row, but it can be switched off by the dont_measure routine
        ${$self}{measureRow} = 1;

        # call the dont_measure routine
        $self->dont_measure( mode => "row", row => $_ ) if ${$self}{dontMeasure};

        # remove \\ and anything following it
        my $endPiece = q();
        if ( $_ =~ m/(\\\\.*)/ ) {
            if ( ${$self}{alignFinalDoubleBackSlash} ) {
                $logger->trace("alignFinalDoubleBackSlash active for ${$self}{name}") if ($is_t_switch_active);

                # for example, if we want:
                #
                #  Name & \shortstack{Hi \\ Lo} \\      <!--- Note this row!
                #  Foo  & Bar                   \\
                #
                # in the first row, note that the first \\
                # needs to be ignored, and we align by
                # the final double back slash

                $_ =~ s/(\\\\
                          (?:                      
                              (?!                 
                                  (?:\\\\)           
                              ).            # any character, but not \\
                          )*?$              # non-greedy
                        )//sx;
                $endPiece = $1;
            }
            else {
                $_ =~ s/(\\\\.*)//;
                $endPiece = $1;
            }
        }

        # remove any trailing comments
        my $trailingComments;
        if ( $_ =~ m/$trailingCommentRegExp/ ) {
            $_ =~ s/($trailingCommentRegExp)//;
            $trailingComments = $1;
        }

        # some rows shouldn't be formatted
        my $unformattedRow = $_;

        my $numberOfAmpersands = () = $_ =~ /$delimiterRegEx/g;
        $maximumNumberOfAmpersands = $numberOfAmpersands if ( $numberOfAmpersands > $maximumNumberOfAmpersands );

        # remove space at the beginning of a row, surrounding &, and at the end of the row
        $_ =~ s/(?<!\\)\h*($delimiterRegEx)\h*/$1/g;
        $_ =~ s/^\h*//g;
        $_ =~ s/\h*$//g;

        # if the line finishes with an &, then add an empty space,
        # otherwise the column count is off
        $_ .= ( $_ =~ m/$delimiterRegEx$/ ? " " : q() );

        # store the columns, which are either split by &
        # or otherwise simply the current line, if for example, the current line simply
        # contains \multicolumn{8}... \\  (see test-cases/texexchange/366841-zarko.tex, for example)
        my @columns = ( $_ =~ m/$delimiterRegEx/ ? split( /($delimiterRegEx)/, $_ ) : $_ );

        $columnCounter = -1;
        my $spanning = 0;

        foreach my $column (@columns) {

            # if a column contains only the delimiter, then we need to
            #       - measure it
            #       - add it and its length to the previous cell
            #       - remove it from the columns array
            #
            if ( $column =~ m/$delimiterRegEx/ ) {

                # update the delimiter to be used, and its associated length
                # for the *previous* cell
                my $spanningOffSet = ( $spanning > 0 ? $spanning - 1 : 0 );
                ${ $cellStorage[$rowCounter][ $columnCounter - $spanningOffSet ] }{delimiter} = $1;
                ${ $cellStorage[$rowCounter][ $columnCounter - $spanningOffSet ] }{delimiterLength}
                    = &get_column_width($1);

                # keep track of maximum delimiter width
                $maxDelimiterWidth[ $columnCounter - $spanningOffSet ] = (
                    defined $maxDelimiterWidth[ $columnCounter - $spanningOffSet ]
                    ? max( $maxDelimiterWidth[ $columnCounter - $spanningOffSet ],
                        ${ $cellStorage[$rowCounter][ $columnCounter - $spanningOffSet ] }{delimiterLength} )
                    : ${ $cellStorage[$rowCounter][ $columnCounter - $spanningOffSet ] }{delimiterLength}
                );

                # importantly, move on to the next column!
                next;
            }

            # otherwise increment the column counter, and proceed
            $columnCounter++;

            # reset spanning (only applicable if multiColumnGrouping)
            $spanning = 0;

            # if a column has finished with a \ then we need to add a trailing space,
            # otherwise the \ can be put next to &. See test-cases/texexchange/112343-gonzalo for example
            $column .= ( $column =~ m/\\$/ ? " " : q() );

            # basic cell storage
            $cellStorage[$rowCounter][$columnCounter] = (
                {   width           => &get_column_width($column),
                    entry           => $column,
                    type            => ( $numberOfAmpersands > 0 ? "X" : "*" ),
                    groupPadding    => 0,
                    colSpan         => ".",
                    delimiter       => "",
                    delimiterLength => 0,
                    measureThis     => ( $numberOfAmpersands > 0 ? ${$self}{measureRow} : 0 ),
                    DBSpadding      => 0,
                }
            );

            #
            # if content is to be aligned after \\
            # (using alignContentAfterDoubleBackSlash) then we need to
            # adjust the width of the first cell
            #
            if ( defined $input{beforeDBSlengths} and $columnCounter == 0 ) {
                my $currentRowDBSlength = ${ $input{beforeDBSlengths} }[ $rowCounter + 1 ];
                ${ $cellStorage[$rowCounter][$columnCounter] }{width}
                    += $currentRowDBSlength + ( $input{maxDBSlength} - $currentRowDBSlength );

                ${ $cellStorage[$rowCounter][$columnCounter] }{DBSpadding}
                    = ( $input{maxDBSlength} - $currentRowDBSlength );

            }

            # possible hidden children, see https://github.com/cmhughes/latexindent.pl/issues/85
            if ( ( ${$self}{measureHiddenChildren} or ${$self}{measureVerbatim} )
                and $column =~ m/.*?$tokens{beginOfToken}/s )
            {
                $self->hidden_child_cell_row_width( $column, $rowCounter, $columnCounter );
            }

            # store the maximum column width
            $maxColumnWidth[$columnCounter] = (
                defined $maxColumnWidth[$columnCounter]
                ? max( $maxColumnWidth[$columnCounter], ${ $cellStorage[$rowCounter][$columnCounter] }{width} )
                : ${ $cellStorage[$rowCounter][$columnCounter] }{width}
            ) if ${ $cellStorage[$rowCounter][$columnCounter] }{type} eq "X";

            # \multicolumn cell
            if ( ${$self}{multiColumnGrouping} and $column =~ m/\\multicolumn\{(\d+)\}/ and $1 > 1 ) {
                $spanning = $1;

                # adjust the type
                ${ $cellStorage[$rowCounter][$columnCounter] }{type} = "$spanning";

                # some \multicol cells can have their spanning information removed from type
                # so we store it in colSpan as well
                ${ $cellStorage[$rowCounter][$columnCounter] }{colSpan} = $spanning;

                # and don't measure it
                ${ $cellStorage[$rowCounter][$columnCounter] }{measureThis} = 0;

                # create 'gap' columns
                for ( my $j = $columnCounter + 1; $j <= $columnCounter + ( $spanning - 1 ); $j++ ) {
                    $cellStorage[$rowCounter][$j] = (
                        {   type              => "-",
                            entry             => '',
                            width             => 0,
                            individualPadding => 0,
                            groupPadding      => 0,
                            colSpan           => ".",
                            delimiter         => "",
                            delimiterLength   => 0,
                            DBSpadding        => 0,
                            measureThis       => 0
                        }
                    );
                }

                # store the minimum spanning value
                $minMultiColSpan[$columnCounter] = (
                    defined $minMultiColSpan[$columnCounter]
                    ? min( $minMultiColSpan[$columnCounter], $spanning )
                    : $spanning
                );

                # adjust the column counter
                $columnCounter += $spanning - 1;
            }
        }

        # store the information
        push(
            @formattedBody,
            {   row                => $_,
                endPiece           => $endPiece,
                trailingComment    => $trailingComments,
                numberOfAmpersands => $numberOfAmpersands,
                unformattedRow     => $unformattedRow
            }
        );

    }

    # store the maximum number of ampersands
    ${$self}{maximumNumberOfAmpersands} = $maximumNumberOfAmpersands;

    # blocks with nested multicolumns need some pre checking
    $self->multicolumn_pre_check if ${$self}{multiColumnGrouping};

    # maximum column width loop, and individual padding
    $self->individual_padding;

    # multi column rearrangement and multicolumn padding
    $self->multicolumn_padding if ${$self}{multiColumnGrouping};

    # multi column post check to ensure that multicolumn commands have received appropriate padding
    $self->multicolumn_post_check if ${$self}{multiColumnGrouping};

    # output to log file
    if ($is_t_switch_active) {
        &pretty_print_cell_info($_)
            for (
            "entry",       "type",               "colSpan",           "width",
            "measureThis", "maximumColumnWidth", "individualPadding", "groupPadding",
            "delimiter",   "delimiterLength",    "DBSpadding"
            );
    }

    # main formatting loop
    $self->main_formatting;

    if ( defined $input{measure_after_DBS} ) {

        # delete the original body
        ${$self}{body} = q();

        # update the body
        ${$self}{body} .= ${$_}{row} . "\n" for @formattedBody;

        return;
    }

    #
    # final \\ loop
    #
    foreach (@formattedBody) {

        # reset the padding
        my $padding = q();

        # possibly adjust the padding
        if ( ${$_}{row} !~ m/^\h*$/ ) {

            # remove trailing horizontal space if ${$self}{alignDoubleBackSlash} is set to 0
            ${$_}{row} =~ s/\h*$// if ( !${$self}{alignDoubleBackSlash} );

            # format spacing infront of \\
            if (    defined ${$self}{spacesBeforeDoubleBackSlash}
                and ${$self}{spacesBeforeDoubleBackSlash} < 0
                and !${$self}{alignDoubleBackSlash} )
            {
                # zero spaces (possibly resulting in un-aligned \\)
                $padding = q();
            }
            elsif ( defined ${$self}{spacesBeforeDoubleBackSlash}
                and ${$self}{spacesBeforeDoubleBackSlash} >= 0
                and !${$self}{alignDoubleBackSlash} )
            {
                # specified number of spaces (possibly resulting in un-aligned \\)
                $padding = " " x ( ${$self}{spacesBeforeDoubleBackSlash} );
            }
            else {
                # aligned \\
                $padding = " " x max( 0, ( ${$self}{maximumRowWidth} - ${$_}{rowWidth} ) );
            }
        }

        # format the row, and put the trailing \\ and trailing comments back into the row
        ${$_}{row}
            .= $padding
            . ( ${$_}{endPiece}        ? ${$_}{endPiece}        : q() )
            . ( ${$_}{trailingComment} ? ${$_}{trailingComment} : q() );

        # some rows shouldn't be formatted, and may only have trailing comments;
        # see test-cases/alignment/table4.tex for example
        if (( ${$_}{numberOfAmpersands} == 0 and !${$_}{endPiece} )
            or (    ${$_}{numberOfAmpersands} < ${$self}{maximumNumberOfAmpersands}
                and !${$self}{alignRowsWithoutMaxDelims}
                and !${$_}{endPiece} )
            )
        {
            ${$_}{row} = ( ${$_}{unformattedRow} ne "" ? ${$_}{unformattedRow} : q() )
                . ( ${$_}{trailingComment} ? ${$_}{trailingComment} : q() );
        }

        # spaces for leadingBlankColumn in operation
        if ( ${$self}{leadingBlankColumn} > -1 ) {
            $padding = " " x ( ${$self}{leadingBlankColumn} );
            ${$_}{row} =~ s/^\h*/$padding/s;
        }
    }

    #
    # put original body and 'after DBS body' together
    #
    if ( ${$self}{alignContentAfterDoubleBackSlash} ) {

        # check linebreaks at end of body
        ${$self}{linebreaksAtEnd}{body} = ( ${$self}{body} =~ s/\R$//s ? 1 : 0 );

        # check that spacesAfterDoubleBackSlash>=0
        ${$self}{spacesAfterDoubleBackSlash} = max( ${$self}{spacesAfterDoubleBackSlash}, 0 );

        my @beforeDBSlengths      = q();
        my @originalFormattedBody = @formattedBody;
        my $afterDBSbody          = q();
        my $maxDBSlength          = 0;
        foreach (@originalFormattedBody) {
            ${$_}{beforeDBS} = q();
            ${$_}{DBS}       = q();
            if ( ${$_}{row} =~ s/(.*?)(${$self}{doubleBackSlash})\h*//s ) {
                ${$_}{beforeDBS} = $1;
                ${$_}{DBS}       = $2;
            }
            ${$_}{DBS} .= " " x ( ${$self}{spacesAfterDoubleBackSlash} ) if ( ${$_}{DBS} ne '' );
            $afterDBSbody .= ${$_}{row} . "\n";
            push( @beforeDBSlengths, &get_column_width( ${$_}{beforeDBS} . ${$_}{DBS} ) );
            $maxDBSlength = max( $maxDBSlength, &get_column_width( ${$_}{beforeDBS} . ${$_}{DBS} ) );
        }

        ${$self}{body} = $afterDBSbody;
        ${$self}{body} =~ s/\h*\R$//s if !${$self}{linebreaksAtEnd}{body};
        $self->align_at_ampersand(
            measure_after_DBS => 1,
            beforeDBSlengths  => \@beforeDBSlengths,
            maxDBSlength      => $maxDBSlength
        );

        # create new afterDBSbody
        my @afterDBSbody = split( "\n", ${$self}{body} );

        # combine ORIGINAL body and ENDPIECE body
        my $index = -1;
        foreach (@originalFormattedBody) {
            $index++;
            ${$_}{row} = ${$_}{beforeDBS} . ${$_}{DBS};
            ${$_}{row} .= $afterDBSbody[$index];
        }

        @formattedBody = @originalFormattedBody;
    }

    # delete the original body
    ${$self}{body} = q();

    # update the body
    ${$self}{body} .= ${$_}{row} . "\n" for @formattedBody;

    # if the \end{} statement didn't originally have a line break before it, we need to remove the final
    # line break added by the above
    ${$self}{body} =~ s/\h*\R$//s if !${$self}{linebreaksAtEnd}{body};

    # if the \begin{} statement doesn't finish with a line break, then we adjust the indentation
    # to be the length of the begin statement.
    #
    # example:
    #
    #       \begin{align*}1&2\\
    #         3&4\\
    #         5 &    6
    #       \end{align*}
    #
    # goes to
    #
    #       \begin{align*}1 & 2 \\
    #                     3 & 4 \\
    #                     5 & 6
    #       \end{align*}
    #
    # see https://github.com/cmhughes/latexindent.pl/issues/223 for example

    ${$self}{beginAdjusted} = 0;
    if (   !${ ${$self}{linebreaksAtEnd} }{begin}
        and ${ $cellStorage[0][0] }{type} eq "X"
        and ${ $cellStorage[0][0] }{measureThis}
        and ${ $cellStorage[0][0] }{entry} ne '' )
    {

        my $lengthOfBegin = ${$self}{begin};
        if ( ( ${$self}{begin} eq '{' | ${$self}{begin} eq '[' ) and ${$self}{parentBegin} ) {
            $lengthOfBegin = ${$self}{parentBegin} . "{";
        }

        # remove space BEFORE begin statement; see test-cases/alignment/vassar00.tex
        $lengthOfBegin =~ s/\A\s*//s;
        ${$self}{indentation} = " " x ( &get_column_width($lengthOfBegin) );
        $logger->trace("Adjusting indentation of ${$self}{name} in AlignAtAmpersand routine") if ($is_t_switch_active);

        # begin statement has been adjusted
        ${$self}{beginAdjusted} = 1;
    }

    # finally finally, put hidden children back in
    foreach (@hiddenChildrenStorage) {
        ${$self}{body} =~ m/^(.*?)${$_}{id}/m;
        my $addedIndentation   = ( $1 ? " " x &get_column_width($1) : q() );
        my $hiddenChildContent = ${$_}{value};
        $hiddenChildContent =~ s"^"$addedIndentation"mg;
        $hiddenChildContent =~ s"^$addedIndentation""s;
        ${$self}{body}      =~ s/${$_}{id}/$hiddenChildContent/s;
    }

}

sub main_formatting {

    # PURPOSE:
    #   (1) perform the *padding* operations
    #       by adding
    #
    #           <spacesBeforeAmpersand>
    #           &
    #           <spacesEndAmpersand>
    #
    #       to the cell entries, accounting for
    #       the justification being LEFT or RIGHT
    #
    #   (2) measure the row width and store
    #       the maximum row width for use with
    #       the (possible) alignment of the \\
    #
    my $self = shift;

    ${$self}{maximumRowWidth} = 0;

    #
    # objective (1): padding
    #

    $logger->trace("*formatted rows for: ${$self}{name} (without \\\\)") if ($is_t_switch_active);

    my $rowCount = -1;

    # row loop
    foreach my $row (@cellStorage) {
        $rowCount++;

        # clear the temporary row
        my $tmpRow = q();

        # column loop
        foreach my $cell (@$row) {
            if ( ${$cell}{type} eq "*" or ${$cell}{type} eq "-" ) {
                $tmpRow .= ${$cell}{entry};
                next;
            }

            # alignment *after* double back slash, see
            #
            #   test-cases/alignment/issue-393.tex
            #
            $tmpRow .= " " x ${$cell}{DBSpadding};

            # the placement of the padding is dependent on the value of justification
            if ( ${$self}{justification} eq "left" ) {

                #
                # LEFT:
                #
                #   <cell entry> <individual padding> <group padding> ...
                $tmpRow .= ${$cell}{entry};
                $tmpRow .= " " x ${$cell}{individualPadding};
                $tmpRow .= " " x ${$cell}{groupPadding};
            }
            else {
                #
                # RIGHT:
                #
                #   <group padding> <individual padding> <cell entry> ...
                $tmpRow .= " " x ${$cell}{groupPadding};
                $tmpRow .= " " x ${$cell}{individualPadding};
                $tmpRow .= ${$cell}{entry};
            }

            # either way, finish with:  <spacesBeforeAmpersand> & <spacesAfterAmpersand>
            $tmpRow .= " " x ${$self}{spacesBeforeAmpersand};
            $tmpRow .= ${$cell}{delimiter};
            $tmpRow .= " " x ${$self}{spacesAfterAmpersand};
        }

        # if alignRowsWithoutMaxDelims = 0
        # and there are *less than* the maximum number of ampersands, then
        # we undo all of the work above!
        if (   !${$self}{alignRowsWithoutMaxDelims}
            and ${ $formattedBody[$rowCount] }{numberOfAmpersands} < ${$self}{maximumNumberOfAmpersands} )
        {
            $tmpRow = ${ $formattedBody[$rowCount] }{unformattedRow};
        }

        # spacing before \\
        my $finalSpacing = q();
        $finalSpacing = " " x ( ${$self}{spacesBeforeDoubleBackSlash} ) if ${$self}{spacesBeforeDoubleBackSlash} >= 1;
        $tmpRow =~ s/\h*$/$finalSpacing/;

        # if $tmpRow is made up of only horizontal space, then empty it
        $tmpRow = q() if ( $tmpRow =~ m/^\h*$/ );

        # to the log file
        $logger->trace($tmpRow) if ($is_t_switch_active);

        # store this formatted row
        ${ $formattedBody[$rowCount] }{row} = $tmpRow;

        #
        # objective (2): calculate row width and update maximumRowWidth
        #
        my $rowWidth = &get_column_width($tmpRow);

        # possibly update rowWidth if there are hidden children; see test-cases/alignment/hidden-child1.tex and friends
        $rowWidth = $self->hidden_child_row_width( $tmpRow, $rowCount, $rowWidth )
            if ( ${$self}{measureHiddenChildren} or ${$self}{measureVerbatim} );

        ${ $formattedBody[$rowCount] }{rowWidth} = $rowWidth;

        # update the maximum row width
        if ( $rowWidth > ${$self}{maximumRowWidth}
            and
            !( ${ $formattedBody[$rowCount] }{numberOfAmpersands} == 0 and !${ $formattedBody[$rowCount] }{endPiece} ) )
        {
            ${$self}{maximumRowWidth} = $rowWidth;
        }
    }
}

sub dont_measure {

    my $self  = shift;
    my %input = @_;

    if (    $input{mode} eq "cell"
        and ref( \${$self}{dontMeasure} ) eq "SCALAR"
        and ${$self}{dontMeasure} eq "largest"
        and defined ${ $cellStorage[ $input{row} ][ $input{column} ] }{width}
        and defined $maxColumnWidth[ $input{column} ]
        and ${ $cellStorage[ $input{row} ][ $input{column} ] }{width} == $maxColumnWidth[ $input{column} ] )
    {
        # dontMeasure stored as largest, for example
        #
        # lookForAlignDelims:
        #    tabular:
        #       dontMeasure: largest
        $logger->trace(
            "CELL FOUND with maximum column width, $maxColumnWidth[$input{column}], and will not be measured (largest mode)"
        ) if ($is_t_switch_active);
        $logger->trace( "column: ", $input{column}, " width: ",
            ${ $cellStorage[ $input{row} ][ $input{column} ] }{width} )
            if ($is_t_switch_active);
        $logger->trace( "entry: ", ${ $cellStorage[ $input{row} ][ $input{column} ] }{entry} ) if ($is_t_switch_active);
        $logger->trace("--------------------------")                                           if ($is_t_switch_active);
        ${ $cellStorage[ $input{row} ][ $input{column} ] }{measureThis} = 0;
        ${ $cellStorage[ $input{row} ][ $input{column} ] }{type}        = "X";
    }
    elsif ( $input{mode} eq "cell" and ( ref( ${$self}{dontMeasure} ) eq "ARRAY" ) ) {

        # loop through the entries in dontMeasure
        foreach ( @{ ${$self}{dontMeasure} } ) {
            if ( ref( \$_ ) eq "SCALAR" and ${ $cellStorage[ $input{row} ][ $input{column} ] }{entry} eq $_ ) {

                # dontMeasure stored as *strings*, for example:
                #
                #   lookForAlignDelims:
                #      tabular:
                #         dontMeasure:
                #           - \multicolumn{1}{c}{Expiry}
                #           - Tenor
                #           - \multicolumn{1}{c}{$\Delta_{\text{call},10}$}

                $logger->trace("CELL FOUND (this): $_ and will not be measured") if ($is_t_switch_active);
                ${ $cellStorage[ $input{row} ][ $input{column} ] }{measureThis} = 0;
                ${ $cellStorage[ $input{row} ][ $input{column} ] }{type}        = "X";
            }
            elsif ( ref($_) eq "HASH"
                and ${$_}{this}
                and ${ $cellStorage[ $input{row} ][ $input{column} ] }{entry} eq ${$_}{this} )
            {
                # for example:
                #
                #   lookForAlignDelims:
                #      tabular:
                #         dontMeasure:
                #           -
                #               this: \multicolumn{1}{c}{Expiry}
                #               applyTo: cell
                #
                # OR (note that applyTo is optional):
                #
                #   lookForAlignDelims:
                #      tabular:
                #         dontMeasure:
                #           -
                #               this: \multicolumn{1}{c}{Expiry}
                next if ( defined ${$_}{applyTo} and ${$_}{applyTo} ne "cell" );
                $logger->trace("CELL FOUND (this): ${$_}{this} and will not be measured") if ($is_t_switch_active);
                ${ $cellStorage[ $input{row} ][ $input{column} ] }{measureThis} = 0;
                ${ $cellStorage[ $input{row} ][ $input{column} ] }{type}        = "X";
            }
            elsif ( ref($_) eq "HASH" and ${$_}{regex} ) {

                # for example:
                #
                #   lookForAlignDelims:
                #      tabular:
                #         dontMeasure:
                #           -
                #               regex: \multicolumn{1}{c}{Expiry}
                #               applyTo: cell
                #
                # OR (note that applyTo is optional):
                #
                #   lookForAlignDelims:
                #      tabular:
                #         dontMeasure:
                #           -
                #               regex: \multicolumn{1}{c}{Expiry}
                next if ( defined ${$_}{applyTo} and ${$_}{applyTo} ne "cell" );
                my $regex = qr/${$_}{regex}/;
                next unless ${ $cellStorage[ $input{row} ][ $input{column} ] }{entry} =~ m/${$_}{regex}/;
                $logger->trace("CELL FOUND (regex): ${$_}{regex} and will not be measured") if ($is_t_switch_active);
                ${ $cellStorage[ $input{row} ][ $input{column} ] }{measureThis} = 0;
                ${ $cellStorage[ $input{row} ][ $input{column} ] }{type}        = "X";
            }
        }
    }
    elsif ( $input{mode} eq "row" and ( ref( ${$self}{dontMeasure} ) eq "ARRAY" ) ) {
        foreach ( @{ ${$self}{dontMeasure} } ) {

            # move on, unless we have specified applyTo as row:
            #
            #    lookForAlignDelims:
            #       tabular:
            #          dontMeasure:
            #            -
            #                this: \multicolumn{1}{c}{Expiry}
            #                applyTo: row
            #
            # note: *default value* of applyTo is cell
            next unless ( ref($_) eq "HASH" and defined ${$_}{applyTo} and ${$_}{applyTo} eq "row" );
            if ( ${$_}{this} and $input{row} eq ${$_}{this} ) {
                $logger->trace("ROW FOUND (this): ${$_}{this}") if ($is_t_switch_active);
                $logger->trace("and will not be measured")      if ($is_t_switch_active);
                ${$self}{measureRow} = 0;
            }
            elsif ( ${$_}{regex} and $input{row} =~ ${$_}{regex} ) {
                $logger->trace("ROW FOUND (regex): ${$_}{regex}") if ($is_t_switch_active);
                $logger->trace("and will not be measured")        if ($is_t_switch_active);
                ${$self}{measureRow} = 0;
            }
        }
    }
}

sub individual_padding {

    # PURPOSE
    #     (1) the *primary* purpose of this routine is to
    #         measure the *individual padding* of
    #         each cell.
    #
    #         for example, for
    #
    #            111 & 2  & 33333 \\
    #            4   & 55 & 66\\
    #
    #         then the individual padding will be
    #
    #            0    1   0
    #            2    0   3
    #
    #         this is calculated by looping
    #         through the rows & columns
    #         and finding the maximum column widths
    #
    #     (2) the *secondary* purpose of this routine is to
    #         fill in any gaps in the @cellStorage array
    #         for any entries that don't yet exist;
    #
    #         for example,
    #               111 & 2  & 33333 \\
    #               4   & 55 & 66\\
    #               77  & 8   <------ GAP HERE
    #
    #         there is a gap in @cellStorage in the final row,
    #         which is completed by the second loop in the below

    my $self = shift;

    # array to store maximum column widths
    my @maximumColumnWidths;

    # we count the maximum number of columns
    my $maximumNumberOfColumns = 0;

    #
    # maximum column width loop
    #

    $logger->trace("*dontMeasure routine, cell mode") if ( ${$self}{dontMeasure} and $is_t_switch_active );

    # row loop
    my $rowCount = -1;
    foreach my $row (@cellStorage) {
        $rowCount++;

        # column loop
        my $j = -1;
        foreach my $cell (@$row) {
            $j++;

            # if alignRowsWithoutMaxDelims = 0
            # and there are *less than* the maximum number of ampersands, then
            # don't measure this column
            if (   !${$self}{alignRowsWithoutMaxDelims}
                and ${ $formattedBody[$rowCount] }{numberOfAmpersands} < ${$self}{maximumNumberOfAmpersands} )
            {
                ${$cell}{measureThis} = 0;
                ${$cell}{type}        = "*";
            }

            # check if the cell shouldn't be measured
            $self->dont_measure( mode => "cell", row => $rowCount, column => $j ) if ${$self}{dontMeasure};

            # it's possible to have delimiters of different lengths, for example
            #
            #       \begin{tabbing}
            #       	1   # 22  \> 333   # 4444     \\
            #       	xxx # aaa #  yyyyy # zzzzzzzz \\
            #       	.   #     #  &     #          \\
            #
            #       	          ^^
            #       	          ||
            #       \end{tabbing}
            #
            # note that this has a delimiter of \> (length 2) and # (length 1)
            #
            # furthermore, it's possible to specify the delimiter justification as "left" or "right"
            if ( ${$cell}{delimiterLength} > 0 and ${$cell}{delimiterLength} < $maxDelimiterWidth[$j] ) {
                if ( ${$self}{delimiterJustification} eq "left" ) {
                    ${$cell}{delimiter} .= " " x ( $maxDelimiterWidth[$j] - ${$cell}{delimiterLength} );
                }
                elsif ( ${$self}{delimiterJustification} eq "right" ) {
                    ${$cell}{delimiter}
                        = " " x ( $maxDelimiterWidth[$j] - ${$cell}{delimiterLength} ) . ${$cell}{delimiter};
                }

                # update the delimiterLength
                ${$cell}{delimiterLength} = $maxDelimiterWidth[$j];
            }

            # to keep leadingBlankColumn on, we need to check:
            #
            #   - are we in the first column?
            #   - is leadingBlankColumn 0 or more?
            #   - cell width of first column equal 0?
            #   - are we measuring this cell?
            #
            # see test-cases/alignment/issue-275a.tex and the associated logfile
            #
            if ( $j == 0 and ${$self}{leadingBlankColumn} > -1 and ${$cell}{width} > 0 and ${$cell}{measureThis} == 1 )
            {
                ${$self}{leadingBlankColumn} = -1;
            }

            # there are some cells that shouldn't be accounted for in measuring,
            # for example {ccc}
            next if !${$cell}{measureThis};

            # otherwise, make the measurement
            $maximumColumnWidths[$j] = (
                defined $maximumColumnWidths[$j]
                ? max( $maximumColumnWidths[$j], ${$cell}{width} )
                : ${$cell}{width}
            );

        }

        # update the maximum number of columns
        $maximumNumberOfColumns = $j if ( $j > $maximumNumberOfColumns );
    }

    #
    # individual padding and gap filling loop
    #

    # row loop
    foreach my $row (@cellStorage) {

        # column loop
        foreach ( my $j = 0; $j <= $maximumNumberOfColumns; $j++ ) {
            if ( defined ${$row}[$j] ) {

                # individual padding
                my $maximum   = ( defined $maximumColumnWidths[$j] ? $maximumColumnWidths[$j] : 0 );
                my $cellWidth = ${$row}[$j]{width};
                ${$row}[$j]{individualPadding} += ( $maximum > $cellWidth ? $maximum - $cellWidth : 0 );
            }
            else {
                # gap filling
                ${$row}[$j] = (
                    {   type              => "-",
                        entry             => '',
                        width             => 0,
                        individualPadding => 0,
                        groupPadding      => 0,
                        measureThis       => 0,
                        colSpan           => ".",
                        delimiter         => "",
                        delimiterLength   => 0,
                        DBSpadding        => 0,
                    }
                );
            }

            # now the gaps have been filled, store the maximumColumnWidth for future reference
            ${$row}[$j]{maximumColumnWidth} = ( defined $maximumColumnWidths[$j] ? $maximumColumnWidths[$j] : 0 );
        }
    }
}

sub multicolumn_pre_check {

    # PURPOSE:
    #     ensure that multiple multicolumn commands are
    #     handled appropriately
    #
    #     example 1
    #
    #           \multicolumn{2}{c}{thing} &       \\
    #           111 & 2                   & 33333 \\
    #           4   & 55                  & 66    \\
    #           \multicolumn{2}{c}{a}     &       \\
    #
    #              ^^^^^^^^
    #              ||||||||
    #
    #     the second multicolumn command should not be measured, but
    #     *should* receive individual padding
    my $self = shift;

    # loop through minMultiColSpan and add empty entries as necessary
    foreach (@minMultiColSpan) {
        $_ = "." if !( defined $_ );
    }

    # ensure that only the *MINIMUM* multicolumn commands are designated
    # to be measured; for example:
    #
    #     \multicolumn{2}{c|}{Ótimo humano}      & Abuso da pontuação & Recompensas densas \\
    #     \multicolumn{3}{c||}{Exploração Fácil}                      & second             \\
    #     Assault     & Asterix                  & Beam Rider         & Alien              \\
    #
    # the \multicolumn{2}{c|}{Ótimo humano} *is* the minimum multicolumn command
    # for the first column, and the \multicolumn{3}{c||}{Exploração Fácil} *is not*
    # to be measured

    # row loop
    my $rowCount = -1;
    foreach my $row (@cellStorage) {
        $rowCount++;

        # column loop
        my $j = -1;
        foreach my $cell (@$row) {
            $j++;
            if ( ${$cell}{type} =~ m/(\d)/ and ( $1 > $minMultiColSpan[$j] ) ) {
                ${$cell}{type}        = "X";
                ${$cell}{measureThis} = 0;
            }
        }
    }

    # now loop back through and ensure that each of the \multicolumn commands
    # are measured correctly
    #
    # row loop
    $rowCount = -1;
    foreach my $row (@cellStorage) {
        $rowCount++;

        # column loop
        my $j = -1;
        foreach my $cell (@$row) {
            $j++;

            # multicolumn entry
            if ( ${$cell}{type} =~ m/(\d+)/ ) {

                my $multiColumnSpan = $1;

                # *inner* row loop
                my $innerRowCount = -1;
                foreach my $innerRow (@cellStorage) {
                    $innerRowCount++;

                    # we only want to measure the *other* rows
                    next if ( $innerRowCount == $rowCount );

                    # column loop
                    my $innerJ = -1;
                    foreach my $innerCell (@$innerRow) {
                        $innerJ++;

                        if ( $innerJ == $j and ${$innerCell}{type} =~ m/(\d)/ and $1 >= $multiColumnSpan ) {
                            if ( ${$cell}{width} < ${$innerCell}{width} ) {
                                ${$cell}{type}              = "X";
                                ${$cell}{measureThis}       = 0;
                                ${$cell}{individualPadding} = ( ${$innerCell}{width} - ${$cell}{width} );
                            }
                            else {
                                ${$innerCell}{type}              = "X";
                                ${$innerCell}{measureThis}       = 0;
                                ${$innerCell}{individualPadding} = ( ${$cell}{width} - ${$innerCell}{width} );
                            }
                        }
                    }
                }
            }
        }
    }
}

sub multicolumn_padding {

    # PURPOSE:
    #   assign multi column padding, for example:
    #
    #       \multicolumn{2}{c}{thing}&\\
    #       111 &2&33333 \\
    #       4& 55&66\\
    #
    #  needs to be transformed into
    #
    #       \multicolumn{2}{c}{thing} &       \\
    #       111 & 2                   & 33333 \\
    #       4   & 55                  & 66    \\
    #
    #                ^^^^^^^^^^^^^^^
    #                |||||||||||||||
    #
    #  and we need to compute the multi column padding,
    #  illustrated by the up arrows in the above
    #
    #  Approach:
    #   (1) measure the "grouping widths" under/above each of the
    #       \multicolumn entries; these are stored within
    #
    #               groupingWidth
    #
    #       for the relevant column entries; in the
    #       above example, they would be stored in the
    #
    #           2
    #           55
    #
    #       entries when justification is LEFT, and in the
    #
    #           111
    #           4
    #
    #       entries when justification is RIGHT. We also calculate
    #       maximum grouping width and store it within $maxGroupingWidth
    #
    #   (2) loop back through and update
    #
    #           groupPadding
    #
    #       for each of the relevant column entries; in the
    #       above example, they would be stored in the
    #
    #           2
    #           55
    #
    #       entries when justification is LEFT, and in the
    #
    #           111
    #           4
    #
    #   (3) finally, account for the \multicolumn command itself;
    #       ensuring that we account for it being wider or narrower
    #       than its spanning columns
    my $self = shift;

    # row loop
    my $rowCount = -1;
    foreach my $row (@cellStorage) {
        $rowCount++;

        # column loop
        my $j = -1;
        foreach my $cell (@$row) {
            $j++;

            # multicolumn entry
            next unless ( ${$cell}{type} =~ m/(\d+)/ );

            my $multiColumnSpan = $1;

            my $maxGroupingWidth = 0;

            # depending on the
            #
            #    justification
            #
            # setting (left or right), we store the
            #
            #    groupingWidth
            #
            # and groupPadding accordingly
            my $justificationOffset = ( ${$self}{justification} eq "left" ? $j + $multiColumnSpan - 1 : $j );

            #
            # phase (1)
            #

            # *inner* row loop
            my $innerRowCount = -1;
            foreach my $innerRow (@cellStorage) {
                $innerRowCount++;

                # we only want to measure the *other* rows
                next if ( $innerRowCount == $rowCount );

                # we will store the width of columns spanned by the multicolumn command
                my $groupingWidth = 0;

                # *inner* column loop
                for ( my $innerJ = 0; $innerJ < $multiColumnSpan; $innerJ++ ) {

                    # some entries should not be measured in the grouping width,
                    # for example
                    #
                    #       \multicolumn{2}{c}{thing} &       \\
                    #       111 & 2                   & 33333 \\
                    #       4   & 55                  & 66    \\
                    #       \multicolumn{2}{c}{a}     &       \\
                    #
                    #          ^^^^^^^^
                    #          ||||||||
                    #
                    # the second \multicolumn entry shouldn't be measured, and it will
                    # have had
                    #
                    #         measureThis
                    #
                    # switched off during multicolumn_pre_check

                    next if !${ $cellStorage[$innerRowCount][ $j + $innerJ ] }{measureThis};

                    $groupingWidth += ${ $cellStorage[$innerRowCount][ $j + $innerJ ] }{width};
                    $groupingWidth += ${ $cellStorage[$innerRowCount][ $j + $innerJ ] }{individualPadding};
                    $groupingWidth += ${ $cellStorage[$innerRowCount][ $j + $innerJ ] }{delimiterLength}
                        if ( $innerJ < $multiColumnSpan - 1 );
                }

                # adjust for
                #           <spacesBeforeAmpersand>
                #           &
                #           <spacesEndAmpersand>
                # note:
                #    we need to multiply this by the appropriate multicolumn
                #    spanning; in the above example:
                #
                #       \multicolumn{2}{c}{thing} &       \\
                #       111 & 2                   & 33333 \\
                #       4   & 55                  & 66    \\
                #
                #   we multiply by (2-1) = 1 because there is *1* ampersand
                #   underneath the multicolumn command
                #
                # note:
                #   the & will have been accounted for in the above section using delimiterLength

                $groupingWidth
                    += ( $multiColumnSpan - 1 ) * ( ${$self}{spacesBeforeAmpersand} + ${$self}{spacesAfterAmpersand} );

                # store the grouping width for the next phase
                ${ $cellStorage[$innerRowCount][$justificationOffset] }{groupingWidth} = $groupingWidth;

                # update the maximum grouping width
                $maxGroupingWidth = max( $maxGroupingWidth, $groupingWidth );

            }

            #
            # phase (2)
            #

            # now that the maxGroupingWidth has been established, loop back
            # through and update groupingWidth for the appropriate cells

            # *inner* row loop
            $innerRowCount = -1;
            foreach my $innerRow (@cellStorage) {
                $innerRowCount++;

                # we only want to make adjustment on the *other* rows
                next if ( $innerRowCount == $rowCount );

                # it's possible that we have multicolumn commands that were *not* measured,
                # and are *narrower* than the maxGroupingWidth, for example:
                #
                #        \multicolumn{3}{l}{aaaa}         &         \\    <------ this entry won't have been measured!
                #        $S_N$ & $=$ & $\SI{1000}{\kV\A}$ & $U_0$ & \\
                #        \multicolumn{3}{l}{bbbbbbb}      &         \\
                #
                if (    ${ $cellStorage[$innerRowCount][$j] }{colSpan} ne "."
                    and ${ $cellStorage[$innerRowCount][$j] }{colSpan} == $multiColumnSpan
                    and !${ $cellStorage[$innerRowCount][$j] }{measureThis}
                    and $maxGroupingWidth > ${ $cellStorage[$innerRowCount][$j] }{width}
                    and ${ $cellStorage[$innerRowCount][$j] }{width} >= ${$cell}{width} )
                {
                    ${ $cellStorage[$innerRowCount][$j] }{individualPadding} = 0;
                    ${ $cellStorage[$innerRowCount][$j] }{groupPadding}
                        = ( $maxGroupingWidth - ${ $cellStorage[$innerRowCount][$j] }{width} );
                }

                my $groupingWidth = ${ $cellStorage[$innerRowCount][$justificationOffset] }{groupingWidth};

                #
                # phase (3)
                #

                # there are two possible cases:
                #
                # (1) the \multicolumn statement is *WIDER* than its grouped columns, e.g.:
                #
                #       \multicolumn{2}{c}{thing} &       \\
                #       111 & 2                   & 33333 \\
                #       4   & 55                  & 66    \\
                #                ^^^^^^^^^^^^^^^
                #                |||||||||||||||
                #
                #     in this situation, we need to adjust each of the group paddings for the cells
                #     marked with an up arrow
                #
                # (2) the \multicolumn statement is *NARROWER* than its grouped columns, e.g.:
                #
                #                                 ||
                #                                 **
                #       \multicolumn{2}{c}{thing}    &       \\
                #       111 & bbbbbbbbbbbbbbbbbbbbbb & 33333 \\
                #       4   & 55                     & 66    \\
                #
                #     in this situation, we need to adjust the groupPadding of the multicolumn
                #     entry, which happens ***once the row loop has finished***
                #
                if ( ${$cell}{width} > $maxGroupingWidth ) {
                    ${ $cellStorage[$innerRowCount][$justificationOffset] }{groupPadding}
                        = ( ${$cell}{width} - $maxGroupingWidth );
                }
            }

            # case (2) from above when \multicolumn statement is *NARROWER* than its grouped columns
            if ( ${$cell}{width} < $maxGroupingWidth ) {
                ${$cell}{groupPadding} += ( $maxGroupingWidth - ${$cell}{width} );
                ${$cell}{individualPadding} = 0;
            }
        }

    }

}

sub multicolumn_post_check {

    # PURPOSE:
    #     ensure that multiple multicolumn commands are
    #     handled appropriately
    #
    #     example 1
    #
    #           \multicolumn{2}{c}{thing} & aaa     & bbb  \\
    #           111 & 2                   & 33333   &      \\
    #           4   & 55                  & 66      &      \\
    #           \multicolumn{3}{c}{a}               &      \\
    #
    #                                 ^^^^^^^^
    #                                 ||||||||
    #
    #     the second multicolumn command needs to have its group padding
    #     accounted for.
    #
    #     *Note* this will not have been done previously, as
    #     this second multicolumn command will have had its type changed to
    #     X, because it spans 3 columns, and there is a cell within its
    #     column that spans *less than 3 columns*
    my $self = shift;

    # row loop
    my $rowCount = -1;
    foreach my $row (@cellStorage) {
        $rowCount++;

        # column loop
        my $j = -1;
        foreach my $cell (@$row) {
            $j++;

            # we only need to account for X-type columns, with an integer colSpan
            next unless ( ${$cell}{type} eq "X" and ${$cell}{colSpan} =~ m/(\d+)/ );

            # multicolumn entry
            my $multiColumnSpan = $1;

            # we store the maximum grouping width
            my $maxGroupingWidth = 0;

            # *inner* row loop
            my $innerRowCount = -1;
            foreach my $innerRow (@cellStorage) {
                $innerRowCount++;

                # we only want to measure the *other* rows
                next if ( $innerRowCount == $rowCount );

                # we will store the width of columns spanned by the multicolumn command
                my $groupingWidth = 0;

              # we need to keep track of the number of ampersands encountered;
              #
              # for example:
              #
              #       \multicolumn{2}{c}{thing} & aaa     & bbb  \\
              #       111 & 2                   & 33333   &      \\
              #       4   & 55                  & 66      &      \\
              #       \multicolumn{3}{c}{a}               &      \\   <!----- focus on the multicolumn entry in this row
              #
              # focusing on the final multicolumn entry (spanning 3 columns),
              # when we loop back through the rows, we will encounter:
              #
              #   row 1: one ampersand
              #
              #       \multicolumn{2}{c}{thing} & aaa    ...
              #
              #   row 2: two ampersands
              #
              #       111 & 2                   & 33333  ...
              #
              #   row 3: two ampersands
              #
              #       4   & 55                  & 66     ...
              #
              # note: we start the counter at -1, because the count of ampersands
              #       is always one behind the cell count; for example, looking
              #       at row 3 in the above, in the above, in cell 1 (entry: 4)
              #       we have 0 ampersands, in cell 2 (entry: 55) we now have 1 ampersands, etc
                my $ampersandsEncountered = -1;

                # column loop
                my @dumRow = @$innerRow;
                foreach ( my $innerJ = 0; $innerJ <= $#dumRow; $innerJ++ ) {

                    # exit the loop if we've reached the multiColumn spanning width
                    last if ( $innerJ >= ($multiColumnSpan) );

                    # don't include '*' or '-' cell types in the calculations
                    my $type = ${ $cellStorage[$innerRowCount][ $j + $innerJ ] }{type};
                    next if ( $type eq "*" or $type eq "-" );

                    # update the grouping width
                    $groupingWidth += ${ $cellStorage[$innerRowCount][ $j + $innerJ ] }{width};
                    $groupingWidth += ${ $cellStorage[$innerRowCount][ $j + $innerJ ] }{individualPadding};

                    # update the number of & encountered
                    $ampersandsEncountered++;
                    $groupingWidth += ${ $cellStorage[$innerRowCount][ $j + $innerJ ] }{delimiterLength}
                        if ( $ampersandsEncountered > 0 );

              # and adjust the column count, if necessary; in the above example
              #
              #       \multicolumn{2}{c}{thing} & aaa     & bbb  \\
              #       111 & 2                   & 33333   &      \\
              #       4   & 55                  & 66      &      \\
              #       \multicolumn{3}{c}{a}               &      \\   <!----- focus on the multicolumn entry in this row
              #
              # the *first entry* \multicolumn{2}{c}{thing} spans 2 columns, so
              # we need to move the column count along accordingly

                    if ( ${ $cellStorage[$innerRowCount][ $j + $innerJ ] }{colSpan} =~ m/(\d+)/ ) {
                        $innerJ += $1 - 1;
                    }

                }

                # adjust for
                #           <spacesBeforeAmpersand>
                #           &
                #           <spacesEndAmpersand>
                # note:
                #    we need to multiply this by the appropriate
                #    number of *ampersandsEncountered*
                #
                #       \multicolumn{2}{c}{thing} &       \\
                #       111 & 2                   & 33333 \\
                #       4   & 55                  & 66    \\

                $groupingWidth
                    += $ampersandsEncountered * ( ${$self}{spacesBeforeAmpersand} + ${$self}{spacesAfterAmpersand} );

                # update the maximum grouping width
                $maxGroupingWidth = max( $maxGroupingWidth, $groupingWidth );
            }

            ${$cell}{individualPadding} = 0;

            # there are cases where the
            #
            #     maxGroupingWidth
            #
            # is *less than* the cell width, for example:
            #
            #    \multicolumn{4}{|c|}{\textbf{Search Results}} \\  <!------ the width of this multicolumn entry
            #    1  & D7  & R                       &          \\           is larger than maxGroupingWidth
            #    2  & D2  & R                       &          \\
            #    \multicolumn{3}{|c|}{\textbf{Avg}} &          \\
            #
            ${$cell}{groupPadding} = max( 0, $maxGroupingWidth - ${$cell}{width} );
        }
    }
}

sub pretty_print_cell_info {

    my $thingToPrint = ( defined $_[0] ? $_[0] : "entry" );

    $logger->trace("*cell information: $thingToPrint");

    $logger->trace( "minimum multi col span: " . join( ",", @minMultiColSpan ) ) if (@minMultiColSpan);

    foreach my $row (@cellStorage) {
        my $tmpLogFileLine = q();
        foreach my $cell (@$row) {
            $tmpLogFileLine .= ${$cell}{$thingToPrint} . "\t";
        }
        $logger->trace( ' ' . $tmpLogFileLine ) if ($is_t_switch_active);
    }

    if ( $thingToPrint eq "type" ) {
        $logger->trace("*key to types:");
        $logger->trace("\tX\tbasic cell, will be measured and aligned");
        $logger->trace("\t*\t will not be measured, and no ampersand");
        $logger->trace("\t-\t phantom/blank cell for gaps");
        $logger->trace("\t[0-9]\tmulticolumn cell, spanning multiple columns");
    }

}

sub double_back_slash_else {
    my $self = shift;
    my ( $DBSStartsOnOwnLine, $DBSFinishesWithLineBreak ) = @_;

    $logger->trace(
        "*double back slash poly-switch work for ${$self}{name}, DBSStartsOnOwnLine, DBSFinishesWithLineBreak")
        if $is_t_switch_active;

    my $DBSRegEx = qr{
         (\s*)
         (?:
           (?<DBS>
             (?<DBSSTATEMENT>
               ${$self}{doubleBackSlash}
             )
             $mSwitchOnlyTrailing 
           )
           |
           (?<ALLCODEBLOCKS>
             $allCodeBlocks 
           )
         )
        }xs;

    ${$self}{body} =~ s/$DBSRegEx/
                    my $begin = $1;
                    my $dbsOrCodeBlock = q();
                    if ($+{DBS}){
                        my $horizontalTrailingSpace=($+{TRAILINGHSPACE}?$+{TRAILINGHSPACE}:q());
                        my $trailingComment=($+{TRAILINGCOMMENT}?$+{TRAILINGCOMMENT}:q());
                        my $linebreaksAtEnd=($+{TRAILINGCOMMENT} ? q() : ( $+{TRAILINGLINEBREAK} ? $+{TRAILINGLINEBREAK} : q()));
                        my $dbsStatement = $+{DBSSTATEMENT};
                        my $horizontalSpaceBeforeDBS = (($begin =~m @^\h*$@s and $DBSStartsOnOwnLine > 0) ? $begin :  q());

                        my $codeBlockObj = LatexIndent::Special->new(name=>"dbs",
                                                          begin=>$begin.$dbsStatement,
                                                          body=>$horizontalTrailingSpace.$trailingComment.$linebreaksAtEnd,
                                                          end=>q(),
                                                          modifyLineBreaksYamlName=>"DBS",
                                                          type=>"middle",
                                                          BeginStartsOnOwnLine=>$DBSStartsOnOwnLine,
                                                          BodyStartsOnOwnLine=>$DBSFinishesWithLineBreak,
                        );

                        $logger->trace("*found: $dbsStatement \t type: DBS")         if $is_t_switch_active;

                        # mlb BEFORE \\
                        $codeBlockObj->_mlb_begin_starts_on_own_line if ${$codeBlockObj}{BeginStartsOnOwnLine} != 0;  

                        # mlb AFTER \\
                        # \\ needs particular attention when DBSFinishesWithLineBreak is -1, see
                        #    issue-426 -l issue-426,double-back-slash-finish-1 -o=+-mod-1
                        ${$codeBlockObj}{body} = " ".${$codeBlockObj}{body} if (${$codeBlockObj}{BodyStartsOnOwnLine} == -1 and ${$codeBlockObj}{body} !~ m"^\h"s );  
                        $codeBlockObj->_mlb_body_starts_on_own_line if ${$codeBlockObj}{BodyStartsOnOwnLine} != 0;  

                        # put DBS block back together
                        $dbsOrCodeBlock = $horizontalSpaceBeforeDBS.${$codeBlockObj}{begin}.${$codeBlockObj}{body}.${$codeBlockObj}{end}; 
                      } else {
                        $dbsOrCodeBlock = $begin.$+{ALLCODEBLOCKS};
                      };
                      $dbsOrCodeBlock;/sgex;

    ${$self}{body} = _mlb_line_break_token_adjust( ${$self}{body} );
    $self->_mlb_after_indentation_token_adjust;
    return;

}

# possible hidden children, see test-cases/alignment/issue-162.tex and friends
#
#     \begin{align}
#     	A & =\begin{array}{cc}      % <!--- Hidden child
#     		     BBB & CCC \\       % <!--- Hidden child
#     		     E   & F            % <!--- Hidden child
#     	     \end{array} \\         % <!--- Hidden child
#
#     	Z & =\begin{array}{cc}      % <!--- Hidden child
#     		     Y & X \\           % <!--- Hidden child
#     		     W & V              % <!--- Hidden child
#     	     \end{array}            % <!--- Hidden child
#     \end{align}
#

# PURPOSE:
#
#   measure CELL width that has hidden children
#
#     	A &     =\begin{array}{cc}
#     	             BBB & CCC \\
#     	             E   & F
#     	         \end{array} \\
#
#     	       ^^^^^^^^^^^^^^^^^^
#     	       ||||||||||||||||||
#
#     	              Cell
#
sub hidden_child_cell_row_width {
    my $self = shift;

    my ( $tmpCellEntry, $rowCounter, $columnCounter ) = @_;

    foreach (@hiddenChildrenStorage) {
        my $hiddenChildID      = ${$_}{id};
        my $hiddenChildContent = ${$_}{value};
        if ( $tmpCellEntry =~ m/.*?$hiddenChildID/s ) {
            $tmpCellEntry =~ s/$hiddenChildID/$hiddenChildContent/s;
        }
    }

    if ( $tmpCellEntry =~ m/$tokens{verbatim}/ ) {
        #
        # verbatim example:
        #
        #    \begin{tabular}{ll}
        #      Testing & Line 1                        \\
        #      Testing & Line 2                        \\
        #      Testing & Line 3 \verb|X| \\
        #      Testing & Line 4                        \\
        #    \end{tabular}
        while ( my ( $verbatimID, $child ) = each %verbatimStorage ) {
            if ( $tmpCellEntry =~ m/.*?$verbatimID/s ) {
                $tmpCellEntry =~ s/$verbatimID/${$child}{begin}${$child}{body}${$child}{end}/s;
            }
        }
    }
    my $bodyLineBreaks = 0;
    $bodyLineBreaks++ while ( $tmpCellEntry =~ m/\R/sg );
    if ( $bodyLineBreaks > 0 ) {
        my $maxRowWidthWithinCell = 0;
        foreach ( split( "\n", $tmpCellEntry ) ) {
            my $currentRowWidth = &get_column_width($_);
            $maxRowWidthWithinCell = $currentRowWidth if ( $currentRowWidth > $maxRowWidthWithinCell );
        }
        ${ $cellStorage[$rowCounter][$columnCounter] }{width} = $maxRowWidthWithinCell;
    }
    else {
        ${ $cellStorage[$rowCounter][$columnCounter] }{width} = &get_column_width($tmpCellEntry);
    }
}

# PURPOSE:
#
#   measure ROW width that has hidden children
#
#     	A & =\begin{array}{cc}
#     		     BBB & CCC \\
#     		     E   & F
#     	     \end{array} \\
#
#     	^^^^^^^^^^^^^^^^^^^^^^
#     	||||||||||||||||||||||
#
#     	          row
#
sub hidden_child_row_width {
    my $self = shift;

    my ( $tmpRow, $rowCount, $rowWidth ) = @_;

    # some alignment blocks have the 'begin' statement on the opening line:
    #
    #           this bit
    #       ||||||||||||||
    #       ``````````````
    #       \begin{align*}1 & 2 \\
    #                     3 & 4 \\
    #                     5 & 6
    #       \end{align*}
    #
    my $lengthOfBegin = 0;
    if (   !${ ${$self}{linebreaksAtEnd} }{begin}
        and ${ $cellStorage[0][0] }{type} eq "X"
        and ${ $cellStorage[0][0] }{measureThis} )
    {

        my $beginToMeasure = ${$self}{begin};
        if ( ( ${$self}{begin} eq '{' | ${$self}{begin} eq '[' ) and ${$self}{parentBegin} ) {
            $beginToMeasure = ${$self}{parentBegin} . "{";
        }
        $lengthOfBegin = &get_column_width($beginToMeasure);
        $tmpRow        = $beginToMeasure . $tmpRow if $rowCount == 0;
    }

    if ( $tmpRow =~ m/.*?$tokens{beginOfToken}/s ) {

        $tmpRow = ( "." x $lengthOfBegin ) . $tmpRow;

        foreach (@hiddenChildrenStorage) {
            my $hiddenChildID      = ${$_}{id};
            my $hiddenChildContent = ${$_}{value};
            if ( $tmpRow =~ m/(^.*)?$hiddenChildID/m ) {
                my $partBeforeId       = $1;
                my $lengthPartBeforeId = &get_column_width($partBeforeId);

                my $tmpBodyToMeasure
                    = join( "\n" . ( "." x ($lengthPartBeforeId) ), split( "\n", $hiddenChildContent ) );

                # remove trailing \\
                $tmpBodyToMeasure =~ s/(\\\\\h*$)//mg;

                $tmpRow =~ s/$hiddenChildID/$tmpBodyToMeasure/s;
            }
        }

        if ( $tmpRow =~ m/$tokens{verbatim}/ ) {
            #
            # verbatim example:
            #
            #    \begin{tabular}{ll}
            #      Testing & Line 1                        \\
            #      Testing & Line 2                        \\
            #      Testing & Line 3 \verb|X| \\
            #      Testing & Line 4                        \\
            #    \end{tabular}
            while ( my ( $verbatimID, $child ) = each %verbatimStorage ) {
                if ( $tmpRow =~ m/.*?$verbatimID/s ) {
                    $tmpRow =~ s/$verbatimID/${$child}{begin}${$child}{body}${$child}{end}/s;
                }
            }
        }

        my $bodyLineBreaks = 0;
        $bodyLineBreaks++ while ( $tmpRow =~ m/\R/sg );
        if ( $bodyLineBreaks > 0 ) {
            my $maxRowWidth = 0;

            foreach ( split( "\n", $tmpRow ) ) {
                my $currentRowWidth = &get_column_width($_);
                $maxRowWidth = $currentRowWidth if ( $currentRowWidth > $maxRowWidth );
            }
            $rowWidth = $maxRowWidth;
        }
        else {
            $rowWidth = &get_column_width($tmpRow);
        }
    }
    elsif (!${ ${$self}{linebreaksAtEnd} }{begin}
        and ${ $cellStorage[0][0] }{type} eq "X"
        and ${ $cellStorage[0][0] }{measureThis} )
    {
        $rowWidth = &get_column_width($tmpRow);
    }

    # possibly draw ruler to log file
    &draw_ruler_to_logfile( $tmpRow, $rowWidth ) if ($is_t_switch_active);
    return $rowWidth;
}

sub draw_ruler_to_logfile {

    # draw a ruler to the log file, useful for debugging
    #
    # example:
    #
    # ----|----|----|----|----|----|----|----|----|----|----|----|----|----|
    #    5   10   15   20   25   30   35   40   45   50   55   60   65   70
    #

    my ( $tmpRow, $maxRowWidth ) = @_;
    $logger->trace("*tmpRow:");

    foreach ( split( "\n", $tmpRow ) ) {
        my $currentRowWidth = &get_column_width($_);
        $logger->trace("$_ \t(length: $currentRowWidth)");
    }

    my $rulerMax = int( $maxRowWidth / 10 + 1.5 ) * 10;
    $logger->trace( ( "----|" x ( int( $rulerMax / 5 ) ) ) );

    my $ruler = q();
    for ( my $i = 1; $i <= $rulerMax / 5; $i++ ) { $ruler .= "   " . $i * 5 }
    $logger->trace($ruler);

}

sub get_column_width {

    my $stringToBeMeasured = $_[0];

    # default length measurement
    # credit/reference: https://perldoc.perl.org/perlunicook#%E2%84%9E-33:-String-length-in-graphemes
    unless ( $switch{GCString} ) {
        my $count = 0;
        while ( $stringToBeMeasured =~ /\X/g ) { $count++ }
        return $count;
    }

    # if GCString active, then use Unicode::GCString
    return Unicode::GCString->new($stringToBeMeasured)->columns();
}
1;
