/* -*- Mode: Vala; indent-tabs-mode: nil; c-basic-offset: 2; tab-width: 2 -*- */
/*
    Multiplication Puzzle
    Copyright (C) 2004-2008,2011 Michael Terry <mike@mterry.name>

    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 3 of the License, 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, see <http://www.gnu.org/licenses/>.
*/

using GLib;

public class GtkMult : Gtk.Window
{
  public static int dpi;
  
  public MultPuzzle puzzle { get; private set; }
  public bool cheated { get; private set; }
  public Gtk.Statusbar status { get; private set; }
  
  public static const double ASPECT = 8.0/6.0;
  
  public void clear_guess_feedback ()
  {
    if (status_timeout_id != 0)
      Source.remove (status_timeout_id);
    pop_guess_feedback ();
  }
  
  construct
  {
    vbox = new Gtk.VBox (false, 0);
    status = null;
    score = new Gtk.Label ("");
    canvas = null;
    status_timeout_id = 0;
    solve_action = null;
    hint_action = null;
    timer = new Timer();
    timer_label = new Gtk.Label("");
    timer_timeout_id = 0;
    
    // Convert pixel space to desired point size
    Value dpiValue = Value (typeof (int));
    get_screen ().get_setting ("gtk-xft-dpi", dpiValue);
    this.dpi = dpiValue.get_int () / Pango.SCALE;
    
    var height = 4*dpi;
    set_default_size((int)(height*ASPECT), height);
    
    Gtk.VBox vb = new Gtk.VBox (false, 0);
    status_box = new Gtk.HBox (false, 0);

    vb.pack_start (setup_menu (), false, false, 0);
    vb.pack_start (vbox, true, true, 0);
    vb.pack_start (status_box, false, false, 0);
    
    var eb = new Gtk.EventBox();
    eb.add(score);
    eb.set_border_width(5);
    status_box.pack_start(eb, false, false, 0);
    
    eb = new Gtk.EventBox();
    eb.add(timer_label);
    eb.set_border_width(5);
    status_box.pack_start(eb, false, false, 0);
    
    add (vb);
    
    add_events (Gdk.EventMask.KEY_PRESS_MASK);
    key_press_event.connect(handle_key_press);
    
    set_title (_("Multiplication Puzzle"));
    
    new_puzzle ();
    
    destroy.connect(Gtk.main_quit);
  }
  
  static unichar a_char = _("A").get_char().toupper();
  static unichar b_char = _("B").get_char().toupper();
  static unichar c_char = _("C").get_char().toupper();
  static unichar d_char = _("D").get_char().toupper();
  static unichar e_char = _("E").get_char().toupper();
  static unichar f_char = _("F").get_char().toupper();
  static unichar g_char = _("G").get_char().toupper();
  static unichar h_char = _("H").get_char().toupper();
  static unichar i_char = _("I").get_char().toupper();
  static unichar j_char = _("J").get_char().toupper();
  static unichar 0_char = _("0").get_char().toupper();
  static unichar 1_char = _("1").get_char().toupper();
  static unichar 2_char = _("2").get_char().toupper();
  static unichar 3_char = _("3").get_char().toupper();
  static unichar 4_char = _("4").get_char().toupper();
  static unichar 5_char = _("5").get_char().toupper();
  static unichar 6_char = _("6").get_char().toupper();
  static unichar 7_char = _("7").get_char().toupper();
  static unichar 8_char = _("8").get_char().toupper();
  static unichar 9_char = _("9").get_char().toupper();
  private bool handle_key_press (Gdk.EventKey event)
  {
    // Don't recognize any keys if ctrl or alt are pressed.
    var state = (event.state & Gtk.accelerator_get_default_mod_mask());
    state = state & (~0x03); // ignores shift and caps lock
    if (state != 0)
      return false;
    
    char ch;
    unichar ev_char = Gdk.keyval_to_unicode(Gdk.keyval_to_upper(event.keyval));
    
    if      (ev_char == a_char) ch = 'A';
    else if (ev_char == b_char) ch = 'B';
    else if (ev_char == c_char) ch = 'C';
    else if (ev_char == d_char) ch = 'D';
    else if (ev_char == e_char) ch = 'E';
    else if (ev_char == f_char) ch = 'F';
    else if (ev_char == g_char) ch = 'G';
    else if (ev_char == h_char) ch = 'H';
    else if (ev_char == i_char) ch = 'I';
    else if (ev_char == j_char) ch = 'J';
    else if (ev_char == 0_char) ch = '0';
    else if (ev_char == 1_char) ch = '1';
    else if (ev_char == 2_char) ch = '2';
    else if (ev_char == 3_char) ch = '3';
    else if (ev_char == 4_char) ch = '4';
    else if (ev_char == 5_char) ch = '5';
    else if (ev_char == 6_char) ch = '6';
    else if (ev_char == 7_char) ch = '7';
    else if (ev_char == 8_char) ch = '8';
    else if (ev_char == 9_char) ch = '9';
    else if (event.keyval == 65307) { // escape; vala needs keysyms
      this.canvas.clear_mode();
      return true;
    }
    else
      return false;
    
    this.canvas.start_choice(ch);
    return true;
  }
  
