#
# Copyright (c) 2006-2010 Nokia Corporation and/or its subsidiary(-ies).
# All rights reserved.
# This component and the accompanying materials are made available
# under the terms of the License "Eclipse Public License v1.0"
# which accompanies this distribution, and is available
# at the URL "http://www.eclipse.org/legal/epl-v10.html".
#
# Initial Contributors:
# Nokia Corporation - initial contribution.
#
# Contributors:
#
# Description: 
# generic_path module
#

import os
import sys
import re
import types
import ctypes

# are we on windows, and if so what is the current drive letter
isWin = sys.platform.lower().startswith("win")
if isWin:
	drive = re.match('^([A-Za-z]:)',os.getcwd()).group(0)

# regex for "bare" drive letters	
driveRE = re.compile('^[A-Za-z]:$')

# Base class

class Path:
	"""This class represents a file path.
	
	A generic path object supports operations without needing to know
	about Windows and Linux differences. The standard str() function can
	obtain a string version of the path in Local format for use by
	platform-specific functions (file opening for example).
	
	We use forward slashes as path separators (even on Windows).
	
	For example,
	
		path1 = generic_path.Path("/foo")
		path2 = generic_path.Path("bar", "bing.bang")
		
		print str(path1.Append(path2))
		
	Prints /foo/bar/bing.bang		on Linux
	Prints c:/foo/bar/bing.bang		on Windows (if c is the current drive)
	""" 
		
	def __init__(self, *arguments):
		"""construct a path from a list of path elements"""
		
		if len(arguments) == 0:
			self.path = ""
			return
		
		list = []
		for i,arg in enumerate(arguments):
			if isWin:
				if i == 0:
					# If the first element starts with \ or / then we will
					# add the current drive letter to make a fully absolute path
					if arg.startswith("\\\\"):
						list.append(arg) # A UNC path - don't mess with it
					elif arg.startswith("\\") or arg.startswith("/"):
						list.append(drive + arg)
					# If the first element is a bare drive then dress it with a \
					# temporarily otherwise "join" will not work properly.
					elif driveRE.match(arg):
						list.append(arg + "\\")
					# nothing special about the first element
					else:
						list.append(arg)
				else:
					if arg.startswith("\\\\"):
						raise ValueError("non-initial path components must not start with \\\\ : %s" % arg)
					else:
						list.append(arg)
				if ";" in arg:
					raise ValueError("An individual windows Path may not contain ';' : %s" % arg)
			else:
				list.append(arg)
	
		self.path = os.path.join(*list)
		
		# normalise to avoid nastiness with dots and multiple separators
		# but do not normalise "" as it will become "."
		if self.path != "":
			self.path = os.path.normpath(self.path)
		
		# always use forward slashes as separators
		self.path = self.path.replace("\\", "/")
		
		# remove trailing slashes unless we are just /
		if self.path != "/":
			self.path = self.path.rstrip("/")
		
	def __str__(self):
		return self.path
	
	def GetNeutralStr(self):
		"""return the path as a string that could be included in other paths."""
		return self.path.replace(":","").replace("/","")

	def GetLocalString(self):
		"""return a string in the local file-system format.
		
		e.g. C:/tmp on Windows or /C/tmp on Linux"""
		return self.path
	
	def isAbsolute(self):
		"test whether this path is absolute or relative"
		# C: is an absolute directory
		return (os.path.isabs(self.path) or driveRE.match(self.path))
	
	def Absolute(self):
		"""return an object for the absolute version of this path.
		
		Prepends the current working directory to relative paths and
		the current drive (on Windows) to /something type paths."""
		# leave C: alone as abspath will stick the cwd on
		if driveRE.match(self.path):
			return Path(self.path)
		else:
			return Path(os.path.abspath(self.path))
	
	def Append(self, *arguments):
		"return an object with path elements added at the end of this path"
		return Join(*((self,) + arguments))
	
	def Prepend(self, *arguments):
		"return an object with path elements added at the start of this path"
		return Join(*(arguments + (self,)))
	
	def isDir(self):
		"test whether this path points to an existing directory"
		# C: is a directory
		return (os.path.isdir(self.path) or driveRE.match(self.path))
	
	def isFile(self):
		"test whether this path points to an existing file"
		return os.path.isfile(self.path)

	def Exists(self):
		"test whether this path exists in the filesystem"
		if driveRE.match(self.path):
			return os.path.exists(self.path + "/")
		else:
			return os.path.exists(self.path)
		
	def Dir(self):
		"return an object for the directory part of this path"
		if driveRE.match(self.path):
			return Path(self.path)
		else:
			return Path(os.path.dirname(self.path))

	def File(self):
		"return a string for the file part of this path"
		return os.path.basename(self.path)

	def Components(self):
		"""return a list of the components of this path."""
		return self.path.split('/')

	def FindCaseless(self):
		"""Given a path which may not be not correct in terms of case,
		search the filesystem to find the corresponding, correct path.
		paths are assumed to be absolute and normalised (which they
		should be in this class).

		Assumes that the path is more right than wrong, i.e. starts
		with the full path and tests for existence - then takes the
		last component off and check for that.

		This will be inefficient if used in cases where the file 
		has a high probability of not existing.
		"""

		if os.path.exists(self.path):
			return Path(self.path)

		unknown_elements = []
		tail = self.path
		head = None
		while tail != '': 
			if os.path.exists(tail):
				break
			else:
				(tail,head) = os.path.split(tail)
				#print "(head,tail) = (%s,%s)\n" % (head,tail)
				unknown_elements.append(head)

		if tail == None:
			result = ""
		else:
			result = tail

		# Now we know the bits that may be wrong so we can search for them
		unknown_elements.reverse()
		for item in unknown_elements:
			possible = os.path.join(result, item) 
			if os.path.exists(possible):
				result = possible
				continue # not finished yet - only this element is ok

			# Nope, we really do have to search for this component of the path
			possible = None
			if result:
				for file in os.listdir(result):
					if file.lower() == item.lower():
						possible = os.path.join(result,file)
						break # find first matching name (might not be right)
				if possible is None:
					result = "" 
					break # really couldn't find the file
				result = possible

		if result == "":
			return None

		return Path(result)

	def From(self,source):
		"""Returns the relative path from 'source' to here."""
		list1 = source.Absolute().Components()
		list2 = self.Absolute().Components()

		# on windows if the drives are different
		# then the relative path is the absolute one.
		if isWin and list1[0] != list2[0]:
			return self.Absolute()

		final_list = []
		for item in list1:
			if list2 != []:
				for widget in list2:
					if item == widget:
						list2.pop(0)
						break
					else:
						final_list.insert(0, "..")
						final_list.append(widget)
						list2.pop(0)
						break
			else:
				final_list.insert(0, "..")

		final_list.extend(list2)

		return Join(*final_list)

	def GetShellPath(self):
		"""Returns correct slashes according to os type as a string
		"""
		if isWin:
			if  "OSTYPE" in os.environ and os.environ['OSTYPE'] == "cygwin" :
				return self.path

			return self.path.replace("/", "\\")

		return self.path

	def GetSpaceSafePath(self):
		"""Returns a version of the path where spaces don't interfere with shell interpretation.
		
		This functionality only applies to Windows - paths containing spaces are assumed to be problematic
		on non-Windows platforms.
		
		On Windows, the path is returned in Windows-specific 8.3 short path form - tilde are used to replace
		spaces and fit path elements within 8.3 requirements.  As 8.3 format paths are not guaranteed to be
		supported on all Windows installs, and can only be calculated if they exist, a newly formated path is
		only returned if it is returned by the Windows API and tested to exist.
		"""
		
		if not isWin:
			return None
		
		from ctypes.wintypes import DWORD, LPSTR, MAX_PATH

		GetShortPathNameA = ctypes.windll.kernel32.GetShortPathNameA
		GetShortPathNameA.restype = DWORD
		GetShortPathNameA.argtypes = LPSTR, LPSTR, DWORD
		
		buffer = ctypes.create_string_buffer(MAX_PATH)
		GetShortPathNameA(self.path, buffer, MAX_PATH)
		
		spacesafe = buffer.value
		
		if not spacesafe or not os.path.exists(spacesafe):
			return None
		
		return spacesafe
		
# Module functions

def Join(*arguments):
	"""Concatenate the given list to make a generic path object. 
	
	This can accept both strings and Path objects, and join
	them "intelligently" to make a complete path."""
	list = []
	for arg in arguments:
		if isinstance(arg, Path):
			list.append(arg.path)
		else:
			list.append(arg)
		
	return Path(*list)

def CurrentDir():
	"return a Path object for the current working directory"
	return Path(os.getcwd())

def NormalisePathList(aList):
	"""Convert a list of strings into a list of Path objects"""
	return map(lambda x: Path(x), aList)

def Where(afile):
	"""Return the location of a file 'afile' in the system path.
	
	On windows, adds .exe onto the filename if it's not there. Returns the first location it found or None if it wasn't found.
	
	>>> Where("python")
	"/usr/bin/python"
	>>> Where("nonexistentfile")
	None
	"""
	location = None
	if sys.platform.startswith("win"):
		if not afile.lower().endswith(".exe"):
			afile += ".exe"
			
	for current_file in [os.path.join(loop_number,afile) for loop_number in
			     os.environ["PATH"].split(os.path.pathsep)]:
		if os.path.isfile(current_file):
			location = current_file
			break
	return location

# end of generic_path module
