#!/usr/bin/perl
# qfe - a front-end for Queue
# Copyright (C) 2008 Laurent Mazet

# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2, or (at your option)
# any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.  */

# Changelog:
# - 0.9.3
#   * columns are sortable
# - 0.9.2
#   * bug fixed
#   * add refresh button
#   * add status bar message when refreshing list
# - 0.9.1
#   * process line arguments
# - 0.9.0
#   * all functions have been implented and tested
# ...
# - 0.0.1
#   * initial release

# Potential bugs:
# - none

# library
use strict;
use warnings;

use Data::Dumper;

use Glib qw(TRUE FALSE);

use Gtk2 -init;
use Gtk2::Helper;
use Gtk2::SimpleMenu;
use Gtk2::SimpleList;

use File::Basename;
use IPC::Open3;

# constants
use constant version => "0.9.3";

# gobal variables
use vars qw/$info_nbrunning $info_nbsleeping $info_nbtotal $status
            $treeview $window @selectors/;
my $queue = "./queue.sh";
my $refresh = 10*1000; # 10 seconds

# process argument
use vars qw/$arg_queue $arg_refresh/;
foreach my $arg (@ARGV) {
  # second argument
  if (defined($arg_queue) && ($arg_queue eq -1)) {
    $arg_queue = $arg;
  } elsif (defined($arg_refresh) && ($arg_refresh eq -1)) {
    $arg_refresh = $arg;
  # first argument
  } elsif ($arg =~ /^-h$/) {
    print "usage: qfe [-b /usr/bin/queue] [-r 10]\n";
    exit 0;
  } elsif ($arg =~ /^-b/) {
    ($arg_queue) = ($arg =~ /^-b=?(.*)$/);
    $arg_queue = -1 if ($arg_queue =~ /^$/);
  } elsif ($arg =~ /^-r/) {
    ($arg_refresh) = ($arg =~ /^-r=?(.*)$/);
    $arg_refresh = -1 if ($arg_refresh =~ /^$/);
  } elsif ($arg =~ /^-v$/) {
    print ("gpp version ".version."\n");
    exit 0;
  }
}
$queue = $arg_queue if (defined($arg_queue));
$refresh = $arg_refresh*1000 if (defined($arg_refresh));

$#selectors = -1;
my $treeview = Gtk2::TreeView->new ();

sub fixed_toggled {
  my ($cell, $path_str) = @_;
  my $model = $treeview->get_model;

  # set new value
  my $path = Gtk2::TreePath->new ($path_str);
  $selectors[$path_str]->{state} ^= 1;
  $model->set ($model->get_iter ($path), 0, $selectors[$path_str]->{state});
}

sub get_info {

  my $dialog = Gtk2::Dialog->new ("Cluster head information",
                                  $window,
                                  [qw/destroy-with-parent/],
                                  'gtk-apply', 'ok',
                                  'gtk-cancel', 'reject');

  # data structure
  my $data; $#{$data} = -1;

  # new entry
  foreach my $name (@_) {
    my $n = $#{$data} + 1;

    #empty data sctructure
    ${$data}[$n] = "";

    # new horizontal box
    my $hbox = Gtk2::HBox->new;

    # new label
    my $label = Gtk2::Label->new ($name.": ");
    $hbox->pack_start ($label, FALSE, FALSE, 2);

    # new entry
    my $entry = Gtk2::Entry->new_with_max_length (64);
    $entry->set_width_chars (32);
    $entry->set_text (${$data}[$n]);
    $entry->signal_connect ("focus_out_event",
      sub { my ($w, undef, $d) = @_;
            ${$d->{a}}[$d->{n}] = $w->get_text;
            return FALSE
      }, {a => $data, n => $n});
    $hbox->pack_start ($entry, TRUE, TRUE, 2);

    # second line
    $dialog->vbox->add ($hbox);

  }

  # show window
  $dialog->show_all ();

  # action
  my $ret = ($dialog->run eq 'ok');

  # close  dialog
  $dialog->destroy;

  return @{$data} if ($ret);
  return "";
}

