#!/usr/bin/env python

"""
Manage and display experimental results.
"""

__license__ = 'MIT License <http://www.opensource.org/licenses/mit-license.php>'
__author__ = 'Lucas Theis <lucas@theis.io>'
__docformat__ = 'epytext'
__version__ = '0.4.3'

import sys
import os
import numpy
import random
import scipy
import socket

sys.path.append('./code')

from argparse import ArgumentParser
from pickle import Unpickler, dump
from subprocess import Popen, PIPE
from os import path
from warnings import warn
from time import time, strftime, localtime
from numpy import ceil, argsort
from numpy.random import rand, randint
from distutils.version import StrictVersion
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
from httplib import HTTPConnection
from getopt import getopt

class Experiment:
	"""
	@type time: float
	@ivar time: time at initialization of experiment

	@type duration: float
	@ivar duration: time in seconds between initialization and saving

	@type script: string
	@ivar script: stores the content of the main Python script

	@type platform: string
	@ivar platform: information about operating system

	@type processors: string
	@ivar processors: some information about the processors

	@type environ: string
	@ivar environ: environment variables at point of initialization

	@type hostname: string
	@ivar hostname: hostname of server running the experiment

	@type cwd: string
	@ivar cwd: working directory at execution time

	@type comment: string
	@ivar comment: a comment describing the experiment

	@type results: dictionary
	@ivar results: container to store experimental results

	@type commit: string
	@ivar commit: git commit hash

	@type modified: boolean
	@ivar modified: indicates uncommited changes

	@type filename: string
	@ivar filename: path to stored results

	@type seed: int
	@ivar seed: random seed used through the experiment

	@type versions: dictionary
	@ivar versions: versions of Python, numpy and scipy
	"""

	def __str__(self):
		"""
		Summarize information about the experiment.

		@rtype: string
		@return: summary of the experiment
		"""

		strl = []

		# date and duration of experiment
		strl.append(strftime('date \t\t %a, %d %b %Y %H:%M:%S', localtime(self.time)))
		strl.append('duration \t ' + str(int(self.duration)) + 's')
		strl.append('hostname \t ' + self.hostname)

		# commit hash
		if self.commit:
			if self.modified:
				strl.append('commit \t\t ' + self.commit + ' (modified)')
			else:
				strl.append('commit \t\t ' + self.commit)

		# results
		strl.append('results \t {' + ', '.join(map(str, self.results.keys())) + '}')

		# comment
		if self.comment:
			strl.append('\n' + self.comment)

		return '\n'.join(strl)



	def __del__(self):
		self.status(None)



	def __init__(self, filename='', comment='', seed=None, server=None, port=8000):
		"""
		If the filename is given and points to an existing experiment, load it.
		Otherwise store the current timestamp and try to get commit information
		from the repository in the current directory.

		@type  filename: string
		@param filename: path to where the experiment will be stored
		
		@type comment: string
		@param comment: a comment describing the experiment

		@type  seed: integer
		@param seed: random seed used in the experiment
		"""

		self.id = 0
		self.time = time()
		self.comment = comment
		self.filename = filename
		self.results = {}
		self.seed = seed
		self.script = ''
		self.cwd = ''
		self.platform = ''
		self.processors = ''
		self.environ = ''
		self.duration = 0
		self.versions = {}
		self.server = ''

		if self.seed is None:
			self.seed = int((time() + 1e6 * rand()) * 1e3) % 4294967295

		# set random seed
		random.seed(self.seed)
		numpy.random.seed(self.seed)

		if self.filename:
			# load given experiment
			self.load()

		else:
			# identifies the experiment
			self.id = randint(1E8)

			# check if a comment was passed via the command line
			parser = ArgumentParser(add_help=False)
			parser.add_argument('--comment')
			optlist, argv = parser.parse_known_args(sys.argv[1:])
			optlist = vars(optlist)

			# remove comment command line argument from argument list
			sys.argv[1:] = argv

			# comment given as command line argument
			self.comment = optlist.get('comment', '')

			# get OS information
			self.platform = sys.platform

			# arguments to the program
			self.argv = sys.argv
			self.script_path = sys.argv[0]

			try:
				with open(sys.argv[0]) as handle:
					# store python script
					self.script = handle.read()
			except:
				warn('Unable to read Python script.')

			# environment variables
			self.environ = os.environ
			self.cwd = os.getcwd()
			self.hostname = socket.gethostname()

			# store some information about the processor(s)
			if self.platform == 'linux2':
				cmd = 'egrep "processor|model name|cpu MHz|cache size" /proc/cpuinfo'
				with os.popen(cmd) as handle:
					self.processors = handle.read()
			elif self.platform == 'darwin':
				cmd = 'system_profiler SPHardwareDataType | egrep "Processor|Cores|L2|Bus"'
				with os.popen(cmd) as handle:
					self.processors = handle.read()

			# version information
			self.versions['python'] = sys.version
			self.versions['numpy'] = numpy.__version__
			self.versions['scipy'] = scipy.__version__

			# store information about git repository
			if path.isdir('.git'):
				# get commit hash
				pr1 = Popen(['git', 'log', '-1'], stdout=PIPE)
				pr2 = Popen(['head', '-1'], stdin=pr1.stdout, stdout=PIPE)
				pr3 = Popen(['cut', '-d', ' ', '-f', '2'], stdin=pr2.stdout, stdout=PIPE)
				self.commit = pr3.communicate()[0][:-1]

				# check if project contains uncommitted changes
				pr1 = Popen(['git', 'status', '--porcelain'], stdout=PIPE)
				pr2 = Popen(['egrep', '^.M'], stdin=pr1.stdout, stdout=PIPE)
				self.modified = pr2.communicate()[0]

				if self.modified:
					warn('Uncommitted changes.')
			else:
				# no git repository
				self.commit = None
				self.modified = False

			# server managing experiments 
			self.server = server
			self.port = port
			self.status('running')



	def status(self, status, **kwargs):
		if self.server:
			try:
				conn = HTTPConnection(self.server, self.port)
				conn.request('GET', '/version/')
				resp = conn.getresponse()

				if not resp.read().startswith('Experiment'):
					raise RuntimeError()

				HTTPConnection(self.server, self.port).request('POST', '', str(dict({
						'id': self.id,
						'version': __version__,
						'status': status,
						'hostname': self.hostname,
						'cwd': self.cwd,
						'script_path': self.script_path,
						'script': self.script,
						'comment': self.comment,
						'time': self.time,
					}, **kwargs)))
			except:
				warn('Unable to connect to \'{0}:{1}\'.'.format(self.server, self.port))



	def progress(self, progress):
		self.status('PROGRESS', progress=progress)



	def save(self, filename=None, overwrite=False):
		"""
		Store results. If a filename is given, the default is overwritten.

		@type  filename: string
		@param filename: path to where the experiment will be stored

		@type  overwrite: boolean
		@param overwrite: overwrite existing files
		"""

		self.duration = time() - self.time

		if filename is None:
			filename = self.filename
		else:
			# replace {0} and {1} by date and time
			tmp1 = strftime('%d%m%Y', localtime(time()))
			tmp2 = strftime('%H%M%S', localtime(time()))
			filename = filename.format(tmp1, tmp2)

			self.filename = filename

		# make sure directory exists
		try:
			os.makedirs(path.dirname(filename))
		except OSError:
			pass

		# make sure filename is unique
		counter = 0
		pieces = path.splitext(filename)

		if not overwrite:
			while path.exists(filename):
				counter += 1
				filename = pieces[0] + '.' + str(counter) + pieces[1]

			if counter:
				warn(''.join(pieces) + ' already exists. Saving to ' + filename + '.')

		# store experiment
		with open(filename, 'wb') as handle:
			dump({
				'version': __version__,
				'id': self.id,
				'time': self.time,
				'seed': self.seed,
				'duration': self.duration,
				'environ': self.environ,
				'hostname': self.hostname,
				'cwd': self.cwd,
				'argv': self.argv,
				'script': self.script,
				'script_path': self.script_path,
				'processors': self.processors,
				'platform': self.platform,
				'comment': self.comment,
				'commit': self.commit,
				'modified': self.modified,
				'versions': self.versions,
				'results': self.results}, handle, 1)

		self.status('SAVE', filename=filename, duration=self.duration)



	def load(self, filename=None):
		"""
		Loads experimental results from the specified file.

		@type  filename: string
		@param filename: path to where the experiment is stored
		"""

		if filename:
			self.filename = filename

		with open(self.filename, 'rb') as handle:
			res = load(handle)

			self.time = res['time']
			self.seed = res['seed']
			self.duration = res['duration']
			self.processors = res['processors']
			self.environ = res['environ']
			self.platform = res['platform']
			self.comment = res['comment']
			self.commit = res['commit']
			self.modified = res['modified']
			self.versions = res['versions']
			self.results = res['results']
			self.argv = res['argv'] \
				if StrictVersion(res['version']) >= '0.3.1' else None
			self.script = res['script'] \
				if StrictVersion(res['version']) >= '0.4.0' else None
			self.script_path = res['script_path'] \
				if StrictVersion(res['version']) >= '0.4.0' else None
			self.cwd = res['cwd'] \
				if StrictVersion(res['version']) >= '0.4.0' else None
			self.hostname = res['hostname'] \
				if StrictVersion(res['version']) >= '0.4.0' else None
			self.id = res['id'] \
				if StrictVersion(res['version']) >= '0.4.0' else None



	def __getitem__(self, key):
		return self.results[key]



	def __setitem__(self, key, value):
		self.results[key] = value



	def __delitem__(self, key):
		del self.results[key]



