
# Copyright 2009-2018 Jaap Karssenberg <jaap.karssenberg@gmail.com>

'''Utilities to work with the clipboard for copy-pasting

Some functions defined here are also re-used for drag-and-drop
functionality, which works similar to the clipboard, but has a less
straight forward API.
'''

from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GdkPixbuf

import logging

from functools import partial

from zim.fs import File, Dir, FS
from zim.newfs import LocalFolder, LocalFile, SEP
from zim.notebook import Path, HRef
from zim.parsing import is_url_re, url_encode, link_type, URL_ENCODE_READABLE
from zim.formats import get_format, ParseTree, ParseTreeBuilder, \
	FORMATTEDTEXT, IMAGE, LINK, TAG, OBJECT, VisitorSkip
from zim.export.linker import StaticExportLinker
from zim.plugins.base.imagegenerator import copy_imagegenerator_src_files


logger = logging.getLogger('zim.gui.clipboard')


# Targets have format (name, flags, id), flags need be 0 to allow
# paste to other widgets and to other application instances
PARSETREE_TARGET_ID = 1
PARSETREE_TARGET_NAME = 'text/x-zim-parsetree'
PARSETREE_TARGET = (PARSETREE_TARGET_NAME, 0, PARSETREE_TARGET_ID)

PAGELIST_TARGET_ID = 3
PAGELIST_TARGET_NAME = 'text/x-zim-page-list'
PAGELIST_TARGET = (PAGELIST_TARGET_NAME, 0, PAGELIST_TARGET_ID)

IMAGE_TARGET_ID = 6
IMAGE_TARGETS = tuple(
	(name, 0, IMAGE_TARGET_ID) for name in [
		'image/png', 'image/x-wmf', 'application/x-navi-animation',
		'image/bmp', 'image/x-bmp', 'image/x-MS-bmp', 'image/gif',
		'image/x-icns', 'image/x-icon', 'image/x-ico', 'image/x-win-bitmap',
		'image/vnd.microsoft.icon', 'application/ico', 'image/ico',
		'image/icon', 'text/ico', 'image/jpeg', 'image/x-portable-anymap',
		'image/x-portable-bitmap', 'image/x-portable-graymap',
		'image/x-portable-pixmap', 'image/x-quicktime', 'image/qtif',
		'image/svg+xml', 'image/svg', 'image/svg-xml',
		'image/vnd.adobe.svg+xml', 'text/xml-svg', 'image/svg+xml-compressed',
		'image/x-tga', 'image/tiff', 'image/x-xbitmap', 'image/x-xpixmap'
	] # TODO - check any win32 specific types to be added
)
#IMAGE_TARGETS = tuple(Gtk.target_list_add_image_targets(info=IMAGE_TARGET_ID))
IMAGE_TARGET_NAMES = tuple([target[0] for target in IMAGE_TARGETS])

# Add image format names as well, seen these being used by MS Office
for format in GdkPixbuf.Pixbuf.get_formats():
	if format.get_mime_types()[0] in IMAGE_TARGET_NAMES:
		name = format.get_name()
		for n in (name, name.upper()):
			IMAGE_TARGET_NAMES += (n,)
			IMAGE_TARGETS += ((n, 0, IMAGE_TARGET_ID),)

#~ print IMAGE_TARGETS
#~ print IMAGE_TARGET_NAMES

URI_TARGET_ID = 7
URI_TARGETS = (('text/uri-list', 0, URI_TARGET_ID),)
#URI_TARGETS = tuple(Gtk.target_list_add_uri_targets(info=URI_TARGET_ID))
	# According to docs we should provide list as arg to this function,
	# but seems docs are not correct
URI_TARGET_NAMES = tuple([target[0] for target in URI_TARGETS])

HTML_TARGET_ID = 8
HTML_TARGET_NAMES = ('text/html', 'HTML Format')
	# "HTML Format" is from MS Word
HTML_TARGETS = tuple([(name, 0, HTML_TARGET_ID) for name in HTML_TARGET_NAMES])

TEXT_TARGET_ID = 9
TEXT_TARGETS = tuple(
	(name, 0, TEXT_TARGET_ID) for name in [
		'UTF8_STRING', 'COMPOUND_TEXT', 'TEXT', 'STRING',
		'text/plain;charset=utf-8', 'text/plain'
	]
)
#TEXT_TARGETS = tuple(Gtk.target_list_add_text_targets(info=TEXT_TARGET_ID))
	# According to docs we should provide list as arg to this function,
	# but seems docs are not correct
TEXT_TARGET_NAMES = tuple([target[0] for target in TEXT_TARGETS])

