# Copyright (C) 2010, 2011 Sebastian Thiel (byronimo@gmail.com) and contributors
#
# This module is part of GitDB and is released under
# the New BSD License: http://www.opensource.org/licenses/bsd-license.php
"""Contains basic implementations for the interface building blocks"""
from git.db.interface import *

from git.util import (
		pool,
		join,
		isfile,
		normpath,
		abspath,
		dirname,
		LazyMixin, 
		hex_to_bin,
		bin_to_hex,
		expandvars,
		expanduser,
		exists,
		is_git_dir
	)

from git.index import IndexFile
from git.config import GitConfigParser
from git.exc import 	(
						BadObject, 
						AmbiguousObjectName,
						InvalidGitRepositoryError,
						NoSuchPathError
						)

from async import ChannelThreadTask

from itertools import chain
import sys
import os


__all__ = (	'PureObjectDBR', 'PureObjectDBW', 'PureRootPathDB', 'PureCompoundDB', 
			'PureConfigurationMixin', 'PureRepositoryPathsMixin', 'PureAlternatesFileMixin',
			'PureIndexDB')


class PureObjectDBR(ObjectDBR):
	
	#{ Query Interface 
		
	def has_object_async(self, reader):
		task = ChannelThreadTask(reader, str(self.has_object_async), lambda sha: (sha, self.has_object(sha)))
		return pool.add_task(task) 
		
	def info_async(self, reader):
		task = ChannelThreadTask(reader, str(self.info_async), self.info)
		return pool.add_task(task)
		
	def stream_async(self, reader):
		# base implementation just uses the stream method repeatedly
		task = ChannelThreadTask(reader, str(self.stream_async), self.stream)
		return pool.add_task(task)
	
	def partial_to_complete_sha_hex(self, partial_hexsha):
		len_partial_hexsha = len(partial_hexsha)
		if len_partial_hexsha % 2 != 0:
			partial_binsha = hex_to_bin(partial_hexsha + "0")
		else:
			partial_binsha = hex_to_bin(partial_hexsha)
		# END assure successful binary conversion
		return self.partial_to_complete_sha(partial_binsha, len(partial_hexsha))
	
	#} END query interface
	
	
class PureObjectDBW(ObjectDBW):
	
	def __init__(self, *args, **kwargs):
		super(PureObjectDBW, self).__init__(*args, **kwargs)
		self._ostream = None
	
	#{ Edit Interface
	def set_ostream(self, stream):
		cstream = self._ostream
		self._ostream = stream
		return cstream
		
	def ostream(self):
		return self._ostream
	
	def store_async(self, reader):
		task = ChannelThreadTask(reader, str(self.store_async), self.store) 
		return pool.add_task(task)
	
	#} END edit interface
	

class PureRootPathDB(RootPathDB):
	
	def __init__(self, root_path):
		self._root_path = root_path
		super(PureRootPathDB, self).__init__(root_path)
		
		
		
	#{ Interface 
	def root_path(self):
		return self._root_path
	
	def db_path(self, rela_path):
		return join(self._root_path, rela_path)
	#} END interface
		

def _databases_recursive(database, output):
	"""Fill output list with database from db, in order. Deals with Loose, Packed 
	and compound databases."""
	if isinstance(database, CompoundDB):
		compounds = list()
		dbs = database.databases()
		output.extend(db for db in dbs if not isinstance(db, CompoundDB))
		for cdb in (db for db in dbs if isinstance(db, CompoundDB)):
			_databases_recursive(cdb, output)
	else:
		output.append(database)
	# END handle database type
	

