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

# Copyright (C) 2019 Rod Engerstad

import math
import random
from random import randint
from random import shuffle
from itertools import product
import colorsys
import tkinter as tk
import sys
import copy

from ripple_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 ScreenShot
from pzl_lib import OpenFileI
from pzl_lib import ViewB
from pzl_lib import Square
from pzl_lib import Egg
from pzl_lib import EditPair
from pzl_lib import UserEdits
from pzl_lib import Editor


class Sgg(object):
    grid_width = None
    grid_height = None
    GIVE_HINT = 0
    APPLY_HINT = 1
    CAUSE = 0
    EFFECT = 1
    SOLVE = 0
    HINT = 1
    ALGORITHM_NAMES = ['ER', 'FN', 'HS', 'NP',
                       'HP', 'NT', 'HT', 'FP']
    algorithm_counts = [0, 0, 0, 0, 0, 0, 0, 0]


class Settings(SettingsB):

    def __init__(self):
        SettingsB.__init__(self)
        self.original_square_size = 54
        self.square_size = self.original_square_size
        self.dic = {}
        self.dic['text_colour'] = '#000000'
        self.dic['cause_colour'] = '#00d341'
        self.dic['effect_colour'] = '#ff0000'
        self.dic['button_colour'] = '#82c026'
        self.dic['error_colour'] = '#ff0000'
        self.dic['font_size'] = '2'
        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'),
            _('Left mouse: give hint, right mouse: simple filter'),
            _('Check for errors, correct errors'),
            _('Show solution'),
            ]

    def make_alg_help(self):
        self.alg_text = [_("Exclusion rule: only one of each digit can occur in any polyomino, so once an answer is set all other occurrences of that digit in the same polyomino can be deleted."), _("Forbidden neighbour: No digit N can occur in an orthogonal cell which is within distance N cells of a cell for which N is the solution; so once an answer (N) is set, any occurences of that digit within N cells, up or down, can be removed."), _("Hidden single: all polyominoes must contain exactly one of each of the digits; if a cell contains the only remaining copy of a digit in a polyomino, that digit must be the answer for that cell, and any other digits in that cell can be deleted."), _("Naked pairs: all polyominoes must contain exactly one of each of the digits; if a pair of cells each contain only the same two digits those digits must be the answers for the two cells, and any other occurrences of those digits in the corresponding polyomino can be deleted."), _("Naked triples: all polyominoes must contain exactly one of each of the digits; if a trio of cells each contain only the same three digits those digits must be the answers for the three cells, and any other occurrences of those digits in the corresponding polyomino can be deleted."), _("Hidden pairs: all polyominoes must contain exactly one of each of the digits; if a pair of cells contain the only copies of two digits in a polyomino, those digits must be the answers for the two cells, and any other digits in those cells can be deleted."), _("Hidden triples: all polyominoes must contain exactly one of each of the digits; if three cells contain the only copies of three digits in a polyomino, those digits must be the answers for the three cells, and any other digits in those cells can be deleted."), _("Forbidden pair: This algorithm combines both rules. No digit N can occur in an orthogonal cell which is within distance N cells of a cell for which N is the solution. All polyominoes must contain exactly one of each of the digits. So, if a pair of cells each contain only the same two digits (N, M), those digits must be the answers for those two cells; and if the two cells are in the same row or column, even though we don't know which cell has which answer we can exclude nearby digits N and M in the same row or column.")]

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

    def load_menus(self):
        self.menu_items.append(MenuItem(
            _('Font Size'),
            [
                '2',
                '3',
                '5',
                '7',
                '8',
                '9',
                '10',
                '11',
                '12',
                '13'],
            '',
            [
                '2',
                '3',
                '5',
                '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, grid_dic):
        self.settings = settings
        self.grid_dic = grid_dic
        self.widget_dic = {}
        bg = self.settings.white
        tk.Frame.__init__(self, parent, relief='flat',
                          highlightbackground=self.settings.grey,
                          highlightthickness=2,
                          bd=2, bg=bg)
        bg = 'white'
        for y, x in product(list(range(Sgg.grid_height)),
                            list(range(Sgg.grid_width))):
            key = (x, y)
            datum = self.grid_dic[key]
            widget = Square(self, self.settings,
                            datum, datum.colour,
                            key, y, x)
            self.widget_dic[key] = widget
        self.grid(row=0, column=0)

    def my_destroy(self):
        self.destroy()

    def add_candidate_callback(self, func):
        for key, widget in list(self.widget_dic.items()):
            candidates = widget.candidates
            for candidate in candidates:
                candidate.callback = func

    def get_square_colour(self, square_index):
        return self.grid_dic[square_index].colour

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


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

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

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

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

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

    def update_clock_box(self, algorithm):
        value = '  '
        if algorithm is not None:
            value = Sgg.ALGORITHM_NAMES[algorithm]
        self.clock.box.set_text(value)


class Polyomino(object):
    def __init__(self, shape_id, pairs_dic):
        self.shape_id = shape_id
        self.datum_dic = {}
        self.givens_dic = {}
        self.size = 0
        self.solved = False
        for key, pair in list(pairs_dic.items()):
            this_id = pair[0]
            if this_id == shape_id:
                answer = pair[1]
                datum = Datum(shape_id, key, answer, self.size)
                self.datum_dic[key] = datum
                if answer < 0:
                    self.givens_dic[key] = datum.answer
                self.size += 1
        if self.size == 1:
            self.solved = True
        for key, datum in list(self.datum_dic.items()):
            datum.update_size(self.size)

    """
    def print_me(self):
        print 'Polyomino', self.shape_id, self.size
        for key, datum in list(self.datum_dic.items()):
            print key, datum.answer, datum.guess, datum.candidates
    """

    def do_single(self):
        self.solved = True
        if self.size == 1:
            datum = list(self.datum_dic.values())[0]
            datum.do_single()

    def is_solved(self):
        self.solved = True
        for key, datum in list(self.datum_dic.items()):
            if datum.guess is None:
                self.solved = False
                break
        return self.solved

    def set_guess(self, key, answer):
        self.datum_dic[key].set_guess(answer)
        self.is_solved()

    def add_given(self, key):
        datum = self.datum_dic[key]
        self.givens_dic[key] = datum.answer
        datum.make_given()

    def other_candidates(self, keys):
        others_keys = []
        other_cands = set([])
        for key, datum in list(self.datum_dic.items()):
            if key not in keys:
                others_keys.append(key)
        for key in others_keys:
            other_cands.update(self.datum_dic[key].candidates)
        return other_cands


class Datum(object):
    """
    The callback tells Polyomino that a change has occurred
    and Polyomino checks to see if the whole Polyomino is solved.
    """
    def __init__(self, shape_id, key, answer, size):
        self.shape_id = shape_id
        self.key = key
        # index needed by pzl_lib
        self.index = self.key
        self.func = None
        self.answer = abs(answer) - 1
        self.guess = None
        self.given = False
        self.neighbours = None
        self.size = size
        self.colour = 'green'
        candidates = list(range(self.size))
        self.candidates = set(candidates)
        if answer < 0:
            self.given = True
            self.guess = self.answer
            self.candidates = set([])

    def update_size(self, size):
        self.size = size
        if self.size > 1 and not self.given:
            candidates = list(range(self.size))
            self.candidates = set(candidates)
            self.is_consistent()

    def add_callback(self, func):
        self.func = func

    def make_given(self):
        self.guess = self.answer
        self.given = True
        self.candidates = set([])

    def reset_guess(self):
        self.guess = None
        self.given = False
        candidates = list(range(self.size))
        self.candidates = set(candidates)
        self.func(self.shape_id)

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

    def set_answer(self):
        self.guess = self.answer
        self.candidates = set([])

    def do_single(self):
        answer = 0
        self.guess = answer
        self.given = True
        self.candidates = set([])

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

            if self.answer != self.guess:
                error += 4
        """
        if error != 0:
            print self.key, error
        """
        return error

    def del_candidate(self, candidate):
        """
        return guess if deleting last candidate
        """

        ret = -1
        if not self.given:
            if candidate in self.candidates:
                self.candidates.remove(candidate)
            if self.guess is None:
                if len(self.candidates) == 1:
                    candidate = self.candidates.pop()
                    self.set_guess(candidate)
                    ret = candidate
        return ret

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

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

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

    def set_guess(self, guess):
        self.guess = guess
        self.candidates = set([])
        self.func(self.shape_id)

    def unset_guess(self):
        self.guess = None
        candidates = list(range(self.size))
        self.candidates = set(candidates)
        self.func(self.shape_id)

    def __str__(self):
        return '(%g %g %g)' % (self.key[0], self.key[1], self.shape_id)


class Model(object):

    def __init__(self, model_data):
        self.polyomino_dic = {}
        self.grid_dic = {}
        self.model_data = model_data
        self.algorithm_counts = [0] * 9
        self.algorithm_scores = [0, 1, 3, 5, 6, 7, 8, 10, 10]
        self.algorithm = None
        self.hint_status = Sgg.GIVE_HINT
        self.difficulty = None
        self.status = True
        self.errors = []
        self.user_edits = UserEdits()
        self.editor = Editor(Sgg.HINT)

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

    def get_hint_status(self):
        return self.hint_status

    def zero_algorithm_counts(self):
        length = len(self.algorithm_counts)
        self.algorithm_counts = [0] * length
        #Sgg.algorithm_counts = [0] * length

    def update_algorithm_counts(self, algorithm, count):
        self.algorithm_counts[algorithm] += count
        Sgg.algorithm_counts[algorithm] += count

    def get_difficulty(self):
        difficulty = 0
        for i in range(len(self.algorithm_counts)):
            difficulty += self.algorithm_counts[i] * self.algorithm_scores[i]
        self.difficulty = difficulty

    def check_errors(self):
        self.status = True
        self.errors = []
        for key, datum in list(self.grid_dic.items()):
            status = datum.is_consistent()
            if status != 0:
                self.errors.append(key)
        if self.errors:
            self.status = False

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

    def get_neighbours(self, key):
        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

        neighbours = []
        x = key[0]
        y = key[1]
        neighbour_key = (x - 1, y - 1)
        if in_range(neighbour_key):
            neighbour_datum = self.grid_dic[neighbour_key]
            neighbours.append(neighbour_datum)

        neighbour_key = (x, y - 1)
        if in_range(neighbour_key):
            neighbour_datum = self.grid_dic[neighbour_key]
            neighbours.append(neighbour_datum)

        neighbour_key = (x + 1, y - 1)
        if in_range(neighbour_key):
            neighbour_datum = self.grid_dic[neighbour_key]
            neighbours.append(neighbour_datum)

        neighbour_key = (x + 1, y)
        if in_range(neighbour_key):
            neighbour_datum = self.grid_dic[neighbour_key]
            neighbours.append(neighbour_datum)

        neighbour_key = (x + 1, y + 1)
        if in_range(neighbour_key):
            neighbour_datum = self.grid_dic[neighbour_key]
            neighbours.append(neighbour_datum)

        neighbour_key = (x, y + 1)
        if in_range(neighbour_key):
            neighbour_datum = self.grid_dic[neighbour_key]
            neighbours.append(neighbour_datum)

        neighbour_key = (x - 1, y + 1)
        if in_range(neighbour_key):
            neighbour_datum = self.grid_dic[neighbour_key]
            neighbours.append(neighbour_datum)

        neighbour_key = (x - 1, y)
        if in_range(neighbour_key):
            neighbour_datum = self.grid_dic[neighbour_key]
            neighbours.append(neighbour_datum)

        return neighbours

    def show_solution(self):
        updates = []
        for y, x in product(list(range(Sgg.grid_height)),
                            list(range(Sgg.grid_width))):
            key = (x, y)
            datum = self.grid_dic[key]
            datum.set_answer()
            updates.append([key, [1, [datum.guess]]])
        return updates

    def is_finished(self):
        if self.status:
            ret = True
            for shape_id, polyomino in list(self.polyomino_dic.items()):
                if not polyomino.solved:
                    ret = False
                    break
        else:
            ret = False
        return ret

    def add_datum_callback(self):
        for key, datum in list(self.grid_dic.items()):
            datum.add_callback(self.datum_callback)

    def datum_callback(self, shape_id):
        polyomino = self.polyomino_dic[shape_id]
        polyomino.is_solved()

    def proximity_1(self, exclusions_dic, edge_index, direction):

        def make_key(item_0, item_1, direction):
            if direction == 0:
                key = (item_0, item_1)
            else:
                key = (item_1, item_0)
            return key

        cause_pairs = []
        effect_pairs = []
        for effect, causes in list(exclusions_dic.items()):
            key = make_key(effect, edge_index, direction)
            datum_e = self.grid_dic[key]
            if datum_e.guess is None:
                for cause in causes:
                    key = make_key(cause, edge_index, direction)
                    datum_c = self.grid_dic[key]
                    guess = datum_c.guess
                    if guess in datum_e.candidates:
                        cause_candidates = [guess]
                        cause_pairs = EditPair(datum_c, cause_candidates)
                        effect_candidates = [guess]
                        effect_pair = EditPair(datum_e,
                                               effect_candidates)
                        effect_pairs.append(effect_pair)
                        break
            if effect_pairs:
                break
        return cause_pairs, effect_pairs

    def forbidden_neighbour(self):
        """
        Check distance between solved candidates.
        """

        def add_exclusions(cause, guess, span, exclusions_dic):
            """
            At position cause in a span (in x or y) we have found
            a set guess of value guess.
            Here we accumulate lists of the positions which
            are in range guess either side of cause.
            exclusions_dic is indexed on position in the span, and
            for each position we list of the positions of known guesses.
            The calling program uses the dic to find candidates which can
            be deleted. The dic keys are affected positions and their
            lists are potential cause positions.
            When we create the dic we don't record
            which guess is at the cause position or which candidates are
            in affected positions: we just know they are within range so that
            if they have the guess candidate it can be deleted.
            """
            for item in range(cause - guess - 1, cause + guess + 2):
                if item > -1 and item < span:
                    exclusions_dic.setdefault(item, []).append(cause)

        effect_pairs = []
        for item_y in range(Sgg.grid_height):
            exclusions_dic = {}
            for item_x in range(Sgg.grid_width):
                for key, datum in list(self.grid_dic.items()):
                    if key[1] == item_y and key[0] == item_x:
                        guess = datum.guess
                        if guess is not None:
                            add_exclusions(item_x, guess,
                                           Sgg.grid_width,
                                           exclusions_dic)
            cause_pairs, effect_pairs = self.proximity_1(exclusions_dic,
                                                         item_y, 0)
            if effect_pairs:
                break
        if not effect_pairs:
            for item_x in range(Sgg.grid_width):
                exclusions_dic = {}
                for item_y in range(Sgg.grid_height):
                    for key, datum in list(self.grid_dic.items()):
                        if key[1] == item_y and key[0] == item_x:
                            guess = datum.guess
                            if guess is not None:
                                add_exclusions(item_y, guess,
                                               Sgg.grid_height,
                                               exclusions_dic)
                cause_pairs, effect_pairs = self.proximity_1(exclusions_dic,
                                                             item_x, 1)
                if effect_pairs:
                    break
        if effect_pairs:
            self.editor.add([cause_pairs], effect_pairs,
                            self.algorithm)

    def exclusion_rule(self):
        """
        If a polyomino has a guess set remove that guess from all
        the polyominos candidates.
        """
        for shape_id, polyomino in list(self.polyomino_dic.items()):
            if not polyomino.solved:
                datum_dic = polyomino.datum_dic
                for key_0, datum_0 in list(datum_dic.items()):
                    guess = datum_0.guess
                    if guess is not None:
                        cause_candidates = [guess]
                        cause_pairs = EditPair(datum_0, cause_candidates)
                        effect_pairs = []
                        for key_1, datum_1 in list(datum_dic.items()):
                            if key_1 != key_0:
                                candidates = datum_1.candidates
                                if guess in candidates:
                                    effect_candidates = [guess]
                                    effect_pair = EditPair(datum_1,
                                                           effect_candidates)
                                    effect_pairs.append(effect_pair)
                        if effect_pairs:
                            self.editor.add([cause_pairs], effect_pairs,
                                            self.algorithm)

    def hidden_single(self):
        """
        Find datum with unique candidate (hidden_single)
        """
        for shape_id, polyomino in list(self.polyomino_dic.items()):
            if not polyomino.solved:
                datum_dic = polyomino.datum_dic
                for key_0, datum_0 in list(datum_dic.items()):
                    others = set([])
                    for key_1, datum_1 in list(datum_dic.items()):
                        if key_1 != key_0:
                            others = others.union(datum_1.candidates)
                    diff = datum_0.candidates.difference(others)
                    if len(diff) == 1:
                        effect_candidates = datum_0.candidates.difference(diff)
                        effect_pair = EditPair(datum_0,
                                               list(effect_candidates))
                        guess = diff.pop()
                        cause_candidates = [guess]
                        cause_pair = EditPair(datum_0,
                                              list(cause_candidates))
                        self.editor.add([cause_pair],
                                        [effect_pair],
                                        self.algorithm)

    def naked_effects(self, datum_dic, keys, cause_candidates,
                      cause_pairs):
        effect_pairs = []
        for key, datum in list(datum_dic.items()):
            if key not in keys:
                candidates = datum.candidates
                effect_candidates = cause_candidates.intersection(candidates)
                if effect_candidates:
                    effect_pair = EditPair(datum, list(effect_candidates))
                    effect_pairs.append(effect_pair)
        if effect_pairs:
            self.editor.add(cause_pairs, effect_pairs, self.algorithm)

    def hidden_effects(self, diff, cause_keys,
                       cause_pairs):
        ret = False
        effect_pairs = []
        for key in cause_keys:
            datum = self.grid_dic[key]
            effect_candidates = list(datum.other_candidates(diff))
            if effect_candidates:
                effect_pair = EditPair(datum, list(effect_candidates))
                effect_pairs.append(effect_pair)
        if effect_pairs:
            self.editor.add(cause_pairs, effect_pairs, self.algorithm)
            ret = True
        return ret

    def naked_pair(self):
        """
        Find pair of datum, each with the same pair of candidates
        and no others.
        """
        for shape_id, polyomino in list(self.polyomino_dic.items()):
            if not polyomino.solved:
                datum_dic = polyomino.datum_dic

                for item_0, item_1 in product(list(datum_dic.items()),
                                              list(datum_dic.items())):
                    if item_0 != item_1:
                        key_0 = item_0[0]
                        datum_0 = item_0[1]
                        c_0 = datum_0.candidates
                        key_1 = item_1[0]
                        datum_1 = item_1[1]
                        c_1 = datum_1.candidates
                        if c_0 and c_1:
                            c_union = c_0.union(c_1)
                            if len(c_union) == 2:
                                candidates = list(c_union)
                                cp_0 = EditPair(datum_0, candidates)
                                cp_1 = EditPair(datum_1, candidates)
                                cause_pairs = [cp_0,
                                               cp_1]
                                self.naked_effects(datum_dic,
                                                   [key_0, key_1],
                                                   c_union,
                                                   cause_pairs)

    def naked_triple(self):
        """
        Find trio of datum, each with the same trio of candidates
        and no others.
        """
        for shape_id, polyomino in list(self.polyomino_dic.items()):
            if not polyomino.solved:
                datum_dic = polyomino.datum_dic

                for item_0, item_1, item_2 in product(list(datum_dic.items()),
                                                      list(datum_dic.items()),
                                                      list(datum_dic.items())):
                    if item_0 != item_1 and \
                       item_0 != item_2 and \
                                 item_1 != item_2:
                        key_0 = item_0[0]
                        datum_0 = item_0[1]
                        c_0 = datum_0.candidates
                        key_1 = item_1[0]
                        datum_1 = item_1[1]
                        c_1 = datum_1.candidates
                        key_2 = item_2[0]
                        datum_2 = item_2[1]
                        c_2 = datum_2.candidates
                        if c_0 and c_1 and c_2:
                            c_union = c_0.union(c_1.union(c_2))
                            if len(c_union) == 3:
                                candidates = list(c_union)
                                cp_0 = EditPair(datum_0, candidates)
                                cp_1 = EditPair(datum_1, candidates)
                                cp_2 = EditPair(datum_2, candidates)
                                cause_pairs = [cp_0,
                                               cp_1,
                                               cp_2]
                                self.naked_effects(datum_dic,
                                                   [key_0, key_1, key_2],
                                                   c_union,
                                                   cause_pairs)

    def hidden_pair(self):
        """
        Find pair of datum, each with the same pair of candidates
        which do not occur in any others in their polyomino
        """
        for shape_id, polyomino in list(self.polyomino_dic.items()):
            if not polyomino.solved:
                datum_dic = polyomino.datum_dic
                for item_0, item_1 in product(list(datum_dic.items()),
                                              list(datum_dic.items())):
                    if item_0 != item_1:
                        key_0 = item_0[0]
                        datum_0 = item_0[1]
                        c_0 = datum_0.candidates
                        key_1 = item_1[0]
                        datum_1 = item_1[1]
                        c_1 = datum_1.candidates
                        if c_0 and c_1:
                            c_union = c_0.union(c_1)
                            c_other = set([])
                            for key_3, datum_3 in list(datum_dic.items()):
                                if key_3 != key_0 and \
                                   key_3 != key_1:
                                    c_3 = datum_3.candidates
                                    c_other = c_other.union(c_3)
                            diff = c_union.difference(c_other)
                            if c_other and len(diff) == 2:
                                # there's 2 of them, so they are hidden pairs
                                # and all other candidates can be removed from
                                # their cells
                                cause_0 = EditPair(datum_0, diff)
                                cause_1 = EditPair(datum_1, diff)
                                causes = [cause_0, cause_1]
                                cause_keys = [key_0, key_1]
                                if self.hidden_effects(diff,
                                                       cause_keys,
                                                       causes):
                                    break

    def hidden_triple(self):
        """
        Find trio of datum, each with the same trio of candidates
        which do not occur in any others in their polyomino
        """
        for shape_id, polyomino in list(self.polyomino_dic.items()):
            if not polyomino.solved:
                datum_dic = polyomino.datum_dic
                for item_0, item_1, item_2 in product(list(datum_dic.items()),
                                                      list(datum_dic.items()),
                                                      list(datum_dic.items())):
                    if item_0 != item_1 and \
                       item_0 != item_2 and \
                                 item_1 != item_2:
                        key_0 = item_0[0]
                        datum_0 = item_0[1]
                        c_0 = datum_0.candidates
                        key_1 = item_1[0]
                        datum_1 = item_1[1]
                        c_1 = datum_1.candidates
                        key_2 = item_2[0]
                        datum_2 = item_2[1]
                        c_2 = datum_2.candidates
                        if c_0 and c_1 and c_2:
                            c_union = c_0.union(c_1.union(c_2))
                            c_other = set([])
                            for key_3, datum_3 in list(datum_dic.items()):
                                if key_3 != key_0 and \
                                   key_3 != key_1 and \
                                            key_3 != key_2:
                                    c_3 = datum_3.candidates
                                    c_other = c_other.union(c_3)
                            diff = c_union.difference(c_other)
                            if c_other and len(diff) == 3:
                                # there's 3 of them, so they are hidden triples
                                # and all other candidates can be removed from
                                # their cells
                                cause_0 = EditPair(datum_0, diff)
                                cause_1 = EditPair(datum_1, diff)
                                cause_2 = EditPair(datum_2, diff)
                                causes = [cause_0, cause_1, cause_2]
                                cause_keys = [key_0, key_1, key_2]
                                if self.hidden_effects(diff,
                                                       cause_keys,
                                                       causes):
                                    break

    def fp1(self, datum, direction):
        """
        direction 0 = left, 1 = right, 2 = up, 3 = down
        in_range
        """
        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

        incs = [[-1, 0], [1, 0], [0, -1], [0, 1]]
        candidates = datum.candidates
        inc_x = incs[direction][0]
        inc_y = incs[direction][1]
        effect_pair = []
        for candidate in candidates:
            x = datum.key[0]
            y = datum.key[1]
            for item in range(candidate + 1):
                y += inc_y
                x += inc_x
                if in_range((x, y)):
                    datum_1 = self.grid_dic[(x, y)]
                    if datum_1.shape_id != datum.shape_id and\
                       candidate in datum_1.candidates:
                        effect_pair = [datum_1, candidate]
                        break
            if effect_pair:
                break
        return effect_pair

    def forbidding_pair(self):
        """
        Find pair of datum, each with the same pair of candidates
        and no others. If those 2 datum are in the same row or column
        candidates in other polyominos in those rows and columns are
        restricted.
        """
        effect_pair = []
        for shape_id, polyomino in list(self.polyomino_dic.items()):
            if not polyomino.solved:
                datum_dic = polyomino.datum_dic
                keys = list(datum_dic.keys())
                for item_0, key_0 in enumerate(keys):
                    for key_1 in keys[item_0 + 1:]:
                        if (key_0[0] == key_1[0] or\
                            key_0[1] == key_1[1]):
                            datum_0 = datum_dic[key_0]
                            datum_1 = datum_dic[key_1]
                            c_0 = datum_0.candidates
                            c_1 = datum_1.candidates
                            cii = c_0.intersection(c_1)
                            if c_0 == c_1 and len(cii) == 2:
                                if key_0[0] == key_1[0]:
                                    if key_0[1] < key_1[1]:
                                        effect_pair = self.fp1(datum_0, 3)
                                        if effect_pair:
                                            break
                                        effect_pair = self.fp1(datum_1, 2)
                                        if effect_pair:
                                            break
                                    else:
                                        effect_pair = self.fp1(datum_0, 2)
                                        if effect_pair:
                                            break
                                        effect_pair = self.fp1(datum_1, 3)
                                        if effect_pair:
                                            break
                                else:
                                    if key_0[0] < key_1[0]:
                                        effect_pair = self.fp1(datum_0, 1)
                                        if effect_pair:
                                            break
                                        effect_pair = self.fp1(datum_1, 0)
                                        if effect_pair:
                                            break
                                    else:
                                        effect_pair = self.fp1(datum_0, 0)
                                        if effect_pair:
                                            break
                                        effect_pair = self.fp1(datum_1, 1)
                                        if effect_pair:
                                            break
                    if effect_pair:
                        break
                if effect_pair:
                    cause_pairs = []
                    candidate = effect_pair[1]
                    candidates = list(c_0)
                    cause_pair = EditPair(datum_0, candidates)
                    cause_pairs.append(cause_pair)
                    cause_pair = EditPair(datum_1, candidates)
                    cause_pairs.append(cause_pair)
                    effect_pairs = EditPair(effect_pair[0], [candidate])
                    self.editor.add(cause_pairs,
                                    [effect_pairs],
                                    self.algorithm)

    def do_hint(self):
        self.editor.set_mode(Sgg.HINT)
        self.algorithm = 0
        self.exclusion_rule()
        if not self.editor.got_data():
            self.algorithm = 1
            self.forbidden_neighbour()
        if not self.editor.got_data():
            self.algorithm = 2
            self.hidden_single()
        if not self.editor.got_data():
            self.algorithm = 3
            self.naked_pair()
        if not self.editor.got_data():
            self.algorithm = 4
            self.hidden_pair()
        if not self.editor.got_data():
            self.algorithm = 5
            self.naked_triple()
        if not self.editor.got_data():
            self.algorithm = 6
            self.hidden_triple()
        if not self.editor.got_data():
            self.algorithm = 7
            self.forbidding_pair()

    def exhaust(self):
        progress = False
        sf_progress = False
        if self.editor.guess_set:
            sf_progress = True
            while sf_progress:
                sf_progress = False
                self.algorithm = 0
                self.exclusion_rule()

                #if not self.editor.got_data():
                #    self.algorithm = 1
                #    self.forbidden_neighbour()
                if self.editor.got_data():
                    progress = True
                    sf_progress = True
                    self.manage_edits(self.algorithm)
        self.editor.guess_set = False
        return progress

    def exhaust_puzzle(self):
        self.check_errors()
        if self.status:
            self.editor.set_mode(Sgg.SOLVE)
            self.editor.guess_set = True
            self.exhaust()
            self.editor.set_mode(Sgg.HINT)

    def manage_edits(self, algorithm):
        """
        In order to use the same code to handle solving and
        providing hints, and to satisfy the requirement that
        exhaust must be applied before the algorithms
        will work we use manage_edits.
        """
        if self.editor.mode == Sgg.SOLVE:
            self.editor.apply()
            count = len(self.editor.changed_datum)

            if count > 0:
                self.update_algorithm_counts(algorithm, count)
        self.editor.reset()

    def solve_puzzle(self):
        ret = False
        self.editor.mode = Sgg.SOLVE
        self.editor.guess_set = True
        self.zero_algorithm_counts()
        self.difficulty = 0
        sf_progress = True
        while sf_progress:
            sf_progress = self.exhaust()
            if not self.is_finished():
                self.algorithm = 1
                self.forbidden_neighbour()
                if not self.editor.got_data():
                    self.algorithm = 2
                    self.hidden_single()
                if not self.editor.got_data():
                    self.algorithm = 3
                    self.naked_pair()
                if not self.editor.got_data():
                    self.algorithm = 4
                    self.hidden_pair()
                if not self.editor.got_data():
                    self.algorithm = 5
                    self.naked_triple()
                if not self.editor.got_data():
                    self.algorithm = 6
                    self.hidden_triple()
                if not self.editor.got_data():
                    self.algorithm = 7
                    self.forbidding_pair()

                if self.editor.got_data():
                    sf_progress = True
                    self.manage_edits(self.algorithm)
            else:
                break
        if self.is_finished():
            ret = True
            self.get_difficulty()
        return ret

    def apply_hint(self):
        if self.editor.got_data():
            for edit_pair in self.editor.effects:
                datum_index = edit_pair.datum.key
                candidates = edit_pair.candidates
                datum = self.grid_dic[datum_index]
                for candidate in candidates:
                    datum.del_candidate(candidate)
                    self.algorithm_counts[self.algorithm] += 1

    def apply_user_edits(self, datum_index, candidate, mouse_index):
        datum = self.grid_dic[datum_index]
        candidates = [candidate]
        if candidate is not None and not datum.given:
            # always start a new apply_ops
            self.user_edits.apply_ops = []
            if mouse_index == 1:
                apply_op = Egg.DELETE_CANDIDATES
                # if last but 1 candidate, this is also a set guess
                # so we need to know which candidates to delete
                if len(datum.candidates) == 2:
                    # its a set_guess too
                    candidates = list(datum.candidates)
                edit_pair = EditPair(datum, candidates)
                # do the edit
                guess = datum.del_candidate(candidate)
                if self.user_edits.edits or datum.is_consistent() != 0:
                    self.user_edits.add([edit_pair, apply_op])
                self.user_edits.add_op([edit_pair, apply_op])
                if guess != -1:
                    apply_op = Egg.SET_GUESS
                    edit_pair = EditPair(datum, [guess])
                    if self.user_edits.edits or datum.is_consistent() != 0:
                        self.user_edits.add([edit_pair, apply_op])
                    self.user_edits.add_op([edit_pair, apply_op])
            elif mouse_index == 2:
                # changed order of next lines 2017-06-13
                # so edit_pair includes guess
                guess = datum.add_candidate(candidate)
                apply_op = Egg.ADD_CANDIDATES
                # next 2 lines 2017-06-13
                if guess != -1 and guess != candidate:
                    candidates.append(guess)
                edit_pair = EditPair(datum, candidates)
                # do the edit
                if self.user_edits.edits or datum.is_consistent() != 0:
                    self.user_edits.add([edit_pair, apply_op])
                self.user_edits.add_op([edit_pair, apply_op])
            elif mouse_index == 3:
                if datum.candidate_is_set(candidate):
                    # for display we need to delete the candidates first
                    del_candidates = list(datum.candidates)
                    apply_op = Egg.DELETE_CANDIDATES
                    edit_pair = EditPair(datum, del_candidates)
                    self.user_edits.add_op([edit_pair, apply_op])
                    # do the edit
                    #datum.set_guess(candidate, False)
                    datum.set_guess(candidate)
                    apply_op = Egg.SET_GUESS
                    edit_pair = EditPair(datum, [candidate])
                    if self.user_edits.edits or datum.is_consistent() != 0:
                        apply_op = Egg.DELETE_CANDIDATES
                        edit_pair = EditPair(datum, del_candidates)
                        self.user_edits.add([edit_pair, apply_op])
                        apply_op = Egg.SET_GUESS
                        edit_pair = EditPair(datum, [candidate])
                        self.user_edits.add([edit_pair, apply_op])
                    self.user_edits.add_op([edit_pair, apply_op])

    def get_colour_ops(self):
        self.user_edits.get_colour_ops(self.errors)

    def get_uncolour_ops(self):
        self.user_edits.get_uncolour_ops(self.errors)

    def get_board_updates(self):
        for key, datum in list(self.grid_dic.items()):
            self.editor.get_update_ops(datum)
        return self.editor.update_ops

    def packer(self, max_polyomino):

        """
        def print_grid(grid):
            for y in range(Sgg.grid_height):
                for x in range(Sgg.grid_width):
                    t = grid[x, y]
                    print t,
                print
            print
        """

        def make_empty_grid():
            grid = {}
            for y, x in product(list(range(Sgg.grid_height)),
                                list(range(Sgg.grid_width))):
                grid[x, y] = 0
            return grid

        def next_start(grid):
            next_xy = None
            for y, x in product(list(range(Sgg.grid_height)),
                                list(range(Sgg.grid_width))):
                if grid[x, y] == 0:
                    next_xy = x, y
                    break
            return next_xy

        def complete(grid):
            ret = True
            for y, x in product(list(range(Sgg.grid_height)),
                                list(range(Sgg.grid_width))):
                if grid[x, y] == 0:
                    ret = False
                    break
            return ret

        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

        def next_cell(grid, polyomino):
            """
            Choose the next cell adjacent to a previous cell
            and trying to make the polyomino irregular in shape.
            """
            def nbrs_ok(polyomino, nbr_0, nbr_1):
                ret = False
                if in_range(nbr_0) and nbr_0 not in polyomino\
                   and in_range(nbr_1) and nbr_1 not in polyomino:
                    ret = True
                return ret
            len_poly = len(polyomino) - 1
            next_key = ()
            reserve = ()
            while True:
                rand = randint(0, len_poly)
                key = polyomino[rand]
                x = key[0]
                y = key[1]
                # up
                y -= 1
                if in_range((x, y)) and grid[(x, y)] == 0:
                    reserve = (x, y)
                    nbr_0 = ((x - 1), y)
                    nbr_1 = ((x + 1), y)
                    if nbrs_ok(polyomino, nbr_0, nbr_1):
                        next_key = (x, y)
                        break
                # right
                x = key[0] + 1
                y = key[1]
                if in_range((x, y)) and grid[(x, y)] == 0:
                    reserve = (x, y)
                    nbr_0 = (x, (y - 1))
                    nbr_1 = (x, (y + 1))
                    if nbrs_ok(polyomino, nbr_0, nbr_1):
                        next_key = (x, y)
                        break
                # down
                x = key[0]
                y = key[1] + 1
                if in_range((x, y)) and grid[(x, y)] == 0:
                    reserve = (x, y)
                    nbr_0 = ((x - 1), y)
                    nbr_1 = ((x + 1), y)
                    if nbrs_ok(polyomino, nbr_0, nbr_1):
                        next_key = (x, y)
                        break
                # left
                x = key[0] - 1
                y = key[1]
                if in_range((x, y)) and grid[(x, y)] == 0:
                    reserve = (x, y)
                    nbr_0 = (x, (y - 1))
                    nbr_1 = (x, (y + 1))
                    if nbrs_ok(polyomino, nbr_0, nbr_1):
                        next_key = (x, y)
                        break
                break
            if not next_key:
                next_key = reserve
            return next_key

        grid = make_empty_grid()
        shape_id = 0
        polyominos = []
        while not complete(grid):
            shape_id += 1
            polyomino = []
            key = next_start(grid)
            grid[key] = shape_id
            polyomino.append(key)
            while True:
                key = next_cell(grid, polyomino)
                if key:
                    grid[key] = shape_id
                    polyomino.append(key)
                    if len(polyomino) == max_polyomino:
                        break
                else:
                    break
            polyominos.append(polyomino)
        return polyominos

    def filler(self, coords_list):
        """
        bad_dic[x,y] is a dic of the candiates which cannot occur
        at (x,y) because they've already been used in the corresponding
        polyomino or are too close to answers set in row y and column x.
        """

        def update_bad_dic(bad_dic, key, value):
            for item in range(key[0] - value - 1, key[0] + value + 2):
                if item > -1 and item < Sgg.grid_width:
                    bad_dic.setdefault((item, key[1]), []).append(value)
            for item in range(key[1] - value - 1, key[1] + value + 2):
                if item > -1 and item < Sgg.grid_height:
                    bad_dic.setdefault((key[0], item), []).append(value)

        coords_list.sort(key=len)
        bad_dic = {}
        self.polyomino_dic = {}
        pairs_dic = {}
        shape_ids = set([])
        for shape_id, coord_list in enumerate(coords_list):
            success = False
            shape_ids.add(shape_id)
            cands = list(range(len(coord_list)))
            for tries in range(len(cands) * len(cands)):
                poly_dic = {}
                shuffle(cands)
                for cand_index, cand in enumerate(cands):
                    key = coord_list[cand_index]
                    try:
                        bad_dic_value = bad_dic[key]
                        if cand not in bad_dic_value:
                            poly_dic[key] = cand
                    except KeyError:
                        poly_dic[key] = cand
                if len(poly_dic) == len(cands):
                    success = True
                    for pkey, pcand in list(poly_dic.items()):
                        update_bad_dic(bad_dic, pkey, pcand)
                        pairs_dic[pkey] = [shape_id, pcand]
                    break
            if not success:
                # failed to find an answer, so give up
                break
        if success:
            # add 1 to answers
            for key, pair in list(pairs_dic.items()):
                pair[1] += 1
            self.make_structures(shape_ids, pairs_dic)
        return success


    def get_puzzle(self, puzzle_number):
        """
        Puzzles are stored as two separate grids of
        size Sgg.grid_width * Sgg.grid_height.
        The first contains the polyominos defined by cells
        with the same number, so that polyomino 1 is defined
        by a cluster of cells denoted by 1s, polyomino 2
        by a cluster of 2s, and so on. The next grid
        contains the answers (1 - N), with clues being negative.
        """
        puzzle = self.model_data.get_puzzle(puzzle_number)
        self.difficulty = puzzle[0]
        Sgg.grid_width = puzzle[1]
        Sgg.grid_height = puzzle[2]
        shapes = puzzle[3:]
        answers = puzzle[3 + Sgg.grid_width * Sgg.grid_height:]
        pairs_dic = {}
        shape_ids = set([])
        item = -1
        for y, x in product(list(range(Sgg.grid_height)),
                            list(range(Sgg.grid_width))):
            item += 1
            shape_id = shapes[item]
            answer = answers[item]
            pairs_dic[(x, y)] = [shape_id, answer]
            shape_ids.add(shape_id)

        self.make_structures(shape_ids, pairs_dic)

    def make_structures(self, shape_ids, pairs_dic):
        self.polyomino_dic = {}
        polyominos = []
        for shape_id in shape_ids:
            polyomino = Polyomino(shape_id, pairs_dic)
            polyominos.append(polyomino)

        self.grid_dic = {}
        for polyomino in polyominos:
            self.grid_dic.update(polyomino.datum_dic)
            self.polyomino_dic[polyomino.shape_id] = polyomino
            polyomino.do_single()
            polyomino.is_solved()
        self.set_neighbours()
        self.add_datum_callback()


    def next_key(self, clue_keys):
        while True:
            x = randint(0, Sgg.grid_width - 1)
            y = randint(0, Sgg.grid_height - 1)
            if (x, y) not in clue_keys and \
               self.grid_dic[(x, y)].guess is None:
                return (x, y)

    def do_polyominos(self):
        for shape_id, polyomino in list(self.polyomino_dic.items()):
            polyomino.givens_dic = {}
            polyomino.solved = False
            datum_dic = polyomino.datum_dic
            # do the polyominos of size 1
            if len(datum_dic) == 1:
                polyomino.do_single()

    def prepare_for_solver(self, clues):
        self.reset_guesses()
        self.do_polyominos()
        self.do_givens(clues)
        self.add_datum_callback()

    def do_givens(self, clues):
        for key in clues:
            datum = self.grid_dic[key]
            polyomino = self.polyomino_dic[datum.shape_id]
            polyomino.add_given(key)

    def make_puzzle(self, clues, difficulty):
        self.reset_guesses()
        self.do_givens(clues)
        puzzle = []
        puzzle.append(difficulty)
        puzzle.append(Sgg.grid_width)
        puzzle.append(Sgg.grid_height)
        for y in range(Sgg.grid_height):
            for x in range(Sgg.grid_width):
                tmp = self.grid_dic[(x, y)].shape_id
                puzzle.append(tmp)
        for y in range(Sgg.grid_height):
            for x in range(Sgg.grid_width):
                datum = self.grid_dic[(x, y)]
                tmp = datum.answer + 1
                if datum.guess is not None:
                    tmp = -tmp
                puzzle.append(tmp)
        return puzzle

    """
    def print_me(self):
        for y in range(Sgg.grid_height):
            for x in range(Sgg.grid_width):
                datum = self.grid_dic[(x, y)]
                print datum.guess,
            print
        print
        for y in range(Sgg.grid_height):
            for x in range(Sgg.grid_width):
                datum = self.grid_dic[(x, y)]
                print datum.answer,
            print
        print
    """

    def harden(self, max_clues, max_tries, min_score):
        """
        Have a grid of polyominos complete with answers.
        Unset guesses and then incrementally add clue keys
        until the grid can be solved.
        Do this max_tries times and save the highest
        scoring arrangement as a puzzle.
        """
        answer_keys = []
        highest_score = 0
        for tries in range(max_tries):
            self.prepare_for_harden()
            clue_keys = []
            clue_count = 0
            while clue_count < max_clues:
                clue_count += 1
                self.prepare_for_solver(clue_keys)
                key = self.next_key(clue_keys)
                clue_keys.append(key)
                if self.solve_puzzle():
                    # have chosen enough random clues and solved the grid
                    if highest_score == 0:
                        highest_score = self.difficulty
                    # if highest score, save it
                    if self.difficulty > highest_score:
                        highest_score = self.difficulty
                        if self.difficulty > min_score:
                            answer_keys = copy.copy(clue_keys)
                    #  force break from loop
                    clue_count = max_clues
        # print 'Finished harden', highest_score, answer_keys
        if answer_keys:
            answer_keys = self.make_puzzle(answer_keys,
                                           highest_score)
        return answer_keys

    def reset_guesses(self):
        for key, datum in list(self.grid_dic.items()):
            datum.reset_guess()

    def prepare_for_harden(self):
        self.add_datum_callback()
        self.reset_guesses()
        for shape_id, polyomino in list(self.polyomino_dic.items()):
            polyomino.givens_dic = {}
            polyomino.solved = False
            datum_dic = polyomino.datum_dic
            #  solve the polyominos of size 1
            if len(datum_dic) == 1:
                polyomino.solved = True

    def generate(self):
        """
        Pack the grid with polyominos.
        Use harden to assign candidates and solve, returning those
        with good score.
        """
        puzzles = []
        min_puzzles = 1000
        max_tries = 200
        min_score = 70
        seed = 1
        random.seed(seed)
        Sgg.grid_width = 6
        Sgg.grid_height = 6
        max_polyomino = 5
        max_clues = 3 * int(math.sqrt(Sgg.grid_width * Sgg.grid_height))
        count = 0
        good_count = 0
        while True:
            count += 1
            coords_list = self.packer(max_polyomino)
            #  coords_list: list of polyominos (x, y) written as lists
            success = self.filler(coords_list)
            if success:
                good_count += 1
                puzzle = self.harden(max_clues, max_tries, min_score)
                if puzzle:
                    puzzles.append(puzzle)
                    # print 'success: got', len(puzzles), 'score', puzzle[0]
                    count = 0
                    if len(puzzles) >= min_puzzles:
                        puzzles = sorted(puzzles)
                        filename = str(Sgg.grid_width) +\
                                   'x' + str(Sgg.grid_height) + '.txt'
                        with open(filename, "w") as fo:
                            counter = 0
                            for puzzle in puzzles:
                                for item in puzzle:
                                    rec = ''
                                    rec += str(item)
                                    rec += ','
                                    counter += 1
                                    #if counter % 20 == 0:
                                    #    rec += '\n'
                                    fo.write(rec)
                        # print 'Normal termination!'
                        exit(1)

    def ready_for_user(self):
        """
        We have a completed puzzle in self.datum_dic.
        Here we prepare it for the user.
        """

        def to_hex(int_input):
            symbols = '0123456789abcdef'
            sym_1 = int(int_input / 16)
            sym_2 = int_input % 16
            return symbols[sym_1] + symbols[sym_2]

        def get_n_colours(n_colours):
            """
            Get n_colours colours evenly spaced over the full spectrum
            """
            HSV_tuples = [(x * 1.0 / n_colours, 0.5, 1.0)
                          for x in range(n_colours)]
            hex_out = []
            for rgb in HSV_tuples:
                rgb = [int(x*255) for x in colorsys.hsv_to_rgb(*rgb)]
                myhex = '#' + ("".join([to_hex(x) for x in rgb]))
                hex_out.append(myhex)
            return hex_out

        clue_count = len(self.polyomino_dic)
        for key, datum in list(self.grid_dic.items()):
            if not datum.given:
                datum.reset_guess()
        colours = get_n_colours(clue_count)
        shuffle(colours)

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

        for shape_id, polyomino in list(self.polyomino_dic.items()):
            if polyomino.size != 1:
                polyomino.solved = False


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.user_edits = None
        self.view = View(root, self.settings)
        self.error_detected = False
        self.puzzle_number = None
        puzzle_number = None
        #if len(sys.argv) > 1:
        #    puzzle_number = int(sys.argv[1])
        screen_shot_state = False
        self.active = False
        self.coloured_squares = []
        self.screen_shot = ScreenShot(PzlG.app_name, 'dump_',
                                      screen_shot_state)
        self.start_game(puzzle_number)

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

    def 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)
            if not self.model.solve_puzzle():
                bad.append(puzzle_number)
                self.model.is_finished()
            done += 1
        # print 'done', done, 'bad', bad, len(bad)
        # print Sgg.algorithm_counts
        exit(1)

    def start_game(self, puzzle_number):
        #self.test()
        self.model = Model(self.model_data)
        #self.model.generate()
        self.set_puzzle_number(puzzle_number)
        self.view.clock_init()
        self.view.clock_restart()
        self.view.game_over()
        self.model.get_puzzle(self.puzzle_number)
        self.model.ready_for_user()
        self.ready_for_hint()
        self.view.thumbs_up()
        self.view.datum = self.model.grid_dic
        self.view.make_board()
        self.add_callbacks()
        self.view.game_on()
        self.error_detected = False
        self.view.entry_box.clear()
        self.view.entry_box.write_box(str(self.puzzle_number))
        self.view.update_score_box(self.model.difficulty)
        self.model.set_hint_status(Sgg.GIVE_HINT)
        self.active = True
        self.flasher = Flasher(self.view.clock.box.label,
                               self.master, self.settings)

    def add_callbacks(self):
        self.view.add_menu_callback(self.service_menu)
        self.view.add_toolbar_callback(self.service_toolbar)
        self.view.add_candidate_callback(self.square_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 == '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.editor.algorithm
            self.view.do_tooltip(button_index, alg_index)
        elif mouse_index == 3:
            if button_index == 1:
                if self.model.status:
                    self.model.check_errors()
                    if self.model.status and not self.model.is_finished():
                        if self.model.get_hint_status() != Sgg.APPLY_HINT:
                            if button_index == 1:
                                self.exhaust()
            elif button_index == 0:
                self.puzzle_number += 1
                self.start_game(self.puzzle_number)
        elif mouse_index == 1:
            if button_index == 0:
                self.start_game(None)
            elif button_index == 1:
                self.check_errors()
                if self.model.status and not self.model.is_finished():
                    self.give_hint()
                    self.screen_shot.snap()
            elif button_index == 2:
                if not self.model.is_finished():
                    self.check_errors()
            elif button_index == 3:
                if not self.model.is_finished():
                    self.show_solution()
                    self.screen_shot.snap()

    def square_event(self, datum_index, candidate, mouse_index):
        """
        Respond to board clicks
        One of the three event servicing functions.
        Mouse index 1 == DELETE
                    2 == REWRITE
                    3 == SET GUESS
        candidate numbers at displayed values - 1
        """
        if self.active:
            PzlG.logger.logger.info("square_event " +
                                    str(mouse_index) +
                                    ' ' +
                                    str(datum_index) +
                                    ' ' +
                                    str(candidate))
            # if necessary abort an active hint
            board_updates = self.model.editor.abort_ops
            self.view.update_squares(board_updates)
            self.model.editor.reset()
            self.model.set_hint_status(Sgg.GIVE_HINT)
            self.model.apply_user_edits(datum_index,
                                        candidate,
                                        mouse_index)

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

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

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

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

    def check_finished(self):
        """
        Check if finished.
        If so, handle buttons, flasher and clock
        """
        if self.active:
            if self.model.is_finished():
                self.show_solution()
                self.screen_shot.snap()
                self.flasher.flash()

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

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

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

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

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


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