package MScheduler;

use MCoreTools;
use Carp;

use strict;
use vars qw(
  @Tasks $CurTask
  $iter $lastick
  $Run
  $Performance
);

use constant TIME_SCALE => 12; # number of mud-seconds per real-second.

BEGIN { eval($^O eq 'MacOS' ? <<'ET_MAC' : <<'ET_OTHER'); die $@ if $@; }
use Mac::Events ();
use constant MACEVENTS => 1;
use vars qw($TimeBase);
$TimeBase ||= time() - Mac::Events::TickCount();
sub mudclock {
  return ($TimeBase + Mac::Events::TickCount()/ 60) * TIME_SCALE;
}
ET_MAC
use constant MACEVENTS => 0;
sub mudclock {
  return (time() - 977500000) * TIME_SCALE; # FIXME: really ought to use Time::HiRes
}
ET_OTHER

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

sub run {
  my ($me) = @_;
  mudlog "Scheduler initializing...";
  $me->_init;
  mudlog "Startup complete.";
  $me->_loop;
  mudlog "Main loop ended.";
  $me->_wrapup;
}

sub _init {
  $iter = $lastick = 0;
  $Run = 1;
  MScheduler->add_task(name => 'Tick', hook => sub {
    MScheduler->reset_me(TICK_INTERVAL);
    MObject->all_do(sub{$_[0]->tick});
    $Performance = sprintf '%.2f iters/sec - %i iters in %i seconds', $iter / (time() - $lastick), $iter, (time() - $lastick)
      unless $iter == 0 or $lastick == time();
    $iter = 0;
    $lastick = time();
  });
  MScheduler->add_task(name => 'Autosave Players', hook => sub {
    MScheduler->reset_me(60*60*12);
    foreach (values %MConnection::Connections) {
      my $po = $_->object;
      next unless $po;
      $po->save_player();
    }
  });
  MConnection->start_prompts;
}

sub _loop {
  while ($Run) {
    $iter++;
    Mac::Events::WaitNextEvent(1) if MACEVENTS;

    MConnection->all_idle();
      
    if ($Tasks[0]{'time'} <= mudclock()) {
      local $CurTask = shift @Tasks;
      _run_hook($CurTask, 'hook');
    }
  }
}

sub _wrapup {
  for (my $i = 0; $i < @Tasks; $i++) {
    _run_hook($Tasks[$i], 'abort');
  }
}

sub stop {
  $Run = 0;
}

sub add_task {
  my ($class, %params) = @_;
  
  my $task = {
    owner => $params{owner} ? (ref $params{owner} ? $params{owner}->id : $params{owner}) : undef,
    hook => $params{hook},
    'time' => ($params{'time'} || 0) + mudclock(),
    name => $params{name},
    abort => $params{abort},
  };
  MScheduler->_insert_task($task);
}

sub remove_task {
  my ($class, $name) = @_;
  for (my $i = 0; $i < @Tasks; $i++) {
    next if $Tasks[$i]{name} ne $name;
    _run_hook($Tasks[$i], 'abort');
    splice @Tasks, $i, 1;
    last;
  }
}  

sub remove_owned {
  my ($class, $owner, %opts) = @_;
  $owner = $owner->id if ref $owner;
  for (my $i = 0; $i < @Tasks; $i++) {
    next unless $Tasks[$i]{owner} and $Tasks[$i]{owner} == $owner 
      and (!$opts{match} or $opts{match}->($Tasks[$i]{name}));
    _run_hook($Tasks[$i], 'abort');
    splice @Tasks, $i--, 1; # $i decremented to match array shifting
  }
}

sub task_owner {
  my ($class, $name) = @_;
  for (my $i = 0; $i < @Tasks; $i++) {
    next if $Tasks[$i]{name} ne $name;
    return $Tasks[$i]{owner};
  }
  return;
}

sub reset_me {
  my ($class, $newtime) = @_;
  $CurTask or confess "MScheduler::reset_me called outside of task hook";
  $CurTask->{'time'} += $newtime;
  MScheduler->_insert_task($CurTask);
}

sub _run_hook {
  my ($tstruct, $key) = @_;
  $tstruct->{$key} or return 0;
  eval {$tstruct->{$key}->( owner => $tstruct->{owner} )};
  if ($@) {
    (my $lt = $@) =~ s#\n# / #g;
    mudlog qq~ERROR/SCHEDULER: death while running $key of task $tstruct->{name}: $lt~;
  }
  1;
}

sub _insert_task {
  my ($class, $task) = @_;

  my $insert_pos = &{sub{
    return 0 unless @Tasks;
    return 0 if $task->{'time'} <= $Tasks[0]{'time'};
    return $#Tasks + 1 if $Tasks[-1]{'time'} <= $task->{'time'};
    my ($lower, $upper) = (0, $#Tasks);
    while ($lower < $upper) {
      return $upper if $upper == $lower + 1;
      my $middle = int(($upper - $lower) / 2 + $lower);
      # print "upper: $upper middle: $middle lower: $lower\n";
      if ($task->{'time'} > $Tasks[$middle]{'time'}) {
        $lower = $middle;
      } elsif ($task->{'time'} < $Tasks[$middle]{'time'}) {
        $upper = $middle;
      } else {
        return $middle + 1;
      }
    }
    die "dainbramage: binary search on scheduler list failed";
  }};
  splice @Tasks, $insert_pos, 0, $task;
}

sub report {
  my ($class) = @_;
  my @rep;

  push @rep, 'Now: ' . format_time(mudclock()) . ", Scale: " . TIME_SCALE . ":1 MUD:real";
  push @rep, sprintf("%-55s %15s %-6s", qw(Name Runs-In Owner));

  push @rep, '-' x 55 . ' ' . '-' x 15 . ' ' . '-' x 6;
  
  foreach (@Tasks) {
    push @rep, sprintf("%-55s %15s %-6s", $_->{name}, format_time($_->{'time'} - mudclock()), $_->{owner} || 'n/a');
  }
  return @rep;
}

sub performance {$Performance}

1;
