#!/usr/bin/env python3
# IFaceTableGen.py - regenerate the IFaceTable.cxx from the Scintilla.iface
# interface definition file.  Based on Scintilla's HFacer.py.
# The header files are copied to a temporary file apart from the section between a //++Autogenerated
# comment and a //--Autogenerated comment which is generated by the printHFile and printLexHFile
# functions. After the temporary file is created, it is copied back to the original file name.
# Requires Python 3.6 or later

import sys

srcRoot = "../.."

sys.path.append(srcRoot + "/scintilla/scripts")

import Face
import FileGenerator

def CommentString(prop):
	if prop and prop["Comment"]:
		return (" -- " + " ".join(prop["Comment"])).replace("<", "&lt;")
	return ""

def ConvertEnu(t):
	if Face.IsEnumeration(t):
		return "int"
	else:
		return t

def GetScriptableInterface(f):
	"""Returns a tuple of (constants, functions, properties)
constants - a sorted list of (name, features) tuples, including all
	constants except for SCLEX_ constants which are presumed not used by
	scripts.  The SCI_ constants for functions are omitted, since they
	can be derived, but the SCI_ constants for properties are included
	since they cannot be derived from the property names.
functions - a sorted list of (name, features) tuples, for the features
	that should be exposed to script as functions.  This includes all
	'fun' functions; it is up to the program to decide if a given
	function cannot be scripted.  It is also up to the caller to
	export the SCI_ constants for functions.
properties - a sorted list of (name, property), where property is a
	dictionary containing these keys: "GetterValue", "SetterValue",
	"PropertyType", "IndexParamType", "IndexParamName", "GetterName",
	"SetterName", "GetterComment", "SetterComment", and "Category".
	If the property is read-only, SetterValue will be 0, and the other
	Setter attribtes will be None.  Likewise for write-only properties,
	GetterValue will be 0 etc.  If the getter and/or setter are not
	compatible with one another, or with our interpretation of how
	properties work, then the functions are instead added to the
	functions list.  It is still up to the language binding to decide
	whether the property can / should be exposed to script."""

	constants = [] # returned as a sorted list
	functions = {} # returned as a sorted list of items
	properties = {} # returned as a sorted list of items

	for name in f.order:
		features = f.features[name]
		if features["Category"] != "Deprecated":
			if features["FeatureType"] == "val":
				constants.append( (name, features) )
			elif features["FeatureType"] in ["fun","get","set"]:
				if features["FeatureType"] == "get":
					propname = name.replace("Get", "", 1)
					properties[propname] = (name, properties.get(propname,(None,None))[1])

				elif features["FeatureType"] == "set":
					propname = name.replace("Set", "", 1)
					properties[propname] = (properties.get(propname,(None,None))[0], name)

				else:
					functions[name] = features

	propertiesCopy = properties.copy()
	for propname, (getterName, setterName) in propertiesCopy.items():
		getter = getterName and f.features[getterName]
		setter = setterName and f.features[setterName]

		getterValue, getterIndex, getterIndexName, getterType = 0, None, None, None
		setterValue, setterIndex, setterIndexName, setterType = 0, None, None, None
		propType, propIndex, propIndexName = None, None, None

		isok = (getterName or setterName) and getter is not setter

		if isok and getter:
			if getter['Param2Type'] == 'stringresult':
				getterType = getter['Param2Type']
			else:
				getterType = getter['ReturnType']
			getterType = ConvertEnu(getterType)
			getterValue = getter['Value']
			getterIndex = getter['Param1Type'] or 'void'
			getterIndexName = getter['Param1Name']

			isok = ((getter['Param2Type'] or 'void') == 'void') or (getterType == 'stringresult')

		if isok and setter:
			setterValue = setter['Value']
			setterType = ConvertEnu(setter['Param1Type']) or 'void'
			setterIndex = 'void'
			if (setter['Param2Type'] or 'void') != 'void':
				setterIndex = setterType
				setterIndexName = setter['Param1Name']
				setterType = ConvertEnu(setter['Param2Type'])

			isok = (setter['ReturnType'] == 'void') or (setter['ReturnType'] == 'int' and setterType=='string')

		if isok and getter and setter:
			isok = ((getterType == setterType) or (getterType == 'stringresult' and setterType == 'string')) and \
				(getterIndex == setterIndex)

		propType = getterType or setterType
		propIndex = getterIndex or setterIndex
		propIndexName = getterIndexName or setterIndexName

		if isok:
			# do the types appear to be useable?  THIS IS OVERRIDDEN BELOW
			isok = (propType in ('int', 'position', 'line', 'pointer', 'colour', 'colouralpha', 'bool', 'string', 'stringresult')
				and propIndex in ('void','int','position','line','string','bool'))

			# getters on string properties follow a different protocol with this signature
			# for a string getter and setter:
			#   get int funcname(void,stringresult)
			#   set void funcname(void,string)
			#
			# For an indexed string getter and setter, the indexer goes in
			# wparam and must not be called 'int length', since 'int length'
			# has special meaning.

			# A bool indexer has a special meaning.  It means "if the script
			# assigns the language's nil value to the property, call the
			# setter with args (0,0); otherwise call it with (1, value)."
			#
			# Although there are no getters indexed by bool, I suggest the
			# following protocol:  If getter(1,0) returns 0, return nil to
			# the script.  Otherwise return getter(0,0).


		if isok:
			properties[propname] = {
				"GetterValue"    : getterValue,
				"SetterValue"    : setterValue,
				"PropertyType"   : propType,
				"IndexParamType" : propIndex,
				"IndexParamName" : propIndexName,
				# The rest of this metadata is added to help generate documentation
				"Category"       : (getter or setter)["Category"],
				"GetterName"     : getterName,
				"SetterName"     : setterName,
				"GetterComment"  : CommentString(getter),
				"SetterComment"  : CommentString(setter)
			}
			#~ print(properties[propname])

			# If it is exposed as a property, the constant name is not picked up implicitly
			# (because the name is different) but its constant should still be exposed.
			if getter:
				constants.append( ("SCI_" + getterName.upper(), getter))
			if setter:
				constants.append( ("SCI_" + setterName.upper(), setter))
		else:
			# Cannot parse as scriptable property (e.g. not symmetrical), so export as functions
			del(properties[propname])
			if getter:
				functions[getterName] = getter
			if setter:
				functions[setterName] = setter

	funclist = list(functions.items())
	funclist.sort()

	proplist = list(properties.items())
	proplist.sort()

	constants.sort()

	return (constants, funclist, proplist)


