#! /usr/bin/env raku

use v6.d;

use Game::Amazing;
use Pod::To::Text;

use NativeCall;
use Gnome::N::N-GObject;

use Gnome::Gdk3::Events;
use Gnome::Gdk3::Types;
use Gnome::Gdk3::Keysyms;

use Gnome::Gtk3::Main;
use Gnome::Gtk3::Label;
use Gnome::Gtk3::Enums;
use Gnome::Gtk3::Window;
use Gnome::Gtk3::Grid;
use Gnome::Gtk3::Button;

use Gnome::Gtk3::TextBuffer;
use Gnome::Gtk3::TextView;
use Gnome::Gtk3::TextTagTable;
use Gnome::Gtk3::TextTag;
use Gnome::Gtk3::TextIter;

use Gnome::Gdk3::RGBA;
use Gnome::GObject::Value;
use Gnome::GObject::Type;

use Gnome::N::X;
# Gnome::N::debug(:on);

my $m;
my $g;
my $h;
my $curr-row;
my $curr-col;
my $moves;
my $finished;
my $saved;
my $curr-zoom;
my $traversable;
my $traversable-path;
my $now;
my $random;
my $file-name;

my Gnome::Gtk3::Window     $w;
my Gnome::Gtk3::TextBuffer $tb;
my Gnome::Gtk3::TextView   $tv;
my Gnome::Gtk3::Label      $lbl;
my Gnome::Gtk3::Button     $btn-exit;
my Gnome::Gtk3::Button     $btn-new1;
my Gnome::Gtk3::Button     $btn-new2;
my Gnome::Gtk3::Button     $btn-new3;
my Gnome::Gtk3::Button     $btn-path;
my Gnome::Gtk3::Button     $btn-save;
my Gnome::Gtk3::Button     $btn-help;
my Gnome::Gtk3::Button     @edit-buttons;

my $help-text = q:to/END/; 
Welcome to amazing-gtk, the playable frontend to 'Game::Amazing'.

You start in the upper left corner, shown in red. Use the array keys to
navigate. The game is finished when you reach the exit, the green box in
the lower right corner, and the arrow keys will be disabled.

Use the 'plus' or 'minus' keys to change the zoom. The initial value is
1, unless changed with the «-z=<int>» command line option. Legal values
are 1, 2, 3, 4 and 5.

Click 'Exit' to exit at any time.

The 'New*-buttons are only active at the start and stop posititions, and
only if you have requested a random maze (i.e. not loaded a maze file).
The 'New -' button will generate a new maze with 1 row and column less,
'New' will use the same size, and 'New +' will increase both with 1.

The 'Path' and 'Save' buttons are only enabled when the game is finished.
Click on 'Path' to see the shortest path through the maze. Click on 'Save'
to save the maze for future use. Take a note of the filename and position.

Specify a maze file on the command line, or none to get a randomly generated
one. Random mazes support size parameters on the command line: »-b=<int>»
(box) is the number of rows and columns. The default value is 25. It is
possible to specify the rows with «-r=<int>» and the columns with «-c=<int>».

The randomly generated mazes will always be traversable, but it is possible
to load untraversable maze files.

Good luck!

© Arne Sommer, 2020-2021. Published under the Artistic License
END



my $help-text-edit = q:to/END/; 
Welcome to the maze editor mode of amazing-gtk, the playable frontend to
'Game::Amazing'.

The shortest path will always be shown, if any. If not, a cverage map is shown
instead.

Use the array keys to navigate. Click on any of the symbol buttons to change
the current cell (except the entrance, shown in red, in the upper left corner,
and the exit, shown in green, in the lower right corner). The shortest
path/coverage map is updated in real time.

The numeric keybord can be used instead of the buttons:
    / -> ═      * -> (space)     * -> ║
    7 -> ╔      8 -> ╦           9 -> ╗
    4 -> ╠      5 -> ╬           6 -> ╣
    1 -> ╚      2 -> ╩           3 -> ╝

You can also use the main keyboard:
    1 -> ═      2 -> (space)     3 -> ║
    q -> ╔      w -> ╦           e -> ╗
    a -> ╠      s -> ╬           d -> ╣
    z -> ╚      x -> ╩           c -> ╝

You can also toggle individual directions on/off, with the following keyboard
shortcuts:
          u -> North 
    h -> West   j -> East
          n -> South

Toggle mode can be disabled with [-t|--toggle-off]. In that case the keys
mentioned above will _set_ the direction, and they can be _removed_ with
the uppercase versions of the same keys.

