#!/usr/bin/python
# -*- coding: UTF-8 -*-

import sys
import glob
import os
import re
import math
import shutil
import errno
import unicodedata
import urllib2
#import xml.etree.ElementTree as etree
from lxml import etree
from xml.sax.saxutils import escape as xmlescape
import cssutils
import logging
import textwrap
import tempfile
import threading
import multiprocessing
from threading import Thread, Lock
from Queue import Queue

# Frames per second. Increasing this renders more frames, the avconf-statements would still need modifications
fps = 25

# using --debug skips the threading, the network fetching of the schedule and
# just renders one type of video
debug = ('--debug' in sys.argv)

# using --offline only skips the network fetching and use a local schedule.de.xml
offline = ('--offline' in sys.argv)

# set charset of output-terminal
reload(sys)
sys.setdefaultencoding('utf-8')

# t: current time, b: begInnIng value, c: change In value, d: duration
# copied from jqueryui
def easeOutCubic(t, b, c, d):
	t=float(t)/d-1
	return c*((t)*t*t + 1) + b

def easeInCubic(t, b, c, d):
	t=float(t)/d
	return c*(t)*t*t + b;

def easeOutQuad(t, b, c, d):
	t=float(t)/d
	return -c *(t)*(t-2) + b;

# try to create all folders needed and skip, they already exist
def ensurePathExists(path):
	try:
		os.makedirs(path)
	except OSError as exception:
		if exception.errno != errno.EEXIST:
			raise

# remove the files matched by the pattern
def ensureFilesRemoved(pattern):
	for f in glob.glob(pattern):
		os.unlink(f)

# Normalizes string, converts to lowercase, removes non-alpha characters,
 #and converts spaces to hyphens.
def slugify(value):
	value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore')
	value = unicode(re.sub('[^\w\s-]', '', value).strip().lower())
	value = unicode(re.sub('[-\s]+', '-', value))
	return value

# create a filename from the events' id and a slugified version of the title
def vorspannFilename(id, title):
	return u'{0:04d}-{1}.dv'.format(id, slugify(unicode(title)))

# svg does not have a method for automatic line breaking, that rsvg is capable of
# so we do it in python as good as we can
def vorspannTitle(title):
	return '</tspan><tspan x="120" dy="35">'.join(textwrap.wrap(title, 50))

def vorspannUrl(id):
	return 'fossgis.de/konferenz/2014/programm/events/'+str(id)+'.de.html'


def abspannFrames():
	# 5 Sekunden

	# 2 Sekunden Fadein Lizenz
	frames = 2*fps
	for i in range(0, frames):
		yield (
			('lizenz','style',    'opacity', "%.4f" % easeInCubic(i, 0, 1, frames)),
		)

	# 3 Sekunden stehenlassen
	frames = 3*fps
	for i in range(0, frames):
		yield (
			('lizenz','style',    'opacity', 1),
		)


def vorspannFrames():
	# 5 Sekunden

	# 1 Sekunde Title Fadein
	frames = 1*fps
	for i in range(0, frames):
		yield (
			('title', 'style',    'opacity', "%.4f" % easeInCubic(i, 0, 1, frames)),
			('text',  'style',    'opacity', 0),
			('lizenz','style',    'opacity', 0),
		)

	# 2 Sekunde Text Fadein
	frames = 2*fps
	for i in range(0, frames):
		yield (
			('title', 'style',    'opacity', 1),
			('text',  'style',    'opacity', "%.4f" % easeInCubic(i, 0, 1, frames)),
			('lizenz','style',    'opacity', 0),
		)

	# 1 Sekunde Lizenz Fadein
	frames = 1*fps
	for i in range(0, frames):
		yield (
			('title', 'style',    'opacity', 1),
			('text',  'style',    'opacity', 1),
			('lizenz','style',    'opacity', "%.4f" % easeInCubic(i, 0, 1, frames)),
		)

	# 1 Sekunde stehen bleiben
	frames = 1*fps
	for i in range(0, frames):
		yield (
			('title', 'style',    'opacity', 1),
			('text',  'style',    'opacity', 1),
			('lizenz','style',    'opacity', 1),
		)