class PureCompoundDB(CompoundDB, PureObjectDBR, LazyMixin, CachingDB):
	def _set_cache_(self, attr):
		if attr == '_dbs':
			self._dbs = list()
		elif attr == '_obj_cache':
			self._obj_cache = dict()
		else:
			super(PureCompoundDB, self)._set_cache_(attr)
	
	def _db_query(self, sha):
		""":return: database containing the given 20 byte sha
		:raise BadObject:"""
		# most databases use binary representations, prevent converting 
		# it everytime a database is being queried
		try:
			return self._obj_cache[sha]
		except KeyError:
			pass
		# END first level cache
		
		for db in self._dbs:
			if db.has_object(sha):
				self._obj_cache[sha] = db
				return db
		# END for each database
		raise BadObject(sha)
	
	#{ PureObjectDBR interface 
	
	def has_object(self, sha):
		try:
			self._db_query(sha)
			return True
		except BadObject:
			return False
		# END handle exceptions
		
	def info(self, sha):
		return self._db_query(sha).info(sha)
		
	def stream(self, sha):
		return self._db_query(sha).stream(sha)

	def size(self):
		return reduce(lambda x,y: x+y, (db.size() for db in self._dbs), 0)
		
	def sha_iter(self):
		return chain(*(db.sha_iter() for db in self._dbs))
		
	#} END object DBR Interface
	
	#{ Interface
	
	def databases(self):
		return tuple(self._dbs)

	def update_cache(self, force=False):
		# something might have changed, clear everything
		self._obj_cache.clear()
		stat = False
		for db in self._dbs:
			if isinstance(db, CachingDB):
				stat |= db.update_cache(force)
			# END if is caching db
		# END for each database to update
		return stat
		
	def partial_to_complete_sha_hex(self, partial_hexsha):
		len_partial_hexsha = len(partial_hexsha)
		if len_partial_hexsha % 2 != 0:
			partial_binsha = hex_to_bin(partial_hexsha + "0")
		else:
			partial_binsha = hex_to_bin(partial_hexsha)
		# END assure successful binary conversion 
		
		candidate = None
		for db in self._dbs:
			full_bin_sha = None
			try:
				if hasattr(db, 'partial_to_complete_sha_hex'):
					full_bin_sha = db.partial_to_complete_sha_hex(partial_hexsha)
				else:
					full_bin_sha = db.partial_to_complete_sha(partial_binsha, len_partial_hexsha)
				# END handle database type
			except BadObject:
				continue
			# END ignore bad objects
			if full_bin_sha:
				if candidate and candidate != full_bin_sha:
					raise AmbiguousObjectName(partial_hexsha)
				candidate = full_bin_sha
			# END handle candidate
		# END for each db
		if not candidate:
			raise BadObject(partial_binsha)
		return candidate
		
	def partial_to_complete_sha(self, partial_binsha, hex_len):
		"""Simple adaptor to feed into our implementation"""
		return self.partial_to_complete_sha_hex(bin_to_hex(partial_binsha)[:hex_len])
	#} END interface
	
		
class PureRepositoryPathsMixin(RepositoryPathsMixin):
	# slots has no effect here, its just to keep track of used attrs
	__slots__  = ("_git_path", '_bare')
	
	#{ Configuration 
	repo_dir = '.git'
	objs_dir = 'objects'
	#} END configuration
	
	#{ Subclass Interface
	def _initialize(self, path):
		epath = abspath(expandvars(expanduser(path or os.getcwd())))

		if not exists(epath):
			raise NoSuchPathError(epath)
		#END check file 

		self._working_tree_dir = None
		self._git_path = None
		curpath = epath
		
		# walk up the path to find the .git dir
		while curpath:
			if is_git_dir(curpath):
				self._git_path = curpath
				self._working_tree_dir = os.path.dirname(curpath)
				break
			gitpath = join(curpath, self.repo_dir)
			if is_git_dir(gitpath):
				self._git_path = gitpath
				self._working_tree_dir = curpath
				break
			curpath, dummy = os.path.split(curpath)
			if not dummy:
				break
		# END while curpath
		
		if self._git_path is None:
			raise InvalidGitRepositoryError(epath)
		# END path not found

		self._bare = self._git_path.endswith(self.repo_dir)
		if hasattr(self, 'config_reader'):
			try:
				self._bare = self.config_reader("repository").getboolean('core','bare') 
			except Exception:
				# lets not assume the option exists, although it should
				pass
		#END check bare flag
		
	#} end subclass interface
	
	#{ Object Interface
	
	def __eq__(self, rhs):
		if hasattr(rhs, 'git_dir'):
			return self.git_dir == rhs.git_dir
		return False
		
	def __ne__(self, rhs):
		return not self.__eq__(rhs)
		
	def __hash__(self):
		return hash(self.git_dir)

	def __repr__(self):
		return "%s(%r)" % (type(self).__name__, self.git_dir)
	
	#} END object interface
	
	#{ Interface
	
	@property
	def is_bare(self):
		return self._bare
		
	@property
	def git_dir(self):
		return self._git_path
		
	@property
	def working_tree_dir(self):
		if self.is_bare:
			raise AssertionError("Repository at %s is bare and does not have a working tree directory" % self.git_dir)
		#END assertion
		return dirname(self.git_dir)
	
	@property
	def objects_dir(self):
		return join(self.git_dir, self.objs_dir)
	
	@property
	def working_dir(self):
		if self.is_bare:
			return self.git_dir
		else:
			return self.working_tree_dir
		#END handle bare state
		
	def _mk_description():
		def _get_description(self):
			filename = join(self.git_dir, 'description')
			return file(filename).read().rstrip()
	
		def _set_description(self, descr):
			filename = join(self.git_dir, 'description')
			file(filename, 'w').write(descr+'\n')
			
		return property(_get_description, _set_description, "Descriptive text for the content of the repository")

	description = _mk_description()
	del(_mk_description)
	
	#} END interface
		
		
