from imp import load_source
from os.path import isfile
from sys import version

NoneType = type(None)

class OptionDuplicateError(IndexError):
	def __init__(self, name):
		super(IndexError, self).__init__("'%s' already exists" % name)	


class OptionNotFoundError(IndexError):
	def __init__(self, name):
		super(IndexError, self).__init__("'%s' does not exist" % name)	


class NameMustBeStringError(Exception):
	def __init__(self):
		super(Exception, self).__init__("option names have to be strings")


class ValidationError(Exception):
	def __init__(self, option):
		super(Exception, self).__init__("invalid value for option '%s'" % option)


class UnsupportedTypeError(TypeError):
	def __init__(self):
		super(TypeError, self).__init__("only list, tuple, dict, bytes, "
			"str, float, complex, int, None, and bool are supported (same " 
			"thing applies to list, dict and tuple contents)")	


class ExceptionInConfigError(Exception):
	"""
	Raised if an exception occurs while importing a config file.

	IN: error (hint: error that occured on import)
	"""
	def __init__(self, error):
		self.error = error
		super(Exception, self).__init__("error occured during config import (%s)" % 
			error.__class__.__name__)

def validate_value(value):
	type_value = type(value)
	if type_value in (list, tuple):
		for element in value:
			if not validate_value(element):
				return False
	elif type_value is dict:
		return validate_value(tuple(value.keys())) and validate_value(tuple(value.values()))
	elif type_value in (bytes, str, float, complex, int, bool, NoneType):
		return True
	else:
		return False
	return True

def make_value(value):
	if validate_value(value):
		if type(value) is str:
			value = '"%s"' % value
		elif type(value) in (list, tuple, dict):
			value = str(value)
		return value
	else:
		raise UnsupportedTypeError()


class _EditableConfig:
	"""
	Automatically created proxy class.

	HINTS: 
	_values: All options with their respective values
	_options: All Option instances that were originally added to 
		the Config instance
	_file: Path to the config file
	_validation_failed: Optional function that's called on a validation error
	_debug: Debug mode on/off (obviously)
	"""
	def __init__(self, values, options, file, validation_failed=None, debug=False):
		self._values = values
		self._options = options
		self._file = file
		self._validation_failed = validation_failed
		self._debug = debug


	def __getattr__(self, name):
		if name in self._values:
			return self._values[name]
		else:
			raise AttributeError("config no attribute '%s'" % name)


	def __setattr__(self, name, value):
		if name == "_values" or name not in self._values:
			self.__dict__[name] = value
		else:
			dump_required = False
			for option in self._options:
				if option.name == name:
					if validate_value(value):
						if option.validator != None:
							if option.validator(value):
								self._values[name] = value
								dump_required = True
							else:
								if self._validation_failed != None:
									self._validation_failed(option.name, value)
								else:
									raise ValidationError(option.name)
						else:
							self._values[name] = value
							dump_required = True
					else:
						raise UnsupportedTypeError()
			if dump_required:
				if self._debug: 
					print("Rewriting config because the value of '%s' changed." % name)
				open(self._file, "w").write(self._dumps())


	def _dumps(self):
		out = ""
		for option in self._options:
			value = make_value(self._values[option.name])
			out += "%s = %s%s\n" % (option.name, value, 
				(" # %s" % option.comment) if option.comment else "")
		return out.rstrip("\n")


	def __repr__(self):
		return self._dumps()


class Option:
	
	def __init__(self, name, default_value, validator=None, comment=""):
		if not type(name) is str:
			raise NameMustBeStringError()
		if name.startswith("__"):
			raise InvalidOptionName()
		self._name = name
		self.default_value = default_value
		self.validator = validator
		self.comment = comment


	@property
	def name(self):
		return self._name

	@name.setter
	def name(self, value):
		if name.startswith("__"):
			raise InvalidOptionName()
		self._name = value		


	def __eq__(self, other):
		if other.__class__ == Option:
			return self.__dict__ == other.__dict__


	def __repr__(self):
		return "%s = %s" % (self.name, str(self.default_value)) 


