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

# Copyright (C) 2018 Rod Engerstad
import sys
import random
from random import randint
import Tkinter as tk
import tkColorChooser

from pzl_lib import PzlG
from pzl_lib import SettingsB
from pzl_lib import Preferences
from pzl_lib import MenuItem
from pzl_lib import Flasher
from pzl_lib import ViewB
from pzl_lib import TextSquare
from hitori_data import ModelData


class Sgg(object):
    """
    Game globals
    """
    grid_width = 6
    grid_height = 6
    cell_count = grid_width * grid_height
    VIRGIN = 0
    CIRCLED = 1
    SHADED = 2
    DELETE = 3
    GIVE_HINT = 0
    APPLY_HINT = 1
    generate = False
    alg_usage = [0] * 20


class Settings(SettingsB):

    def __init__(self):
        SettingsB.__init__(self)
        self.original_square_size = 36
        self.square_size = self.original_square_size
        self.dic = {}
        self.dic['set_colour'] = 'circled_colour'
        self.dic['font_size'] = '2'
        self.dic['virgin_colour'] = '#FFFFFF'
        self.dic['circled_colour'] = '#EDD400'
        self.dic['shaded_colour'] = '#555753'
        self.dic['hint_bg_colour'] = '#FFA500'
        self.dic['hint_match_colour'] = '#FF0000'
        self.dic['cause_match_colour'] = '#FFFFFF'
        self.dic['cause_shaded_colour'] = '#008000'
        self.dic['value_colour'] = '#000000'
        self.dic['error_colour'] = '#FF0000'
        self.dic['un_active'] = 'no'
        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_alg_help(self):
        self.alg_text = [_(" "), _("ES: Shaded cells cannot touch orthogonally. This cell is adjacent to a shaded cell and so must be circled."), _("SM: No row or column can have more than one occurrence of any given number. This cell is aligned with a circled cell with the same number, so it must be shaded."), _("T0: Shaded cells cannot touch orthogonally. This cell is sandwiched between two cells containing the same number. The only way to avoid adjacent shaded cells is to circle this cell."), _("P0: Shaded cells cannot touch orthogonally. This cell is sandwiched between two cells containing the same number. The only way to avoid adjacent shaded cells is to circle this cell."), _("P1: Shaded cells cannot touch orthogonally. This cell is aligned with a pair of adjacent cells with the same number. One of the adjacent cells must be circled to avoid adjacent shaded cells, so this cell must also be shaded to avoid there being two aligned identical values."), _("C0: Shaded cells cannot touch orthogonally. This cell is in a square of cells with the same number. The only configuration which will avoid a pair of adjacent shaded cells and not block access to the corner is to shade this cell and the cell diagonally adjacent. The other two must be circled."), _("C1: Shaded cells cannot touch orthogonally. This cell is in a triangle of cells with the same number. The only configuration which will avoid a pair of adjacent shaded cells and not block access to the corner is to shade this cell. The other two must be circled."), _("C2: Shaded cells cannot touch orthogonally. This cell is one of a pair of adjacent identical numbers and adjacent to a pair of different identical numbers. The only configuration which will avoid a pair of adjacent shaded cells and not block access to the corner is to shade this cell."), _("C3: This cell is next to a pair of cells with the same number. One of these identical cells must be shaded. If the hint cell is shaded, the adjacent cell of the pair must be circled, and the other shaded. But then the cell in the corner would be blocked. Hence the hint cell must be circled."), _("C4: Shaded cells cannot touch orthogonally. This cell is one of a triangle of cells with the same number. If the hint cell is shaded, the adjacent cell of the three must be circled and the other shaded. Then the cell in the corner cannot be shaded and so must be circled. But it cannot be circled as the two shaded cells would blocked its access to the other circled cells. Hence the hint cell must be circled."), _("C5: This cell is next to a pair of cells with identical numbers. One of these must be shaded. If the hint cell is shaded, the adjacent one of the pair must be circled, and the other shaded. But then the cell in the corner must be circled. However, by then it would be blocked. Hence the hint cell must be circled."), _("C6: This cell is next to a pair of cells with the same number. One of these matching cells must be shaded. If the hint cell is shaded the cell in the corner must be circled and the other shaded. But then there would be no access to the circled cell in the corner. So the hint cell must be circled."), _("E0: The hint cell is adjacent to two pairs of adjacent matching cells. If the hint cell is shaded, the touching cell of the pairs must be circled and the one it matches shaded. Then the touching cell of the other pair must be shaded. But now a circled cell is blocked on all sides. Hence the hint cell must be circled."), _("E1: The hint cell is touching three pairs of cells with matching numbers. If the hint cell is shaded, the cells it is touching must be circled. This includes one cell in the matching pairs, so now its matching cell must be shaded and then surrounded by circled cells. Then the partners of the matching cells must be shaded, but this leads to a circled cell being blocked. So the hint cell must be circled."), _("P2: The hint cell is touching one each of two pairs of cell with matching numbers. A cell from one pair is adjacent to one of the other pair. We know that one of each pair must be shaded. If the hint cell were shaded the cells it touches must be circled, including one of each matching pair. This means that the other matching cells must be shaded. But then we would have touching shaded cells. So the hint cell cannot be shaded and must be circled."), _("P3: The hint cell lies between two cell which contain its number. Each of these outer cells is adjacent, on the same side, to cells which have matching numbers. If we circle the hint cell both of the outer matching cells must be shaded. Both of these are adjacent to cells with the same number so one of these adjacent cells must be shaded. But, then we would have two adjacent shaded cells. Hence, the hint cell must be shaded."), _("P4: The hint cell is aligned with two cells which contain its number and which sandwich a pair adjacent cells whose numbers match one another. If the hint cell is circled the other aligned cells with the same number must be shaded. But, one of the pair of sandwiched matching cells must also be shaded, each of which would be adjacent to a shaded cell. Hence the clue cell must be shaded"), _("L0: The hint cell provides the only access to an adjacent circled cell, and so must, itself be circled."), _("L1: The hint cell provides the only access between the circled cells in two parts of the board, and so must be circled.")]

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

    def make_help(self):
        self.help_text = _(" The program starts with a randomly selected puzzle displayed. It shows a grid of white cells, each containing a number between 1 and 9. Players can shade cells with a left mouse click, circle with right mouse, and can undo with middle mouse. At the top of the display is a Toolbar. The jigsaw piece button at the left is a menu which has options for configuring the program and saving the current settings. The next button to the right is used for selecting puzzles: left mouse will select a random puzzle and right mouse the next one in sequence. Next is a wand for selecting and executing hints; a thumb for checking and correcting errors, and a sad smiley for showing the solution. To the right of this are 3 numbers. The first is the puzzle number, the second the puzzle difficulty score and the next a clock which ticks every 5 seconds. The puzzle number display is also an input box: players can position the cursor over this area and type in a number between 0 and 1999 followed by 'Enter' to load the corresponding puzzle. When a hint is requested using the wand a cell in the grid will be shaded the current hint colour (orange by default) and the numbers making up the discovered pattern coloured red or green (default colours). Red indicating matching values and green denoting access. If the player can work out the hint cell's correct colour they can set it in the usual way. If not a further click on the wand will colour it. When a hint is requested the clock display is used to show the code for the algorithm used. If the player clicks the thumb button the grid is checked for errors and they are reported as follows: the thumb turns down and the inccorrect cells are shaded red. A further click on the thumb will remove the red shading and undo all changes since the first mistake was made.If the player requests a hint when errors are present no hint will be given and the thumb will turn down and the mistakes marked. The errors must be corrected using the thumb button before a hint can be given. All the colours can be set using an option in the menu.")

    def load_menus(self):
        self.menu_items.append(MenuItem(
            _('Font Size'),
            ['1',
             '2',
             '3',
             '4',
             '5',
             '6',
             '7',
             '8',
             '9',
             '10',
             '11',
             '12',
             '13'],
            '',
            ['1',
             '2',
             '3',
             '4',
             '5',
             '6',
             '7',
             '8',
             '9',
             '10',
             '11',
             '12',
             '13'],
            'font_size'
            ))
        self.menu_items.append(MenuItem(
            _('Set Colour'),
            [_('Unsolved colour'),
             _('Circled colour'),
             _('Shaded colour'),
             _('Hint background colour'),
             _('Hint match colour'),
             _('Cause match colour'),
             _('Cause shaded colour'),
             _('Value colour'),
             _('Error colour')],
            'N',
            ['virgin_colour',
             'circled_colour',
             'shaded_colour',
             'hint_bg_colour',
             'hint_match_colour',
             'cause_match_colour',
             'cause_shaded_colour',
             'value_colour',
             'error_colour',],
            'set_colour'
        ))
        self.menu_items.append(MenuItem(
            _('Save Settings'),
            '',
            '',
            '',
            'save_settings'
        ))
        self.menu_items.append(MenuItem(
            _('Help'),
            '',
            '',
            '',
            'help'
        ))
        self.menu_items.append(MenuItem(
            _('About'),
            '',
            '',
            '',
            'about'
        ))


