#!/usr/bin/perl -w

eval 'exec /usr/bin/perl -w -S $0 ${1+"$@"}'
    if 0; # not running under some shell
###############################################################################
# Sanity check plugin for the Krazy project.                                  #
# Copyright (C) 2013 by Allen Winter <winter@kde.org>                         #
#                                                                             #
# This program is free software; you can redistribute it and/or modify        #
# it under the terms of the GNU General Public License as published by        #
# the Free Software Foundation; either version 2 of the License, or           #
# (at your option) any later version.                                         #
#                                                                             #
# This program is distributed in the hope that it will be useful,             #
# but WITHOUT ANY WARRANTY; without even the implied warranty of              #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the                #
# GNU General Public License for more details.                                #
#                                                                             #
# You should have received a copy of the GNU General Public License along     #
# with this program; if not, write to the Free Software Foundation, Inc.,     #
# 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.               #
#                                                                             #
###############################################################################

# Tests Python program files for adherence to a coding style

# Supports these environment variables:
#  KRAZY_STYLE_PYTHONSTYLE: a predefined style, currently available styles are "kde" (currently ignored)
#  KRAZY_STYLE_OFFSET: basic indentation level, usually a small integer like 2 or 4 (currently ignored)
#  KRAZY_STYLE_LINEMAX: max number of chars per line allowed (defaults to unlimited)

# Does Not Check:
# * proper indentation, since Python requires that anyway
# * braces and parens, since Python doesn't use those much

# Program options:
#   --help:          print one-line help message and exit
#   --version:       print one-line version information and exit
#   --priority:      report issues of the specified priority only
#   --strict:        report issues with the specified strictness level only
#   --explain:       print an explanation with solving instructions
#   --installed      file is to be installed
#   --quiet:         suppress all output messages
#   --verbose:       print the offending content

# Exits with status=0 if test condition is not present in the source;
# else exits with the number of failures encountered.

use strict;
use Env qw (KRAZY_STYLE_PYTHONSTYLE KRAZY_STYLE_OFFSET KRAZY_STYLE_LINEMAX);
use Cwd 'abs_path';
use Tie::IxHash;
use FindBin qw($Bin);
use lib "$Bin/../../../../lib";
use Krazy::Utils;

my($Prog) = "style";
my($Version) = "1.00";

&parseArgs();

