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

# Copyright (C) 2014 Rod Engerstad

from random import shuffle
import Tkinter as tk
import zlib
import base64

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 Menu
from pzl_lib import Toolbar
from pzl_lib import ClockBox
from pzl_lib import MButton
from pzl_lib import ViewB
from pzl_lib import Square
from pzl_lib import Egg
from pzl_lib import WidgetOps

class Sgg(object):
    """
    Game globals
    """

    ALGORITHM_NAMES = []
    grid_size = 36
    value = 0
    symbol = 1
    result = 2
    blank_black = 3
    blank_white = 4
    hint = 5
    symbol_count = 9
    cell_types = [0, 1, 0, 1, 0, 2, 5,
                  1, 3, 1, 3, 1, 4, 4,
                  0, 1, 0, 1, 0, 2, 5,
                  1, 3, 1, 3, 1, 4, 4,
                  0, 1, 0, 1, 0, 2, 5,
                  2, 4, 2, 4, 2, 4, 4,
                  5, 4, 5, 4, 5, 4, 4]

    datum_to_square = [
        0, 2, 4,
        14, 16, 18,
        28, 30, 32]

    sum_to_square = [
        [0, 2, 4],
        [14, 16, 18],
        [28, 30, 32],
        [0, 14, 28],
        [2, 16, 30],
        [4, 18, 32]
        ]

    square_to_datum = [None] * 49
    square_to_datum[0] = 0
    square_to_datum[2] = 1
    square_to_datum[4] = 2
    square_to_datum[14] = 3
    square_to_datum[16] = 4
    square_to_datum[18] = 5
    square_to_datum[28] = 6
    square_to_datum[30] = 7
    square_to_datum[32] = 8

    square_to_datum[6] = 0
    square_to_datum[20] = 1
    square_to_datum[34] = 2
    square_to_datum[42] = 3
    square_to_datum[44] = 4
    square_to_datum[46] = 5

    sum_to_datum = [
        [0, 1, 2],
        [3, 4, 5],
        [6, 7, 8],
        [0, 3, 6],
        [1, 4, 7],
        [2, 5, 8]]

    hint_squares = [6, 20, 34, 42, 44, 46]


class Settings(SettingsB):

    def __init__(self):
        SettingsB.__init__(self)
        self.dark_grey = '#6e6e6e'
        self.yellow = 'yellow'
        self.original_square_size = 54
        self.square_size = self.original_square_size
        self.dic = {}
        self.dic['set_difficulty'] = '4'
        self.dic['error_colour'] = '#ff0000'
        self.dic['text_colour'] = '#000000'
        self.dic['font_size'] = '1'
        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.tooltip_text = []
        self.make_tooltip()

    def get_tooltip(self, message_index, alg_index):
        text = ''
        title = self.title
        if message_index != 1 or alg_index is None:
            text = self.tooltip_text[message_index]
        else:
            pass
        return text, title

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

    def make_help(self):
        self.help_text = _("The grid contains 6 simple sums: 3 across and 3 down. Initially only the operators (+ add, - subtract, x multiply, / divide) and the result of each sum are shown. To solve the puzzle fill in the numbers 1 to 9 which satisfy the intersecting sums. Each number only appears once in the grid. Arithmetic is performed left to right. The candidate numbers are shown in each of the 9 squares. Left-clicking on a candidate will erase it; middle-clicking will reveal it; right-click will set it as the solution for that square. The green wand icons at the end of each sum provide assistance: left-click will reveal a list of the sets of numbers which satisfy the sum; right-click will remove any candidates that are not in the list; middle-click will restore all the candidates for the sum. At the top of the display is a Toolbar. The jigsaw piece button at the left is a menu which has options for entering puzzles, reading them from files or saving them to disk. The menu includes options for configuring the display and saving the current settings. It also has an option to set the number of sets of numbers that satisfy each sum: roughly speaking, the more sets the harder the puzzle. Next to the jigsaw icon is a calculator-like icon which is used for selecting a random puzzle; a thumb for checking and correcting errors, and a sad smiley for showing the solution. To the right is a clock which ticks every 5 seconds")

    def load_menus(self):
        self.menu_items.append(MenuItem(
            _('Enter New Puzzle'),
            '',
            '',
            '',
            'enter_new_puzzle'
        ))
        self.menu_items.append(MenuItem(
            _('Load Puzzle'),
            '',
            '',
            '',
            'read_puzzle'
        ))
        self.menu_items.append(MenuItem(
            _('Save Puzzle'),
            '',
            '',
            '',
            'save_puzzle'
        ))
        self.menu_items.append(MenuItem(
            _('Set Difficulty'),
            [
                '2',
                '3',
                '4',
                '5',
                '6',
                '7',
                '8',
                '9',
                '10',
                '11',
                '12',
            ],
            '',
            [
                '2',
                '3',
                '4',
                '5',
                '6',
                '7',
                '8',
                '9',
                '10',
                '11',
                '12',
            ],
            'set_difficulty'
        ))
        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 EnterPuzzle(tk.Toplevel):
    """
    Get data from widget in self.lines
    An empty self.lines signals the caller that we've given up
    """
    def __init__(self, incoming_lines, incoming_message):
        self.my_edwin = None
        self.incoming = False
        self.lines = []
        self.message_frame = None
        self.message = 'Please edit data in the white box'
        if incoming_message is not None:
            self.message = incoming_message
        if incoming_lines is not None:
            self.incoming = True
            self.incoming_lines = incoming_lines

        self.expose_ourselves()

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

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

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

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

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

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

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

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

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


