#!/usr/bin/env perl
# See copyright, etc in below POD section.
######################################################################

use warnings;
use Getopt::Long;
#use Data::Dumper; $Data::Dumper::Indent=1; $Data::Dumper::Sortkeys=1; #Debug
use IO::File;
use IO::Dir;
use Pod::Usage;
use strict;
use vars qw($Debug);

our $VERSION = '0.001';
our $Opt_Widen = 1;

#======================================================================
# main

autoflush STDOUT 1;
autoflush STDERR 1;
Getopt::Long::config("no_auto_abbrev");
if (! GetOptions (
          "debug"       => sub { $Debug = 1; },
          "help"        => sub { print "Version $VERSION\n";
                                 pod2usage(-verbose=>2, -exitval => 0,
                                           output=>\*STDOUT, -noperldoc=>1); },
          "narrow!"     => sub { $Opt_Widen = 0; },
          "version"     => sub { print "Version $VERSION\n"; exit(0); },
          "widen!"      => sub { $Opt_Widen = 1; },  # Default, undocumented
          "<>"          => sub { die "%Error: Unknown parameter: $_[0]\n"; },
    )) {
    die "%Error: Bad usage, try 'git_untabify --help'\n";
}

read_patch();

#######################################################################

sub read_patch {
    my $filename = undef;
    my $lineno = 0;
    my $hunk = 0;
    my $editlines = {};
    while (defined(my $line = <STDIN>)) {
        chomp $line;
        if ($line =~ m!^\+\+\+ b/(.*)!) {
            find_edits($editlines);
            edit_file($filename, $editlines);
            $filename = $1;
            $lineno = 0;
            $editlines = {};
            print "FILE $filename\n" if $Debug;
        }
        elsif ($line  =~ m!^@@ -?[0-9]+,?[0-9]* \+?([0-9]+)!) {
            $lineno = $1 - 1;
            ++$hunk;
            print "  LINE $1  $line" if $Debug;
        }
        elsif ($line  =~ m!^ !) {
            ++$lineno;
            $editlines->{$lineno} = {line => $line,
                                     hunk => $hunk,
                                     user_edit => 0, };
        }
        elsif ($line  =~ m!^\+!) {
            ++$lineno;
            print "  $lineno: $line" if $Debug;
            $editlines->{$lineno} = {line => $line,
                                     hunk => $hunk,
                                     user_edit => 1, };
        }
    }
    find_edits($editlines);
    edit_file($filename, $editlines);
}

sub find_edits {
    my $editlines = shift;
    # Expand edit regions so if have edited line, non-edited line, edited
    # line, we will tab expand the middle ones.
    my %hunk_firstlines;
    foreach my $lineno (sort {$a <=> $b} keys %$editlines) {
        my $hunk = $editlines->{$lineno}{hunk};
        $hunk_firstlines{$hunk} ||= $lineno if $editlines->{$lineno}{user_edit};
    }
    my %hunk_lastlines;
    foreach my $lineno (sort {$b <=> $a} keys %$editlines) {
        my $hunk = $editlines->{$lineno}{hunk};
        $hunk_lastlines{$hunk} ||= $lineno if $editlines->{$lineno}{user_edit};
    }

    if ($Opt_Widen) {
        # Expand to include }'s that finish basic block
        foreach my $hunk (keys %hunk_lastlines) {
            while (my $lineno = $hunk_lastlines{$hunk}) {
                ++$lineno;
                if (($editlines->{$lineno}{line} || "")
                    =~ /^[+ ]\s+}[ \t};]*$/) {
                    $hunk_lastlines{$hunk} = $lineno;
                } else {
                    last;
                }
            }
        }
        # Expand to always untabify at least 3 lines (so that future diff will
        # have non-tabs within a edit hunk distance
        foreach my $hunk (keys %hunk_firstlines) {
            if ($hunk_firstlines{$hunk} == $hunk_lastlines{$hunk}) {
                --$hunk_firstlines{$hunk};
                ++$hunk_lastlines{$hunk};
            }
            elsif ($hunk_firstlines{$hunk}+1 == $hunk_lastlines{$hunk}) {
                ++$hunk_lastlines{$hunk};
            }
        }
    }

    foreach my $lineno (sort {$a <=> $b} keys %$editlines) {
        if (($editlines->{$lineno}{line}||"") =~ /\t/) {
            my $hunk = $editlines->{$lineno}{hunk};
            if ($hunk_firstlines{$hunk} <= $lineno && $hunk_lastlines{$hunk} >= $lineno) {
                $editlines->{$lineno}{editit} = 1;
            }
        }
    }
}

sub edit_file {
    my $filename = shift;
    my $editlines = shift;

    my @editits;
    foreach my $lineno (sort {$a <=> $b} keys %$editlines) {
        push @editits, $lineno if $editlines->{$lineno}{editit};
    }

    return if $#editits < 0;
    if (ignore($filename)) {
        print "%Warning: Ignoring $filename\n";
        return;
    }
    print "Edit $filename ",join(",",@editits),"\n";

    my $lineno = 0;
    my @out;
    {
        my $fh = IO::File->new("<$filename") or die "%Error: $! $filename\n";
        while (defined(my $line = $fh->getline)) {
            ++$lineno;
            if ($editlines->{$lineno}{editit}) {
                print $line;
                push @out, untabify($line);
            } else {
                push @out, $line;
            }
        }
        $fh->close;
    }
    {
        my $fh = IO::File->new(">${filename}.untab") or die "%Error: $! ${filename}.untab,";
        $fh->print(join('',@out));
        $fh->close;

        my ($dev,$ino,$mode) = stat($filename);
        chmod $mode, "${filename}.untab";
    }

    rename("${filename}.untab", $filename) or die "%Error: $! ${filename}.untab,";
}

sub ignore {
    my $filename = shift;
    return 1 if ($filename =~ /(Makefile|\.mk)/);
    return 1 if ($filename =~ /\.(y|l|out|vcd)$/);
    return 1 if ($filename =~ /gtkwave/);
    #
    return 0 if ($filename =~ /\.(sv|v|vh|svh|h|vc|cpp|pl)$/);
    return 0;
}

sub untabify {
    my $line = shift;
    my $out = "";
    my $col = 0;
    foreach my $c (split //, $line) {
        if ($c eq "\t") {
            my $destcol = int(($col+8)/8)*8;
            while ($col < $destcol) { ++$col; $out .= " "; }
        } else {
            $out .= $c;
            $col++;
        }
    }
    return $out;
}

#######################################################################
__END__

=pod

=head1 NAME

git_untabify - Pipe a git diff report and untabify differences

=head1 SYNOPSIS

  git diff a..b | git_untabify

=head1 DESCRIPTION

Take a patch file, and edit the files in the destination patch list to
untabify the related patch lines.

=head1 ARGUMENTS

=over 4

=item --help

Displays this message and program version and exits.

=item --narrow

Only edit lines which the user edited.

If not provided, also edit other lines within at least a 3 line area around
the edit, and between any edits within the same hunk.

=item --version

Displays program version and exits.

=back

=head1 DISTRIBUTION

Copyright 2005-2020 by Wilson Snyder.  Verilator is free software; you can
redistribute it and/or modify it under the terms of either the GNU Lesser
General Public License Version 3 or the Perl Artistic License Version 2.0.

=head1 AUTHORS

Wilson Snyder <wsnyder@wsnyder.org>

=head1 SEE ALSO

=cut

######################################################################
### Local Variables:
### compile-command: "./git_untabify "
### End: