from cmd import IDENTCHARS
import sublime, sublime_plugin
import os
import Pywin32.setup
import win32com.client
import win32api
import win32process
import win32gui
import win32con
import tempfile
import subprocess
import re
import urllib
from urllib import request
import time
import os


settings_file = "StataEditor.sublime-settings"

# def is_running(process):
# 	""" Check if process is running """
# 	wmi = win32com.client.GetObject('winmgmts:')
# 	for p in wmi.InstancesOf('win32_process'): 
# 		if re.search(process, p.Name):
# 			return True
# 	return False

def find_files(file_ext):
	""" Create list of all files in project folders with given file extension """
	project_folders = []
	project_data = sublime.active_window().project_data()
	for i in range(0,len(project_data['folders'])):
		project_folders.append(project_data['folders'][i]['path'])
	start_time = time.time()
	for new_path in project_folders:
		for root, dirs, files in os.walk(new_path):
			for file in files:
				if file.endswith(file_ext):
					relDir = os.path.relpath(root, new_path)
					relFile = os.path.join(relDir, file)
					sublime.file_list.append(relFile)
				diff_time = time.time() - start_time
				# With huge folders, the loop can take a long time and freeze ST.
				# This condition tries to limit this problem by
				# breaking if the loop takes more than 1 second.
				# However it can only break between folders, so one huge folder
				# could still lead to memory leaks.
				if diff_time > 1:
					return

def temp_file_exists():
	""" Check a given temp file exists """
	file_name = 'emergency_close_stata_st.dta'
	tmp_dta = os.path.join(tempfile.gettempdir(), file_name)
	for file in os.listdir(tempfile.gettempdir()):
		if file == file_name:
			return True, tmp_dta
	return False, tmp_dta

def plugin_loaded():
	global settings
	settings = sublime.load_settings(settings_file)
	sublime.statamgr = SessionMgr()
	sublime.locked_stata_short_title = None

def StataAutomate(stata_command):
	""" Launch Stata (if needed) and send commands """
	try:
		sublime.stata.DoCommandAsync(stata_command)

	except:
		win32api.WinExec(settings.get("stata_path")) 
		if settings.get("stata_version") >= 15:
			# Extra time is needed for the Dispatch command to hook up to
			# the already launched Stata. Change "waiting_time" setting as needed.
			time.sleep(settings.get("waiting_time"))
		sublime.stata = win32com.client.Dispatch("stata.StataOLEApp")
		sublime.stata.DoCommand("cd " + getDirectory())
		sublime.stata.DoCommandAsync(stata_command)
		if settings.get("file_completions") != False:
			sublime.file_list = []
			for file_ext in settings.get("file_completions").split(","):
				find_files("." + file_ext.strip())

def getDirectory():
	var_dict = sublime.active_window().extract_variables()
	if settings.get("default_path") == "current_path":
		try:
			set_dir = "%(file_path)s" % var_dict
			set_dir = "\"" + set_dir + "\""
		except:
			try:
				set_dir = "%(project_path)s" % var_dict
				set_dir = "\"" + set_dir + "\""
			except:
				set_dir = ""
	elif settings.get("default_path") == "project_path" or settings.get("default_path") == "":
		try:
			set_dir = "%(project_path)s" % var_dict
			set_dir = "\"" + set_dir + "\""
		except:
			set_dir = ""
	else:
		set_dir = settings.get("default_path")
		set_dir = "\"" + set_dir + "\""
	return set_dir

def SelectCode(self,selection):
	all_text = ""
	len_sels = 0
	sels = self.view.sel()
	len_sels = 0
	for sel in sels:
		len_sels = len_sels + len(sel)

	if len_sels == 0 and selection == "default":
		all_text = self.view.substr(self.view.find('(?s).*',0))

	elif len_sels != 0 and selection == "default":
		for sel in sels:
			self.view.sel().add(self.view.line(sel.begin()))
			self.view.sel().add(self.view.line(sel.end()))
		
		for sel in sels:
			all_text = all_text + self.view.substr(sel) + "\n"

	elif selection == "rest_of_file":
		first_sel = sels[0]
		self.view.sel().add(self.view.line(first_sel.begin()))
		orig_sel_region = sublime.Region(first_sel.begin(),self.view.size())
		self.view.sel().add(orig_sel_region)
		for sel in sels:
			all_text = all_text + self.view.substr(sel) + "\n"

	elif selection == "run_above":
		first_sel = sels[0]
		self.view.sel().add(self.view.line(first_sel.begin()))
		orig_sel_region = sublime.Region(0, first_sel.end())
		self.view.sel().add(orig_sel_region)
		for sel in sels:
			all_text = all_text + self.view.substr(sel) + "\n"
			
	elif selection == "line":
		for sel in sels:
			all_text = all_text + self.view.substr(self.view.line(sel)) + "\n"

	elif selection == "selection_only":
		for sel in sels:
			all_text = all_text + self.view.substr(sel) + "\n"

	if all_text[-1] != "\n":
		all_text = all_text + "\n"

	return all_text
	
class StataExecuteCommand(sublime_plugin.TextCommand):
	def run(self, edit, **args):
		sublime.statamgr.print_sessions()
		if 'command' in args:
			all_text = args['command']
		else:
			all_text = SelectCode(self,args["Selection"])
		if settings.get('remove_comments'):
			command = remove_comments(all_text)
		else:
			command = all_text
		command = re.sub('\n+', '\n', command)
		if settings.get('send_using_temp_file'):
			dofile_path = os.path.join(tempfile.gettempdir(), 'st_stata_temp.tmp')

			if settings.get("stata_version") <= 13:
				this_file = open(dofile_path,'w',encoding=settings.get("character_encoding"))

			if settings.get("stata_version") >= 14:
				this_file = open(dofile_path,'w',encoding='utf-8')

			this_file.write(command)
			this_file.close()
			nr_w_close = 0
			command = str(args["Mode"]) + ' "' + dofile_path +'"'
		# StataAutomate(str(args["Mode"]) + ' "' + dofile_path +'"')
		sublime.statamgr.update_sessions()
		self.stata_execute_command(command)
		sublime.statamgr.update_sessions()

	def stata_execute_command(self, command: str):
		if sublime.statamgr.size == 0:
			sublime.statamgr.launch_stata(getDirectory(), command)
		elif sublime.statamgr.size == 1:
			sess = sublime.statamgr.sessions[0]
			assert sess is not None
			sess.send_cmd(command)
		else:
			if settings.get("lock_mode"):
				if sublime.locked_stata_short_title is None:
					if sublime.ok_cancel_dialog(
						"Locked Stata session is not selected. "
						"Do you want to select the locked Stata session now?", 
						"Yes"):
						self.view.run_command(
							"stata_change_locked_session", args={
								'withcommand': True,
								'command': command
							})
				else:
					sess = sublime.statamgr.get_session_by_short_title(
						sublime.locked_stata_short_title
					)
					if sess is None:
						if sublime.ok_cancel_dialog(
							"The locked Stata session is not running. "
							"Do you want to change the Stata session to lock?", 
							"Yes"):
							self.view.run_command(
								"stata_change_locked_session", args={
									'withcommand': True,
									'command': command
								})
					else:
						sess.send_cmd(command)
				# sess = sublime.statamgr.get_session_by_title(sublime.locked_stata_short_title)
				# sess.send_cmd(command)
			else:
				# Ask user which session to send to and run callback on_select
				self.command = command
				self.stata_titles = sublime.statamgr.get_short_title_list()
				self.view.window().show_quick_panel(
					self.stata_titles, self.on_select)
	
	def on_select(self, id):
		if id >= 0:
			sess = sublime.statamgr.get_session_by_short_title(self.stata_titles[id])
			sess.send_cmd(self.command)


class StataHelpExternal(sublime_plugin.TextCommand):
	def run(self,edit):
		self.view.run_command("expand_selection", {"to": "word"})
		sel = self.view.sel()[0]
		help_word = self.view.substr(sel)
		help_command = "help " + help_word

		StataAutomate(help_command)

