#!/usr/bin/python
# -*- coding: utf-8 -*-

# Copyright (C) 2014 Rod Engerstad


import Tkinter as tk
import tkColorChooser
import copy

from pzl_lib import PzlG
from pzl_lib import SettingsB
from pzl_lib import Preferences
from pzl_lib import MenuItem
from pzl_lib import Flasher
from pzl_lib import ScreenShot
from pzl_lib import OpenFileI
from pzl_lib import ViewB
from pzl_lib import Square
from pzl_lib import Egg
from sudoku_model import Sgg
from sudoku_model import SudokuModel
from sudoku_data import ModelData


class Settings(SettingsB):

    def __init__(self):
        SettingsB.__init__(self)
        self.dic = {}
        self.dic['even_box_colour'] = '#ffffff'
        self.dic['odd_box_colour'] = '#dfdfdf'
        self.dic['text_colour'] = '#000000'
        self.dic['cause_colour'] = '#00d341'
        self.dic['effect_colour'] = '#ff0000'
        self.dic['button_colour'] = '#82c026'
        self.dic['error_colour'] = '#ff0000'
        self.dic['hint_mode'] = 'simplest_hint'
        self.dic['save_puzzle'] = 'without_candidates'
        self.dic['set_colour'] = 'even_box_colour'
        self.dic['font_size'] = '2'
        self.original_square_size = 54
        self.square_size = self.original_square_size
        self.menu_items = []

        self.preferences = Preferences(self.dic)
        self.preferences.load()
        if self.dic['hint_mode'] == 'simplest_hint':
            Sgg.simplest_hint = True
        else:
            Sgg.simplest_hint = False
        self.load_menus()
        self.count = len(self.menu_items)
        self.set_fonts()
        self.make_help()
        self.make_tooltip()
        self.make_alg_help()

    def make_alg_help(self):
        self.alg_text = [_("The exclusion rule: only one of each digit can occur in any row, column or box, so once an answer is set all other occurrences of that digit in its row, column and box can be deleted."), _("Hidden single: all rows, columns and boxes must contain exactly one of each of the digits; if a cell contains the only remaining copy of a digit in a row, column or box, that digit must be the answer for that cell, and any other digits in that cell can be deleted."), _("Locked candidate 1: all rows and columns must contain exactly one of each of the digits; if cells in one row (or column) of one box contain the only copies of digits in that box, those digits must provide the answers for that box, and any occurrences of those digits in the row or column outside the box can be deleted."), _("Locked candidate 2: all rows and columns must contain exactly one of each of the digits; if cells in one row (or column) of one box contain the only copies of digits in that row or column, those digits must provide the answers for that row or column, and any occurrences of those digits in the box outside the row or column can be deleted."), _("Naked pairs: all rows, columns and boxes must contain exactly one of each of the digits; if a pair of cells contain the only copies of two digits in a row, column or box, those digits must be the answers for the two cells, and any other occurrences of those digits in the corresponding row, column or box can be deleted."), _("Naked triples: all rows, columns and boxes must contain exactly one of each of the digits; if three cells contain the only copies of three digits in a row, column or box, those digits must be the answers for the three cells, and any other occurrences of those digits in the corresponding row, column or box can be deleted."), _("Hidden pairs: all rows, columns and boxes must contain exactly one of each of the digits; if a pair of cells contain the only copies of two digits in a row, column or box, those digits must be the answers for the two cells, and any other digits in those cells can be deleted."), _("Hidden triples: all rows, columns and boxes must contain exactly one of each of the digits; if three cells contain the only copies of three digits in a row, column or box, those digits must be the answers for the three cells, and any other digits in those cells can be deleted."), _("Naked quads: all rows, columns and boxes must contain exactly one of each of the digits; if four cells contain the only copies of four digits in a row, column or box, those digits must be the answers for the four cells, and any other occurrences of those digits in the corresponding row, column or box can be deleted."), _("X wing: all rows and columns must contain exactly one of each of the digits; if cells in two rows (or columns) contain the only copies of one digit in those two rows (or columns) and those four cells form a rectangle, then that digit is the answer for two of those cells, one in each of the intersecting columns (or rows), and other occurrences of that digit in the two columns (or rows) can be deleted."), _("Known answer!")]

    def make_tooltip(self):
        self.tooltip_text = [
            _('Left-mouse random puzzle, right-mouse next puzzle'),
            _('Left mouse: give hint, right mouse: simple filter'),
            _('Check for errors, correct errors'),
            _('Show solution'),
            ]

    def make_help(self):
        self.help_text = _("The program starts with a randomly selected puzzle displayed. The 9 possible candidate answers are written in the unsolved squares. These numbers are buttons: left-clicking will delete a candidate, middle-clicking on its position will rewrite it, right-clicking will set it as the answer and delete any other candidates present. In the toolbar the jigsaw symbol contains a menu, the next symbol starts a new game; the wand requests a hint, a second click on the wand will perform the hint operation; the thumb checks if the puzzle contains any errors and a second click will clear the errors; the sad smiley shows the solution; the entrybox shows the current puzzle number and can be used to select a new puzzle number; the number next to the puzzle number is the puzzle difficulty score which depends on the algorithms required to find its solution. The clock ticks every 5 seconds but will be replaced by the algorithm number if a hint is requested. If the player right-clicks on the hint button, the program will repeatedly apply simple filter until it has no effect, clearing the candidates as it goes. Otherwise, a left click on the hint button will request a single hint. While the hint is active (shown by the wand icon leaning backwards) clicking on the wand with the middle mouse button will cause a pop-up to appear containing a short explanation of the hint. Hints are shown by highlighting the candidates that can be deleted in red and the reasons in green. A further left click will delete the red candidates and unhighlight the green candidates. The program contains 1000 puzzles.")

    def load_menus(self):
        self.menu_items.append(MenuItem(
            _('Enter New Puzzle'),
            '',
            '',
            '',
            'enter_new_puzzle'
        ))
        self.menu_items.append(MenuItem(
            _('Load Puzzle'),
            '',
            '',
            '',
            'read_puzzle'
        ))
        self.menu_items.append(MenuItem(
            _('Save Puzzle'),
            [_('With candidates'),
             _('Without candidates')],
            'N',
            ['with_candidates', 'without_candidates'],
            'save_puzzle'
        ))
        self.menu_items.append(MenuItem(
            _('Check Puzzle'),
            '',
            '',
            '',
            'check_puzzle'
        ))
        self.menu_items.append(MenuItem(
            _('Hint Mode'),
            [_('Simplest'), _('Most Effective')],
            '',
            ['simplest_hint', 'most_effective_hint'],
            'hint_mode'
        ))
        self.menu_items.append(MenuItem(
            _('Font Size'),
            ['1',
             '2',
             '3',
             '4',
             '5',
             '6',
             '7',
             '8',
             '9',
             '10',
             '11',
             '12',
             '13'],
            '',
            ['1',
             '2',
             '3',
             '4',
             '5',
             '6',
             '7',
             '8',
             '9',
             '10',
             '11',
             '12',
             '13'],
            'font_size'
            ))
        self.menu_items.append(MenuItem(
            _('Set Colour'),
            [_('Even boxes'),
             _('Odd boxes'),
             _('Button'),
             _('Cause'),
             _('Effect'),
             _('Error'),
             _('Text')],
            'N',
            ['even_box_colour',
             'odd_box_colour',
             'button_colour',
             'cause_colour',
             'effect_colour',
             'error_colour',
             'text_colour'],
            'set_colour'
        ))
        self.menu_items.append(MenuItem(
            _('Save Settings'),
            '',
            '',
            '',
            'save_settings'
        ))
        self.menu_items.append(MenuItem(
            _('Help'),
            '',
            '',
            '',
            'help'
        ))
        self.menu_items.append(MenuItem(
            _('About'),
            '',
            '',
            '',
            'about'
        ))