class Config:
	"""
	The central element of Meh (TM).
	IN: 
		options=[] (type: list, hint: a list of options)
		validation_failed=None (type: function, hint: function accepting two 
			parameters that's called when a validation fails)
	
	Example usage:
	from Meh import Config, Option
	config = Config()
	config.add(Option("number", 42, validator=lambda x: type(x) is int))

	CONFIG_PATH = "awesome_config.cfg"
	try:
		config = config.load(CONFIG_PATH)
	except IOError:
		config.dump(CONFIG_PATH)
		config = config.load(CONFIG_PATH)

	print(config.number)
	"""
	def __init__(self, options=[], validation_failed=None, debug=False):
		if type(options) in (list, tuple):
			for option in options:
				if not option.__class__ == Option:
					raise TypeError("all options must be of type Option")
		else:
			raise TypeError("options must be a list or tuple containing options of type Option")
		self.options = options
		self.validation_failed = validation_failed
		self.debug = debug
		self._iterator_index = 0


	def __iter__(self):
		return self


	def __next__(self):
		if self._iterator_index < len(self.options):
			self._iterator_index += 1
			return self.options[self._iterator_index - 1]
		self._iterator_index = 0
		raise StopIteration


	def load(self, file):
		"""
		Returns the actual read- and editable config
		IN: file (type: str, hint: should be a valid path)
		"""
		if isfile(file):
			try:
				config = load_source("config", file)
			except Exception as e:
				raise ExceptionInConfigError(e)
			option_missing = False
			values = {}
			for option in self.options:
				# Make sure all options are avaliable (validators aren't run in this case
				# because there are no values defined)
				if option.name not in dir(config):
					values[option.name] = option.default_value
					option_missing = True
				else:
					# Retrieve the option value
					value = getattr(config, option.name)
					# Make sure validator passes
					if option.validator != None:	
						# If validation doesn't pass
						if not option.validator(value):
							# Resort to default value
							values[option.name] = option.default_value
							if self.validation_failed != None:
								self.validation_failed(option.name, value)
							else:
								raise ValidationError(option.name)
							option_missing = True
						# If validation passes
						else:
							values[option.name] = value
					else:
						values[option.name] = value
				if option_missing:
					self.dump(file)
			return _EditableConfig(values, self.options, file, 
				validation_failed=self.validation_failed, debug=self.debug)
		else:
			error = "'%s' not found" % file
			raise FileNotFoundError(error) if version.startswith("3") else IOError(error)


	def __add__(self, other):
		try:
			self.add(other)
		except TypeError:
			return NotImplemented
		return self


	def __sub__(self, other):
		try:
			self.remove(other)
		except TypeError:
			return NotImplemented
		return self


	def add(self, option):
		"""
		Adds an option to a Config instance
		IN: option (type: Option)
		"""		
		if option.__class__ == Option:
			for _option in self.options:
				if option.name == _option.name:
					raise OptionDuplicateError(_option.name)
			self.options.append(option)
		else:
			raise TypeError("invalid type supplied")


	def remove(self, option):
		"""
		Removes an option from a Config instance
		IN: option (type: Option)
		"""
		if option.__class__ == Option:
			if option in self.options:
				del self.options[self.options.index(option)]
			else:
				raise OptionNotFoundError(option.name)
		else:
			raise TypeError("invalid type supplied")


	def dump(self, file):
		"""
		Writes output of dumps() to the path provided
		IN: file (type: str, hint: should be a valid path)
		"""
		open(file, "w").write(self.dumps())


	def dumps(self):
		"""
		Returns contents of config file as string
		OUT: out (type: str, hint: config content)
		"""
		out = ""
		for option in self.options:
			value = make_value(option.default_value)
			out += "%s = %s%s\n" % (option.name, value, 
				(" # %s" % option.comment) if option.comment else "")
		return out.rstrip("\n")


	def __repr__(self):
		return self.dumps()
