#! /usr/bin/python

""" (c) 2009 JRG Productions """
""" Everything here written 100% by Joe Gillotti """

"""
PieStats! A better, simpler, faster stats system for Soldat
Copyright (C) 2009  Joseph 'jrgp' Gillotti

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""

# Some config
console_log_path = '../logs/'
kill_log_path = '../logs/kills/'
t_prefix = 'pys_'
version = '0.1'

# Get teh kewel shit
import glob 
import re
import MySQLdb 
import time
import os
import sys
import copy
from optparse import OptionParser

# Hold the regexes 
chats_regex = re.compile("\n(\d{2}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) \[(.+)\](.+)")
grabs_regex = re.compile("\n(\d\d-\d\d-\d\d \d\d:\d\d:\d\d) (.*) captured the (Red Flag|Blue Flag|Yellow Flag|Objective)")
returns_regex = re.compile("\n(\d\d-\d\d-\d\d \d\d:\d\d:\d\d) (.*) returned the (Red Flag|Blue Flag|Yellow Flag|Objective)")
caps_regex = re.compile("\n(\d\d-\d\d-\d\d \d\d:\d\d:\d\d) (.*) scores for (Alpha|Bravo|Charlie|Delta) Team")
date_regex = re.compile('(\d{2}\-\d{2}\-\d{2}\-\d{2})')
kill_regex = re.compile("\n---\s(\d{2}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2})\n(.+)\n(.+)\n(Ak\-74|Barret M82A1|Barrett M82A1|Bow|Chainsaw|Clusters|Combat Knife|Desert Eagles|FN Minimi|Flame Bow|Flamer|Grenade|HK MP5|Hands|LAW|M79|Ruger 77|Spas\-12|Stationary gun|Steyr AUG|USSOCOM|XM214 Minigun|Selfkill)")

# Function defs:

def strtotime(str):
	""" Convert dates from console logs into unix time. """
	return int(time.mktime(time.strptime(str, '%y-%m-%d %H:%M:%S'))) 

def getdate(str):
	""" Return date part of file name """
	get = re.findall(date_regex, str)
	if get:
		return get[0]
	else:
		return '00-00-00-00'

def player_id(player):
	""" Get player id. If doesn't exist, insert it and get new id """
	global player_id_cache
	player = player.strip()
	player = db.literal(player)
	
	# exist in cache?
	if player in player_id_cache:
		#print 'cache'
		return player_id_cache[player]
	
	c = db.cursor()
	c.execute('select `id` from `%splayers` where `name` = %s limit 1' % (t_prefix, player))
	if c.rowcount == 1:
		row = c.fetchone()
		c.close()
		player_id_cache[player] = row[0]
		return row[0]
	else:
		c = db.cursor()
		c.execute('insert into `%splayers` set name = %s' % (t_prefix, player))
		id = c.lastrowid
		player_id_cache[player] = id
		c.close()
		db.commit()
		return id

def save_setting(key, value):
	""" Save setting into settings table """
	key = db.literal(key)
	value = 'UNIX_TIMESTAMP()' if value == 'UNIX_TIMESTAMP()' else db.literal(value)

	c = db.cursor()
	c.execute('select s.value from `%ssettings` as s where s.key = %s limit 1' % (t_prefix, key))
	if c.rowcount == 1:
		current = c.fetchone()
		if current[0] != value:
			c = db.cursor()
			c.execute('update `%ssettings` as s set s.value = %s where s.key = %s' % (t_prefix, value, key))
			db.commit()
	else:
		c = db.cursor()
		c.execute('insert into `%ssettings` (`key`, value) values (%s, %s)' % (t_prefix, key, value))
		db.commit() # very fucking important. THIS IS INNODB!!

def inc_setting(setting, new):
	""" Increment a setting. Maybe a stat like number of kills or something """
	key = db.literal(setting)
	new = int(new)
	c = db.cursor()
	c.execute('update `%ssettings` set `value` = `value` + %d where `key` = %s limit 1' % (new, setting))
	affected = c.rowcount
	c.close()
	if affected == 0:
		c = db.cursor()
		c.execute('insert into `%ssettings` set `value` = %d where `key` = %s' % (new, setting))
		c.close()
		db.commit()


def get_setting(key):
	""" get a setting from the db """
	key = db.literal(key)
	c = db.cursor()
	c.execute('select s.value from `%ssettings` as s where s.key = %s limit 1' % (t_prefix, key))
	if c.rowcount == 1:
		value = c.fetchone()
		c.close()
		return value[0]
	else:
		c.close()
		return ''

def latest_console_log():
	""" Get latest console log name and size. """
	log = get_setting('last_console_log')
	if log == '':
		return ('00-00-00-00', '0')
	else:
		return log.split(';')

