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

# Copyright (C) 2018 Rod Engerstad


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

from pzl_lib import PzlG
from pzl_lib import SettingsB
from pzl_lib import Preferences
from pzl_lib import MenuItem
from pzl_lib import Flasher
from pzl_lib import ScreenShot
from pzl_lib import ViewB
from pzl_lib import Egg
from pzl_lib import EditPair
from sudoku_model import Sgg
from sudoku_model import SudokuModel
from arrowsudoku_data import ModelData


class Settings(SettingsB):

    def __init__(self):
        SettingsB.__init__(self)
        self.dic = {}
        self.dic['even_box_colour'] = '#ffffff'
        self.dic['odd_box_colour'] = '#dfdfdf'
        self.dic['arrow_colour'] = 'orange'
        self.dic['text_colour'] = '#000000'
        self.dic['cause_colour'] = '#00d341'
        self.dic['effect_colour'] = '#ff0000'
        self.dic['button_colour'] = '#82c026'
        self.dic['error_colour'] = '#ff0000'
        self.dic['hint_mode'] = 'simplest_hint'
        self.dic['save_puzzle'] = 'without_candidates'
        self.dic['set_colour'] = 'even_box_colour'
        self.dic['font_size'] = '2'
        self.original_square_size = 54
        self.square_size = self.original_square_size
        self.menu_items = []

        self.preferences = Preferences(self.dic)
        self.preferences.load()
        if self.dic['hint_mode'] == 'simplest_hint':
            Sgg.simplest_hint = True
        else:
            Sgg.simplest_hint = False
        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 = [_("The exclusion rule: only one of each digit can occur in any row, column or box, so once an answer is set all other occurrences of that digit in its row, column and box can be deleted."), _("Arrow 1: the candidates on the path are too large to contribute to the maximum possible answer"), _("Arrow 2: each candidate is too small to be the answer, given the minimum path candidates"), _("Arrow 3: the candidates are incompatible with the answer or the possible answers"), _("Hidden single: all rows, columns and boxes must contain exactly one of each of the digits; if a cell contains the only remaining copy of a digit in a row, column or box, that digit must be the answer for that cell, and any other digits in that cell can be deleted."), _("Locked candidate 1: all rows and columns must contain exactly one of each of the digits; if cells in one row (or column) of one box contain the only copies of digits in that box, those digits must provide the answers for that box, and any occurrences of those digits in the row or column outside the box can be deleted."), _("Locked candidate 2: all rows and columns must contain exactly one of each of the digits; if cells in one row (or column) of one box contain the only copies of digits in that row or column, those digits must provide the answers for that row or column, and any occurrences of those digits in the box outside the row or column can be deleted."), _("Naked pairs: all rows, columns and boxes must contain exactly one of each of the digits; if a pair of cells contain the only copies of two digits in a row, column or box, those digits must be the answers for the two cells, and any other occurrences of those digits in the corresponding row, column or box can be deleted."), _("Naked triples: all rows, columns and boxes must contain exactly one of each of the digits; if three cells contain the only copies of three digits in a row, column or box, those digits must be the answers for the three cells, and any other occurrences of those digits in the corresponding row, column or box can be deleted."), _("Hidden pairs: all rows, columns and boxes must contain exactly one of each of the digits; if a pair of cells contain the only copies of two digits in a row, column or box, those digits must be the answers for the two cells, and any other digits in those cells can be deleted."), _("Hidden triples: all rows, columns and boxes must contain exactly one of each of the digits; if three cells contain the only copies of three digits in a row, column or box, those digits must be the answers for the three cells, and any other digits in those cells can be deleted."), _("Naked quads: all rows, columns and boxes must contain exactly one of each of the digits; if four cells contain the only copies of four digits in a row, column or box, those digits must be the answers for the four cells, and any other occurrences of those digits in the corresponding row, column or box can be deleted."), _("X wing: all rows and columns must contain exactly one of each of the digits; if cells in two rows (or columns) contain the only copies of one digit in those two rows (or columns) and those four cells form a rectangle, then that digit is the answer for two of those cells, one in each of the intersecting columns (or rows), and other occurrences of that digit in the two columns (or rows) can be deleted."), _("Known answer!")]

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

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

    def load_menus(self):
        self.menu_items.append(MenuItem(
            _('Hint Mode'),
            [_('Simplest'), _('Most Effective')],
            '',
            ['simplest_hint', 'most_effective_hint'],
            'hint_mode'
        ))
        self.menu_items.append(MenuItem(
            _('Font Size'),
            ['1',
             '2',
             '3',
             '4',
             '5',
             '6',
             '7',
             '8',
             '9',
             '10',
             '11',
             '12'],
            '',
            ['1',
             '2',
             '3',
             '4',
             '5',
             '6',
             '7',
             '8',
             '9',
             '10',
             '11',
             '12'],
            'font_size'
            ))
        self.menu_items.append(MenuItem(
            _('Set Colour'),
            [_('Even boxes'),
             _('Odd boxes'),
             _('Button'),
             _('Cause'),
             _('Effect'),
             _('Error'),
             _('Text'),
             _('Arrow')],
            'N',
            ['even_box_colour',
             'odd_box_colour',
             'button_colour',
             'cause_colour',
             'effect_colour',
             'error_colour',
             'text_colour',
             'arrow_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 ASquare(tk.Canvas):
    """
    A square for arrow sudoku
    """
    def __init__(self, parent, settings, datum, bg):
        self.parent = parent
        self.datum = datum
        self.settings = settings
        self.bg = bg
        self.callback = None
        self.active = False
        self.arrow_colour = self.settings.dic['arrow_colour']
        self.activefill = self.settings.green
        self.guess_id = None
        self.char_size = abs(self.settings.candidate_font.cget("size"))
        self.width = self.char_size * 5
        self.height = self.char_size * 5
        self.centre = [self.width / 2, self.height / 2]
        frac = 0.8
        tmp = frac * abs(self.settings.value_font.cget("size")) / 2.0
        self.radius = tmp
        self.line_width = 2
        self.candidates = []
        tk.Canvas.__init__(self, self.parent, width=self.width,
                           height=self.height, bg=self.bg, bd=0,
                           highlightthickness=1,
                           highlightcolor='black',
                           highlightbackground='black')
        self.grid()
        self.draw_arrows()
        if self.datum.guess is None:
            self.draw_candidates()
        self.draw_guess(self.datum.guess)

    def draw_arrows(self):

        if self.datum.summands:
            self.draw_circle()
            self.draw_arrow(self.datum.lines[0], 'circle')
        elif self.datum.lines:
            for line in self.datum.lines:
                self.draw_arrow(line, ' ')

    def draw_solution(self):
        for candidate in range(9):
            if self.candidates:
                self.candidates[candidate].destroy()

        self.update_idletasks()
        self.draw_arrows()
        self.draw_guess(self.datum.guess)

    def draw_arrow(self, direction, arrow_type):
        if arrow_type == 'circle':
            factor = 0.7071
            factor_1 = self.radius
            factor_2 = factor_1 * factor
        else:
            factor_1 = 0
            factor_2 = 0
        if direction == [0, -1]:  # u
            x0 = self.centre[0]
            y0 = self.centre[1] - factor_1
            x1 = x0
            y1 = 0
        elif direction == [1, 0]:  # r
            x0 = self.centre[0] + factor_1
            y0 = self.centre[1]
            x1 = self.width
            y1 = y0
        elif direction == [0, 1]:  # d
            x0 = self.centre[0]
            y0 = self.centre[1] + factor_1
            x1 = x0
            y1 = self.height
        elif direction == [-1, 0]:  # l
            x0 = self.centre[0] - factor_1
            y0 = self.centre[1]
            x1 = 0
            y1 = y0
        elif direction == [1, 1]:  # dr
            x0 = self.centre[0] + factor_2
            y0 = self.centre[1] + factor_2
            x1 = self.width
            y1 = self.height
        elif direction == [1, -1]:  # ur
            x0 = self.centre[0] + factor_2
            y0 = self.centre[1] - factor_2
            x1 = self.width
            y1 = 0
        elif direction == [-1, -1]:  # ul
            x0 = self.centre[0] - factor_2
            y0 = self.centre[1] - factor_2
            x1 = 0
            y1 = 0
        elif direction == [-1, 1]:  # dl
            x0 = self.centre[0] - factor_2
            y0 = self.centre[1] + factor_2
            x1 = 0
            y1 = self.height

        self.create_line(x0, y0, x1, y1, fill=self.arrow_colour,
                         width=self.line_width, tags='lines')

    def draw_circle(self):
        # circle
        factor = self.radius
        x0 = self.centre[0] - factor
        x1 = self.centre[0] + factor
        y0 = self.centre[1] - factor
        y1 = self.centre[1] + factor
        self.create_oval(x0, y0, x1, y1, outline=self.arrow_colour,
                         width=self.line_width)

    def draw_candidates(self):
        font = self.settings.candidate_font
        x = self.char_size / 2
        y = 2 + self.char_size / 2
        fill = self.bg
        width = 0
        padx = 1
        padx = 0
        labels = []
        for i in range(5):
            text = ' '
            if i in self.datum.candidates:
                text = i + 1
            window = self.create_window(x, y)
            label = tk.Label(self,
                             bd=0,
                             padx=padx,
                             pady=0,
                             highlightthickness=0,
                             fg='black',
                             bg=fill,
                             font=font,
                             width=width,
                             text=text)
            self.itemconfigure(window, window=label)
            x += self.char_size
            labels.append(label)

        x = self.char_size / 2
        y = self.height - self.char_size / 2
        for i in range(5, 9):
            text = ' '
            if i in self.datum.candidates:
                text = i + 1
            window = self.create_window(x, y)
            label = tk.Label(self,
                             bd=0,
                             padx=padx,
                             pady=0,
                             highlightthickness=0,
                             fg='black',
                             bg=fill,
                             font=font,
                             width=width,
                             text=text)
            self.itemconfigure(window, window=label)
            x += self.char_size
            labels.append(label)

        self.candidates = labels
        self.add_bindings()

    def add_bindings(self):
        for candidate_widget in self.candidates:
            candidate_widget.bind("<ButtonRelease-1>", self.command)
            candidate_widget.bind("<ButtonRelease-2>", self.command)
            candidate_widget.bind("<ButtonRelease-3>", self.command)
            candidate_widget.bind("<Enter>", self.command)
            candidate_widget.bind("<Leave>", self.command)

    def command(self, event):
        if event.widget.cget('state') == 'normal':
            if event.type == "7":
                self.active = True
                event.widget.configure(bg=self.activefill)
            elif event.type == "8":
                self.active = False
                event.widget.configure(bg=self.bg)
            elif event.type == "5" and self.active:
                mouse_index = event.num
                self.callback(self.datum.index,
                              self.candidates.index(event.widget),
                              mouse_index)

    def draw_guess(self, guess):
        font = self.settings.value_font
        x = self.centre[0]
        y = self.centre[1]
        text = ' '
        if guess is not None:
            text = guess + 1
        cid = self.create_text(x, y, text=text, font=font,
                               activefill=self.activefill, tags='values')
        self.guess_id = cid

    def my_update(self, update):
        """
        jobs:
        1. set guess
        2. hide candidates
        3. colour candidates (effect)
        4. reveal candidates
        5. colour candidates (cause)
        6. uncolour candidates
        7. highlight error
        8. unhighlight error
        18. apply shading
        DELETE_CANDIDATES = 0
        ADD_CANDIDATES = 1
        SET_GUESS = 2
        COLOUR_CANDIDATES_CAUSE = 3
        COLOUR_CANDIDATES_EFFECT = 4
        UNCOLOUR_CANDIDATES = 5
        HIGHLIGHT_ERROR = 6
        UNHIGHLIGHT_ERROR = 7
        APPLY_SHADING = 8
        For Kakuro, when making some changes we must also
        update the overall cell colour.
        """

        operation = update.operation
        colour = update.colour
        candidates = update.candidates
        guess = update.guess

        if operation == Egg.SET_GUESS:
            text = guess
            if guess != ' ':
                text = int(guess) + 1
            self.itemconfig(self.guess_id, text=text, fill='black')
        elif operation == Egg.DELETE_CANDIDATES:
            if self.candidates:
                for candidate in candidates:
                    self.candidates[candidate].configure(text=' ', bg=colour)
        elif operation == Egg.COLOUR_CANDIDATES_CAUSE:
            if self.candidates:
                for candidate in candidates:
                    self.candidates[candidate].configure(bg=colour)
        elif operation == Egg.COLOUR_CANDIDATES_EFFECT:
            for candidate in candidates:
                self.candidates[candidate].configure(bg=colour)
        elif operation == Egg.ADD_CANDIDATES:
            for candidate in candidates:
                text = candidate+1
                self.candidates[candidate].configure(text=text)
            # make sure guess is not set
            self.itemconfig(self.guess_id, text=' ')
        elif operation == Egg.UNCOLOUR_CANDIDATES:
            if self.candidates:
                for candidate in candidates:
                    self.candidates[candidate].configure(bg=self.bg)
        elif operation == Egg.HIGHLIGHT_ERROR:
            self.itemconfig(self.guess_id, fill=colour, text='X')
        elif operation == Egg.UNHIGHLIGHT_ERROR:
            self.itemconfig(self.guess_id, fill=colour)
        elif operation == Egg.APPLY_SHADING:
            pass
        else:
            PzlG.logger.logger.error("update_square error")


class Board(tk.Frame):
    def __init__(self, parent, settings, datum):
        self.settings = settings
        self.datum = datum
        self.grid_widgets = [None] * 81
        bg = self.settings.black
        tk.Frame.__init__(self, parent, relief='flat',
                          highlightbackground=self.settings.grey,
                          highlightthickness=2,
                          bd=2, bg=bg)
        square_index = 0
        grid_size = 9
        for row in range(grid_size):
            for column in range(grid_size):
                bg_colour = self.get_square_colour(square_index)
                widget = ASquare(self, self.settings,
                                 self.datum[square_index],
                                 bg_colour)
                self.grid_widgets[square_index] = widget
                square_index += 1
                widget.grid(row=row, column=column)
        self.grid(row=0, column=0)

    def my_destroy(self):
        self.destroy()

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

    def show_solution(self):
        for widget in self.grid_widgets:
            widget.draw_solution()

    def get_square_colour(self, square_index):
        coloureds = [3, 4, 5]
        column = square_index % 9
        row = int(square_index / 9)
        bg_colour = self.settings.dic['odd_box_colour']
        if column in coloureds and row not in coloureds:
            bg_colour = self.settings.dic['even_box_colour']
        elif row in coloureds and column not in coloureds:
            bg_colour = self.settings.dic['even_box_colour']
        return bg_colour

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


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

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

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

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

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

    def update_clock_box(self, algorithm):
        names = ['SF', 'A1', 'A2', 'A3', 'H1', 'L1', 'L2', 'N2', 'N3',
                 'H2', 'H3', 'N4', 'XW', '**']
        value = '  '
        if algorithm is not None:
            value = names[algorithm]
        self.clock.box.set_text(value)


class Datum(object):
    """
    Data item for a square
    """
    def __init__(self, my_index):
        self.candidates = set(range(9))
        self.given = False
        self.symbol_index = None
        self.guess = None
        self.index = my_index
        self.neighbours = []
        self.summands = []
        self.lines = []
        self.set_neighbours()

    def add_symbol_index(self, symbol_index):
        self.symbol_index = symbol_index

    def reset(self):
        self.guess = None
        self.candidates = set(range(9))
        self.given = False

    def load(self, candidates):
        """
        Given a set of candidates, initialise a datum.
        """

        if len(candidates) == 1:
            # must be a given answer or completely unknown
            symbol_index = candidates[0] - 1
            if symbol_index != -1:
                self.initialise(symbol_index, True)
            else:
                self.candidates = set(range(9))

            # if -1, it is completely unknown so we let the
            # original initialisation stand, and do nothing here
        else:
            my_candidates = [a-1 for a in candidates]
            self.candidates = set(my_candidates)

    def initialise(self, symbol_index, given):
        self.symbol_index = symbol_index
        if given:
            self.given = True
            self.candidates = set()
            self.guess = symbol_index

    def set_guess(self, symbol_index, given):
        self.guess = symbol_index
        if given:
            self.given = True
        self.candidates = set()

    def unset_guess(self):
        self.guess = None

    def get_guess(self):
        return self.guess

    def guess_is_set(self):
        ret = True
        if self.guess is None:
            ret = False
        return ret

    def get_answer(self):
        answer = self.guess
        if answer is not None:
            answer += 1
        else:
            answer = 0
        return answer

    def get_candidates(self):
        return self.candidates

    def del_candidate(self, candidate):
        """
        return guess if deleting last candidate
        """
        ret = -1
        if not self.given:
            if candidate in self.candidates:
                self.candidates.remove(candidate)
            if self.guess is None:
                if len(self.candidates) == 1:
                    candidate = self.candidates.pop()
                    self.guess = candidate
                    ret = candidate
        return ret

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

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

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

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

            if self.symbol_index is not None and \
                    self.symbol_index != self.guess:
                # guess != symbol_index
                error += 4
        return error

    def candidate_count(self):
        return len(self.candidates)

    def set_neighbours(self):
        """
        Set the neighbours
           7 0 1
           6 . 2
           5 4 3
        """
        self.neighbours = []
        grid_width = 9
        cell_count = 81
        # up 0
        nbr_index = self.index - grid_width
        if nbr_index > -1:
            self.neighbours.append(nbr_index)
        else:
            self.neighbours.append(None)

            # up right 1
        nbr_index = self.index - grid_width + 1
        if nbr_index > -1 and (self.index + 1) % grid_width != 0:
            self.neighbours.append(nbr_index)
        else:
            self.neighbours.append(None)

        # right 2
        nbr_index = self.index + 1
        if (self.index + 1) % grid_width != 0:
            self.neighbours.append(nbr_index)
        else:
            self.neighbours.append(None)

        # down right 3
        nbr_index = self.index + grid_width + 1
        if nbr_index < cell_count and\
           (self.index + 1) % grid_width != 0:
            self.neighbours.append(nbr_index)
        else:
            self.neighbours.append(None)

        # down 4
        nbr_index = self.index + grid_width
        if nbr_index < cell_count:
            self.neighbours.append(nbr_index)
        else:
            self.neighbours.append(None)

        # down left 5
        nbr_index = self.index + grid_width - 1
        if nbr_index < cell_count and self.index % grid_width != 0:
            self.neighbours.append(nbr_index)
        else:
            self.neighbours.append(None)

        # left 6
        nbr_index = self.index - 1
        if self.index % grid_width != 0:
            self.neighbours.append(nbr_index)
        else:
            self.neighbours.append(None)

        # up left 7
        nbr_index = self.index - grid_width - 1
        if nbr_index > -1 and self.index % grid_width != 0:
            self.neighbours.append(nbr_index)
        else:
            self.neighbours.append(None)


class Arrows(object):
    """
    All the arrows for a circled cell
    """
    def __init__(self, datum, editor, circle_datum, ninemer_lists,
                 algorithm):
        self.datum = datum
        self.editor = editor
        self.circle_datum = circle_datum
        self.ninemer_lists = ninemer_lists
        self.arrow_list = []
        self.targets = []
        self.cands_list = []
        self.used_candidates_dics = []
        self.max_cand = None
        self.algorithm = algorithm
        if circle_datum.guess is not None:
            self.targets = [circle_datum.guess + 1]
            self.max_cand = circle_datum.guess + 1
        else:
            self.targets = [x + 1 for x in circle_datum.candidates]
            self.max_cand = max(circle_datum.candidates) + 1

        def get_ninemers(datum_index):
            ret = []
            for n_index in range(3):
                ninemer_list = self.ninemer_lists[n_index]
                list_index = -1
                for ninemer in ninemer_list:
                    list_index += 1
                    if datum_index in ninemer:
                        ret.append(list_index)
                        break
            return ret

        self.ninemer_dics = [{}, {}, {}]
        for summand in circle_datum.summands:
            summand_datum = self.datum[summand]
            # which ninemers is it in?
            ninemers = get_ninemers(summand)
            arrow = Arrow(summand_datum, ninemers)
            self.arrow_list.append(arrow)
            self.cands_list.append(arrow.candidates)
            # for each ninemer dic add an entry for its ninemer of set([])
            # each entry is used to store the candidates used.
            for i in range(3):
                ninemer_dic = self.ninemer_dics[i]
                ninemer_dic[ninemers[i]] = set([])

    def check_targets(self, mode):
        """
        Algorithms A2 and A3, depending on mode
        """

        def init_used_candidates():
            # empty the sets for each entry in the 3 ninemer dics
            for i in range(3):
                new_dic = {}
                old_dic = self.ninemer_dics[i]
                for key, item in list(old_dic.items()):
                    new_dic[key] = set([])
                self.ninemer_dics[i] = new_dic

        def update_used_candidates(arrow, candidate):
            # update candidates for each ninemer
            ret = True
            for i in range(3):
                ninemer = arrow.ninemers[i]
                ninemer_dic = self.ninemer_dics[i]
                if candidate in ninemer_dic[ninemer]:
                    # already used this candidate in this ninemer
                    ret = False
                    break
                else:
                    ninemer_dic[ninemer].add(candidate)
            return ret

        def do_combinations(target):
            # generate all combinations of current candidates
            # and check them against target sums.
            length = len(self.cands_list)
            goods = []
            for i in range(length):
                goods.append(set([]))
            for combination in list(itertools.product(*self.cands_list)):
                good_one = False
                if sum(combination) == target:
                    init_used_candidates()
                    for item in range(length):
                        arrow = self.arrow_list[item]
                        candidate = combination[item]
                        good_one = update_used_candidates(arrow, candidate)
                        if not good_one:
                            break
                if good_one:
                    for order in range(length):
                        goods[order].add(combination[order])
            return goods

        effect_pairs = []

        # delete impossible targets (A2)
        if mode == 1:
            dels = []
            for target in self.targets:
                goods = do_combinations(target)
                gok = False
                for item in range(len(self.cands_list)):
                    good = goods[item]
                    self.arrow_list[item].good_cands |= good
                    if good:
                        gok = True
                if not gok:
                    dels.append(target - 1)

            if dels:
                effect_pair = EditPair(self.circle_datum, dels)
                effect_pairs.append(effect_pair)
                self.editor.add([],
                                effect_pairs,
                                self.algorithm)
        elif mode == 2:
            # delete candidates which cannot contribute to a target (A3)
            for target in self.targets:
                goods = do_combinations(target)
                for item in range(len(self.cands_list)):
                    good = goods[item]
                    self.arrow_list[item].good_cands |= good
            for item in range(len(self.arrow_list)):
                arrow = self.arrow_list[item]
                if set(arrow.candidates) != arrow.good_cands:
                    dels = []
                    for candidate in arrow.candidates:
                        if candidate not in arrow.good_cands:
                            dels.append(candidate - 1)
                    effect_pair = EditPair(arrow.datum, dels)
                    effect_pairs.append(effect_pair)

            if effect_pairs:
                cause_pairs = []
                cause_pair = EditPair(self.circle_datum,
                                      self.circle_datum.candidates)
                cause_pairs.append(cause_pair)

                self.editor.add(cause_pairs,
                                effect_pairs,
                                self.algorithm)

    def path_maxes(self):
        # A1

        def init_used_candidates():
            # empty the sets for each entry in the 3 ninemer dics
            for i in range(3):
                new_dic = {}
                old_dic = self.ninemer_dics[i]
                for key, item in list(old_dic.items()):
                    new_dic[key] = set([])
                self.ninemer_dics[i] = new_dic

        def update_used_candidates(arrow, candidate):
            # update candidates for each ninemer
            ret = True
            for i in range(3):
                ninemer = arrow.ninemers[i]
                ninemer_dic = self.ninemer_dics[i]
                if candidate in ninemer_dic[ninemer]:
                    ret = False
                    break
                else:
                    ninemer_dic[ninemer].add(candidate)
            return ret

        def do_combinations(arrow_list, cands_list):
            # generate all combinations of current candidates
            # and find lowest sum
            length = len(cands_list)
            goods = []
            min_target = 9999
            for combination in list(itertools.product(*cands_list)):
                this_sum = sum(combination)
                if this_sum < min_target:
                    init_used_candidates()
                    for item in range(length):
                        arrow = arrow_list[item]
                        candidate = combination[item]
                        good_one = update_used_candidates(arrow, candidate)
                        if not good_one:
                            break
                if this_sum < min_target:
                    min_target = this_sum
                    goods = combination
            return goods

        effect_pairs = []
        tot = 0
        for arrow in self.arrow_list:
            if arrow.datum.guess is not None:
                tot += arrow.datum.guess + 1
        remainder = self.max_cand - tot

        for item_0 in range(len(self.arrow_list)):
            arrow_0 = self.arrow_list[item_0]
            if arrow_0.datum.guess is None:
                arrow_list = []
                cands_list = []
                for item_1 in range(len(self.arrow_list)):
                    if item_1 != item_0:
                        arrow_1 = self.arrow_list[item_1]
                        if arrow_1.datum.guess is None:
                            arrow_list.append(arrow_1)
                            cands_list.append(arrow_1.candidates)

                goods = do_combinations(arrow_list, cands_list)
                maxc = remainder - sum(goods)
                dels = []
                for candidate in arrow_0.candidates:
                    if candidate > maxc:
                        dels.append(candidate - 1)
                if dels:
                    effect_pair = EditPair(arrow_0.datum, dels)
                    effect_pairs.append(effect_pair)
                    cause_pairs = []
                    cause_pair = EditPair(self.circle_datum,
                                          self.circle_datum.candidates)
                    cause_pairs.append(cause_pair)
                    self.editor.add(cause_pairs,
                                    effect_pairs,
                                    self.algorithm)

    """
    def print_me(self):
        print '------------------------------------------'
        print 'arrows', self.circle_datum.index
        print 'box_dic', self.box_dic
        print 'targets', self.targets
        print 'cands_list', self.cands_list
        for box_index, box_data in self.box_dic.items():
            for box in box_data:
                box.print_me()
    """


class Arrow(object):
    def __init__(self, datum, ninemers):
        self.datum = datum
        self.ninemers = ninemers
        self.candidates = []
        self.good_cands = set([])
        self.used_cands = [{}, {}, {}]
        if datum.guess is not None:
            self.candidates = [datum.guess + 1]
        else:
            self.candidates = [x + 1 for x in datum.candidates]

    """
    def print_me(self):
        print 'arrow'
        print 'datum_index', self.datum.index
        print 'box_index', self.ninemers
        print 'cands', self.candidates
    """


class Model(SudokuModel):

    def __init__(self, model_data):
        SudokuModel.__init__(self, model_data)
        self.algorithms = [(self.arrow_path_maxes, 2),
                           (self.arrow_combinations_1, 2),
                           (self.arrow_combinations_2, 2),
                           (self.hidden_singles, 0),
                           (self.locked_candidate_1, 2),
                           (self.locked_candidate_2, 2),
                           (self.naked_pairs, 0),
                           (self.naked_triples, 0),
                           (self.hidden_pairs, 0),
                           (self.hidden_triples, 0),
                           (self.naked_quads, 0),
                           (self.x_wing, 1),
                           (self.random_answer, 2)]
        self.ninemers = []
        self.create_indexes()

    def init_datum(self):
        self.datum = []
        for datum_index in range(81):
            self.datum.append(Datum(datum_index))

    def get_chain_lines(self):

        for datum in self.datum:
            summands = datum.summands
            if summands:
                # add datum to each entry in summands
                directions = []
                # get line data
                prev_datum = datum
                current = datum.index
                current_x = current % 9
                current_y = int(current / 9)
                for summand in summands:
                    directions = []
                    summand_x = summand % 9
                    summand_y = int(summand / 9)
                    diff_x = int(current_x - summand_x)
                    diff_y = int(current_y - summand_y)
                    direction = [diff_x, diff_y]
                    directions.append(direction)
                    self.datum[summand].lines = directions
                    prev_current = prev_datum.index
                    prev_x = prev_current % 9
                    prev_y = int(prev_current / 9)
                    diff_x = int(summand_x - prev_x)
                    diff_y = int(summand_y - prev_y)
                    direction = [diff_x, diff_y]
                    prev_datum.lines.append(direction)
                    current_x = summand_x
                    current_y = summand_y
                    prev_datum = self.datum[summand]

    def arrow_path_maxes(self):
        self.ninemers = [self.box_indexes, self.column_indexes,
                         self.row_indexes]
        for datum in self.datum:
            if datum.summands:
                # circled cell
                arrows = Arrows(self.datum,
                                self.editor, datum,
                                self.ninemers,
                                self.algorithm)
                arrows.path_maxes()
                if self.editor.got_data():
                    break

    def arrow_combinations_1(self):
        """
        Examine summands to create sets of candidates
        and counts of cells for each box.
        If a cell is solved we add its value to a running total tot.
        We subtract tot from the maximum candidate in the circled cell
        to get remainder.
        To work out the max for any cell on the path we add up the lowest
        possible values from all the other cells and subtract them from
        remainder.
        """

        self.ninemers = [self.box_indexes, self.column_indexes,
                         self.row_indexes]
        for datum in self.datum:
            if datum.summands:
                # circled cell
                arrows = Arrows(self.datum,
                                self.editor, datum,
                                self.ninemers,
                                self.algorithm)
                arrows.check_targets(1)
                if self.editor.got_data():
                    break

    def arrow_combinations_2(self):
        """
        Examine summands to create sets of candidates
        and counts of cells for each box.
        If a cell is solved we add its value to a running total tot.
        We subtract tot from the maximum candidate in the circled cell
        to get remainder.
        To work out the max for any cell on the path we add up the lowest
        possible values from all the other cells and subtract them from
        remainder.
        """

        self.ninemers = [self.box_indexes, self.column_indexes,
                         self.row_indexes]
        for datum in self.datum:
            if datum.summands:
                # circled cell
                arrows = Arrows(self.datum,
                                self.editor, datum,
                                self.ninemers,
                                self.algorithm)
                arrows.check_targets(2)
                if self.editor.got_data():
                    break

    def init_puzzle(self, puzzle_number):
        # ret = [score, self.puzzles[answers_index], arrows]
        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
        puzzle_data = self.fetch_selected_puzzle(puzzle_number)
        self.score = puzzle_data[0]
        answers = puzzle_data[1]
        arrows = puzzle_data[2]
        self.init_datum()
        for datum_index in range(81):
            if answers[datum_index] < 0:
                answer = abs(answers[datum_index])
                given = True
            else:
                answer = answers[datum_index]
                given = False
            self.datum[datum_index].initialise(answer - 1, given)
        for arrow in arrows:
            self.datum[arrow[0]].summands = arrow[1:]
        self.get_chain_lines()

    def difficulty(self):
        scores = [0, 1, 1, 1, 1, 2, 2, 3, 3, 6, 6, 6, 9, 9]
        if self.algorithm_counts[-1] != 0:
            self.score = 999
        else:
            self.score = 0
            total = 0
            for algorithm in range(len(self.algorithm_counts)-1):
                count = self.algorithm_counts[algorithm]
                score = scores[algorithm]
                total += count * score
            self.score = total

    def get_puzzle(self, puzzle_number):
        self.init_puzzle(puzzle_number)
        self.editor.set_mode(Sgg.HINT)

    def good_arrow(self, neighbour_index, used, remainder):
        ret = [None, -1]
        if neighbour_index is not None and neighbour_index not in used:
            neighbour_datum = self.datum[neighbour_index]
            remainder -= (neighbour_datum.symbol_index + 1)
            if remainder >= 0:
                ret = [neighbour_datum, remainder]
        return ret

    """
    def test(self):
        totals = [0] * len(self.algorithm_counts)
        print totals

        bad = []
        for puzzle_number in range(self.model_data.puzzle_count):
            self.get_puzzle(puzzle_number)
            self.unsolved = []
            self.solve_no_guessing()
            if self.unsolved:
                bad.append(puzzle_number)
            else:
                for i in range(len(self.algorithm_counts)):
                    totals[i] += self.algorithm_counts[i]
        print len(bad), bad
        print totals
        exit(1)
    """


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

    def __init__(self, root):
        self.settings = Settings()
        self.master = root
        self.model_data = ModelData()
        self.flasher = None
        self.model = None
        self.error_detected = False
        self.view = View(root, self.settings)
        self.coloured_squares = []
        screen_shot_state = False
        self.active = False
        self.screen_shot = ScreenShot(PzlG.app_name,
                                      'dump_', screen_shot_state)
        puzzle_number = None
        """
        if len(sys.argv) > 1:
            puzzle_number = int(sys.argv[1])
        """
        self.start_game(puzzle_number)

    def start_game(self, puzzle_number):
        self.view.clock_init()
        self.view.clock_restart()
        self.view.game_over()
        self.model = Model(self.model_data)
        self.model.get_puzzle(puzzle_number)
        # self.model.test()
        self.ready_for_hint()
        self.view.thumbs_up()
        self.view.datum = self.model.datum
        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.model.puzzle_number))
        self.view.update_score_box(self.model.score)
        self.active = True
        self.flasher = Flasher(self.view.clock.box.label,
                               self.master, self.settings)

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

    def service_menu(self, *args):
        """
        Respond to menu choices
        One of the three event servicing functions.
        """
        strargs = ''
        for item in args:
            strargs += ' ' + item
        PzlG.logger.logger.info("service_menu " + strargs)
        name = args[0]
        if name == 'hint_mode':
            choice = args[1]
            self.set_hint_mode(choice)
        elif 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 clicks
        One of the three event servicing functions.
        """
        PzlG.logger.logger.info("service_toolbar " +
                                str(button_index) +
                                ' ' +
                                str(mouse_index))

        if mouse_index == 2:
            alg_index = self.model.editor.algorithm
            self.view.do_tooltip(button_index, alg_index)
        elif mouse_index == 3:
            if button_index == 1:
                if not self.model.is_finished():
                    if self.model.get_hint_status() != Sgg.APPLY_HINT:
                        if button_index == 1:
                            self.exhaust()
            elif button_index == 0:
                self.model.puzzle_number += 1
                self.start_game(self.model.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()
                    self.screen_shot.snap()
            elif button_index == 2:
                if not self.model.is_finished():
                    self.check_errors()
            elif button_index == 3:
                if not self.model.is_finished():
                    self.show_solution()
                    self.screen_shot.snap()

    def square_event(self, datum_index, candidate, mouse_index):
        """
        Respond to board clicks
        One of the three event servicing functions.
        """
        if self.active:
            PzlG.logger.logger.info("square_event " +
                                    str(mouse_index) +
                                    ' ' +
                                    str(datum_index) +
                                    ' ' +
                                    str(candidate))
            # if necessary abort an active hint
            board_updates = self.model.editor.abort_ops
            self.view.update_squares(board_updates)
            self.model.editor.reset()
            self.model.set_hint_status(Sgg.GIVE_HINT)
            self.model.apply_user_edits(datum_index,
                                        candidate,
                                        mouse_index)

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

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

    """
    def debug(self):
        #return
        for datum in self.model.datum:
            candidates = datum.candidates
            square = self.view.board.grid_widgets[datum.index]
            if candidates:
                for candidate in range(9):
                    state = square.itemcget(square.candidates[candidate],
                                            'state')
                    if candidate in candidates and state == 'hidden':
                        print 'bad hidden', datum.index, candidate
                    fg = square.candidates[candidate].cget('fg')
                    #print 'bad fg', datum.index, candidate, fg, '#'
                    if candidate in candidates and fg != 'black':
                        print 'bad fg', datum.index, candidate, fg, state
                    elif candidate not in candidates and state == 'normal':
                        print 'bad normal', datum.index, candidate
            else:
                #self.guess_id = cid
                fg = square.itemcget(square.guess_id, 'fill')
                if fg != 'black':
                    print 'bad value', datum.index, fg
    """

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

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

    def check_finished(self):
        """
        Check if finished.
        If so, handle buttons, flasher and clock
        """
        if self.active:
            if self.model.is_finished():
                # next line added 2017-06-13
                self.show_solution()
                self.screen_shot.snap()
                self.flasher.flash()

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

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

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

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

    def set_hint_mode(self, hint_mode_name):
        if hint_mode_name == 'simplest_hint':
            Sgg.simplest_hint = True
        else:
            Sgg.simplest_hint = False

    def set_font_size(self):
        self.settings.do_scaling()
        puzzle_number = self.model.puzzle_number
        self.start_game(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]
                self.start_game(None)
        except KeyError:
            PzlG.logger.logger.error("set_colour " + choice)

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


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