package Perl::Tidy::VerticalAligner;
use strict;
use warnings;
our $VERSION = '20200110';

use Perl::Tidy::VerticalAligner::Alignment;
use Perl::Tidy::VerticalAligner::Line;

# The Perl::Tidy::VerticalAligner package collects output lines and
# attempts to line up certain common tokens, such as => and #, which are
# identified by the calling routine.
#
# There are two main routines: valign_input and flush.  Append acts as a
# storage buffer, collecting lines into a group which can be vertically
# aligned.  When alignment is no longer possible or desirable, it dumps
# the group to flush.
#
#     valign_input -----> flush
#
#     collects          writes
#     vertical          one
#     groups            group

BEGIN {

    # Caution: these debug flags produce a lot of output
    # They should all be 0 except when debugging small scripts

    use constant VALIGN_DEBUG_FLAG_APPEND  => 0;
    use constant VALIGN_DEBUG_FLAG_APPEND0 => 0;
    use constant VALIGN_DEBUG_FLAG_TERNARY => 0;
    use constant VALIGN_DEBUG_FLAG_TABS    => 0;

    my $debug_warning = sub {
        print STDOUT "VALIGN_DEBUGGING with key $_[0]\n";
        return;
    };

    VALIGN_DEBUG_FLAG_APPEND  && $debug_warning->('APPEND');
    VALIGN_DEBUG_FLAG_APPEND0 && $debug_warning->('APPEND0');
    VALIGN_DEBUG_FLAG_TERNARY && $debug_warning->('TERNARY');
    VALIGN_DEBUG_FLAG_TABS    && $debug_warning->('TABS');

}

use vars qw(
  $vertical_aligner_self
  $maximum_alignment_index
  $ralignment_list
  $maximum_jmax_seen
  $minimum_jmax_seen
  $previous_minimum_jmax_seen
  $previous_maximum_jmax_seen
  @group_lines
  $group_level
  $group_type
  $group_maximum_gap
  $marginal_match
  $last_level_written
  $last_leading_space_count
  $extra_indent_ok
  $zero_count
  $last_comment_column
  $last_side_comment_line_number
  $last_side_comment_length
  $last_side_comment_level
  $outdented_line_count
  $first_outdented_line_at
  $last_outdented_line_at
  $diagnostics_object
  $logger_object
  $file_writer_object
  @side_comment_history
  $comment_leading_space_count
  $is_matching_terminal_line
  $consecutive_block_comments

  $cached_line_text
  $cached_line_type
  $cached_line_flag
  $cached_seqno
  $cached_line_valid
  $cached_line_leading_space_count
  $cached_seqno_string

  $valign_buffer_filling
  @valign_buffer

  $seqno_string
  $last_nonblank_seqno_string

  $rOpts

  $rOpts_maximum_line_length
  $rOpts_variable_maximum_line_length
  $rOpts_continuation_indentation
  $rOpts_indent_columns
  $rOpts_tabs
  $rOpts_entab_leading_whitespace
  $rOpts_valign

  $rOpts_fixed_position_side_comment
  $rOpts_minimum_space_to_comment

);

sub initialize {

    (
        my $class, $rOpts, $file_writer_object, $logger_object,
        $diagnostics_object
    ) = @_;

    # variables describing the entire space group:
    $ralignment_list            = [];
    $group_level                = 0;
    $last_level_written         = -1;
    $extra_indent_ok            = 0;    # can we move all lines to the right?
    $last_side_comment_length   = 0;
    $maximum_jmax_seen          = 0;
    $minimum_jmax_seen          = 0;
    $previous_minimum_jmax_seen = 0;
    $previous_maximum_jmax_seen = 0;

    # variables describing each line of the group
    @group_lines = ();                  # list of all lines in group

    $outdented_line_count          = 0;
    $first_outdented_line_at       = 0;
    $last_outdented_line_at        = 0;
    $last_side_comment_line_number = 0;
    $last_side_comment_level       = -1;
    $is_matching_terminal_line     = 0;

    # most recent 3 side comments; [ line number, column ]
    $side_comment_history[0] = [ -300, 0 ];
    $side_comment_history[1] = [ -200, 0 ];
    $side_comment_history[2] = [ -100, 0 ];

    # valign_output_step_B cache:
    $cached_line_text                = "";
    $cached_line_type                = 0;
    $cached_line_flag                = 0;
    $cached_seqno                    = 0;
    $cached_line_valid               = 0;
    $cached_line_leading_space_count = 0;
    $cached_seqno_string             = "";

    # string of sequence numbers joined together
    $seqno_string               = "";
    $last_nonblank_seqno_string = "";

    # frequently used parameters
    $rOpts_indent_columns           = $rOpts->{'indent-columns'};
    $rOpts_tabs                     = $rOpts->{'tabs'};
    $rOpts_entab_leading_whitespace = $rOpts->{'entab-leading-whitespace'};
    $rOpts_fixed_position_side_comment =
      $rOpts->{'fixed-position-side-comment'};
    $rOpts_minimum_space_to_comment = $rOpts->{'minimum-space-to-comment'};
    $rOpts_maximum_line_length      = $rOpts->{'maximum-line-length'};
    $rOpts_variable_maximum_line_length =
      $rOpts->{'variable-maximum-line-length'};
    $rOpts_valign = $rOpts->{'valign'};

    $consecutive_block_comments = 0;
    forget_side_comment();

    initialize_for_new_group();

    $vertical_aligner_self = {};
    bless $vertical_aligner_self, $class;
    return $vertical_aligner_self;
}

sub initialize_for_new_group {
    @group_lines                 = ();
    $maximum_alignment_index     = -1;  # alignments in current group
    $zero_count                  = 0;   # count consecutive lines without tokens
    $group_maximum_gap           = 0;   # largest gap introduced
    $group_type                  = "";
    $marginal_match              = 0;
    $comment_leading_space_count = 0;
    $last_leading_space_count    = 0;
    return;
}

# interface to Perl::Tidy::Diagnostics routines
sub write_diagnostics {
    my $msg = shift;
    if ($diagnostics_object) {
        $diagnostics_object->write_diagnostics($msg);
    }
    return;
}

# interface to Perl::Tidy::Logger routines
sub warning {
    my ($msg) = @_;
    if ($logger_object) {
        $logger_object->warning($msg);
    }
    return;
}

sub write_logfile_entry {
    my ($msg) = @_;
    if ($logger_object) {
        $logger_object->write_logfile_entry($msg);
    }
    return;
}

sub report_definite_bug {
    if ($logger_object) {
        $logger_object->report_definite_bug();
    }
    return;
}

sub get_cached_line_count {
    my $self = shift;
    return @group_lines + ( $cached_line_type ? 1 : 0 );
}

sub get_spaces {

    # return the number of leading spaces associated with an indentation
    # variable $indentation is either a constant number of spaces or an
    # object with a get_spaces method.
    my $indentation = shift;
    return ref($indentation) ? $indentation->get_spaces() : $indentation;
}

sub get_recoverable_spaces {

    # return the number of spaces (+ means shift right, - means shift left)
    # that we would like to shift a group of lines with the same indentation
    # to get them to line up with their opening parens
    my $indentation = shift;
    return ref($indentation) ? $indentation->get_recoverable_spaces() : 0;
}

sub get_stack_depth {

    my $indentation = shift;
    return ref($indentation) ? $indentation->get_stack_depth() : 0;
}

sub make_alignment {
    my ( $col, $token ) = @_;

    # make one new alignment at column $col which aligns token $token
    ++$maximum_alignment_index;

    #my $alignment = new Perl::Tidy::VerticalAligner::Alignment(
    my $nlines    = @group_lines;
    my $alignment = Perl::Tidy::VerticalAligner::Alignment->new(
        column          => $col,
        starting_column => $col,
        matching_token  => $token,
        starting_line   => $nlines - 1,
        ending_line     => $nlines - 1,
        serial_number   => $maximum_alignment_index,
    );
    $ralignment_list->[$maximum_alignment_index] = $alignment;
    return $alignment;
}

sub dump_alignments {
    print STDOUT
"Current Alignments:\ni\ttoken\tstarting_column\tcolumn\tstarting_line\tending_line\n";
    for my $i ( 0 .. $maximum_alignment_index ) {
        my $column          = $ralignment_list->[$i]->get_column();
        my $starting_column = $ralignment_list->[$i]->get_starting_column();
        my $matching_token  = $ralignment_list->[$i]->get_matching_token();
        my $starting_line   = $ralignment_list->[$i]->get_starting_line();
        my $ending_line     = $ralignment_list->[$i]->get_ending_line();
        print STDOUT
"$i\t$matching_token\t$starting_column\t$column\t$starting_line\t$ending_line\n";
    }
    return;
}

sub save_alignment_columns {
    for my $i ( 0 .. $maximum_alignment_index ) {
        $ralignment_list->[$i]->save_column();
    }
    return;
}

sub restore_alignment_columns {
    for my $i ( 0 .. $maximum_alignment_index ) {
        $ralignment_list->[$i]->restore_column();
    }
    return;
}

sub forget_side_comment {
    $last_comment_column = 0;
    return;
}

sub maximum_line_length_for_level {

    # return maximum line length for line starting with a given level
    my $maximum_line_length = $rOpts_maximum_line_length;
    if ($rOpts_variable_maximum_line_length) {
        my $level = shift;
        if ( $level < 0 ) { $level = 0 }
        $maximum_line_length += $level * $rOpts_indent_columns;
    }
    return $maximum_line_length;
}

sub push_group_line {

    my ($new_line) = @_;
    push @group_lines, $new_line;
    return;
}

