import gzip
import hashlib
import os
import re
import StringIO

import apt_pkg


class AbstractRepository:
	def __init__(self, rawIndex=False, gzIndex=True, multi=False, releaseFields=None, missingReleaseFields=()):
		self._hasRawIndex = rawIndex
		self._hasGzIndex = gzIndex
		self._multi = multi

		self._releaseFields = {
			"Origin" : "Repository Simulator",
			"Label" : "Simulator Label",
			"Suite" : "Simulator Suite",
			"Version" : "Simulator Version",
			"Date" : "Simulator Date",
			"Description" : "Simulator Description",
		}
		for fieldname in missingReleaseFields:
			self._releaseFields.pop(fieldname, None)

		if releaseFields is not None:
			self._releaseFields.update(releaseFields)

		self._packageLists = {}
		self._installerPackageLists = {}
		self._sourcePackageLists = {}

	def getReleaseFields(self):
		return self._releaseFields

	def isMulti(self):
		return self._multi

	def createPackageList(self, distribution, component, architecture):
		packageList = _PackageList()
		self._packageLists[(distribution, component, architecture)] = packageList
		return packageList

	def createInstallerPackageList(self, distribution, component, architecture):
		packageList = _InstallerPackageList()
		self._installerPackageLists[(distribution, component, architecture)] = packageList
		return packageList

	def createSourcePackageList(self, distribution, component):
		packageList = _SourcePackageList()
		self._sourcePackageLists[(distribution, component)] = packageList
		return packageList

	def writeToFilesystem(self, baseDir):
		for filename, contents in self.implCalculateFiles().iteritems():
			pathname = os.path.join(baseDir, filename)
			dirname = os.path.dirname(pathname)
			if not os.path.exists(dirname):
				os.makedirs(dirname)
			f = open(pathname, "w")
			try:
				f.write(contents)
			finally:
				f.close()

	def _addIndexFile(self, files, filename, contents):
		if self._hasRawIndex:
			files[filename] = contents
		if 	self._hasGzIndex:
			buf = StringIO.StringIO()
			compressor = gzip.GzipFile(mode="wb", fileobj=buf)
			compressor.write(contents)
			compressor.close()
			files[filename + ".gz"] = buf.getvalue()
			buf.close()
		return files

	def _addReleaseFile(self, files, path, distribution, component, architecture):
		fields = []
		for key, value in (
			("Archive", distribution),
			("Component", component),
			("Architecture", architecture),
		):
			fields.append("%s: %s" % (key, value))
		files[os.path.join(path, "Release")] = "\n".join(fields)

	def implCalculateFiles(self):
		raise NotImplementedError()