# All targets that we can convert to a parsetree, in order of choice
PARSETREE_ACCEPT_TARGETS = (PARSETREE_TARGET, PAGELIST_TARGET) \
	+ IMAGE_TARGETS + URI_TARGETS + TEXT_TARGETS
PARSETREE_ACCEPT_TARGET_NAMES = tuple([target[0] for target in PARSETREE_ACCEPT_TARGETS])
#~ print('ACCEPT', PARSETREE_ACCEPT_TARGET_NAMES)


_internal_selection_data = None # HACK used for workaround for issue #390


# Mimetype text/uri-list is used for drag n drop of URLs
# it is plain text encoded list of urls, separated by \r\n
# Since not all apps follow the standard exactly, do allow for
# just \n separated lists.
#
# Zim page links are also encoded in this format, but no url encoding
# is applied and no scheme is used. The target mimetype can be used to
# distinguish between proper uri lists and lists of page names.

def pack_urilist(links):
	text = ''
	for link in links:
		if is_url_re.match(link):
			link = url_encode(link, mode=URL_ENCODE_READABLE) # just to be sure
		text += '%s\r\n' % link
	return text.encode()


def unpack_urilist(text):
	# FIXME be tolerant here for file://path/to/file uris here
	text = text.strip(b'\x00').decode() # Found trailing NULL character on windows
	lines = text.splitlines() # takes care of \r\n
	return [line for line in lines if line and not line.isspace()]
		# Just to be sure we also skip empty or whitespace lines

# TODO: Probably the serialize formats can replace custom copy/paste
# handlers in TextView and TextBuffer as well


def textbuffer_register_serialize_formats(buffer, notebook, page):
	buffer.register_serialize_format('text/x-zim-parsetree', serialize_parse_tree)
	buffer.register_deserialize_format('text/x-zim-parsetree', deserialize_parse_tree, (notebook, page))
	for name in (PAGELIST_TARGET_NAME,) + URI_TARGET_NAMES:
		buffer.register_deserialize_format(name, deserialize_urilist, (notebook, page))
	for name in IMAGE_TARGET_NAMES: # FIXME, should we limit the list ?
		buffer.register_deserialize_format(name, deserialize_image, (name, notebook, page))

def serialize_parse_tree(register_buf, content_buf, start, end, user_data):
	tree = content_buf.get_parsetree((start, end))
	xml = tree.tostring()
	return xml

def deserialize_parse_tree(register_buf, content_buf, iter, data, length, create_tags, user_data):
	notebook, path = user_data
	tree = ParseTree().fromstring(data)
	tree.resolve_images(notebook, path)
	content_buf.insert_parsetree(iter, tree, interactive=True)
	return True

def deserialize_urilist(register_buf, content_buf, iter, data, length, create_tags, user_data):
	notebook, path = user_data
	links = unpack_urilist(data)
	tree = _link_tree(links, notebook, path)
	content_buf.insert_parsetree(iter, tree, interactive=True)
	return True

def deserialize_image(register_buf, content_buf, iter, data, length, create_tags, user_data):
	# Implementation note: we follow gtk_selection_get_pixbuf() in usage of
	# Gtk.PixbufLoader to capture clipboard data in a pixbuf object.
	# We could skip this, but it allows for on-the-fly conversion of the data
	# type.
	mimetype, notebook, path = user_data

	# capture image
	loader = GdkPixbuf.PixbufLoader()
	loader.write(data)
	loader.close()
	pixbuf = loader.get_pixbuf()

	# save it as an attachment
	dir = notebook.get_attachments_dir(path)
	if not dir.exists():
		logger.debug("Creating attachment dir: %s", dir)
		dir.touch()

	format, extension = _get_image_info(mimetype)
	if format is None or format == 'bmp':
		# default to png format
		# special casing bmp since many window apps use it internally
		# but is quite large to store, so compress by using png
		format, extension = 'png', 'png'

	file = dir.new_file('pasted_image.%s' % extension)
	logger.debug("Saving image from clipboard to %s", file)
	pixbuf.savev(file.path, format, [], [])
	FS.emit('path-created', file) # notify version control

	# and insert it in the page
	links = [file.uri]
	tree = _link_tree(links, notebook, path)
	content_buf.insert_parsetree(iter, tree, interactive=True)
	return True


