# -*- ruby -*-
# vim: set nosta noet ts=4 sw=4:
# encoding: utf-8

require 'logger'
require 'date'

# A mixin that provides a top-level logging subsystem based on Logger.
module Loggability

	# Package version constant
	VERSION = '0.6.0'

	# VCS revision
	REVISION = %q$Revision: 5475fac60ffb $

	# The key for the global logger (Loggability's own logger)
	GLOBAL_KEY = :__global__

	# The methods that are delegated across all loggers
	AGGREGATE_METHODS = [ :level=, :output_to, :write_to, :format_with, :format_as, :formatter= ]

	# Configuration defaults
	CONFIG_DEFAULTS = {
		:__default__ => 'warn STDERR',
	}

	# Regexp for parsing logspec lines in the config
	LOGSPEC_PATTERN = %r{
		^
			\s*
			((?i:debug|info|warn|error|fatal))   # severity
		    (?:
				\s+
				((?:[\w\-/:\.]|\\[ ])+)
			)?
			(?: \s+\(
				(\w+)
			\) )?
			\s*
		$
	}x

	require 'loggability/constants'
	include Loggability::Constants

	require 'loggability/logger'


	##
	# The Hash of modules that have a Logger, keyed by the name they register with
	class << self; attr_reader :log_hosts; end
	@log_hosts = {}


	### Return the library's version string
	def self::version_string( include_buildnum=false )
		vstring = "%s %s" % [ self.name, VERSION ]
		vstring << " (build %s)" % [ REVISION[/: ([[:xdigit:]]+)/, 1] || '0' ] if include_buildnum
		return vstring
	end


	### Cast the given +device+ to a Loggability::Logger, if possible, and return it. If
	### it can't be converted, raises a ArgumentError.
	def self::Logger( device )
		return device if device.is_a?( Loggability::Logger )
		return Loggability::Logger.from_std_logger( device ) if device.is_a?( ::Logger )
		return Loggability::Logger.new( device )
	end


	### Register the specified +host+ as a log host. It should already have been extended
	### with LogHostMethods.
	def self::register_loghost( host )
		key = host.log_host_key
		if self.log_hosts.key?( key )
			self.logger.warn "Replacing existing log host for %p (%p) with %p" %
				[ key, self.log_hosts[key], host ]
		end

		self.logger.debug "Registering %p log host: %p" % [ key, host ] if self.logger
		self.log_hosts[ key ] = host
	end


	### Return the log host key for +object+, using its #log_host_key method
	### if it has one, or returning it as a Symbol if it responds to #to_sym. Returns
	### +nil+ if no key could be derived.
	def self::log_host_key_for( object )
		return object.log_host_key if object.respond_to?( :log_host_key )
		return object.to_sym if object.respond_to?( :to_sym )
		return nil
	end


	### Returns +true+ if there is a log host associated with the given +object+.
	def self::log_host?( object )
		key = self.log_host_key_for( object ) or return false
		return self.log_hosts.key?( key )
	end


	### Return the Loggability::Logger for the loghost associated with +logclient+.
	def self::[]( logclient )
		key = self.log_host_key_for( logclient )
		key ||= GLOBAL_KEY

		return self.log_hosts[ key ].logger
	end


	### Clear out all log hosts except for ones which start with '_'. This is intended
	### to be used for testing.
	def self::clear_loghosts
		self.log_hosts.delete_if {|key,_| !key.to_s.start_with?('_') }
	end


	#
	# :section: Aggregate Methods
	#

	### Call the method with the given +methodname+ across the loggers of all loghosts with
	### the given +arg+ and/or +block+.
	def self::aggregate( methodname, arg, &block )
		# self.log.debug "Aggregating a call to %p with %p to %d log hosts" %
		#	[ methodname, arg, Loggability.log_hosts.length ]
		Loggability.log_hosts.values.each do |loghost|
			# self.log.debug "  %p.logger.%s( %p )" % [ loghost, methodname, arg ]
			loghost.logger.send( methodname, arg, &block )
		end
	end


	##
	# :method: level=
	# :call-seq:
	#   level = newlevel
	#
	# Aggregate method: set the log level on all loggers to +newlevel+. See
	# Loggability::Logger#level= for more info.
	def self::level=( newlevel )
		self.aggregate( :level=, newlevel )
	end


	##
	# :method: output_to
	# :call-seq:
	#   output_to( destination )
	#   write_to( destination )
	#
	# Aggregate method: set all loggers to log to +destination+. See Loggability::Logger#output_to
	# for more info.
	def self::output_to( newdevice )
		self.aggregate( :output_to, newdevice )
	end
	class << self
		alias_method :write_to, :output_to
	end


	##
	# :method: format_with
	# :call-seq:
	#   format_with( formatter )
	#   format_as( formatter )
	#   formatter = formatter
	#
	# Aggregate method: set all loggers to log with the given +formatter+. See
	# Loggability::Logger#format_with for more info.
	def self::format_with( formatter )
		self.aggregate( :format_with, formatter )
	end
	class << self
		alias_method :format_as, :format_with
		alias_method :formatter=, :format_with
	end


	# Extension for 'log hosts'. A <b>log host</b> is an object that hosts a Loggability::Logger
	# object, and is typically the top of some kind of hierarchy, like a namespace
	# module for a project:
	#
	#     module MyProject
	#
	#     end
	#
	# This module isn't mean to be used directly -- it's installed via the Loggability#log_as
	# declaration, which also does some other initialization that you'll likely want.
	#
	#
	module LogHost

		# The logger that will be used when the logging subsystem is reset
		attr_accessor :default_logger

		# The logger that's currently in effect
		attr_reader :logger
		alias_method :log, :logger

		# The key associated with the logger for this host
		attr_accessor :log_host_key


		### Set the logger associated with the LogHost to +newlogger+. If +newlogger+ isn't a
		### Loggability::Logger, it will be converted to one.
		def logger=( newlogger )
			@logger = Loggability::Logger( newlogger )
		end
		alias_method :log=, :logger=

	end # module LogHost


	# Methods to install for objects which call +log_to+.
	module LogClient

		##
		# The key of the log host this client targets
		attr_accessor :log_host_key

		### Return the Loggability::Logger object associated with the log host the
		### client is logging to.
		### :TODO: Use delegation for efficiency.
		def log
			@__log ||= Loggability[ self ].proxy_for( self )
		end


		### Inheritance hook -- set the log host key of subclasses to the same
		### thing as the extended class.
		def inherited( subclass )
			super
			Loggability.log.debug "Setting up subclass %p of %p to log to %p" %
				[ subclass, self, self.log_host_key ]
			subclass.log_host_key = self.log_host_key
		end


		# Stuff that gets added to instances of Classes that are log hosts.
		module InstanceMethods

			### Fetch the key of the log host the instance of this client targets
			def log_host_key
				return self.class.log_host_key
			end


			### Delegate to the class's logger.
			def log
				@__log ||= Loggability[ self.class ].proxy_for( self )
			end

		end # module InstanceMethods

	end # module LogClient


	#
	# :section: LogHost API
	#

	### Register as a log host associated with the given +key+, add the methods from
	### LogHost, and install a Loggability::Logger.
	def log_as( key )
		extend( Loggability::LogHost )
		include( Loggability::LogClient::InstanceMethods ) if self.is_a?( Class )

		self.log_host_key = key.to_sym
		self.logger = self.default_logger = Loggability::Logger.new
		Loggability.register_loghost( self )
	end


	#
	# :section: LogClient API
	#

	### Register as a <b>log client</b> that will log to to the given +loghost+, which can be
	### either the +key+ the host registered with, or the log host object itself. Log messages
	### can be written to the loghost via the LogClient API, which is automatically included.
	def log_to( loghost )
		extend( Loggability::LogClient )
		include( Loggability::LogClient::InstanceMethods ) if self.is_a?( Class )

		self.log_host_key = Loggability.log_host_key_for( loghost )
	end


	# Install a global logger in Loggability itself
	extend( Loggability::LogHost )
	self.log_host_key = GLOBAL_KEY
	self.logger = self.default_logger = Loggability::Logger.new
	Loggability.register_loghost( self )


	#
	# :section: Configurability Support
	#

	# Load the Configurability library if it's installed
	begin
		require 'configurability'
	rescue LoadError
	end


	# Configurability support -- load Loggability configuration from the 'logging' section
	# of the config.
	if defined?( Configurability )
		extend Configurability
		config_key :logging if respond_to?( :config_key )
	end

	### Configurability API -- configure logging.
	def self::configure( config=nil )
		if config
			self.log.debug "Configuring Loggability with custom config."
			confighash = config.to_hash

			# Set up all loggers with defaults first
			if defaultspec = confighash.delete( :__default__ ) || confighash.delete( '__default__' )
				level, format, target = self.parse_config_spec( defaultspec )
				Loggability.level = level if level
				Loggability.format_as( format ) if format
				Loggability.output_to( target ) if target
			end

			# Then let individual configs override.
			confighash.each do |key, logspec|
				unless Loggability.log_host?( key )
					self.log.debug "  no such log host %p; skipping" % [ key ]
					next
				end

				self.log.debug "  configuring logger for %p: %s" % [ key, logspec ]
				level, format, target = self.parse_config_spec( logspec )
				Loggability[ key ].level = level if level
				Loggability[ key ].format_with( format ) if format
				Loggability[ key ].output_to( target ) if target
			end
		else
			self.log.debug "Configuring Loggability with defaults."
		end
	end


	### Parse the specified +spec+ into level,
	def self::parse_config_spec( spec )
		match = LOGSPEC_PATTERN.match( spec ) or
			raise ArgumentError, "Couldn't parse logspec: %p" % [ spec ]
		self.log.debug "  parsed config spec %p -> %p" % [ spec, match ]
		severity, target, format = match.captures

		target = case target
			when 'STDOUT' then $stdout
			when 'STDERR' then $stderr
			else
				target
			end

		return severity, format, target
	end


end # module Loggability