import os
import re
import base64
import zlib
import hashlib
import xml.etree.ElementTree as ET

PY_SEP = os.path.sep

def wrapScope( src ):
	return "(function(){" + src + "})();"

class Resolver:
	resolved = None

	bMap = None
	parentMap = None

	EX_PROP = "prop"
	EX_FUNC = "method"
	EX_CLASS = "class"

	def __init__( self, classMap ):
		self.classMap = classMap
		self._reload()

	def _reload( self ):
		self.bMap = ET.parse( self.classMap )
		# ElementTree does not support Element.find( ".." )
		# Make a parent map to work around
		self.parentMap = { c:p for p in self.bMap.iter() for c in p }

		self.resolved = []

	def resource( self, elem ):
		if "src" in elem.attrib:
			return elem.attrib[ "src" ]

		parent = self.parentMap[ elem ]

		if parent != None:
			return self.resource( parent )

	def resolve( self, c, classList ):
		self.resolved = []
		self.__resolve( c, classList )

	def __resolve( self, c, classList ):

		lista = list( "[@name=\"" + x + "\"]" for x in c.split(".") )

		fx = "/".join( self.EX_CLASS + x for x in lista[:-1] )

		it = lista[-1]
		# Test if class
		elem = self.bMap.find( ".//" + fx + self.EX_CLASS + it )

		if elem == None:
			if fx != '': fx += "/"

			# Test if prop
			elem = self.bMap.find( ".//" + fx + self.EX_PROP + it )

			# test if func
			if elem == None:
				elem = self.bMap.find( ".//" + fx + self.EX_FUNC + it )

			if elem == None:
				raise LookupError( "No such class: " + c )

			imports = self.parentMap[ elem ].findall( "import" )

		else:
			imports = elem.findall( "import" )

		self.resolved.append( c )

		for imp in imports:
			if imp.text not in self.resolved:
				self.__resolve( imp.text, classList )

		classList.append([ c, elem ])

