#!/usr/bin/perl

use strict;
use warnings;
use feature qw( say switch );
no if $] >= 5.017011, warnings => 'experimental::smartmatch';

use Socket qw( AF_INET unpack_sockaddr_in inet_ntoa getaddrinfo );

use Future;

use Getopt::Long;

sub make_readable_handle
{
   pipe( my $reader, my $writer ) or die "Cannot pipe() - $!";

   syswrite $writer, "Hello world!\n" or die "Cannot syswrite - $!";

   return $reader;
}

sub make_child
{
   defined( my $kid = fork ) or die "Cannot fork - $!";

   $kid or exec( "sleep", "1" ) or die "Cannot exec - $!";

   return $kid;
}

sub format_addr
{
   my ( $port, $inaddr ) = unpack_sockaddr_in $_[0];
   sprintf "%s:%d", inet_ntoa( $inaddr ), $port;
}

my %systems;

GetOptions(
   'systems=s' => sub { $systems{$_}++ for split m/,/, $_[1] },
) or exit 1;

unless( keys %systems ) {
   $systems{$_}++ for qw( Glib POE IO::Async AnyEvent );
}

my @jenga;

my @running;

### Glib
if( $systems{Glib} ) {
   require Glib;

   push @running, my $f = Future->new;

   my $handle = make_readable_handle();
   Glib::IO->add_watch( fileno $handle,
      ['in', 'hup', 'err'],
      sub {
         sysread $handle, my $buffer, 8192;
         print "Glib reads $buffer";
         return 0;
      },
   );

   Glib::Timeout->add( 3 * 1000,
      sub {
         say "Glib timer";
         $f->done;
         return 0
      },
   );

   my $kid = make_child();
   Glib::Child->watch_add( $kid,
      sub {
         say "Glib child exited $_[1]";
         return 0;
      },
   );

   push @jenga, "Glib";
}

### POE
if( $systems{POE} ) {
   require POE;
   POE->import(qw(
      Session Kernel
      Wheel::ReadWrite Wheel::Run Filter::Reference
      Component::Server::TCP Component::Client::TCP
   ));

   require IO::Async::Loop::POE if $systems{"IO::Async"}; # because it's going to call POE::Kernel->run;
   POE::Kernel->run;

   given( $jenga[-1] ) {
      when( undef  ) { }
      when( "Glib" ) { require POE::Loop::Glib; }
      default { die "Not sure how to run POE on top of $jenga[-1]\n" }
   }

   push @running, my $f = Future->new;

   my $wheel_readwrite;
   my $wheel_resolver;

   POE::Session->create(
      inline_states => {
         _start => sub {
            # Install a longrunning timer that never actually fires, to keep the kernel alive too
            $_[KERNEL()]->delay( keepalive => 1E9 );

            $wheel_readwrite = POE::Wheel::ReadWrite->new(
               Handle => make_readable_handle,
               InputEvent => 'handle_read',
            );

            $_[KERNEL()]->delay( timer_expire => 3 );

            $_[KERNEL()]->sig( INT => 'sigint' );

            $_[KERNEL()]->sig_child( make_child, 'child_exit' );

            $wheel_resolver = POE::Wheel::Run->new(
               Program => sub {
                  my ( $err, @addrs ) = getaddrinfo( "localhost", "www", { family => AF_INET } );
                  die "$err" if $err;
                  print @{ POE::Filter::Reference->new->put( [ $addrs[0] ] ) };
                  exit 0;
               },
               StdoutFilter => POE::Filter::Reference->new,
               StdoutEvent  => 'resolver_input',
            );

            POE::Component::Server::TCP->new(
               Port => 22123,
               ClientConnected => sub {
                  say "POE listener accepted";
               },
               ClientInput => sub {
                  $_[HEAP()]{client}->put( $_[ARG0()] );
               }
            );

            POE::Component::Client::TCP->new(
               RemoteAddress => "localhost",
               RemotePort    => 22123,
               Connected => sub {
                  $_[HEAP()]{server}->put( "POE connected" );
               },
               ServerInput => sub {
                  say "$_[ARG0()] received";
               },
            );
         },

         handle_read => sub { say "POE reads $_[ARG0()]" },

         timer_expire => sub { say "POE timer" },

         sigint => sub { say "POE SIGINT";
                         $f->done },

         child_exit => sub { say "POE child exited $_[ARG2()]" },

         resolver_input => sub { say "POE resolved " . format_addr( $_[ARG0()]->{addr} ) },
      },
   );

   push @jenga, "POE";
}