sub exec_command {
  my ($cmd) = (@_);

  my($chld_in, $chld_out, $chld_err);
  ##my $pid = open2($chld_out, $chld_in, $cmd);
  my $pid = open3($chld_in, $chld_out, $chld_err, $cmd);
  #$chld_out = $chld_err;

  my ($list, $password, $user) = ("", "", "");
  while (!eof ($chld_out)) {

    my $char = getc($chld_out);
    $list = $list.$char;
    $list =~ s/^using ssh\.\.\.\n//g;
    $list =~ s/^JID.*\n//g;
    $list =~ s/^\*.*\n//g;

    # password
    if ($list =~ /Password:/) {
      $list =~ s/Password://;
      ($password) = get_info ("Password") if (!$password);
      if ($password) { print $chld_in "$password\n" } else { last }
      $password = "";
    }

    # user and password
    if ($list =~ /User:/) {
      $list =~ s/User://;
      ($user, $password) = get_info ("User", "Password") if (!$user);
      if ($user) { print $chld_in "$user\n" } else { last }
      $user = "";
    }
  }
  close ($chld_out);
  close ($chld_in);

  return $list;
}

# basic queue functions
sub add_jobs_to_queue {
  my ($path, $scripts) = (@_);
  my $lastdir = basename($path);
  my $pwd = dirname($path);
  my $cmd = "cd $pwd; $queue -a $lastdir $scripts; cd -";

  my $list = exec_command ($cmd);
  return FALSE if (!$list);

  return TRUE;
}

sub del_jobs_from_queue {
  my ($jids) = (@_);
  my $cmd = "$queue -e $jids";

  my $list = exec_command ($cmd);
  return FALSE if (!$list);

  return TRUE;
}

sub list_jobs_on_queue {
  my $cmd = "$queue -la";

  $::status->set_text("refreshing list...");

  my $list = exec_command ($cmd);
  return FALSE if (!$list);

  # create list store
  my $store = Gtk2::ListStore->new ('Glib::Boolean',
                                    'Glib::Uint',
                                    'Glib::String',
                                    'Glib::String',
                                    'Glib::String',
                                    'Glib::String',
                                    'Glib::String');

  # add data to the list store
  my @new_selectors; $#new_selectors = -1;
  my $n = 0;
  foreach my $job (split(/\n/, $list)) {
    my ($jid, $user, $state, $host, $date, $dir_script) = split(/\t/, $job);

    my $select = FALSE;
    foreach my $s (@selectors) { $select = $s->{state} if ($s->{jid} == $jid) }

    my $iter = $store->append;
    $store->set ($iter, 0, $select, 1, $jid,
                 2, $user, 3, $state, 4, $host, 5, $date, 6, $dir_script);

    $new_selectors[$n++] = { jid => $jid, state => $select };
  }
  $#selectors = -1; @selectors = @new_selectors;

  $treeview->set_model($store);

  $cmd = "$queue -c";
  $list = exec_command ($cmd);
  return FALSE if (!$list);

  my ($nbruns) = ($list =~ /running queues: ([0-9]+)/);
  my ($nbsleeps) = ($list =~ /sleeping queues: ([0-9]+)/);
  my ($nbactives) = ($list =~ /total active queues: ([0-9]+)/);
  $info_nbrunning->set_text ($nbruns);
  $info_nbsleeping->set_text ($nbsleeps);
  $info_nbtotal->set_text ($nbactives);

  $::status->set_text("");

  return TRUE;
}
Glib::Timeout->add($refresh, \&list_jobs_on_queue) if ($refresh > 0);

# main window
$window = Gtk2::Window->new;
$window->signal_connect ("delete_event" , sub{Gtk2->main_quit();});
$window->signal_connect ("destroy", sub{exit});
$window->set_title ("Queue Front End");
$window->set_border_width (2);
$window->set_default_size (480, 320);

# main box
my $window_box = Gtk2::VBox->new (FALSE, 0);
$window->add($window_box);

# bar menu
my $menu_tree = [
  _File  => {
    item_type  => '<Branch>',
    children => [
      '_Add job(s)' => {
        callback => \&add_jobs,
        accelerator => '<ctrl>A',
      },
      '_Cancel job(s)' => {
        callback => \&cancel_jobs,
        accelerator => '<ctrl>C',
      },
      Separator => {
        item_type => '<Separator>',
      },
      '_Refresh' => {
        callback => \&list_jobs_on_queue,
        accelerator => '<ctrl>R',
      },
      Separator => {
        item_type => '<Separator>',
      },
      _Quit => {
        callback => sub { Gtk2->main_quit; },
        accelerator => '<ctrl>Q',
      },
    ],
  },
  _Help => {
    item_type => '<LastBranch>',
    children => [
      _Introduction => {
         callback => \&display_help,
      },
      _About => {
        callback => \&display_about,
      },
    ],
  },
];
my $menu_bar = Gtk2::SimpleMenu->new(menu_tree => $menu_tree);
$window_box->pack_start ($menu_bar->{widget}, FALSE, FALSE, 0);
$window->add_accel_group($menu_bar->{accel_group});