class UserEdits(object):
    """
    Actions performed by user stored as datums
    If we get an undo command we simple reset the cell to grey
    """
    def __init__(self):
        self.edits = []

    def add(self, datum):
        self.edits.append(datum)

    def delete(self, datum):
        datum_index = datum.index
        for item in self.edits:
            item_id = item.index
            if item_id == datum_index:
                try:
                    self.edits.remove(item)
                    datum.reset()
                    break
                except ValueError:
                    pass


class Datum(object):
    """
    Data item for a cell
    States are
    virgin (white)
    circled (yellow)
    shaded (grey)
    """
    def __init__(self, datum_index, value):
        self.index = datum_index
        self.answer = Sgg.CIRCLED
        self.state = Sgg.VIRGIN
        if value < 0:
            self.answer = Sgg.SHADED
        self.value = abs(value)
        self.row = None
        self.column = None
        self.neighbours_gg = []
        self.gneighbours = []
        self.set_row_and_column()
        self.set_neighbours()

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

    def reset(self):
        self.state = Sgg.VIRGIN

    def is_consistent(self):
        if self.state != Sgg.VIRGIN:
            ret = (self.state == self.answer)
        else:
            ret = True
        if not ret:
            PzlG.logger.logger.info("inconsistent datum " + str(self.index))
        return ret

    def set_row_and_column(self):
        self.row = self.index / Sgg.grid_width
        self.column = self.index % Sgg.grid_width

    def is_virgin(self):
        return self.state == Sgg.VIRGIN

    def is_circled(self):
        return self.state == Sgg.CIRCLED

    def is_shaded(self):
        return self.state == Sgg.SHADED

    def set_neighbours(self):
        """
        Set the neighbours
        """

        self.neighbours_gg = []

        # up
        nbr_index = self.index - Sgg.grid_width
        if nbr_index > -1:
            self.neighbours_gg.append(nbr_index)

        # right
        nbr_index = self.index + 1
        if (self.index + 1) % Sgg.grid_width != 0:
            self.neighbours_gg.append(nbr_index)

        # down
        nbr_index = self.index + Sgg.grid_width
        if nbr_index < Sgg.cell_count:
            self.neighbours_gg.append(nbr_index)

        # left
        nbr_index = self.index - 1
        if self.index % Sgg.grid_width != 0:
            self.neighbours_gg.append(nbr_index)


class Node(object):
    """
    A node for Djkstra
    """
    def __init__(self, datum, oob):
        self.index = datum.index
        self.neighbours = []
        for neighbour in datum.gneighbours:
            if neighbour not in oob:
                self.neighbours.append(neighbour)
        self.distance = 999999
        self.unvisited = True
        self.predecessor = None
        self.predecessors = []


class Dijkstra(object):
    """
    Dijkstra algorithm to find path from start to finish
    """
    def __init__(self, datums, start, finish, oob):
        self.path = []
        self.success = False
        self.start_node_index = start
        self.end_node_index = finish
        # node_dic: a dictionary of Nodes indexed on datum.index
        # (only need navigable ones, ie CIRCLED and VIRGIN)
        # next case add Jan 2018!
        if start == finish:
            self.path = []
            return

        self.node_dic = {}
        for datum in datums:
            good_datum = False
            if datum.index not in oob:
                for neighbour in datum.neighbours_gg:
                    if neighbour not in oob:
                        good_datum = True
                        break
            if good_datum:
                self.node_dic[datum.index] = Node(datum, oob)

        # check that end nodes are potentially reachable
        if self.end_node_index in self.node_dic and \
           self.start_node_index in self.node_dic:
            # active_dic a dictionary of nodes indexed on datum.index
            # at the beginning it contains only the start node but
            # we add each node as we meet it. When weve dealt with all
            # a nodes neighbours we remove it from active_dic
            self.active_dic = {}
#            for key, value in self.active_dic.iteritems():
            self.active_dic[self.start_node_index] = \
                                                     self.node_dic[self.start_node_index]
            # try to find the path
            self.doit()

    def doit(self):
        current_node_index = self.start_node_index
        current_node = self.active_dic[current_node_index]
        current_node.distance = 0
        while True:
            # find active node with shortest distance
            # only active nodes can be part of the path
            current_node = None
            shortest_distance = 999999
            for key, value in list(self.active_dic.items()):
                if value.distance < shortest_distance:
                    shortest_distance = value.distance
                    current_node_index = key
                    current_node = value

            if current_node is not None:
                # update distance for all its unvisited neighbours
                for neighbour_index in current_node.neighbours:
                    try:
                        neighbour_node = self.node_dic[neighbour_index]
                        if neighbour_node.unvisited:
                            # first visit so add it to active
                            self.active_dic[neighbour_node.index] =\
                                                                    neighbour_node
                            distance = current_node.distance + 1
                            if distance < neighbour_node.distance:
                                neighbour_node.distance = distance
                                neighbour_node.predecessor =\
                                                             current_node_index

                    except KeyError:
                        pass
                # mark current_node visited and remove it from active_nodes
                current_node.unvisited = False
                del self.active_dic[current_node_index]
                if not self.node_dic[self.end_node_index].unvisited:
                    # we've arrived!
                    self.success = True
                    break
            else:
                # there is no path!
                break

        self.do_path()

    def do_path(self):
        if self.success:
            path = []
            current_end = self.end_node_index
            while True:
                path.append(current_end)
                if current_end == self.start_node_index:
                    break
                current_end = self.node_dic[current_end].predecessor

            path.reverse()
            self.path = path


