# lldb_vectorworks Xcode

"""
This module defines lldb extensions for debugging Vectorworks with lldb / Xcode.
It relies on being run by LLDB's embedded python interpreter, described in
  https://lldb.llvm.org/use/python.html
  https://lldb.llvm.org/use/python-reference.html

The Xcode Vectorworks build should automatically configure a ~/.lldbinit file for you
which applies this module to your debugger environment. If you need to do this
manually, the ~/.lldbinit file should contain the line:

command script import (full-path-to)/lldb_vectorworks.py

This module is *NOT* a general-purpose python module usable in
other (non-LLDB-embedded) python environments.
"""

import sys
import inspect
import lldb
import lldb.formatters.Logger
import struct
import platform

# setup and initialization
CATEGORY_NAME = "vectorworks"
SUMMARY_PREFIX = "Summary_"
SYNTHETIC_PREFIX = "Synthetic_"

#=========================================================================================
def list_extension_components():
	summary_functions = []
	synthetic_classes = []
	for name, value in inspect.getmembers(sys.modules[__name__]):
		if inspect.isfunction(value) and name.startswith(SUMMARY_PREFIX):
			summary_functions.append(name[len(SUMMARY_PREFIX):])
		elif inspect.isclass(value) and name.startswith(SYNTHETIC_PREFIX):
			class_regex = None
			if getattr(value, 'regex', None):
				class_regex = value.regex()

			params = (name[len(SYNTHETIC_PREFIX):], class_regex)
			synthetic_classes.append(params)

	return (summary_functions, synthetic_classes)

#=========================================================================================
# For displaying StdUStr summary in Xcode
# Great writeup on how std::string is stored in memory https://joellaity.com/2020/01/31/string.html
def StdUStrSummary(valobj,internal_dict):
	middle = valobj.GetChildAtIndex(0).GetChildAtIndex(0).GetChildAtIndex(0).GetChildAtIndex(0)
	__s = middle.GetChildMemberWithName('__s')
	
	# __size_ was stored inside the child with no name that is not __data_
	indexOfData = __s.GetIndexOfChildWithName('__data_')
	__size_ = __s.GetChildAtIndex(1 if indexOfData == 0 else 0).GetChildMemberWithName('__size_')
	
	# However recently it was moved to the same level as __data_. Easier to find
	if not __size_.IsValid():
		__size_ = __s.GetChildMemberWithName('__size_')
	modeAndSize = __size_.GetValueAsUnsigned(0)
	
	__is_long_ = __s.GetChildMemberWithName('__is_long_')
	
	# String to be returned later.
	retStr = u'u"'
	
	# On M1 the string mode (long/short) bit is the MSB of the size field
	# On Intel, it's the LSB. The other bits should be shifted by one to get the string length
	# More recently, a separate __is_long_ field has been added to mark if the string is in long or short form
	stringModeMask = 0b10000000 if platform.machine() == "arm64" else 1
	if (modeAndSize & stringModeMask) == 0 and (not __is_long_.IsValid() or __is_long_.GetValueAsUnsigned(0) == 0):  # Short/static form
		if platform.machine() == "arm64":
			size = modeAndSize & 0b01111111
		else:
			size = modeAndSize >> 1
			
		# Current max array size is 11. If size is greater than 11, this object is not 
		# initialized and the content should be empty.
		if size < 12:	
			txCharBuf = __s.GetChildMemberWithName('__data_').GetPointeeData(0, size).uint16
			size = min([len(txCharBuf), size])
			for i in range(size):
				if txCharBuf[i] == 0:
					break;
				retStr = retStr + chr(txCharBuf[i])
			
	else:  # Long/dynamic form
		__l = middle.GetChildMemberWithName('__l')
		# We only show the first 1000 characters.
		size = __l.GetChildMemberWithName('__size_').GetValueAsUnsigned(0)
		if size > 1000:
			size = 1000
		txCharBuf = __l.GetChildMemberWithName('__data_').GetPointeeData(0, size).uint16
		
		size = min([len(txCharBuf), size])
		for i in range(size):
			if txCharBuf[i] == 0:
				break;
			retStr = retStr + chr(txCharBuf[i])
			
	retStr = retStr + u'"'
	
	return retStr.encode('utf-8')


#=========================================================================================
# For displaying TXString summary in Xcode
def TXStringSummary(valobj,internal_dict):
	return StdUStrSummary(valobj.GetChildMemberWithName('stdUStr'), internal_dict)

