#!/usr/bin/perl 

# Molar Mass Calculator
# (c) 2010-2011, Denis G. Samsonenko <d.g.samsonenko@gmail.com>
# LGPL

# Gtk2 gui

use 5.008009;
use strict;
use warnings;
use Glib qw/TRUE FALSE/;
use Gtk2 '-init';
use Gtk2::Ex::Simple::List;
use Locale::gettext;
use MMCalc qw(20240208 :all);
use open ':locale';

# initialization
init() or die "ERROR: Initialization failed! $@";

textdomain("mmcalc");
#bindtextdomain ("mmcalc", "../..");
bind_textdomain_codeset("mmcalc", "utf8");

our $focus_clip = undef;;
our $focus_clear = undef;
our $multy_mode = FALSE;
our $clipboard = Gtk2::Clipboard->get(Gtk2::Gdk->SELECTION_CLIPBOARD);
our @items = ();

# Create a window
our $window = create_window();

# Accelerators
my $accel_group = Gtk2::AccelGroup->new();
$window->add_accel_group($accel_group);

# Create a vertical box
my $vbox = Gtk2::VBox->new(FALSE, 0);
$window->add($vbox);

# Menu
my $menu = create_menu();
$vbox->pack_start($menu, FALSE, FALSE, 0);
$menu->show_all();

my $vbox1 = Gtk2::VBox->new(FALSE, 5);
$vbox1->set_border_width(5);
$vbox->add($vbox1);

my $single_box = create_single_box();
$vbox1->pack_start($single_box, TRUE, TRUE, 0);
$single_box->show_all();

my $multy_box = create_multy_box();
$vbox1->pack_start($multy_box, TRUE, TRUE, 0);

# footer: Copy, Clear, Quit
my $footer = Gtk2::HBox->new(FALSE, 10);
$vbox1->pack_start($footer, FALSE, FALSE, 0);

# "Quit" button
my $bquit = Gtk2::Button->new_from_stock('gtk-quit');
$bquit->signal_connect(clicked => \&delete_event);
$footer->pack_end($bquit, FALSE, FALSE, 0);

# "Clear" botton
my $bclear = Gtk2::Button->new_from_stock('gtk-clear');
$bclear->set_focus_on_click(FALSE);
$bclear->signal_connect(clicked => \&clear);
$footer->pack_end($bclear, FALSE, FALSE, 0);

# "Copy" botton
my $bcopy = Gtk2::Button->new_from_stock('gtk-copy');
$bcopy->set_focus_on_click(FALSE);
$bcopy->signal_connect(clicked => \&copytoclip);
$footer->pack_end($bcopy, FALSE, FALSE, 0);

$footer->show_all();
$vbox1->show();
$vbox->show();
$window->show();

# Rest in main and wait for the fun to begin!
Gtk2->main;


# Functions

sub create_window
{
# Create a window
  my $window = Gtk2::Window->new('toplevel');
  $window->set_title(gtext('Molar Mass Calculator'));
  $window->set_position('center');
  $window->set_default_size(400, 300);

  # set application image
  $window->set_icon_name("mmcalc");
  $window->set_default_icon_name("mmcalc");

  # Here we just set a handler for the delete_event that immediately
  # exits GTK.
  $window->signal_connect(delete_event => \&delete_event);

  # Sets the border width of the window.
  $window->set_border_width(0);
  return $window;
}

