package XML::RPC::Enc::LibXML;

use strict;
use warnings;
use base 'XML::RPC::Enc';
use XML::LibXML;
use XML::Hash::LX;
use Carp;
#use Encode ();

use XML::RPC::Fast ();
our $VERSION = $XML::RPC::Fast::VERSION;
BEGIN {
	if (eval { my $x = pack 'q', -1; 1 }) {
		*_HAVE_BIGINT = sub () { 1 };
		my $maxint = eval q{ 0+"9223372036854775807" };
		*_MAX_BIGINT = sub () { $maxint };
	} else {
		require Math::BigInt;
		*_HAVE_BIGINT = sub () { 0 };
		my $maxint = Math::BigInt->new("0x7fffffffffffffff");
		*_MAX_BIGINT  = sub () { $maxint };
	}
}


=head1 NAME

XML::RPC::Enc::LibXML - Encode/decode XML-RPC using LibXML

=head1 SYNOPSIS

    use XML::RPC::Fast;
    use XML::RPC::Enc::LibXML;
    
    my $rpc = XML::RPC::Fast->new(
        $uri,
        encoder => XML::RPC::Enc::LibXML->new(
            # internal_encoding currently not implemented, always want wide chars
            internal_encoding => undef,
            external_encoding => 'windows-1251',
        )
    );

    $rpc->registerType( base64 => sub {
        my $node = shift;
        return MIME::Base64::decode($node->textContent);
    });

    $rpc->registerType( 'dateTime.iso8601' => sub {
        my $node = shift;
        return DateTime::Format::ISO8601->parse_datetime($node->textContent);
    });

    $rpc->registerClass( DateTime => sub {
        return ( 'dateTime.iso8601' => $_[0]->strftime('%Y%m%dT%H%M%S.%3N%z') );
    });

    $rpc->registerClass( DateTime => sub {
        my $node = XML::LibXML::Element->new('dateTime.iso8601');
        $node->appendText($_[0]->strftime('%Y%m%dT%H%M%S.%3N%z'));
        return $node;
    });

=head1 DESCRIPTION

Default encoder/decoder for L<XML::RPC::Fast>

If MIME::Base64 is installed, decoder for C<XML-RPC> type C<base64> will be setup

If DateTime::Format::ISO8601 is installed, decoder for C<XML-RPC> type C<dateTime.iso8601> will be setup

Also will be setup by default encoders for L<Class::Date> and L<DateTime> (will be encoded as C<dateTime.iso8601>)

Ty avoid default decoders setup:

    BEGIN {
        $XML::RPC::Enc::LibXML::TYPES{base64} = 0;
        $XML::RPC::Enc::LibXML::TYPES{'dateTime.iso8601'} = 0;
    }
    use XML::RPC::Enc::LibXML;

=head1 IMPLEMENTED METHODS

=head2 new

=head2 request

=head2 response

=head2 fault

=head2 decode

=head2 registerType

=head2 registerClass

=head1 SEE ALSO

=over 4

=item * L<XML::RPC::Enc>

Base class (also contains documentation)

=back

=cut

# xml => perl
# args: xml-nodes (children of <value><$type> ... </$type></value>)
# retv: any scalar
our %TYPES;

# perl => xml
# args: object
# retv: ( type => string ) || xml-node
our %CLASS;

our $E;

BEGIN {
	if ( !exists $TYPES{base64} and eval{ require MIME::Base64;1 } ) {
		$TYPES{base64} = sub {
			#defined $E ? $E->encode(
			MIME::Base64::decode(shift->textContent);
		};
	}
	# DateTime is the most "standart" datetime object in perl, try to use it
	if ( !exists $TYPES{'dateTime.iso8601'} and eval{ require DateTime::Format::ISO8601;1 } ) {
		$TYPES{'dateTime.iso8601'} = sub {
			DateTime::Format::ISO8601->parse_datetime(shift->textContent)
		};
	}
}

#%TYPES = (
#	custom => sub { ... },
#	%TYPES,
#);

# We need no modules to predefine encoders for dates
%CLASS = (
	DateTime => sub {
		'dateTime.iso8601',$_[0]->strftime('%Y%m%dT%H%M%S.%3N%z');
	},
	'Class::Date' => sub {
		'dateTime.iso8601',$_[0]->strftime('%Y%m%dT%H%M%S').sprintf( '%+03d%02d', $_[0]->tzoffset / 3600, ( $_[0]->tzoffset % 3600 ) / 60  );
	},
	%CLASS,
);