#=========================================================================================
def __lldb_init_module(debugger, dict):
	#reload(sys)
	#sys.setdefaultencoding('utf-8')

    # 1. use inspect module to list all summary functions and
	# synthetic provider classes.
	summary_functions, synthetic_classes = list_extension_components()
	summary_regexes = {
		#"TXSmallArray" : "^TSmallArray<.+>$"
	}

	# 2. add all summary functions
	for summary in summary_functions:
		# if a regular expression is specified for a type
		# this will override the default, non-regex type
		vardict = { "type" : summary, "regex": summary, "prefix" : SUMMARY_PREFIX, "category" : CATEGORY_NAME, "flags" : "" }
		if summary in summary_regexes:
			vardict["regex"] = summary_regexes[ summary ]
			vardict["flags"] = '-x'

		debugger.HandleCommand("type summary add --category %(category)s -F lldb_vectorworks.%(prefix)s%(type)s %(regex)s %(flags)s" % vardict)

	# 3. add all synthetic child provider classes
	for params in synthetic_classes:
		vardict = { "type" : params[0], "regex": params[1], "prefix" : SYNTHETIC_PREFIX, "category" : CATEGORY_NAME, "flags" : "" }

		if not params[1]:
			vardict["regex"] = params[0]
		else:
			vardict["flags"] = "-x"

		debugger.HandleCommand("type synthetic add --category %(category)s --python-class lldb_vectorworks.%(prefix)s%(type)s %(regex)s %(flags)s" % vardict)
    
    # 4. Add a format type to show TDType as integers
	debugger.HandleCommand("type format add -C no -f Int8 TDType --category %s" % CATEGORY_NAME)

	debugger.HandleCommand("type summary add --summary-string '${var.fData[0]%%y}${var.fData[1]%%y}${var.fData[2]%%y}${var.fData[3]%%y}-${var.fData[4]%%y}${var.fData[5]%%y}-${var.fData[6]%%y}${var.fData[7]%%y}-${var.fData[8]%%y}${var.fData[9]%%y}-${var.fData[10]%%y}${var.fData[11]%%y}${var.fData[12]%%y}${var.fData[13]%%y}${var.fData[14]%%y}${var.fData[15]%%y}' UuidStorage --category %s" % CATEGORY_NAME)

    # TFlag is 16 bits in one short (numbered backward).
	debugger.HandleCommand("type summary add -v -C yes --summary-string '${var.fStorage[15]%%B}' TFlag_shortbit0 --category %s" % CATEGORY_NAME)
	debugger.HandleCommand("type summary add -v -C yes --summary-string '${var.fStorage[14]%%B}' TFlag_shortbit1 --category %s" % CATEGORY_NAME)
	debugger.HandleCommand("type summary add -v -C yes --summary-string '${var.fStorage[13]%%B}' TFlag_shortbit2 --category %s" % CATEGORY_NAME)
	debugger.HandleCommand("type summary add -v -C yes --summary-string '${var.fStorage[12]%%B}' TFlag_shortbit3 --category %s" % CATEGORY_NAME)
	debugger.HandleCommand("type summary add -v -C yes --summary-string '${var.fStorage[11]%%B}' TFlag_shortbit4 --category %s" % CATEGORY_NAME)
	debugger.HandleCommand("type summary add -v -C yes --summary-string '${var.fStorage[10]%%B}' TFlag_shortbit5 --category %s" % CATEGORY_NAME)
	debugger.HandleCommand("type summary add -v -C yes --summary-string '${var.fStorage[9]%%B}' TFlag_shortbit6 --category %s" % CATEGORY_NAME)
	debugger.HandleCommand("type summary add -v -C yes --summary-string '${var.fStorage[8]%%B}' TFlag_shortbit7 --category %s" % CATEGORY_NAME)
	debugger.HandleCommand("type summary add -v -C yes --summary-string '${var.fStorage[7]%%B}' TFlag_shortbit8 --category %s" % CATEGORY_NAME)
	debugger.HandleCommand("type summary add -v -C yes --summary-string '${var.fStorage[6]%%B}' TFlag_shortbit9 --category %s" % CATEGORY_NAME)
	debugger.HandleCommand("type summary add -v -C yes --summary-string '${var.fStorage[5]%%B}' TFlag_shortbit10 --category %s" % CATEGORY_NAME)
	debugger.HandleCommand("type summary add -v -C yes --summary-string '${var.fStorage[4]%%B}' TFlag_shortbit11 --category %s" % CATEGORY_NAME)
	debugger.HandleCommand("type summary add -v -C yes --summary-string '${var.fStorage[3]%%B}' TFlag_shortbit12 --category %s" % CATEGORY_NAME)
	debugger.HandleCommand("type summary add -v -C yes --summary-string '${var.fStorage[2]%%B}' TFlag_shortbit13 --category %s" % CATEGORY_NAME)
	debugger.HandleCommand("type summary add -v -C yes --summary-string '${var.fStorage[1]%%B}' TFlag_shortbit14 --category %s" % CATEGORY_NAME)
	debugger.HandleCommand("type summary add -v -C yes --summary-string '${var.fStorage[0]%%B}' TFlag_shortbit15 --category %s" % CATEGORY_NAME)
 
    # TFlagByte is two bytes in one short (numbered backward).  Showing as hex instead of bool because TFirstSelTDTypeFlag uses it to hold a TDType
	debugger.HandleCommand("type summary add -v -C yes --summary-string '${var.fStorage[8-15]%%x}' TFlagByte_shortbit0 --category %s" % CATEGORY_NAME)
	debugger.HandleCommand("type summary add -v -C yes --summary-string '${var.fStorage[0-7]%%x}' TFlagByte_shortbit8 --category %s" % CATEGORY_NAME)

	#=====================================================================================
	# StdUStr and TXString
	debugger.HandleCommand('type summary add -F lldb_vectorworks.StdUStrSummary StdUStr --category %s' % CATEGORY_NAME)
	debugger.HandleCommand('type summary add -F lldb_vectorworks.TXStringSummary TXString --category %s' % CATEGORY_NAME)

	#=====================================================================================
	# 5. enable the vectorworks category
	debugger.HandleCommand("type category enable %s" % CATEGORY_NAME)
	
