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

# Copyright (C) 2024 Rod Engerstad

import string
import random
from random import shuffle
import copy
import tkinter as tk
import tkinter.font

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 Toolbar
from pzl_lib import Menu
from pzl_lib import ClockBox
from pzl_lib import ViewB
from phrazy_data import ModelData


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

    all_qwerty = ['Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', '/', 'A',
                  'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', '/', 'Z',
                  'X', 'C', 'V', 'B', 'N', 'M', 'Enter', 'Delete']
    user_keyboard = 'QWERTYUIOP/ASDFGHJKL/ZXCVBNM'
    phrase_length = None
    GIVE_HINT = 0
    APPLY_HINT = 1


class Settings(SettingsB):

    def __init__(self):
        SettingsB.__init__(self)
        self.dic = {}
        self.dark_grey = '#6e6e6e'
        self.key_green = '#6AAA64'
        self.key_yellow = '#C9B458'
        self.key_dark_grey = '#787C7E'
        self.light_grey = '#E9EBED'
        self.yellow = '#ffff00'
        self.dic['font_size'] = '1'
        self.dic['max_tries'] = '7'
        self.dic['game_list'] = 'Idioms'
        self.menu_items = []
        self.preferences = Preferences(self.dic)
        self.preferences.load()
        self.load_menus()
        self.count = len(self.menu_items)
        self.max_tries = int(self.dic['max_tries'])
        self.set_fonts()
        # fiddle the counter font
        self.counter_font = tkinter.font.nametofont('TkFixedFont')
        self.counter_font.configure(size=self.counter_font_size)
        self.do_scaling()
        self.original_entry_box_width = 1
        self.entry_box_width = self.original_entry_box_width
        self.entry_box_highthick = 1
        self.entry_box_bg = 'red'
        self.entry_box_fg = 'black'
        self.square_size = 30
        self.make_help()
        self.make_tooltip()

    def make_help(self):
        self.help_text = _("This game is one of very many versions of Phrazle, which in turn is based on Wordle. By entering letters players try to guess a phrase. The program responds by colouring the entered letters: yellow indicates a correct letter in the right word but wrong position, green a correct letter in the correct word in the right position, purple is a correct letter, not already coloured yellow or green, but in the wrong word, and dark grey for the rest. Letters can be entered via the onscreen keyboard or the keyboard. The program contains 3 separate lists of phrases: over 1000 common English idioms, over 700 highly rated film titles, just under 900 of the Guardians 'must read' novels. Players can select which list to use. They can also choose the number of attempts allowed (4 to 20) and the font size.")

    def do_scaling(self):
        inc = 0.1
        mult = int(self.dic['font_size'])
        factor = 0.8 + mult * inc
        size = self.counter_font_size
        self.counter_font.configure(size=int(size * factor))
        size = self.value_font_size
        self.value_font.configure(size=int(size * factor))
        size = self.candidate_font_size
        self.candidate_font.configure(size=int(size * factor))
        size = self.letter_font_size
        self.letter_font.configure(size=int(size * factor))
        size = self.letter_surface_font_size
        self.letter_surface_font.configure(size=int(size * factor))
        size = self.code_font_size
        self.code_font.configure(size=int(size * factor))
        self.square_size = int(self.original_square_size * factor)
        self.candidate_height = 16
        self.candidate_height = int(self.candidate_height * factor)
        self.value_height = 24
        self.value_height = int(self.value_height * factor)
        self.candidate_minsize = 10
        self.candidate_minsize = int(self.candidate_minsize * factor)
        self.entry_box_width = int(self.original_entry_box_width * factor)
        self.wentry_box_width = int(self.original_wentry_box_width * factor)
        self.list_box_width = int(self.original_list_box_width * factor)

    def load_menus(self):
        self.menu_items.append(MenuItem(
            _('List'),
            [
                'Idioms',
                'Films',
                'Novels',
            ],
            '',
            [
                'Idioms',
                'Films',
                'Novels',
            ],
            'game_list'
        ))
        self.menu_items.append(MenuItem(
            _('max_tries'),
            ['4',
             '5',
             '6',
             '7',
             '8',
             '9',
             '10',
             '11',
             '12',
             '13',
             '14',
             '15',
             '16',
             '17',
             '18',
             '19',
             '20'],
            '',
            ['4',
             '5',
             '6',
             '7',
             '8',
             '9',
             '10',
             '11',
             '12',
             '13',
             '14',
             '15',
             '16',
             '17',
             '18',
             '19',
             '20'],
            'max_tries'
            ))
        self.menu_items.append(MenuItem(
            _('Font Size'),
            ['1',
             '2',
             '3',
             '4',
             '5',
             '6',
             '7',
             '8',
             '9',
             '10',
             '11',
             '12',
             '13'],
            '',
            ['1',
             '2',
             '3',
             '4',
             '5',
             '6',
             '7',
             '8',
             '9',
             '10',
             '11',
             '12',
             '13'],
            'font_size'
            ))

        self.menu_items.append(MenuItem(
            _('Save Settings'),
            '',
            '',
            '',
            'save_settings'
        ))
        self.menu_items.append(MenuItem(
            _('Help'),
            '',
            '',
            '',
            'help'
        ))
        self.menu_items.append(MenuItem(
            _('About'),
            '',
            '',
            '',
            'about'
        ))

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

        self.menu_items.append(MenuItem(
            _('max_tries'),
            ['4',
             '5',
             '6',
             '7',
             '8',
             '9',
             '10',
             '11',
             '12',
             '13',
             '14',
             '15',
             '16',
             '17',
             '18',
             '19',
             '20'],
            '',
            ['4',
             '5',
             '6',
             '7',
             '8',
             '9',
             '10',
             '11',
             '12',
             '13',
             '14',
             '15',
             '16',
             '17',
             '18',
             '19',
             '20'],
            'max_tries'
            ))
        self.menu_items.append(MenuItem(
            _('Font Size'),
            ['1',
             '2',
             '3',
             '4',
             '5',
             '6',
             '7',
             '8',
             '9',
             '10',
             '11',
             '12',
             '13'],
            '',
            ['1',
             '2',
             '3',
             '4',
             '5',
             '6',
             '7',
             '8',
             '9',
             '10',
             '11',
             '12',
             '13'],
            'font_size'
            ))

        self.menu_items.append(MenuItem(
            _('Save Settings'),
            '',
            '',
            '',
            'save_settings'
        ))
        self.menu_items.append(MenuItem(
            _('Help'),
            '',
            '',
            '',
            'help'
        ))
        self.menu_items.append(MenuItem(
            _('About'),
            '',
            '',
            '',
            'about'
        ))

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


