from collections import Mapping
from itertools import chain
from copy import deepcopy

class NotFound: pass
"""Used to signify no value was found"""

class BadPrefix(Exception): pass
"""Used to say a prefix value is not a dictionary"""

class MergedOptions(Mapping):
	"""
	Wrapper around multiple dictionaries to behave as one.

	Usage::

		options = MergedOptions.using(options1, options2)

	Is equivalent to::

		options = MergedOptions()
		options.add_options(options1)
		options.add_options(options2)

	The later an option is added, the more influence it has.
	i.e. when a key is accessed, later options are looked at first.

	When options are added, copies are made.

	When you set a key on the merged options, it adds it to a special overrides dictionary.
	This overrides is looked at first when accessing a key.

	When you delete a key, it removes it from the first dictionary it can find.
	This means a key can change value when deleted rather than disappearing altogether

	It will also merge deeply.

	So::

		options1 = {'a':{'b':1, 'c':3}, 'b':5}
		options2 = {'a':{'b':4'}, 'd':7}
		merged = MergedOptions.using(options1, options2)
		merged['a'] == MergedOptions(prefix='a', <same_options>, <same_overrides>)
		merged['a']['b'] == 4
		merged['a']['c'] == 3
		merged['d'] == 7

	You may also get all values for a key with merged.values_for(path)
	Where path is a dot seperated path
	so merged.values_for("a.b") == [4, 1]

	You can also get all deeply nested keys with merged.all_keys()
	so merged.all_keys() == ["d", "a.b", "b", "a.c"]
	"""
	def __init__(self, prefix=None, options=None, overrides=None):
		self.prefix = prefix
		self.options = options
		self.overrides = overrides

		if self.options is None:
			self.options = []

		if self.overrides is None:
			self.overrides = {}

	@classmethod
	def using(cls, *options):
		"""Convenience for calling add_options multiple times"""
		merged = cls()
		for opts in options:
			merged.add_options(opts)
		return merged

	def add_options(self, options):
		"""Add new options"""
		self.options.insert(0, deepcopy(options))

	def values_for(self, path):
		"""
		Get all known values for some path

		First consult the overrides

		Then look at each option dictionary that has been registered.
		"""
		results = []

		try:
			results.append(self.at_path(self.prefix_key(path), self.overrides))
		except (BadPrefix, KeyError):
			pass

		for opts in self.options:
			try:
				results.append(self.at_path(self.prefix_key(path), opts))
			except (BadPrefix, KeyError):
				pass

		for found in results:
			if found is not NotFound:
				yield found

	def __getitem__(self, key):
		"""
		Access a key.

		Return the first value it comes across
		Raise KeyError if nothing has the specified key
		"""
		for val in self.values_for(key):
			if isinstance(val, dict) or isinstance(val, MergedOptions):
				return self.prefixed(self.prefix_key(key).split('.'))
			else:
				return val
		raise KeyError(self.prefix_key(key))

	def __setitem__(self, key, value):
		"""Set a key in the overrides"""
		split = self.prefix_key(key).split('.')
		start, last = split[:-1], split[-1]
		if not start:
			pathed = self.overrides
		else:
			pathed = self.after_prefix(self.overrides, prefix=start, create=True, silent=True)

		pathed[last] = value

	def __delitem__(self, key):
		"""
		Delete a key from the first option dictionary that has it.

		Only deletes from the first occurrence, which means deleting a key
		may just change it's value than get rid of it.

		Raise a KeyError if the key doesn't exist
		"""
		split = self.prefix_key(key).split('.')
		start, last = split[:-1], split[-1]

		prefixed_overrides = self.after_prefix(self.overrides, silent=True, prefix=start)
		if last in prefixed_overrides:
			del prefixed_overrides[last]
			self.clean_prefix(self.overrides)
			return

		for opts in self.options:
			prefixed_opts = self.after_prefix(opts, silent=True, prefix=start)
			if last in prefixed_opts:
				del prefixed_opts[last]
				self.clean_prefix(opts, start)
				return

		raise KeyError(self.prefix_key(key))

	def clean_prefix(self, opts, prefix=NotFound):
		"""If our prefix to opts is an empty dictionary, then remove it"""
		if prefix is NotFound:
			prefix = self.prefix

		if prefix:
			start, last = prefix[:-1], prefix[-1]
			prefixed = self.after_prefix(opts, silent=True, prefix=start)
			if last in prefixed and prefixed[last] == {}:
				del prefixed[last]

	def prefix_key(self, key):
		"""Return a key representing prefix on this class and extra key"""
		return '.'.join((self.prefix or []) + key.split('.'))

	def after_prefix(self, opts, prefix=NotFound, silent=False, create=False):
		"""
		Return part in provided opts after our prefix
		Only complain if the prefix doesn't exist if silent is False
		Otherwise just return an empty dictionary
		"""
		if prefix is NotFound:
			prefix = self.prefix

		if prefix is None:
			return opts

		if isinstance(prefix, basestring):
			prefix = prefix.split(".")

		result = opts
		full_key = []
		for key in prefix:
			full_key.append(key)
			if key in result and (isinstance(result[key], dict) or isinstance(result[key], MergedOptions)):
				result = result[key]
			else:
				if silent:
					if create:
						result[key] = {}
						result = result[key]
					else:
						return {}
				else:
					if key in result:
						raise BadPrefix("{0} (value type {1}) is not a dictionary".format('.'.join(full_key), type(result[key])))
					else:
						raise KeyError('.'.join(full_key))

		return result

	def at_path(self, path, opts):
		"""Return value in provided opts after provided dot seperated string path"""
		if not path:
			return opts

		split = path.split('.')
		start, last = split[:-1], split[-1]
		prefixed = self.after_prefix(opts, prefix=start, silent=False)
		if last in prefixed:
			return prefixed[last]
		else:
			return NotFound

	def prefixed(self, prefix):
		"""Return a MergedOptions object that is this object prefixed with our prefix and provided key"""
		return MergedOptions(prefix=prefix, options=self.options, overrides=self.overrides)

	def all_keys_from(self, opts, prefix=None):
		"""Get us all deeply nested keys"""
		if prefix is None:
			prefix = []

		if isinstance(opts, dict) or isinstance(opts, MergedOptions):
			for key in opts:
				for full_key in self.all_keys_from(opts[key], prefix + [key]):
					yield full_key
		else:
			yield '.'.join(prefix)

	def keys(self):
		"""Return a de-duplicated list of the keys we know about"""
		prefixed_overrides = self.after_prefix(self.overrides, silent=True).keys()
		prefixed_opts = [self.after_prefix(opts, silent=True).keys() for opts in self.options]
		return list(set(chain.from_iterable(prefixed_opts + [prefixed_overrides])))

	def all_keys(self):
		"""Return all keys in our options"""
		all_overrides = [list(self.all_keys_from(self.overrides))]
		opts_keys = [list(self.all_keys_from(opts)) for opts in self.options]
		if self.prefix:
			prefix_key = "{0}.".format('.'.join(self.prefix or ""))
		else:
			prefix_key = ""
		lst = list(key[len(prefix_key):] for key in set(chain.from_iterable(opts_keys + all_overrides)) if key.startswith(prefix_key))
		result = []
		for key in lst:
			key_with_dot = "{0}.".format(key)
			result = [thing for thing in result if not thing.startswith(key_with_dot)]
			result.append(key)
		return result

	def __iter__(self):
		"""Iterate over the keys"""
		return self.keys()

	def __len__(self):
		"""Get number of keys we have"""
		return len(list(self.keys()))

	def items(self):
		"""Get everything as a concrete dictionary"""
		top = {}
		keys = self.all_keys()
		for key in keys:
			result = top
			split = key.split('.')
			start, last = split[:-1], split[-1]
			for part in start:
				if part not in result:
					result[part] = {}
				result = result[part]

			result[last] = self[key]
			if isinstance(result[last], MergedOptions):
				result[last] = result[last].items()

		return top

