#!/usr/bin/perl -w

# Copyright 1999-2000 Patrik Stridvall

# Note that winapi_check are using heuristics quite heavily.
# So always remember that:
#
# "Heuristics are bug ridden by definition. 
#  If they didn't have bugs, then they'd be algorithms."
#
# In other words, reported bugs are only potential bugs not
# real bugs, so they are called issues rather than bugs.
#

use strict;

my $wine_dir;
my $winapi_check_dir;

BEGIN {

    if($0 =~ /^((.*?)\/?tools\/winapi_check)\/winapi_check$/)
    {
	$winapi_check_dir = $1;
	if($2 ne "")
	{
	    $wine_dir = $2;
	} else {
	    $wine_dir = ".";
	}
    }
    @INC = ($winapi_check_dir);

    require "modules.pm";
    require "nativeapi.pm";
    require "output.pm";
    require "preprocessor.pm";
    require "winapi.pm";
    require "winapi_documentation.pm";
    require "winapi_function.pm";
    require "winapi_local.pm";
    require "winapi_global.pm";
    require "winapi_options.pm";
    require "winapi_parser.pm";

    import modules;
    import nativeapi;
    import output;
    import preprocessor;
    import winapi;
    import winapi_documentation;
    import winapi_function;
    import winapi_local;
    import winapi_global;
    import winapi_options;
    import winapi_parser;
}

my $current_dir = ".";
if(length($wine_dir) != 1) {
    my $pwd; chomp($pwd = `pwd`);
    foreach my $n (1..((length($wine_dir) + 1) / 3)) {
	$pwd =~ s/\/([^\/]*)$//;
	$current_dir = "$1/$current_dir";
    }
    $current_dir =~ s/\/.$//;
}

my $output = 'output'->new;

my $options = winapi_options->new($output, \@ARGV, $wine_dir);
if(!defined($options)) {
    $output->write("usage: winapi_check [--help] [<files>]\n");

    exit 1;
} elsif($options->help) {
    $options->show_help;
    exit;
}

sub file_type {
    my $file = shift;

    my $file_dir = $file;
    if(!($file_dir =~ s/^(.*?)\/[^\/]*$/$1/)) {
	$file_dir = ".";
    }
    
    $file_dir =~ s/^$wine_dir\///;

    if($file_dir =~ /^(libtest|program|rc|tests|tools)/ || 
       $file =~ /dbgmain\.c$/ ||
       $file =~ /wineclipsrv\.c$/) # FIXME: Kludge
    {
	return "application";
    } elsif($file_dir =~ /^(debug|miscemu)/) {
	return "emulator";
    } else {
	return "library";
    }
}

sub file_skip {
    local $_ = shift;
    
    if(/agl.c$/) {
	return 1;
    }

    return 0;
}

my $modules = 'modules'->new($options, $output, $wine_dir, $current_dir, \&file_type, "$winapi_check_dir/modules.dat");

my $win16api = 'winapi'->new($options, $output, "win16", "$winapi_check_dir/win16");
my $win32api = 'winapi'->new($options, $output, "win32", "$winapi_check_dir/win32");
my @winapis = ($win16api, $win32api);

if($options->global) {
    'winapi'->read_all_spec_files($modules, $wine_dir, $current_dir, \&file_type, $win16api, $win32api);
} else {
    my @spec_files = $modules->allowed_spec_files($wine_dir, $current_dir);
    'winapi'->read_spec_files($modules, $wine_dir, $current_dir, \@spec_files, $win16api, $win32api);
}

my $nativeapi = 'nativeapi'->new($options, $output, "$winapi_check_dir/nativeapi.dat", "$wine_dir/configure.in", "$wine_dir/include/config.h.in");

for my $name ($win32api->all_functions) {
    my $module16 = $win16api->function_module($name);
    my $module32 = $win32api->function_module($name);
	
    if(defined($module16)) {
	$win16api->found_shared_function($name);
	$win32api->found_shared_function($name);
	
	if($options->shared) {
	    $output->write("*.spec: $name: is shared between $module16 (Win16) and $module32 (Win32)\n");
	}
    }
}

