# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License.  You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
package ModPerl::WrapXS;

use strict;
use warnings FATAL => 'all';

use constant GvUNIQUE => 0; #$] >= 5.008;
use Apache::TestTrace;
use Apache2::Build ();
use ModPerl::Code ();
use ModPerl::TypeMap ();
use ModPerl::MapUtil qw(function_table xs_glue_dirs);
use File::Path qw(rmtree mkpath);
use Cwd qw(fastcwd);
use Data::Dumper;
use File::Spec::Functions qw(catfile catdir);

our $VERSION = '0.01';

my (@xs_includes) = ('mod_perl.h',
                    map "modperl_xs_$_.h", qw(sv_convert util typedefs));

my @global_structs = qw(perl_module);

my $build = Apache2::Build->build_config;
push @global_structs, 'MP_debug_level' unless Apache2::Build::WIN32;

sub new {
    my $class = shift;

    my $self = bless {
       typemap   => ModPerl::TypeMap->new,
       includes  => \@xs_includes,
       glue_dirs => [xs_glue_dirs()],
    }, $class;

    $self->typemap->get;
    $self;
}

sub typemap  { shift->{typemap} }

sub includes { shift->{includes} }

sub function_list {
    my $self = shift;
    my (@list) = @{ function_table() };

    while (my ($name, $val) = each %{ $self->typemap->function_map }) {
        #entries that do not exist in C::Scan generated tables
        next unless $name =~ /^DEFINE_/;
        push @list, $val;
    }

    return \@list;
}

sub get_functions {
    my $self = shift;
    my $typemap = $self->typemap;

    for my $entry (@{ $self->function_list() }) {
        my $func = $typemap->map_function($entry);
        #print "FAILED to map $entry->{name}\n" unless $func;
        next unless $func;

        my ($name, $module, $class, $args) =
          @{ $func } { qw(perl_name module class args) };

        $self->{XS}->{ $module } ||= [];

        #eg ap_fputs()
        if ($name =~ s/^DEFINE_//) {
            $func->{name} =~ s/^DEFINE_//;

            if (needs_prefix($func->{name})) {
                #e.g. DEFINE_add_output_filter
                $func->{name} = make_prefix($func->{name}, $class);
            }
        }

        my $xs_parms = join ', ',
          map { defined $_->{default} ?
                  "$_->{name}=$_->{default}" : $_->{name} } @$args;

        (my $parms = $xs_parms) =~ s/=[^,]+//g; #strip defaults

        my $proto = join "\n",
          (map "    $_->{type} $_->{name}", @$args), "";

        my ($dispatch, $orig_args) =
          @{ $func } {qw(dispatch orig_args)};

        if ($dispatch =~ /^MPXS_/) {
            $name =~ s/^mpxs_//;
            $name =~ s/^$func->{prefix}//;
            push @{ $self->{newXS}->{ $module } },
              ["$class\::$name", $dispatch];
            next;
        }

        my $passthru = @$args && $args->[0]->{name} eq '...';
        if ($passthru) {
            $parms = '...';
            $proto = '';
        }

        my $return_type =
          $name =~ /^DESTROY$/ ? 'void' : $func->{return_type};

        my $attrs = $self->attrs($name);

        my $code = <<EOF;
$return_type
$name($xs_parms)
$proto
$attrs
EOF

        if ($dispatch || $orig_args || $func->{thx}) {
            my $thx = $func->{thx} ? 'aTHX_ ' : "";

            if ($dispatch) {
                $thx = 'aTHX_ ' if $dispatch =~ /^mpxs_/i;
            }
            else {
                if ($orig_args and @$orig_args == @$args) {
                    #args were reordered
                    $parms = join ', ', @$orig_args;
                }

                $dispatch = $func->{name};
            }

            if ($passthru) {
                $thx ||= 'aTHX_ ';
                $parms = 'items, MARK+1, SP';
            }

            $thx =~ s/_ $// unless $parms;

            my $retval = $return_type eq 'void' ?
              ["", ""] : ["RETVAL = ", "OUTPUT:\n    RETVAL\n"];

            $code .= <<EOF;
    CODE:
    $retval->[0]$dispatch($thx$parms);

    $retval->[1]
EOF
        }

        $func->{code} = $code;
        push @{ $self->{XS}->{ $module } }, $func;
    }
}

sub get_value {
    my $e = shift;
    my $val = 'val';

    if ($e->{class} eq 'PV') {
        if (my $pool = $e->{pool}) {
            $pool .= '(obj)';
            $val = "(SvOK(ST(1)) ?
                    apr_pstrndup($pool, val, val_len) : NULL)"
        }
    }

    return $val;
}

