##########################################################################
# This file is part of Vacuum Magic
# Copyright (C) 2008 by UPi <upi at sourceforge.net>
##########################################################################

use strict;
use Compress::Zlib;    # Compression of $RecordedKeys
use MIME::Base64;      # Encoding of compressed data

=comment
A recorded game contains the following information
* The version of the software
* Number of players ($NumGuys)
* The level set (NormalGame / NightmareFlight / ..)
* Difficulty setting
* The recorded random numbers
* The recorded key presses
=cut


#############################################################################
# Game Recorder
#############################################################################

use vars qw ($RecorderOn $RecorderNumGuys $RecorderLevelSet $RecorderDifficultySetting @RecordedRand $RecordedKeys @RecorderPlayerKeys);

sub StartRecorder {
  $RecorderOn = 1;
  &ResetRecorder();
}

sub StopRecorder {
  $RecorderOn = 0;
}

sub ResetRecorder {
  @RecordedRand = ();
  $RecordedKeys = '';
  $RecorderNumGuys = $NumGuys;
  $RecorderLevelSet = $LevelSet;
  $RecorderDifficultySetting = $DifficultySetting;
  @RecorderPlayerKeys = map { @{$_->{keys}} } @Players[0 .. $NumGuys - 1];
}

sub AdvanceRecorder {
  my ($advance) = @_;
  
  my $keysAsString = join('', map { $Keys{$_} ? 1 : 0 } @RecorderPlayerKeys);
  $RecordedKeys .= $keysAsString x $advance;
}


sub AddRandToRecorder {
  push @RecordedRand, $_[0];
  # warn "A ", scalar(@RecordedRand), ":\t$_[0]";
  $_[0];
}

sub SaveRecord {
  my $filename = shift;
  my ($length);
  
  $length = int(length($RecordedKeys) / $RecorderNumGuys / 5);
  
  open RECORD, ">$filename"  or die "Cannot open $filename: $!";
  print RECORD "Vacuum Magic $::Version record file
Length = $length
NumGuys = $RecorderNumGuys
LevelSet = $RecorderLevelSet
DifficultySetting = $RecorderDifficultySetting
Rand = ", &GetRecordedRand(), "\n", GetRecordedKeys();
  close RECORD;
}

sub GetRecordedKeys {
  return encode_base64(compress($RecordedKeys));
}

sub GetRecordedRand {
  return join(',', @RecordedRand);
}

sub ReadRecordHeader {
  local *RECORD = shift;
  my ($line, $length, $numGuys, $levelSet, $difficultySetting);
  
  $line = <RECORD>;
  $line =~ s/[\r\n]+//;
  return "Error reading record file: wrong game version: '$line'"  unless $line eq "Vacuum Magic $::Version record file";
  $line = <RECORD>;
  ($length) = $line =~ /^Length = (\d+)\s/  or return "Error 0 reading record file..";
  $line = <RECORD>;
  ($numGuys) = $line =~ /^NumGuys = ([1-6])\s/  or return "Error 1 reading record file.";
  $line = <RECORD>;
  ($levelSet) = $line =~ /^LevelSet = ([\w\/]+)\s/  or return "Error 2 reading record file.";
  $line = <RECORD>;
  ($difficultySetting) = $line =~ /^DifficultySetting = ([0-3])\s/  or return "Error 4 reading record file.";
  return ($length, $numGuys, $levelSet, $difficultySetting);
}

sub LoadRecord {
  my ($filename, $silent) = @_;
  my ($line, $error, $length, $numGuys, $levelSet, $difficultySetting, $rand, @rand, $keys);
  
  Carp::confess  unless $filename;
  open RECORD, $filename  or return "Cannot open $filename: $!";
  ($error, $numGuys, $levelSet, $difficultySetting) = &ReadRecordHeader(*RECORD{IO});
  return $error  unless $numGuys;
  
  $line = <RECORD>;
  return "Error 3 reading record file."  unless $line =~ /^Rand = /;
  @rand = $line =~ /(\d+)/g;
  read(RECORD, $keys, 131072);
  $keys = uncompress(decode_base64($keys));
  close RECORD;
  
  return new GamePlayback($silent, $numGuys, $levelSet, $difficultySetting, $keys, @rand);
}

sub GetRecordExtension {
  return ".$::Version.vacuum";
}

sub CreateGamePlayback {
  my ($silent) = @_;
  
  return  unless @RecordedRand;
  new GamePlayback($silent, $RecorderNumGuys, $RecorderLevelSet, $RecorderDifficultySetting, $RecordedKeys, @RecordedRand);
}


#############################################################################
package GamePlayback;
#############################################################################

@GamePlayback::ISA = qw(GameObject);

