##
#
#    Copyright 2001, AllAfrica Global Media
#
#    This file is part of XML::Comma
#
#    XML::Comma is free software; you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation; either version 2 of the License, or
#    any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    For more information about XML::Comma, point a web browser at
#    http://xml-comma.org, or read the tutorial included
#    with the XML::Comma distribution at docs/guide.html
#
##

package XML::Comma::AbstractElement;
use vars '$AUTOLOAD';
use strict;

use XML::Comma::Util qw( dbg trim array_includes arrayref_remove );

##
# object fields
#
# _tag                 : this tag name
# _tag_up_path         : full tag pathname (colon concatenated)
# _def                 : our Def object
# _init_index          : set by parent at add time and altered by 
#                      : group_elements()   (used only to sort elements)
#
# _attrs               : a hashref holding element attributes
#
# _read_only           : true/false if this element's content and structure
#                        cannot be changed

####
# Constructor: takes a def and a tag_up_path.
#
# This is rarely, if ever, called by an end-programmer, only from
# within the Comma classes.
#
# The def is required, althouth the _init_def routine can be (and is)
# overridden by children such as Doc, Def and Bootstrap.
#
# The tag_up_path is the full set up tags up the tree, glued together
# with single colons, for example 'Envelope:element1:sub_element'. The
# tag_up_path is required.
#
####
sub new {
  my ( $class, %arg ) = @_; my $self = {}; bless ( $self, $class );
  return $self->_init ( %arg );
}

sub _init {
  my ( $self, %arg ) = @_;
  # set tag and tag_up_path
  die "no tag_up_path to init"  unless  $arg{tag_up_path};
  $self->{_tag} = substr ( $arg{tag_up_path}, rindex($arg{tag_up_path},':')+1 );
  $self->{_tag_up_path} = $arg{tag_up_path};
  $self->{_init_index} = $arg{init_index};
  $self->_init_def ( $arg{def} );
  $self->{_attrs} = {};
  # set Doc_storage if we were passed a Doc_Storage arg
  $self->{Doc_storage} = $arg{Doc_storage}  if  $arg{Doc_storage};
  return $self;
}

sub _init_def {
  $_[0]->{_def} = $_[1] || die "no def given for element creation";
  if ( my @classes = $_[0]->def->get_decorators ) {
    bless ( $_[0], Class::ClassDecorator::hierarchy(ref($_[0]),@classes) );
  }
}


########
#
# Introspection
#
########

sub tag {
  return $_[0]->{_tag};
}

sub tag_up_path {
  return $_[0]->{_tag_up_path}
}

sub def {
  return $_[0]->{_def};
}

sub def_pnotes {
  return XML::Comma::DefManager->get_pnotes ( $_[0]->{_def} );
}

sub pnotes {
  return $_[0]->{_pnotes} ||= {};
}


########
#
# HASH
#
########

sub comma_hash {
  my $self = shift();
  my $digest = XML::Comma->hash_module()->new();
  $digest->add ( $self->_get_hash_add() );
  return $digest->hexdigest();
}


########
#
# method methods -- both nested and non-nested elements allow methods
# to be defined for them.
#
########
# FIX: catch warnings (use of uninitialized value..., etc), not just errors?
sub method {
  my ( $self, $name, @args ) = @_;
  my $method = $self->def()->get_method ( $name );
  if ( $method ) {
    my $return; my @return;
    if ( wantarray ) {
      @return = eval { $method->( $self, @args ); };
    } else {
      $return = eval { $method->( $self, @args ); };
    }
    if ( $@ ) {
      XML::Comma::Log->err ( 'METHOD_ERROR',
                             "'$name' call threw error: $@" );
    }
    return wantarray ? @return : $return;
  } else {
    XML::Comma::Log->err ( 'NO_SUCH_METHOD',
                           "no method '$name' found in '" .
                           $self->tag_up_path() . "'" );
  }
}

sub method_code {
  my ( $self, $name ) = @_;
  return $self->def()->get_method ( $name );
}