&Help() if &helpArg();
&Version() if &versionArg();
&Explain() if &explainArg();
if ($#ARGV != 0){ &Help(); Exit 0; }


# has of all issues we need to keep track of
tie my(%Issues), "Tie::IxHash";

# Check Condition
my($f) = $ARGV[0];

if ($f =~ m/\.py$/) {
  open(F, "$f") || die "Couldn't open $f";
} else {
  print "okay\n" if (!&quietArg());
  Exit 0;
}

$KRAZY_STYLE_LINEMAX = 0 if ( !$KRAZY_STYLE_LINEMAX );
if ( !$KRAZY_STYLE_PYTHONSTYLE ) {
  $KRAZY_STYLE_PYTHONSTYLE = "kde";
}

if ( !$KRAZY_STYLE_OFFSET ) {
  $KRAZY_STYLE_OFFSET = 2;
}

#open file and slurp it in
open(F, "$f") || die "Couldn't open $f";
my(@lines) = <F>;
close(F);

###we will remove the comments as we go

my($cnt) = 0;
my($linecnt) = 0;
my($line) = '';  #current line (non-processed)
my($tline) = ''; #current line (non-processed) with starting whitespace removed
my($lline) = ''; #last line (non-processed)
my($pline) = ''; #processed current line (i.e. comments and strings have been removed)
my($tpline) =''; #processed current line with starting/trailing whitespace removed

my($indox) = 0;  # set when currently in a docstring

#let's begin
&initIssues();
foreach $line (@lines) {
  chomp($line);

  if ($line =~ m+\#.*[Kk]razy:excludeall=.*$Prog+ ||
      $line =~ m+\#.*[Kk]razy:skip+) {
    print "okay\n" if (!&quietArg());
    Exit 0;
  }
  next if ($line =~ m+\#.*[Kk]razy:exclude=.*$Prog+);

  $linecnt++;

  $tline = $line;
  $tline =~ s/^\s*//;   #remove starting whitespace

  $pline = $line;
  $pline =~ s/\#.*$//;  #remove comments
  $pline =~ s/".*"/""/g;#clean-out strings
  $pline =~ s/'.*'/''/g;

  $tpline = $pline;
  $tpline =~ s/^\s*//;  #remove starting whitespace
  $tpline =~ s/\s*$//;  #remove trailing whitespace

  if ($line =~ m/^\s*"""/) {
    if($indox) {
      $indox = 0;
    } else {
      $indox = 1;
    }
  }

  if (($KRAZY_STYLE_LINEMAX > 0) && (length($line) > $KRAZY_STYLE_LINEMAX)) {
    $Issues{'LINELEN'}{'count'}++;
    if ($Issues{'LINELEN'}{'count'} == 1) {
      $Issues{'LINELEN'}{'lines'} = "line\#" . $linecnt;
    } else {
      $Issues{'LINELEN'}{'lines'} .= "," . $linecnt;
    }
    print "$linecnt (line too long) => $line\n" if (&verboseArg());
  }

  if ($indox) {
    $lline = $line;
    next;
  }

  if ($line =~ m/^\s*$/ && $lline =~ m/^\s*$/) {
    $Issues{'BLANKS'}{'count'}++;
    if ($Issues{'BLANKS'}{'count'} == 1) {
      $Issues{'BLANKS'}{'lines'} = "line\#" . $linecnt;
    } else {
      $Issues{'BLANKS'}{'lines'} .= "," . $linecnt;
    }
    print "$linecnt (extra blank lines) => $line\n" if (&verboseArg());
  }

  if ($line =~ m/\s$/) {
    $Issues{'TRAILWHITE'}{'count'}++;
    if ($Issues{'TRAILWHITE'}{'count'} == 1) {
      $Issues{'TRAILWHITE'}{'lines'} = "line\#" . $linecnt;
    } else {
      $Issues{'TRAILWHITE'}{'lines'} .= "," . $linecnt;
    }
    print "$linecnt (trailing whitespace) => $line\n" if (&verboseArg());
  }

  if ($line =~ m/\t/) {
    $Issues{'TAB'}{'count'}++;
    if ($Issues{'TAB'}{'count'} == 1) {
      $Issues{'TAB'}{'lines'} = "line\#" . $linecnt;
    } else {
      $Issues{'TAB'}{'lines'} .= "," . $linecnt;
    }
    print "$linecnt (tab encountered) => $line\n" if (&verboseArg());
  }

  if ($pline =~ m/==/ && $line =~ m/==/ &&
      $line !~ m/[[:space:]]{1}==[[:space:]]{1}/ &&
      $line !~ m/==[[:space:]]*\(/ &&
      $line !~ m/[[:space:]]{1}==$/) {
    $Issues{'EQUALSPACE'}{'count'}++;
    if ($Issues{'EQUALSPACE'}{'count'} == 1) {
      $Issues{'EQUALSPACE'}{'lines'} = "line\#" . $linecnt;
    } else {
      $Issues{'EQUALSPACE'}{'lines'} .= "," . $linecnt;
    }
    print "$linecnt (spaces around '==') => $line\n" if (&verboseArg());
  }

  if ($pline =~ m/!=/ && $line =~ m/!=/ &&
      $line !~ m/[[:space:]]{1}!=[[:space:]]{1}/ &&
      $line !~ m/!=[[:space:]]*\(/ &&
      $line !~ m/[[:space:]]{1}!=$/) {
    $Issues{'EQUALSPACE'}{'count'}++;
    if ($Issues{'EQUALSPACE'}{'count'} == 1) {
      $Issues{'EQUALSPACE'}{'lines'} = "line\#" . $linecnt;
    } else {
      $Issues{'EQUALSPACE'}{'lines'} .= "," . $linecnt;
    }
    print "$linecnt (spaces around '!=') => $line\n" if (&verboseArg());
  }

  if ($pline =~ m/\)/ && $line =~ m/\s\)/) {
    $Issues{'NOSPACEPAREN'}{'count'}++;
    if ($Issues{'NOSPACEPAREN'}{'count'} == 1) {
      $Issues{'NOSPACEPAREN'}{'lines'} = "line\#" . $linecnt;
    } else {
      $Issues{'NOSPACEPAREN'}{'lines'} .= "," . $linecnt;
    }
    print "$linecnt (no space before '\)') => $line\n" if (&verboseArg());
  }

  if ($pline =~ m/\(/ && $line =~ m/\(\s/) {
    $Issues{'PARENNOSPACE'}{'count'}++;
    if ($Issues{'PARENNOSPACE'}{'count'} == 1) {
      $Issues{'PARENNOSPACE'}{'lines'} = "line\#" . $linecnt;
    } else {
      $Issues{'PARENNOSPACE'}{'lines'} .= "," . $linecnt;
    }
    print "$linecnt (no space after '\(') => $line\n" if (&verboseArg());
  }

  if ($pline =~ m/,[[:alnum:]]/ || $line =~ m/, {2,}/) {
    if ($line !~ m/,[[:space:]]*$/ && $line !~ m/<.*,.*>/) {
      $Issues{'COMMASPACE'}{'count'}++;
      if ($Issues{'COMMASPACE'}{'count'} == 1) {
        $Issues{'COMMASPACE'}{'lines'} = "line\#" . $linecnt;
      } else {
        $Issues{'COMMASPACE'}{'lines'} .= "," . $linecnt;
      }
      print "$linecnt (no space after ',') => $line\n" if (&verboseArg());
    }
  }

  if ($line =~ m/[[:space:]],/) {
    $Issues{'SPACECOMMA'}{'count'}++;
    if ($Issues{'SPACECOMMA'}{'count'} == 1) {
      $Issues{'SPACECOMMA'}{'lines'} = "line\#" . $linecnt;
    } else {
      $Issues{'SPACECOMMA'}{'lines'} .= "," . $linecnt;
    }
    print "$linecnt (no space before ',') => $line\n" if (&verboseArg());
  }

  if ($pline =~ m/\+/ && $line =~ m/\+/ && $line !~ m/[[:space:]]{1}\+[[:space:]]{1}/ && $line !~ m/\+=/ && $line !~ m/\+\+/ && $line !~ m/\+[[:digit:]]/ && $line !~ m/^[[:space:]]*\+/ && $line !~ m/\([[:space:]]*\+/ && $line !~ m/\+$/ && $line !~ m/operator\+/ && $line !~ m/,\s\+/) {
    $Issues{'OPERSPACE'}{'count'}++;
    if ($Issues{'OPERSPACE'}{'count'} == 1) {
      $Issues{'OPERSPACE'}{'lines'} = "line\#" . $linecnt;
    } else {
      $Issues{'OPERSPACE'}{'lines'} .= "," . $linecnt;
    }
    print "$linecnt (spaces around '+') => $line\n" if (&verboseArg());
  }

  if ($pline =~ m/-/ && $line =~ m/-/ && $line !~ m/[[:space:]]{1}-[[:space:]]{1}/ && $line !~ m/-=/ && $line !~ m/\->/ && $line !~ m/--/ && $line !~ m/-[[:digit:]]/ && $line !~ m/=[[:space:]]{1}-/ && $line !~ m/^[[:space:]]*-/ && $line !~ m/\([[:space:]]*-/ && $line !~ m/-$/ && $line !~ m/operator-/ && $line !~ m/,\s-/ && $line !~ m/return\s-/) {
    $Issues{'OPERSPACE'}{'count'}++;
    if ($Issues{'OPERSPACE'}{'count'} == 1) {
      $Issues{'OPERSPACE'}{'lines'} = "line\#" . $linecnt;
    } else {
      $Issues{'OPERSPACE'}{'lines'} .= "," . $linecnt;
    }
    print "$linecnt (spaces around '-') => $line\n" if (&verboseArg());
  }

  if ($pline =~ m+/+ && $line =~ m+/+ && $line !~ m+[[:space:]]{1}/[[:space:]]{1}+ && $line !~ m+/=+ && $line !~ m+/$+ && $line !~ m/include/ && $line !~ m+operator/+) {
    $Issues{'OPERSPACE'}{'count'}++;
    if ($Issues{'OPERSPACE'}{'count'} == 1) {
      $Issues{'OPERSPACE'}{'lines'} = "line\#" . $linecnt;
    } else {
      $Issues{'OPERSPACE'}{'lines'} .= "," . $linecnt;
    }
    print "$linecnt (spaces around '/') => $line\n" if (&verboseArg());
  }

  if ($pline =~ m/=/ && $line !~ m/[[:space:]]{1}=[[:space:]]{1}/ && $line !~ m/[=\!><%\+-]=/) {
    $Issues{'ASSIGNSPACE'}{'count'}++;
    if ($Issues{'ASSIGNSPACE'}{'count'} == 1) {
      $Issues{'ASSIGNSPACE'}{'lines'} = "line\#" . $linecnt;
    } else {
      $Issues{'ASSIGNSPACE'}{'lines'} .= "," . $linecnt;
    }
    print "$linecnt (spaces around '=') => $line\n" if (&verboseArg());
  }

  if ($pline =~ m/\s:/) {
    $Issues{'SPACECOLON'}{'count'}++;
    if ($Issues{'SPACECOLON'}{'count'} == 1) {
      $Issues{'SPACECOLON'}{'lines'} = "line\#" . $linecnt;
    } else {
      $Issues{'SPACECOLON'}{'lines'} .= "," . $linecnt;
    }
    print "$linecnt (whitespace preceeding colon) => $line\n" if (&verboseArg());
  }

  if ($pline =~ m/return[[:space:]]*\(.*\)$/) {
    $Issues{'RETURNPAREN'}{'count'}++;
    if ($Issues{'RETURNPAREN'}{'count'} == 1) {
      $Issues{'RETURNPAREN'}{'lines'} = "line\#" . $linecnt;
    } else {
      $Issues{'RETURNPAREN'}{'lines'} .= "," . $linecnt;
    }
    print "$linecnt (no return values in parens) => $line\n" if (&verboseArg());
  }

  $lline = $line;
}

$cnt = &printResults();
if (!$cnt) {
  print "okay\n" if (!&quietArg());
  Exit 0;
} else {
  Exit $cnt;
}

sub Help {
  print "Check for adherence to a coding style\n";
  Exit 0 if &helpArg();
}

sub Version {
  print "$Prog, version $Version\n";
  Exit 0 if &versionArg();
}

sub Explain {
  print "Please follow the CMake coding style guidelines. Find the KDE CMake style guidelines at <http://techbase.kde.org/Policies/CMake_Coding_Style>\n";
  Exit 0 if &explainArg();
}

sub startBlock() {
  my($l)=@_;
  if ($l =~ m/^\s*(if|foreach|function|macro|while|else|elseif)\s*\(/i) {
    return 1;
  } else {
    return 0;
  }
}

sub endBlock() {
  my($l)=@_;
  if ($l =~ m/^\s*(endif|endforeach|endfunction|endmacro|endwhile|else|elseif)\s*\(/i) {
    return 1;
  } else {
    return 0;
  }
}
sub printResults() {
  my($guy);
  my($check_num)=0;
  my($tot)=0;
  my($cline,$rline);
  foreach $guy (keys %Issues) {
    $cline = "$Issues{$guy}{'issue'}:";

    if ($Issues{$guy}{'count'}) {
      $tot += $Issues{$guy}{'count'};
      print "$cline $Issues{$guy}{'lines'}\n";
    }
  }
  return $tot;
}

sub initIssues() {

  $Issues{'TAB'}{'issue'} = 'Do not use tabs';
  $Issues{'TAB'}{'count'} = 0;
  $Issues{'TAB'}{'lines'} = '';

  $Issues{'LINELEN'}{'issue'} = "Line longer than $KRAZY_STYLE_LINEMAX characters";
  $Issues{'LINELEN'}{'count'} = 0;
  $Issues{'LINELEN'}{'lines'} = '';

  $Issues{'TRAILWHITE'}{'issue'} = "Trailing whitespace";
  $Issues{'TRAILWHITE'}{'count'} = 0;
  $Issues{'TRAILWHITE'}{'lines'} = '';

  $Issues{'BLANKS'}{'issue'} = "Extra blank lines";
  $Issues{'BLANKS'}{'count'} = 0;
  $Issues{'BLANKS'}{'lines'} = '';

  $Issues{'EQUALSPACE'}{'issue'} = 'Put spaces around \'==\' and \'!=\'';
  $Issues{'EQUALSPACE'}{'count'} = 0;
  $Issues{'EQUALSPACE'}{'lines'} = '';
  $Issues{'OPERSPACE'}{'issue'} = 'Put spaces around \'+\', \'-\', \'*\', \'/\'';

  $Issues{'OPERSPACE'}{'count'} = 0;
  $Issues{'OPERSPACE'}{'lines'} = '';

  $Issues{'ASSIGNSPACE'}{'issue'} = 'Put spaces around \'=\' assignment';
  $Issues{'ASSIGNSPACE'}{'count'} = 0;
  $Issues{'ASSIGNSPACE'}{'lines'} = '';

  $Issues{'NOSPACEPAREN'}{'issue'} = 'Do not put spaces before a closing paren';
  $Issues{'NOSPACEPAREN'}{'count'} = 0;
  $Issues{'NOSPACEPAREN'}{'lines'} = '';

  $Issues{'PARENNOSPACE'}{'issue'} = 'Do not put spaces after an opening paren';
  $Issues{'PARENNOSPACE'}{'count'} = 0;
  $Issues{'PARENNOSPACE'}{'lines'} = '';

  $Issues{'COMMASPACE'}{'issue'} = 'Bad spacing after a comma';
  $Issues{'COMMASPACE'}{'count'} = 0;
  $Issues{'COMMASPACE'}{'lines'} = '';

  $Issues{'SPACECOMMA'}{'issue'} = 'Bad spacing before a comma';
  $Issues{'SPACECOMMA'}{'count'} = 0;
  $Issues{'SPACECOMMA'}{'lines'} = '';

  $Issues{'SPACECOLON'}{'issue'} = 'No whitespace before colon';
  $Issues{'SPACECOLON'}{'count'} = 0;
  $Issues{'SPACECOLON'}{'lines'} = '';

  $Issues{'RETURNPAREN'}{'issue'} = 'Do not put return values in parens';
  $Issues{'RETURNPAREN'}{'count'} = 0;
  $Issues{'RETURNPAREN'}{'lines'} = '';

}
