#!/usr/bin/perl -w
# Last modify Time-stamp: <Ye Wenbin 2007-11-11 00:08:13>
# Version: v 0.0 2007/11/02 08:25:10
# Author: Ye Wenbin <wenbinye@gmail.com>

use strict;
use warnings;

package Tetris::L18N;
use base qw(Locale::Maketext);

package Tetris::L18N::zh_cn;
use base qw(Tetris::L18N);
our %Lexicon = (
    'Scores' => '得分',
    'Lines' => '行数',
    'Level' => '级别',
    '_Rank' => '积分板(_R)',
    '_File' => '文件(_F)',
    '_Setting' => '设置(_S)',
    '_Help' => '帮助(_H)',
    "Cancel current game?" => '关闭当前运行的游戏吗?',
    'Start level' => '初始等级',
    'Ye Wenbin' => '叶文彬',
    '_AUTO' => 1,
);

package Tetris::L18N::en;
use base qw(Tetris::L18N);
our %Lexicon = (
    '_File' => '_File',
    '_Rank' => '_Rank',
    '_Setting' => '_Setting',
    '_Help' => '_Help',
    '_AUTO' => 1,
);

#{{{  package Cell
package Tetris::Cell;
use Gtk2;
use Goo::Canvas;
use constant SIZE => 16;

our @ISA = qw(Goo::Canvas::Image);

sub new {
    my $_class = shift;
    my $class = ref $_class || $_class;
    my ($root, $color, $x, $y, %options) = @_;
    my $pixbuf;
    unless ( ref $color ) {
        if ( $color =~ /^#/ ) {
            $color = hex2rgb($color);
        } else {
            $color = Gtk2::Gdk::Color->parse($color);
            $color = [ map {$_/257} $color->red, $color->green, $color->blue];
        }
    }
    if ( $options{-plan} ) {
        $pixbuf = xpm_data((rgb2hex(@$color)) x 3);
        delete $options{-plan};
    } else {
        $pixbuf = xpm_data(
            rgb2hex(map {0.6*$_} @$color),
            rgb2hex(map {0.8*$_} @$color),
            rgb2hex(@$color),
        );
    }
    $pixbuf = Gtk2::Gdk::Pixbuf->new_from_xpm_data(@$pixbuf);
    
    my $self = Goo::Canvas::Image->new($root, $pixbuf, $x, $y, %options);
    bless $self, $class;
}

sub rgb2hex {
    my ($r, $g, $b) = @_;
    return sprintf("#%02x%02x%02x", $r, $g, $b);
}

sub hex2rgb {
    my $hex = shift;
    map { hex } substr($hex, 1) =~ /(..)/g;
}

sub xpm_data {
    my ($col1, $col2, $col3) = @_;
    return [split /\n/, <<XPM];
16 16 3 1
+ c $col1
. c $col2
- c $col3
---------------+
--------------++
--............++
--............++
--............++
--............++
--............++
--............++
--............++
--............++
--............++
--............++
--............++
--............++
-+++++++++++++++
++++++++++++++++
XPM
}
#}}}

#{{{  package Table
package Tetris::Table;
use List::Util qw(max);
our @ISA = qw(Goo::Canvas::Group);

sub new {
    my $_class = shift;
    my $class = ref $_class || $_class;
    my ($root, $x, $y, %options) = @_;
    my $self = Goo::Canvas::Group->new($root);
    bless $self, $class;

    for ( qw/columns rows/ ) {
        $self->{$_} = $options{'-'.$_};
    }
    $self->{table} = [];
    $self->{bgcolor} = $options{-bgcolor} || 'black';
    if ( $options{-border} ) {
        $self->{border_color} = $options{-border_color} || 'grey50';
        $self->{offset} = [(Tetris::Cell::SIZE) x 2];
        $self->draw_border();
    }
    $self->draw_table();
    $self->translate($x, $y);
    return $self;
}

sub draw_border {
    my $self = shift;
    my ($rows, $cols) = ($self->{rows}, $self->{columns});
    my $color = $self->{border_color};
    foreach ( 1..($cols+2) ) {
        Tetris::Cell->new($self, $color, ($_-1)*Tetris::Cell::SIZE, 0);
        Tetris::Cell->new($self, $color, ($_-1)*Tetris::Cell::SIZE, ($rows+1)*Tetris::Cell::SIZE);
    }
    foreach ( 1..$rows ) {
        Tetris::Cell->new($self, $color, 0, $_*Tetris::Cell::SIZE);
        Tetris::Cell->new($self, $color, ($cols+1)*Tetris::Cell::SIZE, $_*Tetris::Cell::SIZE);
    }
}