If you edit an existing maze file, it will write it back when you save the
maze. If not, you will get a random filename with the size (rows and columns)
included. Further savings will use the same filename, and overwrite an
earlier version.

The three New-buttons and the Path-button are disabled in edit mode.

Good luck!

© Arne Sommer, 2020-2021. Published under the Artistic License

END

multi MAIN ($file where $file.IO.f && $file.IO.r, :z(:$zoom) where 1 <= $zoom <= 5 = 1, :e(:$edit), :n($no-path), :t($toggle-off), :v($verbose))
{
  $m = Game::Amazing.new: $file;
  $random    = False;
  $file-name = $file;
  start-game($m, 0, init => True, zoom => $zoom, :$edit, :$no-path, :$toggle-off, :$verbose);
}

multi MAIN (:b(:$box) = 25, :r(:$rows) = $box, :c(:$cols) = $box, :s(:$scale) = 7, :z(:$zoom) where 1 <= $zoom <= 5 = 1, :e(:$edit), :n($no-path), :t($toggle-off), :v($verbose))
{
  $m = Game::Amazing.new(:$rows, :$cols, :$scale, ensure-traversable => True, :$verbose);
  $random = True;
  start-game($m, $scale, init => True, zoom => $zoom, :$edit, :$no-path, :$toggle-off, :$verbose);
}

multi MAIN(Bool :h(:$help))
{
  say pod2text($=pod);
}

sub again ($m, $scale, :$rows = $m.rows, :$cols = $m.cols, :$zoom, :$edit = False, :$no-path = False, :$toggle-off = False, :$verbose = False)
{
  $m.new(:$rows, :$cols, :$scale, ensure-traversable => True, :$verbose);
  start-game($m, $scale, :$zoom, :$edit, :$no-path, :$toggle-off, :$verbose);
}

