The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Games::Rezrov::ZIO_Win32;
# z-machine i/o for perls with Win32::Console
# TO DO:
# - handle scrollbars/buffering properly...it would be nice
#   if old game text could be seen via an existing scrollbar.
#   But this requires untangling screen Size() mess.
# - can we set hourglass when busy?

use strict;
use Win32::Console;

use Games::Rezrov::ZIO_Generic;
use Games::Rezrov::ZIO_Color;
use Games::Rezrov::ZConst;

use Carp qw(cluck);

use constant DEBUG => 0;

@Games::Rezrov::ZIO_Win32::ISA = qw(Games::Rezrov::ZIO_Generic
				    Games::Rezrov::ZIO_Color
				    );

my ($upper_lines, $rows, $columns, $in_status);
# number of lines in upper window, geometry

my ($IN, $OUT);
# Win32::Console instances

my @ORIG_SIZE;

if (DEBUG) {
  # debugging; tough to redirect STDERR under win32 :(
  open(LOG, ">zio.log") || die;
  select(LOG);
  $|=1;
  select(STDOUT);
}

my %KEYCODES = (
		38 => Games::Rezrov::ZConst::Z_UP,
		40 => Games::Rezrov::ZConst::Z_DOWN,
		37 => Games::Rezrov::ZConst::Z_LEFT,
		39 => Games::Rezrov::ZConst::Z_RIGHT,
		);

my (%FOREGROUND, %BACKGROUND);
foreach (qw(black
	    blue
	    lightblue
	    red
	    lightred
	    green
	    lightgreen
	    magenta
	    lightmagenta
	    cyan
	    lightcyan
	    brown
	    yellow
	    gray
	    white)) {
    # make hash translating names to color codes exported by Win32::Console
    no strict "refs";
    $FOREGROUND{$_} = ${"main::FG_" . uc($_)};
    $BACKGROUND{$_} = ${"main::BG_" . uc($_)};
}

sub new {
    my ($type, %options) = @_;
    my $self = new Games::Rezrov::ZIO_Generic(%options);
    bless $self, $type;

    if ($options{fg}) {
	$options{fg} = "gray" if $options{fg} eq "white";
	# since INTENSITY mode has no effect "white",
	# use gray instead.  Feh.
	# How to get *true* bold here???
    } else {
	$options{fg} = "gray" unless $options{fg};
	$options{bg} = "blue" unless $options{bg};
	$options{sfg} = "black" unless $options{sfg};
	$options{sbg} = "cyan" unless $options{sbg};
    }

    $self->parse_color_options(\%options);

    foreach ("bg", "fg", "sfg", "sbg") {
      next unless exists $options{$_};
      my $c = $self->$_() || next;
      die sprintf "Unknown color \"%s\"; available colors: %s\n", $c, join(", ", sort keys %FOREGROUND)
	  unless exists $FOREGROUND{$c};
    }
    
    # set up i/o
    $IN = new Win32::Console(STD_INPUT_HANDLE);
    $OUT = new Win32::Console(STD_OUTPUT_HANDLE);

    @ORIG_SIZE = $OUT->Size();
    # save original buffer geometry to restore on exit

    my ($top_row, $bottom_row, $left_col, $right_col);
    ($columns, $left_col, $top_row, $right_col, $bottom_row) = ($OUT->Info())[0,5,6,7,8];
#    die $bottom_row;
    my $visible_rows = ($bottom_row - $top_row) + 1;
    my $visible_columns = ($right_col - $left_col) + 1;

    $OUT->Size($visible_columns, $visible_rows);
    # 11/2004: set the screen buffer to be the same size as the visible area
    # of the window.  Has the effect of removing any scrollbars, etc.
    #
    # Under XP the default number of rows in the buffer is generally
    # larger than the number of visible rows (under Windows 98
    # the two were generally the same).  So without this change we'd be 
    # drawing the status window and/or upper window lines somewhere
    # way up in the buffer, which won't be visible...this is because
    # the Cursor() method moves within the *buffer* rather than the
    # visible area.
    #
    # It's probably possible to finesse this module to only draw to
    # the visible area while leaving the buffers and scrollbars
    # alone, but for now I Don't Care.
    
    my @size = $OUT->Size();
    $columns = $options{"columns"} || $size[0] || die "need columns!";
    $rows = $options{"rows"} || $size[1] || die "need rows!";

    $OUT->Size($columns, $rows);
    # resize again (possible user override limiting rows/cols)

    $upper_lines = 0;
    return $self;
}

sub update {
  $OUT->Flush();
}

sub set_version {
  # called by the game
  my ($self, $status_needed, $callback) = @_;
  Games::Rezrov::StoryFile::rows($rows);
  Games::Rezrov::StoryFile::columns($columns);
  return 0;
}

sub absolute_move {
  # move to X, Y
  $OUT->Cursor($_[1], $_[2]);
}

sub write_string {
  my ($self, $string, $x, $y) = @_;
  $self->absolute_move($x, $y) if defined($x) and defined($y);
#  $OUT->Attr($current_attr);
  $OUT->Attr($self->get_attr());
  $OUT->Write($string);
}