class StataHelpInternal(sublime_plugin.TextCommand):
	def run(self,edit):
		self.view.run_command("expand_selection", {"to": "word"})
		sel = self.view.sel()[0]
		help_word = self.view.substr(sel)
		help_word = re.sub(" ","_",help_word)

		help_adress = "http://www.stata.com/help.cgi?" + help_word
		helpfile_path = os.path.join(tempfile.gettempdir(), 'st_stata_help.txt')

		try:
			a = urllib.request.urlopen(help_adress)
			source_code = a.read().decode("utf-8")
			a.close()

			regex_pattern = re.findall("<!-- END HEAD -->\n(.*?)<!-- BEGIN FOOT -->", source_code, re.DOTALL)
			help_content = re.sub("<h2>|</h2>|<pre>|</pre>|<p>|</p>|<b>|</b>|<a .*?>|</a>|<u>|</u>|<i>|</i>","",regex_pattern[0])
			help_content = re.sub("&gt;",">",help_content)
			help_content = re.sub("&lt;",">",help_content)

			with open(helpfile_path, 'w') as f:
				f.write(help_content)

			self.window = sublime.active_window()
			self.window.open_file(helpfile_path)
		
		except:
			print("Could not retrieve help file")

class StataLocal(sublime_plugin.TextCommand):
	def run(self,edit):
		sels = self.view.sel()
		for sel in sels:
			if len(sel) == 0:
				word_sel = self.view.word(sel.a)
			else:
				word_sel = sel
			word_str = self.view.substr(word_sel)
			word_str = "`"+word_str+"'"
			self.view.replace(edit,word_sel,word_str)

class StataLoad(sublime_plugin.TextCommand):
	def run(self,edit):
		sel = self.view.substr(self.view.sel()[0])
		StataAutomate("use " + sel + ", clear")

class StataForceClose(sublime_plugin.EventListener):
	""" Force Stata to close when Sublime Text closes """
	def on_close(self,view):
		sublime.statamgr.update_sessions()
		# Check if there exists an open Sublime Text window and if there is only
		# one Stata session
		if len(sublime.windows()) == 0 and settings.get('force_close_stata'):
			# Only implemented if there is only one Stata session; otherwise,
			# it is difficult for users to keep track of all the Stata sessions
			if sublime.statamgr.size == 1:
				# Check if an active Stata session has been launched from Sublime Text
				try:
					# print(sublime.stata)
					st = sublime.statamgr.sessions[0]
					# print(st)
					# If there is no emergency backup, prompt message and save backup, then delete the Stata session.
					if temp_file_exists()[0] == False:
						if settings.get('restore_session'):
							sublime.message_dialog("Stata is about to close. Restart\nSublime Text to restore the session.")
							st.comobj.DoCommand("save " + temp_file_exists()[1] + ", replace")
						else:
							sublime.message_dialog("Stata is about to close. By user's setting, the session will not be restored when restarting Sublime Text.")
						st.close()
				except:
					pass

class StataRestore(sublime_plugin.EventListener):
	def on_text_command(self, view, name, args):
		# Check if an emergency backup file exists
		if settings.get('restore_session') and temp_file_exists()[0] == True:
			rest = sublime.ok_cancel_dialog("Stata was forced to shut down as Sublime Text closed. Would you like to restore your previous session?")
			tmp_dta = temp_file_exists()[1]
			if rest == True:
				win32api.WinExec(settings.get("stata_path"))
				sublime.stata = win32com.client.Dispatch("stata.StataOLEApp")
				sublime.stata.DoCommand("cd " + getDirectory())
				sublime.stata.DoCommand('use ' + tmp_dta + ', clear')
				os.remove(tmp_dta)
			else:
				os.remove(tmp_dta)

class StataLaunchCommand(sublime_plugin.TextCommand):
	def run(self, view):
		# for i in range(int(text)):
		sublime.statamgr.launch_stata(getDirectory())
			# time.sleep(3)
# 	def input(self, args):
# 		return PosIntInputHandler()
	
# 	def input_description(self):
# 		return "Number of Stata session to launch:"


# class PosIntInputHandler(sublime_plugin.TextInputHandler):
# 	def name(self):
# 		return "text"
	
# 	def initial_text(self):
# 		return "1"
	
# 	def validate(self, text):
# 		try:
# 			tmp = int(text)
# 			if tmp > 0:
# 				return True
# 			else:
# 				return False
# 		except ValueError:
# 			return False

		
	