sub draw_table {
    my $self = shift;
    my ($sx, $sy) = $self->offset;
    my ($rows, $cols) = ($self->{rows}, $self->{columns});
    my $color = $self->{bgcolor};
    my @table;
    foreach my $i ( 1..$cols ) {
        foreach my $j ( 1..$rows ) {
            $table[$i-1][$j-1] = Tetris::Cell->new(
                $self, $color,
                $sx+($i-1)*Tetris::Cell::SIZE,
                $sy+($j-1)*Tetris::Cell::SIZE,
            );
        }
    }
    $self->{bgtable} = \@table;
}

sub check_pos {
    my $self = shift;
    my ($rows, $cols) = ($self->{rows}, $self->{columns});
    my ($row, $col) = @_;
    if ( $row < 1 || $row > $rows ) {
        return;
    }
    if ( $col < 1 || $col > $cols ) {
        return;
    }
    return 1;
}

sub put_cell {
    my $self = shift;
    my ($row, $col, %options) = @_;
    unless ( $self->check_pos($row, $col) ) {
        return;
    }
    for ( $row, $col ) {
        $_--;
    }
    my $table = $self->{table};
    my ($sx, $sy) = $self->offset();
    if ( $table->[$row][$col] ) {
        $self->remove_cell($row, $col);
    }
    my $color = $options{-color};
    delete $options{-color};
    return $table->[$row][$col] = Tetris::Cell->new(
        $self, $color,
        $sx+$col*Tetris::Cell::SIZE,
        $sy+$row*Tetris::Cell::SIZE,
        %options
    );
}

sub offset {
    my $self = shift;
    if ( exists $self->{offset} ) {
        return @{$self->{offset}};
    }
    else {
        return (0, 0);
    }
}

sub remove_cell {
    my $self = shift;
    my ($row, $col) = @_;
    unless ( $self->check_pos($row, $col) ) {
        return;
    }
    for ( $row, $col ) {
        $_--;
    }
    my $table = $self->{table};
    my $item = $table->[$row][$col];
    if ( $item ) {
        $self->remove_child($self->find_child($item));
        $table->[$row][$col] = undef;
        return 1;
    }
}

sub move_cell {
    my $self = shift;
    my ($row, $col, $newrow, $newcol) = @_;
    unless ( $self->check_pos($row, $col)
                 && $self->check_pos($newrow, $newcol) ) {
        return;
    }
    for ( $row, $col, $newrow, $newcol ) {
        $_--;
    }
    my ($rows, $cols) = ($self->{rows}, $self->{columns});
    my $table = $self->{table};
    my $item = $table->[$row][$col];
    if ( $item ) {
        if ( $self->{table}[$newrow][$newcol] ) {
            $self->remove_cell($newrow+1, $newcol+1);
        }
        # print "Move $item from $row $col to $newrow $newcol\n";
        $item->translate(
            ($newcol-$col)*Tetris::Cell::SIZE,
            ($newrow-$row)*Tetris::Cell::SIZE,
        );
        $table->[$newrow][$newcol] = $item;
        $table->[$row][$col] = undef;
    }
}

sub table {
    return shift->{table};
}

sub cell {
    my $self = shift;
    my ($row, $col) = @_;
    unless ( $self->check_pos($row, $col) ) {
        return;
    }
    return $self->{table}[$row-1][$col-1];
}

sub rows {
    return shift->{rows};
}
sub columns {
    return shift->{columns};
}

sub eliminate_line {
    my $self = shift;
    my %lines = map { $_=>1 } @_;
    return unless %lines;
    # print "eliminate_line @_\n";
    my $line = max(keys %lines);
    my $down = 0;
    my $cols = $self->columns;
    my $table = $self->table;
    while ( $line > 0 ) {
        if ( exists $lines{$line} ) {
            $self->remove_cell($line, $_) for 1..$cols;
            $down++;
        } elsif ( $down ) {
            $self->move_cell($line, $_, $line+$down, $_) for 1..$cols;
        }
        $line--;
    }
    return $down;
}

sub eliminate_line_maybe {
    my $self = shift;
    my @lines = @_;
    $self->eliminate_line(
        grep {
            $self->test_fill($_);
        } @lines
    );
}

sub test_fill {
    my $self = shift;
    my $line = shift;
    $line--;
    my $fill = 1;
    my $cols = $self->columns;
    my $table = $self->table;
    foreach ( 1..$cols ) {
        if ( !defined $table->[$line][$_-1] ) {
            $fill = 0;
            last;
        }
    }
    return $fill;
}

sub clear {
    my $self = shift;
    my $table = $self->{table};
    my ($rows, $cols) = ($self->{rows}, $self->{columns});
    foreach my $i( 1..$rows ) {
        foreach my $j( 1..$cols ) {
            $self->remove_cell($i, $j);
        }
    }
    $self->{table} = [];
}

sub is_full {
    my $self = shift;
    my $full = 0;
    my $table = $self->{table};
    foreach ( @{$table->[0]} ) {
        if ( $_ ) {
            $full = 1;
            last;
        }
    }
    return $full;
}
#}}}

