import wrapt
import functools
import glob
import os
import shutil
import zipfile
from datetime import datetime
from typing import Callable
from box import Box
from checksumdir import dirhash
from . import logger

get_name=lambda some_path:os.path.splitext(os.path.basename(some_path))[0]

def get_parent_dir(some_path,depth=1):
	for i in range(depth):
		some_path=os.path.dirname(os.path.abspath(some_path))
	return some_path

def create_path(*paths,stop_depth=0):
	path=os.path.join(*paths)
	os.makedirs(get_parent_dir(path,stop_depth),exist_ok=True)
	return path

def get_existing_path(paths,safe=False):
	for p in paths:
		if os.path.exists(p): return p
	if not safe:
		raise FileNotFoundError('Could not find any existing path.')

def get_splitted(path):
	folders=[]
	while 1:
		path,folder=os.path.split(path)
		
		if folder!="":
			folders.append(folder)
		else:
			if path!="":
				folders.append(path)
			break
	folders.reverse()
	return folders

def create_zipfile(dir_to_zip,savepath=''):
	"""Create a zip file from all the files under 'dir_to_zip'.

	The output zip file will be saved to savepath.
	If savepath ends with '.zip', then the output zip file will be
	saved AS 'savepath'. Necessary tree subdirectories are created automatically.
	Else, savepath is assumed to be a directory path,
	hence the output zip file will be saved TO 'savepath'
	directory. Necessary tree subdirectories are created automatically.

	:return absolute savepath
	"""
	save_cwd=os.getcwd()
	dir_to_zip=os.path.abspath(dir_to_zip)
	if dir_to_zip in os.path.split(savepath)[0]: raise ValueError(
		'To avoid recursion), resultant "savepath" should not be located inside "dir_to_zip"',
		dict(dir_to_zip=dir_to_zip,savepath=savepath))
	parent_dir,dir_name=os.path.split(dir_to_zip)
	os.chdir(parent_dir)
	if savepath:
		if savepath.endswith('.zip'):
			create_path(savepath,stop_depth=1)
		else:
			create_path(savepath,stop_depth=0)
			savepath=os.path.join(savepath,dir_name+'.zip')
	else: savepath=dir_to_zip+'.zip'
	
	pwd_length=len(os.getcwd())
	with zipfile.ZipFile(savepath,"w",compression=zipfile.ZIP_DEFLATED) as zf:
		for dirpath,dirnames,filenames in os.walk(dir_to_zip):
			for name in sorted(dirnames):
				path=os.path.normpath(os.path.join(dirpath,name))
				zf.write(path,path[pwd_length+1:])
			
			for name in filenames:
				path=os.path.normpath(os.path.join(dirpath,name))
				if os.path.isfile(path):
					zf.write(path,path[pwd_length+1:])
	
	os.chdir(save_cwd)
	return os.path.abspath(savepath)

def generate_name_portions(*args,__kv_delim__='-',**kwargs):
	arg_parts=[str(x) for x in args]
	kwarg_parts=[f"{k}{__kv_delim__}{v}" for k,v in kwargs.items() if v is not None]
	return arg_parts+kwarg_parts

def _get_name_parts_from_depth(path,include_depth):
	if type(include_depth) is Callable:
		parent_parts=include_depth(get_splitted(path))
	elif type(include_depth) is int:
		parent_parts=get_splitted(path)[-include_depth:]
	elif type(include_depth) in [tuple,list]:
		parent_parts=[get_splitted(path)[-x] for x in include_depth]
	elif type(include_depth) is slice:
		parent_parts=get_splitted(path)[-include_depth]
	else:
		raise ValueError(f'include_depth={include_depth}')
	return parent_parts