sub get_structures {
    my $self = shift;
    my $typemap = $self->typemap;

    require Apache2::StructureTable;
    for my $entry (@$Apache2::StructureTable) {
        my $struct = $typemap->map_structure($entry);
        next unless $struct;

        my $class = $struct->{class};

        for my $e (@{ $struct->{elts} }) {
            my ($name, $default, $type, $access_mode) =
              @{$e}{qw(name default type access_mode)};

            (my $cast = $type) =~ s/:/_/g;
            my $val = get_value($e);

            my $type_in = $type;
            my $preinit = "/*nada*/";
            if ($e->{class} eq 'PV' and $val ne 'val') {
                $type_in =~ s/char/char_len/;
                $preinit = "STRLEN val_len;";
            }

            my $attrs = $self->attrs($name);

            my $code;
            if ($access_mode eq 'ro') {
                $code = <<EOF;
$type
$name(obj)
    $class obj

$attrs

    CODE:
    RETVAL = ($cast) obj->$name;

    OUTPUT:
    RETVAL

EOF
            }
            elsif ($access_mode eq 'rw' or $access_mode eq 'r+w_startup') {

                my $check_runtime = $access_mode eq 'rw'
                    ? ''
                    : qq[MP_CROAK_IF_THREADS_STARTED("setting $name");];

                $code = <<EOF;
$type
$name(obj, val=$default)
    $class obj
    $type_in val

    PREINIT:
    $preinit
$attrs

    CODE:
    RETVAL = ($cast) obj->$name;

    if (items > 1) {
         $check_runtime
         obj->$name = ($cast) $val;
    }

    OUTPUT:
    RETVAL

EOF
            }
            elsif ($access_mode eq 'r+w_startup_dup') {

                my $convert = $cast !~ /\bchar\b/
                    ? "mp_xs_sv2_$cast"
                    : "SvPV_nolen";

                $code = <<EOF;
$type
$name(obj, val=Nullsv)
    $class obj
    SV *val

    PREINIT:
    $preinit
$attrs

    CODE:
    RETVAL = ($cast) obj->$name;

    if (items > 1) {
         SV *dup = get_sv("_modperl_private::server_rec_$name", TRUE);
         MP_CROAK_IF_THREADS_STARTED("setting $name");
         sv_setsv(dup, val);
         obj->$name = ($cast)$convert(dup);
    }

    OUTPUT:
    RETVAL

EOF
            }
            elsif ($access_mode eq 'rw_char_undef') {
                my $pool = $e->{pool}
                    or die "rw_char_undef accessors need pool";
                $pool .= '(obj)';
# XXX: not sure where val=$default is coming from, but for now use
# hardcoded Nullsv
                $code = <<EOF;
$type
$name(obj, val_sv=Nullsv)
    $class obj
    SV *val_sv

    PREINIT:
$attrs

    CODE:
    RETVAL = ($cast) obj->$name;

    if (val_sv) {
        if (SvOK(val_sv)) {
            STRLEN val_len;
            char *val = (char *)SvPV(val_sv, val_len);
            obj->$name = apr_pstrndup($pool, val, val_len);
        }
        else {
            obj->$name = NULL;
        }
    }

    OUTPUT:
    RETVAL

EOF
            }

            push @{ $self->{XS}->{ $struct->{module} } }, {
               code  => $code,
               class => $class,
               name  => $name,
            };
        }
    }
}

sub prepare {
    my $self = shift;
    $self->{DIR} = 'WrapXS';
    $self->{XS_DIR} = catdir fastcwd(), 'xs';

    my $verbose = Apache::TestTrace::trace_level() eq 'debug' ? 1 : 0;

    if (-e $self->{DIR}) {
        rmtree([$self->{DIR}], $verbose, 1);
    }

    mkpath [$self->{DIR}], $verbose, 0755;
}

sub class_dirname {
    my ($self, $class) = @_;
    my ($base, $sub) = split '::', $class;
    return "$self->{DIR}/$base" unless $sub; #Apache2 | APR
    return $sub if $sub eq $self->{DIR}; #WrapXS
    return "$base/$sub";
}

sub class_dir {
    my ($self, $class) = @_;

    my $dirname = $self->class_dirname($class);
    my $dir = ($dirname =~ m:/: and $dirname !~ m:^$self->{DIR}:) ?
      catdir($self->{DIR}, $dirname) : $dirname;

    unless (-d $dir) {
        mkpath [$dir], 0, 0755;
        debug "mkdir.....$dir";
    }

    $dir;
}

sub class_file {
    my ($self, $class, $file) = @_;
    catfile $self->class_dir($class), $file;
}

sub cname {
    my ($self, $class) = @_;
    $class =~ s/:/_/g;
    $class;
}

sub open_class_file {
    my ($self, $class, $file) = @_;

    if ($file =~ /^\./) {
        my $sub = (split '::', $class)[-1];
        $file = $sub . $file;
    }

    my $name = $self->class_file($class, $file);

    open my $fh, '>', $name or die "open $name: $!";
    debug "writing...$name";

    return $fh;
}

