#!/usr/bin/env python3

"""A pure Python cross-platform package offering full Clipboard access.

Note:
	Windows standard binary formats such as 'CF_BITMAP' are not supported yet.
	These require more understanding of Windows API than I currently posses.

Attributes:
	__all__ (tuple): All objects and module-level functions offered by this
		package, except :func:`.set` because it conflicts with built-in
		:class:`set`.
	SELECTION (.Selection): Selection object used by module-level functions.
		You don't need to explicitely initialize it unless you're working with
		selection other than 'CLIPBOARD'.
"""

import sys
from collections import OrderedDict, Mapping, ByteString, Sequence
if sys.platform.startswith('win32'):
	from .winclipboard import WinClipboard
	WINDOWS = True
	LINUX = False
else:
	from .xclipboard import XSelection
	WINDOWS = False
	LINUX = True


__all__ = ('Selection', 'get', 'set_text', 'get_text', 'set_with_rich_text',
	'get_with_rich_text', 'clear', 'store', 'wrap_html', 'init')


W_UNICODE = 'CF_UNICODETEXT'
W_HTML = 'HTML Format'
L_TEXT = 'STRING'
L_UNICODE = 'UTF8_STRING'
L_HTML = 'text/html'
ASCII = 'ascii'
UTF8 = 'utf8'
UTF16 = 'utf-16le'


SELECTION = None


class Selection(object):
	"""A selection object.

	By default and on Windows this object represents clipboard.
	On Linux other selections are also supported.

	Attributes:
		selection (str): The selection this object represents.
	"""

	def __init__(self, selection='CLIPBOARD'):
		"""Initialize selection (clipboard).

		Args:
			selection (str): Selection to init. Can be 'CLIPBOARD' (default),
				'PRIMARY' or 'SECONDARY'.
				Note:
					On Windows selection defaults to 'CLIPBOARD' and this
					argument is ignored.
		"""

		if WINDOWS:
			self.selection = 'CLIPBOARD'
			self._interface = WinClipboard()
		else:
			self.selection = selection
			self._interface = XSelection(selection=selection)

	def set(self, content):
		"""Set selection contents to content.

		Args:
			content (Mapping): A mapping where key is format/target and value
				is data to set this format/target to. Value can be
				:class:`str`, :class:`ByteString` or :obj:`None`.
		"""

		if isinstance(content, Mapping):
			self._interface.set(content)
		else:
			raise TypeError('content is not a Mapping')

	def get(self, targets):
		"""Get the contents of specified formats/targets.

		To get the list of available formats/targets include a special target
		'TARGETS'.

		Args:
			targets (Sequence): A sequence of strings specifying which
				formats/targets to get.
		Returns:
			Mapping: A mapping where key is specified format/target
				and value is bytes object representing the data.
				If targets included 'TARGETS', it's value will be a tuple of
				strings representing available formats/targets.
				On Linux :class:`dict` is used, on Windows :class:`OrderedDict`
				is used instead.
		"""

		if isinstance(targets, Sequence):
			return self._interface.get(targets)
		else:
			raise TypeError('targets is not a Sequence')

	def set_text(self, text):
		"""Set the plaintext formats/targets to text.

		Args:
			text (str): Text to set selection to.
		"""

		if isinstance(text, (str, type(None))):
			if WINDOWS:
				if text:
					text = text.encode(UTF16)
				else:
					text = ''.encode(UTF16)
				self.set({W_UNICODE: text})
			else:
				if text:
					string = text.encode(ASCII)
					text = text.encode(UTF8)
				else:
					string = ''.encode(ASCII)
					text = ''.encode(UTF8)
				self.set({L_TEXT: string, L_UNICODE: text})
		else:
			raise TypeError('text is not a str')

	def get_text(self):
		"""Get the contents of selection as plaintext.

		Returns:
			str: The text contained in selection or :obj:`None`.
		"""

		if WINDOWS:
			content = self.get((W_UNICODE, ))
			data = content[W_UNICODE]
			if data:
				return data.decode(UTF16)
			else:
				return data
		else:
			content = self.get((L_TEXT, L_UNICODE))
			data = content[L_UNICODE]
			if data:
				try:
					return data.decode(utf8)
				except UnicodeDecodeError:
					return None
			else:
				data = content[L_TEXT]
				if data:
					return data.decode(ASCII, 'ignore')
				else:
					return None

	def set_with_rich_text(self, text, html):
		"""Set the plaintext and html rich text formats/targets to text and
		html respectively.

		Note:
			html should not include <html> and <body> tags. These are ignored
			on Linux and set automatically by :meth:`wrap_html` on Windows.

		Args:
			text (str): Plain text to set selection to.
			html (str): HTML formatted rich text to set selection to.
		"""

		if (isinstance(text, (str, type(None)))
				and isinstance(html, (str, type(None)))):
			content = []
			if WINDOWS:
				if html:
					html = self._interface.wrap_html(html)
					content.append((W_HTML, html))
				if text:
					text = text.encode(UTF16)
					content.append((W_UNICODE, text))
			else:
				if text:
					string = text.encode(ASCII, 'ignore')
					content.append((L_TEXT, string))
					text = text.encode(UTF8)
					content.append((L_UNICODE, text))
				if html:
					html = html.encode(UTF8)
					content.append((L_HTML, html))
			self.set(OrderedDict(content))
		else:
			raise TypeError('text or html is not str')

	def get_with_rich_text(self):
		"""Get the contents of the selection in plaintext and HTML formats.

		Returns:
			tuple: A tuple where first member is the plaintext string (str)
				and the second member HTML fragment (str) representing
				selection contents.
		"""

		if WINDOWS:
			content = self.get((W_HTML, W_UNICODE))
			html = content[W_HTML]
			if html:
				# Strip HTML Format additions and return just the fragment.
				html = html.decode(UTF8)[131:-38]
			text = content[W_UNICODE]
			if text:
				text = text.decode(UTF16)
		else:
			content = self.get((L_TEXT, L_UNICODE, L_HTML))
			text = content[L_UNICODE]
			if text:
				text = text.decode(UTF8)
			else:
				text = content[L_TEXT]
				if text:
					text = text.decode(ASCII)
			html = content[L_HTML]
			if html:
				try:
					html = html.decode(UTF8)
				except UnicodeDecodeError:
					try:
						html = html.decode(UTF16)
					except UnicodeDecodeError:
						html = html.decode(UTF8, 'ignore')
		return (text, html)

	def clear(self):
		"""Empty selection.
		"""

		self._interface.clear()

	def store(self):
		"""Store selection contents so they're available after script exits.

		Note:
			This method is Linux only and only works for 'CLIPBOARD' selection.
			On Windows it raises :exc:`AttributeError`.
		"""

		self._interface.store()

	def wrap_html(self, fragment):
		"""Wrap HTML fragment so it complies with 'HTML Format' spec.

		Note:
			This method is Windows only and raises :exc:`AttributeError`
			on Linux.
		Args:
			fragment (str): HTML fragment w/o <html> and <body> tags.
		Returns:
			bytes: The wrapped fragment, encoded in UTF-8.
		"""

		return self._interface.wrap_html(fragment)


