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

# Copyright (C) 2017 Rod Engerstad

import math
import Tkinter as tk
from random import shuffle
from random import randint

from wordwheel_data import ModelData
from pzl_lib import PzlG
from pzl_lib import SettingsB
from pzl_lib import Preferences
from pzl_lib import MenuItem
from pzl_lib import MultipleEntry
from pzl_lib import Flasher
from pzl_lib import ColourRamp
from pzl_lib import ViewB


class Settings(SettingsB):

    def __init__(self):
        SettingsB.__init__(self)
        self.dic = {}
        self.dic['font_size'] = '1'
        self.dic['game_type'] = 'find_all'
        self.dic['shading'] = 'shade_on'
        self.dic['game_time'] = '120'
        self.list_box_width = 10
        self.original_entry_box_width = 5
        self.original_list_box_width = 5
        self.menu_items = []
        self.preferences = Preferences(self.dic)
        self.preferences.load()
        self.load_menus()
        self.set_fonts()
        self.count = len(self.menu_items)
        self.set_fonts()
        self.make_help()
        self.make_tooltip()

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

    def make_help(self):
        self.help_text = _("The program starts by showing the scrambled letters of a randomly selected 9-letter word arranged around a wheel. Find the original 9-letter word and as many others of at least 3-letters as you can. All words must include the letter at the centre of the wheel. The number of possible words is shown at the top of the display and will count down as the words are found. Define words by left clicking on the letters. Right click to finish a word, middle click to undo a letter. Alternatively enter the words using the keyboard after first placing the mouse cursor in the white box. Entered words will be transferred to the large grey box. The optional colour coding shows how many new words can be found starting with each letter and of each length. The colours fade as the words are found, white meaning none. At the top of the display is a Toolbar. The jigsaw piece button at the left is a menu which has options for choosing the game type, selecting shading, the maximum game duration, and the font size. To the right of the puzzle icon is the game icon used to start a new game, a hint button, and a sad smiley which is used to show the solution. Right again is a box for entering puzzle numbers. Right again is a counter for the number of words remaining. Right again is a clock which ticks every 5 seconds. The hint button helps with the 9-letter word. Every click will add one of the word's letters to the wide white box. To reorder the letters in the wheel click on the central letter with the middle mouse button. To rescramble the word put the cursor in the puzzle number box and hit 'Enter'.")

    def load_menus(self):
        self.menu_items.append(MenuItem(
            _('Game type'),
            [_('Against clock'),
             _('Find all')],
            '',
            ['against_clock',
             'find_all'],
            'game_type'
        ))
        self.menu_items.append(MenuItem(
            _('Shade cells'),
            [_('Shade on'),
             _('Shade off')],
            '',
            ['shade_on',
             'shade_off'],
            'shading'
        ))
        self.menu_items.append(MenuItem(
            _('Game time'),
            ['20',
             '40',
             '60',
             '80',
             '100',
             '120',
             '140',
             '160',
             '180',
             '200',
             '220',
             '240'],
            '',
            ['20',
             '40',
             '60',
             '80',
             '100',
             '120',
             '140',
             '160',
             '180',
             '200',
             '220',
             '240'],
            'game_time'
        ))
        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'
        ))


class ListBox(object):
    """
    Create and manage a ListBox
    """
    def __init__(self, parent_frame, settings):
        settings.list_box_width = 20
        font = settings.counter_font
        self.y_scroll = tk.Scrollbar(parent_frame, orient='vertical')
        self.y_scroll.grid(row=0, column=1, sticky='NS')
        self.box = tk.Listbox(parent_frame, font=font,
                              yscrollcommand=self.y_scroll.set,
                              width=settings.list_box_width,
                              height=15)
        self.box.grid(row=0, column=0)
        self.y_scroll["command"] = self.box.yview
        self.clear()

    def clear(self):
        self.box.delete(0, last='end')
        self.box.update_idletasks()


class WheelWidget(object):
    def __init__(self, square, widget, windex):
        self.square = square
        self.windex = windex
        self.widget = widget
        self.active = False
        self.callback = None
        square.tag_bind(widget, "<Enter>", self.command)
        square.tag_bind(widget, "<Leave>", self.command)
        square.tag_bind(widget, "<ButtonRelease-1>", self.command)
        square.tag_bind(widget, "<ButtonRelease-2>", self.command)
        square.tag_bind(widget, "<ButtonRelease-3>", self.command)

    def command(self, event):
        mouse_index = None
        if event.type == "7":
            self.active = True
        elif event.type == "8":
            self.active = False
        elif event.type == "5" and self.active:
            mouse_index = event.num
            self.callback(self.windex, mouse_index)