sub valign_input {

    # Place one line in the current vertical group.
    #
    # The input parameters are:
    #     $level = indentation level of this line
    #     $rfields = reference to array of fields
    #     $rpatterns = reference to array of patterns, one per field
    #     $rtokens   = reference to array of tokens starting fields 1,2,..
    #
    # Here is an example of what this package does.  In this example,
    # we are trying to line up both the '=>' and the '#'.
    #
    #         '18' => 'grave',    #   \`
    #         '19' => 'acute',    #   `'
    #         '20' => 'caron',    #   \v
    # <-tabs-><f1-><--field 2 ---><-f3->
    # |            |              |    |
    # |            |              |    |
    # col1        col2         col3 col4
    #
    # The calling routine has already broken the entire line into 3 fields as
    # indicated.  (So the work of identifying promising common tokens has
    # already been done).
    #
    # In this example, there will be 2 tokens being matched: '=>' and '#'.
    # They are the leading parts of fields 2 and 3, but we do need to know
    # what they are so that we can dump a group of lines when these tokens
    # change.
    #
    # The fields contain the actual characters of each field.  The patterns
    # are like the fields, but they contain mainly token types instead
    # of tokens, so they have fewer characters.  They are used to be
    # sure we are matching fields of similar type.
    #
    # In this example, there will be 4 column indexes being adjusted.  The
    # first one is always at zero.  The interior columns are at the start of
    # the matching tokens, and the last one tracks the maximum line length.
    #
    # Each time a new line comes in, it joins the current vertical
    # group if possible.  Otherwise it causes the current group to be dumped
    # and a new group is started.
    #
    # For each new group member, the column locations are increased, as
    # necessary, to make room for the new fields.  When the group is finally
    # output, these column numbers are used to compute the amount of spaces of
    # padding needed for each field.
    #
    # Programming note: the fields are assumed not to have any tab characters.
    # Tabs have been previously removed except for tabs in quoted strings and
    # side comments.  Tabs in these fields can mess up the column counting.
    # The log file warns the user if there are any such tabs.

    my ( $rline_hash, $rfields, $rtokens, $rpatterns ) = @_;
    my $level                     = $rline_hash->{level};
    my $level_end                 = $rline_hash->{level_end};
    my $indentation               = $rline_hash->{indentation};
    my $is_forced_break           = $rline_hash->{is_forced_break};
    my $outdent_long_lines        = $rline_hash->{outdent_long_lines};
    my $is_terminal_ternary       = $rline_hash->{is_terminal_ternary};
    my $is_terminal_statement     = $rline_hash->{is_terminal_statement};
    my $do_not_pad                = $rline_hash->{do_not_pad};
    my $rvertical_tightness_flags = $rline_hash->{rvertical_tightness_flags};
    my $level_jump                = $rline_hash->{level_jump};

    # number of fields is $jmax
    # number of tokens between fields is $jmax-1
    my $jmax = @{$rfields} - 1;

    my $leading_space_count = get_spaces($indentation);

    # set outdented flag to be sure we either align within statements or
    # across statement boundaries, but not both.
    my $is_outdented = $last_leading_space_count > $leading_space_count;
    $last_leading_space_count = $leading_space_count;

    # Patch: undo for hanging side comment
    my $is_hanging_side_comment =
      ( $jmax == 1 && $rtokens->[0] eq '#' && $rfields->[0] =~ /^\s*$/ );
    $is_outdented = 0 if $is_hanging_side_comment;

    # Forget side comment alignment after seeing 2 or more block comments
    my $is_block_comment = ( $jmax == 0 && $rfields->[0] =~ /^#/ );
    if ($is_block_comment) {
        $consecutive_block_comments++;
    }
    else {
        if ( $consecutive_block_comments > 1 ) { forget_side_comment() }
        $consecutive_block_comments = 0;
    }

    VALIGN_DEBUG_FLAG_APPEND0 && do {
        my $nlines = @group_lines;
        print STDOUT
"APPEND0: entering lines=$nlines new #fields= $jmax, leading_count=$leading_space_count last_cmt=$last_comment_column force=$is_forced_break, level_jump=$level_jump, level=$level, group_level=$group_level, level_jump=$level_jump\n";
    };

    # Validate cached line if necessary: If we can produce a container
    # with just 2 lines total by combining an existing cached opening
    # token with the closing token to follow, then we will mark both
    # cached flags as valid.
    if ($rvertical_tightness_flags) {
        if (   @group_lines <= 1
            && $cached_line_type
            && $cached_seqno
            && $rvertical_tightness_flags->[2]
            && $rvertical_tightness_flags->[2] == $cached_seqno )
        {
            $rvertical_tightness_flags->[3] ||= 1;
            $cached_line_valid ||= 1;
        }
    }

    # do not join an opening block brace with an unbalanced line
    # unless requested with a flag value of 2
    if (   $cached_line_type == 3
        && !@group_lines
        && $cached_line_flag < 2
        && $level_jump != 0 )
    {
        $cached_line_valid = 0;
    }

    # patch until new aligner is finished
    if ($do_not_pad) { my_flush() }

    # shouldn't happen:
    if ( $level < 0 ) { $level = 0 }

    # do not align code across indentation level changes
    # or if vertical alignment is turned off for debugging
    if ( $level != $group_level || $is_outdented || !$rOpts_valign ) {

        # we are allowed to shift a group of lines to the right if its
        # level is greater than the previous and next group
        $extra_indent_ok =
          ( $level < $group_level && $last_level_written < $group_level );

        my_flush();

        # If we know that this line will get flushed out by itself because
        # of level changes, we can leave the extra_indent_ok flag set.
        # That way, if we get an external flush call, we will still be
        # able to do some -lp alignment if necessary.
        $extra_indent_ok = ( $is_terminal_statement && $level > $group_level );

        $group_level = $level;

        # wait until after the above flush to get the leading space
        # count because it may have been changed if the -icp flag is in
        # effect
        $leading_space_count = get_spaces($indentation);

    }

    # --------------------------------------------------------------------
    # Collect outdentable block COMMENTS
    # --------------------------------------------------------------------
    my $is_blank_line = "";
    if ( $group_type eq 'COMMENT' ) {
        if (
            (
                   $is_block_comment
                && $outdent_long_lines
                && $leading_space_count == $comment_leading_space_count
            )
            || $is_blank_line
          )
        {
            push_group_line( $rfields->[0] );
            return;
        }
        else {
            my_flush();
        }
    }

    # --------------------------------------------------------------------
    # add dummy fields for terminal ternary
    # --------------------------------------------------------------------
    my $j_terminal_match;

    if ( $is_terminal_ternary && @group_lines ) {
        $j_terminal_match =
          fix_terminal_ternary( $group_lines[-1], $rfields, $rtokens,
            $rpatterns );
        $jmax = @{$rfields} - 1;
    }

    # --------------------------------------------------------------------
    # add dummy fields for else statement
    # --------------------------------------------------------------------

    if (   $rfields->[0] =~ /^else\s*$/
        && @group_lines
        && $level_jump == 0 )
    {

        $j_terminal_match =
          fix_terminal_else( $group_lines[-1], $rfields, $rtokens, $rpatterns );
        $jmax = @{$rfields} - 1;
    }

    # --------------------------------------------------------------------
    # Handle simple line of code with no fields to match.
    # --------------------------------------------------------------------
    if ( $jmax <= 0 ) {
        $zero_count++;

        if ( @group_lines
            && !get_recoverable_spaces( $group_lines[0]->get_indentation() ) )
        {

            # flush the current group if it has some aligned columns..
            if ( $group_lines[0]->get_jmax() > 1 ) { my_flush() }

            # flush current group if we are just collecting side comments..
            elsif (

                # ...and we haven't seen a comment lately
                ( $zero_count > 3 )

                # ..or if this new line doesn't fit to the left of the comments
                || ( ( $leading_space_count + length( $rfields->[0] ) ) >
                    $group_lines[0]->get_column(0) )
              )
            {
                my_flush();
            }
        }

        # start new COMMENT group if this comment may be outdented
        if (   $is_block_comment
            && $outdent_long_lines
            && !@group_lines )
        {
            $group_type                  = 'COMMENT';
            $comment_leading_space_count = $leading_space_count;
            push_group_line( $rfields->[0] );
            return;
        }

        # just write this line directly if no current group, no side comment,
        # and no space recovery is needed.
        if ( !@group_lines && !get_recoverable_spaces($indentation) ) {
            valign_output_step_B( $leading_space_count, $rfields->[0], 0,
                $outdent_long_lines, $rvertical_tightness_flags, $level );
            return;
        }
    }
    else {
        $zero_count = 0;
    }

    # programming check: (shouldn't happen)
    # an error here implies an incorrect call was made
    if ( @{$rfields} && ( @{$rtokens} != ( @{$rfields} - 1 ) ) ) {
        my $nt = @{$rtokens};
        my $nf = @{$rfields};
        warning(
"Program bug in Perl::Tidy::VerticalAligner - number of tokens = $nt should be one less than number of fields: $nf)\n"
        );
        report_definite_bug();
    }
    my $maximum_line_length_for_level = maximum_line_length_for_level($level);

    # --------------------------------------------------------------------
    # create an object to hold this line
    # --------------------------------------------------------------------
    my $new_line = Perl::Tidy::VerticalAligner::Line->new(
        jmax                      => $jmax,
        jmax_original_line        => $jmax,
        rtokens                   => $rtokens,
        rfields                   => $rfields,
        rpatterns                 => $rpatterns,
        indentation               => $indentation,
        leading_space_count       => $leading_space_count,
        outdent_long_lines        => $outdent_long_lines,
        list_type                 => "",
        is_hanging_side_comment   => $is_hanging_side_comment,
        maximum_line_length       => $maximum_line_length_for_level,
        rvertical_tightness_flags => $rvertical_tightness_flags,
        is_terminal_ternary       => $is_terminal_ternary,
        j_terminal_match          => $j_terminal_match,
    );

    # --------------------------------------------------------------------
    # It simplifies things to create a zero length side comment
    # if none exists.
    # --------------------------------------------------------------------
    make_side_comment( $new_line, $level_end );

    # --------------------------------------------------------------------
    # Decide if this is a simple list of items.
    # There are 3 list types: none, comma, comma-arrow.
    # We use this below to be less restrictive in deciding what to align.
    # --------------------------------------------------------------------
    if ($is_forced_break) {
        decide_if_list($new_line);
    }

    # --------------------------------------------------------------------
    # Append this line to the current group (or start new group)
    # --------------------------------------------------------------------
    if ( !@group_lines ) {
        add_to_group($new_line);
    }
    else {
        push_group_line($new_line);
    }

    # output this group if it ends in a terminal else or ternary line
    if ( defined($j_terminal_match) ) {
        my_flush();
    }

    # Force break after jump to lower level
    if ( $level_jump < 0 ) {
        my_flush();
    }

    # --------------------------------------------------------------------
    # Some old debugging stuff
    # --------------------------------------------------------------------
    VALIGN_DEBUG_FLAG_APPEND && do {
        print STDOUT "APPEND fields:";
        dump_array( @{$rfields} );
        print STDOUT "APPEND tokens:";
        dump_array( @{$rtokens} );
        print STDOUT "APPEND patterns:";
        dump_array( @{$rpatterns} );
        dump_alignments();
    };

    return;
}

sub join_hanging_comment {

    my $line = shift;
    my $jmax = $line->get_jmax();
    return 0 unless $jmax == 1;    # must be 2 fields
    my $rtokens = $line->get_rtokens();
    return 0 unless $rtokens->[0] eq '#';    # the second field is a comment..
    my $rfields = $line->get_rfields();
    return 0 unless $rfields->[0] =~ /^\s*$/;    # the first field is empty...
    my $old_line            = shift;
    my $maximum_field_index = $old_line->get_jmax();
    return 0
      unless $maximum_field_index > $jmax;    # the current line has more fields
    my $rpatterns = $line->get_rpatterns();

    $line->set_is_hanging_side_comment(1);
    $jmax = $maximum_field_index;
    $line->set_jmax($jmax);
    $rfields->[$jmax]         = $rfields->[1];
    $rtokens->[ $jmax - 1 ]   = $rtokens->[0];
    $rpatterns->[ $jmax - 1 ] = $rpatterns->[0];
    foreach my $j ( 1 .. $jmax - 1 ) {
        $rfields->[$j]         = " "; # NOTE: caused glitch unless 1 blank, why?
        $rtokens->[ $j - 1 ]   = "";
        $rpatterns->[ $j - 1 ] = "";
    }
    return 1;
}

sub eliminate_old_fields {

    my $new_line = shift;
    my $jmax     = $new_line->get_jmax();
    if ( $jmax > $maximum_jmax_seen ) { $maximum_jmax_seen = $jmax }
    if ( $jmax < $minimum_jmax_seen ) { $minimum_jmax_seen = $jmax }

    # there must be one previous line
    return unless ( @group_lines == 1 );

    my $old_line            = shift;
    my $maximum_field_index = $old_line->get_jmax();

    ###############################################
    # Moved below to allow new coding for => matches
    # return unless $maximum_field_index > $jmax;
    ###############################################

    # Identify specific cases where field elimination is allowed:
    # case=1: both lines have comma-separated lists, and the first
    #         line has an equals
    # case=2: both lines have leading equals

    # case 1 is the default
    my $case = 1;

    # See if case 2: both lines have leading '='
    # We'll require similar leading patterns in this case
    my $old_rtokens   = $old_line->get_rtokens();
    my $rtokens       = $new_line->get_rtokens();
    my $rpatterns     = $new_line->get_rpatterns();
    my $old_rpatterns = $old_line->get_rpatterns();
    if (   $rtokens->[0] =~ /^=>?\d*$/
        && $old_rtokens->[0] eq $rtokens->[0]
        && $old_rpatterns->[0] eq $rpatterns->[0] )
    {
        $case = 2;
    }

    # not too many fewer fields in new line for case 1
    return unless ( $case != 1 || $maximum_field_index - 2 <= $jmax );

    # case 1 must have side comment
    my $old_rfields = $old_line->get_rfields();
    return
      if ( $case == 1
        && length( $old_rfields->[$maximum_field_index] ) == 0 );

    my $rfields = $new_line->get_rfields();

    my $hid_equals = 0;

    my @new_alignments        = ();
    my @new_fields            = ();
    my @new_matching_patterns = ();
    my @new_matching_tokens   = ();

    my $j               = 0;
    my $current_field   = '';
    my $current_pattern = '';

    # loop over all old tokens
    my $in_match = 0;
    foreach my $k ( 0 .. $maximum_field_index - 1 ) {
        $current_field   .= $old_rfields->[$k];
        $current_pattern .= $old_rpatterns->[$k];
        last if ( $j > $jmax - 1 );

        if ( $old_rtokens->[$k] eq $rtokens->[$j] ) {
            $in_match                  = 1;
            $new_fields[$j]            = $current_field;
            $new_matching_patterns[$j] = $current_pattern;
            $current_field             = '';
            $current_pattern           = '';
            $new_matching_tokens[$j]   = $old_rtokens->[$k];
            $new_alignments[$j]        = $old_line->get_alignment($k);
            $j++;
        }
        else {

            if ( $old_rtokens->[$k] =~ /^\=\d*$/ ) {
                last if ( $case == 2 );    # avoid problems with stuff
                                           # like:   $a=$b=$c=$d;
                $hid_equals = 1;
            }
            last
              if ( $in_match && $case == 1 )
              ;    # disallow gaps in matching field types in case 1
        }
    }

    # Modify the current state if we are successful.
    # We must exactly reach the ends of the new list for success, and the old
    # pattern must have more fields. Here is an example where the first and
    # second lines have the same number, and we should not align:
    #  my @a = map chr, 0 .. 255;
    #  my @b = grep /\W/,    @a;
    #  my @c = grep /[^\w]/, @a;

    # Otherwise, we would get all of the commas aligned, which doesn't work as
    # well:
    #  my @a = map chr,      0 .. 255;
    #  my @b = grep /\W/,    @a;
    #  my @c = grep /[^\w]/, @a;

    if (   ( $j == $jmax )
        && ( $current_field eq '' )
        && ( $case != 1 || $hid_equals )
        && ( $maximum_field_index > $jmax ) )
    {
        my $k = $maximum_field_index;
        $current_field   .= $old_rfields->[$k];
        $current_pattern .= $old_rpatterns->[$k];
        $new_fields[$j]            = $current_field;
        $new_matching_patterns[$j] = $current_pattern;

        $new_alignments[$j] = $old_line->get_alignment($k);
        $maximum_field_index = $j;

        $old_line->set_alignments(@new_alignments);
        $old_line->set_jmax($jmax);
        $old_line->set_rtokens( \@new_matching_tokens );
        $old_line->set_rfields( \@new_fields );
        $old_line->set_rpatterns( \@{$rpatterns} );
    }

    # Dumb Down starting match if necessary:
    #
    # Consider the following two lines:
    #
    #  {
    #   $a => 20 > 3 ? 1 : 0,
    #   $xyz => 5,
    #  }

    # We would like to get alignment regardless of the order of the two lines.
    # If the lines come in in this order, then we will simplify the patterns of
    # the first line in sub eliminate_new_fields.  If the lines come in reverse
    # order, then we achieve this with eliminate_new_fields.

    # This update is currently restricted to leading '=>' matches. Although we
    # could do this for both '=' and '=>', overall the results for '=' come out
    # better without this step because this step can eliminate some other good
    # matches.  For example, with the '=' we get:

#  my @disilva = ( "di Silva", "diSilva", "di Si\x{301}lva", "diSi\x{301}lva" );
#  my @dsf     = map "$_\x{FFFE}Fred", @disilva;
#  my @dsj     = map "$_\x{FFFE}John", @disilva;
#  my @dsJ     = map "$_ John", @disilva;

    # without including '=' we get:

#  my @disilva = ( "di Silva", "diSilva", "di Si\x{301}lva", "diSi\x{301}lva" );
#  my @dsf = map "$_\x{FFFE}Fred", @disilva;
#  my @dsj = map "$_\x{FFFE}John", @disilva;
#  my @dsJ = map "$_ John",        @disilva;
    elsif (
        $case == 2

        && @new_matching_tokens == 1
        ##&& $new_matching_tokens[0] =~ /^=/   # see note above
        && $new_matching_tokens[0] =~ /^=>/
        && $maximum_field_index > 2
      )
    {
        my $jmaxm             = $jmax - 1;
        my $kmaxm             = $maximum_field_index - 1;
        my $have_side_comment = $old_rtokens->[$kmaxm] eq '#';

        # We need to reduce the group pattern to be just two tokens,
        # the leading equality or => and the final side comment

        my $mid_field = join "",
          @{$old_rfields}[ 1 .. $maximum_field_index - 1 ];
        my $mid_patterns = join "",
          @{$old_rpatterns}[ 1 .. $maximum_field_index - 1 ];
        my @new_alignments = (
            $old_line->get_alignment(0),
            $old_line->get_alignment( $maximum_field_index - 1 )
        );
        my @new_tokens =
          ( $old_rtokens->[0], $old_rtokens->[ $maximum_field_index - 1 ] );
        my @new_fields = (
            $old_rfields->[0], $mid_field, $old_rfields->[$maximum_field_index]
        );
        my @new_patterns = (
            $old_rpatterns->[0], $mid_patterns,
            $old_rpatterns->[$maximum_field_index]
        );

        $maximum_field_index = 2;
        $old_line->set_jmax($maximum_field_index);
        $old_line->set_rtokens( \@new_tokens );
        $old_line->set_rfields( \@new_fields );
        $old_line->set_rpatterns( \@new_patterns );

        initialize_for_new_group();
        add_to_group($old_line);
    }
    return;
}

# create an empty side comment if none exists
sub make_side_comment {
    my ( $new_line, $level_end ) = @_;
    my $jmax    = $new_line->get_jmax();
    my $rtokens = $new_line->get_rtokens();

    # if line does not have a side comment...
    if ( ( $jmax == 0 ) || ( $rtokens->[ $jmax - 1 ] ne '#' ) ) {
        my $rfields   = $new_line->get_rfields();
        my $rpatterns = $new_line->get_rpatterns();
        $rtokens->[$jmax]     = '#';
        $rfields->[ ++$jmax ] = '';
        $rpatterns->[$jmax]   = '#';
        $new_line->set_jmax($jmax);
        $new_line->set_jmax_original_line($jmax);
    }

    # line has a side comment..
    else {

        # don't remember old side comment location for very long
        my $line_number = $vertical_aligner_self->get_output_line_number();
        my $rfields     = $new_line->get_rfields();
        if (
            $line_number - $last_side_comment_line_number > 12

            # and don't remember comment location across block level changes
            || (   $level_end < $last_side_comment_level
                && $rfields->[0] =~ /^}/ )
          )
        {
            forget_side_comment();
        }
        $last_side_comment_line_number = $line_number;
        $last_side_comment_level       = $level_end;
    }
    return;
}

