"""Module with an implementation for refspec parsing. It is the pure-python
version assuming compatible interface for reference and object types"""

from git.db.interface import ReferencesMixin
from git.exc import BadObject
from git.refs import (
						SymbolicReference, 
						Reference,
						HEAD,
						Head,
						TagReference
					)
from git.refs.head import HEAD
from git.refs.headref import Head
from git.refs.tag import TagReference

from git.objects.base import Object
from git.objects.commit import Commit
from git.util import (
							join,
							isdir, 
							isfile,
							hex_to_bin, 
							bin_to_hex,
							is_git_dir
						)
from string import digits
import os
import re

__all__ = ["PureReferencesMixin"]

#{ Utilities

def short_to_long(odb, hexsha):
	""":return: long hexadecimal sha1 from the given less-than-40 byte hexsha
		or None if no candidate could be found.
	:param hexsha: hexsha with less than 40 byte"""
	try:
		return bin_to_hex(odb.partial_to_complete_sha_hex(hexsha))
	except BadObject:
		return None
	# END exception handling
	
	
def name_to_object(repo, name, return_ref=False):
	"""
	:return: object specified by the given name, hexshas ( short and long )
		as well as references are supported
	:param return_ref: if name specifies a reference, we will return the reference
		instead of the object. Otherwise it will raise BadObject
	"""
	hexsha = None
	
	# is it a hexsha ? Try the most common ones, which is 7 to 40
	if repo.re_hexsha_shortened.match(name):
		if len(name) != 40:
			# find long sha for short sha
			hexsha = short_to_long(repo.odb, name)
		else:
			hexsha = name
		# END handle short shas
	#END find sha if it matches
	
	# if we couldn't find an object for what seemed to be a short hexsha 
	# try to find it as reference anyway, it could be named 'aaa' for instance
	if hexsha is None:
		for base in ('%s', 'refs/%s', 'refs/tags/%s', 'refs/heads/%s', 'refs/remotes/%s', 'refs/remotes/%s/HEAD'):
			try:
				hexsha = SymbolicReference.dereference_recursive(repo, base % name)
				if return_ref:
					return SymbolicReference(repo, base % name)
				#END handle symbolic ref
				break
			except ValueError:
				pass
		# END for each base
	# END handle hexsha

	# didn't find any ref, this is an error
	if return_ref:
		raise BadObject("Couldn't find reference named %r" % name)
	#END handle return ref

	# tried everything ? fail
	if hexsha is None:
		raise BadObject(name)
	# END assert hexsha was found
	
	return Object.new_from_sha(repo, hex_to_bin(hexsha))

def deref_tag(tag):
	"""Recursively dereference a tag and return the resulting object"""
	while True:
		try:
			tag = tag.object
		except AttributeError:
			break
	# END dereference tag
	return tag

def to_commit(obj):
	"""Convert the given object to a commit if possible and return it"""
	if obj.type == 'tag':
		obj = deref_tag(obj)
		
	if obj.type != "commit":
		raise ValueError("Cannot convert object %r to type commit" % obj)
	# END verify type
	return obj

