package Enbugger;

# COPYRIGHT AND LICENCE
#
# Copyright (C) 2007,2008,2009 WhitePages.com, Inc. with primary
# development by Joshua ben Jore.
#
# This program is distributed WITHOUT ANY WARRANTY, including but not
# limited to the implied warranties of merchantability or fitness for
# a particular purpose.
#
# The program is free software.  You may distribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation (either version 2 or any later version)
# and the Perl Artistic License as published by O’Reilly Media, Inc.
# Please open the files named gpl-2.0.txt and Artistic for a copy of
# these licenses.

BEGIN {
    $VERSION = '2.016';
}

use XSLoader ();

BEGIN {
    XSLoader::load( 'Enbugger', $VERSION );


    # Provide minimal debugger hooks.
    #
    # When perl has debugging enabled, it always calls these functions
    # at hook points. It dies if they're missing. These stub functions
    # don't do anything except provide something that will keep perl
    # from dying from lack of hooks.
    {

        # Generate needed code for stubs.
        my $src = "package DB;\n";
        my $need_stubs;
        for my $sub (qw( DB sub )) {
            my $globref = $DB::{$sub};

            # Don't try replacing an existing function.
            if ( $globref and defined &$globref ) {
            }
            else {
                # Generate a stub method.
                $src .= "sub $sub {};\n";
                $need_stubs = 1;
            }
        }

        # Create stubs.
        if ( $need_stubs ) {
            $src .= "return 1;\n";
            my $ok = eval $src;
            die $@ unless $ok;
        }
    }


    # Compile and load everything following w/ debugger hooks.
    #
    # That is, everything I'm asking to compile now could possibly be
    # debugged if we do the loading. Most of everything else in the
    # Enbugger namespace is explicitly removed from the debugger by
    # making sure it's COP nodes are compiled with "nextstate" instead
    # of "dbstate" hooks.
    Enbugger->_compile_with_dbstate();
}


# I don't know the real minimum version. I've gotten failure
# reports from 5.5 that show it's missing the COP opcodes I'm
# altering.
use 5.006_000;

use strict;

use B::Utils ();
use Carp ();
use Scalar::Util ();

# Public class settings.
use vars qw( $DefaultDebugger %DBsub );

use constant (); # just to load it.

BEGIN {
    # Compile all of Enbugger:: w/o debugger hooks.
    Enbugger->_compile_with_nextstate();
}

our( $DEBUGGER, $DEBUGGER_CLASS, %REGISTERED_DEBUGGERS );




######################################################################
# Public API

BEGIN {
    my $src = "no warnings 'redefine';\n";
    for my $sub (qw( stop write )) {
        $src .= <<"SRC";
#line @{[__LINE__+1]} "@{[__FILE__]}"
            sub $sub {
                my ( \$class ) = \@_;

                # Fetch and install the real implementation.
                my \$debuggerSubClass = \$class->DEBUGGER_CLASS;

                *Enbugger::$sub = \$debuggerSubClass->can('_${sub}');

                # Redispatch to the implementation.
                goto &Enbugger::$sub;
            };
SRC
    }

    $src .= "return 1;\n";
    my $ok = eval $src;
    die $@ unless $ok;
}





BEGIN { $DefaultDebugger = 'perl5db' }

sub DEBUGGER_CLASS () {
    unless ( defined $DEBUGGER_CLASS ) {
        Enbugger->load_debugger;
    }

    # Install a replacement method that doesn't know how to load
    # debuggers.
    #
    # There's no need to always have a 100% capable function around
    # once there's no possibility for change.
    my $ok = eval <<"DEBUGGER_CLASS";
#line @{[__LINE__]} "@{[__FILE__]}"
        no warnings 'redefine';
        sub DEBUGGER_CLASS () {
            "\Q$DEBUGGER_CLASS\E"
        }
        return 1;
DEBUGGER_CLASS

    die $@ unless $ok;

    goto &Enbugger::DEBUGGER_CLASS;
}









sub _stop;
sub _write;
sub _load_debugger;






BEGIN {
    # There is an automatically registered "null" debugger which is
    # really just a known empty thing that exists only so I can match
    # against it and thereby know it can be replaced.
    $REGISTERED_DEBUGGERS{''} = {
                                null    => 1,
                                symbols => [qw[ sub DB ]],
                               };
}