sub new {
	my $pkg = shift;
	my $self = bless {
		@_,
		parser => XML::LibXML->new(),
		types  => { },
		class  => { },
		#internal_encoding => undef,
	}, $pkg;
	$self->{external_encoding} = 'utf-8' unless defined $self->{external_encoding};
	return $self;
}


sub registerType {
	my ( $self,$type,$decode ) = @_;
	my $old;
	if (ref $self) {
		$old = $self->{types}{$type};
		$self->{types}{$type} = $decode;
	} else {
		$old = $TYPES{$type};
		$TYPES{$type} = $decode;
	}
	$old;
}

sub registerClass {
	my ( $self,$class,$encode ) = @_;
	my $old;
	if (ref $self) {
		$old = $self->{class}{$class};
		$self->{class}{$class} = $encode;
	} else {
		$old = $CLASS{$class};
		$CLASS{$class} = $encode;
	}
	$old;
}

# Encoder part

sub _unparse_param {
	my $p = shift;
	my $r = XML::LibXML::Element->new('value');

	if ( ref($p) eq 'HASH' ) {
		# struct -> ( member -> { name, value } )*
		my $s = XML::LibXML::Element->new('struct');
		$r->appendChild($s);
		for ( keys %$p ) {
			my $m = XML::LibXML::Element->new('member');
			my $n = XML::LibXML::Element->new('name');
			$n->appendText(defined $E ? $E->decode($_) : $_);
			$m->appendChild($n);
			$m->appendChild(_unparse_param($p->{$_}));
			$s->appendChild($m);
		}
	}
	elsif ( ref($p) eq 'ARRAY' ) {
		my $a = XML::LibXML::Element->new('array');
		my $d = XML::LibXML::Element->new('data');
		$a->appendChild($d);
		$r->appendChild($a);
		for (@$p) {
			$d->appendChild( _unparse_param($_) )
		}
	}
	elsif ( ref($p) eq 'CODE' ) {
		$r->appendChild(hash2xml($p->(), doc => 1)->documentElement);
	}
	elsif (ref $p) {
		if (exists $CLASS{ ref $p }) {
			my ($t,$x) = $CLASS{ ref $p }->($p);
			if (ref $t and eval{ $t->isa('XML::LibXML::Node') }) {
				$r->appendChild($t);
			} else {
				my $v = XML::LibXML::Element->new($t);
				$v->appendText(defined $E ? $E->decode($x) : $x);
				$r->appendChild($v);
			}
		}
		elsif ( UNIVERSAL::isa($p,'SCALAR') ) {
			my $v = XML::LibXML::Element->new(ref $p);
			$v->appendText(defined $E ? $E->decode($$p) : $$p) if defined $$p;
			$r->appendChild($v);
		}
		elsif ( UNIVERSAL::isa($p,'REF') ) {
			my $v = XML::LibXML::Element->new(ref $p);
			$v->appendChild(hash2xml($$p, doc => 1)->documentElement);
			$r->appendChild($v);
		}
		else {
			warn "Bad reference: $p";
			#$result = undef;
		}
	}
	else {
		#no warnings;
		if (!defined $p) {
			my $v = XML::LibXML::Element->new('string');
			$r->appendChild($v);
		}

=for rem

Q: What is the legal syntax (and range) for integers?
   How to deal with leading zeros?
   Is a leading plus sign allowed?
   How to deal with whitespace?

A: An integer is a 32-bit signed number.
   You can include a plus or minus at the beginning of a string of numeric characters.
   Leading zeros are collapsed.
   Whitespace is not permitted.
   Just numeric characters preceded by a plus or minus.

Q: What is the legal syntax (and range) for floating point values (doubles)?
   How is the exponent represented?
   How to deal with whitespace?
   Can infinity and "not a number" be represented?

A: There is no representation for infinity or negative infinity or "not a number".
   At this time, only decimal point notation is allowed, a plus or a minus,
   followed by any number of numeric characters,
   followed by a period and any number of numeric characters.
   Whitespace is not allowed.
   The range of allowable values is implementation-dependent, is not specified.

		# int
		'+0' => 0
		'-0' => 0
		'+1234567' => 1234567
		'0777' => 777
		'0000000000000' => 0
		'0000000000000000000000000000000000000000000000000' => 0
		# not int
		'999999999999999999999999999999999999';

=cut
		elsif ($p =~ m/^([\-+]?)\d+(\.\d+|)$/) {
			my ($have_sign,$is_double) = ($1,$2);
			if ( $is_double ) {
				my $v = XML::LibXML::Element->new('double');
				$v->appendText( $p );
				$r->appendChild($v);
			}
			else {
				my $v;
				# TODO: should we pass sign "+"?
				if( $p == unpack "l", pack "l", $p ) {
					# i4
					$v = XML::LibXML::Element->new('i4');
					$v->appendText(int $p);
				}
				elsif ( _HAVE_BIGINT and $p == unpack "q", pack "q", $p ) {
					# i8
					$v = XML::LibXML::Element->new('i8');
					$v->appendText(int $p);
				}
				elsif ( !_HAVE_BIGINT and abs( my $bi = Math::BigInt->new($p) ) < _MAX_BIGINT ) {
					$v = XML::LibXML::Element->new('i8');
					$v->appendText($bi->bstr);
				}
				else {
					# string
					$v = XML::LibXML::Element->new('string');
					$v->appendText($p);
				}
				$r->appendChild($v);
			}
		}
		else {
			my $v = XML::LibXML::Element->new('string');
			$v->appendText(defined $E ? $E->decode($p) : $p);
			$r->appendChild($v);
		}
	}
	return $r;
}