sub create_single_box
{
  my $calc = MMCalc->new();

  my ($vbox, $entry, $results, $bcalc) = create_box_item(
                    gtext(" Molar mass:\n Number of electrons:\n Content:"));

  # Table
  my $tbox = Gtk2::HBox->new(FALSE, 0);
  my $table = Gtk2::Ex::Simple::List->new(
                              gtext('atom') => 'text',
                              gtext('number') => 'text',
                              gtext('mass %') => 'text',
                              gtext('error') => 'text');
  $tbox->pack_start($table, TRUE, TRUE, 0);
  $vbox->pack_start($tbox, TRUE, TRUE, 0);

  # object instance
  my %instance = (
                    'calc' => $calc,
                    'entry' => $entry,
                    'cliptext' => "",
                    'results' => $results,
                    'table' => $table
                 );

  push @items, [ $vbox, \%instance ];

  $entry->signal_connect('activate' => \&execute, $#items);
  $bcalc->signal_connect('clicked' => \&execute, $#items);

  return $vbox;
}

sub focus_in
{
  $focus_clip = $_[2];
  $focus_clear = $focus_clip;
  return FALSE;
}

sub focus_out
{
  $focus_clear = undef;
  return FALSE;
}

sub create_multy_box
{
  my $vbox = Gtk2::VBox->new(FALSE, 10);

  # default box
  add_box_item(undef, $vbox);
  add_box_item(undef, $vbox);

  # Add, Remove & Quit buttons
  my $footer = Gtk2::HBox->new(FALSE, 10);
  $footer->set_border_width(5);
  $vbox->pack_end($footer, FALSE, FALSE, 0);

  my $add_button = Gtk2::Button->new_from_stock ('gtk-add');
  $add_button->signal_connect(clicked => \&add_box_item, $vbox);
  $footer->pack_start($add_button, FALSE, FALSE, 0);

  my $remove_button = Gtk2::Button->new_from_stock ('gtk-remove');
  $remove_button->signal_connect(clicked => \&remove_box_item);
  $footer->pack_start($remove_button, FALSE, FALSE, 0);

  return $vbox;
}

# add box item to multy_box
sub add_box_item
{
  my $pbox = $_[1];

  my ($new_item, $entry, $results, $bcalc) = create_box_item(gtext(" Molar mass:"));

  my $calc = MMCalc->new();

  # object instance
  my %instance = (
                    'calc' => $calc,
                    'entry' => $entry,
                    'cliptext' => "",
                    'results' => $results,
                    'table' => undef
                 );

  push @items, [ $new_item, \%instance ];

  $pbox->pack_start($new_item, FALSE, TRUE, 0);
  $new_item->show_all();

  $entry->signal_connect('activate' => \&execute, $#items);
  $entry->signal_connect('focus-in-event' => \&focus_in, $#items);
  $entry->signal_connect('focus-out-event' => \&focus_out);

  $bcalc->signal_connect('clicked' => \&execute, $#items);
  $bcalc->signal_connect('focus-in-event' => \&focus_in, $#items);
  $bcalc->signal_connect('focus-out-event' => \&focus_out);

  $results->signal_connect('focus-in-event' => \&focus_in, $#items);
  $results->signal_connect('focus-out-event' => \&focus_out);

  $window->set_focus($entry);
}

# remove box item from multy_box
sub remove_box_item
{
  if ($#items > 1)
  {
    my $item = pop @items;
    $item->[0]->destroy();
  }
  my $number = 1;
  $number = $focus_clip if (defined($focus_clip) and ($focus_clip <= $#items));
  $window->set_focus($items[$number][1]->{entry});
}

sub create_box_item
{
  my $labl_str = shift;

  # Create a vertical box
  my $vbox = Gtk2::VBox->new(FALSE, 5);

  # box: Entry, Calculate
  my $box = Gtk2::HBox->new(FALSE, 5);
  $vbox->pack_start($box, FALSE, FALSE, 0);

  # Entry
  my $entry = Gtk2::Entry->new();
  $box->pack_start($entry, TRUE, TRUE, 0);

  # "Calculate" botton
  my $bcalc = Gtk2::Button->new(gtext("Calculate"));
  $bcalc->set_focus_on_click(FALSE);
  $box->pack_start($bcalc, FALSE, FALSE, 0);

  # Frame
  my $frame = Gtk2::Frame->new();
  $vbox->pack_start($frame, FALSE, TRUE, 0);

  # Label
  my $results = Gtk2::Label->new($labl_str);
  $results->set_alignment(0,0);
  $results->set_selectable(TRUE);
  $frame->add($results);

  return ($vbox, $entry, $results, $bcalc);
}

sub error_message_dialog
# $_[0] = parent window
# $_[1] = error message text
{
  my $error_dialog = Gtk2::MessageDialog->new_with_markup($_[0],
                     [qw/modal destroy-with-parent/],
                     'error', 'close', $_[1]);
  $error_dialog->show_all();
  $error_dialog->run;
  $error_dialog->destroy;
}

sub save_acronyms
# $_[0] = parent window
{
  eval {
    my $file_path = "";
    if (write_acronyms($file_path))
    {
      my $dialog = Gtk2::MessageDialog->new_with_markup($_[0],
                     [qw/modal destroy-with-parent/], 'info', 'ok',
                     gtext("Acronyms list was successfully saved to ") . $file_path . "!");
      $dialog->show_all();
      $dialog->run;
      $dialog->destroy;
    }
    elsif ($file_path)
    {
      error_message_dialog($_[0], gtext("Error: failed to save acronyms.dat to ") .
                           $file_path . "!");
    }
    else
    {
      error_message_dialog($_[0], gtext("Nothing to save!"));
    }
    return TRUE;
  } or do {
     print $@;
     utf8::decode($@) if (utf8::valid($@));
     error_message_dialog($_[0],
        gtext("Exception happened during attempt to save list of acronyms:\n\n") . $@);
  }
}

sub add_edit_acronym
# $_[0] = \@list
# $_[1] = acronym number
# $_[2] = parent widget
# $_[3] = dialog title
{
  my $flag = FALSE;
  my @list = @{$_[0]};
  my $dialog = Gtk2::Dialog->new($_[3], $_[2],
                             [qw/modal destroy-with-parent no-separator/],
                             'gtk-cancel' => 'reject',
                             'gtk-apply' => 'apply');

  my $label1 = Gtk2::Label->new(gtext('Acronym'));
  my $label2 = Gtk2::Label->new(gtext('Formula'));
  my $label3 = Gtk2::Label->new(gtext('Comments'));
  my $entry1 = Gtk2::Entry->new;
  my $entry2 = Gtk2::Entry->new;
  my $entry3 = Gtk2::Entry->new;
  $entry1->set_text($list[$_[1]][0]);
  $entry2->set_text($list[$_[1]][1]);
  $entry3->set_text($list[$_[1]][2]);

  my $table = Gtk2::Table->new(2, 3, FALSE);
  $table->attach_defaults($label1, 0, 1, 0, 1);
  $table->attach_defaults($label2, 1, 2, 0, 1);
  $table->attach_defaults($label3, 2, 3, 0, 1);
  $table->attach_defaults($entry1, 0, 1, 1, 2);
  $table->attach_defaults($entry2, 1, 2, 1, 2);
  $table->attach_defaults($entry3, 2, 3, 1, 2);
  $dialog->get_content_area()->add($table);

  $dialog->show_all();

  if ($dialog->run eq 'apply')
  {
    my $error_message = "";
    my $str1 = $entry1->get_text;
    my $str2 = $entry2->get_text;
    my $str3 = $entry3->get_text;
    $str1 =~ s/^\s+//;
    $str2 =~ s/^\s+//;
    $str3 =~ s/^\s+//;
    $str1 =~ s/\s+$//;
    $str2 =~ s/\s+$//;
    $str3 =~ s/\s+$//;

    splice @list, $_[1], 1;
    # check acronym validity
    if (check_acronym(\@list, $str1, $str2, $str3, $error_message))
    {
      ${$_[0]}[$_[1]][0] = $str1;  # acronym
      ${$_[0]}[$_[1]][1] = $str2;  # formula
      ${$_[0]}[$_[1]][2] = $str3;  # name or comments
      $flag = TRUE;
      $_[2]->set_response_sensitive('apply', TRUE);
      $_[2]->set_response_sensitive('accept', TRUE);
    }
    else
    {
      # error message dialog
      error_message_dialog($dialog, $error_message);
    }
  }
  $dialog->destroy;
  return $flag;
}

sub add_acronym
# $_[0] = \@list
# $_[1] = parent widget
{
  my @list = @{$_[0]};
  push @list, [ gtext('New acronym'), gtext('New formula'), gtext('Comments') ];
  push @{$_[0]}, [ $list[$#list][0], $list[$#list][1], $list[$#list][2]]
       if (add_edit_acronym(\@list, $#list, $_[1], gtext('Add an acronym')));
}

sub edit_acronym
# $_[0] = \@list
# $_[1] = acronym number
# $_[2] = parent widget
{
  add_edit_acronym ($_[0], $_[1], $_[2], gtext('Change the acronym'));
}

# Show acronyms list dialog
sub acronyms_dialog
{
  my $dialog = Gtk2::Dialog->new(gtext('Acronyms list'), $window,
                               [qw/modal destroy-with-parent/],
                               'gtk-add' => '1',
                               'gtk-remove' => '2',
                               'gtk-edit' => '3',
                               'gtk-apply' => 'apply',
                               'gtk-save' => 'accept');
  $dialog->set_default_size(500, 400);

  $dialog->set_response_sensitive('apply', FALSE);
  $dialog->set_response_sensitive('accept', FALSE);

  my $list = Gtk2::Ex::Simple::List->new(gtext('Acronym') => 'text',
                                         gtext('Formula') => 'text',
                                         gtext('Comments') => 'text');
  $list->set_rules_hint (TRUE);
  get_acronyms(\@{$list->{data}});

  my $scrolled_window = Gtk2::ScrolledWindow->new;
  $scrolled_window->add_with_viewport($list);
  $scrolled_window->set_policy('automatic','automatic');
  $dialog->get_content_area()->add($scrolled_window);

  $dialog->show_all();

  my $response;
LOOP:
  {
    $response = $dialog->run;
    (add_acronym(\@{$list->{data}}, $dialog), redo LOOP) if ($response eq '1');
    if ($response eq '2')
    {
      splice(@{$list->{data}}, ($list->get_selected_indices)[0], 1);
      $dialog->set_response_sensitive('apply', TRUE);
      $dialog->set_response_sensitive('accept', TRUE);
      redo LOOP;
    }
    (edit_acronym(\@{$list->{data}}, ($list->get_selected_indices)[0], $dialog), redo LOOP)
        if ($response eq '3');
    (set_acronyms(\@{$list->{data}}), last LOOP) if ($response eq 'apply');
    (set_acronyms(\@{$list->{data}}), save_acronyms($dialog)) if ($response eq 'accept');
  }
  $dialog->destroy;
}

# Menu
sub create_menu_item
# $_[0] = $menu
# $_[1] = item name
# $_[2] = \&call
# $_[3] = accelerator
{
  my $menu_item = Gtk2::MenuItem->new($_[1]);
  $menu_item->signal_connect('activate' => $_[2]);
  $menu_item->add_accelerator('activate', $accel_group,
              Gtk2::Accelerator->parse($_[3]), 'visible') if ($_[3]);
  $_[0]->append($menu_item);
}

sub create_check_menu_item
# $_[0] = $menu
# $_[1] = item name
# $_[2] = \&call
# $_[3] = accelerator
{
  my $menu_item = Gtk2::CheckMenuItem->new($_[1]);
  $menu_item->signal_connect('toggled' => $_[2]);
  $menu_item->add_accelerator('activate', $accel_group,
              Gtk2::Accelerator->parse($_[3]), 'visible') if ($_[3]);
  $_[0]->append($menu_item);
}

sub create_menu_item_from_stock
# $_[0] = $menu
# $_[1] = stock
# $_[2] = \&call
# $_[3] = accelerator
{
  my $menu_item = Gtk2::ImageMenuItem->new_from_stock($_[1], $accel_group);
  $menu_item->signal_connect('activate' => $_[2]);
  $menu_item->add_accelerator('activate', $accel_group,
              Gtk2::Accelerator->parse($_[3]), 'visible') if ($_[3]);
  $_[0]->append($menu_item);
}

sub multy_mode
{
  my $check_menu_item = shift;
  if ($check_menu_item->get_active())
  {
    $single_box->hide();
    $multy_box->show_all();
    my $number = 1;
    $number = $focus_clip if (defined($focus_clip) and ($focus_clip <= $#items));
    $window->set_focus($items[$number][1]->{entry});
    $multy_mode = TRUE;
  }
  else
  {
    $multy_box->hide();
    $single_box->show_all();
    $window->set_focus($items[0][1]->{entry});
    $multy_mode = FALSE;
  }
}

sub gtext
{
  my $str = gettext($_[0]);
  utf8::decode($str);
  return $str;
}

sub create_menu
{
  # Menu bar: File, Tools, Help
  my $menu_bar = Gtk2::MenuBar->new();
  # File
  my $menu_item_file = Gtk2::MenuItem->new(gtext('_File'));
  $menu_bar->append($menu_item_file);
  # Tools
  my $menu_item_tools = Gtk2::MenuItem->new(gtext('_Tools'));
  $menu_bar->append($menu_item_tools);
  # Options
  my $menu_item_options = Gtk2::MenuItem->new(gtext('_Options'));
  $menu_bar->append($menu_item_options);
  # Help
  my $menu_item_help = Gtk2::ImageMenuItem->new(${Gtk2::Stock->lookup('gtk-help')}{'label'});
  $menu_bar->append($menu_item_help);

  # Menu File: Quit
  my $menu_file = Gtk2::Menu->new();
  $menu_item_file->set_submenu($menu_file);
  create_menu_item_from_stock($menu_file, 'gtk-quit', \&delete_event, FALSE);

  # Menu Tools: Clear, Copy, Acronyms
  my $menu_tools = Gtk2::Menu->new();
  $menu_item_tools->set_submenu($menu_tools);
  create_menu_item_from_stock($menu_tools, 'gtk-clear', \&clear, 'Escape');
  create_menu_item_from_stock($menu_tools, 'gtk-copy', \&copytoclip, FALSE);
  create_menu_item($menu_tools, gtext('Ac_ronyms'), \&acronyms_dialog, FALSE);

  # Menu Options: Multy Mode
  my $menu_options = Gtk2::Menu->new();
  $menu_item_options->set_submenu($menu_options);
  create_check_menu_item($menu_options, gtext('_Multy mode'), \&multy_mode, 'F8');

  # Menu Help: About
  my $menu_help = Gtk2::Menu->new();
  $menu_item_help->set_submenu($menu_help);
  create_menu_item_from_stock($menu_help, 'gtk-about', \&help, 'F1');

  return $menu_bar;
}

# Print resuls to text entry
sub print_result
{
  my $str = shift;
  my $results = shift;
  my $entry = shift;

  $results->set_markup($str);
  $window->set_focus($entry);
}

# main function
sub execute
{
  my $number = $_[1];
  my $calc = $items[$number][1]->{calc};
  my $entry = $items[$number][1]->{entry};
  my $textclip = \$items[$number][1]->{textclip};
  my $results = $items[$number][1]->{results};
  my $table = $items[$number][1]->{table};

  # get string
  my $str = $entry->get_text;

  # remove leading and finishing spaces
  $str =~ s/^\s+//;
  $str =~ s/\s+$//;
  return unless ($str);

  # $atoms[$i][0] = atom symbol, [1] = quantity,
  # [2] = concentration, [3] = concentration error
  my @atoms = ();

  # molar mass
  my $mass = 0;

  # mass error
  my $error = 0;

  # number of electrons
  my $electrons = 0;

  # calculate molar mass, contents, concentrations and absolute errors
  if ($calc->calculate($str))
  {
    $mass = $calc->get_mass();
    $error = $calc->get_error();
    $electrons = $calc->get_electrons();
    @atoms = @{$calc->get_atoms()};

    # return if no molar mass
    return 1 unless ($mass);

    # content
    my $contlist = $calc->get_formula();
    ${$textclip} = $contlist . " (%): ";
    $contlist =~ s:(\d+\.\d+|\d+):<sub>$1</sub>:g;

    $str = gtext(" Molar mass: ") . $mass . " ";
    my $pm = "±";
    utf8::decode($pm);
    $str .= $pm . " " . $error if ($error);

    if (defined($table))
    {
      # number of electrons
      $str .= gtext(" g/mol\n Number of electrons: ");
      $str .= $electrons;
      # content
      @{$table->{data}} = () ;
      $str .= gtext("\n Content: ");
      $str .= $contlist;
    }
    else
    {
      $str .= gtext(" g/mol");
    }

    # print mass, concentrations and errors;
    print_result($str, $results, $entry);

    # concentrations
    if ($#atoms > 0)
    {
      # more than one atom
      for (my $i = 0; $i <= $#atoms; ++$i)
      {
        next unless ($atoms[$i][1]);
        if (defined($table))
        {
          if ($error)
          {
            push @{$table->{data}}, [ $atoms[$i][0], $atoms[$i][1], $atoms[$i][2],
                                    $pm . " ". $atoms[$i][3] ];
          }
          else
          {
            push @{$table->{data}}, [ $atoms[$i][0], $atoms[$i][1], $atoms[$i][2], "" ];
          }
        }
        # prepare string for clipboard
        ${$textclip} .= "$atoms[$i][0]" . ", " . sprintf("%.1f", $atoms[$i][2]);
        if ($i < $#atoms)
        {
          ${$textclip} .= "; ";
        }
        else
        {
          ${$textclip} .= ".";
        }
      }
    }
    # one atom
    else
    {
      push @{$table->{data}}, [ $atoms[0][0], $atoms[0][1], "100", "" ] if (defined($table));
      ${$textclip} .= "$atoms[0][0], 100.";
    }
  }
  else
  {
    print_result(gtext(" Syntax error!"), $results, $entry);
    @{$table->{data}} = () if (defined($table));
    ${$textclip} = "";
  }
}

sub clear
{
  my $focus = undef;
  $focus = $items[$focus_clear][1] if (defined($focus_clear));
  $focus = $items[0][1] unless ($multy_mode);

  if (defined($focus))
  {
    $focus->{entry}->set_text('');
    $window->set_focus($focus->{entry});
    my $init_str;
    if (defined($focus->{table}))
    {
      @{$focus->{table}->{data}} = ();
      $init_str = gtext(" Molar mass:\n Number of electrons:\n Content:");
    }
    else
    {
      $init_str = gtext(" Molar mass:");
    }
    $focus->{results}->set_text($init_str);
    $focus->{textclip} = "";
    $focus->{'calc'}->reset();
  }
}

sub copytoclip
{
  my $focus = undef;
  $focus = $items[$focus_clip][1] if (defined($focus_clip));
  $focus = $items[0][1] unless ($multy_mode);
  if (defined($focus))
  {
    $clipboard->set_text($focus->{textclip}) if ($focus->{textclip});
  }
}

sub delete_event
{
  Gtk2->main_quit;
  return FALSE;
}

sub help
{
  my $year = (localtime)[5];
  $year += 1900;

  my $copy = '©';
  utf8::decode($copy);

  my $helptext = "<span  size=\"x-large\"><b>MMCalc: " .
     gtext("Molar mass calculator") . "</b></span>\n\n<b>" .
     gtext("Version: ") . get_version() . "</b>\n<b>" . $copy .
     " 2010-" . $year . " Denis G. Samsonenko</b>\n" .
     "<a href=\"http://www.ogion76.name/home/mmcalc\">" .
     "http://www.ogion76.name/home/mmcalc</a>\n\n" .
     gtext("This program calculates molar mass and percent of each " .
     "element for the given chemical formula.\n\n" .
     "The program contains perl module MMCalc.pm and two perl " .
     "scripts, mmcalc and gmmcalc.\n\n" .
     "mmcalc is a console version of calculator, while gmmcalc " .
     "is GUI version using Gtk+.\n\n" .
     "Examples of valid formulae: H2O, CuSO4*5h2o, hgcl2, c o,\n" .
     "In(NO3)3*4.5H2O, Rb16Cd25,39Sb36.\n\n" .
     "Acronyms are also supported: [Zn2(dabco)(bdc)2]*4DMF,\n" .
     "Pd(acac)2, h4edta. It is possible to add, remove and " .
     "change the acronyms.\n\n" .
     "Using of parentheses, square brackets as well as braces " .
     "is acceptable.\n\n" .
     "MMCalc is free software, released under the GNU LGPL.");

  my $dialog = Gtk2::MessageDialog->new_with_markup($window,
                     [qw/modal destroy-with-parent/],
                     'info', 'ok', $helptext);

  $dialog->set_title(gtext('About MMCalc'));

  # set dialog image
  eval {
    $dialog->get_image->set_from_pixbuf
             (Gtk2::IconTheme->get_default->load_icon
                 ($window->get_icon_name, 64, 'force-svg')
             );
    TRUE;
  } or warn $@;

  # set fucus on the OK button
  $dialog->set_focus($dialog->get_action_area->get_children());

  $dialog->run();
  $dialog->destroy();
}

0;
