;#
;# Copyright (c) 1995-1998
;#	Ikuo Nakagawa. All rights reserved.
;#
;# Redistribution and use in source and binary forms, with or without
;# modification, are permitted provided that the following conditions
;# are met:
;#
;# 1. Redistributions of source code must retain the above copyright
;#    notice unmodified, this list of conditions, and the following
;#    disclaimer.
;# 2. Redistributions in binary form must reproduce the above copyright
;#    notice, this list of conditions and the following disclaimer in the
;#    documentation and/or other materials provided with the distribution.
;#
;# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
;# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
;# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
;# PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS
;# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
;# OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
;# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
;# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
;# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
;# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
;# EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
;#
;# $Id: Param.pm,v 1.19 1998/09/19 03:58:35 ikuo Exp $
;#
;# Useful libraries to treat parameters.
;#
;#	$param = Fan::Param->new(
;#		param_name => 'INIT',
;#		%param_values);
;#
;#	$defaults = Fan::Param->new(
;#		param_name => 'DEFAULT',
;#		param_prefix => "/usr/local/etc",
;#		param_file => "param.conf");
;#
;#	$options = Fan::Param->new(
;#		param_name => 'OPTION',
;#		param_array => \@ARGV);
;#
;# Since any key begins with 'param_' has special meaning for
;# Param package, the first statement in above, %param_value
;# can have no assoc key whose name begins with 'param_'.
;#
;# You can also combine parameters, as:
;#
;#	my $override = 1;
;#	$param = Fan::Param->new(param_name => "TARGET");
;#	$param->combine($init, $override);
;#	$param->combine($defaults, $override);
;#	$param->combine($options, $override);
;#
;# These statements are equevalent with:
;#
;#	$param = Fan::Param->new(param_name => "TARGET");
;#	$param->merge($init, $defaults, $options);
;#
;# You can access to any values in a Param object as follows:
;#
;#	$param->getval('key-to-access');
;#	$param->setval('key-to-access', 'value-to-set');
;#
;# You can restrict to set values for unexpected keys with
;# `param_keys' values for a Param object. For example,
;#
;#	%param_default = (
;#		"key_any" => '',			# any value
;#		"key_digit" => '/^\d+$/ || undef',	# only digits
;#		"key_ipaddr" => '/^\d+\.\d+\.\d+\.\d+$/ || undef',
;#							# ip address
;#		"key_range" => '$_ >= 100 && $_ < 200 || undef',
;#							# number range
;#		"key_path" => '-f $_ || undef'		# real path
;#		"key_abspath" => '/^\// || undef',	# absolute path
;#		"key_proc" => \&callproc		# procedure
;#	);
;#	$param = Fan::Param->new(
;#		param_name => 'PARAM WE WILL ACCESS',
;#		param_keys => \%default_default);
;#
;# if you initialize $param as above,
;#
;# 	$param->setval('not_in_default', 'any_value'),
;#
;# will do nothing and simply returns undef. You can modify
;# $param{$key} only if $param_default{$key} exists and the
;# evaluation of
;#
;#	$_ = $val;
;#	eval $param_default{$key};
;#
;# returns non zero.
;#
package Fan::Param;

use strict;
use vars qw($VERSION $LOG $param_sequence %wants);

use Carp;
use AutoLoader 'AUTOLOAD';

$VERSION = '0.03';
$LOG = 5; # notice level...

;# Sequencial number for Param objects.
$param_sequence = 0;

;# prototypes.
sub DESTROY ($);
sub new ($%);
sub error ($;$);
sub try_check ($$;$);
sub getval ($$);
sub delete ($$);
sub setval ($$$);
sub addval ($$$);
sub dump ($);
sub combine ($@);
sub merge ($@);

;# internal routines.
sub want_ref;
sub want_code;
sub want_hash;
sub want_array;
sub want_boolean;
sub want_integer;
sub want_octal;
sub want_ipv4_addr;
sub want_path;
sub want_file;
sub want_directory;
sub want_timezone;

;# initialize want hash
%wants = (
	'REF'		=> \&want_ref,
	'CODE'		=> \&want_code,
	'HASH'		=> \&want_hash,
	'ARRAY'		=> \&want_array,
	'BOOLEAN'	=> \&want_boolean,
	'INTEGER'	=> \&want_integer,
	'OCTAL'		=> \&want_octal,
	'IPv4_ADDR'	=> \&want_ipv4_addr,
	'PATH'		=> \&want_path,
	'FILE'		=> \&want_file,	
	'DIRECTORY'	=> \&want_directory,
	'TIMEZONE'	=> \&want_timezone,
);

