#!/usr/bin/env python
from __future__ import absolute_import, division, print_function, unicode_literals

"""
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.4'
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
if sys.version.startswith('3.'):
    from http.server import HTTPServer, BaseHTTPRequestHandler
    from http.client import HTTPConnection
else:
    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']) >= StrictVersion('0.3.1') else None
            self.script = res['script'] \
                if StrictVersion(res['version']) >= StrictVersion('0.4.0') else None
            self.script_path = res['script_path'] \
                if StrictVersion(res['version']) >= StrictVersion('0.4.0') else None
            self.cwd = res['cwd'] \
                if StrictVersion(res['version']) >= StrictVersion('0.4.0') else None
            self.hostname = res['hostname'] \
                if StrictVersion(res['version']) >= StrictVersion('0.4.0') else None
            self.id = res['id'] \
                if StrictVersion(res['version']) >= StrictVersion('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 >= StrictVersion('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))