def latest_kill_log():
	""" Get latest kill log name and size. """
	log = get_setting('last_kill_log')
	if log == '':
		return ('00-00-00-00', '0')
	else:
		return log.split(';')

def parse_console_logs():
	""" Parse and collect data from console logs """
	# Need to be able to modify these
	global new_console_log_name, new_console_log_size

	# Latest console log, and size
	last_console_log = latest_console_log()
	last_console_log_date = getdate(last_console_log[0])

	# Get list of console logs
	console_log_files = glob.glob('%sConsoleLog-*-*-*-*.txt' % (console_log_path)) 

	# Nothing?
	if len(console_log_files) == 0:
		return

	# Go through the files
	for filepath in console_log_files:

		# Exclude path
		fn = os.path.basename(filepath)

		# And old file? Skip it
		if getdate(fn) < last_console_log_date:
			print 'Skipping %s' % fn
			continue
		
		# Our latest file?
		elif getdate(fn) == last_console_log_date:
			
			# If it is the same file size, skip it
			if int(last_console_log[1]) == os.path.getsize(filepath):
				print 'Skipping unchanged file %s' % fn
				return # just end here
			else: # Otherwise start working from where we left off
				print 'Working on current file %s' % fn
				f = open(filepath, 'r')
				f.seek(int(last_console_log[1]) - 1, 0)
		
		# See if it's a new file.
		elif getdate(fn) > last_console_log_date:
			print 'Working on new file %s' % fn 
			f = open(filepath, 'r')
		
		# Get file's contents
		contents = f.read()

		# Do chats
		if parse_messages:
			matches = re.findall(chats_regex, contents)
			if matches:
				for match in matches:
					if match[2].strip() != '' and match[2].find('joining game') == -1:
						chats.append({'name':match[1], 'date':strtotime(match[0]), 'msg':match[2]})

		# Do grabs
		matches = re.findall(grabs_regex, contents)
		if matches:
			for match in matches:
				if match[1] in players:
					players[match[1]]['grabs'] += 1
				else:
					players[match[1]] = copy.deepcopy(player_template)
					players[match[1]]['grabs'] = 1

		# Do returns
		matches = re.findall(returns_regex, contents)
		if matches:
			for match in matches:
				if match[1] in players:
					if 'returns' in players[match[1]]:
						players[match[1]]['returns'] += 1
					else:
						players[match[1]]['returns'] = 1
				else:
					players[match[1]] = copy.deepcopy(player_template)
					players[match[1]]['returns'] = 1
		
		# Do caps 
		matches = re.findall(caps_regex, contents)
		if matches:
			for match in matches:
				if match[1] in players:
					if 'caps' in players[match[1]]:
						players[match[1]]['caps'] += 1
					else:
						players[match[1]]['caps'] = 1
				else:
					players[match[1]] = copy.deepcopy(player_template)
					players[match[1]]['caps'] = 1

		# Set most recent file position and name
		new_console_log_size = f.tell()
		new_console_log_name = fn

		# Close this file
		f.close()