# create tree view
$treeview->set_rules_hint (TRUE);

# column for fixed toggles
my $renderer = Gtk2::CellRendererToggle->new;
$renderer->signal_connect (toggled => \&fixed_toggled);
my $column = Gtk2::TreeViewColumn->new_with_attributes
  (" ", $renderer, 'active', 0);
$column->set_sizing ('fixed');
$column->set_fixed_width (20);
$treeview->append_column ($column);

# column for jid
$renderer = Gtk2::CellRendererText->new;
$column = Gtk2::TreeViewColumn->new_with_attributes
  ("JID  ", $renderer, 'text', 1);
$column->set_sort_column_id (1);
$treeview->append_column ($column);

# column for user
$renderer = Gtk2::CellRendererText->new;
$column = Gtk2::TreeViewColumn->new_with_attributes
  ("User", $renderer, 'text', 2);
$column->set_sort_column_id (2);
$treeview->append_column ($column);

# column for status
$renderer = Gtk2::CellRendererText->new;
$column = Gtk2::TreeViewColumn->new_with_attributes
  ("Status", $renderer, 'text', 3);
$column->set_sort_column_id (3);
$treeview->append_column ($column);

# column for node
$renderer = Gtk2::CellRendererText->new;
$column = Gtk2::TreeViewColumn->new_with_attributes
  ("Node", $renderer, 'text',4);
$column->set_sort_column_id (4);
$treeview->append_column ($column);

# column for date
$renderer = Gtk2::CellRendererText->new;
$column = Gtk2::TreeViewColumn->new_with_attributes
  ("Date                      ", $renderer, 'text', 5);
$column->set_sort_column_id (5);
$treeview->append_column ($column);

# column for directory/script
$renderer = Gtk2::CellRendererText->new;
$column = Gtk2::TreeViewColumn->new_with_attributes
  ("Directory/Script", $renderer, 'text', 6);
$column->set_sort_column_id (6);
$treeview->append_column ($column);
$treeview->set_search_column (6);

map { $_->set_resizable (TRUE) } $treeview->get_columns;

## scroll window
my $scrollwin = Gtk2::ScrolledWindow->new;
$scrollwin->set_shadow_type ('etched-in');
$scrollwin->set_policy ('automatic', 'automatic');
$scrollwin->add ($treeview);
$window_box->pack_start ($scrollwin, TRUE, TRUE, 3);

# info bar
my $info_frame = Gtk2::Frame->new ();
$info_frame->set_shadow_type ("none");
$window_box->pack_start ($info_frame, FALSE, FALSE, 0);

my $info_table =  Gtk2::Table->new (6, 1, FALSE);
$info_frame->add ($info_table);

my $info_nbrunning_label = Gtk2::Label->new ("Running queues:");
$info_nbrunning_label->set_justify ("right");
$info_table->attach_defaults ($info_nbrunning_label, 0, 1, 0, 1);
$info_nbrunning = Gtk2::Label->new ();
$info_nbrunning->set_justify ("left");
$info_nbrunning->set_width_chars (10);
$info_table->attach_defaults ($info_nbrunning, 1, 2, 0, 1);

my $info_nbsleeping_label = Gtk2::Label->new ("Sleeping queues:");
$info_nbsleeping_label->set_justify ("right");
$info_table->attach_defaults ($info_nbsleeping_label, 2, 3, 0, 1);
$info_nbsleeping = Gtk2::Label->new ();
$info_nbsleeping->set_justify ("left");
$info_nbsleeping->set_width_chars (10);
$info_table->attach_defaults ($info_nbsleeping, 3, 4, 0, 1);

my $info_nbtotal_label = Gtk2::Label->new ("Total active queues:");
$info_nbtotal_label->set_justify ("right");
$info_table->attach_defaults ($info_nbtotal_label, 4, 5, 0, 1);
$info_nbtotal = Gtk2::Label->new ();
$info_nbtotal->set_justify ("left");
$info_nbtotal->set_width_chars (10);
$info_table->attach_defaults ($info_nbtotal, 5, 6, 0, 1);