class BotanClassResolver:
	R = ""
	CR = None

	classMap = ""
	flagCompress = True
	returnHash = False
	resv = None

	def __init__( self, jwork, BotanRoot, classMap, cacheRoot ):

		self.JWork = jwork;
		self.R = os.path.abspath( BotanRoot )
		self.CR = os.path.abspath( cacheRoot )

		os.makedirs( self.CR, 0o755, exist_ok = True )

		if not os.path.exists( classMap ):
			self.JWork.buildClassMap( self.R, classMap )

		self.resv = Resolver( classMap )

	def BotanFile( self, t ):
		content = ""
		with open( os.path.join( self.R, t ), "r" ) as f:
			content = f.read()

		return content

	def BotanCache( self, t ):
		content = ""
		with open( t, "r" ) as f:
			content = f.read()

		return content

	def cleanList( self, lista ):
		olist = []
		for i in lista:
			if i not in olist:
				olist.append( i )
		return olist

	def jsLookup( self, classList, classFiles ):
		for c in classList:
			src = self.resv.resource( c[1] )

			if src == None:
				raise LookupError( "Cannot find src file for: "  + c[0] )

			if src not in classFiles:
				classFiles.append( src )

	def cssLookup( self, jsList, cssList ):

		for f in jsList:
			possibleList = []

			cssFile = os.path.splitext( f )[0] + ".css"

			if cssFile not in possibleList:
				possibleList.append( cssFile )

			f = f.split( PY_SEP )
			l = len( f )

			for i in range( 1, l ):
				cssFile = PY_SEP.join( x for x in f[:-i] ) + PY_SEP + "@_this.css"
				if cssFile not in possibleList:
					possibleList.append( cssFile )

			possibleList.sort()

			for f in possibleList:
				f = f.replace( "@_this.css", "_this.css" )
				if os.path.exists( os.path.join( self.R, f ) ):
					cssList.append( f )


	def getCache( self, fileList, cName, mode ):
		if self.CR == None:
			return None

		md5 = hashlib.md5( bytearray( "".join( fileList ), "utf-8" ) ).hexdigest()

		cName[0] = oFHash = md5 + "." + mode
		cFHash = md5 + ".c." + mode

		# Raw file
		oFile = os.path.join( self.CR, oFHash )
		# Compressed file
		cFile = os.path.join( self.CR, cFHash )

		dates = list(
			os.path.getmtime( os.path.join( self.R, x ) )
			if os.path.exists( os.path.join( self.R, x ) ) else -1
			for x in fileList
		)

		# Root file date
		dates.append( os.path.getmtime( os.path.join( self.R, "_this.js" ) ) );

		if self.useCache( cFile, dates ) and self.flagCompress == True:
			return cFHash if self.returnHash else self.BotanCache( cFile )

		elif self.useCache( oFile, dates ):
			self.JWork.saveCache.delay(
				oFile
				# Content is None to initiate a compression
				, None
				, mode
				, os.path.join( self.R, "externs" )
			)

			return oFHash if self.returnHash else self.BotanCache( oFile )

	def useCache( self, f, dList ):
		if not os.path.exists( f ):
			return False

		t = os.path.getmtime( f )

		for i in dList:
			if t < i:
				return False

		return True

	def compileJs( self, cList, xList ):
		md5 = [ None ]

		for x in xList:
			cList.remove( x ) if x in cList else None

		cacheFile = self.getCache( cList, md5, "js" )

		if cacheFile != None:
			return cacheFile;

		# The root file
		outputJs = self.BotanFile( "_this.js" )


		for f in cList:
			path = (
				os.path.splitext( f )[0]
					.replace( PY_SEP, "." )
					.replace( "._this", "" )
			)
			struct = ";BotanJS.define( \"" + path + "\" );"

			outputJs += struct + self.BotanFile( f )

		outputJs = wrapScope( outputJs )

		[ self.JWork.saveCache if self.returnHash else self.JWork.saveCache.delay ][0] (
			os.path.join( self.CR, md5[0] )
			, outputJs
			, "js"
			, os.path.join( self.R, "externs" )
		)

		if self.returnHash:
			return md5[0]

		return outputJs

	def compileCss( self, cList, xList ):
		cssIList = []
		cssXList = []
		self.cssLookup( cList, cssIList )
		self.cssLookup( xList, cssXList )

		cList = []
		xList = cssXList

		for x in cssIList:
			cList.append( x ) if x not in xList else None

		md5 = [ None ]
		cacheFile = self.getCache( cList, md5, "css" )

		if cacheFile != None:
			return cacheFile;

		outputCss = ""

		for f in self.cleanList( cList ):
			outputCss += self.BotanFile( f )

		[ self.JWork.saveCache if self.returnHash else self.JWork.saveCache.delay ][0] (
			os.path.join( self.CR, md5[0] ), outputCss, "css"
		)

		if self.returnHash:
			return md5[0]

		return outputCss

	def getAPI( self, code, mode ):
		self.flagCompress = True
		# Should reload on debug mode only
		self.resv._reload()
		flag = mode[0]
		requestAPIs = code
		sp = "/"

		if flag == "o":
			mode = mode[1:]
		elif flag == "r":
			mode = mode[1:]
			self.flagCompress = False
		else:
			self.returnHash = True
			requestAPIs = (
				# decode -> decompress -> split
				zlib.decompress( base64.b64decode( code, None, True ) )
					.decode( "utf-8" )
			)
			sp = ","

		# strip malicious
		requestAPIs = (
				requestAPIs
					.replace( "[^A-Za-z\.\*" + re.escape( sp ) + " ]", "" )
					.split( sp )
			)

		imports = []
		excludes = []

		for apis in requestAPIs:

			if apis == None: continue

			classList = []
			lookupList = imports

			if apis[0] == "-":
				apis = apis[1:]
				lookupList = excludes

			self.resv.resolve( apis, classList )
			self.jsLookup( classList, lookupList )

		if mode == "js":
			return self.compileJs( imports, excludes )
		elif mode == "css":
			return self.compileCss( imports, excludes )

		raise TypeError( "Invalid mode: " + js )