class Pattern(object):
    """
    A pattern

    x,y,z particular values
    n any value
    N any value in corner
    X particular value in corner (matches x)
    Y means particular value on edge (matches y)
    s shaded
    B means particular value circled (matches b)

    results

    C circled                    C
    S shaded                     S

    . gap of any size >0

     """
    def __init__(self, name, pattern, result):
        self.name = name
        self.pattern = pattern
        self.result = result
        self.width = None
        self.height = None
        self.get_size(pattern)
        self.starts = []
        self.offsets = []
        # matching symbols dic
        self.match_dic = {}
        # Shaded and Circled dic
        self.shaded_dic = {}
        self.circled_dic = {}
        self.result_dic = {}
        self.make_ranges()
        self.get_match_dic()
        self.get_shaded_dic()
        self.get_circled_dic()
        self.get_result_dic()

    def get_size(self, pattern):
        self.width = len(pattern[0])
        if len(pattern) > 1:
            # 2d
            self.height = len(pattern)
        else:
            self.height = 1

    def make_ranges(self):
        corners = ['X', 'N']
        edges = ['Y', 'Z']
        edge = False
        corner = False
        # if Y or Z pattern restricted to edge; X corner
        self.starts = []
        self.offsets = []
        for row in range(self.height):
            for column in range(self.width):
                self.offsets.append(column + row * Sgg.grid_width)

        for row_index in range(self.height):
            row = self.pattern[row_index]
            if row[0] in corners:
                if row_index > 0:
                    # bottom left
                    corner = True
                    start = (Sgg.grid_height - self.height) * Sgg.grid_width
                    self.starts = [start]
                    break
                else:
                    # top left
                    corner = True
                    self.starts = [0]
                    break
            elif row[self.width - 1] in corners:
                if row_index > 0:
                    # bottom right
                    corner = True
                    start = (Sgg.grid_height - self.height) * Sgg.grid_width
                    start += Sgg.grid_width - self.width
                    self.starts = [start]
                    break
                else:
                    # top right
                    corner = True
                    start = Sgg.grid_width - self.width
                    self.starts = [start]
                    break
        if not corner:
            # top edge
            row = self.pattern[0]
            for column_index in range(self.width):
                if row[column_index] in edges:
                    edge = True
                    self.starts = list(range(Sgg.grid_width -
                                             self.width + 1))
            if not edge:
                # bottom edge
                row = self.pattern[self.height - 1]
                for column_index in range(self.width):
                    if row[column_index] in edges:
                        edge = True
                        start = (Sgg.grid_height - self.height)
                        start *= Sgg.grid_width
                        self.starts = [start]
                        for i in range(Sgg.grid_width - self.width):
                            self.starts.append(self.starts[-1] + 1)
                if not edge:
                    # left edge
                    for row_index in range(self.height):
                        row = self.pattern[row_index]
                        if row[0] in edges:
                            edge = True
                            for row in range(Sgg.grid_height -
                                             self.height + 1):
                                row_s = Sgg.grid_width * row
                                self.starts.append(row_s)
                    if not edge:
                        # right edge
                        for row_index in range(self.height):
                            row = self.pattern[row_index]
                            if row[self.width - 1] in edges:
                                edge = True
                                start = Sgg.grid_width - self.width
                                for row in range(Sgg.grid_height -
                                                 self.height + 1):
                                    row_s = Sgg.grid_width * row
                                    self.starts.append(row_s + start)

        if not corner and not edge:
            for row in range(Sgg.grid_height - self.height + 1):
                row_s = Sgg.grid_width * row
                for column in range(Sgg.grid_width - self.width + 1):
                    self.starts.append(row_s + column)

    def get_match_dic(self):
        match_symbols = ['x', 'y', 'z', 'X', 'Y', 'Z', 'B', 'b']
        this_dic = {}
        item = -1
        for row in self.pattern:
            for column in row:
                item += 1
                if column in match_symbols:
                    offset = self.offsets[item]
                    try:
                        this_dic[column.lower()].append(offset)
                    except KeyError:
                        this_dic[column.lower()] = [offset]
        self.match_dic = this_dic

    def get_circled_dic(self):
        match_symbols = ['c', 'B']
        this_dic = {}
        item = -1
        for row in self.pattern:
            for column in row:
                item += 1
                if column in match_symbols:
                    offset = self.offsets[item]
                    try:
                        this_dic[column.lower()].append(offset)
                    except KeyError:
                        this_dic[column.lower()] = [offset]
        self.circled_dic = this_dic

    def get_shaded_dic(self):
        match_symbols = ['s']
        this_dic = {}
        item = -1
        for row in self.pattern:
            for column in row:
                item += 1
                if column in match_symbols:
                    offset = self.offsets[item]
                    try:
                        this_dic[column.lower()].append(offset)
                    except KeyError:
                        this_dic[column.lower()] = [offset]
        self.shaded_dic = this_dic

    def get_result_dic(self):
        match_symbols = ['S', 'C']
        this_dic = {}
        item = -1
        for row in self.result:
            for column in row:
                item += 1
                if column in match_symbols:
                    offset = self.offsets[item]
                    try:
                        this_dic[column.lower()].append(offset)
                    except KeyError:
                        this_dic[column.lower()] = [offset]
        self.result_dic = this_dic

    """
    def print_pattern(self, pattern):
        print
        for row in pattern:
            print row
        print
    """


