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

import sys
import os
import re
import argparse
import glob
import shutil

class Error(Exception):
	pass
	
class App:
	def __init__(self, inpath, outpath, html_template, force = False, includes = [], title = 'PiAnoS4 (embedded)'):
		self.inpath = inpath
		self.outpath = outpath
		self.force = force
		self.includes = includes
		if not '.' in self.includes:
			self.includes.append('.')
		self.mainjs = None
		self.version = None
		self.versuffix = None
		self.html_template = html_template
		self.deps = {}
		self.templateDepRE = re.compile(r'"([\w^{]+){([\w\-\./^}]+)}"')
		self.templateVarRE = re.compile(r'([\w^\[]+)\[([\w^\[]+)\]')
		self.vars = {                      
			'title': title
		}
		
	def run(self):
		self.checkInpath()
		self.checkOutpath()
		self.parseTemplate()
		self.checkDeps()
		
		# print "Dependencies (* indicates chosen files):"
		# for d in self.deps.values():
			# print ' %s (%s):' % (d['name'],d['type'])
			# for c in d['candidates']:
				# chosen = '*' if d['file']['name'] == c['name'] else ' '
				# print '  %s %s -> %s' % (chosen,c['version'],c['name'])

		self.copyFiles()
		self.createIndex()
		
		
	def checkInpath(self):
		for d in ['css','js']:
			path = os.path.join(self.inpath,d)
			if not os.path.isdir(path):
				raise Error("input directory does not seem to be a valid PiAnoS 4 release (was looking for '%s')" % path)
		
		jsdir = os.path.join(self.inpath,'js')
		jsfiles = os.listdir(jsdir)
		for js in jsfiles:
			exp = re.compile(r'^pianos-([0-9\.]+)(?:-(.+))?\.min\.js$')
			match = exp.findall(js)
			if match and len(match[0]) == 2:
				self.mainjs = os.path.join(jsdir, js)
				self.version,self.versuffix = match[0]
				break
				
		if not self.version:
			raise Error("could not find pianos-<version>.min.js in %s" % jsdir)
		
		print "Found PiAnoS version "+self.version+('' if not self.versuffix else '-'+self.versuffix)
		
		
	def checkOutpath(self):
		if os.path.isdir(self.outpath) and not self.force:
			raise Error("output directory already exists. Use --force to overwrite.")
		
		
	def parseTemplate(self):
		copies = []
		templateCopyRE = re.compile(r'copy(?:\s)+with="(.+)"(?:\s)+what="(.+)"')
		with file(self.html_template, 'r') as f:
			for rline in f:
				line = rline.strip()
				match = self.templateDepRE.findall(line)
				if match:
					assert len(match) == 1 and len(match[0]) == 2
					self.deps[match[0][1]] = {
						'type': match[0][0],
						'name': match[0][1]
					}
				else:
					match = templateCopyRE.findall(line)
					if match:
						assert len(match) == 1 and len(match[0]) == 2
						copies.append(match[0])
		
		
		for cp in copies:
			if not cp[0] in self.deps:
				raise Error('<copy> tag found, but the with="%s" attribute corresponds to no known dependency' % (cp[0]))
			obj = {'name':cp[1]}
			if 'copy' in self.deps[cp[0]]:
				self.deps[cp[0]]['copy'].append(obj)
			else:
				self.deps[cp[0]]['copy'] = [obj]
			
			
	def checkDeps(self):
		
		for dname,dep in self.deps.iteritems():
			if dep['type'] == 'js':
				extensions = ['.min.js','.js']
				hints = ['.','js', 'jquery', 'js/jquery', 'jquery/js']
			elif dep['type'] == 'main':
				if dep['name'] == 'pianos':
					dep['candidates'] = [{
						'name': self.mainjs,
						'version': self.version,
						'tag': self.versuffix
					}]
					dep['dst_dir'] = 'js'
					dep['dst_file'] = os.path.split(self.mainjs)[1]
					continue
				else:
					raise Error("unknown built-in dependency file type: "+dep['name'])
			elif dep['type'] == 'my':
				dep['candidates'] = [{
					'name': os.path.join(self.inpath, dep['name']),
					'version': self.version,
					'tag': self.versuffix,
				}]
				continue
			elif dep['type'] == 'css':
				extensions = ['.css','.min.css']
				hints = ['.','css', 'jquery', 'css/jquery', 'jquery/css']
			else:
				raise Error("unhandled dependency file type: "+dep['type'])
		
			finere = {}
			for ext in extensions:
				finere[ext] = re.compile(dep['name']+r'(?:-([0-9\.]+)?(\.\w+)?)?'+ext.replace('.','\\.'))
				
			dep['candidates'] = []
			#print "Looking for dependency %s of type %s" % (dep['name'],dep['type'])
			found = False
			for incl in self.includes:
				for hint in hints:
					for ext in extensions:
						name_pattern = os.path.join(incl,hint,dep['name']+'*'+ext)
						#print " trying",name_pattern
						matches = glob.glob(name_pattern)
						for nativem in matches:
							m = nativem.replace('\\','/')
							#print "  Found candidate",m
							confirm = finere[ext].findall(m)
							if len(confirm) == 1:
								verno,tag = confirm[0]
								if not verno and not tag:
									verno = '<unknown>'
								#print "   Confirmation, at version",verno
								found = True
								dep['candidates'].append({'name':m,'version':verno,'tag':tag})
						if found: break
					if found: break
				if found: break
			
			if not found:
				raise Error("could not find the dependency '%s' of type '%s'" % (dep['name'],dep['type']))
		
		
		for dname,dep in self.deps.iteritems():
			if len(dep['candidates']) > 1:
				dep['file'] = sorted(dep['candidates'], lambda x,y: (cmp(x['version'],y['version'])) or cmp(x['tag'],y['tag']), reverse=True)[0]
			else:
				dep['file'] = dep['candidates'][0]
			
		
		for dname,dep in self.deps.iteritems():
			dep['src_fullpath'] = os.path.normpath(dep['file']['name'])
			if not os.path.isfile(dep['src_fullpath']):
				# Make a final check that the file exists so that dependencies that have not gone through
				# the search loop are guaranteed to be OK beyond this point
				raise Error("could not find the dependency '%s' of type '%s'" % (dep['name'],dep['type']))
			
			if not 'dst_dir' in dep:
				path,dname = os.path.split(dep['name'])
				srcpath,fname = os.path.split(dep['src_fullpath'])
				if dep['type'] in ('js','css'):
					path = os.path.join(dep['type'], path)
				dep['dst_dir'] = path
				dep['dst_file'] = fname
				
			dep['dst_fullpath'] = os.path.normpath(os.path.join(self.outpath, dep['dst_dir'],dep['dst_file']))
				

		for dname,dep in self.deps.iteritems():
			if 'copy' in dep:
				newc = []
				for c in dep['copy']:
					if c['name'] == '*':
						srcdir = os.path.split(dep['src_fullpath'])[0]
						allfiles = glob.glob(os.path.join(srcdir,c['name']))
						for f in allfiles:
							newc.append({'name':os.path.split(f)[1]})
					else:
						newc.append(c)
				dep['copy'] = newc
				
		for dname,dep in self.deps.iteritems():
			if 'copy' in dep:
				for c in dep['copy']:
					c['src_dir'] = os.path.split(dep['src_fullpath'])[0]
					c['src_fullpath'] = os.path.join(c['src_dir'],c['name'])
					if os.path.isdir(c['src_fullpath']):
						c['type'] = 'dir'
					elif os.path.isfile(c['src_fullpath']):
						c['type'] = 'file'
					else:
						raise Error("could not find the additional file/dir to copy '%s' along with dependency '%s' of type '%s'" % (c['name'], dep['name'], dep['type']))
					c['dst_dir'] = dep['dst_dir']
					c['dst_file'] = c['name']
					c['dst_fullpath'] = os.path.normpath(os.path.join(self.outpath, c['dst_dir'],c['dst_file']))
				
				
	def copyFiles(self):
		allcopies = []
		for dname,dep in self.deps.iteritems():
			if 'copy' in dep:
				allcopies.extend(dep['copy'])

		if not os.path.isdir(self.outpath):
			os.makedirs(self.outpath)

		for c in sorted(allcopies, key=lambda x:len(x['src_dir']), reverse=True):
			print c['name']
			if c['type'] == 'file':
				dst_dir = os.path.join(self.outpath, c['dst_dir'])
				if not os.path.isdir(dst_dir):
					os.makedirs(dst_dir)
				shutil.copy(c['src_fullpath'], c['dst_fullpath'])
			else:
				if os.path.isdir(c['dst_fullpath']):
					print "Erasing",c['dst_fullpath']
					shutil.rmtree(c['dst_fullpath'])
				print "Copying src to",c['dst_fullpath']
				shutil.copytree(c['src_fullpath'], c['dst_fullpath'])
						
			
		existing_dirs = set()
		for dname,dep in self.deps.iteritems():
			# print "Copying"
			# print "  ",dep['src_fullpath']
			# print "  to"
			# print "  ",dep['dst_fullpath']
			# print "   (%s | %s)" % (dep['dst_dir'],dep['dst_file'])
			
			target_dir, target_name = os.path.split(dep['dst_fullpath'])
			if not target_dir in existing_dirs:
				if not os.path.isdir(target_dir):
					os.makedirs(target_dir)
				existing_dirs.add(target_dir)
			shutil.copy(dep['src_fullpath'], dep['dst_fullpath'])
			
	
	def	substVars(self, line):
		repl = line
		match = self.templateVarRE.search(repl)
		while match:
			if match.group(1) != 'var':
				raise Error("Unknown substitution type: %s" % match.group(1))
			if match.group(2) not in self.vars:
				raise Error("Unknown substitution variable: %s" % match.group(2))
			repl = repl[:match.start()] + self.vars[match.group(2)] + repl[match.end():]
			match = self.templateVarRE.search(repl)
				
		return repl
		
	def createIndex(self):
		target_name = os.path.join(self.outpath, 'index.html')
		with file(self.html_template, 'rb') as input:
			with file(target_name, 'wb') as output:
				for line in input:
					match = self.templateDepRE.findall(line)
					if match:
						assert len(match) == 1 and len(match[0]) == 2
						assert match[0][1] in self.deps
						d = self.deps[match[0][1]]
						pattern = d['type']+'{'+d['name']+'}'
						l = line.replace(pattern, d['dst_dir'].replace('\\','/').strip('/') + '/' + d['dst_file'])
						output.write(self.substVars(l))
					else:
						if '<copy with="' in line:
							continue
						output.write(self.substVars(line))
		