### IO::Async
if( $systems{'IO::Async'} ) {
   my $loop ;
   given( $jenga[-1] ) {
      when( undef  ) { require IO::Async::Loop;       $loop = IO::Async::Loop->new;       }
      when( "Glib" ) { require IO::Async::Loop::Glib; $loop = IO::Async::Loop::Glib->new; }
      when( "POE"  ) { require IO::Async::Loop::POE;  $loop = IO::Async::Loop::POE->new;  }
      default { die "Not sure how to run IO::Async on top of $jenga[-1]\n" }
   }

   push @running, my $f = Future->new;

   require IO::Async::Stream;
   require IO::Async::Timer::Countdown;
   require IO::Async::Signal;
   require IO::Async::PID;
   require IO::Async::Listener;

   $loop->add(
      IO::Async::Stream->new(
         read_handle => make_readable_handle,
         on_read => sub {
            my ( $self, $buffref, $closed ) = @_;
            return if $closed;

            print "IO::Async reads $$buffref";
            $$buffref = "";
            return 0;
         },
      )
   );

   $loop->add(
      IO::Async::Timer::Countdown->new(
         delay => 3,
         on_expire => sub { say "IO::Async timer" },
      )->start
   );

   $loop->add(
      IO::Async::Signal->new(
         name => "INT",
         on_receipt => sub { say "IO::Async SIGINT";
                             $f->done },
      )
   );

   $loop->add(
      IO::Async::PID->new(
         pid => make_child,
         on_exit => sub { say "IO::Async child exited $_[1]" },
      )
   );

   $loop->resolver->getaddrinfo(
      host    => "localhost",
      service => "www",
      family  => "inet",
      on_resolved => sub {
         my ( $addr ) = @_;
         say "IO::Async resolved " . format_addr( $addr->{addr} );
      },
      on_error => sub { say "*** IO::Async cannot resolve - $_[-1]" },
   );

   my $listener = IO::Async::Listener->new(
      on_stream => sub {
         my ( $self, $stream ) = @_;
         say "IO::Async listener accepted";
         $stream->configure(
            on_read => sub {
               my ( $stream, $buffref ) = @_;
               $stream->write( $$buffref );
               $$buffref = "";
            },
         );
         $self->add_child( $stream );
      },
   );
   $loop->add( $listener );
   $listener->listen(
      host     => "127.0.0.1",
      service  => "22124",
      socktype => "stream",
      on_resolve_error => sub { die "IO::Async failed listen - $_[-1]" },
      on_listen_error  => sub { die "IO::Async failed listen - $_[-1]" },
   );

   $loop->connect(
      host     => "localhost",
      service  => "22124",
      socktype => "stream",
      on_stream => sub {
         my ( $stream ) = @_;
         $stream->write( "IO::Async connected\n" );
         $stream->configure(
            on_read => sub {
               my ( $stream, $buffref ) = @_;
               $$buffref =~ s/(.*)\n// or return 0;
               say "$1 received";
               return 1;
            }
         );
         $loop->add( $stream );
      },
      on_resolve_error => sub { die "IO::Async failed connect - $_[-1]" },
      on_connect_error => sub { die "IO::Async failed connect - $_[-1]" },
   );

   push @jenga, "IO::Async";
}

### AnyEvent
# AnyEvent needs to keep objects hanging around outside this scoped block
my @AE_keepscope;
if( $systems{AnyEvent} ) {
   require AnyEvent;

   given( $jenga[-1] ) {
      when( undef     ) { }
      when( "Glib"    ) { $ENV{PERL_AnyEvent_MODEL} = "Glib"; }
      when( "POE"     ) { $ENV{PERL_AnyEvent_MODEL} = "POE"; }
      when( "IO::Async" ) { $ENV{PERL_AnyEvent_MODEL} = "IO::Async"; }
      default { die "Not sure how to run POE on top of $jenga[-1]\n" }
   }

   push @running, my $f = Future->new;

   require AnyEvent::Handle;
   require AnyEvent::Socket;

   my $handle = AnyEvent::Handle->new(
      fh => make_readable_handle,
   );
   $handle->push_read( line => sub {
      say "AnyEvent reads $_[1]";
   });
   push @AE_keepscope, $handle;

   push @AE_keepscope, AnyEvent->timer(
      after => 3,
      cb => sub { say "AnyEvent timer" },
   );

   push @AE_keepscope, AnyEvent->signal(
      signal => "INT",
      cb => sub { say "AnyEvent SIGINT";
                  $f->done },
   );

   push @AE_keepscope, AnyEvent->child(
      pid => make_child,
      cb => sub { say "AnyEvent child exited $_[1]" },
   );

   AnyEvent::Socket::resolve_sockaddr(
      "localhost", "http", "tcp", 4, undef, sub {
         my ( $addr ) = @_;
         say "AnyEvent resolved " . format_addr( $addr->[3] );
      }
   );

   push @AE_keepscope, AnyEvent::Socket::tcp_server(
      "127.0.0.1", "22125", sub {
         say "AnyEvent listener accepted";
         my $handle = AnyEvent::Handle->new(
            fh => $_[0],
         );
         $handle->on_read(sub {
            $handle->push_write( $handle->{rbuf} );
            $handle->{rbuf} = "";
         });
      }
   );

   push @AE_keepscope, AnyEvent::Handle->new(
      connect => [ "localhost", "22125" ],
      on_connect => sub {
         my ( $handle ) = @_;
         $handle->push_write( "AnyEvent connected\n" );
         $handle->push_read( line => sub {
            say "$_[1] received";
         } );
      },
   );

   push @jenga, "AnyEvent";
}

###

say "Running with a jenga stack of " . join( ", ", @jenga );
my $all = Future->needs_all( @running );

given( $jenga[0] ) {
   when( "Glib" ) {
      my $gmain = Glib::MainLoop->new;
      $all->on_done( sub { say "Stopping...\n"; $gmain->quit } );
      $gmain->run;
   }
   when( "POE" ) {
      # I feel so dirty...
      no warnings 'exiting';
      $all->on_done( sub { say "Stopping...\n"; last POE_LOOP } );
      POE_LOOP: { POE::Kernel->run };
   }
   when( "IO::Async" ) {
      my $loop = IO::Async::Loop->new; # gets the same one
      $all->on_done( sub { say "Stopping...\n"; $loop->loop_stop } );
      $loop->loop_forever;
   }
   when( "AnyEvent" ) {
      my $cv = AnyEvent->condvar;
      $all->on_done( sub { say "Stopping...\n"; $cv->send } );
      $cv->recv;
   }
   default {
      die "Not sure how to run a Jenga stack with $jenga[0] on the bottom\n";
   }
}