sub decide_if_list {

    my $line = shift;

    # A list will be taken to be a line with a forced break in which all
    # of the field separators are commas or comma-arrows (except for the
    # trailing #)

    # List separator tokens are things like ',3'   or '=>2',
    # where the trailing digit is the nesting depth.  Allow braces
    # to allow nested list items.
    my $rtokens    = $line->get_rtokens();
    my $test_token = $rtokens->[0];
    if ( $test_token =~ /^(\,|=>)/ ) {
        my $list_type = $test_token;
        my $jmax      = $line->get_jmax();

        foreach ( 1 .. $jmax - 2 ) {
            if ( $rtokens->[$_] !~ /^(\,|=>|\{)/ ) {
                $list_type = "";
                last;
            }
        }
        $line->set_list_type($list_type);
    }
    return;
}

sub eliminate_new_fields {

    my ( $new_line, $old_line ) = @_;
    return unless (@group_lines);
    my $jmax = $new_line->get_jmax();

    my $old_rtokens = $old_line->get_rtokens();
    my $rtokens     = $new_line->get_rtokens();
    my $is_assignment =
      ( $rtokens->[0] =~ /^=>?\d*$/ && ( $old_rtokens->[0] eq $rtokens->[0] ) );

    # must be monotonic variation
    return unless ( $is_assignment || $previous_maximum_jmax_seen <= $jmax );

    # must be more fields in the new line
    my $maximum_field_index = $old_line->get_jmax();
    return unless ( $maximum_field_index < $jmax );

    unless ($is_assignment) {
        return
          unless ( $old_line->get_jmax_original_line() == $minimum_jmax_seen )
          ;    # only if monotonic

        # never combine fields of a comma list
        return
          unless ( $maximum_field_index > 1 )
          && ( $new_line->get_list_type() !~ /^,/ );
    }

    my $rfields       = $new_line->get_rfields();
    my $rpatterns     = $new_line->get_rpatterns();
    my $old_rpatterns = $old_line->get_rpatterns();

    # loop over all OLD tokens except comment and check match
    my $match = 1;
    foreach my $k ( 0 .. $maximum_field_index - 2 ) {
        if (   ( $old_rtokens->[$k] ne $rtokens->[$k] )
            || ( $old_rpatterns->[$k] ne $rpatterns->[$k] ) )
        {
            $match = 0;
            last;
        }
    }

    # first tokens agree, so combine extra new tokens
    if ($match) {
        foreach my $k ( $maximum_field_index .. $jmax - 1 ) {

            $rfields->[ $maximum_field_index - 1 ] .= $rfields->[$k];
            $rfields->[$k] = "";
            $rpatterns->[ $maximum_field_index - 1 ] .= $rpatterns->[$k];
            $rpatterns->[$k] = "";
        }

        $rtokens->[ $maximum_field_index - 1 ] = '#';
        $rfields->[$maximum_field_index]       = $rfields->[$jmax];
        $rpatterns->[$maximum_field_index]     = $rpatterns->[$jmax];
        $jmax                                  = $maximum_field_index;
    }
    $new_line->set_jmax($jmax);
    return;
}

sub fix_terminal_ternary {

    # Add empty fields as necessary to align a ternary term
    # like this:
    #
    #  my $leapyear =
    #      $year % 4   ? 0
    #    : $year % 100 ? 1
    #    : $year % 400 ? 0
    #    :               1;
    #
    # returns 1 if the terminal item should be indented

    my ( $old_line, $rfields, $rtokens, $rpatterns ) = @_;
    return unless ($old_line);

## FUTURE CODING
##     my ( $old_line, $end_line ) = @_;
##     return unless ( $old_line && $end_line );
##
##     my $rfields   = $end_line->get_rfields();
##     my $rpatterns = $end_line->get_rpatterns();
##     my $rtokens   = $end_line->get_rtokens();

    my $jmax        = @{$rfields} - 1;
    my $rfields_old = $old_line->get_rfields();

    my $rpatterns_old       = $old_line->get_rpatterns();
    my $rtokens_old         = $old_line->get_rtokens();
    my $maximum_field_index = $old_line->get_jmax();

    # look for the question mark after the :
    my ($jquestion);
    my $depth_question;
    my $pad = "";
    foreach my $j ( 0 .. $maximum_field_index - 1 ) {
        my $tok = $rtokens_old->[$j];
        if ( $tok =~ /^\?(\d+)$/ ) {
            $depth_question = $1;

            # depth must be correct
            next unless ( $depth_question eq $group_level );

            $jquestion = $j;
            if ( $rfields_old->[ $j + 1 ] =~ /^(\?\s*)/ ) {
                $pad = " " x length($1);
            }
            else {
                return;    # shouldn't happen
            }
            last;
        }
    }
    return unless ( defined($jquestion) );    # shouldn't happen

    # Now splice the tokens and patterns of the previous line
    # into the else line to insure a match.  Add empty fields
    # as necessary.
    my $jadd = $jquestion;

    # Work on copies of the actual arrays in case we have
    # to return due to an error
    my @fields   = @{$rfields};
    my @patterns = @{$rpatterns};
    my @tokens   = @{$rtokens};

    VALIGN_DEBUG_FLAG_TERNARY && do {
        local $" = '><';
        print STDOUT "CURRENT FIELDS=<@{$rfields_old}>\n";
        print STDOUT "CURRENT TOKENS=<@{$rtokens_old}>\n";
        print STDOUT "CURRENT PATTERNS=<@{$rpatterns_old}>\n";
        print STDOUT "UNMODIFIED FIELDS=<@{$rfields}>\n";
        print STDOUT "UNMODIFIED TOKENS=<@{$rtokens}>\n";
        print STDOUT "UNMODIFIED PATTERNS=<@{$rpatterns}>\n";
    };

    # handle cases of leading colon on this line
    if ( $fields[0] =~ /^(:\s*)(.*)$/ ) {

        my ( $colon, $therest ) = ( $1, $2 );

        # Handle sub-case of first field with leading colon plus additional code
        # This is the usual situation as at the '1' below:
        #  ...
        #  : $year % 400 ? 0
        #  :               1;
        if ($therest) {

            # Split the first field after the leading colon and insert padding.
            # Note that this padding will remain even if the terminal value goes
            # out on a separate line.  This does not seem to look to bad, so no
            # mechanism has been included to undo it.
            my $field1 = shift @fields;
            unshift @fields, ( $colon, $pad . $therest );

            # change the leading pattern from : to ?
            return unless ( $patterns[0] =~ s/^\:/?/ );

            # install leading tokens and patterns of existing line
            unshift( @tokens,   @{$rtokens_old}[ 0 .. $jquestion ] );
            unshift( @patterns, @{$rpatterns_old}[ 0 .. $jquestion ] );

            # insert appropriate number of empty fields
            splice( @fields, 1, 0, ('') x $jadd ) if $jadd;
        }

        # handle sub-case of first field just equal to leading colon.
        # This can happen for example in the example below where
        # the leading '(' would create a new alignment token
        # : ( $name =~ /[]}]$/ ) ? ( $mname = $name )
        # :                        ( $mname = $name . '->' );
        else {

            return unless ( $jmax > 0 && $tokens[0] ne '#' ); # shouldn't happen

            # prepend a leading ? onto the second pattern
            $patterns[1] = "?b" . $patterns[1];

            # pad the second field
            $fields[1] = $pad . $fields[1];

            # install leading tokens and patterns of existing line, replacing
            # leading token and inserting appropriate number of empty fields
            splice( @tokens,   0, 1, @{$rtokens_old}[ 0 .. $jquestion ] );
            splice( @patterns, 1, 0, @{$rpatterns_old}[ 1 .. $jquestion ] );
            splice( @fields, 1, 0, ('') x $jadd ) if $jadd;
        }
    }

    # Handle case of no leading colon on this line.  This will
    # be the case when -wba=':' is used.  For example,
    #  $year % 400 ? 0 :
    #                1;
    else {

        # install leading tokens and patterns of existing line
        $patterns[0] = '?' . 'b' . $patterns[0];
        unshift( @tokens,   @{$rtokens_old}[ 0 .. $jquestion ] );
        unshift( @patterns, @{$rpatterns_old}[ 0 .. $jquestion ] );

        # insert appropriate number of empty fields
        $jadd = $jquestion + 1;
        $fields[0] = $pad . $fields[0];
        splice( @fields, 0, 0, ('') x $jadd ) if $jadd;
    }

    VALIGN_DEBUG_FLAG_TERNARY && do {
        local $" = '><';
        print STDOUT "MODIFIED TOKENS=<@tokens>\n";
        print STDOUT "MODIFIED PATTERNS=<@patterns>\n";
        print STDOUT "MODIFIED FIELDS=<@fields>\n";
    };

    # all ok .. update the arrays
    @{$rfields}   = @fields;
    @{$rtokens}   = @tokens;
    @{$rpatterns} = @patterns;
## FUTURE CODING
##     $end_line->set_rfields( \@fields );
##     $end_line->set_rtokens( \@tokens );
##     $end_line->set_rpatterns( \@patterns );

    # force a flush after this line
    return $jquestion;
}

sub fix_terminal_else {

    # Add empty fields as necessary to align a balanced terminal
    # else block to a previous if/elsif/unless block,
    # like this:
    #
    #  if   ( 1 || $x ) { print "ok 13\n"; }
    #  else             { print "not ok 13\n"; }
    #
    # returns a positive value if the else block should be indented
    #
    my ( $old_line, $rfields, $rtokens, $rpatterns ) = @_;
    return unless ($old_line);
    my $jmax = @{$rfields} - 1;
    return unless ( $jmax > 0 );

    # check for balanced else block following if/elsif/unless
    my $rfields_old = $old_line->get_rfields();

    # TBD: add handling for 'case'
    return unless ( $rfields_old->[0] =~ /^(if|elsif|unless)\s*$/ );

    # look for the opening brace after the else, and extract the depth
    my $tok_brace = $rtokens->[0];
    my $depth_brace;
    if ( $tok_brace =~ /^\{(\d+)/ ) { $depth_brace = $1; }

    # probably:  "else # side_comment"
    else { return }

    my $rpatterns_old       = $old_line->get_rpatterns();
    my $rtokens_old         = $old_line->get_rtokens();
    my $maximum_field_index = $old_line->get_jmax();

    # be sure the previous if/elsif is followed by an opening paren
    my $jparen    = 0;
    my $tok_paren = '(' . $depth_brace;
    my $tok_test  = $rtokens_old->[$jparen];
    return unless ( $tok_test eq $tok_paren );    # shouldn't happen

    # Now find the opening block brace
    my ($jbrace);
    foreach my $j ( 1 .. $maximum_field_index - 1 ) {
        my $tok = $rtokens_old->[$j];
        if ( $tok eq $tok_brace ) {
            $jbrace = $j;
            last;
        }
    }
    return unless ( defined($jbrace) );           # shouldn't happen

    # Now splice the tokens and patterns of the previous line
    # into the else line to insure a match.  Add empty fields
    # as necessary.
    my $jadd = $jbrace - $jparen;
    splice( @{$rtokens},   0, 0, @{$rtokens_old}[ $jparen .. $jbrace - 1 ] );
    splice( @{$rpatterns}, 1, 0, @{$rpatterns_old}[ $jparen + 1 .. $jbrace ] );
    splice( @{$rfields}, 1, 0, ('') x $jadd );

    # force a flush after this line if it does not follow a case
    if   ( $rfields_old->[0] =~ /^case\s*$/ ) { return }
    else                                      { return $jbrace }
}

{    # sub check_match
    my %is_good_alignment;

    BEGIN {

        # Vertically aligning on certain "good" tokens is usually okay
        # so we can be less restrictive in marginal cases.
        my @q = qw( { ? => = );
        push @q, (',');
        @is_good_alignment{@q} = (1) x scalar(@q);
    }

    sub check_match {

        # See if the current line matches the current vertical alignment group.
        # If not, flush the current group.
        my ( $new_line, $old_line ) = @_;

        # uses global variables:
        #  $previous_minimum_jmax_seen
        #  $maximum_jmax_seen
        #  $marginal_match
        my $jmax                = $new_line->get_jmax();
        my $maximum_field_index = $old_line->get_jmax();

        # flush if this line has too many fields
        # variable $GoToLoc indicates goto branch point, for debugging
        my $GoToLoc = 1;
        if ( $jmax > $maximum_field_index ) { goto NO_MATCH }

        # flush if adding this line would make a non-monotonic field count
        if (
            ( $maximum_field_index > $jmax )    # this has too few fields
            && (
                ( $previous_minimum_jmax_seen <
                    $jmax )                     # and wouldn't be monotonic
                || ( $old_line->get_jmax_original_line() != $maximum_jmax_seen )
            )
          )
        {
            $GoToLoc = 2;
            goto NO_MATCH;
        }

        # otherwise see if this line matches the current group
        my $jmax_original_line      = $new_line->get_jmax_original_line();
        my $is_hanging_side_comment = $new_line->get_is_hanging_side_comment();
        my $rtokens                 = $new_line->get_rtokens();
        my $rfields                 = $new_line->get_rfields();
        my $rpatterns               = $new_line->get_rpatterns();
        my $list_type               = $new_line->get_list_type();

        my $group_list_type = $old_line->get_list_type();
        my $old_rpatterns   = $old_line->get_rpatterns();
        my $old_rtokens     = $old_line->get_rtokens();

        my $jlimit = $jmax - 1;
        if ( $maximum_field_index > $jmax ) {
            $jlimit = $jmax_original_line;
            --$jlimit unless ( length( $new_line->get_rfields()->[$jmax] ) );
        }

        # handle comma-separated lists ..
        if ( $group_list_type && ( $list_type eq $group_list_type ) ) {
            for my $j ( 0 .. $jlimit ) {
                my $old_tok = $old_rtokens->[$j];
                next unless $old_tok;
                my $new_tok = $rtokens->[$j];
                next unless $new_tok;

                # lists always match ...
                # unless they would align any '=>'s with ','s
                $GoToLoc = 3;
                goto NO_MATCH
                  if ( $old_tok =~ /^=>/ && $new_tok =~ /^,/
                    || $new_tok =~ /^=>/ && $old_tok =~ /^,/ );
            }
        }

        # do detailed check for everything else except hanging side comments
        elsif ( !$is_hanging_side_comment ) {

            my $leading_space_count = $new_line->get_leading_space_count();

            my $max_pad = 0;
            my $min_pad = 0;
            my $saw_good_alignment;

            for my $j ( 0 .. $jlimit ) {

                my $old_tok = $old_rtokens->[$j];
                my $new_tok = $rtokens->[$j];

                # Note on encoding used for alignment tokens:
                # -------------------------------------------
                # Tokens are "decorated" with information which can help
                # prevent unwanted alignments.  Consider for example the
                # following two lines:
                #   local ( $xn, $xd ) = split( '/', &'rnorm(@_) );
                #   local ( $i, $f ) = &'bdiv( $xn, $xd );
                # There are three alignment tokens in each line, a comma,
                # an =, and a comma.  In the first line these three tokens
                # are encoded as:
                #    ,4+local-18     =3      ,4+split-7
                # and in the second line they are encoded as
                #    ,4+local-18     =3      ,4+&'bdiv-8
                # Tokens always at least have token name and nesting
                # depth.  So in this example the ='s are at depth 3 and
                # the ,'s are at depth 4.  This prevents aligning tokens
                # of different depths.  Commas contain additional
                # information, as follows:
                # ,  {depth} + {container name} - {spaces to opening paren}
                # This allows us to reject matching the rightmost commas
                # in the above two lines, since they are for different
                # function calls.  This encoding is done in
                # 'sub send_lines_to_vertical_aligner'.

                # Pick off actual token.
                # Everything up to the first digit is the actual token.
                my $alignment_token = $new_tok;
                if ( $alignment_token =~ /^([^\d]+)/ ) { $alignment_token = $1 }

                # see if the decorated tokens match
                my $tokens_match = $new_tok eq $old_tok

                  # Exception for matching terminal : of ternary statement..
                  # consider containers prefixed by ? and : a match
                  || ( $new_tok =~ /^,\d*\+\:/ && $old_tok =~ /^,\d*\+\?/ );

                # No match if the alignment tokens differ...
                if ( !$tokens_match ) {

                    # ...Unless this is a side comment
                    if (
                        $j == $jlimit

                        # and there is either at least one alignment token
                        # or this is a single item following a list.  This
                        # latter rule is required for 'December' to join
                        # the following list:
                        # my (@months) = (
                        #     '',       'January',   'February', 'March',
                        #     'April',  'May',       'June',     'July',
                        #     'August', 'September', 'October',  'November',
                        #     'December'
                        # );
                        # If it doesn't then the -lp formatting will fail.
                        && ( $j > 0 || $old_tok =~ /^,/ )
                      )
                    {
                        $marginal_match = 1
                          if ( $marginal_match == 0
                            && @group_lines == 1 );
                        last;
                    }

                    $GoToLoc = 4;
                    goto NO_MATCH;
                }

                # Calculate amount of padding required to fit this in.
                # $pad is the number of spaces by which we must increase
                # the current field to squeeze in this field.
                my $pad =
                  length( $rfields->[$j] ) - $old_line->current_field_width($j);
                if ( $j == 0 ) { $pad += $leading_space_count; }

                # remember max pads to limit marginal cases
                if ( $alignment_token ne '#' ) {
                    if ( $pad > $max_pad ) { $max_pad = $pad }
                    if ( $pad < $min_pad ) { $min_pad = $pad }
                }
                if ( $is_good_alignment{$alignment_token} ) {
                    $saw_good_alignment = 1;
                }

                # If patterns don't match, we have to be careful...
                if ( $old_rpatterns->[$j] ne $rpatterns->[$j] ) {

                    # flag this as a marginal match since patterns differ
                    $marginal_match = 1
                      if ( $marginal_match == 0 && @group_lines == 1 );

                    # We have to be very careful about aligning commas
                    # when the pattern's don't match, because it can be
                    # worse to create an alignment where none is needed
                    # than to omit one.  Here's an example where the ','s
                    # are not in named containers.  The first line below
                    # should not match the next two:
                    #   ( $a, $b ) = ( $b, $r );
                    #   ( $x1, $x2 ) = ( $x2 - $q * $x1, $x1 );
                    #   ( $y1, $y2 ) = ( $y2 - $q * $y1, $y1 );
                    if ( $alignment_token eq ',' ) {

                       # do not align commas unless they are in named containers
                        $GoToLoc = 5;
                        goto NO_MATCH unless ( $new_tok =~ /[A-Za-z]/ );
                    }

                    # do not align parens unless patterns match;
                    # large ugly spaces can occur in math expressions.
                    elsif ( $alignment_token eq '(' ) {

                        # But we can allow a match if the parens don't
                        # require any padding.
                        $GoToLoc = 6;
                        if ( $pad != 0 ) { goto NO_MATCH }
                    }

                    # Handle an '=' alignment with different patterns to
                    # the left.
                    elsif ( $alignment_token eq '=' ) {

                        # It is best to be a little restrictive when
                        # aligning '=' tokens.  Here is an example of
                        # two lines that we will not align:
                        #       my $variable=6;
                        #       $bb=4;
                        # The problem is that one is a 'my' declaration,
                        # and the other isn't, so they're not very similar.
                        # We will filter these out by comparing the first
                        # letter of the pattern.  This is crude, but works
                        # well enough.
                        if (
                            substr( $old_rpatterns->[$j], 0, 1 ) ne
                            substr( $rpatterns->[$j],     0, 1 ) )
                        {
                            $GoToLoc = 7;
                            goto NO_MATCH;
                        }

                        # If we pass that test, we'll call it a marginal match.
                        # Here is an example of a marginal match:
                        #       $done{$$op} = 1;
                        #       $op         = compile_bblock($op);
                        # The left tokens are both identifiers, but
                        # one accesses a hash and the other doesn't.
                        # We'll let this be a tentative match and undo
                        # it later if we don't find more than 2 lines
                        # in the group.
                        elsif ( @group_lines == 1 ) {
                            $marginal_match =
                              2;    # =2 prevents being undone below
                        }
                    }
                }

                # Don't let line with fewer fields increase column widths
                # ( align3.t )
                if ( $maximum_field_index > $jmax ) {

                    # Exception: suspend this rule to allow last lines to join
                    $GoToLoc = 8;
                    if ( $pad > 0 ) { goto NO_MATCH; }
                }
            } ## end for my $j ( 0 .. $jlimit)

            # Turn off the "marginal match" flag in some cases...
            # A "marginal match" occurs when the alignment tokens agree
            # but there are differences in the other tokens (patterns).
            # If we leave the marginal match flag set, then the rule is that we
            # will align only if there are more than two lines in the group.
            # We will turn of the flag if we almost have a match
            # and either we have seen a good alignment token or we
            # just need a small pad (2 spaces) to fit.  These rules are
            # the result of experimentation.  Tokens which misaligned by just
            # one or two characters are annoying.  On the other hand,
            # large gaps to less important alignment tokens are also annoying.
            if (   $marginal_match == 1
                && $jmax == $maximum_field_index
                && ( $saw_good_alignment || ( $max_pad < 3 && $min_pad > -3 ) )
              )
            {
                $marginal_match = 0;
            }
            ##print "marginal=$marginal_match saw=$saw_good_alignment jmax=$jmax max=$maximum_field_index maxpad=$max_pad minpad=$min_pad\n";
        }

        # We have a match (even if marginal).
        # If the current line has fewer fields than the current group
        # but otherwise matches, copy the remaining group fields to
        # make it a perfect match.
        if ( $maximum_field_index > $jmax ) {

            ##########################################################
            # FIXME: The previous version had a bug which made side comments
            # become regular fields, so for now the program does not allow a
            # line with side comment to match.  This should eventually be done.
            # The best test file for experimenting is 'lista.t'
            ##########################################################

            my $comment = $rfields->[$jmax];
            $GoToLoc = 9;
            goto NO_MATCH if ($comment);

            # Corrected loop
            for my $jj ( $jlimit .. $maximum_field_index ) {
                $rtokens->[$jj]         = $old_rtokens->[$jj];
                $rfields->[ $jj + 1 ]   = '';
                $rpatterns->[ $jj + 1 ] = $old_rpatterns->[ $jj + 1 ];
            }

##          THESE DO NOT GIVE CORRECT RESULTS
##          $rfields->[$jmax] = $comment;
##          $new_line->set_jmax($jmax);

        }
        return;

      NO_MATCH:

        # variable $GoToLoc is for debugging
        #print "no match from $GoToLoc\n";

        # Make one last effort to retain a match of certain statements
        my $match = salvage_equality_matches( $new_line, $old_line );
        my_flush_code() unless ($match);
        return;
    }
}

sub salvage_equality_matches {
    my ( $new_line, $old_line ) = @_;

    # Reduce the complexity of the two lines if it will allow us to retain
    # alignment of some common alignments, including '=' and '=>'.  We will
    # convert both lines to have just two matching tokens, the equality and the
    # side comment.

    # return 0 or undef if unsuccessful
    # return 1 if successful

    # Here is a very simple example of two lines where we could at least
    # align the equals:
    #  $x = $class->_sub( $x, $delta );
    #  $xpownm1 = $class->_pow( $class->_copy($x), $nm1 );    # x(i)^(n-1)

    # We will only do this if there is one old line (and one new line)
    return unless ( @group_lines == 1 );
    return if ($is_matching_terminal_line);

    # We are only looking for equality type statements
    my $old_rtokens = $old_line->get_rtokens();
    my $rtokens     = $new_line->get_rtokens();
    my $is_equals =
      ( $rtokens->[0] =~ /=/ && ( $old_rtokens->[0] eq $rtokens->[0] ) );
    return unless ($is_equals);

    # The leading patterns must match
    my $old_rpatterns = $old_line->get_rpatterns();
    my $rpatterns     = $new_line->get_rpatterns();
    return if ( $old_rpatterns->[0] ne $rpatterns->[0] );

    # Both should have side comment fields (should always be true)
    my $jmax_old    = $old_line->get_jmax();
    my $jmax_new    = $new_line->get_jmax();
    my $end_tok_old = $old_rtokens->[ $jmax_old - 1 ];
    my $end_tok_new = $rtokens->[ $jmax_new - 1 ];
    my $have_side_comments =
         defined($end_tok_old)
      && $end_tok_old eq '#'
      && defined($end_tok_new)
      && $end_tok_new eq '#';
    if ( !$have_side_comments ) { return; }

    # Do not match if any remaining tokens in new line include '?', 'if',
    # 'unless','||', '&&'. The reason is that (1) this isn't a great match, and
    # (2) we will prevent possibly better matchs to follow.  Here is an
    # example.  The match of the first two lines is rejected, and this allows
    # the second and third lines to match.
    #   my $type = shift || "o";
    #   my $fname  = ( $type eq 'oo'               ? 'orte_city' : 'orte' );
    #   my $suffix = ( $coord_system eq 'standard' ? ''          : '-orig' );
    # This logic can cause some unwanted losses of alignments, but it can retain
    # long runs of multiple-token alignments, so overall it is worthwhile.
    # If we had a peek at the subsequent line we could make a much better
    # decision here, but for now this is not available.
    for ( my $j = 1 ; $j < $jmax_new - 1 ; $j++ ) {
        my $new_tok = $rtokens->[$j];

        # git#16: do not consider fat commas as good aligmnents here
        my $is_good_alignment =
          ( $new_tok =~ /^(=|\?|if|unless|\|\||\&\&)/ && $new_tok !~ /^=>/ );
        return if ($is_good_alignment);
    }

    my $squeeze_line = sub {
        my ($line_obj) = @_;

        # reduce a line down to the three fields surrounding
        # the two tokens, an '=' of some sort and a '#' at the end

        my $jmax     = $line_obj->get_jmax();
        my $jmax_new = 2;
        return unless $jmax > $jmax_new;
        my $rfields     = $line_obj->get_rfields();
        my $rpatterns   = $line_obj->get_rpatterns();
        my $rtokens     = $line_obj->get_rtokens();
        my $rfields_new = [
            $rfields->[0], join( '', @{$rfields}[ 1 .. $jmax - 1 ] ),
            $rfields->[$jmax]
        ];
        my $rpatterns_new = [
            $rpatterns->[0], join( '', @{$rpatterns}[ 1 .. $jmax - 1 ] ),
            $rpatterns->[$jmax]
        ];
        my $rtokens_new = [ $rtokens->[0], $rtokens->[ $jmax - 1 ] ];
        $line_obj->{_rfields}   = $rfields_new;
        $line_obj->{_rpatterns} = $rpatterns_new;
        $line_obj->{_rtokens}   = $rtokens_new;
        $line_obj->set_jmax($jmax_new);
    };

    # Okay, we will force a match at the equals-like token.  We will fix both
    # lines to have just 2 tokens and 3 fields:
    $squeeze_line->($new_line);
    $squeeze_line->($old_line);

    # start over with a new group
    initialize_for_new_group();
    add_to_group($old_line);
    return 1;
}

sub check_fit {

    my ( $new_line, $old_line ) = @_;
    return unless (@group_lines);

    my $jmax                    = $new_line->get_jmax();
    my $leading_space_count     = $new_line->get_leading_space_count();
    my $is_hanging_side_comment = $new_line->get_is_hanging_side_comment();
    my $rtokens                 = $new_line->get_rtokens();
    my $rfields                 = $new_line->get_rfields();
    my $rpatterns               = $new_line->get_rpatterns();

    my $group_list_type = $group_lines[0]->get_list_type();

    my $padding_so_far    = 0;
    my $padding_available = $old_line->get_available_space_on_right();

    # save current columns in case this doesn't work
    save_alignment_columns();

    my $maximum_field_index = $old_line->get_jmax();
    for my $j ( 0 .. $jmax ) {

        my $pad = length( $rfields->[$j] ) - $old_line->current_field_width($j);

        if ( $j == 0 ) {
            $pad += $leading_space_count;
        }

        # remember largest gap of the group, excluding gap to side comment
        if (   $pad < 0
            && $group_maximum_gap < -$pad
            && $j > 0
            && $j < $jmax - 1 )
        {
            $group_maximum_gap = -$pad;
        }

        next if $pad < 0;

        ## OLD NOTES:
        ## This patch helps sometimes, but it doesn't check to see if
        ## the line is too long even without the side comment.  It needs
        ## to be reworked.
        ##don't let a long token with no trailing side comment push
        ##side comments out, or end a group.  (sidecmt1.t)
        ##next if ($j==$jmax-1 && length($rfields->[$jmax])==0);

        # BEGIN PATCH for keith1.txt.
        # If the group began matching multiple tokens but later this got
        # reduced to a fewer number of matching tokens, then the fields
        # of the later lines will still have to fit into their corresponding
        # fields.  So a large later field will "push" the other fields to
        # the right, including previous side comments, and if there is no room
        # then there is no match.
        # For example, look at the last line in the following snippet:

 # my $b_prod_db = ( $ENV{ORACLE_SID} =~ m/p$/ && !$testing ) ? true    : false;
 # my $env       = ($b_prod_db)                               ? "prd"   : "val";
 # my $plant     = ( $OPT{p} )                                ? $OPT{p} : "STL";
 # my $task      = $OPT{t};
 # my $fnam      = "longggggggggggggggg.$record_created.$env.$plant.idash";

        # The long term will push the '?' to the right to fit in, and in this
        # case there is not enough room so it will not match the equals unless
        # we do something special.

        # Usually it looks good to keep an initial alignment of '=' going, and
        # we can do this if the long term can fit in the space taken up by the
        # remaining fields (the ? : fields here).

        # Allowing any matching token for now, but it could be restricted
        # to an '='-like token if necessary.

        if (
               $pad > $padding_available
            && $jmax == 2                        # matching one thing (plus #)
            && $j == $jmax - 1                   # at last field
            && @group_lines > 1                  # more than 1 line in group now
            && $jmax < $maximum_field_index      # other lines have more fields
            && length( $rfields->[$jmax] ) == 0  # no side comment

            # Uncomment to match only equals (but this does not seem necessary)
            # && $rtokens->[0] =~ /^=\d/           # matching an equals
          )
        {
            my $extra_padding = 0;
            foreach my $jj ( $j + 1 .. $maximum_field_index - 1 ) {
                $extra_padding += $old_line->current_field_width($jj);
            }

            next if ( $pad <= $padding_available + $extra_padding );
        }

        # END PATCH for keith1.pl

        # This line will need space; lets see if we want to accept it..
        if (

            # not if this won't fit
            ( $pad > $padding_available )

            # previously, there were upper bounds placed on padding here
            # (maximum_whitespace_columns), but they were not really helpful

          )
        {

            # revert to starting state then flush; things didn't work out
            restore_alignment_columns();
            my_flush_code();
            last;
        }

        # patch to avoid excessive gaps in previous lines,
        # due to a line of fewer fields.
        #   return join( ".",
        #       $self->{"dfi"},  $self->{"aa"}, $self->rsvd,     $self->{"rd"},
        #       $self->{"area"}, $self->{"id"}, $self->{"sel"} );
        next if ( $jmax < $maximum_field_index && $j == $jmax - 1 );

        # looks ok, squeeze this field in
        $old_line->increase_field_width( $j, $pad );
        $padding_available -= $pad;

        # remember largest gap of the group, excluding gap to side comment
        if ( $pad > $group_maximum_gap && $j > 0 && $j < $jmax - 1 ) {
            $group_maximum_gap = $pad;
        }
    }
    return;
}

sub add_to_group {

    # The current line either starts a new alignment group or is
    # accepted into the current alignment group.
    my ($new_line) = @_;
    push_group_line($new_line);

    # initialize field lengths if starting new group
    if ( @group_lines == 1 ) {

        my $jmax    = $new_line->get_jmax();
        my $rfields = $new_line->get_rfields();
        my $rtokens = $new_line->get_rtokens();
        my $col     = $new_line->get_leading_space_count();

        for my $j ( 0 .. $jmax ) {
            $col += length( $rfields->[$j] );

            # create initial alignments for the new group
            my $token = "";
            if ( $j < $jmax ) { $token = $rtokens->[$j] }
            my $alignment = make_alignment( $col, $token );
            $new_line->set_alignment( $j, $alignment );
        }

        $maximum_jmax_seen = $jmax;
        $minimum_jmax_seen = $jmax;
    }

    # use previous alignments otherwise
    else {
        my @new_alignments = $group_lines[-2]->get_alignments();
        $new_line->set_alignments(@new_alignments);
    }

    # remember group jmax extremes for next call to valign_input
    $previous_minimum_jmax_seen = $minimum_jmax_seen;
    $previous_maximum_jmax_seen = $maximum_jmax_seen;
    return;
}

sub dump_array {

    # debug routine to dump array contents
    local $" = ')(';
    print STDOUT "(@_)\n";
    return;
}

# flush() sends the current Perl::Tidy::VerticalAligner group down the
# pipeline to Perl::Tidy::FileWriter.

# This is the external flush, which also empties the buffer and cache
sub flush {

    # the buffer must be emptied first, then any cached text
    dump_valign_buffer();

    if (@group_lines) {
        my_flush();
    }
    else {
        if ($cached_line_type) {
            $seqno_string = $cached_seqno_string;
            valign_output_step_C( $cached_line_text,
                $cached_line_leading_space_count,
                $last_level_written );
            $cached_line_type    = 0;
            $cached_line_text    = "";
            $cached_seqno_string = "";
        }
    }
    return;
}

sub reduce_valign_buffer_indentation {

    my ($diff) = @_;
    if ( $valign_buffer_filling && $diff ) {
        my $max_valign_buffer = @valign_buffer;
        foreach my $i ( 0 .. $max_valign_buffer - 1 ) {
            my ( $line, $leading_space_count, $level ) =
              @{ $valign_buffer[$i] };
            my $ws = substr( $line, 0, $diff );
            if ( ( length($ws) == $diff ) && $ws =~ /^\s+$/ ) {
                $line = substr( $line, $diff );
            }
            if ( $leading_space_count >= $diff ) {
                $leading_space_count -= $diff;
                $level = level_change( $leading_space_count, $diff, $level );
            }
            $valign_buffer[$i] = [ $line, $leading_space_count, $level ];
        }
    }
    return;
}

sub level_change {

    # compute decrease in level when we remove $diff spaces from the
    # leading spaces
    my ( $leading_space_count, $diff, $level ) = @_;
    if ($rOpts_indent_columns) {
        my $olev =
          int( ( $leading_space_count + $diff ) / $rOpts_indent_columns );
        my $nlev = int( $leading_space_count / $rOpts_indent_columns );
        $level -= ( $olev - $nlev );
        if ( $level < 0 ) { $level = 0 }
    }
    return $level;
}

sub dump_valign_buffer {
    if (@valign_buffer) {
        foreach (@valign_buffer) {
            valign_output_step_D( @{$_} );
        }
        @valign_buffer = ();
    }
    $valign_buffer_filling = "";
    return;
}

sub my_flush_comment {

    # Output a group of COMMENT lines

    return unless (@group_lines);
    my $leading_space_count = $comment_leading_space_count;
    my $leading_string      = get_leading_string($leading_space_count);

    # look for excessively long lines
    my $max_excess = 0;
    foreach my $str (@group_lines) {
        my $excess =
          length($str) +
          $leading_space_count -
          maximum_line_length_for_level($group_level);
        if ( $excess > $max_excess ) {
            $max_excess = $excess;
        }
    }

    # zero leading space count if any lines are too long
    if ( $max_excess > 0 ) {
        $leading_space_count -= $max_excess;
        if ( $leading_space_count < 0 ) { $leading_space_count = 0 }
        $last_outdented_line_at = $file_writer_object->get_output_line_number();
        unless ($outdented_line_count) {
            $first_outdented_line_at = $last_outdented_line_at;
        }
        my $nlines = @group_lines;
        $outdented_line_count += $nlines;
    }

    # write the lines
    my $outdent_long_lines = 0;
    foreach my $line (@group_lines) {
        valign_output_step_B( $leading_space_count, $line, 0,
            $outdent_long_lines, "", $group_level );
    }

    initialize_for_new_group();
    return;
}

sub my_flush_code {

    # Output a group of CODE lines

    return unless (@group_lines);

    VALIGN_DEBUG_FLAG_APPEND0
      && do {
        my $group_list_type = $group_lines[0]->get_list_type();
        my ( $a, $b, $c ) = caller();
        my $nlines              = @group_lines;
        my $maximum_field_index = $group_lines[0]->get_jmax();
        my $rfields_old         = $group_lines[0]->get_rfields();
        my $tok                 = $rfields_old->[0];
        print STDOUT
"APPEND0: my_flush_code called from $a $b $c fields=$maximum_field_index list=$group_list_type lines=$nlines extra=$extra_indent_ok first tok=$tok;\n";

      };

    # some small groups are best left unaligned
    my $do_not_align = decide_if_aligned_pair();

    # optimize side comment location
    $do_not_align = adjust_side_comment($do_not_align);

    # recover spaces for -lp option if possible
    my $extra_leading_spaces = get_extra_leading_spaces();

    # all lines of this group have the same basic leading spacing
    my $group_leader_length = $group_lines[0]->get_leading_space_count();

    # add extra leading spaces if helpful
    # NOTE: Use zero; this did not work well
    my $min_ci_gap = 0;

    # output the lines
    foreach my $line (@group_lines) {
        valign_output_step_A( $line, $min_ci_gap, $do_not_align,
            $group_leader_length, $extra_leading_spaces );
    }

    initialize_for_new_group();
    return;
}

sub my_flush {

    # This is the vertical aligner internal flush, which leaves the cache
    # intact
    return unless (@group_lines);

    VALIGN_DEBUG_FLAG_APPEND0 && do {
        my ( $a, $b, $c ) = caller();
        my $nlines = @group_lines;
        print STDOUT
"APPEND0: my_flush called from $a $b $c lines=$nlines, type=$group_type \n";
    };

    # handle a group of COMMENT lines
    if ( $group_type eq 'COMMENT' ) { my_flush_comment() }

    # handle a single line of CODE
    elsif ( @group_lines == 1 ) { my_flush_code() }

    # handle group(s) of CODE lines
    else {

        # LP FIX PART 1
        # If we are trying to add extra indentation for -lp formatting,
        # then we need to try to keep the group intact.  But we have
        # to set the $extra_indent_ok flag to zero in case some lines
        # are output separately.  We fix things up at the bottom.
        # NOTE: this is a workaround but is tentative; we should really look to
        # see if if extra indentation is possible.
        my $rOpt_lp              = $rOpts->{'line-up-parentheses'};
        my $keep_group_intact    = $rOpt_lp && $extra_indent_ok;
        my $extra_indent_ok_save = $extra_indent_ok;
        $extra_indent_ok = 0;

        # we will rebuild alignment line group(s);
        my @new_lines = @group_lines;
        initialize_for_new_group();

        # remove unmatched tokens in all lines
        delete_unmatched_tokens( \@new_lines );

        foreach my $new_line (@new_lines) {

            # Start a new group if necessary
            if ( !@group_lines ) {
                add_to_group($new_line);

                next;
            }

            my $j_terminal_match = $new_line->get_j_terminal_match();
            my $base_line        = $group_lines[0];

            # Initialize a global flag saying if the last line of the group
            # should match end of group and also terminate the group.  There
            # should be no returns between here and where the flag is handled
            # at the bottom.
            my $col_matching_terminal = 0;
            if ( defined($j_terminal_match) ) {

                # remember the column of the terminal ? or { to match with
                $col_matching_terminal =
                  $base_line->get_column($j_terminal_match);

                # set global flag for sub decide_if_aligned_pair
                $is_matching_terminal_line = 1;
            }

            # -------------------------------------------------------------
            # Allow hanging side comment to join current group, if any. This
            # will help keep side comments aligned, because otherwise we
            # will have to start a new group, making alignment less likely.
            # -------------------------------------------------------------

            if ( $new_line->get_is_hanging_side_comment() ) {
                join_hanging_comment( $new_line, $base_line );
            }

            # If this line has no matching tokens, then flush out the lines
            # BEFORE this line unless both it and the previous line have side
            # comments.  This prevents this line from pushing side coments out
            # to the right.
            elsif ( $new_line->get_jmax() == 1 && !$keep_group_intact ) {

                # There are no matching tokens, so now check side comments.
                # Programming note: accessing arrays with index -1 is
                # risky in Perl, but we have verified there is at least one
                # line in the group and that there is at least one field.
                my $prev_comment = $group_lines[-1]->get_rfields()->[-1];
                my $side_comment = $new_line->get_rfields()->[-1];
                my_flush_code() unless ( $side_comment && $prev_comment );

            }

            # -------------------------------------------------------------
            # If there is just one previous line, and it has more fields
            # than the new line, try to join fields together to get a match
            # with the new line.  At the present time, only a single
            # leading '=' is allowed to be compressed out.  This is useful
            # in rare cases where a table is forced to use old breakpoints
            # because of side comments,
            # and the table starts out something like this:
            #   my %MonthChars = ('0', 'Jan',   # side comment
            #                     '1', 'Feb',
            #                     '2', 'Mar',
            # Eliminating the '=' field will allow the remaining fields to
            # line up.  This situation does not occur if there are no side
            # comments because scan_list would put a break after the
            # opening '('.
            # -------------------------------------------------------------

            eliminate_old_fields( $new_line, $base_line );

            # -------------------------------------------------------------
            # If the new line has more fields than the current group,
            # see if we can match the first fields and combine the remaining
            # fields of the new line.
            # -------------------------------------------------------------

            eliminate_new_fields( $new_line, $base_line );

            # -------------------------------------------------------------
            # Flush previous group unless all common tokens and patterns
            # match..

            check_match( $new_line, $base_line );

            # -------------------------------------------------------------
            # See if there is space for this line in the current group (if
            # any)
            # -------------------------------------------------------------
            if (@group_lines) {
                check_fit( $new_line, $base_line );
            }

            add_to_group($new_line);

            if ( defined($j_terminal_match) ) {

                # if there is only one line in the group (maybe due to failure
                # to match perfectly with previous lines), then align the ? or
                # { of this terminal line with the previous one unless that
                # would make the line too long
                if ( @group_lines == 1 ) {
                    $base_line = $group_lines[0];
                    my $col_now = $base_line->get_column($j_terminal_match);
                    my $pad     = $col_matching_terminal - $col_now;
                    my $padding_available =
                      $base_line->get_available_space_on_right();
                    if ( $pad > 0 && $pad <= $padding_available ) {
                        $base_line->increase_field_width( $j_terminal_match,
                            $pad );
                    }
                }
                my_flush_code();
                $is_matching_terminal_line = 0;
            }

            # Optional optimization; end the group if we know we cannot match
            # next line.
            elsif ( $new_line->{_end_group} ) {
                my_flush_code();
            }
        }

        # LP FIX PART 2
        # if we managed to keep the group intact for -lp formatting,
        # restore the flag which allows extra indentation
        if ( $keep_group_intact && @group_lines == @new_lines ) {
            $extra_indent_ok = $extra_indent_ok_save;
        }
        my_flush_code();
    }
    return;
}

sub delete_selected_tokens {

    my ( $line_obj, $ridel ) = @_;

    # remove an unused alignment token(s) to improve alignment chances
    return unless ( defined($line_obj) && defined($ridel) && @{$ridel} );

    my $jmax_old      = $line_obj->get_jmax();
    my $rfields_old   = $line_obj->get_rfields();
    my $rpatterns_old = $line_obj->get_rpatterns();
    my $rtokens_old   = $line_obj->get_rtokens();

    local $" = '> <';
    0 && print <<EOM;
delete indexes: <@{$ridel}>
old jmax: $jmax_old
old tokens: <@{$rtokens_old}>
old patterns: <@{$rpatterns_old}>
old fields: <@{$rfields_old}>
EOM

    my $rfields_new   = [];
    my $rpatterns_new = [];
    my $rtokens_new   = [];

    my $kmax      = @{$ridel} - 1;
    my $k         = 0;
    my $jdel_next = $ridel->[$k];

    # FIXME:
    if ( $jdel_next < 0 ) { print STDERR "bad jdel_next=$jdel_next\n"; return }
    my $pattern = $rpatterns_old->[0];
    my $field   = $rfields_old->[0];
    push @{$rfields_new},   $field;
    push @{$rpatterns_new}, $pattern;
    for ( my $j = 0 ; $j < $jmax_old ; $j++ ) {
        my $token   = $rtokens_old->[$j];
        my $field   = $rfields_old->[ $j + 1 ];
        my $pattern = $rpatterns_old->[ $j + 1 ];
        if ( $k > $kmax || $j < $jdel_next ) {
            push @{$rtokens_new},   $token;
            push @{$rfields_new},   $field;
            push @{$rpatterns_new}, $pattern;
        }
        elsif ( $j == $jdel_next ) {
            $rfields_new->[-1]   .= $field;
            $rpatterns_new->[-1] .= $pattern;
            if ( ++$k <= $kmax ) {
                my $jdel_last = $jdel_next;
                $jdel_next = $ridel->[$k];
                if ( $jdel_next < $jdel_last ) {

                    # FIXME:
                    print STDERR "bad jdel_next=$jdel_next\n";
                    return;
                }
            }
        }
    }

    # ----- x ------ x ------ x ------
    #t      0        1        2        <- token indexing
    #f   0      1        2        3    <- field and pattern

    my $jmax_new = @{$rfields_new} - 1;
    $line_obj->set_rtokens($rtokens_new);
    $line_obj->set_rpatterns($rpatterns_new);
    $line_obj->set_rfields($rfields_new);
    $line_obj->set_jmax($jmax_new);

    0 && print <<EOM;

new jmax: $jmax_new
new tokens: <@{$rtokens_new}>
new patterns: <@{$rpatterns_new}>
new fields: <@{$rfields_new}>
EOM
    return;
}

sub decode_alignment_token {

    # Unpack the values packed in an alignment token
    #
    # Usage:
    #        my ( $raw_tok, $lev, $tag, $tok_count ) =
    #          decode_alignment_token($token);

    # Alignment tokens have a trailing decimal level and optional tag (for
    # commas):
    # For example, the first comma in the following line
    #     sub banner  { crlf; report( shift, '/', shift ); crlf }
    # is decorated as follows:
    #    ,2+report-6  => (tok,lev,tag) =qw( ,   2   +report-6)

    # An optional token count may be appended with a leading dot.
    # Currently this is only done for '=' tokens but this could change.
    # For example, consider the following line:
    #   $nport   = $port = shift || $name;
    # The first '=' may either be '=0' or '=0.1' [level 0, first equals]
    # The second '=' will be '=0.2' [level 0, second equals]
    my ($tok) = @_;
    my ( $raw_tok, $lev, $tag, $tok_count ) = ( $tok, 0, "", 1 );
    if ( $tok =~ /^(\D+)(\d+)([^\.]*)(\.(\d+))?$/ ) {
        $raw_tok   = $1;
        $lev       = $2;
        $tag       = $3 if ($3);
        $tok_count = $5 if ($5);
    }
    return ( $raw_tok, $lev, $tag, $tok_count );
}

{    # sub is_deletable_token

    my %is_deletable_equals;

    BEGIN {
        my @q;

        # These tokens with = may be deleted for vertical aligmnemt
        @q = qw(
          <= >= == =~ != <=>
        );
        @is_deletable_equals{@q} = (1) x scalar(@q);

    }

    sub is_deletable_token {

        # Determine if a token with no match possibility can be removed to
        # improve chances of making an alignment.
        my ( $token, $i, $imax, $jline, $i_eq ) = @_;

        my ( $raw_tok, $lev, $tag, $tok_count ) =
          decode_alignment_token($token);

        # okay to delete second and higher copies of a token
        if ( $tok_count > 1 ) { return 1 }

        # only remove lower level commas
        if ( $raw_tok eq ',' ) {

            return if ( defined($i_eq) && $i < $i_eq );
            return if ( $lev <= $group_level );
        }

        # most operators with an equals sign should be retained if at
        # same level as this statement
        elsif ( $raw_tok =~ /=/ ) {
            return
              unless ( $lev > $group_level || $is_deletable_equals{$raw_tok} );
        }

        # otherwise, ok to delete the token
        return 1;
    }
}

sub delete_unmatched_tokens {
    my ($rlines) = @_;

    # This is a preliminary step in vertical alignment in which we remove as
    # many obviously un-needed alignment tokens as possible.  This will prevent
    # them from interfering with the final alignment.

    return unless @{$rlines};
    my $has_terminal_match = $rlines->[-1]->get_j_terminal_match();

    # ignore hanging side comments in these operations
    my @filtered   = grep { !$_->{_is_hanging_side_comment} } @{$rlines};
    my $rnew_lines = \@filtered;
    my @i_equals;
    my @min_levels;

    my $jmax = @{$rnew_lines} - 1;

    my %is_good_tok;

    # create a hash of tokens for each line
    my $rline_hashes = [];
    foreach my $line ( @{$rnew_lines} ) {
        my $rhash   = {};
        my $rtokens = $line->get_rtokens();
        my $i       = 0;
        my $i_eq;
        my $lev_min;
        foreach my $tok ( @{$rtokens} ) {
            my ( $raw_tok, $lev, $tag, $tok_count ) =
              decode_alignment_token($tok);
            if ( !defined($lev_min) || $lev < $lev_min ) { $lev_min = $lev }

            # Possible future upgrade: for multiple matches,
            # record [$i1, $i2, ..] instead of $i
            $rhash->{$tok} =
              [ $i, undef, undef, $raw_tok, $lev, $tag, $tok_count ];

            # remember the first equals at line level
            if ( !defined($i_eq) && $raw_tok eq '=' ) {
                if ( $lev eq $group_level ) { $i_eq = $i }
            }
            $i++;
        }
        push @{$rline_hashes}, $rhash;
        push @i_equals,   $i_eq;
        push @min_levels, $lev_min;
    }

    # compare each line pair and record matches
    my $rtok_hash = {};
    my $nr        = 0;
    for ( my $jl = 0 ; $jl < $jmax ; $jl++ ) {
        my $nl = $nr;
        $nr = 0;
        my $jr      = $jl + 1;
        my $rhash_l = $rline_hashes->[$jl];
        my $rhash_r = $rline_hashes->[$jr];
        my $count   = 0;                      # UNUSED NOW?
        my $ntoks   = 0;
        foreach my $tok ( keys %{$rhash_l} ) {
            $ntoks++;
            if ( defined( $rhash_r->{$tok} ) ) {
                if ( $tok ne '#' ) { $count++; }
                my $il = $rhash_l->{$tok}->[0];
                my $ir = $rhash_r->{$tok}->[0];
                $rhash_l->{$tok}->[2] = $ir;
                $rhash_r->{$tok}->[1] = $il;
                if ( $tok ne '#' ) {
                    push @{ $rtok_hash->{$tok} }, ( $jl, $jr );
                    $nr++;
                }
            }
        }

        # Set a line break if no matching tokens between these lines
        if ( $nr == 0 && $nl > 0 ) {
            $rnew_lines->[$jl]->{_end_group} = 1;
        }
    }

    # find subgroups
    my @subgroups;
    push @subgroups, [ 0, $jmax ];
    for ( my $jl = 0 ; $jl < $jmax ; $jl++ ) {
        if ( $rnew_lines->[$jl]->{_end_group} ) {
            $subgroups[-1]->[1] = $jl;
            push @subgroups, [ $jl + 1, $jmax ];
        }
    }

    # Loop to process each subgroups
    foreach my $item (@subgroups) {
        my ( $jbeg, $jend ) = @{$item};

        # look for complete ternary or if/elsif/else blocks
        my $nlines = $jend - $jbeg + 1;
        my %token_line_count;
        for ( my $jj = $jbeg ; $jj <= $jend ; $jj++ ) {
            my %seen;
            my $line    = $rnew_lines->[$jj];
            my $rtokens = $line->get_rtokens();
            foreach my $tok ( @{$rtokens} ) {
                if ( !$seen{$tok} ) {
                    $seen{$tok}++;
                    $token_line_count{$tok}++;
                }
            }
        }

        # Look for if/else/elsif and ternary blocks
        my $is_full_block;
        foreach my $tok ( keys %token_line_count ) {
            if ( $token_line_count{$tok} == $nlines ) {
                if ( $tok =~ /^\?/ || $tok =~ /^\{\d+if/ ) {
                    $is_full_block = 1;
                }
            }
        }

        # remove unwanted alignment tokens
        for ( my $jj = $jbeg ; $jj <= $jend ; $jj++ ) {
            my $line    = $rnew_lines->[$jj];
            my $rtokens = $line->get_rtokens();
            my $rhash   = $rline_hashes->[$jj];
            my $i       = 0;
            my $i_eq    = $i_equals[$jj];
            my @idel;
            my $imax = @{$rtokens} - 2;
            my $delete_above_level;

            for ( my $i = 0 ; $i <= $imax ; $i++ ) {
                my $tok = $rtokens->[$i];
                next if ( $tok eq '#' );    # shouldn't happen
                my ( $iii, $il, $ir, $raw_tok, $lev, $tag, $tok_count ) =
                  @{ $rhash->{$tok} };

                # always remove unmatched tokens
                my $delete_me = !defined($il) && !defined($ir);

                # also, if this is a complete ternary or if/elsif/else block,
                # remove all alignments which are not also in every line
                $delete_me ||=
                  ( $is_full_block && $token_line_count{$tok} < $nlines );

                # Remove all tokens above a certain level following a previous
                # deletion.  For example, we have to remove tagged higher level
                # alignment tokens following a => deletion because the tags of
                # higher level tokens will now be incorrect. For example, this
                # will prevent aligning commas as follows after deleting the
                # second =>
                #    $w->insert(
                #	ListBox => origin => [ 270, 160 ],
                #	size    => [ 200,           55 ],
                #    );
                if ( defined($delete_above_level) ) {
                    if ( $lev > $delete_above_level ) {
                        $delete_me ||= 1;    #$tag;
                    }
                    else { $delete_above_level = undef }
                }

                if (
                    $delete_me
                    && is_deletable_token( $tok, $i, $imax, $jj, $i_eq )

                    # Patch: do not touch the first line of a terminal match,
                    # such as below, because j_terminal has already been set.
                    #    if ($tag) { $tago = "<$tag>"; $tagc = "</$tag>"; }
                    #    else      { $tago = $tagc = ''; }
                    # But see snippets 'else1.t' and 'else2.t'
                    && !( $jj == $jbeg && $has_terminal_match && $nlines == 2 )

                  )
                {
                    push @idel, $i;
                    if ( !defined($delete_above_level)
                        || $lev < $delete_above_level )
                    {

                        # delete all following higher level alignments
                        $delete_above_level = $lev;

                        # but keep deleting after => to next lower level
                        # to avoid some bizarre alignments
                        if ( $raw_tok eq '=>' ) {
                            $delete_above_level = $lev - 1;
                        }
                    }
                }
            }

            if (@idel) { delete_selected_tokens( $line, \@idel ) }
        }
    }    # End loop over subgroups

    return;
}

{        # decide_if_aligned_pair

    my %is_if_or;
    my %is_assignment;

    BEGIN {

        my @q = qw(
          if or ||
        );
        @is_if_or{@q} = (1) x scalar(@q);

        @q = qw(
          = **= += *= &= <<= &&=
          -= /= |= >>= ||= //=
          .= %= ^=
          x=
        );
        @is_assignment{@q} = (1) x scalar(@q);
    }

    sub decide_if_aligned_pair {

        # Do not try to align two lines which are not really similar
        return unless ( @group_lines == 2 );
        return if ($is_matching_terminal_line);

        # always align lists
        my $group_list_type = $group_lines[0]->get_list_type();
        return 0 if ($group_list_type);

        my $jmax0          = $group_lines[0]->get_jmax();
        my $jmax1          = $group_lines[1]->get_jmax();
        my $rtokens        = $group_lines[0]->get_rtokens();
        my $leading_equals = ( $rtokens->[0] =~ /=/ );

        # scan the tokens on the second line
        my $rtokens1 = $group_lines[1]->get_rtokens();
        my $saw_if_or;    # if we saw an 'if' or 'or' at group level
        my $raw_tokb = "";    # first token seen at group level
        for ( my $j = 0 ; $j < $jmax1 - 1 ; $j++ ) {
            my ( $raw_tok, $lev, $tag, $tok_count ) =
              decode_alignment_token( $rtokens1->[$j] );
            if ( $raw_tok && $lev == $group_level ) {
                if ( !$raw_tokb ) { $raw_tokb = $raw_tok }
                $saw_if_or ||= $is_if_or{$raw_tok};
            }
        }

        # A marginal match is a match which has different patterns. Normally,
        # we should not allow exactly two lines to match if marginal. But
        # we can allow matching in some specific cases.
        my $is_marginal = $marginal_match;

        # lines with differing number of alignment tokens are marginal
        $is_marginal ||=
          $previous_maximum_jmax_seen != $previous_minimum_jmax_seen
          && !$is_assignment{$raw_tokb};

        # We will use the line endings to help decide on alignments...
        # See if the lines end with semicolons...
        my $rpatterns0 = $group_lines[0]->get_rpatterns();
        my $rpatterns1 = $group_lines[1]->get_rpatterns();
        my $sc_term0;
        my $sc_term1;
        if ( $jmax0 < 1 || $jmax1 < 1 ) {

            # shouldn't happen
        }
        else {
            my $pat0 = $rpatterns0->[ $jmax0 - 1 ];
            my $pat1 = $rpatterns1->[ $jmax1 - 1 ];
            $sc_term0 = $pat0 =~ /;b?$/;
            $sc_term1 = $pat1 =~ /;b?$/;
        }

        if ( !$is_marginal && !$sc_term0 ) {

            # First line of assignment should be semicolon terminated.
            # For example, do not align here:
            #  $$href{-NUM_TEXT_FILES} = $$href{-NUM_BINARY_FILES} =
            #    $$href{-NUM_DIRS} = 0;
            if ( $is_assignment{$raw_tokb} ) {
                $is_marginal = 1;
            }
        }

        # Try to avoid some undesirable alignments of opening tokens
        # for example, the space between grep and { here:
        #  return map { ( $_ => $_ ) }
        #    grep     { /$handles/ } $self->_get_delegate_method_list;
        $is_marginal ||=
             ( $raw_tokb eq '(' || $raw_tokb eq '{' )
          && $jmax1 == 2
          && $sc_term0 ne $sc_term1;

        # Undo the marginal match flag in certain cases,
        if ($is_marginal) {

            # Two lines with a leading equals-like operator are allowed to
            # align if the patterns to the left of the equals are the same.
            # For example the following two lines are a marginal match but have
            # the same left side patterns, so we will align the equals.
            #     my $orig = my $format = "^<<<<< ~~\n";
            #     my $abc  = "abc";
            # But these have a different left pattern so they will not be
            # aligned
            #     $xmldoc .= $`;
            #     $self->{'leftovers'} .= "<bx-seq:seq" . $';

            # First line semicolon terminated but second not, usually ok:
            #               my $want = "'ab', 'a', 'b'";
            #               my $got  = join( ", ",
            #                    map { defined($_) ? "'$_'" : "undef" }
            #                          @got );
            #  First line not semicolon terminated, Not OK to match:
            #   $$href{-NUM_TEXT_FILES} = $$href{-NUM_BINARY_FILES} =
            #      $$href{-NUM_DIRS} = 0;
            my $pat0 = $rpatterns0->[0];
            my $pat1 = $rpatterns1->[0];

            ##########################################################
            # Turn off the marginal flag for some types of assignments
            ##########################################################
            if ( $is_assignment{$raw_tokb} ) {

                # undo marginal flag if first line is semicolon terminated
                # and leading patters match
                if ($sc_term0) {    # && $sc_term1) {
                    $is_marginal = $pat0 ne $pat1;
                }
            }
            elsif ( $raw_tokb eq '=>' ) {

                # undo marginal flag if patterns match
                $is_marginal = $pat0 ne $pat1;
            }
            elsif ( $raw_tokb eq '=~' ) {

                # undo marginal flag if both lines are semicolon terminated
                # and leading patters match
                if ( $sc_term1 && $sc_term0 ) {
                    $is_marginal = $pat0 ne $pat1;
                }
            }

            ######################################################
            # Turn off the marginal flag if we saw an 'if' or 'or'
            ######################################################

            # A trailing 'if' and 'or' often gives a good alignment
            # For example, we can align these:
            #  return -1     if $_[0] =~ m/^CHAPT|APPENDIX/;
            #  return $1 + 0 if $_[0] =~ m/^SECT(\d*)$/;

            # or
            #  $d_in_m[2] = 29          if ( &Date_LeapYear($y) );
            #  $d         = $d_in_m[$m] if ( $d > $d_in_m[$m] );

            if ($saw_if_or) {

                # undo marginal flag if both lines are semicolon terminated
                if ( $sc_term0 && $sc_term1 ) {
                    $is_marginal = 0;
                }
            }
        }

        ###############################
        # Set the return flag:
        # Don't align if still marginal
        ###############################
        my $do_not_align = $is_marginal;

        # But try to convert them into a simple comment group if the first line
        # a has side comment
        my $rfields             = $group_lines[0]->get_rfields();
        my $maximum_field_index = $group_lines[0]->get_jmax();
        if ( $do_not_align
            && ( length( $rfields->[$maximum_field_index] ) > 0 ) )
        {
            combine_fields();
            $do_not_align = 0;
        }
        return $do_not_align;
    }
}

sub adjust_side_comment {

    my $do_not_align = shift;

    # let's see if we can move the side comment field out a little
    # to improve readability (the last field is always a side comment field)
    my $have_side_comment       = 0;
    my $first_side_comment_line = -1;
    my $maximum_field_index     = $group_lines[0]->get_jmax();
    my $i                       = 0;
    foreach my $line (@group_lines) {
        if ( length( $line->get_rfields()->[$maximum_field_index] ) ) {
            $have_side_comment       = 1;
            $first_side_comment_line = $i;
            last;
        }
        $i++;
    }

    my $kmax = $maximum_field_index + 1;

    if ($have_side_comment) {

        my $line = $group_lines[0];

        # the maximum space without exceeding the line length:
        my $avail = $line->get_available_space_on_right();

        # try to use the previous comment column
        my $side_comment_column = $line->get_column( $kmax - 2 );
        my $move                = $last_comment_column - $side_comment_column;

##        my $sc_line0 = $side_comment_history[0]->[0];
##        my $sc_col0  = $side_comment_history[0]->[1];
##        my $sc_line1 = $side_comment_history[1]->[0];
##        my $sc_col1  = $side_comment_history[1]->[1];
##        my $sc_line2 = $side_comment_history[2]->[0];
##        my $sc_col2  = $side_comment_history[2]->[1];
##
##        # FUTURE UPDATES:
##        # Be sure to ignore 'do not align' and  '} # end comments'
##        # Find first $move > 0 and $move <= $avail as follows:
##        # 1. try sc_col1 if sc_col1 == sc_col0 && (line-sc_line0) < 12
##        # 2. try sc_col2 if (line-sc_line2) < 12
##        # 3. try min possible space, plus up to 8,
##        # 4. try min possible space

        if ( $kmax > 0 && !$do_not_align ) {

            # but if this doesn't work, give up and use the minimum space
            if ( $move > $avail ) {
                $move = $rOpts_minimum_space_to_comment - 1;
            }

            # but we want some minimum space to the comment
            my $min_move = $rOpts_minimum_space_to_comment - 1;
            if (   $move >= 0
                && $last_side_comment_length > 0
                && ( $first_side_comment_line == 0 )
                && $group_level == $last_level_written )
            {
                $min_move = 0;
            }

            if ( $move < $min_move ) {
                $move = $min_move;
            }

            # previously, an upper bound was placed on $move here,
            # (maximum_space_to_comment), but it was not helpful

            # don't exceed the available space
            if ( $move > $avail ) { $move = $avail }

            # we can only increase space, never decrease
            if ( $move > 0 ) {
                $line->increase_field_width( $maximum_field_index - 1, $move );
            }

            # remember this column for the next group
            $last_comment_column = $line->get_column( $kmax - 2 );
        }
        else {

            # try to at least line up the existing side comment location
            if ( $kmax > 0 && $move > 0 && $move < $avail ) {
                $line->increase_field_width( $maximum_field_index - 1, $move );
                $do_not_align = 0;
            }

            # reset side comment column if we can't align
            else {
                forget_side_comment();
            }
        }
    }
    return $do_not_align;
}

sub valign_output_step_A {

    ###############################################################
    # This is Step A in writing vertically aligned lines.
    # The line is prepared according to the alignments which have
    # been found. Then it is shipped to the next step.
    ###############################################################

    my ( $line, $min_ci_gap, $do_not_align, $group_leader_length,
        $extra_leading_spaces )
      = @_;
    my $rfields                   = $line->get_rfields();
    my $leading_space_count       = $line->get_leading_space_count();
    my $outdent_long_lines        = $line->get_outdent_long_lines();
    my $maximum_field_index       = $line->get_jmax();
    my $rvertical_tightness_flags = $line->get_rvertical_tightness_flags();

    # add any extra spaces
    if ( $leading_space_count > $group_leader_length ) {
        $leading_space_count += $min_ci_gap;
    }

    my $str = $rfields->[0];

    # loop to concatenate all fields of this line and needed padding
    my $total_pad_count = 0;
    for my $j ( 1 .. $maximum_field_index ) {

        # skip zero-length side comments
        last
          if (
            ( $j == $maximum_field_index )
            && ( !defined( $rfields->[$j] )
                || ( length( $rfields->[$j] ) == 0 ) )
          );

        # compute spaces of padding before this field
        my $col = $line->get_column( $j - 1 );
        my $pad = $col - ( length($str) + $leading_space_count );

        if ($do_not_align) {
            $pad =
              ( $j < $maximum_field_index )
              ? 0
              : $rOpts_minimum_space_to_comment - 1;
        }

        # if the -fpsc flag is set, move the side comment to the selected
        # column if and only if it is possible, ignoring constraints on
        # line length and minimum space to comment
        if ( $rOpts_fixed_position_side_comment && $j == $maximum_field_index )
        {
            my $newpad = $pad + $rOpts_fixed_position_side_comment - $col - 1;
            if ( $newpad >= 0 ) { $pad = $newpad; }
        }

        # accumulate the padding
        if ( $pad > 0 ) { $total_pad_count += $pad; }

        # add this field
        if ( !defined $rfields->[$j] ) {
            write_diagnostics("UNDEFined field at j=$j\n");
        }

        # only add padding when we have a finite field;
        # this avoids extra terminal spaces if we have empty fields
        if ( length( $rfields->[$j] ) > 0 ) {
            $str .= ' ' x $total_pad_count;
            $total_pad_count = 0;
            $str .= $rfields->[$j];
        }
        else {
            $total_pad_count = 0;
        }

        # update side comment history buffer
        if ( $j == $maximum_field_index ) {
            my $lineno = $file_writer_object->get_output_line_number();
            shift @side_comment_history;
            push @side_comment_history, [ $lineno, $col ];
        }
    }

    my $side_comment_length = ( length( $rfields->[$maximum_field_index] ) );

    # ship this line off
    valign_output_step_B( $leading_space_count + $extra_leading_spaces,
        $str, $side_comment_length, $outdent_long_lines,
        $rvertical_tightness_flags, $group_level );
    return;
}

sub get_extra_leading_spaces {

    #----------------------------------------------------------
    # Define any extra indentation space (for the -lp option).
    # Here is why:
    # If a list has side comments, sub scan_list must dump the
    # list before it sees everything.  When this happens, it sets
    # the indentation to the standard scheme, but notes how
    # many spaces it would have liked to use.  We may be able
    # to recover that space here in the event that all of the
    # lines of a list are back together again.
    #----------------------------------------------------------

    my $extra_leading_spaces = 0;
    if ($extra_indent_ok) {
        my $object = $group_lines[0]->get_indentation();
        if ( ref($object) ) {
            my $extra_indentation_spaces_wanted =
              get_recoverable_spaces($object);

            # all indentation objects must be the same
            for my $i ( 1 .. @group_lines - 1 ) {
                if ( $object != $group_lines[$i]->get_indentation() ) {
                    $extra_indentation_spaces_wanted = 0;
                    last;
                }
            }

            if ($extra_indentation_spaces_wanted) {

                # the maximum space without exceeding the line length:
                my $avail = $group_lines[0]->get_available_space_on_right();
                $extra_leading_spaces =
                  ( $avail > $extra_indentation_spaces_wanted )
                  ? $extra_indentation_spaces_wanted
                  : $avail;

                # update the indentation object because with -icp the terminal
                # ');' will use the same adjustment.
                $object->permanently_decrease_available_spaces(
                    -$extra_leading_spaces );
            }
        }
    }
    return $extra_leading_spaces;
}

sub combine_fields {

    # combine all fields except for the comment field  ( sidecmt.t )
    # Uses global variables:
    #  @group_lines
    my $maximum_field_index = $group_lines[0]->get_jmax();
    foreach my $line (@group_lines) {
        my $rfields = $line->get_rfields();
        foreach ( 1 .. $maximum_field_index - 1 ) {
            $rfields->[0] .= $rfields->[$_];
        }
        $rfields->[1] = $rfields->[$maximum_field_index];

        $line->set_jmax(1);
        $line->set_column( 0, 0 );
        $line->set_column( 1, 0 );

    }
    $maximum_field_index = 1;

    foreach my $line (@group_lines) {
        my $rfields = $line->get_rfields();
        for my $k ( 0 .. $maximum_field_index ) {
            my $pad = length( $rfields->[$k] ) - $line->current_field_width($k);
            if ( $k == 0 ) {
                $pad += $line->get_leading_space_count();
            }

            if ( $pad > 0 ) { $line->increase_field_width( $k, $pad ) }

        }
    }
    return;
}

sub get_output_line_number {

    # the output line number reported to a caller is the number of items
    # written plus the number of items in the buffer
    my $self   = shift;
    my $nlines = @group_lines;
    return $nlines + $file_writer_object->get_output_line_number();
}

sub valign_output_step_B {

    ###############################################################
    # This is Step B in writing vertically aligned lines.
    # Vertical tightness is applied according to preset flags.
    # In particular this routine handles stacking of opening
    # and closing tokens.
    ###############################################################

    my ( $leading_space_count, $str, $side_comment_length, $outdent_long_lines,
        $rvertical_tightness_flags, $level )
      = @_;

    # handle outdenting of long lines:
    if ($outdent_long_lines) {
        my $excess =
          length($str) -
          $side_comment_length +
          $leading_space_count -
          maximum_line_length_for_level($level);
        if ( $excess > 0 ) {
            $leading_space_count = 0;
            $last_outdented_line_at =
              $file_writer_object->get_output_line_number();

            unless ($outdented_line_count) {
                $first_outdented_line_at = $last_outdented_line_at;
            }
            $outdented_line_count++;
        }
    }

    # Make preliminary leading whitespace.  It could get changed
    # later by entabbing, so we have to keep track of any changes
    # to the leading_space_count from here on.
    my $leading_string =
      $leading_space_count > 0 ? ( ' ' x $leading_space_count ) : "";

    # Unpack any recombination data; it was packed by
    # sub send_lines_to_vertical_aligner. Contents:
    #
    #   [0] type: 1=opening non-block    2=closing non-block
    #             3=opening block brace  4=closing block brace
    #   [1] flag: if opening: 1=no multiple steps, 2=multiple steps ok
    #             if closing: spaces of padding to use
    #   [2] sequence number of container
    #   [3] valid flag: do not append if this flag is false
    #
    my ( $open_or_close, $tightness_flag, $seqno, $valid, $seqno_beg,
        $seqno_end );
    if ($rvertical_tightness_flags) {
        (
            $open_or_close, $tightness_flag, $seqno, $valid, $seqno_beg,
            $seqno_end
        ) = @{$rvertical_tightness_flags};
    }

    $seqno_string = $seqno_end;

    # handle any cached line ..
    # either append this line to it or write it out
    if ( length($cached_line_text) ) {

        # Dump an invalid cached line
        if ( !$cached_line_valid ) {
            valign_output_step_C( $cached_line_text,
                $cached_line_leading_space_count,
                $last_level_written );
        }

        # Handle cached line ending in OPENING tokens
        elsif ( $cached_line_type == 1 || $cached_line_type == 3 ) {

            my $gap = $leading_space_count - length($cached_line_text);

            # handle option of just one tight opening per line:
            if ( $cached_line_flag == 1 ) {
                if ( defined($open_or_close) && $open_or_close == 1 ) {
                    $gap = -1;
                }
            }

            if ( $gap >= 0 && defined($seqno_beg) ) {
                $leading_string      = $cached_line_text . ' ' x $gap;
                $leading_space_count = $cached_line_leading_space_count;
                $seqno_string        = $cached_seqno_string . ':' . $seqno_beg;
                $level               = $last_level_written;
            }
            else {
                valign_output_step_C( $cached_line_text,
                    $cached_line_leading_space_count,
                    $last_level_written );
            }
        }

        # Handle cached line ending in CLOSING tokens
        else {
            my $test_line = $cached_line_text . ' ' x $cached_line_flag . $str;
            if (

                # The new line must start with container
                $seqno_beg

                # The container combination must be okay..
                && (

                    # okay to combine like types
                    ( $open_or_close == $cached_line_type )

                    # closing block brace may append to non-block
                    || ( $cached_line_type == 2 && $open_or_close == 4 )

                    # something like ');'
                    || ( !$open_or_close && $cached_line_type == 2 )

                )

                # The combined line must fit
                && (
                    length($test_line) <=
                    maximum_line_length_for_level($last_level_written) )
              )
            {

                $seqno_string = $cached_seqno_string . ':' . $seqno_beg;

                # Patch to outdent closing tokens ending # in ');'
                # If we are joining a line like ');' to a previous stacked
                # set of closing tokens, then decide if we may outdent the
                # combined stack to the indentation of the ');'.  Since we
                # should not normally outdent any of the other tokens more than
                # the indentation of the lines that contained them, we will
                # only do this if all of the corresponding opening
                # tokens were on the same line.  This can happen with
                # -sot and -sct.  For example, it is ok here:
                #   __PACKAGE__->load_components( qw(
                #         PK::Auto
                #         Core
                #   ));
                #
                #   But, for example, we do not outdent in this example because
                #   that would put the closing sub brace out farther than the
                #   opening sub brace:
                #
                #   perltidy -sot -sct
                #   $c->Tk::bind(
                #       '<Control-f>' => sub {
                #           my ($c) = @_;
                #           my $e = $c->XEvent;
                #           itemsUnderArea $c;
                #       } );
                #
                if ( $str =~ /^\);/ && $cached_line_text =~ /^[\)\}\]\s]*$/ ) {

                    # The way to tell this is if the stacked sequence numbers
                    # of this output line are the reverse of the stacked
                    # sequence numbers of the previous non-blank line of
                    # sequence numbers.  So we can join if the previous
                    # nonblank string of tokens is the mirror image.  For
                    # example if stack )}] is 13:8:6 then we are looking for a
                    # leading stack like [{( which is 6:8:13 We only need to
                    # check the two ends, because the intermediate tokens must
                    # fall in order.  Note on speed: having to split on colons
                    # and eliminate multiple colons might appear to be slow,
                    # but it's not an issue because we almost never come
                    # through here.  In a typical file we don't.
                    $seqno_string               =~ s/^:+//;
                    $last_nonblank_seqno_string =~ s/^:+//;
                    $seqno_string               =~ s/:+/:/g;
                    $last_nonblank_seqno_string =~ s/:+/:/g;

                    # how many spaces can we outdent?
                    my $diff =
                      $cached_line_leading_space_count - $leading_space_count;
                    if (   $diff > 0
                        && length($seqno_string)
                        && length($last_nonblank_seqno_string) ==
                        length($seqno_string) )
                    {
                        my @seqno_last =
                          ( split /:/, $last_nonblank_seqno_string );
                        my @seqno_now = ( split /:/, $seqno_string );
                        if (   @seqno_now
                            && @seqno_last
                            && $seqno_now[-1] == $seqno_last[0]
                            && $seqno_now[0] == $seqno_last[-1] )
                        {

                            # OK to outdent ..
                            # for absolute safety, be sure we only remove
                            # whitespace
                            my $ws = substr( $test_line, 0, $diff );
                            if ( ( length($ws) == $diff ) && $ws =~ /^\s+$/ ) {

                                $test_line = substr( $test_line, $diff );
                                $cached_line_leading_space_count -= $diff;
                                $last_level_written =
                                  level_change(
                                    $cached_line_leading_space_count,
                                    $diff, $last_level_written );
                                reduce_valign_buffer_indentation($diff);
                            }

                            # shouldn't happen, but not critical:
                            ##else {
                            ## ERROR transferring indentation here
                            ##}
                        }
                    }
                }

                $str                 = $test_line;
                $leading_string      = "";
                $leading_space_count = $cached_line_leading_space_count;
                $level               = $last_level_written;
            }
            else {
                valign_output_step_C( $cached_line_text,
                    $cached_line_leading_space_count,
                    $last_level_written );
            }
        }
    }
    $cached_line_type = 0;
    $cached_line_text = "";

    # make the line to be written
    my $line = $leading_string . $str;

    # write or cache this line
    if ( !$open_or_close || $side_comment_length > 0 ) {
        valign_output_step_C( $line, $leading_space_count, $level );
    }
    else {
        $cached_line_text                = $line;
        $cached_line_type                = $open_or_close;
        $cached_line_flag                = $tightness_flag;
        $cached_seqno                    = $seqno;
        $cached_line_valid               = $valid;
        $cached_line_leading_space_count = $leading_space_count;
        $cached_seqno_string             = $seqno_string;
    }

    $last_level_written       = $level;
    $last_side_comment_length = $side_comment_length;
    $extra_indent_ok          = 0;
    return;
}

