package UML::MetaMetaModel::FactoryBoot;

use 5.6.1;
use strict;
#use warnings;

our $AUTHOR = q{ ks.perl@kurtstephens.com 2004/05/06 };
our $VERSION = do { my @r = (q$Revision: 1.4 $ =~ /\d+/g); sprintf "%d." . "%03d" x $#r, @r };

=head1 NAME

UML::MetaMetaModel::FactoryBoot - Bootstrapping factory

=head1 SYNOPSIS

=head1 DESCRIPTION

This module is used to create a bootstrapping UML meta-meta-model from
the meta-model description.

=head1 USAGE

=head1 EXPORT

None exported.

=head1 AUTHOR

Kurt Stephens, ks.perl@kurtstephens.com 2003/05/06

=head1 SEE ALSO

L<UML::XMI|UML::XMI>

=head1 VERSION

$Revision: 1.4 $

=head1 METHODS

=cut


####################################################################################

use base qw(UML::MetaMetaModel::Factory);

####################################################################################

use UML::MetaMetaModel::Util qw(:all);

use Carp qw(confess);

####################################################################################

sub initialize
{
  my ($self) = @_;
  
  $self->{'classMap'} ||= 1;

  $self->SUPER::initialize;

  $self->{'packagePrefix'} ||= 'UML::MetaMetaModel';

  $self;
}


#######################################################################


my @cls;
my %cls;
my %attr;
my @assoc;
my $__cls_supers;
my $__cls_method = { };
my $__cls_method_patch;

my $base_package = __PACKAGE__ . '::Object';

my @obj;
our @factory_map;
our $model;


sub model
{
  confess("no model") unless $model;
  $model;
}

sub factory_map
{
  confess("no model") unless @factory_map;
  \@factory_map;
}


sub factory
{
  UML::MetaMetaModel::FactoryBoot::Object->__factory;
}


sub __true { 1 }


sub create
{
  my ($self, $meta, @args) = @_;

  # $DB::single = 1;

  # Translate meta name to fully-qualified name, if possible.
  my $meta_q = $meta;
  my $meta_cls = $cls{$meta};
  if ( $meta_cls ) {
    # $DB::single = 1;
    $meta_q = $meta_cls->{'.name_q'};
  }

  # Construct an object.
  my $obj = $base_package->new
  (
   '.className' => $meta_q,
   '.class' => $meta_cls,
   ".isa$meta" => 1,
   @args,
   );
  
  # Bless it into some package.
  {
    # If we have a meta Classifier object,
    # use it, otherwise punt until later.
    my $cls = $cls{$meta_q} || $cls{$meta};
    $cls = $cls && $cls->{'.package'};
    $cls ||= $base_package;
    
    bless($obj, $cls);
  }


  # Get a fully qualified name for the object.
  my ($name, $name_q);
  if ( $obj->{'name'} ) {
    my @pkg;
    my $ns = $obj;
    while ( $ns->{'namespace'} ) {
      push(@pkg, $ns->{'name'});
      $ns = $ns->{'namespace'};
    }
    @pkg = reverse @pkg;
    $name = $obj->{'name'};
    $name_q = join('::', @pkg);
    $obj->{'.name_q'} = $name_q;
  }


  ##########################################################################
  ## Initialization
  ##
  ## All of these initializers are hand-coded.
  ## This should cover all the
  ## the objects generated by UML::Import::MetaMetaModel.
  ##
  ## They are arranged in Generalization parent pre-order so
  ## things like namespace can get set up by subclasses
  ## before the Namespace initializer.
  ##
  ## If anything major changes in MetaModel.spec,
  ## esp. in the Foundation package.
  ## This code might be broken.
  ##
  my $x;

  # die("Attribute 2561!!!\n") if $obj->{'_id'} eq 2561;

  # Multiplicity
  if ( grep($meta eq $_, 'Multiplicity') ) {
    $obj->{'range'} ||= [ ];
  }

  # EnumerationLiteral
  if ( grep($meta eq $_, 'EnumerationLiteral') ) {
    __many_one($obj, 'literal', 'enumeration');
    $obj->{'namespace'} ||= $obj->{'enumeration'};
    # $DB::single = 1;
  }

  # Enumeration
  if ( grep($meta eq $_, 'Enumeration') ) {
    __one_many($obj, 'enumeration', 'literal');
    grep($_->{'namespace'} = $obj, @{$obj->{'literal'}});
  }

  # AssociationEnd
  if ( $meta eq 'AssociationEnd' ) {
    __many_one($obj, 'association', 'participant');
    __many_one($obj, 'connection', '_association'); # HACK!!!!
  }

  # Association
  if ( grep($meta eq $_, 'Association', 'AssociationClass') ) {
    $obj->{'.isaAssociation'} = 1;
    __one_many($obj, '_association', 'connection'); # HACK!!!!

    my $x = $obj->{'connection'};
    if ( 0 &&
	 grep($_->{'name'} eq 'connection', @$x) &&
	 grep($_->{'name'} eq '_association', @$x) && 1 ) {
      $DB::single = 1;
    }

    push(@assoc, $obj);
  }
  
  # StructuralFeature
  if ( grep($meta eq $_, 'Attribute') ) {
    $obj->{'.isaStructuralFeature'} = 1;
    __many_one($obj, 'typedFeature', 'type');

    $attr{$obj->{'owner'}}{$obj->{'name'}} = $obj;
    $obj->{'namespace'} ||= $obj->{'owner'};
  }

  # Feature
  if ( grep($meta eq $_, 'Attribute') ) {
    $obj->{'.isaFeature'} = 1;
    __many_one($obj, 'feature', 'owner');
  }

  # Class
  if ( grep($meta eq $_, 'Class', 'AssociationClass') ) {
    $obj->{'.isaClass'} = 1;
  }

  # Model
  if ( grep($meta eq $_, 'Model') ) {
    $obj->{'.isaModel'} = 1;
    $model ||= $obj;
  }  

  # Package
  if ( grep($meta eq $_, 'Model', 'Package') ) {
    $obj->{'.isaPackage'} = 1;
    $obj->{'importedElement'} ||= [ ];
  }

  # Namespace
  if ( grep($meta eq $_, 'Model', 'Package', 'Class', 'AssociationClass', 'Enumeration', 'Primitive') ) {
    $obj->{'.isaNamespace'} = 1;
    __one_many($obj, 'namespace', 'ownedElement');

    my $package = join("::", 
			      grep(length($_),
				   $self->{'packagePrefix'},
				   $name_q,
				   )
			      );
    # Remember the Classifier's Perl package.
    # $DB::single = 1;
    $obj->{'.package'} = $package;

    # Make a package to bless into.
    # Make the package bounce up to the $base_package.
    eval qq{
      package $package;
      our \@ISA = qw($base_package);
      our \$VERSION = '3.1415926';

      sub __model_name { '$name_q' }

    }; die $@ if $@;

    # Patch in the classifier, so UML::Export::XMI will work.
    {
      no strict 'refs';

      *{"${package}::__classifier"} = sub { $obj; }; 

      *{"${package}::isa$name"} = \&__true;

      my $name_ = $name_q;
      $name_ =~ s/::/__/sg;
      *{"${package}::isa$name_"} = \&__true;
    }

    # Add to factory map.
    push(@factory_map,
	 $name, $package,
	 $name_q, $package,
	 );
	 
  }

  # Classifier
  if ( grep($meta eq $_, 'Class', 'AssociationClass', 'Enumeration', 'Primitive') ) {
    $obj->{'.isaClassifier'} = 1;
    __one_many($obj, 'owner', 'feature');
    __one_many($obj, 'particpant', 'association');

    confess("Class name collision '$name_q'") if $cls{$name_q};
    warn("short Class name collision '$name'") if $cls{$name};

    push(@cls, $obj);
    $cls{$name_q} = $obj;
    $cls{$name} = $obj;
  }

  # Generalization
  if ( grep($meta eq $_, 'Generalization') ) {
    $obj->{'.isaGeneralization'} = 1;
    __many_one($obj, 'generalization', 'child');
    __many_one($obj, 'specialization', 'parent');
  }

  # Dependency
  if ( grep($meta eq $_, 'Usage') ) {
    $obj->{'.isaDependency'} = 1;
    __one_many($obj, 'supplier', 'supplierDependency');
    __one_many($obj, 'client', 'clientDependency');
  }

  # Relationship
  if ( grep($meta eq $_, 'Generalization', 'Usage') ) {
    $obj->{'.isaRelationship'} = 1;
  }

  # GeneralizableElement
  if ( grep($meta eq $_, 'Model', 'Package', 'Class', 'AssociationClass', 'Enumeration', 'Primitive') ) {
    $obj->{'.isaGeneralizableElement'} = 1;
    __one_many($obj, 'child', 'generalization');
    __one_many($obj, 'parent', 'specialization');
  }

  # ModelElement
  if ( 1 ) {
    $obj->{'.isaModelElement'} = 1;
    __many_one($obj, 'ownedElement', 'namespace');
  }
  
  # Element
  if ( 1 ) {
    $obj->{'.isaElement'} = 1;
  }

  if ( 0 && $obj->{'.isaAssociationClass'} ) {
    print STDERR "$meta_q $obj->{name} {\n\t";
    print STDERR join("\n\t", map("$_ = " . $obj->{$_}, sort keys %$obj));
    print STDERR "\n} = $obj\n";
    
    # $DB::single = 1;
    #$DB::single = 1 if $meta eq 'Generalization';
    #$DB::single = 1 if $meta eq 'Attribute';
    $DB::single = 1 if $meta eq 'AssocationEnd';
  }

  # confess("FOAOSDFOSDFO") unless $obj->{'.name_q'};

  push(@obj, $obj);

  $obj;
}


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

  # $DB::single = 1;

  if ( 0 ) {
    print STDERR "========================================\n";
    print STDERR "== flush $kind\n";
  }

  if ( $kind eq 'Generalization' ) { 
    # Generalizations have all been added.
    # Now start caching in __cls_supers().
    $__cls_supers = { };

    # Backpatch @ISA.
    for my $cls ( @cls ) {
      my $pkg = $cls->{'.package'};

      my @isa = map($_->{'.package'},
		    map($_->{'parent'},
			@{$cls->{'generalization'}},
			)
		    );

      push(@isa, $base_package) unless @isa;
      # $DB::single = 1 if @isa;

      {
	no strict 'refs';
	@{"${pkg}::ISA"} = @isa;
      }
    }
  }
  elsif ( $kind eq 'Attribute' ) {
    # Attributes have all been added.
    # Now start caching accessors.
    $__cls_method = { };
  }
  elsif ( $kind eq 'Association' ) {
    # Attributes and Associations have all been added.

    for my $obj ( @assoc ) {
      # Add AssociationEnds as pseudo-Attributes.
      for my $end ( @{$obj->{'connection'}} ) {
	my $cls = $end->{'participant'};
	for my $end_x ( grep($_ ne $end, @{$obj->{'connection'}}) ) {
	  if ( $end_x->{'name'} ) {
	    # confess("FKSDFKSDLFKSD") if $end_x->{'name'} eq 'ownedElement';
	    $end_x->{'.owner'} = $cls; # So $pkg_impl will work.
	    $attr{$cls}{$end_x->{'name'}} = $end_x;
	  }
	}
      }
    }

    # Recache accessors and patch them in.
    $__cls_method = { };
    $__cls_method_patch = 1;
  }
  elsif ( $kind eq 'Model' ) {
    # Rebless into Class package.
    for my $obj ( @obj ) {
      my $cls = $cls{$obj->{'.className'}};
      $obj->{'.className'} = $cls->{'.name_q'}; # Backpatch.
      bless($obj, $cls->{'.package'});
    }
  }

  $self;
}



