"""
This module is based on a rox module (LGPL):

http://cvs.sourceforge.net/viewcvs.py/rox/ROX-Lib2/python/rox/mime.py?rev=1.21&view=log

This module provides access to the shared MIME database.

types is a dictionary of all known MIME types, indexed by the type name, e.g.
types['application/x-python']

Applications can install information about MIME types by storing an
XML file as <MIME>/packages/<application>.xml and running the
update-mime-database command, which is provided by the freedesktop.org
shared mime database package.

See http://www.freedesktop.org/standards/shared-mime-info-spec/ for
information about the format of these files.

(based on version 0.13)
"""

import os
import stat
import fnmatch

import xdg.BaseDirectory
import xdg.Locale

from xml.dom import Node, minidom, XML_NAMESPACE

FREE_NS = 'http://www.freedesktop.org/standards/shared-mime-info'

types = {}		# Maps MIME names to type objects

exts = None		# Maps extensions to types
globs = None		# List of (glob, type) pairs
literals = None		# Maps liternal names to types
magic = None

def _get_node_data(node):
	"""Get text of XML node"""
	return ''.join([n.nodeValue for n in node.childNodes]).strip()

def lookup(media, subtype = None):
	"Get the MIMEtype object for this type, creating a new one if needed."
	if subtype is None and '/' in media:
		media, subtype = media.split('/', 1)
	if (media, subtype) not in types:
		types[(media, subtype)] = MIMEtype(media, subtype)
	return types[(media, subtype)]

class MIMEtype:
	"""Type holding data about a MIME type"""
	def __init__(self, media, subtype):
		"Don't use this constructor directly; use mime.lookup() instead."
		assert media and '/' not in media
		assert subtype and '/' not in subtype
		assert (media, subtype) not in types

		self.media = media
		self.subtype = subtype
		self._comment = None

	def _load(self):
		"Loads comment for current language. Use get_comment() instead."
		resource = os.path.join('mime', self.media, self.subtype + '.xml')
		for path in xdg.BaseDirectory.load_data_paths(resource):
			doc = minidom.parse(path)
			if doc is None:
				continue
			for comment in doc.documentElement.getElementsByTagNameNS(FREE_NS, 'comment'):
				lang = comment.getAttributeNS(XML_NAMESPACE, 'lang') or 'en'
				goodness = 1 + (lang in xdg.Locale.langs)
				if goodness > self._comment[0]:
					self._comment = (goodness, _get_node_data(comment))
				if goodness == 2: return

	# FIXME: add get_icon method
	def get_comment(self):
		"""Returns comment for current language, loading it if needed."""
		# Should we ever reload?
		if self._comment is None:
			self._comment = (0, str(self))
			self._load()
		return self._comment[1]

	def __str__(self):
		return self.media + '/' + self.subtype

	def __repr__(self):
		return '[%s: %s]' % (self, self._comment or '(comment not loaded)')

class MagicRule:
	def __init__(self, f):
		self.next=None
		self.prev=None

		#print line
		ind=''
		while True:
			c=f.read(1)
			if c=='>':
				break
			ind+=c
		if not ind:
			self.nest=0
		else:
			self.nest=int(ind)

		start=''
		while True:
			c=f.read(1)
			if c=='=':
				break
			start+=c
		self.start=int(start)
		
		hb=f.read(1)
		lb=f.read(1)
		self.lenvalue=ord(lb)+(ord(hb)<<8)

		self.value=f.read(self.lenvalue)

		c=f.read(1)
		if c=='&':
			self.mask=f.read(self.lenvalue)
			c=f.read(1)
		else:
			self.mask=None

		if c=='~':
			w=''
			while c!='+' and c!='\n':
				c=f.read(1)
				if c=='+' or c=='\n':
					break
				w+=c
			
			self.word=int(w)
		else:
			self.word=1

		if c=='+':
			r=''
			while c!='\n':
				c=f.read(1)
				if c=='\n':
					break
				r+=c
			#print r
			self.range=int(r)
		else:
			self.range=1

		if c!='\n':
			raise 'Malformed MIME magic line'

	def getLength(self):
		return self.start+self.lenvalue+self.range

	def appendRule(self, rule):
		if self.nest<rule.nest:
			self.next=rule
			rule.prev=self

		elif self.prev:
			self.prev.appendRule(rule)
		
	def match(self, buffer):
		if self.match0(buffer):
			if self.next:
				return self.next.match(buffer)
			return True

	def match0(self, buffer):
		l=len(buffer)
		for o in range(self.range):
			s=self.start+o
			e=s+self.lenvalue
			if l<e:
				return False
			if self.mask:
				test=''
				for i in range(self.lenvalue):
					c=ord(buffer[s+i]) & ord(self.mask[i])
					test+=chr(c)
			else:
				test=buffer[s:e]

			if test==self.value:
				return True

	def __repr__(self):
		return '<MagicRule %d>%d=[%d]%s&%s~%d+%d>' % (self.nest,
							      self.start,
							      self.lenvalue,
							      `self.value`,
							      `self.mask`,
							      self.word,
							      self.range)

