The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Games::Rezrov::ZIO_Termcap;
#
# z-machine I/O for perls with Term::Cap
#

BEGIN {
#  $ENV{"PERL_RL"} = 'Gnu';
#  $ENV{"PERL_RL"} = 'Perl';
  # don't set unless debugging: if requested readline isn't
  # present, may default to useless "Stub" version
}

use strict;
use Term::Cap;
use POSIX;

use Games::Rezrov::GetKey;
use Games::Rezrov::GetSize;
use Games::Rezrov::ZConst;
use Games::Rezrov::ZIO_Tools;
use Games::Rezrov::ZIO_Generic;

@Games::Rezrov::ZIO_Termcap::ISA = qw(Games::Rezrov::ZIO_Generic);

#
# termcap(5) capabilities:
#
use constant ATTR_OFF => 'me';
use constant ATTR_REVERSE => 'mr';
use constant ATTR_BOLD => 'md';
use constant ATTR_UNDERLINE => 'us';

use constant CURSOR_MOVE => 'cm';
use constant CURSOR_UP => 'up';

use constant CLEAR_TO_EOL => 'ce';
use constant CLEAR_SCREEN => 'cl';
use constant DELETE_LINE => 'dl';
use constant DELETE_CHAR => 'dc';

use constant AUDIO_BELL => 'bl';
use constant VISIBLE_BELL => 'vb';

use constant SET_SCROLL_REGION => 'cs';
use constant SCROLL_ONE_LINE => 'sf';

use constant BACKSPACE_BOOLEAN => 'bs';
use constant BACKSPACE_ALTERNATE => 'bc';
#
# end termcap
#

use constant MANUAL_BACKSPACE => 0x08;
# ^H

use constant SCROLL_STYLE_CS => 1;
use constant SCROLL_STYLE_DEL => 2;

use Games::Rezrov::MethodMaker qw(
  scrolling_style
  backspace_sequence
  read_own_lines
			  );

#my $HAVE_STATUS_LINE;

# again, a lot of statics for speed...
my $UPPER_LINES;
my $terminal;

my ($abs_x, $abs_y) = (0,0);
# current cursor position

# in v4/v5, we have a "lower" and an "upper" window.
# in <= v3, we have a window and a status line; status line will be
# considered "upper".  BROKEN: seastalker!

my ($rows, $columns);

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

  $self->zio_options(\%options);
  $self->readline_init();

  # set up Term::Cap
  my $termios = new POSIX::Termios();
  $termios->getattr();
  my $ospeed = $termios->getospeed();
  $terminal = Tgetent Term::Cap { TERM => undef, OSPEED => $ospeed };
  
  $terminal->Trequire(
#		      CURSOR_X,
#		      CURSOR_LOWER_LEFT,
		      CLEAR_TO_EOL,
		     );

  $self->init_scrolling_style();