sub __add_if_absent
{
  my $a = $_[0] ||= [ ];
  shift;
  for my $x ( @_ ) {
    push(@$a, $x) unless grep($_ eq $x, @$a);
  }
}


sub __one_many
# $one_obj, $one_role, $many_role
{
  my ($one_obj, $one_role, $many_role) = @_;

  #local $" = ', '; print STDERR "$one_obj->{__className} $one_role 1 === $many_role *\n";
  grep($_->{$one_role} = $one_obj, @{$one_obj->{$many_role} ||= [ ]});
}


sub __many_one
# $many_obj, $many_role, $one_role
{
  my ($many_obj, $many_role, $one_role) = @_;

  my $x  = $many_obj->{$one_role};
  #local $" = ', '; print STDERR "$many_obj->{__className} $many_role * === $one_role $x";
  
  __add_if_absent($x->{$many_role}, $many_obj) if $x;
}



sub __cls_supers
{
  my ($cls) = @_;
  my $x = $__cls_supers && $__cls_supers->{$cls};
  unless ( $x ) {
    my @supers;
    my @x = ( $cls );
    while ( @x ) {
      my $cls = pop @x;
      next if grep($_ eq $cls, @supers);
      push(@supers, $cls);
      push(@x, map($_->{'parent'}, @{$cls->{'generalization'}}));
    }

    $x = \@supers;
    $__cls_supers->{$cls} = $x if $__cls_supers;

    if ( 0 ) {
      print STDERR "\n\n__cls_supers $cls->{name} = ", join(', ',  map($_->{'name'}, @supers)), "\n\n\n"
      if $__cls_supers;
    }
  }

  @$x;
}