class NPattern(object):
    """Receives a pattern defined as text.
    Calculates all symmetric versions of the pattern,
    and then creates an individual
    search pattern (using Pattern) for each case.
    """

    def __init__(self, text):
        def to_tuples(text):
            tmp = []
            if len(text) == 1:
                tmp.append(text[0])
            else:
                for line in text:
                    row = tuple(line)
                    tmp.append(row)
            return tmp

        name = text[0]
        pattern_in = text[1]
        result_in = text[2]
        patterns = []
        results = []
        self.search_patterns = []
        patternse, resultse = self.expand(pattern_in, result_in)
        patterns = []
        results = []
        for pattern in patternse:
            patterns.append(to_tuples(pattern))
        patternse = patterns
        patterns = []
        for result in resultse:
            results.append(to_tuples(result))

        resultse = results
        results = []
        for pattern in patternse:
            patterns += self.rotate(pattern)

        for result in resultse:
            results += self.rotate(result)

        self.make_search_patterns(name, patterns, results)

    def expand_sideways(self, pattern_in):
        # expand sideways
        new_patterns = []
        row = pattern_in[0]
        pre = ""
        column = 0
        while row[column] != '.':
            pre += row[column]
            column += 1
        # assume 2 dots!
        post = row[column + 2:]
        new_count = Sgg.grid_width - len(pre) - len(post) + 1
        for count in range(new_count):
            new_pattern = pre + "n" * count
            new_pattern += post
            new_patterns.append([new_pattern])
        return new_patterns

    def expand_lengthways(self, pattern_in, dots_index):
        # expand lengthways
        new_patterns = []
        pre = []
        width = len(pattern_in[0])
        for row in range(dots_index):
            pre.append(pattern_in[row])
        post = []
        for row in range(dots_index + 1, len(pattern_in)):
            post.append(pattern_in[row])
        nns = "n" * width
        len_pre = len(pre)
        len_post = len(post)
        new_count = Sgg.grid_height - len_pre - len_post + 1
        for count in range(new_count):
            new_pattern = []
            for row in range(len_pre):
                new_pattern.append(pre[row])
            for cnt in range(count):
                new_pattern.append(nns)
            for row in range(len_post):
                new_pattern.append(post[row])
            new_patterns.append(new_pattern)
        return new_patterns

    def contains_dots(self, pattern_in):
        # is expansion required?
        dots_index = None
        for row_index, row in enumerate(pattern_in):
            dots = 0
            for item in row:
                if item == '.':
                    dots += 1
            if dots == 2:
                dots_index = row_index
        return dots_index

    def expand_1(self, pattern_in, result_in):
        """
        Takes a single pattern and looks for the first dots
        Returns expanded set of patterns, having replaced the
        first set of dots with nns.
        If there are no dots it simply returns the input as
        a list with one member.
        """
        new_patterns = []
        new_results = []
        dots_index = self.contains_dots(pattern_in)
        if dots_index is not None:
            if dots_index == 0:
                new_patterns = self.expand_sideways(pattern_in)
                new_results = self.expand_sideways(result_in)
            else:
                new_patterns = self.expand_lengthways(pattern_in, dots_index)
                new_results = self.expand_lengthways(result_in, dots_index)
        else:
            # no expansion required
            new_patterns.append(pattern_in)
            new_results.append(result_in)
        return new_patterns, new_results

    def expand(self, pattern_in, result_in):
        """
        Takes a single pattern
        """
        new_patterns = []
        new_results = []
        new_patterns, new_results = self.expand_1(pattern_in,
                                                  result_in)
        pattern_count = len(new_patterns)
        while True:
            prev_pattern_count = pattern_count
            newer_patterns = []
            newer_results = []
            for item in range(len(new_patterns)):
                pin = new_patterns[item]
                rin = new_results[item]
                dots_index = self.contains_dots(pin)
                if dots_index is not None:
                    new_ps, new_rs = self.expand_1(pin, rin)
                    newer_patterns = newer_patterns + new_ps
                    newer_results = newer_results + new_rs
                else:
                    newer_patterns.append(pin)
                    newer_results.append(rin)
            pattern_count = len(newer_patterns)
            if pattern_count == prev_pattern_count:
                break
            new_patterns = newer_patterns
            new_results = newer_results
        return new_patterns, new_results

    def make_search_patterns(self, name, patterns, results):
        self.search_patterns = []
        for pattern_index, pattern in enumerate(patterns):
            search_pattern = Pattern(name + str(pattern_index),
                                     pattern, results[pattern_index])
            self.search_patterns.append(search_pattern)

    def rotate(self, text):
        """
        rotate 90, 180, 270 degrees
        """
        tmp = []
        rotated_90 = list(zip(*text[::-1]))
        rotated_180 = list(zip(*rotated_90[::-1]))
        rotated_270 = list(zip(*rotated_180[::-1]))
        tmp.append(text)
        tmp.append(rotated_90)
        tmp.append(rotated_180)
        tmp.append(rotated_270)
        return tmp

    """
    def print_pattern(self, pattern):
        print
        for row in pattern:
            print row

    def print_patterns(self, patterns):
        print 'print_patterns'
        print patterns
        print
        for pattern in patterns:
            for row in pattern:
                print row
            print
        print 'end print_patterns'
    """


class Hint(object):
    def __init__(self, datum_index, action, algorithm, causes):
        self.datum_index = datum_index
        self.action = action
        self.algorithm = algorithm
        self.causes = causes


