package LATMOS::Accounts::Bases::Objects;

use 5.010000;
use strict;
use warnings;

use overload '""' => 'stringify';

use LATMOS::Accounts::Log;
use LATMOS::Accounts::Bases::Attributes;
use LATMOS::Accounts::Utils;
use Crypt::Cracklib;

our $VERSION = (q$Rev: 2072 $ =~ /^Rev: (\d+) /)[0];

=head1 NAME

LATMOS::Accounts::Bases::Objects - Base class for account objects

=head1 SYNOPSIS

  use LATMOS::Accounts::Bases::Objects;
  LATMOS::Accounts::Bases::Objects->new($base, $type, $id);

=head1 DESCRIPTION

=head1 FUNCTIONS

=cut

=head2 is_supported

If exists, must return true or false if the object is supported or not

=cut

=head2 list($base)

List object supported by this module existing in base $base

Must be provide by object class

    sub list {
        my ($class, $base) = @_;
    }

=cut

=head2 listReal($base)

List object supported by this module existing in base $base

Can be override by base driver. The result must exclude specials object such alias.

=cut

sub listReal {
    my ($class, $base) = @_;
    $class->list($base);
}

=head2 list_from_rev($base, $rev)

List objects created or modified after base revision C<$rev>.

=cut

=head2 new($base, $id)

Create a new object having $id as uid.

=cut

sub new {
    my ($class, $base, $id, @args) = @_;
    # So can be call as $class->SUPER::new()
    bless {
        _base => $base,
        _type => lc(($class =~ m/::([^:]*)$/)[0]),
        _id => $id,
    }, $class;
}

=head2 _create($class, $base, $id, %data)

Must create a new object in database.

Is called if underling base does not override create_object

    sub _create(
        my ($class, $base, $id, %data)
    }

=cut

=head2 type

Return the type of the object

=cut

sub type {
    my ($self) = @_;
    if (ref $self) {
        return $self->{_type}
    } else {
        return lc(($self =~ /::([^:]+)$/)[0]);
    }
}

=head2 base

Return the base handle for this object.

=cut

sub base {
    return $_[0]->{_base}
}

=head2 id

Must return the unique identifier for this object

=cut

sub id {
    my ($self) = @_;
    $self->{_id}
}

=head2 AclID

Return object for acl check

=cut

sub AclID { $_[0]->id }

=head2 Iid

Return internal id if different from Id

=cut

sub Iid {
    my ($self) = @_;
    $self->id
}

=head2 stringify

Display object as a string

=cut

sub stringify {
    my ($self) = @_;

    return $self->id
}

=head2 list_canonical_fields($for)

Object shortcut to get the list of field supported by the object.

=cut

sub list_canonical_fields {
    my ($self, $for) = @_;
    $for ||= 'rw';
    $self->_canonical_fields($for);
}

=head2 attribute ($attribute)

Return L<LATMOS::Accounts::Bases::Attributes> object for C<$attribute>

=cut

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

    my $attrinfo;
    if (! ref $attribute) {
        $attrinfo = $self->_get_attr_schema(
            $self->base)->{$attribute}
        or do { return };
        $attrinfo->{name} = $attribute;
    } else {
        $attrinfo = $attribute;
    }

    return LATMOS::Accounts::Bases::Attributes->new(
        $attrinfo,
        $self,
    );
}   

sub _canonical_fields {
    my ($class, $base, $for) = @_;
    $for ||= 'rw';
    my $info = $base->_get_attr_schema($class->type);
    my @attrs = map { $base->attribute($class->type, $_) } keys %{$info || {}};
    @attrs = grep { ! $_->ro } @attrs if($for =~ /w/);
    @attrs = grep { $_->readable } @attrs if($for =~ /r/);
    @attrs = grep { !$_->hidden }  @attrs unless($for =~ /a/);
    map { $_->name } @attrs;
}

=head2 GetOtypeDef

This function is called to provide sub object definition. Must be overwritten
per object class when need.

Must return a hashref listing each sub object type and their related key atribute:

    return {
        addresses => 'user',
    }

=cut

sub GetOtypeDef {
    my ($class) = @_;

    return;
}