class PureConfigurationMixin(ConfigurationMixin):
	
	#{ Configuration
	system_config_file_name = "gitconfig"
	repo_config_file_name = "config"
	#} END
	
	def __init__(self, *args, **kwargs):
		"""Verify prereqs"""
		super(PureConfigurationMixin, self).__init__(*args, **kwargs)
		assert hasattr(self, 'git_dir')
	
	def _path_at_level(self, level ):
		# we do not support an absolute path of the gitconfig on windows , 
		# use the global config instead
		if sys.platform == "win32" and level == "system":
			level = "global"
		#END handle windows
			
		if level == "system":
			return "/etc/%s" % self.system_config_file_name
		elif level == "global":
			return normpath(expanduser("~/.%s" % self.system_config_file_name))
		elif level == "repository":
			return join(self.git_dir, self.repo_config_file_name)
		#END handle level
		
		raise ValueError("Invalid configuration level: %r" % level)
		
	#{ Interface
	
	def config_reader(self, config_level=None):
		files = None
		if config_level is None:
			files = [ self._path_at_level(f) for f in self.config_level ]
		else:
			files = [ self._path_at_level(config_level) ]
		#END handle level
		return GitConfigParser(files, read_only=True)
		
	def config_writer(self, config_level="repository"):
		return GitConfigParser(self._path_at_level(config_level), read_only=False)
	
	
	#} END interface
	
	
class PureIndexDB(IndexDB):
	#{ Configuration
	IndexCls = IndexFile
	#} END configuration
	
	@property
	def index(self):
		return self.IndexCls(self)
	
	
class PureAlternatesFileMixin(object):
	"""Utility able to read and write an alternates file through the alternates property
	It needs to be part of a type with the git_dir or db_path property.
	
	The file by default is assumed to be located at the default location as imposed
	by the standard git repository layout"""
	
	#{ Configuration
	alternates_filepath = os.path.join('info', 'alternates')	# relative path to alternates file
	
	#} END configuration
	
	def __init__(self, *args, **kwargs):
		super(PureAlternatesFileMixin, self).__init__(*args, **kwargs)
		self._alternates_path()	# throws on incompatible type
	
	#{ Interface 
	
	def _alternates_path(self):
		if hasattr(self, 'git_dir'):
			return join(self.git_dir, 'objects', self.alternates_filepath)
		elif hasattr(self, 'db_path'):
			return self.db_path(self.alternates_filepath)
		else:
			raise AssertionError("This mixin requires a parent type with either the git_dir property or db_path method")
		#END handle path
	
	def _get_alternates(self):
		"""The list of alternates for this repo from which objects can be retrieved

		:return: list of strings being pathnames of alternates"""
		alternates_path = self._alternates_path()

		if os.path.exists(alternates_path):
			try:
				f = open(alternates_path)
				alts = f.read()
			finally:
				f.close()
			return alts.strip().splitlines()
		else:
			return list()
		# END handle path exists

	def _set_alternates(self, alts):
		"""Sets the alternates

		:parm alts:
			is the array of string paths representing the alternates at which 
			git should look for objects, i.e. /home/user/repo/.git/objects

		:raise NoSuchPathError:
		:note:
			The method does not check for the existance of the paths in alts
			as the caller is responsible."""
		alternates_path = self._alternates_path() 
		if not alts:
			if isfile(alternates_path):
				os.remove(alternates_path)
		else:
			try:
				f = open(alternates_path, 'w')
				f.write("\n".join(alts))
			finally:
				f.close()
			# END file handling 
		# END alts handling

	alternates = property(_get_alternates, _set_alternates, doc="Retrieve a list of alternates paths or set a list paths to be used as alternates")
	
	#} END interface