#  die join "---", $terminal->Tputs("cs"), $terminal->Tgoto("cs", 42, 1);


  attr_off();

  if (0) {
    # boolean testing code
    my @booleans = ("5i","am","bs","bw","da","db","eo","es","gn","hc","HC","hs","hz","in","km","mi","ms","NP","NR","nx","os","ul","xb","xn","xo","xs","xt");
    foreach my $b (@booleans) {
      my $v = $terminal->Tputs($b, 0);
      printf "%s: '%s'\n", $b, defined $v ? "defined" : "undef";
    }
    die;
  }

  #
  #  find backspace sequence
  #
  my $backspace;
  if (defined $terminal->Tputs(BACKSPACE_BOOLEAN, 0)) {
    # boolean -- only defined, not an actual value  :/
    # if set, ctrl-H performs backspace
    $backspace = pack "c", MANUAL_BACKSPACE;
  } else {
    my $alt = $terminal->Tputs(BACKSPACE_ALTERNATE, 0);
    # "Backspace, if not ^H"
    if ($alt) {
      $backspace = $alt;
    } else {
      print STDERR "WARNING: can't get backspace sequence!\n";
      $backspace = pack "c", MANUAL_BACKSPACE;
      # shouldn't happen
    }
  }
  $self->backspace_sequence($backspace);

  if ($options{"columns"} and $options{"rows"}) {
    $columns = $options{"columns"};
    $rows = $options{"rows"};
  } else {
    ($columns, $rows) = get_size();
    if ($columns and $rows) {
      if ($options{"flaky"} and Games::Rezrov::GetKey::can_read_single()) {
	$self->read_own_lines(1);
      } else {
	$rows-- if $self->scrolling_style() == SCROLL_STYLE_DEL;
	# hack: steal the last line on the display.
	# a newline on the last line causes the window to scroll
	# automatically, adding an unrequested newline and erasing
	# the status line.  Fixable without "cs"?
      }
    } else {
      print "I couldn't guess the number of rows and columns in your display,\n";
      print "so you must use -rows and -columns to specify them manually.\n";
      exit;
    }
  }

  $self->set_upper_lines(0);

  return $self;
}

sub update {
  # force screen refresh
  $|=1;
  print "";
  $|=0;
}

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

sub write_string {
  my ($self, $string, $x, $y) = @_;
  $self->absolute_move($x, $y) if defined($x) and defined($y);
#  printf STDERR "ws: %s at (%d,%d)\n", $string, $abs_x, $abs_y;
  print $string;
  $abs_x += length($string);
}

sub newline {
  if (++$abs_y >= $rows) {
    # scroll needed
    if ($_[0]->scrolling_style() == SCROLL_STYLE_CS) {
      $_[0]->absolute_move(0, $rows);
      # saw a ref somewhere that said cursor had to be in
      # the lower left corner for scroll cmd to work
#      print $terminal->Tputs(SCROLL_ONE_LINE);
      do_term(SCROLL_ONE_LINE);
      $abs_y = $rows;
    } else {
      #    print STDERR "scrolling!\n";
      my $restore = $_[0]->get_position(1);
      $_[0]->absolute_move(0, $UPPER_LINES);
      do_term(DELETE_LINE);
      # scroll the lower window by deleting the first line
      # FIX ME: ONLY IN LOWER WINDOW
      #    &$restore();
      $abs_y = $rows - 1;
    }
  }
  $_[0]->absolute_move(0, $abs_y);

  Games::Rezrov::StoryFile::register_newline();
}

sub write_zchar {
  print chr($_[1]);
  $abs_x++;
}

sub absolute_move {
  # col, row
  my $self = shift;
  ($abs_x, $abs_y) = @_;
  print $terminal->Tgoto(CURSOR_MOVE, $abs_x, $abs_y);
}

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) = ($abs_x, $abs_y);
  if ($sub) {
    return sub { $self->absolute_move($x, $y); };
  } else {
    return ($abs_x, $abs_y);
  }
}

sub status_hook {
  my ($self, $type) = @_;
  # 0 = before
  # 1 = after
  if ($type == 0) {
    # before printing status line
    attr_reverse();
  } else {
    # after printing status line
    attr_off();
  }
}

sub get_input {
  my ($self, $max, $single_char, %options) = @_;
  my $result;
  if ($single_char) {
    $result = get_key();
  } else {
    if ($self->read_own_lines()) {
      # manual
      $result = $self->manual_read_line($options{"-preloaded"});
    } elsif ($self->using_term_readline()) {
      # Term::ReadLine enabled
      $result = $self->readline($options{"-preloaded"});
    } else {
#      system "stty -echo";
      $result = <STDIN>;
      # this doesn't work with v5+ preloaded input
      unless (defined $result) {
	$result = "";
	print "\n";
      }
    }
    chomp $result;
    $result = "" unless defined($result);

    $self->newline() if $self->scrolling_style() == SCROLL_STYLE_DEL;
    # ugh, FIX ME

  }
  return $result;
}