=head2 get_field($field)

Return the value for $field, must be provide by data base.

    sub get_field {
        my ($self, $field)
    }

=cut

=head2 get_c_field($cfield)

Return the value for canonical field $cfield.

Call driver specific get_field()

=cut

sub get_c_field {
    my ($self, $cfield) = @_;
    $self->base->check_acl($self, $cfield, 'r') or do {
        $self->base->log(LA_DEBUG, "Permission denied to get %s/%s",
            $self->id, $cfield
        );
        return;
    };
    return $self->_get_c_field($cfield);
}

=head2 get_attributes($attr)

Like get_c_field but always return an array

=cut

sub get_attributes {
    my ($self, $cfield) = @_;
    my $res = $self->get_c_field($cfield);
    if ($res) {
        return(ref $res ? @{$res} : $res);
    } else {
        return;
    }
}

sub _get_attributes {
    my ($self, $cfield) = @_;
    my $res = $self->_get_c_field($cfield);
    if ($res) {
        return(ref $res ? @{$res} : ($res));
    } else {
        return;
    }
}

sub _get_c_field {
    my ($self, $cfield) = @_;

    my $attribute = $self->attribute($cfield) or do {
        $self->base->log(LA_WARN, "Unknown attribute $cfield for object type " . $self->otype);
        return;
    };
    $attribute->readable or do {
        $self->base->log(LA_WARN, "Attribute $cfield is not readable");
        return;
    };
    return $attribute->get; 
}

=head2 GetAttributeValue($cfield)

Return the value to exposed to other base

=cut

sub GetAttributeValue {
    my ($self, $cfield) = @_;

    return $self->get_c_field($cfield);
}


sub _deref_attribute {
    my ( $self, $attribute ) = @_;

    my ( $derefAttr, $target ) = $attribute =~ /([^.]+)(.*)?/;

    my $name = $self->get_c_field( $derefAttr ) or return;

    if ( $target ) {
        my $Attr = $self->attribute( $derefAttr );
        if ( my $otype = $Attr->reference ) {
            my @values;
            foreach my $n ( ref $name ? @$name : $name ) {
                my $obj = $self->base->get_object( $otype, $n ) or return;
                my $res = $obj->_deref_attribute( $target );
                push(@values, grep { $_ } (ref $res ? @$res : $res));
            }
            return scalar(@values) <= 1 ? $values[0] : \@values
        }
    } else {
        return $name;
    }

}

=head2 queryformat ($fmt)

Return formated string according C<$fmt>

=cut

sub queryformat {
    my ($self, $fmt) = @_;
    $fmt ||= ''; # avoid undef
    $fmt =~ s/\\n/\n/g;
    $fmt =~ s/\\t/\t/g;

    my $old;
    do {
        $old = $fmt;
        $fmt =~ s&(?<!%)(?:%\{([\?!]+)?([^:}%]*)(?::([^}%]*))?\})&
            my $op = $1;
            my $attr = $2;
            my $val = '';
            my $modifier = $3 || '';
            
            if ($attr =~ /^(\w+)\((.*)\)$/) {
                $val = $self->base->QFunc($1, $2);
            } else {
                $val = $self->_deref_attribute($2);
            }

            my $res = '';
            $val = '' unless( defined( $val ) );

            if ($op) {
                if ($op eq '?') {
                    $res = $val ? $3 : '';
                } elsif ($op eq '?!') {
                    $res = $val ? '' : $3;
                }
                if ($res =~ /^(\w+)\((.*)\)$/) {
                    $res = $self->base->QFunc($1, $2);
                }
            } else {
                $res = $val;
                foreach (split('\|' , $modifier)) {
                    /upper/ and do {
                        $res = uc($res);
                        next;
                    };
                    /ucfirst/ and do {
                        $res = ucfirst($res);
                        next;
                    };
                    /lower/ and do {
                        $res = lc($res);
                        next;
                    };
                    /lcfirst/ and do {
                        $res = lcfirst($res);
                        next;
                    };
                    /ascii/ and do {
                        $res = LATMOS::Accounts::Utils::to_ascii($res);
                        next;
                    };
                    /substr\s+(\d+)\s+(\d+)?/ and do {
                        $res = substr($res, $1, $2);
                        next;
                    };
                    /join\s+(\S+)/ and do {
                        $res = join($1, ref $res ? @$res : $res );
                        next;
                    };
                    $res = sprintf('%' . ($modifier || 's'), ref $val ? join(',', @$val) : (defined($val) ? $val : ''))
                }
            }
            $res
        &egx;
    } while($old ne $fmt);
    $fmt;
}

