#!/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) 2012-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 CMake files for adherence to a coding style

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

# 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_CMAKESTYLE 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.03";

&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/CMakeLists\.txt/ || $f =~ m/\.cmake$/) {
  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_CMAKESTYLE ) {
  $KRAZY_STYLE_CMAKESTYLE = "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($indent) = 0; #indentation level for current line
my($lindent)= 0; #indentation level for previous line

#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

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

  $indent = length($line) - length($tline) if (length($tline) > 0);
  if ($indent % $KRAZY_STYLE_OFFSET) {
    $Issues{'BADINDENTOFF'}{'count'}++;
    if ($Issues{'BADINDENTOFF'}{'count'} == 1) {
      $Issues{'BADINDENTOFF'}{'lines'} = "line\#" . $linecnt;
    } else {
      $Issues{'BADINDENTOFF'}{'lines'} .= "," . $linecnt;
    }
    print "$linecnt (bad indent offset) => $tline\n" if (&verboseArg());
  }

  if ($indent != $lindent) {
    #allow to get bigger by $KRAZY_STYLE_OFFSET if previous line started a block
    if (&startBlock($lline) || $lline =~ m/\(\s*$/) {
      if ($lindent != $indent - $KRAZY_STYLE_OFFSET ) {
	$Issues{'BADINDENT'}{'count'}++;
	if ($Issues{'BADINDENT'}{'count'} == 1) {
	  $Issues{'BADINDENT'}{'lines'} = "line\#" . $linecnt;
	} else {
	  $Issues{'BADINDENT'}{'lines'} .= "," . $linecnt;
	}
	print "$linecnt (bad block starting indent offset) => $tline\n" if (&verboseArg());
      }
      goto ncheck;
    }
    #allow to get smaller by $KRAZY_STYLE_OFFSET if current line ended a block
    if (&endBlock($line) || $line =~ m/^\s*\)\s*$/) {
      if ($lindent < $indent + $KRAZY_STYLE_OFFSET ) {
	$Issues{'BADINDENT'}{'count'}++;
	if ($Issues{'BADINDENT'}{'count'} == 1) {
	  $Issues{'BADINDENT'}{'lines'} = "line\#" . $linecnt;
	} else {
	  $Issues{'BADINDENT'}{'lines'} .= "," . $linecnt;
	}
	print "$linecnt (bad block closing indent offset) => $tline\n" if (&verboseArg());
      }
      goto ncheck;
    }
    if ($lline =~ m/\)\s*$/) {
      $Issues{'BADINDENT'}{'count'}++;
      if ($Issues{'BADINDENT'}{'count'} == 1) {
	$Issues{'BADINDENT'}{'lines'} = "line\#" . $linecnt;
      } else {
	$Issues{'BADINDENT'}{'lines'} .= "," . $linecnt;
      }
      print "$linecnt (bad indentation) => $tline\n" if (&verboseArg());
      goto ncheck;
    }
  }

 ncheck:
  if (($KRAZY_STYLE_LINEMAX > 0) && (length($line) > $KRAZY_STYLE_LINEMAX)) {
    if ($line !~ m/^\s*(message|macro_log_feature|option)\s*\(/) {
      $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 ($line =~ m/^\s*$/ && $lline =~ m/^\s*$/ && $linecnt > 1) {
    $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 ($tpline =~ m/\s{2,}+/) {
    $Issues{'EXTRASPACE'}{'count'}++;
    if ($Issues{'EXTRASPACE'}{'count'} == 1) {
      $Issues{'EXTRASPACE'}{'lines'} = "line\#" . $linecnt;
    } else {
      $Issues{'EXTRASPACE'}{'lines'} .= "," . $linecnt;
    }
    print "$linecnt (extra whitespace between arguments) => $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/\(/ && $pline =~ 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 opening paren) => $line\n" if (&verboseArg());
  }

  if ($pline =~ m/\s\(/) {
    if ($pline !~ m/(NOT|OR|AND)\s\(/i) {
      $Issues{'NOCONDSPACE'}{'count'}++;
      if ($Issues{'NOCONDSPACE'}{'count'} == 1) {
        $Issues{'NOCONDSPACE'}{'lines'} = "line\#" . $linecnt;
      } else {
        $Issues{'NOCONDSPACE'}{'lines'} .= "," . $linecnt;
      }
      print "$linecnt (no space between command and opening paren) => $line\n" if (&verboseArg());
    }
  }

  if ($tpline !~ m/^\)/ && $pline =~ m/\)/ && $pline =~ 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 closing paren) => $line\n" if (&verboseArg());
  }

  if ($pline =~ m/(else|endif|endforeach|endfunction|endmacro|endwhile)\s*\([\w\s_]+/i) {
    $Issues{'NONEMPTYEND'}{'count'}++;
    if ($Issues{'NONEMPTYEND'}{'count'} == 1) {
      $Issues{'NONEMPTYEND'}{'lines'} = "line\#" . $linecnt;
    } else {
      $Issues{'NONEMPTYEND'}{'lines'} .= "," . $linecnt;
    }
    print "$linecnt (block closing commands should be empty) => $line\n" if (&verboseArg());
  }

  if ($pline =~ m/(\w+)\s*\(/) {
    my $command = $1;
    my $lcommand = lc($1);
    if ($command ne $lcommand) {
      $Issues{'NONLOWER'}{'count'}++;
      if ($Issues{'NONLOWER'}{'count'} == 1) {
	$Issues{'NONLOWER'}{'lines'} = "line\#" . $linecnt;
      } else {
	$Issues{'NONLOWER'}{'lines'} .= "," . $linecnt;
      }
      print "$linecnt (commands should be all lower-case $command) => $line\n" if (&verboseArg());
    }
  }

  if ($pline =~ m/\(.*\).*\(.*\)/) {
    $Issues{'MULTISTATE'}{'count'}++;
    if ($Issues{'MULTISTATE'}{'count'} == 1) {
      $Issues{'MULTISTATE'}{'lines'} = "line\#" . $linecnt;
    } else {
      $Issues{'MULTISTATE'}{'lines'} .= "," . $linecnt;
    }
    print "$linecnt (only 1 command permitted per line) => $line\n" if (&verboseArg());
  }

  $lline = $line;
  $lindent = $indent;
}

$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{'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{'NOCONDSPACE'}{'issue'} = 'Do not put spaces before the opening paren';
  $Issues{'NOCONDSPACE'}{'count'} = 0;
  $Issues{'NOCONDSPACE'}{'lines'} = '';

  $Issues{'NONLOWER'}{'issue'} = 'Non-lowercase command';
  $Issues{'NONLOWER'}{'count'} = 0;
  $Issues{'NONLOWER'}{'lines'} = '';

  $Issues{'NONEMPTYEND'}{'issue'} = 'Non-empty block closing command';
  $Issues{'NONEMPTYEND'}{'count'} = 0;
  $Issues{'NONEMPTYEND'}{'lines'} = '';

  $Issues{'MULTISTATE'}{'issue'} = 'Multiple commands on 1 line';
  $Issues{'MULTISTATE'}{'count'} = 0;
  $Issues{'MULTISTATE'}{'lines'} = '';

  $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{'BADINDENT'}{'issue'} = "Bad indentation";
  $Issues{'BADINDENT'}{'count'} = 0;
  $Issues{'BADINDENT'}{'lines'} = '';

  $Issues{'BADINDENTOFF'}{'issue'} = "Bad ident offset";
  $Issues{'BADINDENTOFF'}{'count'} = 0;
  $Issues{'BADINDENTOFF'}{'lines'} = '';

  $Issues{'EXTRASPACE'}{'issue'} = "Extra spaces between command arguments";
  $Issues{'EXTRASPACE'}{'count'} = 0;
  $Issues{'EXTRASPACE'}{'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'} = '';
}
