# encoding: utf-8

import time
import os
import platform
import sys
import errno
import string
import random
import codecs
from StringIO import StringIO
from contextlib import closing
from contextlib import contextmanager
from functools import wraps
from collections import defaultdict, OrderedDict, namedtuple


def if_not_none(value, func):
	return func(value) if value is not None else None


class Obj:
	def __init__(self, kw):
		self.__dict__.update(kw)

	def __repr__(self):
		# TODO: add unicode support, enquote string values in representation
		return "obj(" + ", ".join(["%s=%s" % (k, v) for k, v in self.__dict__.iteritems()]) + ")"


def obj(**kw):
	return Obj(kw)


def parse_bool(string):
	if string in ('true', '1'):
		return True
	elif string in ('false', '0'):
		return False
	else:
		raise Exception("Invalid boolean value string '%s', expected 'true' or 'false'" % (string,))

utf8_tree_symbols = {'|': u'│ ', '|-': u'├ ', '`-': u'└ ', ' ': '  '}
ascii_tree_symbols = {'|': u'|  ', '|-': u'|- ', '`-': u'`- ', ' ': '   '}


def draw_tree(root, symbols, spacing=0):
	def draw(node, indent, child_indent, stream):
		name, children = node
	
		lines = name.split('\n')
		stream.write(u'%s%s\n' % (indent, lines[0]))
		
		for line in lines[1:]:
			stream.write(u'%s%s\n' % (child_indent, line))
	
		if children:
			spacing_line = u'%s\n' % ('\n'.join([child_indent+symbols['|']] * spacing))
			if spacing > 0: 
				stream.write(spacing_line)
			for child in children[:-1]:
				draw(child, child_indent+symbols['|-'], child_indent+symbols['|'], stream)
				if spacing > 0: 
					stream.write(spacing_line)
			draw(children[-1], child_indent+symbols['`-'], child_indent+symbols[' '], stream)
	
	with closing(StringIO()) as stream:
		draw(root, u'', u'', stream)
		return stream.getvalue()


def generate_random_password(chars_to_include=''):
	"""Return a string of random characters.

	Arguments:
	chars_to_include: A list of characters to include. In addition to randomly
	chosen characters, characters from the list will be included in random
	order. The password length will increase accordingly.
	"""
	chars = [random.choice(string.lowercase) for _ in range(3)] + \
			[random.choice(string.uppercase) for _ in range(3)] + \
			[random.choice(string.digits) for _ in range(4)] + \
			[c for c in chars_to_include]
	random.shuffle(chars)
	return "".join(chars)


def sort_list_by_another(lst, order_lst):
	"""
	Sort one list with the order taken from another list
	"""
	return [value for value in order_lst if value in lst]


def ilen(iterable):
	"""Get number of elements in iterable collection.
	"""
	count = 0
	for _ in iterable:
		count += 1
	return count


def partition(iterable, func):
	condition_true = []
	condition_false = []

	for item in iterable:
		if func(item):
			condition_true.append(item)
		else:
			condition_false.append(item)
	return condition_true, condition_false


def try_until(condition_func, intervals, sleep=time.sleep):
	def func():
		return True if condition_func() else None
	return poll_data(func, intervals, sleep) is not None


def poll_data(func, intervals, sleep=time.sleep):
	value = func()
	if value is not None:
		return value

	for interval in intervals:
		sleep(interval)
		value = func()
		if value is not None:
			return value

	return None


def polling_intervals(starting, max_attempt_interval, max_total_time=None):
	"""Generate sequence of times for polling some potentially long-lasting process.
	"""
	interval = starting
	total = 0

	while interval < max_attempt_interval and (max_total_time is None or total < max_total_time):
		yield interval
		total += interval
		interval *= 2

	while max_total_time is None or total < max_total_time:
		yield max_attempt_interval
		total += max_attempt_interval


def unused(*args):
	"""This function does nothing and is used to prevent code analyzing tools from reporting
	variable or imported module as unused.

	Please describe why this unused variable is needed next to function call.
	"""
	args = args


def all_equal(lst):
	return len(lst) == 0 or all(item == lst[0] for item in lst[1:])


