#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (C) 2018 Inuvika Inc.
# http://www.inuvika.com
# Author David PHAM-VAN <d.phamvan@inuvika.com> 2018
# All rights reserved

from __future__ import print_function

import sys
import os
import io
import base64
import re
import threading
import argparse
import webbrowser
import gettext
import glob
import signal
import tempfile
import traceback

import requests
import wx
try:
	import wx.html2
	WEB_VIEW = True
except:
	import wx.html
	import lxml.etree
	WEB_VIEW = False

if os.name == 'nt':
	from windows_keyboard_hook import install_hook

# Monkey patch to force IPv4 to avoid auth issue with SM
import socket
old_getaddrinfo = socket.getaddrinfo
def new_getaddrinfo(*args, **kwargs):
	responses = old_getaddrinfo(*args, **kwargs)
	return [response
		for response in responses
			if response[0] == socket.AF_INET
	]

socket.getaddrinfo = new_getaddrinfo

# Add support for localization
locale_dir = '/usr/share/locale'
if os.name == 'nt':
		import locale
		if os.getenv('LANG') is None:
			lang, enc = locale.getdefaultlocale()
			os.environ['LANG'] = lang
		app_dir = os.path.dirname(os.path.realpath(sys.argv[0]))
		locale_dir = os.path.join(app_dir, 'locale')

gettext.bindtextdomain('ovd-client', locale_dir)
gettext.textdomain('ovd-client')


class AbstractCommunication(object):

	def __init__(self):
		self.data_img_regex = r'src="data:image\/([a-zA-Z]*);base64,([^"]*)"'
		self.data_color_regex = r"(background-color|color):\s*([^\";]*)"

	def get_message(self, session_manager, token):
		raise NotImplementedError

	def ack_message(self, session_manager, token, response, callback):
		raise NotImplementedError

	def convert_embedded_images(self, html, fs_handler):
		output = ''
		matches = re.finditer(self.data_img_regex, html, flags=re.MULTILINE)
		start = 0

		for match in matches:
			output += html[start:match.start()]
			start = match.end()
			filename = 'img_%s.%s' % (start, match.group(1))
			fs_handler.AddFile(filename, bytearray(base64.b64decode(match.group(2))))
			output += 'src="memory:%s"' % filename

		output += html[start:]
		return output

	def rewrite_styles(self, content):
		parser = lxml.etree.HTMLParser()
		root = lxml.etree.fromstring(content, parser=parser)

		for node in root.xpath('//*[@style]'):
			font = lxml.etree.Element("font")
			matches1 = re.finditer(self.data_color_regex, node.attrib["style"], re.MULTILINE | re.UNICODE | re.VERBOSE)

			for rule in matches1:
				if rule.group(1) == 'background-color':
					font.attrib['bgcolor'] = rule.group(2)
				elif rule.group(1) == 'color':
					font.attrib['color'] = rule.group(2)

			node.getparent().replace(node, font)
			font.append(node)

		result = lxml.etree.tostring(root, pretty_print=True, method="html")
		return result


class SmCommunication(AbstractCommunication):

	def get_message(self, session_manager, token):
		req = requests.get(
			'https://{sm}/ovd/client/message'.format(
				sm=session_manager,
			),
			params={'token': token},
			verify=False,
		)

		if req.status_code != 200:
			print("Error:", req.text)
			return

		return req.json()

	def ack_message(self, session_manager, token, response, callback):
		req = requests.put(
			'https://{sm}/ovd/client/message'.format(
				sm=session_manager,
			),
			params={'token': token, 'response': response},
			verify=False,
		)

		callback(None if req.status_code == 200 else req.content)


