package RapidApp::BgTaskInterface;
use Moose;
extends 'RapidApp::AppBase';

use RapidApp::BgTask::TaskPool;
use HTML::Entities;

use RapidApp::Include 'sugar', 'perlutil';

=head1 NAME

RapidApp::BgTaskInterface

=head1 SYNOPSIS

  package MyModule;
  use Moose;
  extends 'RapidApp::BgTaskInterface';
  sub getTask {
    my $task= ... # load a RapidApp::BgTask::Task object, somehow.  See notes on 'getTask'
	return $task;
  }
  1;

=head1 DESCRIPTION

This module provides the interfaces needed for the Ext.ux.RapidApp.BgTaskRenderPanel.
In the future, it will also have the methods needed for the planned javascript interface
that provides a line of stdin, and tabs for stdout/stderr, and a metadata inspector.

=head1 ATTRIBUTES

=head2 create_active

Whether or not the refresh polling should be initiated during the constructor of the
javascript panel.  This only affects the javascript generated by $self->renderPanel_js
(and $self->content)

=head2 callback_params

Parameters which should be sent to the client during creation and returned to the
controller on each request.  Same as Ext's "base_params", but with a better name.
This only takes effect when $self->renderPanel_js is called.

=head2 read_enabled

Whether javascript read requests are permitted to read stream data from the job.
You will likely want to override this attribute with a custom function.

=head2 write_enabled

Whether javascript write requests are permitted to write to streams of the job.
You will likely want to override this attribute with a custom function.

=cut

has create_active   => ( is => 'rw', isa => 'Bool', default => 1 );
has callback_params => ( is => 'rw', isa => 'HashRef' );
has read_enabled    => ( is => 'rw', isa => 'Bool', default => 1 );
has write_enabled   => ( is => 'rw', isa => 'Bool', default => 0 );

override_defaults(
	auto_viewport => 1
);

sub BUILD {
	my $self= shift;
	
	$self->apply_actions(map { $_ => $_ }
		qw( writeInput readOutput )
	);
}

=head2 $self->getTask() [abstract]

Returns a RapidApp::BgTask::TaskClient object.

This method is abstract, and needs to be implemented in the derived class.
Simple (but inefficient) implementation:

   # open connection to task supervisor on each request
   my $taskPool= RapidApp::BgTask::TaskPool->new(path => '/tmp/foo');
   my ($t)= $taskPool->search({ meta => { name => 'My Task' }});
   die "Can't find task" unless $t;

Better implementation by caching the taskpool and pid:

   # keep taskPool allocated somewhere else (in a ::Model perhaps, or in the main application)
   package MyApp;
   has taskPool => ( is => 'ro', default => sub { RapidApp::BgTask::TaskPool->new(path => '/tmp/foo', mkdirIfMissing => [ 0755, undef, undef ] ) } );
   
   package MyModule;
   if (my $pid= $self->c->session->{'my_module_task_name_pid'}) {
     my $task= $self->c->taskPool->taskByPid($pid);
     return $task if $task && $task->connect(my $err);
   }
   my ($task)= $self->c->taskPool->search({ meta => { name => 'My Task' }});
   die "Can't find task" unless $task;
   return $task;

An even better implementation would be to hold open the connection to the task supervisor,
but then you need a way to close the connection if it goes unused for a while, to conserve
resources.

=cut
sub getTask {
	die "getTask needs to be implemented in the derived class.  See BgTaskInterface docs for suggestions.";
}

sub content {
	goto &renderPanel_js;
}

=head2 renderPanel_js

Get the javascript to create a BgTaskRenderPanel fromo this module's configuration.

=cut
sub renderPanel_js {
	my $self= shift;
	
	return mixedjs
		'(new Ext.ux.RapidApp.BgTaskRenderPanel(',
		  { controllerUrl => $self->module_path,
		    callbackParams => $self->callback_params,
			initStart => $self->create_active, },
		'))';
}

=head2 writeInput

Write data to STDIN of the job.

=cut
sub writeInput {
	my $self= shift;
	die usererr "Permission denied" unless $self->write_enabled;
	
	my $data= $self->c->request->params->{data};
	defined $data or die "Missing required parameter 'data'";
	
	my $task= $self->getTask;
	my $ret= $task->stream(0)->write($data);
	return {
		success => 1,
		streamInfo => $ret->{info},
		exitStatus => $ret->{info}{err}? $task->info->{exitStatus} : {},
	};
}

=head2 readOutput

Read from STDOUT of the job.  No support yet for STDERR, so you should redirect
STDERR into STDOUT within the job.  (This is the only way to see them in proper
sequence anyway.  The Supervisor has no record of which lines of data came first
between two streams.  Adding such timestamps might not be a bad idea, but would
only tell which stream the Supervisor read first, not which had data on it first.)

=cut
sub readOutput {
	my $self= shift;
	die usererr "Permission denied" unless $self->read_enabled;
	
	my $lastLineOfs= $self->c->request->params->{lastLineOfs};
	$lastLineOfs ||= 0;
	
	my $task= $self->getTask;
	my $dataOfs= $lastLineOfs > 0? $lastLineOfs-1 : 0;
	my $ret;
	my $reset= 0;
	$ret= $task->stream(1)->peek($dataOfs);
	if ($ret->{ofs} < $dataOfs) {
		$reset= 1;
		$ret= $task->stream(1)->peek(0);
	}
	$dataOfs= $ret->{ofs};
	
	my @lines= split(/\n/, $ret->{data});
	
	# calculate line offsets
	my @lineOffsets;
	for (@lines) {
		push @lineOffsets, $dataOfs;
		$dataOfs+= length($_)+1;
	}
	
	# discard first partial line, if we aren't at beginning of stream
	if (@lineOffsets && $lineOffsets[0] > 0) {
		shift @lines;
		shift @lineOffsets;
	}
	
	return {
		success => 1,
		reset => $reset,
		lines => [ map { _tty2html($_) } @lines ], # htmlize the output
		lineOffsets => \@lineOffsets,
		streamInfo => $ret->{info},
		exitStatus => $ret->{info}{eof}? $task->info->{exitStatus} : {},
	};
}

=head2 _tty2html

Convert terminalisms into HTML.

At the moment, this just handles "\r" processing.  In the future it will
also handle colors.  No support is planned for cursor-moving features that
cross line boundaries.

=cut
sub _tty2html {
	my $line= shift;
	my @segments= split /\r/, $line;
	
	# Last segment overwrites previous segments.
	$line= pop @segments;
	# Now, to get a "console overwrite" effect, we apply the tail ends of previous segments.
	for (reverse @segments) {
		$line .= substr( $_, length($line) ) if length($_) > length($line);
	}
	return encode_entities($line);
}

1;
