#!/usr/bin/env perl

=head1 NAME

fixcomments.pl

=head1 SYNOPSIS

fixcomments.pl [options] [infile]

=head1 OPTIONS

=over 4

=item B<infile>

Input file to process (uses stdin if not specified)

=item B<--help|-h>

A little help

=item B<--verbose|-v>

Output debug messages (to stderr), repeat for even more output

=back

=head1 DESCRIPTION

This script will read a Fortran file and attempt to add doxify comments
to both FUNCTIONs and SUBROUTINEs.

=cut

use strict;
use warnings;
use open qw(:std :utf8);
use FindBin;
use lib "$FindBin::RealBin/lib";
use Pod::Usage qw(pod2usage);
use Getopt::Long;

my $verbose = 0;
my $help = 0;

GetOptions(
    'verbose+' => \$verbose,
    'help|?' => \$help) or pod2usage(2);
pod2usage(1) if $help;

sub print_debug {
    print(STDERR "DEBUG: @_\n") if ($verbose > 0);
}

# Regular expressions for matching
my $DOXYGEN_HEADER = "^!>";

# The empty string
my $EMPTY = q{};

# Toggle variables to keep track of which doxygen item is being processed
# in the current header.
my ($hasParam,
    $hasBrief,
    $hasPar,
    $hasDate,
    $hasVersion,
    $hasAuthor,
    $hasNote,
    $hasRetVal,
    $hasReturn,
    $hasRandom,
    $hasRemainder);

# Variables with s at the end contains the actual text read in from
# existing doxygen header
my (%params,
    $briefs,
    $dates,
    $versions,
    $pars,
    $authors,
    $notes,
    $retVals,
    $returns,
    $randoms,
    $remainders);

# If currently in INTERFACE block
my $insideInterface = 0;

# Whether the SUBROUTINE/FUNCTION definition contains an & line continuation
my $hasAmpersand = 0;

# Whether when processing subroutine definition we are in brackets
my $inParentheses = 0;

# Whether the procedure definition contains the RETURN value
my $hasReturnAsArg = 0;

# Keeps track of which parameters in the current procedure have
# a matching doxygen header
my %matched;

# Buffer for lines
my $buffer = $EMPTY;

# The old doxygen header as read from file
my $oldheader = $EMPTY;

# Any unprocessed lines
my $leftoverlines = $EMPTY;

# The name of the procedure being processed
my $procedureName = $EMPTY;

# Whether the procedure is a function
my $isFunction;

initVariables();