def parsetree_from_selectiondata(selectiondata, notebook, path=None, text_format='plain'):
	'''Function to get a parsetree based on the selectiondata contents
	if at all possible. Used by both copy-paste and drag-and-drop
	methods.

	The 'notebook' and optional 'path' arguments are used to format
	links relative to the page which is the target for the pasting or
	drop operation.

	NOTE: this method can have a side affect where files are placed in the
	attachment folder of the page.

		- For image data, a new image file is created
		- For parsetree data containing inline images, the image files are
	  	  copied to the attachment folder

	@param selectiondata: a C{Gtk.SelectionData} object
	@param notebook: a L{Notebook} object
	@param path: a L{Path} object
	@param text_format: format to parse pasted text, as a special case

		- "verbatim" will wrap content in VERBARIM_BLOCK or VERBATIM element based on the content
		- "verbatim-pre" will wrap the content in a VERBATIM_BLOCK element and
		- "verbatim-code" will wrap the content in a VERBATIM element

	@returns: a L{ParseTree} or C{None}
	'''
	targetname = selectiondata.get_target().name()
	if targetname == PARSETREE_TARGET_NAME:
		parsetree = ParseTree().fromstring(selectiondata.get_data())
		src_notebook = parsetree._pop_root_attrib('notebook')
		src_page = parsetree._pop_root_attrib('page')
		if src_notebook and src_notebook != notebook.interwiki:
			# Update for cross notebook link
			parsetree.replace(
				(LINK, IMAGE, OBJECT),
				partial(_replace_links_to_interwiki_and_copy_images, src_notebook, notebook, path)
			)
		elif src_page and path and src_page != path.name:
			# Update to new page
			parsetree.replace(
				(LINK, IMAGE, OBJECT),
				partial(_replace_links_to_page_and_copy_images, notebook, path)
			)
		else:
			# Leave links alone, remove "_href" attribs
			parsetree.replace(
				(LINK, IMAGE, OBJECT),
				_strip_link_and_image_attribs
			)
		return parsetree
	elif targetname == PAGELIST_TARGET_NAME \
	or targetname in URI_TARGET_NAMES:
		links = selectiondata.get_uris()
		return _link_tree(links, notebook, path)
	elif targetname in TEXT_TARGET_NAMES:
		# plain text parser should highlight urls etc.
		# FIXME some apps drop text/uri-list as a text/plain mimetype
		# try to catch this situation by a check here
		text = selectiondata.get_text()
		if text:
			if text_format in ('verbatim', 'verbatim-pre', 'verbatim-code'):
				if text_format == 'verbatim':
					tag_name = 'pre' if '\n' in text else 'code'
				else:
					tag_name = text_format[9:]
				builder = ParseTreeBuilder(partial=True)
				builder.start('zim-tree', {})
				builder.start(tag_name, {})
				builder.text(text)
				builder.end(tag_name)
				builder.end('zim-tree')
				return builder.get_parsetree()
			else:
				return get_format(text_format).Parser().parse(text, partial=True)
		else:
			return None
	elif targetname in IMAGE_TARGET_NAMES:
		# save image
		pixbuf = selectiondata.get_pixbuf()
		if not pixbuf:
			return None

		dir = notebook.get_attachments_dir(path)
		assert isinstance(dir, LocalFolder) or hasattr(dir, '_folder') and isinstance(dir._folder, LocalFolder)
			# XXX: assert we have local path  - HACK to deal with FilesAttachmentFolder
		if not dir.exists():
			logger.debug("Creating attachment dir: %s", dir)
			dir.touch()

		format, extension = _get_image_info(targetname)
		if format is None or format == 'bmp':
			# default to png format
			# special casing bmp since many window apps use it internally
			# but is quite large to store, so compress by using png
			format, extension = 'png', 'png'

		file = dir.new_file('pasted_image.%s' % extension)
		logger.debug("Saving image from clipboard to %s", file)
		pixbuf.savev(file.path, format, [], [])
		FS.emit('path-created', file) # notify version control

		links = [file.uri]
		return _link_tree(links, notebook, path)
	else:
		return None