class EnterPuzzle(tk.Toplevel):
    """
    Get data from widget in self.lines
    An empty self.lines signals the caller that we've given up
    """
    def __init__(self, incoming_lines, incoming_message):
        self.my_edwin = None
        self.incoming = False
        self.lines = []
        self.message_frame = None
        self.message = 'Please edit data in the white box'
        if incoming_message is not None:
            self.message = incoming_message
        if incoming_lines is not None:
            self.incoming = True
            self.incoming_lines = incoming_lines
        self.expose_ourselves()

    def expose_ourselves(self):
        """
        expose the window
        """
        tk.Toplevel.__init__(self)
        self.title('Enter puzzle')
        self.resizable(False, False)
        base_frame = tk.Frame(self)
        base_frame.grid(row=0, column=0)
        data_frame = tk.Frame(base_frame)
        data_frame.grid(row=0, column=0)
        edwin = tk.Text(data_frame, width=81, height=12, background='white')
        edwin.grid(row=0, column=0)
        self.my_edwin = edwin

        if self.incoming:
            text = self.make_edits(self.incoming_lines)
            self.my_edwin.insert(tk.END, text)
            bg = 'red'
            self.message_frame = tk.Label(data_frame, text=self.message,
                                          bd=2,
                                          bg=bg)
            self.message_frame.grid(row=1, column=0, sticky='EW')
            ok_frame = tk.Frame(data_frame)
            ok_frame.grid(row=2, column=0)

            tk.Button(ok_frame, text='LOAD',
                      command=self.get_data).grid(row=0, column=0)
            tk.Button(ok_frame, text='QUIT',
                      command=self.destroy).grid(row=0, column=1)

        else:
            bg = 'green'
            self.message_frame = tk.Label(data_frame, text=self.message,
                                          bd=2, bg=bg)
            self.message_frame.grid(row=1, column=0, sticky='EW')
            ok_frame = tk.Frame(data_frame)
            ok_frame.grid(row=2, column=0)

            tk.Button(ok_frame, text='LOAD',
                      command=self.get_data).grid(row=0, column=0)
            tk.Button(ok_frame, text='QUIT',
                      command=self.my_destroy).grid(row=0, column=1)

        self.grab_set()
        self.focus_set()
        self.wait_window()

    def make_edits(self, input_lines):
        tmp = ''
        for i in range(len(input_lines)):
            tmp += input_lines[i]
            tmp += '\n'
        return tmp

    def get_data(self):
        content = self.my_edwin.get(1.0, tk.END)
        content = content[:-1]
        strings = content.splitlines()
        lines = []
        for line in strings:
            lines.append(str(line))
        self.lines = lines
        self.destroy()

    def my_destroy(self):
        """
        empty self.lines so caller knows we've given up
        """
        self.lines = []
        self.destroy()