class Model(object):
    def __init__(self, settings, model_data):
        self.settings = settings
        self.model_data = model_data
        self.datum = []
        self.puzzle_number = None
        self.score = None
        self.rows = []
        self.columns = []
        self.errors = []
        self.user_edits = UserEdits()
        self.hints = []
        self.finished = False
        self.status = True
        self.moves = []
        self.search_patterns = []
        self.algorithm = None
        self.alg_count = 20
        self.alg_usage = [0] * self.alg_count
        encircle_shaded = ['Encircle_shaded.', ['sn'], ['nC']]
        shade_matches = ['Shade_matches.',
                         ['B', '..', 'b'], ['B', '..', 'S']]
        triplet_0 = ['Triplet_0.', ['xxx'], ['xCx']]
        pair_0 = ['Pair_0.', ['xnx'], ['xCx']]
        pair_1 = ['Pair_1.', ['xx..x'], ['xx..S']]
        corner_0 = ['Corner_0.', ['Xx', 'xx'], ['Sx', 'xS']]
        corner_1 = ['Corner_1.', ['Xx', 'xn'], ['Sx', 'xn']]
        corner_2 = ['Corner_2.', ['Xy', 'xy'], ['Sy', 'xS']]
        corner_3 = ['Corner_3.', ['Xx', 'nn'], ['xx', 'Cn']]
        corner_4 = ['Corner_4.', ['Nn', 'xx'], ['NC', 'xx']]
        corner_5 = ['Corner_5.', ['Nx', 'nx'], ['Nx', 'Cx']]
        corner_6 = ['Corner_6.', ['Xn', 'xn'], ['XC', 'xn']]
        edge_0 = ['edge_0.', ['nn', 'Yx', 'Yx', 'nn'],
                  ['Cn', 'Yx', 'Yx', 'Cn']]
        edge_1 = ['edge_1.', ['nnn', 'Yxz', 'Yxz', 'nnn'],
                  ['CCn', 'Yxz', 'Yxz', 'CCn']]
        pair_2 = ['Pair_2.', ['xy', '..', 'xn', 'ny'],
                  ['xy', '..', 'xC', 'Cy']]
        pair_3 = ['Pair_3.', ['xy', '..', 'ny', '..', 'xy'],
                  ['xy', '..', 'nS', '..', 'xy']]
        pair_4 = ['Pair_4.', ['xyyx..x'], ['xyyx..S']]

        self.algorithms = [
            self.connectivity_1,
            self.connectivity_2
            ]
        # pattern numbers - 1 - 18
        # algorithm numbers 0, 19 - 20
        self.text_patterns = [
            encircle_shaded,
            shade_matches,
            triplet_0,
            pair_0,
            pair_1,
            corner_0,
            corner_1,
            corner_2,
            corner_3,
            corner_4,
            corner_5,
            corner_6,
            edge_0,
            edge_1,
            pair_2,
            pair_3,
            pair_4]

    def get_search_patterns(self):
        self.search_patterns = []
        for text_pattern in self.text_patterns:
            patterns = NPattern(text_pattern).search_patterns
            for pattern in patterns:
                self.search_patterns.append(pattern)

    def update_alg_usage(self, alg):
        self.alg_usage[alg] += 1
        Sgg.alg_usage[alg] += 1

    def zero_alg_usage(self):
        self.alg_usage = [0] * self.alg_count

    def get_score(self):
        scores = [1, 1, 1, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3,
                  4, 5, 5, 5, 5, 2, 3]
        score = 0
        for alg_index in range(self.alg_count):
            score += self.alg_usage[alg_index] * scores[alg_index]
        return score

    def make_indexes(self):
        self.rows = []
        self.columns = []
        for row in range(Sgg.grid_height):
            this_row = []
            for column in range(Sgg.grid_width):
                this_row.append(column + row * Sgg.grid_width)
            self.rows.append(this_row)
        for column in range(Sgg.grid_width):
            this_column = []
            for row in range(Sgg.grid_height):
                this_column.append(column + row * Sgg.grid_height)
            self.columns.append(this_column)

    def unique_number(self):
        """
        Any cell whose value is unique to its row and column must be circled
        """

        result = []
        if self.settings.dic['un_active'] != 'yes':
            return result
        alg = self.algorithm
        for datum_0 in self.datum:
            if datum_0.state == Sgg.VIRGIN:
                rows = self.rows[datum_0.row]
                columns = self.columns[datum_0.column]
                this_index = datum_0.index
                this_value = datum_0.value
                unique = True
                for item in rows:
                    datum = self.datum[item]
                    if datum.index != this_index and\
                       datum.value == this_value and\
                       datum.state != Sgg.SHADED:
                        unique = False
                        break
                if unique:
                    for item in columns:
                        datum = self.datum[item]
                        if datum.index != this_index and\
                           datum.value == this_value and\
                           datum.state != Sgg.SHADED:
                            unique = False
                            break
                    if unique:
                        this_result = Hint(this_index, Sgg.CIRCLED, alg, [])
                        result.append(this_result)

        return result

    def connectivity_1(self):
        """
        Every circled cell must be able to reach all others.
        If we find a circled cell which only touches a single virgin
        then that virgin must be circled, otherwise the circled cell
        will not be connected.
        """

        result = []
        alg = self.algorithm
        for datum in self.datum:
            if datum.state == Sgg.CIRCLED:
                # this is a circled cell
                outs = []
                for nbr_index in datum.neighbours_gg:
                    nbr_datum = self.datum[nbr_index]
                    if nbr_datum.state == Sgg.CIRCLED:
                        # it has a circled neighbour so is connected
                        outs = [None, None]
                        break
                    elif nbr_datum.state == Sgg.VIRGIN:
                        # touches a virgin
                        outs.append(nbr_index)
                if len(outs) == 1:
                    # only touches 1 virgin so the virgin must be circled
                    causes = [[], [outs[0]], []]
                    this_result = Hint(outs[0], Sgg.CIRCLED, alg, causes)
                    result.append(this_result)
                    break
        return result

    def set_gneighbours(self, oob):
        """
        For Dijkstra we need neighbours of clue_cells and unknowns
        """
        for datum in self.datum:
            datum.gneighbours = []
            if datum.index not in oob:
                for neighbour in datum.neighbours_gg:
                    if self.datum[neighbour].index not in oob:
                        datum.gneighbours.append(neighbour)

    def connectivity_2(self):
        """
        Every circled cell must be able to reach all others.
        Here we find all VIRGINS and check that their neighbours
        can reach one another if the VIRGIN were to be SHADED.
        """

        result = []
        alg = self.algorithm
        virgins = []
        shaded = []
        for datum in self.datum:
            if datum.state == Sgg.VIRGIN:
                virgins.append(datum)
            elif datum.state == Sgg.SHADED:
                shaded.append(datum.index)

        for virgin in virgins:
            # add virgin to shaded (which are out of bounds for Dijkstra)
            shaded.append(virgin.index)
            self.set_gneighbours(shaded)
            # try path between pairs of virgin's neighbours
            blocker = False
            for nbr_index_0 in virgin.neighbours_gg:
                for nbr_index_1 in virgin.neighbours_gg:
                    if nbr_index_1 != nbr_index_0:
                        dijkstra = Dijkstra(self.datum,
                                            nbr_index_0,
                                            nbr_index_1,
                                            shaded)
                        if not dijkstra.path:
                            # no path so virgin cannot be shaded (so circled)
                            causes = [[], [virgin.index], []]
                            this_result = Hint(virgin.index,
                                               Sgg.CIRCLED,
                                               alg,
                                               causes)
                            result.append(this_result)
                            blocker = True
                            break

                if blocker:
                    break
            if blocker:
                break
            shaded.pop()
        return result

    def apply_pattern(self, pattern):
        """
        for each pattern.starts
        try pattern_match_dic
        """
        codes = {'s': Sgg.SHADED, 'c': Sgg.CIRCLED}
        starts = pattern.starts
        match_dic = pattern.match_dic
        shaded_dic = pattern.shaded_dic
        circled_dic = pattern.circled_dic
        result_dic = pattern.result_dic
        name = pattern.name
        if name.find('.0') != -1:
            self.algorithm += 1
        alg = self.algorithm
        result = []
        good = True
        for start in starts:
            causes = []
            good = True
            cause = []
            for symbol, offsets in list(match_dic.items()):
                this_value = self.datum[start + offsets[0]].value
                for offset in offsets:
                    if this_value != self.datum[start + offset].value:
                        # mismatch
                        good = False
                        break
                    cause.append(start + offset)
                if not good:
                    break
            if good:
                causes.append(cause)
                cause = []
                for symbol, offsets in list(shaded_dic.items()):
                    for offset in offsets:
                        if not self.datum[start + offset].is_shaded():
                            # mismatch
                            good = False
                            break
                        cause.append(start + offset)
                    if not good:
                        break
                if good:
                    causes.append(cause)
                    cause = []
                    for symbol, offsets in list(circled_dic.items()):
                        for offset in offsets:
                            if not self.datum[start + offset].is_circled():
                                # mismatch
                                good = False
                                break
                            cause.append(start + offset)
                        if not good:
                            break
                    if good:
                        causes.append(cause)
                        for symbol, offsets in list(result_dic.items()):
                            code = codes[symbol]
                            for offset in offsets:
                                datum = self.datum[start + offset]
                                if datum.is_virgin():
                                    this_result = Hint(start + offset,
                                                       code, alg, causes)
                                    result.append(this_result)
                                    break
        return result

    def apply_searches(self):
        self.algorithm = 0
        self.hints = []
        self.hints = self.unique_number()
        if not self.hints:
            self.algorithm = 0
            for pattern in self.search_patterns:
                self.hints = self.apply_pattern(pattern)
                if self.hints:
                    break
            if not self.hints:
                for algorithm in self.algorithms:
                    self.algorithm += 1
                    self.hints = algorithm()
                    if self.hints:
                        break

    def do_hint(self):
        if not self.is_consistent():
            self.status = False
        else:
            self.apply_searches()

    def solver(self):
        self.zero_alg_usage()
        status = False
        progress = True
        while progress:
            progress = False
            self.apply_searches()
            """ when generating cannot check consistency
            if not self.check_consistency():
                status = False
                break
            """
            self.check_finished()
            if self.is_finished():
                status = True

            if self.hints:
                for hint in self.hints:
                    self.datum[hint.datum_index].set_guess(hint.action)
                    self.update_alg_usage(hint.algorithm)

                self.hints = []
                progress = True
        return status

    def fill_shaded_1(self):
        """
        Fill grid with datum.answer = Sgg.CIRCLED
        Then randomly set max_shaded cells to datum.answer = Sgg.SHADED
        Return list of the shaded indexes.
        """
        max_shaded = 23
        datum_indexes = list(range(Sgg.cell_count))
        random.shuffle(datum_indexes)
        for datum in self.datum:
            datum.answer = Sgg.CIRCLED
            datum.value = 1

        shaded = []

        for datum_index in datum_indexes:
            datum = self.datum[datum_index]
            bad = False
            for nbr_index in datum.neighbours_gg:
                nbr_datum = self.datum[nbr_index]
                if nbr_datum.answer == Sgg.SHADED:
                    bad = True
                    break
            if not bad:
                datum.state = Sgg.VIRGIN
                datum.answer = Sgg.SHADED
                shaded.append(datum_index)
                if len(shaded) == max_shaded:
                    break
        for datum_index in shaded:
            self.datum[datum_index].answer = Sgg.SHADED
        return shaded

    def fill_shaded(self):
        while True:
            shaded = self.fill_shaded_1()
            if self.are_connected():
                break
        return shaded

    def fill_grid_1(self, shaded):
        counter = 0
        while True:
            counter += 1
            done = False
            rows_used = [list() for row in range(Sgg.grid_height)]
            columns_used = [list() for column in range(Sgg.grid_width)]
            rows_from = [list(range(1, Sgg.grid_width + 1))
                         for row in range(Sgg.grid_height)]
            datum_index = -1
            for row in range(Sgg.grid_height):
                row_used = rows_used[row]
                row_from = rows_from[row]
                random.shuffle(row_from)
                for column in range(Sgg.grid_width):
                    column_used = columns_used[column]
                    datum_index += 1
                    datum = self.datum[datum_index]
                    datum.state = Sgg.VIRGIN
                    done = True
                    if datum.answer != Sgg.SHADED:
                        # only 1 value allowed here
                        done = False
                        for value in row_from:
                            if value not in row_used and\
                               value not in column_used:
                                datum.answer = Sgg.CIRCLED
                                datum.value = value
                                row_used.append(value)
                                column_used.append(value)
                                done = True
                                break
                            if not done:
                                pass
                    if not done:
                        break
                if not done:
                    break
            if done:
                break

        return rows_used, columns_used

    def fill_grid(self, shaded):
        rows_used, columns_used = self.fill_grid_1(shaded)

        # do shaded

        len_shaded = len(shaded)
        for datum_index in shaded:
            datum = self.datum[datum_index]
            dindex = randint(0, len_shaded)
            if rows_used[datum.row] and dindex % 2 == 0:
                value = random.choice(rows_used[datum.row])
                datum.answer = Sgg.SHADED
                datum.value = value
            elif columns_used[datum.column] and dindex % 2 != 0:
                value = random.choice(columns_used[datum.column])
                datum.answer = Sgg.SHADED
                datum.value = value

    def are_connected(self):
        """
        Check that all circled cells are connected
        """
        done = []
        doing = set([])
        target = 0
        for datum in self.datum:
            if datum.answer == Sgg.CIRCLED:
                target += 1
                datum_index = datum.index
        doing.add(datum_index)
        while True:
            try:
                datum_index = doing.pop()
                datum = self.datum[datum_index]
                for nbr in datum.neighbours_gg:
                    if nbr not in done and\
                       self.datum[nbr].answer == Sgg.CIRCLED:
                        doing.add(nbr)
                done.append(datum_index)

            except KeyError:
                break
        return len(done) == target

    def no_connection(self):
        """
        Every circled cell must be able to reach all others.
        Check all circled cells to see if they
        can reach one other circled cell.
        If one cannot we return False
        """
        ret = []
        nbr_indexes = [0, 2, 4, 6]
        for datum in self.datum:
            if datum.state == Sgg.CIRCLED:
                # this is a circled cell
                blocked = True
                for nbr in datum.neighbours_gg:
                    nbr_datum = self.datum[nbr]
                    if nbr_datum.state == Sgg.CIRCLED or\
                       nbr_datum.state == Sgg.VIRGIN:
                        # it has a circled or virgin neighbour
                        # so is or could be connected
                        blocked = False
                        break
                if blocked:
                    # found a blocked cell
                    ret = [datum.index]
                    break
        return ret

    def non_unique_number(self, indexes):
        """
        Find values which occur more than once in a row or column.
        """
        result = []
        for strip in indexes:
            strip_dic = {}
            for item in strip:
                datum = self.datum[item]
                if datum.answer != Sgg.SHADED:
                    try:
                        strip_dic[datum.value].append(item)
                    except KeyError:
                        strip_dic[datum.value] = [item]
            result.append(strip_dic)
        return result

    def duplicate_circles(self, indexes):
        ret = []
        duplicate_dics = self.non_unique_number(indexes)
        for duplicate_dic in duplicate_dics:
            for value, items in list(duplicate_dic.items()):
                ret = []
                if len(items) > 1:
                    for item in items:
                        if self.datum[item].answer == Sgg.CIRCLED:
                            ret.append(item)
                    if len(ret) > 1:
                        break
            if len(ret) > 1:
                break
            else:
                ret = []
        return ret

    def adjacent_shades(self, indexes):
        ret = []
        for strip in indexes:
            for item_index, item in enumerate(strip[:-1]):
                itemp1 = strip[item_index + 1]
                if self.datum[item].state == Sgg.SHADED and\
                   self.datum[itemp1].state == Sgg.SHADED:
                    ret = [item, itemp1]
                    break
            if ret:
                break
        return ret

    def is_grid_ok(self):
        """
        1. > 1 cells with same value circled on same strip
        Next 2 should not occur, but check anyway.
        2. shaded cells adjacent
        3. circled cell that cannot connect
        """

        ret = self.duplicate_circles(self.columns)
        if ret:
            pass
        else:
            ret = self.duplicate_circles(self.rows)
            if ret:
                pass
            else:
                ret = self.adjacent_shades(self.columns)
                if ret:
                    pass
                else:
                    ret = self.adjacent_shades(self.rows)
                    if ret:
                        pass
                    else:
                        self.no_connection()
                        if ret:
                            pass
        return ret

    def generate(self):
        seed = 3
        random.seed(seed)
        puzzles = []
        min_puzzles = 250
        min_puzzles = 1000
        min_puzzles = 20
        min_score = 105
        size = 9
        Sgg.grid_width = size
        Sgg.grid_height = size
        Sgg.cell_count = Sgg.grid_width * Sgg.grid_height
        self.datum = []
        for datum in range(Sgg.cell_count):
            self.datum.append(Datum(datum, 1))
        self.make_indexes()
        self.get_search_patterns()
        max_tries = 10
        self.settings.dic['un_active'] = 'no'
        while True:
            shaded = self.fill_shaded()
            tries = 0
            while True:
                tries += 1
                for datum_index in shaded:
                    datum = self.datum[datum_index]
                    datum.state = Sgg.VIRGIN
                    datum.answer = Sgg.SHADED
                self.fill_grid(shaded)
                success = self.solver()
                if success:
                    score = self.get_score()
                    if score >= min_score:
                        puzzle = [score, size, size]
                        for datum in self.datum:
                            if datum.state == Sgg.CIRCLED:
                                puzzle.append(datum.value)
                            elif datum.state == Sgg.SHADED:
                                puzzle.append(-datum.value)
                        puzzles.append(puzzle)
                        break
                if tries > max_tries:
                    break
            if len(puzzles) >= min_puzzles:
                puzzles = sorted(puzzles)
                filename = str(seed) + '.txt'
                with open(filename, "w") as fo:
                    rec = ''
                    for puzzle in puzzles:
                        for item in puzzle:
                            rec += str(item)
                            rec += ','
                    fo.write(rec)
                #print Sgg.alg_usage
                break

    def get_puzzle(self, puzzle_number):
        self.puzzle_number = puzzle_number
        puzzle = self.model_data.get_puzzle(puzzle_number)
        self.score = puzzle[0]
        Sgg.grid_width = puzzle[1]
        Sgg.grid_height = puzzle[2]
        Sgg.cell_count = Sgg.grid_width * Sgg.grid_height
        puzzle_data = puzzle[3:]
        self.datum = []
        for cell in range(Sgg.cell_count):
            self.datum.append(Datum(cell, puzzle_data[cell]))
        self.make_indexes()
        self.get_search_patterns()

    def check_consistency(self):
        self.status = True
        for datum in self.datum:
            if not datum.is_consistent():
                self.status = False
                break
        return self.status

    def check_errors(self):
        self.status = True
        self.errors = []
        for datum in self.datum:
            if not datum.is_consistent():
                self.status = False
                self.errors.append(datum)

    def check_finished(self):
        self.finished = True
        for datum in self.datum:
            if datum.state == Sgg.VIRGIN:
                self.finished = False
                break

    def is_consistent(self):
        consistent = False
        self.check_errors()
        if self.status:
            consistent = True
        return consistent

    def is_finished(self):
        return self.finished

    def apply_user_click(self, action, datum_index):
        """
        All user actions are stored in Datum as guess.
        If bad status we accummulate edits
        """
        ret = []
        datum = self.datum[datum_index]
        self.check_errors()
        if action is Sgg.CIRCLED:
            datum.set_guess(Sgg.CIRCLED)
            if self.status and datum.is_consistent() and\
               datum.answer == Sgg.CIRCLED:
                pass
            else:
                if self.user_edits is None:
                    self.user_edits = UserEdits()
                self.user_edits.add(datum)
            ret = [datum.index]
        elif action is Sgg.SHADED:
            datum.set_guess(Sgg.SHADED)
            if self.status and datum.is_consistent() and\
               datum.answer == Sgg.SHADED:
                pass
            else:
                if self.user_edits is None:
                    self.user_edits = UserEdits()
                self.user_edits.add(datum)
            ret = [datum.index]
        elif action is Sgg.DELETE:
            if datum.state == Sgg.SHADED or datum.state == Sgg.CIRCLED:
                datum.reset()
                if datum in self.user_edits.edits:
                    self.user_edits.delete(datum)
                ret = [datum.index]
        return ret

    def undo_user_edits(self):
        changes = []
        for datum in self.user_edits.edits:
            changes.append(datum.index)
            datum.reset()
        self.user_edits = None
        return changes

    def delete_hint(self):
        self.hints = []

    """
    def print_puzzle(self):
        datum_index = -1
        for row in range(Sgg.grid_height):
            for column in range(Sgg.grid_width):
                datum_index += 1
                datum = self.datum[datum_index]
                print abs(datum.value),
            print
        print
        datum_index = -1
        for row in range(Sgg.grid_height):
            for column in range(Sgg.grid_width):
                datum_index += 1
                datum = self.datum[datum_index]
                if datum.state == Sgg.CIRCLED:
                    print '0',
                elif datum.state == Sgg.SHADED:
                    print '*',
                else:
                    print '.',
            print
        print
        datum_index = -1
        for row in range(Sgg.grid_height):
            for column in range(Sgg.grid_width):
                datum_index += 1
                datum = self.datum[datum_index]
                if datum.answer == Sgg.CIRCLED:
                    print '0',
                elif datum.answer == Sgg.SHADED:
                    print '*',
                else:
                    print '.',
            print
        print
    """


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

        square_index = 0
        for row in range(Sgg.grid_height):
            for column in range(Sgg.grid_width):
                update = self.get_update(square_index, 'black', 'i')
                text = update[0]
                fg = update[1]
                bg = update[2]
                square = TextSquare(self, self.settings, square_index, fg,
                                    bg, text)
                square.grid(row=row, column=column)
                square.grid_propagate(0)
                self.grid_widgets.append(square)
                square_index += 1
        self.grid(row=0, column=0)

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

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

    def get_update(self, square_index, fg, bg):
        """
        if necessary get bg and fg for square_index
        if bg == 'i' initialise (and undo)
        if bg == 'e' colour for error
        return list of updates for widget
        """
        def get_bg(datum):
            if datum.state == Sgg.VIRGIN:
                bg = self.settings.dic['virgin_colour']
            elif datum.state == Sgg.CIRCLED:
                bg = self.settings.dic['circled_colour']
            elif datum.state == Sgg.SHADED:
                bg = self.settings.dic['shaded_colour']
            return bg

        datum = self.model.datum[square_index]
        if bg == 'i':
            fg = self.settings.dic['value_colour']
            bg = get_bg(datum)
        elif bg == 'e':
            bg = self.settings.dic['error_colour']
            fg = self.settings.dic['value_colour']
        elif bg == '':
            widget = self.grid_widgets[square_index]
            bg = widget.get_bg()
        if fg == '':
            widget = self.grid_widgets[square_index]
            fg = widget.get_fg()
        update = [datum.value, fg, bg]
        return update

    def apply_update(self, square_index, fg, bg):
        update = self.get_update(square_index, fg, bg)
        self.grid_widgets[square_index].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.model = None
        self.image_dic = {}
        self.make_image_dic()
        self.image_dic['start'] = [self.images.start_hitori, 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.model)

    def reset_board(self):
        for square_index in range(Sgg.cell_count):
            self.board.apply_update(square_index, 'black', 'i')

    def update_clock_box(self, algorithm):
        names = ['UN', 'ES', 'SM', 'T0', 'P0', 'P1', 'C0', 'C1',
                 'C2', 'C3', 'C4', 'C5', 'C6', 'E0', 'E1',
                 'P2', 'P3', 'P4', 'L0', 'L1']

        value = '  '
        if algorithm is not None:
            value = names[algorithm]
        self.clock.box.set_text(value)


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

    def __init__(self, root):
        self.settings = Settings()
        self.master = root
        self.flasher = None
        self.model_data = ModelData()
        self.model = None
        self.hint_status = Sgg.GIVE_HINT
        self.status = False
        self.active = False
        self.error_detected = False
        self.puzzle_number = None
        self.exposed = {}
        self.view = View(root, self.settings)
        puzzle_number = None
        """
        if len(sys.argv) > 1:
            puzzle_number = int(sys.argv[1])
        """

        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 start_game(self, puzzle_number):
        #self.test()
        #self.generate()
        self.set_puzzle_number(puzzle_number)
        self.view.clock_init()
        self.view.clock_restart()
        self.view.game_over()
        self.model = Model(self.settings, self.model_data)
        self.model.get_puzzle(self.puzzle_number)
        success = self.model.is_grid_ok()
        self.view.thumbs_up()
        self.view.model = self.model
        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.score)
        self.status = False
        self.active = True
        self.hint_status = Sgg.GIVE_HINT
        self.flasher = Flasher(self.view.clock.box.label,
                               self.master, self.settings)

    def generate(self):
        self.model = Model(self.settings, self.model_data)
        self.model.generate()
        exit(1)

    def test(self):
        max_trial = 1
        bad = []
        want = 6
        got = []
        freqs = [0] * 200
        for trial in range(max_trial):
            for puzzle_number in range(self.model_data.puzzle_count):
                if puzzle_number not in bad:
                    self.model = Model(self.settings, self.model_data)
                    self.model.get_puzzle(puzzle_number)
                    success = self.model.solver()
                    score = self.model.get_score()
                    if not success:
                        bad.append(puzzle_number)
                    else:
                        freqs[score] += 1
                        """
                        if self.model.alg_usage[want] > 0:
                            got.append(puzzle_number)
                            if len(got) > 5:
                                #print want, got
                                exit(1)
                        """
        # print bad
        exit(1)

    def add_callbacks(self):
        self.view.add_menu_callback(self.service_menu)
        self.view.add_toolbar_callback(self.service_toolbar)
        self.view.entry_box.add_callback(self.get_puzzle_number)
        self.view.board.add_callback(self.square_event)

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

        strargs = ''
        for item in args:
            strargs += ' ' + item
        PzlG.logger.logger.info("service_menu " + strargs)
        name = args[0]
        if name == 'font_size':
            self.set_font_size()
        elif name == 'set_colour':
            choice = args[1]
            self.set_colour(choice)
        elif name == 'save_settings':
            self.settings.preferences.save()
        elif name == 'about':
            self.view.about()
        elif name == 'help':
            self.view.help()

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

        if mouse_index == 2:
            alg_index = None
            if self.model.hints:
                alg_index = self.model.hints[0].algorithm
            self.view.do_tooltip(button_index, alg_index)
        elif mouse_index == 3:
            if button_index == 0:
                self.puzzle_number += 1
                self.start_game(self.puzzle_number)
            elif button_index == 1 and self.model.status:
                self.model.check_errors()
                if self.model.status and not self.model.is_finished():
                    if self.hint_status != Sgg.APPLY_HINT:
                        self.exhaust_puzzle()
        elif mouse_index == 1:
            if button_index == 0:
                self.start_game(None)
            elif button_index == 1:
                if not self.model.is_finished():
                    self.give_hint()
            elif button_index == 2:
                if not self.model.is_finished():
                    self.check_errors()
            elif button_index == 3:
                if not self.model.is_finished():
                    self.show_solution()

    def square_event(self, datum_index, mouse_index):
        """
        left mouse shade
        middle mouse delete
        right mouse circle
        """

        if self.active:
            PzlG.logger.logger.info("square_event " +
                                    str(mouse_index) +
                                    ' ' +
                                    str(datum_index))
            # need to remove any active hint
            if self.model.hints:
                self.view.hint_off()
                self.view.reset_board()
                self.model.hints = []
            self.hint_status = Sgg.GIVE_HINT

            changes = []
            if mouse_index == 1:
                # player has left clicked on this cell
                # ie wants to shade it
                changes = self.model.apply_user_click(Sgg.SHADED,
                                                      datum_index)
            elif mouse_index == 2:
                # player has middle clicked on this cell
                # ie wants to delete
                changes = self.model.apply_user_click(Sgg.DELETE,
                                                      datum_index)
            elif mouse_index == 3:
                # player has right clicked on this cell
                # ie wants to circle it
                changes = self.model.apply_user_click(Sgg.CIRCLED,
                                                      datum_index)
            if changes:
                # send list of changed cells to board
                for datum_index in changes:
                    self.view.board.apply_update(datum_index, 'black', 'i')
            self.check_finished()

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

    def apply_hint(self):
        if self.model.is_consistent():
            hint = self.model.hints[0]
            datum = self.model.datum[hint.datum_index]
            datum.set_guess(hint.action)
            self.unshow_hint()

    def unshow_hint(self):
        hint = self.model.hints[0]
        datum = self.model.datum[hint.datum_index]
        fg = self.settings.dic['value_colour']
        self.view.board.apply_update(datum.index, fg, 'i')
        causes = hint.causes
        for cause in causes:
            for datum_index in cause:
                self.view.board.apply_update(datum_index, fg, 'i')

    def show_hint(self):
        colours = [self.settings.dic['hint_match_colour'],
                   self.settings.dic['cause_shaded_colour'],
                   self.settings.dic['cause_match_colour']]

        hint = self.model.hints[0]
        causes = hint.causes
        bg = self.settings.dic['hint_bg_colour']
        self.view.board.apply_update(hint.datum_index, '', bg)
        for colour_index, cause in enumerate(causes):
            for datum_index in cause:
                colour = colours[colour_index]
                self.view.board.apply_update(datum_index, colour, '')

    def give_hint(self):
        if self.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.hints:
                self.view.hint_on()
                self.show_hint()
                self.view.update_clock_box(self.model.hints[0].algorithm)
                self.view.clock.set_show(False)

            self.hint_status = Sgg.APPLY_HINT
        else:
            if self.model.hints:
                self.apply_hint()
                self.ready_for_hint()
                self.check_finished()
            self.hint_status = Sgg.GIVE_HINT

    def check_errors(self):
        if self.error_detected:
            # may have deleted edits
            if self.model.user_edits is not None:
                # we know there's an error already, so now we correct it
                self.tidy_error()
            self.model.check_errors()
            if not self.model.status:
                PzlG.logger.logger.error("errors_check ")
                return
            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 colour_errors(self):
        """
        Colour the error squares
        """
        bg = self.settings.dic['error_colour']
        fg = self.settings.dic['value_colour']
        for datum in self.model.errors:
            self.view.board.apply_update(datum.index, fg, bg)

    def tidy_error(self):
        """
        update view and undo errors
        """
        fg = self.settings.dic['value_colour']
        changes = self.model.undo_user_edits()
        for datum_index in changes:
            self.view.board.apply_update(datum_index, fg, 'i')

        self.model.errors = []
        self.model.delete_hint()
        self.model.status = True
        self.error_detected = False
        self.view.thumbs_up()

    def show_solution(self):
        self.active = False
        fg = self.settings.dic['value_colour']
        for datum_index in range(Sgg.cell_count):
            datum = self.model.datum[datum_index]
            if datum.answer == Sgg.CIRCLED:
                bg = self.settings.dic['circled_colour']
            elif datum.answer == Sgg.SHADED:
                bg = self.settings.dic['shaded_colour']
            self.view.board.apply_update(datum_index, fg, bg)

        self.view.game_over()
        self.view.clock_stop()

    def exhaust_puzzle(self):
        self.model.algorithm = 0
        bg = self.settings.dic['circled_colour']
        fg = self.settings.dic['value_colour']
        progress = True
        while progress:
            progress = False
            self.model.hints = self.model.unique_number()
            if self.model.hints:
                for hint in self.model.hints:
                    datum = self.model.datum[hint.datum_index]
                    datum.set_guess(hint.action)
                    self.model.update_alg_usage(hint.algorithm)
                    self.view.board.apply_update(datum.index, fg, bg)

                self.model.hints = []
                progress = True
        self.model.check_finished()

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

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

    def set_colour(self, choice):
        error_in = self.settings.dic['error_colour']
        try:
            bg_colour = self.settings.dic[choice]
            result = tkColorChooser.askcolor(bg_colour)
            if result[1] is not None:
                self.settings.dic[choice] = result[1]
        except KeyError:
            PzlG.logger.logger.error("set_colour " + choice)
        self.start_game(self.puzzle_number)

        if self.settings.dic['error_colour'] != error_in:
            # error colour changed
            # if to white, switch on unique_number algorithm
            if self.settings.dic['error_colour'] == '#ffffff':
                self.settings.dic['error_colour'] = '#FF0000'
                self.settings.dic['un_active'] = 'yes'
            else:
                self.settings.dic['un_active'] = 'no'

    def get_puzzle_number(self, text):
        if text.find('XYZZY') != -1:
            self.view.xyzzy()
            self.view.entry_box.clear()
        else:
            try:
                puzzle_number = int(text)
                self.start_game(puzzle_number)
            except ValueError:
                self.view.entry_box.clear()


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