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

# Copyright (C) 2020 Rod Engerstad

import random
from random import shuffle
from random import randint
import itertools
import Tkinter as tk
import tkColorChooser

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


class Sgg(object):
    """
    Game globals
    """
    ZERO = 0
    ONE = 1
    UNSET = -1
    ROW = 0
    COLUMN = 1
    size = 10
    generate = False
    GIVE_HINT = 0
    APPLY_HINT = 1


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['font_size'] = '2'
        self.dic['set_colour'] = 'grid_colour'
        self.dic['grid_colour'] = '#DFDFDF'
        self.dic['text_colour'] = 'black'
        self.dic['hint_colour'] = '#555753'
        self.dic['hint_colour'] = 'orange'
        self.dic['error_colour'] = '#FF0000'
        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 = [_("alg_0: Because of the No Triples rule, if two adjacent items are set and equal, those either side must be different"), _("alg_1: Because of the No Triples rule, if two items separated by one space are equal, the one between must be different"), _("alg_2: If all the 1s or 0s for a strip are done we can fill in the others."), _("alg_3: If all but one of either 0 or 1 are done, test the remaining symbol in all empty positions. If only one position does not cause the No Triples rule to be broken, it must be the correct one."), _("alg_4: When few choices are remaining, some may be eliminated by applying the No Triples rule and the No duplicates rule to all their permutations, which together, may reveal a certain position must be occupied by a particulat symbol.")]

    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 cells, a few containing clues. Players complete the grid using a left mouse click for a 0 and right click for a 1. Guesses can be removed 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 2999 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). If the player can work out the hint cell's correct value they can set it in the usual way. If not a further click on the wand will set 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 incorrect cells are shaded red. A further click on the thumb will remove the red shading and remove the erroneous guesses. 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'),
            [_('Grid colour'),
             _('Text colour'),
             _('Hint colour'),
             _('Error colour')],
            'N',
            ['grid_colour',
             'text_colour',
             'hint_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 Board(tk.Frame):
    def __init__(self, parent, settings, model):
        self.settings = settings
        self.model = model
        grid_dic = model.grid_dic
        self.grid_widgets = []
        bg = self.settings.green
        tk.Frame.__init__(self, parent, relief='flat',
                          highlightbackground=self.settings.grey,
                          highlightthickness=2,
                          bd=2, bg=bg)

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

        square_index = 0
        for row in range(Sgg.size):
            for column in range(Sgg.size):
                text = grid_dic[column, row]
                if text == Sgg.UNSET:
                    text = ' '
                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 do_square_update(self, coord, text, fg, bg):
        widget_index = coord[0] + coord[1] * Sgg.size
        widget = self.grid_widgets[widget_index]
        update = [text, fg, bg]
        widget.my_update(update)
        self.update_idletasks()

    def apply_user_click(self, text, cell_index):
        coord = (cell_index % Sgg.size, cell_index // Sgg.size)
        fg = self.settings.dic['text_colour']
        bg = self.settings.dic['grid_colour']
        self.do_square_update(coord, text, fg, bg)

    def show_hint(self):
        """
        Show a hint on the grid.
        """
        if self.model.hint:
            fg = self.settings.dic['text_colour']
            bg = self.settings.dic['hint_colour']
            text = '?'
            coord = (self.model.hint[0], self.model.hint[1])
            self.do_square_update(coord, text, fg, bg)

    def apply_hint(self):
        """
        Remove hint symbol from grid and reveal answer
        """
        if self.model.hint:
            fg = self.settings.dic['text_colour']
            bg = self.settings.dic['grid_colour']
            text = self.model.hint[2]
            coord = (self.model.hint[0], self.model.hint[1])
            self.do_square_update(coord, text, fg, bg)

    def unshow_hint(self):
        """
        Remove hint symbol from grid
        """
        if self.model.hint:
            fg = self.settings.dic['text_colour']
            bg = self.settings.dic['grid_colour']
            text = ' '
            coord = (self.model.hint[0], self.model.hint[1])
            self.do_square_update(coord, text, fg, bg)

    def show_errors(self):
        fg = self.settings.dic['text_colour']
        bg = self.settings.dic['error_colour']
        for coord in self.model.errors:
            text = self.model.grid_dic[coord]
            self.do_square_update(coord, text, fg, bg)

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


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

    def __init__(self, master, settings):
        ViewB.__init__(self, master, settings)
        self.model = None
        self.image_dic = {}
        self.make_image_dic()
        self.image_dic['start'] = [self.images.start_binary, 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 update_clock_box(self, algorithm):
        value = '  '
        if algorithm is not None:
            self.clock.box.set_text(algorithm)


class Model(object):
    """
    Here we handle the data including solving and generating,
    plus keeping the users guesses up to data.
    The active copy of the grid is grid_dic.
    Rows and columns are independent and much of the processing
    is handled using individual rows and columns, termed "strips".
    """
    def __init__(self, model_data):
        self.model_data = model_data
        self.status = True
        self.grid_dic = {}
        self.answer_dic = {}
        self.clue_dic = {}
        self.difficulty = None
        self.hint = []
        self.errors = []
        self.algorithm = None
        self.algorithms = [
            (self.alg_0),
            (self.alg_1),
            (self.alg_2),
            (self.alg_3),
            (self.alg_4),
            ]
        self.alg_counts = Model.zero_alg_counts()

    @staticmethod
    def is_set(sym):
        """
        Return True is 0 or 1
        """
        return sym != Sgg.UNSET

    @staticmethod
    def get_other(sym):
        """
        get other symbol
        """
        return Sgg.ONE if sym == Sgg.ZERO else Sgg.ZERO

    """
    def print_grid(self):
        sym_dic = {0: '0', 1: '1', -1: '.'}
        for y in range(Sgg.size):
            for x in range(Sgg.size):
                print sym_dic[self.grid_dic[x, y]],
            print
    """

    @staticmethod
    def has_threesome(strip):
        """
        Return True if strip contains 3 the same and adjacent.
        """
        ret = False
        run_length = 0
        current_sym = Sgg.UNSET
        for sym in strip:
            if Model.is_set(sym):
                if current_sym == sym:
                    run_length += 1
                    if run_length == 3:
                        ret = True
                        break
                else:
                    current_sym = sym
                    run_length = 1
            else:
                current_sym = sym
                run_length = 0
        return ret

    @staticmethod
    def has_bad_count(strip):
        """
        Return True if strip has too many of either symbol
        """
        ret = False
        counts = Model.do_counts(strip)
        if counts[0] > Sgg.size // 2 or counts[1] > Sgg.size // 2:
            ret = True
        return ret

    def get_strip(self, direc, strip_index):
        """
        Return a row or column as a list of symbols
        """
        strip = []
        if direc == Sgg.ROW:
            for x in range(Sgg.size):
                strip.append(self.grid_dic[x, strip_index])
        else:
            for y in range(Sgg.size):
                strip.append(self.grid_dic[strip_index, y])
        return strip

    @staticmethod
    def do_counts(strip):
        """
        count Sgg.ZEROS and Sgg.ONES and Sgg.UNSET for a strip
        """
        indices = [Sgg.ZERO, Sgg.ONE, Sgg.UNSET]
        counts = [0, 0, 0]
        for sym in strip:
            indic = indices.index(sym)
            counts[indic] += 1
        return counts

    @staticmethod
    def alg_0_2(strip, strip_index, ranges):
        """
        If 2 adjacent items are set and equal, those either side
        must be different
        """
        hint = []
        for item in range(ranges[2]):
            inc = ranges[3]
            if strip[item] == strip[item + 1] and\
               Model.is_set(strip[item]) and\
               item + inc > -1 and item + inc < Sgg.size and\
               not Model.is_set(strip[item + inc]):
                if ranges[0] == Sgg.ROW:
                    hint = [item + inc, strip_index,
                            Model.get_other(strip[item])]
                else:
                    hint = [strip_index, item + inc,
                            Model.get_other(strip[item])]
                break
        return hint

    def alg_0_1(self, ranges):
        """
        If adjacent items are set and equal, those either side
        must be different
        """
        hint = []
        for strip_index in range(ranges[1]):
            strip = self.get_strip(ranges[0], strip_index)
            hint = self.alg_0_2(strip, strip_index, ranges)
            if hint:
                break
        return hint

    def alg_0(self):
        """
        If 2 adjacent items are set and equal, those either side
        must be different
        """
        ranges = [(0, Sgg.size, Sgg.size - 1, 2),
                  (0, Sgg.size, Sgg.size - 1, -1),
                  (1, Sgg.size, Sgg.size - 1, 2),
                  (1, Sgg.size, Sgg.size - 1, -1)]

        for range_index in range(4):
            hint = self.alg_0_1(ranges[range_index])
            if hint:
                break
        self.hint = hint

    @staticmethod
    def alg_1_2(strip, strip_index, direc):
        """
        If 2 1-apart items are set and equal, the one between must be different
        """
        hint = []
        for item in range(Sgg.size - 2):
            if strip[item] == strip[item + 2] and\
               Model.is_set(strip[item]) and not Model.is_set(strip[item + 1]):
                if direc == Sgg.ROW:
                    hint = [item + 1, strip_index, Model.get_other(strip[item])]
                else:
                    hint = [strip_index, item + 1, Model.get_other(strip[item])]
                if hint:
                    break
        return hint

    def alg_1_1(self, direc):
        """
        If 2 1-apart items are set and equal, the one between must be different
        """
        for strip_index in range(Sgg.size):
            strip = self.get_strip(direc, strip_index)
            hint = self.alg_1_2(strip, strip_index, direc)
            if hint:
                break
        return hint

    def alg_1(self):
        """
        If 2 1-apart items are set and equal, the one between must be different
        """
        for range_index in range(2):
            hint = self.alg_1_1(range_index)
            if hint:
                break
        self.hint = hint

    def alg_2_1(self, direc):
        """
        If all the ones or zeros are set for a strip we can fill in the others
        """
        hint = []
        for strip_index in range(Sgg.size):
            strip = self.get_strip(direc, strip_index)
            counts = Model.do_counts(strip)
            for sym_index, sym in enumerate([Sgg.ZERO, Sgg.ONE]):
                other_sym = Model.get_other(sym)
                if sum(counts[0:2]) != Sgg.size and\
                   counts[sym_index] == Sgg.size // 2:
                    for item_index, item in enumerate(strip):
                        if direc == Sgg.ROW:
                            if not Model.is_set(item):
                                hint = [item_index, strip_index, other_sym]
                                break
                        else:
                            if not Model.is_set(item):
                                hint = [strip_index, item_index, other_sym]
                                break
                if hint:
                    break
            if hint:
                break
        return hint

    def alg_2(self):
        """
        If all the ones or zeros are set for a strip we can fill in the others
        """
        for range_index in range(2):
            hint = self.alg_2_1(range_index)
            if hint:
                break
        self.hint = hint

    @staticmethod
    def get_impossible_position(sym, strip, holes):
        """
        sym sym occurs SIZE / 2 - 1 times in strip and the remaining
        holes are in holes.
        Try sym in each hole to see if filling in the other holes with
        the other sym creates any runs of 3.
        """
        my_other_sym = Model.get_other(sym)
        my_strip = strip[:]
        bad_pos = None
        for hole in holes:
            my_strip[hole] = sym
            for strip_index, this_sym in enumerate(my_strip):
                if not Model.is_set(this_sym):
                    my_strip[strip_index] = my_other_sym
            if Model.has_threesome(my_strip):
                bad_pos = hole
                break
            my_strip = strip[:]
        return bad_pos

    @staticmethod
    def get_holes(strip):
        """
        Get unfinished positions in this strip
        """
        holes = []
        for sym_index, sym in enumerate(strip):
            if not Model.is_set(sym):
                holes.append(sym_index)
        return holes

    def alg_3_1(self, direc):
        """
        If we have 4 ones or 4 zeros in a |10| strip we need 1 more.
        Try it in all positions to see if using that position
        would mean that we'd get 3 consecutive others elsewhere.
        """
        hint = []
        syms = [Sgg.ZERO, Sgg.ONE]
        for strip_index in range(Sgg.size):
            strip = self.get_strip(direc, strip_index)
            counts = Model.do_counts(strip)
            for sym_index, sym in enumerate(syms):
                if counts[sym_index] == Sgg.size // 2 - 1:
                    holes = Model.get_holes(strip)
                    bad_pos = Model.get_impossible_position(sym, strip, holes)
                    if bad_pos is not None:
                        my_other_sym = Model.get_other(sym)
                        if direc == Sgg.ROW:
                            hint = [bad_pos, strip_index, my_other_sym]
                            break
                        else:
                            hint = [strip_index, bad_pos, my_other_sym]
                            break
            if hint:
                break
        return hint

    def alg_3(self):
        """
        If we have SIZE / 2 - 1 ones or zeros in a strip we need 1 more.
        Try it in all positions to see if using that position
        would mean that we'd get 3 consecutive others elsewhere.
        """
        for direc in [Sgg.ROW, Sgg.COLUMN]:
            hint = self.alg_3_1(direc)
            if hint:
                break
        self.hint = hint

    def get_conserved_symbol(self, direc, strip_index, unmatched):
        """
        Is either symbol common to a single position?
        If so it must be the solution.
        """

        hint = []
        original_strip = self.get_strip(direc, strip_index)
        ans = -1
        for pos, sym in enumerate(original_strip):
            if not Model.is_set(sym):
                count_0 = 0
                count_1 = 0
                for un_match in unmatched:
                    if un_match[pos] == Sgg.ZERO:
                        count_0 += 1
                    else:
                        count_1 += 1

                if count_0 == len(unmatched):
                    ans = Sgg.ZERO
                elif count_1 == len(unmatched):
                    ans = Sgg.ONE
                if ans != -1:
                    if direc == Sgg.ROW:
                        hint = [pos, strip_index, ans]
                    else:
                        hint = [strip_index, pos, ans]
                    break
            ans = -1
        return hint

    @staticmethod
    def remove_non_unique(strips, matches):
        """
        Save any strip which does not match any of the finished strips
        """
        unmatched = []
        for strip in strips:
            ok_strip = True
            for match in matches:
                if strip == match:
                    ok_strip = False
                    break
            if ok_strip:
                unmatched.append(strip)
        return unmatched

    @staticmethod
    def remove_threes(perms):
        """
        Return perms which don't have runs of 3 the same.
        """
        good_perms = []
        for t_strip in perms:
            if not Model.has_threesome(t_strip):
                good_perms.append(t_strip)
        return good_perms

    def make_possible_strips(self, direc, strip_index):
        """
        Find the numbers of Sgg.ZEROs and Sgg.ONEs that are needed
        to finished strip_index, get all their permutations and
        make all the possible strips.
        """
        needed = []
        perms = []
        t_strip = self.get_strip(direc, strip_index)
        counts = Model.do_counts(t_strip)
        for i in range(Sgg.size // 2 - counts[Sgg.ZERO]):
            needed.append(Sgg.ZERO)
        for i in range(Sgg.size // 2 - counts[Sgg.ONE]):
            needed.append(Sgg.ONE)
        for perm in itertools.permutations(needed):
            if perm not in perms:
                perms.append(perm)

        # use perms to make the possible strips

        possible_strips = []
        for perm in perms:
            t_strip = self.get_strip(direc, strip_index)
            pi = 0
            for pos, sym in enumerate(t_strip):
                if not Model.is_set(sym):
                    t_strip[pos] = perm[pi]
                    pi += 1
            possible_strips.append(t_strip)

        return possible_strips

    def alg_4_3(self, direc, strip_index, matches):
        """
        strip_index in direction direc is unfinished,
        Find the numbers of Sgg.ZEROs and Sgg.ONEs that are needed
        to finished strip_index, and get all their permutations (alg_4_4)
        Remove any possibilities with threesomes. (remove_threes)
        Try all perms and compare to each of the matching strips.
        Save the ones that don't match any finished strips in unmatched.
        Look to see if any unfinished position in the original
        strip is always occupied by the same symbol. If so it must be
        the the answer.
        """

        possible_strips = self.make_possible_strips(direc, strip_index)
        possible_strips = Model.remove_threes(possible_strips)

        # save any strip which does not match any of the finished strips

        unmatched = Model.remove_non_unique(possible_strips, matches)

        return self.get_conserved_symbol(direc, strip_index, unmatched)

    def get_matching_finished(self, direc, done, nd_strip):
        """
        Get finished strips which match the finished items in nd_strip.
        """
        matches = []
        for d in done:
            d_strip = self.get_strip(direc, d)
            match = True
            for item in range(Sgg.size):
                if Model.is_set(nd_strip[item]) and\
                   nd_strip[item] != d_strip[item]:
                    match = False
                    break
            if match:
                matches.append(d_strip)
        return matches

    def alg_4_2(self, direc, done, not_done):
        """
        Given strip indexes of finished (done) and unfinished (not_done).
        """

        hint = []
        for nd in not_done:
            matches = []
            nd_strip = self.get_strip(direc, nd)
            matches = self.get_matching_finished(direc,
                                                 done, nd_strip)
            hint = self.alg_4_3(direc, nd, matches)
            if hint:
                break
        return hint

    def alg_4_1(self, direc):
        """
        Find finished strips (done).
        Find unfinished strips with few unknowns (not_done)
        """
        hint = []
        done = []
        not_done = []
        for strip_index in range(Sgg.size):
            strip = self.get_strip(direc, strip_index)
            counts = Model.do_counts(strip)
            if counts[0] + counts[1] == Sgg.size:
                done.append(strip_index)
            elif Sgg.size // 2 - counts[0] <= 2 or\
                 Sgg.size // 2 - counts[1] <= 2:
                not_done.append(strip_index)
        if not_done:
            hint = self.alg_4_2(direc, done, not_done)
        return hint

    def alg_4(self):
        """
        Find finished strips (done).
        Find unfinished strips with few unknowns (not_done)
        Find the numbers of Sgg.ZEROs and Sgg.ONEs that are needed
        to finished strip_index, and get all their permutations (alg_4_4)
        Remove any possibilities with threesomes. (remove_threes)
        Try all perms and compare to each of the matching strips.
        Save the ones that don't match any finished strips in unmatched.
        Look to see if any unfinished position in the original
        strip is always occupied by the same symbol.
        If so it must be the answer.
        """

        for direc in [Sgg.ROW, Sgg.COLUMN]:
            self.hint = self.alg_4_1(direc)
            if self.hint:
                break

    def apply_hint(self):
        """
        Add a hint to the grid.
        """
        if self.hint:
            self.grid_dic[self.hint[0], self.hint[1]] = self.hint[2]
            self.alg_counts[self.algorithm] += 1

    def is_finished(self):
        """
        grid is finished if all rows have Sgg.size 0 and 1
        That there are the correct number of 0 and 1 is already checked.
        """
        finished = True
        for pos in range(Sgg.size):
            strip = self.get_strip(Sgg.ROW, pos)
            counts = self.do_counts(strip)
            if counts[0] != Sgg.size // 2 or counts[1] != Sgg.size // 2:
                finished = False
                break
        return finished

    @staticmethod
    def zero_alg_counts():
        return [0, 0, 0, 0, 0]

    def check_hint(self):
        """
        Does hint match answer?
        """
        ret = True
        coord = (self.hint[0], self.hint[1])
        if self.answer_dic[coord] != self.hint[2]:
            ret = False
        return ret

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

    def do_hint(self):
        """
        If not finished try all algorithms. If one succeeds, return.
        """
        self.hint = []
        if not self.is_finished():
            self.apply_algorithms()
            if self.hint:
                if not self.check_hint():

                    # error detected

                    self.hint = []
                    self.status = False

    def solver_1(self):
        """
        Try all algorithms. If one succeeds, apply it.
        """
        self.do_hint()
        if self.hint:
            self.apply_hint()

    def solver(self):
        """
        Keep trying and applying all algorithms until finished or failed.
        """
        self.status = True
        self.alg_counts = Model.zero_alg_counts()
        while True:
            self.solver_1()
            if not self.hint:
                break

    def get_puzzle(self, puzzle_index):
        """
        Get puzzle puzzle_index from model_data and set grid_size
        """
        size, grid, answers, diff = self.model_data.get_puzzle(puzzle_index)
        Sgg.size = size
        self.grid_dic = grid
        self.clue_dic = dict(grid)
        self.answer_dic = answers
        self.difficulty = diff

    @staticmethod
    def get_gen_strip(strips, direc, strip_index):
        """
        strips is a list of row strips:
        [[row_0],[row_1],,,]
        """

        strip = []
        if strips:
            if direc == Sgg.ROW:
                row = strips[strip_index]
                strip = row[:]
            else:
                for row in strips:
                    strip.append(row[strip_index])
        return strip

    @staticmethod
    def is_unique(strips, strip):
        """
        is strip different to all strips in strips?
        """
        ret = True
        for astrip in strips:
            if strip == astrip:
                ret = False
                break
        return ret

    @staticmethod
    def are_unique_1(t_strips):
        unique = True
        for strip_index, strip in enumerate(t_strips):
            for strip_index_1 in range(strip_index + 1, Sgg.size):
                if strip == t_strips[strip_index_1]:
                    unique = False
                    break
            if not unique:
                break

        return unique

    @staticmethod
    def sget_strips(strips, direc):
        t_strips = []
        for strip_index in range(Sgg.size):
            strip = Model.get_gen_strip(strips, direc, strip_index)
            t_strips.append(strip)
        return t_strips

    @staticmethod
    def are_unique(strips):
        """
        Check that all rows are different from all other rows
        and that all cols are different from all other cols.
        """
        t_strips = Model.sget_strips(strips, Sgg.COLUMN)
        unique = Model.are_unique_1(t_strips)

        return unique

    @staticmethod
    def look_back(strip):
        """
        Check last 2 symbols and totals to see which can be added.
        """
        zero_ok = False
        one_ok = False
        ok_syms = []
        ok_totals = [Sgg.UNSET, Sgg.UNSET]
        if len(strip) > 1:
            if strip[-2] != strip[-1]:
                ok_syms = [Sgg.ZERO, Sgg.ONE]
            else:
                if strip[-1] == Sgg.ZERO:
                    ok_syms = [Sgg.UNSET, Sgg.ONE]
                else:
                    ok_syms = [Sgg.ZERO, Sgg.UNSET]
        else:
            ok_syms = [Sgg.ZERO, Sgg.ONE]

        counts = Model.do_counts(strip)
        if counts[0] < Sgg.size // 2:
            ok_totals[0] = Sgg.ZERO
        if counts[1] < Sgg.size // 2:
            ok_totals[1] = Sgg.ONE
        if ok_syms[0] == Sgg.ZERO and ok_totals[0] == Sgg.ZERO:
            zero_ok = True
        if ok_syms[1] == Sgg.ONE and ok_totals[1] == Sgg.ONE:
            one_ok = True
        return zero_ok, one_ok

    @staticmethod
    def next_sym(strip_left, strip_up):
        """
        Try to get the next symbol (0 or 1)
        """

        syms = [Sgg.ZERO, Sgg.ONE]
        sym = Sgg.UNSET

        left_zero_ok, left_one_ok = Model.look_back(strip_left)
        up_zero_ok, up_one_ok = Model.look_back(strip_up)

        # if both ZERO and ONE OK, choose randomly

        if left_zero_ok and left_one_ok and up_zero_ok and up_one_ok:
            sym = syms[random.randint(0, 1)]

        # if only ZERO OK, add a ZERO

        elif left_zero_ok and up_zero_ok:
            sym = Sgg.ZERO

        # if only ONE OK, add a ONE

        elif left_one_ok and up_one_ok:
            sym = Sgg.ONE
        if len(strip_left) > 2:
            if strip_left[-1] == strip_left[-2] == strip_left[-3]:
                exit(0)
        return sym

    @staticmethod
    def make_row(strips):
        """
        Try to make a row
        """
        strip_left = []
        strip_up = []
        col_index = 0
        row_index = len(strips) - 1
        while len(strip_left) < Sgg.size:

            if row_index > -1:
                strip_up = Model.get_gen_strip(strips, Sgg.COLUMN, col_index)
            sym = Model.next_sym(strip_left, strip_up)
            if sym == Sgg.UNSET:
                strip_left = []
                break
            else:
                strip_left.append(sym)
                col_index += 1

        return strip_left

    @staticmethod
    def generate_1():
        """
        Try to get a conforming grid
        """

        tries = 0
        count = 0
        strips = []
        while len(strips) < Sgg.size:
            strip = Model.make_row(strips)
            if strip and Model.is_unique(strips, strip):
                strips.append(strip)
                tries = 0
                count += 1
            else:
                tries += 1

            if tries > Sgg.size:
                strips = []
                count = 0
                break
        if count == Sgg.size:
            if not Model.are_unique(strips):
                strips = []
        return strips

    def generate_3(self, strips):
        """
        Have full grid as strips.
        Gradually remove values until we cannot solve it.
        """

        clue_grid_dic = {}
        answer_grid_dic = {}
        trios = []
        for y, strip in enumerate(strips):
            for x, item in enumerate(strip):
                trios.append([x, y, item])
                answer_grid_dic[x, y] = item

        shuffle(trios)

        # initial clues are all the answers

        clue_grid_dic = dict(answer_grid_dic)
        self.answer_dic = dict(clue_grid_dic)
        popped = []
        while True:
            pop = trios.pop()
            popped.append(pop)

            # remove this popped clue

            clue_grid_dic[pop[0], pop[1]] = Sgg.UNSET

            # copy to grid_dic and try to solve

            self.grid_dic = dict(clue_grid_dic)
            self.solver()
            finished = self.is_finished()
            if not finished:

                # failed to solve so replace last clue

                popped.pop()
                clue_grid_dic[pop[0], pop[1]] = pop[2]

            if not trios:
                # tried all positions
                break

        return clue_grid_dic

    def generate_2(self, min_puzzles):
        alg_scores = [1, 1, 1, 3, 6]
        puzzles = []
        total = 0
        while len(puzzles) < min_puzzles:
            strips = self.generate_1()

            if strips:
                clue_grid_dic = self.generate_3(strips)
                self.grid_dic = dict(clue_grid_dic)
                self.solver()
                if self.is_finished():
                    score = 0
                    for count_index, count in enumerate(self.alg_counts):
                        score += count * alg_scores[count_index]

                    puzzle = []
                    for row in range(Sgg.size):
                        for column in range(Sgg.size):
                            clue = clue_grid_dic[row, column]
                            answer = self.grid_dic[row, column]
                            if clue != Sgg.UNSET:
                                puzzle.append(answer + 2)
                            else:
                                puzzle.append(answer)

                    puzzles.append([score, Sgg.size, puzzle])
                    total += 1
                    #if total % 100 == 0:
                    #    print total
        return puzzles

    def generate(self):
        """
        Generate puzzles by first constructing conforming grids
        and then repeatedly solving them whilst repeatedly removing
        clues, until all positions have been tried. Sort the final
        puzzles into score order.
        """
        seed = 1
        random.seed(seed)
        min_puzzles = 1000

        puzzles = []
        sizes = [8, 10, 12]
        for size in sizes:
            Sgg.size = size
            puzzles += self.generate_2(min_puzzles)

        puzzles = sorted(puzzles)
        output = []
        for puzzle in puzzles:
            output.append(puzzle[0])
            output.append(puzzle[1])
            for item in puzzle[2:][0]:
                output.append(item)
        #print output

    def check_errors(self):
        """
        When we know the answers, check grid_dic
        """
        self.errors = []
        for coord, item in list(self.grid_dic.items()):
            if Model.is_set(item) and self.answer_dic[coord] != item:
                self.errors.append(coord)

    def errors_exist(self):
        return self.errors

    def undo_errors(self):
        for coord in self.errors:
            self.grid_dic[coord] = Sgg.UNSET
        self.errors = []

    def apply_user_click(self, mouse_index, coord):
        """
        User has clicked at coord coord
        """
        if mouse_index == 1:
            self.grid_dic[coord] = Sgg.ZERO
        elif mouse_index == 3:
            self.grid_dic[coord] = Sgg.ONE
        elif mouse_index == 2:
            self.grid_dic[coord] = Sgg.UNSET


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

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

    def start_game(self, puzzle_number):
        """
        Prepare for a game or generate puzzles
        """
        self.model = Model(self.model_data)
        if Sgg.generate:
            self.model.generate()
            exit(1)
        # self.test()
        self.set_puzzle_number(puzzle_number)
        self.view.clock_init()
        self.view.clock_restart()
        self.view.game_over()
        self.model.get_puzzle(self.puzzle_number)
        PzlG.logger.logger.info("start_game with puzzle number " +\
                                str(self.puzzle_number))
        self.ready_for_hint()
        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.difficulty)
        self.flasher = Flasher(self.view.clock.box.label,
                               self.master, self.settings)
        self.active = True
        self.hint_status = Sgg.GIVE_HINT

    def add_callbacks(self):
        """
        Set up callbacks
        """
        self.view.add_menu_callback(self.service_menu)
        self.view.add_toolbar_callback(self.service_toolbar)
        self.view.entry_box.add_callback(self.get_puzzle_number)
        self.view.board.add_callback(self.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):
        """
        Respond to Toolbar choices
        """
        PzlG.logger.logger.info("service_toolbar " +
                                str(button_index) +
                                ' ' +
                                str(mouse_index))

        if mouse_index == 2:
            alg_index = None
            if self.model.hint:
                alg_index = self.model.hint[0]
                alg_index = self.model.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 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, cell_index, mouse_index):
        """
        left mouse add zero
        middle mouse delete
        right mouse add one
        """

        if self.active:
            coord = (cell_index % Sgg.size, cell_index // Sgg.size)
            PzlG.logger.logger.info("square_event " +
                                    str(mouse_index) +
                                    ' ' +
                                    str(coord))
            # need to remove any active hint

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

            if mouse_index == 1:
                # player has left clicked on this cell
                # ie wants to add a zero
                self.model.apply_user_click(mouse_index, coord)
                text = Sgg.ZERO
                self.view.board.apply_user_click(text, cell_index)
            elif mouse_index == 2:
                # player has middle clicked on this cell
                # ie wants to delete
                # check it is not a clue
                if self.model.clue_dic[coord] == Sgg.UNSET:
                    self.model.apply_user_click(mouse_index, coord)
                    text = ' '
                    self.view.board.apply_user_click(text, cell_index)
            elif mouse_index == 3:
                # player has right clicked on this cell
                # ie wants to add a one
                self.model.apply_user_click(mouse_index, coord)
                text = Sgg.ONE
                self.view.board.apply_user_click(text, cell_index)
            self.check_finished()
            self.model.check_errors()

    """
    def test(self):
        # Try so solve all puzzles in ModelData

        print 'test'
        all_alg_counts = Model.zero_alg_counts()
        self.model.alg_counts = Model.zero_alg_counts()
        not_solved = []
        for puzzle_index in range(self.model_data.puzzle_count):
            self.model.get_puzzle(puzzle_index)
            self.model.solver()
            finished = self.model.is_finished()

            if not finished:
                print 'unsolved'
                self.model.print_grid()
                not_solved.append(puzzle_index)
            else:
                for alg_index, count in enumerate(self.model.alg_counts):
                    all_alg_counts[alg_index] += count
        print 'unsolved'
        print len(not_solved), not_solved
        print 'alg_counts', all_alg_counts
        exit(1)
    """

    def ready_for_hint(self):
        """
        If the puzzle not finished, check for errors
        and if found show them.
        Otherwise set hint mode the GIVE_HINT and set the display.
        """
        if not self.model.is_finished():
            self.model.check_errors()
            if self.model.errors:
                self.colour_errors()
                self.error_detected = True
            else:
                self.hint_status = Sgg.GIVE_HINT
                self.view.hint_off()
                self.view.thumbs_up()
                self.error_detected = False
            self.view.clock.set_show(True)
            self.model.hint = []

    def give_hint(self):
        """
        If in GIVE_HINT mode check for errors.
          If found show them to user
          Else find a hint, display it and change hint mode
        Else apply the hint, prepare for next and check if finished.
        """
        if self.hint_status == Sgg.GIVE_HINT:
            self.model.check_errors()
            if self.model.errors_exist():
                self.view.thumbs_down()
                self.colour_errors()
                self.error_detected = True
            else:
                self.error_detected = False
                self.model.do_hint()
                if self.model.hint:
                    self.view.hint_on()
                    self.view.board.show_hint()
                    self.view.update_clock_box(self.model.algorithm)
                    self.view.clock.set_show(False)
                    self.hint_status = Sgg.APPLY_HINT
        else:
            PzlG.logger.logger.info("apply hint (" +
                                    str(self.model.hint[0]) + ',' +
                                    str(self.model.hint[1]) + ')' + ' ' +
                                    str(self.model.hint[2]))

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

    def check_errors(self):
        """
        If an error already known, sort it out.
        Otherwise check for errors.
        """
        if self.error_detected:
            # we know there's an error already, so now we correct it
            self.tidy_error()
            self.ready_for_hint()
        else:
            self.model.check_errors()
            if self.model.errors_exist():
                self.view.thumbs_down()
                self.error_detected = True
                self.colour_errors()

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

    def show_solution(self):
        fg = self.settings.dic['text_colour']
        bg = self.settings.dic['grid_colour']
        for coord, text in list(self.model.answer_dic.items()):
            self.view.board.do_square_update(coord, text, fg, bg)

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

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

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

        # do the display first
        self.view.board.unshow_errors()
        self.model.undo_errors()
        self.error_detected = False
        self.view.thumbs_up()

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

    def set_colour(self, choice):
        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)

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

    def set_puzzle_number(self, puzzle_number):
        """
        Select or set random puzzle number.
        """
        if puzzle_number is None or puzzle_number < 0 or \
                puzzle_number > self.model_data.puzzle_count - 1:
            puzzle_number = randint(0, self.model_data.puzzle_count - 1)

        self.puzzle_number = puzzle_number


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