commit af6f5b4261dbaa63a4472ef4511a4c4b8c53c776
Author: Parin Porecha <parinporecha@gmail.com>
Date:   Mon May 5 17:39:41 2014 +0530

    Implemented save game feature
    
    Created SudokuSaver class in sudoku-saver.vala
    Some features of this class are -
    
    - Saves a game as a JSON file thus giving the benefit of readability
    - Saves position of board values explicitly.
      Values which are empty and have no notes aren't saved.
    - A backup of the game is created everytime before the save file is overwritten.
    - If a save file cannot be parsed, the latest backup is read instead.
    - On game completion, the save file is moved to finished/ folder and all the backups are deleted.
    - Has a get_latest_savegame() function which will be used on Sudoku initialization in the future.

diff --git a/TODO b/TODO
index b3529a4..2188932 100644
--- a/TODO
+++ b/TODO
@@ -31,17 +31,13 @@ mcatanzaro: Too confusing in general:
    mouse over the text to see anything, and even then there's no explanation of
    what it is.
 
-
-No way to save a game
-
-No way to know if a game has already been played or not:
-  For this SudokuTracker class needs to be ported. Once it is complete, enable the corresponding options -
+enable the options -
   'MarkAsPlayedToggle' and 'includeOldGamesToggle' in data/print_games.ui (for multiple print dialog)
 
 Currently we only read sudokus from data files:
   We need to complete the port of SudokuGenerator class to be able to generate sudokus as desired.
 
-Port saver.py for saving games, fullscreen mode feature, highlighter feature
+Port fullscreen mode feature, highlighter feature
 
 mcatanzaro: We're removing Fullscreen from most other games because it usually
             looks pretty bad. Games maximized with the header bar usually fit
@@ -61,8 +57,6 @@ mcatanzaro: This number pad was from an earlier design; I think we can remove
 
 - The preview window does not resize to it's original size (the size at startup) after 'New Game' is clicked.
 
-- No way to save a game (port saver.py)
-
 - Sudoku master has total 19 menu options (8 in 'Game', 5 in 'Settings', 4 in 'Tools', 2 in 'Help')
   Current vala-port design and mockups have only one panel menu and it cannot accomodate all the 19 options
   Ask aday about this.
@@ -78,3 +72,5 @@ mcatanzaro: This number pad was from an earlier design; I think we can remove
   Instead, they are displayed at random positions in the cells (looks random, maybe there is a bug in display offsets)
 
 - Print (single sudoku) should be disabled when not in-game
+
+- Message Dialogs haven't been ported. Port gtk_goodies/dialog_extras.py
diff --git a/configure.ac b/configure.ac
index 81358cd..5e66425 100644
--- a/configure.ac
+++ b/configure.ac
@@ -21,6 +21,7 @@ PKG_CHECK_MODULES(GNOME_SUDOKU, [
   gio-2.0
   gtk+-3.0 >= $GTK_REQUIRED
   gee-0.8
+  json-glib-1.0
 ])
 
 AC_PATH_PROG([APPDATA_VALIDATE], [appdata-validate], [/bin/true])
diff --git a/data/gnome-sudoku.ui b/data/gnome-sudoku.ui
index ba1bc34..55ca191 100644
--- a/data/gnome-sudoku.ui
+++ b/data/gnome-sudoku.ui
@@ -153,6 +153,41 @@
                                 <property name="position">2</property>
                             </packing>
                         </child>
