#!/usr/bin/env python

from Tkinter import *

import tkSimpleDialog
import tkMessageBox
import asynchat
import asyncore
import socket
import string
import random

class Connection(asynchat.async_chat):
	def __init__(self, host, port):
		asynchat.async_chat.__init__(self)
		self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
		self.buffer = ''
		self.set_terminator('\r\n')
		self.connect((host,port))
	def handle_connect(self):
		commands.connected()
	def collect_incoming_data(self, data):
		self.buffer = self.buffer + data
	def found_terminator(self):
		data = self.buffer
		self.buffer = ''
		print "got data:", data
		commands.q.put(data)
	def handle_close(self):
		print "closed"
		self.close()
		commands.disconnected()

class CommandQueue:
	def __init__(self):
		self.q = []
		self.ctl = 1
	def put(self, data):
		self.q.append(data)
		self.flush()
	def flush(self):
		if self.ctl == 1:
			while 1:
				try:
					c = self.q[0]
					del self.q[0]
					self.ctl = -1
					commands.recv(c)
					if self.ctl == -1:
						self.ctl = 1
				except IndexError:
					return
	def start(self):
		self.ctl = 1
		self.flush()
	def stop(self):
		self.ctl = 0

class ConnectDialog(tkSimpleDialog.Dialog):
	def body(self, master):
		l = Label(master, text="Connect to server")
		l.grid(row=0, column=0, columnspan=2)
		
		lhost = Label(master, text="Host:")
		lport = Label(master, text="Port:")

		lhost.grid(row=1, column=0, sticky=W)
		lport.grid(row=2, column=0, sticky=W)
		
		self.ehost = Entry(master)
		self.eport = Entry(master)

		self.ehost.insert(0, host)
		self.eport.insert(0, port)
		
		self.ehost.grid(row=1, column=1)
		self.eport.grid(row=2, column=1)
		
		return self.ehost
	def validate(self):
		try:
			port = string.atoi(self.eport.get())
			return 1
		except ValueError:
			return 0
	def apply(self):
		host = self.ehost.get()
		port = string.atoi(self.eport.get())
		commands.do_connect(host, port)

class GamesDialog(tkSimpleDialog.Dialog):
	def body(self, master):
		Label(master, text="Select the game to join").pack()
		self.v = IntVar()
		Radiobutton(master, text="Create a new game", variable=self.v, value=0).pack(anchor=W)
		for g in commands.games.keys():
			players = commands.games[g]
			p = string.join(players, ', ')
			n = len(players)
			t = "Game %d, %d players: %s" % (g, n, players)
			Radiobutton(master, text=t, variable=self.v, value=g).pack(anchor=W)
		frame = Frame(master)
		Label(frame, text="Player Name:").grid(row=0, column=0, pady=10)
		self.name = Entry(frame)
		self.name.insert(0, "Player")		# FIXME
		self.name.grid(row=0, column=1, pady=10)
		frame.pack()
	def apply(self):
		commands.join_game(self.v.get(), self.name.get())
			
class PlayDialog(tkSimpleDialog.Dialog):
	def body(self, master):
		l = Label(master, text="Start playing?")
		l.pack()
		return l
	def apply(self):
		commands.start_game()
	
