# coding=utf-8
from __future__ import absolute_import

__author__ = "Gina Häußge <osd@foosel.net>"
__license__ = 'The MIT License <http://opensource.org/licenses/MIT>'
__copyright__ = "Copyright (C) 2015 Gina Häußge - Released under terms of the MIT License"

from sphinx.directives.code import CodeBlock

import sphinx.highlighting
from sphinx.highlighting import PygmentsBridge
from sphinx.ext import doctest
from sphinx.util.texescape import tex_hl_escape_map_new

from docutils.nodes import General, FixedTextElement, literal_block, container
from docutils.parsers.rst import directives

from six import text_type

from pygments import highlight
from pygments.filters import VisibleWhitespaceFilter, ErrorToken
from pygments.lexers.python import PythonConsoleLexer
from pygments.util import ClassNotFound

def _merge_dict(a, b):
	"""
	Little helper to merge two dicts a and b on the fly.
	"""
	result = dict(a)
	result.update(b)
	return result

class literal_block_ext(General, FixedTextElement):
	"""
	Custom node which is basically the same as a :class:`literal_block`, just with whitespace support and introduced
	in order to be able to have a custom visitor.
	"""

	@classmethod
	def from_literal_block(cls, block):
		"""
		Factory method constructing an instance exactly copying all attributes over from ``block`` and settings a
		custom ``tagname``.
		"""
		new = literal_block_ext()
		for a in ("attributes", "basic_attributes", "child_text_separator", "children", "document", "known_attributes",
		          "line", "list_attributes", "local_attributes", "parent", "rawsource", "source"):
			setattr(new, a, getattr(block, a))
		new.tagname = "literal_block_ext"
		return new

class CodeBlockExt(CodeBlock):
	"""
	This is basically an extension of a regular :class:`CodeBlock` directive which just supports an additional option
	``whitespace`` which if present will enable (together with everything else in here) to render whitespace in
	code blocks.
	"""

	option_spec = _merge_dict(CodeBlock.option_spec, dict(whitespace=directives.flag))

	def run(self):
		# get result from parent implementation
		code_block = CodeBlock.run(self)

		def find_and_wrap_literal_block(node):
			"""
			Recursive method to turn all literal blocks located within a node into :class:`literal_block_ext`.
			"""

			if isinstance(node, container):
				# container node => handle all children
				children = []
				for child in node.children:
					children.append(find_and_wrap_literal_block(child))
				node.children = children
				return node

			elif isinstance(node, literal_block):
				# literal block => replace it
				return self._wrap_literal_block(node)

			else:
				# no idea what that is => leave it alone
				return node

		# replace all created literal_blocks with literal_block_ext instances
		return map(find_and_wrap_literal_block, code_block)

	def _wrap_literal_block(self, node):
		literal = literal_block_ext.from_literal_block(node)
		literal["whitespace"] = "whitespace" in self.options
		return literal