sub request {
	my $self = shift;
	local @CLASS{keys %{ $self->{class} }} = values %{ $self->{class} };
	local $E = Encode::find_encoding($self->{internal_encoding})
		or croak "Could not find encoding $self->{internal_encoding}"
		if defined $self->{internal_encoding};
	my $method = shift;
	my $doc = XML::LibXML::Document->new('1.0',$self->{external_encoding});
	my $root = XML::LibXML::Element->new('methodCall');
	$doc->setDocumentElement($root);
	my $n = XML::LibXML::Element->new('methodName');
	$n->appendText(defined $E ? $E->decode($method) : $method);
	$root->appendChild($n);
	my $prms = XML::LibXML::Element->new('params');
	$root->appendChild($prms);
	for my $v (@_) {
		my $p = XML::LibXML::Element->new('param');
		$p->appendChild( _unparse_param($v) );
		$prms->appendChild($p);
	}
	my $x = $doc->toString;
	utf8::encode($x) if utf8::is_utf8($x);
	return $x;
}

sub response {
	my $self = shift;
	local @CLASS{keys %{ $self->{class} }} = values %{ $self->{class} };
	local $E = Encode::find_encoding($self->{internal_encoding})
		or croak "Could not find encoding $self->{internal_encoding}"
		if defined $self->{internal_encoding};
	my $doc = XML::LibXML::Document->new('1.0',$self->{external_encoding});
	my $root = XML::LibXML::Element->new('methodResponse');
	$doc->setDocumentElement($root);
	my $prms = XML::LibXML::Element->new('params');
	$root->appendChild($prms);
	for my $v (@_) {
		my $p = XML::LibXML::Element->new('param');
		$p->appendChild( _unparse_param($v) );
		$prms->appendChild($p);
	}
	my $x = $doc->toString;
	utf8::encode($x) if utf8::is_utf8($x);
	return $x;
}

sub fault {
	my $self = shift;
	local @CLASS{keys %{ $self->{class} }} = values %{ $self->{class} };
	local $E = Encode::find_encoding($self->{internal_encoding})
		or croak "Could not find encoding $self->{internal_encoding}"
		if defined $self->{internal_encoding};
	my ($code,$err) = @_;
	my $doc = XML::LibXML::Document->new('1.0',$self->{external_encoding});
	my $root = XML::LibXML::Element->new('methodResponse');
	$doc->setDocumentElement($root);
	my $f = XML::LibXML::Element->new('fault');
	my $v = XML::LibXML::Element->new('value');
	my $s = XML::LibXML::Element->new('struct');
	for (qw(faultCode faultString)){
		my $m = XML::LibXML::Element->new('member');
		my $n = XML::LibXML::Element->new('name');
		$n->appendText(defined $E ? $E->decode($_) : $_);
		$m->appendChild($n);
		$m->appendChild(_unparse_param(shift));
		$s->appendChild($m);
	}
	$v->appendChild($s);
	$f->appendChild($v);
	$root->appendChild($f);
	my $x = $doc->toString;
	utf8::encode($x) if utf8::is_utf8($x);
	return $x;
}

# Decoder part
our $src;
sub decode {
	my $self = shift;
	my $string = shift;
	#utf8::encode $string if utf8::is_utf8($string);
	local $src = $string;
	$self->_parse( $self->{parser}->parse_string($string) )
}