class Puzzle(object):
    """
    Make puzzles using dictionaries and shuffled candidates
    trimer = the 3 digit numbers for each of the 6 sums.
    symbol_result a string containing the symbols and total/result for
    each of the 6 sums.
    """
    def __init__(self, dictionaries):
        self.symbol_results_dic = dictionaries.symbol_results_dic
        self.trimers_dic = dictionaries.trimers_dic
        self.symbol_results_dic = dictionaries.symbol_results_dic
        self.symbol_result = [None] * 6
        self.trimer = [None] * 6
        self.trimer_count = 0
        self.max_choices = 4
        self.digits = []
        self.digits_left = list(range(1, 10))
        self.max_tries = 50
        self.tries = 0
        self.success = True

    def make_first_sum(self):
        """
        We make sums, removing the digits used from digits_left
        """
        self.success = True
        self.trimer_count = 0
        self.digits_left = list(range(1, 10))
        shuffle(self.digits_left)
        out_index = 0
        while True:
            self.digits = self.digits_left[:]
            trimer = []
            for _ in range(3):
                digit = self.digits.pop()
                trimer.append(digit)
            trimer_entry = self.trimers_dic[str(trimer)]
            symbol_result_out = self.get_symbol_result(trimer_entry)
            if symbol_result_out is not None:
                self.trimer[out_index] = trimer
                self.symbol_result[out_index] = symbol_result_out[0]
                self.digits_left = self.digits[:]
                break
            else:
                shuffle(self.digits_left)

    def get_symbol_result(self, trimer_entry):
        """
        Try to find a symbols/result combo with required
        number of trimer combinations.
        """
        symbol_result_out = None
        symbol_results_count = trimer_entry[0]
        symbol_results = trimer_entry[1]
        for item_index in range(0, symbol_results_count):
            symbol_result = symbol_results[item_index]

            symbols = symbol_result[0]
            combinations = self.symbol_results_dic[symbols]
            combination_count = len(combinations)

            if combination_count >= self.max_choices-1 and \
                    combination_count <= \
                    self.max_choices + 1:
                symbol_result_out = symbol_results[item_index]
                self.trimer_count += combination_count
                break

        return symbol_result_out

    def do_shuffle_sum(self, out_index):
        """
        Shuffle remaining digits and try to find a matching
        symbols/results combination.
        """
        self.tries = 0
        while True:
            self.digits = self.digits_left[:]
            trimer = []
            for _ in range(3):
                digit = self.digits.pop()
                trimer.append(digit)
            trimer_entry = self.trimers_dic[str(trimer)]
            symbol_result_out = self.get_symbol_result(trimer_entry)
            if symbol_result_out is not None:
                self.trimer[out_index] = trimer
                self.symbol_result[out_index] = symbol_result_out[0]
                self.digits_left = self.digits[:]
                break
            else:
                shuffle(self.digits_left)
                self.tries += 1
                if self.tries > self.max_tries:
                    self.success = False
                    return

    def do_it(self):
        count = 0
        while True:
            self.make_first_sum()
            if self.success:
                self.do_shuffle_sum(1)
                if self.success:
                    self.do_shuffle_sum(2)
            if self.success:
                self.do_last_three()
                if self.success:
                    return
            count += 1
            if count > 1000:
                return

    def do_fixed_sum(self, in_index, out_index):
        ret = False
        trimer = []
        trimer.append(self.trimer[0][in_index])
        trimer.append(self.trimer[1][in_index])
        trimer.append(self.trimer[2][in_index])

        trimer_entry = self.trimers_dic[str(trimer)]
        symbol_result_out = self.get_symbol_result(trimer_entry)
        if symbol_result_out is not None:
            self.trimer[out_index] = trimer
            self.symbol_result[out_index] = symbol_result_out[0]
            self.digits_left = self.digits[:]
            ret = True
        return ret

    def do_last_three(self):
        self.success = False
        ret = self.do_fixed_sum(0, 3)
        if ret:
            ret = self.do_fixed_sum(1, 4)
            if ret:
                ret = self.do_fixed_sum(2, 5)
                if ret:
                    self.success = True

    def make_puzzle(self):
        while True:
            self.trimer_count = 0
            self.do_it()
            if not self.success:
                self.max_choices += 1
            else:
                return self.success


class Datum(object):
    """
    Data item for a Value in a sum
    """
    def __init__(self):
        self.candidates = set(range(Sgg.symbol_count))
        self.value = None
        self.guess = None

    def reset(self, value):
        if value is not None:
            self.value = value
        self.guess = None
        self.candidates = set(range(Sgg.symbol_count))

    def set_guess(self, value):
        self.guess = value
        self.candidates = set()

    def del_candidate(self, candidate):
        """
        delete candidate
        """
        if candidate in self.candidates:
            self.candidates.remove(candidate)
            if self.guess is None:
                if len(self.candidates) == 1:
                    candidate = self.candidates.pop()
                    self.guess = candidate + 1

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

    def is_correct(self):
        status = False
        if self.guess == self.value:
            status = True
        return status

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

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

            if self.value != self.guess:
                # guess != answer
                error += 4
        return error


class SolveDatum(object):
    """
    Data about a value in a sum.
    """
    def __init__(self, value, stuff_item):
        self.value = value
        self.sum_across = stuff_item[0]
        self.position_across = stuff_item[1]
        self.sum_down = stuff_item[2]
        self.position_down = stuff_item[3]

    def get_intersect(self, sum_index, position):
        ret_sum = -1
        ret_pos = -1
        if sum_index < 3:
            # must be looking for down intersect
            if self.sum_across == sum_index and \
                    self.position_across == position:
                ret_sum = self.sum_down
                ret_pos = self.position_down
        else:
            # must be looking for across intersect
            if self.sum_down == sum_index and \
                    self.position_down == position:
                ret_sum = self.sum_across
                ret_pos = self.position_across
        return ret_sum, ret_pos


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

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


