The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#  You may distribute under the terms of the GNU General Public License
#
#  (C) Paul Evans, 2008-2010 -- leonerd@leonerd.org.uk

package Circle::FE::Gtk::Widget::Scroller;

use strict;
use warnings;

use constant type => "Scroller";

use Glib qw( TRUE FALSE );

use POSIX qw( strftime );
use Text::Balanced qw( extract_bracketed );

# The perl bindings don't make this very easy
use constant PANGO_WEIGHT_BOLD => 700;

sub build
{
   my $class = shift;
   my ( $obj, $tab ) = @_;

   my $widget = Gtk2::ScrolledWindow->new();
   $widget->set_policy( 'never', 'always' );

   my $textview = Gtk2::TextView->new();
   $widget->add( $textview );

   $textview->set_editable( FALSE );
   $textview->set_cursor_visible( FALSE );
   $textview->set_wrap_mode( 'word-char' );

   $textview->set_indent( -50 );

   $textview->modify_font( $tab->get_font );
   $textview->modify_base( normal => $tab->get_theme_colour( "bgcolor" ) );
   $textview->modify_text( normal => $tab->get_theme_colour( "textcolor" ) );

   my $buffer = $textview->get_buffer;

   my $self = bless {
      tab => $tab,
      textview => $textview,
      textbuffer => $buffer,
      scrollwindow => $widget,
      last_datestamp => "",
      linestarts => [],
   };

   $tab->{scroller} = $self;

   $self->{start_para_mark} = $buffer->create_mark( "start_para", $buffer->get_end_iter, TRUE );
   my $endmark = $buffer->create_mark( "end", $buffer->get_end_iter, FALSE );

   $obj->watch_property(
      property => "displayevents",
      on_set => sub {
         $textview->set_buffer( Gtk2::TextBuffer->new( $buffer->get_tag_table ) );
         $self->append_event( $_ ) for @{ $_[0] };
         $textview->set_buffer( $buffer );
         $textview->scroll_mark_onscreen( $endmark );
      },
      on_push => sub {
         my $at_end = $self->at_end;
         $self->append_event( $_ ) for @_;
         $textview->scroll_mark_onscreen( $endmark ) if $at_end;
      },
      on_shift => sub {
         $self->shift_events( $_[0] );
      },
      on_splice => sub { 'TODO' },
      want_initial => 1,
   );

   return $widget;
}

# Helper "methods"

sub apply_format_colour
{
   my $self = shift;
   my ( $tags, $attr, $colour ) = @_;

   @$tags = grep { !$_->get( "$attr-set" ) } @$tags;

   push @$tags, $self->{$attr . "tags"}->{$colour} ||= $self->{textbuffer}->create_tag( undef,
      $attr => $colour,
   );
}

my %format_attrs = (
   bold      => [ weight => PANGO_WEIGHT_BOLD ],
   italic    => [ style => 'italic' ],
   underline => [ underline => 'single' ],
);

sub apply_format_simple
{
   my $self = shift;
   my ( $tags, $name ) = @_;

   my ( $attr, $value ) = @{ $format_attrs{$name} };

   @$tags = grep { !$_->get( "$attr-set" ) } @$tags;

   push @$tags, $self->{simpletags}->{$name} ||= $self->{textbuffer}->create_tag( undef,
      $attr => $value,
   );
}

sub apply_indent
{
   my $self = shift;

   my $buffer = $self->{textbuffer};
   
   my $end = $buffer->get_end_iter;
   my $start_para = $buffer->get_iter_at_mark( $self->{start_para_mark} );

   my $text = $buffer->get_text( $start_para, $end, FALSE );

   my $layout = Gtk2::Pango::Layout->new( $self->{textview}->get_pango_context );
   $layout->set_text( $text );
   my ( $ink_rect, $logical_rect ) = $layout->get_pixel_extents;

   my $width = $logical_rect->{width};

   my $tag = $buffer->create_tag( undef, indent => -$width );
   $buffer->apply_tag( $tag, $start_para, $end );
}