def main():
	parser = argparse.ArgumentParser(
		description='prepares the PiAnoS 4.x embedded (standalone) distribution for third parties by copying required files from a pre-built release.',
		epilog='You must first create the PiAnoS release (using the tools/build.py script) then execute this script, specifying the output folder. Because dependencies (jQuery, jQuery UI, d3.js, etc.) are not normally part of the PiAnoS source tree, you may also specify additional directories where these can be found. This script is needed because the standard PiAnoS application is designed for either standalone (pure HTML) or client-server (Apache+PHP+PostgreSQL) modes - the embedded (standalone) mode is a third mode that is not natively handled by PiAnoS.'
	)
	parser.add_argument('release_dir', help='the directory created by the tools/build.py script of PiAnoS 4. It is not the top directory of the PiAnoS source, nor the .tar.gz release file - it actually is the content of the latter.')
	default_output_dir = os.path.join('.','pianos4-embedded')
	parser.add_argument('output_dir', help="the output directory where the embedded distribution of PiAnoS for UFLR will be created. Defaults to '%s'"%default_output_dir, nargs='?', default=default_output_dir)
	parser.add_argument('-f','--force', action='store_true', help='overwrite output files')
	parser.add_argument('-I','--include', metavar='DIRECTORY', help='add the specified directories to the list of include paths where javascript dependencies are searched', nargs='*', default=[])
	default_template = os.path.join('.','pianos4-embedded-index.html')
	parser.add_argument('-t','--template', metavar='FILE', help="use another HTML template than the default '%s'"%default_template, default=default_template)
	default_title = 'PiAnoS 4'
	parser.add_argument('--title', metavar='VALUE', help="Set the HTML page title to VALUE. Defaults to '%s'"%default_title, default=default_title)
	
	args = parser.parse_args()
	
	try:
		app = App(args.release_dir, args.output_dir, args.template, force=args.force, includes=args.include, title=args.title)
		app.run()
	except Error,err:
		print "ERROR:",err
		return 1
	
	return 0
	
	
if __name__ == '__main__':
	sys.exit(main())
	