class BattleScreen(Toplevel):
	def __init__(self, parent, year, surprise, aships, dships, acolor, dcolor):
		Toplevel.__init__(self, parent)
		self.transient(parent)
		self.width = 210
		self.canvas = Canvas(self, width=self.width, height=100, bg="white")
		self.abmp = "@aship.xbm"
		self.dbmp = "@dship.xbm"
		self.sbmp = "@shot.xbm"
		self.ash = []
		self.dsh = []
		self.shnumscale = 1
		while aships > 100 or dships > 100:
			self.shnumscale = self.shnumscale * 3
			aships = aships / 3
			dships = dships / 3
		self.draw_and_reg_ships(aships, dships, acolor, dcolor)
		self.parent = parent
		self.canvas.pack(padx=5, pady=5)
		self.st = StringVar()
		if surprise:
			self.st.set("Surprise Attack!")
		self.status = Label(self, textvariable=self.st)
		self.status.pack()
		self.delay(1000)
	def delay(self, time, func=None):
		commands.q.stop()
		self.update()
		self.after(time)
		if func:
			func()
		commands.q.start()
	def draw_and_reg_ships(self, aships, dships, acolor, dcolor):
		for i in range(aships):
			x = 8 * (i / 10) + 14
			y = 8 * (i % 10) + 14
			ship = self.canvas.create_bitmap(x, y, bitmap=self.abmp, foreground=acolor)
			self.ash.insert(0, ship)
		for i in range(dships):
			x = self.width - 14 - 8 * (i / 10)
			y = 86 - 8 * (i % 10)
			ship = self.canvas.create_bitmap(x, y, bitmap=self.dbmp, foreground=dcolor)
			self.dsh.insert(0, ship)
	def shoot(self, a, d):
		[ax, ay] = self.canvas.coords(a)
		[dx, dy] = self.canvas.coords(d)
		frames = 20
		vx = (dx - ax) / float(frames)
		vy = (dy - ay) / float(frames)
		s = self.canvas.create_bitmap(ax, ay, bitmap=self.sbmp)
		for i in range(frames):
			self.canvas.move(s, vx, vy)
			self.delay(2)
		self.canvas.delete(s)
	def choose(self, n):
		if n > 9:
			return random.randint(0, 9)
		elif n <= 1:
			return 0
		else:
			return random.randint(0, n - 1)
	def attk(self, who, howmany):
		if who:
			a = self.ash
			d = self.dsh
		else:
			a = self.dsh
			d = self.ash
		if len(a) < 1 or len(d) < 1: 
			return
		h = int(round(float(howmany) / self.shnumscale))
		while(h > 0):
			i = self.choose(len(a))
			j = self.choose(len(d))
			self.shoot(a[i], d[j])
			self.canvas.delete(d[j])
			del d[j]
			h = h - 1	
	def bwin(self, winner, id, planet):
		if winner:
			self.st.set("The attacker wins!") 
		else:
			self.st.set("The defender wins!")
		field.update_owner(planet, id)
		self.delay(1000, self.destroy)

class Planet:
	def __init__(self, x, y):
		self.cx = field.width * x + 1
		self.cy = field.width * y + 1
		self.ships = -1
		self.indust = -1
		self.bmp = field.canvas.create_bitmap(self.cx, self.cy, bitmap=field.bitmap, anchor=NW)
		field.canvas.tag_bind(self.bmp, "<Enter>", self.enter)
#		field.canvas.tag_bind(self.bmp, "<Leave>", self.leave)
		field.canvas.tag_bind(self.bmp, "<B1-Motion>", self.drag)
	def update(self, id, x, y, owner, ships, indust):
		self.id = id
		self.x = x
		self.y = y
		self.owner = owner
		if ships != -1:
			self.ships = ships
		if indust != -1:
			self.indust = indust
		field.canvas.itemconfigure(self.bmp, foreground=players.colour(owner))
	def update_ships(self, ships):
		self.ships = ships
	def update_owner(self, owner):
		self.owner = owner
		field.canvas.itemconfigure(self.bmp, foreground=players.colour(owner))
	def enter(self, event):
		planetinfo.show(self)
		field.mark(0, self)
	def leave(self, event):
		field.unmark()
		planetinfo.hide()
	def drag(self, event):
		if self.owner == players.myself:
			(target, ) = field.canvas.find_closest(event.x, event.y, start="planet")
			field.draw_arrow(self.bmp, target)
			commands.scale.configure(to=self.ships)
			commands.total.set(self.ships)
 
class Field:
	def __init__(self):
		self.canvas = Canvas(bg="white", borderwidth=0, relief=RIDGE, height=400, width=400)
		self.canvas.grid(row=0, column=0, rowspan=3)
		file = "planet.xbm"
		bitmap = BitmapImage(file=file)
		self.width = bitmap.width()
		self.height = bitmap.height()
		self.bitmap = "@" + file
		self.id = {}
		self.bmp = {}
		self.src = -1
		self.dst = -1
		self.box = None
	def setsize(self, x, y):
		w = self.width * x
		h = self.height * y
		self.canvas.configure(width=w, height=h)
		self.x = x
		self.y = y
	def update(self, id, x, y, owner, ships, indust):
		try:
			p = self.id[id]
		except KeyError:
			p = Planet(x, y)
			self.id[id] = p
			self.bmp[p.bmp] = p
		p.update(id, x, y, owner, ships, indust)
	def update_ships(self, id, ships):
		p = self.id[id]
		p.update_ships(ships)
	def update_owner(self, id, owner):
		p = self.id[id]
		p.update_owner(owner)
	def draw_arrow(self, src, dst):
		try:
			a = self.arrow
			try:
				d = self.bmp[dst]	# destination planet
			except KeyError:
				return
		except AttributeError:
			a = self.canvas.create_line(0, 0, 0, 0, arrow="last")
			self.arrow = a
		s = self.bmp[src]	# source planet
		d = self.bmp[dst]
		w = self.width / 2
		h = self.height / 2
		self.src = s.id
		self.dst = d.id
		commands.en_dis_move()
		self.canvas.coords(a, s.cx + w, s.cy + h, d.cx + w, d.cy + h)
	def hide_arrow(self):
		self.src = -1
		self.dst = -1
		try:
			self.canvas.delete(self.arrow)
			del self.arrow
		except AttributeError:
			pass
		commands.amount.set(0)
		commands.total.set(0)
		commands.scale.configure(to=0)
		commands.en_dis_move()
	def mark(self, id, p=None):
		if self.box:
			self.unmark()
		if p == None:
			p = self.id[id]
		[x, y] = self.canvas.coords(p.bmp)
		[x1, y1] = [x - 1, y - 1]
		[x2, y2] = [x1 + self.width, y1 + self.height]
		self.box = self.canvas.create_rectangle(x1, y1, x2, y2)
	def unmark(self):
		if self.box:
			self.canvas.delete(self.box)
			self.box = None

