# Copyright 2005  Movial
#
# 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.

# lib/parse.py -- Parses binary and source package lists (from the
# work/lists directory).  The BinaryList and SourceList singletons
# provide several ways to access the Binary and Source data objects.
# Packages provided by Scratchbox can be accessed via the ScratchboxList
# singleton.  The lists will be parsed on demand when the singletons are
# first accessed using the instance().

import os, sys, glob
import sb.config
import config

# Python 2.3 doesn't have the built-in set keyword.
import sets
set = sets.Set

##
## Data objects

class Package:
	"""Holds the identity information of a package."""

	def __init__(self, package, version):
		self.package = package
		self.version = version

	def __str__(self):
		return str(self.package)

	def __repr__(self):
		return "Package(%s, %s)" % (repr(self.package), repr(self.version))

	def __cmp__(self, p):
		if self.package < p:
			return -1
		elif self.package > p:
			return 1
		else:
			return 0

	def __hash__(self):
		return hash(self.package)

class Source (Package):
	"""Holds the dependency information of a source package."""

	def __init__(self, package, version, binaries, build_depends, build_depends_indep):
		Package.__init__(self, package, version)
		self.binaries = binaries
		self.build_depends = build_depends
		self.build_depends_indep = build_depends_indep

	def __repr__(self):
		return "Source(%s, %s, %s, %s, %s)" % \
		       (repr(self.package), repr(self.version), repr(self.binaries),
		        repr(self.build_depends), repr(self.build_depends_indep))

	def all_build_depends(self):
		return self.build_depends + self.build_depends_indep

class Binary (Package):
	"""Holds the dependency information of a binary package."""

	def __init__(self, package, version, source, depends, pre_depends, priority, essential):
		Package.__init__(self, package, version)
		self.source = source
		self.depends = depends
		self.pre_depends = pre_depends
		self.priority = priority
		self.essential = essential

	def __repr__(self):
		return "Binary(%s, %s, %s, %s, %s, %s, %s)" % \
		       (repr(self.package), repr(self.version), repr(self.source), repr(self.depends),
		        repr(self.pre_depends), repr(self.priority), repr(self.essential))

	def all_depends(self):
		return self.depends + self.pre_depends

##
## Package indices

class Singleton:
	def instance(type):
		if not hasattr(type, "object"):
			type.object = type()
		return type.object

	instance = classmethod(instance)

class SourceList (Singleton):
	by_source = {}
	by_binary = {}

	def __init__(self):
		print >>sys.stderr, "parse: Reading source package lists ..."

		for path in glob.glob("work/lists/*_Sources"):
			for source in gen_sources(path):
				# only store the latest version of source, if many versions are available in sources.list
				if source.package in self.by_source and os.system("dpkg --compare-versions "+ source.version +" le "+  self.by_source[source.package].version) == 0:
					continue

				self.by_source[source.package] = source

				for name in source.binaries:
					self.by_binary[name] = source

class BinaryList (Singleton):
	by_binary = {}
	by_source = {}
	essential = set()

	def __init__(self):
		print >>sys.stderr, "parse: Reading binary package lists ..."

		for path in glob.glob("work/lists/*_Packages"):
			for binary in gen_binaries(path):
				if binary.package in self.by_binary and os.system("dpkg --compare-versions "+ binary.version +" le "+  self.by_binary[binary.package].version) == 0:
					continue
				self.by_binary[binary.package] = binary

				binaries = self.by_source.get(binary.source)
				if binaries:
					binaries.add(binary)
				else:
					self.by_source[binary.source] = set([binary])

				if binary.essential:
					self.essential.add(binary)

##
## Scratchbox packages