class Board(tk.Frame):
    def __init__(self, parent, settings, datum):
        self.settings = settings
        self.datum = datum
        self.grid_widgets = [None] * 81
        bg = self.settings.black
        tk.Frame.__init__(self, parent, relief='flat',
                          highlightbackground=self.settings.grey,
                          highlightthickness=2,
                          bd=2, bg=bg)
        square_index = 0
        grid_size = 9
        for row in range(grid_size):
            for column in range(grid_size):
                bg_colour = self.get_square_colour(square_index)
                widget = Square(self, self.settings,
                                self.datum[square_index],
                                bg_colour, square_index, row, column)
                self.grid_widgets[square_index] = widget
                square_index += 1
        self.grid(row=0, column=0)

    def my_destroy(self):
        self.destroy()

    def add_candidate_callback(self, func):
        for widget in self.grid_widgets:
            candidates = widget.candidates
            for candidate in candidates:
                candidate.callback = func

    def get_square_colour(self, square_index):
        coloureds = [3, 4, 5]
        column = square_index % 9
        row = int(square_index / 9)
        bg_colour = self.settings.dic['odd_box_colour']
        if column in coloureds and row not in coloureds:
            bg_colour = self.settings.dic['even_box_colour']
        elif row in coloureds and column not in coloureds:
            bg_colour = self.settings.dic['even_box_colour']
        return bg_colour

    def update_square(self, update):
        """
        set some colours
        """
        operation = update.operation
        cell_id = update.cell_id
        widget = self.grid_widgets[cell_id]
        bg_colour = self.get_square_colour(cell_id)
        update.colour = bg_colour
        if operation == Egg.COLOUR_CANDIDATES_EFFECT:
            # colour effect candidates
            bg_colour = self.settings.dic['effect_colour']
            update.colour = bg_colour
        elif operation == Egg.COLOUR_CANDIDATES_CAUSE:
            # colour cause candidates
            bg_colour = self.settings.dic['cause_colour']
            update.colour = bg_colour
        elif operation == Egg.HIGHLIGHT_ERROR:
            bg_colour = self.settings.dic['error_colour']
            update.colour = bg_colour
        widget.my_update(update)


class View(ViewB):
    """
    Stick blobs on the screen and respond to callbacks
    """

    def __init__(self, master, settings):
        ViewB.__init__(self, master, settings)
        self.datum = None
        self.image_dic = {}
        self.make_image_dic()
        self.image_dic['start'] = [self.images.start_sudoku, 0, True]
        self.do_layout(self)
        self.entry_box.activate()

    def make_board(self):
        """
        draw the squares
        """
        if self.board is not None:
            self.board.my_destroy()
        self.board = Board(self.top_frame, self.settings, self.datum)

    def update_squares(self, updates):
        for update in updates:
            self.board.update_square(update)

    def add_candidate_callback(self, func):
        self.board.add_candidate_callback(func)

    def sg_open_file(self, write, title):
        """
        Use gui to open and file for reading or writing
        """
        self.entry_box.deactivate()
        init_dir = PzlG.app_saves
        filetypes = [('All files', '*')]
        this_file = OpenFileI(self, init_dir, filetypes, write, title)
        self.entry_box.activate()
        return this_file.my_file

    def update_clock_box(self, algorithm):
        names = ['SF', 'H1', 'L1', 'L2', 'N2', 'N3',
                 'H2', 'H3', 'N4', 'XW', '**']
        value = '  '
        if algorithm is not None:
            value = names[algorithm]
        self.clock.box.set_text(value)


class ReadPuzzle(object):
    """
    Read in a puzzle
    """
    def __init__(self, my_file):
        self.lines = []
        if my_file:
            self.get_data_from_file(my_file)

    def get_data_from_file(self, my_file):
        lines = my_file.read()
        self.lines = lines.splitlines()


class ParsePuzzle(object):
    """
    Parse a puzzle read from widget or file
    """
    def __init__(self, lines):
        self.lines = lines
        self.format = False
        self.bad_line_no = -1
        self.status = False
        self.puzzle_values = []
        self.message = ''
        self.parse_it()
        if not self.status:
            self.message = 'Error on line ' + str(self.bad_line_no + 1)

    def guess_data_format(self):
        """
        Crude guesswork
        """
        max_chars_per_line = 81
        min_chars_per_line = 9
        max_line = 0
        for line in self.lines:
            line_len = len(line)
            good_stuff_this_line = 0
            for item in range(line_len):
                tmp = line[item]
                if tmp == '.' or tmp.isdigit():
                    good_stuff_this_line += 1
            if good_stuff_this_line > max_line:
                max_line = good_stuff_this_line

        if max_line >= min_chars_per_line:
            # a grid on 1 line or a puzzle with answers and candidates
            if max_line == min_chars_per_line:
                # simple format
                self.format = 1
            elif max_line == max_chars_per_line:
                # Royle format
                self.format = 2
            else:
                # lines of intermediate length so assume candidates included
                self.format = 3

    def parse_data1a(self, min_chars_per_line):
        """
        Simple format plus all the ones with unnecessary crap
        """

        square_index = 0
        self.puzzle_values = []
        line_num = 0
        for line in self.lines:
            good_data_this_line = []
            for item in range(len(line)):
                tmp = line[item]
                good = False
                if tmp == '.':
                    tmp = 0
                    good = True
                elif tmp.isdigit():
                    good = True
                if good:
                    good_data_this_line.append(int(tmp))
            # if we got enough good chars, keep them, otherwise discard:
            len_data = len(good_data_this_line)
            if len_data >= min_chars_per_line:
                for item in range(min(len_data, 9)):
                    self.puzzle_values.append(good_data_this_line[item])
                    square_index += 1
                if len_data < 9:
                    self.bad_line_no = line_num
            if square_index >= 81:
                break
            line_num += 1
        if square_index == 81:
            self.status = True
            lol = [[a] for a in self.puzzle_values]
            self.puzzle_values = lol

    def parse_data1(self):
        """
        Simple format plus all the ones with unnecessary crap
        """

        min_chars_per_line = 9
        self.parse_data1a(min_chars_per_line)
        if not self.status:
            min_chars_per_line = 5
            self.parse_data1a(min_chars_per_line)

    def parse_data2(self):
        """
        Gordon Royle format - ie all data on 1 line
        here we just take the first line with 81 good chars
        """

        # keep reading til we find a line with enough data
        # - ie get first possibility

        for line in self.lines:
            good_data_this_line = []
            for item in range(len(line)):
                tmp = line[item]
                good = False
                if tmp == '.':
                    tmp = 0
                    good = True
                elif tmp.isdigit():
                    good = True
                if good:
                    good_data_this_line.append(int(tmp))

            # if we got 81 good chars, keep them, otherwise discard:
            len_data = len(good_data_this_line)
            if len_data == 81:
                for item in range(81):
                    self.puzzle_values.append(good_data_this_line[item])
                lol = [[a] for a in self.puzzle_values]
                self.puzzle_values = lol
                self.status = True
                break
            if not self.status:
                self.bad_line_no = 0

    def parse_data3(self):
        """ format which includes candidates
        parse in 2 stages:
        filter out the crap
        parse the line into clumps of numbers for each cell
        """
        result = []
        good_data = []
        for line in self.lines:
            good_stuff_this_line = ''
            in_spaces = True
            for item in range(len(line)):
                tmp = line[item]
                if tmp == '.' or tmp.isdigit():
                    good_stuff_this_line += tmp
                    in_spaces = False
                elif tmp == ' ':
                    if not in_spaces:
                        good_stuff_this_line += tmp
                        in_spaces = True
            if len(good_stuff_this_line) >= 9:
                good_stuff_this_line += ' '
                good_data.append(good_stuff_this_line)

        num_lines = len(good_data)

        if num_lines == 9:
            line_num = 0
            for line in good_data:
                blocks = line.split()
                # must have 9 good blocks
                if len(blocks) != 9:
                    self.bad_line_no = line_num
                    break
                for block in blocks:
                    int_items = []
                    for item in block:
                        int_items.append(int(item))
                    result.append(int_items)
                line_num += 1
            if line_num == 9:
                # ie we didn't bail out
                self.puzzle_values = result
                self.status = True

    def parse_it(self):
        """
        Do it all setting values and status
        """
        if self.lines:
            self.guess_data_format()
            if self.format == 1:
                self.parse_data1()
            elif self.format == 2:
                self.parse_data2()
            elif self.format == 3:
                self.parse_data3()