sub new {
  my ($class, $silent, $numGuys, $levelSet, $difficultySetting, $keys, @rand) = @_;
  
  my $self = new GameObject(
    'silent' => $silent,
    'recordedKeys' => $keys,
    'keyPointer' => 0,
    'snumguys' => $numGuys,
    'sdifficultySetting' => $difficultySetting,
    'skeys' => {},
    'sevents' => {},
    'sgameobjects' => [],
    'sgame' => ($silent ? new SilentPlaybackGame($::Game, \@rand) : new NormalPlaybackGame($::Game, \@rand)),
    'splayers' => [ @::Players[0 .. $numGuys-1] ],
    'slevel' => undef,
    'sdifficulty' => undef,
  );
  bless $self, $class;
  
  $self->Switch();
  &::InitLevelFactory($levelSet);
  my ($p, $k, $i);
  $i = 0;
  &::InitPlayers();
  foreach $p (@::Players) {
    foreach $k (@{$p->{keys}}) {
      $k = ++$i;
    }
  }
  $::Game->ResetGame();
  $self->Switch();
  
  return $self;
}

sub Delete {
  my $self = shift;
  
  $self->Switch();
  my @gameObjects = @::GameObjects;
  foreach (@gameObjects) { %$_ = (); }
  undef $::Game;
  undef $::Level;
  undef @::Players;
  undef @::GameObjects;
  @Guy::Guys = @Slurp::Slurps = @Koules::Koules = ();
  $PangZeroBoss::Boss = undef;
  $self->Switch();
  $self->SUPER::Delete();
}

sub Switch {
  my $self = shift;
  
  my ($numguys, $diffsetting, $game, $level, @players, %keys, %events, @gameobjects, $difficulty);
  $numguys = $::NumGuys;  $::NumGuys = $self->{snumguys};    $self->{snumguys} = $numguys;
  $game    = $::Game;     $::Game    = $self->{sgame};       $self->{sgame}    = $game;
  $level   = $::Level;    $::Level   = $self->{slevel};      $self->{slevel}   = $level;
  @players = @::Players;  @::Players = @{$self->{splayers}}; $self->{splayers} = \@players;
  %keys    = %::Keys;     %::Keys    = %{$self->{skeys}};    $self->{skeys}    = \%keys;
  %events  = %::Events;   %::Events  = %{$self->{sevents}};  $self->{sevents}  = \%events;
  @gameobjects = @::GameObjects;  @::GameObjects = @{$self->{sgameobjects}};  $self->{sgameobjects} = \@gameobjects;
  $difficulty  = $::Difficulty;   $::Difficulty  = $self->{sdifficulty};      $self->{sdifficulty}  = $difficulty;
  $diffsetting = $::DifficultySetting; $::DifficultySetting = $self->{sdifficultySetting}; $self->{sdifficultySetting} = $diffsetting;
  
  # Note that @Guy::Guys and @Slurp::Slurps are not Switch()ed.
  # This is for performance, but should not be a problem... unless.
}

sub Advance {
  my ($self) = @_;
  my ($charsPerAdvance, $recordedKeys, $keyOffset, $i, $pressed);

  $charsPerAdvance = $self->{snumguys} * 5;
  $self->Switch();
  foreach ( 1 .. ($self->{skip} ? 10 : 1) ) {
    $keyOffset = $charsPerAdvance * $self->{keyPointer};
    if ($keyOffset >= length($self->{recordedKeys})) {
      $self->Switch();
      $self->Delete();
      return;
    }
    ++$self->{keyPointer};
    $recordedKeys = $self->{recordedKeys};
    %::Events = ();
    for ($i = 1; $i <= $charsPerAdvance; ++$i) {
      $pressed = substr($recordedKeys, $keyOffset++, 1);
      if ($pressed and not $::Keys{$i}) {
        $::Events{$i} = 1;
      }
      $::Keys{$i} = $pressed;
    }
    $::Game->AdvanceGame();
  }
  $self->Switch();
}

sub Draw {
  my ($self) = @_;
  my ($specialProjection);
  
  $self->Switch();
  $::Game->DrawScoreBoard();
  $specialProjection = $::Game->{specialProjection};
  $specialProjection->SpecialProjection()  if $specialProjection;
  foreach (@::GameObjects) {
    $_->Draw();
  }
  $::Game->ResetProjection();
  $self->Switch();
}


##########################################################################
package NormalPlaybackGame;
##########################################################################

@NormalPlaybackGame::ISA = qw(NormalGame);

sub new {
  my ($class, $outerGame, $rand) = @_;
  my $self = new NormalGame;
  %$self = (%$self,
    randPointer => 0,
    randQueue   => $rand,
    outerGame   => $outerGame,
  );
  bless $self, $class;
}