# init info bar
$info_nbrunning->set_text("2");
$info_nbsleeping->set_text("72");
$info_nbtotal->set_text("74");

# status bar
my $status_frame = Gtk2::Frame->new ();
$status_frame->set_shadow_type ("etched-out");
$window_box->pack_end ($status_frame, FALSE, FALSE, 0);
my $status_box = Gtk2::HBox->new (FALSE, 0);
$status_frame->add ($status_box);
$status = Gtk2::Label->new ();
$status->set_justify ("left");
$status_box->pack_start ($status, FALSE, FALSE, 1);

# list
list_jobs_on_queue;

# show all
$window->show_all ();
Gtk2->main;

# auxiliary functions

# show dialog/message
sub show {
  my ($title, @widgets) = (@_);

  # init dialog
  my $dialog = Gtk2::Dialog->new ($title, $window,
                                  [qw/destroy-with-parent/],
                                  'gtk-close', 'none');
  $dialog->signal_connect (response => sub { $_[0]->destroy });

  # text zone
  foreach my $widget (@widgets) {
    $dialog->vbox->pack_start ($widget, TRUE, TRUE, 2);
  }

  # show window
  $dialog->show_all ();
}

# callback functions

# add jobs
sub add_jobs {

  # init dialog
  my $dialog = Gtk2::Dialog->new ('Add job(s)', $window,
                                  [qw/destroy-with-parent/],
                                  'gtk-ok' => 'apply',
                                  'gtk-cancel' => 'reject');
  $dialog->set_default_response ('reject');

  # data structure
  my $data = { directory => "", scripts => "", dialog => undef};

  # directory entry
  my $hbox = Gtk2::HBox->new;
  my $name = Gtk2::Label->new ("Directory: ");
  $hbox->pack_start ($name, FALSE, FALSE, 2);

  my $entry = $data->{directory_entry} =
    Gtk2::Entry->new_with_max_length (2048);
  $entry->set_width_chars (32);
  $entry->set_text ($data->{directory});
  $entry->signal_connect ("focus_out_event",
                          sub { my ($w, undef, $d) = @_;
                               $d->{directory} = $w->get_text;
                               return FALSE }, $data);
  $hbox->pack_start ($entry, TRUE, TRUE, 2);

  # browse
  my $browse = Gtk2::Button->new_from_stock("gtk-find");
  $browse->signal_connect ("clicked",
    sub { my (undef, $d) = @_;
          return if (defined($d->{fs}));
          $d->{fs} = Gtk2::FileSelection->new ('Save name');
          $d->{fs}->set_select_multiple (TRUE);
          if ('ok' eq $d->{fs}->run) {
            my @list = $d->{fs}->get_selections;
            my @array; $#array = -1;
            foreach my $path (@list) {
              my ($script, $directory, undef) = fileparse ($path);
              push (@array, $script);
              $d->{directory} = $directory;
            }
            $d->{scripts} = join(" ", @array);
            $d->{directory_entry}->set_text($d->{directory});
            $d->{scripts_entry}->set_text($d->{scripts});
          }
          $d->{fs}->destroy;
          undef($d->{fs}) }, $data);
  $hbox->pack_end ($browse, FALSE, TRUE, 2);

  # first line
  $dialog->vbox->pack_start ($hbox, TRUE, TRUE, 2);

  # script entry
  $hbox = Gtk2::HBox->new;
  $name = Gtk2::Label->new ("Scripts: ");
  $hbox->pack_start ($name, FALSE, FALSE, 2);

  $entry =  $data->{scripts_entry} =
    Gtk2::Entry->new_with_max_length (4096);
  $entry->set_width_chars (32);
  $entry->set_text ($data->{scripts});
  $entry->signal_connect ("focus_out_event",
                          sub { my ($w, undef, $d) = @_;
                               $d->{scripts} = $w->get_text;
                               return FALSE }, $data);
  $hbox->pack_start ($entry, TRUE, TRUE, 2);

  # second line
  $dialog->vbox->pack_start ($hbox, TRUE, TRUE, 2);

  # response function
  $dialog->signal_connect (response =>
    sub {
      my ($w, $r, $d) = @_;

      # apply action
      if ($r eq 'apply') {
        if ((!$d->{directory}) || (!$d->{scripts})) {
          $::status->set_text("directory and scripts are not set");
        } elsif (add_jobs_to_queue($d->{directory}, $d->{scripts})) {
          $status->set_text("scripts added");
        } else {
          $status->set_text("problem when adding scripts");
        }
      }

      # close file selector
      $d->{fs}->destroy if (defined($d->{fs}));
      undef($d->{fs});

      $w->destroy ();
    }, $data);

  # show dialog
  $dialog->show_all ();
}