class Datum(object):
    """
    Data item for a square
    """
    def __init__(self, my_index):
        self.candidates = set(range(9))
        self.given = False
        self.symbol_index = None
        self.guess = None
        self.index = my_index

    def dup(self):
        my_copy = copy.copy(self)
        my_copy.candidates = set(list((self.candidates)))
        return my_copy

    def add_symbol_index(self, symbol_index):
        self.symbol_index = symbol_index

    def reset(self):
        self.guess = None
        self.candidates = set(range(9))
        self.given = False

    def load(self, candidates):
        """
        Given a set of candidates, initialise a datum.
        """

        if len(candidates) == 1:
            # must be a given answer or completely unknown
            symbol_index = candidates[0] - 1
            if symbol_index != -1:
                self.initialise(symbol_index, True)
            else:
                self.candidates = set(range(9))

            # if -1, it is completely unknown so we let the
            # original initialisation stand, and do nothing here
        else:
            my_candidates = [a-1 for a in candidates]
            self.candidates = set(my_candidates)

    def initialise(self, symbol_index, given):
        self.symbol_index = symbol_index
        if given:
            self.given = True
            self.candidates = set()
            self.guess = symbol_index

    def set_guess(self, symbol_index, given):
        self.guess = symbol_index
        if given:
            self.given = True
        self.candidates = set()

    def unset_guess(self):
        self.guess = None

    def get_guess(self):
        return self.guess

    def guess_is_set(self):
        ret = True
        if self.guess is None:
            ret = False
        return ret

    def get_answer(self):
        answer = self.guess
        if answer is not None:
            answer += 1
        else:
            answer = 0
        return answer

    def get_candidates(self):
        return self.candidates

    def del_candidate(self, candidate):
        """
        return guess if deleting last candidate
        """
        ret = -1
        if not self.given:
            if candidate in self.candidates:
                self.candidates.remove(candidate)
            if self.guess is None:
                if len(self.candidates) == 1:
                    candidate = self.candidates.pop()
                    self.guess = candidate
                    ret = candidate
        return ret

    def add_candidate(self, candidate):
        ret = -1
        if not self.given:
            self.candidates.add(candidate)
            if self.guess is not None:
                ret = self.guess
                # next line 2017-06-13
                self.candidates.add(ret)
                self.guess = None
        return ret

    def other_candidates(self, candidate):
        return self.candidates.difference(candidate)

    def candidate_is_set(self, candidate):
        ret = False
        if candidate in self.candidates:
            ret = True
        return ret

    def is_consistent(self):
        """
        guess unset but no candidates left: bit 0
        guess set and candidate(s) True: bit 1
        guess != symbol_index: Error bit 2
        """
        error = 0
        if self.guess is None:
            if self.symbol_index is not None and \
                    self.symbol_index not in self.candidates:
                error = 1
        else:
            if self.candidates:
                error = 2

            if self.symbol_index is not None and \
                    self.symbol_index != self.guess:
                # guess != symbol_index
                error += 4
        return error

    def candidate_count(self):
        return len(self.candidates)