class ParsePuzzle(object):
    """
    Parse a puzzle read from widget or file
    Then send it to the solver.
    """
    def __init__(self, dictionaries, lines):
        self.operators = ['+', '-', 'x', '/']
        self.numbers = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
        self.unknown = ['.']
        self.ok_numbers = self.numbers[1:] + self.unknown
        self.dictionaries = dictionaries
        self.lines = lines
        self.puzzle = []
        self.solved = False
        self.solved_puzzle = None
        self.symbol_results = []
        self.message = ''
        self.parse_it()

    def check_1(self, items, ok):
        good = True
        for item in items:
            if item not in ok:
                good = False
        return good

    def check_2(self, items):
        good = True
        for item in items:
            try:
                value = int(item)
            except ValueError:
                good = False
                break
        return good

    def check_3(self, dic_key):
        good = True
        try:
            combinations = self.dictionaries.symbol_results_dic[dic_key]
        except KeyError:
            good = False
        return good

    def parse_it(self):
        """
        Parse the puzzle file. Split out the numbers, operators and totals
        to check them. Then stitch them back together to make puzzle.
        """
        puzzle = []
        record_lines = [None] * 7
        good = True
        if len(self.lines) != 6:
            message = "Not 6 lines"
            good = False
        if good:
            #do sum lines
            for line_no in range(0, 5, 2):
                record_line = []
                line = self.lines[line_no]
                if len(line) < 6:
                    message = "line %d too short " % (line_no + 1,) + line
                    good = False
                    break
                numbers = line[0] + line[2] + line[4]
                operators = line[1] + line[3]
                totals = [line[6:]]
                good = self.check_1(numbers, self.ok_numbers)
                if not good:
                    message = "line %d bad number " % (line_no + 1,) + line
                    break
                good = self.check_1(operators, self.operators)
                if not good:
                    message = "line %d bad operator " % (line_no + 1,) + line
                    break
                good = self.check_2(totals)
                if not good:
                    message = "line %d bad total " % (line_no + 1,) + line
                    break
                # check there's an entry in the dictionary
                dic_key = operators[0] + operators[1] + totals[0]
                good = self.check_3(dic_key)
                if not good:
                    message = "line %d bad total " % (line_no + 1,) + line
                    break
                # save dic_key
                self.symbol_results.append(dic_key)
                # now do the encoding
                o_index = 0
                for number in numbers:
                    bit = '   '
                    if number != '.':
                        bit = ' ' + number + ' '
                    record_line.append(bit)
                    if o_index < 2:
                        record_line.append(operators[o_index])
                    o_index += 1
                record_line.append(totals[0])
                record_line.append('')
                record_lines[line_no] = record_line
        if good:
            #do operator lines
            for line_no in range(1, 5, 2):
                record_line = []
                line = self.lines[line_no]
                if len(line) < 5:
                    message = "line %d too short " % (line_no + 1,) + line
                    good = False
                    break
                operators = line.split('#')
                good = self.check_1(operators, self.operators)
                if not good:
                    message = "line %d bad operator " % (line_no + 1,) + line
                    break

                # now do the encoding
                o_index = 0
                for operator in operators:
                    record_line.append(operator)
                    if o_index < 2:
                        record_line.append(' ')
                    o_index += 1
                record_line.append('')
                record_line.append('')
                record_lines[line_no] = record_line
        if good:
            # do totals line
            line_no = 5
            line = self.lines[line_no]
            if len(line) < 5:
                message = "line %d too short " % (line_no + 1,) + line
                good = False
            else:
                totals = line.split('#')
                good = self.check_2(totals)
                if not good:
                    message = "line %d bad total " % (line_no + 1,) + line
            if good:
                record_line = []
                # now do the encoding
                o_index = 0
                for total in totals:
                    record_line.append(total)
                    if o_index < 2:
                        record_line.append(' ')
                    o_index += 1
                record_line.append('')
                record_line.append('')
                record_lines[line_no] = record_line
                record_line = ['', '', '', '', '', '', '']
                record_lines[6] = record_line
                puzzle = []
                for record_line in record_lines:
                    for item in record_line:
                        puzzle.append(item)

                # final checks: operators and totals for vertical sums
                for index_2 in range(7, 12, 2):
                    dic_key = puzzle[index_2] + \
                        puzzle[index_2 + 14] + \
                        puzzle[index_2 + 28].strip()
                    good = self.check_3(dic_key)
                    if not good:
                        message = "line %d bad total " % (line_no + 1,) + line
                        break
                    self.symbol_results.append(dic_key)
        if good:
            self.solved_puzzle = SolvePuzzle(self.dictionaries, puzzle)
            self.solved = self.solved_puzzle.solved
            if self.solved is not True:
                message = "Unable to solve puzzle"
                good = False
        if not good:
            self.message = message
        self.puzzle = puzzle