def _link_tree(links, notebook, path):
	# Convert a list of links (of any type) into a parsetree
	#~ print('LINKS: ', links)
	#~ print('NOTEBOOK and PATH:', notebook, path)
	builder = ParseTreeBuilder()
	builder.start(FORMATTEDTEXT)
	for i in range(len(links)):
		if i > 0:
			builder.text(' ')

		link = links[i]
		type = link_type(link)
		isimage = False
		if type == 'interwiki':
			prefix = notebook.interwiki + '?'
			if link.startswith(prefix):
				link = link[len(prefix):]
				type = link_type(link)
		elif type == 'file':
			try:
				file = File(link)
				isimage = file.isimage()
			except:
				pass

		logger.debug('Pasting link: %s (type: %s, isimage: %s)', link, type, isimage)

		if isimage:
			src = notebook.relative_filepath(file, path) or file.uri
			builder.append(IMAGE, {'src': src})
		elif link.startswith('@'):
			# FIXME - is this ever used ??
			builder.append(TAG, {'name': links[i][1:]}, links[i])
		else:
			name = None
			if type == 'page':
				anchor = None
				if '#' in link:
					link, anchor = link.split('#', 1)
				target = Path(Path.makeValidPageName(link)) # Assume links are always absolute
				href = notebook.pages.create_link(path, target)
				href.anchor = anchor
				link = href.to_wiki_link()
				if notebook.config['Notebook']['short_links']:
					name = href.parts()[-1]
					if anchor:
						name += '#' + anchor
			elif type == 'file':
				file = File(link) # Assume links are always URIs
				link = notebook.relative_filepath(file, path) or file.uri

			builder.append(LINK, {'href': link}, name or link)

	builder.end(FORMATTEDTEXT)
	tree = builder.get_parsetree()
	tree.resolve_images(notebook, path)
	tree.decode_urls()
	return tree


def _strip_link_and_image_attribs(node):
	if node.tag == LINK:
		node.attrib.pop('_href', None)
		return node
	elif node.tag == IMAGE \
		or (node.tag == OBJECT and node.get('type').startswith('image+')):
			node.attrib.pop('_src', None)
			return node
	else:
		raise VisitorSkip


def _replace_links_to_interwiki_and_copy_images(src_interwiki, notebook, new_path, node):
	if node.tag == LINK:
		abs_href = node.attrib.pop('_href', None)
		if abs_href:
			my_type = link_type(abs_href)
			if my_type == 'page':
				oldhref = HRef.new_from_wiki_link(node.get('href')) # *not* abs_href
				new_href = src_interwiki + '?' + abs_href
				new_href += '#' + oldhref.anchor if oldhref.anchor else ''
			elif my_type == 'file':
				# NOTE: no proper syntax for this type of link - just abs file link
				#       should be improved - e.g. path:./file style links like in docuwiki
				new_href = abs_href
			else:
				logger.warn('Could not update link of type "%s": %s', my_type, abs_href)
				raise VisitorSkip

			if node.gettext() == node.get('href'): # *not* abs_href
				node[:] = [new_href]
			node.set('href', new_href)
			return node
		else:
			raise VisitorSkip
	elif node.tag == IMAGE:
		# Just copy all images - image links to other notebook don't make sense
		abs_src = node.attrib.pop('_src', None)
		if abs_src:
			return _copy_image(notebook, new_path, abs_src, node)
		else:
			raise VisitorSkip
	elif node.tag == OBJECT:
		abs_src = node.attrib.pop('_src', None)
		if abs_src and node.get('type').startswith('image+'):
			return _copy_image_object(notebook, new_path, abs_src, node)
		else:
			raise VisitorSkip
	else:
		raise AssertionError('unknown tag')


def _replace_links_to_page_and_copy_images(notebook, new_path, node):
	if node.tag == LINK:
		abs_href = node.attrib.pop('_href', None)
		if abs_href:
			my_type = link_type(abs_href)
			if my_type == 'page':
				target = Path(abs_href)
				oldhref = HRef.new_from_wiki_link(node.get('href')) # *not* abs_href
				return notebook._update_link_tag(node, new_path, target, oldhref)
			elif my_type == 'file':
				new_href = notebook.relative_filepath(File(abs_href), new_path)
				if node.gettext() == node.get('href'): # *not* abs_href
					node[:] = [new_href]
				node.set('href', new_href)
				return node
			else:
				logger.warn('Could not update link of type "%s": %s', my_type, abs_href)
				raise VisitorSkip
		else:
			raise VisitorSkip
	elif node.tag == IMAGE:
		# Only copy direct attachments - else the image already was a link
		# to a file outside of the attachment folder
		abs_src = node.attrib.pop('_src', None)
		if abs_src:
			if node.get('src').replace('\\', '/').startswith('./'):
				return _copy_image(notebook, new_path, abs_src, node)
			else:
				return _update_image(notebook, new_path, abs_src, node)
		else:
			raise VisitorSkip
	elif node.tag == OBJECT:
		abs_src = node.attrib.pop('_src', None)
		if abs_src and node.get('type').startswith('image+'):
			if node.get('src').replace('\\', '/').startswith('./'):
				return _copy_image_object(notebook, new_path, abs_src, node)
			else:
				return _update_image(notebook, new_path, abs_src, node)
		else:
			raise VisitorSkip
	else:
		raise AssertionError('unknown tag')


def _update_image(notebook, new_path, abs_src, node):
	new_src = notebook.relative_filepath(File(abs_src), new_path)
	node.set('src', new_src)
	return node