class TextSquare(tk.Label):
    """
    Make our own text button.
    """
    def __init__(self, parent, settings, datum):
        self.datum = datum
        self.settings = settings
        self.callback = None
        self.active = False
        font = self.settings.counter_font
        fg = 'black'
        bg = datum.colour
        text = datum.answer

        bd = 1
        padx = 3
        if not text:
            padx = 0
            text = ''
            bd = 0
        else:
            text = ' '

        width = 1
        height = 1
        relief = 'raised'
        highlightbackground = 'black'
        tk.Label.__init__(self,
                          parent,
                          bd=bd,
                          relief=relief,
                          highlightthickness=0,
                          highlightbackground=highlightbackground,
                          bg=bg,
                          fg=fg,
                          font=font,
                          width=width,
                          height=height,
                          padx=padx,
                          state=tk.NORMAL,
                          text=text)
        self.grid(row=0, column=0)
        self.grid_propagate(0)

    def my_destroy(self):
        self.destroy()

    def my_update(self):
        bg = self.datum.colour
        text = self.datum.guess
        self.configure(bg=bg, text=text)


class TryArea():
    """
    Surface to show the players attempts
    and provide keyboard entry.
    """
    def __init__(self, parent, settings, model, bg):
        self.settings = settings
        self.model = model
        self.cells = {}
        max_tries = self.settings.max_tries
        try_area_frame = tk.Frame(parent,
                                  relief='flat',
                                  bd=1, bg=bg)
        for try_row in range(max_tries):
            try_row_frame = tk.Frame(try_area_frame,
                                     relief='flat',
                                     bd=0, bg=bg)
            self.make_cells(try_row_frame, try_row)
            try_row_frame.grid(row=try_row, column=0)

        try_area_frame.grid(row=0, column=0)

    def make_cells(self, outer_frame, y):
        this_attempt = self.model.attempts[y]
        for x in range(Sgg.phrase_length):
            datum = this_attempt.datum[x]
            cell = TextSquare(outer_frame, self.settings, datum)
            cell.grid(row=y, column=x)
            self.cells[x, y] = cell

    def update_cell(self, my_key):
        cell = self.cells[my_key]
        cell.my_update()