=head2 set_fields(%data)

Set values for this object. %data is a list or peer field => values.

    sub set_fields {
        my ($self, %data) = @_;
    }

=cut

=head2 checkValues ($base, $obj, %attributes)

Allow to pre-check values when object are modified or created

C<$obj> is either the new id at object creation or the object itself on modification.

=cut

sub checkValues {
    my ($class, $base, $obj, %attributes) = @_;

    return 1;
}

=head2 check_allowed_values ($attr, $values)

Check if value C<$values> is allowed for attributes C<$attr>

=cut

sub check_allowed_values {
    my ($self, $attr, $values) = @_;
    $self->base->check_allowed_values($self->type, $attr, $values);
}

=head2 attr_allow_values ($attr)

Return allowed for attribute C<$attr>

=cut

sub attr_allow_values {
    my ($self, $attr) = @_;
    return $self->base->obj_attr_allowed_values(
        $self->type,
        $attr,
    );
}

=head2 set_c_fields(%data)

Set values for this object. %data is a list or peer 
canonical field => values. Fields names are translated.

=cut

sub set_c_fields {
    my ($self, %cdata) = @_;
    foreach my $cfield (keys %cdata) {
        $self->base->check_acl($self, $cfield, 'w') or do { 
            $self->base->log(LA_ERR, "Cannot modify %s on %s/%s: %s",
                $cfield, $self->type, $self->id, "permission denied");
            return;
        };
    }

    foreach my $cfield (keys %cdata) {
        $self->check_allowed_values($cfield, $cdata{$cfield}) or do {
            $self->base->log(LA_ERR, "Cannot modify %s/%s: %s",
                $self->type, $self->id, "non authorized value ($cfield : $cdata{$cfield}");
            return;
        };
    }

    $self->_set_c_fields(%cdata);
}

sub _set_c_fields {
    my ($self, %cdata) = @_;
    my %data;
    my $sub;
    my $res = 0;
    foreach my $cfield (keys %cdata) {

        if (my ($sotype, $key, $scfield) = $cfield =~ /^(\w+)(?:\[(\w+)\])?\.(.*)/) {
            $key ||= '_';
            $sub->{$sotype}{$key}{$scfield} = $cdata{$cfield};
            next;
        }

        my $attribute = $self->attribute($cfield) or do {
            $self->base->log(LA_ERR,
                "Cannot set unsupported attribute %s to %s (%s)",
                $cfield, $self->id, $self->type
            );
            return;
        };
        $attribute->ro and do {
            $self->base->log(LA_ERR,
                "Cannot set read-only attribute %s to %s (%s)",
                $cfield, $self->id, $self->type
            );
            return;
        };

        if (!$attribute->checkinput($cdata{$cfield})) {
            $self->base->log(LA_ERR,
                "Value for attribute %s to %s (%s) does not match requirements",
                $cfield, $self->id, $self->type
            );
            return;
        };
    }

    if (!$self->checkValues($self->base, $self, %cdata)) {
        my $last = LATMOS::Accounts::Log::lastmessage(LA_ERR);
        $self->base->log(LA_ERR,
            "Cannot update %s (%s): wrong value%s",
            $self->id, $self->type,
            ($last ? ": $last" : $last)
        );
        return;
    }

    my %updated = ();
    foreach my $cfield (keys %cdata) {
        my $attribute = $self->attribute($cfield) or do {
            $self->base->log(LA_ERR,
                "Cannot set unsupported attribute %s to %s (%s)",
                $cfield, $self->id, $self->type
            );
            return;
        };
        if ($attribute->set($cdata{$cfield})) {
            $updated{$cfield} = $attribute->monitored;
        }
    }

    my $subupdate = 0;
    if ($sub) {

        # Security: if caller is create_c_object calling it to check permission ?
        # See below
        # my ($caller) = caller();
        # my $subcreate = $caller eq 'create_c_object' ? 'create_c_object' : __SUB__;

        # Trying to create subobject
        foreach my $sotype (keys %$sub) {
            my $SubKeyRef = $self->base->GetSubObjectKey($self->otype, $sotype) or do {
                $self->base->log(LA_ERR, "Cannot create object type $sotype, subtype of " . $self->otype . " not defined");
                return;
            };
            foreach my $skey (keys %{ $sub->{$sotype} || {} }) {
                # TODO Check if object exists !
                # Building id
                my $info = $sub->{$sotype}{$skey} || {};
                # For id: if key is given using it, otherwise using random
                my $sid =
                    $info->{$SubKeyRef} ||
                    $self->id . '-' . join('', map { ('a' .. 'z')[rand(26)] } (0 .. 6));
                $info->{$SubKeyRef} = $self->id;

                # Here we don't check permission to create sub object:
                $self->base->_create_c_object($sotype, $sid, %{ $info || {} }) or return;
                $subupdate++;
            }
        }
    }


    if (keys %updated) {
        $self->PostSetAttribute() or do {
            $self->base->log(LA_ERR, "PostSetAttribute failed when updating %s/%s",
                $self->type, $self->id);
            return;
        };

        $self->ReportChange('Update', 'Attributes %s have been updated', join(', ', sort keys %updated));
        foreach (sort keys %updated) {
            $self->ReportChange('Attributes', '%s set to %s', $_, 
                (ref $cdata{$_}
                    ? join(', ', sort @{ $cdata{$_} })
                    : $cdata{$_}) || '(none)')
                if ($updated{$_});
        }
    }
    return(scalar(keys %updated) + $subupdate);
}