def _copy_image(notebook, new_path, abs_src, node):
	src_file = LocalFile(abs_src)
	folder = notebook.get_page(new_path).attachments_folder
	new_file = folder.new_file(src_file.basename)
	src_file.copyto(new_file)
	node.set('src', '.' + SEP + new_file.basename)
	return node


def _copy_image_object(notebook, new_path, abs_src, node):
	src_file = LocalFile(abs_src)
	folder = notebook.get_page(new_path).attachments_folder
	new_file = copy_imagegenerator_src_files(src_file, folder)
	node.set('src', '.' + SEP + new_file.basename)
	return node


def _get_image_info(targetname):
	# Target name for images is supposed to be mimetype, we check
	# in available pixbuf writers for this type and return the
	# format name and file extension
	for format in GdkPixbuf.Pixbuf.get_formats():
		name = format.get_name()
		mimetypes = format.get_mime_types()
		if targetname in (name, name.upper()) or targetname in mimetypes:
			if format.is_writable():
				return name, format.get_extensions()[0]
			else:
				return None, None
	else:
		return None, None


class MockSelectionData(object):
	'''Adapter to allow usage of C{ClipboardData} as input for
	C{parsetree_from_selectiondata()}
	'''

	def __init__(self, target, clipboard_data):
		self.target = target
		self.data = clipboard_data

	def get_target(self):
		target_name = self.target[0]
		return Gdk.Atom.intern(target_name, False)

	def get_data(self):
		target_id = self.target[-1]
		return self.data.get_data_as(target_id)

	def get_text(self):
		return self.data.get_data_as(TEXT_TARGET_ID)

	def get_uris(self):
		if self.target in URI_TARGETS:
			return self.data.get_data_as(URI_TARGET_ID)
		else:
			return unpack_urilist(self.get_data())

	def get_pixbuf(self):
		raise NotImplementedError


class ClipboardData(object):
	'''Wrapper for data that can be set on the clipboard and pasted
	multiple formats
	'''

	targets = ()

	def get_data_as(self, targetid):
		'''Return data in the requested target format
		@param targetid: the target id
		@implementation: must be implemented by sub-classes
		'''
		raise NotImplementedError


class UriData(ClipboardData):

	targets = URI_TARGETS + TEXT_TARGETS

	def __init__(self, *obj):
		uris = []
		text = []
		for o in obj:
			if isinstance(o, (File, Dir)):
				uris.append(o.uri)
				text.append(o.user_path)
			else:
				uri = o if isinstance(o, str) else o.uri
				uris.append(uri)
				text.append(uri)
		self.uris = tuple(uris)
		self.text = ' '.join(text)

	def get_data_as(self, targetid):
		if targetid == URI_TARGET_ID:
			return self.uris
		else:
			return self.text


class InterWikiLinkData(UriData):

	targets = (PARSETREE_TARGET,) + UriData.targets

	def __init__(self, href, url):
		UriData.__init__(self, url)
		self.interwiki_href = href

	def get_data_as(self, targetid):
		if targetid == PARSETREE_TARGET_ID:
			parsetree = _link_tree((self.interwiki_href,), None, None)
			return parsetree.tostring()
		else:
			return UriData.get_data_as(self, targetid)


class ParseTreeData(ClipboardData):

	targets = (PARSETREE_TARGET,) + HTML_TARGETS + TEXT_TARGETS

	def __init__(self, notebook, path, parsetree, format):
		self.notebook = notebook # FIXME - should not need to keep this reference - resolve parsetree immediatly
		self.path = path
		self.parsetree = parsetree
		self.format = format

	def get_data_as(self, targetid):
		if targetid == PARSETREE_TARGET_ID:
			_set_attributes_to_resolve_links(self.parsetree, self.notebook, self.path)
			return self.parsetree.tostring()
		elif targetid == HTML_TARGET_ID:
			dumper = get_format('html').Dumper(
				linker=StaticExportLinker(self.notebook, source=self.path))
			html = ''.join(dumper.dump(self.parsetree))
			return wrap_html(html, self.format)
		elif targetid == TEXT_TARGET_ID:
			if self.format in ('wiki', 'plain'):
				dumper = get_format(self.format).Dumper()
			else:
				dumper = get_format(self.format).Dumper(
					linker=StaticExportLinker(self.notebook, source=self.path))

			return ''.join(dumper.dump(self.parsetree))
		else:
			raise ValueError('Unknown target id %i' % targetid)


def _set_attributes_to_resolve_links(parsetree, notebook, path):
	parsetree._set_root_attrib('notebook', notebook.interwiki)
	parsetree._set_root_attrib('page', path.name)
	parsetree.replace(
		(LINK, IMAGE, OBJECT),
		partial(_resolve_links_and_images, notebook, path)
	)