class ScratchboxList (Singleton):
	by_binary = {}
	toolchain_names = set()
	host_names = set()
	all_names = set()

	def __init__(self):
		print >>sys.stderr, "parse: Reading toolchain package list ..."

		path = sb.config.get_compiler_info().dir + "/packages/Packages"
		for block in gen_blocks(path):
			name = block["Package"]
			version = block["Version"]

			self.by_binary[name] = Package(name, version)
			self.toolchain_names.add(name)

		print >>sys.stderr, "parse: Reading host tools list ..."

		command = "/scratchbox/devkits/debian/sbox-list-packages.sh"
		file = os.popen("%s -l" % command)

		while True:
			line = file.readline()
			if not line:
				break

			name, version = line.strip().split("=", 1)
			self.host_names.add(name)

		status = file.close()
		if status is not None:
			raise Exception, "%s exited with status: %d" % (command, status)

		self.all_names = self.toolchain_names | self.host_names

def clean_depend(depend):
	"""Cleans a single dependency and applies arch specialisation.
	   Strips version information and applies architecture
           specialisations (using Scratchbox's target architecture).
	   TODO: check/store for checking the version info """
	   
	if not depend:
		return None
	
	depend = depend.strip()
	parts = depend.split(None, 1)

	if len(parts) == 0: #XXX:huh
		print "weird depend: "+depend
		return None;
	
	if len(parts) > 1:
		i = parts[1].find("[")
		if i >= 0:
			architecture = sb.config.get_compiler_info().arch
			archs = parts[1][i+1:parts[1].index("]")]
			for arch in archs.split():
				if arch[0] == "!":
					take = True
					if arch[1:] == architecture:
						take = False
						break
				else:
					take = False
					if arch == architecture:
						take = True
						break
				if not take:
					return None;

	return parts[0]

## Parsing functions

def parse_depends(line):
	"""Splits a dependency string into a list of package names."""

	if not line:
		return []

	# Fix line
	new_line = None
	for i in xrange(1, len(line)):
		if line[i] == '(' and not line[i-1].isspace():
			if not new_line:
				new_line = line[:i] + " ("
			else:
				new_line += " ("
	if new_line:
		line = new_line

	clean = []
	alternatives = {}
	
	for detail in line.split(","):
#		detail = detail.split("|", 1) # XXX: wrong, wrong
		parts =  detail.split("|")
		detail = parts[0]
		detail = detail.strip()
		
		if len(parts) > 1:
			alternatives[detail]=[]
			for alt in parts:
				alt=clean_depend(alt)
				if alt != None:
					alternatives[detail].append(alt)
			
		detail=clean_depend(detail)
		if detail != None:
			clean.append(detail)

	return clean

def gen_lines(path):
	"""Generates all lines in file PATH with newlines stripped."""

	f = open(path)
	while True:
		line = f.readline()
		if len(line) == 0:
			yield line
			break
		yield line[:-1]
	f.close()

def gen_blocks(path):
	"""Generates all package blocks in file PATH as dictionaries."""

	block = {}
	for line in gen_lines(path):
		if len(line) == 0:
			if block:
				yield block
				block = {}
		elif not line[0].isspace():
			parts = line.split(": ", 1)
			if len(parts) > 1:
				block[parts[0]] = parts[1]

def gen_sources(path):
	"""Generates all package information in file PATH as Source objects."""

	for block in gen_blocks(path):
		package = block["Package"]
		version = block["Version"]

		binaries = block["Binary"].split(", ")
		build_depends = parse_depends(block.get("Build-Depends"))
		build_depends_indep = parse_depends(block.get("Build-Depends-Indep"))

		yield Source(package, version, binaries, build_depends, build_depends_indep)

def gen_binaries(path):
	"""Generates all package information in file PATH as Binary objects."""

	for block in gen_blocks(path):
		package = block["Package"]
		version = block["Version"]

		source = block.get("Source", package).split(None, 1)[0]
		depends = parse_depends(block.get("Depends"))
		pre_depends = parse_depends(block.get("Pre-Depends"))
		priority = block["Priority"]
		essential = block.get("Essential", "") == "yes"

		yield Binary(package, version, source, depends, pre_depends, priority, essential)