=head2 PostSetAttribute

This function is call to compute data when object is modify.

=cut

sub PostSetAttribute {
    my ($self) = @_;

    return 1;
}

=head2 addAttributeValue($attribute, $value)

Add a value to a multivalue attributes

=cut

sub _addAttributeValue {
    my ($self, $attribute, @values) = @_;

    my @oldvalues = grep { $_ } $self->_get_attributes($attribute);
    $self->_set_c_fields($attribute => [ @oldvalues, @values ]);
}

sub addAttributeValue {
    my ($self, $attribute, @values) = @_;

    my @oldvalues = grep { $_ } $self->_get_attributes($attribute);
    $self->set_c_fields($attribute => [ @oldvalues, @values ]);
}

=head2 delAttributeValue($attribute, $value)

Remove a value to a multivalue attributes

=cut

sub _delAttributeValue {
    my ($self, $attribute, @values) = @_;

    my @oldvalues = grep { $_ } $self->_get_attributes($attribute);

    foreach my $value (@values) {
        @oldvalues = grep { $_ ne $value } @oldvalues;
    }

    $self->_set_c_fields($attribute => @oldvalues ? [ @oldvalues, ] : undef );
}

sub delAttributeValue {
    my ($self, $attribute, @values) = @_;

    my @oldvalues = grep { $_ } $self->_get_attributes($attribute);

    foreach my $value (@values) {
        @oldvalues = grep { $_ ne $value } @oldvalues;
    }

    $self->set_c_fields($attribute => @oldvalues ? [ @oldvalues, ] : undef );
}

=head2 set_password($password)

Set the password into the database, $password is the clear version
of the password.

This function store it into userPassword canonical field if supported
using crypt unix and md5 algorythm (crypt md5), the salt is 8 random
caracters.

The base driver should override it if another encryption is need.

=cut

sub set_password {
    my ($self, $clear_pass) = @_;
    if ($self->base->check_acl($self, 'userPassword', 'w')) {
        if ($self->_set_password($clear_pass)) {
             $self->ReportChange('Password', 'user password has changed');
             return 1;
        } else {
            return;
        }
    } else {
        $self->base->log(LA_ERROR, "Permission denied for %s to change its password",
            $self->id);
        return;
    }
}