class Board(tk.Frame):
    def __init__(self, parent, settings, model, colour_ramp):
        """
        widgets
        0-8   9        10-18       19-21   22-24
        8 + centre, 9mers letters, 3,4,5,  6,7,8
        """

        self.parent = parent
        self.settings = settings
        self.model = model
        self.colour_ramp = colour_ramp
        shading = False
        if self.settings.dic['shading'] == 'shade_on':
            shading = True
        self.wheel_widgets = []
        self.ninemer_widgets = []
        self.counter_widgets = []
        bg = self.settings.green
        tk.Frame.__init__(self, parent, relief='flat',
                          highlightthickness=0,
                          bd=0, bg=bg)
        self.board = tk.Frame(self, bg=self.settings.green, bd=2)
        font = self.settings.value_font
        font_size = -1 * font.cget("size")
        radius = font_size * 4
        canvas_width = radius * 2.2
        canvas_height = canvas_width - 15
        x_centre = canvas_width / 2
        y_centre = radius
        self.canvas_width = canvas_width
        self.canvas_height = canvas_height
        self.square = tk.Canvas(self.board, width=self.canvas_width,
                                highlightthickness=0,
                                height=self.canvas_height, bd=0,
                                bg=bg)
        # do wheel
        x0 = x_centre - radius
        x1 = x_centre + radius
        y0 = 0
        y1 = radius * 2
        rad2 = radius / 1.3
        texts = self.model.word
        width = 1
        outline = 'black'
        activefill = 'yellow'
        extent = 45
        start = 135.
        angle = 22.5
        for i in range(8):
            colour_index = 0
            if shading:
                colour_index = self.model.left_letter[texts[i]]
            fill = self.get_colour(colour_index)
            idw = self.square.create_arc(x0, y0, x1, y1, fill=fill,
                                         style=tk.PIESLICE, extent=extent,
                                         start=start,
                                         activefill=activefill,
                                         width=width, outline=outline)
            self.wheel_widgets.append(WheelWidget(self.square, idw, i))
            start -= 45
            px = x_centre - rad2 * math.cos(math.radians(angle))
            py = y_centre - rad2 * math.sin(math.radians(angle))
            angle += 45
            text = texts[i]
            idw = self.square.create_text(px, py, text=text, font=font,
                                          activefill=activefill)
            self.wheel_widgets.append(WheelWidget(self.square, idw, i))

        # do central letter
        text = texts[8]
        colour_index = 0
        if shading:
            colour_index = self.model.left_letter[text]
        fill = self.get_colour(colour_index)
        rad = 17
        x0 = x_centre - rad
        y0 = y_centre - rad
        x1 = x_centre + rad
        y1 = y_centre + rad
        idw = self.square.create_oval(x0, y0, x1, y1, fill=fill,
                                      width=width, outline=outline,
                                      activefill=activefill)
        self.wheel_widgets.append(WheelWidget(self.square, idw, 8))
        idw = self.square.create_text(x_centre, y_centre, text=text,
                                      font=font, activefill=activefill)
        self.wheel_widgets.append(WheelWidget(self.square, idw, 8))
        self.square.grid(row=0, column=0)

        # do rectangle

        font = self.settings.counter_font
        bg = self.settings.green
        self.rectangle = tk.Frame(self.board, relief='flat',
                                  bd=2, bg=self.settings.grey)
        self.text_frame = tk.Frame(self.rectangle, relief='flat',
                                   bd=2, bg=self.settings.green)
        self.text_frame.grid(row=0, column=0)
        text_frame_t = tk.Frame(self.text_frame, relief='flat',
                                bd=2, bg=self.settings.green)
        text_frame_t.grid(row=0, column=0)
        fill = 'white'
        text = ' '
        label_frame = tk.Frame(text_frame_t)
        label_frame.grid(row=0, column=0)
        for i in range(9):
            idl = tk.Label(label_frame,
                           bd=1,
                           padx=0,
                           pady=0,
                           highlightthickness=1,
                           fg='black',
                           bg=fill,
                           font=font,
                           width=2,
                           height=1,
                           text=text)
            idl.grid(row=0, column=i)
            self.ninemer_widgets.append(idl)
        entry_frame = tk.Frame(self.text_frame, relief='flat',
                               bd=2, bg=self.settings.green)
        entry_frame.grid(row=1, column=0)
        texts = ['3', '4', '5']
        for i in range(3):
            colour_index = 0
            if shading:
                colour_index = self.model.left_length[i + 3]
            fill = self.get_colour(colour_index)
            text = texts[i]
            label_frame = tk.Frame(entry_frame)
            label_frame.grid(row=0, column=i, sticky='E')
            idl = tk.Label(label_frame,
                           bd=0,
                           padx=0,
                           pady=0,
                           highlightthickness=0,
                           fg='black',
                           bg=fill,
                           font=font,
                           width=2,
                           height=1,
                           text=text)
            idl.grid(row=0, column=0)
            self.counter_widgets.append(idl)

        # do entry box

        self.wentry_box_frame = tk.Frame(entry_frame, relief='sunken')
        self.wentry_box_frame.grid(row=0, column=3)
        self.settings.entry_box_type = 1
        self.settings.entry_box_type = 0
        texts = ['6', '7', '8']
        for i in range(3):
            text = texts[i]
            label_frame = tk.Frame(entry_frame)
            label_frame.grid(row=0, column=i+4, sticky='W')
            colour_index = 0
            if shading:
                colour_index = self.model.left_length[i + 6]
            fill = self.get_colour(colour_index)
            ide = tk.Label(label_frame,
                           bd=0,
                           padx=0,
                           pady=0,
                           highlightthickness=0,
                           fg='black',
                           bg=fill,
                           font=font,
                           width=2,
                           height=1,
                           text=text)
            ide.grid(row=0, column=0)
            self.counter_widgets.append(ide)

        # do list box

        self.rectangle.grid(row=1, column=0)
        self.list_box_frame = tk.Frame(self.board, relief='sunken')
        self.list_box_frame.grid(row=2, column=0)
        self.list_box = ListBox(self.list_box_frame,
                                self.settings)

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

    def my_destroy(self):
        self.destroy()

    def get_colour(self, colour_index):
        colour_index = max(colour_index, 0)
        colour_index = min(colour_index, self.colour_ramp.length - 1)
        return self.colour_ramp.colours[colour_index]

    def update_widgets(self):
        colour_index = 0
        for item in range(6):
            if self.settings.dic['shading'] == 'shade_on':
                colour_index = self.model.left_length[item + 3]
            bg = self.get_colour(colour_index)
            widget = self.counter_widgets[item]
            widget.configure(bg=bg)
        letter_index = -1
        for item in range(18):
            widget = self.wheel_widgets[item].widget
            if item % 2 == 0:
                letter_index += 1
                letter = self.model.word[letter_index]
                if self.settings.dic['shading'] == 'shade_on':
                    colour_index = self.model.left_letter[letter]
                bg = self.get_colour(colour_index)
                self.square.itemconfig(widget, fill=bg)
            else:
                self.square.itemconfig(widget, text=letter)
        self.update_idletasks()

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

    def update_ninemer_widget(self, letter, letter_index):
        self.ninemer_widgets[letter_index].configure(text=letter)


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

    def __init__(self, master, settings):
        ViewB.__init__(self, master, settings)
        self.image_dic = {}
        self.make_image_dic()
        self.do_layout(self)
        self.model = None
        self.colour_ramp = ColourRamp(None)
        self.board = None
        self.entry_boxes = None
        self.list_box_width = None
        self.original_entry_box_width = None
        self.original_list_box_width = None

    def start_game(self, redraw):
        if redraw or self.board is None:
            self.make_board()
            self.make_entry_boxes()
        else:
            self.board.model = self.model
            self.board.update_widgets()
            for position in range(9):
                self.board.update_ninemer_widget(' ', position)
            self.board.list_box.clear()
        self.game_on()

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

    def make_board(self):
        """
        draw the squares
        """

        if self.board is not None:
            self.board.my_destroy()
        self.list_box_width = 10
        self.original_list_box_width = 10
        self.board = Board(self.top_frame, self.settings, self.model,
                           self.colour_ramp)

    def show_remainder(self):
        total_words = self.model.word_count
        found_words = len(self.model.user_words)
        perc = 100.0*(float(found_words) / total_words)
        text = str(total_words) + ' ' + \
            str(found_words) + ' ' + str(round(perc, 1))+'%'
        self.board.list_box.box.insert('end', text)

        for length in range(10):
            for word in self.model.words[length]:
                self.board.list_box.box.insert('end', word)
        self.board.list_box.box.update_idletasks()

    def make_entry_boxes(self):
        frames = []
        frames.append(self.entry_box)
        frames.append(self.board.wentry_box_frame)
        self.entry_boxes = MultipleEntry(self.settings,
                                         frames)