class ExperimentRequestHandler(BaseHTTPRequestHandler):
	"""
	Renders HTML showing running and finished experiments.
	"""

	xpck_path = ''
	running = {}
	finished = {}

	def do_GET(self):
		"""
		Renders HTML displaying running and saved experiments.
		"""

		# number of bars representing progress
		max_bars = 20

		if self.path == '/version/':
			self.send_response(200)
			self.send_header('Content-type', 'text/plain')
			self.end_headers()

			self.wfile.write('Experiment {0}'.format(__version__))

		elif self.path.startswith('/running/'):
			id = int([s for s in self.path.split('/') if s != ''][-1])

			# display running experiment
			if id in ExperimentRequestHandler.running:
				self.send_response(200)
				self.send_header('Content-type', 'text/html')
				self.end_headers()

				self.wfile.write(HTML_HEADER)
				self.wfile.write('<h2>Experiment</h2>')

				instance = ExperimentRequestHandler.running[id]

				num_bars = int(instance['progress']) * max_bars / 100

				self.wfile.write('<table>')
				self.wfile.write('<tr><th>Experiment:</th><td>{0}</td></tr>'.format(
					os.path.join(instance['cwd'], instance['script_path'])))
				self.wfile.write('<tr><th>Hostname:</th><td>{0}</td></tr>'.format(instance['hostname']))
				self.wfile.write('<tr><th>Status:</th><td class="running">{0}</td></tr>'.format(instance['status']))
				self.wfile.write('<tr><th>Progress:</th><td class="progress"><span class="bars">{0}</span>{1}</td></tr>'.format(
					'|' * num_bars, '|' * (max_bars - num_bars)))
				self.wfile.write('<tr><th>Start:</th><td>{0}</td></tr>'.format(
					strftime('%a, %d %b %Y %H:%M:%S', localtime(instance['time']))))
				self.wfile.write('<tr><th>Comment:</th><td>{0}</td></tr>'.format(
					instance['comment']  if instance['comment'] else '-'))
				self.wfile.write('</table>')

				self.wfile.write('<h2>Script</h2>')
				self.wfile.write('<pre>{0}</pre>'.format(instance['script']))
				self.wfile.write(HTML_FOOTER)

			elif id in ExperimentRequestHandler.finished:
				self.send_response(302)
				self.send_header('Location', '/finished/{0}/'.format(id))
				self.end_headers()

			else:
				self.send_response(200)
				self.send_header('Content-type', 'text/html')
				self.end_headers()

				self.wfile.write(HTML_HEADER)
				self.wfile.write('<h2>404</h2>')
				self.wfile.write('Requested experiment not found.')
				self.wfile.write(HTML_FOOTER)

		elif self.path.startswith('/finished/'):
			self.send_response(200)
			self.send_header('Content-type', 'text/html')
			self.end_headers()

			self.wfile.write(HTML_HEADER)

			id = int([s for s in self.path.split('/') if s != ''][-1])

			# display finished experiment
			if id in ExperimentRequestHandler.finished:
				instance = ExperimentRequestHandler.finished[id]

				if id in ExperimentRequestHandler.running:
					progress = ExperimentRequestHandler.running[id]['progress']
				else:
					progress = 100

				num_bars = int(progress) * max_bars / 100

				self.wfile.write('<h2>Experiment</h2>')
				self.wfile.write('<table>')
				self.wfile.write('<tr><th>Experiment:</th><td>{0}</td></tr>'.format(
					os.path.join(instance['cwd'], instance['script_path'])))
				self.wfile.write('<tr><th>Results:</th><td>{0}</td></tr>'.format(
					os.path.join(instance['cwd'], instance['filename'])))
				self.wfile.write('<tr><th>Status:</th><td class="finished">{0}</td></tr>'.format(instance['status']))
				self.wfile.write('<tr><th>Progress:</th><td class="progress"><span class="bars">{0}</span>{1}</td></tr>'.format(
					'|' * num_bars, '|' * (max_bars - num_bars)))
				self.wfile.write('<tr><th>Start:</th><td>{0}</td></tr>'.format(
					strftime('%a, %d %b %Y %H:%M:%S', localtime(instance['time']))))
				self.wfile.write('<tr><th>End:</th><td>{0}</td></tr>'.format(
					strftime('%a, %d %b %Y %H:%M:%S', localtime(instance['duration']))))
				self.wfile.write('<tr><th>Comment:</th><td>{0}</td></tr>'.format(
					instance['comment']  if instance['comment'] else '-'))
				self.wfile.write('</table>')

				self.wfile.write('<h2>Results</h2>')

				try:
					experiment = Experiment(os.path.join(instance['cwd'], instance['filename']))
				except:
					self.wfile.write('Could not open file.')
				else:
					self.wfile.write('<table>')
					for key, value in experiment.results.items():
						self.wfile.write('<tr><th>{0}</th><td>{1}</td></tr>'.format(key, value))
					self.wfile.write('</table>')

				self.wfile.write('<h2>Script</h2>')
				self.wfile.write('<pre>{0}</pre>'.format(instance['script']))


			else:
				self.wfile.write('<h2>404</h2>')
				self.wfile.write('Requested experiment not found.')

			self.wfile.write(HTML_FOOTER)

		else:
			files = []

			if 'xpck_path' in ExperimentRequestHandler.__dict__:
				if ExperimentRequestHandler.xpck_path != '':
					for path in ExperimentRequestHandler.xpck_path.split(':'):
						files += [os.path.join(path, f) for f in os.listdir(path) if f.endswith('.xpck')]
				
			if 'XPCK_PATH' in os.environ:
				for path in os.environ['XPCK_PATH'].split(':'):
					files += [os.path.join(path, f) for f in os.listdir(path) if f.endswith('.xpck')]

			self.send_response(200)
			self.send_header('Content-type', 'text/html')
			self.end_headers()

			self.wfile.write(HTML_HEADER)
			self.wfile.write('<h2>Running</h2>')

			# display running experiments
			if ExperimentRequestHandler.running:
				self.wfile.write('<table>')
				self.wfile.write('<tr>')
				self.wfile.write('<th>Experiment</th>')
				self.wfile.write('<th>Hostname</th>')
				self.wfile.write('<th>Status</th>')
				self.wfile.write('<th>Progress</th>')
				self.wfile.write('<th>Start</th>')
				self.wfile.write('<th>Comment</th>')
				self.wfile.write('</tr>')

				# sort ids by start time of experiment 
				times = [instance['time'] for instance in ExperimentRequestHandler.running.values()]
				ids = ExperimentRequestHandler.running.keys()
				ids = [ids[i] for i in argsort(times)][::-1]

				for id in ids:
					instance = ExperimentRequestHandler.running[id]
					num_bars = int(instance['progress']) * max_bars / 100

					self.wfile.write('<tr>')
					self.wfile.write('<td class="filepath"><a href="/running/{1}/">{0}</a></td>'.format(
						instance['script_path'], instance['id']))
					self.wfile.write('<td>{0}</td>'.format(instance['hostname']))
					self.wfile.write('<td class="running">{0}</td>'.format(instance['status']))
					self.wfile.write('<td class="progress"><span class="bars">{0}</span>{1}</td>'.format(
						'|' * num_bars, '|' * (max_bars - num_bars)))
					self.wfile.write('<td>{0}</td>'.format(strftime('%a, %d %b %Y %H:%M:%S',
						localtime(instance['time']))))
					self.wfile.write('<td class="comment">{0}</td>'.format(
						instance['comment']  if instance['comment'] else '-'))
					self.wfile.write('</tr>')

				self.wfile.write('</table>')

			else:
				self.wfile.write('No running experiments.')

			self.wfile.write('<h2>Saved</h2>')

			# display saved experiments
			if ExperimentRequestHandler.finished:
				self.wfile.write('<table>')
				self.wfile.write('<tr>')
				self.wfile.write('<th>Results</th>')
				self.wfile.write('<th>Status</th>')
				self.wfile.write('<th>Progress</th>')
				self.wfile.write('<th>Start</th>')
				self.wfile.write('<th>End</th>')
				self.wfile.write('<th>Comment</th>')
				self.wfile.write('</tr>')

				# sort ids by start time of experiment 
				times = [instance['time'] + instance['duration']
					for instance in ExperimentRequestHandler.finished.values()]
				ids = ExperimentRequestHandler.finished.keys()
				ids = [ids[i] for i in argsort(times)][::-1]

				for id in ids:
					instance = ExperimentRequestHandler.finished[id]

					if id in ExperimentRequestHandler.running:
						progress = ExperimentRequestHandler.running[id]['progress']
					else:
						progress = 100

					num_bars = int(progress) * max_bars / 100

					self.wfile.write('<tr>')
					self.wfile.write('<td class="filepath"><a href="/finished/{1}/">{0}</a></td>'.format(
						instance['filename'], instance['id']))
					self.wfile.write('<td class="finished">saved</td>')
					self.wfile.write('<td class="progress"><span class="bars">{0}</span>{1}</td>'.format(
						'|' * num_bars, '|' * (max_bars - num_bars)))
					self.wfile.write('<td>{0}</td>'.format(strftime('%a, %d %b %Y %H:%M:%S',
						localtime(instance['time']))))
					self.wfile.write('<td>{0}</td>'.format(strftime('%a, %d %b %Y %H:%M:%S',
						localtime(instance['time'] + instance['duration']))))
					self.wfile.write('<td class="comment">{0}</td>'.format(
						instance['comment']  if instance['comment'] else '-'))
					self.wfile.write('</tr>')

				self.wfile.write('</table>')

			else:
				self.wfile.write('No saved experiments.')

			self.wfile.write(HTML_FOOTER)



	def do_POST(self):
		instances = ExperimentRequestHandler.running
		instance = eval(self.rfile.read(int(self.headers['Content-Length'])))
		
		if instance['status'] is 'PROGRESS':
			if instance['id'] not in instances:
				instances[instance['id']] = instance
				instances[instance['id']]['status'] = 'running'
			instances[instance['id']]['progress'] = instance['progress']

		elif instance['status'] is 'SAVE':
			ExperimentRequestHandler.finished[instance['id']] = instance
			ExperimentRequestHandler.finished[instance['id']]['status'] = 'saved'

		else:
			if instance['id'] in instances:
				progress = instances[instance['id']]['progress']
			else:
				progress = 0
			instances[instance['id']] = instance
			instances[instance['id']]['progress'] = progress

		if instance['status'] is None:
			try:
				del instances[instance['id']]
			except:
				pass