def printIFaceTableCXXFile(facesAndIDs):
	out = []
	f, fLex, ids = facesAndIDs
	(constants, functions, properties) = GetScriptableInterface(f)
	# Lexilla only defines constants, no functions or properties
	(constantsLex, _, _) = GetScriptableInterface(fLex)
	constants.extend(constantsLex)
	constants.extend(ids)
	constants.sort()

	out.append("")
	out.append("static IFaceConstant ifaceConstants[] = {")

	if constants:
		lastName = constants[-1][0]
		for name, features in constants:
			comma = "" if name == lastName else ","
			val = features["Value"]
			if int(val, base=0) >= 0x8000000:
				val = "static_cast<int>(" + val + ")"
			out.append('\t{"%s",%s}%s' % (name, val, comma))

		out.append("};")
	else:
		out.append('{"",0}};')

	# Write an array of function descriptions.  This can be
	# used as a sort of compiled typelib.

	out.append("")
	out.append("static IFaceFunction ifaceFunctions[] = {")
	if functions:
		lastName = functions[-1][0]
		for name, features in functions:
			comma = "" if name == lastName else ","

			returnType = ConvertEnu(features["ReturnType"])
			param1Type = ConvertEnu(features["Param1Type"]) or "void"
			param2Type = ConvertEnu(features["Param2Type"]) or "void"

			# Fix-up: if a param is an int (or position) named length, change to iface_type_length.
			if param1Type in ["int", "position"] and features["Param1Name"] == "length":
				param1Type = "length"

			if param2Type in ["int", "position"] and features["Param2Name"] == "length":
				param2Type = "length"

			out.append('\t{"%s", %s, iface_%s, {iface_%s, iface_%s}}%s' % (
				name, features["Value"], returnType, param1Type, param2Type, comma
			))

		out.append("};")
	else:
		out.append('{"",0,iface_void,{iface_void,iface_void}} };')


	out.append("")
	out.append("static IFaceProperty ifaceProperties[] = {")
	if properties:
		lastName = properties[-1][0]
		for propname, property in properties:
			comma = "" if propname == lastName else ","
			out.append('\t{"%s", %s, %s, iface_%s, iface_%s}%s' % (
				propname,
				property["GetterValue"],
				property["SetterValue"],
				property["PropertyType"], property["IndexParamType"],
				comma
			))

		out.append("};")
		out.append("")
	else:
		out.append('{"", 0, iface_void, iface_void} };')

	out.append("enum {")
	out.append("\tifaceFunctionCount = %d," % len(functions))
	out.append("\tifaceConstantCount = %d," % len(constants))
	out.append("\tifacePropertyCount = %d" % len(properties))
	out.append("};")
	out.append("")
	return out

def convertStringResult(s):
	if s == "stringresult":
		return "string"
	else:
		return s