sub _set_password {
    my ($self, $clear_pass) = @_;
    if (my $attribute = $self->base->attribute($self->type, 'userPassword')) {
        my $res = $self->set_fields($attribute->iname, $self->base->passCrypt($clear_pass));
        $self->base->log(LA_NOTICE, 'Mot de passe changé pour %s', $self->id)
            if($res);
        return $res;
    } else {
        $self->base->log(LA_WARN,
            "Cannot set password: userPassword attributes is unsupported");
    }
}

=head2 check_password ($password)

Check given password is secure using L<Crypt::Cracklib>

=cut

sub check_password {
    my ( $self, $password ) = @_;
    my $dictionary = $self->base->config('cracklib_dictionnary');

    if ($password !~ /^[[:ascii:]]*$/) {
       return "the password must contains ascii characters only";
    }

    return fascist_check($password, $dictionary);
}

=head2 InjectCryptPasswd($cryptpasswd)

Inject a password encrypted using standard UNIX method.

=cut

sub InjectCryptPasswd {
    my ($self, $cryptpasswd) = @_;

    if ($self->can('_InjectCryptPasswd')) {
        return $self->_InjectCryptPasswd($cryptpasswd);
    } else {
        $self->base->log(LA_ERR, 'Injecting unix crypt password is not supported');
        return;
    }
}

=head2 search ($base, @filter)

Search object matching C<@filter>

=cut

sub search {
    my ($class, $base, @filter) = @_;
    my @results;
    my %parsed_filter;
    while (my $item = shift(@filter)) {
        # attr=foo => no extra white space !
        # \W is false, it is possible to have two char
        my ($attr, $mode, $val) = $item =~ /^(\w+)(?:(\W)(.+))?$/ or next;
        if (!$mode) {
            $mode = '~';
            $val = shift(@filter);
        }
        push(
            @{$parsed_filter{$attr}},
            {
                attr => $attr,
                mode => $mode,
                val  => $val,
            }
        );
    }
    foreach my $id ($base->list_objects($class->type)) {
        my $obj = $base->get_object($class->type, $id);
        my $match = 1;
        foreach my $field (keys %parsed_filter) {
            $base->attribute($class->type, $field) or
                la_log(LA_WARN, "Unsupported attribute %s", $field);
            my $tmatch = 0;
            foreach (@{$parsed_filter{$field}}) {
                my $value = $_->{val};
                my $fval = $obj->_get_c_field($field) || '';
                if ($value eq '*') {
                    if ($fval ne '') {
                        $tmatch = 1;
                        last;
                    }
                } elsif ($value eq '!') {
                    if ($fval eq '') {
                        $match = 1;
                        last;
                    }
                } elsif ($_->{mode} eq '=') {
                    if ($fval eq $value) {
                        $tmatch = 1;
                        last;
                    }
                } elsif($_->{mode} eq '~') {
                    if ($fval =~ m/\Q$value\E/i) {
                        $tmatch = 1;
                        last;
                    }
                }
            }
            $match = 0 unless($tmatch);
        }
        push(@results, $id) if($match);
    }
    @results;
}

=head2 attributes_summary ($base, $attribute)

Return list of values existing in base for C<$attribute>

=cut

sub attributes_summary {
    my ($class, $base, $attribute) = @_;
    my $attr = $base->attribute($class->type, $attribute) or do {
        $base->log(LA_WARN, "Cannot instantiate %s attribute for class %s", $attribute, $class->type);
        return;
    };
    if (!$attr->readable) {
        $base->log(LA_WARN, l('Attribute %s is not readable', $attribute));
        return;
    }
    if (!$base->check_acl($class->type, $attribute, 'r')) {
        $base->log(LA_WARN, l('Permission denied to read attribute %s', $attribute));
        return;
    }
    my %values;
    foreach my $id ($base->list_objects($class->type)) {
        my $obj = $base->get_object($class->type, $id);
        my $value = $obj->_get_c_field($attribute);
        if ($value) {
            if (ref $value) {
                foreach (@$value) {
                    $values{$_} = 1;
                }
            } else {
                $values{$value} = 1;
            }
        }
    }
    return sort(keys %values);
}

=head2 attributes_summary_by_object ($base, $attribute)