sub clear_to_eol {
  do_term(CLEAR_TO_EOL);
}

sub clear_screen {
  do_term(CLEAR_SCREEN);
}

sub split_window {
  # split upper window to specified number of lines
  my ($self, $lines) = @_;
  $self->set_upper_lines($lines);
  # needed for scrolling the lower window
}

sub do_term {
  print $terminal->Tputs($_[0], 0) if $terminal;
}

sub attr_off {
  do_term(ATTR_OFF);
}

sub attr_reverse {
  do_term(ATTR_REVERSE);
}

sub attr_bold {
  do_term(ATTR_BOLD);
}

sub attr_underline {
  do_term(ATTR_UNDERLINE);
}

sub set_text_style {
  # sect15.html#set_text_style
  my ($self, $text_style) = @_;
  attr_off();
  attr_reverse() if ($text_style & Games::Rezrov::ZConst::STYLE_REVERSE);
  attr_bold() if ($text_style & Games::Rezrov::ZConst::STYLE_BOLD);
  attr_underline() if ($text_style & Games::Rezrov::ZConst::STYLE_ITALIC);
}

sub cleanup {
  # don't just rely on DESTROY, doesn't work for interrupts
  attr_off();
  $_[0]->set_upper_lines(0);
  # reset scrolling if necessary
#  $_[0]->clear_screen();
  $_[0]->absolute_move(0, $rows) if $terminal;
  # move to last line
}

sub story {
  return (defined $_[1] ? $_[0]->{"story"} = $_[1] : $_[0]->{"story"});
}

sub manual_read_line {
  # read a line manually
  # incompatible with ReadLine, but compatible with preloaded input
  my ($self, $buf) = @_;
  $buf = "" unless defined $buf;
  my ($ord, $char);
  while (1) {
    $char = get_key();
    $ord = ord($char);
    if ($ord == Games::Rezrov::ZConst::ASCII_DEL or
	$ord == Games::Rezrov::ZConst::ASCII_BS) {
      if (my $len = length($buf)) {
	print $self->backspace_sequence();
	do_term(DELETE_CHAR);
	$buf = substr($buf, 0, $len - 1);
      }
    } elsif ($ord == Games::Rezrov::ZConst::ASCII_LF or
	     $ord == Games::Rezrov::ZConst::ASCII_CR) {
      return $buf;
    } elsif ($ord >= 32 and $ord <=127) {
      $buf .= $char;
      print $char;
    }
  }
}

sub set_upper_lines {
  my ($self, $ul) = @_;
  $UPPER_LINES = $ul if defined $ul;
  if ($self->scrolling_style() == SCROLL_STYLE_CS) {
    # set the scroll region (protect upper window)

#    my $reserve = $UPPER_LINES + ($HAVE_STATUS_LINE ? 1 : 0);
    my $reserve = $UPPER_LINES;
    # I forget why we don't need to include the status line...
    # ...or is this an ominous sign? :/

    print $terminal->Tgoto(SET_SCROLL_REGION, $rows, $reserve) if $terminal;
    # HACK
    # this "kind of" works, but args are reversed (1st=col 2nd=row)
    # might this break on some systems?
    # Seems to work under Linux but I don't think Tgoto is meant
    # to be used this way.  The "correct" way?
  }
}

sub test_terminal_capabilities {
  # return 1 if given list of capabilities available
  eval {
    local $SIG{__WARN__} = sub {};
    $terminal->Trequire(@_);
  };
  return $@ ? 0 : 1;
}

sub init_scrolling_style {
  # if terminal has "cs" attribute we can use all lines on the screen.
  # the "del" style is still buggy.

  $_[0]->scrolling_style(test_terminal_capabilities(SET_SCROLL_REGION) ?
			 SCROLL_STYLE_CS : SCROLL_STYLE_DEL);
#  $_[0]->scrolling_style(SCROLL_STYLE_DEL);
}

1;