package MModules;
use strict;

use vars qw(
  @ISA
  %ModulesLoaded
  %ModuleUnloadLists
  $ModuleEvalName
  %Subs
  $EtcList
  $AUTOLOAD
  @CurrentLoading
);

use MInitializable;
@ISA = qw(MInitializable);

use Carp;
use MCoreTools;
use File::Path;

sub _initialize {
  my ($class) = @_;
  require MDefList; initialize MDefList;
  require MObject; initialize MObject;

  MDefList->new(
    source => 'CORE',
    name => 'Subs',
    type => 'CODE',
    multiple => 0,
    mirror => \%Subs,
    validator => sub {
      my ($source, $key) = @_;
      $key =~ /^_/ and croak "Subroutine names may not start with underscores";
      $key !~ /[a-z]/ and croak "Subroutine names must contain a lowercase letter";
    },
  );

  $EtcList = MDefList->new(
    source => 'CORE',
    name => 'Etc',
  );
}

sub load_modules {
  my ($class) = @_;
  local *MODDIR;
  opendir MODDIR, rdir($::Config{mod_path}) or mudlog("Error reading modules directory: $!"), return;
  while (defined (local $_ = readdir MODDIR)) {
    next unless /^(.*)\.mod$/;
    $class->load_module($1);
  }
  closedir MODDIR;
}

sub load_module {
  (undef, my $MM_tname) = @_;

  $MM_tname =~ /^([\w!]+)$/ or croak "Bad module name '$MM_tname' passed to load_module";
  my $MM_name = lc $1;

  return 1 if $ModulesLoaded{$MM_name};

  if (grep $_ eq $MM_name, @CurrentLoading) {
    croak "Infinite recursion in module loading: @CurrentLoading $MM_name";
  }
  push @CurrentLoading, $MM_name;
  
  my $MM_path = "$::Config{mod_path}/$MM_name.mod";
  my $MM_filepath = rfile($MM_path);
  my $MM_fh = IO::File->new($MM_filepath) or do {
    mudlog("$MM_path - module file not found");
    pop @CurrentLoading;
    return 0;
  };

  # load code
  mudlog "Loading module '$MM_name'...";
  mudlog_indent();
  $MM_fh->untaint; # Safe to eval code stored on local disk, and the file path
                # has been checked.
  my $MM_code = qq{package MModules::ModuleContext;\nuse strict;\n#line 1 "$MM_filepath"\n};
  $MM_code .= <$MM_fh> until $MM_fh->eof;
  close $MM_fh;
  
  # set this before eval, to enable certain behavior with Require() and modules declaring deflists
  $ModulesLoaded{$MM_name} = 1;
  
  # execute code
  local $ModuleEvalName = $MM_name;
  eval $MM_code;
  if ($@) {
    return if $@ =~ "module load failed";
    mudlog "Error in loading $ModuleEvalName:\n$@";
    MModules->unload_module($MM_name);

    pop @CurrentLoading;
    mudlog_outdent();
    return 0;
  } else {

    pop @CurrentLoading;
    mudlog_outdent();
    return 1;
  }
}

sub unload_module {
  (undef, my $tname) = @_;

  $tname =~ /^([\w!]+)$/ or croak "Bad module name '$tname' passed to unload_module";
  my $name = $1;

  if ($ModuleUnloadLists{$name}) {
    mudlog "Unloading module '$name'...";
  
    foreach (values %{$ModuleUnloadLists{$name}}) {
      $_->();
    }
    delete $ModuleUnloadLists{$name};
  }
  delete $ModulesLoaded{$name};
  return;
}

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

  return $ModulesLoaded{$name};
}

{ package MModules::ModuleContext;
  use MCoreTools;
  
  sub AUTOLOAD {
    use vars '$AUTOLOAD';
    my ($sub) = $AUTOLOAD =~ /::([^:]+)$/;
    goto &{
       $MModules::Subs{$sub} or croak "Undefined subroutine '$sub' called from module"
    };
  }

  sub Require {
    foreach (@_) {
      MModules->loaded($_)
        or MModules->load_module($_)
        or die "Loading of prerequisite module '$_' failed";
    }
  }
    
  sub Define {
    @_ % 2 and croak "Odd number of elements in arguments for Define: @_";
    my (%lists) = @_;
    my $source = "module $MModules::ModuleEvalName";
    my $root = MDefList->root;
    
    foreach my $lname (keys %lists) {
      my $list = ($lname eq '.' ? $root : $root->get($lname)) or carp("Define(): nonexistent deflist '$lname'"), next;
      my $items = $lists{$lname};
      foreach (keys %$items) {
      
        # deflists get their items preserved
        my $old;
        if (!$list->multiple and $old = $list->get($_) and ref $old eq 'MDefList') {
          $list->remove($source, $_);
          if (ref $items->{$_} eq 'MDefList') {
            $items->{$_}->addFromList($old);
          }
        }
        
        $list->add($source, $_, $items->{$_});

        # this goes after, so if adding the item to the list dies for whatever reason, it won't be in the unload list
        my $item = $_;
        $MModules::ModuleUnloadLists{$MModules::ModuleEvalName}{"definition $lname $item"}
          = sub { print "DEBUG: unload closure for $lname / $item\n" if 0; ($lname eq '.' ? MDefList->root : MDefList->root->get($lname))->remove($source, $item); }
          unless !$list->multiple and ref($items->{$_}) eq 'MDefList';
      
      }
    }
    return;
  }
  
  sub Etc {
    my ($key, $default_val) = @_;
    return $MModules::EtcList->get($key) || $MModules::EtcList->add("module $MModules::ModuleEvalName", $key, $default_val);
  }
  
  sub Unloader {
    my ($code) = @_;
    $MModules::ModuleUnloadLists{$MModules::ModuleEvalName}{"unloader $code"} = $code;
  }
}

1;
