package Babble::Plugin::SubstituteAndReturn;

use Moo;

my $s_FLAGS_RE = qr/([msixpodualgcern]*+)$/;
my $y_FLAGS_RE = qr/([cdsr]*+)$/;

sub _get_flags {
  my ($text) = @_;
  $text =~ /^s/ ? $s_FLAGS_RE : $y_FLAGS_RE;
}

sub _transform_binary {
  my ($self, $top) = @_;
  my $replaced;
  do {
    $replaced = 0;
    $top->each_match_within(BinaryExpression => [
       [ 'left' => '(?>(?&PerlPrefixPostfixTerm))' ],
       '(?>(?&PerlOWS)) =~ (?>(?&PerlOWS))',
       [ 'right' => '(?>
                        (?&PerlSubstitution)
                      | (?&PerlTransliteration)
                     )' ],
    ] => sub {
      my ($m) = @_;
      my ($left, $right);
      eval {
        ($left, $right) = $m->subtexts(qw(left right));
        1
      } or return;
      my ($flags) = $right =~ _get_flags($right);
      return unless (my $newflags = $flags) =~ s/r//g;

      # find chained substitutions
      #   ... =~ s///r =~ s///r =~ s///r
      my $top_text = $top->text;
      pos( $top_text ) = $m->start + length $m->text;
      my $chained_subs_length = 0;
      my @chained_subs;
      while( $top_text =~ /
        \G
          (
            (?>(?&PerlOWS)) =~ (?>(?&PerlOWS))
            ((?>
                (?&PerlSubstitution)
              | (?&PerlTransliteration)
            ))
          )
          @{[ $m->grammar_regexp ]}
        /xg ) {
        $chained_subs_length += length $1;
        push @chained_subs, $2;
      }
      for my $subst_c (@chained_subs) {
        my ($f_c) = $subst_c =~ _get_flags($subst_c);
        die "Chained substitution must use the /r modifier"
          unless (my $nf_c = $f_c) =~ s/r//g;
        $subst_c =~ s/\Q${f_c}\E$/${nf_c}/;
      }

      $right =~ s/\Q${flags}\E$/${newflags}/;
      $left =~ s/\s+$//;
      my $genlex = '$'.$m->gensym;

      if( @chained_subs ) {
        my $chained_for = 'for ('.$genlex.') { '
          . join("; ", @chained_subs)
          . ' }';
        $top->replace_substring(
          $m->start,
          length($m->text) + $chained_subs_length,
          '(map { (my '.$genlex.' = $_) =~ '.$right.'; '.$chained_for.' '.$genlex.' }'
          .' '.$left.')[0]'
        );
      } else {
        $m->replace_text(
          '(map { (my '.$genlex.' = $_) =~ '.$right.'; '.$genlex.' }'
          .' '.$left.')[0]'
        );
      }

      $replaced++;
    });
  } while( $replaced );
}

sub _transform_contextualise {
  my ($self, $top) = @_;

  my $contextual_subst = 0;
  do {
    my %subst_pos;
    # Look for substitution without binding operator:
    # First look for an expression that begins with Substitution.
    $top->each_match_within(Expression => [
      [ subst => '(?>
                      (?&PerlSubstitution)
                    | (?&PerlTransliteration)
                  )' ],
    ] => sub {
      my ($m) = @_;
      my ($subst) = @{$m->submatches}{qw(subst)};
      my ($flags) = $subst->text =~ _get_flags($subst->text);
      return unless $flags =~ /r/;
      $subst_pos{$m->start} = 1;
    });
    # Then remove Substitution within a BinaryExpression
    $top->each_match_within(BinaryExpression => [
       [ 'left' => '(?>(?&PerlPrefixPostfixTerm))' ],
       '(?>(?&PerlOWS)) =~ (?>(?&PerlOWS))',
       [ 'right' => '(?>
                         (?&PerlSubstitution)
                       | (?&PerlTransliteration)
                     )' ],
    ] => sub {
      my ($m) = @_;
      delete $subst_pos{ $m->start + $m->submatches->{right}->start };
    });

    # Insert context variable and binding operator
    my @subst_pos = sort keys %subst_pos;
    $contextual_subst = @subst_pos;
    my $diff = 0;
    my $replace = '$_ =~ ';
    while( my $pos = shift @subst_pos ) {
      $top->replace_substring($pos + $diff, 0, $replace);
      $diff += length $replace;
    }
  } while( $contextual_subst);
}

sub transform_to_plain {
  my ($self, $top) = @_;

  $self->_transform_contextualise($top);

  $self->_transform_binary($top);
}

1;
__END__

=head1 NAME

Babble::Plugin::SubstituteAndReturn - Plugin for /r flag for substitution and transliteration

=head1 SYNOPSIS

Converts usage of the C<s///r> and C<tr///r> syntax to substitution and
transliteration without the C</r> flag.

=head1 SEE ALSO

L<E<sol>r flag|Syntax::Construct/"/r">

=cut