def _resolve_links_and_images(notebook, src_path, node):
	if node.tag == LINK:
		href = node.get('href')
		my_type = link_type(href)
		if my_type == 'page':
			target = notebook.pages.resolve_link(src_path, HRef.new_from_wiki_link(href))
			node.set('_href', target)
		elif my_type == 'file':
			target = notebook.resolve_file(href, src_path)
			node.set('_href', target.uri)
		return node
	elif node.tag == IMAGE:
		target = notebook.resolve_file(node.get('src'), src_path)
		node.set('_src', target.uri)
		return node
	elif node.tag == OBJECT:
		if node.get('type').startswith('image+'):
			# Objects based on generated images
			target = notebook.resolve_file(node.get('src'), src_path)
			node.set('_src', target.uri)
			return node
		else:
			raise VisitorSkip
	else:
		raise AssertionError('unknown tag')


class PageLinkData(ClipboardData):

	targets = (PARSETREE_TARGET, PAGELIST_TARGET) + TEXT_TARGETS

	def __init__(self, notebook, path, anchor=None, text=None):
		assert isinstance(path, Path)
		self.notebook = notebook
		self.path = path
		self.anchor = anchor
		self.text = text

	def get_data_as(self, targetid):
		if targetid == PAGELIST_TARGET_ID:
			link = "%s?%s" % (self.notebook.interwiki, self.path.name)
			if self.anchor:
				link += "#" + self.anchor
			return pack_urilist((link,))
		elif targetid == TEXT_TARGET_ID:
			link = self.path.name
			if self.anchor:
				link += "#" + self.anchor
			return link
		elif targetid == PARSETREE_TARGET_ID:
			link = self.path.name
			if self.anchor:
				link += "#" + self.anchor
			if self.text:
				text = self.text
			elif self.notebook.config['Notebook']['short_links']:
				href = HRef.new_from_wiki_link(link)
				text = href.parts()[-1]
				if self.anchor:
					text += '#' + self.anchor
			else:
				text = link

			# Same logic as _set_attributes_to_resolve_links() but no need to resolve again
			builder = ParseTreeBuilder()
			builder.start(FORMATTEDTEXT)
			builder.append(LINK, {'href': link, '_href': self.path.name}, text)
			builder.end(FORMATTEDTEXT)
			parsetree = builder.get_parsetree()
			parsetree._set_root_attrib('notebook', self.notebook.interwiki)
			parsetree._set_root_attrib('page', '-') # force resolve on paste also on same page
			return parsetree.tostring()
		else:
			raise ValueError('Unknown target id %i' % targetid)