sub start-game ($m, $scale, :$init = False, :$zoom, :$edit = False, :$no-path = False, :$toggle-off = False, :$verbose = False)
{
  $curr-row  = 0;
  $curr-col  = 0;
  $moves     = 0;
  $finished  = False;
  $saved     = False;
  $curr-zoom = 1;

  $traversable      = $m.is-traversable(:force);
  $traversable-path = $m.get-path;

  $now = now;

  if $init
  {
    $g = Gnome::Gtk3::Main.new;
    $w = Gnome::Gtk3::Window.new;

    $w.set_title($edit ?? 'Amazingly Raku - Editor' !! 'Amazingly Raku'  );
    $w.set-border-width(20);

    my Gnome::Gtk3::Grid $grid .= new;
    $w.gtk-container-add($grid);
    
    $lbl .= new(text => 'Use +/- to change zoom, and arrow keys to navigate.');
    $grid.gtk-grid-attach( $lbl, 0, 1+4, 7, 1);

    $btn-exit .= new(:label('Exit'));
    $grid.gtk-grid-attach( $btn-exit, 0, 2+4, 1, 1);
    $btn-exit.set_tooltip_text('Exit');

    $btn-new1 .= new(:label('New -'));
    $grid.gtk-grid-attach( $btn-new1, 1, 2+4, 1, 1);
    $btn-new1.set_tooltip_text('New game with smaller size');
 
    $btn-new2 .= new(:label('New'));
    $grid.gtk-grid-attach( $btn-new2, 2, 2+4, 1, 1);
    $btn-new2.set_tooltip_text('New game with same size');

    $btn-new3 .= new(:label('New +'));
    $grid.gtk-grid-attach( $btn-new3, 3, 2+4, 1, 1);
    $btn-new3.set_tooltip_text('New game with larger size');

    $btn-path .= new(:label('Path'));
    $grid.gtk-grid-attach( $btn-path, 4, 2+4, 1, 1);
    $btn-path.set_tooltip_text('Show the shortest path for a finished game');

    $btn-save .= new(:label('Save'));
    $grid.gtk-grid-attach( $btn-save, 5, 2+4, 1, 1);
    $btn-save.set_tooltip_text('Save the game');

    $btn-help .= new(:label('Help'));
    $grid.gtk-grid-attach( $btn-help, 6, 2+4, 1, 1);
    $btn-help.set_tooltip_text('Show the Help window');

    $btn-path.set-sensitive(False);
    $btn-save.set-sensitive(False) unless $edit;
    unless $random { $_.set-sensitive(False) for ($btn-new1, $btn-new2, $btn-new3); }

    $tb .= new;
    $tv .= new;
    $tv.set_buffer($tb);
    $tv.set_editable(False);
    $tv.set_cursor_visible(False);
    $tv.set_monospace(True);
    $grid.gtk-grid-attach( $tv, 0, 0, 7, 4);

    if $edit
    {
      dir-button('═', 1, 1); dir-button(' ', 1, 2); dir-button('║', 1, 3);
      dir-button('╔', 2, 1); dir-button('╦', 2, 2); dir-button('╗', 2, 3);
      dir-button('╠', 3, 1); dir-button('╬', 3, 2); dir-button('╣', 3, 3); 
      dir-button('╚', 4, 1); dir-button('╩', 4, 2); dir-button('╝', 4, 3); 
    }
    
    sub dir-button ($label, $row, $col)
    {
      my Gnome::Gtk3::Button $btn .= new(:label($label));
      $grid.gtk-grid-attach($btn, $col +7, $row -1, 1, 1);
      $btn.set_tooltip_text("Set current symbol to «$label»");
      @edit-buttons.push: $btn;
    }
  }
  
  $tb.set_text($m.as-string.chomp);

  setup-tags($tb) if $init;
  
  set-zoom($zoom, :silent);
  set-message('Use +/- to change zoom, and arrow keys to navigate.');

  set-maze-cell('start',   0, 0);
  set-maze-cell('current', 0, 0);
  set-maze-cell('exit',    $m.rows -1, $m.cols -1);

  if $init
  {
    class AppSignalHandlers
    {
      method exit-program ( :$widget --> Int )
      {
        $g.gtk-main-quit;
        return 1;
      }

      method again ( :$widget, :$offset = 0 --> Int)
      {
	$btn-path.set-sensitive(False);
	$btn-save.set-sensitive(False) unless $edit;
        again($m, $scale, rows => $m.rows + $offset, cols => $m.cols + $offset, zoom => $curr-zoom, :$edit, :$no-path, :$toggle-off, :$verbose);
	return 1;
      }

      method path ( :$widget --> Int )
      {
        $m.is-traversable
	 ?? show-shortest-path()
	 !! show-coverage();
	return 1;
      }
      
      method save ( :$widget --> Int )
      {
        return if $saved;
	$file-name = $file-name
	  ?? $m.save($file-name)
          !! $m.save(:with-size);
	  
        set-message("Saved maze as <span foreground='blue'>{ $file-name }</span>");
	$saved = True unless $edit;
	$btn-save.set-sensitive(False) unless $edit;
	return 1;
      }
      
      method help-close
      {
        $h.close;
	$h = Any;
        $btn-help.set-sensitive(False);
      }

      method help-open ( :$widget --> Int )
      {
        $btn-help.set-sensitive(False);
        open-help;
	return 1;
      }

      method change-symbol ( :$widget, :$symbol --> Int  ) ## Duplicate of sub
      {      
        return 1 if $curr-row == $curr-col == 0;
	return 1 if $curr-row == $m.rows -1 && $curr-col == $m.cols -1;
   
	$m.set-cell($curr-row, $curr-col, $symbol);
        $tb.set_text($m.as-string.chomp);
	set-zoom($curr-zoom, :force, :silent) unless $curr-zoom == 1;
        set-maze-cell('current', $curr-row, $curr-col);

        self.is-traversable(:force)
	  ?? show-shortest-path()
	  !! show-coverage();
	  
	return 1;
      }

      method keyboard-event ( GdkEvent $event, :$widget, :$time --> Int )
      {
        my $key = $event.event-key.keyval;
	
        $btn-help.grab-focus;

        if $key == (GDK_KEY_plus | GDK_KEY_minus)
        {
          if $key == GDK_KEY_plus && $curr-zoom < 5
          {
            set-zoom($curr-zoom + 1);
          }
          elsif $key == GDK_KEY_minus && $curr-zoom > 1
          {
            set-zoom($curr-zoom - 1);
          }
          return 1;
        }
    
        if $key == (GDK_KEY_Down | GDK_KEY_Up | GDK_KEY_Left | GDK_KEY_Right)
        {  
	  return 1 if $finished;
	  
          my $old-row = $curr-row;
          my $old-col = $curr-col;

          if $edit
	  {
            if    $key == GDK_KEY_Down  { $curr-row++ if $curr-row < $m.rows -1}
            elsif $key == GDK_KEY_Up    { $curr-row-- if $curr-row >= 1 }
            elsif $key == GDK_KEY_Left  { $curr-col-- if $curr-col >= 1 }
            elsif $key == GDK_KEY_Right { $curr-col++ if $curr-col < $m.cols -1}
          }
	  else
	  {
            my $directions = $m.get-directions($curr-row, $curr-col);

            if    $key == GDK_KEY_Down  { $curr-row++ if $directions ~~ /S/ }
            elsif $key == GDK_KEY_Up    { $curr-row-- if $directions ~~ /N/ }
            elsif $key == GDK_KEY_Left  { $curr-col-- if $directions ~~ /W/ }
            elsif $key == GDK_KEY_Right { $curr-col++ if $directions ~~ /E/ }
          }
	  
          if $old-row != $curr-row || $old-col != $curr-col
          {
            if $edit
	    {
	      unset-maze-cell('current', $old-row, $old-col);
	    }
	    elsif ! $no-path
	    {
	      set-maze-cell('path', $old-row,  $old-col);
	    }
	    else
	    {
	      unset-maze-cell('current', $old-row, $old-col);
	    }
	    
            set-maze-cell('current', $curr-row, $curr-col);
    	    $moves++;
          }

          if ($curr-row == $curr-col == 0)
	  {
            if $random { $_.set-sensitive(True) for ($btn-new1, $btn-new2, $btn-new3); }
          }
          elsif ($curr-row == $m.rows -1 && $curr-col == $m.cols -1)
          {
	    unless $edit
	    {
              $finished = True;
    	      finished;
	    }
	    
	    if $random { $_.set-sensitive(True) for ($btn-new1, $btn-new2, $btn-new3); }
            $btn-path.set-sensitive(True);
            $btn-save.set-sensitive(True) unless $edit;
	  }
	  elsif $old-row == $old-col == 0
	  {
            $_.set-sensitive(False) for ($btn-new1, $btn-new2, $btn-new3);
          }
   
          return 1;
        }

        if $key == (GDK_KEY_1 |GDK_KEY_2 | GDK_KEY_3 |
	            GDK_KEY_q |GDK_KEY_w | GDK_KEY_e |
                    GDK_KEY_a |GDK_KEY_s | GDK_KEY_d |
	            GDK_KEY_z |GDK_KEY_x | GDK_KEY_c |
		    
	            GDK_KEY_KP_Divide | GDK_KEY_KP_Multiply | GDK_KEY_KP_Subtract |
	            GDK_KEY_KP_1 | GDK_KEY_KP_2 | GDK_KEY_KP_3 |
		    GDK_KEY_KP_4 | GDK_KEY_KP_5 | GDK_KEY_KP_6 |
		    GDK_KEY_KP_7 | GDK_KEY_KP_8 | GDK_KEY_KP_9 |
		    
		    GDK_KEY_u |GDK_KEY_h | GDK_KEY_j | GDK_KEY_n |
		    GDK_KEY_U |GDK_KEY_H | GDK_KEY_J | GDK_KEY_N)
        {  
	  return 1 unless $edit;
	  return 1 if $curr-row == $curr-col == 0;
	  return 1 if $curr-row == $m.rows -1 && $curr-col == $m.cols -1;

          if    $key == GDK_KEY_KP_Divide   | GDK_KEY_1 { change-symbol('═'); }
	  elsif $key == GDK_KEY_KP_Multiply | GDK_KEY_2 { change-symbol(' '); }
	  elsif $key == GDK_KEY_KP_Subtract | GDK_KEY_3 { change-symbol('║'); }
          elsif $key == GDK_KEY_KP_7        | GDK_KEY_q { change-symbol('╔'); }
	  elsif $key == GDK_KEY_KP_8        | GDK_KEY_w { change-symbol('╦'); }
	  elsif $key == GDK_KEY_KP_9        | GDK_KEY_e { change-symbol('╗'); }
	  elsif $key ==	GDK_KEY_KP_4        | GDK_KEY_a { change-symbol('╠'); }
	  elsif $key == GDK_KEY_KP_5        | GDK_KEY_s { change-symbol('╬'); }
	  elsif $key == GDK_KEY_KP_6        | GDK_KEY_d { change-symbol('╣'); }
	  elsif $key ==	GDK_KEY_KP_1        | GDK_KEY_z { change-symbol('╚'); }
	  elsif $key == GDK_KEY_KP_2        | GDK_KEY_x { change-symbol('╩'); }
	  elsif $key == GDK_KEY_KP_3        | GDK_KEY_c { change-symbol('╝'); }

	  elsif $key == GDK_KEY_u { $toggle-off ?? add-direction('N') !! toggle-direction('N'); }
	  elsif $key == GDK_KEY_h { $toggle-off ?? add-direction('W') !! toggle-direction('W'); }
	  elsif $key == GDK_KEY_j { $toggle-off ?? add-direction('E') !! toggle-direction('E'); }
	  elsif $key == GDK_KEY_n { $toggle-off ?? add-direction('S') !! toggle-direction('S'); }
	  
	  elsif $key == GDK_KEY_U { remove-direction('N') if $toggle-off; }
	  elsif $key == GDK_KEY_H { remove-direction('W') if $toggle-off; }
	  elsif $key == GDK_KEY_J { remove-direction('E') if $toggle-off; }
	  elsif $key == GDK_KEY_N { remove-direction('S') if $toggle-off; }
        }

        return 0;
      }

      method mouse-event ( GdkEvent $event, :widget($window) --> Int )
      {
        return 1;
      }
    }

    my AppSignalHandlers $ash .= new(:$w);

    $btn-exit.register-signal($ash, 'exit-program', 'clicked');
    $btn-new1.register-signal($ash, 'again',        'clicked', offset => -1, :$verbose);
    $btn-new2.register-signal($ash, 'again',        'clicked', offset =>  0, :$verbose);
    $btn-new3.register-signal($ash, 'again',        'clicked', offset =>  1, :$verbose);
    $btn-path.register-signal($ash, 'path',         'clicked');
    $btn-save.register-signal($ash, 'save',         'clicked');
    $btn-help.register-signal($ash, 'help-open',    'clicked');

    $w.register-signal($ash, 'exit-program',   'destroy');
    $w.register-signal($ash, 'keyboard-event', 'key-press-event', :time(now));

    if $edit
    {
      $_.register-signal($ash, 'change-symbol', 'clicked', symbol => $_.get-label) for @edit-buttons;
      show-shortest-path if $traversable;
    }

    $w.show-all;
    $g.gtk-main;
  }

  sub set-maze-cell ($tag, $row, $col, :$size = 1)
  {
    my $pos1 = $tb.get-iter-at-line-offset($row, $col);
    my $pos2 = $tb.get-iter-at-line-offset($row, $col + $size);

    $tag eq 'path'
      ?? $tb.remove_tag_by_name('current', $pos1, $pos2)
      !! $tb.remove_tag_by_name('path', $pos1, $pos2);

    $tb.apply_tag_by_name($tag, $pos1, $pos2);
  }
  
  sub unset-maze-cell ($tag, $row, $col)
  {
    my $pos1 = $tb.get-iter-at-line-offset($row, $col);
    my $pos2 = $tb.get-iter-at-line-offset($row, $col +1);
    $tb.remove_tag_by_name($tag, $pos1, $pos2);
  }
  
  sub set-zoom ($new-zoom, :$silent, :$force)
  {
    unless $force
    {
      return 0 unless 1 <= $new-zoom <= 5;
      return 0 if $new-zoom == $curr-zoom;
      
      $tb.remove_tag_by_name("zoom$curr-zoom", $tb.get_start_iter, $tb.get_end_iter);
    }
    
    $tb.apply_tag_by_name("zoom$new-zoom", $tb.get_start_iter, $tb.get_end_iter);

    if $new-zoom < $curr-zoom && !$force
    {
      $w.resize(1,1);
    }

    $curr-zoom = $new-zoom;
    set-message("Zoom: $curr-zoom") unless $silent;
    return 1;
  }

  sub setup-tags ($parent)
  {
    my Gnome::Gtk3::TextTagTable $ttt .= new(:native-object($parent.get-tag-table));

    my Gnome::Gtk3::TextTag $tt_short .= new(:tag-name<short>);
    my Gnome::GObject::Value $gv      .= new(:type(G_TYPE_STRING), :value<yellow>);
    $tt_short.set-property('background', $gv);
    $ttt.gtk-text-tag-table-add($tt_short);

    my Gnome::Gtk3::TextTag $tt_cover .= new(:tag-name<cover>);
    $gv                               .= new(:type(G_TYPE_STRING), :value<orange>);
    $tt_cover.set-property('background', $gv);
    $ttt.gtk-text-tag-table-add($tt_cover);
    
    my Gnome::Gtk3::TextTag $tt_curr  .= new(:tag-name<current>);
    $gv                              .= new(:type(G_TYPE_STRING), :value<blue>);
    $tt_curr.set-property('background', $gv);
    $gv                               .= new(:type(G_TYPE_STRING), :value<white>);
    $tt_curr.set-property('foreground', $gv);
    $ttt.gtk-text-tag-table-add($tt_curr);

    my Gnome::Gtk3::TextTag $tt_path  .= new(:tag-name<path>);
    $gv                               .= new(:type(G_TYPE_STRING), :value<blue>);
    $tt_path.set-property('foreground', $gv);
    $ttt.gtk-text-tag-table-add($tt_path);


    my Gnome::Gtk3::TextTag $tt_zoom1 .= new(tag-name => 'zoom1');
    $gv                               .= new(:type(G_TYPE_DOUBLE), :value<1.1e0>);
    $tt_zoom1.set-property('scale', $gv);
    $ttt.gtk-text-tag-table-add($tt_zoom1);

    my Gnome::Gtk3::TextTag $tt_zoom2 .= new(tag-name => 'zoom2');
    $gv                               .= new(:type(G_TYPE_DOUBLE), :value<1.2e0>);
    $tt_zoom2.set-property('scale', $gv);
    $ttt.gtk-text-tag-table-add($tt_zoom2);

    my Gnome::Gtk3::TextTag $tt_zoom3 .= new(tag-name => 'zoom3');
    $gv                               .= new(:type(G_TYPE_DOUBLE), :value<1.4e0>);
    $tt_zoom3.set-property('scale', $gv);
    $ttt.gtk-text-tag-table-add($tt_zoom3);

    my Gnome::Gtk3::TextTag $tt_zoom4 .= new(tag-name => 'zoom4');
    $gv                               .= new(:type(G_TYPE_DOUBLE), :value<1.6e0>);
    $tt_zoom4.set-property('scale', $gv);
    $ttt.gtk-text-tag-table-add($tt_zoom4);

    my Gnome::Gtk3::TextTag $tt_zoom5 .= new(tag-name => 'zoom5');
    $gv                               .= new(:type(G_TYPE_DOUBLE), :value<1.8e0>);
    $tt_zoom5.set-property('scale', $gv);
    $ttt.gtk-text-tag-table-add($tt_zoom5);

    my Gnome::Gtk3::TextTag $tt_start .= new(:tag-name<start>);
    $gv                               .= new(:type(G_TYPE_STRING), :value<red>);
    $tt_start.set-property('foreground', $gv);
    $ttt.gtk-text-tag-table-add($tt_start);

    my Gnome::Gtk3::TextTag $tt_exit  .= new(:tag-name<exit>);
    $gv                               .= new(:type(G_TYPE_STRING), :value<green>);
    $tt_exit.set-property('foreground', $gv);
    $ttt.gtk-text-tag-table-add($tt_exit);
    
    
    $gv.clear-object;
  }
  
  sub show-shortest-path
  {
    my $path = $m.get-path;
    my $row = 0;
    my $col = 0;
  
    set-maze-cell('start', 0, 0);
    set-maze-cell('short', 0, 0);
  
    for $path.comb -> $direction
    {
      if    $direction eq "N" { $row--; }
      elsif $direction eq "S" { $row++; }
      elsif $direction eq "E" { $col++; }
      elsif $direction eq "W" { $col--; }
    
      set-maze-cell('short', $row, $col)
    }

    set-maze-cell('exit',    $m.rows -1, $m.cols -1);

    set-message("Shortest path: { $path.chars } steps. Difficulty rating: { $m.get-difficulty }"); 
  }

  sub show-coverage
  {
    my @coverage = $m.get-coverage;
    my $count = 0;

    my $start-row;
    my $start-col;
    my $size = 0;

    set-maze-cell('start', 0, 0);
    
    for ^$m.rows -> $row
    {
      for ^$m.cols -> $col
      {
        if @coverage[$row][$col]
        {
	  if $size == 0 { $start-row = $row; $start-col = $col; }
	  $count++;
          $size++;

          if $col == $m.cols -1
	  {
            set-maze-cell('cover', $start-row, $start-col, :$size);
	    $size = 0;
          }
	}
	elsif $size
	{
          set-maze-cell('cover', $start-row, $start-col, :$size);
	  $size = 0;
	}
      }
    }

    set-maze-cell('cover', $start-row, $start-col, :$size) if $size;

    set-maze-cell('exit',    $m.rows -1, $m.cols -1);
    
    set-message("Coverage: { ($count / $m.cols / $m.rows * 100).round }%");
  }

  sub open-help
  {   
    $h = Gnome::Gtk3::Window.new;

    $h.set_title('Amazingly Raku | Help');
    $h.set-border-width(20);
    my Gnome::Gtk3::Grid $grid .= new;
    $h.gtk-container-add($grid);

    my Gnome::Gtk3::TextBuffer $tb .= new;
    my Gnome::Gtk3::TextView   $tv .= new;
    $tv.set_buffer($tb);
    $tv.set_editable(False);
    $tv.set_cursor_visible(False);

    # $tv.set_monospace(True);
    $grid.gtk-grid-attach( $tv, 0, 0, 1, 1);
    $tb.set_text($edit ?? $help-text-edit !! $help-text);

    my Gnome::Gtk3::Button $btn .= new(:label('Close'));
    $grid.gtk-grid-attach($btn, 0, 1, 1, 1);
    $btn.set_tooltip_text('Close this window');
    my AppSignalHandlers $ash .= new(:$w);
    $btn.register-signal($ash, 'help-close', 'clicked');
    $h.show-all;
  }

  sub set-message ($message)
  {
    $lbl.set-markup($message);
  }

  sub finished
  {
    set-message("Finished in $moves moves ({ ($moves / $traversable-path.chars * 100).Int }% and { floor(now - $now) } seconds).");
  }

  sub change-symbol ($symbol) ## Duplicate of method
  {
    return if $curr-row == $curr-col == 0;
    return if $curr-row == $m.rows -1 && $curr-col == $m.cols -1;
	
    $m.set-cell($curr-row, $curr-col, $symbol);
    $tb.set_text($m.as-string.chomp);
    set-zoom($curr-zoom, :force, :silent) unless $curr-zoom == 1;
    set-maze-cell('current', $curr-row, $curr-col);

     $m.is-traversable(:force)
       ?? show-shortest-path()
       !! show-coverage();
  }
  
  sub remove-direction ($direction)
  {
    return if $curr-row == $curr-col == 0;
    return if $curr-row == $m.rows -1 && $curr-col == $m.cols -1;

    return unless $m.remove-direction($curr-row, $curr-col, $direction);
    
    $tb.set_text($m.as-string.chomp);
    set-zoom($curr-zoom, :force, :silent) unless $curr-zoom == 1;
    set-maze-cell('current', $curr-row, $curr-col);

    $m.is-traversable(:force)
      ?? show-shortest-path()
      !! show-coverage();
  }
  
  sub add-direction ($direction)
  {
    return if $curr-row == $curr-col == 0;
    return if $curr-row == $m.rows -1 && $curr-col == $m.cols -1;

    return unless $m.add-direction($curr-row, $curr-col, $direction);
    
    $tb.set_text($m.as-string.chomp);
    set-zoom($curr-zoom, :force, :silent) unless $curr-zoom == 1;
    set-maze-cell('current', $curr-row, $curr-col);

    $m.is-traversable(:force)
      ?? show-shortest-path()
      !! show-coverage();
  }
  
  sub toggle-direction ($direction)
  {
    return if $curr-row == $curr-col == 0;
    return if $curr-row == $m.rows -1 && $curr-col == $m.cols -1;

    return unless $m.toggle-direction($curr-row, $curr-col, $direction);
    
    $tb.set_text($m.as-string.chomp);
    set-zoom($curr-zoom, :force, :silent) unless $curr-zoom == 1;
    set-maze-cell('current', $curr-row, $curr-col);

    $m.is-traversable(:force)
      ?? show-shortest-path()
      !! show-coverage();
  }

}