class MagicType:
	def __init__(self, mtype):
		self.mtype=mtype
		self.top_rules=[]
		self.last_rule=None

	def getLine(self, f):
		nrule=MagicRule(f)

		if nrule.nest and self.last_rule:
			self.last_rule.appendRule(nrule)
		else:
			self.top_rules.append(nrule)

		self.last_rule=nrule

		return nrule

	def match(self, buffer):
		for rule in self.top_rules:
			if rule.match(buffer):
				return self.mtype

	def __repr__(self):
		return '<MagicType %s>' % self.mtype
	
class MagicDB:
	def __init__(self):
		self.types={}   # Indexed by priority, each entry is a list of type rules
		self.maxlen=0

	def mergeFile(self, fname):
		f=file(fname, 'r')
		line=f.readline()
		if line!='MIME-Magic\0\n':
			raise 'Not a MIME magic file'

		while True:
			shead=f.readline()
			#print shead
			if not shead:
				break
			if shead[0]!='[' or shead[-2:]!=']\n':
				raise 'Malformed section heading'
			pri, tname=shead[1:-2].split(':')
			#print shead[1:-2]
			pri=int(pri)
			mtype=lookup(tname)

			try:
				ents=self.types[pri]
			except:
				ents=[]
				self.types[pri]=ents

			magictype=MagicType(mtype)
			#print tname

			#rline=f.readline()
			c=f.read(1)
			f.seek(-1, 1)
			while c and c!='[':
				rule=magictype.getLine(f)
				#print rule
				if rule and rule.getLength()>self.maxlen:
					self.maxlen=rule.getLength()

				c=f.read(1)
				f.seek(-1, 1)

			ents.append(magictype)
			#self.types[pri]=ents
			if not c:
				break

	def match(self, path, max_pri=100, min_pri=0):
		try:
			buf=file(path, 'r').read(self.maxlen)
			pris=self.types.keys()
			pris.sort(lambda a, b: -cmp(a, b))
			for pri in pris:
				#print pri, max_pri, min_pri
				if pri>max_pri:
					continue
				if pri<min_pri:
					break
				for type in self.types[pri]:
					m=type.match(buf)
					if m:
						return m
		except:
			pass
		
		return None
	
	def __repr__(self):
		return '<MagicDB %s>' % self.types
			

# Some well-known types
text = lookup('text', 'plain')
inode_block = lookup('inode', 'blockdevice')
inode_char = lookup('inode', 'chardevice')
inode_dir = lookup('inode', 'directory')
inode_fifo = lookup('inode', 'fifo')
inode_socket = lookup('inode', 'socket')
inode_symlink = lookup('inode', 'symlink')
inode_door = lookup('inode', 'door')
app_exe = lookup('application', 'executable')

_cache_uptodate = False

def _cache_database():
	global exts, globs, literals, magic, _cache_uptodate

	_cache_uptodate = True

	exts = {}		# Maps extensions to types
	globs = []		# List of (glob, type) pairs
	literals = {}		# Maps liternal names to types
	magic = MagicDB()

	def _import_glob_file(path):
		"""Loads name matching information from a MIME directory."""
		for line in file(path):
			if line.startswith('#'): continue
			line = line[:-1]

			type_name, pattern = line.split(':', 1)
			mtype = lookup(type_name)

			if pattern.startswith('*.'):
				rest = pattern[2:]
				if not ('*' in rest or '[' in rest or '?' in rest):
					exts[rest] = mtype
					continue
			if '*' in pattern or '[' in pattern or '?' in pattern:
				globs.append((pattern, mtype))
			else:
				literals[pattern] = mtype

	for path in xdg.BaseDirectory.load_data_paths(os.path.join('mime', 'globs')):
		_import_glob_file(path)
	for path in xdg.BaseDirectory.load_data_paths(os.path.join('mime', 'magic')):
		magic.mergeFile(path)

	# Sort globs by length
	globs.sort(lambda a, b: cmp(len(b[0]), len(a[0])))

