#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
#  king_phisher/client/widget/extras.py
#
#  Redistribution and use in source and binary forms, with or without
#  modification, are permitted provided that the following conditions are
#  met:
#
#  * Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
#  * Redistributions in binary form must reproduce the above
#    copyright notice, this list of conditions and the following disclaimer
#    in the documentation and/or other materials provided with the
#    distribution.
#  * Neither the name of the project nor the names of its
#    contributors may be used to endorse or promote products derived from
#    this software without specific prior written permission.
#
#  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
#  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
#  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
#  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
#  OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
#  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
#  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
#  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
#  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
#  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
#  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#

import codecs
import datetime
import logging
import os

from king_phisher import its
from king_phisher import templates
from king_phisher import utilities
from king_phisher.client import gui_utilities

import boltons.strutils
import jinja2
import markdown
import mdx_partial_gfm
from gi.repository import Gdk
from gi.repository import GObject
from gi.repository import Gtk

try:
	from gi.repository import WebKit2 as WebKitX
	has_webkit2 = True
except ImportError:
	from gi.repository import WebKit as WebKitX
	has_webkit2 = False

if its.mocked:
	_Gtk_CellRendererText = type('Gtk.CellRendererText', (object,), {'__module__': ''})
	_Gtk_FileChooserDialog = type('Gtk.FileChooserDialog', (object,), {'__module__': ''})
	_Gtk_Frame = type('Gtk.Frame', (object,), {'__module__': ''})
	_WebKitX_WebView = type('WebKitX.WebView', (object,), {'__module__': ''})
else:
	_Gtk_CellRendererText = Gtk.CellRendererText
	_Gtk_FileChooserDialog = Gtk.FileChooserDialog
	_Gtk_Frame = Gtk.Frame
	_WebKitX_WebView = WebKitX.WebView

################################################################################
# Cell renderers
################################################################################
class CellRendererPythonText(_Gtk_CellRendererText):
	"""
	A base :py:class:`Gtk.CellRendererText` class to facilitate rendering native
	Python values into strings of various formats.
	"""
	python_value = GObject.Property(type=object, flags=GObject.ParamFlags.READWRITE)
	__gtype_name__ = 'CellRendererPythonText'
	def __init__(self, *args, **kwargs):
		Gtk.CellRendererText.__init__(self, *args, **kwargs)

	def do_render(self, *args, **kwargs):
		value = self.render_python_value(self.get_property('python-value'))
		value = '' if value is None else str(value)
		self.set_property('text', value)
		Gtk.CellRendererText.do_render(self, *args, **kwargs)

	def render_python_value(self, value):
		"""
		The method to render *value* into a string to be displayed within the
		cell.

		:param value: The Python value to render.
		:rtype: str
		:return: Either the value rendered as a string or ``None``. Returning
			``None`` will cause the cell to be displayed as empty.
		"""
		raise NotImplementedError()

class CellRendererBytes(CellRendererPythonText):
	"""
	A custom :py:class:`.CellRendererPythonText` to render numeric values
	representing bytes.
	"""
	python_value = GObject.Property(type=int, flags=GObject.ParamFlags.READWRITE)
	@staticmethod
	def render_python_value(value):
		if isinstance(value, int):
			return boltons.strutils.bytes2human(value, 1)

class CellRendererDatetime(CellRendererPythonText):
	"""
	A custom :py:class:`.CellRendererPythonText` to render numeric values
	representing bytes.
	"""
	format = GObject.Property(type=str, flags=GObject.ParamFlags.READWRITE, default=utilities.TIMESTAMP_FORMAT)
	def render_python_value(self, value):
		if isinstance(value, datetime.datetime):
			return value.strftime(self.props.format)

class CellRendererInteger(CellRendererPythonText):
	"""
	A custom :py:class:`.CellRendererPythonText` to render numeric values with
	comma separators.
	"""
	python_value = GObject.Property(type=int, flags=GObject.ParamFlags.READWRITE)
	@staticmethod
	def render_python_value(value):
		if isinstance(value, int):
			return "{:,}".format(value)