class StataChangeModeCommand(sublime_plugin.TextCommand):
	def run(self, view):
		self.view.window().show_quick_panel(
			['Free mode', 'Lock mode'], self.on_select
		)
	
	def on_select(self, id):
		if id == 0:
			settings.set('lock_mode', False)
		elif id > 0:
			settings.set('lock_mode', True)

class StataDisplayModeCommand(sublime_plugin.TextCommand):
	def run(self, view):
		if settings.get('lock_mode'):
			msg = 'lock mode'
		else:
			msg = 'free mode'
		msg = 'StataEditor is running under the ' + msg
		sublime.message_dialog(msg)

class StataChangeLockedSessionCommand(sublime_plugin.TextCommand):
	def run(self, view, **args):
		if not settings.get('lock_mode'):
			sublime.error_message(
				'StataEditor is NOT under the lock mode! '
				'Please switch to the lock mode first.'
			)
		else:
			sublime.statamgr.update_sessions()
			self.stata_titles = sublime.statamgr.get_short_title_list()
			if len(args) > 0:
				self.withcommand = args['withcommand']
			else:
				self.withcommand = False
			if self.withcommand:
				self.command = args['command']
			if len(self.stata_titles) == 0:
				prompt_no_stata()
			else:
				self.view.window().show_quick_panel(
					self.stata_titles, self.on_select
				)
	
	def on_select(self, id):
		if id >= 0:
			sublime.locked_stata_short_title = self.stata_titles[id]
			if self.withcommand:
				sess = sublime.statamgr.get_session_by_short_title(sublime.locked_stata_short_title)
				sess.send_cmd(self.command)

class StataCloseCommand(sublime_plugin.TextCommand):
	def run(self, view):
		sublime.statamgr.update_sessions()
		self.stata_titles = sublime.statamgr.get_short_title_list()
		if len(self.stata_titles) == 0:
			prompt_no_stata()
		else:
			self.view.window().show_quick_panel(
				self.stata_titles, self.on_select
			)
	
	def on_select(self, id):
		if id >= 0:
			sublime.statamgr.close_stata(self.stata_titles[id])

class StataCurrentDirectory(sublime_plugin.TextCommand):
	def run(self, view):
		self.view.run_command('stata_execute', {
			'command': 'cd ' + getDirectory(),
			'Mode': 'include'
		})

def prompt_no_stata():
	sublime.error_message('There is no Stata session running!')

# Exceptions
class Error(Exception):
	pass

def get_stata_short_title(title: str) -> str:
	"""Return the short title after Stata version"""
	# Only record part of title up to Stata version
	pattern = r'((?:\d*\s*-\s*)?(Stata\/(IC|SE|MP))\s\d*(\.\d*)?)'
	matched = re.match(pattern, title)
	if matched is not None:
		return matched.groups(0)[0]
	else:
		raise Error('Invalid title for the Stata window')


class Session:
	"""Any session, including Sublime text

	Attributes:
		_pid: int, process ID
		_exec_path: str, path of executable
		_hwnd: int, HWND of the main window
		_title: str, title of the main window
	"""
	def __init__(
		self, pid: int, exec_path: str, hwnd: int, title: str
	) -> None:
		self._pid = pid
		self._exec_path = exec_path
		self._hwnd = hwnd
		self._title = title

	def set_foreground(self) -> None:
		"""Set the window as foreground and keep the cursor in the window.
		If the window is minimized, win32gui.SetForegroundWindow() will not
		show the window, but only activate it. As a result, on the task bar,
		the window icon is highlighted. In this case, we use 
		win32gui.ShowWindow() to restore the window and keep the cursor in it.
		If it is not minimized, win32gui.SetForegroundWindow() will
		bring the window to the front and keep the cursor in it.
		"""
		# Check if the window is minimized
		try:
			win_place = win32gui.GetWindowPlacement(self._hwnd)
		except:
			print(self._hwnd)
		if win_place[1] == win32con.SW_SHOWMINIMIZED:
			win32gui.ShowWindow(self._hwnd, win32con.SW_RESTORE)
		else:
			try:
				win32gui.SetForegroundWindow(self._hwnd)
			except BaseException as e:
				print('Error while setting foreground window')
				if e.args[1] == 'SetForegroundWindow' \
						and e.args[2] == 'No error message is available':
					# Type alt key
					shell = win32com.client.Dispatch("WScript.Shell")
					shell.SendKeys('%')
					shell.SendKeys('%')
					win32gui.SetForegroundWindow(self._hwnd)