class PygmentsBridgeExt(object):
	"""
	Wrapper for :class:`PygmentsBridge`, delegates everything to the wrapped ``bridge`` but :method:`highlight_block`,
	which calls the parent implementation for lexer selection, then
	"""

	def __init__(self, bridge, whitespace):
		self._bridge = bridge
		self._whitespace = whitespace

	def __getattr__(self, item):
		return getattr(self._bridge, item)

	def highlight_block(self, source, lang, opts=None, warn=None, force=False, **kwargs):
		if not self._whitespace:
			return self._bridge.highlight_block(source, lang, opts=opts, warn=warn, force=force, **kwargs)

		# We are still here => we need to basically do everything the parent implementation does (and does so in a very
		# unextensible way...), but inject the whitespace filter into the used lexer just before the highlighting run
		# and remove it afterwards so the lexer can be safely reused.
		#
		# For this we define a context manager that will allow us to wrap a lexer and modify its filters on the fly to
		# include the whitespace filter.

		class whitespace(object):
			def __init__(self, lexer):
				self._lexer = lexer
				self._orig_filters = lexer.filters
				self._orig_tabsize = lexer.tabsize

			def __enter__(self):
				new_filters = list(self._orig_filters)
				new_filters.append(VisibleWhitespaceFilter(spaces=True, tabs=True, tabsize=self._lexer.tabsize))
				self._lexer.filters = new_filters
				self._lexer.tabsize = 0
				return self._lexer

			def __exit__(self, type, value, traceback):
				self._lexer.filters = self._orig_filters
				self._lexer.tabsize = self._orig_tabsize

		# Then a ton of copy-pasted code follows. Sadly, we need to do this since we have no way to inject ourselves
		# into the highlighting call otherwise - lexer selection and actual call are tightly coupled in the original
		# "highlight_block" method, with no means for external code to inject different functionality.
		#
		# Unless otherwise marked ("MODIFIED"), any code in this method after this line is copied verbatim from the
		# implementation of sphinx.highlighting.PygmentsBridge, released under the Simplified BSD License, the copyright
		# lies with the respective authors.

		if not isinstance(source, text_type):
			source = source.decode()

		# find out which lexer to use
		if lang in ('py', 'python'):
			if source.startswith('>>>'):
				# interactive session
				lexer = sphinx.highlighting.lexers['pycon']
			elif not force:
				# maybe Python -- try parsing it
				if self.try_parse(source):
					lexer = sphinx.highlighting.lexers['python']
				else:
					lexer = sphinx.highlighting.lexers['none']
			else:
				lexer = sphinx.highlighting.lexers['python']
		elif lang in ('python3', 'py3') and source.startswith('>>>'):
			# for py3, recognize interactive sessions, but do not try parsing...
			lexer = sphinx.highlighting.lexers['pycon3']
		elif lang == 'guess':
			try:
				lexer = sphinx.highlighting.guess_lexer(source)
			except Exception:
				lexer = sphinx.highlighting.lexers['none']
		else:
			if lang in sphinx.highlighting.lexers:
				lexer = sphinx.highlighting.lexers[lang]
			else:
				try:
					lexer = sphinx.highlighting.lexers[lang] = sphinx.highlighting.get_lexer_by_name(lang, **opts or {})
				except ClassNotFound:
					if warn:
						warn('Pygments lexer name %r is not known' % lang)
						lexer = sphinx.highlighting.lexers['none']
					else:
						raise
				else:
					lexer.add_filter('raiseonerror')

		if not isinstance(source, text_type):
			source = source.decode()

		# trim doctest options if wanted
		if isinstance(lexer, PythonConsoleLexer) and self._bridge.trim_doctest_flags:
			source = doctest.blankline_re.sub('', source)
			source = doctest.doctestopt_re.sub('', source)

		# highlight via Pygments
		formatter = self._bridge.get_formatter(**kwargs)
		try:
			# MODIFIED: replaced by whitespace wrapped call
			with whitespace(lexer) as l:
				hlsource = highlight(source, l, formatter)
			# /MODIFIED
		except ErrorToken:
			# this is most probably not the selected language,
			# so let it pass unhighlighted

			# MODIFIED: replaced by whitespace wrapped call
			with whitespace(sphinx.highlighting.lexers["none"]) as l:
				hlsource = highlight(source, l, formatter)
			# /MODIFIED
		if self._bridge.dest == 'html':
			return hlsource
		else:
			if not isinstance(hlsource, text_type):  # Py2 / Pygments < 1.6
				hlsource = hlsource.decode()
			return hlsource.translate(tex_hl_escape_map_new)


class whitespace_highlighter(object):
	"""
	Context manager for adapting the used highlighter on a translator for a given node's whitespace properties.
	"""
	def __init__(self, translator, node):
		self.translator = translator
		self.node = node

		self._orig_highlighter = self.translator.highlighter

	def __enter__(self):
		whitespace = self.node["whitespace"] if "whitespace" in self.node else False
		if whitespace:
			self.translator.highlighter = PygmentsBridgeExt(self._orig_highlighter, whitespace)
		return self.translator

	def __exit__(self, exc_type, exc_val, exc_tb):
		self.translator.highlighter = self._orig_highlighter


def visit_literal_block_ext(translator, node):
	"""
	When our custom code block is visited, we temporarily exchange the highlighter used in the translator, call the
	visitor for regular literal blocks, then switch back again.
	"""
	with whitespace_highlighter(translator, node):
		translator.visit_literal_block(node)


def depart_literal_block_ext(translator, node):
	"""
	Just call the depart function for regular literal blocks.
	"""
	with whitespace_highlighter(translator, node):
		translator.depart_literal_block(node)


def setup(app):
	# custom directive
	app.add_directive("code-block-ext", CodeBlockExt)

	# custom node type
	handler = (visit_literal_block_ext, depart_literal_block_ext)
	app.add_node(literal_block_ext, html=handler, latex=handler, text=handler)

	return dict(version="0.1")