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

# Copyright (C) 2022 Rod Engerstad


import tkinter as tk
import tkinter.colorchooser
import random
from random import shuffle

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 moonlight_data import ModelData


class Sgg():
    """
    Game globals
    """
    grid_width = 6
    GIVE_HINT = 0
    APPLY_HINT = 1
    STAR = 'k'
    CLOUD = 'j'
    EMPTY = 'l'
    DELETE = 2
    alg_counts = [0 for x in range(11)]

    # Graphics
    NOT_CLOUD_OPEN = 0
    NOT_STAR_OPEN = 1
    GUESS_IS_CLOUD = 2
    GUESS_IS_STAR = 3
    CLOUD_OPEN = 4
    STAR_OPEN = 5
    UNSET_GUESS = 6
    HIGHLIGHT = 7
    UNHIGHLIGHT = 8

    SHAPE_ERROR = 10
    OPEN_CLOUD_ERROR = 11
    OPEN_STAR_ERROR = 12
    UNDO_SHAPE_ERROR = 13
    UNDO_OPEN_CLOUD_ERROR = 14
    UNDO_OPEN_STAR_ERROR = 15


class Settings(SettingsB):

    def __init__(self):
        SettingsB.__init__(self)
        self.dic = {}
        self.dic['font_size'] = '4'
        self.dic['board_colour'] = 'grey'
        self.dic['grid_colour'] = 'black'
        self.dic['highlight_colour'] = 'yellow'
        self.dic['star_colour'] = 'yellow'
        self.dic['cloud_colour'] = 'black'
        self.dic['error_colour'] = 'red'
        self.dic['set_colour'] = 'board_colour'
        self.original_square_size = 30
        self.square_size = self.original_square_size
        self.clock_box_width = 6
        self.score_box_width = 6
        self.tick_interval = 5
        self.menu_items = []
        self.preferences = Preferences(self.dic)
        self.preferences.load()
        self.max_time = None
        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: The cell next to the unlit side of a moon cannot be a star, the cell next to the lit side of of a moon cannot be a cloud."), _("Alg_1: If there is only one star_open or cloud_open in a strip it means any star_opens or cloud_opens in the orthogonal strips can be unset."), _("Alg_2: If a moon is lit from a side no cloud can come between it and the nearest star."), _("Alg_3 If a moon is lit from a side no star can exist on the opposite side."), _("Alg_4: If a moon is not lit from a side and has open_stars on this side there must be a cloud on this unlit side between it and all the open_stars. So no cloud can be beyond or level with the furthest open_star."), _("Alg_5: If a moon is not lit from a side and has open_stars on this side there must be a cloud or a blocking object on this unlit side between it and all the open_stars. Hence there cannot be a cloud on the other side of the moon."), _("Alg_6: If a moon is lit from a side and no cloud can exist on the other side, no star can exist on the edge of the lit side. But if there is more than one lit moon in the strip, it is more complicated!"), _("Alg_7: If a moon is not lit from a side an open_star cannot be nearest."), _("Alg_8: If a moon is not lit from a side the nearest object cannot be a star. There must be another moon or a cloud between its unlit side and the nearest star. If it is a cloud all other clouds can be deleted. Find such moons and then track away. If a cloud is the first object found all other clouds in the strip can be deleted."), _("Alg_9: If, in a strip, either there is 1 star_open or a set star between a lit and an unlit face, all clouds not between the two moons can be deleted."), _("Alg_10: If there is only one star_open or one cloud_open in a strip they can be converted to stars or clouds.")]

    def make_help(self):
        self.help_text = _("The program starts with a randomly selected puzzle displayed. It will consist of a square grid of cells, most of which contain only a black and a yellow button. Others have circular moons, most of which have both black and white segments, the white areas being the faces of the moon that are illuminated by, initially missing, yellow stars in its row and column. Also missing are square black clouds. These block the light from any star in the same row or column. The object of the game is to place one star and one cloud in each row and column of the grid in order to satisfy the lighting of the moons. Potentially any cell without a moon can be a star or a cloud. While they remain visible this potential is denoted by the yellow and black buttons. We call these star_open and cloud_open buttons. A right click on either button will remove it, indicating that the player has decided that the cell cannot be a star or a cloud. A left click will reinstate a button. A left click in the centre of a cell will set a star, a right click a cloud.  A middle click in a star or cloud will remove it. In the toolbar the jigsaw symbol contains a menu, the next symbol starts a new game; the wand requests a hint, a second click on the wand will perform the hint operation; the thumb checks if the puzzle contains any errors and a second click will clear the errors; the sad smiley shows the solution; the entrybox shows the current puzzle number and can be used to select a new puzzle number; the number next to the puzzle number is the puzzle difficulty score. The clock ticks every 5 seconds but will be replaced by the algorithm code if a hint is requested. A left click on the hint button will request a hint. While the hint is active (shown by the wand icon leaning leftwards) clicking on the wand with the middle mouse button will cause a pop-up to appear containing a short explanation of the hint. Hints are shown by highlighting a cell whose solution can be deduced by the current state of the puzzle. A further left click on the wand will set that solution. The program contains 500 puzzles roughly sorted in order of difficulty.")

    def make_tooltip(self):
        self.tooltip_text = [
            _('Start game'),
            _('Give hint, Apply hint'),
            ]

    def load_menus(self):
        self.menu_items.append(MenuItem(
            _('Font Size'),
            ['2',
             '4',
             '6',
             '8',
             '10',
             '12'],
            '',
            ['2',
             '4',
             '6',
             '8',
             '10',
             '12'],
            'font_size'
            ))
        self.menu_items.append(MenuItem(
            _('Set Colour'),
            [_('Board'),
             _('Grid'),
             _('Highlight'),
             _('Error')],
            'N',
            ['board_colour',
             'grid_colour',
             'highlight_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'
        ))

    def do_scaling(self):
        inc = 0.1
        mult = int(self.dic['font_size'])
        factor = 0.8 + mult * inc
        self.square_size = 50
        self.square_size = int(self.square_size * factor)


class PSquare():
    """
    Create moon shapes
    """
    def __init__(self, canvas, callback, key, x0, y0, x1, y1, width, colour):
        self.canvas = canvas
        self.key = key
        self.callback = callback

        if colour == 'black':
            self.w_id = self.canvas.create_rectangle(x0, y0, x1, y1,
                                                     width=width,
                                                     fill=colour)
        else:
            self.w_id = self.canvas.create_oval(x0, y0, x1, y1,
                                                width=width,
                                                fill=colour)
        self.add_bindings()

    def add_bindings(self):
        self.canvas.tag_bind(self.w_id, "<ButtonRelease-2>", self.command)
        self.canvas.tag_bind(self.w_id, "<ButtonRelease-3>", self.command)
        self.canvas.tag_bind(self.w_id, "<ButtonRelease-1>", self.command)
        self.canvas.tag_bind(self.w_id, "<Enter>", self.command)
        self.canvas.tag_bind(self.w_id, "<Leave>", self.command)

    def command(self, event):
        self.callback(event, self.key)

    def delete(self):
        self.canvas.delete(self.w_id)


class MSquare():
    """
    Create grid square for outline
    """
    def __init__(self, canvas, settings, key, x0, y0, x1, y1):
        self.settings = settings
        self.canvas = canvas
        self.key = key
        self.callback = None
        outline = self.settings.dic['grid_colour']
        fill = self.settings.dic['board_colour']
        active = self.settings.dic['grid_colour']
        self.w_id = self.canvas.create_rectangle(x0, y0, x1, y1,
                                                 outline=outline,
                                                 fill=fill,
                                                 width=1,
                                                 activeoutline=active)

    def add_bindings(self):
        self.canvas.tag_bind(self.w_id, "<ButtonRelease-2>", self.command)
        self.canvas.tag_bind(self.w_id, "<ButtonRelease-3>", self.command)
        self.canvas.tag_bind(self.w_id, "<ButtonRelease-1>", self.command)
        self.canvas.tag_bind(self.w_id, "<Enter>", self.command)
        self.canvas.tag_bind(self.w_id, "<Leave>", self.command)

    def command(self, event):
        self.callback(event, self.key)


class MButton():
    """
    Create the two buttons needed for each active Moonlight cell
    """
    def __init__(self, canvas, settings, datum, square_size, delta,
                 callback):
        self.key = datum.key
        self.settings = settings
        self.buttons = [None, None]
        self.bg = ''
        self.active = False
        self.callback = callback
        self.activefill = 'green'
        x = self.key[0]
        y = self.key[1]
        buttons = []
        radius = square_size / 2.3
        outline_width = 1
        padx = 0
        size = 8

        x0 = x * square_size
        y0 = y * square_size
        x_fudge = outline_width * int(x0 / square_size)
        y_fudge = outline_width * int(y0 / square_size)
        x_centre = x0 + square_size / 2
        y_centre = y0 + square_size / 2
        x_centre += x_fudge + 1
        y_centre += y_fudge + 1

        x0 = x_centre - radius + delta
        x1 = x_centre + radius - delta
        y0 = y_centre - radius + delta

        window = canvas.create_window(x0, y0)

        fill = 'yellow'
        button_0 = tk.Frame(canvas,
                            bd=0,
                            padx=padx,
                            pady=0,
                            highlightthickness=0,
                            bg=fill,
                            width=size,
                            height=size)
        canvas.itemconfigure(window, window=button_0)

        window = canvas.create_window(x1, y0)

        fill = 'black'
        button_1 = tk.Frame(canvas,
                            bd=0,
                            padx=padx,
                            pady=0,
                            highlightthickness=0,
                            bg=fill,
                            width=size,
                            height=size)
        canvas.itemconfigure(window, window=button_1)

        if datum.is_open():
            buttons = [button_0, button_1]
            for button in buttons:
                button.bind("<ButtonRelease-1>", self.command)
                button.bind("<ButtonRelease-2>", self.command)
                button.bind("<ButtonRelease-3>", self.command)
                button.bind("<Enter>", self.command)
                button.bind("<Leave>", self.command)
        self.buttons = buttons

    def set_star_open(self, status):
        if status is True:
            bg = 'yellow'
        else:
            bg = self.settings.dic['board_colour']
        self.buttons[0].configure(bg=bg)
        self.bg = bg

    def set_cloud_open(self, status):
        if status is True:
            bg = 'black'
        else:
            bg = self.settings.dic['board_colour']
        self.buttons[1].configure(bg=bg)
        self.bg = bg

    def unset(self):
        bg = self.settings.dic['board_colour']
        self.buttons[0].configure(bg=bg)
        self.buttons[1].configure(bg=bg)
        self.bg = bg

    def command(self, event):
        if True:
            if event.type == "7":
                self.active = True
                self.bg = event.widget.cget('bg')
                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
                button_index = self.buttons.index(event.widget)
                self.callback(self.key, button_index,
                              mouse_index)


class Board(tk.Canvas):
    def __init__(self, parent, settings):
        self.parent = parent
        self.settings = settings
        self.model = None
        self.square_callback = None
        self.button_callback = None
        self.grid_margin = 0
        self.square_size = self.settings.square_size
        self.board_size = 2 * self.grid_margin +\
            Sgg.grid_width * (self.square_size + 1)
        self.squares_dic = {}
        self.shapes_dic = {}
        self.buttons_dic = {}
        self.delta = 2
        self.delta = 4
        self.line_width = 1
        self.circle_width = 0
        bg = self.settings.green
        self.activefill = self.settings.green
        self.board_colour = self.settings.dic['board_colour']
        self.grid_colour = self.settings.dic['grid_colour']
        bg = self.board_colour
        tk.Canvas.__init__(self, self.parent, width=self.board_size,
                           height=self.board_size,
                           bg=bg)

        self.draw_grid()
        self.tk_focusFollowsMouse()
        self.grid(row=0, column=0)
        self.update_idletasks()

    def init_widgets(self, square_callback, button_callback):
        self.square_callback = square_callback
        self.button_callback = button_callback
        self.shapes_dic = {}
        self.buttons_dic = {}
        for key, datum in list(self.model.datum_dic.items()):
            self.shapes_dic[key] = None
            self.buttons_dic[key] = [None, None]
        self.draw_buttons()

    def my_destroy(self):
        self.destroy()

    def add_callbacks(self):
        for key, widget in list(self.squares_dic.items()):
            widget.callback = self.square_callback
            widget.add_bindings()

    def draw_grid(self):
        outline_width = 1
        unit = self.settings.square_size
        unit2 = unit / 2
        for y in range(1, Sgg.grid_width + 1):
            fudge = y * outline_width
            yc = y * unit + fudge - unit2
            y0 = yc - unit2
            y1 = yc + unit2
            for x in range(1, Sgg.grid_width + 1):
                key = (x - 1, y - 1)
                fudge = x * outline_width
                xc = x * unit + fudge - unit2
                x0 = xc - unit2
                x1 = xc + unit2
                square = MSquare(self, self.settings,
                                 key, x0, y0, x1, y1)
                self.squares_dic[key] = square

    def do_statics(self, shape, colours, x0, y0, x1, y1):
        """
        Draw the statis figures ie moons
        """
        extent = shape[1]
        start = shape[0]
        colour_0 = colours[1]
        colour_1 = colours[0]
        if shape[2] == 0:
            circle = self.create_oval(x0, y0, x1, y1,
                                      width=self.circle_width,
                                      fill=colour_1)
            seg = self.create_arc(x0, y0, x1, y1,
                                  width=self.circle_width,
                                  start=start, extent=extent,
                                  fill=colour_0)
        elif shape[2] == 1:
            circle = self.create_oval(x0, y0, x1, y1,
                                      width=self.circle_width,
                                      fill=colour_0)
            seg = self.create_arc(x0, y0, x1, y1,
                                  width=self.circle_width,
                                  start=start, extent=extent,
                                  fill=colour_1)
        elif shape[2] == 2:
            colour_0 = colours[0]
            circle = self.create_oval(x0, y0, x1, y1,
                                      width=self.circle_width,
                                      fill=colour_0)
        elif shape[2] == 3:
            colour_0 = colours[1]
            circle = self.create_oval(x0, y0, x1, y1,
                                      width=self.circle_width,
                                      fill=colour_0)

    def do_mobiles(self, datum, shape, colours, x0, y0, x1, y1):
        """
        Draw the mobile figures ie stars and clouds
        """
        key = datum.key
        if key in self.shapes_dic and self.shapes_dic[key] is not None:
            self.delete_shape(key)
        colour_0 = colours[1]
        if shape[2] == 4:
            # star
            colour_0 = colours[2]
            widget = PSquare(self, self.square_callback, key, x0, y0, x1, y1,
                             self.circle_width, colour_0)

            self.shapes_dic[key] = widget

        elif shape[2] == 5:
            # cloud
            colour_0 = colours[1]
            widget = PSquare(self, self.square_callback, key, x0, y0, x1, y1,
                             self.circle_width, colour_0)
            self.shapes_dic[key] = widget

    def draw_shape(self, datum):
        """
        ie a circle or square centred at x_centre, y_centre in booard coords
        """
        shape_dic = {'a': (2, 2, 3),
                     'b': (0, 90, 0),
                     'c': (270, 90, 0),
                     'd': (180, 90, 0),
                     'e': (90, 90, 0),
                     'f': (90, 180, 1),
                     'g': (180, 180, 0),
                     'h': (90, 180, 0),
                     'i': (180, 180, 1),
                     'k': (2, 2, 4),
                     'j': (1, 1, 5),
                     'l': ()}

        colours = ['white', 'black', 'yellow']
        radius = self.square_size / 3
        outline_width = 1

        x = datum.key[0]
        y = datum.key[1]

        x0 = x * self.square_size
        y0 = y * self.square_size
        x_fudge = outline_width * int(x0 / self.square_size)
        y_fudge = outline_width * int(y0 / self.square_size)
        x_centre = x0 + self.square_size / 2
        y_centre = y0 + self.square_size / 2
        x_centre += x_fudge + 1
        y_centre += y_fudge + 1

        x0 = x_centre - radius + self.delta
        x1 = x_centre + radius - self.delta
        y0 = y_centre - radius + self.delta
        y1 = y_centre + radius - self.delta

        shape = shape_dic[datum.guess]

        self.do_statics(shape, colours, x0, y0, x1, y1)
        self.do_mobiles(datum, shape, colours, x0, y0, x1, y1)

    def draw_shapes(self):
        for key, datum in list(self.model.datum_dic.items()):
            if datum.guess and datum.guess != 'l':
                self.draw_shape(datum)

    def draw_buttons(self):
        for key, datum in list(self.model.datum_dic.items()):
            if not datum.is_moon():
                self.buttons_dic[key] = MButton(self, self.settings, datum,
                                                self.square_size,
                                                self.delta,
                                                self.button_callback)

    def highlight_square(self, key):
        widget = self.squares_dic[key]
        colour = self.settings.dic['highlight_colour']
        widget.canvas.itemconfig(widget.w_id, outline=colour)

    def unhighlight_square(self, key):
        widget = self.squares_dic[key]
        colour = self.settings.dic['grid_colour']
        widget.canvas.itemconfig(widget.w_id, outline=colour)
        self.update_idletasks()

    def delete_shape(self, key):
        widget = self.shapes_dic[key]
        if widget is not None:
            widget.canvas.delete(widget.w_id)
            self.shapes_dic[key] = None

    def close_buttons(self):
        for key, datum in list(self.model.datum_dic.items()):
            if not datum.is_moon():
                self.buttons_dic[key].unset()

    def update_board(self, update):
        """
        """
        key = update[0]
        job = update[1]
        if job == Sgg.HIGHLIGHT:
            widget = self.squares_dic[key]
            colour = self.settings.dic['highlight_colour']
            widget.canvas.itemconfig(widget.w_id, activeoutline=colour)
        elif job == Sgg.UNHIGHLIGHT:
            widget = self.squares_dic[key]
            colour = self.settings.dic['grid_colour']
            widget.canvas.itemconfig(widget.w_id, activeoutline=colour)
        elif job == Sgg.NOT_CLOUD_OPEN:
            status = False
            self.buttons_dic[key].set_cloud_open(status)
        elif job == Sgg.NOT_STAR_OPEN:
            status = False
            self.buttons_dic[key].set_star_open(status)
        elif job == Sgg.CLOUD_OPEN:
            status = True
            self.buttons_dic[key].set_cloud_open(status)
        elif job == Sgg.STAR_OPEN:
            status = True
            self.buttons_dic[key].set_star_open(status)
        elif job == Sgg.GUESS_IS_STAR:
            datum = self.model.datum_dic[key]
            self.draw_shape(datum)
        elif job == Sgg.GUESS_IS_CLOUD:
            datum = self.model.datum_dic[key]
            self.draw_shape(datum)
        elif job == Sgg.UNSET_GUESS:
            self.delete_shape(key)
        elif job == Sgg.SHAPE_ERROR:
            widget = self.shapes_dic[key]
            colour = self.settings.dic['error_colour']
            widget.canvas.itemconfig(widget.w_id, fill=colour)
        elif job == Sgg.OPEN_CLOUD_ERROR:
            colour = self.settings.dic['error_colour']
            button = self.buttons_dic[key]
            button.buttons[1].configure(bg=colour)
        elif job == Sgg.OPEN_STAR_ERROR:
            colour = self.settings.dic['error_colour']
            button = self.buttons_dic[key]
            button.buttons[0].configure(bg=colour)
        elif job == Sgg.UNDO_SHAPE_ERROR:
            self.delete_shape(key)
        """
        elif job == Sgg.UNDO_OPEN_CLOUD_ERROR:
            colour = self.settings.dic['cloud_colour']
            button = self.buttons_dic[key]
            button.buttons[1].configure(bg=colour)
        elif job == Sgg.UNDO_OPEN_STAR_ERROR:
            colour = self.settings.dic['star_colour']
            button = self.buttons_dic[key]
            button.buttons[0].configure(bg=colour)
        """


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.board = None
        self.image_dic = {}
        self.make_image_dic()
        self.image_dic['start'] = [self.images.start_moonlight, 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)

    def draw_board(self):
        self.board.draw_shapes()
        self.board.update_idletasks()

    def update_clock_box(self, algorithm):
        value = str(algorithm)
        if algorithm is not None:
            self.clock.box.set_text(value)


class Datum():
    """
    Cell item
    """
    def __init__(self, key, answer):
        self.key = key
        self.answer = answer
        self.guess = ''
        self.star_open = True
        self.cloud_open = True
        self.finished = False
        if self.is_moon():
            self.guess = answer
            self.star_open = False
            self.cloud_open = False
            self.finished = False
        self.buttons = [None, None]

    def is_moon(self):
        return self.answer in ('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i')

    def is_blocking(self):
        return self.guess and self.guess not in [Sgg.STAR, Sgg.CLOUD]

    def is_finished(self):
        return self.finished

    def is_open(self):
        return self.answer in ('j', 'k', 'l')

    def is_mobile(self):
        return self.guess in ('j', 'k')

    def is_star(self):
        return self.guess == Sgg.STAR

    def set_guess(self, guess):
        self.guess = guess
        self.star_open = False
        self.cloud_open = False
        if self.guess == self.answer:
            self.finished = True

    def unset_guess(self):
        self.star_open = True
        self.cloud_open = True
        self.guess = ''
        self.finished = False

    def is_unset(self):
        return not self.guess

    def is_set(self):
        return self.guess

    def set_star_open(self, status):
        self.star_open = status

    def set_cloud_open(self, status):
        self.cloud_open = status

    def star_is_open(self):
        return self.star_open or self.guess == Sgg.STAR

    def unset_star_open_is_open(self):
        return self.star_open and self.is_unset()

    def unset_cloud_open_is_open(self):
        return self.cloud_open and self.is_unset()

    def cloud_is_open(self):
        return self.cloud_open or self.guess == Sgg.CLOUD

    def is_right(self):
        return self.guess and self.guess == self.answer

    def is_wrong(self):
        ret = 0
        if self.guess and self.guess != self.answer:
            #print('error 0', self.key, self.guess, self.answer)
            ret = Sgg.SHAPE_ERROR
        if not self.guess and (self.answer == Sgg.CLOUD) and not \
           self.cloud_is_open():
            #print('error 2', self.key, self.guess, self.answer, self.cloud_open)
            ret = Sgg.OPEN_CLOUD_ERROR
        if not self.guess and (self.answer == Sgg.STAR) and not self.star_open:
            #print('error 1', self.key, self.guess, self.answer, self.star_open)
            ret = Sgg.OPEN_STAR_ERROR
        return ret

    def __str__(self):
        return str(self.key) + str(self.answer) + str(self.guess)


class Model():

    incs = [(-1, 0), (1, 0), (0, 1), (0, -1)]
    # order:            l    r    d    u
    goods_dic = {'a': ['k', 'k', 'k', 'k'],
                 'b': ['j', 'k', 'j', 'k'],
                 'c': ['j', 'k', 'k', 'j'],
                 'd': ['k', 'j', 'k', 'j'],
                 'e': ['k', 'j', 'j', 'k'],
                 'f': ['j', 'k', 'k', 'k'],
                 'g': ['k', 'k', 'k', 'j'],
                 'h': ['k', 'j', 'k', 'k'],
                 'i': ['k', 'k', 'j', 'k']}

    # lit sides      alg_1
    lit_incs_dic = {'a': [],
                    'b': [[-1, 0], [0, 1]],
                    'c': [[-1, 0], [0, -1]],
                    'd': [[0, -1], [1, 0]],
                    'e': [[1, 0], [0, 1]],
                    'f': [[-1, 0]],
                    'g': [[0, -1]],
                    'h': [[1, 0]],
                    'i': [[0, 1]]}

    # unlit sides    alg_2
    unlit_incs_dic = {'a': [],
                      'b': [[1, 0], [0, -1]],
                      'c': [[1, 0], [0, 1]],
                      'd': [[0, 1], [-1, 0]],
                      'e': [[-1, 0], [0, -1]],
                      'f': [[1, 0]],
                      'g': [[0, 1]],
                      'h': [[-1, 0]],
                      'i': [[0, -1]]}

    # unlit sides plus
    unlit_incs_p_dic = {'a': [[-1, 0], [1, 0], [0, -1], [0, 1]],
                        'b': [[1, 0], [0, -1]],
                        'c': [[1, 0], [0, 1]],
                        'd': [[0, 1], [-1, 0]],
                        'e': [[-1, 0], [0, -1]],
                        'f': [[1, 0], [0, -1], [0, 1]],
                        'g': [[0, 1], [-1, 0], [1, 0]],
                        'h': [[-1, 0], [0, -1], [0, 1]],
                        'i': [[0, -1], [-1, 0], [1, 0]]}

    # opposite sides plus
    lit_incs_p_dic = {'a': [[1, 0], [-1, 0], [0, 1], [0, -1]],
                      'b': [[-1, 0], [0, 1]],
                      'c': [[-1, 0], [0, -1]],
                      'd': [[0, -1], [1, 0]],
                      'e': [[1, 0], [0, 1]],
                      'f': [[-1, 0], [0, 1], [0, -1]],
                      'g': [[0, -1], [1, 0], [-1, 0]],
                      'h': [[1, 0], [0, 1], [0, -1]],
                      'i': [[0, 1], [1, 0], [-1, 0]]}

    @staticmethod
    def in_range(key):
        ret = True
        x = key[0]
        y = key[1]
        if x < 0 or x >= Sgg.grid_width or y < 0 or y >= Sgg.grid_width:
            ret = False
        return ret

    @staticmethod
    def add_keys(key_0, key_1):
        return (key_0[0] + key_1[0], key_0[1] + key_1[1])

    @staticmethod
    def swap_key(i, j, swap):
        """
        if swap != 0 swap the order
        """
        if swap != 0:
            ret = (j, i)
        else:
            ret = (i, j)
        return ret

    @staticmethod
    def open_buttons(key):
        updates = []
        updates.append([key, Sgg.STAR_OPEN])
        updates.append([key, Sgg.CLOUD_OPEN])
        return updates

    @staticmethod
    def close_buttons(key):
        updates = []
        updates.append([key, Sgg.NOT_STAR_OPEN])
        updates.append([key, Sgg.NOT_CLOUD_OPEN])
        return updates

    def __init__(self, model_data):
        self.model_data = model_data
        self.datum_dic = {}
        self.finished = False
        self.moons = []
        self.status = True
        self.hint = []
        self.hint_status = Sgg.GIVE_HINT
        self.errors = []
        self.puzzle_number = None
        self.difficulty = None
        self.algorithm = None
        self.algorithms = [
            (self.alg_0),
            (self.alg_1),
            (self.alg_2),
            (self.alg_3),
            (self.alg_4),
            (self.alg_5),
            (self.alg_6),
            (self.alg_7),
            (self.alg_8),
            (self.alg_9),
            (self.alg_10),
            ]
        self.alg_counts = [0 for x in self.algorithms]

    def get_puzzle(self, puzzle_number):
        if puzzle_number is None or puzzle_number < 0\
           or puzzle_number > self.model_data.puzzle_count - 1:
            puzzle_number = random.randint(0, self.model_data.puzzle_count - 1)
        self.puzzle_number = puzzle_number
        puzzle = self.model_data.get_puzzle(puzzle_number)
        self.make_grid(puzzle)
        self.difficulty = self.get_difficulty()

    def make_grid(self, puzzle):
        self.moons = []
        if puzzle[1].isdigit():
            start = 2
            Sgg.grid_width = int(puzzle[0:2])
        else:
            start = 1
            Sgg.grid_width = int(puzzle[0])
        self.datum_dic = {}
        y = -1
        x = -1
        for i, item in enumerate(puzzle[start:]):
            if i % Sgg.grid_width == 0:
                x = 0
                y += 1
            else:
                x += 1

            key = (x, y)
            datum = Datum(key, item)
            self.datum_dic[key] = datum
            if datum.is_moon():
                self.moons.append(key)
        self.finished = False

    def is_open(self, key):
        return self.datum_dic[key].is_open()

    def set_guess(self, key, object_type):
        updates = []
        if object_type == Sgg.STAR:
            updates += self.set_star_open(key, True)
            updates.append([key, Sgg.GUESS_IS_STAR])
        else:
            updates += self.set_cloud_open(key, True)
            updates.append([key, Sgg.GUESS_IS_CLOUD])
        self.datum_dic[key].set_guess(object_type)
        updates.append([key, Sgg.NOT_CLOUD_OPEN])
        updates.append([key, Sgg.NOT_STAR_OPEN])
        return updates

    def unset_guess(self, key):
        updates = []
        datum = self.datum_dic[key]
        if datum.is_star():
            updates += self.unset_star_open(key)
        else:
            updates += self.unset_cloud_open(key)
        return updates

    def unset_star_open(self, in_key):
        """
        Undo clicked on star guess.
        Need to set star_open for its orthogonal strips
        If status is False, this may be the last but one
        and so the remaining one can be set as a guess.
        """
        updates = []

        in_x = in_key[0]
        in_y = in_key[1]
        open_x = []
        open_y = []
        for key, datum in list(self.datum_dic.items()):
            if not datum.is_moon():
                if key[0] == in_x and not datum.star_is_open():
                    open_x.append(key)
                if key[1] == in_y and not datum.star_is_open():
                    open_y.append(key)

        key = open_x[0]
        x = key[0]
        for y in range(Sgg.grid_width):
            key = (x, y)
            datum = self.datum_dic[key]
            if not datum.is_moon() and not datum.is_right():
                datum.set_star_open(True)
                updates.append([key, Sgg.STAR_OPEN])

        key = open_y[0]
        y = key[1]
        for x in range(Sgg.grid_width):
            key = (x, y)
            datum = self.datum_dic[key]
            if not datum.is_moon() and not datum.is_right():
                datum.set_star_open(False)
                updates.append([key, Sgg.STAR_OPEN])

        self.datum_dic[in_key].unset_guess()
        updates.append([in_key, Sgg.UNSET_GUESS])
        updates += Model.open_buttons(in_key)
        return updates

    def unset_cloud_open(self, in_key):
        """
        Undo clicked on cloud guess.
        Need to set cloud_open for its orthogonal strips
        If status is False, this may be the last but one
        and so the remaining one can be set as a guess.
        """
        updates = []

        in_x = in_key[0]
        in_y = in_key[1]
        open_x = []
        open_y = []
        for key, datum in list(self.datum_dic.items()):
            if not datum.is_moon():
                if key[0] == in_x and not datum.cloud_is_open():
                    open_x.append(key)
                if key[1] == in_y and not datum.cloud_is_open():
                    open_y.append(key)

        key = open_x[0]
        x = key[0]
        for y in range(Sgg.grid_width):
            key = (x, y)
            datum = self.datum_dic[key]
            if not datum.is_moon() and not datum.is_right():
                datum.set_cloud_open(True)
                updates.append([key, Sgg.CLOUD_OPEN])

        key = open_y[0]
        y = key[1]
        for x in range(Sgg.grid_width):
            key = (x, y)
            datum = self.datum_dic[key]
            if not datum.is_moon() and not datum.is_right():
                datum.set_cloud_open(False)
                updates.append([key, Sgg.CLOUD_OPEN])

        self.datum_dic[in_key].unset_guess()
        updates.append([in_key, Sgg.UNSET_GUESS])
        updates += Model.open_buttons(in_key)
        return updates

    def set_star_open(self, in_key, status):
        """
        Change status of star button.
        If status is False, this may be the last but one
        and so the remaining one can be set as a guess.
        """
        updates = []
        self.datum_dic[in_key].set_star_open(status)
        if status:
            updates.append([in_key, Sgg.STAR_OPEN])
        else:
            updates.append([in_key, Sgg.NOT_STAR_OPEN])
            in_x = in_key[0]
            in_y = in_key[1]
            open_x = []
            open_y = []
            for key, datum in list(self.datum_dic.items()):
                if key != in_key and not datum.is_moon():
                    if key[0] == in_x and datum.star_is_open():
                        open_x.append(key)
                    if key[1] == in_y and datum.star_is_open():
                        open_y.append(key)
            opens = [open_x, open_y]
            for swap, open_z in enumerate(opens):
                if len(open_z) == 1:
                    key = open_z[0]
                    if self.datum_dic[key].answer == Sgg.STAR:
                        self.datum_dic[key].set_guess(Sgg.STAR)
                        updates.append([key, Sgg.GUESS_IS_STAR])
                        updates += Model.close_buttons(key)
                        if swap == 0:
                            y = key[1]
                            x = 0
                            inc = (1, 0)
                        else:
                            x = key[0]
                            y = 0
                            inc = (0, 1)
                        key = (x, y)
                        for z in range(Sgg.grid_width):
                            datum = self.datum_dic[key]
                            if not datum.is_moon():
                                datum.set_star_open(False)
                                updates.append([key, Sgg.NOT_STAR_OPEN])
                            key = Model.add_keys(key, inc)
        return updates

    def set_cloud_open(self, in_key, status):
        """
        Change status of cloud button.
        If status is False, this may be the last but one
        and so the remaining one can be set as a guess.
        """

        updates = []
        self.datum_dic[in_key].set_cloud_open(status)
        if status:
            updates.append([in_key, Sgg.CLOUD_OPEN])
        else:
            updates.append([in_key, Sgg.NOT_CLOUD_OPEN])
            in_x = in_key[0]
            in_y = in_key[1]
            open_x = []
            open_y = []
            for key, datum in list(self.datum_dic.items()):
                if key != in_key and not datum.is_moon():
                    if key[0] == in_x and datum.cloud_is_open():
                        open_x.append(key)
                    if key[1] == in_y and datum.cloud_is_open():
                        open_y.append(key)
            opens = [open_x, open_y]
            for swap, open_z in enumerate(opens):
                if len(open_z) == 1:
                    key = open_z[0]
                    if self.datum_dic[key].answer == Sgg.CLOUD:
                        self.datum_dic[key].set_guess(Sgg.CLOUD)
                        updates.append([key, Sgg.GUESS_IS_CLOUD])
                        updates += Model.close_buttons(key)

                        if swap == 0:
                            y = key[1]
                            x = 0
                            inc = (1, 0)
                        else:
                            x = key[0]
                            y = 0
                            inc = (0, 1)
                        key = (x, y)
                        for z in range(Sgg.grid_width):
                            datum = self.datum_dic[key]
                            if not datum.is_moon():
                                datum.set_cloud_open(False)
                                updates.append([key, Sgg.NOT_CLOUD_OPEN])
                            key = Model.add_keys(key, inc)
        return updates

    def set_star_opens(self, in_key, status, done_cols, done_rows):
        """
        in_key has just changed status.
        done_cols and done_rows are the strips with correctly
        set stars so we can set them accordingly.
        Here we deal with the orthogonal datums that are affected.
        If set, set_guess, unset open status for those orthogonal
        and update board accordingly.
        If unset, unset_guess, set open status for those orthogonal
        and update board accordingly.
        """
        updates = []
        if status:
            job = Sgg.STAR_OPEN
        else:
            job = Sgg.NOT_STAR_OPEN
        in_x = in_key[0]
        in_y = in_key[1]
        for key, datum in list(self.datum_dic.items()):
            if key != in_key and not datum.is_moon():
                if key[0] not in done_cols and\
                   key[1] not in done_rows:
                    if key[0] == in_x:
                        if not datum.is_right():
                            datum.set_star_open(status)
                            updates.append([key, job])
                    if key[1] == in_y:
                        if not datum.is_right():
                            datum.set_star_open(status)
                            updates.append([key, job])
        return updates

    def set_cloud_opens(self, in_key, status, done_cols, done_rows):
        """
        in_key has just changed status.
        done_cols and done_rows are the strips with correctly
        set clouds so we can set them accordingly.
        Here we deal with the orthogonal datums that are affected.
        If set, set_guess, unset open status for those orthogonal
        and update board accordingly.
        If unset, unset_guess, set open status for those orthogonal
        and update board accordingly.
        """
        updates = []
        if status:
            job = Sgg.CLOUD_OPEN
        else:
            job = Sgg.NOT_CLOUD_OPEN
        in_x = in_key[0]
        in_y = in_key[1]
        for key, datum in list(self.datum_dic.items()):
            if key != in_key and not datum.is_moon():
                if key[0] not in done_cols and\
                   key[1] not in done_rows:
                    if key[0] == in_x:
                        if not datum.is_right():
                            datum.set_cloud_open(status)
                            updates.append([key, job])
                    if key[1] == in_y:
                        if not datum.is_right():
                            datum.set_cloud_open(status)
                            updates.append([key, job])
        return updates

    def is_finished(self):
        return self.finished

    def set_solution(self):
        for key, datum in list(self.datum_dic.items()):
            datum.set_guess(datum.answer)

    def check_errors(self):
        self.errors = []
        self.status = True
        for key, datum in list(self.datum_dic.items()):
            datum_status = datum.is_wrong()
            if datum_status:
                self.errors.append([key, datum_status])
                self.status = False

    def correct_errors(self):
        """
        Do grid and GUI
        """
        updates = []
        star_cols, star_rows = self.get_done(Sgg.STAR)
        cloud_cols, cloud_rows = self.get_done(Sgg.CLOUD)
        self.status = True
        for key, datum in list(self.datum_dic.items()):
            datum_status = datum.is_wrong()
            if datum_status == Sgg.SHAPE_ERROR:
                datum.unset_guess()
                updates.append([key, Sgg.UNDO_SHAPE_ERROR])
                if key[0] in cloud_cols or key[1] in cloud_rows:
                    # need to reverse unset_guess
                    datum.cloud_open = False
                else:
                    updates.append([key, Sgg.CLOUD_OPEN])
                if key[0] in star_cols or key[1] in star_rows:
                    # need to reverse unset_guess
                    datum.star_open = False
                else:
                    updates.append([key, Sgg.STAR_OPEN])
                updates += self.set_cloud_opens(key, True,
                                                cloud_cols, cloud_rows)
                updates += self.set_star_opens(key, True, star_cols, star_rows)
            elif datum_status == Sgg.OPEN_CLOUD_ERROR:
                datum.set_cloud_open(True)
                updates.append([key, Sgg.CLOUD_OPEN])
                updates += self.set_cloud_opens(key, True,
                                                cloud_cols, cloud_rows)
            elif datum_status == Sgg.OPEN_STAR_ERROR:
                datum.set_star_open(True)
                updates.append([key, Sgg.STAR_OPEN])
                updates += self.set_star_opens(key, True, star_cols, star_rows)

        return updates

    def get_difficulty(self):
        """
        Difficulty is number of choices for star position!
        """
        l_ro = ['d', 'e', 'h']
        r_lo = ['b', 'c', 'f']
        t_bo = ['b', 'e', 'i']
        b_to = ['c', 'd', 'g']

        difficulty = 0
        for swap in range(2):
            if swap == 0:
                l_r = l_ro
                r_l = r_lo
            else:
                l_r = t_bo
                r_l = b_to
            for j in range(Sgg.grid_width):
                # get moons
                blocks = []
                rtol = -1
                ltor = -1
                for i in range(Sgg.grid_width):
                    key = Model.swap_key(i, j, swap)
                    datum = self.datum_dic[key]
                    if datum.is_moon():
                        if datum.answer in l_r:
                            ltor = i
                        elif datum.answer in r_l:
                            rtol = i
                        else:
                            blocks.append(i)

                # get dist
                dist = -1
                if ltor != -1:
                    if rtol != -1:
                        # sun must be between
                        dist = rtol - ltor - 1
                    else:
                        # blocks or right edge
                        if blocks:
                            # sun between ltor and first block to its right
                            for block in blocks:
                                if block > ltor:
                                    dist = block - ltor - 1
                                    break
                            if dist == -1:
                                # all blocks to left
                                dist = Sgg.grid_width - ltor - 1
                        else:
                            # no blocks
                            dist = Sgg.grid_width - ltor - 1
                elif rtol != -1:
                    # rtol, no ltor
                    if blocks:
                        for k in range(len(blocks) - 1, -1, -1):
                            block = blocks[k]
                            if block < rtol:
                                dist = rtol - block - 1
                                break
                        if dist == -1:
                            # blocks to right
                            dist = rtol
                    else:
                        # no blocks
                        dist = rtol
                else:
                    if dist == -1:
                        dist = Sgg.grid_width - len(blocks)
                difficulty += dist
        return difficulty

    def track(self, key_in, inc):
        """
        track away from key_in in one direction.
        return guess for the first object met or 'k'.
        """
        ret = 'k'
        inc_x = inc[0]
        inc_y = inc[1]
        x = key_in[0]
        y = key_in[1]
        while True:
            x += inc_x
            y += inc_y
            key = (x, y)
            key = Model.add_keys(key_in, inc)
            if not Model.in_range(key):
                break
            if self.datum_dic[key].is_mobile():
                ret = self.datum_dic[key].guess
                break
        return ret

    def strip_has_star(self, key_in, inc):
        # inc should be [0, 1] for vertical scan, [1, 0] horizontal
        ret = []
        if inc[0] == 0:
            # scanning vertically
            y = key_in[1]
            for x in range(Sgg.grid_width):
                key = (x, y)
                datum = self.datum_dic[key]
                if datum.is_star():
                    ret.append(key)
        else:
            x = key_in[0]
            for y in range(Sgg.grid_width):
                key = (x, y)
                datum = self.datum_dic[key]
                if datum.is_star():
                    ret.append(key)
        return ret

    def get_done(self, object_type):
        done_keys = []
        for key, datum in list(self.datum_dic.items()):
            if datum.is_finished() and datum.guess == object_type:
                done_keys.append(key)

        done_rows = []
        done_cols = []
        for done_key in done_keys:
            done_rows.append(done_key[1])
            done_cols.append(done_key[0])
        return done_cols, done_rows

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

    def alg_0(self):
        """
        Use the moon's illumination to set their immediate
        neighbours star and cloud status values: the cell
        next to the unlit side of a moon cannot be a star,
        the cell next to the lit side of of a moon cannot
        be a cloud.
        """
        self.hint = []
        for moon_key in self.moons:
            guess = self.datum_dic[moon_key].guess
            good = Model.goods_dic[guess]
            for dinc, inc in enumerate(Model.incs):
                sym = good[dinc]
                key = Model.add_keys(moon_key, inc)
                if Model.in_range(key):
                    datum = self.datum_dic[key]
                    if datum.is_unset():
                        if sym == Sgg.CLOUD and \
                           datum.cloud_is_open():
                            self.hint.append([key, Sgg.NOT_CLOUD_OPEN])
                        elif sym == Sgg.STAR and \
                             datum.star_is_open():
                            self.hint.append([key, Sgg.NOT_STAR_OPEN])

    def alg_1(self):
        """
        For each strip count open stars and open clouds.
        If there is only one it means any open stars or open
        clouds in the orthogonal strips can be unset. Note this
        algorithm does not set the remaining open stars and open
        clouds as stars or clouds.
        """
        self.hint = []
        for swap in range(2):
            for i in range(Sgg.grid_width):
                star_count = 0
                cloud_count = 0
                last_star = -1
                last_cloud = -1
                for j in range(Sgg.grid_width):
                    key = Model.swap_key(i, j, swap)
                    datum = self.datum_dic[key]
                    if datum.star_is_open():
                        star_count += 1
                        last_star = j
                    if datum.cloud_is_open():
                        cloud_count += 1
                        last_cloud = j

                if star_count == 1:
                    last_key = Model.swap_key(i, last_star, swap)
                    for k in range(Sgg.grid_width):
                        key = Model.swap_key(k, last_star, swap)
                        if key != last_key:
                            datum = self.datum_dic[key]
                            if datum.star_is_open():
                                self.hint.append([key, Sgg.NOT_STAR_OPEN])

                if cloud_count == 1:
                    last_key = Model.swap_key(i, last_cloud, swap)
                    for k in range(Sgg.grid_width):
                        key = Model.swap_key(k, last_cloud, swap)
                        if key != last_key:
                            datum = self.datum_dic[key]
                            if datum.cloud_is_open():
                                self.hint.append([key, Sgg.NOT_CLOUD_OPEN])

    def track_1(self, key_in, inc):
        """
        track away from key_in in one direction.
        Any cloud_open we meet can be switched off.
        """
        ret = ()
        key = tuple(key_in)
        while True:
            key = Model.add_keys(key, inc)
            if not Model.in_range(key):
                break
            if self.datum_dic[key].is_blocking():
                break
            if self.datum_dic[key].cloud_is_open():
                ret = key
                break
            if self.datum_dic[key].star_is_open():
                break
        return ret

    def track_2(self, key_in, inc):
        """
        track away from key_in in one direction.
        Return first star_open met or ()
        """
        ret = ()
        key = tuple(key_in)
        while True:
            key = Model.add_keys(key, inc)
            if not Model.in_range(key):
                break

            if not self.datum_dic[key].is_blocking():
                if self.datum_dic[key].star_is_open():
                    ret = key
                    break
        return ret

    def alg_2(self):
        """
        If a moon is lit from a side no cloud can come between
        it and the nearest star.
        """
        # lit sides
        incs_dic = Model.lit_incs_dic

        self.hint = []
        for moon_key in self.moons:
            guess = self.datum_dic[moon_key].guess
            incs = incs_dic[guess]
            for dinc, inc in enumerate(incs):
                ret = self.track_1(moon_key, inc)
                if ret:
                    self.hint.append([ret, Sgg.NOT_CLOUD_OPEN])

    def alg_3(self):
        """
        If a moon is lit from a side no star can exist on the opposite side.
        """
        # unlit sides
        incs_dic = Model.unlit_incs_dic
        self.hint = []
        for moon_key in self.moons:
            guess = self.datum_dic[moon_key].guess
            incs = incs_dic[guess]
            for dinc, inc in enumerate(incs):
                ret = self.track_2(moon_key, inc)
                if ret:
                    self.hint.append([ret, Sgg.NOT_STAR_OPEN])

    def track_3a(self, key_in, inc, object_code):
        """
        track away from key_in in one direction
        looking for an open object_code.
        """
        ret = ()
        key = tuple(key_in)
        while True:
            key = Model.add_keys(key, inc)
            if not Model.in_range(key):
                break
            if object_code == Sgg.STAR and \
               self.datum_dic[key].star_is_open():
                ret = key
                break
            if object_code == Sgg.CLOUD and \
               self.datum_dic[key].cloud_is_open():
                ret = key
                break
        return ret

    def track_5(self, key_in, inc, object_code):
        """
        track away from key_in in one direction
        looking for an open object_code.
        """
        ret = ()
        key = tuple(key_in)
        while True:
            key = Model.add_keys(key, inc)
            if not Model.in_range(key):
                break
            if self.datum_dic[key].is_blocking():
                break
            if object_code == Sgg.STAR and \
               self.datum_dic[key].star_is_open():
                ret = key
                break
            if object_code == Sgg.CLOUD and \
               self.datum_dic[key].cloud_is_open():
                ret = key
                break
        return ret

    def get_lasts(self, key_in, inc):
        """
        track away from key_in in one direction
        to find the last open_clouds and open_stars.
        """
        last_star = ()
        last_cloud = ()
        tracking_left = False
        tracking_right = False
        tracking_up = False
        tracking_down = False
        if inc[0] == 0:
            if inc[1] < 0:
                # tracking up
                tracking_up = True
            else:
                tracking_down = True
        elif inc[1] == 0:
            if inc[0] < 0:
                # tracking left
                tracking_left = True
            else:
                tracking_right = True

        key = tuple(key_in)
        while True:
            key = Model.add_keys(key, inc)
            if not Model.in_range(key):
                break

            if self.datum_dic[key].is_blocking():
                break
            if self.datum_dic[key].cloud_is_open():
                last_cloud = key

            if self.datum_dic[key].star_is_open():
                last_star = key

        return tracking_right, tracking_left, tracking_up, tracking_down,\
            last_star, last_cloud

    def check_lasts_3(self,
                      tracking_right,
                      tracking_left,
                      tracking_up,
                      tracking_down,
                      last_star,
                      last_cloud):

        ret = ()

        if last_star and last_cloud:
            if tracking_right:
                if last_star[0] >= last_cloud[0]:
                    ret = last_star
            if tracking_left:
                if last_star[0] <= last_cloud[0]:
                    ret = last_star
            if tracking_up:
                if last_star[1] <= last_cloud[1]:
                    ret = last_star
            if tracking_down:
                if last_star[1] >= last_cloud[1]:
                    ret = last_star
        return ret

    def check_lasts_4(self,
                      tracking_right,
                      tracking_left,
                      tracking_up,
                      tracking_down,
                      last_star,
                      last_cloud):
        ret = ()
        if last_star and last_cloud:
            if tracking_right:
                if last_cloud[0] >= last_star[0]:
                    ret = last_cloud
            if tracking_left:
                if last_cloud[0] <= last_star[0]:
                    ret = last_cloud
            if tracking_up:
                if last_cloud[1] <= last_star[1]:
                    ret = last_cloud
            if tracking_down:
                if last_cloud[1] >= last_star[1]:
                    ret = last_cloud
        return ret

    def track_3b(self, key_in, inc):
        """
        track away from key_in in one direction
        to find the last open_clouds and open_stars.
        """

        tracking_right, tracking_left, tracking_up, tracking_down,\
            last_star, last_cloud = self.get_lasts(key_in, inc)

        return self.check_lasts_3(tracking_right,
                                  tracking_left,
                                  tracking_up,
                                  tracking_down,
                                  last_star,
                                  last_cloud)

    def track_4(self, key_in, inc):
        """
        track away from key_in in one direction
        to find the last open_clouds and open_stars.
        """
        tracking_right, tracking_left, tracking_up, tracking_down,\
            last_star, last_cloud = self.get_lasts(key_in, inc)

        return self.check_lasts_4(tracking_right,
                                  tracking_left,
                                  tracking_up,
                                  tracking_down,
                                  last_star,
                                  last_cloud)

    def alg_4(self):
        """
        If a moon is not lit from a side and has open stars on this side
        there must be a cloud on this unlit side between it and all the
        open stars. So no cloud can be beyond or level with the
        furthest open star.
        """
        incsa_dic = Model.unlit_incs_p_dic

        self.hint = []
        for moon_key in self.moons:
            guess = self.datum_dic[moon_key].guess
            incsa = incsa_dic[guess]
            for dinc, inca in enumerate(incsa):
                ret = self.track_4(moon_key, inca)
                if ret:
                    self.hint.append([ret, Sgg.NOT_CLOUD_OPEN])

    def alg_5(self):
        """
        If a moon is not lit from a side and has open stars on this side
        there must be a cloud or a blocking object on this unlit side
        between it and all the open stars. Hence there cannot be a cloud
        on the other side of the moon.
        """
        # unlit sides
        # opposite sides
        incsa_dic = Model.unlit_incs_p_dic
        incsb_dic = Model.lit_incs_p_dic
        self.hint = []
        for moon_key in self.moons:
            guess = self.datum_dic[moon_key].guess
            incsa = incsa_dic[guess]
            incsb = incsb_dic[guess]
            # is there a star on this side?
            for dinc, inca in enumerate(incsa):
                if Model.in_range(Model.add_keys(moon_key, inca)):
                    ret = self.track_5(moon_key, inca, Sgg.STAR)
                    if ret:
                        # is there no star on the other side?
                        ret = self.track_5(moon_key, incsb[dinc],
                                           Sgg.STAR)
                        if not ret:
                            # if there is a cloud on this side
                            # it can be deleted
                            ret = self.track_5(moon_key, incsb[dinc],
                                               Sgg.CLOUD)

                            if ret:
                                self.hint.append([ret, Sgg.NOT_CLOUD_OPEN])

    def alg_6(self):
        """
        If a moon is lit from a side and no cloud can exist
        on the other side, no star can exist on the edge of the lit side.
        But is there's more than one lit moon in the strip, it is
        more complicated!
        """
        # unlit sides
        # lit sides
        incsa_dic = Model.unlit_incs_dic
        incsb_dic = Model.lit_incs_dic

        self.hint = []
        for moon_key in self.moons:
            guess = self.datum_dic[moon_key].guess
            incsa = incsa_dic[guess]
            for dinc, inca in enumerate(incsa):
                if inca[0] != 0:
                    # in x
                    ckey = (0, moon_key[1])
                else:
                    ckey = (moon_key[0], 0)
                if not self.facing_lit_moons(ckey, inca):
                    ret = self.track_3a(moon_key, inca, Sgg.CLOUD)
                    if not ret:
                        # no cloud one way
                        incsb = incsb_dic[guess]
                        ret = self.track_3b(moon_key, incsb[dinc])
                        if ret:
                            count = self.strip_has_star(ret, incsb[dinc])
                            if len(count) < 2:
                                self.hint.append([ret, Sgg.NOT_STAR_OPEN])

    def track_7(self, key_in, inc):
        """
        track away from key_in in one direction.
        """
        ret = ()
        key = tuple(key_in)

        while True:
            key = Model.add_keys(key, inc)
            if not Model.in_range(key):
                break
            if self.datum_dic[key].is_blocking():
                break
            if self.datum_dic[key].star_is_open():
                ret = key
                break
            if self.datum_dic[key].cloud_is_open():
                break
        return ret

    def alg_7(self):
        """
        If a moon is not lit from a side an open_star cannot be nearest.
        """
        # unlit sides
        incs_dic = Model.unlit_incs_p_dic
        self.hint = []
        for moon_key in self.moons:
            guess = self.datum_dic[moon_key].guess
            incs = incs_dic[guess]
            for dinc, inc in enumerate(incs):
                ret = self.track_7(moon_key, inc)
                if ret:
                    self.hint.append([ret, Sgg.NOT_STAR_OPEN])

    def facing_lit_moons(self, key_in, inc):
        """
        If a strip contains 2 lit moons and the lit sides face one another
        there must be a star_open between them.

        We track l to r and down so the following pairs matter.
        d b,c,h and d b,e,i
        e b,c,h and cdg
        h b,c,f
        i c,d,g
        """
        mates_lr_dic = {'d': ['b', 'c', 'f'],
                        'e': ['b', 'c', 'f'],
                        'h': ['b', 'c', 'f'],
                        'a': [],
                        'b': [],
                        'c': [],
                        'f': [],
                        'g': [],
                        'i': [],
                        }
        mates_ud_dic = {'b': ['c', 'd', 'g'],
                        'e': ['c', 'd', 'g'],
                        'i': ['c', 'd', 'g'],
                        'a': [],
                        'c': [],
                        'd': [],
                        'f': [],
                        'g': [],
                        'h': [],
                        }
        ret = False
        across = False
        if inc[0] != 0:
            across = True
            mates_dic = mates_lr_dic
        else:
            mates_dic = mates_ud_dic
        key = tuple(key_in)
        if across:
            y = key_in[1]
            key_0 = ()
            for x in range(Sgg.grid_width):
                key = (x, y)
                datum = self.datum_dic[key]
                if datum.is_moon():
                    if not key_0:
                        # first moon this strip
                        mates = mates_dic[datum.answer]
                        if mates:
                            key_0 = tuple(key)
                    else:
                        # another moon
                        if datum.answer in mates:
                            # lit moons facing one another
                            ret = True
                        else:
                            # facing wrong way. May be another pair.
                            mates = mates_dic[datum.answer]
                            if mates:
                                key_0 = tuple(key)
                            else:
                                key_0 = ()
        else:
            x = key_in[0]
            key_0 = ()
            for y in range(Sgg.grid_width):
                key = (x, y)
                datum = self.datum_dic[key]
                if datum.is_moon():
                    if not key_0:
                        # first moon this strip
                        mates = mates_dic[datum.answer]
                        if mates:
                            key_0 = tuple(key)
                    else:
                        # another moon
                        if datum.answer in mates:
                            # lit moons facing one another
                            ret = True
                        else:
                            # facing wrong way. May be another pair.
                            mates = mates_dic[datum.answer]
                            if mates:
                                key_0 = tuple(key)
                            else:
                                key_0 = ()

        return ret

    def alg_10(self):
        """
        Get single star_opens and cloud_opens
        Does not work with solver, but never needed for current puzzles,
        however these singles can occur when solving by hand.
        """
        singles = []
        single_stars = []
        single_clouds = []
        for swap in range(2):
            for j in range(Sgg.grid_width):
                single_star = ()
                for i in range(Sgg.grid_width):
                    key = Model.swap_key(i, j, swap)
                    datum = self.datum_dic[key]
                    if datum.unset_star_open_is_open():
                        if not single_star:
                            single_star = tuple(key)
                        else:
                            single_star = ()
                            break
                single_cloud = ()
                for i in range(Sgg.grid_width):
                    key = Model.swap_key(i, j, swap)
                    datum = self.datum_dic[key]
                    if datum.unset_cloud_open_is_open():
                        if not single_cloud:
                            single_cloud = tuple(key)
                        else:
                            single_cloud = ()
                            break
                if single_star and single_star not in single_stars:
                    single_stars.append(single_star)
                if single_cloud and single_cloud not in single_clouds:
                    single_clouds.append(single_cloud)
        for star in single_stars:
            self.hint.append([star, Sgg.GUESS_IS_STAR])
        for cloud in single_clouds:
            self.hint.append([cloud, Sgg.GUESS_IS_CLOUD])

    def get_single_stars(self):
        """
        Check rows and columns to find keys of single star_opens.
        Note that if they differ there's a strip which can have
        a star_open deleted. BUT we only run this after the function
        which deletes orthogonal star_opens, so it should not occur.
        """

        singles = []
        for y in range(Sgg.grid_width):
            single = ()
            for x in range(Sgg.grid_width):
                key = (x, y)
                datum = self.datum_dic[key]
                if datum.star_is_open():
                    if not single:
                        single = tuple(key)
                    else:
                        single = ()
                        break
            if single:
                singles.append(single)
        return singles

    def track_8(self, key_in, inc):
        """
        track away from key_in in one direction
        to find the first open_star.
        If one is found there must be something blocking.
        if that something is a cloud all other clouds in
        the strip can be deleted.
        """
        lit_dic = {
            (-1, 0): ['d', 'e', 'h'],
            (1, 0): ['b', 'c', 'f'],
            (0, -1): ['b', 'e', 'i'],
            (0, 1): ['c', 'd', 'g']
            }

        lit_syms = lit_dic[inc]
        ret = False
        if inc[0] == 0:
            key_s = (key_in[0], 0)
        elif inc[1] == 0:
            key_s = (0, key_in[1])
        clouds = []
        key = tuple(key_in)
        while True:
            key = Model.add_keys(key, inc)
            if not Model.in_range(key):
                break
            if self.datum_dic[key].cloud_is_open():
                clouds.append(key)

            if self.datum_dic[key].is_moon():
                # is it lit?
                if self.datum_dic[key].answer in lit_syms:
                    for key in clouds:
                        self.hint.append([key, Sgg.NOT_CLOUD_OPEN])
                        ret = True
                else:
                    key = key_s
                    while True:
                        if not Model.in_range(key):
                            break
                        if self.datum_dic[key].cloud_is_open():
                            if key not in clouds:
                                self.hint.append([key, Sgg.NOT_CLOUD_OPEN])
                                ret = True
                        key = Model.add_keys(key, inc)
                break
        return ret

    def alg_8(self):
        """
        If a moon is not lit from a side the nearest object cannot
        be a star. There must be another moon or a cloud between
        its unlit side and the nearest star. If it is a cloud all other
        clouds can be deleted. Find such moons and then track away.
        If a cloud is the first object found all other clouds in the strip
        can be deleted.
        """
        incs = Model.incs
        self.hint = []
        singles = self.get_single_stars()
        for key in singles:
            datum = self.datum_dic[key]
            for inc in incs:
                self.track_8(key, inc)

    def alg_9(self):
        """
        If only 1 star_open or star in a strip
        Then, if open cloud between star and an unlit face, all
        other clouds can be deleted.
        """
        unlits_0 = [['a', 'c', 'd', 'f', 'g', 'h'],
                    ['a', 'b', 'e', 'f', 'h', 'i']]
        unlits_1 = [['a', 'b', 'c', 'f', 'g', 'i'],
                    ['a', 'd', 'e', 'g', 'h', 'i']]
        unlits = [unlits_0, unlits_1]
        self.hint = []
        for swap in range(2):
            # do strips
            for i in range(Sgg.grid_width):
                star_count = 0
                clouds = []
                last_star = ()
                for j in range(Sgg.grid_width):
                    key = Model.swap_key(i, j, swap)
                    datum = self.datum_dic[key]
                    if datum.star_is_open():
                        star_count += 1
                        last_star = tuple(key)
                    if datum.cloud_is_open():
                        clouds.append(key)

                done = False
                if star_count == 1 and len(clouds) > 1:
                    # this i has a single star and multiple clouds
                    # track  away from star.
                    # if cloud followed by unlit moon all other
                    # clouds can be deleted
                    inc = Model.swap_key(0, -1, swap)
                    good_clouds = []
                    key = last_star
                    while True:
                        key = Model.add_keys(key, inc)
                        if not Model.in_range(key):
                            break
                        datum = self.datum_dic[key]
                        if datum.cloud_is_open():
                            good_clouds.append(key)
                        elif datum.is_moon():
                            if datum.answer in unlits[swap][0]:
                                # all clouds not in good_clouds can be deleted
                                for cloud in clouds:
                                    if cloud not in good_clouds:
                                        self.hint.append([cloud,
                                                          Sgg.NOT_CLOUD_OPEN])
                                        done = True
                            else:
                                break
                    if not done:
                        inc = Model.swap_key(0, 1, swap)
                        good_clouds = []
                        key = last_star
                        while True:
                            key = Model.add_keys(key, inc)
                            if not Model.in_range(key):
                                break
                            datum = self.datum_dic[key]
                            if datum.cloud_is_open():
                                good_clouds.append(key)
                            elif datum.is_moon():
                                if datum.answer in unlits[swap][1]:
                                    # all clouds not in good_clouds
                                    # can be deleted
                                    for cloud in clouds:
                                        if cloud not in good_clouds:
                                            self.hint.append([cloud,
                                                              Sgg.NOT_CLOUD_OPEN])
                                            done = True
                                else:
                                    break

    def do_hint(self):
        self.check_errors()
        if self.status:
            self.apply_algorithms()

    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 apply_hints(self):
        for hint in self.hint:
            key = hint[0]
            datum = self.datum_dic[key]
            if not datum.guess:
                if hint[1] == Sgg.NOT_CLOUD_OPEN:
                    datum.set_cloud_open(False)
                elif hint[1] == Sgg.NOT_STAR_OPEN:
                    datum.set_star_open(False)
                elif hint[1] == Sgg.GUESS_IS_STAR:
                    datum.set_guess(Sgg.STAR)
                elif hint[1] == Sgg.GUESS_IS_CLOUD:
                    datum.set_guess(Sgg.CLOUD)
                self.alg_counts[self.algorithm] += 1
                Sgg.alg_counts[self.algorithm] += 1

    def solve_puzzle(self):
        progress = True
        while progress and not self.is_finished():
            progress = False
            self.apply_algorithms()
            if self.hint:
                progress = True
                self.apply_hints()
                self.check_errors()
                if not self.status:
                    break
                self.check_solver_finished()

    def check_solver_finished(self):
        self.finished = True
        for y in range(Sgg.grid_width):
            stars = 0
            clouds = 0
            for x in range(Sgg.grid_width):
                datum = self.datum_dic[(x, y)]
                if datum.star_is_open():
                    stars += 1
                elif datum.cloud_is_open():
                    clouds += 1
            if stars != 1 or clouds != 1:
                self.finished = False
                break

    def check_finished(self):
        #"""
        self.finished = True
        for key, datum in list(self.datum_dic.items()):
            if datum.answer in [Sgg.STAR, Sgg.CLOUD]:
                if datum.guess != datum.answer:
                    self.finished = False

    def print_grid(self):
        for y in range(Sgg.grid_width):
            line = ""
            for x in range(Sgg.grid_width):
                datum = self.datum_dic[(x, y)]
                out = '.'
                if datum.guess:
                    out = datum.guess
                """
                if datum.answer == Sgg.STAR:
                    out = '*'
                if datum.answer == Sgg.STAR:
                    out = '$'
                elif datum.is_moon():
                    out = '#'
                """
                line += out
            print(line)


class Generate():
    """
    Generate grids.
    1. add 1 star and 1 cloud to each row and column.
    2. add moons which are consistent with the current lighting.
    3. send to solver
    """

    def __init__(self, model):
        random.seed(0)
        self.model = model
        self.x_stars = {}
        self.y_stars = {}
        while True:
            self.grid_dic = {}
            self.grid_codes = {}
            self.do_it()

    def add_object(self):
        empty = []
        for key, item in list(self.grid_dic.items()):
            if item == Sgg.EMPTY:
                empty.append(key)
        shuffle(empty)
        for key in empty:
            # look l, r, u, d to select correct object
            self.grid_dic[key] = '('
            break

    def add_objects(self, add):
        ret = True
        rows = list(range(Sgg.grid_width))
        cols = list(range(Sgg.grid_width))
        shuffle(rows)
        shuffle(cols)

        for i in range(Sgg.grid_width):
            x = cols[i]
            y = rows[i]
            key = (x, y)
            if self.grid_dic[key] == Sgg.EMPTY:
                self.grid_dic[key] = add
            else:
                ret = False
                break
        return ret

    def init(self):
        self.grid_dic = {}
        for y in range(Sgg.grid_width):
            for x in range(Sgg.grid_width):
                key = (x, y)
                self.grid_dic[key] = Sgg.EMPTY

    def add_stars_and_clouds(self):
        """
        1. use init to make an empty g rid
        2.
        """
        while True:
            self.init()
            ret = self.add_objects(Sgg.STAR)
            ret = self.add_objects(Sgg.CLOUD)
            if ret:
                break

    def get_keys(self):
        keys = []
        for y in range(Sgg.grid_width):
            for x in range(Sgg.grid_width):
                key = (x, y)
                if self.grid_dic[key] == Sgg.EMPTY:
                    keys.append(key)
        return keys

    def encode_puzzle(self):
        puzzle = str(Sgg.grid_width)
        for y in range(Sgg.grid_width):
            for x in range(Sgg.grid_width):
                key = (x, y)
                puzzle += (self.grid_dic[key])
        return puzzle

    def get_codes(self):
        """
        traverse the grid:
        0,j to width,j
        width,j to 0,j
        for all j
        j,0 to j,width
        j,width to j,0
        for all j

        key_in is 0,j with inc = 1,0  (0,j to width,j) or
                  j,width with inc = -1,0 (j,width to j,0) or
                  0,j with inc = 0,1 (0,j to 0,width) or
                  j,width with inc = 0,-1 (j,width to j,0)
        """
        #          r       l        d       u
        #          0       1        2       3   code indexes
        incs = [(1, 0), (-1, 0), (0, 1), (0, -1)]
        moon_dic = {(1, 1, 1, 1): 'a',
                    (0, 1, 1, 0): 'b',
                    (0, 1, 0, 1): 'c',
                    (1, 0, 0, 1): 'd',
                    (1, 0, 1, 0): 'e',
                    (0, 1, 1, 1): 'f',
                    (1, 1, 0, 1): 'g',
                    (1, 0, 1, 1): 'h',
                    (1, 1, 1, 0): 'i',
                    (0, 0, 0, 0): 'k'}

        codes = {}
        for i in range(Sgg.grid_width):
            for j in range(Sgg.grid_width):
                tmp = []
                for k in range(4):
                    tmp.append(None)
                    codes[i, j] = tmp

        for i, inc in enumerate(incs[0:2]):
            # do a width for each direction
            y0 = 0
            if inc[0] < 0:
                x0 = Sgg.grid_width - 1
            else:
                x0 = 0
            x = x0
            y = y0
            for j in range(Sgg.grid_width):
                x = x0
                key = tuple([x, y])
                lit = False
                code = 1
                for k in range(Sgg.grid_width):
                    sym = self.grid_dic[key]
                    if sym == Sgg.STAR:
                        lit = True
                    elif sym == Sgg.EMPTY and lit:
                        code = 0
                    else:
                        lit = False
                        code = 1
                    codes_item = codes[key]
                    codes_item[i] = code
                    key = Model.add_keys(key, inc)
                y += 1

        for i, inc in enumerate(incs[2:]):
            # do a column for each direction
            if inc[1] < 0:
                y0 = Sgg.grid_width - 1
            else:
                y0 = 0
            x0 = 0
            x = x0
            y = y0
            for j in range(Sgg.grid_width):
                y = y0
                key = tuple([x, y])
                lit = False
                code = 1
                for k in range(Sgg.grid_width):
                    sym = self.grid_dic[key]
                    if sym == Sgg.STAR:
                        lit = True
                    elif sym == Sgg.EMPTY and lit:
                        code = 0
                    else:
                        lit = False
                        code = 1
                    codes_item = codes[key]
                    codes_item[i + 2] = code
                    key = Model.add_keys(key, inc)
                x += 1
        self.grid_codes = {}
        for key, item in list(codes.items()):
            self.grid_codes[key] = moon_dic[tuple(item)]

    def get_stars(self):
        """
        Get keys of stars.
        x_stars = stars indexed on x coord
        y_stars = stars indexed on y coord
        """
        self.x_stars = {}
        self.y_stars = {}
        for key, item in list(self.grid_dic.items()):
            if item == Sgg.STAR:
                self.x_stars[key[0]] = key
                self.y_stars[key[1]] = key

    def sum_code(self, code_in, inc):
        dic_incs = {(1, 0): 0, (-1, 0): 1, (0, -1): 3, (0, 1): 2}
        dic = {
            'a': ['a', 'a', 'a', 'a'],
            'b': ['i', 'b', 'b', 'f'],
            'c': ['g', 'c', 'f', 'c'],
            'd': ['d', 'g', 'h', 'd'],
            'e': ['e', 'i', 'e', 'h'],
            'f': ['a', 'f', 'f', 'f'],
            'g': ['g', 'g', 'a', 'g'],
            'h': ['h', 'a', 'h', 'h'],
            'i': ['i', 'i', 'i', 'a'],
            'l': ['l', 'l', 'l', 'l'],
        }
        codes = dic[code_in]
        return codes[dic_incs[inc]]

    def track(self, key_in, inc_in):
        """
        We have just placed a moon at key_in and want to find
        which cells may be affected.
        Need to scan in both directions.
        inc = 0 l and r
        inc = 1 u and d
        """
        if inc_in:
            # horizontal
            star_key = self.y_stars[key_in[1]]
            if star_key[0] < key_in[0]:
                inc = (1, 0)
                # until we meet block or new moon
                # if the latter we update those to its right
                # upto the next block or edge
                # otherwise theres nothing to do
            else:
                inc = (-1, 0)
                # until we meet block or new moon
                # if the latter we update those to its left
                # upto the next block or edge
                # otherwise theres nothing to do
        else:
            star_key = self.x_stars[key_in[0]]
            if star_key[1] > key_in[1]:
                inc = (0, -1)
            else:
                inc = (0, 1)
        update = False
        key = tuple(star_key)
        while True:
            key = Model.add_keys(key, inc)
            if not Model.in_range(key):
                break
            if update:
                if self.grid_dic[key] == Sgg.EMPTY:
                    code = self.grid_codes[key]
                    new_code = self.sum_code(code, inc)
                    self.grid_codes[key] = new_code
                else:
                    # a block, so stop
                    break
            else:
                if key == key_in:
                    # start updating
                    update = True
                elif self.grid_dic[key] != Sgg.EMPTY:
                    break
                else:
                    # if we get here we have a key which
                    # must be set so it cannot block the new moon
                    # so we remove it from the list of keys.
                    if key in self.keys:
                        self.keys.remove(key)

    def do_it(self):
        """
        1. use init to make an empty grid
        2. use add_stars_and_clouds to make an empty grid and
           one star and one cloud to each row and column
        3. incrementally add moons and try to solve
        """
        min_done = int(Sgg.grid_width / 2)
        max_done = Sgg.grid_width + int(Sgg.grid_width / 2)
        Sgg.grid_width = 6
        max_puzzles = 200
        puzzle_count = 0
        puzzles = ""
        while puzzle_count < max_puzzles:
            tries = 0
            got_one = False
            while True:
                tries += 1
                self.model.grid_dic = {}
                self.add_stars_and_clouds()
                self.get_stars()
                self.get_codes()
                self.keys = self.get_keys()
                shuffle(self.keys)
                count = 0
                # choose a key
                while self.keys:
                    key = self.keys.pop()
                    code = self.grid_codes[key]
                    self.grid_dic[key] = code
                    self.track(key, 0)
                    self.track(key, 1)
                    count += 1
                    #exit(1)
                    if count > min_done:
                        puzzle = self.encode_puzzle()
                        self.model.make_grid(puzzle)
                        self.model.solve_puzzle()
                        if self.model.is_finished():
                            print('@@@@@@@@@@@@@@@@@@@', puzzle_count)
                            self.model.print_grid()
                            print(puzzle)
                            self.print_it()
                            puzzles += puzzle
                            puzzle_count += 1
                            got_one = True
                            #exit(7)
                            break

                    if count >= max_done:
                        break
                if got_one:
                    break

        print(puzzles)
        exit(1)

    def print_it(self):
        print()
        for y in range(Sgg.grid_width):
            line = ""
            for x in range(Sgg.grid_width):
                key = (x, y)
                sym = self.grid_dic[key]
                if sym == Sgg.EMPTY:
                    sym = '.'
                line += sym
            print(line)


class ModelDataX():

    def __init__(self):
        self.puzzles = ''
        self.puzzle_count = None
        self.get_puzzles()

    def get_puzzles(self):
        puzzles = []
        i = 0
        while i < len(self.puzzles):
            if self.puzzles[i + 1].isdigit():
                start = i + 2
                size = int(self.puzzles[i:i + 2])
            else:
                start = i + 1
                size = int(self.puzzles[i])
            #Sgg.grid_width = size
            j = start + size * size
            puzzle = self.puzzles[i:j]
            i = j
            puzzles.append(puzzle)
        self.puzzles = puzzles
        self.puzzle_count = len(self.puzzles)

    def get_puzzle(self, puzzle_number):
        return self.puzzles[puzzle_number]


class Controller():
    """
    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.active = False
        self.hint_requested = False
        self.error_detected = False
        self.view = View(root, self.settings)
        puzzle_number = None
        self.start_game(puzzle_number)

    def solve_puzzle(self):
        progress = True
        while progress and not self.model.is_finished():
            progress = False
            self.model.apply_algorithms()
            if self.model.hint:
                progress = True
                self.apply_hint()
                self.model.check_errors()
                if not self.model.status:
                    break
                self.check_finished()

    def test(self):
        done = 0
        bad = []
        #new = []
        #scores = []

        for puzzle_number in range(self.model_data.puzzle_count):
            print(puzzle_number)
            self.model = Model(self.model_data)
            self.model.get_puzzle(puzzle_number)
            # diff = self.model.get_difficulty()
            # scores.append([diff, puzzle_number])
            self.model.solve_puzzle()
            if not self.model.is_finished():
                bad.append(puzzle_number)
                print()
                self.model.print_grid()
                print()
            # else:
            #    self.model.get_difficulty()
                #print puzzle_number, ' score', self.model.difficulty
                #puzzle = self.model_data.get_puzzle(puzzle_number)
                #puzzle[0] = self.model.difficulty
                #new.append(puzzle)
            # self.model.print_grid()
            done += 1

        """
        scores = sorted(scores)
        #print(scores)
        puzzles = ""
        for score in scores:
            puzzle_number = score[1]
            self.model = Model(self.model_data)
            puzzle = self.model_data.get_puzzle(puzzle_number)
            puzzles += puzzle
            #print(puzzle_number, puzzle)
        print(puzzles)
        """
        print('done', done, 'bad', bad, len(bad))
        print('Algorithm counts ', self.model.alg_counts)
        print('Alg counts ', Sgg.alg_counts)
        exit(1)

    def start_game(self, puzzle_number):
        #self.test()
        #self.model = Model(self.model_data)
        #Generate(self.model)
        self.model = Model(self.model_data)
        self.model.get_puzzle(puzzle_number)
        self.view.clock_init()
        self.view.clock_restart()
        self.view.make_board()
        self.view.board.model = self.model
        self.view.draw_board()
        self.view.board.init_widgets(self.grid_event, self.button_event)
        self.ready_for_hint()
        self.add_callbacks()
        self.view.entry_box.clear()
        self.view.entry_box.write_box(str(self.model.puzzle_number))
        self.view.update_score_box(self.model.difficulty)
        self.model.set_hint_status(Sgg.GIVE_HINT)
        self.view.game_on()
        self.flasher = Flasher(self.view.clock.box.label,
                               self.master, self.settings)
        self.active = True

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

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

    def service_toolbar(self, button_index, mouse_index):
        PzlG.logger.logger.info("service_toolbar " +
                                str(button_index) +
                                ' ' +
                                str(mouse_index))
        if mouse_index == 2:
            alg_index = self.model.algorithm
            self.view.do_tooltip(button_index, alg_index)
        elif mouse_index == 3:
            if 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()
            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 grid_event(self, event, key):
        mouse_dic = {1: Sgg.STAR, 2: Sgg.DELETE, 3: Sgg.CLOUD}
        if self.active:
            if event.type == "7":
                # 'enter'
                job = Sgg.HIGHLIGHT
                self.view.board.update_board([key, job])
            elif event.type == "8":
                # 'leave'
                job = Sgg.UNHIGHLIGHT
                self.view.board.update_board([key, job])
            elif event.type == "5":
                mouse_index = event.num
                PzlG.logger.logger.info("grid_event " +
                                        str(key))
                self.update_model_gui(key, mouse_dic[mouse_index])
                if self.error_detected:
                    self.check_errors_1()
                else:
                    self.check_finished()
                self.model.print_grid()

    def button_event(self, key, button_index, mouse_index):
        if self.active:
            """
            """
            if mouse_index == 1:
                status = True
            elif mouse_index == 3:
                status = False
            if button_index == 0:
                updates = self.model.set_star_open(key, status)
            elif button_index == 1:
                updates = self.model.set_cloud_open(key, status)
            self.apply_updates(updates)
            # check hint pending
            if self.model.hint:
                hkey = self.model.hint[0][0]
                self.view.board.unhighlight_square(hkey)
                self.model.hint_status = Sgg.GIVE_HINT
                self.view.hint_off()
            if self.error_detected:
                self.check_errors_1()
            else:
                self.check_finished()

    def apply_hint(self):
        updates = []
        hint = self.model.hint[0]
        key = hint[0]
        datum = self.model.datum_dic[key]
        if not datum.guess:
            if hint[1] == Sgg.NOT_CLOUD_OPEN:
                updates = self.model.set_cloud_open(key, False)
            elif hint[1] == Sgg.NOT_STAR_OPEN:
                updates = self.model.set_star_open(key, False)
            elif hint[1] == Sgg.GUESS_IS_CLOUD:
                updates += self.model.set_guess(key, Sgg.CLOUD)
                updates += self.model.set_cloud_opens(key, False, [], [])
            elif hint[1] == Sgg.GUESS_IS_STAR:
                updates += self.model.set_guess(key, Sgg.STAR)
                updates += self.model.set_star_opens(key, False, [], [])
            self.apply_updates(updates)
            self.model.alg_counts[self.model.algorithm] += 1
            Sgg.alg_counts[self.model.algorithm] += 1
            self.model.algorithm = None

    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.model.hint_status = Sgg.GIVE_HINT
                self.view.hint_off()
                self.view.thumbs_up()
                self.error_detected = False
            self.view.clock.set_show(True)

    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.model.hint_status == Sgg.GIVE_HINT:
            self.model.check_errors()
            if self.model.errors:
                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()
                    key = self.model.hint[0][0]
                    self.view.board.highlight_square(key)
                    self.view.update_clock_box(self.model.algorithm)
                    self.view.clock.set_show(False)
                    self.model.hint_status = Sgg.APPLY_HINT
        else:
            key = self.model.hint[0][0]
            text = 'Hint: ' +\
                str(key[0]) + ',' + str(key[1])
            PzlG.logger.logger.info(text)
            self.apply_hint()
            self.view.board.unhighlight_square(key)
            self.ready_for_hint()
            self.check_finished()

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

    def show_solution(self):
        self.model.set_solution()
        self.view.board.close_buttons()
        self.view.board.draw_shapes()

    def check_errors(self):
        if self.error_detected:
            # may have deleted edits
            if self.model.errors:
                # we know there's an error already, so now we correct it
                self.uncolour_errors()
                updates = self.model.correct_errors()
                self.apply_updates(updates)
            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_errors_1(self):
        self.model.check_errors()
        if not self.model.status:
            self.view.thumbs_down()
            self.error_detected = True
            self.colour_errors()
        else:
            self.view.thumbs_up()

    def update_model_gui(self, key, object_code):
        """
        A guess has been set or unset.
        If set, set_guess, unset open status for those orthogonal
        and update board accordingly.
        If unset, unset_guess, set open status for those orthogonal
        and update board accordingly.
        """
        updates = []
        if object_code == Sgg.STAR:
            updates += self.model.set_guess(key, Sgg.STAR)
            updates += self.model.set_star_opens(key, False, [], [])
        elif object_code == Sgg.CLOUD:
            updates += self.model.set_guess(key, Sgg.CLOUD)
            updates += self.model.set_cloud_opens(key, False, [], [])
        elif object_code == Sgg.DELETE:
            if self.model.datum_dic[key].is_set():
                updates += self.model.unset_guess(key)
        self.apply_updates(updates)
        self.view.board.update_idletasks()

    def apply_updates(self, updates):
        """
        Update the GUI with changes from Model
        """
        for update in updates:
            self.view.board.update_board(update)

    def colour_errors(self):
        """
        Colour the errors
        """
        updates = self.model.errors
        self.apply_updates(updates)

    def uncolour_errors(self):
        """
        Uncolour coloured squares
        """
        for error in self.model.errors:
            error[1] += 3
        updates = self.model.errors
        self.apply_updates(updates)

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

    def set_colour(self, choice):
        try:
            bg_colour = self.settings.dic[choice]
            result = tkinter.colorchooser.askcolor(bg_colour)
            if result[1] is not None:
                self.settings.dic[choice] = result[1]
                self.start_game(self.model.puzzle_number)
        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)
            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")