sub Rand {
  my ($self) = shift;
  
  my $rand = $self->{randQueue}->[$self->{randPointer}++];
  # warn "R $self->{randPointer}:\t@_ -> $rand";
  &::ConfessWithDump()  unless defined $rand;
  # print STDERR (caller(1))[3], ": Rand(@_) = $rand\n";
  return $rand;
#   my ($package, $filename, $line, $subroutine) = caller(1); $::RandLog2 .= "Rand called at $::Game->{anim} from $filename:$line sub $subroutine\n" . &::GetObjectDump();
#   my $anim = $self->{randQueue}->[$self->{randPointer}++];
#   unless ($anim == $self->{anim}) {
#     open RANDLOG, ">randlog1.txt"; print RANDLOG $::RandLog1; close RANDLOG;
#     open RANDLOG, ">randlog2.txt"; print RANDLOG $::RandLog2; close RANDLOG;
#     Carp::confess("$anim and $self->{anim} do not match.\n", &::DumpObjects())  
#   }
}

sub SetSpecialProjection {
  my $self = shift;
  
  return $self->{outerGame}->SetSpecialProjection(@_)  if $self->{outerGame};
}

sub SetTimeEffect {
  my $self = shift;
  
  $self->SUPER::SetTimeEffect(@_);
  $self->{outerGame}->SetTimeEffect(@_)  if $self->{outerGame};
}



##########################################################################
package SilentPlaybackGame;
##########################################################################

@SilentPlaybackGame::ISA = qw(NormalPlaybackGame);

sub new {
  my $class = shift;
  my $self = new NormalPlaybackGame(@_);
  bless $self, $class;
  $self->{silent} = 1;
  return $self;
}

sub SetMusic {}

sub FadeMusic {}

sub PlaySound {}

sub FadeInBackground {}

sub FadeOutBackground {}

sub SetBackground {}

sub NewMenuItem { return (); }

sub OnGameOver {
  my ($self) = @_;
  
  $::Level = new GameOverLevel($::Level->{level});
  $::Level->Initialize();
  $self->{gameOver} = 1;
}

sub SetSpecialProjection {
  &GameBase::SetSpecialProjection(@_);
}

sub SetTimeEffect {
  &GameBase::SetTimeEffect(@_);
}

sub DrawScoreBoard {}


#############################################################################
package Menu;
#############################################################################

sub DoRecorderMenu {
  my $self = shift;
  my $recall = $self->EnterSubMenu();
  
  $self->CollectSavedGames();
  
  my ($y, $yinc) = (400, -40);
  push @{$self->{menuItems}}, (
    new MenuItem( 100, $y += $yinc, ::T("Back to main menu"),
      'button' => sub { $self->{abortgame} = 1; } ),
  );
  if (@::RecordedRand) {
    push @{$self->{menuItems}}, (
      new MenuItem( 100, $y += $yinc, ::T("Play back last game"),
        'tooltip' => ::T("Replay your last flight."),
        'button' => sub { $self->PlayLastGame() }, ),
      new MenuItem( 100, $y += $yinc, ::T("Save the last game"),
        'tooltip' => ::T("Save the last game to disc."),
        'button' => sub { $self->SaveLastGame() }, ),
    );
  }
  if (@{$self->{files}}) {
    push @{$self->{menuItems}}, (
      $self->{filesMenuItem} = new MenuItem(  68, $y += $yinc, '< ' . ::T('Play saved game:'),
        'tooltip' => '',
        'button' => sub { my $item = shift; $self->StartPlayback($item->{fileIndex}); },
        'fileIndex' => 0,
        'lastFileIndex' => scalar(@{$self->{files}}) - 1,
        'update' => sub { my $item = shift; $item->SetParameter($self->{files}->[$item->{fileIndex}] . ' >'); $self->UpdateRecorderTooltip($item->{fileIndex}); },
        'left' => sub { my $item = shift; --$item->{fileIndex}  if $item->{fileIndex} > 0; },
        'right' => sub { my $item = shift; ++$item->{fileIndex}  if $item->{fileIndex} < $item->{lastFileIndex}; },
      ),
    );
  }

  $self->RunSubMenu();
  $self->LeaveSubMenu($recall);
}