class SolvePuzzle(object):
    """
    Try to solve a puzzle
    """
    def __init__(self, dictionaries, puzzle):
        self.dictionaries = dictionaries
        self.sum_strings = []
        self.puzzle = puzzle
        self.progress = False
        self.solve_datums = []
        self.combinations_list = []
        self.combinations = [None] * 6
        self.symbol_result = [None] * 6
        self.trimer = [None] * 6
        self.solved = False
        self.convert_puzzle()
        self.solve_it()

    def must_have(self, combinations, position, value):
        """
        If we know a particular value in a sum, then all
        combinations which intersect its position must contain
        that value at their corresponding position.
        Delete those that don't.
        """
        deletions = []
        item = 0
        for combination in combinations:
            if combination[position] != value:
                deletions.append(item)
            item += 1
        for item in range(len(deletions)-1, -1, -1):
            delete_item = deletions[item]
            del combinations[delete_item]
            self.progress = True

    def do_must_have(self):
        """
        If we know a particular value in a sum, then all
        combinations which intersect its position must contain
        that value at their corresponding position.
        Delete those that don't.
        """
        self.progress = False
        for solve_datum in self.solve_datums:
            if solve_datum.value != -1:
                # definite value found
                self.must_have(self.combinations_list[solve_datum.sum_across],
                               solve_datum.position_across,
                               solve_datum.value)
                self.must_have(self.combinations_list[solve_datum.sum_down],
                               solve_datum.position_down,
                               solve_datum.value)

    def cannot_have(self, combinations, value):
        """
        Remove combinations which contain used numbers/values.
        """
        deletions = []
        item = 0
        for combination in combinations:
            if value in combination:
                deletions.append(item)
            item += 1
        for item in range(len(deletions)-1, -1, -1):
            delete_item = deletions[item]
            del combinations[delete_item]
            self.progress = True

    def do_cannot_have(self):
        """
        Remove combinations which contain used numbers.
        """
        self.progress = False
        for solve_datum in self.solve_datums:
            if solve_datum.value != -1:
                # definite value found
                for sum_index in range(6):
                    if sum_index != solve_datum.sum_across and \
                            sum_index != solve_datum.sum_down:
                        self.cannot_have(self.combinations_list[sum_index],
                                         solve_datum.value)

    def update_solutions(self):
        """
        If theres only 1 combination for a sum it must be the answer.
        So update the solutions.
        If there are any positions in a list of combinations in which
        only a single value occurs it must be the answer.
        So update the solutions.
        """
        self.progress = False
        sum_index = 0
        for combinations in self.combinations_list:
            if len(combinations) == 1:
                # unique combination, hence sum sum_index is solved
                combination = combinations[0]
                for position in range(3):
                    for solve_datum in self.solve_datums:
                        if solve_datum.value == -1:
                            # needs solving
                            if solve_datum.sum_across == sum_index and \
                                    solve_datum.position_across == position:
                                solve_datum.value = combination[position]
                                self.progress = True
                            if solve_datum.sum_down == sum_index and \
                                    solve_datum.position_down == position:
                                solve_datum.value = combination[position]
                                self.progress = True
            sum_index += 1
        # now look to see if there are any positions in a list of
        # combinations in which only a single value occurs

        for sum_index in range(3):
            combinations = self.combinations_list[sum_index]
            for position in range(3):
                present = []
                for combination in combinations:
                    present.append(combination[position])
                present_set = set(present)
                if len(present_set) == 1:
                    # only 1 value at this position
                    solve_datum_index = sum_index * 3 + position
                    solve_datum = self.solve_datums[solve_datum_index]
                    if solve_datum.value == -1:
                        solve_datum.value = present_set.pop()
                        self.progress = True

    def must_contain(self, combinations, position, values):
        """
        If we know a particular set of values at an intersection
        combinations which intersect its position must contain
        that values at their corresponding position.
        Delete those that don't.
        """
        deletions = []
        item = 0
        for combination in combinations:
            if combination[position] not in values:
                deletions.append(item)
            item += 1
        for item in range(len(deletions)-1, -1, -1):
            delete_item = deletions[item]
            del combinations[delete_item]
            self.progress = True

    def find_intersects(self, sum_index, position):
        for solve_datum in self.solve_datums:
            i_sum, i_pos = solve_datum.get_intersect(sum_index, position)
            if i_sum != -1:
                return i_sum, i_pos

    def cross_combinations(self):
        """
        Eliminate combinations whose intersections are incompatible
        """
        self.progress = False
        for sum_index in range(3):
            combinations = self.combinations_list[sum_index]
            for position in range(3):
                present = []
                for combination in combinations:
                    present.append(combination[position])
                present_set = set(present)
                # which combination does this intersect with?
                i_sum, i_pos = self.find_intersects(sum_index, position)
                i_combinations = self.combinations_list[i_sum]
                i_present = []
                for i_combination in i_combinations:
                    i_present.append(i_combination[i_pos])
                i_present_set = set(i_present)
                common = present_set.intersection(i_present_set)
                self.must_contain(combinations, position, common)
                self.must_contain(i_combinations, i_pos, common)

    def is_solved(self):
        self.solved = True
        for solve_datum in self.solve_datums:
            if solve_datum.value == -1:
                self.solved = False
                break

    def make_trimers(self):
        trimer_index = 0
        for row in range(3):
            trimrec = []
            for column in range(3):
                datum_index = row * 3 + column
                trimrec.append(self.solve_datums[datum_index].value)
            self.trimer[trimer_index] = trimrec
            trimer_index += 1
        for column in range(3):
            trimrec = []
            for row in range(3):
                datum_index = row * 3 + column
                trimrec.append(self.solve_datums[datum_index].value)
            self.trimer[trimer_index] = trimrec
            trimer_index += 1

    def make_datums(self):
        stuff = [
            [0, 0, 3, 0], [0, 1, 4, 0], [0, 2, 5, 0],
            [1, 0, 3, 1], [1, 1, 4, 1], [1, 2, 5, 1],
            [2, 0, 3, 2], [2, 1, 4, 2], [2, 2, 5, 2]]

        values = []
        for sum_index in range(3):
            sum_string = self.sum_strings[sum_index]
            for item in range(0, 5, 2):
                val_str = sum_string[item]
                if val_str != '.':
                    val = int(val_str)
                else:
                    val = -1
                values.append(val)

        self.solve_datums = []
        for value_index in range(9):
            solve_datum = SolveDatum(values[value_index], stuff[value_index])
            self.solve_datums.append(solve_datum)

    def convert_puzzle(self):
        """
        Convert from the lazy original format used for laying out the
        puzzle in the widget to 6 sum_strings of form:
        [['1'],['+'],['2'],['+'],['3'],['99']]
        """
        t_sum_strings = []
        for p_index in range(0, 35, 14):
            t_sum_string = []
            # value
            item_string = self.puzzle[p_index]
            if item_string == '   ':
                item = '.'
            else:
                item = item_string[1]
            t_sum_string.append(item)
            # operator
            item_string = self.puzzle[p_index + 1]
            t_sum_string.append(item_string)
            # value
            item_string = self.puzzle[p_index + 2]
            if item_string == '   ':
                item = '.'
            else:
                item = item_string[1]
            t_sum_string.append(item)
            # operator
            item_string = self.puzzle[p_index + 3]
            t_sum_string.append(item_string)
            # value
            item_string = self.puzzle[p_index + 4]
            if item_string == '   ':
                item = '.'
            else:
                item = item_string[1]
            t_sum_string.append(item)
            # total
            item_string = self.puzzle[p_index + 5]
            t_sum_string.append(item_string)
            t_sum_strings.append(t_sum_string)

        for p_index in range(0, 5, 2):
            t_sum_string = []
            # value
            item_string = self.puzzle[p_index]
            if item_string == '   ':
                item = '.'
            else:
                item = item_string[1]
            t_sum_string.append(item)
            # operator
            item_string = self.puzzle[p_index + 7]
            t_sum_string.append(item_string)
            # value
            item_string = self.puzzle[p_index + 14]
            if item_string == '   ':
                item = '.'
            else:
                item = item_string[1]
            t_sum_string.append(item)
            # operator
            item_string = self.puzzle[p_index + 21]
            t_sum_string.append(item_string)
            # value
            item_string = self.puzzle[p_index + 28]
            if item_string == '   ':
                item = '.'
            else:
                item = item_string[1]
            t_sum_string.append(item)
            # total
            item_string = self.puzzle[p_index + 35]
            t_sum_string.append(item_string)
            t_sum_strings.append(t_sum_string)
        self.sum_strings = t_sum_strings

    def solve_it(self):
        combinations_list = []
        for sum_index in range(6):
            sum_string = self.sum_strings[sum_index]
            symbols = sum_string[1] + sum_string[3] + sum_string[5].strip()
            combinations = self.dictionaries.symbol_results_dic[symbols]
            self.combinations[sum_index] = combinations
            combinations_list.append(combinations[:])
        self.combinations_list = combinations_list
        self.make_datums()

        # keep looping while some overall progress is maintained
        overall_progress = True
        while overall_progress:
            overall_progress = False
            self.progress = True
            while self.progress:
                self.cross_combinations()

            self.update_solutions()
            if self.progress:
                overall_progress = True
            self.is_solved()
            if self.solved:
                break

            self.do_must_have()
            if self.progress:
                overall_progress = True

            self.update_solutions()
            if self.progress:
                overall_progress = True

            self.do_cannot_have()
            if self.progress:
                overall_progress = True

        if self.solved:
            self.make_trimers()