;#

;# A special marker for AutoSplit.
1;
__END__

;# Destroy a Param object.
;#
sub DESTROY ($) {
	my $self = shift;

	# Log message for debugging purpose
	carp("Param DESTROYING $self [$self->{param_name}]") if $LOG > 5;
}

;# Create a new Param object.
;#
sub new ($%) {
	my $this = shift;
	my $class = ref($this) || $this;
	my %param = @_;
	my $self = { param_error => 0 };

	# Count up param objects.
	$param_sequence++;

	# Pick up some special parameters.
	$self->{param_name} = $param{param_name} || "Param[$param_sequence]";

	# Check keys param object.
	if (ref($param{param_keys}) eq 'HASH') {
		$self->{param_keys} = $param{param_keys}; # save ref
	}

	# Check keys for nesting parameters.
	if (ref($param{param_nest}) eq 'HASH') {
		$self->{param_nest} = $param{param_nest}; # save ref
	}

	# Create a new object.
	bless $self, $class or return undef;

	# Log message for debugging purpose
	carp("Param CREATING $self [$self->{param_name}]") if $LOG > 5;

	# Register (key, val) pairs in %param.
	my $key;
	my $val;
	while (($key, $val) = each %param) {
		$self->setval($key, $val) if $key !~ /^param_/;
	}

	# Return myself.
	$self;
}

;#
sub error ($;$) {
	my $self = shift;

	if (@_) {
		$self->{param_error} = shift;
	}
	$self->{param_error};
}

;#
;#
sub addkey ($$;$) {
	my $p = shift;
	my $key = shift;
	my $val = @_ ? shift : '';

	$p->{param_keys}->{$key} = $val;
}

;#
;#
sub try_check ($$;$) {
	my $p = shift;
	my $key = shift;
	my $h = $p->{param_keys}; # hash for keys
	local $_ = 1; # default return value.

	# Validation of the given key.
	if ($key =~ /^param_/ || (ref($h) eq 'HASH' && !exists($h->{$key}))) {
#		carp("$p: key=$key invalid key") if $LOG > 4;
		confess("$p: key=$key invalid key") if $LOG > 4;
		$p->{param_error}++;
		return undef;
	}

	# Validation of the given value, if exists.
	if (@_ && ref($h) eq 'HASH' && exists($h->{$key})) {
		my $val = $h->{$key};
		my $x = shift; # backup
		$_ = $x;

		# copy from default wants tables.
		if (!ref($val) && defined($wants{$val})) {
			$val = $wants{$val};
		}

		# check value types
		if ($_ eq '') {
			; # null string is o.k.
		} elsif ($val eq '') {
			; # o.k.
		} elsif (ref($val) eq 'CODE') {
			$_ = &{$val}($_);
		} elsif (defined(eval($val))) {
			; # good.
		} else {
			carp $@ if $@ && $LOG > 3; # evaluation error...
			undef $_;
		}
		if (!defined($_)) {
			croak("Param ($key, $x) invalid value") if $LOG > 4;
			$p->{param_error}++;
			return undef;
		}
	}

	# Result is the converted value.
	$_;
}

;#
;#
sub getval ($$) {
	my $p = shift;
	my $key = shift;

	$p->try_check($key) || return undef;
	defined($p->{$key}) ? $p->{$key} : undef;
}

;#
;#
sub delete ($$) {
	my $p = shift;
	my $key = shift;

	$p->try_check($key) || return undef;
	exists($p->{$key}) ? CORE::delete($p->{$key}) : 0;
}

;#
;#
sub setval ($$$) {
	my $p = shift;
	my $key = shift;
	my $val = shift;

	my $x = $p->try_check($key, $val);
	defined($x) ? ($p->{$key} = $x) : undef;
}

;#
;#
sub addval ($$$) {
	my $p = shift;
	my $key = shift;
	my $val = shift;

	$val = $p->{$key}."\n".$val;
	my $x = $p->try_check($key, $val);
	defined($x) ? ($p->{$key} = $x) : undef;
}

;# dump parameters
;#
sub dump ($) {
	my $p = shift;

	print "* $p name=$p->{param_name}\n";

	my @keys = sort keys %{$p};
	my $key;

	for $key (grep(/^param_/, @keys), grep(!/^param_/, @keys)) {
		my $val = $p->{$key};
		if ($val =~ /\n/) {
			my $s = $val =~ s/^\n// ? '+' : '';
			print " $key $s=\n";
			for $s (split(/\n/, $val)) {
				print "  $s\n";
			}
		} else {
			print " $key = $val\n";
		}
	}
	1;
}