################################################################################
# Column definitions
################################################################################
class ColumnDefinitionBase(object):
	"""
	A base class for defining attributes of columns to be displayed within
	:py:class:`~Gtk.TreeView` instances.
	"""
	__slots__ = ('title', 'width')
	cell_renderer = None
	"""The :py:class:`~Gtk.CellRenderer` to use for renderering the content."""
	g_type = None
	"""The type to specify in the context of GObjects."""
	python_type = None
	"""The type to specify in the context of native Python code."""
	sort_function = None
	"""
	An optional custom sort function to use for comparing values. This is
	necessary when :py:attr:`.g_type` is not something that can be automatically
	sorted. If specified, this function will be passed to
	:py:meth:`Gtk.TreeSortable.set_sort_func`.
	"""
	def __init__(self, title, width):
		self.title = title
		"""
		The title of the column to be displayed within the
		:py:class:`~Gtk.TreeView` instance.
		"""
		self.width = width
		"""An integer specifying the width of the column."""

	@property
	def name(self):
		"""
		The title converted to lowercase and with spaces replaced with
		underscores.
		"""
		return self.title.lower().replace(' ', '_')

class ColumnDefinitionBytes(ColumnDefinitionBase):
	cell_renderer = CellRendererBytes
	g_type = python_type = int
	def __init__(self, title, width=25):
		super(ColumnDefinitionBytes, self).__init__(title, width)

class ColumnDefinitionDatetime(ColumnDefinitionBase):
	cell_renderer = CellRendererDatetime
	g_type = object
	python_type = datetime.datetime
	sort_function = staticmethod(gui_utilities.gtk_treesortable_sort_func)
	def __init__(self, title, width=25):
		super(ColumnDefinitionDatetime, self).__init__(title, width)

class ColumnDefinitionInteger(ColumnDefinitionBase):
	cell_renderer = CellRendererInteger
	g_type = python_type = int
	def __init__(self, title, width=15):
		super(ColumnDefinitionInteger, self).__init__(title, width)

class ColumnDefinitionString(ColumnDefinitionBase):
	cell_renderer = Gtk.CellRendererText
	g_type = python_type = str
	def __init__(self, title, width=30):
		super(ColumnDefinitionString, self).__init__(title, width)

################################################################################
# Miscellaneous
################################################################################
class FileChooserDialog(_Gtk_FileChooserDialog):
	"""Display a file chooser dialog with additional convenience methods."""
	def __init__(self, title, parent, **kwargs):
		"""
		:param str title: The title for the file chooser dialog.
		:param parent: The parent window for the dialog.
		:type parent: :py:class:`Gtk.Window`
		"""
		utilities.assert_arg_type(parent, Gtk.Window, arg_pos=2)
		super(FileChooserDialog, self).__init__(title, parent, **kwargs)
		self.parent = self.get_parent_window()

	def quick_add_filter(self, name, patterns):
		"""
		Add a filter for displaying files, this is useful in conjunction
		with :py:meth:`.run_quick_open`.

		:param str name: The name of the filter.
		:param patterns: The pattern(s) to match.
		:type patterns: list, str
		"""
		if not isinstance(patterns, (list, tuple)):
			patterns = (patterns,)
		new_filter = Gtk.FileFilter()
		new_filter.set_name(name)
		for pattern in patterns:
			new_filter.add_pattern(pattern)
		self.add_filter(new_filter)

	def run_quick_open(self):
		"""
		Display a dialog asking a user which file should be opened. The
		value of target_path in the returned dictionary is an absolute path.

		:return: A dictionary with target_uri and target_path keys representing the path chosen.
		:rtype: dict
		"""
		self.set_action(Gtk.FileChooserAction.OPEN)
		self.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
		self.add_button(Gtk.STOCK_OPEN, Gtk.ResponseType.ACCEPT)
		self.show_all()
		response = self.run()
		if response == Gtk.ResponseType.CANCEL:
			return None
		target_path = self.get_filename()
		if not os.access(target_path, os.R_OK):
			gui_utilities.show_dialog_error('Permissions Error', self.parent, 'Can not read the selected file.')
			return None
		target_uri = self.get_uri()
		return {'target_uri': target_uri, 'target_path': target_path}

	def run_quick_save(self, current_name=None):
		"""
		Display a dialog which asks the user where a file should be saved. The
		value of target_path in the returned dictionary is an absolute path.

		:param set current_name: The name of the file to save.
		:return: A dictionary with target_uri and target_path keys representing the path choosen.
		:rtype: dict
		"""
		self.set_action(Gtk.FileChooserAction.SAVE)
		self.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
		self.add_button(Gtk.STOCK_SAVE, Gtk.ResponseType.ACCEPT)
		self.set_do_overwrite_confirmation(True)
		if current_name:
			self.set_current_name(current_name)
		self.show_all()
		response = self.run()
		if response == Gtk.ResponseType.CANCEL:
			return None
		target_path = self.get_filename()
		if os.path.isfile(target_path):
			if not os.access(target_path, os.W_OK):
				gui_utilities.show_dialog_error('Permissions Error', self.parent, 'Can not write to the selected file.')
				return None
		elif not os.access(os.path.dirname(target_path), os.W_OK):
			gui_utilities.show_dialog_error('Permissions Error', self.parent, 'Can not write to the selected path.')
			return None
		target_uri = self.get_uri()
		return {'target_uri': target_uri, 'target_path': target_path}

	def run_quick_select_directory(self):
		"""
		Display a dialog which asks the user to select a directory to use. The
		value of target_path in the returned dictionary is an absolute path.

		:return: A dictionary with target_uri and target_path keys representing the path chosen.
		:rtype: dict
		"""
		self.set_action(Gtk.FileChooserAction.SELECT_FOLDER)
		self.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
		self.add_button(Gtk.STOCK_OPEN, Gtk.ResponseType.ACCEPT)
		self.show_all()
		response = self.run()
		if response == Gtk.ResponseType.CANCEL:
			return None
		target_uri = self.get_uri()
		target_path = self.get_filename()
		return {'target_uri': target_uri, 'target_path': target_path}