##
# convenience pointer to def->applied_macros()
##

sub applied_macros {
  my $self = shift;
  return $self->def()->applied_macros ( @_ );
}


##
#
# read-only utility methods
#
##

sub set_read_only {
  my $self = shift();
  $self->{_read_only} = 1;
  # recurse if nested
  if ( $self->def()->is_nested() ) {
    foreach my $el ( $self->elements() ) {
      $el->set_read_only();
    }
  }
  return $self;
}

sub unset_read_only {
  my $self = shift();
  $self->{_read_only} = 0;
  # recurse if nested
  if ( $self->def()->is_nested() ) {
    foreach my $el ( $self->elements() ) {
      $el->unset_read_only();
    }
  }
  return $self;
}

sub get_read_only {
  return $_[0]->{_read_only};
}

sub assert_not_read_only {
  XML::Comma::Log->err ( 'READ_ONLY_ERR', 'cannot change a read_only document' )
      if $_[0]->{_read_only};
}


##
#
# Simple attribute manipulation -- simple because attribute usage is
# discouraged
#
##

# set_attr takes a list/hash of key=>value pairs and merges that into
# this elements current attr hash
sub set_attr {
  my ( $self, @rest ) = @_;
  my %hash = ( %{ $self->{_attrs} }, @rest );
  $self->{_attrs} = \%hash;
  return;
}

sub get_attr {
  my ( $self, $key ) = @_;
  return $self->{_attrs}->{$key};
}

# returns a string suitable for embedding into an open tag --
# including a leading space
sub attr_string {
  my $self = shift();
  my $string = '';
  foreach ( keys(%{$self->{_attrs}}) ) {
    $string .= " $_=\"" . $self->{_attrs}->{$_} . '"';
  }
  return $string;
}
#
##

####
# AUTOLOAD
#
#
####

sub AUTOLOAD {
  my ( $self, @args ) = @_;
  # strip out local method name and stick into $m
  $AUTOLOAD =~ /::(\w+)$/;  my $m = $1;
  $self->auto_dispatch ( $m, @args );
}


####
#
# finish_initial_read() -- called by parser so that sub-classes can do
# housekeeping and setup after being fully read in. Generally,
# sub-classes that override this method should call
# SUPER::finish_initial_read() when they're finished, so that this
# method can run any defined initial_read hooks.
#
####

sub finish_initial_read {
  unless ( $_[0]->{Doc_storage}->{read_args}->{no_read_hooks} ) {
    eval {
      foreach my $hook
        ( @{$_[0]->def()->get_hooks_arrayref('read_hook')} ) {
          $hook->( $_[0] );
        }
    }; if ( $@ ) {
      XML::Comma::Log->err
          ( 'READ_HOOK_ERROR', "in " . $_[0]->tag_up_path() . ": $@" );
    }
  }
  # destroy read_args ref
  #delete ${$_[0]->{Doc_storage}}{read_args};
}

####
#
# call_on_delete() -- When an element is deleted from a NestedElement,
# call this sub. Normally empty, but BlobElements, for example, need
# to erase their underlying files.
#
####

sub call_on_delete {
}

##
# Empty DESTROY: we don't want to autoload this
##
sub DESTROY {
#  print $_[0]->{_tag} . "\n";
#   print 'D: ' . $_[0] . "\n";
#    print '   index    ' . ($::index||'<undef>')."\n";
#    print '   ind def  ' . ($::index->{_def}||'<undef>')."\n";
#    print '   dmg def  ' . XML::Comma::DefManager->for_path('DocumentDefinition')->{_nested_lookup_table}->{nested_element}->[5] . "\n";


#    if ( (! defined $::index->{_def}) && (! $::index_dumped)) {
#      print "  $::index\n";
#      map { print "  $_ --> " . ($::index->{$_} || '<undef>') . "\n" } keys(%{$::index});
#      $::index_dumped++;
#    }
}

1;