=begin pod

=head1 NAME

amazing-gtk - Play mazes in a window (using GTK)

=head1 SYNOPSIS

Try to find your way through a maze, either user specified (by 'mazemaker' or
this program in edit mode) or randomly generated.

Usage:

    amazing-gtk [-z|--zoom=<Int>] [-n|--no-path] [-s|--scale=<Int>] <maze-file>
    amazing-gtk [-z|--zoom=<Int>] [-n|--no-path] [-s|--scale=<Int>] [-b|--box=<Int>] [-r|--rows=<Int>] [-c|--cols=<Int>] 
    amazing-gtk [-z|--zoom=<Int>] [-e|--edit] [-t|--toggle-off] [-s|--scale=<Int>] <maze-file>
    amazing-gtk [-z|--zoom=<Int>] [-e|--edit] [-t|--toggle-off] [-s|--scale=<Int>] [-b|--box=<Int>] [-r|--rows=<Int>] [-c|--cols=<Int>]
    amazing-gtk [-h|--help]

Use the «[-e|--edit]» option to open the maze in edit mode. See the 'EDIT MODE' section for
more information.

Use the [-z|--zoom=<Int>] to set the initial zoom (size of the symbols). The default value is
1, and legal values are 1, 2, 3, 4 and 5. The zoom level can be changed any time with the «+»
(plus) and «-» (minus) keys.