class AutomaticRepository(AbstractRepository):
	def readFromFilesystem(self, baseDir):
		self._baseDir = baseDir
		distDir, dists, _ = os.walk(os.path.join(baseDir, "dists")).next()
		def loadPackages(packageListFactory, key, indexFile):
			packageList = packageListFactory(*key)
			if os.path.exists(indexFile):
				for packageInfo in parseTagFile(indexFile):
					packageList.addPackage(packageInfo["Package"], **packageInfo)


		for dist in dists:
			releaseFile = os.path.join(distDir, dist, "Release")
			if not os.path.exists(releaseFile):
				print "%s not found" % releaseFile
				continue
			release = parseTagFile(releaseFile)[0]
			for field in self._releaseFields.keys():
				self._releaseFields[field] = release.get(field, "")

			for component in release["Components"].split(" "):
				for architecture in release["Architectures"].split(" "):
					loadPackages(
						self.createPackageList,
						(dist, component, architecture),
						os.path.join(distDir, dist, component, "binary-%s" % architecture , "Packages")
					)

					loadPackages(
						self.createInstallerPackageList,
						(dist, component, architecture),
						os.path.join(distDir, dist, component, "debian-installer", "binary-%s" % architecture , "Packages")
					)

				loadPackages(
					self.createSourcePackageList,
					(dist, component),
					os.path.join(distDir, dist, component, "source", "Sources")
				)


	def getPackage(self, name, distribution, component, architecture):
		packageList = self._packageLists.get((distribution, component, architecture))
		return packageList.getPackage(name)

	def getPackageNamesForPresentFiles(self):
		""" Obtain package names corresponding to package files present (for cleanup tests) """
		packageNames = []
		packageRe = re.compile("^([^_]+)_(.*)\.(dsc|(diff|tar)\.gz|deb|udeb)")
		for dirpath, dirname, filenames in os.walk(self._baseDir):
			for filename in filenames:
				match = packageRe.match(filename)
				if match:
					packageNames.append(match.group(1))
		return packageNames

	def getAvailablePackageNames(self, distribution, component, architecture):
		""" Obtain package names that are installable (in index files and package files) """
		return self._getAvailablePackageNames(
			self._packageLists,
			(distribution, component, architecture),
			self._collectSingleFilePackageNames,
		)

	def getAvailableInstallerPackageNames(self, distribution, component, architecture):
		return self._getAvailablePackageNames(
			self._installerPackageLists,
			(distribution, component, architecture),
			self._collectSingleFilePackageNames,
		)

	def getAvailableVersions(self, packageName, distribution, component, architecture):
		def collectSingleFilePackageVersion(availableVersions, package):
			if self._packageFileExists(package):
				availableVersions.append(package["Version"])

		versions = self._getAvailablePackageNames(
			self._packageLists,
			(distribution, component, architecture),
			collectSingleFilePackageVersion,
		)
		versions.sort()
		return tuple(versions)

	def _collectSingleFilePackageNames(self, availableNames, package):
		if self._packageFileExists(package):
			availableNames.append(package["Package"])

	def getAvailableSourcePackageNames(self, distribution, component):
		def collector(availableNames, package):
			for filename in package["filenames"]:
				if os.path.exists(os.path.join(self._baseDir, package["Directory"], filename)):
					availableNames.append(package["Package"])

		return self._getAvailablePackageNames(
			self._sourcePackageLists,
			(distribution, component),
			collector,
		)

	def _packageFileExists(self, package):
		filename = os.path.join(self._baseDir, package["Filename"])
		exists = os.path.exists(filename)
		if not exists:
			print "*** %s not found" % filename
		return exists

	def _getAvailablePackageNames(self, packageLists, key, collector):
		packageList = packageLists.get(key)
		if packageList is None:
			return []

		availableNames = []
		for package in packageList.iterfields():
			collector(availableNames, package)
		return availableNames


	def _collectPackageFiles(self, files, filenameGenerator):
		distContents = {}

		def process(packageLists, extraPathElement, generateReleaseFile):
			for (distribution, component, architecture), packageList in packageLists.iteritems():
				distComponents, distArchitectures = distContents.setdefault(distribution, ([], []))
				distComponents.append(component)
				distArchitectures.append(architecture)

				def generateFilename(binaryPackage, filename):
					return filenameGenerator(distribution, component, architecture, binaryPackage, filename)

				indexPath = os.path.join(
					"dists", distribution, component, extraPathElement,
					"binary-%s" % architecture
				)

				self._addIndexFile(
					files,
					os.path.join(indexPath, "Packages"),
					packageList.calculateIndexFile(architecture, generateFilename),
				)

				if generateReleaseFile:
					self._addReleaseFile(files, indexPath, distribution, component, architecture)


		process(self._packageLists, "", generateReleaseFile=True)
		process(self._installerPackageLists, "debian-installer", generateReleaseFile=False)

		for (distribution, (components, architectures)) in distContents.iteritems():
			parts = []
			for field in ("Origin", "Label", "Suite", "Version", "Date", "Description"):
				if field in self._releaseFields:
					parts.append("%s: %s" % (field, self._releaseFields[field]))
			parts.append("Codename: %s" % distribution)
			parts.append("Architectures: %s" % " ".join(architectures))
			parts.append("Components: %s" % " ".join(components))

			files[os.path.join("dists", distribution, "Release")] = "\n".join(parts)


	def _collectSourcePackageFiles(self, files, directoryGenerator):
		for (distribution, component), packageList in self._sourcePackageLists.iteritems():
			def generateDirectory(sourcePackage):
				return directoryGenerator(distribution, component, sourcePackage)

			indexPath = os.path.join("dists", distribution, component, "source")
			self._addIndexFile(
				files,
				os.path.join(indexPath, "Sources"),
				packageList.calculateIndexFile(files, generateDirectory)
			)
			self._addReleaseFile(files, indexPath, distribution, component, "source")



class PooledRepository(AutomaticRepository):
	def implCalculateFiles(self):
		files = {}
		def generatePoolFilename(distribution, component, architecture, binaryPackage, filename):
			fullname = os.path.join("pool", component, filename[0], filename)
			files[fullname] = "simulated contents of %s" % filename
			return fullname

		def generateSourceDirectory(distribution, component, sourcePackage):
			return os.path.join("dists", distribution, component, "source", sourcePackage["Section"])

		self._collectPackageFiles(files, generatePoolFilename)
		self._collectSourcePackageFiles(files, generateSourceDirectory)
		return files



