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

# Copyright (C) 2022 Rod Engerstad

import string
import random
import os
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 wordy_data import ModelData


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

    word_length = 5
    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']


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.dic['game'] = 'Wordle'
        self.dic['game'] = 'Absurdle'
        self.yellow = '#ffff00'
        self.dic['font_size'] = '1'
        self.dic['word_length'] = '5'
        self.dic['max_tries'] = '7'
        self.dic['counts_mode'] = 'Hide Counts'
        self.dic['set_wordlist'] = 'Built-in'
        self.menu_items = []
        self.preferences = Preferences(self.dic)
        self.preferences.load()
        self.load_menus()
        self.count = len(self.menu_items)
        self.word_length = int(self.dic['word_length'])
        Sgg.word_length = self.word_length
        self.max_tries = int(self.dic['max_tries'])
        self.game = self.dic['game']
        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 = 'white'
        self.entry_box_fg = 'black'
        self.square_size = 30
        self.make_help()
        self.make_tooltip()

    def make_help(self):
        self.help_text = _("Two games are included: the original Wordle https://en.wikipedia.org/wiki/Wordle and Absurdle https://www.theguardian.com/us-news/2022/jan/13/absurdle-machiavellian-version-of-wordle. Players try to guess a word. The program responds by colouring the entered letters: yellow indicates a correct letter in the wrong position, green a correct letter in the correct position, and dark grey for others. The mechanics of both games are the same but the underlying algorithms are very different. Wordle simply chooses a word and players try to guess it. Absurdle does not start by choosing a word, rather the player makes guesses to narrow down the list of possible words and tries to force the algorithm to make a choice. Players can choose which type of game to play, the word length (4, 5 or 6 letters), the number of attempts allowed (4 to 20), whether or not to show a count of the remaining words, the font size and whether to use their own word list. Handling word lists is described in the web site.")

    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 make_tooltip(self):
        self.tooltip_text = [_('New game'), _('Show solution')]

    def load_menus(self):
        self.menu_items.append(MenuItem(
            _('Game'),
            [
                'Wordle',
                'Absurdle',
            ],
            '',
            [
                'Wordle',
                'Absurdle',
            ],
            'game'
        ))
        self.menu_items.append(MenuItem(
            _('word length'),
            ['4',
             '5',
             '6'],
            '',
            ['4',
             '5',
             '6'],
            'word_length'
            ))
        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(
            _('Counts Mode'),
            ['Show Counts',
             'Hide Counts'],
            '',
            ['Show Counts',
             'Hide Counts'],
            'counts_mode'
            ))

        self.menu_items.append(MenuItem(
            _('Set Wordlist'),
            ['Built-in',
             'User-supplied'],
            '',
            ['Built-in',
             'User-supplied'],
            'set_wordlist'
        ))

        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, square_index, fg, bg, text):
        self.settings = settings
        self.square_index = square_index
        self.active = False
        font = self.settings.value_font
        width = 2
        height = 1
        bg = 'white'
        relief = 'raised'
        highlightbackground = 'black'
        tk.Label.__init__(self,
                          parent,
                          bd=1,
                          relief=relief,
                          highlightthickness=0,
                          highlightbackground=highlightbackground,
                          bg=bg,
                          fg=fg,
                          font=font,
                          width=width,
                          height=height,
                          state=tk.NORMAL,
                          padx=3,
                          text=text)
        self.grid(row=0, column=0)
        self.grid_propagate(0)

    def my_destroy(self):
        self.destroy()

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

    def set_text(self, text):
        self.configure(text=text)


class TryArea():
    """
    Surface to show the players attempts
    """
    def __init__(self, parent, settings, big_font, small_font,
                 bg):
        self.settings = settings
        self.big_font = big_font
        self.small_font = small_font
        self.bg = bg
        self.size = self.settings.square_size
        self.cells = []
        self.next_cell = 0
        self.current_row = self.next_cell // Sgg.word_length
        max_tries = self.settings.max_tries
        try_area_frame = tk.Frame(parent,
                                  relief='flat',
                                  bd=1, bg=self.bg)
        for try_row in range(max_tries):
            try_row_frame = tk.Frame(try_area_frame,
                                     relief='flat',
                                     bd=0, bg=self.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, item_index):
        for item in range(Sgg.word_length):
            cell = TextSquare(outer_frame, self.settings, item, 'black',
                              self.bg, ' ')
            cell.grid(row=item_index, column=item)
            self.cells.append(cell)
        item_index += 1

    def add_letter(self, letter):
        self.cells[self.next_cell].set_text(letter)
        self.next_cell += 1

    def delete_letter(self):
        if self.cells:
            self.next_cell -= 1
            self.cells[self.next_cell].set_text(' ')

    def colour_bg(self, cell_index, bg):
        self.cells[cell_index].colour_bg(bg)

    def update_word(self, colours):
        for i, colour in enumerate(colours):
            j = self.current_row * Sgg.word_length + i
            self.colour_bg(j, colour)
        if colours[0] == self.settings.white:
            # bad word
            for i in range(Sgg.word_length):
                self.delete_letter()

        self.current_row = self.next_cell // Sgg.word_length