def rev_parse(repo, rev):
	"""
	:return: Object at the given revision, either Commit, Tag, Tree or Blob
	:param rev: git-rev-parse compatible revision specification, please see
		http://www.kernel.org/pub/software/scm/git/docs/git-rev-parse.html
		for details
	:note: Currently there is no access to the rev-log, rev-specs may only contain
		topological tokens such ~ and ^.
	:raise BadObject: if the given revision could not be found
	:raise ValueError: If rev couldn't be parsed
	:raise IndexError: If invalid reflog index is specified"""
	
	# colon search mode ?
	if rev.startswith(':/'):
		# colon search mode
		raise NotImplementedError("commit by message search ( regex )")
	# END handle search
	
	obj = None
	ref = None
	output_type = "commit"
	start = 0
	parsed_to = 0
	lr = len(rev)
	while start < lr:
		if rev[start] not in "^~:@":
			start += 1
			continue
		# END handle start
		
		token = rev[start]
		
		if obj is None:
			# token is a rev name
			if start == 0:
				ref = repo.head.ref
			else:
				if token == '@':
					ref = name_to_object(repo, rev[:start], return_ref=True)
				else:
					obj = name_to_object(repo, rev[:start])
				#END handle token
			#END handle refname
			
			if ref is not None:
				obj = ref.commit
			#END handle ref
		# END initialize obj on first token
		
		
		start += 1
		
		# try to parse {type}
		if start < lr and rev[start] == '{':
			end = rev.find('}', start)
			if end == -1:
				raise ValueError("Missing closing brace to define type in %s" % rev)
			output_type = rev[start+1:end]	# exclude brace
			
			# handle type 
			if output_type == 'commit':
				pass # default
			elif output_type == 'tree':
				try:
					obj = to_commit(obj).tree
				except (AttributeError, ValueError):
					pass	# error raised later
				# END exception handling
			elif output_type in ('', 'blob'):
				if obj.type == 'tag':
					obj = deref_tag(obj)
				else:
					# cannot do anything for non-tags
					pass
				# END handle tag
			elif token == '@':
				# try single int
				assert ref is not None, "Require Reference to access reflog"
				revlog_index = None
				try:
					# transform reversed index into the format of our revlog
					revlog_index = -(int(output_type)+1)
				except ValueError:
					# TODO: Try to parse the other date options, using parse_date
					# maybe
					raise NotImplementedError("Support for additional @{...} modes not implemented")
				#END handle revlog index
				
				try:
					entry = ref.log_entry(revlog_index)
				except IndexError:
					raise IndexError("Invalid revlog index: %i" % revlog_index)
				#END handle index out of bound
				
				obj = Object.new_from_sha(repo, hex_to_bin(entry.newhexsha))
				
				# make it pass the following checks
				output_type = None
			else:
				raise ValueError("Invalid output type: %s ( in %s )"  % (output_type, rev))
			# END handle output type
			
			# empty output types don't require any specific type, its just about dereferencing tags
			if output_type and obj.type != output_type:
				raise ValueError("Could not accomodate requested object type %r, got %s" % (output_type, obj.type))
			# END verify ouput type
			
			start = end+1					# skip brace
			parsed_to = start
			continue
		# END parse type
		
		# try to parse a number
		num = 0
		if token != ":":
			found_digit = False
			while start < lr:
				if rev[start] in digits:
					num = num * 10 + int(rev[start])
					start += 1
					found_digit = True
				else:
					break
				# END handle number
			# END number parse loop
			
			# no explicit number given, 1 is the default
			# It could be 0 though 
			if not found_digit:
				num = 1
			# END set default num
		# END number parsing only if non-blob mode
		
		
		parsed_to = start
		# handle hiererarchy walk
		try:
			if token == "~":
				obj = to_commit(obj)
				for item in xrange(num):
					obj = obj.parents[0]
				# END for each history item to walk
			elif token == "^":
				obj = to_commit(obj)
				# must be n'th parent
				if num:
					obj = obj.parents[num-1]
			elif token == ":":
				if obj.type != "tree":
					obj = obj.tree
				# END get tree type
				obj = obj[rev[start:]]
				parsed_to = lr
			else:
				raise ValueError("Invalid token: %r" % token)
			# END end handle tag
		except (IndexError, AttributeError):
			raise BadObject("Invalid Revision in %s" % rev)
		# END exception handling
	# END parse loop
	
	# still no obj ? Its probably a simple name
	if obj is None:
		obj = name_to_object(repo, rev)
		parsed_to = lr
	# END handle simple name
	
	if obj is None:
		raise ValueError("Revision specifier could not be parsed: %s" % rev)

	if parsed_to != lr:
		raise ValueError("Didn't consume complete rev spec %s, consumed part: %s" % (rev, rev[:parsed_to]))
	
	return obj

#} END utilities

class PureReferencesMixin(ReferencesMixin):
	"""Pure-Python refparse implementation"""
	
	re_hexsha_only = re.compile('^[0-9A-Fa-f]{40}$')
	re_hexsha_shortened = re.compile('^[0-9A-Fa-f]{4,40}$')
	
	#{ Configuration
	# Types to use when instatiating references
	TagReferenceCls = TagReference
	HeadCls = Head
	ReferenceCls = Reference
	HEADCls = HEAD
	CommitCls = Commit
	#} END configuration
	
	def resolve(self, name):
		return self.resolve_object(name).binsha
		
	def resolve_object(self, name):
		return rev_parse(self, name)
		
	@property
	def references(self):
		return self.ReferenceCls.list_items(self)
		
	@property
	def heads(self):
		return self.HeadCls.list_items(self)
		
	@property
	def tags(self):
		return self.TagReferenceCls.list_items(self)
	
	def tag(self, name):
		return self.TagReferenceCls(self, self.TagReferenceCls.to_full_path(name))
	
	def commit(self, rev=None):
		if rev is None:
			return self.head.commit
		else:
			return self.resolve_object(str(rev)+"^0")
		#END handle revision
		
	def iter_trees(self, *args, **kwargs):
		return ( c.tree for c in self.iter_commits(*args, **kwargs) )

	def tree(self, rev=None):
		if rev is None:
			return self.head.commit.tree
		else:
			return self.resolve_object(str(rev)+"^{tree}")

	def iter_commits(self, rev=None, paths='', **kwargs):
		if rev is None:
			rev = self.head.commit
		
		return self.CommitCls.iter_items(self, rev, paths, **kwargs)

		
	@property
	def head(self):
		return self.HEADCls(self,'HEAD')
		
	def create_head(self, path, commit='HEAD', force=False, logmsg=None ):
		return self.HeadCls.create(self, path, commit, force, logmsg)
		
	def delete_head(self, *heads, **kwargs):
		return self.HeadCls.delete(self, *heads, **kwargs)
		
	def create_tag(self, path, ref='HEAD', message=None, force=False, **kwargs):
		return self.TagReferenceCls.create(self, path, ref, message, force, **kwargs)
		
	def delete_tag(self, *tags):
		return self.TagReferenceCls.delete(self, *tags)
		
		
	# compat
	branches = heads
	refs = references