class QwertyKey():
    """
    Key on qwerty keyboard.
    """
    def __init__(self, frame, text, font, key_index, callback):
        self.frame = frame
        self.label = None
        self.key_index = key_index
        self.callback = callback
        self.active = False
        self.bg = 'white'
        self.make_label(text, font)
        self.frame.bind("<ButtonRelease-1>", self.command)
        self.frame.bind("<ButtonRelease-3>", self.command)
        self.frame.bind("<Enter>", self.command)
        self.frame.bind("<Leave>", self.command)
        self.label.bind("<ButtonRelease-1>", self.command)
        self.label.bind("<ButtonRelease-3>", self.command)
        self.label.bind("<Enter>", self.command)
        self.label.bind("<Leave>", self.command)

    def make_label(self, text, font):
        relief = 'flat'
        fg = 'black'
        bd = 1
        highlightthickness = 0
        label = tk.Label(self.frame,
                         bd=bd,
                         bg=self.bg,
                         fg=fg,
                         relief=relief,
                         font=font,
                         text=text,
                         highlightthickness=highlightthickness)

        label.grid(row=0, column=0)
        self.label = label

    def command(self, event):
        if event.type == "7":
            self.active = True
            self.frame.configure(bg='black')
        elif event.type == "8":
            self.active = False
            self.frame.configure(bg=self.bg)
        elif event.type == "5" and self.active:
            self.callback(self.key_index)

    def colour_bg(self, bg):
        self.label.configure(bg=bg)