The path is highlighetd behind you, as you go along the maze. This can be disabled with the 
[-n|--no-path] option.

If you specify a maze-file, usually with the «.maze» extension, it will be loaded. The maze
does not have to be traversable.

If you don't specify a maze-file, the program will generate a traversable one for you
automatically. The default size is 25 rows and columns, but this can be changed with the
[-b|--box=<Int>] option. If you want to only set one of them, use [-r|--rows=<Int>] for
the number of rows, or [-c|--cols=<Int>] for the number of columns.

The maze symbols with two exits ('╗', '═', '╝', '╔', '║' and '╚') have weight 1, and the rest,
with three ('╦', '╠', '╣' and '╩') and four ('╬') exits have weight 7. That means that the
last symbols are 7 times more likely to be choosen. You can override that with
[-s|--scale=<Int>].

Spurious exits out of the maze are possible, but will not work as exits. The randomly generated
mazes does not have them, to avoid confusion.

The randomly generated mazes will always be traversable, and it may take some time to generate
the maze as the program will generate random mazes until it gets one that is traversable.
Especially for lower [-s|--scale=<Int>] values and large mazes. If you want to see how many
mazes the program generates before it arrives at a traversable one, use the (undocumented)
[-v|--verbose] option.

Examples:

    amazing-gtk -h
    
    amazing-gtk -d mazes/25x25-ok.maze
    amazing-gtk mazes/25x25-ok.maze

    amazing-gtk
    amazing-gtk -b=20
    amazing-gtk -b=20 -c=30
    amazing-gtk -r=20 -c=30
    amazing-gtk -s=1 -n