class XUnpickler(Unpickler):
	"""
	An extension of the Unpickler class which resolves some backwards
	compatibility issues of Numpy.
	"""

	def find_class(self, module, name):
		"""
		Helps Unpickler to find certain Numpy modules.
		"""

		try:
			numpy_version = StrictVersion(numpy.__version__)

			if numpy_version >= '1.5.0':
				if module == 'numpy.core.defmatrix':
					module = 'numpy.matrixlib.defmatrix'

		except ValueError:
			pass

		return Unpickler.find_class(self, module, name)



def load(file):
	return XUnpickler(file).load()
		


def main(argv):
	"""
	Load and display experiment information.
	"""

	if len(argv) < 2:
		print 'Usage:', argv[0], '[--server] [--port=<port>] [--path=<path>] [filename]'
		return 0

	optlist, argv = getopt(argv[1:], '', ['server', 'port=', 'path='])
	optlist = dict(optlist)

	if '--server' in optlist:
		try:
			ExperimentRequestHandler.xpck_path = optlist.get('--path', '')
			port = optlist.get('--port', 8000)

			# start server
			server = HTTPServer(('', port), ExperimentRequestHandler)
			server.serve_forever()

		except KeyboardInterrupt:
			server.socket.close()

		return 0

	# load experiment
	experiment = Experiment(sys.argv[1])

	if len(argv) > 1:
		# print arguments
		for arg in argv[1:]:
			try:
				print experiment[arg]
			except:
				print experiment[int(arg)]
		return 0

	# print summary of experiment
	print experiment

	return 0



HTML_HEADER = '''<html>
	<head>
		<title>Experiments</title>
		<style type="text/css">
			body {
				font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
				font-size: 11pt;
				color: black;
				background: white;
				padding: 0pt 20pt;
			}

			h2 {
				margin-top: 20pt;
				font-size: 16pt;
			}

			table {
				border-collapse: collapse;
			}

			tr:nth-child(even) {
				background: #f4f4f4;
			}

			th {
				font-size: 12pt;
				text-align: left;
				padding: 2pt 10pt 3pt 0pt;
			}

			td {
				font-size: 10pt;
				padding: 3pt 10pt 2pt 0pt;
			}

			pre {
				font-size: 10pt;
				background: #f4f4f4;
				padding: 5pt;
			}

			a {
				text-decoration: none;
				color: #04a;
			}

			.running {
				color: #08b;
			}

			.finished {
				color: #390;
			}

			.comment {
				min-width: 200pt;
				font-style: italic;
			}

			.progress {
				color: #ccc;
			}

			.progress .bars {
				color: black;
			}
		</style>
	</head>
	<body>'''

HTML_FOOTER = '''
	</body>
</html>'''



if __name__ == '__main__':
	sys.exit(main(sys.argv))