sub _parse_param {
	my $v = shift;
	for my $t ($v->childNodes) {
		next if ref $t eq 'XML::LibXML::Text';
		my $type = $t->nodeName;
		#print $t->nodeName,"\n";
		if ($type eq 'string') {
			return defined $E ? $E->encode(''.$t->textContent) : ''.$t->textContent;
		}
		elsif ($type eq 'i4' or $type eq 'int') {
			return int $t->textContent;
		}
		elsif ($type eq 'double') {
			return 0+$t->textContent;
		}
		elsif ($type eq 'bool') {
			$v = $t->textContent;
			return $v eq 'false' ? 0 : !!$v ? 1 : 0;
		}
		elsif ($type eq 'struct') {
			my $r = {};
			for my $m ($t->childNodes) {
				my ($mn,$mv);
				if ($m->nodeName eq 'member') {
					for my $x ($m->childNodes) {
						#print "\tmember:".$x->nodeName,"\n";
						if ($x->nodeName eq 'name') {
							$mn = $x->textContent;
							#last;
						}
						elsif ($x->nodeName eq 'value') {
							$mv = _parse_param ($x);
							$mn and last;
						}
					}
					if (defined $E) {
						$mn = $E->encode($mn);
						$mv = $E->encode($mv);
					}
					$r->{$mn} = $mv;
				}
			}
			return $r;
		}
		elsif ($type eq 'array') {
			my $r = [];
			for my $d ($t->childNodes) {
				#print "\tdata:".$d->nodeName,"\n";
				unless (defined $d) {
					warn "!!! Internal bug: childNodes return undef. XML=\n$src";
					next;
				}
				if ($d->nodeName eq 'data') {
					for my $x ($d->childNodes) {
						#print "\tdata:".$x->nodeName,"\n";
						if ($x->nodeName eq 'value') {
							push @$r, _parse_param ($x);
						}
					}
				}
			}
			return $r;
		}
#		elsif ($type eq 'base64') {
#			return decode_base64($t->textContent);
#		}
#		elsif ($type eq 'dateTime.iso8601') {
#			return $t->textContent;
#		}
		else {
			if (exists $TYPES{$type} and $TYPES{$type}) {
				return $TYPES{$type}( $t->childNodes );
			} else {
				my @children = $t->childNodes;
				@children or return bless( \do{ my $o }, $type );
				if (( @children > 1 ) xor ( ref $children[0] ne 'XML::LibXML::Text' )) {
					#print STDERR + (0+@children)."; $type => ",ref $children[0], ' ', $children[0]->nodeName, "\n";
					return bless \(xml2hash($t)->{$type}),$type;
				} else {
					#print STDERR + "*** ".(0+@children)."; $type => ",ref $children[0], ' ', $children[0]->nodeName, "\n";
					return bless \(
						defined $E ? $E->encode($children[0]->textContent) : $children[0]->textContent
					),$type;
				}
			}
		}
		last;
	}
	return defined $E ? $E->encode($v->textContent) : $v->textContent
}

sub _parse {
	my $self = shift;
	my $doc = shift;
	my @r;
	my $root = $doc->documentElement;
	local @TYPES{keys %{ $self->{types} }} = values %{ $self->{types} };
	local $E = Encode::find_encoding($self->{internal_encoding})
		or croak "Could not find encoding $self->{internal_encoding}"
		if defined $self->{internal_encoding};
	for my $p ($doc->findnodes('//param')) {
	#for my $ps ($root->childNodes) {
	#	if ($ps->nodeName eq 'params') {
	#		for my $p ($ps->childNodes) {
	#			if ($p->nodeName eq 'param') {
					#print $p->nodeName,"\n";
					for my $v ($p->childNodes) {
						if ($v->nodeName eq 'value') {
							#print $p->nodeName,'=',_parse_param($v),"\n";
							push @r, _parse_param ($v);
						}
					}
	#			}
	#		}
	#	}
	}
	for my $m ($doc->findnodes('//methodName')) {
		unshift @r, defined $E ? $E->encode($m->textContent) : $m->textContent;
		last;
	}
	unless(@r) {
	for my $f ($doc->findnodes('//fault')) {
		my ($c,$e);
		
		for ($f->childNodes) {
			if ( $_->nodeName eq 'value' ) {
				my $flt  = _parse_param ( $_ );
				$c = $flt->{faultCode};
				$e = $flt->{faultString};
				last;
			} else {
				$c = defined $E ? $E->encode($_->textContent) : $_->textContent if $_->nodeName eq 'faultCode';
				$e = defined $E ? $E->encode($_->textContent) : $_->textContent if $_->nodeName eq 'faultString';
			}
		}
		return { fault => { faultCode => $c, faultString => $e } };
	}
	}
	#warn "@r";
	return @r;
}

=head1 COPYRIGHT & LICENSE

Copyright (c) 2008-2009 Mons Anderson.

This program is free software; you can redistribute it and/or modify it
under the same terms as Perl itself.

=head1 AUTHOR

Mons Anderson, C<< <mons@cpan.org> >>

=cut

1;
