#!/usr/bin/env python
# -*- coding: utf-8 -*-
##############################################################################
# TermRecord.py                                                              #
#                                                                            #
# This file can either run the 'script' command as a wrapper, or parse its   #
# output with timing information.  It produces self-contained or dynamic     #
# HTML files capable of replaying the terminal session with just a modern    #
# browser.                                                                   #
#                                                                            #
#                                                                            #
#   Authors: Wolfgang Richter <wolfgang.richter@gmail.com>                   #
#                                                                            #
#                                                                            #
#   Copyright 2014 Wolfgang Richter and licensed under the MIT License.      #
#                                                                            #
##############################################################################



from argparse import ArgumentParser, FileType
from contextlib import closing
from json import dumps
from math import ceil
from os.path import basename,dirname
from struct import unpack
from subprocess import Popen
from sys import platform
from tempfile import NamedTemporaryFile

from jinja2 import FileSystemLoader, Template
from jinja2.environment import  Environment



DEFAULT_TEMPLATE    =   '/usr/local/share/TermRecord/templates/static.jinja2'



# http://blog.taz.net.au/2012/04/09/getting-the-terminal-size-in-python/
def probeDimensions(fd=1):
    """
    Returns height and width of current terminal. First tries to get
    size via termios.TIOCGWINSZ, then from environment. Defaults to 25
    lines x 80 columns if both methods fail.

    :param fd: file descriptor (default: 1=stdout)
    """
    try:
        import fcntl, termios, struct
        hw = struct.unpack('hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, '1234'))
    except:
        try:
            hw = (os.environ['LINES'], os.environ['COLUMNS'])
        except:  
            hw = (24, 80)

    return hw

# http://stackoverflow.com/a/8220141/3362361
def testOSX():
    return platform == 'darwin' 

def runScript(command=None):
    timingfname = None
    scriptfname = None
    CMD = ['script']

    with NamedTemporaryFile(delete=False) as timingf:
        with NamedTemporaryFile(delete=False) as scriptf:
            timingfname = timingf.name 
            scriptfname = scriptf.name

    CMD.append('-t%s' % timingfname)

    if command:
        CMD.append('-c')
        CMD.append(command)

    CMD.append(scriptfname)

    proc = Popen(CMD)
    proc.wait()
    return open(scriptfname, 'r'), open(timingfname, 'r') 

def runTtyrec(command=None):
    scriptfname = None
    CMD = ['ttyrec']

    with NamedTemporaryFile(delete=False) as scriptf:
        scriptfname = scriptf.name

    if command:
        CMD.append('-e')
        CMD.append(command)

    CMD.append(scriptfname)

    proc = Popen(CMD)
    proc.wait()
    return open(scriptfname, 'r') 

def getTiming(timef):
    timing = None 
    with closing(timef):
        timing = [l.strip().split(' ') for l in timef]
        timing = [(int(ceil(float(r[0]) * 1000)), int(r[1])) for r in timing]
    return timing

def scriptToJSON(scriptf, timing=None):
    ret = []

    with closing(scriptf):
        scriptf.readline() # ignore first header line from script file 
        offset = 0
        for t in timing:
            data = scriptf.read(t[1]).decode('utf-8')
            offset += t[0]
            ret.append((data, offset))
    return dumps(ret)

def parseTtyrec(scriptf):
    pos = 0
    offset = 0
    oldtime = 0
    ret = []

    with closing(scriptf):
        data = scriptf.read()
        while pos < len(data):
            secs,usecs,amount = unpack('iii', data[pos:pos+12])
            pos += 12
            timing = int(ceil(secs * 1000 + float(usecs) / 1000))
            if oldtime: offset += timing - oldtime
            oldtime = timing
            ret.append((data[pos:pos+amount].decode('utf-8'), offset))
            pos += amount
    return dumps(ret)

def renderTemplate(json, dimensions, templatename, outfname=None):
    fsl = FileSystemLoader(dirname(templatename), 'utf-8')
    e = Environment()
    e.loader = fsl

    templatename = basename(templatename)
    rendered = e.get_template(templatename).render(json=json,
                                                   dimensions=dimensions)

    if not outf:
        return rendered

    with closing(outf):
        outf.write(rendered)



if __name__ == '__main__':
    argparser   =   ArgumentParser(description=
                                        'Stores terminal sessions into HTML.')

    argparser.add_argument('-b', '--backend', type=str, default='script',
                           help='use either script or ttyrec', required=False)
    argparser.add_argument('-c', '--command', type=str,
                           help='run a command and quit', required=False)
    argparser.add_argument('-d', '--dimensions', type=int,
                           metavar=('h','w'), nargs=2,
                           help='dimensions of terminal', required=False)
    argparser.add_argument('--json',
                           help='output only JSON', required=False)
    argparser.add_argument('--js',
                           help='output only JavaScript', required=False)
    argparser.add_argument('-m', '--template-file', type=str,
                           default=DEFAULT_TEMPLATE,
                           help='file to use as HTML template', required=False)
    argparser.add_argument('-o', '--output-file', type=FileType('w'),
                           help='file to output HTML to', required=False)
    argparser.add_argument('-s', '--script-file', type=FileType('r'),
                           help='script file to parse', required=False)
    argparser.add_argument('-t', '--timing-file', type=FileType('r'),
                           help='timing file to parse', required=False)

    ns = argparser.parse_args()

    backend     =   ns.backend
    command     =   ns.command
    dimensions  =   ns.dimensions
    tmpname     =   ns.template_file
    scriptf     =   ns.script_file
    outf        =   ns.output_file
    timef       =   ns.timing_file 
    json_only   =   ns.json
    js_only     =   ns.js
    isOSX       =   testOSX()

    if backend != 'ttyrec' and ((scriptf and not timef) or \
                                (timef and not scriptf)):
        argparser.error('Both SCRIPT_FILE and TIMING_FILE have to be ' +
                        'specified together.')
        exit(1)
   
    if not dimensions:
        dimensions = probeDimensions() if not scriptf else (24,80)

    if not scriptf:
        if isOSX or backend == 'ttyrec':
            scriptf = runTtyrec(command)
        else:
            scriptf,timef = runScript(command)

    if isOSX or backend == 'ttyrec':
        json = parseTtyrec(scriptf)
    else:
        timing = getTiming(timef)
        json = scriptToJSON(scriptf, timing)

    if json_only:
        print json
    elif js_only:
        print 'JS ONLY PLEASE IMPLEMENT ME.'
    elif tmpname and outf:
        renderTemplate(json, dimensions, tmpname, outf)
    elif tmpname:
        print renderTemplate(json, dimensions, tmpname)