class StataSession(Session):
	"""Stata session

	Attributes:
		_pid: int, process ID
		_exec_path: str, path of executable
		_hwnd: int, HWND of the main window
		_title: str, title of the main window
		_short_title: str, title up to Stata version
		_comobj: win32com.client.CDispatch, the COM Object for the Stata Session
	"""

	def __init__(
			self, pid: int, exec_path: str, hwnd: int, title: str, 
			comobj: win32com.client.CDispatch
		) -> None:
		super().__init__(pid, exec_path, hwnd, title)
		self._short_title = get_stata_short_title(title)
		self._comobj = comobj

	@property
	def pid(self) -> int:
		return self._pid
	
	@property
	def exec_path(self) -> str:
		return self._exec_path
	
	@property
	def hwnd(self) -> int:
		return self._hwnd
	
	@property
	def title(self) -> str:
		return self._title
	
	@property
	def short_title(self) -> str:
		return self._short_title

	@property
	def comobj(self) -> str:
		return self._comobj

	@comobj.setter
	def comobj(self, value):
		self._comobj = value

	def close(self):
		"""Close the session"""
		win32gui.PostMessage(self._hwnd, win32con.WM_CLOSE, 0, 0)

	def is_closed(self):
		"""Detect if the Stata session is closed."""
		return not win32gui.IsWindowVisible(self._hwnd)

	def send_cmd(self, command: str):
		# Session for the current sublime text (exec_path is not important here)
		subl_sess = get_current_window()
		# Activate Stata window
		super().set_foreground()
		if settings.get("use_stata_automation"):
			self.send_cmd_stata_automation(command)
		else:
			self.send_cmd_winapi(command)
		# Keep the cursor in sublime text
		if settings.get("keep_cursor_in_subl"):
			subl_sess.set_foreground()
		if settings.get("file_completions") != False:
			sublime.file_list = []
			for file_ext in settings.get("file_completions").split(","):
				find_files("." + file_ext.strip())
			

	def send_cmd_stata_automation(self, command: str):
		if self._comobj is not None:
			self._comobj.DoCommandAsync(command)
		else:
			self.send_cmd_winapi(command)

	def send_cmd_winapi(self, command: str):
		# ClassName of the command window and command to send, depending on version
		cmd_classname = self._get_cmdwin_classname(settings.get("stata_version"))
		# Get the hwnd and id of the command window text box (its parent is the
		#	 command panel, whose parent is the Stata window)
		cmd_hwnd = self._find_ctrlhwnds_by_classname(
			self._hwnd, cmd_classname
		)[0]
		# Send code
		cmd = self._reformat_command(command, settings.get("stata_version"))
		win32gui.SendMessage(cmd_hwnd, win32con.WM_SETTEXT, None, cmd)
		time.sleep(0.2)
		win32gui.PostMessage(cmd_hwnd, win32con.WM_KEYDOWN, win32con.VK_RETURN, 1)


	def __repr__(self) -> str:
		return '	Process ID: ' + str(self._pid) + '\n' + \
			'	Executable path: ' + str(self._exec_path) + '\n' + \
			'	HWND: ' + str(self._hwnd) + '\n' + \
			'	Title: ' + str(self._title) + '\n' + \
			'	Short title: ' + str(self._short_title) + '\n' + \
			'	Has COM?: ' + str(self._comobj is not None) + '\n'
	
	def __lt__(self, other) -> int:
		"""This is used to sort sessions by the numbers given in Stata"""
		return self.get_window_number() < other.get_window_number()

	def get_window_number(self) -> int:
		return 1 if self._title[:5] == "Stata" \
			else int(re.match(r'^(\d*)\s', self._title).groups(0)[0])
	
	@staticmethod
	def _get_cmdwin_classname(stata_version: float) -> str:
		"""Return the class name of command window based on Stata version"""
		if stata_version >= 14:
			cmdwin_classname = 'Scintilla'
		elif stata_version <= 13:
			cmdwin_classname = 'RichEdit20A'
		return cmdwin_classname

	@staticmethod
	def _find_ctrlhwnds_by_classname(parent_hwnd, target_classname: str) -> list:
		"""Construct a list of control HWNDs among child windows whose class name 
		matches the target one"""
		ctrlhwnds = []

		def _callback_find_by_classname(hwnd, classname):
			if win32gui.GetClassName(hwnd) == classname:
				ctrlhwnds.append(hwnd)

		win32gui.EnumChildWindows(
			parent_hwnd, _callback_find_by_classname, target_classname
		)
		return ctrlhwnds

	@staticmethod
	def _reformat_command(command: str, stata_version: float) -> str:
		"""Reformat Stata command based on Stata version"""
		# Thanks to ado-mode: Starting from Stata 14, commands need to be converted
		# to utf-16 before sending to Stata: Add \x00 at the end if the number of
		# hex (4-bit) is not a multiplier of 4. Otherwise, only the first character
		# of the string is sent.

		def convert_to_utf16(text):
			""" Convert str to utf-16 to send to Stata 14+ using AutoIt """
			# Encode text in utf-8 and convert to integer for easy left-shifting
			text_utf8 = text.encode('utf-8')
			nhex = len(text_utf8) * 2
			# Number of 4-bit to add at the end
			nshift = (nhex % 4) // 2
			# Shift
			text_new = text + "\u0000" * nshift
			# Convert to str in utf-16
			return text_new.encode('utf-8').decode('utf-16')
		
		if stata_version >= 14:
			outcmd = convert_to_utf16(command)
		else:
			outcmd = command
		return outcmd