sub load_debugger {
    my ( $class, $requested_debugger ) = @_;

    # Choose a debugger to load if none was specified.
    if ( not defined $requested_debugger ) {

        # Don't bother if we've already loaded a debugger.
        return if $DEBUGGER;

        # Choose the default.
        $requested_debugger = $DefaultDebugger;
    }

    # Don't load a debugger if there is one loaded already.
    #
    # Enbugger already populates %DB:: with &DB and &sub so I'll check
    # for something that I didn't create.
    my %debugger_symbols =
      map {; $_ => 0b01 }
        keys %DB::;


    # Compare all registered debuggers to our process.
    my %debugger_matches;
    for my $debugger ( keys %REGISTERED_DEBUGGERS ) {
        
        # Find the intersection vs the difference.
        my $intersection = 0;
        my %match = %debugger_symbols;
        for my $symbol ( @{$REGISTERED_DEBUGGERS{$debugger}{symbols}} ) {
            if ( ( $match{$symbol} |= 0b10 ) == 0b11 ) {
                ++ $intersection;
            }
        }
        
        # Score.
        my $difference =
          keys(%match) - $intersection;
        my $score = $difference / $intersection;
        
        $debugger_matches{$debugger} = $score;
    }

    # Select the best matching debugger.
    my ( $best_debugger ) =
      sort { $debugger_matches{$a} <=> $debugger_matches{$b} }
        keys %debugger_matches;
    
    
    # It is ok to replace the null debugger but an error to replace
    # anything else. Also, there's nothing to do if we've already
    # loaded the requested debugger.
    if ( $REGISTERED_DEBUGGERS{$best_debugger}{null} ) {
    }
    elsif ( $best_debugger eq $requested_debugger ) {
        return;
    }
    else {
        Carp::confess("Can't replace the existing $best_debugger debugger with $requested_debugger");
    }


    # Debugger's name -> Debugger's class.
    $DEBUGGER = $requested_debugger;
    $DEBUGGER_CLASS = "${class}::$DEBUGGER";

    # Debugger's class -> Debugger's .pm file.
    my $debugger_class_file = $DEBUGGER_CLASS;
    $debugger_class_file =~ s#::#/#g;
    $debugger_class_file .= '.pm';

    # Load the file.
    #
    # Be darn sure we're compiling COP nodes with pp_nextstate
    # instead of pp_dbstate. It sucks to start debugging your
    # debugger by accident. Incidentally... this is a great place
    # to hack if you /do/ want to make debugging a debugger a
    # possibility.
    #
    # Further, note that some debugger supports have already been loaded 
    # by __PACKAGE__->register_debugger(...) below. In general, this
    # is for things I've needed to use myself.
    Enbugger->_compile_with_nextstate();
    require $debugger_class_file;
    $DEBUGGER_CLASS->_load_debugger;
    $DEBUGGER_CLASS->instrument_runtime;


    # Subsequent compilation will use pp_dbstate like expected.
    $DEBUGGER_CLASS->_instrumented_ppaddr();

    return;
}



sub _uninstrumented_ppaddr { $_[0]->_compile_with_nextstate() }
sub _instrumented_ppaddr   { $_[0]->_compile_with_dbstate()   }






sub _load_debugger;





sub register_debugger {
    my ( $class, $debugger ) = @_;
    
    # name -> class
    my $enbugger_subclass = "Enbugger::$debugger";

    # class -> module file
    my $enbugger_subclass_file = $enbugger_subclass;
    $enbugger_subclass_file =~ s<::></>g;
    $enbugger_subclass_file .= '.pm';

    # Load it. *Assume* PL_ppaddr[OP_NEXTSTATE] is something
    # useful like Perl_pp_nextstate still.
    #
    # TODO: localize PL_ppaddr[OP_NEXTSTATE] during this compilation to 
    # be Perl_pp_nextstate.
    require $enbugger_subclass_file;


    my $src = <<"REGISTER_DEBUGGER";
#line @{[__LINE__]} "@{[__FILE__]}"
        sub load_$debugger {
            my ( \$class ) = \@_;
            \$class->load_debugger( '$debugger' );
            return;
        };
REGISTER_DEBUGGER

    $src .= "return 1;\n";
    my $ok = eval $src;
    die $@ unless $ok;
}





sub load_source {
    my ( $class ) = @_;

    # Load the original program.
    $class->load_file($0);

    # Load all modules.
    for ( grep { defined and -e } values %INC ) {
        $class->load_file($_);
    }

    $class->initialize_dbline;

    return;
}


sub initialize_dbline {
     my $file;
     for ( my $cx = 1; my ( $package, $c_file ) = caller $cx; ++ $cx ) {
         if ( $package !~ /^Enbugger/ ) {
             $file = $c_file;
             last;
         }
     }

     if ( not defined $file ) {
         *DB::dbline = [];
         *DB::dbline = {};
         Enbugger::set_magic_dbfile( \%DB::dbline );
     }
     else {
         no strict 'refs';
         *DB::dbline = \*{"main::_<$file"};
     }
}