class Model(object):

    def __init__(self, model_data):
        self.model_data = model_data
        self.words = []
        self.left_length = []
        self.left_letter = {}
        self.user_words = []
        self.this_word = ''
        self.word = ''
        self.ninemer = ''
        self.centre = ''
        self.hints = []
        self.word_count = 0
        order = list(range(9))
        shuffle(order)
        self.hint_order = order
        self.hint_index = 0
        for _ in range(10):
            self.words.append({})
            self.left_length.append([])
        self.mouse_word = ''

    def analyse_puzzle(self, puzzle):
        self.words = []
        self.left_length = []
        self.left_letter = {}
        words = []
        for length in range(10):
            self.words.append({})
            words.append({})
            self.left_length.append([])
        length = 0
        item = 0
        while item < len(puzzle):
            value = puzzle[item]
            if value == 'X':
                length += 1
            else:
                if value:
                    words[length][value] = True
            item += 1
        dic = words[9]
        self.word = list(dic.keys())[0]
        self.ninemer = list(dic.keys())[0]
        self.choose_centre()
        self.word_count = 0
        for length in range(10):
            dic = words[length]
            for key, value in list(dic.items()):
                if self.centre in key:
                    self.words[length][key] = True
                    self.word_count += 1
        for length in range(10):
            self.left_length[length] = len(self.words[length])
        for letter in self.word:
            self.left_letter[letter] = 0
        for length in range(10):
            dic = self.words[length]
            for key, value in list(dic.items()):
                self.left_letter[key[0]] += 1
        self.shuffle_word()

    def choose_centre(self):
        order = list(range(9))
        shuffle(order)
        centre = order[0]
        self.centre = self.word[centre]
        if centre != 8:
            self.word = self.word[:centre] +\
                        self.word[centre + 1:9] + self.word[centre]

    def shuffle_word(self):
        order = list(range(8))
        shuffle(order)
        shuffled_word = ''
        for item in order:
            shuffled_word += self.word[item]
        self.word = shuffled_word + self.word[8:]

    def get_puzzle(self, puzzle_number):
        puzzle = self.model_data.get_puzzle(puzzle_number)
        self.analyse_puzzle(puzzle)

    def good_word(self, word):
        ret = False
        length = len(word)
        if length > 2 and length < 10:
            if word in self.words[length]:
                del self.words[length][word]
                self.left_length[length] = len(self.words[length])
                self.left_letter[word[0]] -= 1
                ret = True
                self.user_words.append(word)
        return ret

    def new_word(self):
        self.mouse_word = ''

    def new_letter(self, letter):
        self.mouse_word += letter

    def delete_letter(self):
        if self.mouse_word:
            self.mouse_word = self.mouse_word[0:-1]

    def all_words_found(self):
        ret = True
        for length in range(10):
            if self.words[length]:
                ret = False
                break
        return ret

    def do_hint(self):
        letter = ''
        next_index = None
        if self.hint_index < 9:
            next_index = self.hint_order[self.hint_index]
            letter = self.ninemer[next_index]
            self.hint_index += 1
        return letter, next_index


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

    def __init__(self, root):
        self.settings = Settings()
        self.master = root
        self.flasher = None
        self.model_data = ModelData()
        self.model = None
        self.puzzle_number = None
        self.active = False
        self.ninemer_done = False
        puzzle_number = None
        self.view = View(root, self.settings)
        self.start_game(puzzle_number, True)

    def start_game(self, puzzle_number, redraw):
        self.model = Model(self.model_data)
        self.view.model = self.model
        self.set_puzzle_number(puzzle_number)
        self.model.get_puzzle(self.puzzle_number)
        self.active = True
        self.ninemer_done = False
        self.view.clock_init()
        self.view.clock_restart()
        self.view.start_game(redraw)
        self.add_callbacks()
        self.view.entry_boxes.clear(0)
        self.view.entry_boxes.write_box(str(self.puzzle_number), 0)
        self.view.update_score_box(self.model.word_count)
        self.flasher = Flasher(self.view.clock.box.label,
                               self.master, self.settings)

    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 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 == 3:
            if button_index == 0:
                self.puzzle_number += 1
                self.start_game(self.puzzle_number, False)
        elif mouse_index == 1:
            if button_index == 0:
                self.start_game(None, False)
            elif button_index == 1 and not self.ninemer_done:
                self.give_hint()
            elif button_index == 2:
                if self.active:
                    self.show_solution()
                    self.active = False

    def service_entry_boxes(self, texts):
        """
        The entry_boxes are 0: puzzle_number, 1: word
        text[0] will always contain the puzzle number.
        If text[1] is not empty we assume that a new word
        has been entered. If it is empty we assume that
        we should load a puzzle.
        """
        if not texts[1]:
            try:
                puzzle_number = int(texts[0])
            except ValueError:
                text = texts[0].upper()
                if text.find('XYZZY') != -1:
                    self.view.xyzzy()
                    # clear entry_box
                puzzle_number = None
            self.start_game(puzzle_number, False)
        else:
            word = texts[1]
            user_word = word.upper()
            self.model.this_word = user_word
            self.view.entry_boxes.clear(1)
            good_word = self.model.good_word(user_word)
            if good_word:
                self.view.board.list_box.box.insert('end', user_word)
                self.view.board.update_widgets()
                self.model.mouse_word = user_word
                self.ninemer_finished()
                self.model.new_word()
            self.check_game_over()

    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 == 'game_type':
            self.set_game_type()
        elif name == 'game_time':
            self.set_game_time()
        elif name == 'shading':
            self.start_game(self.puzzle_number, False)
        elif name == 'font_size':
            self.set_font_size()
            self.start_game(self.puzzle_number, True)
        elif name == 'save_settings':
            self.settings.preferences.save()
        elif name == 'about':
            self.view.about()
        elif name == 'help':
            self.view.help()

    def wheel_event(self, square_index, mouse_index):
        """
        Respond to mouse cursor in grid
        """
        if self.active:
            if mouse_index == 1:
                # add letter
                letter = self.model.word[square_index]
                self.model.new_letter(letter)
                self.view.entry_boxes.add_letter(letter, 1)
            elif mouse_index == 2:
                if not self.model.mouse_word:
                    self.model.shuffle_word()
                    self.view.board.update_widgets()
                else:
                    # delete letter
                    self.model.delete_letter()
                    self.view.entry_boxes.delete_letter(1)
            elif mouse_index == 3:
                # finished word
                user_word = self.model.mouse_word
                good_word = self.model.good_word(user_word)
                if good_word:
                    self.view.board.list_box.box.insert('end', user_word)
                    self.view.board.update_widgets()
                    self.ninemer_finished()
                self.model.new_word()
                self.view.entry_boxes.clear(1)
            self.check_game_over()
        else:
            if mouse_index == 2:
                self.model.shuffle_word()
                self.view.board.update_widgets()

    def ninemer_finished(self):
        if len(self.model.mouse_word) == 9:
            for letter_index in range(9):
                letter = self.model.mouse_word[letter_index]
                self.view.board.update_ninemer_widget(letter, letter_index)
            self.view.toolbar.key_off('hint')
            self.ninemer_done = True
        self.view.update_score_box(self.model.word_count -
                                   len(self.model.user_words))

    def add_callbacks(self):
        self.view.add_menu_callback(self.service_menu)
        self.view.add_toolbar_callback(self.service_toolbar)
        self.view.entry_boxes.add_callback(self.service_entry_boxes)
        self.view.board.add_callback(self.wheel_event)
        self.view.clock.callback = self.check_game_over

    def give_hint(self):
        letter, letter_index = self.model.do_hint()
        if letter_index is not None:
            self.view.board.update_ninemer_widget(letter, letter_index)
        else:
            self.ninemer_done = True

    def set_game_type(self):
        self.settings.max_time = None
        if self.settings.dic['game_type'] == 'against_clock':
            self.settings.max_time = int(self.settings.dic['game_time'])
        self.start_game(self.puzzle_number, False)

    def set_game_time(self):
        self.settings.max_time = int(self.settings.dic['game_time'])
        self.start_game(self.puzzle_number, False)

    def set_font_size(self):
        self.view.entry_boxes.clear(0)
        self.settings.do_scaling()

    def check_game_over(self):
        if self.settings.dic['game_type'] == 'against_clock':
            if self.view.clock.finished:
                self.active = False
        else:
            if self.model.all_words_found():
                self.active = False
        if not self.active:
            self.show_solution()

    def show_solution(self):
        self.view.game_over()
        self.view.clock_stop()
        self.view.clock.finished = False
        if not self.ninemer_done:
            while True:
                letter, letter_index = self.model.do_hint()
                if letter_index is not None:
                    self.view.board.update_ninemer_widget(letter,
                                                          letter_index)
                else:
                    break
        self.view.show_remainder()
        self.flasher.flash()

    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, False)
            except ValueError:
                self.view.entry_box.clear()


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