def get_type_by_name(path):
	"""Returns type of file by its name, or None if not known"""
	if not _cache_uptodate:
		_cache_database()

	leaf = os.path.basename(path)
	if leaf in literals:
		return literals[leaf]

	lleaf = leaf.lower()
	if lleaf in literals:
		return literals[lleaf]

	ext = leaf
	while 1:
		p = ext.find('.')
		if p < 0: break
		ext = ext[p + 1:]
		if ext in exts:
			return exts[ext]
	ext = lleaf
	while 1:
		p = ext.find('.')
		if p < 0: break
		ext = ext[p+1:]
		if ext in exts:
			return exts[ext]
	for (glob, mime_type) in globs:
		if fnmatch.fnmatch(leaf, glob):
			return mime_type
		if fnmatch.fnmatch(lleaf, glob):
			return mime_type
	return None

def get_type_by_contents(path, max_pri=100, min_pri=0):
	"""Returns type of file by its contents, or None if not known"""
	if not _cache_uptodate:
		_cache_database()

	return magic.match(path, max_pri, min_pri)

def get_type(path, follow=1, name_pri=100):
	"""Returns type of file indicated by path.
	path	 - pathname to check (need not exist)
	follow   - when reading file, follow symbolic links
	name_pri - Priority to do name matches.  100=override magic"""
	if not _cache_uptodate:
		_cache_database()
	
	try:
		if follow:
			st = os.stat(path)
		else:
			st = os.lstat(path)
	except:
		t = get_type_by_name(path)
		return t or text

	if stat.S_ISREG(st.st_mode):
		t = get_type_by_contents(path, min_pri=name_pri)
		if not t: t = get_type_by_name(path)
		if not t: t = get_type_by_contents(path, max_pri=name_pri)
		if t is None:
			if stat.S_IMODE(st.st_mode) & 0111:
				return app_exe
			else:
				return text
		return t
	elif stat.S_ISDIR(st.st_mode): return inode_dir
	elif stat.S_ISCHR(st.st_mode): return inode_char
	elif stat.S_ISBLK(st.st_mode): return inode_block
	elif stat.S_ISFIFO(st.st_mode): return inode_fifo
	elif stat.S_ISLNK(st.st_mode): return inode_symlink
	elif stat.S_ISSOCK(st.st_mode): return inode_socket
	return inode_door

def install_mime_info(application, package_file):
	"""Copy 'package_file' as ~/.local/share/mime/packages/<application>.xml.
	If package_file is None, install <app_dir>/<application>.xml.
	If already installed, does nothing. May overwrite an existing
	file with the same name (if the contents are different)"""
	application += '.xml'

	new_data = file(package_file).read()

	# See if the file is already installed
	package_dir = os.path.join('mime', 'packages')
	resource = os.path.join(package_dir, application)
	for x in xdg.BaseDirectory.load_data_paths(resource):
		try:
			old_data = file(x).read()
		except:
			continue
		if old_data == new_data:
			return	# Already installed

	global _cache_uptodate
	_cache_uptodate = False

	# Not already installed; add a new copy
	# Create the directory structure...
	new_file = os.path.join(xdg.BaseDirectory.save_data_path(package_dir), application)

	# Write the file...
	file(new_file, 'w').write(new_data)

	# Update the database...
	command = 'update-mime-database'
	if os.spawnlp(os.P_WAIT, command, command, xdg.BaseDirectory.save_data_path('mime')):
		os.unlink(new_file)
		raise Exception("The '%s' command returned an error code!\n" \
				  "Make sure you have the freedesktop.org shared MIME package:\n" \
				  "http://standards.freedesktop.org/shared-mime-info/") % command