my %includes;
{   
    my @files = map {
	s/^.\/(.*)$/$1/;
	$_; 
    } split(/\n/, `find . -name \\*.h`);
    
    foreach my $file (@files) {
	my $file_dir = $file;
	if(!($file_dir =~ s/(.*?)\/[^\/]*$/$1/)) {
	    $file_dir = ".";
	}
   
	$includes{$file} = { name => $file };

	open(IN, "< $file");
	while(<IN>) {
	    if(/^\s*\#\s*include\s*\"(.*?)\"/) {
		my $header = $1;
		if(-e "$file_dir/$header") {
		    $includes{$file}{includes}{"$file_dir/$header"}++;
		} elsif(-e "$file_dir/../$header") { # FIXME: This is not correct
		    $includes{$file}{includes}{"$file_dir/../$header"}++; # FIXME: This is not correct
		} elsif(-e "$wine_dir/include/$header") {
		    $includes{$file}{includes}{"include/$header"}++;
		} else {
		    $output->write("$file: #include \"$header\" is not a local include\n");
		}
	    }
	}
	close(IN);
    }

    my @files2 = ("acconfig.h", "poppack.h", "pshpack1.h", "pshpack2.h", "pshpack4.h", "pshpack8.h",
                  "storage.h", "ver.h");
    foreach my $file2 (@files2) {
	$includes{"include/$file2"}{used}++;
    }    
}

my %declared_functions;

my $progress_output;
my $progress_current=0;
my $progress_max=scalar($options->c_files);

if($options->headers) {
    $progress_max += scalar($options->h_files);

    foreach my $file ($options->h_files) {
	my %functions;
	
	$progress_current++;
	if($options->progress) {
	    $output->progress("$file: file $progress_current of $progress_max");
	}
	
	my $found_function = sub {
	    my $documentation = shift;
	    my $linkage = shift;
	    my $return_type = shift;
	    my $calling_convention = shift;
	    my $internal_name = shift;
	    my $refargument_types = shift;
	    my @argument_types = @$refargument_types;
	    my $refargument_names = shift;
	    my @argument_names = @$refargument_names;
	    my $refargument_documentations = shift;
	    my @argument_documentations = @$refargument_documentations;
	    my $statements = shift;

	    foreach my $winapi (@winapis) {
		my $module = $winapi->function_module($internal_name);
		if(!defined($module)) { next }

		my $external_name = $winapi->function_external_name($internal_name);
		# FIXME: Kludge because of the THUNK variants
		if(!defined($external_name)) {
		    next;
		}

		my $output_function = sub {
		    my $message = shift;

		    $output->write("$file: $module: $return_type ");
		    $output->write("$calling_convention ") if $calling_convention;
		    $output->write("$internal_name(" . join(",", @argument_types) . "): $message\n");
		};
		
		if(!defined($declared_functions{$winapi->name}{$external_name})) {
		    $declared_functions{$winapi->name}{$external_name} = "$file";
		} elsif($options->headers_duplicated) {
		    my $message = "declared more than once";
		    if($file ne $declared_functions{$winapi->name}{$external_name}) {
			$message .= ", first declaration in '" . $declared_functions{$winapi->name}{$external_name} . "'";
		    }
	            &$output_function("$message");
		}

		if($options->headers_misplaced) {
		    if($file =~ /^include\/[^\/]*$/ && $winapi->name eq "win16") {
			&$output_function("declaration misplaced");
		    } elsif($file =~ /^include\/wine\/[^\/]*$/ && $winapi->name eq "win32") {
			&$output_function("declaration misplaced");
		    }
		}
	    }
	};
	
	my $found_preprocessor = sub {
	    my $directive = shift;
	    my $argument = shift;
	};
	
        winapi_parser::parse_c_file $options, $output, $file, $found_function, $found_preprocessor;
    }
}

my %module_pseudo_stub_count16;
my %module_pseudo_stub_count32;