sub CollectSavedGames {
  my $self = shift;
  my ($glob, @files, $filename, %files, $extension, $shortPath);
  
  $self->{savedDir} = &::GetSavedGameDirectory()  unless $self->{savedDir};
  $extension = &::GetRecordExtension();
  $glob = "$self->{savedDir}/*$extension";
  $glob =~ s/\\/\\\\/g; $glob =~ s/(\s)/\\$1/g;  # Hooray for C:\Documents and Settings
  @files = glob($glob);
  foreach $filename (@files) {
    unless (open RECORD, $filename) {
      warn "Cannot open $filename: $!";
      next;
    }
    my ($length, $numGuys, $levelSet, $difficultySetting) = &::ReadRecordHeader(*RECORD{IO});
    close RECORD;
    unless ($numGuys) {
      print STDERR "Couldn't load $filename: $length\n";
      next;
    }
    $shortPath = $filename;
    $shortPath =~ s|.*/||;
    $shortPath =~ s|$extension$||;
    $files{$shortPath} = {
      length => $length,
      numGuys => $numGuys,
      levelSet => $levelSet,
      difficultySetting => $difficultySetting,
      filename => $filename,
      shortPath => $shortPath,
    };
  }
  $self->{fileDesc} = \%files;
  $self->{files} = [ sort { lc($a) cmp lc($b) } keys %files ];
}

sub UpdateRecorderTooltip {
  my ($self, $fileIndex) = @_;
  my ($shortFilename, $fileDesc, $tooltip, $length, $time);
  
  $shortFilename = $self->{files}->[$fileIndex];
  $fileDesc = $self->{fileDesc}->{$shortFilename};
  $length = $fileDesc->{length};
  $length = sprintf("%02d:%02d", $length / 6000, ($length / 100) % 60);
  $time = (stat $fileDesc->{filename})[9];
  $time = localtime($time);  # TODO Localize
  $tooltip = [ "$shortFilename: " . ::Tss('%1 with %2 player(s)', $fileDesc->{levelSet}, $fileDesc->{numGuys}),
    $::DifficultySettingNames[$fileDesc->{difficultySetting}] . '; ' . ::Ts("Game length is %s", $length),
    ::Ts("Game recorded at %s", $time),
    ::T("Press Enter to view, left/right to select") ];
  $self->{filesMenuItem}->{tooltip} = $tooltip;
  $self->ShowTooltip($tooltip);
}

sub PlayLastGame {
  my $self = shift;
  
  $self->StopPlayback();
  my $playback = &::CreateGamePlayback();
  $self->RunPlayback($playback);
}

sub StartPlayback {
  my ($self, $fileIndex) = @_;
  my ($shortFilename, $filename, $playback);
  
  $self->StopPlayback();
  $shortFilename = $self->{files}->[$fileIndex];
  $filename = $self->{fileDesc}->{$shortFilename}->{filename};
  return $self->RecordToMovie($filename)  if $::MovieMode;
  $playback = &::LoadRecord($filename);
  return warn $playback  unless ref $playback;
  $self->RunPlayback($playback);
}

sub SaveLastGame {
  my $self = shift;
  my ($recall, $prompt, $input, $filename, $confirmation);
  $recall = $self->EnterSubMenu();
  
  my ($y, $yinc) = (400, -40);
  $prompt = new MenuItem( 100, $y += $yinc, ::T("Saving your last flight to disc") );
  $input = new MenuItem( 100, $y += $yinc * 2, ::T("Filename:"),
    'tooltip' => [::T('Please enter the filename and press Enter.'), ::T('Press Esc to cancel'), ] );
  $prompt->Center();
  $filename = $input->InputText(20, "[A-Za-z0-9_]");
  # TODO Check for existing file
  if (length($filename)) {
    $filename .= &::GetRecordExtension();
    &::SaveRecord(&::GetSavedGameDirectory() . '/' . $filename);
    $confirmation = new MenuItem(100, $y += $yinc, ::T("Your game was saved."));
    do { $self->MenuAdvance() } until %::Events;
    $confirmation->Delete();
  }
  $prompt->Delete();
  $input->Delete();
  $self->LeaveSubMenu($recall);
  $self->{abortgame} = 1;
}

sub RunPlayback {
  my ($self, $playback) = @_;
  my (@gameObjects, $difficulty, $tooltip, $helpText);
  
  @gameObjects = @::GameObjects;
  $helpText = new MenuItem(5, $::ScreenHeight-35, ::T("Press F to Fast Forward"));
  $helpText->Show();
  @::GameObjects = ($helpText, $playback);
  $difficulty = $::Difficulty;
  $::Difficulty = 0;
  $self->{silent} = 1;
  $tooltip = $self->{tooltip};
  $self->{tooltip} = [];
  
  do {
    $self->MenuAdvance();
    $playback->{skip} = $::Keys{::SDLK_f};
  } until ( $playback->{deleted} or $self->{abortgame} );
  
  @::GameObjects = @gameObjects;
  $::Difficulty = $difficulty;
  delete $self->{silent};
  $self->{tooltip} = $tooltip;
  $playback->Delete();
  $self->{abortgame} = 0;
  $::Game->SetSpecialProjection();
  &::SetMusic('Menu');
}

1;