;# Combine some parameters for Param objects.
;# $p->combine($a, $b, ..., $z, $flag) will combine as follows:
;# in order of $a, $b, ..., $z, copy parameter values to $p.
;# If $flag is non zero, override is permitted.
;#
sub combine ($@) {
	my $p = shift; # output object
	my @list = ();
	my $count = 0;
	my $n;

	# check Param objects.
	while (defined($n = shift) && ref($n) && $n->isa('Fan::Param')) {
		push(@list, $n);
	}

	# now $n is the flag of override.
	my $param;
	for $param (@list) {
		my $key;
		my $val;
		while (($key, $val) = each %{$param}) {
			next if $key =~ /^param_/;
			if (exists($p->{$key})) {
				if ($val =~ /^\n/) { # append
					$val = $p->{$key}.$val;
				} elsif (!$n) { # not override
					next;
				}
			}
			if ($p->try_check($key)) {
				$p->{$key} = $val; # copy
				$count++;
			} else {
				; # simply ignored
			}
		}
	}

	# succeeded
	1;
}

;# $p->merge($a, $b, ..., $z) is same as
;# $p->combine($a, $b, ..., $z, 1);
;#
sub merge ($@) {
	my $p = shift;

	$p->combine(@_, 1);
}

;# Subroutines for check operations
;#
sub want_ref {
	my $x = shift;

	if (@_) {
		ref($x) eq shift || return undef;
	} else {
		ref($x) || return undef;
	}
	$x;
}

;#
sub want_code {
	want_ref(shift, 'CODE');
}

;#
sub want_hash {
	want_ref(shift, 'HASH');
}

;#
sub want_array {
	want_ref(shift, 'ARRAY');
}

;# want boolean value,
;# converted to 1 or 0.
;#
sub want_boolean {
	my $x = shift;

	return $& ? 1 : 0 if $x =~ /^\d+$/;
	return 1 if $x =~ /^(yes|t|true|do|will)$/i;
	return 0 if $x =~ /^(no|nil|false|dont|wont)$/i;
	undef;
}

;# want decimal value,
;# force to be converted to an integer.
;#
sub want_decimal {
	my $x = shift;

	return $& + 0 if $x =~ /^\d+$/;
	undef;
}

;# want octal value,
;#
sub want_octal {
	my $x = shift;

	return $& if $x =~ /^[0-7]+$/;
	undef;
}

;# want an integer value (with or without sign),
;# force to be an integer.
;#
sub want_integer {
	my $x = shift;
	my $flag = 1;

	if ($x =~ s/^-//) {
		$flag = -1;
	} elsif ($x =~ s/^\+//) {
		;
	}

	return $flag * $& if $x =~ /^\d+$/;
	undef;
}

;# want IPv4 address.
;#
sub want_ipv4_addr {
	my $x = shift;

	return $& if $x =~ /^\d+\.\d+\.\d+\.\d+$/;
	undef;
}

;# want_path($string, $eval)
;# convert a tilda notation (like ~ftp).
;#
sub want_path {
	my $path = shift;
	my $dir = '';

# warn("input is \"$path\"\n");

	# Expand pathname first.
	# For example, "~ikuo/src/hogehoge" will expanded to
	# "/home/ikuo/src/hogehoge".
	if ($path =~ s|^~([^/]*)||) {
		if ($1 ne '') {
			$dir = (getpwnam($1))[7];
		} else {
			$dir = $ENV{'HOME'} || (getpwuid($<))[7];
		}
		$path = $dir.$path;
	}

	# Result must not be null string.
	return undef if $path eq '';

	# Evaluation test.
	if (@_) {
		local $_ = $path;

		if(!defined(eval shift)) {
			carp $@ if $@ && $LOG > 3;
# warn("result is undef\n");
			return undef;
		}
		$path = $_;
	}

# warn("result is path\n");
	# Result is $path.
	$path;
}

;#
sub want_file {
	want_path(shift, '-f $_ || undef');
}

;#
sub want_directory {
	want_path(shift, '-d $_ || undef');
}

;# want timezone.
;# converted to ``sign . %02d . %02d ''.
;#
sub want_timezone {
	my $tz = shift;

	if ($tz =~ /^(\+|-)(\d\d?)(\d\d)$/) {
		return sprintf("%s%02d%02d", $1, $2, $3);
	} elsif ($tz eq 'GMT') {
		return '+0000';	
	} elsif ($tz eq 'JST') {
		return '+0900';	
	}
	undef;
}

;# end of Fan::Param module