#=========================================================================================
def make_string4(F,L):
	strval = u''
	G = F.GetData().uint8
	for X in range(L):
		V = G[X]
		if V == 0:
			break
		strval = strval + chr(V)
	return u'u"' + strval + u'"'


#=========================================================================================
# Summaries
# In order to specify regex formats, update the 'summary_regexes' dict
# in __lldb_init_module.

def Summary_TFolderIdentifier(valobj, internal_dict):
	return valobj.GetChildMemberWithName("fcsPath").GetSummary()

def Summary_TFileIdentifier(valobj, internal_dict):
	return valobj.GetChildMemberWithName("fcsPath").GetSummary()

def Summary_ConstGSCStrPtr(valobj, internal_dict):
	return make_string4(valobj, 1000)


#=========================================================================================
# Synthetic Child Providers
# Any classes which implement a staticmethod called 'regex'
# will be treated as a regular expression format type.

class Synthetic_TSmallArray:
	@staticmethod
	def regex():
		return "^TSmallArray<.+>$"

	def __init__(self, valobj, internal_dict):
		self.item_count = 0
		self.valobj = valobj
		self.member_index_map = {
			"fFreeMemWhenDone" : 0,
			"fNumAllocItems" : 1,
			"fNumUsedItems" : 2,
			"fDataSize" : 3,
			"fDataOffset" : 4,
			"fCountOffset" : 5,
			"fCountZeroBased" : 6,
			"fGrowGeometrically" : 7,
			"fDataHandle" : 8
		}

		self.index_member_map = {}
		for key,value in self.member_index_map.items():
			self.index_member_map[value] = key

		self.free_mem_when_done = 0
		self.num_alloc_items = 0
		self.data_offset = 0
		self.data_size = 0
		self.template_type = None

	def num_children(self):
		return 10 + self.num_alloc_items

	def get_child_index(self,name):
		if name in self.member_index_map:
			return self.member_index_map[ name ]

		return None

	def get_child_at_index(self,index):
		if index < 0 or index > self.num_children():
			return None

		if index >= 0 and index <= 8:
			member_name = self.index_member_map[index]
			return self.valobj.GetChildMemberWithName(member_name)
		elif index >= 9:
			child_number = index - 9
			data_handle = self.valobj.GetChildMemberWithName("fDataHandle").Dereference()
			child_index = self.data_offset + (child_number * self.data_size)
			child_name = ("fDataHandle[%i]" % child_number)

			return data_handle.CreateChildAtOffset( child_name, child_index, self.template_type )

		return None

	def update(self):		
		free_mem_when_done = self.valobj.GetChildMemberWithName("fFreeMemWhenDone").GetValue()
		if (free_mem_when_done):
			format_string = "<" + (len(free_mem_when_done) * 'b')
			self.free_mem_when_done = int(struct.unpack( format_string, free_mem_when_done )[-1])
		else:
			format_string = ""
			self.free_mem_when_done = 0

		self.num_alloc_items = int(self.valobj.GetChildMemberWithName("fNumAllocItems").GetValue())

		self.data_offset = int(self.valobj.GetChildMemberWithName("fDataOffset").GetValue())
		self.data_size = int(self.valobj.GetChildMemberWithName("fDataSize").GetValue())

		self.template_type = self.find_type()

	def has_children(self):
		return True

	def find_type(self):
		mytype = self.valobj.GetType().GetUnqualifiedType()

		if mytype.IsReferenceType():
			output_type = mytype.GetDereferencedType()
		elif mytype.GetNumberOfTemplateArguments() > 0:
			output_type = mytype.GetTemplateArgumentType(0)
		else:
			output_type = mytype

		return output_type

#=========================================================================================