=head1 PLAYING

You start at the upper left corner (shown in green, and blue initially).

Use the arrow keys to navigate through the maze. Illegal moves are not possible. Visited
cells are highlighted as you go along (except when disabled), to make it easier to see
where you have been. 

Your task is to traverse the maze and arrive at the exit (the green box in the lower
right corner). Randomly generated mazes will always be traversable, but a maze loaded
from a file may or may not be traversable.

You are presented with the score when finished, the time it took and the number of steps.
The latter is compared to the shortest path through the maze. Click on the «Path» button
to see the shortest path (or rather one of them, as there may be several with the same
length). Click on the «Save» button to save the maze with a random filename. Note that
the number of rows and columns are included in the name, which is shown.

Random maze mode gives you the possibility to start new games, but only at the entrance or
exit. (You can always go back to the entrance, if stuck, to start a new game.) Click on
the «New -» button to start with a smaller maze (1 less in both row and columns), «New»
to get one with the same size, and «new +» for a larger maze (1 more in both row and
columns).

Click on the «Exit» button (or press «Control-C» in the shell) to exit.

=head1 EDIT MODE

Navigate with the arrow keys as in the game, but you can now move to neighbouring cells
regardless of connections. 

The shortest path is highligted in yellow. If the maze is untraversable, the part of the
maze reachable from the entrance is shown in orange (a coverage map).