class Qwerty():
    """
    Surface to show letter use
    """
    def __init__(self, parent, settings, model, bg, callback):
        self.model = model
        alphabet, slash_posns = self.handle_user_keyboard()
        self.settings = settings
        self.callback = callback
        self.qkeys = []
        self.bg = bg
        self.cells = []
        self.key_index = 0
        qwerty_frame = tk.Frame(parent, relief='flat', bd=1, bg=bg)
        first = 0
        for i, last in enumerate(slash_posns):
            self.do_qwerty_row(alphabet[first:last], i, qwerty_frame)
            first = last

        commands = ['Enter', 'Delete']
        self.do_qwerty_row(commands, i + 1, qwerty_frame)
        Sgg.all_qwerty = list(alphabet)
        Sgg.all_qwerty += commands
        qwerty_frame.grid(row=2, column=0)

    def handle_user_keyboard(self):
        user_keyboard = self.model.user_keyboard
        slash_count = user_keyboard.count('/')
        slash_posns = []
        txt = ""
        if not slash_count:
            txt = user_keyboard[:]
            width = len(user_keyboard)
            rem = width % 3
            one_width = width // 3

            if rem == 0:
                slash_posns = [one_width + 1,
                               one_width + 2 + one_width,
                               width - 1]
            else:
                slash_posns = [one_width + 2,
                               one_width + 3 + one_width,
                               width]
        else:
            minus = 0
            i = 0
            while i < len(user_keyboard):
                if user_keyboard[i] == '/':
                    minus += 1
                    slash_posns.append(i + 1 - minus)
                else:
                    txt += user_keyboard[i]
                i += 1
            slash_posns.append(i - minus)
        return txt, slash_posns

    def do_qwerty_row(self, text, this_row, qwerty_frame):
        font = self.settings.value_font
        bg = self.bg

        row_frame = tk.Frame(
            qwerty_frame,
            highlightthickness=0,
            bg=bg,
        )
        for i, text_item in enumerate(text):

            frame = tk.Frame(
                row_frame,
                bd=2,
                highlightbackground=self.settings.grey,
                highlightthickness=1,
                bg=bg
                )

            qkey = QwertyKey(frame, text_item, font,
                             self.key_index, self.callback)
            self.qkeys.append(qkey)
            self.key_index += 1
            frame.grid(row=0, column=i)
        row_frame.grid(row=this_row, column=0)

    def update_qwerty(self, colours, qwerty_indexes):
        """
        Need to show the best colour.
        Best colour is best of the current and the incoming.
        NB incoming letters may appear more than once.
        Solution is to fill in the colours in rank order.
        """
        colour_rank = ['#787C7E', '#C9B458', '#6AAA64', '#800080']

        if colours[0] != self.settings.white:
            for rank in colour_rank:
                for i, colour in enumerate(colours):
                    if colour == rank:
                        j = qwerty_indexes[i]
                        self.qkeys[j].colour_bg(colour)

    def update_qwerty_key(self, qwerty_index):
        colour = '#787C7E'
        self.qkeys[qwerty_index].colour_bg(colour)


class UISurface(tk.Frame):
    """
    The complete surface for the game.
    """
    def __init__(self, parent, settings, model,
                 qwerty_callback):
        self.settings = settings
        self.model = model
        self.qwerty_surface = None
        self.try_surface = None
        self.try_cells = []
        self.qwerty_cells = []
        bg = self.settings.white

        tk.Frame.__init__(self, parent, relief='flat',
                          highlightthickness=0,
                          bd=0, bg=bg)

        self.try_frame = tk.Frame(self, parent)

        self.try_surface = TryArea(self.try_frame, settings,
                                   self.model,
                                   bg)
        self.try_cells = self.try_surface.cells
        self.try_frame.grid(row=0, column=0)

        self.qwerty_surface = Qwerty(self, settings, self.model,
                                     bg, qwerty_callback)
        self.qwerty_cells = self.qwerty_surface.cells
        self.grid(row=0, column=0)

    def my_destroy(self):
        self.destroy()

    def update_qwerty(self, colours, qwerty_indexes):
        self.qwerty_surface.update_qwerty(colours, qwerty_indexes)

    def update_qwerty_key(self, qwerty_index):
        self.qwerty_surface.update_qwerty_key(qwerty_index)

    def update_cell(self, my_key):
        self.try_surface.update_cell(my_key)


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

    def __init__(self, master, settings, model):
        ViewB.__init__(self, master, settings)
        self.model = model
        self.ui_surface = None
        self.image_dic = {}
        self.make_image_dic()
        self.qwerty_callback = None
        self.do_layout(self)

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

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

        self.clock_box = ClockBox(self, self.number_frame)
        self.clock_box.grid(row=0, column=2, sticky='NSEW')
        bg = self.settings.white
        self.top_frame = tk.Frame(self.outer_frame, relief='flat',
                                  bd=1, bg=bg)
        self.top_frame.grid(row=1, column=0)

    def make_ui_surface(self):
        if self.ui_surface is not None:
            self.ui_surface.my_destroy()
        self.ui_surface = UISurface(self.top_frame,
                                    self.settings, self.model,
                                    self.qwerty_callback)

    def add_callback(self, qwerty_callback):
        self.qwerty_callback = qwerty_callback

    def update_cell(self, my_key):
        self.ui_surface.try_surface.update_cell(my_key)

    def update_qwerty(self, colours, qwerty_indexes):
        self.ui_surface.update_qwerty(colours, qwerty_indexes)

    def update_qwerty_key(self, qwerty_index):
        self.ui_surface.update_qwerty_key(qwerty_index)