sub _append_formatted_inner
{
   my $self = shift;
   my ( $format, $args, $enditer, $buffer, $tags ) = @_;

   my $tab = $self->{tab};

   while( length $format ) {
      if( $format =~ s/^\$(\w+)// ) {
         my $val = exists $args->{$1} ? $args->{$1} : "<No such variable $1>";
         defined $val or $val = "<Variable $1 is not defined>";

         my @parts = ref $val eq "ARRAY" ? @$val : ( $val );

         foreach my $part ( @parts ) {
            my ( $text, %format ) = ref $part eq "ARRAY" ? @$part : ( $part );

            my @parttags = @$tags;

            foreach my $a ( keys %format ) {
               $self->apply_format_colour( \@parttags, foreground => $tab->translate_theme_colour( $format{fg} ) ), next if $a eq "fg";
               $self->apply_format_colour( \@parttags, background => $tab->translate_theme_colour( $format{bg} ) ), next if $a eq "bg";

               $self->apply_format_simple( \@parttags, 'bold'      ), next if $a eq "b"; 
               $self->apply_format_simple( \@parttags, 'italic'    ), next if $a eq "i";
               $self->apply_format_simple( \@parttags, 'underline' ), next if $a eq "u";

               $self->apply_indent, next if $a eq "indent";

               print STDERR "Unhandled BE attr $a\n";
            }

            $buffer->insert_with_tags( $enditer, $text, @parttags );
         }
      }
      elsif( $format =~ m/^\{/ ) {
         my $piece = extract_bracketed( $format, "{}" );
         s/^{//, s/}$// for $piece;

         if( $piece =~ m/ / ) {
            my ( $code, $content ) = split( m/ /, $piece, 2 );

            my @newtags = @$tags;
            my ( $type, $arg ) = split( m/:/, $code, 2 );

            # Fake switch-alike
            for my $a ( $type ) {
               $self->apply_format_colour( \@newtags, foreground => $arg ), next if $a eq "fg";
               $self->apply_format_colour( \@newtags, background => $arg ), next if $a eq "bg";

               $self->apply_format_simple( \@newtags, 'bold'      ), next if $a eq "b";
               $self->apply_format_simple( \@newtags, 'italic'    ), next if $a eq "i";
               $self->apply_format_simple( \@newtags, 'underline' ), next if $a eq "u";

               $self->apply_indent, next if $a eq "indent";

               print STDERR "Unrecognised format code $a\n";
            }

            $self->_append_formatted_inner( $content, $args, $enditer, $buffer, \@newtags );
         }
         else {
            $self->_append_formatted_inner( $piece, $args, $enditer, $buffer, $tags );
         }
      }
      else {
         $format =~ s/^([^\$\{]+)//;
         my $val = $1;
         $buffer->insert_with_tags( $enditer, $val, @$tags );
      }
   }
}

sub append_formatted
{
   my $self = shift;
   my ( $format, $args ) = @_;

   my $buffer = $self->{textbuffer};
   my $enditer = $buffer->get_end_iter;

   $buffer->move_mark( $self->{start_para_mark}, $buffer->get_end_iter );

   $self->_append_formatted_inner( $format, $args, $enditer, $buffer, [] );

}

sub append_event
{
   my $self = shift;
   my ( $ev ) = @_;

   my ( $event, $time, $args ) = @$ev;

   my $tab = $self->{tab};

   my @time = localtime( $time );

   my $datestamp = strftime( $tab->get_theme_var( "datestamp" ), @time );
   my $timestamp = strftime( $tab->get_theme_var( "timestamp" ), @time );

   my $buffer = $self->{textbuffer};

   if( $buffer->get_line_count > 1 or $buffer->get_text( $buffer->get_start_iter, $buffer->get_end_iter, TRUE ) ne "" ) {
      $buffer->insert( $buffer->get_end_iter, "\n" );
   }

   push @{ $self->{linestarts} }, $buffer->create_mark( undef, $buffer->get_end_iter, TRUE );

   if( $datestamp ne $self->{last_datestamp} ) {
      $self->append_formatted( $tab->get_theme_var( "datemessage" ) . "\n", { datestamp => $datestamp } );
      $self->{last_datestamp} = $datestamp;
   }

   my $format = $tab->get_theme_var( $event );
   defined $format or $format = "No format defined for event $event";

   $self->append_formatted( $timestamp . $format, $args );
}

sub shift_events
{
   my $self = shift;
   my ( $howmany ) = @_;

   defined $howmany or $howmany = 1;

   my $buffer = $self->{textbuffer};

   my $start = $buffer->get_start_iter;
   my $end   = $howmany < @{ $self->{linestarts} } ? $buffer->get_iter_at_mark( $self->{linestarts}->[$howmany] )
                                                   : $buffer->get_end_iter;

   $buffer->delete( $start, $end );
   my @deleted_marks = splice @{ $self->{linestarts} }, 0, $howmany, ();
   $buffer->delete_mark( $_ ) for @deleted_marks;
}

sub scroll
{
   my $self = shift;
   my ( $dir ) = @_;

   my $adj = $self->{scrollwindow}->get_vadjustment;

   if( $dir eq 'up' ) {
      $adj->set_value( $adj->value - $adj->page_increment );
   }
   elsif( $dir eq 'down' ) {
      my $newval = $adj->value + $adj->page_increment;
      my $max = $adj->upper - $adj->page_size;
      # Check we don't go too far
      $newval = $max if $newval > $max;
      $adj->set_value( $newval );
   }
   elsif( $dir eq 'top' ) {
      $adj->set_value( $adj->lower );
   }
   elsif( $dir eq 'bottom' ) {
      $adj->set_value( $adj->upper - $adj->page_size );
   }
}

sub at_end
{
   my $self = shift;

   my $adj = $self->{scrollwindow}->get_vadjustment;

   return $adj->get_value == $adj->upper - $adj->page_size;
}

1;