foreach my $file ($options->c_files) {
    my %functions = ();

    my $file_module16 = $modules->allowed_modules_in_file("$current_dir/$file");
    my $file_module32 = $modules->allowed_modules_in_file("$current_dir/$file");

    $progress_current++;
    if($options->progress) {
	$output->progress("$file: file $progress_current of $progress_max");
    }

    my $file_dir = $file;
    if(!($file_dir =~ s/(.*?)\/[^\/]*$/$1/)) {
	$file_dir = ".";
    }
   
    my $file_type = file_type($file);

    if(file_skip($file)) {
	next;
    }

    my $found_function = sub {
	my $documentation = shift;
	my $linkage = shift;
	my $return_type = shift;
	my $calling_convention = shift;
	my $internal_name = shift;
	my $refargument_types = shift;
	my @argument_types = @$refargument_types;
	my $refargument_names = shift;
	my @argument_names = @$refargument_names;
	my $refargument_documentations = shift;
	my @argument_documentations = @$refargument_documentations;
	my $statements = shift;

	my $external_name16 = $win16api->function_external_name($internal_name);
	my $external_name32 = $win32api->function_external_name($internal_name);

	if($options->global) {
	    $win16api->found_type($return_type) if $options->win16;
	    $win32api->found_type($return_type) if $options->win32;
	    for my $argument (@argument_types) {
		$win16api->found_type($argument) if $options->win16;
		$win32api->found_type($argument) if $options->win32;
	    }
	    
	    $win16api->found_function($internal_name) if $options->win16;
	    $win32api->found_function($internal_name) if $options->win32;
	}
	
	if($file_type ne "application") {
	    my $module16 = $win16api->function_module($internal_name);
	    my $module32 = $win32api->function_module($internal_name);

	    if(defined($module16)) {
		$modules->found_module_in_dir($module16, $file_dir);
	    }
	    if(defined($module32)) {
		$modules->found_module_in_dir($module32, $file_dir);
	    }

	    my $previous_function;	    
	    if(defined($functions{$internal_name})) {
		$previous_function = $functions{$internal_name};
	    }

	    my $function = 'winapi_function'->new;
	    $functions{$internal_name} = $function;
	   
 	    $function->documentation($documentation);
	    $function->linkage($linkage);
	    $function->file($file);
	    $function->return_type($return_type); 
	    $function->calling_convention($calling_convention);
	    $function->external_name16($external_name16);
	    $function->external_name32($external_name32);
	    $function->internal_name($internal_name);
	    $function->argument_types([@argument_types]);
	    $function->argument_names([@argument_names]);
 	    $function->argument_documentations([@argument_documentations]);
	    $function->statements($statements);
	    $function->module16($module16);
	    $function->module32($module32);

	    my $prefix = "";
	    $prefix .= "$file: ";
	    if(defined($module16) && !defined($module32)) {
		$prefix .= "$module16: ";
	    } elsif(!defined($module16) && defined($module32)) {
		$prefix .= "$module32: ";
	    } elsif(defined($module16) && defined($module32)) {
		$prefix .= "$module16 & $module32: ";
	    } else {
		$prefix .= "<>: ";
	    }
            $prefix .= "$return_type ";
	    $prefix .= "$calling_convention " if $calling_convention;
	    $prefix .= "$internal_name(" . join(",", @argument_types) . "): ";
	    $output->prefix($prefix);

	    if($options->local && $options->misplaced &&
	       $linkage ne "extern" && $statements) 
	    {
		if($options->win16 && $options->report_module($module16)) {
		    my $match = 0;
		    foreach my $module (split(/ & /, $module16)) {
			foreach my $file_module (split(/ & /, $file_module16)) {
			    if($module eq $file_module) {
				$match++;
			    }
			}
		    }
		    if(!$match && $file ne "library/port.c" && !$nativeapi->is_function($internal_name)) {
			$output->write("is misplaced\n");
		    }
		}

		if($options->win32 && $options->report_module($module32)) {
		    my $match = 0;
		    foreach my $module (split(/ & /, $module32)) {
			foreach my $file_module (split(/ & /, $file_module32)) {
			    if($module eq $file_module) {
				$match++;
			    }
			}
		    }
		    if(!$match && $file ne "library/port.c" && !$nativeapi->is_function($internal_name)) {
			$output->write("is misplaced\n");
		    }
		}
	    }

	    if($options->local && $options->headers && $options->prototype) {
		if($options->win16 && $options->report_module($module16)) {
		    if(!defined($external_name16) || (!$nativeapi->is_function($external_name16) && 
		       !defined($declared_functions{$win16api->name}{$external_name16})))
		    {
			if(!defined($external_name16) || ($external_name16 !~ /^DllEntryPoint$/ &&
			    $internal_name !~ /^I(?:Malloc|Storage)16_fn/ &&
			    $internal_name !~ /^(?:\Q$module16\E|THUNK|WIN16)_\Q$external_name16\E(?:16)?$/))
			{
			    $output->write("no prototype\n");
			}
		    }
		}

		if($options->win32 && $options->report_module($module32)) {
		    if(!defined($external_name32) || (!$nativeapi->is_function($external_name32) && 						          !defined($declared_functions{$win32api->name}{$external_name32})))
		    {
			if(!defined($external_name32) || ($external_name32 !~ /^Dll(?:
			   Install|CanUnloadNow|GetClassObject|GetVersion|
			   RegisterServer|RegisterServerEx|UnregisterServer)|DriverProc$/x && 
			   $internal_name !~ /^COMCTL32_Str/ &&
			   $internal_name !~ /^(?:\Q$module32\E|wine)_(?:\Q$external_name32\E|\d+)$/))
			{
			    $output->write("no prototype\n");
			}
		    }
		}
	    }

	    if($options->local && $options->argument) {
		if($options->win16 && $options->report_module($module16)) {
		  winapi_local::check_function $options, $output,
		    $return_type, $calling_convention, $external_name16, $internal_name, [@argument_types], $nativeapi, $win16api;
		}	
		if($options->win32 && $options->report_module($module32)) {
		  winapi_local::check_function $options, $output,
		    $return_type, $calling_convention, $external_name32, $internal_name, [@argument_types], $nativeapi, $win32api;
		}
	    }

	    if($options->local && $options->statements) {
		if($options->win16 && $options->report_module($module16)) {
		  winapi_local::check_statements $options, $output, $win16api, \%functions, $function;
		}

		if($options->win32 && $options->report_module($module32)) {
		  winapi_local::check_statements $options, $output, $win32api, \%functions, $function;
		}
	    }

	    if($options->stubs) {
		if(defined($statements) && $statements =~ /FIXME[^;]*stub/) {
		    if($options->win16 && $options->report_module($module16)) {
			foreach my $module (split(/ \& /, $module16)) {
			    $module_pseudo_stub_count16{$module}++;
			}
		    }
		    if($options->win32 && $options->report_module($module32)) {
			foreach my $module (split(/ \& /, $module32)) {
			    $module_pseudo_stub_count32{$module}++;
			}
		    }
		}
	    }
	    
	    if($options->local && $options->documentation &&
	       !defined($previous_function) &&
	       (defined($module16) || defined($module32)) &&
	       $linkage ne "extern" && $statements)
	    {
	        winapi_documentation::check_documentation $options, $output, $win16api, $win32api, $function;
	    }
	    $output->prefix("");
	}
    };

    my $config = 0;
    my $conditional = 0;
    my $found_include = sub {
	local $_ = shift;
	if(/^\"config\.h\"/) {
	    $config++;
	}
    };
    my $found_conditional = sub {
	local $_ = shift;

	$nativeapi->found_conditional($_);

	if($options->config) {
	    if($file_type ne "application") {
		if(!$nativeapi->is_conditional($_)) {
		    if(/^HAVE_/ && !/^HAVE_(IPX|MESAGL|BUGGY_MESAGL|WINE_CONSTRUCTOR)$/)
		    {
			$output->write("$file: $_ is not declared as a conditional\n");
		    }
		} else {
		    $conditional++;
		    if(!$config) {
			$output->write("$file: conditional $_ used but config.h is not included\n");
		    }
		}
	    }
	}
    };
    my $preprocessor = 'preprocessor'->new($found_include, $found_conditional);
    my $found_preprocessor = sub {
	my $directive = shift;
	my $argument = shift;

	$preprocessor->directive($directive, $argument);

	if($options->config) {
	    if($directive eq "include") {
		my $header;
		my $check_protection;
		my $check_local;
		if($argument =~ /^<(.*?)>$/) {
		   $header = $1;
		   if($file_type ne "application") {
		       $check_protection = 1;
		   } else {
		       $check_protection = 0;
		   }
		   $check_local = 0;
		} elsif($argument =~ /^"(.*?)"$/) {
		   $header = $1;
		   $check_protection = 0;
		   $check_local = 1;
		}

		if($check_protection) {
		    if((-e "$wine_dir/include/$header" || -e "$file_dir/$header")) {
			if($header !~ /^ctype.h$/) {
			    $output->write("$file: #include \<$header\> is a local include\n");
			}
		    }

		    my $macro = uc($header);
		    $macro =~ y/\.\//__/;
		    $macro = "HAVE_" . $macro;
		    
		    if($nativeapi->is_conditional_header($header)) { 
			if(!$preprocessor->is_def($macro)) {
			    if($macro =~ /^HAVE_X11/) {
				# Do nothing X Windows is handled differently
			    } elsif($macro =~ /^HAVE_(.*?)_H$/) {
				if($header ne "alloca.h" && !$preprocessor->is_def("STATFS_DEFINED_BY_$1")) {
				    $output->write("$file: #$directive $argument: is a conditional include, " . 
						   "but is not protected\n");
				}
			    }
			}
		    } elsif($preprocessor->is_def($macro)) {
			$output->write("$file: #$directive $argument: is protected, " .
				       "but is not a conditional include\n");
		    }
		}

		if($check_local) {
		    if(-e "$file_dir/$header") {
			$includes{"$file_dir/$header"}{used}++;
			foreach my $name (keys(%{$includes{"$file_dir/$header"}{includes}})) {
			    $includes{$name}{used}++;
			}
		    } elsif(-e "$file_dir/../$header") { # FIXME: Kludge
			$includes{"$file_dir/../$header"}{used}++; # FIXME: This is not correct
			foreach my $name (keys(%{$includes{"$file_dir/../$header"}{includes}})) { # FIXME: This is not correct
			    $includes{$name}{used}++;
			}
		    } elsif($header eq "controls.h") { # FIXME: Kludge
			$includes{"dlls/user/$header"}{used}++;
			foreach my $name (keys(%{$includes{"dlls/user/$header"}{includes}})) {
			    $includes{$name}{used}++;
			}
		    } elsif(-e "$wine_dir/include/$header") {
			$includes{"include/$header"}{used}++;
			foreach my $name (keys(%{$includes{"include/$header"}{includes}})) {
			    $includes{$name}{used}++;
			}
		    } else {
			$output->write("$file: #include \"$header\" is not a local include\n");
		    }
		}
	    }
	}
    };
  
    winapi_parser::parse_c_file $options, $output, $file, $found_function, $found_preprocessor;
    
    if($options->config_unnessary) {
	if($config && $conditional == 0) {
	    $output->write("$file: includes config.h but do not use any conditionals\n");
	}
    }

    winapi_local::check_file $options, $output, $file, \%functions;
}