class Datum():
    """
    A data point in the phrase
    """
    def __init__(self, answer):
        self.answer = answer
        self.guess = ''
        self.word_ends = []
        self.colour = 'white'

    def set_colour(self, colour):
        self.colour = colour

    def __repr__(self):
        ret = 'Datum %s %s %s %s' % (self.answer, self.guess,
                                     self.colour, self.word_ends)
        return ret


class Attempt():
    """
    phrase is full length phrase, word_cat is the concatenated phrase
    len_cat = len(word_cat)
    A datum for each position in the phrase, with those for spaces being
    used to hold the positions of the previous word's start end end.
    len_phrase = length of phrase
    So the datum keep in step with the positions in the phrase and the widgets.
    But the users letters do not include spaces so we have to translate
    between those positions and the datum indexes.

    D == Datum, N == None
    A CATCH TWENTY TWO
    DNDDDDDNDDDDDDNDDD  size len_phrase

    word_mumber is the number of the word
    A CATCH TWENTY TWO
    0 11111 222222 333   size len_cat

    input_index is the count of the users letters
    A CATCH TWENTY TWO
    0 12345 678901 234  size len_cat

    word_number + input_index = widget_index
    A CATCH TWENTY TWO  size len_cat
    0 11111 222222 333  size len_cat
    0 12345 678901 234  size len_cat
    0 23456 890123 567

    """
    def __init__(self, phrase):
        self.phrase = phrase
        self.len_phrase = len(phrase)
        self.word_number = []
        self.words = []
        self.datum = []
        self.word_ends = []
        self.word_cat = ""
        self.len_cat = None
        self.parse_phrase()

    def parse_phrase(self):

        word_index = 0
        pos = 0
        this_word = ""
        word_end = [0]
        while pos < len(self.phrase):
            if self.phrase[pos] == ' ':
                self.datum.append(Datum(''))
                self.words.append(this_word)
                self.word_cat += this_word
                word_index += 1
                this_word = ""
                word_end.append(pos)
                self.word_ends.append(word_end)
                self.datum[-2].word_ends = word_end
                word_end = [pos + 1]
            else:
                letter = self.phrase[pos]
                self.datum.append(Datum(letter))
                this_word += letter
                self.word_number.append(word_index)
            pos += 1
        # do last word
        self.words.append(this_word)
        self.word_cat += this_word
        self.len_cat = len(self.word_cat)
        word_end.append(pos)
        self.datum[-1].word_ends = word_end
        self.word_ends.append(word_end)

    def __repr__(self):
        ret = ""
        for word in self.words:
            r = ' %s ' % (word)
            ret += r + '\n'
        return ret


