# coding=utf-8
__author__ = "Gina Häußge <osd@foosel.net>"
__license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html'

import os
import traceback
import sys
import re
import tempfile
import logging
import shutil
from functools import wraps
import warnings


logger = logging.getLogger(__name__)

def warning_decorator_factory(warning_type):
	def specific_warning(message, stacklevel=1):
		def decorator(func):
			@wraps(func)
			def func_wrapper(*args, **kwargs):
				# we need to increment the stacklevel by one because otherwise we'll get the location of our
				# func_wrapper in the log, instead of our caller (which is the real caller of the wrapped function)
				warnings.warn(message, warning_type, stacklevel=stacklevel + 1)
				return func(*args, **kwargs)
			return func_wrapper

		return decorator
	return specific_warning

deprecated = warning_decorator_factory(DeprecationWarning)
pending_deprecation = warning_decorator_factory(PendingDeprecationWarning)

def get_formatted_size(num):
	"""
	Taken from http://stackoverflow.com/a/1094933/2028598
	"""
	for x in ["bytes","KB","MB","GB"]:
		if num < 1024.0:
			return "%3.1f%s" % (num, x)
		num /= 1024.0
	return "%3.1f%s" % (num, "TB")


def is_allowed_file(filename, extensions):
	return "." in filename and filename.rsplit(".", 1)[1] in extensions


def get_formatted_timedelta(d):
	if d is None:
		return None
	hours = d.days * 24 + d.seconds // 3600
	minutes = (d.seconds % 3600) // 60
	seconds = d.seconds % 60
	return "%02d:%02d:%02d" % (hours, minutes, seconds)


def get_formatted_datetime(d):
	if d is None:
		return None

	return d.strftime("%Y-%m-%d %H:%M")


def get_class(name):
	"""
	Taken from http://stackoverflow.com/a/452981/2028598
	"""
	parts = name.split(".")
	module = ".".join(parts[:-1])
	m = __import__(module)
	for comp in parts[1:]:
		m = getattr(m, comp)
	return m


def get_exception_string():
	locationInfo = traceback.extract_tb(sys.exc_info()[2])[0]
	return "%s: '%s' @ %s:%s:%d" % (str(sys.exc_info()[0].__name__), str(sys.exc_info()[1]), os.path.basename(locationInfo[0]), locationInfo[2], locationInfo[1])


def get_free_bytes(path):
	"""
	Taken from http://stackoverflow.com/a/2372171/2028598
	"""
	if sys.platform == "win32":
		import ctypes
		freeBytes = ctypes.c_ulonglong(0)
		ctypes.windll.kernel32.GetDiskFreeSpaceExW(ctypes.c_wchar_p(path), None, None, ctypes.pointer(freeBytes))
		return freeBytes.value
	else:
		st = os.statvfs(path)
		return st.f_bavail * st.f_frsize


def get_dos_filename(input, existingFilenames, extension=None):
	if input is None:
		return None

	if extension is None:
		extension = "gco"

	filename, ext = input.rsplit(".", 1)
	return find_collision_free_name(filename, extension, existingFilenames)


def find_collision_free_name(input, extension, existingFilenames):
	filename = re.sub(r"\s+", "_", input.lower().translate({ord(i):None for i in ".\"/\\[]:;=,"}))

	counter = 1
	power = 1
	while counter < (10 * power):
		result = filename[:(6 - power + 1)] + "~" + str(counter) + "." + extension
		if result not in existingFilenames:
			return result
		counter += 1
		if counter == 10 * power:
			power += 1

	raise ValueError("Can't create a collision free filename")


def safe_rename(old, new, throw_error=False):
	"""
	Safely renames a file.

	On Windows this is achieved by first creating a backup file of the new file (if it
	already exists), thus moving it, then renaming the old into the new file and finally removing the backup. If
	anything goes wrong during those steps, the backup (if already there) will be renamed to its old name and thus
	the operation hopefully result in a no-op.

	On other operating systems :func:`shutil.move` will be used instead.

	@param old the path to the old file to be renamed
	@param new the path to the new file to be created/replaced
	"""

	if sys.platform == "win32":
		fh, backup = tempfile.mkstemp()
		os.close(fh)

		try:
			if os.path.exists(new):
				silent_remove(backup)
				os.rename(new, backup)
			os.rename(old, new)
			os.remove(backup)
		except OSError as e:
			# if anything went wrong, try to rename the backup file to its original name
			logger.error("Could not perform safe rename, trying to revert")
			if os.path.exists(backup):
				silent_remove(new)
				os.rename(backup, new)
			if throw_error:
				raise e
	else:
		# on anything else than windows it's ooooh so much easier...
		shutil.move(old, new)


def silent_remove(file):
	"""
	Silently removes a file. Does not raise an error if the file doesn't exist.

	@param file the path of the file to be removed
	"""

	try:
		os.remove(file)
	except OSError:
		pass


def sanitize_ascii(line):
	return unicode(line, 'ascii', 'replace').encode('ascii', 'replace').rstrip()


def filter_non_ascii(line):
	"""
	Returns True if the line contains non-ascii characters, false otherwise

	@param line the line to test
	"""

	try:
		unicode(line, 'ascii').encode('ascii')
		return False
	except ValueError:
		return True


def dict_merge(a, b):
	'''recursively merges dict's. not just simple a['key'] = b['key'], if
	both a and bhave a key who's value is a dict then dict_merge is called
	on both values and the result stored in the returned dictionary.

	Taken from https://www.xormedia.com/recursively-merge-dictionaries-in-python/'''

	from copy import deepcopy

	if not isinstance(b, dict):
		return b
	result = deepcopy(a)
	for k, v in b.iteritems():
		if k in result and isinstance(result[k], dict):
			result[k] = dict_merge(result[k], v)
		else:
			result[k] = deepcopy(v)
	return result


def dict_clean(a, b):

	from copy import deepcopy
	if not isinstance(b, dict):
		return a

	result = deepcopy(a)
	for k, v in a.iteritems():
		if not k in b:
			del result[k]
		elif isinstance(v, dict):
			result[k] = dict_clean(v, b[k])
		else:
			result[k] = deepcopy(v)
	return result


def dict_contains_keys(a, b):
	if not isinstance(a, dict) or not isinstance(b, dict):
		return False

	for k, v in a.iteritems():
		if not k in b:
			return False
		elif isinstance(v, dict):
			if not dict_contains_keys(v, b[k]):
				return False

	return True


class Object(object):
	pass

def interface_addresses(family=None):
	import netifaces
	if not family:
		family = netifaces.AF_INET

	for interface in netifaces.interfaces():
		try:
			ifaddresses = netifaces.ifaddresses(interface)
		except:
			continue
		if family in ifaddresses:
			for ifaddress in ifaddresses[family]:
				if not ifaddress["addr"].startswith("169.254."):
					yield ifaddress["addr"]

def address_for_client(host, port):
	import socket

	for address in interface_addresses():
		try:
			sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
			sock.bind((address, 0))
			sock.connect((host, port))
			return address
		except:
			continue