class Player:
	def __init__(self, id, name, client):
		self.id = id
		self.name = name
		self.client = client

class Players:
	colours = ["black", "red", "blue", "green", "yellow"]
	def __init__(self):
		self.frame = Frame(borderwidth=3, relief=RIDGE)
		l = Label(self.frame, text="Players")
		l.pack()
		self.frame.grid(row=0, column=1, sticky=N+S+E+W)
		independent = Player(0, "Independent", "")
		self.id = { 0: independent }  
	def add(self, id, name, client):
		if not id in self.id.keys():
			self.id[id] = Player(id, name, client)
	def colour(self, id):
		try:
			return self.colours[id]
		except IndexError:
			r = random.randint(0, 15) * 16
			g = random.randint(0, 15) * 16
			b = random.randint(0, 15) * 16
			rgb = "#%02x%02x%02x" % (r, g, b)
			self.colours[id] = rgb
			return rgb

class Stats:
	def __init__(self):
		self.frame = Frame(bg="white", borderwidth=3, relief=RIDGE)
		l = Label(self.frame, text="Stats")
		y = Label(self.frame, text="Year")
		ls = Label(self.frame, text="Ships")
		ltot = Label(self.frame, text="Total")
		lp = Label(self.frame, text="Port")
		lt = Label(self.frame, text="Transit")

		l.grid(row=0, column=0, columnspan=2)
		y.grid(sticky=W)
		ls.grid(columnspan=2)
		ltot.grid(sticky=W)
		lp.grid(sticky=W)
		lt.grid(sticky=W)

		self.frame.grid(row=1, column=1, sticky=N+S+E+W)

class PlanetInfo:
	def __init__(self):
		self.frame = Frame(borderwidth=3, relief=RIDGE)
		self.l = Label(self.frame, text="Planet Information")
		self.lx = Label(self.frame, text="X:")
		self.ly = Label(self.frame, text="Y:")
		self.lowner = Label(self.frame, text="Owner:")
		self.lships = Label(self.frame, text="Ships:")
		self.lindust = Label(self.frame, text="Indust:")
		self.vx = Label(self.frame)
		self.vy = Label(self.frame)
		self.vowner = Label(self.frame)
		self.vships = Label(self.frame)
		self.vindust = Label(self.frame)
		self.l.grid(row=0, column=0, columnspan=2)
		self.lx.grid(row=1, sticky=W)
		self.ly.grid(row=2, sticky=W)
		self.lowner.grid(row=3, sticky=W)
		self.lships.grid(row=4, sticky=W)
		self.lindust.grid(row=5, sticky=W)
		self.vx.grid(row=1, column=1, sticky=W)
		self.vy.grid(row=2, column=1, sticky=W)
		self.vowner.grid(row=3, column=1, sticky=W)
		self.vships.grid(row=4, column=1, sticky=W)
		self.vindust.grid(row=5, column=1, sticky=W)		
		self.frame.columnconfigure(1, weight=1)
		self.frame.grid(row=2, column=1, sticky=N+S+W+E)
	def show(self, planet):
		self.vx.configure(text=planet.x)
		self.vy.configure(text=planet.y)
		player = players.id[planet.owner]
		colour = players.colour(planet.owner)
		self.vowner.configure(text=player.name, fg=colour)
		ships = planet.ships
		indust = planet.indust
		if ships == -1:
			ships = "Unknown"
		if indust == -1:
			indust = "Unknown"		
		self.vships.configure(text=ships)
		self.vindust.configure(text=indust)
	def hide(self):
		self.vx.configure(text="")
		self.vy.configure(text="")
		self.vowner.configure(text="")
		self.vships.configure(text="")
		self.vindust.configure(text="")
		
