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

# Copyright (C) 2022 Rod Engerstad

from random import randint
import tkinter as tk
import tkinter.colorchooser

from knotty_data import ModelData
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 ViewB
from pzl_lib import TButton


class Sgg():
    grid_width = None
    grid_height = None
    GIVE_HINT = 0
    APPLY_HINT = 1


class Settings(SettingsB):
    def __init__(self):
        SettingsB.__init__(self)
        self.original_square_size = 54
        self.square_size = self.original_square_size
        self.dic = {}
        self.dic['set_colour'] = 'Colour_1'
        self.dic['Colour_1'] = '#7F96FF'
        self.dic['Colour_2'] = '#ADFF7F'
        self.dic['Colour_3'] = '#7FDCFF'
        self.dic['Colour_4'] = '#FFC57F'
        self.dic['Colour_5'] = '#F3FF7F'
        self.dic['Colour_6'] = '#7FFF96'
        self.dic['Colour_7'] = '#FF7FC5'
        self.dic['Colour_8'] = '#7FFFDC'
        self.dic['Colour_9'] = '#AD7FFF'
        self.dic['text_colour'] = '#000000'
        self.dic['button_colour'] = '#82c026'
        self.dic['Error_Colour'] = '#ff0000'
        self.dic['Hint_Colour'] = '#ff0000'
        self.dic['font_size'] = '10'
        self.menu_items = []
        self.preferences = Preferences(self.dic)
        self.preferences.load()
        self.load_menus()
        self.count = len(self.menu_items)
        self.set_fonts()
        self.fiddle_fonts()
        self.make_help()
        self.make_tooltip()

    def fiddle_fonts(self):
        # fiddle the code font
        self.code_font_size = -8
        self.value_font_size = -30
        self.code_font = tk.font.nametofont('TkFixedFont')
        self.code_font.configure(size=self.code_font_size)
        self.value_font.configure(weight='bold')
        self.do_scaling()

    def make_help(self):
        self.help_text = _("The puzzle grid contains black spacer cells and coloured polyominoes. In the finished puzzle each cell contains a single letter and these combine to make intersecting horizontal and vertical words on the grid. At the start of a game every cell displays along its top edge a copy of all of the solution letters housed by its polyomino. The player must work out which letter is the solution for which cell, each letter being used only once. A left mouse click deletes a letter, middle mouse sets a letter as the solution and deletes all the others in the cell, right mouse restores ALL the letters for a cell. When a letter is set as the solution for a cell the program will automatically remove that letter from the other cells in the polyomino. In addition, if a cell is left with only a single letter the program will automatically set that letter as the solution for the cell. The puzzle icon contains a menu which allows players to set the font size and choose the colours used. The next icon to the right selects the next puzzle (left mouse randomly, right mouse the next in the list). Right again is the hint button which will colour red a letter which is not the solution for a cell. Right again, the thumb icon checks for errors. Any found are coloured red and will be reset by a futher click on the thumb. Right again, the smiley will show the solution. The box to the right shows the current puzzle number and can be used to select a different puzzle number. The game contains 600 square puzzles, 200 each of sizes 4, 5 and 6 cells across.")

    def make_tooltip(self):
        self.tooltip_text = [_('New game'), _('Give Hint/Execute Hint'),
                             _('Check for errors, correct errors'),
                             _('Show solution')]

    def load_menus(self):
        self.menu_items.append(MenuItem(
            _('Font Size'),
            [
                '5',
                '7',
                '8',
                '9',
                '10',
                '11',
                '12',
                '13'],
            '',
            [
                '5',
                '7',
                '8',
                '9',
                '10',
                '11',
                '12',
                '13'],
            'font_size'
        ))
        self.menu_items.append(MenuItem(
            _('Set Colour'),
            [_('Colour_1'),
             _('Colour_2'),
             _('Colour_3'),
             _('Colour_4'),
             _('Colour_5'),
             _('Colour_6'),
             _('Colour_7'),
             _('Colour_8'),
             _('Colour_9'),
             _('Error_Colour'),
             _('Hint_Colour')],
            'N',
            ['Colour_1',
             'Colour_2',
             'Colour_3',
             'Colour_4',
             'Colour_5',
             'Colour_6',
             'Colour_7',
             'Colour_8',
             'Colour_9',
             _('Error_Colour'),
             _('Hint_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 KSquare(tk.Frame):
    """
    Draw a box with choices and answer for Knotwords
    """
    def __init__(self, parent, settings, datum):

        def make_frame(parent, bg, width, height):
            bd = 0
            relief = 'flat'
            frame = tk.Frame(
                parent,
                bd=bd,
                relief=relief,
                highlightthickness=0,
                bg=bg,
                width=width,
                height=height)
            return frame

        self.settings = settings
        bg = self.settings.white
        tk.Frame.__init__(self, parent, relief='raised',
                          highlightbackground=self.settings.black,
                          highlightthickness=1,
                          bd=1, bg=bg)
        row = datum.key[1]
        column = datum.key[0]
        self.grid(row=row, column=column)
        font = self.settings.code_font
        size = abs(font.cget('size')) * 1.1

        width = size * 4
        height = size
        bg = datum.colour
        if datum.answer == '*':
            bg = 'black'
        top_frame = make_frame(self, bg, width, height)
        top_frame.grid(row=0, column=0)
        top_frame.grid_propagate(0)

        choices_frame = make_frame(top_frame, bg, width, height)
        choices_frame.grid(row=0, column=0)

        if datum.answer == '*':
            text = ' '
        else:
            text = ''
        fg = self.settings.dic['text_colour']
        label = tk.Label(
            self,
            bd=0,
            highlightthickness=0,
            bg=bg,
            fg=fg,
            text=text,
            font=self.settings.value_font)
        label.grid(sticky='EW')

        relief = 'raised'
        highlightthickness = 0

        if not datum.is_blank():
            bd = 0
            for col, letter in enumerate(datum.letters):
                button = TButton(datum.key,
                                 col,
                                 choices_frame,
                                 letter,
                                 font,
                                 bd,
                                 bg,
                                 fg,
                                 relief,
                                 highlightthickness)
                button.get_widget().grid(row=0, column=col)
                datum.add_widget(button)
        datum.value_widget = label
        self.update_idletasks()


class Board(tk.Frame):
    def __init__(self, parent, settings, datum_dic):
        self.settings = settings
        self.datum_dic = datum_dic
        self.widget_dic = {}
        bg = self.settings.white
        tk.Frame.__init__(self, parent, relief='flat',
                          highlightbackground=self.settings.grey,
                          highlightthickness=2,
                          bd=2, bg=bg)
        for key, datum in list(self.datum_dic.items()):
            widget = KSquare(self, self.settings,
                             datum)
            self.widget_dic[key] = widget
            datum.widget = widget
        self.grid(row=0, column=0)

    def my_destroy(self):
        self.destroy()

    def add_board_callback(self, func):
        for key, datum in list(self.datum_dic.items()):
            if not datum.is_blank():
                for choice_index, letter in enumerate(datum.letters):
                    widget = datum.get_widget(choice_index)
                    widget.callback = func


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

    def __init__(self, master, settings):
        ViewB.__init__(self, master, settings)
        self.datum_dic = None
        self.image_dic = {}
        self.make_image_dic()
        self.image_dic['start'] = [self.images.start_knotty, 0, True]
        self.error_colour = settings.dic['Error_Colour']
        self.hint_colour = settings.dic['Hint_Colour']
        self.do_layout(self)
        self.entry_box.activate()

    def update_settings(self, settings):
        self.error_colour = settings.dic['Error_Colour']
        self.hint_colour = settings.dic['Hint_Colour']

    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_dic)

    def handle_updates(self, updates):

        for update in updates:
            if update[0] == 'disable':
                key = update[1]
                choice_index = update[2]
                datum = self.datum_dic[key]
                widget = datum.get_widget(choice_index)
                widget.hide_1()
            elif update[0] == 'enable':
                key = update[1]
                choice_index = update[2]
                datum = self.datum_dic[key]
                widget = datum.get_widget(choice_index)
                widget.expose()
            elif update[0] == 'set':
                # set guess
                key = update[1]
                choice_index = update[2]
                datum = self.datum_dic[key]
                letter = datum.get_letter(choice_index)
                self.datum_dic[key].value_widget.configure(text=letter)
            elif update[0] == 'unset':
                # unset guess
                key = update[1]
                choice_index = update[2]
                datum = self.datum_dic[key]
                letter = ''
                self.datum_dic[key].value_widget.configure(text=letter)
            elif update[0] == 'set_error':
                key = update[1]
                choice_index = update[2]
                datum = self.datum_dic[key]
                widget = datum.get_widget(choice_index)
                widget.colour_fg(self.error_colour)
            elif update[0] == 'set_hint':
                key = update[1]
                choice_index = update[2]
                datum = self.datum_dic[key]
                widget = datum.get_widget(choice_index)
                widget.colour_fg(self.hint_colour)
            elif update[0] == 'unset_hint':
                key = update[1]
                choice_index = update[2]
                datum = self.datum_dic[key]
                widget = datum.get_widget(choice_index)
                colour = 'black'
                widget.colour_fg(colour)
        self.update_idletasks()

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


class Datum():
    """
    Cell item
    """
    def __init__(self, key, answer, poly_id):
        self.key = key
        self.answer = answer
        self.poly_id = poly_id
        self.letters = ''
        self.widgets = []
        self.status = []
        self.poly = None
        self.colour = ''
        self.guess = ''
        self.value_widget = None
        self.word_across = None
        self.word_down = None

    def init_choices(self, letters):
        self.letters = letters
        self.widgets = []
        self.status = []
        for item in letters:
            self.status.append(True)

    def get_letter(self, choice_index):
        return self.letters[choice_index]

    def add_widget(self, widget):
        self.widgets.append(widget)

    def get_widget(self, choice_index):
        return self.widgets[choice_index]

    def set_status(self, status_index, status):
        self.status[status_index] = status

    def last_active(self):
        ret = -1
        count = 0
        for i, status in enumerate(self.status):
            if status:
                count += 1
                ret = i

        if count != 1:
            ret = -1
        return ret

    def remaining(self):
        count = 0
        for i, status in enumerate(self.status):
            if status:
                count += 1
        return count

    def has_letter(self, letter):
        ret = False
        for i, status in enumerate(self.status):
            if status and self.letters[i] == letter:
                ret = True
                break
        return ret

    def set_guess(self, guess):
        self.guess = guess

    def unset_guess(self):
        self.guess = ''

    def is_finished(self):
        return self.is_blank() or self.guess == self.answer

    def set_colour(self, colour):
        self.colour = colour

    def get_error_indexes(self):
        ret = []
        for i, status in enumerate(self.status):
            ret.append(('set_error', self.key, i))
        return ret

    def is_blank(self):
        return self.answer == '*'

    def is_wrong(self):
        ret = False
        if not self.is_blank():
            if self.guess:
                if self.guess != self.answer:
                    ret = True
            else:
                if not self.has_letter(self.answer):
                    ret = True
        if ret:
            ret = self.get_error_indexes()
        else:
            ret = []
        return ret

    def get_poly_id(self):
        return self.poly_id

    def disable(self, choice_index, updates):
        """
        Set status False and add command to updates
        """
        if self.status[choice_index]:
            self.set_status(choice_index, False)
            updates.append(('disable', self.key, choice_index))

    def enable(self, choice_index, updates):
        """
        Set status True, unset guess and add commands to updates
        """
        if self.guess:
            for i, status in enumerate(self.status):
                if not status:
                    self.status[i] = True
                    updates.append(('enable', self.key, i))
            self.unset_guess()
            updates.append(('unset', self.key, choice_index))
        else:
            if not self.status[choice_index]:
                self.set_status(choice_index, True)
                updates.append(('enable', self.key, choice_index))

    def my_set(self, choice_index, updates):
        """
        Set guess, set status False for the cell and the rest of the poly
        and add commands to updates
        """
        self.set_guess(self.letters[choice_index])
        updates.append(('set', self.key, choice_index))
        for i, choice in enumerate(self.status):
            if i != choice_index and choice:
                updates.append(('disable', self.key, i))
                self.set_status(i, False)

        poly = self.poly
        for p_datum in poly.datums:
            if p_datum is self:
                if p_datum.status[choice_index]:
                    p_datum.set_status(choice_index, False)
                    updates.append(('disable', p_datum.key, choice_index))
            else:
                # we've set choice_index for this datum
                # now disable it for other datums in the poly
                if p_datum.status[choice_index]:
                    if p_datum.remaining() > 1 and\
                       p_datum.get_letter(choice_index) !=\
                        p_datum.answer:
                        p_datum.set_status(choice_index, False)
                        updates.append(('disable', p_datum.key, choice_index))

    def my_reset(self, updates):
        """
        Set status True for all letters, unset guess and add
        commands to updates
        """
        if self.guess:
            self.unset_guess()
            updates.append(('unset', self.key, -1))
        for i, status in enumerate(self.status):
            updates.append(('enable', self.key, i))
            self.set_status(i, True)

    def my_set_hint(self, choice_index, updates):
        updates.append(('set_hint', self.key, choice_index))

    def my_unset_hint(self, choice_index, updates):
        updates.append(('unset_hint', self.key, choice_index))

    def my_apply_hint(self, choice_index, updates):
        self.disable(choice_index, updates)

    def __str__(self):
        return str(self.key) + ' ' + str(self.poly_id) +\
            ' ' + str(self.answer) + ' ' + str(self.colour)


class Poly():
    """
    Group of Cells
    """
    def __init__(self, poly_id, datum):
        self.poly_id = poly_id
        self.datums = [datum]
        self.finished = False

    def is_finished(self):
        ret = True
        for datum in self.datums:
            if not datum.is_finished():
                ret = False
                break
        if ret:
            self.finished = True
        return self.finished


class Model():
    """
    Get the puzzle data, build the puzzle structures.
    Manage edits and create commands for the graphics.
    """
    def __init__(self, model_data, settings):
        self.model_data = model_data
        self.settings = settings
        self.datum_dic = {}
        self.poly_dic = {}
        self.updates = []
        self.errors = []
        self.status = True
        self.hint = []

    def check_me(self):
        for key, datum in list(self.datum_dic.items()):
            datum.check()

    def get_puzzle(self, puzzle_index):
        puzzle = self.model_data.get_puzzle(puzzle_index)
        width = int(puzzle[0])
        height = int(puzzle[1])
        Sgg.grid_width = width
        Sgg.grid_height = height

        counter = 0
        for item in range(2, len(puzzle) - 1, 2):
            x = counter % width
            y = counter // width
            key = (x, y)
            poly_id = int(puzzle[item])
            datum = Datum(key, puzzle[item + 1], poly_id)
            self.datum_dic[key] = datum
            if poly_id in self.poly_dic:
                self.poly_dic[poly_id].datums.append(datum)
            else:
                self.poly_dic[poly_id] = Poly(poly_id, datum)
            counter += 1

        choices_dic = {}
        # get the colours and choices from each poly
        c_dic = {}
        for key, poly in list(self.poly_dic.items()):
            for datum in poly.datums:
                poly_id = poly.poly_id - 1
                choices_dic.setdefault(key, []).append(datum.answer)
        for key, choice in list(choices_dic.items()):
            c_dic[key] = "".join(sorted(choice))
        # now for each poly add them to their respective datum
        for key, poly in list(self.poly_dic.items()):
            for i, datum in enumerate(poly.datums):
                if not datum.is_blank():
                    datum.init_choices(c_dic[datum.poly_id])
                else:
                    datum = None
                    poly.finished = True

        for key, datum in list(self.datum_dic.items()):
            datum.poly = self.poly_dic[datum.poly_id]

        self.prepare_colours()

    def get_poly(self, datum):
        return self.poly_dic[datum.get_poly_id()]

    def do_disable(self, key, choice_index):
        datum = self.datum_dic[key]
        datum.disable(choice_index, self.updates)

    def do_enable(self, key, choice_index):
        datum = self.datum_dic[key]
        datum.enable(choice_index, self.updates)

    def do_set(self, key, choice_index):
        datum = self.datum_dic[key]
        datum.my_set(choice_index, self.updates)

    def do_reset(self, key):
        datum = self.datum_dic[key]
        datum.my_reset(self.updates)

    def clean_edit(self):
        while True:
            changes = False
            for key, datum in list(self.datum_dic.items()):
                if datum:
                    last_active = datum.last_active()
                    if last_active != -1:
                        changes = True
                        # only 1 active letter this datum
                        # set it as guess
                        self.do_disable(key, last_active)
                        self.do_set(key, last_active)
            if not changes:
                break

    def edit(self, key, choice_index, mouse_index):
        """
        For simplicity, when the cell is flagged as error
        reset all letters when an enable is requested.
        """
        self.updates = []
        if mouse_index == 1:
            self.do_disable(key, choice_index)
        elif mouse_index == 3:
            if key in self.errors:
                self.do_reset(key)
            else:
                self.do_enable(key, choice_index)
        elif mouse_index == 2:
            self.do_set(key, choice_index)
        self.clean_edit()

    def set_solution(self):
        self.updates = []
        for key, datum in list(self.datum_dic.items()):
            if datum.status:
                for choice_index, status in enumerate(datum.status):
                    if status:
                        self.updates.append(('disable', key, choice_index))
                answer_index = datum.letters.index(datum.answer)
                self.updates.append(('set', key, answer_index))

    def check_errors(self):
        self.updates = []
        for key, datum in list(self.datum_dic.items()):
            errors = datum.is_wrong()
            if errors:
                self.errors.append(key)
                self.updates += errors

    def is_finished(self):
        ret = True
        for key, datum in list(self.datum_dic.items()):
            if not datum.is_finished():
                ret = False
                break
        return ret

    def prepare_colours(self):

        colours = []
        txt = "Colour_"
        for item in range(1, 10):
            txtp = txt + str(item)
            colours.append(self.settings.dic[txtp])

        for key, datum in list(self.datum_dic.items()):
            colour = colours[datum.poly_id - 1]
            datum.set_colour(colour)

    def get_hint(self):
        """
        Find the datum with the most remaining letters.
        A hint is simply e letter from this datum
        which is not the answer and hence can be deleted.
        """
        biggest = 0
        best_letter = None
        best_key = None
        for key, datum in list(self.datum_dic.items()):
            if not datum.is_blank():
                remaining = datum.remaining()
                if remaining > biggest:
                    for choice_index, status in enumerate(datum.status):
                        if status:
                            if datum.get_letter(choice_index) !=\
                               datum.answer:
                                best_key = key
                                best_letter = choice_index
                                biggest = remaining
                                break
                            # duplicate letter?
                            cip1 = choice_index + 1
                            if cip1 < len(datum.status) and\
                               datum.status[cip1] and\
                               datum.get_letter(cip1) == datum.answer:
                                best_key = key
                                best_letter = cip1
                                biggest = remaining
                                break
        datum = self.datum_dic[best_key]
        datum.my_set_hint(best_letter, self.updates)
        self.hint = [best_key, best_letter]

    def apply_hint(self):
        best_key = self.hint[0]
        best_letter = self.hint[1]
        datum = self.datum_dic[best_key]
        datum.my_apply_hint(best_letter, self.updates)
        self.clean_edit()
        self.hint = []

    def unshow_hint(self):
        best_key = self.hint[0]
        best_letter = self.hint[1]
        datum = self.datum_dic[best_key]
        datum.my_unset_hint(best_letter, self.updates)


class Controller():
    def __init__(self, root):
        self.settings = Settings()
        self.master = root
        self.flasher = None
        self.model_data = ModelData()
        self.model = Model(self.model_data, self.settings)
        self.view = View(root, self.settings)
        self.hint_status = Sgg.GIVE_HINT
        self.active = False
        self.error_detected = False
        puzzle_index = None
        self.puzzle_number = puzzle_index
        self.start_game(puzzle_index)

    def set_puzzle_number(self, puzzle_number):
        if puzzle_number is None or puzzle_number < 0 or \
                puzzle_number > self.model_data.puzzle_count - 1:
            puzzle_number = randint(0, self.model_data.puzzle_count - 1)
        self.puzzle_number = puzzle_number

    def start_game(self, puzzle_number):
        self.model = Model(self.model_data, self.settings)
        self.set_puzzle_number(puzzle_number)
        self.view.clock_init()
        self.view.clock_restart()
        self.view.game_over()
        self.model.get_puzzle(self.puzzle_number)
        self.view.thumbs_up()
        self.view.datum_dic = self.model.datum_dic
        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.puzzle_number))
        self.hint_status = Sgg.GIVE_HINT
        self.active = True
        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_board_callback(self.board_event)
        self.view.entry_box.add_callback(self.get_puzzle_number)

    def service_menu(self, *args):
        """
        Respond to menu choices
        """

        strargs = ''
        for item in args:
            strargs += ' ' + item
        PzlG.logger.logger.info("service_menu " + strargs)
        name = args[0]
        if 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):
        PzlG.logger.logger.info("service_toolbar " +
                                str(button_index) +
                                ' ' +
                                str(mouse_index))
        if mouse_index == 2:
            self.view.do_tooltip(button_index, None)
        elif mouse_index == 3:
            if button_index == 0:
                self.puzzle_number += 1
                self.start_game(self.puzzle_number)
        elif mouse_index == 1:
            if button_index == 0:
                self.start_game(None)
            elif button_index == 1:
                self.check_errors()
                if self.model.status and not self.model.is_finished():
                    self.give_hint()
            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()

    def board_event(self, key, choice_index, mouse_index):
        """
        Respond to board clicks
        One of the three event servicing functions.
        Mouse index 1 == DELETE
                    2 == SET GUESS
                    3 == ENABLE
        """
        if self.active:
            PzlG.logger.logger.info("board_event " +
                                    str(mouse_index) +
                                    ' ' +
                                    str(key) +
                                    ' ' +
                                    str(choice_index))
            # need to remove any active hint

            if self.model.hint:
                self.view.hint_off()
                self.model.unshow_hint()
                self.model.hint = []
                self.hint_status = Sgg.GIVE_HINT
                self.view.handle_updates(self.model.updates)
            if mouse_index == 1:
                self.model.edit(key, choice_index, mouse_index)
            elif mouse_index == 3:
                self.model.edit(key, choice_index, mouse_index)
            elif mouse_index == 2:
                # set guess and hide guess in all its poly
                self.model.edit(key, choice_index, mouse_index)
            self.view.handle_updates(self.model.updates)
            self.model.updates = []
            self.check_finished()

    def give_hint(self):
        if self.hint_status == Sgg.GIVE_HINT:
            self.model.check_errors()
            if self.model.updates:
                self.view.handle_updates(self.model.updates)
                self.view.thumbs_down()
                self.error_detected = True
            else:
                self.error_detected = False
                self.model.get_hint()
                if self.model.hint:
                    self.view.handle_updates(self.model.updates)
                    self.model.updates = []
                    self.view.hint_on()
                    self.view.clock.set_show(False)
                    self.hint_status = Sgg.APPLY_HINT
        else:
            self.model.apply_hint()
            self.view.handle_updates(self.model.updates)
            self.model.updates = []
            self.hint_status = Sgg.GIVE_HINT
            self.view.hint_off()
            self.check_finished()

    def clean_errors(self):
        # we know there's an error already, so now we correct it
        for key in self.model.errors:
            self.model.do_reset(key)
        if self.model.updates:
            self.view.handle_updates(self.model.updates)
            self.view.thumbs_up()
            self.error_detected = False
            self.view.thumbs_up()
        self.model.clean_edit()
        self.model.errors = []
        self.model.updates = []

    def check_errors(self):
        """
        If an error already known, sort it out.
        Otherwise check for errors.
        """
        if self.error_detected:
            # we know there's an error already, so now we correct it
            self.clean_errors()
        else:
            self.model.check_errors()
            if self.model.updates:
                self.view.handle_updates(self.model.updates)
                self.view.thumbs_down()
                self.error_detected = True

    def check_finished(self):
        """
        Check if finished.
        If so, handle buttons, flasher and clock
        """
        if self.active:
            if self.model.is_finished():
                self.view.hint_off()
                self.view.game_over()
                self.view.clock.set_show(True)
                self.view.clock_stop()
                self.flasher.flash()
                self.active = False

    def apply_hint(self):
        self.model.apply_hint()
        self.view.handle_updates(self.model.updates)

    def show_solution(self):
        self.model.set_solution()
        self.view.handle_updates(self.model.updates)

    def set_font_size(self):
        self.settings.fiddle_fonts()
        self.start_game(self.puzzle_number)

    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)
            except ValueError:
                self.view.entry_box.clear()

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


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")
