The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Class::Easy::Log;
# $Id: Log.pm,v 1.3 2009/07/20 18:00:10 apla Exp $

use Class::Easy::Import;
use Class::Easy::Log::Tie;
use Class::Easy ();

# log4perl has categories, layouts and appenders
our $default_layout = '[%P] [%M(%L)] [%c] %m%n';

Class::Easy::make_accessor (__PACKAGE__, 'category');
Class::Easy::make_accessor (__PACKAGE__, 'tied');
Class::Easy::make_accessor (__PACKAGE__, 'layout');

my $driver_config = {};
our $int_loggers   = {
	default => bless {
		category => 'default', broker => '', tied => 0
	}, __PACKAGE__
};

my $java_mappings = {
	L => 'line',
	P => 'pid',
	r => 'ts_start',
	R => 'ts_log',
	c => 'category',
	C => 'package',
	d => 'date',
	F => 'file',
	H => 'hostname',
	l => 'where',
	m => 'message',
	M => 'method',
	n => 'newline',
	p => 'priority',
	T => 'stack',
};

our $hostname;
if (Class::Easy::try_to_use ('Sys::Hostname')) {
	$hostname = Sys::Hostname->can('hostname')->();
}

Class::Easy::Log->configure_driver (
	id => 'log4perl', package => 'Log::Log4perl', constructor => 'get_logger',
	log => 'debug', # default logging level
	
);

# basic logger:    logger ('sql');
# log4perl logger: logger (log4perl => 'sql');
# also you'll need to configure log4perl somewhere:
# Log::Log4perl::init (...);
# Class::Easy::Log->configure_driver (
#	type => 'log4perl', package => 'Log::Log4perl', constructor => 'get_logger'
# );

sub configure_driver {
	my $class = shift;
	my $params = {@_};
	
	if (Class::Easy::try_to_use ($params->{package})) {
		$driver_config->{$params->{id}} = $params;
	}
}

sub logger { # create logger
	
	my $driver_id;
	my $category;
	my $appender;
	
	my $ref;
	
	if (defined $_[1]) {
		$ref = ref \$_[1];
	}
	
	unless (@_) { # if type omitted, we use current package name as type
		$category = (caller)[0];
	} elsif (scalar (@_) == 2 and $ref eq 'GLOB' and defined *{$_[1]}{IO}) {
		$category = $_[0];
		$appender = $_[1];
	} elsif ((@_ == 2 or @_ == 1) and exists $driver_config->{$_[0]}) {
		$driver_id = $_[0];
		$category = @_ == 1 ? (caller)[0] : $_[1];
	} elsif (@_ == 1) {
		$category = $_[0];
	} else {
		die "you must use logger (), logger (driver), logger (category) or logger (driver => category)";
	}
	
	my $self;
	
	unless (defined $driver_id) { # basic internal driver require no processing
		
		my $existing_logger = $int_loggers->{$category};
		
		$self = $existing_logger || bless {
			category => $category,
			broker   => '',
		}, 'Class::Easy::Log';
		
		unless (defined $existing_logger) {
			$int_loggers->{$category} = $self;
			
			Class::Easy::make_accessor ((caller)[0], 'log_'.$category, default => sub {
				my $caller1  = [caller (1)];
				my $caller0  = [caller];

				unshift @_, $category, $self, $caller1, $caller0;
				goto &_wrapper;
			});

			Class::Easy::make_accessor ((caller)[0], 'timer_'.$category, default => sub {
				Class::Easy::Timer->new (@_, $self)
			});
		}

	} elsif (defined $driver_config->{$driver_id}) { # driver defined
		my $driver = $driver_config->{$driver_id};
		$self = $driver->{package}->can ($driver->{constructor})->($driver->{package}, $category);

		Class::Easy::make_accessor ((caller)[0], 'log_'.$category, default => sub {
			goto &{$self->can ($driver->{log})};
		});
		
		# make_accessor ((caller)[0], 'log_'.$type, default => \&Class::Easy::Log::message);
	}
	
	if ($appender) {
		$self->appender ($appender);
	}
	
	return $self;
}