+                        <child>
+                            <object class="GtkBox" id="savegame_box">
+                                <property name="visible">True</property>
+                                <child>
+                                    <object class="GtkBox" id="savegame_grid">
+                                        <property name="visible">True</property>
+                                        <property name="orientation">vertical</property>
+                                        <child>
+                                            <placeholder/>
+                                        </child>
+                                        <child>
+                                            <object class="GtkLabel" id="savegame_label">
+                                                <property name="visible">True</property>
+                                                <property name="can_focus">False</property>
+                                                <property name="label" translatable="yes">Latest Saved</property>
+                                                <attributes>
+                                                    <attribute name="weight" value="bold"/>
+                                                </attributes>
+                                            </object>
+                                        </child>
+                                    </object>
+                                    <packing>
+                                        <property name="expand">False</property>
+                                        <property name="fill">True</property>
+                                        <property name="padding">20</property>
+                                    </packing>
+                                </child>
+                            </object> <!-- End of save game box -->
+                            <packing>
+                                <property name="expand">True</property>
+                                <property name="fill">True</property>
+                                <property name="position">3</property>
+                                <property name="padding">20</property>
+                            </packing>
+                        </child>
                     </object> <!-- End of new start_box -->
                 </child>
                 <child>
diff --git a/src/Makefile.am b/src/Makefile.am
index ba46571..6b16b45 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -15,6 +15,7 @@ gnome_sudoku_SOURCES = \
 	sudoku-view.vala \
 	number-picker.vala \
 	sudoku-printer.vala \
+	sudoku-saver.vala \
 	$(BUILT_SOURCES)
 
 gnome_sudoku_CFLAGS = \
@@ -31,7 +32,8 @@ gnome_sudoku_VALAFLAGS = \
 	--pkg posix \
 	--pkg gio-2.0 \
 	--pkg gtk+-3.0 \
-	--pkg gee-0.8
+	--pkg gee-0.8 \
+	--pkg json-glib-1.0
 
 gnome-sudoku-resources.c: gnome-sudoku.gresource.xml $(shell $(GLIB_COMPILE_RESOURCES) --generate-dependencies gnome-sudoku.gresource.xml)
 	$(AM_V_GEN) $(GLIB_COMPILE_RESOURCES) --target=$@ --sourcedir=$(srcdir) --generate-source $<
diff --git a/src/gnome-sudoku.vala b/src/gnome-sudoku.vala
index 9d27713..90c96c8 100644
--- a/src/gnome-sudoku.vala
+++ b/src/gnome-sudoku.vala
@@ -28,9 +28,12 @@ public class Sudoku : Gtk.Application
     private SudokuView medium_preview;
     private SudokuView hard_preview;
     private SudokuView very_hard_preview;
+    private SudokuView savegame_preview;
 
     private SudokuStore sudoku_store;
 
+    private SudokuSaver  saver;
+
     // Help Stuff
     private Box naked_single_items;
     private Box hidden_single_items;
@@ -108,12 +111,14 @@ public class Sudoku : Gtk.Application
         });
 
         sudoku_store = new SudokuStore ();
+        saver = new SudokuSaver ();
         //SudokuGenerator gen = new SudokuGenerator();
 
         var easy_grid = (Box) builder.get_object ("easy_grid");
         var medium_grid = (Box) builder.get_object ("medium_grid");
         var hard_grid = (Box) builder.get_object ("hard_grid");
         var very_hard_grid = (Box) builder.get_object ("very_hard_grid");
+        var savegame_grid = (Box) builder.get_object ("savegame_grid");
 
         var easy_board = sudoku_store.get_random_easy_board ();
         //gen.make_symmetric_puzzle(Random.int_range(0, 4));
@@ -171,11 +176,34 @@ public class Sudoku : Gtk.Application
             return false;
         });
 
+        var savegame = saver.get_latest_savegame ();
+        if (savegame == null)
+            savegame = new SudokuGame (sudoku_store.get_random_easy_board ());
+        //gen.make_symmetric_puzzle(Random.int_range(0, 4));
+        // gen.generate (DifficultyRating.medium_range);
+        savegame_preview = new SudokuView (savegame, true);
+        savegame_preview.show ();
+        savegame_grid.pack_start (savegame_preview);
+
+        savegame_grid.button_press_event.connect ((event) => {
+            if (event.button == 1)
+                start_game (savegame.board);
+
+            return false;
+        });
+
         show_start ();
 
         builder.connect_signals (this);
 
         window.show ();