class Model():
    """
    Check the user input, etc
    """
    def __init__(self, model_data, settings):
        self.settings = settings
        self.model_data = model_data
        self.user_keyboard = Sgg.user_keyboard
        self.word_dic = self.model_data.dic
        self.phrases = self.model_data.phrases
        self.attempts = []
        self.target = ''
        self.new_phrase = None
        self.qwerty_indexes = []
        self.input_index = 0
        self.try_index = -1
        self.max_try_index = self.settings.max_tries
        self.widget_key = None
        self.targets = []
        self.guesses = []
        self.datum_index = None
        self.len_cat = None
        self.this_attempt = None
        self.this_datum = None
        self.letter_index = 0
        self.finished = False
        self.success = False
        self.callback = None
        self.hint_bank = []
        self.hint = ''

    def new_game(self):
        self.finished = False
        self.success = False
        self.input_index = 0
        self.max_try_index = self.settings.max_tries
        self.try_index = -1
        self.letter_index = 0
        self.attempts = []
        self.qwerty_indexes = []
        self.hint_bank = []
        self.hint = ''
        self.get_phrase()

    def get_phrase(self):
        if self.settings.dic['game_list'] == 'Idioms':
            self.phrases = self.model_data.idioms
        elif self.settings.dic['game_list'] == 'Films':
            self.phrases = self.model_data.films
        elif self.settings.dic['game_list'] == 'Novels':
            self.phrases = self.model_data.novels
        word_index = random.randint(0, len(self.phrases)-1)
        self.target = self.phrases[word_index]
        Sgg.phrase_length = len(self.target)
        base_attempt = Attempt(self.target)
        self.len_cat = base_attempt.len_cat
        self.targets = base_attempt.words

        for i in range(self.max_try_index):
            self.attempts.append(copy.deepcopy(base_attempt))
        self.hint_bank = list('QWERTYUIOPASDFGHJKLZXCVBNM')
        for letter in self.target:
            if letter != ' ' and letter in self.hint_bank:
                self.hint_bank.remove(letter)
        self.next_attempt()

    def get_guesses(self):

        datums = self.this_attempt.datum
        guess = ""
        self.guesses = []
        for i, datum in enumerate(datums):
            if datum.answer:
                guess += datum.guess
                if datum.guess in self.hint_bank:
                    self.hint_bank.remove(datum.guess)
            else:
                self.guesses.append(guess)
                guess = ""
        self.guesses.append(guess)

    def letter_ok(self):
        ret = False
        if self.input_index < self.len_cat:
            ret = True
        return ret

    def return_ok(self):
        ret = False
        if self.input_index == self.len_cat:
            ret = True
        return ret

    def delete_ok(self):
        ret = False
        if self.input_index > 0:
            ret = True
        return ret

    def check_phrase(self):
        self.get_guesses()
        match, colours = self.check_phrase_1()
        indexes = list(self.qwerty_indexes)

        if match:
            self.finished = True
            self.success = True
            self.callback()
        elif self.try_index == self.max_try_index - 1:
            self.finished = True

        self.input_index = 0
        self.qwerty_indexes = []
        return colours, indexes

    def check_cat(self, colour_in):
        """
        """
        colours = []
        guess = "".join(self.guesses)
        target = "".join(self.targets)
        # count mismatch letter freqs
        counts = {}
        for t in target:
            if t in counts:
                counts[t] += 1
            else:
                counts[t] = 1

        for g, c in zip(guess, colour_in):
            if c in [self.settings.key_green, self.settings.key_yellow]:
                counts[g] -= 1

        for t, g, c in zip(target, guess, colour_in):
            if c == self.settings.key_dark_grey:
                if t != g and g in target and g in counts and counts[g] > 0:
                    colours.append('#800080')
                    counts[g] -= 1
                else:
                    colours.append(c)
            else:
                colours.append(c)
        return colours

    def check_phrase_1(self):
        """
        Cases:
        1. letter in current word in current position GREEN (G)
        2. letter in correct word, wrong position YELLOW (Y)
        3. letter in phrase but wrong word PURPLE (P)
        4. letter not in phrase GREY (B)

        Initially ignore widget indexes, then fill them in after
        """

        colours = []
        match = True
        for target, guess in zip(self.targets, self.guesses):
            if guess in self.word_dic:
                counts = {}
                for t, g in zip(target, guess):
                    if t != g:
                        if t in counts:
                            counts[t] += 1
                        else:
                            counts[t] = 1
                for t, g in zip(target, guess):
                    if t == g:
                        colours.append(self.settings.key_green)
                    elif g in target and g in counts and counts[g] > 0:
                        colours.append(self.settings.key_yellow)
                        counts[g] -= 1
                        match = False
                    else:
                        colours.append(self.settings.key_dark_grey)
                        match = False
            else:
                colours = [self.settings.white for x in
                           range(Sgg.phrase_length)]
                match = False
                break

        colours = self.check_cat(colours)
        return match, colours

    def next_attempt(self):
        if self.try_index == self.max_try_index - 1:
            self.try_index = -1
            self.letter_index = 0
            self.attempts = []
        else:
            self.input_index = 0
            self.try_index += 1
            self.this_attempt = self.attempts[self.try_index]

    def update_colours(self, colours):
        datums = self.this_attempt.datum
        minus = 0
        for i, datum in enumerate(datums):
            if datum.answer:
                datum.colour = colours[i - minus]
            else:
                minus += 1

    def set_datum_index(self):
        """
        Set the current datum
        """
        self.datum_index = self.this_attempt.word_number\
            [self.input_index] + self.input_index

    def is_word_end(self):
        ret = False
        datum = self.this_attempt.datum[self.datum_index]
        if datum.word_ends:
            ret = True
        return ret

    def word_ok(self):
        ret = True
        if self.is_word_end():
            datum = self.this_attempt.datum[self.datum_index]
            word_ends = datum.word_ends
            guess = ""
            for i in range(word_ends[0], word_ends[1]):
                guess += self.this_attempt.datum[i].guess
            if guess not in self.word_dic:
                ret = False
        return ret

    def colour_word(self, colour):
        datum = self.this_attempt.datum[self.datum_index]
        word_ends = datum.word_ends
        for i in range(word_ends[0], word_ends[1]):
            self.this_attempt.datum[i].colour = colour

    def add_letter(self, letter, key_index):

        self.qwerty_indexes.append(key_index)
        self.set_datum_index()
        self.this_datum = self.this_attempt.datum[self.datum_index]
        self.this_datum.guess = letter
        self.widget_key = self.datum_index, self.try_index
        self.input_index += 1

    def delete_letter(self):
        """
        Delete letter.
        """
        ret = False
        if self.input_index > 0:
            ret = True
            self.qwerty_indexes.pop()
            self.input_index -= 1
            self.set_datum_index()
            self.this_datum = self.this_attempt.datum[self.datum_index]
            self.this_datum.guess = ' '
            self.widget_key = self.datum_index, self.try_index
        return ret

    def is_finished(self):
        return self.finished

    def add_callback(self, callback):
        self.callback = callback

    def set_solution(self):
        for i in range(Sgg.phrase_length):
            self.this_attempt.datum[i].guess =\
                self.this_attempt.datum[i].answer
            self.this_attempt.datum[i].colour = 'yellow'

    def give_hint(self):
        self.hint = ''
        if self.hint_bank:
            shuffle(self.hint_bank)
            self.hint = self.hint_bank.pop()


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

    def __init__(self, root):
        self.master = root
        self.settings = Settings()
        self.model_data = ModelData()
        self.model = Model(self.model_data, self.settings)
        self.view = View(root, self.settings, self.model)
        self.view.bind("<Key>", self.keyboard_event)
        self.add_callbacks()
        self.flasher = None
        self.active = False
        self.bad_word = False
        self.hint_status = Sgg.GIVE_HINT
        self.start_game()

    def add_callbacks(self):
        self.view.add_menu_callback(self.service_menu)
        self.view.add_toolbar_callback(self.service_toolbar)
        self.view.add_callback(self.qwerty_event)
        self.model.add_callback(self.show_result)

    def start_game(self):
        self.model.new_game()
        self.bad_word = False
        self.view.clock_init()
        self.view.clock_restart()
        self.flasher = Flasher(self.view.clock.box.label,
                               self.master, self.settings)
        self.view.game_over()
        self.active = True
        self.view.make_ui_surface()
        self.hint_status = Sgg.GIVE_HINT
        self.view.game_on()

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

    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 == 'max_tries':
            self.set_max_tries()
        elif name == 'game_list':
            self.set_new_game()
        elif name == 'save_settings':
            self.settings.preferences.save()
        elif name == 'about':
            self.view.about()
        elif name == 'help':
            self.view.help()

    def give_hint(self):
        if self.hint_status == Sgg.GIVE_HINT:
            if not self.bad_word:
                self.model.give_hint()
                if self.model.hint:
                    self.view.hint_on()
                    self.hint_status = Sgg.APPLY_HINT
        else:
            self.apply_hint()
            self.hint_status = Sgg.GIVE_HINT
            self.view.hint_off()

    def apply_hint(self):
        key_index = Sgg.all_qwerty.index(self.model.hint)
        self.view.update_qwerty_key(key_index)

    def update_colours(self):
        datums = self.model.this_attempt.datum
        for i, datum in enumerate(datums):
            if datum.answer:
                widget_key = i, self.model.try_index
                self.view.update_cell(widget_key)

    def show_bad_word(self):
        datum = self.model.this_attempt.datum[self.model.datum_index]
        word_ends = datum.word_ends
        for i in range(word_ends[0], word_ends[1]):
            widget_key = i, self.model.try_index
            self.view.update_cell(widget_key)

    def keyboard_event(self, event_code):
        """
        Keyboard event
        """
        self.input_event(1, event_code.keysym)

    def qwerty_event(self, qwerty_index):
        """
        Keyboard event
        """
        self.input_event(0, qwerty_index)

    def input_event(self, device, input_code):
        if device == 1:
            # keyboard
            PzlG.logger.logger.info("keyboard_event " +
                                    str(input_code))
            DEL = 'BackSpace'
            EOL = 'Return'
            keysym = input_code
        else:
            # onscreen keyboard
            DEL = 'Delete'
            EOL = 'Enter'
            keysym = Sgg.all_qwerty[input_code]
            PzlG.logger.logger.info("onscreen keyboard_event " +
                                    str(input_code))

        if self.active:
            if not self.bad_word and\
               keysym in string.ascii_letters and\
               self.model.letter_ok():
                letter = keysym.upper()
                qwerty_index = Sgg.all_qwerty.index(letter)
                self.model.add_letter(letter, qwerty_index)
                self.view.update_cell(self.model.widget_key)
                if not self.model.word_ok():
                    self.bad_word = True
                    self.model.colour_word('red')
                    self.show_bad_word()
                    self.model.colour_word('white')
                else:
                    if self.model.is_word_end():
                        if self.model.word_ok():
                            self.show_bad_word()
                            self.bad_word = False
            else:
                if not self.bad_word and\
                   keysym == EOL and\
                   self.model.return_ok():
                    colours, qwerty_indexes = self.model.check_phrase()
                    self.model.update_colours(colours)
                    self.view.update_qwerty(colours, qwerty_indexes)
                    self.update_colours()
                    if self.model.is_finished():
                        self.active = False
                        if self.model.success:
                            self.game_over(True)
                        else:
                            self.show_result()
                            self.game_over(False)
                    self.model.next_attempt()

                elif keysym == DEL and self.model.delete_ok():
                    if self.model.delete_letter():
                        self.view.update_cell(self.model.widget_key)
                        self.bad_word = False

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

    def set_max_tries(self):
        self.settings.max_tries = int(self.settings.dic['max_tries'])
        self.start_game()

    def game_over(self, success):
        self.active = False
        self.view.game_over()
        self.view.clock_stop()
        if success:
            self.flasher.flash()

    def set_new_game(self):
        self.settings.game_list = self.settings.dic['game_list']
        self.start_game()

    def show_result(self):
        texts = ['Oh dear!', 'Never give up!', 'You did quite well.',
                 'Did you learn anything?', 'Keep trying!',
                 "Tricky isn't it?"]
        if not self.model.success:
            item = random.randint(0, len(texts) - 1)
            text = texts[item] + '\n' + self.model.target
            self.view.show_message('Phrazy', text)

    def show_solution(self):
        self.model.set_solution()
        self.update_colours()
        self.game_over(False)


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