Click on the symbol buttons to the right of the maze to change the current symbol (where
the cursor is). You can also use keyboard shortcuts, on the numeric keyboard. The layout
is the same as with the buttons:

    / -> ═      * -> (space)     * -> ║
    7 -> ╔      8 -> ╦           9 -> ╗
    4 -> ╠      5 -> ╬           6 -> ╣
    1 -> ╚      2 -> ╩           3 -> ╝

Or the following keys on the main part of the keyboard:

    1 -> ═      2 -> (space)     3 -> ║
    q -> ╔      w -> ╦           e -> ╗
    a -> ╠      s -> ╬           d -> ╣
    z -> ╚      x -> ╩           c -> ╝

You can also toggle individual directions ob/off, with the following keyboard shortcuts:

          u -> North 
    h -> West   j -> East
          n -> South

Toggle mode can be disabled with [-t|--toggle-off]. In that case the keys mentioned
above will _set_ the direction, and they can be _removed_ with the uppercase versions
of the same keys.

If you edit an existing maze file, it will write it back when you save the maze. If not,
you will get a random filename with the size (rows and columns) included. Further savings
will use the same filename, and overwrite an earlier version.

The three New-buttons and the Path-button are disabled in edit mode.

=head1 SEE ALSO

This program is part of the Raku module «Game::Amazing».

=head1 AUTHOR

Arne Sommer <arne@perl6.eu>

=head1 COPYRIGHT AND LICENSE

Copyright 2020-2021 Arne Sommer

This program is free software; you can redistribute it and/or modify it under the Artistic License 2.0.

=end pod
