#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
    4digits - A guess-the-number game, aka Bulls and Cows
    Copyright (c) 2004-2007 Pan Yongzhi <http://fourdigits.sourceforge.net>

    4digits is a guess-the-number puzzle game. You are given eight times
    to guess a four-digit number. One digit is marked A if its value and
    position are both correct, and marked B if only its value is correct.
    You win the game when you get 4A0B. Good luck!

    4digits 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 of
    the License, or (at your option) any later version.

    4digits 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 4digits; if not, write to the Free Software Foundation,
    Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
"""

import sys

try:
    import pygtk
    pygtk.require('2.0')
    import gtk
    import gtk.glade
except ImportError:
    print 'python-gtk2 is required to run 4digits.'
    print 'No python-gtk2 was found on your system.'
    sys.exit(1)

try:
    import gnome.ui
except ImportError:
    print 'python-gnome2 is required to run 4digits.'
    print 'No python-gnome2 is found on your system.'
    sys.exit(1)

import os
import random
import time
import webbrowser

# The first is the current directory, so you do not have to install 
# 4digits to play it. The second is the installation directory.
gladefiles = [os.path.join(os.path.dirname(__file__), '4digits.glade'),
    os.path.normpath(os.path.join(
    os.path.dirname(__file__), '../share/4digits/4digits.glade'))]
helpfiles = [os.path.join(os.path.dirname(__file__), 'doc/index.html'),
    os.path.normpath(os.path.join(
    os.path.dirname(__file__), '../share/doc/4digits/index.html'))]
score_file = os.path.join(os.getenv('HOME'), '.4digits/4digits.4digits.scores')

def _(arg):
    """Mark translatable strings"""
    return arg

def load_glade(dialog = None):
    """Try to load dialog from the different possible glade files."""
    for gladefile in gladefiles:
        try:
            return gtk.glade.XML(gladefile, dialog) 
        except RuntimeError:
            continue # Just ignore the error, the next file will be tested
    # Still here
    print "Gladefile not found! Checked paths were:"
    print gladefiles
    sys.exit(2)
    
class FourDigits:
    """This is the main class"""
    def __init__(self):
        """GUI initialization"""
        self.widget_tree = load_glade()
        self.toolbar = self.widget_tree.get_widget('toolbar')
        self.view_toolbar = self.widget_tree.get_widget('view_toolbar')

        self.hint_table = self.widget_tree.get_widget('box_hints')
        self.hint_hseparator = self.widget_tree.get_widget('hint_hseparator')
        self.view_hint_table = self.widget_tree.get_widget('view_hint_table')
        self.autofill_hints = self.widget_tree.get_widget('view_autofill_hints')

        self.entry = self.widget_tree.get_widget('entry')
        self.entry.grab_focus()

        self.ok_button = self.widget_tree.get_widget('ok_button')
        self.ok_button.set_sensitive(True)
        self.entry.set_sensitive(True)

        self.table = self.widget_tree.get_widget("state_table")
        for widget in ('g0', 'g1', 'g2', 'g3', 'g4', 'g5', 'g6', 'g7',
                'r0', 'r1', 'r2', 'r3', 'r4', 'r5', 'r6', 'r7'):
            setattr(self, widget, self.widget_tree.get_widget(widget))
        self.info_label = self.widget_tree.get_widget('info_label')
        self.time_label = self.widget_tree.get_widget('time_label')
        
        self.score_view = self.widget_tree.get_widget('score_view')
        
        self.cb_hint = []
        self.label_hint = []
        self.build_hint_table()
        dic = {'on_main_window_destroy': gtk.main_quit,
                'on_quit_activate': gtk.main_quit,
                'on_ok_clicked': self.on_entry_activate,
                'on_new_game_activate': self.on_new_game_activate,
                'on_view_toolbar_toggled': self.on_view_toolbar_toggled,
                'on_view_hint_table_toggled': self.on_view_hint_table_toggled,
                'on_entry_activate': self.on_entry_activate,
                'on_entry_changed': self.on_entry_changed,
                'on_help_activate' : self.on_help_activate,
                'on_about_activate' : self.on_about_activate,
                'on_score_activate': self.on_score_activate}
        self.widget_tree.signal_autoconnect(dic)
        # new game initialization
        self.game = NewRound()

    def build_hint_table(self):
        """Create the controls for the hint table"""
        box_hints = self.widget_tree.get_widget('box_hints')
        for name in (0, 40):
            table = gtk.Table(rows=11, columns=6)
            box_hints.pack_start(table)

            # Create row labels
            for row in range(0, 10):
                label = gtk.CheckButton(str(row))
                table.attach(label, 0, 1, row+1, row+2)
                label.connect('toggled', self.change_row, row+name)
                self.label_hint.append(label)

            for col in range(1, 5):
                # Create column labels
                label = gtk.Label(str(col))
                table.attach(label, col, col+1, 0, 1)
                # Create Checkboxes
                for row in range(0, 10):
                    checkbutton = gtk.CheckButton()
                    table.attach(checkbutton, col, col+1, row+1, row+2)
                    self.cb_hint.append(checkbutton)

            if name == 0: # First table
                box_hints.pack_start(gtk.VSeparator())
        self.init_hint_table()

    def change_row(self, widget, row):
        """Toggle a rows state."""
        enable = widget.get_active()
        for col in range(0, 4):
            self.cb_hint[10 * col + row].set_sensitive(enable)

    def init_hint_table(self):
        """Reset all controls in the hinttable to their default state."""
        for i in range(0, 40):
            self.cb_hint[i].set_active(True)
            self.cb_hint[i+40].set_active(False)
            self.cb_hint[i+40].set_sensitive(False)
        self.cb_hint[0].set_active(False)
        for row in range(0, 10):
            self.label_hint[row].set_active(True)
            self.label_hint[row+10].set_active(False)
    
    #         1    2    3    4                   1    2    3    4
    #  0/0    00   10   20   30          0/10    40   50   60   70
    #  1/1    01   11   21   31          1/11    41   51   61   71
    #  2/2    02   12   22   32          2/12    42   52   62   72
    # ...

    def get_checkbox(self, row, col, tablenr=0):
        """Get the checkbox at the specified position"""
        return self.cb_hint[tablenr * 40 + col * 10 + row]

    def get_label(self, row, tablenr=0):
        """Get the label at the specified position"""
        return self.label_hint[tablenr*10 + row]
    
    def on_entry_activate(self, widget):
        """when input is accepted"""
        A, B = 0, 0
        number = ''
        # check input
        if self.game.guess < 8:
            number = self.entry.get_text()
            if number == '':
                self.error_process(_('Must input something.'))
                return False
            elif number[0] == '0':
                self.error_process(_('First digit cannot be zero.'))
                return False
            try:
                number = repr(int(number))
            except ValueError:
                self.error_process(_('Must input a number.'))
                return False
            if len(number) < 4:
                self.error_process(_('Must input four digits.'))
                return False
            elif len(set(number)) <4:
                self.error_process(_('Four digits must be unique.'))
                return False
            elif number in self.game.guesses:
                self.error_process(_("You've already guessed it."))
                return False
            self.game.guesses.append(number)
            # process input
            for i in xrange(4):
                for j in xrange(4):
                    if self.game.answer[i] == int(number[j]):
                        if i == j:
                            A += 1
                        else:
                            B += 1
            guess_label = getattr(self, 'g' + repr(self.game.guess))
            result_label = getattr(self, 'r' + repr(self.game.guess))
            guess_label.set_text(number)
            result_label.set_text('%dA%dB' % (A, B))

            if (self.autofill_hints.get_active()):
                self.fill_hints(number, A, B)
            
            # win
            if A == 4:
                self.info_label.set_text(_('You win! :)'))
                self.get_time_taken_till_now()
                self.time_label.set_text(_('Used %.1f s.') % 
                    self.game.time_taken)
                self.ok_button.set_sensitive(False)
                self.entry.set_sensitive(False)
                if self.is_high_score(self.game.time_taken):
                    new_score_rank = self.write_score(self.game.time_taken)
                    self.show_score(new_score_rank)
            # lose
            elif self.game.guess == 7:
                answer = ''
                for i in xrange(4):
                    answer += repr(self.game.answer[i])
                self.info_label.set_text(_('Haha, you lose. It is %s.') % 
                    answer)
                self.get_time_taken_till_now()
                self.time_label.set_text(_('Wasted %.1f s.') % 
                    self.game.time_taken)
                self.ok_button.set_sensitive(False)
                self.entry.set_sensitive(False)
        self.game.guess += 1
        self.entry.grab_focus()

    def clear_row(self, row):
        """Clear a complete row"""
        self.get_label(row, 0).set_active(False)
        for col in range(0, 4):
            self.get_checkbox(row, col, 0).set_active(False)
            self.get_checkbox(row, col, 1).set_active(False)

    def fill_hints(self, number, A, B):
        """Autocomplete the hint table"""
        number = [int(x) for x in number]
        if A == 0 and B == 0:
            for digit in number:
                self.clear_row(digit)
            return

        if (A+B) == 4:
            for digit in range(0, 10):
                if digit in number:
                    self.get_label(digit, 0).set_active(True)
                    self.get_label(digit, 1).set_active(True)
                else:
                    self.clear_row(digit)

        if A == 0: # Only B's
            for digit_pos in range(0, 4):
                self.get_checkbox(number[digit_pos], digit_pos, 
                    0).set_active(False)
                self.get_checkbox(number[digit_pos], digit_pos, 
                    1).set_active(False)

        if A == 4:# Everything right
            for digit_pos in range(0, 4):
                for pos2 in range(0, 4):
                    if pos2 == digit_pos:
                        continue
                    self.get_checkbox(number[digit_pos], pos2, 
                        0).set_active(False)
                self.get_checkbox(number[digit_pos], digit_pos, 
                    1).set_active(True)

    def on_entry_changed(self, widget):
        """Start timer as soon as the user enters the first digit"""
        self.info_label.set_text('')
        if self.game.on_entry_cb_first_called_in_this_round == True:
            self.time_label.set_text(_('Timer started...'))
            self.game.time_start = time.time()
            self.game.on_entry_cb_first_called_in_this_round = False
  
    def on_view_toolbar_toggled(self, widget):
        """Toggle toolbar visibility"""
        if self.view_toolbar.get_active():
            self.toolbar.show()
        else:
            self.toolbar.hide()

    def on_view_hint_table_toggled(self, widget):
        """Toggle hint table visibility"""
        if self.view_hint_table.get_active():
            self.hint_table.show_all()
            self.hint_hseparator.show()
        else:
            self.hint_table.hide_all()
            self.hint_hseparator.hide()
    
    def on_help_activate(self, widget):
        """Show help."""
        for helpfile in helpfiles:
            try:
                file(helpfile)
            except IOError:
                continue
            webbrowser.open(helpfile)

    def on_about_activate(self, widget):
        """Show about dialog"""
        about = AboutDialog().about_dialog
        about.run()
        about.destroy()
    
    def on_score_activate(self, new_score_rank):
        """Show scores"""
        score_dialog = ScoreDialog()
        dlg = score_dialog.score_dialog
        score_view = score_dialog.score_view
        sv_selection = score_view.get_selection()
        sv_selection.set_mode(gtk.SELECTION_NONE)
        column = gtk.TreeViewColumn(
                'Name', gtk.CellRendererText(), text = 0)
        score_view.append_column(column)
        column = gtk.TreeViewColumn(
                'Score', gtk.CellRendererText(), text = 1)
        score_view.append_column(column)
        column = gtk.TreeViewColumn(
                'Date', gtk.CellRendererText(), text = 2)
        score_view.append_column(column)
        dlg.scoreList = gtk.ListStore(str, str, str)
        score_view.set_model(dlg.scoreList)

        try:
            scores = [line.split(' ', 6) for line in file(score_file, 'r')]
        except IOError:
            scores = []

        for line in scores:
            score_tup = line[0], line[1], ' '.join(line[2:]).rstrip('\n')
            dlg.scoreList.append(score_tup)
        # high light the current high score entry
        try:
            sv_selection.set_mode(gtk.SELECTION_SINGLE)
            sv_selection.select_path(new_score_rank)
        except TypeError:
            sv_selection.set_mode(gtk.SELECTION_NONE)

        dlg.run()
        dlg.destroy()

    def on_new_game_activate(self, widget):
        """New game initialization"""
        self.game = NewRound()
        self.ok_button.set_sensitive(True)
        self.entry.set_sensitive(True)
        self.entry.grab_focus()
        # won't start the timer when you just start a new game
        self.game.on_entry_cb_first_called_in_this_round = False
        self.entry.set_text('')
        self.game.on_entry_cb_first_called_in_this_round = True
        self.info_label.set_text(_('Ready'))
        self.time_label.set_text('')

        for i in xrange(8):
            getattr(self, 'g' + repr(i)).set_text('')
            getattr(self, 'r' + repr(i)).set_text('')
        
        self.init_hint_table()

    def error_process(self, msg):
        """Show error message in statusbar"""
        self.info_label.set_text(msg)
        self.entry.grab_focus()

    def get_time_taken_till_now(self):
        """Get time since start of the game"""
        self.game.time_end = time.time()
        self.game.time_taken = self.game.time_end - self.game.time_start
        self.game.time_taken = round(self.game.time_taken, 1)

    def is_high_score(self, time_taken):
        """Is this time a highscore"""
        try:
            scores = [line.split(' ', 6) for line in file(score_file, 'r')]
        except IOError:
            return True # List does not exist yet
        if len(scores) < 10:
            return True
        scores = sorted(scores, key = lambda x: float(x[1][:-1]))
        if time_taken < float(scores[-1][1][:-1]):
            return True
        else:
            return False

    def write_score(self, time_taken):
        """Write highscore file"""
        date = time.strftime("%a %b %d %H:%M:%S %Y") 
        new_score = "%s %ss %s\n" % (
                os.getenv('USERNAME'), time_taken, date)
        try:
            saved_scores = open(score_file, 'r').readlines()
        except IOError:
            saved_scores = [] 
        saved_scores.append(new_score)
        scores = [line.split(' ', 6) for line in saved_scores]
        scores = sorted(scores, key = lambda x: float(x[1][:-1]))
        scores = scores[:10]

        # find the index of the new score
        new_score = new_score.split(' ', 6)
        new_score_rank = scores.index(new_score)
        try:
            fp = open(score_file, 'w')
        except IOError:
            # Most likely the directory does not exist
            new_dir = os.path.dirname(score_file)
            os.mkdir(new_dir)
            fp = open(score_file, 'w') # Try again
        for score in scores:
            fp.write(' '.join(score))
        fp.close()
        return new_score_rank

    def show_score(self, new_score_rank):
        """Show highscore dialog"""
        self.on_score_activate(new_score_rank)

class AboutDialog:
    """the about dialog"""
    def __init__(self):
        self.widget_tree = load_glade('about_dialog')
        self.about_dialog = self.widget_tree.get_widget("about_dialog")
        gtk.about_dialog_set_url_hook(
                lambda about_dialog, url: webbrowser.open(url))
        self.about_dialog.set_website(
                'http://fourdigits.sourceforge.net')
        self.about_dialog.set_website_label(
                'http://fourdigits.sourceforge.net')

class ScoreDialog:
    """the score dialog"""
    def __init__(self):
        self.widget_tree = load_glade('score_dialog')
        self.score_dialog = self.widget_tree.get_widget('score_dialog')
        self.score_view = self.widget_tree.get_widget('score_view')

class NewRound:
    """This class contains data in one round of the game"""
    def __init__(self):
        while True:
            self.answer = random.sample(range(10), 4)
            if self.answer[0] != 0:
                break
        if self.answer == [4, 6, 1, 9]:
            print 'You are the luckiest guy on the planet!'
        self.guess = 0
        self.guesses = []
        self.time_start = 0
        self.time_end = 0
        self.time_taken = 0
        self.on_entry_cb_first_called_in_this_round = True

if __name__ == "__main__":
    FourDigits = FourDigits()
    gnome.init('4digits', '0.8')
    gtk.main()