sub module_version {
    local $_ = shift;
    require mod_perl2;
    # XXX: for now APR gets its libapr-0.9 version
    return /^APR/ ? "0.009000" : "$mod_perl2::VERSION";
}

sub write_makefilepl {
    my ($self, $class) = @_;

    my $fh = $self->open_class_file($class, 'Makefile.PL');

    my $includes = $self->includes;
    my $xs = (split '::', $class)[-1] . '.c';
    my $deps = {$xs => ""};

    if (my $mod_h = $self->mod_h($class, 1)) {
        $deps->{$xs} .= " $mod_h";
    }

    local $Data::Dumper::Terse = 1;
    $deps = Dumper $deps;

    my $noedit_warning = $self->ModPerl::Code::noedit_warning_hash();
    require mod_perl2;
    my $version = module_version($class);

    print $fh <<EOF;
$noedit_warning

use lib qw(../../../lib); #for Apache2::BuildConfig
use ModPerl::BuildMM ();

ModPerl::BuildMM::WriteMakefile(
    'NAME'    => '$class',
    'VERSION' => '$version',
    'depend'  => $deps,
);
EOF

    close $fh;
}

sub mod_h {
    my ($self, $module, $complete) = @_;

    my $dirname = $self->class_dirname($module);
    my $cname = $self->cname($module);
    my $mod_h = "$dirname/$cname.h";

    for ($self->{XS_DIR}, @{ $self->{glue_dirs} }) {
        my $file = "$_/$mod_h";
        $mod_h = $file if $complete;
        return $mod_h if -e $file;
    }

    undef;
}

sub mod_pm {
    my ($self, $module, $complete) = @_;

    my $dirname = $self->class_dirname($module);
    my ($base, $sub) = split '::', $module;
    my $mod_pm = "$dirname/${sub}_pm";

    for ($self->{XS_DIR}, @{ $self->{glue_dirs} }) {
        my $file = "$_/$mod_pm";
        $mod_pm = $file if $complete;
        return $mod_pm if -e $file;
    }

    undef;
}

sub class_c_prefix {
    my $class = shift;
    $class =~ s/:/_/g;
    $class;
}

sub class_mpxs_prefix {
    my $class = shift;
    my $class_prefix = class_c_prefix($class);
    "mpxs_${class_prefix}_";
}

sub needs_prefix {
    my $name = shift;
    $name !~ /^(ap|apr|mpxs)_/i;
}

sub make_prefix {
    my ($name, $class) = @_;
    my $class_prefix = class_mpxs_prefix($class);
    return $name if $name =~ /^$class_prefix/;
    $class_prefix . $name;
}

sub isa_str {
    my ($self, $module) = @_;
    my $str = "";

    if (my $isa = $self->typemap->{function_map}->{isa}->{$module}) {
        while (my ($sub, $base) = each %$isa) {
#XXX cannot set isa in the BOOT: section because XSLoader local-ises
#ISA during bootstrap
#            $str .= qq{    av_push(get_av("$sub\::ISA", TRUE),
#                                   newSVpv("$base",0));}
            $str .= qq{\@$sub\::ISA = '$base';\n}
        }
    }

    $str;
}

sub boot {
    my ($self, $module) = @_;
    my $str = "";

    if (my $boot = $self->typemap->{function_map}->{boot}->{$module}) {
        $str = '    mpxs_' . $self->cname($module) . "_BOOT(aTHX);\n";
    }

    $str;
}

my $notshared = join '|', qw(TIEHANDLE); #not sure why yet

sub attrs {
    my ($self, $name) = @_;
    my $str = "";
    return $str if $name =~ /$notshared$/o;
    $str = "    ATTRS: unique\n" if GvUNIQUE;
    $str;
}

