# $Id$
#
# Copyright (c) 2005-2007 Daisuke Maki <daisuke@endeworks.jp>
# All rights reserved.

package XML::RSS::LibXML::ImplBase;
use strict;
use warnings;
use base qw(Class::Accessor::Fast);
use Carp qw(croak);
use XML::RSS::LibXML::MagicElement;
use XML::RSS::LibXML::Namespaces;

sub rss_accessor
{
    my $self = shift;
    my $name = shift;
    my $c    = shift;

    if (! exists $c->{$name}) {
        croak "Unregistered entity: Can't access $name field in object of class " . ref($self);
    }

    my $ret;


    if (@_ == 1) {
        if (ref $_[0]) { #  eval { $_[0]->isa('XML::RSS::LibXML::MagicElement') }) {
            $ret = $c->{$name};
            $c->{$name} = $_[0];
        } else {
            $ret = $c->{$name}->{$_[0]};
            if (ref $ret && eval { $ret->isa('XML::RSS::LibXML::ElementSpec') }) {
                $ret = undef;
            }
        }
    } elsif (@_ > 1) {
        my %hash = @_;
        my $definition = $self->accessor_definition;

        foreach my $key (keys %hash) {
            $self->validate_accessor($definition, $name, $key, $hash{$key}) if $definition;

            if ($key =~ /^(?:rdf|dc|syn|taxo|admin|content|cc)$/) {
                if (! exists $c->namespaces->{$key}) {
                    $c->add_module(prefix => $key, uri => XML::RSS::LibXML::Namespaces::lookup_uri($key));
                }
            }

#            $self->store_element($c, $c->{$name}, $key, $hash{$key});
            $self->set_value($c, $name, $key, $hash{$key});
            if (my $uri = $c->namespaces->{$key}) {
                $self->set_value($c, $name, $uri, $hash{$key});
#                $self->store_element($c, $c->{$name}, $uri, $hash{$key});
            }
        }
        $ret = $c->{$name};
    } else {
        $ret = $c->{$name};
        if (ref $ret && eval { $ret->isa('XML::RSS::LibXML::ElementSpec') }) {
            $ret = undef;
        }
    }

    return $ret;
}

sub definition {}
sub accessor_definition { }

sub validate_accessor
{
    my ($self, $definition, $prefix, $key, $value) = @_;

    if (! defined $value) {
        croak "Undefined value in XML::RSS::LibXML::validate_accessor";
    }
    my $spec = $definition->{$prefix}{$key};
    croak "$key cannot exceed " . $spec->[1] . " characters in length"
        if defined $spec->[1] && length($value) > $spec->[1];
}

sub set_value
{
    my ($self, $c, $prefix, $key, $value) = @_;

    if (eval { $c->{$prefix}->isa('XML::RSS::LibXML::ElementSpec') }) {
        $c->{$prefix} = +{ %{ $c->{$prefix} } };
    }
    $c->{$prefix}{$key} = $value;
}

sub validate_item { }

sub channel   { shift->rss_accessor('channel', @_) }
sub image     { shift->rss_accessor('image', @_) }
sub textinput { shift->rss_accessor('textinput', @_) }
sub skipDays  { shift->rss_accessor('skipDays', @_) }
sub skipHours { shift->rss_accessor('skipHours', @_) }

sub reset
{
    my ($self, $c) = @_;

    # internal hash
    $c->_internal({});

    # init num of items to 0
    $c->num_items(0);

    # initialize items
    $c->{items} = [];

    my $definition = $self->definition;
    while (my ($k, $v) = each(%$definition)) {
        $c->{$k} = +{%{$v}};
        bless($c->{$k}, 'XML::RSS::LibXML::ElementSpec')
            if (ref($v) eq 'XML::RSS::LibXML::ElementSpec');
    }

    return;
}

sub store_element
{
    my ($self, $container, $name, $value) = @_;

    my $v = $container->{$name};
    if (! $v || eval { $v->isa('XML::RSS::LibXML::ElementSpec') }) {
        $container->{$name} = $value;
    } elsif (ref($v) eq 'ARRAY') {
        push @$v, $value;
    } else {
        $container->{$name} = [ $v, $value ];
    }
}

sub parse_dom { }

sub parse_base
{
    my ($self, $c, $dom) = @_;
    my $xc = $c->create_xpath_context(scalar $c->namespaces);
    if (my $b = $xc->findvalue('/rss/@xml:base', $dom)) {
        $c->base($b);
    } else {
        $c->base(undef);
    }
}