+
+        window.delete_event.connect ((event) => {
+            if (game_box.visible)
+                saver.save_game (game);
+
+            return false;
+        });
     }
 
     private void start_game (SudokuBoard board)
@@ -234,7 +262,7 @@ public class Sudoku : Gtk.Application
         game.board.completed.connect (() => {
             view.dance ();
 
-            var time = game.timer.elapsed ();
+            var time = game.get_total_time_played ();
 
             for (var i = 0; i < game.board.rows; i++)
             {
@@ -244,6 +272,8 @@ public class Sudoku : Gtk.Application
                 }
             }
 
+            saver.add_game_to_finished (game);
+
             var dialog = new MessageDialog(null, DialogFlags.DESTROY_WITH_PARENT, MessageType.INFO, ButtonsType.NONE, "Well done, you completed the puzzle in %f seconds", time);
 
             dialog.add_button ("Same difficulty again", 0);
@@ -474,6 +504,8 @@ public class Sudoku : Gtk.Application
 
     public void quit_cb ()
     {
+        if (game_box.visible)
+            saver.save_game (game);
         window.destroy ();
     }
 
diff --git a/src/sudoku-board.vala b/src/sudoku-board.vala
index 0569cca..6201c1a 100644
--- a/src/sudoku-board.vala
+++ b/src/sudoku-board.vala
@@ -12,6 +12,8 @@ public class SudokuBoard
     private bool[,] possible_in_col;            /* if specific value is possible in specific col */
     private bool[,,] possible_in_block;         /* if specific value is possible in specific block */
 
+    public double previous_played_time { set; get; default = 0; }
+
     /* Number of rows in one block */
     private int _block_rows;
     public int block_rows
@@ -471,6 +473,23 @@ public class SudokuBoard
         stdout.printf (" ]");
     }
 
+    public string to_string (bool original_state = false)
+    {
+        var board_string = "";
+        for (var i = 0; i < _rows; i++)
+        {
+            for (var j = 0; j < _cols; j++)
+            {
+                if (is_fixed[i, j])
+                    board_string += cells[i, j].to_string ();
+                else
+                    board_string += original_state ? "0" : cells[i, j].to_string ();
+            }
+        }
+
+        return board_string;
+	}
+
     public int[,] get_cells()
     {
         return cells;
diff --git a/src/sudoku-game.vala b/src/sudoku-game.vala
index 42c2473..005d1fa 100644
--- a/src/sudoku-game.vala
+++ b/src/sudoku-game.vala
@@ -155,4 +155,9 @@ public class SudokuGame
             add_to_stack (ref to, -1, -1, num);
         }
     }
+
+    public double get_total_time_played ()
+    {
+        return board.previous_played_time + timer.elapsed ();
+    }
 }
