# debpartial_mirror - partial debian mirror package tool
# (c) 2004 Otavio Salvador <otavio@debian.org>, Nat Budin <natb@brandeis.edu>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.	See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA	02111-1307	USA

import logging
import os.path
import ConfigParser
import re
import string


class ConfigException(Exception):
	def getErrorMessage(self):
		return "Unknwon configuration error: %s" % self.__class__.__name__


class InternalError(ConfigException):
	def __init__(self, reason):
		self.reason = reason

	def getErrorMessage(self):
		return "You have found a bug, please report it: %s" % self.reason
		
		
class InvalidOption(ConfigException):
	"""
	Exception called when a invalid option is found in configuration
	file.
	"""
	def __init__(self, section, option, reason):
		self.section = section
		self.option = option
		self.reason = reason

	def getErrorMessage(self):
		return "Wrong option [%s] found in [%s] section : %s" % (
			self.option,
			self.section,
			self.reason,
		)
		

class RequiredOptionMissing(ConfigException):
	"""
	Exception called when a required option in the config file is not
	present.
	"""
	def __init__(self, section, option):
		self.section = section
		self.option = option

	def getErrorMessage(self):
		return 	"Required option [%s] not found in [%s] section." % (self.option, self.section)

		
class InvalidSection(ConfigException):
	"""
	Exception called when a invalid section is found in configuration
	file.
	"""
	def __init__(self, section, reason):
		self.section = section
		self.reason = reason

	def getErrorMessage(self):
		return "Invalid section [%s] : %s" % (self.section, self.reason)

		
class ConfigSection:
	_required = []
	_allowed = []
	_options_with_type = {}
	_allowed_in_filter_field = [
		'subsection',
		'priority',
		'name',
		'exclude-priority',
		'exclude-subsection',
		'exclude-name',
		'include-from',
		'exclude-from',
		'include-script',
		'exclude-script',
		'field-.*',
		'exclude-field-.*',
	]
	_name = ""

	def __init__(self, config, section):
		self.config = config
		self.section = section
		self._allowed_in_filter_regexp = re.compile("|".join(self._allowed_in_filter_field))

		# fix up self._allowed to include all required options too
		for item in self._required:
			if item not in self._allowed:
				self._allowed.append(item)

		self.confs = {}

		for item, value in self.config.items(self.section):
			value = self.__cast_option(item, value)
			if item not in self._allowed:
				rv = self.__parse_variable_options(item)
				if rv is None:
					raise InvalidOption(self.section, item, "%s section" % self._name)
					
				allowed_key, variable, match = rv
				if variable == 'BACKEND':
					if match not in self.config.sections():
						raise InvalidOption(
							self.section, item,
							"[%s] is not the name of a backend" % match,
						)					
				else:
					raise InternalError("[%s] matches unknown variable [%s]" % (item, variable))
					
			self.confs[item] = value

		for item in self._required:
			if not self.confs.has_key(item):
				rv = self.__parse_variable_options(item)
				if rv is None:
					raise RequiredOptionMissing(self.section, item)

	def __parse_variable_options(self, item):
		allowedRe = re.compile("(.*)@(.*)@(.*)")

		for allowed_key in self._allowed:
			allowedMatch = allowedRe.match(allowed_key)
			if allowedMatch is None:
				continue
			before, variable, after = allowedMatch.groups()
			itemMatch = re.match("%s(.*)%s" % (before, after), item)
			if itemMatch is not None:
				return allowed_key, variable, itemMatch.group(1)
		return None

	def __cast_option(self, option, value):
		if option not in self._options_with_type:
			try:
				allowed_key, variable, match = self.__parse_variable_options(option)
				option = allowed_key
			except TypeError:
				pass
		if option in self._options_with_type:
			if self._options_with_type[option] == 'list':
				return value.split()
			elif self._options_with_type[option] == 'boolean':
				value = string.lower(value)
				if value in ('0', 'false', 'no', 'off'):
					return False
				elif value in ('1', 'true', 'yes', 'on'):
					return True
				else:
					raise InvalidOption(self.section, option, "'%s' is not a valid boolean value" % value)
			elif self._options_with_type[option] == 'filter':
				opts = value.split()
				ret = {}
				for opt in opts:
					key, val = opt.split(':', 1)
					if not re.match(self._allowed_in_filter_regexp, key):
						raise InvalidOption(self.section, option, "[%s] is not a filter field" % key)
					if key in ('include-from', 'exclude-from'):
						if not os.path.exists(val):
							raise InvalidOption(self.section, option, "file [%s] doesn't exist" % val) 
					if ret.has_key(key):
						raise InvalidOption(self.section, option, "[%s] option has repeated entries." % key) 

					ret[key] = val
				return ret
		else:
			return value


class ConfigGlobal(ConfigSection):
	_required = [
		'mirror_dir',
		'architectures',
		'components',
		'distributions',
		'get_suggests',
		'get_recommends',
		'get_provides',
		'get_sources',
		'get_packages',
	]

	_allowed = [
		'debug',
		'standalone',
		'display', # WARN: this keyword has not been implemented yet
	]

	_options_with_type = {
		'architectures': 'list',
		'distributions': 'list',
		'get_provides': 'boolean',
		'get_recommends': 'boolean',
		'get_suggests': 'boolean',
		'get_sources': 'boolean',
		'get_packages': 'boolean',
		'components': 'list',
		'standalone' : 'boolean',
	}

	_name = "global"