sub parse_namespaces
{   
    my ($self, $c, $dom) = @_;

    my %namespaces = $self->parse_namespaces_recurse($c, $dom->documentElement());

    while (my ($prefix, $uri) = each %namespaces) {
        $c->add_module(prefix => $prefix, uri => $uri);
    }
}

sub parse_namespaces_recurse
{
    my ($self, $c, $parent) = @_;

    my %namespaces;
    foreach my $node ($parent->findnodes('./*')) {
        my %h = $self->parse_namespaces_recurse($c, $node);
        %namespaces = (%namespaces, %h);
    }
    return (%namespaces, $c->get_namespaces($parent));
}

sub parse_taxo
{
    my ($self, $c, $dom, $container, $parent) = @_;

    my $xc = $c->create_xpath_context(scalar $c->namespaces);
    my @nodes = $xc->findnodes('taxo:topics/rdf:Bag/rdf:li', $parent);
    return unless @nodes;

    my $uri = XML::RSS::LibXML::Namespaces::lookup_uri('taxo');
    if (! exists $c->namespaces->{taxo}) {
        $c->add_module(prefix => 'taxo', uri => $uri);
    }

    $container->{taxo} ||= [];
    foreach my $p (@nodes) {
        push @{ $container->{taxo} }, $p->findvalue('@resource');
    }
    $container->{$uri} = $container->{taxo};
}
    
sub parse_misc_simple
{
}

sub may_have_children {
    qw(channel item image textinput skipHours skipDays)
}

sub parse_children
{
    my ($self, $c, $node, $xpath) = @_;

    my %h;

    $xpath ||= './*';
    my $xc = $c->create_xpath_context(scalar $c->namespaces);
    foreach my $child ($xc->findnodes($xpath, $node)) {
        my $prefix = $child->getPrefix();
        my $name   = $child->localname();
        # XXX - this is probably the only case where we need to explicitly
        # normalize a name
        $name = 'textinput' if ($name eq 'textInput');
        my $val    = undef;
        if ($child->findnodes('./*')) {
            if (!grep { $_ eq $name } $self->may_have_children) {
                # Urk. Should have been encoded and wasn't! Stupid thing.
                $val = join '', map { $_->toString } $child->childNodes;
            } else {
                $val = $self->parse_children($c, $child);
            }
        } else {
            my $text   = $child->textContent();
            $text = '' if $text !~ /\S/ ;

            # argh. it has attributes. we do our little hack...
            if ($child->hasAttributes) {
                $val = XML::RSS::LibXML::MagicElement->new(
                    content => $text,
                    attributes => [ $child->attributes ]
                );
            } else {
                $val = $text;
            }
        }

        # XXX - XML::RSS now can store multiple elements in a slot.
        # This we detect and change the underlying structure from a
        # scalar to an array

        if ($prefix) {
            $h{$prefix} ||= {};
            $self->store_element($h{$prefix}, $name, $val);

            # XML::RSS requires us to allow access to elements both from
            # the prefix and the namespace
            $h{$c->{namespaces}{$prefix}} ||= {};
            $self->store_element($h{$c->{namespaces}{$prefix}}, $name, $val);
        } else {
            $self->store_element(\%h, $name, $val);
        }
    }
    return wantarray ? %h : \%h;
}

sub as_string
{
    my ($self, $c, $format) = @_;

    my $dom = $self->create_dom($c);
    return $dom->toString($format);
}

sub create_dom
{
    my ($self, $c) = @_;

    my $dom  = $self->create_document($c);
    $self->create_dtd($c, $dom);
    $self->create_pi($c, $dom);
    $self->create_rootelement($c, $dom);
    $self->create_namespaces($c, $dom);
    $self->create_channel($c, $dom);
    $self->create_items($c, $dom);

    return $dom;
}

sub create_pi
{
    my ($self, $c, $dom) = @_;

    my $styles = $c->stylesheets;
    foreach my $style (@$styles) {
        my $pi = $dom->createProcessingInstruction('xml-stylesheet');
        $pi->setData(type => 'text/xsl', href => $style);
        $dom->appendChild($pi);
    }
}

sub create_document 
{   
    my $self = shift;
    my $c    = shift;
    return XML::LibXML::Document->new('1.0', $c->encoding);
}   

sub create_rootelement {}
sub create_dtd {}
sub create_channel {}
sub create_items {}

sub create_misc_simple
{
    my ($self, $c, $dom, $parent) = @_;

    my $definition = $self->definition;
    while (my($p, $children) = each %$definition) {
        next if ! $c->{$p};

        my @nodes;
        while (my($e, $value) = each %$children) {
            if (defined $value) {
                my $node = $dom->createElement($e);
                $node->appendText($value);
                push @nodes, $node;
            }
        }

        if (@nodes) {
            my $local_parent = $dom->createElement($p);
            $local_parent->appendChild($_) for @nodes;
            $parent->appendChild($local_parent);
        }
    }
}

