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

# Copyright (C) 2021 Rod Engerstad

import random
from random import randint
from random import shuffle
import tkinter as tk

from marupeke_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 TextSquare


class Sgg(object):
    GIVE_HINT = 0
    APPLY_HINT = 1
    width = 10
    height = 10
    offsets = [[(0, -1), (0, -2)],
               [(1, -1), (2, -2)],
               [(1, 0), (2, 0)],
               [(1, 1), (2, 2)],
               [(0, 1), (0, 2)],
               [(-1, 1), (-2, 2)],
               [(-2, 0), (-2, 0)],
               [(-1, -1), (-2, -2)],
               [(0, -1), (0, 1)],
               [(1, 0), (-1, 0)],
               [(1, 1), (-1, -1)],
               [(-1, 1), (1, -1)]]
    offsets = [[(0, -1), (0, -2)],
               [(1, -1), (2, -2)],
               [(1, 0), (2, 0)],
               [(1, 1), (2, 2)],
               [(0, 1), (0, 2)],
               [(-1, 1), (-2, 2)],
               [(-1, 0), (-2, 0)],
               [(-1, -1), (-2, -2)],
               [(0, -1), (0, 1)],
               [(1, 0), (-1, 0)],
               [(1, 1), (-1, -1)],
               [(-1, 1), (1, -1)]]


class Settings(SettingsB):

    def __init__(self):
        SettingsB.__init__(self)
        self.dic = {}
        self.dic['font_size'] = '3'
        self.dic['text_colour'] = 'black'
        self.dic['grid_colour'] = 'white'
        self.dic['error_colour'] = 'red'
        self.menu_items = []
        self.preferences = Preferences(self.dic)
        self.preferences.load()
        self.load_menus()
        self.count = len(self.menu_items)
        self.set_fonts()
        self.make_help()
        self.make_tooltip()
        self.make_alg_help()

    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 make_alg_help(self):
        self.alg_text = [_("Algorithm: This cell is either in the middle of two identical solved cells or immediatley adjacent to a pair of identical solved cells.")]

    def make_help(self):
        self.help_text = _("Fill in each empty cell with either an O or an X, so that no more than two consecutive cells, either horizontally, vertically or diagonally, contain the same symbol. A right click sets an X. A left click sets a O. Middle mouse deletes either. The jigsaw puzzle icon contains a menu used for configuring the program. A left click on the next icon to the right selects a random puzzle, a right click the next puzzle in sequence. The wand requests a hint, a second click on the wand will perform the hint operation. The thumb checks if the puzzle contains errors and a second click will clear any errors. The sad smiley shows the solution. The entrybox shows the current puzzle number and can be used to select a new puzzle number. There are 1000 built-in puzzles.")

    def load_menus(self):
        self.menu_items.append(MenuItem(
            _('Font Size'),
            ['1',
             '2',
             '3',
             '4',
             '5',
             '6',
             '7',
             '8',
             '9',
             '10',
             '11',
             '12',
             '13'],
            '',
            ['1',
             '2',
             '3',
             '4',
             '5',
             '6',
             '7',
             '8',
             '9',
             '10',
             '11',
             '12',
             '13'],
            'font_size'
            ))
        self.menu_items.append(MenuItem(
            _('Save Settings'),
            '',
            '',
            '',
            'save_settings'
        ))
        self.menu_items.append(MenuItem(
            _('Help'),
            '',
            '',
            '',
            'help'
        ))
        self.menu_items.append(MenuItem(
            _('About'),
            '',
            '',
            '',
            'about'
        ))