class ConfigBackendMirror(ConfigSection):
	_allowed = [
		'server',
		'architectures',
		'components',
		'distributions',
		'filter',
		'files',
		'get_suggests',
		'get_recommends',
		'get_provides',
		'get_sources',
		'get_packages',
		'resolve_deps_using',
		'lock',
		'standalone',
		'signature_key',
	]

	_options_with_type = {
		'architectures': 'list',
		'distributions': 'list',
		'filter': 'filter',
		'files': 'list',
		'get_provides': 'boolean',
		'get_recommends': 'boolean',
		'get_suggests': 'boolean',
		'get_sources': 'boolean',
		'get_packages': 'boolean',
		'components': 'list',
		'resolve_deps_using': 'list',
		'lock': 'boolean',
		'standalone' : 'boolean',
	}

	_name = "mirror backend"


class ConfigBackendMerge(ConfigSection):
	_allowed = [
		'filter_@BACKEND@',
		'backends',
		'name',
		'sources_only',
		'origin',
		'label',
		'suite',
		'codename',
		'version',
		'description',
		'signature_key',
		'standalone',
	]

	_options_with_type = {
		'backends': 'list',
		'filter_@BACKEND@': 'filter',
		'standalone' : 'boolean',
	}

	_name = "merge backend"


class Config(ConfigParser.ConfigParser):
	"""
	Store the configurations used by our system.
	"""

	def __init__(self, filename):
		ConfigParser.ConfigParser.__init__(self)
		self.read(filename)

		self.confs = {}
		self.section_objs = {}

		self.backends = {}

		for section in self.sections():
			section_type = self.__get_section_type(section)
			sectionObj = section_type(self, section)
			self.section_objs[section] = sectionObj
			self.confs[section] = sectionObj.confs
			self.confs[section]['name'] = section

		for section in self.section_objs.values():
			if not isinstance(section, ConfigGlobal):
				self.backends[section.section] = section

		# Check backend dependencies
		for backend in self.backends.keys():
			self.check_dependencies(backend)

	def __get_section_type(self, section):
		# detect which config type this is
		if section == 'GLOBAL':
			return ConfigGlobal
		elif 'backends' in self.options(section):
			return ConfigBackendMerge
		elif 'server' in self.options(section):
			return ConfigBackendMirror
		else:
			raise InvalidSection(section, "Unknown section type")

	def get_dependencies(self, backend):
		"""
		Get the list of backends that the given backend depends on
		"""
		if isinstance(self.get_backend(backend), ConfigBackendMirror):
			keyword = 'resolve_deps_using'
		elif isinstance(self.get_backend(backend), ConfigBackendMerge):
			keyword = "backends"
		else:
			raise InvalidSection(backend, "Don't know how to resolve dependencies")
		try:
			dependencies = self.get_option(keyword, backend)
			return dependencies
		except InvalidOption:
			dependencies = []
			return dependencies

	def check_dependencies(self, backend):
		"""
		Checks whether the given beckend depends on valid backend names
		"""
		dependencies = self.get_dependencies(backend)
		for dependency in dependencies:
			if dependency not in self.backends.keys():
				raise InvalidSection(dependency, "missing repository")

	def get_backends(self):

		# Sort backends
		unsorted = self.backends.values()
		sorted = []
		names  = []

		while not len(unsorted) == 0:
			backend = unsorted.pop(0)
			name = backend.section

			dependencies = self.get_dependencies(name)

			if len(dependencies) == 0:
				sorted.append(backend)
				names.append(name)
				continue

			deps_ok = True

			for dependency in dependencies:
				if dependency not in names:
					deps_ok = False
					break

			if deps_ok:
				sorted.append(backend)
				names.append(backend.section)
			else:
				unsorted.append(backend)

		mirrors = []
		merges	= []

		for backend in sorted:
			if isinstance(self.get_backend(backend.section), \
						  ConfigBackendMirror):
				mirrors.append(backend)
			elif isinstance(self.get_backend(backend.section), \
							ConfigBackendMerge):
				merges.append(backend)

		return (mirrors, merges)

	def get_backend(self, name):
		if name in self.section_objs:
			return self.section_objs[name]
		raise InvalidSection(name, "no such backend")

	def get_option(self, option, section='GLOBAL'):
		# specified, fall back to GLOBAL
		if self.confs[section].has_key(option):
			return self.confs[section][option]

		if section != 'GLOBAL':
			logging.debug("[%s] is not present in section [%s]." \
						  "Fallback to global section." % (option, section))
			try:
				return self.get_option(option, 'GLOBAL')
			except InvalidOption, msg:
				raise InvalidOption(section, option, "not found even in global")
			except InvalidSection, msg:
				raise InvalidSection(msg.section)
		else:
			raise InvalidOption(section, option, "not found")

		return self.confs[section][option]


	def dump(self):
		for section, options in self.confs.items():
			print '\n' + section
			for item, value in options.items():
				print "	 %s = %s" % (item, self.get_option(item, section))