class Model(SudokuModel):

    def __init__(self, model_data):
        SudokuModel.__init__(self, model_data)
        self.create_indexes()
        self.algorithms = [(self.hidden_singles, 0),
                           (self.locked_candidate_1, 2),
                           (self.locked_candidate_2, 2),
                           (self.naked_pairs, 0),
                           (self.naked_triples, 0),
                           (self.hidden_pairs, 0),
                           (self.hidden_triples, 0),
                           (self.naked_quads, 0),
                           (self.x_wing, 1),
                           (self.random_answer, 2)]

    def init_datum(self):
        self.datum = []
        for datum_index in range(81):
            self.datum.append(Datum(datum_index))

    def difficulty(self):
        scores = [0, 1, 2, 2, 3, 3, 6, 6, 6, 9]
        if self.algorithm_counts[-1] != 0:
            self.score = 999
        else:
            self.score = 0
            total = 0
            for algorithm in range(len(self.algorithm_counts)-1):
                count = self.algorithm_counts[algorithm]
                score = scores[algorithm]
                total += count * score
            self.score = total

    def init_external_puzzle(self, puzzle_values, answers):
        self.puzzle_number = -1
        self.load_puzzle(puzzle_values)
        for datum_index in range(81):
            self.datum[datum_index].initialise(answers[datum_index], False)

    def next_guess_datum(self):
        """
        Find the datum with fewest remaining candidates
        """

        lowest = 10
        next_one = None
        for datum in self.datum:
            if not datum.given and datum.guess is None:
                candidates = len(datum.candidates)
                if candidates < lowest:
                    lowest = candidates
                    next_one = datum
        if next_one is not None:
            ret = next_one.dup()
        return ret

    def alternative_guess(self):
        """
        Previous guess led to inconsistency so we need to try
        a different candidate for the current square or for a
        previous square.
        """

        ret = False
        previous_snapshots = False
        while self.snapshots:
            # already made at least one guess
            # so see if there are more candidates for the current square
            previous_snapshots = True
            snapshot = self.snapshots[-1]
            guess_datum = snapshot[1]
            tried = snapshot[2]
            candidates = guess_datum.candidates
            if len(tried) == len(candidates):
                # all tried already, so try an earlier snapshot
                self.snapshots.pop()
            else:
                # still untried candidates
                for candidate in candidates:
                    if candidate not in tried:
                        tried.append(candidate)
                        ret = True
                        break
            if ret:
                break
        if ret:
            # prepare for next trial solution
            ret = self.handle_snapshots(False)
        else:
            # either this is a first guess or there's a serious
            # problem with the puzzle
            if not previous_snapshots:
                # OK: it's a first guess
                # prepare for next trial solution
                ret = self.handle_snapshots(True)
