#!/bin/env python
"""ThudBoard - The Discworld Boardgame without rules

Version 1.3
Copyright 2003, 2004 by Marc Boeren

More information
----------------
Read the docs (index.html, howto.html, download.html and about.html) 
for more information about ThudBoard and how to use ThudBoard.

TODO
----
skins (partially done)
web-save option
keyboard-bindings
two-player internet-connection game
cleanup of code
file > print?
rules-plugin
AI-plugin

"""
import sys
import os
import copy
import Tkinter as tk
#from tkMessageBox import askyesno
from bugfix_askyesno import fixed_askyesno as askyesno
from tkMessageBox import showwarning
from tkFileDialog import askopenfilename
from tkFileDialog import asksaveasfilename
from tkFileDialog import askdirectory
from battle import ThudMove, ThudBattle
import skins
from webbrowser import open as webbrowser_open
import ConfigParser

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#

text_mini_howto = """Quick How-to:

Left-click-and-drag the piece you 
wish to move. Release when the 
piece is over the new tile or the 
piece you want to capture.

Right-click to capture pieces.

Left-click to finalize the move.

Undo a move by selecting the 
previous line in the moves-list 
or by selecting the name of the 
battle above the list.

Copy the highlighted line by
clicking the copy-icon. Paste 
below the highlighted line by 
clicking the paste-icon.

Select 'Help', then 'howto' 
above to learn more.
"""

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#

def main():
    app = App()
    app.mainloop()

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#

class Size(object):
    """Size is a placeholder for items that have x, y, dx and dy values."""
    def __init__(self, x, y, dx, dy):
        self.x = x
        self.y = y
        self.dx = dx
        self.dy = dy
    def inside(self, x, y):
        """Return True is x, y is inside this Size, treated as a rectangle."""
        if self.x<=x<=self.x+self.dx \
        and self.y<=y<=self.y+self.dy:
            return True
        return False

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#