class SessionMgr:
	"""Stata session manager
	
	Attributes:
		_sessions: list of Session
	"""

	def __init__(self) -> None:
		self._sessions = []

	def print_sessions(self) -> None:
		"""Print sessions (for debug)"""
		for i, s in enumerate(self._sessions):
			print('Session ' + str(i+1))
			print(s)

	@property
	def sessions(self) -> list:
		return self._sessions

	@property
	def size(self) -> int:
		return len(self._sessions)

	def raise_error(self, msg: str) -> None:
		raise Error(msg)

	def close_stata(self, title):
		"""Close the stata window identifid by the title."""
		stata_sess = self.get_session_by_short_title(title)
		if stata_sess is not None:
			stata_sess.close()

	def launch_stata(self, directory: str, command: str=None) -> None:
		"""Launch stata, change to current directory, and (possibly send code)."""
		self.update_sessions()
		# Get hwnd of the current sublime text window
		subl_sess = get_current_window()
		# Record before launch
		old_titles = self._get_short_title_set()
		last_nsess = len(old_titles)
		# Launch Stata session
		win32api.WinExec(settings.get("stata_path"))
		if settings.get("use_stata_automation"):
			if settings.get("stata_version") >= 15:
				# Extra time is needed for the Dispatch command to hook up to
				# the already launched Stata. Change "waiting_time" setting as needed.
				time.sleep(settings.get("waiting_time"))
			sessobj = win32com.client.Dispatch('stata.StataOLEApp')
		else:
			sessobj = None
		# It takes about 400 ms to launch Stata using Stata Automation
		time.sleep(0.5)
		# Wait until the launch is successful (max 10s)
		i = 0
		while True:
			i += 1
			self.update_sessions()
			time.sleep(0.1)
			nsess = len(self._sessions)
			if nsess > last_nsess:
				break
			if i > 100:
				sublime.error_message(
					'Failed to launch Stata in 10 seconds.'
					'Please try again.')
				return
		# Register COM Object of the new session
		new_sess_title = list(self._get_short_title_set() - old_titles)[0]
		new_sess = self.get_session_by_short_title(new_sess_title)
		new_sess.comobj = sessobj
		# Send code to enter the current directory to the new session
		new_sess.send_cmd('cd ' + directory)
		# Send code to execute
		if command is not None:
			new_sess.send_cmd(command)
		# Activate sublime text
		subl_sess.set_foreground()
		sublime.statamgr.print_sessions()
	
	def update_sessions(self):
		"""Update current stata sessions. This should be done any time before
		and after commands are submitted."""
		self._sessions = self._update_stata_sessions()
		# print('Number of sessions: ' + str(len(self._sessions)))
		if settings.get('lock_mode') and self._have_same_short_title():
			sublime.error_message(
				'At least two Stata sessions have the same title, '
				'so it is impossible to identify Stata sessions by title. '
				'Please try to close and reopen those Stata sessions.'
			)

	def get_short_title_list(self) -> list:
		"""Return a list of short titles in the sessions"""
		return [sess.short_title for sess in self._sessions]
	
	
	def _have_same_short_title(self) -> bool:
		titleset = self._get_short_title_set()
		titlelist = self.get_short_title_list()
		if len(titleset) < len(titlelist):
			return True
		else:
			return False
	
	def _get_short_title_set(self) -> set:
		"""Return a set of short titles in the sessions"""
		return set([sess.short_title for sess in self._sessions])
	
	def _get_stata_pids(self) -> list:
		"""Return a list of PIDs of Stata processes based on stata_path."""
		# collect all the running processes
		processes = win32com.client.GetObject('winmgmts:').ExecQuery(
			'Select * from win32_process')
		# Return PIDs whose executable path match Stata's
		# For some programs, which may raise PermissionError, such as Cisco AMP
		outlist = []
		for p in processes:
			try:
				if isinstance(p.ExecutablePath, str) and os.path.samefile(
					p.ExecutablePath, settings.get('stata_path')
				):
					outlist.append(p.ProcessId)
			except PermissionError:
				continue
		return outlist
		# return [
		# 	p.ProcessId for p in processes
		# 	if isinstance(p.ExecutablePath, str) and 
		# 		os.path.samefile(
		# 			p.ExecutablePath, settings.get('stata_path')
		# 		)
		# ]

	def _update_stata_sessions(self) -> list:
		"""Return a list of Session after removing closed ones."""
		# NOTE: One process ID may correspond to multiple windows related to
		# Stata, e.g. the Viewer or Data Editor window, but here we want to
		# identify the main window only.
		# Pattern to match Stata main window (exclude Graph or Viewer, etc.)
		pattern = r'(\d*\s*-\s*)?(Stata\/(IC|SE|MP))\s\d*(\.\d*)?'
		# Get a list of process ID of stata based on executable path
		stata_pid_list = self._get_stata_pids()
		# Construct Sessions
		stata_sessions = []
		for pid in stata_pid_list:
			# Find main window by matching the window title
			tmp_hwnds = [
				h for h in self._find_hwnds_by_pid(pid)
				if (win32api.GetWindowLong(h, win32con.GWL_STYLE) and
					win32con.WS_VISIBLE) 
					and (re.match(pattern, win32gui.GetWindowText(h)) is not None)
				]
			if len(tmp_hwnds) > 1:
				# This shouldn't happen, but leave it here for future bug reporting
				self.raise_error(
					'Warning: Multiple Stata main windows '
					'detected for a single Stata process'
				)
			elif len(tmp_hwnds) == 0:
				# This shouldn't happen either, but leave it here for future bug reporting
				self.raise_error(
					'Warning: Unable to find Stata window '
					'for the Stata process'
				)
			elif len(tmp_hwnds) == 1:
				hwnd = tmp_hwnds[0]
				title = win32gui.GetWindowText(hwnd)
				short_title = get_stata_short_title(title)
				sess = self._get_internal_session_from_short_title(short_title)
				if sess is None:
					stata_sessions.append(StataSession(
						pid, settings.get('stata_path'), hwnd, title, None
					))
				else:
					stata_sessions.append(StataSession(
						pid, settings.get('stata_path'), hwnd, title, sess.comobj
					))

		# Sort the list (The first session is "Stata/MP ..." and 
		#	 others are "X - Stata/MP ..." where X starts from 2.)
		stata_sessions.sort()
		return stata_sessions
	
	def _get_internal_session_from_short_title(self, short_title):
		"""Get the Stata session in _sessions with the title."""
		for sess in self._sessions:
			if sess.short_title == short_title:
				return sess
		else:
			return None
	
	def get_session_by_short_title(self, target_title: str) -> Session:
		"""Get session whose short title matches the target title in the manager"""
		matchedlist = [s for s in self._sessions if s.short_title == target_title]
		if len(matchedlist) == 0:
			return None
		elif len(matchedlist) > 1:
			raise Error('More than one HWND found for the given title.')
		else:
			return matchedlist[0]

	@staticmethod
	def _find_hwnds_by_pid(target_pid: int) -> list:
		"""Construct a list of HWNDs whose PID matches the target PID"""
		hwnds = []

		def _callback_find_by_pid(hwnd, target_pid: int):
			"""Add the HWND if its PID matches the target PID"""
			_, processid = win32process.GetWindowThreadProcessId(hwnd)
			if processid == target_pid:
				hwnds.append(hwnd)
		
		win32gui.EnumWindows(_callback_find_by_pid, target_pid)
		return hwnds