def idsFromDocumentation(filename):
	""" Read the Scintilla documentation and return a list of all the features
	in the same order as they are explained in the documentation.
	Also include the previous header with each feature. """
	idsInOrder = []
	segment = ""
	with open(filename) as f:
		for s in f:
			if "<h2" in s:
				segment = s.split(">")[1].split("<")[0]
			if 'id="SCI_' in s:
				idFeature = s.split('"')[1]
				#~ print(idFeature)
				idsInOrder.append([segment, idFeature])
	return idsInOrder

nonScriptableTypes = [
	"cells",
	"textrange",
	"findtext",
	"formatrange",
	"textrangefull",
	"findtextfull",
	"formatrangefull"
]

def printIFaceTableHTMLFile(faceAndIDs):
	out = []
	f, ids, idsInOrder = faceAndIDs
	(constants, functions, properties) = GetScriptableInterface(f)
	explanations = {}
	for name, features in functions:
		featureDefineName = "SCI_" + name.upper()
		explanation = ""
		href = ""
		hrefEnd = ""
		href = "<a href='https://www.scintilla.org/ScintillaDoc.html#" + featureDefineName + "'>"
		hrefEnd = "</a>"

		if features['Param1Type'] in nonScriptableTypes or features['Param2Type'] in nonScriptableTypes:
			#~ print(name, features)
			continue

		parameters = ""
		stringresult = ""
		if features['Param2Type'] == "stringresult":
			stringresult = "string "
			if features['Param1Name'] and features['Param1Name'] != "length":
				parameters += features['Param1Type'] + " " + features['Param1Name']
		else:
			if features['Param1Name']:
				parameters += ConvertEnu(features['Param1Type']) + " " + features['Param1Name']
				if features['Param1Name'] == "length" and features['Param2Type'] == "string":
					# special case removal
					parameters = ""
			if features['Param2Name']:
				if parameters:
					parameters += ", "
				parameters += ConvertEnu(features['Param2Type']) + " " + features['Param2Name']

		returnType = stringresult
		if not returnType and Face.IsEnumeration(features["ReturnType"]):
			returnType = "int "
		if not returnType and features["ReturnType"] != "void":
			returnType = convertStringResult(features["ReturnType"]) + " "

		explanation += '%seditor:%s%s%s(%s)' % (
			returnType,
			href,
			name,
			hrefEnd,
			parameters
		)
		if features["Comment"]:
			explanation += '<span class="comment">%s</span>' % CommentString(features)

		explanations[featureDefineName] = explanation

	for propname, property in properties:
		functionName = property['SetterName'] or property['GetterName']
		featureDefineName = "SCI_" + functionName.upper()
		explanation = ""
		href = "<a href='https://www.scintilla.org/ScintillaDoc.html#" + featureDefineName + "'>"
		hrefEnd = "</a>"

		direction = ""
		if not property['SetterName']:
			direction = " read-only"
		if not property['GetterName']:
			direction = " write-only"
		indexExpression = ""
		if property["IndexParamType"] != "void":
			indexExpression = "[" + property["IndexParamType"] + " " + property["IndexParamName"] + "]"

		explanation += '%s editor.%s%s%s%s%s' % (
			convertStringResult(property["PropertyType"]),
			href,
			propname,
			hrefEnd,
			indexExpression,
			direction
		)
		if property["SetterComment"]:
			explanation += '<span class="comment">%s</span>' % (property["SetterComment"].replace("<", "&lt;"))
		explanations[featureDefineName] = explanation

	lastSegment = ""
	for segment, featureId in idsInOrder:
		if featureId in explanations:
			if segment != lastSegment:
				out.append('\t<h2>' + segment + '</h2>')
				lastSegment = segment
			out.append('\t<p>' + explanations[featureId] + '</p>')
	out.append("")
	return out

def ReadMenuIDs(filename):
	ids = []
	with open(filename) as f:
		for line in f:
			if line.startswith("#define"):
				#~ print line
				try:
					_d, name, number = line.split()
					if name.startswith("IDM_"):
						ids.append((name, {"Value":number}))
				except ValueError:
					# No value present
					pass
	return ids

def RegenerateAll():
	faceLex = Face.Face()
	faceLex.ReadFromFile(srcRoot + "/lexilla/include/LexicalStyles.iface")
	face = Face.Face()
	face.ReadFromFile(srcRoot + "/scintilla/include/Scintilla.iface")
	menuIDs  = ReadMenuIDs(srcRoot + "/scite/src/SciTE.h")
	idsInOrder = idsFromDocumentation(srcRoot + "/scintilla/doc/ScintillaDoc.html")
	FileGenerator.Regenerate(srcRoot + "/scite/src/IFaceTable.cxx", "//", printIFaceTableCXXFile([face, faceLex, menuIDs]))
	FileGenerator.Regenerate(srcRoot + "/scite/doc/PaneAPI.html", "<!--",
		printIFaceTableHTMLFile([face, menuIDs, idsInOrder]))

if __name__=="__main__":
	RegenerateAll()