class MultilineEntry(_Gtk_Frame):
	"""
	A custom entry widget which can be styled to look like
	:py:class:`Gtk.Entry` but accepts multiple lines of input.
	"""
	__gproperties__ = {
		'text': (str, 'text', 'The contents of the entry.', '', GObject.ParamFlags.READWRITE),
		'text-length': (int, 'text-length', 'The length of the text in the GtkEntry.', 0, 0xffff, 0, GObject.ParamFlags.READABLE)
	}
	__gtype_name__ = 'MultilineEntry'
	def __init__(self, *args, **kwargs):
		Gtk.Frame.__init__(self, *args, **kwargs)
		self.get_style_context().add_class('multilineentry')
		textview = Gtk.TextView()
		self.add(textview)

	def do_get_property(self, prop):
		textview = self.get_child()
		if prop.name == 'text':
			buffer = textview.get_buffer()
			return buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter(), False)
		elif prop.name == 'text-length':
			return 0
		raise AttributeError('unknown property: ' + prop.name)

	def do_set_property(self, prop, value):
		textview = self.get_child()
		if prop.name == 'text':
			textview.get_buffer().set_text(value)
		elif prop.name == 'text-length':
			raise ValueError('read-only property: ' + prop.name)
		else:
			raise AttributeError('unknown property: ' + prop.name)