#{{{  package Shape
package Tetris::Shape;
use List::Util qw(min max sum);
use Data::Dumper qw(Dumper);
sub new {
    my $_class = shift;
    my $class = ref $_class || $_class;
    my $self = {};
    bless $self, $class;
    my %options = @_;
    foreach ( keys %options ) {
        next unless /^-/;
        $self->{substr($_, 1)} = $options{$_};
    }
    return $self;
}

sub draw {
    my $self = shift;
    my $shape = $self->{shape}[$self->{type}];
    my $i = $#$shape;
    while ( $i>-1 && !grep {$_>0} @{$shape->[$i]} ) {
        $i--;
    }
    my $table = $self->{table};
    my $color = $self->{color};
    my ($row, $col) = ($self->{row}, $self->{col});
    if ( !defined $row ) {      # if no row give, put the shape visible
        my @r = map { sum(@{$_}) } @{$shape};
        my $i = $#r;
        while ( $i>-1 ) {
            last if $r[$i]>0;
            $i--;
        }
        $row = -$i+1;
        $self->{row} = $row;
    }
    my @cells;
    foreach my $r( @$shape ) {
        foreach ( 0..$#$r ) {
            if ( $r->[$_] > 0 ) {
                my $put = 0;
                if ( $row > 0 ) {
                    # print "$table $row, $col+$_, $color\n";
                    $table->put_cell($row, $col+$_, -color => $color);
                    $put = 1;
                }
                push @cells, [$row, $col+$_, $put];
            }
        }
        $row++;
    }
    $self->{cells} = \@cells;
}

sub move_down {
    my $self = shift;
    if ( $self->hit_test(1, 0) ) {
        return 1;
    }
    my $cells = $self->{cells};
    my $table = $self->{table};
    my $color = $self->{color};
    foreach ( sort { $b->[0]<=>$a->[0] } @$cells ) { # move the max row first
        my ($r, $c, $put) = @{$_};
        if ( $put ) {
            $table->move_cell($r, $c, $r+1, $c);
        } elsif ( $r >= 0 ) {
            $table->put_cell($r+1, $c, -color => $color);
            $_->[2] = 1;
        }
        $_->[0]++;
    }
    $self->{row}++;
    return 0;
}

sub move_left {
    my $self = shift;
    my $cells = $self->{cells};
    my $table = $self->{table};
    if ( $self->hit_test(0, -1) ) {
        return;
    }
    foreach ( sort {$a->[1] <=> $b->[1] }  @$cells ) {
        my ($r, $c, $put) = @{$_};
        if ( $put ) {
            $table->move_cell($r, $c, $r, $c-1);
        }
        $_->[1]--;
    }
    $self->{col}--;
}

sub move_right {
    my $self = shift;
    my $cells = $self->{cells};
    my $table = $self->{table};
    if ( $self->hit_test(0, 1) ) {
        return;
    }
    foreach ( sort {$b->[1]<=>$a->[1] } @$cells ) {
        my ($r, $c, $put) = @{$_};
        if ( $put ) {
            $table->move_cell($r, $c, $r, $c+1);
        }
        $_->[1]++;
    }
    $self->{col}++;
}

sub rotate {
    my $self = shift;
    my $cells = $self->{cells};
    my $shapes = $self->{shape};
    my $table = $self->{table};
    my $type = $self->{type};   # backup
    my $col = $self->{col};     # backup
    my $cols = $table->columns;
    $self->{type} = ($type+1) % scalar(@$shapes);
    my ($hit, $collide_cells) = $self->hit_test(0, 0);
    # print Dumper($hit, $collide_cells), "\n";
    if ( $hit ) {
        my $reason = 0;
        my @cols = sort {$a<=>$b} map {$_->[1]} @$collide_cells;
        if ( $cols[0] < 1 ) {
            $self->{col} = $self->{col} + (1-$cols[0]);
        } elsif ( $cols[-1] > $cols ) {
            $self->{col} = $col - ($cols[-1]-$cols);
        } else {
            $self->{type} = $type;
            $self->{col} = $col;
            return;
        }
        if ( $self->hit_test(0, 0) ) {
            $self->{type} = $type;
            $self->{col} = $col;
            return;
        }
    }
    # main::dump_table($table->{table});
    # print join("\n", map {join("\t", @{$_})} @$cells), "\n";
    foreach ( @$cells ) {
        $table->remove_cell($_->[0], $_->[1]) if $_->[2];
    }
    $self->draw();
    # main::dump_table($table->{table});
    # print join("\n", map {join("\t", @{$_})} @$cells), "\n";
}