class Commands:
	def __init__(self):
		self.frame = Frame(borderwidth=3, relief=RIDGE)
		self.amount = IntVar()
		self.amount.set(0)
		self.total = IntVar()
		self.total.set(0)
		self.scale = Scale(self.frame, orient=HORIZONTAL, showvalue=0, variable=self.amount, command=self.en_dis_move, from_=0, to=0)
		self.lamount = Label(self.frame, textvariable=self.amount)
		self.lof = Label(self.frame, text="of")
		self.ltotal = Label(self.frame, textvariable=self.total)
		self.bmove = Button(self.frame, text="Move", command=self.move, state=DISABLED)
		self.bdone = Button(self.frame, text="Turn Done", command=self.done)
		self.scale.pack(side=LEFT)
		self.lamount.pack(side=LEFT)
		self.lof.pack(side=LEFT)
		self.ltotal.pack(side=LEFT)
		self.bmove.pack(side=LEFT)
		self.bdone.pack(side=RIGHT)
		self.frame.grid(row=3, column=0, columnspan=2, sticky=N+S+E+W)
		self.connect_state = 0
		self.games = {}
		self.cmdhandler = {	'GSRV' : self.cmd_gsrv,
					'CLOK' : self.cmd_clok,
					'GAME' : self.cmd_game,
					'SLCT' : self.cmd_slct,
					'JNOK' : self.cmd_jnok,
					'GFLD' : self.cmd_gfld,
					'GPLR' : self.cmd_gplr,
					'PLNT' : self.cmd_plnt,
					'YEAR' : self.cmd_year,
					'MVOK' : self.cmd_mvok,
					'FORT' : self.cmd_fort,
					'BATT' : self.cmd_batt,
					'ATTK' : self.cmd_attk,
					'BWIN' : self.cmd_bwin,
					'KILL' : self.cmd_kill,
					'WINN' : self.cmd_winn,
					'ERRP' : self.cmd_errp,
					'ERRC' : self.cmd_errc,
					'ERRM' : self.cmd_errm,
					'ERRJ' : self.cmd_errj	}
		self.q = CommandQueue()
	def send(self, data):
		"Send data to the server"
		if self.connect_state:
			print "sending:", data
			self.conn.push(data + '\r\n')
		else:
			print "Error: Not connected, trying to send", data
	def recv(self, data):
		"Receive some data from the server"
		try:
			cmd = string.split(data, None, 1)
			c = string.upper(cmd[0])
		except IndexError:
			return
		try:
			args = cmd[1]
		except IndexError:
			args = ''
		
		del cmd
		if c in self.cmdhandler.keys():
			self.cmdhandler[c](args)
		else:
			print "Unknown command", c, args
	def connect(self):
		"The menu command Connect..."
		ConnectDialog(root, "Connect to server")
	def disconnect(self):
		"The menu command Disconnect"
		if self.connect_state:
			print "Disconnecting"
			gamemenu.entryconfig(1, state=DISABLED)
			self.send("QUIT")
			root.after(5000, self.hangup)
	def quit(self):
		"The menu command Quit"
		if tkMessageBox.askokcancel("Confirm exit", "Are you sure you want to quit?"):
			self.disconnect()
			root.destroy()
	def hangup(self):
		"Hang up a dead connection"
		if self.connect_state:
			print "Hanging up"
			self.conn.close()
			self.disconnected()
		else:
			print "Not connected, no need to hang up"
	def disconnected(self):
		"Invoked when the connection is closed"
		gamemenu.entryconfig(0, state=NORMAL)
		gamemenu.entryconfig(1, state=DISABLED)
		self.connect_state = 0
	def do_connect(self, host, port):
		"With the host and port number, actually connect"
		print "Connecting to host", host, "port", port
		self.conn = Connection(host, port)
		self.socket_check()
	def connected(self):
		"Invoked when the connection is succesful"
		print "Connected"
		gamemenu.entryconfig(0, state=DISABLED)
		gamemenu.entryconfig(1, state=NORMAL)
		self.connect_state = 1	
	def join_game(self, id, name):
		self.send("JOIN %d %s" % (id, name))
	def start_game(self):
		self.send("PLAY")
	def socket_check(self):
		"Periodically read incoming data from the server"
		asyncore.poll(timeout=0.0)
		self.check = root.after(100, self.socket_check)
	# All the commands from the server
	def cmd_gsrv(self, args):
		self.send("GCLT 0.21 gclt.py")	# FIXME hardwired
	def cmd_clok(self, args):
		pass
	def cmd_game(self, args):
		[idstr, nplayers, rest] = string.split(args, None, 2)
		players = string.split(rest)
		id = string.atoi(idstr)
		self.games[id] = players
	def cmd_slct(self, args):
		GamesDialog(root, "Select game to join")
	def cmd_jnok(self, args):
		[gamestr, idstr, name] = string.split(args, None, 3)
		game = string.atoi(gamestr)
		id = string.atoi(idstr)
		players.myself = id
		if id == 1:
			PlayDialog(root, "Start the game?")
	def cmd_gfld(self, args):
		[x, y, plnt, plr, protver] = string.split(args, None, 5)
		[x, y, plnt, plr] = map(string.atoi, [x, y, plnt, plr])
		field.setsize(x, y)
						# FIXME ships>other data?
	def cmd_gplr(self, args):
		[idstr, name, client] = string.split(args, None, 2)
		id = string.atoi(idstr)
		players.add(id, name, client)
	def cmd_plnt(self, args):
		[id, x, y, owner, ships, indust] = \
		     map(string.atoi, string.split(args, None, 6))
		field.update(id, x, y, owner, ships, indust)
	def cmd_year(self, args):
		self.bdone.configure(state=NORMAL)
	def cmd_mvok(self, args):
		[idstr, shipsstr, eta] = string.split(args, None, 3)
		id = string.atoi(idstr)
		ships = string.atoi(shipsstr)
		field.update_ships(id, ships)
	def cmd_fort(self, args):	# FIXME: this doesn't seem to get called? (panu)
		[id, owner, tships] = \
		     map(string.atoi, string.split(args, None, 5)[1:])
		field.update_ships(id, tships)
		field.update_owner(id, owner)
	def cmd_batt(self, args):
		arg = string.split(args, None, 7)
		year = arg[0]
		[planet, attacker, defender, surprise, aships, dships] = \
			map(string.atoi, arg[1:7])
		acolour = players.colour(attacker)
		dcolour = players.colour(defender)
		field.mark(planet)
		self.bscreen = BattleScreen(root, year, surprise, aships, dships, acolour, dcolour)
	def cmd_attk(self, args):
		arg = string.split(args)
		[who, howmany] = map(string.atoi, arg[:2])
		self.bscreen.attk(who, howmany)
	def cmd_bwin(self, args):
		arg = string.split(args)
		who = string.atoi(arg[0])
		winner = string.atoi(arg[1])
		planet = string.atoi(arg[4])
		self.bscreen.bwin(who, winner, planet)
		field.unmark()
	def cmd_fort(self, args):
		pass
	def cmd_kill(self, args):
		[idstr, reason] = string.split(args, None, 1)
		print "args:", args
		print "when split:", string.split(args, None, 1)
		tkMessageBox.showinfo("Player gone", reason)
	def cmd_winn(self, args):
		id = string.atoi(args)
		name = players.id[id].name
		tkMessageBox.showinfo("Winner", "%s is the winner!" % name)
	def cmd_errp(self, args):
		tkMessageBox.showerror("Error", "Permission error: %s" % args)
	def cmd_errc(self, args):
		tkMessageBox.showerror("Error", "Command error: %s" % args)
	def cmd_errm(self, args):
		tkMessageBox.showerror("Error", "Error moving: %s" % args)
	def cmd_errj(self, args):
		tkMessageBox.showerror("Error", "Error joining: %s" % args)
	# Buttons
	def move(self):
		a = self.amount.get()
		s = field.src
		d = field.dst
		self.send("MOVE %d %d %d" % (s, d, a))
		field.hide_arrow()
	def done(self):
		field.hide_arrow()
		self.bdone.configure(state=DISABLED)
		self.send("DONE")
	def en_dis_move(self, value=None):
		a = self.amount.get()	# Seems we can't trust the value...
		s = field.src
		d = field.dst
		if a > 0 and s >= 0 and d >= 0:
			self.bmove.configure(state=NORMAL)
		else:
			self.bmove.configure(state=DISABLED)

port = "42529"
host = "localhost"
		
root = Tk()
#root.tk_strictMotif(1)

menu = Menu()
gamemenu = Menu(menu, tearoff=0)
menu.add_cascade(label="Game", menu=gamemenu)

field = Field()
players = Players()
stats = Stats()
planetinfo = PlanetInfo()
commands = Commands()

gamemenu.add_command(label="Connect...", command=commands.connect)
gamemenu.add_command(label="Disconnect", command=commands.disconnect, state=DISABLED)
gamemenu.add_separator()
gamemenu.add_command(label="Quit", command=commands.quit)

field.setsize(20, 20)

root.config(menu=menu)
root.columnconfigure(1, minsize=200)

gamemenu.invoke(0)
root.mainloop()