class WebKitHTMLView(_WebKitX_WebView):
	"""
	A WebView widget with additional convenience methods for rendering simple
	HTML content from either files or strings. If a link is opened within the
	document, the webview will emit the 'open-uri' signal instead of navigating
	to it.
	"""
	__gsignals__ = {
		'open-remote-uri': (GObject.SIGNAL_RUN_FIRST, None, (str, (WebKitX.NavigationPolicyDecision if has_webkit2 else WebKitX.WebPolicyDecision)))
	}
	template_env = templates.TemplateEnvironmentBase(loader=templates.FindFileSystemLoader())
	"""
	The :py:class:`~king_phisher.templates.TemplateEnvironmentBase` instance to
	use when rendering template content. The environment uses the
	:py:class:`~king_phisher.templates.FindFileSystemLoader` loader.
	"""
	def __init__(self):
		super(WebKitHTMLView, self).__init__()
		self.logger = logging.getLogger('KingPhisher.Client.' + self.__class__.__name__)

		if has_webkit2:
			self.get_context().set_cache_model(WebKitX.CacheModel.DOCUMENT_VIEWER)
			self.connect('decide-policy', self.signal_decide_policy)
		else:
			self.connect('navigation-policy-decision-requested', self.signal_decide_policy_webkit)

		self.connect('button-press-event', self.signal_button_pressed)

	def do_open_remote_uri(self, uri, decision):
		self.logger.debug('received request to open uri: ' + uri)

	def load_html_data(self, html_data, html_file_uri=None):
		"""
		Load arbitrary HTML data into the WebKit engine to be rendered.

		:param str html_data: The HTML data to load into WebKit.
		:param str html_file_uri: The URI of the file where the HTML data came from.
		"""
		if isinstance(html_file_uri, str) and not html_file_uri.startswith('file://'):
			html_file_uri = 'file://' + html_file_uri

		if has_webkit2:
			self.load_html(html_data, html_file_uri)
		else:
			if html_file_uri is None:
				html_file_uri = 'file://' + os.getcwd()
			self.load_string(html_data, 'text/html', 'UTF-8', html_file_uri)

	def load_html_file(self, html_file):
		"""
		Load arbitrary HTML data from a file into the WebKit engine to be
		rendered.

		:param str html_file: The path to the file to load HTML data from.
		"""
		with codecs.open(html_file, 'r', encoding='utf-8') as file_h:
			html_data = file_h.read()
		self.load_html_data(html_data, html_file)

	def load_markdown_data(self, md_data, html_file_uri=None, gh_flavor=True, template=None, template_vars=None):
		"""
		Load markdown data, render it into HTML and then load it in to the
		WebKit engine. When *gh_flavor* is enabled, the markdown data is
		rendered using partial GitHub flavor support as provided by
		:py:class:`~mdx_partial_gfm.PartialGithubFlavoredMarkdownExtension`. If
		*template* is specified, it is used to load a Jinja2 template using
		:py:attr:`.template_env` into which the markdown data is passed in the
		variable ``markdown`` along with any others specified in the
		*template_vars* dictionary.

		:param str md_data: The markdown data to render into HTML for displaying.
		:param str html_file_uri: The URI of the file where the HTML data came from.
		:param bool gh_flavor: Whether or not to enable partial GitHub markdown syntax support.
		:param str template: The name of a Jinja2 HTML template to load for hosting the rendered markdown.
		:param template_vars: Additional variables to pass to the Jinja2 :py:class:`~jinja2.Template` when rendering it.
		:return:
		"""
		extensions = []
		if gh_flavor:
			extensions = [mdx_partial_gfm.PartialGithubFlavoredMarkdownExtension()]
		md_data = markdown.markdown(md_data, extensions=extensions)
		if template:
			template = self.template_env.get_template(template)
			template_vars = template_vars or {}
			template_vars['markdown'] = jinja2.Markup(md_data)
			html = template.render(template_vars)
		else:
			html = md_data
		return self.load_html_data(html, html_file_uri=html_file_uri)

	def load_markdown_file(self, md_file, **kwargs):
		"""
		Load markdown data from a file and render it using
		:py:meth:`~.load_markdown_data`.

		:param str md_file: The path to the file to load markdown data from.
		:param kwargs: Additional keyword arguments to pass to :py:meth:`~.load_markdown_data`.
		"""
		with codecs.open(md_file, 'r', encoding='utf-8') as file_h:
			md_data = file_h.read()
		return self.load_markdown_data(md_data, md_file, **kwargs)

	def signal_button_pressed(self, _, event):
		if event.button == Gdk.BUTTON_SECONDARY:
			# disable right click altogether
			return True

	# webkit2 signal handler
	def signal_decide_policy(self, _, decision, decision_type):
		if decision_type == WebKitX.PolicyDecisionType.NAVIGATION_ACTION:
			uri_request = decision.get_request()
			uri = uri_request.get_uri()
			if uri.startswith('file:'):
				decision.use()
			else:
				decision.ignore()
				self.emit('open-remote-uri', uri, decision)

	# webkit signal handler
	def signal_decide_policy_webkit(self, view, frame, request, action, policy):
		uri = request.get_uri()
		if uri.startswith('file://'):
			policy.use()
		else:
			policy.ignore()
			self.emit('open-remote-uri', uri, policy)