def get_current_window() -> Session:
	"""Get Session of the current window"""
	hwnd = win32gui.GetForegroundWindow()
	pid = win32process.GetWindowThreadProcessId(hwnd)[1]
	matchedlist = [
		p.ExecutablePath 
		for p in win32com.client.GetObject('winmgmts:').ExecQuery(
			'Select * from win32_process'
		) if p.ProcessId == pid
	]
	exec_path = matchedlist[0]
	return Session(
		pid, exec_path, hwnd, win32gui.GetWindowText(hwnd)
	)



def remove_comments(text: str) -> str:
	"""Remove comments from the code.
	
	Comments are defined as follows:
		* From the beginning-of-line '*' to the end of the line
		* From any '//' to the end of the line
		* From any '///' to the end of line, then remove '\n'.
		* Any character between matched pairs of '/*' and '*/' or '/*' and the
		end of file.
	"""
	# Deal with ///
	lines = text.split('\n')
	newlines = []
	i = 0
	while i < len(lines):
		while True:
			matched = re.search(r'///', lines[i])
			if matched is not None:
				lines[i] = lines[i][:matched.start()] + lines[i+1]
				lines.pop(i+1)
			else:
				newlines.append(lines[i])
				i += 1
				break
	# Deal with //
	for i in range(len(newlines)):
		matched = re.search(r'(?:^|\s)//[^/]', newlines[i])
		if matched is not None:
			newlines[i] = newlines[i][:matched.start()]
	# Deal with *
	for i in range(len(newlines)):
		if len(newlines[i].strip()) > 0 and newlines[i].strip()[0] == '*':
			newlines[i] = ''
	outstr = '\n'.join(newlines)
	# Deal with /*
	stack = []
	removelist = []
	for i in range(len(outstr)):
		if outstr[i:i+2] == '/*':
			stack.append(i)
		elif outstr[i:i+2] == '*/':
			removelist.append((stack.pop(), i+2))
	if len(stack) > 0:
		for elem in stack:
			removelist.append((elem, len(outstr)))
	outstrlist = list(outstr)
	for i in range(len(outstrlist)):
		for elem in removelist:
			if elem[0] <= i and i < elem[1]:
				outstrlist[i] = ''
	return ''.join(outstrlist)



if __name__ == '__main__':
	# settings = {
	#	 'stata_version': 17,
	#	 'stata_path': r'C:/Program Files/Stata17/StataMP-64.exe',
	#	 'activate_stata_before_send': True,
	#	 'keep_cursor_in_subl': True
	# }
	# mgr = SessionMgr(settings)
	# mgr.launch_stata(r'C:/Users/flcon/Documents')
	# mgr.update_sessions()
	# mgr.print_sessions()
	# mgr.send_cmd_to_session('sysuse auto\nsum', mgr.sessions[0].title)
	# mgr.launch_stata(r'C:\\', 'sysuse auto\n desc')
	code = '''sysuse /// asdfasdfsdf 
auto, ////sdfsdf
clear
sum

// sdfsdfsdf
sum // sdfsdf dsfs // ssdfsdf 
sum /* sdfsdfsdf */, detail
sum /*
dfsdf
sdf
sd
fs
df
*/, detail
sum /* asdfsdf sdfsdf
sdf
sd
fsd

    '''
	print(remove_comments(code))