# cancel jobs
sub cancel_jobs {

  # init dialog
  my $dialog = Gtk2::Dialog->new ('Cancel job(s)', $window,
                                  [qw/destroy-with-parent/],
                                  'gtk-ok' => 'apply',
                                  'gtk-cancel' => 'reject');
  $dialog->set_default_response ('reject');

  # message entry
  my $hbox = Gtk2::HBox->new;
  my $label = Gtk2::Label->new ('Do you really want do cancel jobs?');
  $hbox->pack_start ($label, FALSE, FALSE, 2);

  # first line
  $dialog->vbox->pack_start ($hbox, TRUE, TRUE, 2);

  # init buffer
  my $textbuffer = Gtk2::TextBuffer->new();
  foreach my $s (@selectors)
    { $textbuffer->insert_at_cursor($s->{jid}." ") if ($s->{state}) }

  # job id list
  my $frame = Gtk2::Frame->new ();
  $frame->set_shadow_type ("etched-in");
  my $hpaned = Gtk2::HPaned->new;
  $hpaned->set_border_width (5);
  $frame->add ($hpaned);
  my $textview = Gtk2::TextView->new_with_buffer ($textbuffer);
  $textview->set_wrap_mode('word');
  my $sw = Gtk2::ScrolledWindow->new;
  $sw->set_policy ('automatic', 'automatic');
  $hpaned->add1 ($sw);
  $sw->add ($textview);

  # second line
  $dialog->vbox->pack_start ($frame, TRUE, TRUE, 2);

  # response function
  $dialog->signal_connect (response =>
    sub {
      my ($w, $r, $tb) = @_;

      # apply action
      if ($r eq 'apply') {
        my $text = $tb->get_text($tb->get_bounds(), FALSE);
        $text =~ s/\s/ /g; $text =~ s/^ +//; $text =~ s/ +$//;
        if (!$text) {
          $::status->set_text("job ids are not set");
        } elsif ($text =~ /[^ \d]/) {
          $::status->set_text("job ids are not set properly");
        } elsif (del_jobs_from_queue($text)) {
          $status->set_text("jobs cancelled");
        } else {
          $status->set_text("problem when cancelling jobs");
        }
      }

      $w->destroy ();
    }, $textbuffer);

  # show dialog
  $dialog->show_all ();
}

# display help
sub display_help {
  my @labels;
  my $k = -1;

  $labels[++$k] = Gtk2::Label->new;
  $labels[$k]->set_markup ('<b>Key bindings</b>');
  $labels[$k]->set_justify ('center');
  $labels[++$k] = Gtk2::Label->new;
  $labels[$k]->set_markup ('
Command line arguments:
 -b path for queue program [/usr/bin/queue]
 -r refresh delay [10]

Keyboard shortcuts:
* add job(s) to the queue &lt;Control-A&gt;
* cancel job(s) to the queue &lt;Control-C&gt;
* refresh queue information &lt;Control-R&gt;
* quit application &lt;Control-Q&gt;
');

  show ('Help message', @labels);
}

# display about
sub display_about {
  my @labels;
  my $k = -1;

  $labels[++$k] = Gtk2::Label->new;
  $labels[$k]->set_markup ('<b>About Queue Front End v' . version . '</b>');
  $labels[$k]->set_justify ('center');
  $labels[++$k] = Gtk2::Label->new;
  $labels[$k]->set_markup ('
A front end to manage the cluster queue.
');
  $labels[$k]->set_justify ('left');
  $labels[++$k] = Gtk2::Label->new;
  $labels[$k]->set_markup ('Copyright 2008 Soft\'N\'Design
Laurent Mazet <u><span color="blue">mazet@softndesign.org</span></u>');
  $labels[$k]->set_justify ('center');

  show ('About...', @labels);
}