def cached(func):
	"""Cache function results.
	(cached) call the function func as usual: func(args)
	clear cache for function func as: func.clear()
	"""
	cache = {}

	@wraps(func)
	def wrapper(*args, **kw):
		key = (args, frozenset(kw.items()))
		if key in cache:
			return cache[key]
		else:
			value = func(*args, **kw)
			cache[key] = value
			return value

	wrapper.clear = lambda: cache.clear()
	return wrapper


def format_list(lst):
	return ", ".join(["'%s'" % (elem,) for elem in lst])


def format_multiline_list(lst):
	return "\n".join(["- %s" % (elem,) for elem in lst])


def group_by(seq, key_func):
	"""
	Example:

	group_by(
		[obj(x=1, y=2), obj(x=3, y=4), obj(x=1, y=5)],
		lambda o: o.x
	)
	{
		1: [obj(x=1, y=2), obj(x=1, y=5)],
		3: [obj(x=3, y=4)]
	}
	"""
	d = defaultdict(list)
	for item in seq:
		d[key_func(item)].append(item)
	return dict(d)


def group_by_id(seq, key_func):
	"""
	Example:

	group_by(
		[obj(x=1, y=2), obj(x=3, y=4), obj(x=5, y=6)],
		lambda o: o.x
	)
	{
		1: obj(x=1, y=2),
		3: obj(x=3, y=4),
		5: obj(x=5, y=6)
	}
	"""
	d = {}
	for item in seq:
		key = key_func(item)
		assert key not in d  # assert that key is really "id" - so it is unique
		d[key] = item
	return d


def group_value_by_id(seq, key_func, value_func):
	"""
	Example:

	group_by(
		[obj(x=1, y=2), obj(x=3, y=4), obj(x=5, y=6)],
		lambda o: o.x,
		lambda o: o.y
	)
	{
		1: 2,
		3: 4,
		5: 6
	}
	"""
	d = {}
	for item in seq:
		key = key_func(item)
		assert key not in d  # assert that key is really "id" - so it is unique
		d[key] = value_func(item)
	return d


def mkdir_p(path):
	try:
		os.makedirs(path)
	except OSError as exc:
		if exc.errno == errno.EEXIST:
			pass
		else: 
			raise


def between_inclusive(value, bottom, top):
	return value >= bottom and value <= top


def between_exclusive(value, bottom, top):
	return value > bottom and value < top


def find_only(lst, match_func, error_message):
	matching_items = [item for item in lst if match_func(item)]
	if len(matching_items) != 1:
		raise Exception(error_message)
	return matching_items[0]


def find_first(lst, match_func, default_value=None):
	for item in lst:
		if match_func(item):
			return item
	return default_value


def exists(lst, match_func):
	for item in lst:
		if match_func(item):
			return True

	return False


def replace_str_prefix(string, old_prefix, new_prefix):
	if string.startswith(old_prefix):
		return new_prefix + string[len(old_prefix):]
	else:
		raise Exception("String '%s': expected that is starts with '%s'" % (string, old_prefix))


def split_list_by_count(lst, count):
	sublst = []
	for item in lst:
		sublst.append(item)
		if len(sublst) >= count:
			yield sublst
			sublst = []

	if len(sublst) > 0:  # the last part
		yield sublst


def safe_string_repr(string):
	"""If string can be interpreted as UTF-8, then return it as unicode string. Otherwise return its __repr__.
	Could be useful for safe logging, when usually you want to get just plain string, but if there are some issues
	with encoding - at least get its Python representation.
	"""
	try:
		return u"%s" % (string,)
	except UnicodeError:
		return u"%r" % (string,)


def strip_multiline_spaces(string):
	"""Utility function that strips leading tabs and spaces from multi-line strings.
	Useful when you want to indent multi-line string in a code, but don't want
	to split it over multiple strings for correct indentation
	"""
	return u"\n".join([line.strip() for line in string.strip(" \n\t").split("\n")])


def partition_list(the_list, chunk_size=20):
	# borrowed from
	# http://stackoverflow.com/questions/312443/how-do-you-split-a-list-into-evenly-sized-chunks-in-python
	for idx in xrange(0, len(the_list), chunk_size):
		yield the_list[idx : idx + chunk_size]