class Model(object):
    """
    A puzzle consists of 6 sums containing 9 values so that
    each of the 9 values is in 2 sums. The data associated
    with each of the 9 values are stored in Datum.
    We also need to know the symbol and result/total
    strings for each of the 6 sums (symbol_result)
    and the dictionary entries for each of the 6
    symbol_result strings (combinations).
    Lazy_puzzle is the version of the puzzle used to add
    the puzzle to the GUI.
    Lines are the records encoding the puzzle which are read from
    file of entry_widget.
    """
    def __init__(self, dictionaries):
        self.status = True
        self.datum = []
        for _ in range(9):
            self.datum.append(Datum())
        self.symbol_result = [None] * 6
        self.combinations = [None] * 6
        self.dictionaries = dictionaries
        self.puzzle = Puzzle(self.dictionaries)
        self.errors = []
        self.lazy_puzzle = []
        self.lines = []
        self.get_solvable_puzzle()

    def get_solvable_puzzle(self):
        while True:
            self.get_puzzle()
            solved_puzzle = SolvePuzzle(self.dictionaries, self.lazy_puzzle)
            if solved_puzzle.solved:
                # empty lines so save_puzzle will generate
                self.lines = []
                break

    def update_datums(self):
        """
        stick the values into datums
        """
        datum_index = 0
        for sum_index in range(3):
            trimer = self.puzzle.trimer[sum_index]
            for element_index in range(3):
                self.datum[datum_index].reset(
                    trimer[element_index])
                datum_index += 1

    def get_puzzle(self):
        ret = self.puzzle.make_puzzle()
        if ret:
            self.update_datums()
            for sum_index in range(6):
                self.symbol_result[sum_index] = \
                    self.puzzle.symbol_result[sum_index]
                self.combinations[sum_index] = \
                    self.dictionaries.symbol_results_dic[
                        self.symbol_result[sum_index]]
            self.make_lazy_puzzle()

    def make_lines(self):
        """
        Convert the lazy original format to the succinct
        one used for puzzle input.
        """
        lines = []
        for widths in range(6):
            puzzle_item = widths * 7
            line = ''
            if widths % 2 == 0:
                line += '.'
                line += self.lazy_puzzle[puzzle_item+1].strip()
                line += '.'
                line += self.lazy_puzzle[puzzle_item+3].strip()
                line += '.'
                line += '='
                line += self.lazy_puzzle[puzzle_item+5].strip()
            else:
                line += self.lazy_puzzle[puzzle_item].strip()
                line += '#'
                line += self.lazy_puzzle[puzzle_item+2].strip()
                line += '#'
                line += self.lazy_puzzle[puzzle_item+4].strip()

            lines.append(line)
        self.lines = lines

    def save_puzzle(self, my_file):
        """
        Save puzzle to a file.
        If the puzzle was generated we need to create the data in lines.
        Otherwise we save what was read from file or input widget.
        """
        if not self.lines:
            self.make_lines()
        for line in self.lines:
            record = line + '\n'
            my_file.write(record)
        my_file.close()

    def read_puzzle(self, my_file):
        puzzle = ReadPuzzle(my_file)
        if puzzle.lines:
            parsed = ParsePuzzle(self.dictionaries, puzzle.lines)
            if not parsed.solved:
                puzzle_lines = puzzle.lines
                puzzle_message = parsed.message
                while True:
                    puzzle = EnterPuzzle(puzzle_lines, puzzle_message)
                    if puzzle.lines:
                        parsed = ParsePuzzle(self.dictionaries, puzzle.lines)
                        if parsed.solved:
                            break
                        else:
                            puzzle_lines = puzzle.lines
                            puzzle_message = parsed.message
                    else:
                        break
            if parsed.solved:
                self.lazy_puzzle = parsed.puzzle
                self.combinations = parsed.solved_puzzle.combinations
                self.puzzle.trimer = parsed.solved_puzzle.trimer
                self.puzzle.symbol_result = \
                    parsed.solved_puzzle.symbol_result
                self.update_datums()

    def enter_puzzle(self):
        puzzle_lines = None
        puzzle_message = None
        while True:
            puzzle = EnterPuzzle(puzzle_lines, puzzle_message)
            if puzzle.lines:
                parsed = ParsePuzzle(self.dictionaries, puzzle.lines)
                if parsed.solved:
                    self.lazy_puzzle = parsed.puzzle
                    self.combinations = parsed.solved_puzzle.combinations
                    self.puzzle.trimer = parsed.solved_puzzle.trimer
                    self.puzzle.symbol_result = \
                        parsed.solved_puzzle.symbol_result
                    self.update_datums()
                    break
                else:
                    puzzle_lines = puzzle.lines
                    puzzle_message = parsed.message
            else:
                break

    def make_lazy_puzzle(self):
        """
        From symbol_results for each sum make a version of the puzzle
        data which can easily be loaded into the GUI. This version is
        termed the lazy format as I didn't bother to think about it.
        """
        puzzle = []
        for sum_index in range(3):
            puzzle.append('   ')
            puzzle.append(self.symbol_result[sum_index][0])
            puzzle.append('   ')
            puzzle.append(self.symbol_result[sum_index][1])
            puzzle.append('   ')
            puzzle.append(self.symbol_result[sum_index][2:].rjust(3))
            puzzle.append('')
            if sum_index == 2:
                puzzle.append(self.symbol_result[3][2:].rjust(3))
            else:
                puzzle.append(self.symbol_result[3][sum_index])
            puzzle.append(' ')
            if sum_index == 2:
                puzzle.append(self.symbol_result[4][2:].rjust(3))
            else:
                puzzle.append(self.symbol_result[4][sum_index])
            puzzle.append(' ')
            if sum_index == 2:
                puzzle.append(self.symbol_result[5][2:].rjust(3))
            else:
                puzzle.append(self.symbol_result[5][sum_index])
            puzzle.append('')
            puzzle.append('')
        for sum_index in range(7):
            puzzle.append('')
        self.lazy_puzzle = puzzle

    def filter_1(self, sum_index, combinations):
        """
        for sum sum_index delete candidates not in combinations
        """
        datum_indexes = Sgg.sum_to_datum[sum_index]
        for item_index in range(3):
            datum_index = datum_indexes[item_index]
            datum = self.datum[datum_index]
            for candidate in range(1, 10):
                keep = False
                for combination in combinations:
                    if combination[item_index] == candidate:
                        keep = True
                if not keep:
                    datum.del_candidate(candidate-1)
                    error = datum.is_consistent()
                    if error != 0:
                        PzlG.logger.logger.error("filter_1 ")
                        self.status = False

    def check_errors(self):
        self.status = True
        self.errors = []
        for datum_index in range(9):
            datum = self.datum[datum_index]
            error = datum.is_consistent()
            if error != 0:
                # NB: record datum_index
                self.errors.append(datum_index)
        if self.errors:
            self.status = False

    def reset(self, sum_index):
        """
        for sum sum_index
        """
        datum_indexes = Sgg.sum_to_datum[sum_index]
        for item_index in range(3):
            datum_index = datum_indexes[item_index]
            datum = self.datum[datum_index]
            datum.reset(None)

    def show_solution(self):
        for datum_index in range(9):
            datum = self.datum[datum_index]
            datum.set_guess(datum.value)

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

    def reset_errors(self):
        updates = []
        a_op = Egg.ADD_CANDIDATES
        s_op = Egg.SET_GUESS
        for datum_index in self.errors:
            datum = self.datum[datum_index]
            datum.reset(None)
            square_index = Sgg.datum_to_square[datum_index]
            for candidate in range(9):
                wop = WidgetOps(a_op, square_index, None, [candidate])
                updates.append(wop)
            wop = WidgetOps(s_op, square_index, ' ', [])
            updates.append(wop)
        self.errors = []
        self.status = True
        return updates

    def get_square_updates(self, datum_index):
        updates = []
        square_index = Sgg.datum_to_square[datum_index]
        datum = self.datum[datum_index]
        d_op = Egg.DELETE_CANDIDATES
        a_op = Egg.ADD_CANDIDATES
        s_op = Egg.SET_GUESS
        for candidate in range(9):
            if not datum.candidate_is_set(candidate):
                wop = WidgetOps(d_op, square_index, None, [candidate])
                updates.append(wop)
            else:
                wop = WidgetOps(a_op, square_index, None, [candidate])
                updates.append(wop)
        if datum.guess is not None:
            wop = WidgetOps(s_op, square_index, datum.guess - 1, [])
            updates.append(wop)
        else:
            wop = WidgetOps(s_op, square_index, ' ', [])
            updates.append(wop)
        return updates

    def get_strip_updates(self, sum_index):
        updates = []
        datum_indexes = Sgg.sum_to_datum[sum_index]
        for item_index in range(3):
            datum_index = datum_indexes[item_index]
            square_updates = self.get_square_updates(datum_index)
            updates.extend([x for x in square_updates])
        return updates

    def apply_user_edits(self, square_index, candidate, mouse_index):
        """
        """
        updates = []
        if candidate is not None:
            datum_index = Sgg.square_to_datum[square_index]
            datum = self.datum[datum_index]
            if mouse_index == 1:
                # del candidate
                datum.del_candidate(candidate)
            elif mouse_index == 2:
                # reveal candidate
                datum.add_candidate(candidate)
            elif mouse_index == 3:
                # set value
                datum.set_guess(candidate+1)

            updates = self.get_square_updates(datum_index)
        return updates