sub hit_test {
    my $self = shift;
    my ($dx, $dy) = @_;
    my ($row, $col) = ($self->{row}, $self->{col});
    $row += $dx;
    $col += $dy;
    my @cells;
    my $collides = 0;
    my $table = $self->{table};
    my $shape = $self->{shape}[$self->{type}];
    my %cells;
    map { $cells{$_->[0]}{$_->[1]} = 1 } @{$self->{cells}};
    foreach my $r ( @$shape ) {
        foreach ( 0..$#$r ) {
            if ( $r->[$_] > 0 ) {
                next if exists $cells{$row}{$col+$_};
                if ($row>$table->rows || $col+$_ > $table->columns
                        || $col+$_ < 1 || $table->cell($row, $col+$_)) {
                    push @cells, [$row, $col+$_];
                    # print "$row, $col+$_\n";
                    $collides = 1;
                }
            }
        }
        $row++;
    }
    if ( wantarray ) {
        return ($collides, \@cells);
    } else {
        return $collides;
    }
}

sub cells {
    return shift->{cells};
}
#}}}

package main;
use List::Util qw(min max sum);
use Goo::Canvas;
use Gtk2 '-init';
use Glib qw(TRUE FALSE);
use FindBin qw($Bin);
use Data::Dumper qw(Dumper);

use Encode qw(encode decode);
our $lh = Tetris::L18N->get_handle() || Tetris::L18N->get_handle('en');
sub gettext { return decode('utf8', $lh->maketext(@_)) }

#{{{  Configuration
our $history;
our $shapes = parse_shapes();
our $next_shape;
our $timer;
our $timer_pause;
our $game_start;
our $after_load_function;
our %Config = (
    keybindings => {
        65361 => \&move_left,   # left
        65362 => \&rotate,      # up
        65363 => \&move_right,  # right
        65364 => \&move_down,   # down
        ord(' ') => \&down,     # space
        ord('p') => \&pause,    # p
        65293 => \&new_game,    # enter
    },
    rows => 20,
    cols => 10,
    style => [
        'blue', 'purple', 'yellow', 'magenta',  'cyan', 'green',
        'red', 'deeppink', 'hotpink', 'skyblue', 'gold',
    ],
    start_level => 0,
    down_step => 3,
    max_rank_list => 10,
);
my $default_conf_file = ".perltetris";
my $config_file;
my $home;
eval { require File::HomeDir };
if ( $@ ) {
    $home = $ENV{HOME} || $Bin;
}
else {
    $home = File::HomeDir->my_home;
}
if ( -e "$home/.tetris" ) {
    $config_file = "$home/$default_conf_file";
    eval { require "$home/$default_conf_file" };
    if ( $@ ) {
        print STDERR "Error when load config file $home/$default_conf_file: $@\n";
    }
}
else {
    eval { require Tetris::Config; };
    $config_file = $INC{'Tetris/Config.pm'};
}
our ($score, $lines, $level) = (0, 0, $Config{start_level});
#}}}

our $window = Gtk2::Window->new('toplevel');
$window->signal_connect('delete_event' => sub { Gtk2->main_quit; });

our $vbox = Gtk2::VBox->new();
our $menu = create_menu();
our $canvas = create_canvas();
$vbox->add($menu);
$vbox->add($canvas);
$window->add($vbox);
$window->show_all;

if ( defined $after_load_function
 && ref $after_load_function eq 'CODE' ) {
    $after_load_function->();
}

Gtk2->main;

sub END {
    write_history();
}

sub setting {
    return if $game_start;
    my $dia = Gtk2::Dialog->new(
        gettext('Setting'), $window,
        'modal', 'gtk-ok' => 'ok',
        'gtk-cancel' => 'cancel',
    );
    my $vbox = $dia->vbox;
    my $table = Gtk2::Table->new(2, 2);
    my $label = Gtk2::Label->new(gettext("Start level"));
    my $but = Gtk2::SpinButton->new_with_range(0, 10, 1);
    $but->set_value($Config{start_level});
    $table->attach_defaults($label, 0, 1, 0, 1);
    $table->attach_defaults($but, 1, 2, 0, 1);
    $vbox->add($table);
    $vbox->show_all();
    score(0);
    my $response = $dia->run;
    if ( $response eq 'ok' ) {
        $Config{start_level} = $but->get_value;
        $level = $Config{start_level};
        update_label();
    }
    $dia->destroy;
}

sub about {
    return if $game_start;
    my $dia = Gtk2::AboutDialog->new();
    $dia->set_authors(gettext('Ye Wenbin'));
    $dia->run;
    $dia->destroy;
}

sub stop_game {
    if ( $timer ) {
        Glib::Source->remove($timer);
    }
    remove_heading();
    ($score, $lines, $level) = (0, 0, $Config{start_level});
    update_label();
    $canvas->{table}->clear;
    $canvas->{preview}->clear;
    $game_start = 0;
}

