#!/usr/bin/ruby
# -*- coding: utf-8 -*-
require 'pathname'

class TestBase
  def log(message)
    puts message if !SUMMARY_MODE
  end

  def is_test_header?(filename)
    return true if filename =~ /test\.h$/
    return true if filename =~ /testguard\.h/

    return false
  end

  def is_generated?(filename)
    # add exception for auto-generated file
    return true if filename =~ /config\.h$/
    return true if filename =~ /typemaps\.h/
    return true if filename =~ /typemaps\.cpp/
    return true if filename =~ /serialization\.h/
    return true if filename =~ /hpxserialization\.cpp/
    return true if filename =~ /test\.cpp$/
    return true if filename =~ /test\.cu$/

    return false
  end

  def is_source?(filename)
    return true if filename =~ /\.cu/
    return true if filename =~ /\.cpp/

    return false
  end

  def is_commonly_excluded?(filename)
    is_test_header?(filename) || is_generated?(filename) || is_source?(filename)
  end
end

class FindUndocumentedClasses < TestBase
  def test(filename, content)
    return 0 if is_commonly_excluded?(filename)

    fragment = "[^<>,]*"
    nested_template_parameters = "<#{fragment}>#{fragment}"
    parameter_pattern = "#{fragment}(#{nested_template_parameters})?"
    template_pattern = "template<((#{parameter_pattern},\n?)*#{parameter_pattern})?>\\s*\\n"

    ret = 0

    content.scan(/\n\n(#{template_pattern})?class (\w+)[^;]*$/) do
      log "#{filename}: found undocumented class »#{$6}«"
      ret += 1
    end

    return ret
  end
end

class FindUnmatchedHeaderGuards < TestBase
  def test(filename, content)
    return 0 if is_commonly_excluded?(filename)

    relative_path = Pathname.new(filename).relative_path_from(PATH).to_s
    if !(filename =~ /src\/libgeodecomp\//)
      relative_path = "libgeodecomp/#{relative_path}"
    end

    macro = relative_path.upcase.gsub(/[\/\.]/, "_")

    expected_guard = <<EOF
#ifndef #{macro}
#define #{macro}
EOF

    if !(content =~ /#{expected_guard}/)
      first_line = content.split("\n")[0]
      log "#{filename}: could not find expected header guard (expected: #{expected_guard}, found: #{first_line})."
      return 1
    end

    return 0
  end
end

class FindFixmes < TestBase
  def test(filename, content)
    ret = 0

    begin
      content.scan(/.*fixme.*/i) do
        log "#{filename}: #{$&}"
        ret += 1
      end
    rescue ArgumentError => e
    end

    return ret
  end
end

class FindBadIncludes < TestBase
  def test(filename, content)
    return 0 if is_generated?(filename)

    ret = 0

    begin
      content.scan(/^#include.*".*"/) do
        match = $&
        if filename =~ /src\/libgeodecomp/
          ret += 1
          log "#{filename}: found malformed include »#{match}«"
        end
      end
    rescue ArgumentError => e
    end

    return ret
  end
end

class FindSizeTWithOutStd < TestBase
  def test(filename, content)
    ret = 0

    begin
      content.scan(/(std::)?size_t[^_].*/) do
        if $1.nil?
          ret += 1
          log "#{filename}: size_t used when std::size_t should have been used: »#{$&}«"
        end
      end
    rescue ArgumentError => e
    end

    return ret
  end
end

class FindUnprefixedTargetNames < TestBase
  def test(filename, content)
    return 0 unless filename =~ /(testbed|examples)/

    expected_name = "libgeodecomp_" + Pathname.new(filename).parent.relative_path_from(PATH).to_s.gsub(/\//, "_")
    ret = 0

    ["executable", "library"].each do |target_token|
      content.scan(/add_#{target_token}\((\S*) /i) do
        match = $1
        unless (match =~ /^#{expected_name}/)
          # warn user, may clash with user targets if LibGeoDecomp is a subproject of other CMake projects:
          log "#{filename}: non-safeguarded #{target_token} target name »#{match}« found"
          ret += 1
        end
      end
    end

    return ret
  end
end

def scour_files(files, tester)
  failures = 0

  files.each do |file|
    content = File.read(file)
    failures += tester.test(file, content)
  end

  delimiter = "=" * 80
  puts delimiter if !SUMMARY_MODE
  puts "#{tester.class}: #{failures}/#{files.size} tests failed"
  puts delimiter if !SUMMARY_MODE
end

DEFAULT_PATH=Pathname.new($0).parent.parent.parent.realpath + "src"

if (ARGV.size > 2) || ((ARGV.size > 1) && (ARGV[0] != "-s"))
  STDERR.puts <<EOF
A simple detector which scans for common patterns of poor coding style
(e.g. undocumented classes or malformed header guards).

Usage: #{$0} [-s] [PATH]

...where PATH is the source directory you wish to check. Default path is #{DEFAULT_PATH.to_s}
EOF
  exit 1
end

SUMMARY_MODE = (ARGV.size > 1)
PATH = Pathname.new(ARGV.last || DEFAULT_PATH)

pattern = PATH + "**" + "*.{h,cpp,cu}"
cpp_files = Dir.glob(pattern)
scour_files(cpp_files, FindUndocumentedClasses.new)
scour_files(cpp_files, FindUnmatchedHeaderGuards.new)
scour_files(cpp_files, FindFixmes.new)
scour_files(cpp_files, FindBadIncludes.new)
scour_files(cpp_files, FindSizeTWithOutStd.new)

pattern = PATH + "**" + "CMakeLists.*txt"
cmake_files = Dir.glob(pattern)

scour_files(cmake_files, FindUnprefixedTargetNames.new)
