#!/usr/bin/perl -w
#
# Walk through a perl script and reformat perl comments
# using Text::Autoformat.
#
# usage:
# perlcomment -l72 myfile.pl >myfile.new
#
# where -l specifies the maximum comment line length.
#
# You will be given an opportunity to accept or reject each proposed
# change.
#
# This file demonstrates using Perl::Tidy to walk through a perl file
# and find all of its comments.  It offers to reformat each group of
# consecutive full-line comments with Text::Autoformat.
#
# This may or may not be useful, depending on your coding style.
# Change it to suit your own purposes; see sub get_line().
#
# Uses: Text::Autoformat
#       Perl::Tidy
#
# Steve Hancock, March 2003
# Based on a suggestion by Tim Maher
# Also discussed in git#127
#
# Note that the indentation which is assumed for blocks of comments is whatever
# the indentation is in the input file. A potential problem is that if the
# input file has very long comments which have been out-dented, this may not
# give the desired result. This problem can be avoided by doing a preliminary
# formatting pass with perltidy which includes the parameter
# --nooutdent-long-comments (-nolc).

# TODO: (just ideas that probably won't get done)
# -Handle lines of stars, dashes, etc better
# -Need flag to limit changes to lines greater than some minimum length
# -reformat side and hanging side comments

use strict;
use Getopt::Std;
use Text::Autoformat;
$| = 1;
use vars qw($opt_l $opt_h);

main();

sub main {
    my $usage = <<EOM;
   usage: perlcomment [ -ln ] filename >outfile
          where n=line length (default 72)
EOM

    getopts('hl:') or die "$usage";
    if ($opt_h) { die $usage }
    if ( !defined $opt_l ) {
        $opt_l = 72;
    }
    else {
        $opt_l =~ /^\d+$/ or die "$usage";
    }

    unless ( @ARGV == 1 ) { die $usage }
    my $file = $ARGV[0];
    autoformat_file( $file, $opt_l );
    return;
}

sub autoformat_file {
    my ( $file, $line_length ) = @_;
    use Perl::Tidy;
    use IO::File;
    my $fh = IO::File->new( $file, 'r' );
    unless ($fh) { die "cannot open '$file': $!\n" }
    my $formatter = CommentFormatter->new($line_length);

    my $err = perltidy(
        'formatter' => $formatter,     # callback object
        'source'    => $fh,
        'argv'      => "-npro -se",    # don't need .perltidyrc
                                       # errors to STDOUT
    );
    if ($err) {
        die "Error calling perltidy\n";
    }
    $fh->close();
}

#####################################################################
#
# The CommentFormatter object has a write_line() method which receives
# tokenized lines from perltidy
#
#####################################################################

package CommentFormatter;

sub new {
    my ( $class, $line_length ) = @_;
    my $comment_block = "";
    bless {
        _rcomment_block          => \$comment_block,
        _maximum_comment_length  => 0,
        _line_length             => $line_length,
        _in_hanging_side_comment => 0,
      },
      $class;
}

sub write_line {

    # This is called from perltidy line-by-line
    # Comments will be treated specially (reformatted)
    # Other lines go to stdout immediately
    my $self           = shift;
    my $line_of_tokens = shift;
    my $line_type      = $line_of_tokens->{_line_type};
    ## my $input_line_number = $line_of_tokens->{_line_number};
    my $input_line  = $line_of_tokens->{_line_text};      # the original line
    my $rtoken_type = $line_of_tokens->{_rtoken_type};    # type of tokens
    my $rtokens     = $line_of_tokens->{_rtokens};        # text of tokens

    # Just print non-code, non-comment lines
    if (
        $line_type ne 'CODE'           # if it's not code,
        || !@$rtokens                  # or is a blank line
        || $$rtoken_type[-1] ne '#'    # or the last token isn't a comment
      )
    {
        $self->print($input_line);
        $self->{_in_hanging_side_comment} = 0;
        return;
    }

    # Now we either have:
    # - a line with a side comment (@$rtokens >1), or
    # - a full line comment (@$rtokens==1)

    # Output a line with a side comment, but remember it
    if ( @$rtokens > 1 ) {
        $self->print($input_line);
        $self->{_in_hanging_side_comment} = 1;
        return;
    }

    # A hanging side comment is a full-line comment immediately
    # following a side comment or another hanging side comment.
    # Output a hanging side comment directly
    if ( $self->{_in_hanging_side_comment} ) {
        $self->print($input_line);
        return;
    }

    # Now we know we have a full-line, non-hanging, comment
    # Decide what to do --

    # output comment without any words directly, since these don't get
    # handled well by autoformat yet.  For example, a box of stars.
    # TODO: we could truncate obvious separator lines to the desired
    # line length
    if ( $$rtokens[-1] !~ /\w/ ) {
        $self->print($input_line);
    }

    # otherwise, append this comment to the group we are collecting
    else {
        $self->append_comment($input_line);
    }
    return;
}

sub print {
    my ( $self, $input_line ) = @_;
    $self->flush_comments();
    print $input_line;
}

sub append_comment {
    my ( $self, $input_line ) = @_;
    my $rcomment_block         = $self->{_rcomment_block};
    my $maximum_comment_length = $self->{_maximum_comment_length};
    $$rcomment_block .= $input_line;
    if ( length($input_line) > $maximum_comment_length ) {
        $self->{_maximum_comment_length} = length($input_line);
    }
}

{
    my ( $separator1, $separator2, $separator3 );

    BEGIN {
        $separator1 = '-' x 2 . ' Original ' . '-' x 60 . "\n";
        $separator2 = '-' x 2 . ' Modified ' . '-' x 60 . "\n";
        $separator3 = '-' x 72 . "\n";
    }

    sub flush_comments {

        my ($self)                 = @_;
        my $rcomment_block         = $self->{_rcomment_block};
        my $line_length            = $self->{_line_length};
        my $maximum_comment_length = $self->{_maximum_comment_length};
        if ($$rcomment_block) {
            my $comments = $$rcomment_block;

            # we will just reformat lines longer than the desired length for now
            # TODO: this can be changed
            if ( $maximum_comment_length > $line_length ) {
                my $formatted_comments =
                  Text::Autoformat::autoformat( $comments,
                    { right => $line_length, all => 1 } );

                if ( $formatted_comments ne $comments ) {
                    print STDERR $separator1;
                    print STDERR $$rcomment_block;
                    print STDERR $separator2;
                    print STDERR $formatted_comments;
                    print STDERR $separator3;
                    if ( ifyes("Accept Changes? [Y/N]") ) {
                        $comments = $formatted_comments;
                    }
                }
            }
            print $comments;
            $$rcomment_block = "";
            $self->{_maximum_comment_length} = 0;
        }
    }
}

sub query {
    my ($msg) = @_;
    print STDERR $msg;
    my $ans = <STDIN>;
    chomp $ans;
    return $ans;
}

sub queryu {
    return uc query(@_);
}

sub ifyes {
    my $count = 0;
  ASK:
    my $ans = queryu(@_);
    if    ( $ans =~ /^Y/ ) { return 1 }
    elsif ( $ans =~ /^N/ ) { return 0 }
    else {
        $count++;
        if ( $count > 6 ) { die "error count exceeded in ifyes\n" }
        print STDERR "Please answer 'Y' or 'N'\n";
        goto ASK;
    }
}

# called once after the last line of a file
sub finish_formatting {
    my $self = shift;
    $self->flush_comments();
}