Return list of peer object <=> values

=cut

sub attributes_summary_by_object {
    my ($class, $base, $attribute) = @_;
    my $attr = $base->attribute($class->type, $attribute) or do {
        $base->log(LA_WARN, "Cannot instantiate %s attribute for class %s", $attribute, $class->type);
        return;
    };
    if (!$attr->readable) {
        $base->log(LA_WARN, l('Attribute %s is not readable', $attribute));
        return;
    }
    if (!$base->check_acl($class->type, $attribute, 'r')) {
        $base->log(LA_WARN, l('Permission denied to read attribute %s', $attribute));
        return;
    }
    my %values;
    foreach my $id ($base->list_objects($class->type)) {
        my $obj = $base->get_object($class->type, $id);
        my $value = $obj->_get_c_field($attribute);
        if ($value) {
            if (ref $value) {
                foreach (@$value) {
                    push(@{ $values{ $id } }, $_);
                }
            } else {
                push(@{ $values{ $id } }, $value);
            }
        }
    }
    return %values;
}

=head2 find_next_numeric_id ($base, $field, $min, $max)

Find next free uniq id for attribute C<$field>

=cut

sub find_next_numeric_id {
    my ($class, $base, $field, $min, $max) = @_;
    $base->attribute($class->type, $field) or return;
    $min ||= 
        $field eq 'uidNumber' ? 500 :
        $field eq 'gidNumber' ? 500 :
        1;
    $max ||= 65635;
    $base->log(LA_DEBUG, "Trying to find %s in range %d - %d",
        $field, $min, $max);
    my %existsid;
    $base->temp_switch_unexported(sub {
        foreach ($base->list_objects($class->type)) {
            my $obj = $base->get_object($class->type, $_) or next;
            my $id = $obj->_get_c_field($field) or next;
            $existsid{$id + 0} = 1;
        }
    }, 1);
    $min += 0;
    $max += 0;
    for(my $i = $min; $i <= $max; $i++) {
        $existsid{$i + 0} or do {
            $base->log(LA_DEBUG, "Next %s found: %d", $field, $i);
            return $i;
        };
    }
    return;
}

=head2 ListChildObjects

List dependant objects.

=cut

sub ListChildObjects {
    my ( $self ) = @_;

    return;
}

=head2 DataDump($config)

Return a structure about the object

=cut

sub DataDump {
    my ($self, $config, $base) = @_;

    # {
    #   base => # base latmoslocal: object address/thauvin-GY
    #   otype => 'otype',
    #   id => 'name',
    #   roAttrs => []
    #   Attrs => {
    #       'Attr' => [],
    #   }
    #   subObjs => {
    #       'otype' => [],
    #   }
    # }


    $config->{level} ||= 0;
    my $otype = $self->type;
    $base ||= $self->base;
    my $dump = {
        otype => $otype,
        id => ref $self ? $self->id : 'N/A',
    };
    if (ref $self) {
        $dump->{base} = $base->label;
    }

    my %roAttrs = ();
    foreach my $attr (sort { $a cmp $b } $base->list_canonical_fields($otype,
        $config->{only_rw} ? 'rw' : 'r')) {
        my $oattr = ref $self ? $self->attribute($attr) : $base->attribute($otype, $attr);
        next if ($oattr->hidden);

        if (ref $self) {
            my $val = $self->get_c_field($attr);
            if ($val || $config->{empty_attr}) {
                my @vals = ref $val ? @{ $val } : $val;
                $dump->{Attrs}{$attr} = \@vals;
                $roAttrs{ $attr } = 1 if ($oattr->ro);
            }
        } else {
            $dump->{Attrs}{$attr} = undef;
            $roAttrs{ $attr } = 1 if ($oattr->ro);
        }

        $dump->{roAttrs} = [ sort keys %roAttrs ]
            unless($config->{noSchema});
    }

    if ($config->{cb}) {
        $config->{cb}->($config, $dump);
    }

    my $SubOtype = undef;

    if (@{ $config->{SubOtype} || []}) {
        $SubOtype = { map { $_ => 1 } @{ $config->{SubOtype} } };
    }

    if (ref $self && $config->{recur}) {
          my %subobj = $self->ListChildObjects;
          foreach my $otype (sort keys %subobj) {
              if ($SubOtype) {
                  $SubOtype->{$otype} or next;
              }
              foreach my $oname (sort @{ $subobj{$otype} }) {
                  my $obj = $self->base->get_object($otype, $oname) or next;
                  push(@{ $dump->{subObjs}{$otype} }, $obj->DataDump({ %{$config || {}}, recur => $config->{recur}, level => $config->{level} + 2 }));
              }
          }
    }

    return $dump;
}