class NonPooledRepository(AutomaticRepository):
	def implCalculateFiles(self):
		files = {}
		def generateFilename(distribution, component, architecture, binaryPackage, filename):
			fullname = os.path.join(
				"dists", distribution, component, "binary-%s" % architecture, binaryPackage["Section"], filename
			)
			files[fullname] = "simulated contents of %s" % filename
			return fullname

		def generateSourceDirectory(distribution, component, sourcePackage):
			return os.path.join("dists", distribution, component, "source", sourcePackage["Section"])

		self._collectPackageFiles(files, generateFilename)
		self._collectSourcePackageFiles(files, generateSourceDirectory)
		return files


class TrivialRepository(AbstractRepository):
	pass


class _PackageList:
	def __init__(self):
		self._packages = []
		self._packagesByName = {}

	def addPackage(self, name, **kw):
		nameParts = name.split("_")
		if len(nameParts) > 1:
			name = nameParts[0]
			defaultVersion = nameParts[1]
		else:
			defaultVersion ="1.0"

		fields = {
			"Version" : defaultVersion,
			"Priority" : "optional",
			"Architecture" : "any",
			"Section" : "python",
		}
		fields.update(kw)
		self._packages.append((name, fields))
		self._packagesByName.setdefault(name, {})[fields["Version"]] = fields

	def calculateIndexFile(self, defaultArchitecture, filenameGenerator):
		lines = []
		for packageName, fields in self._packages:
			architecture = fields["Architecture"]
			if architecture != "all":
				architecture = defaultArchitecture
			lines.append("Package: %s" % packageName)
			lines.append("Architecture: %s" % architecture)
			lines.append("Filename: %s" % filenameGenerator(fields,
				"%s_%s_%s.%s" % (
					packageName, fields["Version"], architecture, self.getExtension()
			)))
			for fieldName, fieldValue in fields.iteritems():
				lines.append("%s: %s" % (fieldName, fieldValue))
			lines.append("")

		return "\n".join(lines)

	def getExtension(self):
		return "deb"

	def getPackage(self, packageName, version=None):
		packages = self._packagesByName[packageName]
		if len(packages) == 1 and version is None:
			return packages.values()[0]
		if version is None:
			raise RuntimeError("Multiple versions exist for '%s' but no version spectified" % packageName)

		return packages[version]

	def iterfields(self):
		for name, fields in self._packages:
			yield fields

class _InstallerPackageList(_PackageList):
	def getExtension(self):
		return "udeb"


class _SourcePackageList:
	def __init__(self):
		self._packages = {}

	def addPackage(self, name, **kw):
		fields = {
			"Version" : "1.0-1",
			"Section" : "python",
			"Format" : "1.0",
		}
		fields.update(kw)
		fileInfo = kw.pop("Files", None)
		if fileInfo is not None:
			filenames = []
			for line in fileInfo.split("\n"):
				md5sum, size, filename = line.strip().split(" ")
				filenames.append(filename)
			fields["filenames"] = filenames

		self._packages[name] = fields

	def calculateIndexFile(self, files, directoryGenerator):
		lines = []

		def addFile(filename, directory):
			contents = "simulated contents of %s" % filename
			files[os.path.join(directory, filename)] = contents
			return " %s %s %s" % (
				hashlib.md5(contents).hexdigest(),
				len(contents),
				filename,
			)

		for packageName, fields in self._packages.iteritems():
			lines.append("Package: %s" % packageName)
			directory = directoryGenerator(fields)
			fields["Directory"] = directory

			for fieldName, fieldValue in fields.iteritems():
				lines.append("%s: %s" % (fieldName, fieldValue))

			lines.append("Files:")
			fullVersion = fields["Version"]
			upstreamVersion = fullVersion.split("-")[0]
			filenames = []
			for suffix in (
				"%s.dsc" % fullVersion,
				"%s.orig.tar.gz" % upstreamVersion,
				"%s.diff.gz" % fullVersion,
			):
				filename="%s_%s" % (packageName, suffix)
				lines.append(addFile(filename, directory))
				filenames.append(filename)
			lines.append("")
			fields["filenames"] = filenames

		return "\n".join(lines)


def parseTagFile(filename):
	sectionMaps = []
	f = open(filename, "r")
	try:
		for section in apt_pkg.TagFile(f):
			m = {}
			for key in section.keys():
				m[key] = section[key]
			sectionMaps.append(m)
		return sectionMaps
	finally:
		f.close()