def init(selection='CLIPBOARD'):
	"""Initialize module-level selection object with a given selection.

	Args:
		selection (str): Selection to init. Valid selections are 'CLIPBOARD'
			(default), 'PRIMARY' and 'SECONDARY'. On Windows this argument
			is ignored.
	"""

	global SELECTION
	SELECTION = Selection(selection=selection)


def set(content):
	"""Set selection contents to content.

	Args:
		content (Mapping): A mapping where key is format/target and value
			is data to set this format/target to. Value can be
			:class:`str`, :class:`ByteString` or :obj:`None`.
	"""

	global SELECTION
	if SELECTION is None:
		SELECTION = Selection()
	SELECTION.set(content)


def get(targets):
	"""Get the contents of specified formats/targets.

	To get the list of available formats/targets include a special target
	'TARGETS'.

	Args:
		targets (Sequence): A sequence of strings specifying which
			formats/targets to get.
	Returns:
		Mapping: A mapping where key is specified format/target
			and value is bytes object representing the data.
			If targets included 'TARGETS', it's value will be a tuple of
			strings representing available formats/targets.
			On Linux :class:`dict` is used, on Windows :class:`OrderedDict`
			is used instead.
	"""

	global SELECTION
	if SELECTION is None:
		SELECTION = Selection()
	return SELECTION.get(targets)


def set_text(text):
	"""Set the plaintext formats/targets to text.

	Args:
		text (str): Text to set selection to.
	"""

	global SELECTION
	if SELECTION is None:
		SELECTION = Selection()
	SELECTION.set_text(text)


def get_text():
	"""Get the contents of selection as plaintext.

	Returns:
		str: The text contained in selection or :obj:`None`.
	"""

	global SELECTION
	if SELECTION is None:
		SELECTION = Selection()
	return SELECTION.get_text()


def set_with_rich_text(text, html):
	"""Set the plaintext and html rich text formats/targets to text and
	html respectively.

	Note:
		html should not include <html> and <body> tags. These are ignored
		on Linux and set automatically by :meth:`wrap_html` on Windows.

	Args:
		text (str): Plain text to set selection to.
		html (str): HTML formatted rich text to set selection to.
	"""

	global SELECTION
	if SELECTION is None:
		SELECTION = Selection()
	SELECTION.set_with_rich_text(text, html)


def get_with_rich_text():
	"""Get the contents of the selection in plaintext and HTML formats.

	Returns:
		tuple: A tuple where first member is the plaintext string (str)
			and the second member HTML fragment (str) representing
			selection contents.
	"""

	global SELECTION
	if SELECTION is None:
		SELECTION = Selection()
	return SELECTION.get_with_rich_text()


def clear():
	"""Empty selection.
	"""

	global SELECTION
	if SELECTION is None:
		SELECTION = Selection()
	SELECTION.clear()


def store():
	"""Store selection contents so they're available after script exits.

	Note:
		This method is Linux only and only works for 'CLIPBOARD' selection.
		On Windows it raises :exc:`AttributeError`.
	"""

	global SELECTION
	if SELECTION is None:
		SELECTION = Selection()
	SELECTION.store()


def wrap_html(fragment):
	"""Wrap HTML fragment so it complies with 'HTML Format' spec.

	Note:
		This method is Windows only and raises :exc:`AttributeError`
		on Linux.
	Args:
		fragment (str): HTML fragment w/o <html> and <body> tags.
	Returns:
		bytes: The wrapped fragment, encoded in UTF-8.
	"""

	global SELECTION
	if SELECTION is None:
		SELECTION = Selection()
	return SELECTION.wrap_html(fragment)