class App(tk.Tk):
    def __init__(self):
        # setup app-window
        self.startup = True
        tk.Tk.__init__(self)
        self.skin = skins.init()
        self.configure(background=self.skin.color['background'])
        self.wm_title("ThudBoard")
        try:
            self.wm_iconbitmap('thud.ico')
        except:
            pass # if it doesn't work, well, then I'll do without the icon
        self.wm_minsize(790, 540)
        self.wm_maxsize(790, 540)
        self.wm_geometry('790x540+0+0')
        self.wm_resizable(False, False)
        self.focus_force()
        # setup globals
        pointsize = self.calc_pointsize(10.7)
        try: 
            winver = sys.winver
            self.font = ("microsoft sans serif", pointsize)
        except:
            self.font = ("helvetica", pointsize)
        self.toggle_boardmap = False
        self.toggle_highlight = True
        self.toggle_score = True
        self.toggle_comment = False
        self.submenu = False
        self.skip_next_click = False
        self.saved = True
        self.start_coord = ""
        self.drag_coord = ""
        self.highlight_area = None
        self.scrollersensitivity = 100
        self.scrollerid = 0
        self.motionposition = (0, 0)
        self.pieces = {}
        self.mru_count = 8
        self.mru = ['',] * self.mru_count
        # determine user battle/configdir
        # existing dirs have preference, then, 
        # user-specifics dirs have preference over common dirs
        self.configdir = os.getcwd()
        self.battledir = os.path.join(os.getcwd(), "battles")
        if not os.path.isdir(self.battledir): # no common dir
            configdir = os.path.join(os.path.expanduser("~"), ".thudboard")
            battledir = os.path.join(configdir, "battles")
            try: os.makedirs(battledir)
            except: pass
            if os.path.isdir(battledir): # success creating userdir
                self.configdir = configdir
                self.battledir = battledir
            else: # create common dir
                try: os.makedirs(self.battledir)
                except: pass
        # load saved options
        self.load_options(os.path.join(self.configdir, "thudboard.cfg"))
        self.listlines = self.toggle_comment and 19 or 25
        # setup board
        self.size_battlename = Size(599, 115, 171, 13)
        self.size_list = Size(599, 157, 171, 13)
        self.size_thudurl = Size(599, 518, 171, 13)
        self.tile = Size(252, 252, 36, 36)
        self.DrawInitialize()
        # setup events
        self.EventInitialize()
        # go!
        self.background_disabled = False
        self.startup = True
        self.file_new()
        self.skip_next_click = False

    def EventInitialize(self):
        self.protocol("WM_DELETE_WINDOW", self.file_quit)
        self.bind('<FocusIn>', self.focus_in)
        self.canvas.bind('<Motion>', self.piece_highlight)
        self.canvas.bind('<ButtonPress-1>', self.drag_start)
        self.canvas.bind('<B1-Motion>', self.drag)
        self.canvas.bind('<ButtonRelease-1>', self.drag_stop)
        self.canvas.bind('<ButtonRelease-3>', self.hide_piece)
        self.background.bind('<ButtonRelease-1>', self.background_area_click)
        self.background.bind('<ButtonPress-1>', self.background_scrollers)
        self.background.bind('<Motion>', self.background_motion)
        self.bind('<Control-c>', self.cmd_copy)
        self.bind('<Control-v>', self.cmd_paste)
        self.commentsbox.bind('<FocusOut>', self.update_comment)

    def calc_pointsize(self, pixelapprox):
        try:
            dpiy = self.winfo_screenheight()/(self.winfo_screenmmheight()/25.4)
        except:
            return 8
        pointsize = 72.0*pixelapprox/dpiy
        return int(pointsize)

    # event handlers
    def update_comment(self, event):
        comment = self.commentsbox.get("1.0", tk.END).strip()
        if self.battle_index>=0 and comment!=self.battle.moves[self.battle_index].comment:
            self.battle.moves[self.battle_index].comment = comment
            self.saved = False
        elif self.battle_index==-1 and comment!=self.battle.comment:
            self.battle.comment = comment
            self.saved = False
            self.update_infoimg()

    def focus_in(self, event):
        if not self.startup:
            if self.battle_index>=0:
                dy, h = self.battle.get_offset_and_height(self.battle_index, self.list_offset)
                self.MoveHighlight(dy*self.size_list.dy, h*self.size_list.dy)
            else:
                self.MoveHighlightBattlename()
        self.startup = False

    def cmd_copy_click(self, event):
        self.background.focus_set()
        self.update_comment(None)
        self.cmd_copy(event)

    def cmd_copy(self, event):
        if self.background.focus_get() == self.commentsbox: return
        if self.battle.index<0: return
        txt = [line.strip() for line in self.battle.txt_battle_move().splitlines()]
        txt = " ".join(txt).replace("~ ", "")
        comment = self.battle.moves[self.battle_index].comment.strip()
        if comment:
            comment = "# "+"\n# ".join(comment.split('\n'))
            txt += (comment.find('\n') < 0 and " " or "\n") + comment
        self.clipboard_clear()
        self.clipboard_append(txt)

    def cmd_paste_click(self, event):
        self.temp_disable_background_click()
        self.cmd_paste(event)

    def cmd_paste(self, event):
        if self.background.focus_get() == self.commentsbox: return
        try:
            txt = self.selection_get(selection='CLIPBOARD')
        except:
            txt = ''
        if txt:
            parts = txt.split('#')
            txt = parts[0].strip()
            comment = ''
            if len(parts)>1:
                comment = "\n".join([p.strip() for p in parts[1:]])
            txt = "".join(txt.splitlines()).replace(" ", "")
            test = ThudMove(-1, "", "", [])
            test.load_str(txt, comment)
            self.moving = True
            self.move = test
            self.end_move()

    def background_motion(self, event):
        self.piece_highlight_off(event)
        self.motion_highlight_area(event)
        self.motionposition = (event.x, event.y)

    def motion_highlight_area(self, event): 
        # this could be nicer, perhaps combined with the click-handler
        clickable_areas = dict()
        clickable_areas[False] = [
            Size(600, 79, 24, 14),# self.menu_file_click),
            Size(628, 79, 59, 14),# self.menu_save_click),
            Size(691, 79, 44, 14),# self.menu_opts_click),
            Size(739, 79, 30, 14),# self.menu_help_click),
            Size(599, 114, 171, 14),# self.battlename_click),
            #Size(599, 157, 171, 13*25),# self.list_click),
            #Size(599, 146, 171, 10),# self.scrollup_click),
            #Size(599, 483, 171, 10),# self.scrolldown_click),
            Size(599, 510, 171, 14),# self.thudurl_click),
            ]
        clickable_areas['file'] = [
            Size(610, 91, 80, 14),
            Size(610,106, 80, 14),
            Size(610,121, 80, 14),
            Size(610,136, 80, 14),
            #Size(610,151, 80, 4),
            Size(610,156, 80, 14),
            Size(610,171, 80, 14),
            Size(610,186, 80, 14),
            Size(610,201, 80, 14),
            Size(610,216, 80, 14),
            Size(610,231, 80, 14),
            Size(610,246, 80, 14),
            Size(610,261, 80, 14),
            ]
        clickable_areas['options'] = [
            Size(701, 91, 80, 14),
            Size(701,106, 80, 14),
            Size(701,121, 80, 14),
            Size(701,136, 80, 14),
            ]
        clickable_areas['help'] = [
            Size(679, 91, 80, 14),
            Size(679,106, 80, 14),
            Size(679,121, 80, 14),
            ]
        anyarea = False
        for area in clickable_areas[self.submenu]:
            if area.inside(event.x, event.y):
                if area!=self.highlight_area:
                    self.highlight_area = area
                    anyarea = True
                    self.background.delete("highlight_area")
                    self.background.create_rectangle(area.x, area.y,
                                     area.x + area.dx, area.y + area.dy,
                                     tag="highlight_area", 
                                     outline=self.skin.color['background'])
        if not anyarea:
            self.background.delete("highlight_area")

    def background_scrollers(self, event):
        self.background.focus_set()
        clickable_areas = [
            (Size(599, 146, 171, 10), -1),
            (Size(599, 483, 171, 10), +1),
            ]
        if self.toggle_comment:
            clickable_areas[1] = (Size(599, 405, 171, 10), +1)
        if not self.skip_next_click:
            for (area, direction) in clickable_areas:
                if area.inside(event.x, event.y):
                    self.motionposition = (event.x, event.y)
                    self.scrollerid = self.background.after(100, self.pollscroller, area, direction)

    def pollscroller(self, area, direction):
        if area.inside(self.motionposition[0], self.motionposition[1]):
            self.UpdateScroll(direction)
            self.scrollerid = self.background.after(self.scrollersensitivity, self.pollscroller, area, direction)
        elif self.scrollerid: 
            self.background.after_cancel(self.scrollerid)
            self.scrollerid = 0

    def temp_disable_background_click(self):
        self.background_disabled = True
        self.background.after(500, self.enable_background_click)
    def enable_background_click(self):
        self.background_disabled = False

    def background_area_click(self, event):
        if self.background_disabled:
            return
        if self.scrollerid:
            self.background.after_cancel(self.scrollerid)
            self.scrollerid = 0
        # handle possible outstanding stuff
        self.RemoveSubmenu()
        if self.move != ThudMove(-1, '??', '??', [], True, False):
            self.end_move()
        self.start_coord = self.drag_coord = ""
        # note: overlapping areas will be handled in order
        clickable_areas = [
            (Size(600, 79, 24, 15), self.menu_file_click),
            (Size(628, 79, 59, 15), self.menu_save_click),
            (Size(691, 79, 44, 15), self.menu_opts_click),
            (Size(739, 79, 30, 15), self.menu_help_click),
            (Size(599, 114, 171, 15), self.battlename_click),
            (Size(599, 157, 171, 13*self.listlines), self.list_click),
            (Size(599, 146, 171, 10), self.scrollup_click),
            (Size(599, 483, 171, 10), self.scrolldown_click),
            (Size(599, 510, 171, 15), self.thudurl_click),
            ]
        if self.toggle_comment:
            clickable_areas[7]= (Size(599, 405, 171, 10), self.scrolldown_click)

        if not self.skip_next_click:
            for (area, handler) in clickable_areas:
                if area.inside(event.x, event.y):
                    handler(event, area)
        self.skip_next_click = False

    # file menu
    def menu_file_click(self, event, area = Size(0,0,0,0)):
        items = [('new', self.file_new), 
                 ('open', self.file_open), 
                 ('save as', self.file_save_as),
                 ('save snapshot', self.file_save_snapshot),
                 #('skins', self.file_skins),
                 #('quit', self.file_quit),
                 ('_________', None), # separator
                 ]
        for index, item in zip(range(len(self.mru)), self.mru):
            handler = getattr(self, 'file_mru%d' % index)
            mruname = os.path.splitext(os.path.split(item)[1])[0]
            items.append(('%d. '%(index+1)+mruname[0:10]+(mruname[10:] and '...' or ''), handler))
        self.HandleSubmenu('file', area.x+4, area.y+4, items)

    def file_mru(self, mruindex):
        self.RemoveSubmenu()
        self.skip_next_click = True
        if not self.saved and not askyesno('Open game', \
                'Game in progress is not saved.\nDo you still want to open another game?'):
            return
        filename = self.mru[mruindex]
        if filename:
            if self.battle.load(filename):
                self.new_mru(filename)
                self.UpdateBattle()
                self.saved = True
                self.update_infoimg()
            else:
                showwarning('Load failed', \
                'Loading the battle failed. The file or folder may be write-only or corrupt.')
        self.skip_next_click = True

    def file_mru0(self, event = None):
        self.file_mru(0)
    def file_mru1(self, event = None):
        self.file_mru(1)
    def file_mru2(self, event = None):
        self.file_mru(2)
    def file_mru3(self, event = None):
        self.file_mru(3)
    def file_mru4(self, event = None):
        self.file_mru(4)
    def file_mru5(self, event = None):
        self.file_mru(5)
    def file_mru6(self, event = None):
        self.file_mru(6)
    def file_mru7(self, event = None):
        self.file_mru(7)

    def file_new(self, event = None):
        self.RemoveSubmenu()
        if not self.saved and not askyesno('New game', \
                'Game in progress is not saved.\nDo you still want to start a new game?'):
            return
        self.battle = ThudBattle()
        self.battle.battledir = self.battledir
        self.UpdateBattle()
        self.saved = True
        self.update_infoimg()
        self.skip_next_click = True

    def file_open(self, event = None):
        self.RemoveSubmenu()
        self.skip_next_click = True
        if not self.saved and not askyesno('Open game', \
                'Game in progress is not saved.\nDo you still want to open another game?'):
            return
        ft = [("Thud Battles", ".thud"),
              ("All files", "*")]
        filepath = os.path.split(self.battle.get_filename())[0]
        filename = askopenfilename(initialdir=filepath, filetypes = ft)
        if filename:
            if self.battle.load(filename):
                self.new_mru(filename)
                self.UpdateBattle()
                self.saved = True
                self.update_infoimg()
            else:
                showwarning('Load failed', \
                'Loading the battle failed. The file or folder may be write-only or corrupt.')

    def file_save_as(self, event = None):
        self.RemoveSubmenu()
        ft = [("Thud Battles", ".thud"),
              ("All files", "*")]
        filepath = os.path.split(self.battle.get_filename())[0]
        filename = asksaveasfilename(initialdir=filepath, filetypes = ft)
        if filename and filename!='<anonymous battle>':
            if not os.path.splitext(filename)[1]:
                filename+= ".thud"
            self.SaveGame(filename)
        self.skip_next_click = True

    def file_save_snapshot(self, event = None):
        self.RemoveSubmenu()
        ft = [("Thud Battles", ".thud"),
              ("All files", "*")]
        filepath = os.path.split(self.battle.get_filename())[0]
        filename = asksaveasfilename(initialdir=filepath, filetypes = ft)
        if filename and filename!='<anonymous battle>':
            if not os.path.splitext(filename)[1]:
                filename+= ".thud"
            if not self.battle.save_position(filename, self.battle_index):
                showwarning('Save failed', \
                'Saving the snapshot failed. The file or folder may be read-only or the disk may be full.')
            else:
                self.new_mru(filename)
        self.skip_next_click = True

    def file_quit(self, event = None):
        self.RemoveSubmenu()
        if not self.saved and not askyesno('Exit game', \
                'Game in progress is not saved.\nDo you still want to quit?'):
            return
        self.save_options(os.path.join(self.configdir, "thudboard.cfg"))
        self.skip_next_click = True
        self.destroy()

    def file_skins(self, event = None):
        self.RemoveSubmenu()
        dirname = askdirectory(initialdir='./img')
        if dirname:
            try:
                self.skin = skins.init(dirname)
                self.RedrawBoard()
            except:
                self.skin = skins.init()
                self.RedrawBoard()
            self.EventInitialize()
            self.UpdateBattle()
        self.skip_next_click = True

    # quicksave menu
    def menu_save_click(self, event, area = Size(0,0,0,0)):
        # quicksave won't work unless there is a filename
        if self.battle.name=='<anonymous battle>':
            ft = [("Thud Battles", ".thud"),
                  ("All files", "*")]
            filepath = os.path.split(self.battle.get_filename())[0]
            filename = asksaveasfilename(initialdir=filepath, filetypes = ft)
            if filename and filename!='<anonymous battle>':
                if not os.path.splitext(filename)[1]:
                    filename+= ".thud"
                self.SaveGame(filename)
        else:
            self.SaveGame()

    # options menu
    def menu_opts_click(self, event, area = Size(0,0,0,0)):
        items = [('boardmap', self.opts_boardmap, self.toggle_boardmap and self.skin.check), 
                 ('highlight', self.opts_highlight, self.toggle_highlight and self.skin.check), 
                 ('score', self.opts_score, self.toggle_score and self.skin.check),
                 ('comment', self.opts_comment, self.toggle_comment and self.skin.check)]
        self.HandleSubmenu('options', area.x+4, area.y+4, items)

    def opts_boardmap(self, event = None):
        self.RemoveSubmenu()
        self.toggle_boardmap = not self.toggle_boardmap
        self.canvas.delete(self.boardimg)
        img = self.toggle_boardmap and self.skin.boardgrid or self.skin.board
        self.boardimg = self.canvas.create_image(0, 0, image=img, anchor=tk.NW)
        self.canvas.tag_lower(self.boardimg, "thudstone")
        self.DrawPieces()
        self.skip_next_click = True

    def opts_highlight(self, event = None):
        self.RemoveSubmenu()
        self.toggle_highlight = not self.toggle_highlight
        self.skip_next_click = True

    def opts_score(self, event = None):
        self.RemoveSubmenu()
        self.toggle_score = not self.toggle_score
        self.DrawScore()
        self.skip_next_click = True
        
    def opts_comment(self, event = None):
        self.RemoveSubmenu()
        self.toggle_comment = not self.toggle_comment
        self.DrawCommentbox()
        self.skip_next_click = True

    # help menu
    def menu_help_click(self, event, area = Size(0,0,0,0)):
        items = [('contents', self.help_contents), 
                 ('howto', self.help_howto), 
                 ('about', self.help_about)]
        self.HandleSubmenu('help', area.x+area.dx-96, area.y+4, items) # right align

    def help_contents(self, event = None):
        self.RemoveSubmenu()
        webbrowser_open("file:///"+os.getcwd()+"/docs/index.html")
        self.skip_next_click = True

    def help_howto(self, event = None):
        self.RemoveSubmenu()
        webbrowser_open("file:///"+os.getcwd()+"/docs/howto.html")
        self.skip_next_click = True

    def help_about(self, event = None):
        self.RemoveSubmenu()
        webbrowser_open("file:///"+os.getcwd()+"/docs/about.html")
        self.skip_next_click = True

    # other items clicked
    def battlename_click(self, event, area = Size(0,0,0,0)):
        self.battle_index = -1
        self.battle.get_position(self.battle_index)
        self.MoveHighlightBattlename()
        self.DrawPieces()
        self.show_comment()

    def list_click(self, event, area = Size(0,0,0,0)):
        dy = ((event.y-self.size_list.y)/self.size_list.dy)
        i = self.battle.get_index(dy, self.list_offset)
        if i<0: return
        self.battle_index = i
        position = self.battle.get_position(self.battle_index)
        dy, h = self.battle.get_offset_and_height(self.battle_index, self.list_offset)
        self.DrawPieces()
        self.MoveHighlight(dy*self.size_list.dy, h*self.size_list.dy)
        self.show_comment()

    def scrollup_click(self, event, area = Size(0,0,0,0)):
        self.UpdateScroll(-1)

    def scrolldown_click(self, event, area = Size(0,0,0,0)):
        self.UpdateScroll(+1)

    def thudurl_click(self, event, area = Size(0,0,0,0)):
        webbrowser_open("http://www.thudgame.com")

    # handle the highlighters in the moves-list
    def MoveHighlight(self, dy, h):
        self.background.delete("move")
        if dy+h<=13*self.listlines and not self.battle_index<self.list_offset:
            self.background.create_rectangle(self.size_list.x, 
                                         self.size_list.y+dy, 
                                         self.size_list.x+self.size_list.dx, 
                                         self.size_list.y+dy+h, 
                                         tag="move", 
                                         outline=self.skin.color['background'], fill=self.skin.color['background'])
            self.background.create_line(self.size_list.x, 
                                         self.size_list.y+dy+h, 
                                         self.size_list.x+self.size_list.dx+1, 
                                         self.size_list.y+dy+h, 
                                         fill=self.skin.color['text'], 
                                         tag="move")
            self.copyicon = self.background.create_image(self.size_list.x+self.size_list.dx-30, 
                                         self.size_list.y+dy, 
                                         image=self.skin.copy, anchor=tk.NW,
                                         tag="move")
            self.background.tag_bind(self.copyicon, '<Button-1>', self.cmd_copy_click)
            self.update_paste(self.size_list.x+self.size_list.dx-15, self.size_list.y+dy)
            self.background.tag_lower("move", "movetxt")

    def MoveHighlightBattlename(self):
        self.background.delete("move")
        self.background.create_rectangle(self.size_battlename.x, 
                                     self.size_battlename.y, 
                                     self.size_battlename.x+self.size_battlename.dx, 
                                     self.size_battlename.y+self.size_battlename.dy, 
                                     tag="move", 
                                     outline=self.skin.color['background'], fill=self.skin.color['background'])
        self.background.create_line(self.size_battlename.x, 
                                     self.size_battlename.y+self.size_battlename.dy, 
                                     self.size_battlename.x+self.size_battlename.dx+1, 
                                     self.size_battlename.y+self.size_battlename.dy, 
                                     fill=self.skin.color['text'], 
                                     tag="move")
        self.update_paste(self.size_battlename.x+self.size_battlename.dx-15, self.size_battlename.y)
        self.background.tag_lower("move", "battlename")
        self.show_comment()

    def update_paste(self, x, y):
        try:
            txt = self.selection_get(selection='CLIPBOARD')
        except:
            txt = ''
        pasteimg = self.skin.noclip
        if txt:
            parts = txt.split('#')
            txt = parts[0].strip()
            comment = ''
            if len(parts)>1:
                comment = "\n".join([p.strip() for p in parts[1:]])
            txt = "".join(txt.splitlines()).replace(" ", "")
            test = ThudMove(-1, "", "", [])
            if test.load_str(txt, comment):
                pasteimg = self.skin.nopaste
                if self.battle.check_move(test):
                    pasteimg = self.skin.paste
        self.pasteicon = self.background.create_image(x, y, image=pasteimg, anchor=tk.NW, tag="move")
        if pasteimg==self.skin.paste:
            self.background.tag_bind(self.pasteicon, '<Button-1>', self.cmd_paste_click)

    def WriteMoves(self, movetxt):
        self.background.delete("movetxt")
        move_lines = movetxt.splitlines()
        h = 13
        dx = 30
        y = self.size_list.y
        moveno = (self.list_offset+1)/2
        for line in move_lines:
            dy = ((y-self.size_list.y)/self.size_list.dy)
            i = self.battle.get_index(dy, self.list_offset)
            locked = (i<=self.battle.saved_index)
            line = line.replace(">>> ", "").replace("~ ", "")
            if locked and line.strip()[0]!="x":
                self.background.create_image(self.size_list.x, y, 
                                             tag="movetxt", 
                                             image=self.skin.lock, anchor=tk.NW)
            if line.strip().startswith('d'):
                moveno+= 1
                self.background.create_text(self.size_list.x+dx, y, 
                                            text="%d." % moveno, font=self.font, 
                                            fill=self.skin.color['text'], 
                                            tag="movetxt", anchor=tk.NE)
            self.background.create_text(self.size_list.x+dx, y, 
                                        text=line, font=self.font, 
                                        fill=self.skin.color['text'], 
                                        tag="movetxt", anchor=tk.NW)
            y+= h

    def UpdateBattle(self):
        self.DrawCommentbox()
        self.battle_index = self.battle.index
        self.background.itemconfigure(self.text_gamename, text=self.battle.name)
        self.wm_title("ThudBoard [%s]" % self.battle.name)
        self.list_offset = self.battle.index
        mutable_offset = [self.list_offset]
        movetxt = "\n".join(self.battle.txt_battle_moves(mutable_offset, self.listlines, history_lines=12))
        self.list_offset = mutable_offset[0]
        if not self.startup:
            self.WriteMoves(movetxt)
        #self.background.itemconfigure(self.movetxt, text=movetxt)
        position = self.battle.get_position(self.battle_index)
        dy, h = self.battle.get_offset_and_height(self.battle_index, self.list_offset)
        self.MoveHighlight(dy*self.size_list.dy, h*self.size_list.dy)
        self.DrawPieces()
        self.init_move()
        self.show_comment()
        self.update_infoimg()

    # create square-highlighter and make it move with the mouse
    def piece_highlight_off(self, event = None):
        self.canvas.itemconfigure("highlight", state='hidden')

    def piece_highlight(self, event):
        if not self.toggle_highlight: return
        diffx, diffy = (event.x-self.tile.x), (event.y-self.tile.y)
        diffx = self.tile.dx * (diffx / self.tile.dx)
        diffy = self.tile.dy * (diffy / self.tile.dy)
        x = (self.tile.x+diffx)/self.tile.dx
        y = (self.tile.y+diffy)/self.tile.dy
        try:
            position = "%s%d" % (self.battle.remapx[x], 1+y)
        except:
            position = ""
        self.tile.x+= diffx
        self.tile.y+= diffy
        self.canvas.move("highlight", diffx, diffy)
        if position in self.battle.valid_position:
            self.canvas.itemconfigure(self.highlight_tooltip, text=position)
            self.canvas.itemconfigure("highlight", state='normal')
            self.canvas.lift("highlight")
        else:
            self.canvas.itemconfigure("highlight", state='hidden')

    def drag_highlight(self, position):
        self.canvas.delete("drag_highlight")
        if not self.toggle_highlight: return
        if position in self.battle.valid_position:
            x, y = self.GetPositionOrigin(position)
            self.canvas.create_rectangle(x, y, 
                                         x+self.tile.dx, 
                                         y+self.tile.dy, 
                                         tag="drag_highlight", 
                                         width=2, outline=self.skin.color['background'])
            self.canvas.create_rectangle(x+self.tile.dx-26, 
                                         y+self.tile.dy-14, 
                                         x+self.tile.dx, 
                                         y+self.tile.dy, 
                                         tag="drag_highlight", 
                                         outline=self.skin.color['background'], fill=self.skin.color['background'])
            self.drag_highlight_tooltip = self.canvas.create_text(
                                         x+self.tile.dx-3, 
                                         y+self.tile.dy, 
                                         text=position, font=self.font, 
                                         fill=self.skin.color['text'], 
                                         tag="drag_highlight", anchor=tk.SE)

    # drag'n'drop pieces
    def drag_start(self, event):
        self.background.focus_set()
        self.background.delete("submenu")
        x, y = event.x/self.tile.dx, event.y/self.tile.dy
        try:
            position = "%s%d" % (self.battle.remapx[x], 1+y)
        except:
            position = ""
        if position in self.pieces:
            self.startposition = self.dragposition = position
            if position==self.move.to_pos:
                # continue drag
                self.update_move(to_pos = self.startposition)
            elif self.move.from_pos=='??':
                self.update_move(from_pos = self.startposition, to_pos = '??')
            else:
                self.end_move()
                self.update_move(from_pos = self.startposition, to_pos = '??')
            self.canvas.lift(self.pieces[self.startposition])
        else:
            self.end_move()
            self.startposition = self.dragposition = ""

    def drag(self, event):
        if not self.dragposition: return
        x, y = event.x/self.tile.dx, event.y/self.tile.dy
        try:
            position = "%s%d" % (self.battle.remapx[x], 1+y)
        except:
            position = ""
        self.drag_highlight(position)
        if position in self.battle.valid_position:
            diffx = (self.battle.mapx[position[0]]-self.battle.mapx[self.dragposition[0]]) \
                     *self.tile.dx
            diffy = (int(position[1:])-int(self.dragposition[1:])) \
                     *self.tile.dy
            self.canvas.move(self.pieces[self.startposition], diffx, diffy)
            self.dragposition = position
            self.update_move(to_pos = self.dragposition)

    def drag_stop(self, event):
        if not self.dragposition: return
        x, y = event.x/self.tile.dx, event.y/self.tile.dy
        try:
            position = "%s%d" % (self.battle.remapx[x], 1+y)
        except:
            position = ""
        if position not in self.battle.valid_position:
            position = self.dragposition
        if position!=self.startposition:
            self.update_move(to_pos = position)
            if position in self.pieces:
                self.update_move(thud_list_append = [position])
                self.canvas.itemconfigure(self.pieces[position], 
                                          state='hidden')
                del(self.pieces[position])
            self.pieces[position] = self.pieces[self.startposition]
            del(self.pieces[self.startposition])
        self.startposition = self.dragposition = ""
        self.drag_highlight("H8")

    # hide captured pieces
    def hide_piece(self, event):
        x, y = event.x/self.tile.dx, event.y/self.tile.dy
        try:
            position = "%s%d" % (self.battle.remapx[x], 1+y)
        except:
            position = ""
        if position in self.pieces:
            self.update_move(thud_list_append = [position])
            self.canvas.itemconfigure(self.pieces[position], state='hidden')
            del(self.pieces[position])

    # handle moves
    def init_move(self):
        self.move = ThudMove(-1, '??', '??', [], True, False)
        self.moving = False

    def start_move(self):
        self.move = ThudMove(-1, '??', '??', [], True, False)
        self.moving = True
        self.show_move()

    def update_move(self, from_pos=None, to_pos=None, thud_list_append=None):
        if not self.moving: 
            self.start_move()
        if not from_pos is None:
            self.move.from_pos = from_pos
            self.move.piece = self.battle.piece_on(from_pos)
        if not to_pos is None:
            self.move.to_pos = to_pos
        if not thud_list_append is None:
            self.move.thud_list+= thud_list_append
        self.show_move()

    def end_move(self):
        if not self.moving: 
            self.show_move()
            return
        if self.move.from_pos=='??' or self.move.to_pos=='??':
            self.DrawPieces() # updates self.pieces!
            self.init_move()
            self.show_move()
            return
        if self.battle.add_thudmove(self.move):
            self.saved = False
            self.battle_index = self.battle.index
        else:
            self.bell()
            if self.battle_index<self.battle.saved_index:
                showwarning('Locked move', \
                            'The move is locked.\nChange the lock by selecting a line and saving the battle.')
        # refresh battle positions
        position = self.battle.get_position(self.battle_index)
        dy, h = self.battle.get_offset_and_height(self.battle_index, self.list_offset)
        mutable_offset = [self.list_offset]
        movetxt = "\n".join(self.battle.txt_battle_moves(mutable_offset, self.listlines))
        self.list_offset = mutable_offset[0]
        self.WriteMoves(movetxt)
        #self.background.itemconfigure(self.movetxt, text=movetxt)
        self.DrawPieces()
        self.MoveHighlight(dy*self.size_list.dy, h*self.size_list.dy)
        self.init_move()
        self.show_move()
        self.DrawCommentbox()
        self.show_comment()
        self.update_infoimg()

    def show_move(self):
        if not self.moving: 
            self.background.delete("movingtxt")
            self.background.delete("moving")
            return
        if self.battle.index==-1:
            dy, h = -1, 1
        else:
            dy, h = self.battle.get_offset_and_height(self.battle.index, self.list_offset)
        dy = (dy+h) * self.size_list.dy
        h = self.move.get_height() * self.size_list.dy
        if dy+h>13*self.listlines:
            self.UpdateScroll(+1)
            dy, h = self.battle.get_offset_and_height(self.battle.index, self.list_offset)
            dy = (dy+h) * self.size_list.dy
            h = self.move.get_height() * self.size_list.dy
        self.background.delete("movingtxt")
        self.current_movetxt = self.background.create_text(
                                     self.size_list.x, 
                                     self.size_list.y+dy, 
                                     text=self.move.txt_move(), 
                                     font=self.font, 
                                     fill=self.skin.color['text'], 
                                     tag="movingtxt", anchor=tk.NW)
        self.background.delete("moving")
        self.background.create_rectangle(self.size_list.x, 
                                     self.size_list.y+dy, 
                                     self.size_list.x+self.size_list.dx, 
                                     self.size_list.y+dy+h, 
                                     tag="moving", 
                                     outline=self.skin.color['background'], fill=self.skin.color['background'])
        self.background.create_line(self.size_list.x, 
                                     self.size_list.y+dy, 
                                     self.size_list.x+self.size_list.dx+1, 
                                     self.size_list.y+dy, 
                                     fill=self.skin.color['text'], 
                                     tag="moving")
        self.background.tag_lower("moving", "movingtxt")

    def GetPositionOrigin(self, position):
        posx = self.battle.mapx[position[0]]
        posy = int(position[1:])-1
        return posx * self.tile.dx, posy * self.tile.dy

    def DrawInitialize(self):
        # draw background
        self.background = tk.Canvas(self, width=790, height=540, 
                                borderwidth=0, bg=self.skin.color['background'], highlightthickness=0)
        self.background.pack()
        self.RedrawBoard()

    def RedrawBoard(self):
        self.background.delete()
        for x in range(4):
            for y in range(5):
                self.background.create_image(x*200, y*130, 
                                             image=self.skin.bcktile, 
                                             anchor=tk.NW)
        # draw board
        self.canvas = tk.Canvas(self.background, width=540, height=540, 
                                borderwidth=0, bg=self.skin.color['background'], highlightthickness=0)

        img = self.toggle_boardmap and self.skin.boardgrid or self.skin.board
        self.boardimg = self.canvas.create_image(0, 0, image=img, anchor=tk.NW)
        self.canvas.create_image(7*self.tile.dx, 7*self.tile.dy, 
                                 image=self.skin.stone, tag="thudstone", anchor=tk.NW)
        self.background.create_window(0, 0, window=self.canvas, anchor=tk.NW)

        self.background.create_image(540, 0, image=self.skin.score, anchor=tk.NW)
        scorefont = ('arial', 14, 'bold')
        self.score_dwarf = self.background.create_text(560, 390, text="0", 
                                                       font=scorefont, fill=self.skin.color['score'])
        self.score_troll = self.background.create_text(560, 410, text="0", 
                                                       font=scorefont, fill=self.skin.color['score'])
        self.background.create_image(590,   9, image=self.skin.thudtitle, anchor=tk.NW)
        self.background.create_image(590,  72, image=self.skin.banner, anchor=tk.NW)
        self.background.create_image(590, 107, image=self.skin.banner, anchor=tk.NW)
        self.background.create_image(590, 142, image=self.skin.list, anchor=tk.NW)
        self.background.create_image(590, 503, image=self.skin.banner, anchor=tk.NW)

        self.menu_file = self.background.create_text(604, 86, font=self.font, text="File", fill=self.skin.color['text'], anchor=tk.W)
        self.menu_save = self.background.create_text(632, 86, font=self.font, text="Quicksave", fill=self.skin.color['text'], anchor=tk.W)
        self.menu_opts = self.background.create_text(695, 86, font=self.font, text="Options", fill=self.skin.color['text'], anchor=tk.W)
        self.menu_help = self.background.create_text(743, 86, font=self.font, text="Help", fill=self.skin.color['text'], anchor=tk.W)
        self.text_gamename = self.background.create_text(685, 121, font=self.font, text="<anonymous battle>", fill=self.skin.color['text'], tag="battlename")
        self.text_about = self.background.create_text(685, 517, font=self.font, fill=self.skin.color['text'], 
                                                      text="http://www.thudgame.com")
        self.movetxt = self.background.create_text(
                                     self.size_list.x, 
                                     self.size_list.y, 
                                     text=text_mini_howto, 
                                     font=self.font, 
                                     fill=self.skin.color['text'], 
                                     tag="movetxt", anchor=tk.NW)
        # create highlight
        self.canvas.create_rectangle(self.tile.x, self.tile.y, 
                                     self.tile.x+self.tile.dx, 
                                     self.tile.y+self.tile.dy, 
                                     tag="highlight", 
                                     width=2, outline=self.skin.color['background'])
        self.canvas.create_rectangle(self.tile.x+self.tile.dx-26, 
                                     self.tile.y+self.tile.dy-14, 
                                     self.tile.x+self.tile.dx, 
                                     self.tile.y+self.tile.dy, 
                                     tag="highlight", 
                                     outline=self.skin.color['background'], fill=self.skin.color['background'])
        self.highlight_tooltip = self.canvas.create_text(
                                     self.tile.x+self.tile.dx-3, 
                                     self.tile.y+self.tile.dy, 
                                     text="H8", font=self.font, 
                                     fill=self.skin.color['text'], 
                                     tag="highlight", anchor=tk.SE)
        self.canvas.itemconfigure("highlight", state='hidden')
        # initially hidden commentsbox
        self.downimg = self.background.create_image(599, 409, image=self.skin.down, anchor=tk.NW, tag="downimg", state='hidden')
        self.infoimg = self.background.create_image(599, 115, image=self.skin.info, anchor=tk.NW, tag="infoimg", state='hidden')
        self.commentsbox = tk.Text(self.background, borderwidth=1, background=self.skin.color['score'], foreground=self.skin.color['text']);
        self.DrawCommentbox()

    # draw pieces on the board
    def DrawPieces(self):
        def place_piece(piece, coord):
            if piece not in (self.battle.dwarf, self.battle.troll): return
            posx = self.battle.mapx[coord[0]]
            posy = int(coord[1:])-1
            img = piece is self.battle.dwarf and self.skin.dwarf or self.skin.troll
            tag = piece is self.battle.dwarf and 'dwarf' or 'troll'
            self.pieces[coord] = self.canvas.create_image(
                                            posx*self.tile.dx, 
                                            posy*self.tile.dy, 
                                            image=img, tag=tag, 
                                            anchor=tk.NW)
        for piece in self.pieces.values():
            self.canvas.delete(piece)
        self.pieces = {}
        position = self.battle.get_position(self.battle.index)
        for piece in (self.battle.dwarf, self.battle.troll):
            for coord in position[piece]:
                place_piece(piece, coord)
        # update score
        self.DrawScore()

    def DrawScore(self):
        position = self.battle.get_position(self.battle.index)
        d = len(position[self.battle.dwarf])
        cd = 32-d
        t = len(position[self.battle.troll])
        ct = 8-t
        if self.toggle_score: 
            d = cd
            t = ct
        self.background.itemconfigure(self.score_dwarf, text="%d" % d)
        self.background.itemconfigure(self.score_troll, text="%d" % t)
        # draw pieces
        self.background.delete("captures")
        x, y = 542, 353-11*cd
        for i in range(cd):
            self.background.create_image(x, y, image=self.skin.dwarf, anchor=tk.NW, tag="captures")
            y+= 11
        y = 512-11*ct
        for i in range(ct):
            self.background.create_image(x, y, image=self.skin.troll, anchor=tk.NW, tag="captures")
            y+= 11

    def RemoveSubmenu(self):
        self.background.delete("submenu")
        self.background.delete("highlight_area")
        self.submenu = False

    def HandleSubmenu(self, id, x, y, items):
        self.submenu = id
        self.background.create_image(x, y, image=self.skin.submenutop, tag="submenu", anchor=tk.NW)
        y+=8
        dx=8
        self.submenus = []
        for i in range(len(items)):
            img = False
            if len(items[i])>2:
                img = items[i][2] # may still be false
                dx=18
            if items[i][0].startswith('__') and items[i][1]==None: # separator
                h = 5
                self.submenus.append(self.background.create_image(x, y, image=self.skin.submenusep, tag="submenu", anchor=tk.NW))
                self.submenus.append(self.background.create_text(x+dx, y+7, tag="submenu", text='', fill=self.skin.color['text'], anchor=tk.W, font=self.font))
            else:
                h = 15
                self.submenus.append(self.background.create_image(x, y, image=self.skin.submenuitem, tag="submenu", anchor=tk.NW))
                if img:
                    checkboximg = self.background.create_image(x+8, y+1, image=img, tag="submenu", anchor=tk.NW)
                    self.background.tag_bind(checkboximg, '<Button-1>', items[i][1])
                self.submenus.append(self.background.create_text(x+dx, y+7, tag="submenu", text=items[i][0], fill=self.skin.color['text'], anchor=tk.W, font=self.font))
                self.background.tag_bind(self.submenus[2*i], '<Button-1>', items[i][1])
                self.background.tag_bind(self.submenus[2*i+1], '<Button-1>', items[i][1])
            y+= h
        self.background.create_image(x, y, image=self.skin.submenubottom, tag="submenu", anchor=tk.NW)

    # actually save the game
    def SaveGame(self, filename = None):
        if not self.battle.save(filename):
            showwarning('Save failed', \
                        'Saving the battle failed. The file or folder may be read-only or the disk may be full.')
            return
        self.battle.saved_index = self.battle_index
        self.saved = True
        self.new_mru(filename or self.battle.get_filename())
        mutable_offset = [self.list_offset]
        movetxt = "\n".join(self.battle.txt_battle_moves(mutable_offset, self.listlines))
        self.list_offset = mutable_offset[0]
        self.WriteMoves(movetxt)
        self.background.itemconfigure(self.text_gamename, text=self.battle.name)
        self.wm_title("ThudBoard [%s]" % self.battle.name)

    def UpdateScroll(self, direction):
        self.list_offset+= direction
        if self.list_offset<0:
            self.list_offset=0
        if self.list_offset>=len(self.battle.moves):
            self.list_offset=len(self.battle.moves)-1
        mutable_offset = [self.list_offset]
        movetxt = "\n".join(self.battle.txt_battle_moves(mutable_offset, self.listlines))
        self.list_offset = mutable_offset[0]
        self.WriteMoves(movetxt)
        # now, move highlighted line...
        dy, h = self.battle.get_offset_and_height(self.battle_index, self.list_offset)
        self.MoveHighlight(dy*self.size_list.dy, h*self.size_list.dy)

    def save_options(self, filename):
        config = ConfigParser.ConfigParser()
        config.read(filename)

        if not config.has_section('options'):
            config.add_section('options')
        if not config.has_section('mru'):
            config.add_section('mru')
        try:
            config.set('options', 'boardmap', self.toggle_boardmap)
            config.set('options', 'highlight', self.toggle_highlight)
            config.set('options', 'score', self.toggle_score)
            config.set('options', 'comment', self.toggle_comment)
            for index, value in zip(range(self.mru_count), self.mru[0:self.mru_count]):
                config.set('mru', 'mru%d' % index, value)
            fp = open(filename, 'w+b')
            config.write(fp)
            fp.close()
        except:
            showwarning('Save options failed', \
                        'Saving the configuration failed. The file "'+filename+'" or folder may be read-only or corrupt.')

    def load_options(self, filename):
        config = ConfigParser.ConfigParser()
        config.read(filename)
        try:
            self.toggle_boardmap = config.getboolean('options', 'boardmap')
            self.toggle_highlight = config.getboolean('options', 'highlight')
            self.toggle_score = config.getboolean('options', 'score')
            self.toggle_comment = config.getboolean('options', 'comment')
            # config.items is new in python 2.3, so this will raise
            # an exception in 2.2, which is why mru doesn't work there.
            mruitems = config.items('mru')
            mruitems.sort() # uses item[0] ('mru%d') for sorting
            self.mru = [item[1] for item in mruitems]
        except:
            pass # no warning, defaults will be used
        self.mru = self.mru[0:self.mru_count]
        while (len(self.mru)<self.mru_count): self.mru.append('')

    def new_mru(self, filename):
        try: self.mru.remove(filename)
        except ValueError: pass # not in list, which is fine
        while (len(self.mru)<self.mru_count): self.mru.append('')
        self.mru.insert(0, filename)
        self.mru = self.mru[0:self.mru_count]

    def DrawCommentbox(self):
        if self.startup: return
        if self.toggle_comment:
            self.commentsbox.pack()
            self.commentsbox.place_configure(x=599, y=419, width=172, height=70)
            self.background.itemconfigure("downimg", state='normal')
            self.listlines = 19
        else:
            self.commentsbox.place_forget()
            self.commentsbox.pack_forget()
            self.background.itemconfigure("downimg", state='hidden')
            self.listlines = 25
            self.update_infoimg()
        mutable_offset = [self.list_offset]
        movetxt = "\n".join(self.battle.txt_battle_moves(mutable_offset, self.listlines))
        self.list_offset = mutable_offset[0]
        self.WriteMoves(movetxt)

    def show_comment(self):
        if self.battle_index>=0:
            self.commentsbox.delete("1.0", tk.END)
            self.commentsbox.insert(tk.END, self.battle.moves[self.battle_index].comment)
            #self.commentsbox.edit_modified(False)
        elif self.battle_index==-1:
            self.commentsbox.delete("1.0", tk.END)
            self.commentsbox.insert(tk.END, self.battle.comment)
            #self.commentsbox.edit_modified(False)

    def update_infoimg(self):
        if self.battle.has_comments():
            self.background.itemconfigure("infoimg", state='normal')
        else:
            self.background.itemconfigure("infoimg", state='hidden')


#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~#

if __name__=='__main__':
    main()