  Gtk.Action solve_action;
  Gtk.Action hint_action;
  private Gtk.Widget setup_menu ()
  {
    var action_group = new Gtk.ActionGroup ("gmult-actions");
    
    var action = new Gtk.Action ("GameMenuAction", _("_Game"), null, null);
    action_group.add_action (action);
    
    action = new Gtk.Action ("NewAction", null, null, Gtk.Stock.NEW);
    action.activate.connect(on_menu_game_new);
    action_group.add_action_with_accel (action, "<control>N");
    
    action = new Gtk.Action ("HintAction", _("_Hint"), null, null);
    action.activate.connect(on_menu_game_hint);
    action_group.add_action_with_accel (action, "<control>H");
    this.hint_action = action;
    
    action = new Gtk.Action ("SolveAction", _("_Solve"), null, null);
    action.activate.connect(on_menu_game_solve);
    action_group.add_action (action);
    this.solve_action = action;
    
    action = new Gtk.Action ("CloseAction", null, null, Gtk.Stock.CLOSE);
    action.activate.connect(on_menu_game_close);
    action_group.add_action_with_accel (action, "<control>W");
    
    action = new Gtk.Action ("HelpMenuAction", _("_Help"), null, null);
    action_group.add_action (action);
    
    action = new Gtk.Action ("AboutAction", null, null, Gtk.Stock.ABOUT);
    action.activate.connect(on_menu_help_about);
    action_group.add_action (action);
    
    var ui = """
<ui>
  <menubar>
    <menu name="GameMenu" action="GameMenuAction">
      <menuitem name="New" action="NewAction" />
      <separator />
      <menuitem name="Hint" action="HintAction" />
      <menuitem name="Solve" action="SolveAction" />
      <separator />
      <menuitem name="Close" action="CloseAction" />
    </menu>
    <menu name="HelpMenu" action="HelpMenuAction">
      <menuitem name="About" action="AboutAction"/>
    </menu>
  </menubar>
</ui>""";

    var manager = new Gtk.UIManager ();
    try {
    manager.add_ui_from_string (ui, -1);
    } catch (Error e)  {
      error ("Internal error: bad ui string.\n");
    }
    manager.insert_action_group (action_group, 0);
    add_accel_group (manager.get_accel_group ());
    
    var menu_item = manager.get_widget ("/ui/menubar/GameMenu/New") as Gtk.MenuItem;
    menu_item.select.connect(on_menu_select_new);
    menu_item.deselect.connect(on_menu_deselect);
    
    menu_item = manager.get_widget ("/ui/menubar/GameMenu/Hint") as Gtk.MenuItem;
    menu_item.select.connect(on_menu_select_hint);
    menu_item.deselect.connect(on_menu_deselect);
    
    menu_item = manager.get_widget ("/ui/menubar/GameMenu/Solve") as Gtk.MenuItem;
    menu_item.select.connect(on_menu_select_solve);
    menu_item.deselect.connect(on_menu_deselect);
    
    menu_item = manager.get_widget ("/ui/menubar/GameMenu/Close") as Gtk.MenuItem;
    menu_item.select.connect(on_menu_select_close);
    menu_item.deselect.connect(on_menu_deselect);
    
    menu_item = manager.get_widget ("/ui/menubar/HelpMenu/About") as Gtk.MenuItem;
    menu_item.select.connect(on_menu_select_about);
    menu_item.deselect.connect(on_menu_deselect);
    
    return manager.get_widget ("/ui/menubar");
  }