sub create_menu {
    my $menu_bar = Gtk2::MenuBar->new;
    # File
    my $file_menu = Gtk2::Menu->new;
    # |- New
    my $new_menuitem = Gtk2::ImageMenuItem->new_from_stock('gtk-new', undef);
    $new_menuitem->signal_connect('activate' => \&new_game);
    $file_menu->append($new_menuitem);
    # |- Stop
    my $stop_menuitem = Gtk2::ImageMenuItem->new_from_stock('gtk-close', undef);
    $stop_menuitem->signal_connect('activate' => \&stop_game);
    $file_menu->append($stop_menuitem);
    # |- Rank
    my $rank_menuitem = Gtk2::MenuItem->new_with_mnemonic(gettext('_Rank'));
    $rank_menuitem->signal_connect( 'activate' => sub { rank_dia() } );
    $file_menu->append($rank_menuitem);
    # |- Exit
    my $exit_menuitem = Gtk2::ImageMenuItem->new_from_stock('gtk-quit', undef);
    $exit_menuitem->signal_connect('activate' => sub { Gtk2->main_quit });
    $file_menu->append($exit_menuitem);

    # Setting
    my $setting_menu = Gtk2::Menu->new;
    # |- Settings
    my $setting_menuitem = Gtk2::ImageMenuItem->new_from_stock('gtk-preferences', undef);
    $setting_menuitem->signal_connect('activate' => \&setting);
    $setting_menu->append($setting_menuitem);

    # Help
    my $help_menu = Gtk2::Menu->new;
    # |- About
    my $about_menuitem = Gtk2::ImageMenuItem->new_from_stock('gtk-about', undef);
    $about_menuitem->signal_connect('activate' => \&about);
    $help_menu->append($about_menuitem);

    my $file_menuitem = Gtk2::MenuItem->new(gettext("_File"));
    $file_menuitem->set_submenu($file_menu);
    $menu_bar->append( $file_menuitem );
    
    my $setting_menuitem2 = Gtk2::MenuItem->new(gettext("_Setting"));
    $setting_menuitem2->set_submenu($setting_menu);
    $menu_bar->append($setting_menuitem2);

    my $help_menuitem = Gtk2::MenuItem->new(gettext("_Help"));
    $help_menuitem->set_submenu($help_menu);
    $menu_bar->append($help_menuitem);
    return $menu_bar;
}

sub create_canvas {
    my $canvas = Goo::Canvas->new;
    $canvas->set_size_request(330, 400);
    my $root = $canvas->get_root_item;
    my ($rows, $cols) = ($Config{rows}, $Config{cols});
    my $offset = [ 10, 10 ];
    my $border = 1;
    my $padding = 16;
    
    $canvas->{table} = Tetris::Table->new(
        $root, $offset->[0], $offset->[1],
        -columns => $cols,
        -rows => $rows,
        -border => $border,
    );
    my $px = $offset->[0]+($cols+( $border ? 2 : 0) )*Tetris::Cell::SIZE + $padding;
    $canvas->{preview} = Tetris::Table->new(
        $root, $px, $offset->[1],
        -columns => 4,
        -rows => 4,
        -border => $border,
    );
    use_keymap( $window, $Config{keybindings} );
    my $text_group = Goo::Canvas::Group->new($root);
    $text_group->translate( $px, $offset->[1] + 7 * Tetris::Cell::SIZE );
    my $text_spacing = 20;
    my @label = (
        Gtk2::Label->new(make_string('Scores', $score)),
        Gtk2::Label->new(make_string('Lines', $lines)),
        Gtk2::Label->new(make_string('Level', $level)),
    );
    foreach ( 0..$#label ) {
        $label[$_]->set_alignment(0, 0);
        Goo::Canvas::Widget->new(
            $text_group, $label[$_], 0, $text_spacing*$_,
            100, 20,
        );
    }
    $canvas->{labels} = \@label;
    # $canvas->{labels} = [
    #     Goo::Canvas::Text->new(
    #         $text_group, make_string('Scores', $score), 0, 0, -1, 'nw',
    #     ),
    #     Goo::Canvas::Text->new(
    #         $text_group, make_string("Lines", $lines), 0, $text_spacing, -1, 'nw',
    #     ),
    #     Goo::Canvas::Text->new(
    #         $text_group, make_string("Level", $level), 0, $text_spacing*2, -1, 'nw',
    #     )
    #     ];
    return $canvas;
}

sub make_string {
    my ($text, $num) = @_;
    my $str =  gettext($text) . ": " . $num;
    return $str;
}

sub use_keymap {
    my ($wid, $keymap) = @_;
    if ( exists $wid->{keymap_sigid} ) {
        $wid->signal_handler_disconnect($wid->{keymap_sigid});
    }
    $wid->{keymap_sigid} = $wid->signal_connect(
        'key-press-event' => \&on_key_pressed, $keymap
    );
}