=head2 text_dump ($handle, $config, $base)

Dump object into C<$handle>

=cut

sub text_dump {
    my ($self, $handle, $config, $base) = @_;
    print $handle $self->dump($config, $base);
    return 1;
}

=head2 dump

Return dump for this object

=cut

sub dump {
    my ($self, $InitConfig, $base) = @_;

    $InitConfig->{level} ||= 0;
    $base ||= $self->base;
    my $dump = '';

    $InitConfig->{cb} = sub {
        my ( $config, $Dump ) = @_;

        if ($config->{level}) {
            $dump .= "\n";
        }

        if (ref $self) {
            $dump .= sprintf "%s# base %s: object %s/%s\n",
                ' ' x $config->{level},
                $Dump->{base}, $Dump->{otype}, $Dump->{id};
        }
        $dump .= sprintf(
            "%s# %s\n",
            ' ' x $config->{level},
            scalar(localtime)
        );

        my %roAttrs = map { $_ => 1 } @{ $Dump->{roAttrs} || [] };

        foreach my $attr (sort { $a cmp $b } sort keys %{ $Dump->{Attrs} || {} }) {
            my $val = $Dump->{Attrs}{$attr};
            my $oattr = $base->attribute($Dump->{otype}, $attr);
            if ($val) {
                if (my @allowed = $base->obj_attr_allowed_values($Dump->{otype}, $attr)) {
                    $dump .= sprintf("%s# %s must be%s: %s\n",
                        ' ' x $config->{level},
                        $attr,
                        ($oattr->mandatory ? '' : ' empty or either'),
                        join(', ', @allowed)
                    );
                }

                foreach (@$val) {
                    $_ ||= '';
                    s/\r?\n/\\n/g;
                    $dump .= sprintf("%s%s%s%s:%s\n",
                        ' ' x $config->{level},
                        $roAttrs{$attr} ? '# (ro) ' : '',
                        $config->{level} ? $Dump->{otype} . '[' . $Dump->{id} . '].' : '',
                        $attr, $_ ? " $_" : '');
                }
            } elsif ( $config->{empty_attr} || ! ref $self) {
                if (my @allowed = $base->obj_attr_allowed_values($Dump->{otype}, $attr)) {
                    $dump .= sprintf("%s# %s must be%s: %s\n",
                        ' ' x $config->{level},
                        $attr,
                        ($oattr->mandatory ? '' : ' empty or either'),
                        join(', ', @allowed)
                    );
                }
                $dump .= sprintf("%s%s%s%s:\n",
                    ' ' x $config->{level},
                    $roAttrs{$attr} ? '# (ro) ' : '',
                    $config->{level} ? $Dump->{otype} . '[' . $Dump->{id} . '].' : '',
                    $attr);
            }
        }
    };

    $self->DataDump($InitConfig, $base);

    return $dump;
}

=head2 ReportChange($changetype, $message, @args)

Possible per database way to log changes

=cut

sub ReportChange {
    my ($self, $changetype, $message, @args) = @_;

    $self->base->ReportChange(
        $self->type,
        $self->id,
        $self->Iid,
        $changetype, $message, @args
    )
}

1;

__END__


=head1 SEE ALSO

L<LATMOS::Accounts::Bases>

=head1 AUTHOR

Thauvin Olivier, E<lt>olivier.thauvin.ipsl.fr@localdomainE<gt>

=head1 COPYRIGHT AND LICENSE

Copyright (C) 2009 by Thauvin Olivier

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself, either Perl version 5.10.0 or,
at your option, any later version of Perl 5 you may have available.

=cut