$output->hide_progress;

if($options->global) {
    winapi_documentation::report_documentation $options, $output;

    if($options->stubs) {
	if($options->win16) {
	    my %module_stub_count16;
	    my %module_total_count16;

	    foreach my $name ($win16api->all_functions,$win16api->all_functions_stub) {
		foreach my $module (split(/ \& /, $win16api->function_module($name))) {
		    if($win16api->function_stub($name)) {
			$module_stub_count16{$module}++;
		    }
		    $module_total_count16{$module}++;
		}
	    }

	    foreach my $module ($win16api->all_modules) {
		if($options->report_module($module)) {
		    my $real_stubs = $module_stub_count16{$module};
		    my $pseudo_stubs = $module_pseudo_stub_count16{$module};

		    if(!defined($real_stubs)) { $real_stubs = 0; }
		    if(!defined($pseudo_stubs)) { $pseudo_stubs = 0; }

		    my $stubs = $real_stubs + $pseudo_stubs;
		    my $total = $module_total_count16{$module};

		    if(!defined($total)) { $total = 0;}

		    $output->write("*.c: $module: ");
		    $output->write("$stubs of $total functions are stubs ($real_stubs real, $pseudo_stubs pseudo)\n");
		}
	    }
	}
	
	if($options->win32) {
	    my %module_stub_count32;
	    my %module_total_count32;

	    foreach my $name ($win32api->all_functions,$win32api->all_functions_stub) {
		foreach my $module (split(/ \& /, $win32api->function_module($name))) {
		    if($win32api->function_stub($name)) {
			$module_stub_count32{$module}++;
		    }
		    $module_total_count32{$module}++;
		}
	    }

	    foreach my $module ($win32api->all_modules) {
		if($options->report_module($module)) {
		    my $real_stubs = $module_stub_count32{$module};
		    my $pseudo_stubs = $module_pseudo_stub_count32{$module};

		    if(!defined($real_stubs)) { $real_stubs = 0; }
		    if(!defined($pseudo_stubs)) { $pseudo_stubs = 0; }

		    my $stubs = $real_stubs + $pseudo_stubs;
		    my $total = $module_total_count32{$module};

		    if(!defined($total)) { $total = 0;}

		    $output->write("*.c: $module: ");
		    $output->write("$stubs of $total functions are stubs ($real_stubs real, $pseudo_stubs pseudo)\n");
		}
	    }
	}
    }

    if($options->headers) {
	foreach my $name (sort(keys(%includes))) {
	    if(!$includes{$name}{used}) {
		if($options->include) {
		    $output->write("*.c: $name: include file is never used\n");
		}
	    }
	}
    }

    winapi_global::check $options, $output, $win16api, $nativeapi if $options->win16;
    winapi_global::check $options, $output, $win32api, $nativeapi if $options->win32;

    $modules->global_report;
    $nativeapi->global_report;
}