class Board(tk.Frame):
    def __init__(self, parent, settings, model):
        self.settings = settings
        self.model = model
        grid_dic = model.datum_dic
        self.grid_widgets = []
        bg = self.settings.green
        tk.Frame.__init__(self, parent, relief='flat',
                          highlightbackground=self.settings.grey,
                          highlightthickness=2,
                          bd=2, bg=bg)

        fg = self.settings.dic['text_colour']
        bg = self.settings.dic['grid_colour']

        cell_index = 0
        for y in range(Sgg.height):
            for x in range(Sgg.width):
                guess = grid_dic[x, y].value
                if guess == -1:
                    text = 'O'
                elif guess == -2:
                    text = 'X'
                elif guess == 3:
                    text = ' '
                    bg = 'black'
                else:
                    text = ' '

                cell = TextSquare(self, self.settings, cell_index, fg,
                                  bg, text)
                bg = self.settings.dic['grid_colour']

                cell.grid(row=y, column=x)
                cell.grid_propagate(0)
                self.grid_widgets.append(cell)
                cell_index += 1
        self.grid(row=0, column=0)

    def my_destroy(self):
        for widget in self.grid_widgets:
            widget.my_destroy()
        self.destroy()

    def add_callback(self, func):
        for widget in self.grid_widgets:
            widget.callback = func

    def do_cell_update(self, key, text, fg, bg):
        widget_index = key[0] + key[1] * Sgg.width
        widget = self.grid_widgets[widget_index]
        update = [text, fg, bg]
        widget.my_update(update)
        self.update_idletasks()

    def apply_user_click(self, text, cell_index):
        key = (cell_index % Sgg.width, cell_index // Sgg.width)
        fg = self.settings.dic['text_colour']
        bg = self.settings.dic['grid_colour']
        self.do_cell_update(key, text, fg, bg)

    def mint(self):
        syms = [' ', 'O', 'X']
        hint = self.model.hint
        x = hint[0][0]
        y = hint[0][1]
        key = (x, y)
        guess = syms[hint[1]]
        fg = self.settings.dic['text_colour']
        bg = self.settings.dic['grid_colour']
        return key, guess, bg, fg

    def show_hint(self):
        """
        Show a hint on the grid.
        """
        if self.model.hint:
            key, guess, bg, fg = self.mint()
            text = '?'
            self.do_cell_update(key, text, fg, bg)

    def apply_hint(self):
        """
        Remove hint symbol from grid and reveal answer
        """
        if self.model.hint:
            key, guess, bg, fg = self.mint()
            self.do_cell_update(key, guess, fg, bg)

    def unshow_hint(self):
        """
        Remove hint symbol from grid
        """
        if self.model.hint:
            key, guess, bg, fg = self.mint()
            text = ' '
            self.do_cell_update(key, text, fg, bg)

    def show_errors(self):
        syms = {1: 'O', 2: 'X'}
        fg = self.settings.dic['error_colour']
        bg = self.settings.dic['grid_colour']
        for key in self.model.errors:
            text = syms[self.model.datum_dic[key].guess]
            self.do_cell_update(key, text, fg, bg)

    def unshow_errors(self):
        fg = self.settings.dic['text_colour']
        bg = self.settings.dic['grid_colour']
        for key in self.model.errors:
            text = ' '
            self.do_cell_update(key, text, fg, bg)


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.image_dic = {}
        self.make_image_dic()
        self.image_dic['start'] = [self.images.start_marupeke, 0, True]
        self.do_layout(self)
        self.entry_box.activate()

    def make_board(self):
        """
        draw the cells
        """

        if self.board is not None:
            self.board.my_destroy()
        self.board = Board(self.top_frame, self.settings, self.model)


class Datum(object):
    """
    A cell in the grid

    active means the cell is not blacked out and so is a given clue
    or requires to be solved.
    given means a provided clue
    """
    def __init__(self, value, key):
        sym_dic = {1: 1, 2: 2, 3: 3, -1: 1, -2: 2, 4: 4}
        self.key = key
        self.value = value
        self.guess = None
        self.neighbours = []
        self.answer = sym_dic[value]
        self.active = True
        self.given = False
        if value < 0:
            self.guess = sym_dic[value]
            self.given = True
        elif value == 3:
            self.active = False

    def is_active(self):
        return self.active

    def is_given(self):
        return self.given

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

    def set_guess_1(self, guess):
        self.guess = guess
        self.answer = guess
        self.value = 'o'

    def unset_guess(self):
        self.guess = None

    def unset_guess_1(self):
        self.guess = None
        self.value = 4

    def is_solved(self):
        """
        solved if active and guess != None
        solvable if active and guess is None
        """
        return self.guess is not None and self.is_active()

    def is_solvable(self):
        """
        solved if active and guess != None
        solvable if active and guess is None
        """
        return self.guess is None and self.is_active()

    def switch_off(self):
        if self.active:
            self.guess = None

    def switch_on(self):
        if self.active:
            self.guess = self.answer

    def is_correct(self, guess):
        return guess == self.answer and not self.given

    def is_wrong(self):
        return self.guess is not None and self.guess != self.answer


class Model(object):
    def __init__(self, model_data):
        self.model_data = model_data
        self.datum_dic = {}
        self.unsolved = []
        self.hint = []
        self.solved = []
        self.clues_used = []
        self.errors = []
        self.score = 0
        self.height = 10
        self.width = 10

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

    def get_puzzle(self, puzzle_index):
        puzzle = self.model_data.get_puzzle(puzzle_index)
        if puzzle:
            self.width = puzzle[0]
            self.height = puzzle[1]
            puzzle = puzzle[2:]
            self.init_grid(puzzle)
            self.set_neighbours()

    def apply_hint(self):
        """
        Add a hint to the grid.
        hint = [key, guess]
        """
        if self.hint:
            key = self.hint[0]
            guess = self.hint[1]
            self.datum_dic[key].set_guess(guess)
            self.update_unsolved(key)
            self.check_finished()

    def update_unsolved(self, key):
        if key in self.unsolved:
            self.unsolved.remove(key)

    def check_hint(self):
        key = self.hint[0]
        return self.datum_dic[key].is_correct(self.hint[1])

    def do_hint(self):
        """
        If not finished try all algorithms. If one succeeds, return.
        We get unsolved, because we do not keep it uptodate when
        the user is fiddling about.
        """
        self.hint = []
        if not self.is_finished():
            self.solve()
            if self.hint:
                if not self.check_hint():
                    # error detected
                    PzlG.logger.logger.error("solve error! (" +
                                             str(self.hint[0][0]) + ',' +
                                             str(self.hint[0][1]) + ')' + ' ' +
                                             str(self.hint[1]))
                    self.hint = []

    def solve(self):
        oppo_dic = {1: 2, 2: 1}
        self.hint = []
        for key in self.unsolved:
            datum = self.datum_dic[key]
            for nbrs in datum.neighbours:
                if nbrs[0] and nbrs[1]:
                    nbd_0 = self.datum_dic[nbrs[0]]
                    nbd_1 = self.datum_dic[nbrs[1]]
                    if nbd_0.is_solved() and nbd_1.is_solved():
                        if nbd_0.guess == nbd_1.guess:
                            self.hint = [key, oppo_dic[nbd_0.guess]]
                            return

    def do_solve(self):
        """
        Try to solve the grid, keeping unsolved uptodate as we progress.
        """

        while self.unsolved:
            self.solve()
            if self.hint:
                key = self.hint[0]
                guess = self.hint[1]
                datum = self.datum_dic[key]
                datum.set_guess(guess)
                self.update_unsolved(key)
            else:
                return

    def set_neighbours(self):
        for key, datum in list(self.datum_dic.items()):
            datum.neighbours = self.get_neighbours(key)

    def is_finished(self):
        return not self.unsolved

    def check_finished(self):
        return self.is_finished()

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

    def apply_user_click(self, mouse_index, key):
        """
        User has clicked at coord key
        """
        guesses = [0, 1, 0, 2]
        datum = self.datum_dic[key]
        if mouse_index == 2:
            if datum.is_correct(datum.guess):
                self.unsolved.append(key)
            datum.unset_guess_1()
        else:
            guess = guesses[mouse_index]
            datum.set_guess(guess)
            if datum.is_correct(datum.guess):
                self.update_unsolved(key)

    def in_range(self, key):
        x = key[0]
        y = key[1]
        return x > -1 and x < self.width and \
            y > -1 and y < self.height

    def get_neighbours(self, key):
        offsets = Sgg.offsets
        all_neighbours = []
        x = key[0]
        y = key[1]
        for offset_pair in offsets:
            neighbours = [(), ()]
            offset_0 = offset_pair[0]
            neighbour_0_key = (x + offset_0[0], y + offset_0[1])
            if self.in_range(neighbour_0_key):
                offset_1 = offset_pair[1]
                neighbour_1_key = (x + offset_1[0], y + offset_1[1])
                if self.in_range(neighbour_1_key):
                    neighbour_0_datum = self.datum_dic[neighbour_0_key]
                    neighbour_1_datum = self.datum_dic[neighbour_1_key]
                    if neighbour_0_datum.is_active() and\
                       neighbour_1_datum.is_active():
                        neighbours = [neighbour_0_key, neighbour_1_key]
            all_neighbours.append(neighbours)
        return all_neighbours

    def init_grid(self, puzzle):
        self.datum_dic = {}
        i = 0
        for y in range(self.height):
            for x in range(self.width):
                key = (x, y)
                self.datum_dic[key] = Datum(puzzle[i], key)
                if puzzle[i] == 1 or puzzle[i] == 2:
                    self.unsolved.append(key)
                i += 1

    """
    def print_me(self, puzzle, job):
        sym_dic = {1: 'O', 2: 'X', 3: '#', -1: 'o',
                   -2: 'x', 0: 'o', 4: '-', 5: '@'}
        if job == 0:
            i = 0
            for y in range(self.height):
                for x in range(self.width):
                    print sym_dic[puzzle[i]],
                    i += 1
            print
        elif job == 1:
            i = 0
            for y in range(self.height):
                for x in range(self.width):
                    if self.datum_dic[(x, y)].value == 4:
                        print '!',
                    elif self.datum_dic[(x, y)].value == 5:
                        print '@',
                    elif not self.datum_dic[(x, y)].is_active():
                        print '#',
                    elif not self.datum_dic[(x, y)].is_solved():
                        print '.',
                    else:
                        print sym_dic[self.datum_dic[(x, y)].guess],
                    i += 1
                print
        elif job == 2:
            for y in range(self.height):
                for x in range(self.width):
                    print self.datum_dic[(x, y)].guess,
                print

        elif job == 3:
            for y in range(self.height):
                for x in range(self.width):
                    print self.datum_dic[(x, y)].answer,
                print
    """
    def generate(self):
        puzzles = []
        size = [self.width, self.height]
        max_puzzles = 1000
        max_crap = 15
        seed = 0
        random.seed(seed)
        count = 0
        while True:
            self.generate_1()
            # count the number of 5s needed to make this puzzle consistent
            crap = self.count_5s()
            if crap < max_crap:
                puzzle = self.generate_2()
                if puzzle:
                    count += 1
                    puzzles += size
                    puzzles += puzzle
                    # print 'count', count
                    if count == max_puzzles:
                        self.save_puzzles(puzzles)
                        exit(1)

    def save_puzzles(self, puzzles):
        filename = 'tpuzzles.txt'
        with open(filename, "w") as fo:
            rec = ''
            for item in puzzles:
                rec += str(item)
                rec += ','
            fo.write(rec)

    def make_grid(self):
        self.datum_dic = {}
        sym = 4
        for y in range(self.height):
            for x in range(self.width):
                key = (x, y)
                self.datum_dic[key] = Datum(sym, key)
        self.set_neighbours()

    def generate_1(self):
        """
        1. make a grid with answer 4
        2. try to replace the 4s with O and X. If they clash with neighbours
           set them to 5
        """
        other_dic = {1: 2, 2: 1}
        self.make_grid()
        keys = list(self.datum_dic.keys())
        for key in keys:
            datum = self.datum_dic[key]
            sym = random.randint(1, 2)
            datum.set_guess_1(sym)
            if not self.sym_ok(key):
                sym = other_dic[sym]
                datum.set_guess_1(sym)
                if not self.sym_ok(key):
                    datum.set_guess_1(5)

    def sym_ok(self, key):
        """
        Try 0 or X to see if it clashes with any of key's neighbours
        """
        ret = True
        datum = self.datum_dic[key]
        for nbrs in datum.neighbours:
            if nbrs[0] and nbrs[1]:
                nbd_0 = self.datum_dic[nbrs[0]]
                nbd_1 = self.datum_dic[nbrs[1]]
                if nbd_0.is_solved() and nbd_1.is_solved():
                    if nbd_0.guess == nbd_1.guess and\
                       datum.guess == nbd_0.guess:
                        ret = False
        return ret

    def count_5s(self):
        count = 0
        for key, datum in list(self.datum_dic.items()):
            if datum.guess == 5:
                count += 1
        return count

    def analyse_clue(self, key):
        """
        If 2 neighbours of this datum can give a hint return them
        """
        ret = []
        datum = self.datum_dic[key]
        nbrs = list(datum.neighbours)
        shuffle(nbrs)
        for nbr in nbrs:
            if nbr[0] and nbr[1]:
                d0 = self.datum_dic[nbr[0]]
                d1 = self.datum_dic[nbr[1]]
                if d0.answer == d1.answer and\
                   d0.is_solvable() and d1.is_solvable():
                    ret = [nbr[0], nbr[1]]
                    break
        return ret

    def analyse_clue_1(self, key):
        """
        This key is solved. Does it have a neighbour that if solved
        would give a hint?
        """
        ret = []
        datum = self.datum_dic[key]
        nbrs = list(datum.neighbours)
        shuffle(nbrs)
        for nbr in nbrs:
            if nbr[0] and nbr[1]:
                d0 = self.datum_dic[nbr[0]]
                d1 = self.datum_dic[nbr[1]]
                if d0.answer == datum.answer:
                    if d0.is_solvable():
                        ret = [nbr[0]]
                        break
                if d1.answer == datum.answer:
                    if d1.is_solvable():
                        ret = [nbr[1]]
                        break
                if d0.answer == d1.answer:
                    if d0.is_solved() and d1.is_solvable():
                        ret = [nbr[1]]
                        break
                    if d0.is_solvable() and d1.is_solved():
                        ret = [nbr[0]]
                        break
        return ret

    def get_solved(self):
        self.solved = []
        for key, datum in list(self.datum_dic.items()):
            if datum.is_solved():
                self.solved.append(key)

    def get_primary_clues(self, clues):
        """
        Look through the list of solvable cells.
        If we find a cell which can be solved by revealing 2
        of its neighbours:
        1. remove it from the list
        For the 2 neighbours:
        1. remove them from the list of unsolved cells
        2. set them as solved
        3. add them to the list of clues used
        """
        progress = []
        while True:
            clue = clues.pop()
            clue_pair = self.analyse_clue(clue)
            if clue_pair:
                for aclue in clue_pair:
                    self.unsolved.remove(aclue)
                    self.datum_dic[aclue].switch_on()
                    self.clues_used.append(aclue)
                progress = clue_pair
                break
        return progress

    def get_secondary_clue(self):
        progress = []
        self.get_solved()
        shuffle(self.solved)
        for clue_try in self.solved:
            cluel = self.analyse_clue_1(clue_try)
            if cluel:
                clue = cluel[0]
                self.unsolved.remove(clue)
                self.datum_dic[clue].switch_on()
                self.clues_used.append(clue)
                progress = cluel
                break
        return progress

    def generate_2(self):
        """
        primary_clues are a pair of unsolved cells which are
        either adjacent or separated by a single cell, and are
        such that when set as clues will provide the solution
        to a nearby cell.
        A secondary_clue is an unsolved cell, which when set,
        will, in conjunction with a solved cell, provide a
        solution to a neighbouring cell.
        Here we randomly select primary_clues and then, with its
        aid solve as much of the grid as we can. Then we randomly
        select secondary_clues to see if we can completely solve the grid.
        If we select max_clues without solving the grid we give up and
        try another primary_clues.
        """
        puzzle = []
        max_clues = 15
        max_primaries = 10
        # full_puzzle is all answers (not 3 or 5) set to -1, -2
        full_puzzle = self.dump_grid()
        # clue_keys are the keys of all answers
        clue_keys = self.get_clue_keys(full_puzzle)
        # Try max_trials seed clue_pairs
        for trial in range(max_primaries):
            self.init_grid(full_puzzle)
            self.set_neighbours()
            self.switch_off_clues()
            clues = list(clue_keys)
            shuffle(clues)
            self.unsolved = list(clues)
            self.clues_used = []
            self.get_primary_clues(clues)
            self.do_solve()
            if self.is_finished():
                # pretty unlikely, but...
                puzzle = self.finalise(self.clues_used)
                return puzzle
            while len(self.clues_used) < max_clues:
                if self.get_secondary_clue():
                    self.do_solve()
                    if self.is_finished():
                        puzzle = self.finalise(self.clues_used)
                        return puzzle
                else:
                    break
        return []

    def dump_grid(self):
        look_dic = {1: -1, 2: -2, 5: 3, None: 3}
        puzzle = []
        for y in range(self.height):
            for x in range(self.width):
                puzzle.append(look_dic[self.datum_dic[x, y].guess])
        return puzzle

    def get_clue_keys(self, puzzle):
        """
        Get the keys for cells with X or O answers
        """
        clue_keys = []
        for item_index, item in enumerate(puzzle):
            if item < 0 and item != 3:
                key = (item_index % self.width, item_index / self.width)
                clue_keys.append(key)
        return clue_keys

    def switch_off_clues(self):
        for key, datum in list(self.datum_dic.items()):
            datum.switch_off()

    def clean_clues(self):
        discard = set([])
        for key, datum in list(self.datum_dic.items()):
            datum = self.datum_dic[key]
            if datum.is_solved():
                for nbrs in datum.neighbours:
                    if nbrs[0] and nbrs[1]:
                        nbd_0 = self.datum_dic[nbrs[0]]
                        nbd_1 = self.datum_dic[nbrs[1]]
                        if nbd_0.is_solved() and nbd_1.is_solved():
                            if nbd_0.guess == nbd_1.guess:
                                discard.add(key)
        return discard

    def finalise(self, clues_used):
        self.switch_off_clues()
        for clue in clues_used:
            if self.datum_dic[clue].is_active():
                self.datum_dic[clue].switch_on()

        discard = self.clean_clues()
        for key in discard:
            clues_used.remove(key)

        self.switch_off_clues()
        keys = list(self.datum_dic.keys())
        for clue in clues_used:
            if self.datum_dic[clue].is_active():
                self.datum_dic[clue].switch_on()
                keys.remove(clue)
        self.unsolved = keys
        puzzle = []
        for y in range(self.height):
            for x in range(self.width):
                key = (x, y)
                item = self.datum_dic[key].answer
                if item is None:
                    puzzle.append(3)
                elif key in clues_used:
                    puzzle.append(-item)
                else:
                    puzzle.append(item)
        return self.final_check(puzzle)

    def final_check(self, puzzle):
        """
        It is possible that the cleanup process has made the puzzle
        unsolvable
        """
        self.init_grid(puzzle)
        self.set_neighbours()
        self.do_solve()
        if not self.is_finished():
            puzzle = []
        return puzzle


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

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

    def test(self):
        done = 0
        bad = []
        for puzzle_number in range(self.model_data.puzzle_count):
            self.model = Model(self.model_data)
            self.model.get_puzzle(puzzle_number)
            self.model.do_solve()
            if not self.model.is_finished():
                bad.append(puzzle_number)
            done += 1
        # print done, bad
        exit(1)

    def start_game(self, puzzle_number):
        """
        Prepare for a game or generate puzzles
        """
        self.model = Model(self.model_data)
        # self.model.generate()
        # self.test()
        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)
        PzlG.logger.logger.info("start_game with puzzle number " +
                                str(self.puzzle_number))
        self.view.thumbs_up()
        self.view.model = self.model
        self.view.make_board()
        self.add_callbacks()
        self.ready_for_hint()
        self.view.game_on()
        self.error_detected = False
        self.view.entry_box.clear()
        self.view.entry_box.write_box(str(self.puzzle_number))
        self.flasher = Flasher(self.view.clock.box.label,
                               self.master, self.settings)
        self.active = True
        self.hint_status = Sgg.GIVE_HINT

    def add_callbacks(self):
        """
        Set up callbacks
        """
        self.view.add_menu_callback(self.service_menu)
        self.view.add_toolbar_callback(self.service_toolbar)
        self.view.entry_box.add_callback(self.get_puzzle_number)
        self.view.board.add_callback(self.cell_event)

    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 == 'save_settings':
            self.settings.preferences.save()
        elif name == 'about':
            self.view.about()
        elif name == 'help':
            self.view.help()

    def service_toolbar(self, button_index, mouse_index):
        """
        Respond to Toolbar choices
        """
        PzlG.logger.logger.info("service_toolbar " +
                                str(button_index) +
                                ' ' +
                                str(mouse_index))

        if mouse_index == 2:
            alg_index = None
            if self.model.hint:
                alg_index = 0
            self.view.do_tooltip(button_index, alg_index)
        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:
                if 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 cell_event(self, cell_index, mouse_index):
        """
        left mouse add zero
        middle mouse delete
        right mouse add one
        """
        key = (cell_index % Sgg.width, cell_index // Sgg.width)
        datum = self.model.datum_dic[key]

        if self.active and not datum.is_given() and datum.is_active():
            PzlG.logger.logger.info("cell_event " +
                                    str(mouse_index) +
                                    ' ' +
                                    str(key))

            # need to remove any active hint

            if self.model.hint:
                self.view.hint_off()
                self.view.board.unshow_hint()
                self.model.hint = []
                self.hint_status = Sgg.GIVE_HINT

            if mouse_index == 1:
                # player has left clicked on this cell
                # ie wants to add a 'O'
                self.model.apply_user_click(mouse_index, key)
                text = 'O'
                self.view.board.apply_user_click(text, cell_index)
            elif mouse_index == 2:
                # player has middle clicked on this cell
                # ie wants to delete
                self.model.apply_user_click(mouse_index, key)
                text = ' '
                self.view.board.apply_user_click(text, cell_index)
            elif mouse_index == 3:
                # player has right clicked on this cell
                # ie wants to add an 'X'
                self.model.apply_user_click(mouse_index, key)
                text = 'X'
                self.view.board.apply_user_click(text, cell_index)
            self.check_finished()
            self.model.check_errors()

    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.show_errors()
                self.error_detected = True
            else:
                self.hint_status = Sgg.GIVE_HINT
                self.view.hint_off()
                self.view.thumbs_up()
                self.error_detected = False
            self.model.hint = []

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

        if self.hint_status == Sgg.GIVE_HINT:
            self.model.check_errors()
            if self.model.errors:
                self.view.thumbs_down()
                self.show_errors()
                self.error_detected = True
            else:
                self.error_detected = False
                self.model.do_hint()
                if self.model.hint:
                    self.view.hint_on()
                    self.view.board.show_hint()
                    self.hint_status = Sgg.APPLY_HINT
        else:
            hint = self.model.hint
            PzlG.logger.logger.info("apply hint (" +
                                    str(hint[0][0]) + ',' +
                                    str(hint[0][1]) + ')' + ' ' +
                                    str(hint[1]))

            self.view.board.apply_hint()
            self.model.apply_hint()
            self.ready_for_hint()
            self.check_finished()

    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.tidy_error()
            self.ready_for_hint()
        else:
            self.model.check_errors()
            if self.model.errors:
                self.view.thumbs_down()
                self.error_detected = True
                self.show_errors()

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

    def show_solution(self):
        syms = {1: 'O', 2: 'X'}
        fg = self.settings.dic['text_colour']
        bg = self.settings.dic['grid_colour']
        for y in range(self.model.width):
            for x in range(self.model.width):
                datum = self.model.datum_dic[x, y]
                if datum.is_active():
                    text = syms[datum.answer]
                    self.view.board.do_cell_update((x, y), text,
                                                   fg, bg)

        self.view.game_over()
        self.view.clock_stop()
        self.active = False

    def show_errors(self):
        """
        Colour the error cells
        """
        self.view.board.show_errors()

    def tidy_error(self):
        """
        update view and undo errors
        """

        # do the display first
        self.view.board.unshow_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.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_puzzle_number(self, puzzle_number):
        """
        Select or set random 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


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