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

# Copyright (C) 2022 Rod Engerstad

from math import sqrt
from random import randint
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 ViewB
from pzl_lib import MenuItem
from pzl_lib import Flasher
from pzl_lib import ColourRamp
from pzl_lib import TextSquare
from gridword_data import ModelData


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

    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']

    grid_size = None

class Settings(SettingsB):
    """
    Settings
    """
    def __init__(self):
        SettingsB.__init__(self)
        self.dic = {}
        self.dic['font_size'] = '1'
        self.entry_box_width = 3
        self.menu_items = []
        self.preferences = Preferences(self.dic)
        self.preferences.load()
        self.load_menus()
        self.count = len(self.menu_items)
        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.square_size = 30
        self.make_help()
        self.make_tooltip()

    def make_help(self):
        self.help_text = _("When a puzzle is selected by clicking the icon next to the jigsaw button, or by entering a number into the box, the on-screen keyboard is shaded and a target score is displayed. Make words in the grid using the shaded letters in the keyboard. To place a letter, first left-click on the desired position in the grid (a dot will appear) and then left click on the desired letter in the on-screen keyboard. To remove a letter or a dot from the grid, right-click on it.  As words are added the current score will be displayed at the top. The score being the sum of the lengths of the longest words in each row and column. Only words of length > 2 will count. The grids have sizes 4x4, 5x5, and 6x6, with puzzle numbers 0 to 34, 35 to 169 and 170 to 304, respectively.")

    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.code_font_size
        self.code_font.configure(size=int(size * factor))
        self.square_size = int(self.original_square_size * factor)
        self.value_height = 12
        self.value_height = int(self.value_height * factor)
        size = self.value_font_size
        self.value_font.configure(size=int(size * 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(
            _('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 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:
            mouse_index = event.num
            self.callback(0, self.key_index, mouse_index)

    def update_key(self, bg):
        self.frame.configure(bg=bg)
        self.label.configure(bg=bg)


class Qwerty():
    """
    Area 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.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

        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.font
        bg = self.settings.white

        row_frame = tk.Frame(
            qwerty_frame,
            highlightbackground='black',
            highlightthickness=0,
            bg=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_key(self, key_index):
        qkey = self.model.qkeys[key_index]
        colour = qkey.colour
        self.qkeys[key_index].update_key(colour)


class Board(tk.Frame):
    """
    Handle the grid
    """
    def __init__(self, parent, settings, model):
        self.settings = settings
        self.model = model
        self.callback = None
        self.grid_widgets = []
        bg = self.settings.green
        tk.Frame.__init__(self, parent, relief='flat',
                          highlightbackground=self.settings.grey,
                          highlightthickness=2,
                          bd=2, bg=bg)

        fg = 'black'
        bg = 'grey'

        cell_index = 0
        for y in range(Sgg.grid_size):
            for x in range(Sgg.grid_size):
                text = ''
                cell = TextSquare(self, self.settings, cell_index, fg,
                                  bg, text)
                cell.grid(row=y, column=x)
                cell.grid_propagate(0)
                self.grid_widgets.append(cell)
                cell_index += 1
        self.grid(row=0, column=0)

    def add_letter(self, grid_index, letter):
        self.grid_widgets[grid_index].set_text(letter)

    def my_destroy(self):
        for widget in self.grid_widgets:
            widget.my_destroy()
        self.destroy()

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

    def colour_cells(self, cell_indexes, bg1, bg2):
        for cell_index in range(Sgg.grid_size * Sgg.grid_size):
            colour_index = cell_indexes[cell_index]
            if colour_index == 1:
                self.grid_widgets[cell_index].colour_bg(bg1)
            elif colour_index == 2:
                self.grid_widgets[cell_index].colour_bg(bg2)

class PlayArea(tk.Frame):
    """
    The complete area for the game.
    """
    def __init__(self, parent, settings, model,
                 qwerty_callback, board_callback):
        self.settings = settings
        self.model = model
        self.qwerty = None
        self.grid_area = None
        bg = self.settings.red

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

        self.grid_frame = tk.Frame(self, parent)
        big_font = self.settings.value_font
        bg = self.settings.white

        self.grid_area = Board(self.grid_frame, self.settings, self.model)
        self.grid_frame.grid(row=0, column=0)
        self.grid_area.add_callback(board_callback)

        bg = self.settings.white
        self.qwerty = Qwerty(self, settings, self.model,
                             big_font, bg, qwerty_callback)

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

    def my_destroy(self):
        self.destroy()

    def add_letter(self, grid_index, letter):
        self.grid_area.add_letter(grid_index, letter)

    def update_key_colour(self, key_index):
        self.qwerty.update_key(key_index)



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.play_area = None
        self.image_dic = {}
        self.make_image_dic()
        self.image_dic['start'] = [self.images.start_gridword, 0, True]
        self.qwerty_callback = None
        self.board_callback = None
        self.do_layout(self)
        self.entry_box.activate()

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

    def make_play_area(self):
        if self.play_area is not None:
            self.play_area.my_destroy()
        self.play_area = PlayArea(self.top_frame,
                                  self.settings, self.model,
                                  self.qwerty_callback, self.board_callback)
        self.colour_keys()

    def update_key_colour(self, key_index):
        self.play_area.update_key_colour(key_index)

    def colour_keys(self):
        for key_index in range(26):
            self.model.update_key_colour(key_index)
            self.update_key_colour(key_index)

    def add_callbacks(self, qwerty_callback, board_callback):
        self.qwerty_callback = qwerty_callback
        self.board_callback = board_callback


class QKey():
    """
    A key of a qwerty keyboard
    """
    def __init__(self, letter, count, colour):
        self.letter = letter
        self.count = count
        self.colour = colour

    def add_letter(self):
        """
        Add to count for this letter.
        The colour will be updated externally
        """
        self.count += 1

    def delete_letter(self):
        """
        Delete count for this letter.
        The colour will be updated externally
        """
        self.count = max(0, self.count - 1)


class Model():
    """
    Manage the data.
    user_keyboard ---> qkeys. List of qkey. qkey[letter, count, colour]
    puzzle ---> grid_cells. List of letters place by user.
    qkeys[key_index].count set up or down here and colour accordingly.
    grid_cells[grid_index] letters added or deleted.
    """
    def __init__(self, model_data, settings):
        self.settings = settings
        self.model_data = model_data
        self.colour_ramp = ColourRamp(None)
        self.colour_ramp.colours[0] = 'white'
        self.user_keyboard = [x for x in Sgg.qwerty if x != '/']
        self.qkeys = []
        self.grid_cells = []
        self.dic = self.model_data.dic
        self.score = 0
        self.answer_score = 0
        self.target_score = 0
        self.finished = False
        self.puzzle_number = None
        self.puzzle = ''

    def letter_to_index(self, letter):
        return self.user_keyboard.index(letter)

    def index_to_letter(self, key_index):
        return self.user_keyboard[key_index]

    def key_is_loaded(self, key_index):
        return self.qkeys[key_index].count > 0

    def key_count_plus(self, key_index):
        qkey = self.qkeys[key_index]
        qkey.count += 1
        colour_index = min(qkey.count, self.colour_ramp.length - 1)
        qkey.colour = self.colour_ramp.colours[colour_index]

    def key_count_minus(self, key_index):
        qkey = self.qkeys[key_index]
        qkey.count -= 1
        colour_index = min(qkey.count, self.colour_ramp.length - 1)
        qkey.colour = self.colour_ramp.colours[colour_index]

    def update_key_colour(self, key_index):
        qkey = self.qkeys[key_index]
        colour_index = qkey.count
        colour_index = min(colour_index, self.colour_ramp.length - 1)
        return self.colour_ramp.colours[colour_index]

    def update_grid(self, grid_index, letter):
        self.grid_cells[grid_index] = letter
        self.score_grid()

    def get_puzzle(self, puzzle_number):
        self.puzzle_number = puzzle_number
        self.puzzle = self.model_data.get_puzzle(self.puzzle_number)
        Sgg.grid_size = int(sqrt(len(self.puzzle)))
        self.grid_cells = ['' for x in self.puzzle]
        self.answer_score = self.score_puzzle(self.puzzle)
        self.target_score = self.answer_score
        self.prepare_puzzle()

    def init_qkeys(self):
        self.qkeys = []
        for i in range(26):
            self.qkeys.append(QKey(Sgg.qwerty[i], 0, 'white'))

    def prepare_puzzle(self):
        """
        Add the puzzle to qkeys
        """
        self.init_qkeys()
        for letter in self.puzzle:
            key_index = self.letter_to_index(letter)
            self.qkeys[key_index].add_letter()
        for key_index in range(26):
            qkey = self.qkeys[key_index]
            colour_index = qkey.count
            colour_index = min(colour_index, self.colour_ramp.length - 1)
            qkey.colour = self.colour_ramp.colours[colour_index]

    def get_longest(self, letters):
        """
        Get the length and letters of the longest word this string
        """
        length_letters = len(letters)
        max_word = 6
        for word_length in range(min(max_word, length_letters), 2, -1):
            # possible first positions this start, this word_length
            dic = self.dic[word_length]

            for first in range(length_letters - word_length + 1):
                word = letters[first:first + word_length]
                if word in dic:
                    return [word_length, list(range(first,
                                                    first+word_length))]
        return [0, []]

    def score_puzzle(self, puzzle):
        # do rows
        score = 0
        for y in range(Sgg.grid_size):
            letters = puzzle[y * Sgg.grid_size: (y + 1) * Sgg.grid_size]
            res = self.get_longest(letters)
            score += res[0]

        for x in range(Sgg.grid_size):
            letters = ""
            for y in range(Sgg.grid_size):
                letters += puzzle[y * Sgg.grid_size + x]
            res = self.get_longest(letters)
            score += res[0]

        return score

    def colour_result(self):
        # do rows
        colour_indexes = [0 for x in range(Sgg.grid_size * Sgg.grid_size)]
        rows = []
        cols = []
        puzzle = ""
        for cell in self.grid_cells:
            if cell and cell != '.':
                puzzle += cell
            else:
                puzzle += ' '
        for y in range(Sgg.grid_size):
            letters = puzzle[y * Sgg.grid_size: (y + 1) * Sgg.grid_size]
            res = self.get_longest(letters)
            if res[0]:
                j = y * Sgg.grid_size
                tmp = [x + j for x in res[1]]
                for i in tmp:
                    if i not in rows:
                        rows.append(i)

        for x in range(Sgg.grid_size):
            letters = ""
            for y in range(Sgg.grid_size):
                letters += puzzle[y * Sgg.grid_size + x]
            res = self.get_longest(letters)
            if res[1]:
                tmp = []
                for y in res[1]:
                    tmp.append(y * Sgg.grid_size + x)
                    for i in tmp:
                        if i not in cols:
                            cols.append(i)
        for i in rows:
            colour_indexes[i] += 1
        for i in cols:
            colour_indexes[i] += 1

        return colour_indexes

    def score_grid(self):
        done = 0
        puzzle = ""
        for cell in self.grid_cells:
            if cell and cell != '.':
                puzzle += cell
                done += 1
            else:
                puzzle += ' '
        self.score = self.score_puzzle(puzzle)
        if done == len(puzzle):
            self.finished = True

    def new_game(self):
        self.finished = False

    def is_finished(self):
        return self.finished


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.puzzle_number = None
        self.model.get_puzzle(self.puzzle_number)
        self.view = View(root, self.settings, self.model)
        self.add_callbacks()
        self.letter = ''
        self.flasher = None
        self.active = False
        self.active_position = None
        self.active_letter = None
        self.start_game(None)

    def set_puzzle_number(self, puzzle_number):
        if puzzle_number is None or puzzle_number < 0 or \
                puzzle_number > self.model_data.puzzle_count - 1:
            puzzle_number = randint(0, self.model_data.puzzle_count - 1)
        self.puzzle_number = puzzle_number

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

    def start_game(self, puzzle_number):
        self.set_puzzle_number(puzzle_number)
        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.view.entry_box.clear()
        self.view.entry_box.write_box(str(self.puzzle_number))
        self.model.new_game()
        self.model.get_puzzle(self.puzzle_number)
        self.view.update_score_box(self.model.answer_score)
        self.active = True
        self.active_letter = ''
        self.active_position = None
        self.view.make_play_area()
        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(None)
            elif button_index == 1:
                if not self.model.is_finished():
                    self.show_solution()
        elif mouse_index == 3:
            if button_index == 0:
                self.puzzle_number += 1
                self.start_game(self.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 == 'save_settings':
            self.settings.preferences.save()
        elif name == 'about':
            self.view.about()
        elif name == 'help':
            self.view.help()

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

        if self.active:
            if event_code not in (2, 3):
                PzlG.logger.logger.info("qwerty_event " +
                                        str(event_code) +
                                        ' ' +
                                        str(key_index) +
                                        ' ' +
                                        str(mouse_index))

            if event_code == 0:
                if mouse_index == 1:
                    if self.active_position is not None:
                        if self.model.key_is_loaded(key_index):
                            self.model.key_count_minus(key_index)
                            self.view.update_key_colour(key_index)
                            self.active_letter = \
                                self.model.index_to_letter(key_index)
                            self.view.play_area.\
                                grid_area.add_letter(self.active_position,
                                                     self.active_letter)
                            self.model.update_grid(self.active_position,
                                                   self.active_letter)
                            self.view.update_score_box(self.model.score)
                            self.active_position = None
                            self.active_letter = ''

            if self.model.is_finished():
                self.game_over()

            self.view.score_box.update_idletasks()



    def square_event(self, grid_index, mouse_index):
        """
        Respond to mouse cursor in grid
        """
        if self.active:
            PzlG.logger.logger.info("board_event " +
                                    str(grid_index) +
                                    ' ' +
                                    str(mouse_index))

            if mouse_index == 1:
                letter = self.model.grid_cells[grid_index]
                if not self.active_letter and letter == '':
                    # add letter
                    self.active_letter = '.'
                    if self.model.answer_score != 0:
                        self.model.answer_score = 0
                        self.view.update_score_box(self.model.answer_score)
                    self.active_position = grid_index
                    self.view.play_area.grid_area.add_letter(self.active_position, self.active_letter)
                    self.model.update_grid(self.active_position, self.active_letter)
            elif mouse_index == 3:
                #delete letter
                letter = self.model.grid_cells[grid_index]
                if letter == '.':
                    self.active_position = None
                    self.active_letter = ''
                    self.view.play_area.grid_area.add_letter(grid_index, '')
                    self.model.update_grid(grid_index, '')
                elif letter != '':
                    self.view.play_area.grid_area.add_letter(grid_index, '')
                    self.model.update_grid(grid_index, '')
                    self.view.update_score_box(self.model.score)
                    key_index = self.model.letter_to_index(letter)
                    self.model.key_count_plus(key_index)
                    self.view.update_key_colour(key_index)

            self.view.score_box.update_idletasks()

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

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

    def game_over(self):
        self.active = False
        colour_indexes = self.model.colour_result()
        size = len(self.model.colour_ramp.colours)
        colour_0 = self.model.colour_ramp.colours[size // 2]
        colour_1 = self.model.colour_ramp.colours[-1]
        self.view.play_area.grid_area.colour_cells(colour_indexes,
                                                   colour_0, colour_1)
        self.view.game_over()
        self.view.score_box.update_idletasks()
        self.view.clock_stop()
        if self.model.score >= self.model.target_score:
            self.flasher.flash()

    def show_solution(self):
        for grid_index, item in enumerate(self.model.puzzle):
            self.view.play_area.grid_area.add_letter(grid_index, item)
            self.model.grid_cells[grid_index] = item
        colour_indexes = self.model.colour_result()
        size = len(self.model.colour_ramp.colours)
        colour_0 = self.model.colour_ramp.colours[size // 2]
        colour_1 = self.model.colour_ramp.colours[-1]

        self.view.play_area.grid_area.colour_cells(colour_indexes,
                                                   colour_0, colour_1)


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