#!/usr/bin/perl -w
use strict;
use locale;
use utf8;
use Fcntl;

use Time::HiRes;

use POE;
use Curses;
use Curses::UI::POE;
use File::HomeDir;

use Config::General;

# use Unicode::String qw/latin1 utf8/;
# use Text::Sentence qw( split_sentences );
use POE qw ( Wheel::Curses );

# use Tie::Persistent;

package WordNoFilter;
sub filter { $_[0] }

package WordReverseFilter;
sub filter { reverse $_[0] }

package WordFirstLastFilter;

sub filter {
    $_ = shift;
    return join "-", map { filter($_) } split /-/ if (/[-]/);
    return join "_", map { filter($_) } split /_/ if (/[_]/);
    @_ = split //;
    my @a = scalar(@_) ? (shift) : ();
    push @a, shift while ( $a[$#a] =~ /[(\["'`_\-?!,;.]/ and scalar(@_) );
    my @l = scalar(@_) ? (pop) : ();
    if ( scalar(@_) and $_[$#_] eq "'" and $l[0] eq "s" ) {

        # genitive ending "gutenberg's" --> take last 3 chars
        unshift @l, pop;
        unshift @l, pop;
    }
    unshift @l, pop while ( $l[0] =~ /[(\["'`_\-?!,;.]/ and scalar(@_) );
    push( @a, splice( @_, rand @_, 1 ) ) while (@_);
    join "", @a, @l;
}

package WordNoVowelFilter;
my $prob = .8;

sub filter {
    join "",
      map { s/[AaIiOoUuEeÄäÖöÜü]/./ if ( rand(1) > $prob ); $_ } split //,
      shift;
}

package WordChunkFilter;
our $ivP         = 0;
our $ivW         = 0;
our $inc         = 5;
our $incLA       = 6;
our $wantNextPar = 0;
our $substChar   = ".";

sub default {
    my $p = shift;
    $p->{paragraph}    = 0;
    $p->{word}         = 0;
    $p->{inc}          = 1;
    $p->{incLookAhead} = 6;
    $p->{wantNextPar}  = 10000;
    $p->{substchar}    = "'";
}

sub save {
    my $p = shift;
    $p->{paragraph}    = $ivP;
    $p->{word}         = $ivW;
    $p->{inc}          = $inc;
    $p->{incLookAhead} = $incLA;
    $p->{wantNextPar}  = $wantNextPar;
    $p->{substchar}    = $substChar;
}

sub load {
    my $p = shift;
    $ivP         = $p->{paragraph};
    $ivW         = $p->{word};
    $inc         = $p->{inc};
    $incLA       = $p->{incLookAhead};
    $wantNextPar = $p->{wantNextPar};
    $substChar   = $p->{substchar};
}

sub wantPar {
    my $p = shift;
    return $p == $ivP;
}

sub filter {
    my ( $w, $p, $c ) = @_;
    return $substChar x length($w) if ( $p != $ivP );
    return $w if ( $c >= $ivW and $c < $ivW + $incLA );
    return $substChar x length($w);
}

sub atBeginPar {
	return $ivW <=0;
}

sub nextPar {
    $ivP++ if ( $wantNextPar > 0 );
    $wantNextPar--;
    $ivW = -$inc;
}

package main;

my %filters = (
    WordNoFilter        => \&WordNoFilter::filter,
    WordReverseFilter   => \&WordReverseFilter::filter,
    WordFirstLastFilter => \&WordFirstLastFilter::filter,
    WordNoVowelFilter   => \&WordNoVowelFilter::filter,
    WordChunkFilter     => \&WordChunkFilter::filter,
);

my %defaultcfg = (
    speed  => .1,
    file   => "$ARGV[0]",
    filter => "WordNoFilter",
    ypos   => 0,
);

WordChunkFilter::default( \%defaultcfg );

my $rcfile = File::HomeDir->my_home() . "/.speedreadrc";
if ( !-e $rcfile ) {
    sysopen( FH, $rcfile, O_WRONLY | O_EXCL | O_CREAT, 0644 )
      or die "can't open $rcfile: $!";
}
my $conf = new Config::General(
    -DefaultConfig         => \%defaultcfg,
    -ConfigFile            => $rcfile,
    -AllowMultiOptions     => "no",
    -MergeDuplicateBlocks  => "yes",
    -MergeDuplicateOptions => "yes",
);

my %cfg = $conf->getall();

if ( $ARGV[0] ) {
    if ( $ARGV[0] ne $cfg{file} ) {
        $cfg{file}      = $ARGV[0];
        $cfg{paragraph} = 0;
        $cfg{word}      = 0;
    }
}

$conf->save_file( $rcfile, \%cfg );

my @lines;
my $lines  = "";
my $filter = $filters{WordNoFilter};

my @parcache = ();

sub applyWordFilter {
    my ( $f, $l ) = @_;
    my @pars = split /\n\n/, $l;
    my $res = "";
    foreach my $p ( 0 .. $#pars ) {
        my $par = "";
        if ( $filter eq $filters{WordChunkFilter} ) {
            next
              if ( ( $WordChunkFilter::ivP + 1 < $p )
                or $p < $WordChunkFilter::ivP );
            if ( $parcache[$p] && !WordChunkFilter::wantPar($p) ) {
                $res .= $parcache[$p] . "\n\n";
                next;
            }
        }
        my @words = split /[\s]+/, $pars[$p];
        if ( $cfg{filter} eq "WordChunkFilter" ) {
            if ( $WordChunkFilter::ivP == $p ) {
                if (
                    $WordChunkFilter::ivW >= $#words + $WordChunkFilter::incLA )
                {
                    WordChunkFilter::nextPar();

					# $poe_kernel->delay("nt",4*$cfg{speed});
                }
            }
        }
        foreach my $w ( 0 .. $#words ) {
            my $t = $words[$w] =~ /([.;,!?-]+)$/ ? $1 : "";
            $words[$w] =~ s/[\.;,!\?-]+$//g;
            $par .= &$f( $words[$w], $p, $w ) . $t;
            $par .= " ";
        }
        $parcache[$p] = $par;
        $res .= "$par\n\n";
    }
    return $res;
}

my $cui = new Curses::UI::POE
  -color_support => 1,
  inline_states  => {

    _start => sub {
        my ( $c, $k, $s ) = @_[ HEAP, KERNEL, SESSION ];
        my $split = int( $Curses::LINES - 8 );
        $c->{win} = $c->add(
            'win1', 'Window',
            -border => 1,
            -y      => 1,
            -height => $split -bfg => 'red'
        );
        $c->{stat} = $c->add(
            'stat', 'Window',
            -border   => 1,
            -y        => $split + 1 -bfg => 'red'
              -height => 5,
            -title => "Status"
        );
        my @menu = (
            {
                -label   => 'File',
                -submenu => [
                    {
                        -label => 'Open       ^O',
                        -value => sub { load_file() }
                    },
                    {
                        -label => 'Exit       ^Q',
                        -value => \&exit_dialog
                    }
                ]
            },
            {
                -label   => 'Exercise',
                -submenu => [
                    {
                        -label => 'Normal Read      F2',
                        -value => sub { selectFilter("WordNoFilter"); }
                    },
                    {
                        -label => 'Reversed Words   F3',
                        -value => sub { selectFilter("WordReverseFilter"); }
                    },
                    {
                        -label => 'Jumbled Words    F4',
                        -value => sub { selectFilter("WordFirstLastFilter"); }
                    },
                    {
                        -label => 'Missing Vowels   F5',
                        -value => sub { selectFilter("WordNoVowelFilter"); }
                    },
                    {
                        -label => 'Moving Chunks    F7',
                        -value => sub { selectFilter( "WordChunkFilter", 1 ); }
                    },
                    {
                        -label => 'Jumping Chunks   F8',
                        -value => sub {
                            selectFilter( "WordChunkFilter",
                                $WordChunkFilter::incLA );
                          }
                    },
                ]
            },
            {
                -label   => 'Control',
                -submenu => [
                    {
                        -label => 'Restart         C-R',
                        -value => sub {
                            $cfg{paragraph} = 0;
                            $cfg{word}      = 0;
                            readFromCfg();
                          }
                    },
                    {
                        -label => 'Stop            C-C',
                        -value => sub { $k->delay( "nt", 1000000 ); }
                    },
                    {
                        -label => 'Start           C-S',
                        -value => sub { $k->delay( "nt", 1000000 ); }
                    },
                ]
            },
            {
                -label   => 'Help',
                -submenu => [
                    {
                        -label => "About",
                        -value => sub {
                            $c->dialog(
							        "Speedread V0.5\nProgrammed by Hannes Schulz <mail at hannes-schulz dot de>\n\n"
                                  . "Speedread is a collection of exercises to help you improve your reading speed. You should use a >=40 line virtual terminal.\n"
                                  . "Adjust font and width to your taste using your terminal's options."
                            );
                          }
                    }
                ]
            }
        );

		
        $c->{menu} = $c->add(
            'menu', 'Menubar',
            -menu => \@menu,
            -fg   => "blue",
            -bg   => "white"
        );
        $c->{tv} = $c->{win}->add(
            'mytextviewer', 'TextViewer',
            -text       => applyWordFilter( $filter, $lines, "" ),
            -wrapping   => 1,
            -vscrollbar => 1,
        );
        $c->{stv} = $c->{stat}->add(
            'stattxt', 'TextViewer',
            -text     => applyWordFilter( $filter, $lines, "" ),
            -wrapping => 1,
        );
        $c->{curses} = POE::Wheel::Curses->new( InputEvent => 'got_input' );
    },

    nostatus => sub { my $c = $_[HEAP]; $c->nostatus },

    got_input => sub {
        my ( $c, $keystroke ) = $_[ HEAP, ARG0 ];
        $keystroke = uc( keyname($keystroke) ) if $keystroke =~ /^\d{2,}$/;
        if ( $keystroke eq "\cL" ) {
            $c->focus( undef, 1 );
            $c->draw();
            return;
        }
        if ( $keystroke eq "\c+" ) {
            $cfg{speed} *= 1.1;
            readFromCfg();
            return;
        }
        if ( $keystroke eq "\c-" ) {
            $cfg{speed} *= 0.9;
            readFromCfg();
            return;
        }
        $c->{tv}->text($keystroke);
        $c->focus( undef, 1 );
        $c->draw();
    },

    _end => sub {
        $_[HEAP]->{cui}->leave_curses();
        exit(0);
    },

    nt => sub {
        my ( $c, $k ) = @_[ HEAP, KERNEL ];
        if ( $filter == $filters{WordChunkFilter} ) {
            $WordChunkFilter::ivW += $WordChunkFilter::inc;
            $c->{tv}->text( applyWordFilter( $filter, $lines ) );
            $c->focus( undef, 1 );
            $c->draw();
			if(WordChunkFilter::atBeginPar()){
				$k->delay( "nt", 4*$cfg{speed} );
			}else{
				$k->delay( "nt", $cfg{speed} );
			}
        }
        else {
            $c->{tv}->cursor_down;
            $c->{tv}->draw;
            $c->focus( undef, 1 );
            $c->draw();
            $k->delay( "nt", 2 * $cfg{speed} );
        }
      }

  };

sub exit_dialog() {
    $cfg{ypos} = $cui->{tv}->{-ypos};
    WordChunkFilter::save( \%cfg );
    $conf->save_file( $rcfile, \%cfg );
    $cui->leave_curses();
    exit(0);
}

sub readFromCfg {
    my $init = shift;

    setOptStatTxt();
    $cui->{win}->title( $cfg{file} );

    # set word chunk filter to correct position
    WordChunkFilter::load( \%cfg );

    # first call this func
    if ($init) {

        # open file
        if ( open F, $cfg{file} ) {
            @lines = <F>;
            $lines = join "", @lines;
            close(F);
        }

        # display text
        $cui->{tv}->text( applyWordFilter( $filter, $lines ) );

        # go to last position
        $cui->{tv}->{-ypos} = $cfg{ypos};
    }
    else {

        # choose filter
        my $filter_old = $filter;
        $filter = $filters{ $cfg{filter} };
        if ( $filter_old != $filter ) {
            @parcache = ();
            $cui->{tv}->text( applyWordFilter( $filter, $lines ) );
        }
    }

    # update display
    $cui->focus( undef, 1 );
    $cui->draw();
}

sub selectFilter {
    $cfg{filter} = shift;
    shift;
    if ( $_ && $cfg{filter} eq "WordChunkFilter" ) {
        $WordChunkFilter::inc = $_;
    }
    readFromCfg();
}

sub setStdStatTxt {
    $cui->{stv}->text(
            "F2  -- Normal Text     F3  -- Jumbled      F4 -- Reversed Words\n"
          . "F5  -- Missing Vowels  F7  -- Movin Chunks F8 -- Jumping Chunks\n\n"
          . "+/- -- Adjust speed    C-S -- Start       C-O -- Open file\n"
          . "C-N -- Next paragraph  C-R -- Restart     C-C -- Stop" );

    $cui->focus( undef, 1 );
    $cui->draw();
}

sub setOptStatTxt {
    $cui->{stv}->text( "F1 to see key bindings\n" . "Speed="
          . $cfg{speed}
          . "\nFilter="
          . $cfg{filter}
          . "\nFile="
          . $cfg{file}
          . "\tJumping="
          . $cfg{inc}
          . "\t#Words="
          . $cfg{incLookAhead} );

    $cui->focus( undef, 1 );
    $cui->draw();
}

sub load_file {
    $_ = $cui->loadfilebrowser();
    if ($_) {
        $cfg{file} = $_;
        readFromCfg(1);
    }
}

setStdStatTxt();

$cui->set_binding( sub { $cui->{menu}->focus() }, "\cX" );
$cui->set_binding(    # restart from beginning
    sub {
        $cfg{paragraph} = 0;
        $cfg{word}      = 0;
        readFromCfg();
    },
    "\cR"
);

$cui->set_binding(
    sub {
        $cfg{speed} *= 0.9;
        WordChunkFilter::save( \%cfg );
        readFromCfg();
    },
    "+"
);
$cui->set_binding(
    sub {
        $cfg{speed} *= 1.1;
        WordChunkFilter::save( \%cfg );
        readFromCfg();
    },
    "-"
);

$cui->set_binding( sub { $WordChunkFilter::wantNextPar++ }, "\cN" );
$cui->set_binding(
    sub { $cui->focus( undef, 1 ); $cui->layout(); $cui->draw(); }, "\cL" );
$cui->set_binding( sub { $poe_kernel->delay( "nt", 1 ); },       "\cS" );
$cui->set_binding( sub { $poe_kernel->delay( "nt", 1000000 ); }, "\cC" );
$cui->set_binding( \&restartPar,    "\cP" );
$cui->set_binding( \&exit_dialog,   "\cQ" );
$cui->set_binding( \&setStdStatTxt, KEY_F(1) );
$cui->set_binding( sub { selectFilter("WordNoFilter") },        KEY_F(2) );
$cui->set_binding( sub { selectFilter("WordFirstLastFilter") }, KEY_F(3) );
$cui->set_binding( sub { selectFilter("WordReverseFilter") },   KEY_F(4) );
$cui->set_binding( sub { selectFilter("WordNoVowelFilter") },   KEY_F(5) );
$cui->set_binding( sub { selectFilter( "WordChunkFilter", 1 ); }, KEY_F(7) );
$cui->set_binding(
    sub { selectFilter( "WordChunkFilter", $WordChunkFilter::incLA ); },
    KEY_F(8) );
$cui->set_binding( sub { load_file() }, "\cO" );

readFromCfg(1);

$cui->focus( undef, 1 );
$cui->draw();
$poe_kernel->run;