sub on_key_pressed {
    my ($w, $ev, $keymap) = @_;
    my $key_nr = $ev->keyval;
    my $cb = $keymap->{$key_nr};
    $cb->($w) if $cb;
    return FALSE;
}

sub new_game {
    if ( $game_start ) {
        local $timer_pause = 1;
        my $stop = 1;
        my $dia = Gtk2::MessageDialog->new(
            $window, 'destroy-with-parent',
            'question',
            'yes-no',
            gettext("Cancel current game?"),
        );
        $dia->set_default_response('yes');
        my $response = $dia->run();
        $dia->destroy;
        if ( $response eq 'no' ) {
            return;
        }
    }
    remove_heading();
    $timer_pause = 0;
    $game_start = 1;
    ($score, $lines, $level) = (0, 0, $Config{start_level});
    update_label();
    if ( $timer ) {
        Glib::Source->remove($timer);
    }
    $timer = Glib::Timeout->add(speed(), \&update);
    $canvas->{table}->clear;
    my $s = int(rand(scalar(@$shapes)));
    my $t = int(rand(scalar(@{$shapes->[$s]})));
    $next_shape = [$s, $t];
    new_shape();
}

sub show_heading {
    my $text = shift;
    if ( exists $canvas->{heading} && $canvas->{heading} ) {
        $canvas->{heading}->set(
            'text' => $text
        );
    }
    else {
        $canvas->{heading} =
            Goo::Canvas::Text->new(
                $canvas->get_root_item, $text, 50, 200, -1, 'nw',
                'font' => 'Sans Bold 24',
                'fill-color' => 'red'
            );
    }
}

sub remove_heading {
    if ( exists $canvas->{heading} && $canvas->{heading} ) {
        my $root = $canvas->get_root_item;
        $root->remove_child($root->find_child($canvas->{heading}));
        $canvas->{heading} = undef;
    }
}

sub new_shape {
    my $shape  = Tetris::Shape->new(
        -shape => $shapes->[$next_shape->[0]],
        -type => $next_shape->[1],
        -color => $Config{style}[$next_shape->[0] % @{$Config{style}}],
        -col => int($Config{cols}/2)-2,
        -table => $canvas->{table},
    );
    $shape->draw();
    $canvas->{shape} = $shape;
    # Draw preview
    my $s = int(rand(scalar(@$shapes)));
    my $t = int(rand(scalar(@{$shapes->[$s]})));
    $next_shape = [$s, $t];
    $canvas->{preview}->clear;
    $shape = Tetris::Shape->new(
        -shape => $shapes->[$s],
        -type => $t,
        -color => $Config{style}[$s  % @{$Config{style}}],
        -row => 1,
        -col => 1,
        -table => $canvas->{preview},
    );
    $shape->draw();
}

sub update {
    # print "update $timer_pause\n";
    return TRUE if $timer_pause;
    my $shape  = $canvas->{shape};
    my $hit = $shape->move_down;
    if ( $hit ) {
        done();
    }
    return TRUE;
}

sub done {
    my $shape = $canvas->{shape};
    my $table = $canvas->{table};
    my %row = map { $_->[0] => 1 } @{$shape->cells};
    my $ln = $table->eliminate_line_maybe( keys %row );
    score($ln);
    if ( $table->is_full ) {
        game_over();
    }
    else {
        new_shape();
    }
}

sub score {
    my $ln = shift || 0;
    my $oldscore = $score;
    my @score = ( 0, 10, 20, 40, 60 );
    # my @score = ( 0, 20, 40, 70, 100 );    
    $score += $score[$ln];
    $lines += $ln;

    if ( int($score/100) > int($oldscore/100) ) {
        # print "levelup $level at $score\n";
        $level++;
        $level = $level % 11;   # level: 0-10
        Glib::Source->remove($timer);
        $timer = Glib::Timeout->add(speed(), \&update);
    }
    update_label();
}

sub update_label {
    my $labels = $canvas->{labels};
    $labels->[0]->set_label( make_string("Scores", $score));
    $labels->[1]->set_label( make_string("Lines", $lines));
    $labels->[2]->set_label( make_string("Level", $level));
}

sub speed {
    return int(500/($level*0.5+1));
}