sub valign_output_step_C {

    ###############################################################
    # This is Step C in writing vertically aligned lines.
    # Lines are either stored in a buffer or passed along to the next step.
    # The reason for storing lines is that we may later want to reduce their
    # indentation when -sot and -sct are both used.
    ###############################################################
    my @args = @_;

    # Dump any saved lines if we see a line with an unbalanced opening or
    # closing token.
    dump_valign_buffer() if ( $seqno_string && $valign_buffer_filling );

    # Either store or write this line
    if ($valign_buffer_filling) {
        push @valign_buffer, [@args];
    }
    else {
        valign_output_step_D(@args);
    }

    # For lines starting or ending with opening or closing tokens..
    if ($seqno_string) {
        $last_nonblank_seqno_string = $seqno_string;

        # Start storing lines when we see a line with multiple stacked opening
        # tokens.
        # patch for RT #94354, requested by Colin Williams
        if ( $seqno_string =~ /^\d+(\:+\d+)+$/ && $args[0] !~ /^[\}\)\]\:\?]/ )
        {

            # This test is efficient but a little subtle: The first test says
            # that we have multiple sequence numbers and hence multiple opening
            # or closing tokens in this line.  The second part of the test
            # rejects stacked closing and ternary tokens.  So if we get here
            # then we should have stacked unbalanced opening tokens.

            # Here is a complex example:

            # Foo($Bar[0], {  # (side comment)
            # 	baz => 1,
            # });

            # The first line has sequence 6::4.  It does not begin with
            # a closing token or ternary, so it passes the test and must be
            # stacked opening tokens.

            # The last line has sequence 4:6 but is a stack of closing tokens,
            # so it gets rejected.

            # Note that the sequence number of an opening token for a qw quote
            # is a negative number and will be rejected.
            # For example, for the following line:
            #    skip_symbols([qw(
            # $seqno_string='10:5:-1'.  It would be okay to accept it but
            # I decided not to do this after testing.

            $valign_buffer_filling = $seqno_string;

        }
    }
    return;
}