# While there are still lines to read in our INPUT file
while (<>) {
    # Get the line we've just read in
    my $currline = $_;

    # If an existing doxygen header is encountered then process
    if (matchDoxygenHeaderDefinition($currline)) {
        print_debug("Matched doxygen header");
        $currline = processDoxygenHeader($currline);
    }

    # Are we inside an interface block?
    # If so we don't want to add any comments here
    if ( $currline =~ m/^\s*INTERFACE\s*\n/xms ) {
        print_debug("Start of INTERFACE encountered");
        $insideInterface = 1;
    }
    if ( $currline =~ m/^\s*END\s+INTERFACE\s*\n/xms ) {
        print_debug("End of INTERFACE encountered");
        $insideInterface = 0;
    }

    # Look for procedure (SUBROUTINE/FUNCTION definitions)
    # These can be the initial definition line, or a continuation line
    # We don't add comments to code inside an interface block
    if ((matchSubroutineDefinition($currline) || $hasAmpersand)
        && !($insideInterface)) {
        print_debug("Matched subroutine definition");
        processSubroutineDefinition($currline);
    } else {
        print_debug("Subroutine definition not matched");
        if ($oldheader eq $EMPTY) {
            # No header remaining so just print out line as read
            print_debug("Empty old header, writing out line:");
            print_debug($currline);
            print $currline;
        } else {
            # Header has been processed, need to do something with remaining lines
            print_debug("Non-empty old header for line:");
            print_debug($currline);
            if (($currline =~ m/^\s*$/xms) ||
                ($currline =~ m/\#if/xms)) {
                # Blank line or #if line, so store for printing later
                print_debug("Empty line or #if, storing for later");
                $leftoverlines = $leftoverlines . $currline;
            } elsif ($currline =~ m/^\s*!\s*/xms) {
                # Have a comment so add to header
                print_debug("Inline comment, adding to header");
                $oldheader = $oldheader . $currline;
            } else {
                # If it was a MODULE or TYPE header we still need to write
                # it back out to file.
                print_debug("Writing existing non FUNCTION/SUBROUTINE header to file");
                print $oldheader;
                print $leftoverlines;
                print $currline;
                # Reset variables after output
                initVariables();
            }
        }
    } # End of the if (($currline is SUBROUTINE or FUNCTION ) block
} # End of main while loop

# Perhaps do a second pass to remove any double occurrences of !> *** type lines

sub initToggles {
    $hasParam = 0;
    $hasBrief = 0;
    $hasPar = 0;
    $hasDate = 0;
    $hasVersion = 0;
    $hasAuthor = 0;
    $hasNote = 0;
    $hasRetVal = 0;
    $hasRandom = 0;
    $hasRemainder = 0;
    return;
}

sub initVariables {
    initToggles();
    undef %params;
    undef %matched;
    $briefs = $EMPTY;
    $dates = $EMPTY;
    $versions = $EMPTY;
    $pars = $EMPTY;
    $authors = $EMPTY;
    $notes = $EMPTY;
    $retVals = $EMPTY;
    $returns = $EMPTY;
    $randoms = $EMPTY;
    $remainders = $EMPTY;
    $buffer = $EMPTY;
    $oldheader = $EMPTY;
    $leftoverlines = $EMPTY;
    $procedureName = $EMPTY;
    $isFunction = 0;
    $inParentheses = 0;
    return;
}

# Look for a doxygen header definition in an input line.
sub matchDoxygenHeaderDefinition {
    my ($lineToProcess) = @_;

    print_debug("Trying to match doxygen header $DOXYGEN_HEADER against line");
    print_debug($lineToProcess);

    my $match = 0;

    if ($lineToProcess =~ m/$DOXYGEN_HEADER/xms) {
        $match = 1;
    }

    print_debug("Doxygen header match value: $match");
    return $match;
}

# Look for SUBROUTINE or FUNCTION definitions.
sub matchSubroutineDefinition {
    my ($lineToProcess) = @_;

    print_debug("Trying to match subroutine definition against line:");
    print_debug($lineToProcess);

    # Immediately discount lines with comments at the start
    my $commentPattern = '^\!';
    if ($lineToProcess =~ m/$commentPattern/xms) {
        print_debug("Matched comment");
        return 0;
    }
    # Assume that lines contain SUBROUTINE or FUNCTION followed by space
    # and then the name of the procedure which may have a space before the (
    my $patternProcWithBrackets = '(SUBROUTINE|FUNCTION)'    # Subroutine or function
                                 .'\s+'                      # followed by one or more whitespace characters
                                 .'(\w|\[|\])+'              # followed by one or more of (word or [ or ])
                                 .'\s*'                      # followed by zero or more whitespace characters
                                 .'\(';                      # followed by open bracket.

    # Need to allow for SUBROUTINE without any arguments or brackets too
    my $patternSubNoBracketsAtStartOfLine = '^\s*'           # Start with zero or more whitespace
                                           .'SUBROUTINE'     # then subroutine
                                           .'\s*'            # then zero or more whitespace
                                           .'(\w|\[|\])+'    # then one or more of (word or [ or ])
                                           .'\s*.*';         # then zero or more whitespace

    # We also protect against adding comments to commented out SUBROUTINE/FUNCTION calls
    my $patternCommentedOut = '!\s*(SUBROUTINE|FUNCTION)';

    # Protect against definitions inside quotes
    my $patternInQuotes = '"\s*(SUBROUTINE|FUNCTION)';

    my $match1 = 0;
    my $match2 = 0;
    my $match3 = 0;
    my $match4 = 0;

    if ($lineToProcess =~ m/$patternProcWithBrackets/xms) {
        $match1 = 1;
    }
    if ($lineToProcess =~ m/$patternSubNoBracketsAtStartOfLine/xms) {
        $match2 = 1;
    }
    if ($lineToProcess =~ m/$patternCommentedOut/xms) {
        $match3 = 1;
    }
    if ($lineToProcess =~ m/$patternInQuotes/xms) {
        $match4 = 1;
    }

    my $match = (($match1 || $match2) && !($match3) && !($match4));
    print_debug("Subroutine definition match value: $match");
    return $match
}

sub processDoxygenHeader {

    my ($currline) = @_;

    print_debug("Processing Doxygen Header");

    # Each time we find a header we need to reset toggles and their data
    initVariables();
    my $paramName = $EMPTY;

    # Start of do-while over match on ($currline =~ m/^\s*!/i)
    do {
        print_debug("Processing header line:");
        print_debug($currline);
        # Keep the headers safe, may need them! We use the oldheader variable to
        # keep the complete headers for MODULE and TYPE intact such that we
        # can just dump them straight back out without making any changes.
        $oldheader = $oldheader . $currline;
        # Pick up parameters - matches the word param separated by spaces
        if (($currline =~ m/!>\s\\param\s+(\S+)\s*/xms) ||
            ($currline =~ m/!>\s\\param\[.*\]\s+(\S+)\s*/xms)) {

            # Get the param name
            $paramName = $1;

            print_debug("Got header for parameter $paramName");

            # Cover the case where two arguments have the same name
            if (exists $params{$paramName}) {
                $paramName = $paramName . "new";
            }
            # Store the param into a hash indexed by its name
            $params{$paramName} = $currline;
            # Set matched for this param to zero as we don't
            # yet have a match in the argument list
            $matched{$paramName} = 0;
            initToggles();
            $hasParam = 1;
        } elsif ($currline =~ m/!>\s\\brief\s*/xms) {
            print_debug("Got briefs header");
            $briefs = $briefs . $currline;
            initToggles();
            $hasBrief = 1;
        } elsif ($currline =~ m/!>\s\\date*/xms) {
            print_debug("Got dates header");
            $dates = $dates . $currline;
            initToggles();
            $hasDate = 1;
        } elsif ($currline =~ m/!>\s\\version\s*/xms) {
            print_debug("Got version header");
            $versions = $versions . $currline;
            initToggles();
            $hasVersion = 1;
        } elsif ($currline =~ m/!>\s\\par\s*/xms) {
            print_debug("Got par header");
            $pars = $pars . $currline;
            initToggles();
            $hasPar = 1;
        } elsif ($currline =~ m/!>\s\\author\s*/xms) {
            print_debug("Got author header");
            $authors = $authors . $currline;
            initToggles();
            $hasAuthor= 1;
        } elsif ($currline =~ m/!>\s\\note\s*/xms) {
            print_debug("Got note header");
            $notes = $notes . $currline;
            initToggles();
            $hasNote = 1;
        } elsif ($currline =~ m/!>\s\\retval\s*/xms) {
            print_debug("Got retval header");
            $retVals = $retVals . $currline;
            initToggles();
            $hasRetVal = 1;
        } elsif ($currline =~ m/!>\s\\returns\s*/xms) {
            print_debug("Got return header");
            $returns = $returns . $currline;
            initToggles();
            $hasReturn = 1;
        } elsif ($currline =~ m/!>\s\\return\s*/xms) {
            print_debug("Got return header");
            $returns = $returns . $currline;
            initToggles();
            $hasReturn = 1;
        } elsif ($currline =~ m/!>\s\\\S+/xms) {
            # Randoms contains anything else that looks
            # like a DOXYGEN header. with a \whatever
            # Must check to see if line has already been commented.
            # We also avoid commenting a blank \param line
            print_debug("Got random header");
            if (($currline !~ m/UNKNOWN_DOXYGEN_COMMENT/xms) &&
                ($currline !~ m/UNKNOWN_COMMENT/xms) &&
                ($currline !~ m/!>\s*\\param\s*\n/xms)) {
                $randoms = $randoms . $currline;
                chomp($randoms);
                # Add on text for output
                $randoms = $randoms . " UNKNOWN_DOXYGEN_COMMENT\n";
            } else {
                # Otherwise just add to randoms
                $randoms = $randoms . $currline;
            }
            initToggles();
            $hasRandom = 1;
        } elsif ($currline =~ m/^!>\s*/xms) {
            # Handle multi line entries. Append what you find onto the
            # previous one read in until you get to another \
            # The \brief, \param, \par, \author, random and remainder
            # entries can all be multi-line.
            # The \version and \date entries should be single line and
            # thus we don't have an elsif for them.
            print_debug("Got line continuation");
            if ($hasParam) {
                $params{$paramName} = $params{$paramName} . $currline;
            } elsif ($hasBrief) {
                $briefs = $briefs . $currline;
            } elsif ($hasPar) {
                $pars = $pars . $currline;
            } elsif ($hasAuthor) {
                $authors = $authors . $currline;
            } elsif ($hasNote){
                $notes = $notes . $currline;
            } elsif ($hasRetVal) {
                $retVals = $retVals . $currline;
            } elsif ($hasReturn) {
                $returns = $returns . $currline;
            } elsif ($hasRandom) {
                # Must check to see if line has already been commented
                if (($currline !~ m/UNKNOWN_DOXYGEN_COMMENT/xms) &&
                    ($currline !~ m/UNKNOWN_COMMENT/xms)) {
                    $randoms = $randoms . $currline;
                    chomp($randoms);
                    # Add on text for output
                    $randoms = $randoms . " UNKNOWN_DOXYGEN_COMMENT\n";
                } else {
                    $randoms = $randoms . $currline;
                }
            } else {
                # Get any header lines beginning with "!> some text but
                # no \ thus not in DOXYGEN format
                $hasRemainder = 1;
                # Must check to see if the line has already been
                # commented and also that it's not empty
                if (($currline !~ m/UNKNOWN_COMMENT/xms) &&
                    ($currline !~ m/!>\s*\n/xms)) {
                    # Add on text for output
                    $currline =~ s/!>/!> \\note UNKNOWN_COMMENT /gx;
                    $remainders = $remainders . $currline;
                } else {
                    $remainders = $remainders . $currline;
                }
            }
        } elsif ($currline !~ m/^!\s*\*/xms) {
            # Any other header line that's not "***..." or a Doxygen
            # comment, ie "! some comment or other"
            print_debug("Got non-doxygen line");
            if ($hasBrief) {
                # Put comment in brief, replacing ! with !>
                $currline =~ s/!/!>/xms;
                $briefs = $briefs . $currline;
            } else {
                $currline =~ s/^\s+//;
                # Must check to see if line has already been commented
                if ($currline !~ m/UNKNOWN_COMMENT/xms) {
                    # Add on text for output, changing ! for !>
                    $currline =~ s/!/!> \\note UNKNOWN_COMMENT /gx;
                    $remainders = $remainders . $currline;
                } else {
                    $remainders = $remainders . $currline;
                }
            }
        }
        # Get the next line in the header block
        $currline = <>;
    } while ($currline =~ m/^\s*!/xms); # Rule as to when you have finished a header. Currently Anything beginning with !
    return $currline;
} # End of header processing subroutine

sub processSubroutineDefinition {

    my ($currline) = @_;

    print_debug("Processing Subroutine line:");
    print_debug($currline);
    # functionLine will contain the procedure definition with any
    # unrequired text stripped off, $currline will still contain the actual code
    my $functionLine = $EMPTY;
    if (!($hasAmpersand)) {
        # Remove anything preceding the SUBROUTINE or FUNCTION definition
        # e.g. RECURSIVE, REAL, INTEGER, (KIND=dp), ELEMENTAL etc etc.
        $currline =~ /((\bFUNCTION\b|\bSUBROUTINE\b).+)/x;
        # $1 contains whatever remains after removing anything before the
        # word SUBROUTINE or FUNCTION
        $functionLine = $1;
    } else {
        $functionLine = $currline;
    }
    # Remove BIND(*) from the definition
    my $tmp = $functionLine;
    $tmp =~ s/\bBIND\b\(\w+\W*\w*\W*\w*\W*\)//x;
    $functionLine = $tmp;

    # Strip the newline char from the end
    chomp($functionLine);

    $hasAmpersand = 0;
    $hasReturnAsArg = 0;
    my $lelement = $EMPTY;

    # Split the subroutine or function definition by space or comma
    my @string = split(/([,\(\)\s+]+)/x, $functionLine);

    while (my ($idx, $p) = each @string) {
        defined $p or next;  # continue with the next item if $p is undefined

        $p =~ s/^\s+|\s+$//gx;

        $p ne $EMPTY or next;  # continue with the next item if empty
        $p ne "," or next;  # or simply the ','

        print_debug("Processing item $p");
        if ($p eq "&") {
            # If we encounter an & as the last element in this line it starts a line continuation,
            # otherwise it is the continuation point of a previous one and we can ignore it
            $idx == $#string or next;
            print_debug("Got & line continuation");
            $hasAmpersand = 1;
            # Buffer the line details as we can't print out yet
            $buffer = "$buffer$currline";
        } elsif ($p eq "(") {
            print_debug("Entered parentheses");
            $inParentheses = 1;
        } elsif ($p eq ")") {
            print_debug("Left parentheses");
            $inParentheses = 0;
        } elsif ($p eq ",") {
            print_debug("Comma");
        } else {
            if ($inParentheses) {
                print_debug("In parentheses, parameter: $p, previous parameter: $lelement");
                # Must have either a parameter of a function return value
                if (($lelement =~ m/RESULT/ixms) && ($hasReturnAsArg)) {
                    print_debug("Got return parameter $p");
                    if ($returns eq $EMPTY) {
                        # If the previous element was RESULT outside of
                        # parantheses then this element must be whatever
                        # gets returned by the procedure.
                        # if no return data is available print out the ...
                        # to header
                        # Only stored for now so it is always printed
                        # in the same place (after the \params)
                        $returns = "!> \\return ...\n";
                    }
                } else {
                    # Must be parameter
                    # Update the matched hash table so that all arguments
                    # in subroutine/function header are set as true
                    print_debug("Processing parameter $p");
                    $matched{$p} = 1;
                    if (!(exists($params{$p})) || !(defined($params{$p})) || ($params{$p} eq $EMPTY)) {
                        # If the entry for this parameter is missing we use
                        # the standard text for a missing entry
                        print_debug("Missing entry for parameter $p, creating blank");
                        print "!> \\param $p ...\n";
                    } else {
                        print_debug("Using existing entry for parameter $p");
                        if ($params{$p} !~ m/\\param\s*(\w+|\[.*\]\s+\w+)\s*\n/xms ) {
                            # Entry must contain some text after the parameter name
                            print_debug("Using entry unchanged");
                            print $params{$p};
                        } else {
                            if ($params{$p} =~ m/!>\s*\n$/x) {
                                # We need to guard against \param entries which have
                                # no text but have a blank line "!>" line appended
                                # Need to split this parameter into it's individual lines
                                my @tmpString = split(/\n/x, $params{$p});
                                # Find out how many lines we have in this entry
                                my $tmpLen = scalar(@tmpString);
                                # Append on the ... to the first line
                                $tmpString[0] = $tmpString[0] . " ...";
                                for (my $i = 0; $i < $tmpLen; $i++) {
                                    # Re-add the carriage return to each line
                                    print "$tmpString[$i]\n";
                                }
                            } else {
                                chomp($params{$p});
                                print $params{$p}," ...\n";
                            }
                        }
                    }
                }
            } else {
                print_debug("Processing element not in parentheses: $p");
                if ($lelement =~ m/(SUBROUTINE|FUNCTION)/ixms) {
                    # Previous element is FUNCTION or SUBROUTINE so this must be name
                    $procedureName = $p;
                    print_debug("Got procedure name $procedureName");
                    if ($lelement =~ m/FUNCTION/ixms) {
                        $isFunction = 1;
                    }
                    if (($briefs eq $EMPTY) or ($briefs eq "!> \\brief\n")) {
                        # \brief does not exist or is present but empty - add text
                        print "! **************************************************************************************************\n";
                        print "!> \\brief ...\n";
                    } else {
                        # \brief exists and contains text
                        print $briefs;
                    }
                } elsif ($p =~ m/RESULT/ixms) {
                    # Check to see if parameter is RESULT for a FUNCTION
                    print_debug("Need to get function result parameter");
                    $hasReturnAsArg = 1;
                }
            }
            $lelement = $p; # Take a note of the array element for comparison, ignoring & and ()
        }
    } # End of for loop over @string

    # Code to loop over all the elements of matched hash table. If any
    # remain that were not in the function/subroutine header we want to
    # make sure we keep this data in the header. It could be a comment
    # or it could refer to old arguments that no longer exist.
    # However, we don't want to throw the text away. We only print out
    # if the argument doesn't match one of the arguments *and* there is
    # no more of the procedure definition left to read, i.e. hasAmpersand is false
    foreach my $paramName (sort keys %params) {
        # We need to sort the keys otherwise the output order of the hash
        # is given in internal order
        if (!($matched{$paramName}) && !($hasAmpersand)) {
            if ($params{$paramName} eq '!> \param '.$paramName." ...\n") {
                # there was no comment, just drop parameter
            } elsif ($params{$paramName} !~ m/UNMATCHED_PROCEDURE_ARGUMENT/) {
                # Must protect against updating an existing comment
                # Get rid of \n so UNMATCHED* text can be appended on.
                chomp($params{$paramName});
                print $params{$paramName} . " UNMATCHED_PROCEDURE_ARGUMENT: please check \n";
            } else {
                print $params{$paramName};
            }
        }
    }

    # If after looping through the elements there is no ampersand
    # we close off the comment and write out the procedure definition
    if (!($hasAmpersand)) {
        if ($returns ne $EMPTY) {
            # Print RESULT value first so that it should come straight after the \param definitions
            print $returns;
        } else {
            # Get return value from function name
            if ($isFunction) {
                print "!> \\return ...\n";
            }
        }
        if ($retVals ne $EMPTY) {
            # Print return values definitions second so that they come after any \return definitions
            print $retVals;
        }
        if (($dates eq $EMPTY) || ($dates eq "!> \\date\n")) {
            # dates entry empty or exists and contains no text
###            print "!> \\date MISSING_COMMENT: Unknown\n";  # Use this line if you want to add text to the entry
            print $dates;
        } else {
            print $dates;
        }
        if (($pars eq $EMPTY) || ($pars eq "!> \\par History\n")) {
            # pars entry empty or exists but contains no text
###            print "!> \\par History\n"; # Use this line if you want to add text to the entry
###            print "!>     MISSING_COMMENT: Unknown\n"; # Use this line if you want to add text to the entry
            print $pars;
        } else {
            print $pars;
        }
        if (($authors eq $EMPTY) || ($authors eq "!> \\author\n")) {
            # authors empty or exists but contains no text
###            print "!> \\author MISSING_COMMENT: Unknown\n"; # Use this line if you want to add text to the entry
            print $authors;
        } else {
            print $authors;
        }
        if ($versions ne $EMPTY) {
            print $versions;
        }
        if ($randoms ne $EMPTY) {
            print $randoms;
        }
        if ($notes ne $EMPTY){
            print $notes;
        }
        if ($remainders ne $EMPTY) {
            # Dumps out whatever else remainded in the header (e.g. stuff beginning !> without a \ or stuff beginning with just a !) for the SUBROUTINE/FUNCTION at the end
            print $remainders;
        }
        print "! **************************************************************************************************\n";
        print "$leftoverlines$buffer$currline";
        # Reset all the variables after writing out the header
        initVariables();
        return;
    }
}