def parse_kill_logs():
	""" Parse and collect data from console logs """
	
	# Need to be able to modify these
	global new_kill_log_size, new_kill_log_name

	# Latest kill log, and size
	last_kill_log = latest_kill_log()
	last_kill_log_date = getdate(last_kill_log[0])

	# Get list of kill logs
	kill_log_files = glob.glob('%sKillLog-*-*-*-*.txt' % (kill_log_path))	

	# Nothing?
	if len(kill_log_files) == 0:
		return

	# Go through the files
	for filepath in kill_log_files:

		# Exclude path
		fn = os.path.basename(filepath)

		# And old file? Skip it
		if getdate(fn) < last_kill_log_date:
			print 'Skipping %s' % fn
			continue
		
		# Our latest file?
		elif getdate(fn) == last_kill_log_date:
			
			# If it is the same file size, skip it
			if int(last_kill_log[1]) == os.path.getsize(filepath):
				print 'Skipping unchanged file %s' % fn
				return # just end here
			else: # Otherwise start working from where we left off
				print 'Working on current file %s' % fn
				f = open(filepath, 'r')
				f.seek(int(last_kill_log[1]) - 1, 0)
		
		# See if it's a new file.
		elif getdate(fn) > last_kill_log_date:
			print 'Working on new file %s' % fn 
			f = open(filepath, 'r')
		
		# Get file's contents
		contents = f.read()		
		
		# Do kills :D
		matches = re.findall(kill_regex, contents)
		if matches:
			for match in matches:

				#print match

				# Localize and prepare for database
				kill_date = strtotime(match[0])
				weapon = re.sub(' ', '-', match[3]) 
				
				# Fix weapon bug
				weapon = 'USSOCOM' if match[1] != match[2] and weapon == 'Selfkill' else weapon
				
				# A suicide?
				suicide = True if match[1] == match[2] else False
				
				# Make sure match[1] is there
				if match[1] not in players:
					players[match[1]] = copy.deepcopy(player_template)

				# Make sure match[2] is there, if not match[1], since that's a waste of a few microseconds
				if not suicide and match[2] not in players:
					players[match[2]] = copy.deepcopy(player_template)
				
				# Make sure weapons used by killer is stat'd
				if match[1] not in weapon_stats:
						weapon_stats[match[1]] = copy.deepcopy(weapon_stats_template)

				# Make sure weapons used by victim is stat'd
				if not suicide and match[2] not in weapon_stats:
						weapon_stats[match[2]] = copy.deepcopy(weapon_stats_template)
					
				# Make sure weapon is there
				if weapon not in weapons:
					weapons[weapon] = copy.deepcopy(weapon_template)
				
				# What?
				if suicide:
					players[match[1]]['suicides'] += 1
					weapons[weapon]['suicides'] += 1
					weapon_stats[match[1]][weapon+'_s'] += 1
				else:
					players[match[1]]['kills'] = players[match[1]]['kills'] + 1
					players[match[2]]['deaths'] += 1
					players[match[1]]['last_kill'] = kill_date
					weapons[weapon]['kills'] += 1
					weapon_stats[match[1]][weapon+'_k'] += 1
					weapon_stats[match[2]][weapon+'_d'] += 1
				
		# Set most recent file position and name
		new_kill_log_size = f.tell()
		new_kill_log_name = fn					
		
		# close file
		f.close()


# End function defs. Get working with them:

# Fun little message
print '[*] JRG Productions PyStats v%s' % version

# Connect to mysql
try:
	db = MySQLdb.connect('localhost', 'flieslikeabrick', 'tehnpw', 'soldat')
except MySQLdb.OperationalError, error:
	print 'Failed connecting to MySQL - %s' % error[1]
	sys.exit()

# Deal with CLI stuff	
cli_parser = OptionParser()
cli_parser.add_option('-p', '--prefix', dest='prefix', type='string', help='Use this prefix for the tables')
cli_parser.add_option('-f', '--folder', dest='path', type='string', help='Use this log/ path')
cli_parser.add_option('-m', dest='messages', action='store_true', default=False, help='Record all in-game messages')
cli_parser.add_option('-c', dest='clear', action='store_true', default=False, help='Empty tables')
cli_parser.add_option('-d', dest='delete', action='store_true', default=False, help='Delete tables')
cli_parser.add_option('-i', dest='install', action='store_true', default=False, help='Install. Create tables with this prefix.')
(cli_options, cli_args) = cli_parser.parse_args(sys.argv)

# Custom database prefixes
if cli_options.prefix:
	print '[*] Using table preifx prefix %s' % cli_options.prefix
	t_prefix = cli_options.prefix

# Work with in game messages? Slow and a waste of space. 
if cli_options.messages:
	parse_messages = True
else:
	parse_messages = False

# Install the tables under this prefix?
if cli_options.install:
	tables_file = '%s/tables.sql' % sys.path[0]
	if os.path.exists(tables_file):
		print 'Installing..'
		f = open(tables_file, 'r')
		statements = f.read()
		f.close()
		statements = re.sub('\/\*prefix\*\/', t_prefix, statements)
		statements = statements.split(';')
		for statement in statements:
			if statement.strip() != '':
				c = db.cursor()
				c.execute(statement)
				c.close()
				db.commit()
	else:
		print 'Cannot find tables.sql '
	
	sys.exit()

# Do we wanna clear everything?
if cli_options.clear:
	print 'Clearing..'
	# The order here is important..
	to_clear = ['weapon_stats', 'messages', 'weapons', 'settings', 'players']
	for table in to_clear:
		c = db.cursor()
		c.execute('truncate if exists `%s%s`' % (t_prefix, table))
		db.commit()
		c.close()
	sys.exit()

# Do we wanna delete everything?
if cli_options.delete:
	print 'Deleting..'
	# The order here is important..
	to_delete = ['weapon_stats', 'messages', 'weapons', 'settings', 'players']
	for table in to_delete:
		c = db.cursor()
		c.execute('drop table IF EXISTS `%s%s`' % (t_prefix, table))
		db.commit()
		c.close()
	sys.exit()

# Custom logfile paths
if cli_options.path:
	new_path = cli_options.path if cli_options.path[-1] == '/' else cli_options.path+'/'
	console_log_path = new_path
	kill_log_path = '%skills/' % new_path
	print "[*] Using %s for paths" % cli_options.path