#                ret = True
            else:
                # bad puzzle
                ret = False
        return ret

    def handle_snapshots(self, make):
        """
        When solving an external puzzle we may need to guess.
        If so, we first copy the current datums so we can recover
        the state if we guess wrong.
        We also save the datum for which we are going to guess
        and a list of guesses tried. This allows us to try each
        candidate in turn and, if none succeed, to know we have to
        backtrack to a previous snapshot.
        """
        ret = True
        if make:
            # save a snapshot
            saved_datums = []
            for datum in self.datum:
                new_datum = datum.dup()
                saved_datums.append(new_datum)
            guess_datum = self.next_guess_datum()
            if guess_datum is not None:
                # choose a candidate
                for candidate in range(9):
                    if candidate in guess_datum.candidates:
                        break
                tried = [candidate]
                snapshot = [saved_datums, guess_datum, tried]
                self.snapshots.append(snapshot)
                self.set_guess(self.datum[guess_datum.index], candidate, False)
            else:
                ret = False
        else:
            # retrieve a snapshot
            snapshot = self.snapshots[-1]
            saved_datums = snapshot[0]
            guess_datum = snapshot[1]
            tried = snapshot[2]
            candidate = tried[-1]
            self.datum = []
            for datum in saved_datums:
                new_datum = datum.dup()
                self.datum.append(new_datum)
            self.set_guess(self.datum[guess_datum.index], candidate, False)
        return ret

    def solve_puzzle(self):
        self.zero_algorithm_counts()
        solved = False
        answers = []
        self.check_errors()
        if self.status:
            self.editor.reset()
            self.editor.set_mode(Sgg.SOLVE)
            self.snapshots = []
            guessing_required = False
            while True:
                self.apply_all_algs()
                self.check_errors()
                if not self.status:
                    # at some point we made a bad guess
                    if not self.alternative_guess():
                        # there is no guess, so puzzle
                        # has a serious problem and we give up
                        break
                else:
                    if self.is_finished():
                        solved = True
                        break
                    else:
                        # need to make another guess
                        self.handle_snapshots(True)
                        guessing_required = True
            if solved:
                if guessing_required:
                    self.algorithm_counts[-1] = 1
                for i in range(81):
                    answers.append(self.datum[i].guess)

        return solved, answers

    def get_puzzle(self, puzzle_number):
        self.init_puzzle(puzzle_number)
        self.editor.set_mode(Sgg.HINT)

    def save_puzzle(self, my_file, my_format):
        if my_format == 'without_candidates':
            datum_index = 0
            for _ in range(9):
                record = ''
                for column in range(9):
                    answer = self.datum[datum_index].get_answer()
                    record += str(answer)
                    datum_index += 1
                record += '\n'
                my_file.write(record)
            my_file.close()
        elif my_format == 'with_candidates':
            datum_index = 0
            for row in range(9):
                record = ''
                for column in range(9):
                    answer = self.datum[datum_index].get_answer()
                    if answer == 0:
                        candidates = self.datum[datum_index].get_candidates()
                        for candidate in candidates:
                            record += str(candidate+1)
                    else:
                        record += str(answer)
                    record += ' '
                    datum_index += 1
                record += '\n'
                my_file.write(record)
            my_file.close()

    def load_puzzle(self, puzzle_values):
        """
        Load the entered puzzle values into datums
        """
        self.init_datum()
        for datum_index in range(81):
            self.datum[datum_index].load(puzzle_values[datum_index])

    def read_puzzle(self, my_file):
        """
        Read a puzzle from a file.
        Try to parse it. If fail stick it in an editor.
        Try to solve it. If fail stick it in an editor.
        Return the read in values and the answer.
        """
        answers = []
        puzzle = ReadPuzzle(my_file)
        solved = False
        if puzzle.lines:
            parsed = ParsePuzzle(puzzle.lines)
            if parsed.status:
                self.load_puzzle(parsed.puzzle_values)
                solved, answers = self.solve_puzzle()
                if not solved:
                    parsed.message = 'Unable to solve puzzle'
                    parsed.status = False
            if not parsed.status:
                puzzle_lines = puzzle.lines
                puzzle_message = parsed.message
                while True:
                    puzzle = EnterPuzzle(puzzle_lines, puzzle_message)
                    if puzzle.lines:
                        parsed = ParsePuzzle(puzzle.lines)
                        if parsed.status:
                            self.load_puzzle(parsed.puzzle_values)
                            solved, answers = self.solve_puzzle()
                            if not solved:
                                parsed.message = 'Unable to solve puzzle'
                                parsed.status = False
                        if not parsed.status:
                            puzzle_lines = puzzle.lines
                            puzzle_message = parsed.message
                        else:
                            break
                    else:
                        break
        return solved, parsed.puzzle_values, answers

    def enter_puzzle(self):
        """
        Read a puzzle from an editor.
        Try to parse it. If fail stick it back in the editor.
        Try to solve it. If fail stick it back in the editor.
        Return the read in values and the answer.
        """
        puzzle_lines = None
        puzzle_message = None
        puzzle_values = None
        answers = None
        solved = False
        while True:
            puzzle = EnterPuzzle(puzzle_lines, puzzle_message)
            if puzzle.lines:
                parsed = ParsePuzzle(puzzle.lines)
                if parsed.status:
                    puzzle_values = parsed.puzzle_values
                    self.load_puzzle(puzzle_values)
                    solved, answers = self.solve_puzzle()
                    if not solved:
                        parsed.message = 'Unable to solve puzzle'
                        parsed.status = False
                if not parsed.status:
                    puzzle_lines = puzzle.lines
                    puzzle_message = parsed.message
                else:
                    break
            else:
                break
        return solved, puzzle_values, answers

    def handle_copies(self, backup):
        datums = []
        if backup:
            datums = backup
            self.datum = []
            for datum in datums:
                new_datum = datum.dup()
                self.datum.append(new_datum)
        else:
            for datum in self.datum:
                new_datum = datum.dup()
                datums.append(new_datum)
        return datums

    def check_uniqueness(self):
        """
        Check the puzzle for uniqueness - ie having a single solution.
        Solve using logic alone.
         If solved we are done.
         else
          Make a list of unsolved datums and remaining candidates
          Try each in turn allowing guessing.
          If we get more than 1 solution, uniquess failed.
          Else OK
        """
        # save start state
        symbol_index = []
        state_in = self.handle_copies([])
        self.unsolved = []
        self.solve_no_guessing()
        if not self.status:
            # bad puzzle: no solution found
            ret = False
        else:
            if not self.unsolved:
                # no need to check further
                ret = True
            else:
                """
                Need to make a copy of the current state of the puzzle
                and then reinstate it for each pair.
                First we make a copy of the current answers
                and set them to None in datum so that the consistency
                checker does not flag them for disagrements between
                answer and guess! Reinstate them afterwards.
                As we find solutions we replace them in state_no_guess.
                """
                for datum in self.datum:
                    symbol_index.append(datum.symbol_index)
                    datum.symbol_index = None
                state_no_guess = self.handle_copies([])
                # solve by starting with all the datums and candidates
                # in self.unsolved (allowing guessing)
                ret = True
                solution = []
                for pair in self.unsolved:
                    successful_datum = None
                    datum_index = pair[0]
                    candidates = pair[1]
                    for candidate in candidates:
                        self.handle_copies(state_no_guess)
                        self.set_guess(self.datum[datum_index],
                                       candidate, False)
                        self.datum[datum_index].symbol_index = None
                        solved, answers = self.solve_puzzle()
                        ret = True
                        if solved:
                            if solution:
                                # save the result for this datum
                                successful_datum = self.datum[datum_index].dup()
                                solution_index = 0
                                for answer in answers:
                                    if answer != solution[solution_index]:
                                        # different answer: not unique
                                        ret = False
                                        break
                                    solution_index += 1
                                if not ret:
                                    solution_index = 0
                                    for answer in answers:
                                        if answer != solution[solution_index]:
                                            # different answer: not unique
                                            pass
                                        solution_index += 1
                            else:
                                solution = copy.copy(answers)
                        if not ret:
                            break
                    if ret is False:
                        break
                    if successful_datum is not None:
                        # replace this datum now we know it is good and
                        # we have tried all candidates
                        state_no_guess[datum_index] = successful_datum

            if symbol_index:
                # replace symbol_indexes
                for symbol_i in range(len(symbol_index)):
                    self.datum[symbol_i].symbol_index = symbol_index[symbol_i]
        self.handle_copies(state_in)
        return ret

    def get_progress(self):
        """
        Get a list of the unsolved datum and their candidates.
        """
        undone = 0
        for datum in self.datum:
            if not datum.guess_is_set():
                undone += len(datum.candidates)
        return undone

    def get_unsolved_data(self):
        """
        Get a list of the unsolved datum and their candidates.
        """
        self.unsolved = []
        for datum in self.datum:
            if not datum.guess_is_set():
                pair = datum.index, datum.candidates
                self.unsolved.append(pair)