sub valign_output_step_D {

    ###############################################################
    # This is Step D in writing vertically aligned lines.
    # Write one vertically aligned line of code to the output object.
    ###############################################################

    my ( $line, $leading_space_count, $level ) = @_;

    # The line is currently correct if there is no tabbing (recommended!)
    # We may have to lop off some leading spaces and replace with tabs.
    if ( $leading_space_count > 0 ) {

        # Nothing to do if no tabs
        if ( !( $rOpts_tabs || $rOpts_entab_leading_whitespace )
            || $rOpts_indent_columns <= 0 )
        {

            # nothing to do
        }

        # Handle entab option
        elsif ($rOpts_entab_leading_whitespace) {

         # Patch 12-nov-2018 based on report from Glenn. Extra padding was
         # not correctly entabbed, nor were side comments:
         # Increase leading space count for a padded line to get correct tabbing
            if ( $line =~ /^(\s+)(.*)$/ ) {
                my $spaces = length($1);
                if ( $spaces > $leading_space_count ) {
                    $leading_space_count = $spaces;
                }
            }

            my $space_count =
              $leading_space_count % $rOpts_entab_leading_whitespace;
            my $tab_count =
              int( $leading_space_count / $rOpts_entab_leading_whitespace );
            my $leading_string = "\t" x $tab_count . ' ' x $space_count;
            if ( $line =~ /^\s{$leading_space_count,$leading_space_count}/ ) {
                substr( $line, 0, $leading_space_count ) = $leading_string;
            }
            else {

                # shouldn't happen - program error counting whitespace
                # - skip entabbing
                VALIGN_DEBUG_FLAG_TABS
                  && warning(
"Error entabbing in valign_output_step_D: expected count=$leading_space_count\n"
                  );
            }
        }

        # Handle option of one tab per level
        else {
            my $leading_string = ( "\t" x $level );
            my $space_count =
              $leading_space_count - $level * $rOpts_indent_columns;

            # shouldn't happen:
            if ( $space_count < 0 ) {

                # But it could be an outdented comment
                if ( $line !~ /^\s*#/ ) {
                    VALIGN_DEBUG_FLAG_TABS
                      && warning(
"Error entabbing in valign_output_step_D: for level=$group_level count=$leading_space_count\n"
                      );
                }
                $leading_string = ( ' ' x $leading_space_count );
            }
            else {
                $leading_string .= ( ' ' x $space_count );
            }
            if ( $line =~ /^\s{$leading_space_count,$leading_space_count}/ ) {
                substr( $line, 0, $leading_space_count ) = $leading_string;
            }
            else {

                # shouldn't happen - program error counting whitespace
                # we'll skip entabbing
                VALIGN_DEBUG_FLAG_TABS
                  && warning(
"Error entabbing in valign_output_step_D: expected count=$leading_space_count\n"
                  );
            }
        }
    }
    $file_writer_object->write_code_line( $line . "\n" );
    return;
}