class ClipboardManager(object):
	'''Wrapper for C{Gtk.Clipboard}, supporting specific data formats'''

	def __init__(self, name):
		'''Constructor
		@param name: clipboard name, can be either "CLIPBOARD" or "PRIMARY",
		see C{Gtk.Clipboard} for details.
		'''
		assert name in ('CLIPBOARD', 'PRIMARY')
		atom = Gdk.SELECTION_CLIPBOARD if name == 'CLIPBOARD' else Gdk.SELECTION_PRIMARY
		self.clipboard = Gtk.Clipboard.get(atom)
		self.data = None

	def clear(self):
		if self.data is not None:
			self.clipboard.clear()
		self.data = None

	def set_clipboard_data(self, clipboard_data):
		'''Set an item on the clipboard
		@param clipboard_data: a L{ClipboardData} object
		'''
		self.data = clipboard_data
		#self.clipboard.set_with_data(self.data.targets, self._get, self._clear) \
		#	or logger.warn('Failed to set data on clipboard')

		### set_with_data() workaround
		assert TEXT_TARGETS[0] in self.data.targets
		text = self.data.get_data_as(TEXT_TARGET_ID)
		self.clipboard.set_text(text, -1)
		self.data._workaround_text = text
		###

	def _get(self, clipboard, selectiondata, targetid):
		logger.debug(
			"Clipboard requests data as '%s', we have %r",
			selectiondata.get_target().name(),
			self.data
		)
		data = self.data.get_data_as(targetid)
		if targetid == TEXT_TARGET_ID:
			selectiondata.set_text(data, -1)
		elif targetid == URI_TARGET_ID:
			selectiondata.set_uris(data)
		else:
			selectiondata.set(PARSETREE_TARGET_NAME, 8, data)

	def _clear(self):
		self.data = None

	def set_text(self, text):
		'''Set text to the clipboard
		@param text: text to set on the clipboard
		@note: DO NOT USE THIS METHOD if you can use L{set_parsetree()}
		instead
		'''
		self.clipboard.set_text(text, -1)

	def get_text(self):
		'''Get text from the clipboard.
		@returns: (unicode) text or C{None}
		@note: DO NOT USE THIS METHOD if you can use L{get_parsetree()}
		instead
		'''
		return self.clipboard.wait_for_text()

	def set_parsetree(self, notebook, path, parsetree, format='plain'):
		'''Copy a parsetree to the clipboard. The parsetree can be pasted by
		the user either as formatted text within zim or as plain text outside
		zim. The tree can be the full tree for 'page', but also a selection.

		@param notebook: the L{Notebook} object
		@param path: the L{Path} object - used to resolve links etc.
		@param parsetree: the actual L{ParseTree} to be set on the clipboard
		@keyword format: the format to use for pasting text, e.g. 'wiki' or 'plain'
		'''
		self.set_clipboard_data(
			ParseTreeData(notebook, path, parsetree, format) )

	def get_parsetree(self, notebook=None, path=None, text_format='plain'):
		'''Get a parsetree from the clipboard.

		Can handle various data types and convert them to L{ParseTree}
		objects. So they can be pasted directly in a text buffer.

		The 'notebook' and optional 'path' arguments are used to format
		links relative to the page which is the target for the pasting or
		drop operation. Otherwise absolute links will be used.

		@param notebook: a L{Notebook} object
		@param path: a L{Path} object
		@param text_format: format to parse pasted text

		@returns: a L{ParseTree} or C{None}
		'''
		(has_data, atoms) = self.clipboard.wait_for_targets()
		logger.debug('Targets available for paste: %s, we want parsetree', atoms)

		if not has_data:
			return None

		### set_with_data() workaround
		if atoms and self.data and hasattr(self.data, '_workaround_text') \
			and any(a.name() in TEXT_TARGET_NAMES for a in atoms):
				text = self.clipboard.wait_for_text()
				if text == self.data._workaround_text:
					targets = sorted(
						filter(lambda t: t in PARSETREE_ACCEPT_TARGETS, self.data.targets),
						key=lambda t: PARSETREE_ACCEPT_TARGETS.index(t)
					)
					assert len(targets) > 0
					logger.debug('Requesting data for %s -- using set_with_data() workaround', targets[0])
					selectiondata = MockSelectionData(targets[0], self.data)
					return parsetree_from_selectiondata(selectiondata, notebook, path, text_format)
		###

		atoms = sorted(
					filter(lambda a: a and a.name() in PARSETREE_ACCEPT_TARGET_NAMES, atoms),
					key=lambda a: PARSETREE_ACCEPT_TARGET_NAMES.index(a.name())
				)
				# NOTE: "atoms" can contain None values, see issue #774
		if atoms:
			atom = atoms[0]  # TODO why choose 1st index?
			logger.debug('Requesting data for %s', atom)
			selectiondata = self.clipboard.wait_for_contents(atom)
			if selectiondata:
				return parsetree_from_selectiondata(selectiondata, notebook, path, text_format)
			else:
				logger.warn('Did not get requested data from clipboard')
				return None
		else:
			logger.warn('Could not paste - no compatible data types on clipboard')
			return None

	def set_pagelink(self, notebook, path, anchor=None, text=None):
		'''Copy a page name to the clipboard. The page name can be pasted by the
		user either as a link within zim or as text outside zim.
		@param notebook: a L{Notebook} object
		@param path: a L{Path} object
		'''
		logger.debug("set_pagelink %r %r anchor=%s text=%s", notebook, path, anchor, text)
		self.set_clipboard_data(PageLinkData(notebook, path, anchor, text))

	def set_interwikilink(self, href, url):
		'''Copy an interwiki link to the clipboard
		@param href: the link as shown in zim, e.g. "wp?foobar"
		@param url: the expanded url for this interwiki link, e.g.
		"http://en.wikipedia.org/wiki/foobar"
		'''
		self.set_clipboard_data(InterWikiLinkData(href, url))

	def set_uri(self, *uris):
		'''Copy an uri to the clipboard
		@param uri: an uri as string, or an object with an attribute C{uri}
		'''
		self.set_clipboard_data(UriData(*uris))


Clipboard = ClipboardManager("CLIPBOARD") #: Singleton object for the default clipboard


SelectionClipboard = ClipboardManager("PRIMARY") #: Singleton object for the selection clipboard (unix)




########### Code to deal with HTML formatting on windows ############

HTML_HEAD = '''\
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
<meta name="Description" content="Copy-Paste Buffer">
<meta name="Generator" content="Zim">'''