sub create_taxo
{
    my ($self, $c, $dom, $parent) = @_;

    my $list  = $c->{taxo};
    if (! $list || @$list <= 0) {
        return;
    }

    my $topic = $dom->createElement('taxo:topics');
    my $bag   = $dom->createElement('rdf:Bag');
    foreach my $taxo (@$list) {
        my $node = $dom->createElement('rdf:li');
        $node->setAttribute(resource => $taxo);
        $bag->appendChild($node);
    }
    $topic->appendChild($bag);
    $parent->appendChild($topic);
}

sub create_extra_modules
{
    my ($self, $c, $dom, $parent, $namespaces) = @_;

    while (my ($prefix, $uri) = each %$namespaces) {
        next if $prefix =~ /^(?:dc|syn|taxo|rss\d\d)$/;
        next if ! defined $c->{$prefix};

        while (my($e, $value) = each %{ $c->{$prefix} }) {
            my $node = $dom->createElement("$prefix:$e");
            $node->appendText($value);
            $parent->appendChild($node);
        }
    }
}
 
sub create_namespaces
{       
    my $self = shift;
    my $c    = shift;
    my $dom  = shift;
    my $root = $dom->getDocumentElement() or
        croak "No document element found?!";
    my $namespaces = $c->namespaces;
    while (my($prefix, $url) = each %$namespaces) {
        next if $prefix =~ /^rss\d\d$/;
        next if $prefix =~ /^#default$/;
        $root->setNamespace($url, $prefix, 0);
    }
} 

sub create_element_from_spec
{
    my ($self, $c, $dom, $parent, $specs) = @_;

    my $root = $dom->getDocumentElement();

    my $node;
    while (my ($e, $spec) = each %$specs) {
        my( $callback, $list );
        if (ref $spec eq 'HASH') {
            $callback = $spec->{callback};
            $list = $spec->{candidates};
        } elsif (ref $spec eq 'ARRAY') {
            $list = $spec;
        }
        foreach my $p (@$list) {
            my ($prefix, $value);
            if (ref $p && ref $p eq 'HASH') {
                if ($c->{$p->{module}}) {
                    $prefix = $p->{module};
                    $value  = $c->{$p->{module}}{$p->{element}};
                }
            } else {
                $value = $c->{$p};
            }

            if (defined $value) {
                if ($prefix) {
                    $root->setNamespace(
                        XML::RSS::LibXML::Namespaces::lookup_uri($prefix),
                        $prefix,
                        0
                    );
                } 

                $node = $dom->createElement($e);
                if (ref $value && eval { $value->isa('XML::RSS::LibXML::MagicElement') }) {
                    foreach my $attr ($value->attributes) {
                        $node->setAttribute($attr, $value->{$attr});
                    }
                } elsif ($callback) {
                    $callback->($value);
                }
                $node->appendText($value);
                $parent->appendChild($node);
                last;
            }
        }
    }
}

sub add_item
{
    my $self = shift;
    my $c    = shift;
    my $h    = ref($_[0]) eq 'HASH' ? $_[0] : {@_};

    $self->validate_item($c, $h);

    my $guid = $h->{guid};
    if (defined $guid) {
        # guid should *only* be MagicElement
        if (! eval { $guid->isa('XML::RSS::LibXML::MagicElement') }) {
            $h->{permaLink} = $guid;
        } else {
            if (my $is_permalink = $guid->{isPermaLink}) {
                if ($is_permalink eq 'true') {
                    $h->{permaLink} = $guid->{_content};
                }
            } else {
                $h->{permaLink} = $guid->{_content};
            }
        }
    } elsif (defined (my $permaLink = $h->{permaLink})) {
        $h->{guid} = XML::RSS::LibXML::MagicElement->new(
            content => $permaLink,
            attributes => { isPermaLink => 'true' }
        );
    }

    my $namespaces = $c->namespaces;
    foreach my $p (keys %$namespaces) {
        if ($h->{$p}) {
            $h->{ $namespaces->{$p} } = $h->{$p};
        }
    }

    # add the item to the list 
    if (defined($h->{mode}) && delete $h->{mode} eq 'insert') {
        unshift(@{$c->items}, $h);
    }
    else {
        push(@{$c->items}, $h);
    }

    # return reference to the list of items
    return $c->{items};
}

1;

__END__

=head1 NAME

XML::RSS::LibXML::ImplBase - Implementation Base For XML::RSS::LibXML 

=head1 SYNOPSIS

  # Internal use only

=head1 DESCRIPTION

=cut