{    # begin get_leading_string

    my @leading_string_cache;

    sub get_leading_string {

        # define the leading whitespace string for this line..
        my $leading_whitespace_count = shift;

        # Handle case of zero whitespace, which includes multi-line quotes
        # (which may have a finite level; this prevents tab problems)
        if ( $leading_whitespace_count <= 0 ) {
            return "";
        }

        # look for previous result
        elsif ( $leading_string_cache[$leading_whitespace_count] ) {
            return $leading_string_cache[$leading_whitespace_count];
        }

        # must compute a string for this number of spaces
        my $leading_string;

        # Handle simple case of no tabs
        if ( !( $rOpts_tabs || $rOpts_entab_leading_whitespace )
            || $rOpts_indent_columns <= 0 )
        {
            $leading_string = ( ' ' x $leading_whitespace_count );
        }

        # Handle entab option
        elsif ($rOpts_entab_leading_whitespace) {
            my $space_count =
              $leading_whitespace_count % $rOpts_entab_leading_whitespace;
            my $tab_count = int(
                $leading_whitespace_count / $rOpts_entab_leading_whitespace );
            $leading_string = "\t" x $tab_count . ' ' x $space_count;
        }

        # Handle option of one tab per level
        else {
            $leading_string = ( "\t" x $group_level );
            my $space_count =
              $leading_whitespace_count - $group_level * $rOpts_indent_columns;

            # shouldn't happen:
            if ( $space_count < 0 ) {
                VALIGN_DEBUG_FLAG_TABS
                  && warning(
"Error in get_leading_string: for level=$group_level count=$leading_whitespace_count\n"
                  );

                # -- skip entabbing
                $leading_string = ( ' ' x $leading_whitespace_count );
            }
            else {
                $leading_string .= ( ' ' x $space_count );
            }
        }
        $leading_string_cache[$leading_whitespace_count] = $leading_string;
        return $leading_string;
    }
}    # end get_leading_string

sub report_anything_unusual {
    my $self = shift;
    if ( $outdented_line_count > 0 ) {
        write_logfile_entry(
            "$outdented_line_count long lines were outdented:\n");
        write_logfile_entry(
            "  First at output line $first_outdented_line_at\n");

        if ( $outdented_line_count > 1 ) {
            write_logfile_entry(
                "   Last at output line $last_outdented_line_at\n");
        }
        write_logfile_entry(
            "  use -noll to prevent outdenting, -l=n to increase line length\n"
        );
        write_logfile_entry("\n");
    }
    return;
}
1;