def pauseFrames():
	# 7 Sekunden

	# 3 Sekunde Text FadeIn
	frames = 3*fps
	for i in range(0, frames):
		yield (
			('text','style',    'opacity', "%.4f" % easeInCubic(i, 0, 1, frames)),
		)

	# 3 Sekunde Text FadeOut
	frames = 3*fps
	for i in range(0, frames):
		yield (
			('text','style',    'opacity', "%.4f" % easeInCubic(i, 1, -1, frames)),
		)

	# 1 Sekunde stehen lassen
	frames = 1*fps
	for i in range(0, frames):
		yield (
			('text','style',    'opacity', 0),
		)

cssutils.ser.prefs.lineSeparator = ' '
cssutils.log.setLevel(logging.ERROR)

def render(infile, outfile, sequence, parameters={}, workdir='artwork'):
	# in debug mode we have no thread-worker which prints its progress
	if debug:
		print "generating {0} from {1}".format(outfile, infile)

	# make sure a .frames-directory exists in out workdir
	ensurePathExists(os.path.join(workdir, '.frames'))

	# open and parse the input file
	with open(os.path.join(workdir, infile), 'r') as fp:
		svgstr = fp.read()
		for key in parameters.keys():
			svgstr = svgstr.replace(key, xmlescape(str(parameters[key])))

		svg = etree.fromstring(svgstr)

	# find all images and force them to absolute file-urls
	namespaces = {'xlink': 'http://www.w3.org/1999/xlink', 'svg': 'http://www.w3.org/2000/svg'}
	for el in svg.findall(".//svg:image[@xlink:href]", namespaces=namespaces):
		el.attrib['{http://www.w3.org/1999/xlink}href'] = 'file:///' + os.path.realpath(workdir) + '/' + el.attrib['{http://www.w3.org/1999/xlink}href']


	# frame-number counter
	frameNr = 0

	# iterate through the animation seqence frame by frame
	# frame is a ... tbd
	for frame in sequence():
		# print a line for each and every frame generated
		if debug:
			print "frameNr {0:2d} => {1}".format(frameNr, frame)

		# open the output-file (named ".gen.svg" in the workdir)
		with open(os.path.join(workdir, '.gen.svg'), 'w') as fp:
			# apply the replace-pairs to the input text, by finding the specified xml-elements by thier id and modify thier css-parameter the correct value
			for replaceinfo in frame:
				(id, type, key, value) = replaceinfo

				for el in svg.findall(".//*[@id='"+id.replace("'", "\\'")+"']"):
					if type == 'style':
						style = cssutils.parseStyle( el.attrib['style'] if 'style' in el.attrib else '' )
						style[key] = unicode(value)
						el.attrib['style'] = style.cssText

					elif type == 'attr':
						el.attrib[key] = value

			# write the generated svg-text into the output-file
			fp.write( etree.tostring(svg) )

		# invoke inkscape to convert the generated svg-file into a png inside the .frames-directory
		os.system('cd {0} && inkscape --export-png=.frames/{1:04d}.png .gen.svg >/dev/null 2>&1'.format(workdir, frameNr))

		# incrwement frame-number
		frameNr += 1

	# remove the dv we are about to (re-)generate
	ensureFilesRemoved(os.path.join(workdir, outfile))

	# invoke avconv aka ffmpeg and renerate a lossles-dv from the frames
	#  if we're not in debug-mode, suppress all output
	os.system('cd {0} && avconv -ar 48000 -ac 2 -f s16le -i /dev/zero -f image2 -i .frames/%04d.png -target pal-dv -aspect 16:9 -shortest "{1}"'.format(workdir, outfile) + ('' if debug else '>/dev/null 2>&1'))

	# as before, in non-debug-mode the thread-worker does all progress messages
	if debug:
		print "cleanup"

	# remove the .frames-dir with all frames in it
	shutil.rmtree(os.path.join(workdir, '.frames'))

	# remove the generated svg
	ensureFilesRemoved(os.path.join(workdir, '.gen.svg'))



# Download the Events-Schedule and parse all Events out of it. Yield a tupel for each Event
def events():
	print "downloading pentabarf schedule"

	# use --offline to skip networking
	if offline:
		# parse the offline-version
		schedule = etree.parse('schedule.de.xml').getroot()

	else:
		# download the schedule
		response = urllib2.urlopen('http://eh14.easterhegg.eu/pages/fahrplan/schedule.xml')

		# read xml-source
		xml = response.read()

		# parse into ElementTree
		schedule = etree.fromstring(xml)

	# iterate all days
	for day in schedule.iter('day'):
		# iterate all rooms
		for room in day.iter('room'):
			# iterate events on that day in this room
			for event in room.iter('event'):
				# aggregate names of the persons holding this talk
				personnames = []
				for person in event.find('persons').iter('person'):
					personnames.append(person.text)

				# yield a tupel with the event-id, event-title and person-names
				yield ( int(event.get('id')), event.find('title').text, event.find('subtitle').text or '', ', '.join(personnames) )