sub appender {
	my $self     = shift;
	# my $appender = shift;
	
	if (@_) {
		$self->{tied} = 1;
		tie $self->{broker} => 'Class::Easy::Log::Tie', $_[0];
	} else {
		$self->{tied} = 0;
		untie $self->{broker};
	}

}

# example usage: 
# logger (sql); # create sub log_sql
# log_sql ('message'); # log message, but nobody receive this message
# logger (sql => 'STDERR'); # now any log messages go to the STDERR

sub _parse_layout {
	my $logger = shift;
	
	$logger->{layout} ||= $default_layout;
	
	return $logger
		if defined $logger->{_layout} and $logger->{layout} eq $logger->{_layout};
	
	my $layout = $logger->{layout};
	
	my $layout_format = '';
	my @layout_fields = ();
	while ($layout =~ /([^\%]*)\%([^\%cCdFHlLmMnpPrRTxX]*)([\%cCdFHlLmMnpPrRTxX])/g) {
		
		$layout_format .= "$1\%$2";
		if ($3 eq 'L' or $3 eq 'P') {
			$layout_format .= 'd';
		} elsif ($3 eq 'r' or $3 eq 'R') {
			$layout_format .= 'd';
		} elsif ($3 eq '%') {
			$layout_format .= '%';
		} else {
			$layout_format .= 's';
		}
		push @layout_fields, $java_mappings->{$3}
			unless $3 eq '%';
	}
	# TODO: create more failsafe solution
	$layout_format .= substr ($layout, length($layout_format));
	
	$logger->{_layout_format} = $layout_format;
	$logger->{_layout_fields} = \@layout_fields;
	$logger->{_layout} = $layout;
	
	return $logger;
}

sub _format_log {
	my $self = shift;
	
	my $time = time;
	
	my $values = {
		pid      => $$,
		category => $self->{category},
		newline  => "\n",
		ts_start => $time - $^T,
		hostname => $hostname, # doesn't reflect hostname changes in runtime
		date     => $time,
		@_
	};

#	TODO: make sure all these values supported
#	R => 'ts_log',   # use timer_${logger} instead
#	C => 'package',  # useless, because we have %M = method
#	F => 'file',     # who cares about script files?
#	l => 'where',    # wtf?
#	p => 'priority', # log level, if written not for robots
#	T => 'stack',    # everything loves java stacks
#	TODO: add date formatting support
	
#	use Data::Dumper;
#	warn Dumper $self->{_layout_fields};
#	warn Dumper [map {$values->{$_}} @{$self->{_layout_fields}}];
	
#	warn $self->{_layout_format}, join (', ', @{$self->{_layout_fields}}), (join ', ', map {
#		$values->{$_}
#	} @{$self->{_layout_fields}});
	
	return sprintf ($self->{_layout_format}, (map {
		$values->{$_}
	} @{$self->{_layout_fields}}));
	
}

sub _wrapper {
	my $category = shift;
	my $logger   = shift;
	my $caller1  = shift;
	my $caller0  = shift;
	
	my $sub  = $caller1->[3] || 'main';
	my $line = $caller0->[2];
	
	# my ($package, $filename, $line, $subroutine, $hasargs, $wantarray,
	# $evaltext, $is_require, $hints, $bitmask)
	
	$logger->_parse_layout;
		
	$logger->{broker} = $logger->_format_log (
		message => join ('', @_),
		method  => $sub,
		line    => $line
	);
	
	return 1;
}

sub debug {
	my $caller1  = [caller (1)];
	my $caller0  = [caller];

	unshift @_, 'default', $int_loggers->{default}, $caller1, $caller0;

	goto &_wrapper;
}

sub debug_depth {
	my $caller1  = [caller (2)];
	my $caller0  = [caller (1)];

	unshift @_, 'default', $int_loggers->{default}, $caller1, $caller0;

	goto &_wrapper;
}

sub critical {
	my $sub  = (caller (1))[3] || 'main';
	my $line = (caller)[2];
	
	my $logger = logger ('DIE')->_parse_layout;
	
	die $logger->_format_log (
		message => join ('', @_),
		method  => $sub,
		line    => $line
	);
}

sub catch_stderr {
	my $ref = shift;
	tie *STDERR => 'Class::Easy::Log::Tie', $ref;
}

sub release_stderr {
	untie *STDERR;
}

1;