sub newline {
  # newline/scroll
  my ($x, $y) = $OUT->Cursor();
  if (++$y >= $rows) {
      # scroll needed
      my $last_line = $rows - 1;
      $y = $last_line;
      my $top = $upper_lines;
    #	$OUT->Write(sprintf "before: at %d,%d, top=%d last=%d\n", $x, $y, $top, $last_line);
#    log_it(sprintf "before: at %d,%d, top=%d last=%d\n", $x, $y, $top, $last_line);
    #	sleep(1);
      $OUT->Scroll(0, $top + 1, $columns - 1, $last_line,
		   0, $top, Games::Rezrov::ZConst::ASCII_SPACE, $_[0]->get_attr(0),
		   0, $top, $columns - 1, $last_line);
      # ugh: we have to specify the clipping region, or else
      # Win32::Console barfs about uninitialized variables (with -w)
  }
  Games::Rezrov::StoryFile::register_newline();
  $_[0]->absolute_move(0, $y);
}

sub write_zchar {
#    log_it("wzchar: " . chr($_[1]));
  $OUT->Attr($_[0]->get_attr());
  $OUT->Write(chr($_[1]));
}

sub status_hook {
  my ($self, $type) = @_;
  # 0 = before
  # 1 = after
  if ($type == 0) {
    # before printing status line
    $OUT->Cursor(0,0);
    $in_status = 1;
    $OUT->FillAttr($self->get_attr(), $columns, 0, 0);
  } else {
    # after printing status line
    $in_status = 0;
  }
}

sub get_input {
    my ($self, $max, $single_char, %options) = @_;
#    $IN->Flush();
    # don't flush input (allow buffered keystrokes)
    
    my ($start_x, $y) = $OUT->Cursor();
    my $buf = $options{"-preloaded"} || "";
    # preloaded text in the buffer, but already displayed by the game; ugh.
    my @event;
    my ($code, $char);

    if ($self->listening) {
      $buf = $self->recognize_line();
      $OUT->Write($buf);
    } else {
      while (1) {
	@event = $IN->Input();
	next unless defined $event[0];
	my $known;
	if ($event[0] == 1 and $event[1]) {
	  # a key pressed
	  $code = $event[5];
	  if ($code == 0) {
	    # non-character key pressed
	    if ($KEYCODES{$event[3]}) {
	      $code = $KEYCODES{$event[3]};
	      $known = 1;
	    } else {
	      log_it(sprintf "got unknown non-char: %s", join ",", @event);
	    }
	  }

	  if ($single_char and ($known or ($code >= 1 and $code <= 127))) {
	    return chr($code);
	  } elsif ($code == Games::Rezrov::ZConst::ASCII_BS) {
	    if (length($buf) > 0) {
	      #	  log_it("backsp " . length($buf) . " " . $buf);
	      my ($x, $y) = $OUT->Cursor();
	      $OUT->Cursor($x - 1, $y);
	      $OUT->Write(" ");
	      $OUT->Cursor($x - 1, $y);
	      $buf = substr($buf, 0, length($buf) - 1);
	    }
	  } elsif ($code == Games::Rezrov::ZConst::ASCII_CR) {
	    last;
	  } else {
	    if ($code >= 32 and $code <= 127) {
	      $char = chr($code);
	      $buf .= $char;
	      $OUT->Attr($self->get_attr(0));
	      $OUT->Write($char);
	    }
	  }
	}
      }
    }
    $self->newline();
    return $buf;
}

sub clear_screen {
    $OUT->Cls($_[0]->get_attr(0));
#    log_it("cls");
}

sub clear_to_eol {
    $OUT->Attr($_[0]->get_attr(0));
    $OUT->Write(' ' x ($columns - ($OUT->Cursor())[1]));
}

sub split_window {
  # split upper window to specified number of lines
  my ($self, $lines) = @_;
  #  $w_main->setscrreg($lines, $rows - 1);
  $upper_lines = $lines;
  #  print STDERR "split_window to $lines\n";
}

sub can_change_title {
  return 1;
}

sub can_use_color {
  return 1;
}

sub set_game_title {
  $OUT->Title($_[1]);
}

sub log_it {
  if (DEBUG) {
    print LOG $_[0] . "\n";
  }
}

sub get_attr {
    # return attribute code for color/style currently in effect.
    my ($self, $mask) = @_;
    
    $mask = Games::Rezrov::StoryFile::font_mask() unless defined($mask);
    # might be called with an override
    my ($fg, $bg);
    if ($in_status) {
	$fg = $self->sfg();
	$bg = $self->sbg();
    } else {
	if ($mask & Games::Rezrov::ZConst::STYLE_REVERSE) {
	  $fg = $self->bg();
	  $bg = $self->fg();
	} else {
	  $fg = $self->fg();
	  $bg = $self->bg();
	}
    }

    my $code = $BACKGROUND{$bg} | $FOREGROUND{$fg};
    $code |= main::FOREGROUND_INTENSITY if 
	($mask & (Games::Rezrov::ZConst::STYLE_BOLD|Games::Rezrov::ZConst::STYLE_ITALIC));
    return $code;
}

sub get_position {
  # with no arguments, return absolute X and Y coordinates.
  # With an argument, return a sub that will restore the current cursor
  # position.
  my ($self, $sub) = @_;
  my ($x, $y) = $OUT->Cursor();
  if ($sub) {
    return sub { $OUT->Cursor($x, $y); };
  } else {
    return ($x, $y);
  }
}

sub cleanup {
    $OUT->Size(@ORIG_SIZE) if $OUT;
    # restore original window buffer sizes
}

1;