## BugPath
##
## Locates all of the necessary directories to provide access to data and asset files.
##
## It exposes several useful variables. Those ending in "Dir" are full paths to directories
## while those ending in "Folder" or "Name" are the base name of the file/folder.
## All directory functions return valid directories or the Python None value if not found.
##
## isMac()
##   Returns True if running on a Mac operating system.
##
## isMod()
##   Returns True if running as a mod, False otherwise.
##
## isNoCustomAssets()
##   Returns True if the NoCustomAssets setting is 1 in the mod's INI file, 
##   False if 0 or when not running as a mod.
##
## getModName()
##   Returns CvModName.modName
##
## getModDir(), getModFolder()
##   Directory of the loaded mod, or None if no mod is loaded.
##
## getUserDir()
##   The "My Documents\My Games" directory on Windows or the "Documents"
##   directory on MacOS X. Both are in the user's private documents area.
##   If it cannot be found, it will be the directory containing <root-dir>.
##
## getRootDir()
##   Directory containing the CivilizationIV.ini file.
##   If it cannot be found, will look in <user-dir> for a folder named <app-folder>
##   Use the CvAltRoot module to override the location found by this module.
##
## getAppDir(), getAppFolder()
##   Directory containing the BTS application (CIV4BeyondSword.exe on Windows).
##   This should be "Beyond the Sword" for most installations.
##
## getDataDir()
##   Directory containing the mod's data files: user settings, help files, etc.
##   This is the first folder containing a folder with a name matching the value of
##   SETTINGS_FOLDER ("UserSettings" by default).
##
##	 Search order:
##
##	   <user-dir>\<mod-name>	  C:\Users\<user>\Documents\BUG Mod
##	   <root-dir>\<mod-name>	  C:\Users\<user>\Documents\Beyond the Sword\BUG Mod
##	   <app-dir>\<mod-name>	   C:\Programs\Sid Meier's Civilization IV\Beyond the Sword\BUG Mod
##	   <mod-dir>\<data-folder>	C:\Programs\Sid Meier's Civilization IV\Beyond the Sword\Mods\BUG Mod x.x\Data
##	   <mod-dir>				  C:\Programs\Sid Meier's Civilization IV\Beyond the Sword\Mods\BUG Mod x.x
##
## getSettingsDir()
##   Directory containing the mod's user settings.
##
## getInfoDir()
##   Directory containing the mod's informational files such as the readme and help files.
##
## TODO
##
##   Add override for dataDir.
##   Add some other setting instead of modName for dataDir search.
##
## Based on CvPath by Dr. Elmer Jiggles.
##
## Copyright (c) 2008 The BUG Mod.
##
## Author: EmperorFool

from CvPythonExtensions import *
import os
import os.path
import sys
import BugConfigTracker
import BugUtil


## Constants

DATA_FOLDER = "Data"
SETTINGS_FOLDER = "UserSettings"
INFO_FOLDER = "Info"

MODS_FOLDER = "Mods"
ASSETS_FOLDER = "Assets"
CUSTOM_ASSETS_FOLDER = "CustomAssets"