  private Canvas canvas;
  private Gtk.VBox vbox;
  private Timer timer;
  private Gtk.Label timer_label;
  private Gtk.Label score;
  private Gtk.HBox status_box;
  private uint status_timeout_id;
  private uint timer_timeout_id;
  
  private void new_puzzle ()
  {
    if (canvas != null)
      vbox.remove (canvas);
    
    if (status != null)
      status_box.remove (status);
    
    // easiest way to reset the statusbar
    status = new Gtk.Statusbar ();
    status_box.pack_start (status, true, true, 0);
    status_box.reorder_child (status, 0);
    
    puzzle = new MultPuzzle (3, 2);
    
    canvas = new Canvas (this);
    vbox.pack_start (canvas, true, true, 0);
    
    Gdk.Geometry geom = Gdk.Geometry();
    geom.min_aspect = ASPECT;
    geom.max_aspect = ASPECT;
    set_geometry_hints(canvas, geom, Gdk.WindowHints.ASPECT);
    
    timer.reset();
    update_timer();
    if (timer_timeout_id != 0)
      Source.remove(timer_timeout_id);
    timer_timeout_id = Timeout.add_seconds (1, update_timer);
    
    cheated = false;
    
    show_all ();
    update_score ();
    on_puzzle_change(puzzle);
    
    if (status_timeout_id != 0)
      Source.remove (status_timeout_id);
    status_timeout_id = 0;
    
    puzzle.guessed.connect(on_puzzle_guess);
    puzzle.changed.connect(on_puzzle_change);
  }
  
  private void on_puzzle_guess (MultPuzzle p, int digit, MultPuzzleChar letter, MultPuzzleGuessStatus response)
  {
    update_score ();
    
    var context_id = status.get_context_id ("guess-feedback");
    clear_guess_feedback ();
    
    if (puzzle.is_done)
    {
      status.push (context_id, _("Congratulations!"));
    }
    else
    {
      string message_pattern = null;
      
      switch (response)
      {
      case MultPuzzleGuessStatus.WRONG:
        // Translators: First argument is letter, second is digit
        message_pattern = _("Incorrect — %1$s is not %2$s");
        break;
      case MultPuzzleGuessStatus.CORRECT:
        // Translators: First argument is letter, second is digit
        message_pattern = _("Correct — %1$s is %2$s");
        break;
      default:
      case MultPuzzleGuessStatus.KNOWN:
      case MultPuzzleGuessStatus.INVALID:
        // shouldn't happen
        break;
      }
      
      if (message_pattern != null) {
        var letter_str = "%c".printf ((char)letter);
        var digit_str = "%i".printf (digit);
        status.push (context_id, message_pattern.printf (_(letter_str), _(digit_str)));
        status_timeout_id = Timeout.add_seconds (5, pop_guess_feedback);
      }
    }
  }
  
  private void on_puzzle_change (MultPuzzle p)
  {
    solve_action.set_sensitive (!p.is_done);
    hint_action.set_sensitive (!p.is_done);
  }

  private bool pop_guess_feedback ()
  {
    var context_id = status.get_context_id ("guess-feedback");
    status.pop (context_id);
    status_timeout_id = 0;
    return false;
  }
  
  private void update_score ()
  {
    var wrong = puzzle.wrong_guesses;
    
    var text = "%s: %i%s".printf (_("Incorrect Guesses"), wrong, cheated ? "*" : "");
    
    score.set_text (text);
  }
  
  private bool update_timer()
  {
    if (puzzle.is_done)
      return false;
    
    var secs = (uint)timer.elapsed(null);
    var text = "%s: %02u:%02u:%02u".printf(_("Time"), secs/60/60, secs/60%60, secs%60);
    timer_label.set_text(text);
    return true;
  }
  
  private void on_menu_deselect ()
  {
    var context_id = status.get_context_id ("menu-hover");
     status.pop (context_id);
  }
  
  private void on_menu_game_close ()
  {
    destroy ();
  }
  
  private void on_menu_select_close ()
  {
    var context_id = status.get_context_id ("menu-hover");
    clear_guess_feedback ();
    status.push (context_id, _("Close Multiplication Puzzle"));
  }
  
  private void on_menu_game_new ()
  {
    new_puzzle ();
  }
  
  private void on_menu_select_new ()
  {
    var context_id = status.get_context_id ("menu-hover");
    clear_guess_feedback ();
    status.push (context_id, _("Start a new game"));
  }
  