sub rank_dia {
    my $score = shift;
    my ($idx, $new_iter, $new_entry);
    my $max = $Config{max_rank_list}-1;
    if ( defined $score ) {
        if ( !defined $history ) {
            $history = [ $new_entry ];
            $idx = 0;
        } else {
            $new_entry = [$Config{name} || getlogin || getpwuid($<) || 'Nobody', $score];
            $history = [ sort {$b->[1] <=> $a->[1]} @$history ];
            $idx = 0;
            while ( $idx <= $#$history ) {
                last if $score >= $history->[$idx][1];
                $idx++;
            }
            if ( $idx > $max && $idx > $#$history ) {
                return;
            } else {
                splice(@$history, $idx, 0, $new_entry);
                if ( $#$history > $max ) {
                    $#$history = $max;
                }
            }
        }
    }
    if ( @$history == 0 ) {
        my $dia = Gtk2::MessageDialog->new(
            $window, 'destroy-with-parent',
            'info',
            'ok',
            "No rank list yet!",
        );
        $dia->run;
        $dia->destroy;
        return FALSE;
    }
    my $dia = Gtk2::Dialog->new(
        'Rank', $window,
        ['modal', 'destroy-with-parent'],
        'gtk-ok' => 'ok',
    );
    my $vbox = $dia->vbox;
    my $store = Gtk2::ListStore->new( qw/Glib::String Glib::Int/ );
    foreach ( 0..$#$history ) {
        my $iter = $store->append();
        $store->set($iter,
                    0, $history->[$_][0],
                    1, $history->[$_][1],
                );
        if ( defined $idx && $idx == $_ ) {
            $new_iter  = $iter;
        }
    }
    my $treeview = Gtk2::TreeView->new($store);
    my $col = Gtk2::TreeViewColumn->new();
    $col->set_title('name');
    my $ren = Gtk2::CellRendererText->new;
    if ( defined $score ) {
        $ren->set_property('editable' => TRUE);
        $ren->{'renderer_number'} = 0;
        $ren->signal_connect(
            edited => sub {
                my ($cell, $path_string, $new_text) = @_;
                $new_entry->[0] = $new_text;
                $store->set($new_iter, 0, $new_text);
                $cell->set_property('editable' => FALSE);
                return FALSE;
            }
        );
    }
    $col->pack_start($ren, FALSE);
    $col->add_attribute($ren, text=>0);
    $treeview->append_column($col);

    my $col2 = Gtk2::TreeViewColumn->new();
    $col2->set_title('score');
    my $ren2 = Gtk2::CellRendererText->new;
    $col2->pack_start($ren2, FALSE);
    $col2->add_attribute($ren2, text=>1);
    $treeview->append_column($col2);

    $vbox->pack_start($treeview, FALSE, FALSE, 0);
    $dia->show_all;
    if ( defined $score ) {
        $treeview->set_cursor($store->get_path($new_iter), $col, TRUE);
    }
    $dia->signal_connect(
        response => sub {
            $dia->destroy;
            return FALSE;
        }
    );
}

sub game_over {
    Glib::Source->remove($timer);
    show_heading('Game Over');
    $game_start = 0;

    rank_dia($score);
}

sub rotate {
    return unless $game_start;
    # print "rotate\n";
    $canvas->{shape}->rotate;
}

sub down {
    return unless $game_start;
    my $shape = $canvas->{shape};
    while ( !$shape->move_down ) { }
    done();
}

sub move_down {
    return unless $game_start;
    # print "down\n";
    foreach ( 1..$Config{down_step} ) {
        $canvas->{shape}->move_down;
    }
}

sub move_right {
    return unless $game_start;
    # print "right\n";
    $canvas->{shape}->move_right;
}

sub move_left {
    return unless $game_start;
    # print "left\n";
    $canvas->{shape}->move_left;
}

sub pause {
    # print "pause $timer_pause\n";
    $timer_pause = !$timer_pause;
    if ( $timer_pause ) {
        show_heading('Pause');
    } else {
        remove_heading();
    }
    return FALSE;
}

sub parse_shapes {
    my $str;
    while ( <DATA> ) {
        next if /^#/;
        last if /^__END__/;
        $str .= $_;
    }
    my @shapes = grep {defined $_} map { shape_from_string($_) }
        ( split /\n\n/, $str );
    return \@shapes;
}