MY_DOCUMENTS_FOLDER_REG_KEYS = (
	("XP", r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders", "Personal"),
	("Vista", r"Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders", "Personal"),
)
MY_GAMES_FOLDER = "My Games"


def isMac():
	"""
	Returns True if running on a Mac operating system.
	"""
	return sys.platform == 'darwin'


## Mod Info, Folder and Path Accessors

def getModName():
	"""
	Returns the name of the mod as specified in CvModName.
	
	See the file "Assets/Python/Contrib/CvModName.py" for more information.
	"""
	initModName()
	return _modName

def isMod():
	"""
	Returns True if running as a mod, False otherwise.
	"""
	initModFolder()
	return _isMod

def isNoCustomAssets():
	"""
	Returns True if the NoCustomAssets setting is 1 in the mod's INI file, 
	False if 0 or when not running as a mod.
	"""
	initNoCustomAssetsSetting()
	return _noCustomAssets

def getModDir():
	"""
	Returns the full path of the mod directory if running as a mod.
	
	Can be overridden using a CvModFolder module, but this shouldn't be necessary
	as the folder's name is acquired from BTS itself.
	"""
	initModFolder()
	return _modDir

def getModFolder():
	"""
	Returns the name of the mod directory if running as a mod.
	"""
	initModFolder()
	return _modFolder

def getUserDir():
	"""
	Returns the full path of the user's Documents/My Games directory.
	
	See the file "Info/CvAltRoot.py" for more information.
	"""
	initRootFolder()
	return _userDir

def getRootDir():
	"""
	Returns the full path of the directory containing the file "CivilizationIV.ini".
	
	See the file "Info/CvAltRoot.py" for more information.
	"""
	initRootFolder()
	return _rootDir

def getDataDir():
	"""
	Returns the full path of the directory containing the mod's data files: user settings, help files, etc.
	"""
	initDataFolder()
	return _dataDir

def getSettingsDir():
	"""
	Returns the full path of the directory containing the mod's user settings.
	"""
	initDataFolder()
	return _settingsDir

def getInfoDir():
	"""
	Returns the full path of the directory containing the mod's informational files: readme, help files, etc.
	"""
	initDataFolder()
	return _infoDir

def getAppDir():
	"""
	Returns the full path of the directory containing the "Civ4BeyondSword.exe" application.
	"""
	initAppFolder()
	return _appDir

def getAppFolder():
	"""
	Returns the name of the directory containing the "Civ4BeyondSword.exe" application.
	"""
	initAppFolder()
	return _appFolder


## Finding and Creating Files and Directories

def findAssetFile(name, subdir=None):
	"""
	Returns the full path to the named asset file by searching the paths above.
	"""
	initSearchPaths()
	for dir in _assetFileSearchPaths:
		path = getFilePath(dir, name, subdir)
		if path:
			return path
	if subdir:
		BugUtil.warn("BugPath - cannot find asset file %s in %s", name, subdir)
	else:
		BugUtil.warn("BugPath - cannot find asset file %s", name)
	return None

def findDataFile(name, subdir=None):
	"""
	Returns the full path to the named data file.
	"""
	return getFilePath(getDataDir(), name, subdir)

def findSettingsFile(name, subdir=None):
	"""
	Locates and returns the path to the named configuration file or None if not found.
	"""
	return getFilePath(getSettingsDir(), name, subdir)

def findIniFile(name, subdir=None):
	"""
	Locates and returns the path to the named configuration file or None if not found.
	
	Deprecated: Use findSettingsFile() instead.
	"""
	return findSettingsFile(name, subdir)

def findMainModIniFile():
	"""
	Locates and returns the path to the configuration file named for the mod or None if not found.
	"""
	if getModName():
		return findSettingsFile(getModName() + ".ini")
	BugUtil.warn("BugPath - mod name not set")
	return None

def findInfoFile(name, subdir=None):
	"""
	Locates and returns the path to the named informational file or None if not found.
	"""
	return getFilePath(getInfoDir(), name, subdir)


def createDataFile(name, subdir=None):
	"""
	Returns the path to the named data file.
	"""
	return createFile(getDataDir(), name, subdir)

def createSettingsFile(name, subdir=None):
	"""
	Returns the path to the named configuration file.
	"""
	return createFile(getSettingsDir(), name, subdir)

def createIniFile(name, subdir=None):
	"""
	Returns the path to the named configuration file.
	
	Deprecated: Use createSettingsFile() instead.
	"""
	return createSettingsFile(name, subdir)

def createInfoFile(name, subdir=None):
	"""
	Returns the path to the named informational file.
	"""
	return createFile(getInfoDir(), name, subdir)


def findDir(name):
	"""
	Locates the named directory in dataDir.
	"""
	path = join(getDataDir(), name)
	if isdir(path):
		return path
	return None

def makeDir(name):
	"""
	Creates a new directory in the dataDir folder.
	"""
	path = join(getDataDir(), name)
	if path and not isdir(path):
		try:
			safeInfoPath("BugPath - creating '%s'", path)
			os.makedirs(path)
			return path
		except IOError:
			BugUtil.trace("Cannot create directory '%s' in '%s", name, getDataDir())
	return path

def findOrMakeDir(name):
	"""
	Locates or creates the specified directory and returns the path to it.
	"""
	return findDir(name) or makeDir(name)


## Initialization

def init():
	"""
	Initializes the entire module.
	"""
	BugUtil.debug("BugPath - initializing...")
	initAppFolder()
	initModName()
	initModFolder()
	initNoCustomAssetsSetting()
	initRootFolder()
	initDataFolder()
	initSearchPaths()


## Application Directory

_appDir = None
_appFolder = "Beyond the Sword"
_appFolderInitDone = False
def initAppFolder():
	"""
	Locates the directory that contains the BTS application.
	"""
	global _appFolderInitDone
	if _appFolderInitDone:
		return
	BugUtil.debug("BugPath - initializing application folder")
	global _appDir, _appFolder
	
	# Determine the app directory that holds the executable and Mods
	# as well as the folder name inside MY_GAMES_FOLDER that holds CustomAssets and Mods.
	if isMac():
		_appDir = os.getcwd()
	else:
		if isfile(sys.executable):
			#BugUtil.debug("BugPath - exe is '%s'", sys.executable)
			_appDir = dirname(sys.executable)
	if _appDir:
		_appFolder = basename(_appDir)
		safeInfoPath("BugPath - app dir is '%s'", _appDir)
		safeDebugPath("BugPath - app folder is '%s'", _appFolder)
	else:
		BugUtil.warn("BugPath - no executable found")
	_appFolderInitDone = True


## Mod Display Name

_modName = None
_modNameInitDone = False
def initModName():
	"""
	Pulls the modName attribute from the CvModName module.
	"""
	global _modNameInitDone
	if _modNameInitDone:
		return
	global _modName
	try:
		import CvModName
		_modName = CvModName.modName
		safeInfoPath("BugPath - mod name is '%s'", _modName)
	except ImportError:
		BugUtil.error("CvModName.py module not present")
	except AttributeError:
		BugUtil.error("CvModName.py module has no modName setting")
	_modNameInitDone = True

	
## Mod Directory

_isMod = False
_modDir = None
_modFolder = None
_modFolderInitDone = False
def initModFolder():
	"""
	Checks if BUG is running as a mod and sets the folder and name if so.
	"""
	global _modFolderInitDone
	if _modFolderInitDone:
		return
	if not CyGame().isFinalInitialized():
		BugUtil.debug("BugInit - game not fully initialized")
		return
	BugUtil.debug("BugPath - initializing mod folder")
	global _modDir, _modFolder
	try:
		replay = CyReplayInfo()
		replay.createInfo(0)
		modDir = replay.getModName().replace('\\', r'/')
	except:
		BugUtil.trace("replay not ready")
	else:
		if modDir and len(modDir) > 2:
			if not setModDir(abspath(modDir)):
				BugUtil.error("Replay provided invalid mod directory")
	if not _modDir:
		BugUtil.debug("BugPath - checking CvModFolder")
		try:
			import CvModFolder
			modFolder = CvModFolder.modFolder
		except ImportError:
			BugUtil.debug("BugPath - CvModFolder module not present")
		except AttributeError:
			BugUtil.error("CvModFolder.py module has no modFolder setting")
		else:
			if not setModDir(join(getRootDir(), MODS_FOLDER, modFolder)):
				if not setModDir(join(getAppDir(), MODS_FOLDER, modFolder)):
					BugUtil.error("Cannot find mod folder using '%s' from CvModFolder.py", modFolder)
	if not _modFolder:
		BugUtil.debug("BugPath - no mod directory found")
	_modFolderInitDone = True

def setModDir(dir):
	safeDebugPath("BugPath - checking mod dir '%s'", dir)
	if isdir(dir):
		global _isMod, _modDir, _modFolder
		_isMod = True
		_modDir = dir
		_modFolder = basename(dir)
		safeInfoPath("BugPath - mod dir is '%s'", dir)
		safeDebugPath("BugPath - mod folder is '%s'", _modFolder)
		BugConfigTracker.add("Mod_Directory", _modDir)
		return True
	return False


## NoCustomAssets Setting

_noCustomAssets = False
_noCustomAssetsSettingInitDone = False
def initNoCustomAssetsSetting():
	"""
	Checks if the NoCustomAssets setting is enabled when running as a mod.
	"""
	global _noCustomAssetsSettingInitDone
	if _noCustomAssetsSettingInitDone:
		return
	if isMod():
		global _noCustomAssets
# EF: don't check game option because BUG is initialized only once at startup
#		if CyGame().isOption(GameOptionTypes.GAMEOPTION_LOCK_MODS):
#			_noCustomAssets = True
#			BugUtil.debug("BugPath - Lock Modified Assets is set")
#		else:
		if getModDir() and getModFolder():
			BugUtil.debug("BugPath - checking for NoCustomAssets")
			try:
				from configobj import ConfigObj
				config = ConfigObj(join(getModDir(), getModFolder() + ".ini"), encoding='utf_8')
				_noCustomAssets = config["CONFIG"].as_bool("NoCustomAssets")
			except:
				BugUtil.trace("BugPath - failed to parse mod INI file for NoCustomAssets")
		BugUtil.info("BugPath - NoCustomAssets is %s", _noCustomAssets)
	_noCustomAssetsSettingInitDone = True


## User and Root Directories

_userDir = None
_rootDir = None
_rootFolderInitDone = False
def initRootFolder():
	"""
	Finds the directory that contains the CivilizationIV.ini file and the user's documents directory.
	"""
	global _rootFolderInitDone
	if _rootFolderInitDone:
		return
	BugUtil.debug("BugPath - initializing system folders")
	global _rootDir, _userDir
	
	# override root dir from CvAltRoot
	try:
		import CvAltRoot
		altRootDir = CvAltRoot.rootDir
		if not setRootDir(altRootDir):
			BugUtil.error("Directory from CvAltRoot.py is not valid or does not contain CivilizationIV.ini")
	except ImportError:
		BugUtil.debug("BugPath - CvAltRoot module not present")
	except AttributeError:
		BugUtil.error("CvAltRoot.py module has no rootDir setting")
	except IOError, (errno, strerror):
		BugUtil.trace("Error accessing directory from CvAltRoot.py: [%d] %s", errno, strerror)
	
	# user dir
	if isMac():
		# Mac OS X
		if not setUserDir(join(os.environ['HOME'], "Documents")):
			BugUtil.warn("Cannot find user's Documents folder")
	else:
		# Windows
		import _winreg
		def getRegValue(root, subkey, name):
			key = _winreg.OpenKey(root, subkey)
			try:
				value = _winreg.QueryValueEx(key, name)
				return value[0]
			finally:
				key.Close()
		
		for version, key, subkey in MY_DOCUMENTS_FOLDER_REG_KEYS:
			try:
				myDocuments = getRegValue(_winreg.HKEY_CURRENT_USER, key, subkey)
			except:
				pass
			else:
				if setUserDir(join(myDocuments, MY_GAMES_FOLDER)):
					safeInfoPath("BugPath - found valid Windows %s My Documents folder registry key", version)
					break
		else:
			BugUtil.debug("BugPath - no valid My Documents registry key")
	
	# try to determine missing dir from other dir
	if not _rootDir:
		if _userDir:
			if isMac():
				setRootDir(join(_userDir, "Civilization IV " + getAppFolder()))
			else:
				setRootDir(join(_userDir, getAppFolder()))
	else:
		if not _userDir:
			setUserDir(dirname(_rootDir))
	# if neither is found, fail
	if not _rootDir:
		BugUtil.error("Cannot find CivilizationIV.ini; see %s/CvAltRoot.py for instructions", INFO_FOLDER)
	_rootFolderInitDone = True

def setRootDir(dir):
	safeDebugPath("BugPath - Checking root dir '%s'", dir)
	if isdir(dir) and isfile(join(dir, "CivilizationIV.ini")):
		global _rootDir
		_rootDir = dir
		safeInfoPath("BugPath - root dir is '%s'", dir)
		BugConfigTracker.add("Root_Directory", _rootDir)
		return True
	return False

def setUserDir(dir):
	safeDebugPath("BugPath - Checking user dir '%s'", dir)
	if isdir(dir):
		global _userDir
		_userDir = dir
		safeInfoPath("BugPath - user dir is '%s'", dir)
		return True
	return False


## Data Directory

_dataDir = None
_settingsDir = None
_infoDir = None
_dataFolderInitDone = False
def initDataFolder():
	"""
	Finds the first directory that contains a folder named SETTINGS_FOLDER.
	"""
	global _dataFolderInitDone
	if _dataFolderInitDone:
		return
	BugUtil.debug("BugPath - initializing data folder")
	
	dataDirs = (
		join(getUserDir(), getModName()),	# My Games\BUG Mod
		join(getRootDir(), getModName()),	# My Games\BTS\BUG Mod
		join(getAppDir(), getModName()),	 # Civ4\BTS\BUG Mod
		join(getModDir(), DATA_FOLDER),	  # Civ4\BTS\Mods\BUG Mod 3.6\Data
		join(getModDir()),				   # Civ4\BTS\Mods\BUG Mod 3.6
	)
	for dir in dataDirs:
		if setDataDir(dir):
			break
	else:
		BugUtil.error("No valid data directory containing %s found", SETTINGS_FOLDER)
	_dataFolderInitDone = True

def setDataDir(dir):
	if isdir(dir):
		safeDebugPath("BugPath - Checking data dir '%s'", dir)
		settingsDir = join(dir, SETTINGS_FOLDER)
		if isdir(settingsDir):
			safeInfoPath("BugPath - data dir is '%s'", dir)
			global _dataDir, _settingsDir, _infoDir
			_dataDir = dir
			_settingsDir = settingsDir
			_infoDir = join(dir, INFO_FOLDER)
			BugConfigTracker.add("Settings_Directory", _settingsDir)
			return True
	return False


## Asset Directories

_assetFileSearchPaths = []
_searchPathsInitDone = False
def initSearchPaths():
	"""
	Adds the CustomAssets, mod Assets and BTS Assets directories to a list of search paths.
	"""
	global _searchPathsInitDone
	if _searchPathsInitDone:
		return
	BugUtil.debug("BugPath - initializing asset search paths")
	
	assetDirs = [
		join(getModDir(), ASSETS_FOLDER),
		join(getAppDir(), ASSETS_FOLDER),
	]
	# EF: Mod's no longer access CustomAssets folder; too many issues
	if not isNoCustomAssets() and not isMod():
		assetDirs.insert(0, join(getRootDir(), CUSTOM_ASSETS_FOLDER))
	for dir in assetDirs:
		addAssetFileSearchPath(dir)
	
	if _assetFileSearchPaths:
		BugConfigTracker.add("Asset_Search_Paths", _assetFileSearchPaths)
	else:
		BugUtil.error("No asset directories found")
	_searchPathsInitDone = True

def addAssetFileSearchPath(path):
	"""
	Adds the given path to the search list if it is a directory.
	"""
	if isdir(path):
		_assetFileSearchPaths.append(path)


## None-Safe Path/Directory/File Functions

def getFilePath(root, name, subdir=None):
	"""
	Returns the full path to the named file, or None if it doesn't exist.
	"""
	if not root:
		BugUtil.warn("Invalid root directory looking for '%s'", name)
	if subdir:
		path = join(root, subdir, name)
	else:
		path = join(root, name)
	if isfile(path):
		return path
	safeDebugPath("BugPath - not a file: '%s'", path)
	return None

def createFile(root, name, subdir=None):
	"""
	Returns the path to a new file to be created.
	"""
	if subdir:
		return join(root, subdir, name)
	else:
		return join(root, name)

def join(*paths):
	for path in paths:
		if path is None:
			return None
	return os.path.join(*paths)

def dirname(path):
	if path is None:
		return None
	return os.path.dirname(path)

def basename(path):
	if path is None:
		return None
	return os.path.basename(path)

def abspath(path):
	if path is None:
		return None
	return os.path.abspath(path)

def isdir(path):
	if path is None:
		return False
	return os.path.isdir(path)

def isfile(path):
	if path is None:
		return False
	return os.path.isfile(path)


## Non-English-Safe Logging

def safeDebugPath(message, path):
	try:
		BugUtil.debug(message, path)
	except:
		pass

def safeInfoPath(message, path):
	try:
		BugUtil.info(message, path)
	except:
		pass