# debug-mode selected by --debug switch
if debug:
	print "!!! DEBUG MODE !!!"
	title = 'Sleep? Ain\'t nobody got time for that!'
	subtitle = 'Physiologie von Schlaf und Wachzustand'

	render(
		'vorspann.svg',
		os.path.join('..', str(5725)+".dv"),
		vorspannFrames,
		{
			'$id': 5725,
			'$title': title,
			'$subtitle': subtitle,
			'$personnames': 'Christina'
		}
	)

	# render(
	# 	'abspann.svg',
	# 	'../outro.dv',
	# 	abspannFrames
	# )

	render('pause.svg',
		'../pause.dv',
		pauseFrames
	)

	sys.exit(0)



# threaded task queue
tasks = Queue()


# for too long titles ;)
titlemap = {
	#708: "Neue WEB-Anwendungen des LGRB Baden-Württemberg im Überblick"
}

# iterate over all events extracted from the schedule xml-export
for (id, title, subtitle, personnames) in events():
	if id in titlemap:
		title = titlemap[id]

	# generate a task description and put them into the queue
	tasks.put((
		'vorspann.svg',
		str(id)+".dv",
		vorspannFrames,
		{
			'$id': id,
			'$title': title,
			'$subtitle': subtitle,
			'$personnames': personnames
		}
	))

# place a task for the outro into the queue
tasks.put((
	'abspann.svg',
	'outro.dv',
	abspannFrames
))

# place the pause-sequence into the queue
tasks.put((
	'pause.svg',
	'pause.dv',
	pauseFrames
))

# one working thread per cpu
num_worker_threads = multiprocessing.cpu_count()
print "{0} tasks in queue, starting {1} worker threads".format(tasks.qsize(), num_worker_threads)

# put a sentinel for each thread into the queue to signal the end
for _ in range(num_worker_threads):
	tasks.put(None)

# this lock ensures, that only one thread at a time is writing to stdout
# and avoids output from multiple threads intermixing
printLock = Lock() 
def tprint(str):
	# aquire lock
	printLock.acquire()

	# print thread-name and message
	print threading.current_thread().name+': '+str

	# release lock
	printLock.release()


# thread worker
def worker():
	# generate a tempdir for this worker-thread and use the artwork-subdir as temporary folder
	tempdir = tempfile.mkdtemp()
	workdir = os.path.join(tempdir, 'artwork')

	# save the current working dir as output-dir
	outdir = os.getcwd()

	# print a message that we're about to initialize our environment
	tprint("initializing worker in {0}, writing result to {1}".format(tempdir, outdir))

	# copy the artwork-dir into the tempdir
	shutil.copytree('artwork', workdir)

	# loop until all tasks are done (when the thread fetches a sentinal from the queue)
	while True:
		# fetch a task from the queue
		task = tasks.get()

		# if it is a stop-sentinal break out of the loop
		if task == None:
			break

		# print that we're about to render a task
		tprint('rendering {0}'.format(task[1]))

		# render options
		opts = (
			# argument 0 is the input file. prepend the workdir
			os.path.join(workdir, task[0]),

			# argument 1 is the output file. prepend the outdir
			os.path.join(outdir, task[1]),

			# argument 2 is the frame generator, nothing to do here
			task[2],

			# argument 3 are the extra parameters
			task[3] if len(task) > 3 else {},

			# argument 4 is the workdir path
			workdir
		)

		# render with these arguments
		render(*opts)

		# print that we're finished
		tprint('finished {0}, {1} tasks left'.format(task[1], max(0, tasks.qsize() - num_worker_threads)))

		# mark the task as finished
		tasks.task_done()

	# all tasks from the queue done, clean up
	tprint("cleaning up worker")

	# remove the tempdir
	shutil.rmtree(tempdir)

	# mark the sentinal as done
	tasks.task_done()


# generate and start the threads
for i in range(num_worker_threads):
	t = Thread(target=worker)
	t.daemon = True
	t.start()

# wait until they finished doing the work
tasks.join()
print "all worker threads ended"