diff --git a/src/sudoku-saver.vala b/src/sudoku-saver.vala
new file mode 100644
index 0000000..072a9e6
--- /dev/null
+++ b/src/sudoku-saver.vala
@@ -0,0 +1,307 @@
+/* -*- Mode: vala; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+
+using Gee;
+
+public class SudokuSaver
+{
+    private string savegame_dir;
+    private string finishgame_dir;
+
+    public SudokuSaver() {
+        try {
+
+            savegame_dir = Environment.get_home_dir () + "/.config/gnome-sudoku/saved";
+            finishgame_dir = Environment.get_home_dir () + "/.config/gnome-sudoku/finished";
+
+            {
+                var file = File.new_for_path (savegame_dir);
+
+                if (!file.query_exists ())
+                    file.make_directory ();
+            }
+
+            {
+                var file = File.new_for_path (finishgame_dir);
+
+                if (!file.query_exists ())
+                    file.make_directory ();
+            }
+        }  catch (Error e) {
+            error ("%s", e.message);
+        }
+    }
+
+    public SudokuGame get_latest_savegame ()
+    {
+        var game = new SudokuGame (new SudokuBoard ());
+        try {
+            string filename = get_latest_save_filename ();
+            if (filename == null)
+                return null;
+
+            game = parse_json_to_game (savegame_dir + "/" + filename, ref game);
+        } catch (Error e) {
+            error ("%s", e.message);
+        }
+
+        return game;
+    }
+
+    public void save_game (SudokuGame game)
+    {
+        create_gamefile_in_directory (game, savegame_dir);
+    }
+
+    public void add_game_to_finished (SudokuGame game)
+    {
+        string file_name = create_gamefile_in_directory (game, finishgame_dir);
+        delete_savefile_and_backups (file_name);
+    }
+
+    private string get_latest_save_filename ()
+    {
+        var file = File.new_for_path (savegame_dir);
+
+        if (!file.query_exists () || file.query_file_type (0) != FileType.DIRECTORY)
+            return null;
+
+        FileEnumerator enumerator = file.enumerate_children (
+                "standard::*",
+                FileQueryInfoFlags.NOFOLLOW_SYMLINKS,
+                null);
+
+        FileInfo info = null;
+        string latest_filename = null;
+        int64 latest_timestamp = 0;
+        while ((info = enumerator.next_file (null)) != null) {
+
+            if (!info.get_name ().has_suffix (".save"))
+                continue;
+
+            Json.Parser parser = new Json.Parser ();
+            try {
+                parser.load_from_file (savegame_dir + "/" + info.get_name ());
+            } catch (Error e) {
+                if (!restore_backup (savegame_dir + "/" + info.get_name ()))
+                    continue;
+                parser.load_from_file (savegame_dir + "/" + info.get_name ());
+            }
+
+            Json.Node node = parser.get_root ();
+            Json.Reader reader = new Json.Reader (node);
+            reader.read_member ("last_modified");
+            assert (reader.is_value ());
+            var cur_timestamp = reader.get_int_value ();
+            if (cur_timestamp > latest_timestamp) {
+                latest_filename = info.get_name ();
+                latest_timestamp = cur_timestamp;
+            }
+        }
+
+        return latest_filename;
+    }
+
+    private string create_gamefile_in_directory (SudokuGame game, string dir)
+    {
+        var json_str = serialize_game_to_json (game);
+        var file_name = game.board.to_string (true) + ".save";
+        var file_path = dir + "/" + file_name;
+        save_string_to_file (json_str, file_path);
+        return file_name;
+    }
+
+    private string serialize_game_to_json (SudokuGame game)
+    {
+        var board = game.board;
+        var board_cells = board.get_cells ();
+        Json.Builder builder = new Json.Builder ();
+
+        builder.begin_object ();
+        builder.set_member_name ("difficulty_rating");
+        builder.add_double_value (board.difficulty_rating);
+        builder.set_member_name ("difficulty_catagory");
+        builder.add_string_value (board.get_difficulty_catagory ().to_string ());
+        builder.set_member_name ("time_elapsed");
+        builder.add_double_value (game.get_total_time_played ());
+
+        builder.set_member_name ("cells");
+        builder.begin_array ();
+
+        for (var i = 0; i < board.rows; i++)
+        {
+            for (var j = 0; j < board.cols; j++)
+            {
+
+                if (board_cells[i, j] == 0)
+                    continue;
+
+                builder.begin_object ();
+
+                builder.set_member_name ("position");
+                builder.begin_array ();
+                builder.add_int_value (i);
+                builder.add_int_value (j);
+                builder.end_array ();
+                builder.set_member_name ("value");
+                builder.add_int_value (board_cells[i, j]);
+                builder.set_member_name ("fixed");
+                builder.add_boolean_value (board.is_fixed[i, j]);
+                builder.set_member_name ("notes");
+                builder.add_string_value ("");
+
+                builder.end_object ();
+            }
+        }
+
+        builder.end_array ();
+        builder.set_member_name ("last_modified");
+        builder.add_int_value (GLib.get_real_time ());
+
+        builder.end_object ();
+
+        Json.Generator generator = new Json.Generator ();
+        generator.set_pretty (true);
+        Json.Node root = builder.get_root ();
+        generator.set_root (root);
+
+        return generator.to_data (null);
+    }
+
+    private SudokuGame parse_json_to_game (string file_path, ref SudokuGame game)
+    {
+        Json.Parser parser = new Json.Parser ();
+        try {
+            parser.load_from_file (file_path);
+        } catch (Error e) {
+            if (restore_backup (file_path))
+                return parse_json_to_game (file_path, ref game);
+            return null;
+        }
+
+        var board = game.board;
+        Json.Node node = parser.get_root ();
+        Json.Reader reader = new Json.Reader (node);
+        reader.read_member ("cells");
+        assert (reader.is_array ());
+
+        for (var i = 0; i < reader.count_elements (); i++)
+        {
+            reader.read_element (i);	// Reading a cell
+
+            reader.read_member ("position");
+            assert (reader.is_array ());
+            assert (reader.count_elements () == 2);
+            reader.read_element (0);
+            assert (reader.is_value ());
+            var row = (int) reader.get_int_value ();
+            reader.end_element ();
+
+            reader.read_element (1);
+            assert (reader.is_value ());
+            var col = (int) reader.get_int_value ();
+            reader.end_element ();
+            reader.end_member ();
+
+            reader.read_member ("value");
+            assert (reader.is_value ());
+            var val = (int) reader.get_int_value ();
+            reader.end_member ();
+
+            reader.read_member ("fixed");
+            assert (reader.is_value ());
+            var is_fixed = reader.get_boolean_value ();
+            reader.end_member ();
+
+            reader.end_element ();
+
+            board.insert (row, col, val, is_fixed);
+        }
+        reader.end_member ();
+
+        reader.read_member ("time_elapsed");
+        assert (reader.is_value ());
+        game.board.previous_played_time = (double) reader.get_double_value ();
+        reader.end_member ();
+
+        return game;
+    }
+
+    private void save_string_to_file (string str, string file_path)
+    {
+        var file = File.new_for_path (file_path);
+
+        if (file.query_exists ())
+            create_backup (file);
+        try {
+            FileUtils.set_contents (file_path, str);
+        } catch (Error e) {
+            error ("%s", e.message);
+        }
+    }
+
+    private void create_backup (File file)
+    {
+        var i = 1;
+        while (true)
+        {
+            File backup = File.new_for_path (file.get_path () + "." + i.to_string () + ".bak");
+            if (!backup.query_exists ())
+            {
+                try {
+                    file.copy (backup, 0);
+                } catch (Error e) {
+                    error ("%s", e.message);
+                }
+                break;
+            }
+            i++;
+        }
+    }
+
+    private bool restore_backup (string file_path)
+    {
+        var i = 0;
+        File file;
+        while (true)
+        {
+            file = File.new_for_path (file_path + "." + (i+1).to_string () + ".bak");
+            if (!file.query_exists ())
+            {
+                if (i == 0)
+                    return false;
+
+                File orig_save = File.new_for_path (file_path);
+                File valid_backup = File.new_for_path (file_path + "." + i.to_string () + ".bak");
+                valid_backup.move (orig_save, FileCopyFlags.OVERWRITE);
+                break;
+            }
+            i++;
+        }
+
+        return true;
+    }
+
+    private void delete_savefile_and_backups (string file_name)
+    {
+        var file = File.new_for_path (savegame_dir + "/" + file_name);
+
+        if (!file.query_exists ())
+            return;
+
+        file.delete ();
+
+        file = File.new_for_path (savegame_dir);
+        FileEnumerator enumerator = file.enumerate_children (
+                "standard::*",
+                FileQueryInfoFlags.NOFOLLOW_SYMLINKS,
+                null);
+
+        FileInfo info = null;
+        while ((info = enumerator.next_file (null)) != null) {
+             if (info.get_name ().has_prefix (file_name) && info.get_name ().has_suffix (".bak")) {
+                File backup = File.new_for_path (savegame_dir + "/" + info.get_name ());
+                backup.delete ();
+            }
+        }
+    }
+}