class Controller(object):
    """
    Set up and control
    """

    def __init__(self, root):
        self.settings = Settings()
        self.master = root
        self.model_data = ModelData()
        self.flasher = None
        self.model = None
        self.error_detected = False
        self.view = View(root, self.settings)
        self.coloured_squares = []
        screen_shot_state = False
        self.active = False
        self.screen_shot = ScreenShot(PzlG.app_name,
                                      'dump_', screen_shot_state)
        self.start_game(None)

    def start_game(self, puzzle_number):
        self.view.clock_init()
        self.view.clock_restart()
        self.view.game_over()
        self.model = Model(self.model_data)
        self.model.get_puzzle(puzzle_number)
        self.ready_for_hint()
        self.view.thumbs_up()
        self.view.datum = self.model.datum
        self.view.make_board()
        self.add_callbacks()
        self.view.game_on()
        self.error_detected = False
        self.view.entry_box.clear()
        self.view.entry_box.write_box(str(self.model.puzzle_number))
        self.view.update_score_box(self.model.score)
        self.active = True
        self.flasher = Flasher(self.view.clock.box.label,
                               self.master, self.settings)

    def start_external_game(self, puzzle_values, answers):
        self.view.clock_init()
        self.view.clock_restart()
        self.model.init_external_puzzle(puzzle_values, answers)
        strargs = ''
        for items in puzzle_values:
            for item in items:
                strargs += str(item)
            strargs += ' '
        PzlG.logger.logger.info("puzzle_data " + strargs)
        self.ready_for_hint()
        self.view.thumbs_up()
        self.view.datum = self.model.datum
        self.view.make_board()
        self.add_callbacks()
        self.view.game_on()
        self.error_detected = False
        self.view.config(cursor="arrow")
        self.view.update()
        self.view.entry_box.clear()
        self.view.entry_box.write_box(str(self.model.puzzle_number))
        self.view.update_score_box(self.model.score)
        self.active = True
        self.flasher = Flasher(self.view.clock.box.label,
                               self.master, self.settings)

    def start_unique_game(self):
        self.view.clock_init()
        self.view.clock_restart()
        self.ready_for_hint()
        self.view.thumbs_up()
        self.view.datum = self.model.datum
        self.view.make_board()
        self.add_callbacks()
        self.view.game_on()
        self.error_detected = False
        self.view.config(cursor="arrow")
        self.view.update()
        self.view.entry_box.clear()
        self.view.entry_box.write_box(str(self.model.puzzle_number))
        self.view.update_score_box(self.model.score)
        self.flasher = Flasher(self.view.clock.box.label,
                               self.master, self.settings)

    def add_callbacks(self):
        self.view.add_menu_callback(self.service_menu)
        self.view.add_toolbar_callback(self.service_toolbar)
        self.view.add_candidate_callback(self.square_event)
        self.view.entry_box.add_callback(self.get_puzzle_number)

    def service_menu(self, *args):
        """
        Respond to menu choices
        One of the three event servicing functions.
        """
        strargs = ''
        for item in args:
            strargs += ' ' + item
        PzlG.logger.logger.info("service_menu " + strargs)
        name = args[0]
        if name == 'hint_mode':
            choice = args[1]
            self.set_hint_mode(choice)
        elif name == 'read_puzzle':
            self.read_puzzle()
        elif name == 'enter_new_puzzle':
            self.enter_puzzle()
        elif name == 'save_puzzle':
            choice = args[1]
            self.save_puzzle(choice)
        elif name == 'check_puzzle':
            self.check_uniqueness()
        elif name == 'font_size':
            self.set_font_size()
        elif name == 'set_colour':
            choice = args[1]
            self.set_colour(choice)
        elif name == 'save_settings':
            self.settings.preferences.save()
        elif name == 'about':
            self.view.about()
        elif name == 'help':
            self.view.help()

    def service_toolbar(self, button_index, mouse_index):
        """
        Respond to toolbar clicks
        One of the three event servicing functions.
        """
        PzlG.logger.logger.info("service_toolbar " +
                                str(button_index) +
                                ' ' +
                                str(mouse_index))

        if mouse_index == 2:
            alg_index = self.model.editor.algorithm
            self.view.do_tooltip(button_index, alg_index)
        elif mouse_index == 3:
            if button_index == 1:
                if not self.model.is_finished():
                    if self.model.get_hint_status() != Sgg.APPLY_HINT:
                        if button_index == 1:
                            self.exhaust()
            elif button_index == 0:
                self.model.puzzle_number += 1
                self.start_game(self.model.puzzle_number)
        elif mouse_index == 1:
            if button_index == 0:
                self.start_game(None)
            elif button_index == 1:
                if not self.model.is_finished():
                    self.give_hint()
                    self.screen_shot.snap()
            elif button_index == 2:
                if not self.model.is_finished():
                    self.check_errors()
            elif button_index == 3:
                if not self.model.is_finished():
                    self.show_solution()
                    self.screen_shot.snap()

    def square_event(self, datum_index, candidate, mouse_index):
        """
        Respond to board clicks
        One of the three event servicing functions.
        """
        if self.active:
            PzlG.logger.logger.info("square_event " +
                                    str(mouse_index) +
                                    ' ' +
                                    str(datum_index) +
                                    ' ' +
                                    str(candidate))
            # if necessary abort an active hint
            board_updates = self.model.editor.abort_ops
            self.view.update_squares(board_updates)
            self.model.editor.reset()
            self.model.set_hint_status(Sgg.GIVE_HINT)
            self.model.apply_user_edits(datum_index,
                                        candidate,
                                        mouse_index)

            board_updates = self.model.user_edits.apply_ops
            self.view.update_squares(board_updates)
            self.check_finished()

    def ready_for_hint(self):
        if not self.model.is_finished():
            self.model.editor.reset()
            self.model.set_hint_status(Sgg.GIVE_HINT)
            if self.model.status:
                self.view.hint_off()
                self.view.thumbs_up()
                self.error_detected = False
            # turn on the clock
            self.view.clock.set_show(True)

    def give_hint(self):
        if self.model.get_hint_status() == Sgg.GIVE_HINT:
            self.model.do_hint()
            if not self.model.status:
                self.view.thumbs_down()
                self.colour_errors()
                self.error_detected = True
            elif self.model.progress:
                self.view.hint_on()
                board_updates = self.model.editor.show_ops
                self.view.update_squares(board_updates)
            self.view.update_clock_box(self.model.editor.algorithm)
            self.view.clock.set_show(False)
            self.model.set_hint_status(Sgg.APPLY_HINT)
        else:
            self.model.apply_hint()
            board_updates = self.model.editor.apply_ops
            self.view.update_squares(board_updates)
            self.ready_for_hint()
            self.check_finished()

    def check_errors(self):
        if self.error_detected:
            # may have deleted edits
            if self.model.user_edits.edits:
                # we know there's an error already, so now we correct it
                self.uncolour_errors()
                self.model.user_edits.undo()
                board_updates = self.model.user_edits.undo_ops
                self.view.update_squares(board_updates)
            self.model.user_edits.reset()
            self.model.check_errors()
            if not self.model.status:
                PzlG.logger.logger.error("check_errors ")
                exit(1)
            self.ready_for_hint()
        else:
            self.model.check_errors()
            if not self.model.status:
                self.view.thumbs_down()
                self.error_detected = True
                self.colour_errors()

    def check_finished(self):
        """
        Check if finished.
        If so, handle buttons, flasher and clock
        """
        if self.active:
            if self.model.is_finished():
                # next line added 2017-06-13
                self.show_solution()
                self.screen_shot.snap()
                self.flasher.flash()

    def show_solution(self):
        self.model.show_solution()
        board_updates = self.model.get_board_updates()
        self.view.update_squares(board_updates)
        self.view.hint_off()
        self.view.game_over()
        self.view.clock.set_show(True)
        self.view.clock_stop()
        self.active = False

    def exhaust(self):
        """
        Apply simple_filter_exhaustively
        """
        self.model.exhaust_puzzle()
        # now update the view
        board_updates = self.model.get_board_updates()
        self.view.update_squares(board_updates)
        self.screen_shot.snap()
        self.check_finished()

    def colour_errors(self):
        """
        Colour the error squares
        """
        self.model.get_colour_ops()
        board_updates = self.model.user_edits.show_error_ops
        self.view.update_squares(board_updates)

    def uncolour_errors(self):
        """
        Uncolour coloured squares
        """
        self.model.get_uncolour_ops()
        board_updates = self.model.user_edits.unshow_error_ops
        self.view.update_squares(board_updates)

    def set_hint_mode(self, hint_mode_name):
        if hint_mode_name == 'simplest_hint':
            Sgg.simplest_hint = True
        else:
            Sgg.simplest_hint = False

    def set_font_size(self):
        self.settings.do_scaling()
        puzzle_number = self.model.puzzle_number
        self.start_game(puzzle_number)

    def set_colour(self, choice):
        try:
            bg_colour = self.settings.dic[choice]
            result = tkColorChooser.askcolor(bg_colour)
            if result[1] is not None:
                self.settings.dic[choice] = result[1]
                self.start_game(None)
        except KeyError:
            PzlG.logger.logger.error("set_colour " + choice)

    def save_puzzle(self, choice):
        my_file = self.view.sg_open_file(True, _('Output file'))
        if my_file:
            self.model.save_puzzle(my_file, choice)

    def check_uniqueness(self):
        self.view.game_over()
        self.view.entry_box.deactivate()
        self.view.config(cursor="watch")
        self.view.update()
        ret = self.model.check_uniqueness()
        self.view.config(cursor="arrow")
        self.view.update()
        self.view.entry_box.activate()
        if ret:
            self.view.unique()
            self.start_unique_game()
        else:
            self.view.non_unique()
            self.start_game(None)

    def read_puzzle(self):
        my_file = self.view.sg_open_file(False, _('Input file'))
        if my_file:
            self.active = False
            self.view.game_over()
            self.view.entry_box.deactivate()
            self.view.config(cursor="watch")
            self.view.update()
            solved, puzzle_values, answers = \
                self.model.read_puzzle(my_file)
            self.view.config(cursor="arrow")
            self.view.update()
            self.view.entry_box.activate()
            if solved:
                self.model.difficulty()
                self.start_external_game(puzzle_values, answers)
            else:
                self.start_game(None)

    def enter_puzzle(self):
        self.active = False
        self.view.game_over()
        self.view.entry_box.deactivate()
        self.view.config(cursor="watch")
        self.view.update()
        solved, puzzle_values, answers = \
            self.model.enter_puzzle()
        self.view.config(cursor="arrow")
        self.view.update()
        self.view.entry_box.activate()
        if solved:
            self.model.difficulty()
            self.start_external_game(puzzle_values, answers)
        else:
            self.start_game(None)

    def get_puzzle_number(self, text):
        if text.find('XYZZY') != -1:
            self.view.xyzzy()
            self.view.entry_box.clear()
        else:
            try:
                puzzle_number = int(text)
                self.start_game(puzzle_number)
                self.screen_shot.snap()
            except ValueError:
                self.view.entry_box.clear()


INITIALISE = PzlG()
if __name__ == '__main__':
    try:
        ROOT = tk.Tk()
        ROOT.withdraw()
        APP = Controller(ROOT)
        ROOT.mainloop()
        PzlG.logger.logger.info("Stop program")
    except:
        PzlG.logger.logger.info("Failure!")
        PzlG.logger.logger.exception("Unhandled exception during main")