sub write_xs {
    my ($self, $module, $functions) = @_;

    my $fh = $self->open_class_file($module, '.xs');
    print $fh $self->ModPerl::Code::noedit_warning_c(), "\n";
    print $fh "\n#define MP_IN_XS\n\n";

    my @includes = @{ $self->includes };

    if (my $mod_h = $self->mod_h($module)) {
        push @includes, $mod_h;
    }

    for (@includes) {
        print $fh qq{\#include "$_"\n\n};
    }

    my $last_prefix = "";

    for my $func (@$functions) {
        my $class = $func->{class};
        my $prefix = $func->{prefix};
        $last_prefix = $prefix if $prefix;

        if ($func->{name} =~ /^mpxs_/) {
            #e.g. mpxs_Apache2__RequestRec_
            my $class_prefix = class_c_prefix($class);
            if ($func->{name} =~ /$class_prefix/) {
                $prefix = class_mpxs_prefix($class);
            }
        }

        $prefix = $prefix ? "  PREFIX = $prefix" : "";
        print $fh "MODULE = $module    PACKAGE = $class $prefix\n\n";

        print $fh $func->{code};
    }

    if (my $destructor = $self->typemap->destructor($last_prefix)) {
        my $arg = $destructor->{argspec}[0];

        print $fh <<EOF;
void
$destructor->{name}($arg)
    $destructor->{class} $arg

EOF
    }

    print $fh "MODULE = $module\n";
    print $fh "PROTOTYPES: disabled\n\n";
    print $fh "BOOT:\n";
    print $fh $self->boot($module);
    print $fh "    items = items; /* -Wall */\n\n";

    if (my $newxs = $self->{newXS}->{$module}) {
        for my $xs (sort { $a->[0] cmp $b->[0] } @$newxs) {
            print $fh qq{   cv = newXS("$xs->[0]", $xs->[1], __FILE__);\n};
            print $fh qq{   GvUNIQUE_on(CvGV(cv));\n} if GvUNIQUE;
        }
    }

    if ($module eq 'APR::Pool' && Apache2::Build::PERL_HAS_ITHREADS) {
        print $fh "    modperl_opt_interp_unselect = APR_RETRIEVE_OPTIONAL_FN(modperl_interp_unselect);\n\n";
        print $fh "    modperl_opt_thx_interp_get  = APR_RETRIEVE_OPTIONAL_FN(modperl_thx_interp_get);\n\n";
    }

    close $fh;
}

sub write_pm {
    my ($self, $module) = @_;

    my $isa = $self->isa_str($module);

    my $code = "";
    if (my $mod_pm = $self->mod_pm($module, 1)) {
        open my $fh, '<', $mod_pm;
        local $/;
        $code = <$fh>;
        close $fh;
    }

    my $base   = (split '::', $module)[0];
    unless (-e "lib/$base/XSLoader.pm") {
        $base = 'Apache2';
    }
    my $loader = join '::', $base, 'XSLoader';

    my $fh = $self->open_class_file($module, '.pm');
    my $noedit_warning = $self->ModPerl::Code::noedit_warning_hash();
    my $use_apr = ($module =~ /^APR::\w+$/) ? 'use APR ();' : '';
    my $version = module_version($module);

    print $fh <<EOF;
$noedit_warning

package $module;

use strict;
use warnings FATAL => 'all';

$isa
$use_apr
use $loader ();
our \$VERSION = '$version';
$loader\::load __PACKAGE__;

$code

1;
__END__
EOF
}

my %typemap = (
    'Apache2::RequestRec' => 'T_APACHEOBJ',
    'apr_time_t'         => 'T_APR_TIME',
    'APR::Table'         => 'T_HASHOBJ',
    'APR::Pool'          => 'T_POOLOBJ',
    'apr_size_t *'       => 'T_UVPTR',
);

sub write_typemap {
    my $self = shift;
    my $typemap = $self->typemap;
    my $map = $typemap->get;
    my %seen;

    my $fh = $self->open_class_file('ModPerl::WrapXS', 'typemap');
    print $fh $self->ModPerl::Code::noedit_warning_hash(), "\n";

    my %entries = ();
    my $max_key_len = 0;
    while (my ($type, $class) = each %$map) {
        $class ||= $type;
        next if $seen{$type}++ || $typemap->special($class);

        if ($class =~ /::/) {
            $entries{$class} = $typemap{$class} || 'T_PTROBJ';
            $max_key_len = length $class if length $class > $max_key_len;
        }
        else {
            $entries{$type} = $typemap{$type} || "T_$class";
            $max_key_len = length $type if length $type > $max_key_len;
        }
    }

    for (sort keys %entries) {
        printf $fh "%-${max_key_len}s %s\n", $_, $entries{$_};
    }

    close $fh;
}

sub write_typemap_h_file {
    my ($self, $method) = @_;

    $method = $method . '_code';
    my ($h, $code) = $self->typemap->$method();
    my $file = catfile $self->{XS_DIR}, $h;

    open my $fh, '>', $file or die "open $file: $!";
    print $fh $self->ModPerl::Code::noedit_warning_c(), "\n";
    print $fh $code;
    close $fh;
}

sub write_lookup_method_file {
    my $self = shift;

    my %map = ();
    while (my ($module, $functions) = each %{ $self->{XS} }) {
        my $last_prefix = "";
        for my $func (@$functions) {
            my $class = $func->{class};
            my $prefix = $func->{prefix};
            $last_prefix = $prefix if $prefix;

            my $name = $func->{perl_name} || $func->{name};
            $name =~ s/^DEFINE_//;

            if ($name =~ /^mpxs_/) {
                #e.g. mpxs_Apache2__RequestRec_
                my $class_prefix = class_c_prefix($class);
                if ($name =~ /$class_prefix/) {
                    $prefix = class_mpxs_prefix($class);
                }
            }
            elsif ($name =~ /^ap_sub_req/) {
                $prefix = 'ap_sub_req_';
            }

            $name =~ s/^$prefix// if $prefix;

            push @{ $map{$name} }, [$module, $class];
        }

        # pure XS wrappers don't have the information about the
        # arguments they receive, since they manipulate the arguments
        # stack directly. therefore for these methods we can't tell
        # what are the objects they are invoked on
        for my $xs (@{ $self->{newXS}->{$module} || []}) {
            push @{ $map{$1} }, [$module, undef] if $xs->[0] =~ /.+::(.+)/;
        }
    }

    local $Data::Dumper::Terse    = 1;
    local $Data::Dumper::Sortkeys = 1;
    $Data::Dumper::Terse    = $Data::Dumper::Terse;    # warn
    $Data::Dumper::Sortkeys = $Data::Dumper::Sortkeys; # warn
    my $methods = Dumper(\%map);
    $methods =~ s/\n$//;

    my $package = "ModPerl::MethodLookup";
    my $file = catfile "lib", "ModPerl", "MethodLookup.pm";
    debug "creating $file";
    open my $fh, ">$file" or die "Can't open $file: $!";

    my $noedit_warning = $self->ModPerl::Code::noedit_warning_hash();

    print $fh <<EOF;
$noedit_warning
package $package;

use strict;
use warnings;

my \$methods = $methods;

EOF

    print $fh <<'EOF';

use base qw(Exporter);
use mod_perl2;

our @EXPORT = qw(print_method print_module print_object);
our $VERSION = $mod_perl2::VERSION;
use constant MODULE => 0;
use constant OBJECT  => 1;

my $modules;
my $objects;

sub _get_modules {
    for my $method (sort keys %$methods) {
        for my $item ( @{ $methods->{$method} }) {
            push @{ $modules->{$item->[MODULE]} }, [$method, $item->[OBJECT]];
        }
    }
}

sub _get_objects {
    for my $method (sort keys %$methods) {
        for my $item ( @{ $methods->{$method} }) {
            next unless defined $item->[OBJECT];
            push @{ $objects->{$item->[OBJECT]} }, [$method, $item->[MODULE]];
        }
    }
}

# if there is only one replacement method in 2.0 API we can
# automatically lookup it, up however if there are more than one
# (e.g. new()), we need to use a fully qualified value here
# of course the same if the package is not a mod_perl one.
#
# the first field represents the replacement method or undef if none
# exists, the second field is for extra comments (e.g. when there is
# no replacement method)
my $methods_compat = {
    # Apache2::
    gensym            => ['Symbol::gensym',
                          'or use "open my $fh, $file"'],
    module            => ['Apache2::Module::loaded',
                          ''],
    define            => ['exists_config_define',
                          ''],
    httpd_conf        => ['add_config',
                          ''],
    SERVER_VERSION    => ['get_server_version',
                          ''],
    can_stack_handlers=> [undef,
                          'there is no more need for that method in mp2'],

    # Apache2::RequestRec
    soft_timeout      => [undef,
                          'there is no more need for that method in mp2'],
    hard_timeout      => [undef,
                          'there is no more need for that method in mp2'],
    kill_timeout      => [undef,
                          'there is no more need for that method in mp2'],
    reset_timeout     => [undef,
                          'there is no more need for that method in mp2'],
    cleanup_for_exec  => [undef,
                          'there is no more need for that method in mp2'],
    send_http_header  => ['content_type',
                          ''],
    header_in         => ['headers_in',
                          'this method works in mod_perl 1.0 too'],
    header_out        => ['headers_out',
                          'this method works in mod_perl 1.0 too'],
    err_header_out    => ['err_headers_out',
                          'this method works in mod_perl 1.0 too'],
    register_cleanup  => ['cleanup_register',
                          ''],
    post_connection   => ['cleanup_register',
                          ''],
    content           => [undef, # XXX: Apache2::Request::what?
                          'use CGI.pm or Apache2::Request instead'],
    clear_rgy_endav   => ['special_list_clear',
                          ''],
    stash_rgy_endav   => [undef,
                          ''],
    run_rgy_endav     => ['special_list_call',
                          'this method is no longer needed'],
    seqno             => [undef,
                          'internal to mod_perl 1.0'],
    chdir_file        => [undef, # XXX: to be resolved
                          'temporary unavailable till the issue with chdir' .
                          ' in the threaded env is resolved'],
    log_reason        => ['log_error',
                          'not in the Apache 2.0 API'],
    READLINE          => [undef, # XXX: to be resolved
                          ''],
    send_fd_length    => [undef,
                          'not in the Apache 2.0 API'],
    send_fd           => ['sendfile',
                          'requires an offset argument'],
    is_main           => ['main',
                          'not in the Apache 2.0 API'],
    cgi_var           => ['subprocess_env',
                          'subprocess_env can be used with mod_perl 1.0'],
    cgi_env           => ['subprocess_env',
                          'subprocess_env can be used with mod_perl 1.0'],
    each_byterange    => [undef,
                          'now handled internally by ap_byterange_filter'],
    set_byterange     => [undef,
                          'now handled internally by ap_byterange_filter'],

    # Apache::File
    open              => [undef,
                          ''],
    close             => [undef, # XXX: also defined in APR::Socket
                          ''],
    tmpfile           => [undef,
                          'not in the Apache 2.0 API, ' .
                          'use File::Temp instead'],

    # Apache::Util
    size_string       => ['format_size',
                          ''],
    escape_uri        => ['unescape_path',
                          ''],
    escape_url        => ['escape_path',
                          'and requires a pool object'],
    unescape_uri      => ['unescape_url',
                          ''],
    unescape_url_info => [undef,
                          'use CGI::Util::unescape() instead'],
    escape_html       => [undef, # XXX: will be ap_escape_html
                          'ap_escape_html now requires a pool object'],
    parsedate         => ['parse_http',
                          ''],
    validate_password => ['password_validate',
                          ''],

    # Apache::Table
    #new               => ['make',
    #                      ''], # XXX: there are other 'new' methods

    # Apache::Connection
    auth_type         => ['ap_auth_type',
                          'now resides in the request object'],
};

sub avail_methods_compat {
    return keys %$methods_compat;
}

sub avail_methods {
    return keys %$methods;
}

sub avail_modules {
    my %modules = ();
    for my $method (keys %$methods) {
        for my $item ( @{ $methods->{$method} }) {
            $modules{$item->[MODULE]}++;
        }
    }
    return keys %modules;
}

sub preload_all_modules {
    _get_modules() unless $modules;
    eval "require $_" for keys %$modules;
}

sub _print_func {
    my $func = shift;
    my @args = @_ ? @_ : @ARGV;
    no strict 'refs';
    print( ($func->($_))[0]) for @args;
}

sub print_module { _print_func('lookup_module', @_) }
sub print_object { _print_func('lookup_object', @_) }

sub print_method {
    my @args = @_ ? @_ : @ARGV;
    while (@args) {
         my $method = shift @args;
         my $object = (@args &&
             (ref($args[0]) || $args[0] =~ /^(Apache2|ModPerl|APR)/))
             ? shift @args
             : undef;
         print( (lookup_method($method, $object))[0]);
    }
}

sub sep { return '-' x (shift() + 20) . "\n" }

# what modules contain the passed method.
# an optional object or a reference to it can be passed to help
# resolve situations where there is more than one module containing
# the same method. Inheritance is supported.
sub lookup_method {
    my ($method, $object) = @_;

    unless (defined $method) {
        my $hint = "No 'method' argument was passed\n";
        return ($hint);
    }

    # strip the package name for the fully qualified method
    $method =~ s/.+:://;

    if (exists $methods_compat->{$method}) {
        my ($replacement, $comment) = @{$methods_compat->{$method}};
        my $hint = "'$method' is not a part of the mod_perl 2.0 API\n";
        $comment = length $comment ? " $comment\n" : "";

        # some removed methods have no replacement
        return $hint . "$comment" unless defined $replacement;

        $hint .= "use '$replacement' instead. $comment";

        # if fully qualified don't look up its container
        return $hint if $replacement =~ /::/;

        my ($modules_hint, @modules) = lookup_method($replacement, $object);
        return $hint . $modules_hint;
    }
    elsif (!exists $methods->{$method}) {
        my $hint = "Don't know anything about method '$method'\n";
        return ($hint);
    }

    my @items = @{ $methods->{$method} };
    if (@items == 1) {
        my $module = $items[0]->[MODULE];
        my $hint = "To use method '$method' add:\n" . "\tuse $module ();\n";
        # we should really check that the method matches the object if
        # any was passed, but it may not always work
        return ($hint, $module);
    }
    else {
        if (defined $object) {
            my $class = ref $object || $object;
            for my $item (@items) {
                # real class or inheritance
                if ($class eq $item->[OBJECT] or
                    (ref($object) && $object->isa($item->[OBJECT]))) {
                    my $module = $item->[MODULE];
                    my $hint = "To use method '$method' add:\n" .
                        "\tuse $module ();\n";
                    return ($hint, $module);
                }
            }
            # fall-through
            local $" = ", ";
            my @modules = map $_->[MODULE], @items;
            my $hint = "Several modules (@modules) contain method '$method' " .
                "but none of them matches class '$class';\n";
            return ($hint);

        }
        else {
            my %modules = map { $_->[MODULE] => 1 } @items;
            # remove dups if any (e.g. $s->add_input_filter and
            # $r->add_input_filter are loaded by the same Apache2::Filter)
            my @modules = keys %modules;
            my $hint;
            if (@modules == 1) {
                $hint = "To use method '$method' add:\n\tuse $modules[0] ();\n";
                return ($hint, $modules[0]);
            }
            else {
                $hint = "There is more than one class with method '$method'\n" .
                    "try one of:\n" . join '', map {"\tuse $_ ();\n"} @modules;
                return ($hint, @modules);
            }
        }
    }
}

# what methods are contained in the passed module name
sub lookup_module {
    my ($module) = shift;

    unless (defined $module) {
        my $hint = "no 'module' argument was passed\n";
        return ($hint);
    }

    _get_modules() unless $modules;

    unless (exists $modules->{$module}) {
        my $hint = "don't know anything about module '$module'\n";
        return ($hint);
    }

    my @methods;
    my $max_len = 6;
    for ( @{ $modules->{$module} } ) {
        $max_len = length $_->[0] if length $_->[0] > $max_len;
        push @methods, $_->[0];
    }

    my $format = "%-${max_len}s %s\n";
    my $banner = sprintf($format, "Method", "Invoked on object type");
    my $hint = join '',
        ("\nModule '$module' contains the following XS methods:\n\n",
         $banner,  sep(length($banner)),
         map( { sprintf $format, $_->[0], $_->[1]||'???'}
             @{ $modules->{$module} }),
         sep(length($banner)));

    return ($hint, @methods);
}

# what methods can be invoked on the passed object (or its reference)
sub lookup_object {
    my ($object) = shift;

    unless (defined $object) {
        my $hint = "no 'object' argument was passed\n";
        return ($hint);
    }

    _get_objects() unless $objects;

    # a real object was passed?
    $object = ref $object || $object;

    unless (exists $objects->{$object}) {
        my $hint = "don't know anything about objects of type '$object'\n";
        return ($hint);
    }

    my @methods;
    my $max_len = 6;
    for ( @{ $objects->{$object} } ) {
        $max_len = length $_->[0] if length $_->[0] > $max_len;
        push @methods, $_->[0];
    }

    my $format = "%-${max_len}s %s\n";
    my $banner = sprintf($format, "Method", "Module");
    my $hint = join '',
        ("\nObjects of type '$object' can invoke the following XS methods:\n\n",
         $banner, sep(length($banner)),
         map({ sprintf $format, $_->[0], $_->[1]} @{ $objects->{$object} }),
         sep(length($banner)));

    return ($hint, @methods);

}

1;
EOF
    close $fh;
}

sub write_module_versions_file {
    my $self = shift;

    my $file = catfile "lib", "ModPerl", "DummyVersions.pm";
    debug "creating $file";
    open my $fh, ">$file" or die "Can't open $file: $!";

    my $noedit_warning = $self->ModPerl::Code::noedit_warning_hash();
    print $fh "$noedit_warning\n";

    my @modules = keys %{ $self->{XS} };
    push @modules, qw(ModPerl::MethodLookup);

    my $len = 0;
    for (@modules) {
        $len = length $_ if length $_ > $len;
    }

    require mod_perl2;
    $len += length '$::VERSION';
    for (@modules) {
        my $ver = module_version($_);
        printf $fh "package %s;\n%-${len}s = %s;\n\n",
            $_, '$'.$_."::VERSION", $ver;
    }
}

sub generate {
    my $self = shift;

    $self->prepare;

    for (qw(ModPerl::WrapXS Apache2 APR ModPerl)) {
        $self->write_makefilepl($_);
    }

    $self->write_typemap;

    for (qw(typedefs sv_convert)) {
        $self->write_typemap_h_file($_);
    }

    $self->get_functions;
    $self->get_structures;
    $self->write_export_file('exp') if Apache2::Build::AIX;
    $self->write_export_file('def') if Apache2::Build::WIN32;

    while (my ($module, $functions) = each %{ $self->{XS} }) {
#        my ($root, $sub) = split '::', $module;
#        if (-e "$self->{XS_DIR}/$root/$sub/$sub.xs") {
#            $module = join '::', $root, "Wrap$sub";
#        }
        $self->write_makefilepl($module);
        $self->write_xs($module, $functions);
        $self->write_pm($module);
    }

    $self->write_lookup_method_file;
    $self->write_module_versions_file;
}

#three .sym files are generated:
#global   - global symbols
#ithreads - #ifdef USE_ITHREADS functions
#inline   - __inline__ functions
#the inline symbols are needed #ifdef MP_DEBUG
#since __inline__ will be turned off

my %multi_export = map { $_, 1 } qw(exp);

sub open_export_files {
    my ($self, $name, $ext) = @_;

    my $dir = $self->{XS_DIR};
    my %handles;
    my @types = qw(global inline ithreads);

    if ($multi_export{$ext}) {
        #write to multiple files
        for my $type (@types) {
            my $file = "$dir/${name}_$type.$ext";

            open my $fh, '>', $file or
              die "open $file: $!";

            $handles{$type} = $fh;
        }
    }
    else {
        #write to one file
        my $file = "$dir/$name.$ext";

        open my $fh, '>', $file or
          die "open $file: $!";

        for my $type (@types) {
            $handles{$type} = $fh;
        }
    }

    \%handles;
}

sub func_is_static {
    my ($self, $entry) = @_;
    if (my $attr = $entry->{attr}) {
        return 1 if grep { $_ eq 'static' } @$attr;
    }

    #C::Scan doesnt always pickup static __inline__
    return 1 if $entry->{name} =~ /^mpxs_/o;

    return 0;
}

sub func_is_inline {
    my ($self, $entry) = @_;
    if (my $attr = $entry->{attr}) {
        return 1 if grep { $_ eq '__inline__' } @$attr;
    }
    return 0;
}

sub export_file_header_exp {
    my $self = shift;
    "#!\n";
}

sub export_file_format_exp {
    my ($self, $val) = @_;
    "$val\n";
}

sub export_file_header_def {
    my $self = shift;
    "LIBRARY\n\nEXPORTS\n\n";
}

sub export_file_format_def {
    my ($self, $val) = @_;
    "   $val\n";
}

my $ithreads_exports = join '|', qw{
modperl_cmd_interp_
modperl_interp_
modperl_list_
modperl_tipool_
modperl_svptr_table_clone$
modperl_mgv_require_module$
};

sub export_func_handle {
    my ($self, $entry, $handles) = @_;

    if ($self->func_is_inline($entry)) {
        return $handles->{inline};
    }
    elsif ($entry->{name} =~ /^($ithreads_exports)/) {
        return $handles->{ithreads};
    }

    $handles->{global};
}

sub write_export_file {
    my ($self, $ext) = @_;

    my %files = (
        modperl => $ModPerl::FunctionTable,
        apache2 => $Apache2::FunctionTable,
        apr     => $APR::FunctionTable,
    );

    my $header = \&{"export_file_header_$ext"};
    my $format = \&{"export_file_format_$ext"};

    while (my ($key, $table) = each %files) {
        my $handles = $self->open_export_files($key, $ext);

        my %seen; #only write header once if this is a single file
        for my $fh (values %$handles) {
            next if $seen{$fh}++;
            print $fh $self->$header();
        }

        # add the symbols which aren't the function table
        if ($key eq 'modperl') {
            my $fh = $handles->{global};
            for my $name (@global_structs) {
                print $fh $self->$format($name);
            }
        }

        for my $entry (@$table) {
            next if $self->func_is_static($entry);
            my $name = $entry->{name};

            my $fh = $self->export_func_handle($entry, $handles);

            print $fh $self->$format($name);
        }

        %seen = (); #only close handle once if this is a single file
        for my $fh (values %$handles) {
            next if $seen{$fh}++;
            close $fh;
        }
    }
}

sub stats {
    my $self = shift;

    $self->get_functions;
    $self->get_structures;

    my %stats;

    while (my ($module, $functions) = each %{ $self->{XS} }) {
        $stats{$module} += @$functions;
        if (my $newxs = $self->{newXS}->{$module}) {
            $stats{$module} += @$newxs;
        }
    }

    return \%stats;
}

sub generate_exports {
    my ($self, $fh) = @_;

    if (!$build->should_build_apache) {
        print $fh <<"EOF";
/* This is intentionnaly left blank, only usefull for static build */
const void *modperl_ugly_hack = NULL;
EOF
        return;
    }

    print $fh <<"EOF";
/*
 * This is indeed a ugly hack!
 * See also src/modules/perl/mod_perl.c for modperl_ugly_hack
 * If we don't build such a list of exported API functions, the over-zealous
 * linker can and will remove the unused functions completely. In order to
 * avoid this, we create this object and modperl_ugly_hack to create a
 * dependency between all the exported API and mod_perl.c
 */
const void *modperl_ugly_hack = NULL;
EOF

    for my $entry (@$ModPerl::FunctionTable) {
        next if $self->func_is_static($entry);
        unless (Apache2::Build::PERL_HAS_ITHREADS) {
            next if $entry->{name} =~ /^($ithreads_exports)/;
        }
        ( my $name ) = $entry->{name} =~ /^modperl_(.*)/;
        print $fh <<"EOF";
#ifndef modperl_$name
const void *modperl_hack_$name = (const void *)modperl_$name;
#endif

EOF
    }
}

1;
__END__