  private void on_menu_game_solve ()
  {
    cheated = true;
    
    puzzle.solve ();
    
    var context_id = status.get_context_id ("guess-feedback");
    clear_guess_feedback ();
    status.push (context_id, _("Puzzle solved"));
    
    update_score ();
  }
  
  private void on_menu_select_solve ()
  {
    var context_id = status.get_context_id ("menu-hover");
    clear_guess_feedback ();
    status.push (context_id, _("Solve this game"));
  }
  
  private void on_menu_game_hint()
  {
    cheated = true;
    
    // Pick a random unknown digit.  Keep trying until we actually solve one
    // (because the digit may not have been in the puzzle -- we want to be
    // more useful than that.
    var character = MultPuzzleChar.INVALID;
    var digit = 0;
    var needed = this.puzzle.get_needed_digits();
    var num_needed = 0;
    foreach (bool n in needed)
      num_needed += n ? 1 : 0;
    if (num_needed == 0) // shouldn't happen
      return;
    
    var choice = Random.int_range(0, num_needed);
    for (int i = 0; i < 10; ++i) {
      if (needed[i] && choice == 0) {
        character = this.puzzle.solve_digit(i);
        digit = i;
        break;
      }
      else if (needed[i])
        --choice;
    }
    
    var context_id = status.get_context_id ("guess-feedback");
    clear_guess_feedback ();
    
    // Translators: First argument is letter, second is digit
    var message_pattern = _("%1$s is %2$s");
    var letter_str = "%c".printf ((char)character);
    var digit_str = "%i".printf (digit);
    status.push (context_id, message_pattern.printf (_(letter_str), _(digit_str)));
    status_timeout_id = Timeout.add_seconds (5, pop_guess_feedback);
    
    update_score ();
  }
  
  private void on_menu_select_hint ()
  {
    var context_id = status.get_context_id ("menu-hover");
    clear_guess_feedback ();
    status.push (context_id, _("Reveal a digit at random"));
  }
  
  // These need to be class-wide to prevent an odd compiler syntax error.
  private const string[] authors = {"Michael Terry <mike@mterry.name>",
                                    null};
  private const string[] artists = {"Eugenia Loli-Queru <eloli@hotmail.com>",
                                    "Michael Terry <mike@mterry.name>",
                                    null};
  
  private Gtk.AboutDialog about { get; set; default = null; }
  private void on_menu_help_about ()
  {
    if (about != null)
    {
      about.present ();
      return;
    }
    
    about = new Gtk.AboutDialog ();
    about.title = _("About Multiplication Puzzle");
    about.authors = authors;
    about.artists = artists;
    about.translator_credits = _("translator-credits");
    about.logo_icon_name = Config.PACKAGE;
    about.version = Config.VERSION;
    about.copyright = "© 2003—2011 Michael Terry";
    about.website = "https://launchpad.net/gmult";
    about.license_type = Gtk.License.GPL_3_0;
    
    about.set_transient_for (this);
    about.response.connect((dlg, resp) => {dlg.destroy (); about = null;});
    
    about.show ();
  }
  
  private void on_menu_select_about ()
  {
    var context_id = status.get_context_id ("menu-hover");
    clear_guess_feedback ();
    status.push (context_id, _("About Multiplication Puzzle"));
  }
  
  public static int main(string[] args)
  {
    GLib.Intl.textdomain (Config.GETTEXT_PACKAGE);
    GLib.Intl.bindtextdomain (Config.GETTEXT_PACKAGE, Config.LOCALE_DIR);
    GLib.Intl.bind_textdomain_codeset (Config.GETTEXT_PACKAGE, "UTF-8");
    
    OptionContext context = new OptionContext ("");
    context.add_group (Gtk.get_option_group (false)); // allow console use
    try {
      context.parse (ref args);
    } catch (Error e) {
      printerr ("%s\n\n%s", e.message, context.get_help (true, null));
      return 1;
    }
    
    Gtk.init (ref args); // to open display ('cause we passed false above)
    
    GLib.Environment.set_application_name (_("Multiplication Puzzle"));
    Gtk.IconTheme.get_default ().append_search_path (Config.THEME_DIR);
    Gtk.Window.set_default_icon_name (Config.PACKAGE);
    
    var window = new GtkMult ();
    window.show_all();
    Gtk.main ();
    
    return 0;
  }
}