class Folder(os.PathLike):
	
	def __init__(self,path='',reactive=True,assert_exists=False):
		if isinstance(path,Folder): path=path.path
		path=os.path.abspath(os.path.normpath(path))
		if self._is_file(path): path=get_parent_dir(path)
		if not self._exists(path):
			# possible_extension=os.path.splitext(path)[-1]
			# if possible_extension:
			# 	logger.warning('Provided folder path (which does not exist) appears to have an extension %s',possible_extension)
			# 	pass
			if assert_exists: raise AssertionError('Path should already exist.',dict(path=path))
			elif reactive: create_path(path)
			
		
		self.path=path
		self._class=functools.partial(self.__class__)
		self._inherit_class=self._class
		self.reactive=reactive
	
	@property
	def name(self) -> str:
		return os.path.split(self.path)[-1]
	
	def __str__(self):
		# return f"{self.__class__.__name__}(r'{self.path}')"
		return self.path
	
	def get_filepath(self,*name_args,ext='',delim='_',include_depth=None,datetime_loc_index=None,iterated=False,**name_kwargs):
		parts=list()
		if include_depth: parts.extend(_get_name_parts_from_depth(self.path,include_depth))
		parts.extend(generate_name_portions(*name_args,**name_kwargs))
		if datetime_loc_index is not None: parts.insert(datetime_loc_index,datetime.now().strftime("%Y-%m-%d_%H-%M-%S"))
		assert parts,'Nothing is provided to create filepath.'
		
		output_path=os.path.join(self.path,delim.join(map(str,parts)).replace(' ',delim)+ext)
		if iterated:
			return create_iterated_path(output_path)
		else:
			return output_path
	
	def _exists(self,path):
		return os.path.exists(path)
	
	def _is_file(self,path):
		return os.path.isfile(path)
	
	def _create_dir(self,dirpath,**kwargs):
		return self._inherit_class(dirpath,**kwargs)
	
	def create(self,*name_args,delim='_',datetime_loc_index=None,iterated=False,reactive=None,**name_kwargs):
		"""
		:param name_args: args that are converted to name portions.
		:param delim: delimiter to use between the name string portions
		:param datetime_loc_index: index location of datetime in the resultant list of name portions.
		:param iterated: if iterated==True, method it will first check for existence of the folder. If exists, it creates a folder with new name,
		which is generated by incrementing the folder name.
		:param reactive: if not provided, default value is used. If provided, new folder will be created with specified value.
		:param name_kwargs: kwargs that are converted to name portions.
		:return: new instance of the class.
		:rtype: Folder
		"""
		new_dirpath=self.get_filepath(*name_args,ext='',delim=delim,include_depth=None,
		                              datetime_loc_index=datetime_loc_index,iterated=iterated,**name_kwargs)
		# new_folder=self._create_dir(new_dirpath,**({} if reactive is None else dict(reactive=reactive)))
		new_folder=self._create_dir(new_dirpath,**dict(reactive=self.reactive if reactive is None else reactive))
		return new_folder
	
	def delete(self):
		# assert self.reactive,f'Folder {self.path} is not reactive'
		shutil.rmtree(self.path,ignore_errors=True)
	
	@logger.trace(skimpy=True)
	def clear(self):
		# assert self.reactive,f'Folder {self.path} is not reactive'
		self.delete()
		# try:
		#
		# except FileNotFoundError:
		# 	# lu.simple_logger.warning('FileNotFoundError when trying to clear up the results folder. Passing on.')
		# 	pass
		create_path(self.path)
		return self
	
	@property
	def children(self):
		return self.glob_search('*')
	
	def glob_search(self,*patterns,recursive=True):
		return glob.glob(self.get_filepath(*patterns),recursive=recursive)
	
	@logger.trace(skimpy=True)
	def zip(self,zip_filepath='',forced=False):
		zip_filepath=zip_filepath or self.parent().get_filepath(self.name,ext='.zip')
		if len(get_splitted(zip_filepath))==1: zip_filepath=self.parent().get_filepath(zip_filepath)
		if not zip_filepath.endswith('.zip'): zip_filepath+='.zip'
		# if not self.reactive: return zip_filepath
		
		# assert self.reactive,f'Folder {self.path} is not reactive'
		
		if os.path.exists(zip_filepath):
			if forced: os.remove(zip_filepath)
			else: return zip_filepath
		
		create_zipfile(self.path,zip_filepath)
		return zip_filepath
	
	@logger.trace(skimpy=True)
	def unzip(self,zip_filepath,create_subdir=True):
		# assert self.reactive,f'Folder {self.path} is not reactive'
		# if save_path_formatter is None: save_path_formatter=lambda x:x
		# zip_path=[zip_filepath,
		#           zip_filepath+'.zip',
		#           self.get_filepath(zip_filepath),
		#           self.get_filepath(zip_filepath,ext='.zip')]
		# zip_path=get_existing_path(zip_path)
		# assert not os.path.isdir(zip_path),f'The folder "{zip_filepath}" cannot be unzipped.'
		if create_subdir:
			zipfilename=os.path.splitext(zip_filepath)[0]
			dst_folder_path=self.create(zipfilename).path
		else:
			dst_folder_path=self.path
		if not self.reactive: return self._inherit_class(dst_folder_path)
		shutil.unpack_archive(zip_filepath,dst_folder_path,'zip')
		return self._inherit_class(dst_folder_path)
	
	@logger.trace(skimpy=True)
	def rename(self,new_name):
		new_path=self.parent().get_filepath(new_name)
		# new_path=os.path.join(get_parent_dir(self.path),new_name)
		if self.reactive: os.rename(self.path,new_path)
		self.path=new_path
		return self
	
	@logger.trace(skimpy=True)
	def move_to(self,dst_path):
		if not isinstance(dst_path,str): dst_path=dst_path.path
		# if self.reactive: shutil.move(self.path,dst_path)
		shutil.move(self.path,dst_path)
		self.path=os.path.join(dst_path,self.name)
		return self
	
	def parent(self,depth=1):
		"""

		:rtype: Folder
		"""
		return self._inherit_class(get_parent_dir(self.path,depth=depth))
	
	@logger.trace(skimpy=True)
	def copy_to(self,dst_folder,new_name='',forced=False):
		"""

		:rtype: Folder
		"""
		dst_folder_path=self._inherit_class(dst_folder).get_filepath(new_name or self.name)
		delete_dst=lambda:shutil.rmtree(dst_folder_path,ignore_errors=True)
		resulter=lambda:self._class(dst_folder_path)
		
		if os.path.exists(dst_folder_path):
			if forced: delete_dst()
			else:
				source_hashsum=dirhash(self.path)
				destination_hashsum=dirhash(dst_folder_path)
				if source_hashsum==destination_hashsum: return resulter()
				else: delete_dst()
		
		shutil.copytree(self.path,dst_folder_path)
		return resulter()
	
	def size(self,byte_factor=1e-9):
		total_size=0
		for dirpath,dirnames,filenames in os.walk(self.path):
			for f in filenames:
				fp=os.path.join(dirpath,f)
				total_size+=os.path.getsize(fp)
		return total_size*byte_factor
	
	@property
	def exists(self):
		return self._exists(self.path)
	
	# def create_iterated(self,start=None,delim='_',empty_ok=False):
	# 	if empty_ok:
	# 		validator_func=lambda existing_path:not self._inherit_class(existing_path).children
	# 	else:
	# 		validator_func=lambda existing_path:existing_path==self.path
	#
	# 	new_iter_folder_path=create_iterated_path(self.path,start,delim,validator_func=validator_func)
	# 	last_part=new_iter_folder_path.split(delim)[-1]
	# 	if last_part.isnumeric() and int(last_part)==(start or 0):
	# 		new_folder=self.rename(new_iter_folder_path)
	# 	else:
	# 		new_folder=self.__class__(new_iter_folder_path)
	# 		if not new_folder==self and not self.children(): self.delete()
	# 	return new_folder
	
	def __getitem__(self,item):
		item=str(item)
		if os.path.splitext(item)[-1]:
			return self.get_filepath(item)
		else:
			return self.create(item)
	
	def __eq__(self,other):
		if isinstance(other,str):
			return self.path==other
		else:
			return self.path==other.path
	
	def __setitem__(self,key,value):
		filename,ext=os.path.splitext(key)
		if isinstance(value,str):
			string_obj=value
		elif type(value) in [list,tuple]:
			string_obj='\n'.join(map(str,value))
		elif isinstance(value,dict):
			ext=ext or '.yaml'
			if ext in ['.yaml','.yml']:
				Box(value).to_yaml(filename=self.get_filepath(filename,ext=ext))
				return
			elif ext=='.json':
				Box(value).to_json(filename=self.get_filepath(filename,ext=ext))
				return
			else:
				string_obj='\n'.join(map(lambda kv:f"{kv[0]}: {kv[1]}",value.items()))
		
		else:
			string_obj=str(value)
		# def writer(filehandler):
		# 	for k,v in value.items():
		# 		filehandler.writelines([f"{k}: {v}"])
		
		with open(self.get_filepath(filename,ext=ext or '.txt'),'w') as fh:
			fh.write(str(string_obj))
	
	def __repr__(self):
		return f"{self.__class__.__name__}(r'{self.path}')"
	
	# return self.path
	
	def __fspath__(self):
		return self.path

def create_iterated_path(iterated_path: str,start=None,delim='_',validator_func=None) -> str:
	if validator_func is None: validator_func=lambda x:False
	base_path,item_name_with_ext=os.path.split(iterated_path)
	item_name,ext=os.path.splitext(item_name_with_ext)
	
	curr_iter=0 if start is None else start
	while True:
		new_item_name=delim.join([item_name,f'{curr_iter}'])
		new_itempath=os.path.join(base_path,new_item_name+ext)
		if not os.path.exists(new_itempath) or validator_func(new_itempath): return new_itempath
		curr_iter+=1