sub shape_from_string {
    my $str = shift;
    my @lines = grep {$_} split /\n/, $str;
    return if grep {/^#/} @lines;
    return if $#lines != 3;
    my @shape;
    foreach ( 0..$#lines ) {
        my @p = map {[split / /]} split /  +/, $lines[$_];
        map { push @{$shape[$_]}, $p[$_] } 0..$#p;
    }
    return \@shape;
}
    
sub dump_table {
    my $table = shift;
    for my $i( 1..$Config{rows} ) {
        for my $j( 1..$Config{cols} ) {
            if ( $table->[$i-1][$j-1] ) {
                print "$i, $j\n";
            }
        }
    }
}

sub write_history {
    my $str;
    my $found_mark;
    open(my $out, ">", \$str) or die "Can't write to string: $!\n";
    my $start_mark = "# HISTORY: Don't edit from this line to the line marked with END HISTORY.";
    my $end_mark = "# END HISTORY";
    my $conf =  $start_mark . "\n" .
        Data::Dumper->Dump([$history], ['history']) .
                $end_mark . "\n";
    if ( -e $config_file ) {
        open(my $fh, $config_file) or die "Can't open file $config_file: $!";
        while ( <$fh> ) {
            if ( /\Q$start_mark/ ) {
                $found_mark = 1;
                while ( <$fh>) {
                    last if /\Q$end_mark/;
                }
                print $out $conf;
            } else {
                print $out $_;
            }
        }
        close($fh);
    }
    if ( !$found_mark ) {
        print $out $conf;
        print $out "1;\n";
    }
    close($out);
    open(my $fh, ">$config_file") or die "Can't create file $config_file: $!";
    print $fh $str;
    close($fh);
}

1;

__DATA__
0 0 0 0
0 1 1 0
0 1 1 0
0 0 0 0

0 0 0 0   0 0 7 0
7 7 7 7   0 0 7 0
0 0 0 0   0 0 7 0
0 0 0 0   0 0 7 0

0 0 0 0   0 0 0 4
0 4 4 0   0 0 4 4
0 0 4 4   0 0 4 0
0 0 0 0   0 0 0 0

0 0 0 0   0 0 5 0
0 0 5 5   0 0 5 5
0 5 5 0   0 0 0 5
0 0 0 0   0 0 0 0

0 0 0 0   0 0 2 0   0 2 0 0   0 0 2 2
0 2 2 2   0 0 2 0   0 2 2 2   0 0 2 0
0 0 0 2   0 2 2 0   0 0 0 0   0 0 2 0
0 0 0 0   0 0 0 0   0 0 0 0   0 0 0 0

0 0 0 0   0 3 3 0   0 0 0 0   0 0 3 0
0 3 3 3   0 0 3 0   0 0 0 3   0 0 3 0
0 3 0 0   0 0 3 0   0 3 3 3   0 0 3 3
0 0 0 0   0 0 0 0   0 0 0 0   0 0 0 0

0 0 6 0   0 0 6 0   0 0 0 0   0 0 6 0
0 6 6 6   0 0 6 6   0 6 6 6   0 6 6 0
0 0 0 0   0 0 6 0   0 0 6 0   0 0 6 0
0 0 0 0   0 0 0 0   0 0 0 0   0 0 0 0
__END__

=head1 NAME

tetris -  A tetris game

=head1 SYNOPSIS

perl tetris.pl

=head1 CONFIGURATION

The configuration file should be the file with name ".tetris" under
HOME directory. Another option is using Tetris/Config.pm in any
directory of @INC.

Here is an example of configuration:
    # -*- perl -*-
    %Config = (
        %Config,
        'start_level' => 3,
        'down_step' => 2,
        'keybindings' => {
            %{$Config{keybindings}},
            ord('j') => \&move_left,
            ord('l') => \&move_right,
            ord('k') => \&rotate,
            ord('n') => \&new_game,
        }
    );
    push @$shapes, shape_from_string(<<SHAPE);
    0 0 8 0   0 8 0 0   0 8 8 8   0 0 0 8
    0 0 8 0   0 8 8 8   0 0 8 0   0 8 8 8
    0 8 8 8   0 8 0 0   0 0 8 0   0 0 0 8
    0 0 0 0   0 0 0 0   0 0 0 0   0 0 0 0
    SHAPE
    
    push @$shapes, shape_from_string(<<SHAPE);
    0 0 9 0   0 0 0 0   0 0 0 0   0 0 9 0
    0 0 9 9   0 0 9 9   0 9 9 0   0 9 9 0
    0 0 0 0   0 0 9 0   0 0 9 0   0 0 0 0
    0 0 0 0   0 0 0 0   0 0 0 0   0 0 0 0
    SHAPE
         
The key specification can get from this script:

    use Gtk2::Gdk::Keysyms;
    use Glib qw/TRUE FALSE/;
    use Gtk2 -init;
    
    my $window = Gtk2::Window->new ('toplevel');
    $window->signal_connect (delete_event => sub { Gtk2->main_quit });
    $window->signal_connect('key-press-event' => \&show_key);
    	
  	my $label = Gtk2::Label->new();
   	$label->set_markup("<span foreground=\"blue\" size=\"x-large\">Type something on the keyboard!</span>");
    	
    $window->add ($label);
    $window->show_all;
    $window->set_position ('center-always');
    
    Gtk2->main;

    sub show_key {
        my ($widget,$event,$parameter)= @_;
        my $key_nr = $event->keyval();
    	foreach my $key (keys %Gtk2::Gdk::Keysyms) {
    		my $key_compare = $Gtk2::Gdk::Keysyms{$key};
    		if ($key_compare == $key_nr) {
                print "'$key' => $key_nr,\n";
            }
        }
    	return FALSE;
    }

Code to run after the GUI setup, add to code ref $after_load_function.

=cut