class Board(tk.Frame):
    def __init__(self, parent, settings, images, datum, puzzle):
        self.settings = settings
        self.images = images
        self.datum = datum
        self.puzzle = puzzle
        self.grid_widgets = [None] * Sgg.symbol_count * Sgg.symbol_count
        bg = self.settings.black
        tk.Frame.__init__(self, parent, relief='flat',
                          highlightbackground=self.settings.grey,
                          highlightthickness=2,
                          bd=2, bg=bg)
        width = self.settings.square_size
        height = self.settings.square_size
        square_index = 0
        datum_index = 0
        for row in range(7):
            for column in range(7):
                puzzle_item_type = Sgg.cell_types[square_index]
                bg = self.settings.white
                widget = None
                if puzzle_item_type == Sgg.value:
                    # value square
                    bg_colour = 'white'
                    widget = Square(self, self.settings,
                                    self.datum[datum_index],
                                    bg_colour, square_index, row, column)
                    datum_index += 1
                else:

                    if puzzle_item_type == Sgg.blank_black:
                        bg = self.settings.dark_grey
                    if puzzle_item_type == Sgg.blank_white:
                        bg = self.settings.dark_grey
                    if puzzle_item_type == Sgg.result:
                        bg = self.settings.grey
                    if puzzle_item_type == Sgg.hint:
                        bg = self.settings.green
                    frame = tk.Frame(
                        self,
                        bd=0,
                        relief='flat',
                        highlightbackground=self.settings.black,
                        highlightthickness=1,
                        bg=bg,
                        width=width,
                        height=height
                    )
                    frame.grid(row=row, column=column, sticky='NSEW')
                    frame.grid_propagate(0)
                    frame.columnconfigure(0, weight=1)
                    frame.rowconfigure(0, weight=1)
                    if puzzle_item_type == Sgg.symbol:
                        # symbol square
                        text = self.puzzle[square_index]
                        label = tk.Label(
                            frame,
                            bd=0,
                            highlightbackground=self.settings.black,
                            highlightthickness=1,
                            bg=bg,
                            font=self.settings.value_font,
                            text=text)

                        label.grid(row=0, column=0, sticky='NSEW')
                        widget = label
                    elif puzzle_item_type == Sgg.result:
                        # result square
                        text = self.puzzle[square_index]
                        label = tk.Label(
                            frame,
                            bd=0,
                            highlightbackground=self.settings.black,
                            highlightthickness=1,
                            bg=bg,
                            font=self.settings.value_font,
                            text=text)
                        label.grid(row=0, column=0, sticky='NSEW')
                        widget = label
                        widget.grid_propagate(0)
                    elif puzzle_item_type == Sgg.hint:
                        # hint square
                        mbutton = MButton(frame, image=self.images.big_hint)
                        mbutton.grid(row=0, column=0, sticky='NSEW')
                        mbutton.grid_propagate(0)
                        menu = tk.Menu(mbutton, tearoff=0)
                        mbutton['menu'] = menu
                        widget = [mbutton, menu]
                self.grid_widgets[square_index] = widget
                square_index += 1
        self.grid(row=0, column=0)

    def my_destroy(self):
        self.destroy()

    def add_candidate_callback(self, func):
        c_widgets = [0, 2, 4, 14, 16, 18, 28, 30, 32]
        for wi in c_widgets:
            widget = self.grid_widgets[wi]
            candidates = widget.candidates
            for candidate in candidates:
                candidate.callback = func

    def update_square(self, update):
        """
        set some colours
        """
        operation = update.operation
        cell_id = update.cell_id
        widget = self.grid_widgets[cell_id]
        bg_colour = 'white'
        update.colour = bg_colour
        if 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.puzzle = None
        self.image_dic = {}
        self.make_image_dic()
        self.do_layout(self)

    def make_image_dic(self):
        """
        image, button_index, start
        """
        dic = {}
        dic['start'] = [self.images.start_sumpuzzle, 0, True]
        dic['consistent'] = [self.images.consistent, 1, True]
        dic['inconsistent'] = [self.images.inconsistent, 1, False]
        dic['show_solution'] = [self.images.smiley, 2, True]
        dic['length'] = [None, 3, False]
        self.image_dic = dic

    def do_layout(self, root):
        bg = self.settings.grey
        self.outer_frame = tk.Frame(root, bg=bg)
        self.outer_frame.grid()
        bg = self.settings.green
        self.control_frame = tk.Frame(self.outer_frame,
                                      bd=2, bg=bg, relief='raised')
        self.control_frame.grid(row=0, column=0, sticky='EW')
        self.menu_bar = tk.Frame(self.control_frame, relief='raised',
                                 bd=1, bg=bg)
        self.menu_bar.grid(row=0, column=0, sticky='W')
        self.menu = Menu(self.settings, self.menu_bar, self.images.file)
        self.toolbar = Toolbar(self.settings, self.image_dic,
                               self.control_frame)
        self.toolbar.grid(row=0, column=1, sticky='W')
        self.number_frame = tk.Frame(self.control_frame, relief='raised',
                                     bg=bg)
        self.number_frame.grid(row=0, column=2, sticky='NS')
        self.clock_box = ClockBox(self, self.number_frame)
        self.clock_box.grid(row=0, column=2, sticky='NSEW')
        self.top_frame = tk.Frame(self.outer_frame, relief='flat',
                                  bd=1, bg=bg)
        self.top_frame.grid(row=1, column=0, sticky='NSEW')
        self.board = None

    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.images, self.datum, self.puzzle)

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

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

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

    def add_hint_callback(self, func):
        for square_index in Sgg.hint_squares:
            widgets = self.board.grid_widgets[square_index]
            widget = widgets[0]
            widget.callback = func
            # add button index too
            widget.button_index = square_index

    def add_combinations(self, combinations):
        """
        Stick the combinations on the board
        """
        menu_index = 0
        for square_index in range(49):
            puzzle_item_type = Sgg.cell_types[square_index]
            if puzzle_item_type == Sgg.hint:
                widget = self.board.grid_widgets[square_index]
                menu = widget[1]
                # this is horrible, but cannot find alternative!
                counter = 0
                while counter < 50:
                    ret = menu.delete(0)
                    counter += 1
                combination = combinations[menu_index]
                for comb_index in range(len(combination)):
                    menu.add_command(label=combination[comb_index])
                menu_index += 1

    def reset_hint_buttons(self, status):
        for square_index in Sgg.hint_squares:
            widgets = self.board.grid_widgets[square_index]
            widget = widgets[0]
            if status:
                widget.enabled()
            else:
                widget.disabled()


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

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

    def start_game(self):
        self.view.clock_init()
        self.view.clock_restart()
        self.view.game_over()
        self.model.get_solvable_puzzle()
        self.view.datum = self.model.datum
        self.view.puzzle = self.model.lazy_puzzle
        self.view.make_board()
        self.add_callbacks()
        self.add_combinations()
        self.view.game_on()
        self.view.reset_hint_buttons(True)
        self.flasher = Flasher(self.view.clock.box.label,
                               self.master, self.settings)
        strargs = ''
        for item in self.model.lazy_puzzle:
            strargs += ' ' + item
        PzlG.logger.logger.info("puzzle_data " + strargs)

    def start_external_game(self):
        self.view.clock_init()
        self.view.clock_restart()
        self.view.game_over()
        self.view.datum = self.model.datum
        self.view.puzzle = self.model.lazy_puzzle
        self.view.make_board()
        self.add_callbacks()
        self.add_combinations()
        self.view.game_on()
        self.view.reset_hint_buttons(True)
        self.flasher = Flasher(self.view.clock.box.label,
                               self.master, self.settings)
        strargs = ''
        for item in self.model.lazy_puzzle:
            strargs += ' ' + item
        PzlG.logger.logger.info("puzzle_data " + strargs)

    def add_combinations(self):
        self.view.add_combinations(self.model.combinations)

    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.add_hint_callback(self.hint_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 == 'set_difficulty':
            self.set_difficulty()
            self.start_game()
        elif name == 'font_size':
            self.set_font_size()
        elif name == 'read_puzzle':
            self.read_puzzle()
        elif name == 'enter_new_puzzle':
            self.enter_puzzle()
        elif name == 'save_puzzle':
            self.save_puzzle()
        elif name == 'save_settings':
            self.settings.preferences.save()
        elif name == 'about':
            self.view.about()
        elif name == 'help':
            self.view.help()

    def service_toolbar(self, button_index, mouse_index):
        PzlG.logger.logger.info("service_toolbar " +
                                str(button_index) +
                                ' ' +
                                str(mouse_index))
        if mouse_index == 2:
            self.view.do_tooltip(button_index, None)
        elif mouse_index == 1:
            if button_index == 0:
                self.start_game()
                self.screen_shot.snap()
            elif button_index == 1:
                if not self.model.is_finished():
                    self.check_errors()
            elif button_index == 2:
                if not self.model.is_finished():
                    self.show_solution()

    def square_event(self, square_index, candidate, mouse_index):
        """
        User edit
        """
        if not self.model.is_finished():
            updates = self.model.apply_user_edits(square_index,
                                                  candidate,
                                                  mouse_index)
            self.view.update_squares(updates)
            self.screen_shot.snap()
            self.check_finished()

    def hint_event(self, square_index, mouse_index):
        """
        User has clicked on a hint button
        mouse 1: popup menu of combinations
        mouse 2: restore a strip of candidates
        mouse 3: apply a simple filter
        """
        # make sure the puzzle is consistent first
        self.model.check_errors()
        if self.model.status:
            # must be OK
            sum_index = Sgg.square_to_datum[square_index]
            if mouse_index == 3:
                # apply filter
                self.model.filter_1(sum_index,
                                    self.model.combinations[
                                        sum_index])
                board_updates = self.model.get_strip_updates(sum_index)
                self.view.update_squares(board_updates)
            elif mouse_index == 2:
                # reset all candidates
                self.model.reset(sum_index)
                board_updates = self.model.get_strip_updates(sum_index)
                self.view.update_squares(board_updates)

        elif not self.error_detected:
            # first sign of trouble, so tell the world
            self.view.thumbs_down()
            self.colour_errors()
            self.view.reset_hint_buttons(False)
            self.error_detected = True

    def check_errors(self):
        if self.error_detected:
            # may have deleted edits
            self.model.check_errors()
            if self.model.errors:
                board_updates = self.model.reset_errors()
                self.uncolour_errors()
                self.view.update_squares(board_updates)
            self.view.reset_hint_buttons(True)
            self.error_detected = False
            self.ready_for_hint()
        else:
            self.model.check_errors()
            if not self.model.status:
                self.view.thumbs_down()
                self.colour_errors()
                self.view.reset_hint_buttons(False)
                self.error_detected = True

    def colour_errors(self):
        """
        Colour the error squares and note their indexes for uncolouring.
        """
        self.coloured_squares = []
        board_updates = []
        error_op = Egg.HIGHLIGHT_ERROR
        text = ' '
        for datum_index in self.model.errors:
            square_index = Sgg.datum_to_square[datum_index]
            wop = WidgetOps(error_op, square_index, text, [])
            board_updates.append(wop)
            self.coloured_squares.append(datum_index)
        self.view.update_squares(board_updates)

    def uncolour_errors(self):
        """
        Uncolour coloured squares and empty the list.
        """
        board_updates = []
        error_op = Egg.UNHIGHLIGHT_ERROR
        text = ' '
        for datum_index in self.coloured_squares:
            square_index = Sgg.datum_to_square[datum_index]
            wop = WidgetOps(error_op, square_index, text, [])
            board_updates.append(wop)
        self.coloured_squares = []
        self.view.update_squares(board_updates)

    def check_finished(self):
        """
        Check if finished.
        If so, handle buttons, flasher and clock
        """
        if not self.model.is_finished():
            self.ready_for_hint()
        else:
            self.screen_shot.snap()
            self.view.game_over()
            self.view.reset_hint_buttons(False)
            self.view.clock_stop()
            self.flasher.flash()

    def set_difficulty(self):
        difficulty = int(self.settings.dic['set_difficulty'])
        self.model.puzzle.max_choices = difficulty

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

    def read_puzzle(self):
        my_file = self.view.sg_open_file(False, _('Input file'))
        if my_file:
            self.model.read_puzzle(my_file)
            if self.model.lazy_puzzle:
                self.start_external_game()

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

    def enter_puzzle(self):
        self.model.enter_puzzle()
        if self.model.lazy_puzzle:
            self.start_external_game()

    def show_solution(self):
        self.model.show_solution()
        for sum_index in range(6):
            board_updates = self.model.get_strip_updates(sum_index)
            self.view.update_squares(board_updates)
        self.view.game_over()
        self.view.reset_hint_buttons(False)
        self.view.clock_stop()

    def ready_for_hint(self):
        if not self.model.is_finished():
            if self.model.status:
                self.view.reset_hint_buttons(True)
                self.view.thumbs_up()


class ModelData(object):
    """
    Two dictionaries:
    a. all the trimers that satisfy each symbol/result combination
    b. all the symbol/results that can be used with a trimer
    """
    def __init__(self):
        self.symbol_results_dic = '\
 '
        self.trimers_dic = '\
 '
        self.symbol_results_dic = base64.b64decode(self.symbol_results_dic)
        self.symbol_results_dic = zlib.decompress(self.symbol_results_dic)
        self.make_symbol_results_dic()
        self.trimers_dic = base64.b64decode(self.trimers_dic)
        self.trimers_dic = zlib.decompress(self.trimers_dic)
        self.make_trimers_dic()

    def make_trimers_dic(self):
        """
        Example entry in this dic:
            '[3,9,1]':
                [
            12 ,
            [
            ['-/-6',2],
            ['-+-5',4],
            ['x/27',4],
            ['xx27',6],
            ['x+28',6],
            ['+/12',6],
            ['x-26',10],
            ['+x12',10],
            ['-x-6',18],
            ['+-11',24],
            ['---7',36],
            ['++13',42] ]],
        """
        # a word is, say, '-/-6'
        # a key is 3 consecutive digits
        dic = {}
        items = self.trimers_dic.decode('utf8').split(',')
        this_key = "["
        word = ""
        want_key = True
        want_sum = False
        key_count = 0
        pair_count = 0
        pairs = []
        for item in items:
            if item[0].isdigit():
                if want_key:
                    this_key += item
                    key_count += 1
                    if key_count < 3:
                        this_key += ", "
                    else:
                        this_key += "]"
                        want_key = False
                elif want_sum:
                    # must be sum
                    this_sum = int(item)
                    pair = [word, this_sum]
                    pairs.append(pair)
                    want_sum = False
                    pair_count += 1
                else:
                    # must be new key
                    entry = [pair_count, pairs]
                    dic[this_key] = entry
                    want_key = True
                    this_key = "[" + item + ", "
                    key_count = 1
                    pair_count = 0
                    pairs = []
            else:
                # must be word
                word = item
                want_sum = True

        entry = [pair_count, pairs]
        dic[this_key] = entry
        self.trimers_dic = dic

    def make_symbol_results_dic(self):
        """
        Example entry in this dic
            '++23':
            [
            [6,8,9],
            [6,9,8],
            [8,6,9],
            [8,9,6],
            [9,6,8],
            [9,8,6],
            ],
        """
        ops = ['+', '-', 'x', '/']
        dic = {}
        digits = []
        triples = []
        this_key = None
        items = self.symbol_results_dic.decode('utf8').split(',')
        for item in items:
            if item[0] in ops:
                # new key
                if triples:
                    dic[this_key] = triples
                this_key = item
                digits = []
                triples = []
            else:
                # must be digit
                if len(digits) < 3:
                    digits.append(int(item))
                if len(digits) == 3:
                    triples.append(digits)
                    digits = []
        if triples:
            dic[this_key] = triples
        self.symbol_results_dic = dic


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