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

# Copyright (C) 2024 Rod Engerstad


import tkinter as tk
import tkinter.colorchooser
import random
from random import shuffle
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 tracks_data import ModelData


class Sgg():
    """
    Game globals
    """
    grid_width = 8
    grid_height = 8
    GIVE_HINT = 0
    APPLY_HINT = 1
    alg_counts = [0 for x in range(8)]


class Settings(SettingsB):

    def __init__(self):
        SettingsB.__init__(self)
        self.dic = {}
        self.dic['font_size'] = '4'
        self.dic['grid_size'] = '8'
        self.dic['board_colour'] = 'white'
        self.dic['grid_colour'] = 'grey'
        self.dic['cursor_colour'] = 'green'
        self.dic['track_colour'] = 'blue'
        self.dic['clue_colour'] = 'black'
        self.dic['error_colour'] = 'red'
        self.dic['counts_colour'] = 'green'
        self.dic['set_colour'] = 'board_colour'
        self.original_square_size = 30
        self.square_size = self.original_square_size
        self.clock_box_width = 6
        self.score_box_width = 6
        self.tick_interval = 5
        self.menu_items = []
        self.preferences = Preferences(self.dic)
        self.preferences.load()
        self.max_time = None
        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 = [_("If a row or column has all its tracks, we can fill the remaining cells with 'X'"), _("If a cell is receiving track from an adjacent cell, mark it with 'o' so we know it is part of the track, even though we don't know of what type."), _("Rare special cases."), _("If a cell is unset check if its track must exit in only one direction."), _("If a cell is known to be in the path (ie is set to 'o') check to see if is can only continue in a particular direction."), _("If the number of unset cells in a strip equals the number of path cells still required, they must all be 'o'."), _("To be part of the track a cell must have two neighbours which must also be part of the track. If that is not possible the cell is not part of the track."), _("If a strip requires one more track section and that section must be adjacent to a cell set as 'o', then any other unset cells in the strip can be set to 'x'.")]

    def make_help(self):
        self.help_text = _("The object of the game is to find the unique path which passes through the grid without intersecting itself and while passing through all the initial clues. The row of numbers along the top of the grid and the column down the right hand side contain the counts of the number of track sections in each column and row. When the player moves the cursor over a cell in the grid, shapes will appear defining the cell's possible track shapes. The shape shown will depend on where within the cell the cursor is positioned. By moving the cursor in the cell the player will quickly learn where in the cell to position the cursor in order to produce the wanted shape. In addition to the 6 shapes the letters 'o' and 'x' also appear. 'o' means this cell is part of the path and 'x' means that this cell is not part of the path. A left mouse click will add the shape or letter to the cell. Right click will remove any previously placed symbol or shape. The jigsaw symbol is a menu for configuring the game. A left click on the PZL tracks symbol next to the jigsaw starts a new game by loading a randomly selected puzzle. A right click on this symbol will load the next puzzle. A click on the wand requests a hint (shown by a '?'). If the player can work out which is the correct shape/symbol they can add it using a left click. Otherwise a further click on the hint button will cause the shape/symbol to be added. If a hint is requested when the grid contains errors, no hint will be given, the thumb icon turns down and the incorrect cells are displayed. They can be removed using right clicks or by a single click on the thumb icon. At any time clicking the thumb checks the current grid for errors, a second click will remove them. The sad smiley will display the solution. The entry box contains the current puzzle number and can be used to enter numbers. To its right is a clock. If a hint is displayed a number code (0 - 6) for the hint will replace the text of the clock until the hint is applied. The game contains 500 puzzles: 100 6x6, 200 8x8, 100 10x10 and 100 12x12. However, the game offers an unlimited number of puzzles, as it can create them very quickly, if the player enters a negative puzzle number. To be used in conjunction with this there is an option in the jigsaw menu for selecting the grid size for the randomly generated puzzles. Other options in the menu include a facility for the player to choose the colour of all the game's components and to set the font size.")

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

    def load_menus(self):
        self.menu_items.append(MenuItem(
            _('Font Size'),
            [
                '1',
                '2',
                '4',
                '6',
                '8',
                '10',
                '12'],
            '',
            ['1',
             '2',
             '4',
             '6',
             '8',
             '10',
             '12'],
            'font_size'
            ))
        self.menu_items.append(MenuItem(
            _('Grid Size'),
            [
                '4',
                '6',
                '8',
                '10',
                '12'],
            '',
            [
                '4',
                '6',
                '8',
                '10',
                '12'],
            'grid_size'
            ))
        self.menu_items.append(MenuItem(
            _('Set Colour'),
            [_('Board'),
             _('Grid'),
             _('Track'),
             _('Cursor'),
             _('Clue'),
             _('Counts'),
             _('Error')],
            'N',
            ['board_colour',
             'grid_colour',
             'track_colour',
             'cursor_colour',
             'clue_colour',
             'counts_colour',
             'error_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'
        ))

    def do_scaling(self):
        inc = 0.1
        mult = int(self.dic['font_size'])
        factor = 0.8 + mult * inc
        self.square_size = 50
        self.square_size = int(self.square_size * factor)
        size = self.value_font_size
        self.value_font.configure(size=int(size * factor))


class Board(tk.Canvas):
    def __init__(self, parent, settings):
        self.parent = parent
        self.settings = settings
        self.model = None
        self.grid_margin = 0
        self.square_size = self.settings.square_size
        self.board_width = 2 * self.grid_margin +\
            (Sgg.grid_width + 1) * (self.square_size)
        self.board_height = 2 * self.grid_margin +\
            (Sgg.grid_height + 1) * (self.square_size)
        bg = self.settings.green
        self.activefill = self.settings.green
        self.board_colour = self.settings.dic['board_colour']
        self.grid_colour = self.settings.dic['grid_colour']
        bg = self.board_colour
        tk.Canvas.__init__(self, self.parent, width=self.board_width,
                           height=self.board_height,
                           bg=bg)

        self.callback = None
        self.add_bindings()
        self.draw_grid()
        self.tk_focusFollowsMouse()
        self.grid(row=0, column=0)
        self.update_idletasks()

    def add_bindings(self):
        self.bind("<Motion>", self.command)
        self.bind("<ButtonRelease-2>", self.command)
        self.bind("<ButtonRelease-3>", self.command)
        self.bind("<ButtonRelease-1>", self.command)

    def command(self, event):
        edge = 0.2
        x = event.x / self.square_size
        y = event.y / self.square_size
        if x < edge or x >= Sgg.grid_width or\
           y < 1 or y > Sgg.grid_height + 1 - edge:
            key = (-1, -1)
        else:
            x = int(event.x / self.square_size)
            y = int(event.y / self.square_size)
            key = (x, y - 1)
        self.callback(event, key)

    def add_callback(self, square_callback):
        self.callback = square_callback

    def my_destroy(self):
        self.destroy()

    def draw_grid(self):
        colour = self.settings.dic['grid_colour']
        width = 1
        unit = self.settings.square_size
        x0 = 0
        x1 = Sgg.grid_width * unit
        for y in range(1, Sgg.grid_height + 2):
            y0 = y * unit
            y1 = y0
            my_id = self.create_line(x0, y0, x1, y1, width=width, fill=colour)

        y0 = unit
        y1 = (Sgg.grid_height + 1) * unit
        for x in range(0, Sgg.grid_width + 1):
            x0 = x * unit
            x1 = x0
            my_id = self.create_line(x0, y0, x1, y1, width=width, fill=colour)

    def draw_shape(self, key, shape, colour, width, tags):
        font = self.settings.value_font
        my_id = None
        unit = self.settings.square_size
        unit2 = unit / 2

        if shape == 'b':
            x0 = key[0] * unit
            y0 = (key[1] + 1) * unit
            y0 += unit2
            x1 = x0 + unit
            y1 = y0
            my_id = self.create_line(x0, y0, x1, y1, width=width, fill=colour,
                                     tags=tags)
        elif shape == 'c':
            x0 = key[0] * unit
            x0 += unit2
            y0 = (key[1] + 1) * unit
            x1 = x0
            y1 = y0 + unit
            my_id = self.create_line(x0, y0, x1, y1, width=width, fill=colour,
                                     tags=tags)
        elif shape == 'e':
            # top left corner
            x = key[0] * unit
            y = (key[1] + 1) * unit
            x0 = x - unit2
            y0 = y - unit2
            x1 = x0 + unit
            y1 = y0 + unit
            start = 270
            extent = 90
            my_id = self.create_arc(x0, y0, x1, y1,
                                    start=start,
                                    extent=extent,
                                    width=width,
                                    outline=colour,
                                    style=tk.ARC,
                                    fill='',
                                    tags=tags)
        elif shape == 'd':
            # bottom left corner
            start = 0
            extent = 90
            x = key[0] * unit
            y = (key[1] + 1) * unit
            x0 = x - unit2
            y0 = y + unit2
            x1 = x0 + unit
            y1 = y0 + unit
            my_id = self.create_arc(x0, y0, x1, y1,
                                    start=start,
                                    extent=extent,
                                    width=width,
                                    outline=colour,
                                    style=tk.ARC,
                                    fill='',
                                    tags=tags)
        elif shape == 'f':
            # top right corner
            start = 180
            extent = 90
            x = key[0] * unit
            y = (key[1] + 1) * unit
            x0 = x + unit2
            y0 = y - unit2
            x1 = x0 + unit
            y1 = y0 + unit
            my_id = self.create_arc(x0, y0, x1, y1,
                                    start=start,
                                    extent=extent,
                                    width=width,
                                    outline=colour,
                                    style=tk.ARC,
                                    fill='',
                                    tags=tags)
        elif shape == 'g':
            # bottom right corner
            start = 90
            extent = 90
            x = key[0] * unit
            y = (key[1] + 1) * unit + unit
            x0 = x + unit2
            y0 = y - unit2
            x1 = x0 + unit
            y1 = y0 + unit
            my_id = self.create_arc(x0, y0, x1, y1,
                                    start=start,
                                    extent=extent,
                                    width=width,
                                    outline=colour,
                                    style=tk.ARC,
                                    fill='',
                                    tags=tags)
        elif shape == 'x':
            x0 = key[0] * unit
            y0 = (key[1] + 1) * unit
            xys = [x0 + unit2, y0 + unit2]
            text = 'x'
            my_id = self.create_text(xys, text=text, font=font, fill=colour,
                                     tags=tags)
        elif shape == 'o':
            x0 = key[0] * unit
            y0 = (key[1] + 1) * unit
            xys = [x0 + unit2, y0 + unit2]
            text = 'o'
            my_id = self.create_text(xys, text=text, font=font, fill=colour,
                                     tags=tags)
        elif shape == '?':
            x0 = key[0] * unit
            y0 = (key[1] + 1) * unit
            xys = [x0 + unit2, y0 + unit2]
            text = '?'
            my_id = self.create_text(xys, text=text, font=font, fill=colour,
                                     tags=tags)

        return my_id

    def draw_shapes(self, job):
        tags = 'clues'
        clue_colour = self.settings.dic['clue_colour']
        track_colour = self.settings.dic['track_colour']
        board_colour = self.settings.dic['board_colour']

        for key, datum in list(self.model.datum_dic.items()):
            if job == 0:
                shape = datum.guess
                width = 20
                shape_id = self.draw_shape(key, shape, clue_colour, width, tags)
            else:
                width = 20
                thin = 14
                shape = datum.answer
                if datum.clue:
                    shape_id = self.draw_shape(key, shape,
                                               clue_colour,
                                               width, tags)
                    shape_id = self.draw_shape(key, shape,
                                               board_colour,
                                               thin, tags)
                else:
                    shape_id = self.draw_shape(key, shape,
                                               track_colour,
                                               width, tags)
                    shape_id = self.draw_shape(key, shape,
                                               board_colour,
                                               thin, tags)

    def colour_errors(self):
        colour = self.settings.dic['error_colour']
        width = 20
        tags = 'error'
        self.delete_errors()
        for key in self.model.errors:
            shape = self.model.datum_dic[key].guess
            shape_id = self.draw_shape(key, shape, colour, width, tags)
            self.model.datum_dic[key].shape_id = shape_id

    def delete_errors(self):
        self.delete('error')

    def draw_edges(self):
        font = self.settings.value_font
        colour = self.settings.dic['counts_colour']
        unit = self.settings.square_size
        unit2 = unit / 2
        size = self.model.puzzle[0]
        col = self.model.column_clues
        row = self.model.row_clues
        for i, item in enumerate(col):
            text = item
            x = i * unit + unit2
            y = unit2
            xys = [x, y]
            jd = self.create_text(xys, text=text, font=font, fill=colour)

        for i, item in enumerate(row):
            text = item
            x = unit * size + unit2
            y = (i + 1) * unit + unit2
            xys = [x, y]
            jd = self.create_text(xys, text=text, font=font, fill=colour)

    def delete_tags(self, tags):
        self.delete(tags)


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

    def __init__(self, master, settings):
        ViewB.__init__(self, master, settings)
        self.model = None
        self.board = None
        self.image_dic = {}
        self.make_image_dic()
        self.image_dic['start'] = [self.images.start_tracks, 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)

    def update_clock_box(self, algorithm):
        value = str(algorithm)
        if algorithm is not None:
            self.clock.box.set_text(value)


class Datum():
    """
    Cell item
    Track_types a-g, o, x
    """
    def __init__(self, key, answer):
        self.key = key
        self.answer = answer.lower()
        self.shape_id = None
        self.status = True
        self.clue = False
        self.finished = False
        if answer.isupper():
            self.clue = True
            self.guess = answer.lower()
            self.finished = True
        else:
            self.guess = None
            if self.answer == 'a':
                self.finished = True

    def is_finished(self):
        return self.finished

    def set_guess(self, guess, shape_id):
        """
        Finished if guess == answer, answer == 'a'
        """

        if guess == self.answer:
            self.finished = True
        elif self.answer == 'a' and guess != 'x':
            self.status = False
        elif self.answer != 'a' and guess == 'x':
            self.status = False
        elif self.answer != 'a' and guess != self.answer and guess != 'o':
            self.status = False

        self.guess = guess
        self.shape_id = shape_id

    def unset_guess(self):
        self.guess = None
        shape_id = self.shape_id
        self.shape_id = None
        self.finished = False
        if self.answer == 'a':
            self.finished = True
        self.status = True
        return shape_id

    def is_unset(self):
        return self.guess is None

    def __str__(self):
        return str(self.key) + str(self.answer) + str(self.guess)\
            + str(self.shape_id)


class Model():

    @staticmethod
    def in_range(key):
        ret = True
        x = key[0]
        y = key[1]
        if x < 0 or x >= Sgg.grid_width or y < 0 or y >= Sgg.grid_height:
            ret = False
        return ret

    @staticmethod
    def add_keys(key_0, key_1):
        return (key_0[0] + key_1[0], key_0[1] + key_1[1])

    def __init__(self, model_data):
        self.model_data = model_data
        self.east = [1, 0]
        self.west = [-1, 0]
        self.north = [0, -1]
        self.south = [0, 1]
        self.points = [self.east, self.west, self.north, self.south]
        self.good_west = ['b', 'f', 'g']
        self.good_east = ['b', 'e', 'd']
        self.good_north = ['c', 'd', 'g']
        self.good_south = ['c', 'e', 'f']
        self.good_steps = {
            'a': [],
            'b': [[self.east] + [self.west]],
            'c': [[self.north] + [self.south]],
            'd': [[self.south] + [self.west]],
            'e': [[self.north] + [self.west]],
            'f': [[self.east] + [self.north]],
            'g': [[self.east] + [self.south]],
            'o': [self.points],
            'x': [],
            }
        self.bad_west = ['c', 'e', 'd', 'x']
        self.bad_east = ['c', 'f', 'g', 'x']
        self.bad_north = ['b', 'e', 'f', 'x']
        self.bad_south = ['b', 'd', 'g', 'x']
        self.goods = [self.good_east, self.good_west,
                      self.good_north, self.good_south]
        self.good = set(self.good_east) | set(self.good_west) |\
            set(self.good_north) | set(self.good_south)
        self.goodx = self.good | set(['x', 'o'])
        self.goodin = [self.good_west, self.good_east,
                       self.good_north, self.good_south]
        """
        sym to (). eg b(1,0) continues with b,e or d
        fom symbol to direction
        eg d going west needs bfg, d going south needs cef
        """
        self.from_to = {
            'b(1, 0)': ['b', 'e', 'd'],
            'b(-1, 0)': ['b', 'f', 'g'],
            'c(0, 1)': ['c', 'e', 'f'],
            'c(0, -1)': ['c', 'd', 'g'],
            'd(0, 1)': ['c', 'e', 'f'],
            'd(-1, 0)': ['b', 'f', 'g'],
            'e(0, -1)': ['c', 'd', 'g'],
            'e(-1, 0)': ['b', 'f', 'g'],
            'f(1, 0)': ['b', 'd', 'e'],
            'f(0, -1)': ['c', 'd', 'g'],
            'g(0, 1)': ['c', 'e', 'f'],
            'g(1, 0)': ['b', 'd', 'e'],
            'o(-1, 0)': ['b', 'f', 'g', 'o'],
            'o(1, 0)': ['b', 'd', 'e', 'o'],
            'o(0, 1)': ['c', 'e', 'f', 'o'],
            'o(0, -1)': ['c', 'd', 'g', 'o'],
            'a(1, 0)': [],
            'a(-1, 0)': [],
            'a(0, 1)': [],
            'a(0, -1)': [],
            }

        self.gfrom_to = {
            '(-1, 0)(0, -1)': 'f',
            '(-1, 0)(1, 0)': 'b',
            '(-1, 0)(0, 1)': 'g',
            '(0, -1)(0, 1)': 'c',
            '(0, -1)(1, 0)': 'g',
            '(0, -1)(-1, 0)': 'd',
            '(0, 1)(0, -1)': 'c',
            '(0, 1)(1, 0)': 'f',
            '(0, 1)(-1, 0)': 'e',
            '(1, 0)(-1, 0)': 'b',
            '(1, 0)(0, -1)': 'e',
            '(1, 0)(0, 1)': 'd',
            '(0, -1)(0, -1)': 'c',
            '(0, 1)(0, 1)': 'c',
            '(-1, 0)(-1, 0)': 'b',
            '(1, 0)(1, 0)': 'b',
            }

        self.datum_dic = {}
        self.finished = False
        self.column_clues = []
        self.row_clues = []
        self.column_ulaid = []
        self.row_ulaid = []
        self.status = True
        self.hints = []
        self.hint_status = Sgg.GIVE_HINT
        self.errors = []
        self.puzzle_number = None
        self.difficulty = None
        self.ggrid = {}
        self.gpoints = []
        self.puzzle = []
        self.path = []
        self.algorithm = None
        self.algorithms = [(self.alg_0),
                           (self.alg_1),
                           (self.alg_2),
                           (self.alg_3),
                           (self.alg_4),
                           (self.alg_5),
                           (self.alg_6),
                           (self.alg_7)]

        self.alg_counts = [0 for x in self.algorithms]

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

        self.puzzle_number = puzzle_number
        self.puzzle = self.model_data.get_puzzle(puzzle_number)
        self.get_puzzle_1()

    def get_puzzle_1(self):
        Sgg.grid_width = self.puzzle[0]
        Sgg.grid_height = self.puzzle[1]
        self.make_grid()
        self.init_counts()

    def init_counts(self):
        self.column_clues = [0 for i in range(Sgg.grid_width)]
        self.row_clues = [0 for i in range(Sgg.grid_height)]
        for key, datum in list(self.datum_dic.items()):
            if datum.answer != 'a':
                self.column_clues[key[0]] += 1
                self.row_clues[key[1]] += 1

    def do_counts(self):
        self.column_ulaid = [0 for i in range(Sgg.grid_width)]
        self.row_ulaid = [0 for i in range(Sgg.grid_height)]
        for key, datum in list(self.datum_dic.items()):
            if datum.guess in self.good or datum.guess == 'o':
                self.column_ulaid[key[0]] += 1
                self.row_ulaid[key[1]] += 1

        t = [self.column_clues[i] - self.column_ulaid[i]
             for i in range(Sgg.grid_width)]
        self.column_ulaid = t
        t = [self.row_clues[i] - self.row_ulaid[i]
             for i in range(Sgg.grid_height)]
        self.row_ulaid = t

    def make_grid(self):
        i = 3
        for y in range(Sgg.grid_height):
            for x in range(Sgg.grid_width):
                key = (x, y)
                datum = Datum(key, self.puzzle[i])
                self.datum_dic[key] = datum
                i += 1

        self.finished = False

    def set_guess(self, key, guess, shape_id):
        """
        Set guess and shape_id (only matters for user interaction)
        and update the clue counts.
        """
        datum = self.datum_dic[key]
        prev_shape_id = datum.shape_id
        datum.set_guess(guess, shape_id)
        return prev_shape_id

    def unset_guess(self, key):
        """
        Unset guess and shape_id (only matters for user interaction)
        and update the clue counts.
        """
        datum = self.datum_dic[key]
        prev_shape_id = datum.shape_id
        datum.unset_guess()
        return prev_shape_id

    def is_finished(self):
        return self.finished

    def set_solution(self):
        for key, datum in list(self.datum_dic.items()):
            datum.set_guess(datum.answer, None)

    def check_errors(self):
        self.errors = []
        self.status = True
        for key, datum in list(self.datum_dic.items()):
            if not datum.status:
                self.errors.append(key)
                self.status = False

    def undo_errors(self):
        for key in self.errors:
            self.unset_guess(key)
        self.errors = []

    def add_and_check(self, key, point, codes, yes_no):
        """
        Add point to key and if in range check if guess is in codes
        (yes_no = True
        or if yes_no is False return True if out of range or in codes
        """
        key_1 = Model.add_keys(key, point)
        if yes_no:
            ret = False
            if Model.in_range(key_1) and\
               self.datum_dic[key_1].guess in codes:
                ret = True
        else:
            ret = False
            if not Model.in_range(key_1) or\
               self.datum_dic[key_1].guess in codes:
                ret = True
        return ret

    def alg_0(self):
        """
        If a row or column has all its tracks, we can
        fill the remaining cells with 'X'
        """

        self.hints = []
        for col, count in enumerate(self.column_ulaid):
            if count == 0:
                for key, datum in list(self.datum_dic.items()):
                    if key[0] == col and (datum.guess is None or
                                          datum.guess not in self.goodx):
                        self.hints.append([key, 'x'])
        for row, count in enumerate(self.row_ulaid):
            if count == 0:
                for key, datum in list(self.datum_dic.items()):
                    if key[1] == row and (datum.guess is None or
                                          datum.guess not in self.goodx):
                        self.hints.append([key, 'x'])

    def alg_1(self):
        """
        If a cell is receiving track from an adjacent cell,
        mark it with 'o' so we know it is part of the track,
        even though we don't know of what type.
        """

        self.hints = []
        for key, datum in list(self.datum_dic.items()):
            for i, point in enumerate(self.points):
                if self.add_and_check(key, point, self.goods[i], True):
                    if datum.guess is None:
                        self.hints.append([key, 'o'])

    def alg_2(self):
        """
        Special cases
        """

        self.hints = []

        # NW
        key_h = (0, 0)
        key = (1, 0)
        if self.datum_dic[key].guess == 'g':
            if self.datum_dic[key_h].guess is None:
                self.hints.append([key_h, 'x'])
        key = (0, 1)
        if self.datum_dic[key].guess == 'g':
            if self.datum_dic[key_h].guess is None:
                self.hints.append([key_h, 'x'])

        # NE
        key_h = (Sgg.grid_width - 1, 0)
        key = (Sgg.grid_width - 2, 0)
        if self.datum_dic[key].guess == 'd':
            if self.datum_dic[key_h].guess is None:
                self.hints.append([key_h, 'x'])

        key = (Sgg.grid_width - 1, 1)
        if self.datum_dic[key].guess == 'd':
            if self.datum_dic[key_h].guess is None:
                self.hints.append([key_h, 'x'])

        # SW
        key_h = (0, Sgg.grid_height - 1)
        key = (0, Sgg.grid_height - 2)
        if self.datum_dic[key].guess == 'f':
            if self.datum_dic[key_h].guess is None:
                self.hints.append([key_h, 'x'])

        key = (1, Sgg.grid_height - 1)
        if self.datum_dic[key].guess == 'f':
            if self.datum_dic[key_h].guess is None:
                self.hints.append([key_h, 'x'])

        # SE
        key_h = (Sgg.grid_width - 1, Sgg.grid_height - 1)
        key = (Sgg.grid_width - 1, Sgg.grid_height - 2)
        if self.datum_dic[key].guess == 'e':
            if self.datum_dic[key_h].guess is None:
                self.hints.append([key_h, 'x'])

        key = (Sgg.grid_width - 2, Sgg.grid_height - 1)
        if self.datum_dic[key].guess == 'e':
            if self.datum_dic[key_h].guess is None:
                self.hints.append([key_h, 'x'])

    def alg_3(self):
        """
        If a cell is empty check if its track must exit in only one
        direction.
        """

        self.hints = []
        for key, datum in list(self.datum_dic.items()):
            if datum.guess == 'o':
                # try west blocks
                if self.add_and_check(key, self.west, self.bad_west,
                                      False):
                    if self.add_and_check(key, self.north, self.bad_north,
                                          False):
                        # blocked west and north
                        self.hints.append([key, 'g'])

                    # try south blocks
                    if self.add_and_check(key, self.south, self.bad_south,
                                          False):
                        # blocked west south
                        self.hints.append([key, 'f'])

                    # try east blocks
                    if self.add_and_check(key, self.east, self.bad_east,
                                          False):
                        # blocked west east
                        self.hints.append([key, 'c'])

                # try east blocks
                if self.add_and_check(key, self.east, self.bad_east,
                                      False):
                    if self.add_and_check(key, self.north, self.bad_north,
                                          False):
                        # blocked east north
                        self.hints.append([key, 'd'])

                    if self.add_and_check(key, self.south, self.bad_south,
                                          False):
                        self.hints.append([key, 'e'])

                # try north south
                if self.add_and_check(key, self.north, self.bad_north,
                                      False):
                    if self.add_and_check(key, self.south, self.bad_south,
                                          False):
                        self.hints.append([key, 'b'])

    def alg_4(self):
        """
        If a cell is known to be in the path (ie is set to 'o')
        check to see if is can only continue in a particular direction.
        """
        self.hints = []
        for key, datum in list(self.datum_dic.items()):
            if datum.guess == 'o':
                if self.add_and_check(key, self.west, self.good_west, True):
                    if self.add_and_check(key, self.east, self.good_east, True):
                        self.hints.append([key, 'b'])

                    if self.add_and_check(key, self.south, self.good_south,
                                          True):
                        self.hints.append([key, 'd'])

                    if self.add_and_check(key, self.north, self.good_north,
                                          True):
                        self.hints.append([key, 'e'])

                if self.add_and_check(key, self.north, self.good_north, True):
                    if self.add_and_check(key, self.south, self.good_south,
                                          True):
                        self.hints.append([key, 'c'])
                if self.add_and_check(key, self.east, self.good_east, True):
                    if self.add_and_check(key, self.south, self.good_south,
                                          True):
                        self.hints.append([key, 'g'])
                    if self.add_and_check(key, self.north, self.good_north,
                                          True):
                        self.hints.append([key, 'f'])

    def alg_5(self):
        """
        If the number of unset cells in a strip equals the number
        of path cells still required, they must all be 'o'.
        """

        self.hints = []
        unset_cols = [0 for x in range(Sgg.grid_width)]
        unset_rows = [0 for x in range(Sgg.grid_height)]
        for key, datum in list(self.datum_dic.items()):
            if datum.is_unset():
                unset_cols[key[0]] += 1
                unset_rows[key[1]] += 1

        for col, count in enumerate(self.column_ulaid):
            if count == unset_cols[col]:
                for key, datum in list(self.datum_dic.items()):
                    if key[0] == col and datum.guess is None:
                        self.hints.append([key, 'o'])
        for row, count in enumerate(self.row_ulaid):
            if count == unset_rows[row]:
                for key, datum in list(self.datum_dic.items()):
                    if key[1] == row and datum.guess is None:
                        self.hints.append([key, 'o'])

    def alg_6(self):
        """
        To be part of the track a cell must have two neighbours
        which must also be part of the track. If that is not
        possible the cell is not part of the track.
        """

        self.hints = []
        unsets = []
        for key, datum in list(self.datum_dic.items()):
            if datum.is_unset():
                unsets.append([key])
        progress = True
        while progress:
            progress = False
            for unset in unsets:
                for key in unset:
                    for i, point in enumerate(self.points):
                        key_1 = Model.add_keys(key, point)
                        if Model.in_range(key_1):
                            for unset_1 in unsets:
                                if unset_1 != unset:
                                    if key_1 in unset_1:
                                        new = unset + unset_1
                                        unsets.remove(unset)
                                        unsets.remove(unset_1)
                                        progress = True
                                        unsets.append(new)
                                        break
                            if progress:
                                break
                    if progress:
                        break
                if progress:
                    break

        for unset in unsets:
            link_count = 0
            for key in unset:
                for i, point in enumerate(self.points):
                    key_1 = Model.add_keys(key, point)
                    if Model.in_range(key_1):
                        if self.datum_dic[key_1].guess in self.goodin[i] or\
                           self.datum_dic[key_1].guess == 'o':
                            link_count += 1

            if link_count < 2:
                for key in unset:
                    self.hints.append([key, 'x'])

    def alg_7(self):
        """
        If a strip requires one more track section and that section
        must be adjacent to a cell set as 'o', then any other unset
        cells in the strip can be set to 'x'.

        1. find strips requiring 1 more track
        2. find their empty cells 'empty'
        3. if all empty cells are blocked:
        4. find existing 'o' cells
        5. from empty delete all cells touching 'o' cells
         the remaining empty cells cannot be track so mark as 'x'
        """
        for i, count in enumerate(self.row_ulaid):
            bad_strip = False
            # find strips requiring one more track
            if self.row_clues[i] != 1 and count == 1:
                # Needs 1 more 'o'
                for j in range(Sgg.grid_width):
                    empties = [k for k in range(Sgg.grid_width)
                               if self.datum_dic[k, i].guess is None]
                # check they are all blocked
                im1 = i - 1
                ip1 = i + 1
                good = []
                for empty in empties:
                    key_m = empty, im1
                    key_p = empty, ip1
                    if Model.in_range(key_m) and\
                       self.datum_dic[key_m].guess in self.bad_north or\
                       Model.in_range(key_p) and\
                       self.datum_dic[key_p].guess in self.bad_south:
                        pass
                    else:
                        bad_strip = True
                        break
                if not bad_strip:
                    # find those not adjacent to 'o': must be 'x'
                    for empty in empties:
                        keym = empty - 1, i
                        keyp = empty + 1, i
                        if Model.in_range(keym) and\
                           self.datum_dic[keym].guess == 'o' or\
                               Model.in_range(keyp) and\
                               self.datum_dic[keyp].guess == 'o':
                            pass
                        else:
                            good.append(empty)
                    if good:
                        for g in good:
                            key = g, i
                            self.hints.append([key, 'x'])

        for i, count in enumerate(self.column_ulaid):
            bad_strip = False
            # find strips requiring one more track
            if self.column_clues[i] != 1 and count == 1:
                # Needs 1 more 'o'
                for j in range(Sgg.grid_height):
                    empties = [k for k in range(Sgg.grid_height)
                               if self.datum_dic[i, k].guess is None]
                # check they are all blocked
                im1 = i - 1
                ip1 = i + 1
                good = []
                for empty in empties:
                    key_m = im1, empty
                    key_p = ip1, empty
                    if Model.in_range(key_m) and\
                       self.datum_dic[key_m].guess in self.bad_west or\
                       Model.in_range(key_p) and\
                       self.datum_dic[key_p].guess in self.bad_east:
                        pass
                    else:
                        bad_strip = True
                        break
                if not bad_strip:
                    # find those not adjacent to 'o': must be 'x'
                    for empty in empties:
                        keym = i, empty - 1
                        keyp = i, empty + 1
                        if Model.in_range(keym) and\
                           self.datum_dic[keym].guess == 'o' or\
                               Model.in_range(keyp) and\
                               self.datum_dic[keyp].guess == 'o':
                            pass
                        else:
                            good.append(empty)
                    if good:
                        for g in good:
                            key = i, g
                            self.hints.append([key, 'x'])

    def is_loop(self, key, prev_key, keys):
        """
        Not used. Was thought to be necessary, but leter abandoned.
        """
        points = self.good_steps[self.datum_dic[key].guess][0]
        for i, point in enumerate(points):
            key_1 = Model.add_keys(key, point)
            from_to_code = self.datum_dic[key].guess+str(tuple(point))
            good = self.from_to[from_to_code]
            if key_1 != prev_key and Model.in_range(key_1):
                if self.datum_dic[key_1].guess in good:
                    if key_1 in keys:
                        # a circuit
                        return True
                    # keep trying
                    keys.append(key_1)
                    return self.is_loop(key_1, key, keys)

        return False

    def print_ggrid(self):
        for y in range(Sgg.grid_height):
            line = ""
            for x in range(Sgg.grid_width):
                datum = self.ggrid[(x, y)]
                out = '.'
                if datum == 1:
                    out = '1'
                line += out
            print(line)

    def print_ggrid(self):
        for y in range(Sgg.grid_height):
            line = ""
            for x in range(Sgg.grid_width):
                line += self.ggrid[(x, y)]
            print(line)

    def make_step(self, key):
        shuffle(self.gpoints)
        for i, point in enumerate(self.gpoints):
            key_1 = Model.add_keys(key, point)
            if Model.in_range(key_1) and self.ggrid[key_1] == 'a':
                self.ggrid[key_1] = 'o'
                self.path.append(key_1)
                return self.make_step(key_1)
            if key_1[0] < 1 or key_1[0] >= Sgg.grid_width or\
               key_1[1] < 1 or key_1[1] >= Sgg.grid_height:
                break
        return 0

    def make_ggrid(self):
        for y in range(Sgg.grid_height):
            for x in range(Sgg.grid_width):
                key = (x, y)
                self.ggrid[key] = 'a'
        self.path = []
        self.gpoints = self.points[:]

    def to_track(self):
        def get_end(path):
            key = None
            if path[0] == 0:
                key = -1, path[1]
            elif path[0] == Sgg.grid_width - 1:
                key = Sgg.grid_width, path[1]
            elif path[1] == 0:
                key = path[0], -1
            elif path[1] == Sgg.grid_height - 1:
                key = path[0], Sgg.grid_width
            return key

        out = []
        key_0 = get_end(self.path[0])
        key_1 = get_end(self.path[-1])
        self.path = [key_0] + self.path + [key_1]
        for i in range(1, len(self.path) - 1):
            m = (self.path[i][0] - self.path[i - 1][0],
                 self.path[i][1] - self.path[i - 1][1])
            p = (self.path[i + 1][0] - self.path[i][0],
                 self.path[i + 1][1] - self.path[i][1])

            s = str(m) + str(p)
            t = self.gfrom_to[s]
            out.append(t)
            self.ggrid[self.path[i]] = t
        self.ggrid[self.path[1]] = self.ggrid[self.path[1]].upper()
        self.ggrid[self.path[-2]] = self.ggrid[self.path[-2]].upper()

    def to_puzzle(self):
        puzzle = [Sgg.grid_width, Sgg.grid_height, 99]
        for y in range(Sgg.grid_height):
            for x in range(Sgg.grid_width):
                key = (x, y)
                puzzle.append(self.ggrid[key])
        self.puzzle = puzzle

    def choose_start(self):
        mid = random.randint(2, min(Sgg.grid_width, Sgg.grid_height) - 2)
        edge = random.randint(0, 3)
        return tuple([[0, mid], [Sgg.grid_width - 1, mid],
                      [mid, 0], [mid, Sgg.grid_height - 1]][edge])

    def do_it(self):
        min_path_lens = {4: 9, 6: 18, 8: 32, 10: 40, 12: 44}
        tries_0 = 0
        got_puzzle = False
        while not got_puzzle:
            tries_0 += 1
            self.make_ggrid()
            key = self.choose_start()
            self.ggrid[key] = 'o'
            self.path.append(key)
            tries_1 = 0
            progress = True
            while progress:
                tries_1 += 1
                key_1 = self.make_step(key)
                if key_1 == 0:
                    progress = False
                    the_end = self.path[-1]
                    if len(self.path) > min_path_lens[Sgg.grid_width] and\
                       (the_end[0] == 0 or the_end[0] == Sgg.grid_width - 1 or
                        the_end[1] == 0 or the_end[1] == Sgg.grid_height - 1):
                        # ends at edge
                        got_puzzle = True

    def solve_puzzle_1(self):
        progress = True
        while progress and not self.is_finished():
            progress = False
            self.apply_algorithms()
            if self.hints:
                progress = True
                self.apply_hints()
                self.check_errors()
                if not self.status:
                    break
                self.check_finished()

    def get_unsolved(self):
        """
        Get the cells unsolved using the current clues
        """
        unsolved = []
        for key, datum in list(self.datum_dic.items()):
            if not datum.finished and datum.guess != 'x':
                if key in self.path:
                    unsolved.append(key)
        return unsolved

    def make_clues(self):
        """
        Puzzle is in self.puzzle with only the 2 ends marked as clues.
        Need to choose sufficient extra clues to make the grid solvable.
        self.path has the keys of the path.
        Try to solve with current clues, if fail randomly choose an unused
        cell as a clue and try again.
        """

        saved_puzzle = self.puzzle[:]
        used_clues = [self.path[1], self.path[-2]]
        self.get_puzzle_1()
        self.solve_puzzle_1()

        best_unsolved = self.get_unsolved()
        best_score = len(best_unsolved)
        if best_score == 0:
            return
        while not self.is_finished():
            new_best_unsolved = []
            for trial_key in best_unsolved:
                self.puzzle = saved_puzzle[:]
                # upper the selected clues
                for key in used_clues:
                    pi = key[1] * Sgg.grid_width + key[0] + 3
                    self.puzzle[pi] = self.puzzle[pi].upper()
                # upper the new trial key
                pi = trial_key[1] * Sgg.grid_width + trial_key[0] + 3
                self.puzzle[pi] = self.puzzle[pi].upper()
                self.get_puzzle_1()
                self.make_grid()
                self.solve_puzzle_1()

                unsolved = self.get_unsolved()
                if len(unsolved) <= best_score:
                    new_best_unsolved = unsolved
                    best_score = len(new_best_unsolved)
                    best_trial_key = trial_key
                    if best_score == 0:
                        return
            if new_best_unsolved:
                best_unsolved = new_best_unsolved
                used_clues.append(best_trial_key)

        return

    def generate_1(self, min_puzzles):
        if min_puzzles != 1:
            seed = 1
            random.seed(seed)
        puzzles = []
        # generate 100x6, 200x8, 100,10, 100x12
        puzzle_count = 0
        while puzzle_count < min_puzzles:
            self.do_it()
            self.to_track()
            self.to_puzzle()
            self.make_clues()
            puzzles = puzzles + self.puzzle
            puzzle_count += 1
        return puzzles

    def generate(self):
        needs = [[6, 100], [8, 200], [10, 100], [12, 100]]
        puzzles = []
        for need in needs:
            Sgg.grid_width = need[0]
            Sgg.grid_height = need[0]
            puzzles += self.generate_1(need[1])
        print(puzzles)
        # print('Algorithm counts ', self.alg_counts)
        # print('Alg counts ', Sgg.alg_counts)
        exit(1)

    def set_hint_status(self, status):
        self.hint_status = status

    def do_hint(self):
        self.check_errors()
        if self.status:
            self.apply_algorithms()

    def apply_algorithms(self):
        """
        Try all algorithms. If one succeeds, return.
        """
        self.do_counts()
        self.hints = []
        for self.algorithm, algorithm in enumerate(self.algorithms):
            algorithm()
            if self.hints:
                break

    def apply_hints(self):
        for hint in self.hints:
            key = hint[0]
            shape = hint[1]
            datum = self.datum_dic[key]
            if shape is not None:
                self.set_guess(key, shape, None)
            self.alg_counts[self.algorithm] += 1
            Sgg.alg_counts[self.algorithm] += 1

    def solve_puzzle(self):
        progress = True
        while progress and not self.is_finished():
            progress = False
            self.apply_algorithms()
            if self.hints:
                progress = True
                self.apply_hints()
                self.check_errors()
                if not self.status:
                    break
                self.check_finished()

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

    def print_grid(self):
        for y in range(Sgg.grid_height):
            line = ""
            for x in range(Sgg.grid_width):
                datum = self.datum_dic[(x, y)]
                out = '.'
                if datum.guess:
                    out = datum.guess
                line += out
            print(line)


class Controller():
    """
    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.active = False
        self.hint_requested = False
        self.error_detected = False
        self.view = View(root, self.settings)
        self.error_ids = []
        puzzle_number = random.randint(0,
                                       self.model_data.puzzle_count - 1)
        self.start_game(puzzle_number)

    def solve_puzzle(self):
        progress = True
        while progress and not self.model.is_finished():
            progress = False
            self.model.apply_algorithms()
            if self.model.hints:
                progress = True
                self.apply_hint()
                self.model.check_errors()
                if not self.model.status:
                    break
                self.model.check_finished()
        # print('Alg counts ', Sgg.alg_counts, sum(Sgg.alg_counts))

    def test(self):
        done = 0
        bad = []
        self.view.make_board()

        for puzzle_number in range(self.model_data.puzzle_count):
            self.model = Model(self.model_data)
            self.model.get_puzzle(puzzle_number)
            self.solve_puzzle()
            if not self.model.is_finished():
                bad.append(puzzle_number)
                print()
                self.model.print_grid()
                for key, datum in list(self.model.datum_dic.items()):
                    print(key, datum.status, datum.finished)
                print()
            done += 1

        print('done', done, 'bad', bad, len(bad))
        print('Algorithm counts ', self.model.alg_counts)
        print('Alg counts ', Sgg.alg_counts)
        exit(1)

    def start_game(self, puzzle_number):
        # self.test()
        self.model = Model(self.model_data)
        self.model.puzzle_number = puzzle_number
        # self.model.generate()

        if puzzle_number is not None and puzzle_number < 0:
            min_puzzles = 1
            self.model.generate_1(min_puzzles)
            self.model.get_puzzle_1()
        else:
            self.model.get_puzzle(puzzle_number)
        self.view.clock_init()
        self.view.clock_restart()
        self.view.make_board()
        self.view.board.model = self.model
        self.add_callbacks()
        self.view.board.draw_shapes(0)
        self.view.board.draw_edges()
        self.ready_for_hint()
        self.view.entry_box.clear()
        self.view.entry_box.write_box(str(self.model.puzzle_number))
        self.view.update_score_box(self.model.difficulty)
        self.model.set_hint_status(Sgg.GIVE_HINT)
        self.view.game_on()
        self.flasher = Flasher(self.view.clock.box.label,
                               self.master, self.settings)
        self.active = True

    def add_callbacks(self):
        self.view.add_menu_callback(self.service_menu)
        self.view.add_toolbar_callback(self.service_toolbar)
        self.view.board.add_callback(self.grid_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 == 'grid_size':
            choice = args[1]
            self.set_grid_size(choice)
        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:
            alg_index = self.model.algorithm
            self.view.do_tooltip(button_index, alg_index)
        elif mouse_index == 3:
            if 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:
                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 do_cursor(self, key, event):
        def in_box(box, xy):
            ret = True
            x = xy[0]
            y = xy[1]
            if x <= box[0] or x >= box[2] or y <= box[1] or y >= box[3]:
                ret = False
            return ret

        self.view.board.delete_tags('cursor')
        unit = self.settings.square_size
        unit2 = unit / 2
        unit3 = unit / 3
        unit23 = 2 * unit3
        x0 = key[0] * unit
        y0 = key[1] * unit
        ex = event.x
        ey = event.y - unit

        width = 20
        colour = self.settings.dic['cursor_colour']
        oob = [x0 + unit3, y0 + unit3, x0 + unit23, y0 + unit23]
        box_dic = {'b': [x0, y0 + unit3, x0 + unit, y0 + unit23],
                   'c': [x0 + unit3, y0, x0 + unit23, y0 + unit],
                   'd': [x0, y0 + unit23, x0 + unit3, y0 + unit],
                   'e': [x0, y0, x0 + unit3, y0 + unit3],
                   'f': [x0 + unit23, y0, x0 + unit, y0 + unit3],
                   'g': [x0 + unit23, y0 + unit23, x0 + unit, y0 + unit]}
        this_shape = None
        tags = 'cursor'
        for shape, box in list(box_dic.items()):
            if not in_box(oob, [ex, ey]):
                if in_box(box, [ex, ey]):
                    datum = self.model.datum_dic[key]
                    self.view.board.draw_shape(key, shape, colour, width, tags)
                    this_shape = shape
                    break
            else:
                if ey > y0 + unit2:
                    this_shape = 'x'
                    shape_id = self.view.board.draw_shape(key,
                                                          this_shape,
                                                          colour, width,
                                                          tags)
                    break
                else:
                    this_shape = 'o'
                    shape_id = self.view.board.draw_shape(key,
                                                          this_shape,
                                                          colour,
                                                          width,
                                                          tags)
                    break

        if event.type == '5' and this_shape is not None:
            # key released
            colour = self.settings.dic['track_colour']
            tags = 'guess'
            datum = self.model.datum_dic[key]
            if event.num == 1:
                PzlG.logger.logger.info("grid_event: add track " +
                                        str(this_shape) +
                                        ' ' + str(key))

                shape_id = datum.shape_id
                if shape_id is not None:
                    self.view.board.delete(shape_id)
                shape_id = self.view.board.draw_shape(key,
                                                      this_shape,
                                                      colour,
                                                      width,
                                                      tags)
                self.model.set_guess(key, this_shape, shape_id)

            elif event.num == 3:
                PzlG.logger.logger.info("grid_event: delete from " +
                                        ' ' + str(key))

                shape_id = self.model.unset_guess(key)
                if shape_id is not None:
                    self.view.board.delete(shape_id)
            self.check_finished()

    def grid_event(self, event, key):
        if self.active:
            self.do_cursor(key, event)

    def apply_hint(self):
        colour = self.settings.dic['track_colour']
        width = 20
        tags = 'guess'
        hint = self.model.hints[0]
        key = hint[0]
        shape = hint[1]
        shape_id = self.model.datum_dic[key].shape_id
        if shape_id is not None:
            self.view.board.delete(shape_id)
        shape_id = self.view.board.draw_shape(key, shape, colour, width, tags)
        self.model.set_guess(key, shape, shape_id)
        self.model.alg_counts[self.model.algorithm] += 1
        Sgg.alg_counts[self.model.algorithm] += 1

    def ready_for_hint(self):
        """
        If the puzzle not finished, check for errors
        and if found show them.
        Otherwise set hint mode the GIVE_HINT and set the display.
        """

        if not self.model.is_finished():
            self.model.check_errors()
            if self.model.errors:
                self.colour_errors()
                self.error_detected = True
            else:
                self.model.hint_status = Sgg.GIVE_HINT
                self.view.hint_off()
                self.view.thumbs_up()
                self.error_detected = False
            self.view.clock.set_show(True)

    def give_hint(self):
        """
        If in GIVE_HINT mode check for errors.
          If found show them to user
          Else find a hint, display it and change hint mode
        Else apply the hint, prepare for next and check if finished.
        """
        width = 20
        if self.model.hint_status == Sgg.GIVE_HINT:
            self.model.check_errors()
            if self.model.errors:
                self.view.thumbs_down()
                self.error_detected = True
                self.colour_errors()
            else:
                self.error_detected = False
                self.model.do_hint()
                if self.model.hints:
                    self.view.hint_on()
                    key = self.model.hints[0][0]
                    shape = '?'
                    colour = self.settings.dic['error_colour']
                    tags = 'hint'
                    shape_id = self.model.datum_dic[key].shape_id
                    if shape_id is not None:
                        self.view.board.delete(shape_id)
                    shape_id = self.view.board.draw_shape(key, shape, colour,
                                                          width, tags)
                    self.model.datum_dic[key].shape_id = shape_id
                    self.view.update_clock_box(self.model.algorithm)
                    self.view.clock.set_show(False)
                    self.model.hint_status = Sgg.APPLY_HINT
        else:
            key = self.model.hints[0][0]
            text = 'Hint: ' +\
                str(key[0]) + ',' + str(key[1])
            PzlG.logger.logger.info(text)
            self.apply_hint()
            self.ready_for_hint()
            self.check_finished()

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

    def show_solution(self):
        self.view.board.draw_shapes(1)
        self.active = False
        self.view.game_over()
        self.view.clock.set_show(True)
        self.view.clock_stop()

    def check_errors(self):
        if self.error_detected:
            # may have deleted edits
            self.model.check_errors()
            if self.model.errors:
                self.colour_errors()
                self.tidy_errors()

            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 tidy_errors(self):
        # we know there's an error already, so now we correct it
        self.view.board.delete_errors()
        self.model.undo_errors()
        self.error_detected = False
        self.view.thumbs_up()

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

    def set_grid_size(self, choice):
        size = int(choice)
        Sgg.grid_width = size
        Sgg.grid_height = size
        puzzle_number = -9
        self.model.puzzle_number = puzzle_number
        self.start_game(puzzle_number)

    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.start_game(self.model.puzzle_number)
        except KeyError:
            PzlG.logger.logger.error("set_colour " + choice)

    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)
                if puzzle_number < 0:
                    self.model.puzzle_number = puzzle_number
                    self.model.generate_1(1)
                self.start_game(puzzle_number)
            except ValueError:
                self.view.entry_box.clear()

    def tag_errors(self):
        self.error_ids = []
        for error in self.model.errors:
            datum = self.model.datum_dic[error]
            self.view.board.itemconfigure(datum.shape_id, tags='error')
            self.error_ids.append(datum.shape_id)

    def colour_errors(self):
        self.tag_errors()
        self.view.board.colour_errors()


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