sub load_file {
    my ($class, $file) = @_;
    
    # The symbols by which we'll know ye.
    my $base_symname = "_<$file";
    my $symname          = "main::$base_symname";
    
    no strict 'refs';

    my $glob = \*$symname;

    if ( ! *$symname{ARRAY} && -f $file ) {
        # Read the source.
        # Open the file.
        my $fh;
        if ( not open $fh, '<', $file ) {
            Carp::croak( "Can't open $file for reading: $!" );
        }
        
        # Load our source code. All source must be installed as at least PVIV or
        # some asserts in op.c may fail. Later, I'll assign better pointers to each
        # line in instrument_op.
        local $/ = "\n";
        *$glob = [
            map { Scalar::Util::dualvar( 0, $_ ) }
            ( "BEGIN { require 'perl5db.pl' } # Generated by " . __FILE__,
	      readline $fh )
        ];
    }

    if ( ! *$glob{HASH} ) {
        my %breakpoints;
        Enbugger::set_magic_dbfile(\%breakpoints);
        *$glob = \%breakpoints;
    }
    
    $$symname ||= $file;
    
    return;
}







sub instrument_runtime {
    # Now do the *real* work.
    my ( $class ) = @_;

    # PL_DBgv = gv_fetchpvs("DB::DB", GV_ADDMULTI, SVt_PVGV);
    eval 'sub DB::DB {}' if ! defined &DB::DB;

    # Load the source code for all loaded files. Too bad about (eval 1)
    # though. This doesn't work. Why not!?!
    $class->load_source;

    # PL_DBsingle = GvSV((gv_fetchpvs("DB::single", GV_ADDMULTI, SVt_PV)));
    # if (!SvIOK(PL_DBsingle))
    #     sv_setiv(PL_DBsingle, 0);
    $DB::single = 0 if ! defined $DB::single;

    # PL_DBtrace = GvSV((gv_fetchpvs("DB::trace", GV_ADDMULTI, SVt_PV)));
    # if (!SvIOK(PL_DBtrace))
    #     sv_setiv(PL_DBtrace, 0);
    $DB::trace = 0 if ! defined $DB::trace;

    # PL_DBsignal = GvSV((gv_fetchpvs("DB::signal", GV_ADDMULTI, SVt_PV)));
    # if (!SvIOK(PL_DBsignal))
    #     sv_setiv(PL_DBsignal, 0);
    $DB::signal = 0 if ! defined $DB::signal;

    # Walk over all optrees.
    # * Transform nextstate COP* nodes to dbstate COP* nodes as appropriate
    # * Set ${"main::_<$file"}[X] array elements with COP* pointers
    # * Capture function name start/end line numbers
    B::Utils::walkallops_simple( \ &Enbugger::instrument_op );

    # Provide %DB::sub.
    %DB::sub =
        map { $_ => sprintf '%s:%d-%d', @{$DBsub{$_}} }
        keys %DBsub;
    undef %DBsub;
}





sub instrument_op {
    my ( $op ) = @_;

    # Must be a B::COP node.
    if ( $$op and B::class( $op ) eq 'COP' ) {

        # @{"_<$file"} entries where there are COP entries are
        # dualvars of pointers to the COP nodes that will get
        # OPf_SPECIAL toggled to indicate breakpoints.
	my $ptr  = $$op;
	my $source = do {
	    no strict 'refs';
	    \ @{"main::_<$B::Utils::file"};
	};
	if ( $ptr ) {
	    $source->[$B::Utils::line] = Scalar::Util::dualvar( $ptr, $source->[$B::Utils::line] );
	}

	if ($DBsub{$B::Utils::sub}) {
	    $DBsub{$B::Utils::sub}[1] = $B::Utils::line if $B::Utils::line < $DBsub{$B::Utils::sub}[1];
	    $DBsub{$B::Utils::sub}[2] = $B::Utils::line if $B::Utils::line > $DBsub{$B::Utils::sub}[2];
	}
	else {
	    $DBsub{$B::Utils::sub} = [ $B::Utils::file, ($B::Utils::line) x 2 ];
	}

        #print $op->file ."\t".$op->line."\t".$o->stash->NAME."\t";
        # Disable or enable debugging for this opcode.
        if ( $op->stash->NAME =~ /^(?=[DE])(?:DB|Enbugger)(?:::|\z)/ ) {
            #print 'next';
            Enbugger::_nextstate_cop( $op );
        }
        else {
            Enbugger::_dbstate_cop( $op );
        }
    }
}





sub import {
    my $class = shift @_;

    if ( @_ ) {
        my $selected_debugger = shift @_;
        $DefaultDebugger = $selected_debugger;
    }
}


BEGIN {
    __PACKAGE__->register_debugger( 'perl5db' );
    __PACKAGE__->register_debugger( 'trepan' );
    __PACKAGE__->register_debugger( 'NYTProf' );
}
# TODO: __PACKAGE__->register_debugger( 'ebug' );
# TODO: __PACKAGE__->register_debugger( 'sdb' );
# TODO: __PACKAGE__->register_debugger( 'ptkdb' );


# Anything compiled after this statement runs will be debuggable.
Enbugger->_compile_with_dbstate();

## Local Variables:
## mode: cperl
## mode: auto-fill
## cperl-indent-level: 4
## tab-width: 8
## End:

no warnings 'void'; ## no critic
'But this is the internet, dear, stupid is one of our prime exports.';