def read_file(filename, reader_func, file_may_be_absent=False, default=None):
	try:
		with file(filename, 'r') as dump_file:
			return reader_func(dump_file)
	except IOError as e:
		if e.errno == errno.ENOENT and file_may_be_absent:
			return default
		else:
			raise


def list_get(lst, index, default=None):
	try:
		return lst[index]
	except IndexError:
		return default


def fill_none_namedtuple(tuple_class, **kwargs):
	for field in tuple_class._fields:
		if field not in kwargs:
			kwargs[field] = None
	return tuple_class(**kwargs)


def unique_list(lst):
	result = []
	for item in lst:
		if item not in result:
			result.append(item)
	return result


def sorted_dict(dictionary):
	return OrderedDict(sorted(dictionary.items(), key=lambda i: i[0]))


def mutable_namedtuple(name, fields):
	def __init__(self, *args, **kwargs):
		if len(args) > 0:
			for field, arg in zip(fields, args):
				self.__dict__.update({field: arg})
		assert set(list(fields[0: len(args)]) + kwargs.keys()) == set(fields), "invalid arguments passed to constructor"
		self.__dict__.update(kwargs)
		
	def __repr__(self):
		return name + '(' + ', '.join("%s=%r" % (n, getattr(self, n)) for n in fields) + ')'

	def __eq__(self, other):
		for field in fields:
			if not hasattr(other, field):
				return False
			if getattr(self, field) != getattr(other,  field):
				return False
		return True

	def _asdict(self):
		return { field: getattr(self, field) for field in fields }
			
	return type(name, (object,), {
		'__init__': __init__,
		'__repr__': __repr__,
		'__eq__': __eq__,
		'_asdict': _asdict,
		'_fields': fields
	})


def is_ascii_string(string):
	try:
		string.decode('ascii')
	except UnicodeError:
		return False

	return True


def extend_namedtuple(base, typename, field_names=[]):
	return namedtuple(typename, list(base._fields) + list(field_names))


def get_executable_file_name():
	return os.path.basename(sys.argv[0])


def is_empty(value):
	return value is None or value == ''


def is_empty_list(lst):
	return lst is None or len(lst) == 0

@contextmanager
def open_unicode_file(filename):
	"""Open file detecting if it is UTF-8 or UTF-8 with BOM

	See for more details:
	http://stackoverflow.com/questions/13590749/reading-unicode-file-data-with-bom-chars-in-python
	"""
	minbytes = min(32, os.path.getsize(filename))
	with open(filename, 'rb') as fp_raw:
		raw = fp_raw.read(minbytes)
		if raw.startswith(codecs.BOM_UTF8):
			encoding = 'utf-8-sig'
		else:
			encoding = 'utf-8'

	with codecs.open(filename, 'r', encoding) as fp:
		yield fp


def unique_merge(key, lists):
	"""Merge several lists making unique by key

	Arguments:
	- key - function which accepts item and returns hashable key
	- lists - list of lists with items
	"""
	keys = set()
	result = []
	for lst in lists:
		for item in lst:
			if key(item) not in keys:
				keys.add(key(item))
				result.append(item)

	return result


def merge_dicts(*dicts):
	"""Merge several dictionaries, the latest has more priority

	Arguments:
	pass dictionaries as function's arguments, like: merge_dicts(d1, d2, d3)
	"""
	result = {}
	for d in dicts:
		for k, v in d.iteritems():
			result[k] = v

	return result


def is_string(o):
	"""Check if object is string or not

	:rtype: bool
	"""
	return isinstance(o, basestring)


def is_boolean(o):
	"""Check if object is boolean or not

	:rtype: bool
	"""
	return o in (True, False)


def is_run_on_windows():
	return platform.system() == 'Windows'


def join_nonempty_strs(strs, delim=' '):
	nonempty_strs = [str for str in strs if str is not None and str.strip() != '']
	return delim.join(nonempty_strs)


def split_nonempty_strs(s):
	"""Split string by lines, return list of trimmed non-empty lines

	:type s: string
	:rtype: list[string]
	"""
	return [d.strip() for d in s.split() if d.strip != '']