class OvdMessage(wx.Frame):
	MODE_NORMAL = 0
	MODE_BLOCKING = 1
	MODE_FULLSCREEN = 2

	COLOR_HTML = '#ffffff'
	COLOR_BG = '#d8d8d8'
	COLOR_BORDER = '#808080'
	COLOR_BTN_TXT = '#000000'

	html_start = '<!doctype html>\n<html><head><meta charset="utf-8"></head><body>'
	html_end = '</body></html>'

	def __init__(self, comm, session_manager, token):
		ok = gettext.gettext('Ok')
		self.comm = comm
		self.session_manager = session_manager
		self.token = token
		self.validated = False
		self.mem_fs = wx.MemoryFSHandler()
		message = comm.get_message(session_manager, token)
		# Intentionally throw an exception if no content
		content = self.html_start + message['content'] + self.html_end
		self.title = message['title'] if 'title' in message else 'OVD Alert'
		responses = message['available_responses'] if 'available_responses' in message else (ok, )
		if len(responses) == 0:
			responses = (ok, )
		self.mode = self.MODE_NORMAL
		if 'mode' in message:
			if message['mode'] == 'partial':
				self.mode = self.MODE_BLOCKING
			elif message['mode'] == 'full':
				self.mode = self.MODE_FULLSCREEN

		color = message['color'] if 'color' in message else self.COLOR_BG

		width, height = wx.GetDisplaySize()

		args = {
			'id': -1, 
			'title': self.title, 
			'style': wx.CAPTION | wx.MINIMIZE_BOX | wx.RESIZE_BORDER
		}

		# Define styles
		if self.mode != self.MODE_FULLSCREEN:
			args['size'] = (width // 2, height * 2 // 3)
		else:
			args['style'] |= wx.DEFAULT_FRAME_STYLE

		if self.mode == self.MODE_BLOCKING:
			args['style'] &= ~wx.MINIMIZE_BOX

		super(OvdMessage, self).__init__(None, **args)

		# Set the icon for this window
		try:
			if os.name == 'nt':
				app_dir = os.path.dirname(os.path.realpath(sys.argv[0]))
				icon_path = os.path.join(app_dir, 'ovd-message.png')
				if os.path.exists(icon_path):
					self.SetIcon(wx.Icon(icon_path))
			else:
				icon_path = '/usr/share/pixmaps/ovd-message.png'
				if os.path.exists(icon_path):
					self.SetIcon(wx.Icon(icon_path))
		except:
			print("Unable to find ovd-message.png", file=sys.stderr)

		self.SetMinSize((width // 4, height * 2 // 3))

		self.Bind(wx.EVT_CLOSE, self.on_close)

		vbox = wx.BoxSizer(orient=wx.VERTICAL)

		if color != None:
			self.SetBackgroundColour(color)

		if self.mode == self.MODE_FULLSCREEN:
			self.ShowFullScreen(True)
			vbox.Add((20, height // 4))
			if os.name == 'nt':
				install_hook()

		# Create the border and paddings around the html viewer
		panel_1 = wx.Panel(self)
		sizer_1 = wx.BoxSizer(wx.VERTICAL)
		panel_1.SetSizer(sizer_1)

		# Add the title in fullscreen
		if self.mode == self.MODE_FULLSCREEN:
			title = wx.StaticText(panel_1, label=self.title)
			title.SetForegroundColour(self.COLOR_BORDER)
			title.SetFont(wx.Font(20, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, 0, ""))
			sizer_1.Add(title, 0, wx.BOTTOM | wx.LEFT, 10)

		panel_2 = wx.Panel(panel_1)
		panel_2.SetBackgroundColour(self.COLOR_BORDER)
		sizer_2 = wx.BoxSizer()
		panel_2.SetSizer(sizer_2)
		sizer_1.Add(panel_2, 1, wx.ALL | wx.EXPAND, 10)

		# Create the Html viewer
		if WEB_VIEW:
			html = wx.html2.WebView.New(panel_2, style=wx.BORDER_NONE)
			html.SetPage(content, '')
			self.Bind(wx.html2.EVT_WEBVIEW_NAVIGATING, self.on_navigating, html)
		else:
			html = wx.html.HtmlWindow(panel_2)
			if 'gtk2' in wx.PlatformInfo:
				html.SetStandardFonts()
			content = comm.convert_embedded_images(content, self.mem_fs)
			content = comm.rewrite_styles(content)
			html.SetPage(content)
			self.Bind(wx.html.EVT_HTML_LINK_CLICKED, self.on_navigating, html)

		html.SetBackgroundColour(self.COLOR_HTML)
		sizer_2.Add(html, 1, wx.ALL |wx.EXPAND, 1)
		vbox.Add(panel_1, proportion=1, flag=wx.EXPAND)

		# Buttons background
		self.hbox = wx.BoxSizer(orient=wx.HORIZONTAL)
		vbox.Add(self.hbox, proportion=0, flag=wx.ALL | wx.EXPAND, border=5)
		self.hbox.AddStretchSpacer(1)
		# self.hbox.SetBackgroundColour(self.COLOR_BTN_BG)

		# Create the buttons
		if len(responses) <= 2:
			for label in responses:
				self.hbox.Add((10, 10))
				button = wx.Button(self, label=label)
				button.SetForegroundColour(self.COLOR_BTN_TXT)
				self.hbox.Add(button, proportion=0, flag=wx.ALIGN_CENTER_VERTICAL)
				button.Bind(wx.EVT_BUTTON, self.on_btn_close)
		else:
			self.combo = wx.Choice(self, choices=responses, style=0)
			self.combo.SetSelection(wx.NOT_FOUND)
			self.combo.SetForegroundColour(self.COLOR_BTN_TXT)
			self.Bind(wx.EVT_CHOICE, self.on_combo_change)
			self.hbox.Add(self.combo, proportion=0, flag=wx.ALIGN_CENTER_VERTICAL)

			self.hbox.Add((10, 10))

			self.button = wx.Button(self, label=ok)
			self.button.SetForegroundColour(self.COLOR_BTN_TXT)
			self.hbox.Add(self.button, proportion=0, flag=wx.ALIGN_CENTER_VERTICAL)
			self.button.Bind(wx.EVT_BUTTON, self.on_btn_close)
			self.button.Disable()

		# Set the paddings for the fullscreen window
		if self.mode == self.MODE_FULLSCREEN:
			vbox.Add((20, height // 4))
			mbox = wx.BoxSizer(orient=wx.HORIZONTAL)
			mbox.Add((width // 4, 20))
			mbox.Add(vbox, proportion=1, flag=wx.EXPAND)
			mbox.Add((width // 4, 20))
			self.SetSizer(mbox)
		else:
			self.SetSizer(vbox)

		if self.mode != self.MODE_NORMAL:
			self.SetWindowStyle(self.GetWindowStyle() | wx.STAY_ON_TOP)

		self.Layout()

	def on_close(self, event):
		if self.validated or self.mode == self.MODE_NORMAL:
			event.Skip()
			return

	def on_combo_change(self, event):
		if self.combo.GetSelection() == wx.NOT_FOUND:
			self.button.Disable()
		else:
			self.button.Enable()

	def on_btn_close(self, event):
		if hasattr(self, 'combo'):
			sel = self.combo.GetSelection()
			if sel == wx.NOT_FOUND:
				return
			response = self.combo.GetString(sel)
		else:
			response = event.GetEventObject().GetLabel()

		def do_acknowledge():
			self.comm.ack_message(self.session_manager, self.token, response, self.acknowledged)
		for child in self.hbox.Children:
			if hasattr(child, 'Disable'):
				child.Disable()
		threading.Thread(target=do_acknowledge).start()

	def acknowledged(self, error):
		def on_acknowledged():
			self.validated = True
			self.Close()

			if error is not None:
				print('SM error:', error, file=sys.stderr)
				msg = gettext.gettext('An internal error occurred, please contact your administrator')
				dlg = wx.MessageDialog(self, msg, caption=self.title,
				      style=wx.OK|wx.CENTRE, pos=wx.DefaultPosition)
				dlg.ShowModal()
				for child in self.hbox.Children:
					if hasattr(child, 'Enable'):
						child.Enable()

		wx.CallAfter(on_acknowledged)

	def on_navigating(self, evt):
		url = evt.GetURL() if WEB_VIEW else evt.GetLinkInfo().GetHref()
		if not (url.startswith('file://') or url == 'about:blank'):
			webbrowser.open(url, new=0, autoraise=True)
			if WEB_VIEW:
				evt.Veto()


class HttpsFileSystemHandler(wx.FileSystemHandler):
	def __init__(self):
		wx.FileSystemHandler.__init__(self)

	def CanOpen(self, location):
		return self.GetProtocol(location) == 'https'

	def OpenFile(self, parent_fs, location):
		req = requests.get(location)
		if req.status_code == 200:
			stream = io.BytesIO(req.content)
			return wx.FSFile(stream, location, req.headers['content-type'], '', wx.DateTime.Now())


def main():
	parser = argparse.ArgumentParser(description='OVD Message user interface')
	parser.add_argument('session_manager', metavar='SM', help='The OVD Session Manager IP address')
	parser.add_argument('token', metavar='TOKEN', help='The message ID to retrieve')
	args = parser.parse_args()

	def handler(signum, frame):
		print("Signal handler: check if token is still valid")
		comm = SmCommunication()
		message = comm.get_message(args.session_manager, args.token)
		if message:
			return

		print("Token is no longer valid: exit")
		frm.Close()

	signal.signal(signal.SIGINT, handler)

	wx.FileSystem.AddHandler(wx.MemoryFSHandler())
	wx.FileSystem.AddHandler(HttpsFileSystemHandler())
	app = wx.App(clearSigInt=False)
	frm = OvdMessage(SmCommunication(), args.session_manager, args.token)

	# Define the timer in order to catch signals
	timer = wx.Timer(frm)
	frm.Bind(wx.EVT_TIMER, lambda x: None, timer)

	if sys.platform == "win32" and "LOCALAPPDATA" in os.environ:
		cache_dir = os.path.join(os.environ["LOCALAPPDATA"], "ovd-message")
	else:
		cache_dir = os.path.join(os.path.expanduser('~'), ".cache", "ovd-message")

	if not os.path.isdir(cache_dir):
		os.makedirs(cache_dir)

	for path in glob.glob(os.path.join(cache_dir, "instance_*")):
		try:
			with open(path) as f:
				pid = int(f.readline().strip())
				print("Order token refresh on process", pid)
				os.kill(pid, signal.SIGINT)
		except (ValueError, IOError, OSError) as err:
			print(err)
			continue

	name = None
	with tempfile.NamedTemporaryFile(dir=cache_dir, prefix="instance_", delete=False) as f:
		f.write(b"%i\n" % os.getpid())
		name = f.name

	try:
		timer.Start(1000)
		frm.Show()
		app.MainLoop()
	finally:
		os.remove(name)

if __name__ == '__main__':
	try:
		main()
	except SystemExit:
		pass
	except:
		print('Unexpected error:', file=sys.stderr)
		print(traceback.format_exc(), file=sys.stderr)