# Base player dictionary.
player_template = {'grabs': 0,'caps': 0,'returns': 0,'kills': 0,'deaths': 0,'suicides': 0,'last_kill': 0}

# Base weapon dictionary
weapon_template = {'kills': 0,'suicides': 0}

# Player weapon stats dictionary
weapon_stats_template = {}

# Helps speed
player_id_cache = {}

# Populate it
c = db.cursor()
c.execute('explain `%sweapon_stats`' % t_prefix)
res = c.fetchall()
for field in res:
		if field[0].find('_k') > -1 or field[0].find('_s') > -1 or field[0].find('_d') > -1:
			weapon_stats_template[field[0]] = 0
c.close()

# Hold the chats. A tuple filled with dictionaries.
chats = []

# Hold the players. A multidimensional dictionary. Player names are keys.
players = {}

# Hold the weapons. Also a multidimensional dictionary. Weapon names are keys.
weapons = {}

# Hold the weapon stats
weapon_stats = {}

# New console log position
new_console_log_size = 0
new_console_log_name = ''

# New kill log position
new_kill_log_size = 0
new_kill_log_name = ''

# Handle console logs
parse_console_logs()

# Handle kill logs
parse_kill_logs()

# Start saving info
print '[*] Saving modified players (at least %d)' % len(players)

# Work with new and existing players
for k, v in players.iteritems():
	c = db.cursor()
	c.execute("""
		update `%splayers` set
			`returns` = `returns` + %d,
			`grabs` = `grabs` + %d,
			`caps` = `caps` + %d,
			`kills` = `kills` + %d,
			`deaths` = `deaths` + %d,
			`suicides` = `suicides` + %d,
			`last_kill` = %d
		where
			`id` = %d
		limit 1 
		""" % (
				t_prefix,
				v['returns'], v['grabs'], v['caps'],
				v['kills'], v['deaths'], v['suicides'],
				v['last_kill'],
				player_id(k)
			)
		)
	c.close()
	db.commit()


# Work with weapons
print '[*] Saving modified weapons (%d)' % len(weapons)

# Weapons themselves
for k, v in weapons.iteritems():
	c = db.cursor()
	c.execute("""
		update `%sweapons` set
			`kills` = `kills` + %d,
			`suicides` = `suicides` + %d
		where
			`weapon` = %s
		limit 1
	""" % (t_prefix, v['kills'], v['suicides'], db.literal(k)))
	affected = c.rowcount
	c.close()
	db.commit()
	
	# Weapon not there? Add it
	if affected == 0:
		c = db.cursor()
		c.execute("""
			insert into `%sweapons` set
			`kills` = %d,
			`suicides` = %d,
			`weapon` = %s
		""" % (t_prefix, v['kills'], v['suicides'], db.literal(k)))
		c.close()
		db.commit()

# Weapons for each player. Deliberately a second table.
for k, v in weapon_stats.iteritems():
	player = player_id(k)
	update_values = []
	insert_values = []
	for w, r in v.iteritems():
		update_values.append('`%s` = `%s` + %d' % (w, w, r))
		insert_values.append('`%s` = %d' % (w, r))
	update_string = ', '.join(update_values)
	insert_string = ', '.join(insert_values)
	c = db.cursor()
	c.execute("""
		update `%sweapon_stats` set
			%s
		where
			`player` = %d
		limit 1
	""" % (t_prefix, update_string, player))
	affected = c.rowcount
	c.close()
	db.commit()
	
	# Weapon stat for this player not there? Add it
	if affected == 0:
		c = db.cursor()
		c.execute("""
			insert into `%sweapon_stats` set 
				`player` = %d,
				%s
		""" % (t_prefix, player, insert_string))
		c.close()
		db.commit()
	
# Insert new chats
print '[*] Inserting new chats (%d)' % len(chats)

for chat in chats:
	c = db.cursor()
	c.execute("""
	insert into `%smessages`
	set
		`date` = %d,
		`playerid` = %d,
		`message` = %s
		""" % (t_prefix, chat['date'], player_id(chat['name']), db.literal(chat['msg'])))
	c.close()
	db.commit()


# Save our console log position, if need be
if new_console_log_name != '':
	save_setting('last_console_log', '%s;%d' % (new_console_log_name, new_console_log_size))

# Same goes for kill logs
if new_kill_log_name != '':
	save_setting('last_kill_log', '%s;%d' % (new_kill_log_name, new_kill_log_size))

# Whence updated
save_setting('last_updated', 'UNIX_TIMESTAMP()')

# That might just be it.
print 'Done'