def wrap_html(html, target):
	'''Function to wrap html with appropriate headers based on target type'''
	if target == 'HTML Format':
		return Win32HtmlFormat.encode(html, head=HTML_HEAD)
	else:
		return '''\
<html>
<head>
%s
</head>
<body>
%s
</body>
</html>
''' % (HTML_HEAD, html)


class Win32HtmlFormat:
	'''This class adds support for Windows "HTML Format" clipboard content type

	Code is based on example code from
	U{http://code.activestate.com/recipes/474121/}

	written by Phillip Piper (jppx1[at]bigfoot.com)

	Also see specification at:
	U{http://msdn.microsoft.com/library/default.asp?url=/library/en-us/winui/winui/windowsuserinterface/dataexchange/clipboard/htmlclipboardformat.asp}
	'''

	MARKER_BLOCK_OUTPUT = \
		"Version:1.0\r\n" \
		"StartHTML:%09d\r\n" \
		"EndHTML:%09d\r\n" \
		"StartFragment:%09d\r\n" \
		"EndFragment:%09d\r\n" \
		"StartSelection:%09d\r\n" \
		"EndSelection:%09d\r\n" \
		"SourceURL:%s\r\n"

	#~ MARKER_BLOCK_EX = \
		#~ "Version:(\S+)\s+" \
		#~ "StartHTML:(\d+)\s+" \
		#~ "EndHTML:(\d+)\s+" \
		#~ "StartFragment:(\d+)\s+" \
		#~ "EndFragment:(\d+)\s+" \
		#~ "StartSelection:(\d+)\s+" \
		#~ "EndSelection:(\d+)\s+" \
		#~ "SourceURL:(\S+)"
	#~ MARKER_BLOCK_EX_RE = re.compile(MARKER_BLOCK_EX)

	#~ MARKER_BLOCK = \
		#~ "Version:(\S+)\s+" \
		#~ "StartHTML:(\d+)\s+" \
		#~ "EndHTML:(\d+)\s+" \
		#~ "StartFragment:(\d+)\s+" \
		#~ "EndFragment:(\d+)\s+" \
		#~ "SourceURL:(\S+)"
	#~ MARKER_BLOCK_RE = re.compile(MARKER_BLOCK)

	DEFAULT_HTML_BODY = \
		"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\">" \
		"<HTML><HEAD>%s</HEAD><BODY><!--StartFragment-->%s<!--EndFragment--></BODY></HTML>"

	@classmethod
	def encode(klass, fragment, selection=None, head=None, source=None):
		if selection is None:
			selection = fragment
		if source is None:
			source = "zim://copy-pase-buffer"
		if head is None:
			head = ''

		html = klass.DEFAULT_HTML_BODY % (head, fragment)
		fragmentStart = html.index(fragment)
		fragmentEnd = fragmentStart + len(fragment)
		selectionStart = html.index(selection)
		selectionEnd = selectionStart + len(selection)

		# How long is the prefix going to be?
		dummyPrefix = klass.MARKER_BLOCK_OUTPUT % (0, 0, 0, 0, 0, 0, source)
		lenPrefix = len(dummyPrefix)

		prefix = klass.MARKER_BLOCK_OUTPUT % (
			lenPrefix, len(html) + lenPrefix,
			fragmentStart + lenPrefix, fragmentEnd + lenPrefix,
			selectionStart + lenPrefix, selectionEnd + lenPrefix,
			source
		)
		return prefix + html

	#~ @classmethod
	#~ def decode(self, data):
		#~ """
		#~ Decode the given string to figure out the details of the HTML that's on the string
		#~ """
		#~ # Try the extended format first (which has an explicit selection)
		#~ matches = self.MARKER_BLOCK_EX_RE.match(src)
		#~ if matches:
			#~ self.prefix = matches.group(0)
			#~ self.htmlClipboardVersion = matches.group(1)
			#~ self.html = src[int(matches.group(2)):int(matches.group(3))]
			#~ self.fragment = src[int(matches.group(4)):int(matches.group(5))]
			#~ self.selection = src[int(matches.group(6)):int(matches.group(7))]
			#~ self.source = matches.group(8)
		#~ else:
			#~ # Failing that, try the version without a selection
			#~ matches = self.MARKER_BLOCK_RE.match(src)
			#~ if matches:
				#~ self.prefix = matches.group(0)
				#~ self.htmlClipboardVersion = matches.group(1)
				#~ self.html = src[int(matches.group(2)):int(matches.group(3))]
				#~ self.fragment = src[int(matches.group(4)):int(matches.group(5))]
				#~ self.source = matches.group(6)
				#~ self.selection = self.fragment