class CountArea():
    """
    Surface to show the players attempts
    and provide keyboard entry.
    """
    def __init__(self, parent, settings, font, bg):
        self.settings = settings
        self.font = font
        self.bg = bg
        self.labels = []
        try_area_frame = tk.Frame(parent,
                                  relief='flat',
                                  bd=1, bg=bg)
        relief = 'flat'
        fg = self.settings.key_dark_grey
        bd = 1
        text = '     '
        highlightthickness = 0
        for try_row in range(self.settings.max_tries):
            try_row_frame = tk.Frame(try_area_frame,
                                     relief='flat',
                                     bd=1, bg=bg)

            label = tk.Label(try_row_frame,
                             bd=bd,
                             pady=0,
                             bg=bg,
                             fg=fg,
                             relief=relief,
                             font=font,
                             text=text,
                             highlightthickness=highlightthickness)

            label.grid(row=0, column=0)
            try_row_frame.grid(row=try_row, column=0)
            self.labels.append(label)
        try_area_frame.grid(row=0, column=1)

    def update_count(self, row, value):
        text = str(value).rjust(5)
        self.labels[row].config(text=text)


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, font, bg, callback):
        self.model = model
        alphabet, slash_posns = self.handle_user_keyboard()
        self.settings = settings
        self.callback = callback
        self.qkeys = []
        self.font = font
        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):
        big_font = self.settings.value_font

        row_frame = tk.Frame(
            qwerty_frame,
            highlightthickness=0,
            bg=self.bg,
        )

        highlightbackground = self.settings.grey

        for i, text_item in enumerate(text):

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

            font = big_font
            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_word(self, colours, key_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']

        if colours[0] != self.settings.white:
            for rank in colour_rank:
                for i, colour in enumerate(colours):
                    if colour == rank:
                        j = key_indexes[i]
                        self.qkeys[j].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',
                          highlightbackground=self.settings.white,
                          highlightthickness=0,
                          bd=1, bg=bg)

        self.try_frame = tk.Frame(self, parent)
        big_font = self.settings.value_font
        small_font = self.settings.code_font

        self.try_surface = TryArea(self.try_frame, settings,
                                   big_font, small_font,
                                   bg)
        self.try_cells = self.try_surface.cells

        if self.settings.dic['counts_mode'] == 'Show Counts':
            self.count_surface = CountArea(self.try_frame, settings,
                                           big_font, bg)
        self.try_frame.grid(row=0, column=0)
        self.qwerty_surface = Qwerty(self, settings, self.model,
                                     big_font, bg, qwerty_callback)
        self.qwerty_cells = self.qwerty_surface.cells
        self.grid(row=0, column=0)

    def my_destroy(self):
        self.destroy()

    def add_letter(self, letter):
        self.try_surface.add_letter(letter)

    def delete_letter(self):
        self.try_surface.delete_letter()

    def update_word(self, colours, key_indexes):
        self.try_surface.update_word(colours)
        self.qwerty_surface.update_word(colours, key_indexes)

    def update_count(self, row, text):
        if self.settings.dic['counts_mode'] == 'Show Counts':
            self.count_surface.update_count(row, text)


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.image_dic['start'] = [self.images.start_wordy, 0, True]
        self.qwerty_callback = None
        self.do_layout(self)

    def make_image_dic(self):
        """
        image, button_index, start
        """
        dic = {}
        dic['start'] = [self.images.start_boggle, 0, True]
        dic['show_solution'] = [self.images.smiley, 1, True]
        dic['length'] = [None, 2, 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 add_letter(self, letter):
        self.ui_surface.add_letter(letter)

    def delete_letter(self):
        self.ui_surface.delete_letter()

    def update_word(self, colours, key_indexes):
        self.ui_surface.update_word(colours, key_indexes)

    def update_count(self, row, text):
        if row > -1:
            self.ui_surface.update_count(row, text)


class Model():
    """
    Check the user input, etc
    """
    def __init__(self, model_data, settings):
        self.settings = settings
        self.model_data = model_data
        self.user_keyboard = self.model_data.user_keyboard
        self.dic = self.model_data.dic
        self.fours = self.dic[4]
        self.fives = self.dic[5]
        self.sixes = self.dic[6]
        self.words = self.dic[Sgg.word_length]
        self.guess = ''
        self.target = ''
        self.current_words = []
        self.key_indexes = []
        self.try_index = -1
        self.max_try_index = self.settings.max_tries
        self.finished = False
        self.success = False
        self.callback = None

    def new_game(self):
        self.finished = False
        self.success = False
        self.target = '-----'
        self.max_try_index = self.settings.max_tries
        self.try_index = -1
        self.current_words = list(self.words)

        if self.settings.game == 'Wordle':
            self.get_word()

    def get_word(self):
        word_index = random.randint(0, len(self.words)-1)
        self.target = self.words[word_index]
        self.key_indexes = []

    def letter_ok(self):
        ret = False
        if len(self.key_indexes) < Sgg.word_length:
            ret = True
        return ret

    def return_ok(self):
        ret = False
        if len(self.key_indexes) == Sgg.word_length:
            ret = True
        return ret

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

    def check_word(self):
        if self.settings.game == 'Wordle':
            colours = self.check_wordle_word()
        elif self.settings.game == 'Absurdle':
            colours = self.check_absurdle_word()

        indexes = list(self.key_indexes)

        if self.guess == self.target:
            self.finished = True
            self.success = True
            self.callback()
        elif self.try_index == self.max_try_index - 1:
            self.finished = True
        self.guess = ''
        self.key_indexes = []
        return colours, indexes

    def check_wordle_word(self):
        colours = []
        if self.guess in self.words:
            counts = {}
            for t, g in zip(self.target, self.guess):
                if t != g:
                    if t in counts:
                        counts[t] += 1
                    else:
                        counts[t] = 1
            for t, g in zip(self.target, self.guess):
                if t == g:
                    colours.append(self.settings.key_green)
                elif g in self.target and g in counts and counts[g] > 0:
                    colours.append(self.settings.key_yellow)
                    counts[g] -= 1
                else:
                    colours.append(self.settings.key_dark_grey)
            self.try_index += 1
            self.get_remainder(colours)
        else:
            colours = [self.settings.white for x in range(Sgg.word_length)]
        return colours

    def get_remainder(self, colours):
        current_words = []
        for word in self.words:
            counts = {}
            for w, c in zip(word, colours):
                if c != self.settings.key_green:
                    if w in counts:
                        counts[w] += 1
                    else:
                        counts[w] = 1

            for w, g, c in zip(word, self.guess, colours):
                if c == self.settings.key_green and w != g:
                    # word has mismatch for correct letter
                    break
                if w == g and c != self.settings.key_green:
                    # we know this letter is wrong
                    break
                if c == self.settings.key_yellow:
                    if g not in counts or counts[g] == 0:
                        # needed letter not in word
                        break
                    # keep track of letter counts
                    counts[g] -= 1
                elif c == self.settings.key_dark_grey and g in counts\
                     and counts[g] != 0:
                    break
            else:
                current_words.append(word)
        self.current_words = current_words

    def check_absurdle_word(self):

        def tie_break(n_dic):
            c_dic = {'G': 10, 'Y': 1, 'W': 0}
            smallest = 99
            small_key = ""
            for key, item in list(n_dic.items()):
                score = 0
                for letter in key:
                    score += c_dic[letter]

                if score < smallest:
                    smallest = score
                    small_key = key

            return small_key

        col_dic = {'W': self.settings.key_dark_grey,
                   'G': self.settings.key_green,
                   'Y': self.settings.key_yellow}
        if self.guess in self.words:
            c_dic = {}
            for word in self.current_words:
                colours = ""
                counts = {}
                for t, g in zip(word, self.guess):
                    if t != g:
                        if t in counts:
                            counts[t] += 1
                        else:
                            counts[t] = 1

                for t, g in zip(word, self.guess):
                    if t == g:
                        colours += 'G'
                    elif g in word and g in counts and counts[g] > 0:
                        colours += 'Y'
                        counts[g] -= 1
                    else:
                        colours += 'W'

                c_dic.setdefault(colours, []).append(word)
            big_dic = {}
            biggest = 0
            for key, words in list(c_dic.items()):
                if len(words) > biggest:
                    big_dic = {}
                    big_dic[key] = words
                    biggest = len(words)
                    bcolours = key
                elif len(words) == biggest:
                    big_dic[key] = words
                    bcolours = key

            if len(big_dic) > 1:
                bcolours = tie_break(big_dic)

            rcolours = [col_dic[x] for x in bcolours]
            self.current_words = big_dic[bcolours]
            if len(self.current_words) == 1:
                self.target = self.current_words[0]
            self.try_index += 1
        else:
            rcolours = [self.settings.white for x in range(Sgg.word_length)]
        return rcolours

    def add_qwerty_letter(self, key_index):
        letter = Sgg.all_qwerty[key_index]
        self.key_indexes.append(key_index)
        self.guess += letter
        return letter

    def add_keyboard_letter(self, key_index):
        letter = key_index.upper()
        kb_index = Sgg.all_qwerty.index(letter)
        self.key_indexes.append(kb_index)
        self.guess += letter
        return letter

    def delete_letter(self):
        ret = False
        if self.guess:
            ret = True
            self.guess = self.guess[0:-1]
            self.key_indexes.pop()
        return ret

    def is_finished(self):
        return self.finished

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

    def get_user_words(self, filename):
        """
        Users word list must be called wordy_data.txt
        and consist of a keyboard layout on the first line
        followed by one word per line. eg:
        ABCDEFGHIJKL/MNOPQRSTUV/WXYZÁÉÍÑÓÚÜ

        qwertyuiop/asdfghjkl/zxcvbnm
        aardvark
        aardvark's
        aardvarks
        abaci
        aback
        ....
        zygote
        zygote's
        zygotes

        The keyboard layout must include all the letters in the
        language alphabet and, optionally '/' characters to denote
        the end of each row of letters on the keyboard.

        The words in the list can be of any length and can contain
        symbols such as hyphens and apostrophies. The program
        will ignore any word containing symbols not in the user-supplied
        keyboard layout, for example aardvark's and zygote's, and will
        use only words of length 4, 5 and 6.

        This cleaned-up version of this data will be copied to the pzl
        installation directory:
        .pzl/wordy/wordy_data.txt
        """
        ret = False
        alphabet = string.ascii_uppercase + string.ascii_lowercase
        filename = PzlG.PROGRAMS_path + os.sep + 'wordy_data.txt'
        filename = '.' + os.sep + 'wordy_data.txt'
        if os.path.exists(filename):
            # word list exists
            self.fours = []
            self.fives = []
            self.sixes = []
            alphabet = ""
            upper = ""
            lower = ""
            alpha = ""
            with open(filename, "rt") as my_file:
                words = my_file.read().split()
                alpha_in = words[0]
                for letter in alpha_in:
                    alpha += letter.upper()
                    if letter != '/':
                        lower += letter.lower()
                        upper += letter.upper()
                        alphabet = lower + upper

                for word in words[1:]:
                    for tmp in word:
                        if tmp not in alphabet:
                            break
                    else:
                        if len(word) == 4:
                            self.fours.append(word.upper())
                        elif len(word) == 5:
                            self.fives.append(word.upper())
                        elif len(word) == 6:
                            self.sixes.append(word.upper())
            self.fours = list(dict.fromkeys(self.fours))
            self.fives = list(dict.fromkeys(self.fives))
            self.sixes = list(dict.fromkeys(self.sixes))
            self.user_keyboard = alpha
            self.dic[4] = self.fours
            self.dic[5] = self.fives
            self.dic[6] = self.sixes
            ret = True
        return ret


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

    def __init__(self, root):
        self.master = root
        self.settings = Settings()
        self.model_data = ModelData()
        self.model_data.set_wordlist(True)
        self.model = Model(self.model_data, self.settings)
        if self.settings.dic['set_wordlist'] == 'User-supplied':
            self.set_wordlist(False)
        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.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.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.view.game_on()
        self.model.new_game()

    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.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 == 'word_length':
            self.set_word_length()
        elif name == 'max_tries':
            self.set_max_tries()
        elif name == 'game':
            self.set_new_game()
        elif name == 'counts_mode':
            self.set_new_game()
        elif name == 'set_wordlist':
            if self.settings.dic['set_wordlist'] == 'Built-in':
                self.set_wordlist(True)
            else:
                self.set_wordlist(False)
            self.start_game()
        elif name == 'save_settings':
            self.settings.preferences.save()
        elif name == 'about':
            self.view.about()
        elif name == 'help':
            self.view.help()

    def keyboard_event(self, event_code):
        """
        Keyboard event
        """
        if self.active:
            PzlG.logger.logger.info("keyboard_event " +
                                    str(event_code.keysym))
            if event_code.keysym in string.ascii_letters and self.model.letter_ok():
                letter = self.model.add_keyboard_letter(event_code.keysym)
                self.view.add_letter(letter)
            else:
                if event_code.keysym == 'Return' and self.model.return_ok():
                    colours, key_indexes = self.model.check_word()
                    self.view.update_word(colours, key_indexes)
                    self.view.update_count(self.model.try_index,
                                           text=len(self.model.current_words))
                    if self.model.is_finished():
                        self.active = False
                        if self.model.success:
                            self.game_over(True)
                        else:
                            self.show_result()
                            self.game_over(False)
                elif event_code.keysym == 'BackSpace' and self.model.delete_ok():
                    if self.model.delete_letter():
                        self.view.delete_letter()

    def qwerty_event(self, key_index):
        """
        Onscreen keyboard event
        """

        if self.active:

            if self.model.is_finished():
                self.active = False
            else:

                if Sgg.all_qwerty[key_index] == 'Delete':
                    if self.model.delete_ok():
                        # undo
                        if self.model.delete_letter():
                            self.view.delete_letter()
                elif Sgg.all_qwerty[key_index] == 'Enter':
                    if self.model.return_ok():
                        # New try
                        colours, key_indexes = self.model.check_word()
                        self.view.update_word(colours, key_indexes)
                        self.view.update_count(self.model.try_index,
                                               text=len(self.model.current_words))
                        if self.model.is_finished():
                            self.active = False
                            if self.model.success:
                                self.game_over(True)
                            else:
                                self.show_result()
                                self.game_over(False)
                else:
                    if self.model.letter_ok():
                        letter = self.model.add_qwerty_letter(
                            key_index)
                        self.view.add_letter(letter)

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

    def set_word_length(self):
        self.settings.word_length = int(self.settings.dic['word_length'])
        Sgg.word_length = self.settings.word_length
        self.model.words = self.model_data.dic[Sgg.word_length]
        self.start_game()

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

    def set_new_game(self):
        self.settings.game = self.settings.dic['game']
        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 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)
            if self.settings.game == 'Wordle':
                text = texts[item] + ' ' + self.model.target
            else:
                text = texts[item]
            self.view.show_message('Wordy', text)

    def show_solution(self):
        if self.settings.game == 'Wordle':
            solution = self.model.target
        else:
            solution = self.model.current_words
        title = 'Wordy'
        self.view.show_message(title, solution)

    def set_wordlist(self, built_in):
        """
        if built_in is True the user wants wants to switch to the built-in
        word list which is always available and is simply a matter of
        setting dic accordingly. Otherwise the user is asking to use
        a list supplied by themselves. In this case if model_data has
        read a user word list we simply switch the dic. Otherwise we
        need to read a user-supplied list, parse it and if OK save it
        to the wordy directory and set the current word lists to it
        (ie dic[sixes], fives and fours.
        """
        if built_in:
            self.model_data.set_wordlist(built_in)
        else:
            if self.model_data.my_fives:
                self.model_data.set_wordlist(built_in)
            else:
                self.get_new_wordlist()
        self.model.user_keyboard = self.model_data.user_keyboard
        self.model.words = self.model_data.dic[Sgg.word_length]

    def get_new_wordlist(self):
        filename = ''
        title = 'Get my wordlist'
        filetypes = [('Text files', '*.txt')]
        init_dir = '~/Desktop'
        filename = tkinter.filedialog.askopenfilename(initialdir=init_dir,
                                                      filetypes=filetypes,
                                                      title=title)

        if filename:
            my_fours, my_fives, my_sixes = \
                self.model_data.get_user_words_1(filename)
            if my_fives:
                self.model_data.my_fours = list(dict.fromkeys(my_fours))
                self.model_data.my_fives = list(dict.fromkeys(my_fives))
                self.model_data.my_sixes = list(dict.fromkeys(my_sixes))
                self.model_data.set_wordlist(False)
            self.model.user_keyboard = self.model_data.user_keyboard

            new_filename = PzlG.app_data_path + 'wordy_data.txt'

            with open(new_filename, mode='wt') as my_file:
                my_file.write(self.model_data.user_keyboard + '\n')
                for word in self.model_data.my_fours:
                    my_file.write(word + '\n')
                for word in self.model_data.my_fives:
                    my_file.write(word + '\n')
                for word in self.model_data.my_sixes:
                    my_file.write(word + '\n')
            self.start_game()


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