sub __cls_attr
{
  my ($cls, $name) = @_;

  for my $super ( __cls_supers($cls) ) {
    my $attr = $attr{$super}{$name};
    return $attr if $attr;
  }

  confess("Unknown Attribute '$name' in Class '$cls->{name}'") if $__cls_method_patch;

  undef;
}



#######################################################################


sub __cls_method
{
  my ($self, $operation, $args) = @_;

  # Lookup Class object.
  my $cls = $self->{'.class'} ||= $cls{$self->{'.className'}};

  # Rebless into Class package?
  if ( $cls && ref($self) eq $base_package ) {
    $self->{'.className'} = $cls->{'.name_q'}; # Backpatch.
    bless($self, $cls->{'.package'});
  }

  my $pkg_impl;
  my $method = $__cls_method && $__cls_method->{$cls}{$operation};

  if ( ! $method ) {
    if ( $operation =~ /^isa([A-Z][A-Za-z_0-9]*)$/ ) {
      my $target_cls = $1;
      
      my $value = $self->{".$operation"};
      return $value if defined $value;
      
      $value = grep($_ eq $target_cls, map($_->{'name'}, __cls_supers($cls)));
      $value ||= 0;
      
      # $self->{".$operation"} = $value;
      return $value;
    }
    elsif ( $operation =~ /^add_(.*)$/ ) {
      my $a = $1;
      my $attr = $cls && __cls_attr($cls, $a);
      # $DB::single = 1;
      if ( $attr ) {
	# $DB::single = 1;

	$pkg_impl = $attr;

	unless ( $method = $attr->{'.adder'} ) {
	  if ( Multiplicity_upper($attr->{'multiplicity'}) ne '1' ) {
	    $method = sub {
	      my $self = shift;
	      my $x = $self->{$a} ||= [ ];
	      @$x = @_;
	      $self;
	    };
	  }
	  
	  $attr->{'.adder'} = $method;
	}
      } else {
	# Fall-back method.
	$method = sub {
	  my $self = shift;
	  my $x = $self->{$a};
	  
	  confess("Not a ref") if grep(! ref($_), @_);
	  
	  push(@$x, @_);
	  
	  $self;
	};
      }
    }
    elsif ( $operation =~ /^set_(.*)$/ ) {
      my $a = $1;
      my $attr = $cls && __cls_attr($cls, $a);
      if ( $attr ) {
	$pkg_impl = $attr;

	# $DB::single = 1;

	unless ( $method = $attr->{'.setter'} ) {
	  if ( Multiplicity_upper($attr->{'multiplicity'}) eq '1' ) {
	    $method = sub {
	      $_[0]->{$a} = $_[1];
	      $_[0];
	    };
	  } else {
	    $method = sub {
	      my $self = shift;
	      my $x = $self->{$a} ||= [ ];
	      @$x = @_;
	      $self;
	    };
	  }
	  
	  $attr->{'.setter'} = $method;
	}
      } else {
	# Fall-back method.
	$method = sub {
	  my $self = shift;
	  my $x = $self->{$a};
	  
	  if ( ref($x) eq 'ARRAY' ) {
	    confess("Not ref") if grep(! ref($_), @_);
	    @$x = @_;
	  } else {
	    if ( @_ > 1 ) {
	      $self->{$a} = \@_;
	    } else {
	      $self->{$a} = $_[0];
	    }
	  }
	  
	  $self;
	};
      }
    } else {
      # Getter
      my $a = $operation;
      my $attr = $cls && __cls_attr($cls, $a);
      if ( $attr ) {
	$pkg_impl = $attr;
	
	# $DB::single = 1;
	unless ( $method = $attr->{'.getter'} ) {
	  # $DB::single = 1 if $a eq 'upper';
	  if ( Multiplicity_upper($attr->{'multiplicity'}) ne '1' ) {
	    $method = sub {
	      my $x = $_[0]->{$a} ||= [ ];
	      wantarray ? @$x : $x;
	    };
	  } else {
	    $method = sub {
	      $_[0]->{$a};
	    };
	  }
	  $attr->{'.getter'} = $method;
	}
      } else {
	# Fall-back method.
	$method = sub {
	  my $x = $_[0]->{$a};
	  
	  # $DB::single = 1 if $a eq 'ownedElement' && $_[0]->{'name'} eq 'Model_Management';
	  
	  if ( wantarray ) {
	    if ( ref($x) eq 'ARRAY' ) {
	      return @$x;
	    } else {
	      if ( exists $_[0]->{$a} ) {
		return +( $x );
	      } else {
		return ();
	      }
	    }
	  } else {
	    return $x;
	  }
	};
	
      }
    }

    $__cls_method->{$cls}{$operation} = $method if $__cls_method;
  }

  if ( $method ) {
    if ( $pkg_impl && $__cls_method_patch ) {
      no strict 'refs';

      $pkg_impl = $pkg_impl->{'owner'} || $pkg_impl->{'.owner'} if ref($pkg_impl);
      $pkg_impl = $pkg_impl->{'.package'} if ref($pkg_impl);
      
      # $DB::single = 1;
      # print STDERR "Patching ${pkg_impl}->${operation}()\n";

      *{"${pkg_impl}::${operation}"} = $method;
    }

    $method->($self, @$args);
  } else {
    confess("Unknown Method '$operation' on Class '$self->{__className}'");
  }
}


#######################################################################

package UML::MetaMetaModel::FactoryBoot::Object;



my $_id = 0;

sub new
{
  my ($self, %opts) = @_;

  $opts{'_id'} ||= ++ $_id;

  bless(\%opts, ref($self) || $self)->_initialize;
}


sub _initialize
{
  $_[0];
}


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

  $DB::single = 1;

  $self = bless({ %$self }, ref($self));

  $self->{'_id'} .= '.' . ++ $_id;

  for my $key ( keys %$self ) {
    my $v = $self->{$key};
    if ( ref($v) eq 'ARRAY' ) {
      $self->{$key} = [ @$v ];
    } elsif ( ref($v) eq 'HASH' ) {
      $self->{$key} = { %$v };
    }
  }

  $self;
}



sub __model
{
  $UML::MetaMetaModel::FactoryBoot::model;
}


my $__factory;
sub __factory
{
  unless ( $__factory ) { 
    # $DB::single = 1;

    $__factory = UML::MetaMetaModel::Factory
    ->new(
	  'classMap' => UML::MetaMetaModel::FactoryBoot->factory_map
	  );						  
  }
  $__factory;
}



our $AUTOLOAD;

sub AUTOLOAD
{
  no strict 'refs';
  
  my ($self, @args) = @_;
  local ($1, $2);
  
  my ($package, $operation) = $AUTOLOAD =~ m/^(?:(.+)::)([^:]+)$/;
  return if $operation eq 'DESTROY';
  
  UML::MetaMetaModel::FactoryBoot::__cls_method($self, $operation, \@args);
}


#######################################################################




####################################################################################

1;

####################################################################################


### Keep these comments at end of file: ks.perl@kurtstephens.com 2003/04/06 ###
### Local Variables: ###
### mode:perl ###
### perl-indent-level:2 ###
### perl-continued-statement-offset:0 ###
### perl-brace-offset:0 ###
### perl-label-offset:0 ###
### End: ###

