# -*- coding: utf-8 -*-
#
# pythonconsole.py
#
# Copyright (C) 2006 - Steve Frécinaux
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2, or (at your option)
# any later version.
#
# The Rhythmbox authors hereby grant permission for non-GPL compatible
# GStreamer plugins to be used and distributed together with GStreamer
# and Rhythmbox. This permission is above and beyond the permissions granted
# by the GPL license by which Rhythmbox is covered. If you modify this code
# you may extend this exception to your version of the code, but you are not
# obligated to do so. If you do not wish to do so, delete this exception
# statement from your version.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.

# Parts from "Interactive Python-GTK Console" (stolen from epiphany's console.py)
#     Copyright (C), 1998 James Henstridge <james@daa.com.au>
#     Copyright (C), 2005 Adam Hooper <adamh@densi.com>
# Bits from gedit Python Console Plugin
#     Copyrignt (C), 2005 Raphaël Slinckx

import string
import sys
import re
import os
import traceback

from gi.repository import GLib, Gtk, Gdk, Gio, GObject, Pango, Peas
from gi.repository import RB

import gettext
gettext.install('rhythmbox', RB.locale_dir())

DEBUG_PORT = 5678

try:
	import rpdb2
	have_rpdb2 = True
except:
	have_rpdb2 = False

try:
	import debugpy
	have_debugpy = True
except:
	have_debugpy = False


class PythonConsolePlugin(GObject.Object, Peas.Activatable):
	__gtype_name__ = 'PythonConsolePlugin'

	object = GObject.property (type = GObject.Object)

	def __init__(self):
		GObject.Object.__init__(self)
		self.window = None
		
	def do_activate(self):
		shell = self.object
		app = shell.props.application

		action = Gio.SimpleAction.new("python-console", None)
		action.connect('activate', self.show_console, shell)
		app.add_action(action)

		app.add_plugin_menu_item("tools",
					 "python-console",
					 Gio.MenuItem.new(label=_("Python Console"),
						 	  detailed_action="app.python-console"))

		self.rpdb2_action = Gio.SimpleAction.new("python-debugger-winpdb", None)
		self.rpdb2_action.connect('activate', self.attach_winpdb, shell)
		app.add_action(self.rpdb2_action)

		app.add_plugin_menu_item("tools",
					 "python-debugger-winpdb",
					 Gio.MenuItem.new(label=_("Python Debugger (winpdb)"),
							  detailed_action="app.python-debugger-winpdb"))

		self.debugpy_action = Gio.SimpleAction.new("python-debugger-debugpy", None)
		self.debugpy_action.connect('activate', self.enable_debugpy, shell)
		app.add_action(self.debugpy_action)

		app.add_plugin_menu_item("tools",
					 "python-debugger-debugpy",
					 Gio.MenuItem.new(label=_("Python Debugger (debugpy)"),
							  detailed_action="app.python-debugger-debugpy"))

	def do_deactivate(self):
		shell = self.object
		app = shell.props.application

		app.remove_plugin_menu_item("tools", "python-console")
		app.remove_action("python-console")

		app.remove_plugin_menu_item("tools", "python-debugger-winpdb")
		app.remove_action("python-debugger-winpdb")
		self.rpdb2_action = None

		app.remove_plugin_menu_item("tools", "python-debugger-debugpy")
		app.remove_action("python-debugger-debugpy")
		self.debugpy_action = None
		
		if self.window is not None:
			self.window.destroy()


	def show_console(self, action, parameter, shell):
		if not self.window:
			ns = {'__builtins__' : __builtins__, 
			      'RB' : RB,
			      'shell' : shell}
			console = PythonConsole(namespace = ns, 
			                        destroy_cb = self.destroy_console)
			console.set_size_request(600, 400)
			console.eval('print("' + \
			             _("You can access the main window " \
			             "through the \'shell\' variable :") +
			             '\\n%s" % shell)', False)

			self.window = Gtk.Window()
			self.window.set_title('Rhythmbox Python Console')
			self.window.add(console)
			self.window.connect('destroy', self.destroy_console)
			self.window.show_all()
		else:
			self.window.show_all()
		self.window.grab_focus()

	def attach_winpdb(self, action, parameter, shell):
		if have_rpdb2 is False:
			self.missing_python_module("rpdb2", "winpdb")
			self.rpdb2_action.set_enabled(False)
			return False

		pwd_path = os.path.join(RB.user_data_dir(), "rpdb2_password")
		msg = _("After you press OK, Rhythmbox will wait until you connect to it with winpdb or rpdb2. If you have not set a debugger password in the file %s, it will use the default password ('rhythmbox').") % pwd_path
		dialog = Gtk.MessageDialog(None, 0, Gtk.MessageType.INFO, Gtk.ButtonsType.OK_CANCEL, msg)
		if dialog.run() == Gtk.ResponseType.OK:
			password = "rhythmbox"
			if os.path.exists(pwd_path):
				pwd_file = open(pwd_path)
				password = pwd_file.read().rstrip()
				pwd_file.close()

			def start_debugger(password):
				rpdb2.start_embedded_debugger(password)
				return False

			GLib.idle_add(start_debugger, password)
		dialog.destroy()
		return False
	
	def enable_debugpy(self, action, parameter, shell):
		if have_debugpy is False:
			self.missing_python_module("debugpy", "debugpy")
			self.debugpy_action.set_enabled(False)
			return False
		else:
			try:
				host, port = debugpy.listen(DEBUG_PORT)
				msgtype = Gtk.MessageType.INFO
				msg = _("Rhythmbox is now listening for Debug Adapter Protocol connections on port %d. You can now attach to it using a compatible debugger such as vimspector, nvim-dap or Visual Studio Code.") % port
				self.debugpy_action.set_enabled(False)
			except Exception as e:
				msgtype = Gtk.MessageType.ERROR
				msg = _("Unable to start Debug Adapter Protocol listener: %s") % str(e)

		self.message_dialog(msg, msgtype)
		return False

	def missing_python_module(self, module, debugger):
		msg = _("The %s Python module is not available. Install the module and then restart Rhythmbox to enable debugging with %s.") % (module, debugger)
		self.message_dialog(msg, Gtk.MessageType.ERROR)

	def message_dialog(self, msg, msgtype=Gtk.MessageType.INFO):
		dialog = Gtk.MessageDialog(None, 0, msgtype, Gtk.ButtonsType.OK, msg)
		dialog.connect("response", self.message_dialog_response)
		dialog.show()


	def message_dialog_response(self, dialog, rsp):
		dialog.destroy()

	def destroy_console(self, *args):
		self.window.destroy()
		self.window = None

class PythonConsole(Gtk.ScrolledWindow):

	def get_end_iter(self):
		return self.view.get_buffer().get_end_iter()

	def get_iter_at_mark(self, mark):
		return self.view.get_buffer().get_iter_at_mark(mark)

	def __init__(self, namespace = {}, destroy_cb = None):
		Gtk.ScrolledWindow.__init__(self)

		self.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC);
		self.set_shadow_type(Gtk.ShadowType.NONE)
		self.view = Gtk.TextView()
		self.view.modify_font(Pango.font_description_from_string('Monospace'))
		self.view.set_editable(True)
		self.view.set_wrap_mode(Gtk.WrapMode.CHAR)
		self.add(self.view)
		self.view.show()

		buffer = self.view.get_buffer()
		self.normal = buffer.create_tag("normal")
		self.error  = buffer.create_tag("error")
		self.error.set_property("foreground", "red")
		self.command = buffer.create_tag("command")
		self.command.set_property("foreground", "blue")

		self.__spaces_pattern = re.compile(r'^\s+')		
		self.namespace = namespace

		self.destroy_cb = destroy_cb

		self.block_command = False

		# Init first line
		buffer.create_mark("input-line", self.get_end_iter(), True)
		buffer.insert(self.get_end_iter(), ">>> ")
		buffer.create_mark("input", self.get_end_iter(), True)

		# Init history
		self.history = ['']
		self.history_pos = 0
		self.current_command = ''
		self.namespace['__history__'] = self.history

		# Set up hooks for standard output.
		self.stdout = gtkoutfile(self, sys.stdout.fileno(), self.normal)
		self.stderr = gtkoutfile(self, sys.stderr.fileno(), self.error)

		# Signals
		self.view.connect("key-press-event", self.__key_press_event_cb)
		buffer.connect("mark-set", self.__mark_set_cb)


	def __key_press_event_cb(self, view, event):
		if event.keyval == Gdk.KEY_D and \
		   event.state == Gdk.ModifierType.CONTROL_MASK:
			self.destroy()
		
		elif event.keyval == Gdk.KEY_Return and \
		     event.state == Gdk.ModifierType.CONTROL_MASK:
			# Get the command
			buffer = view.get_buffer()
			inp_mark = buffer.get_mark("input")
			inp = self.get_iter_at_mark(inp_mark)
			cur = self.get_end_iter()
			line = buffer.get_text(inp, cur, True)
			self.current_command = self.current_command + line + "\n"
			self.history_add(line)

			# Prepare the new line
			cur = self.get_end_iter()
			buffer.insert(cur, "\n... ")
			cur = self.get_end_iter()
			buffer.move_mark(inp_mark, cur)
			
			# Keep indentation of precendent line
			spaces = re.match(self.__spaces_pattern, line)
			if spaces is not None:
				buffer.insert(cur, line[spaces.start() : spaces.end()])
				cur = self.get_end_iter()
				
			buffer.place_cursor(cur)
			GLib.idle_add(self.scroll_to_end)
			return True
		
		elif event.keyval == Gdk.KEY_Return:
			# Get the marks
			buffer = view.get_buffer()
			lin_mark = buffer.get_mark("input-line")
			inp_mark = buffer.get_mark("input")

			# Get the command line
			inp = self.get_iter_at_mark(inp_mark)
			cur = self.get_end_iter()
			line = buffer.get_text(inp, cur, True)
			self.current_command = self.current_command + line + "\n"
			self.history_add(line)

			# Make the line blue
			lin = self.get_iter_at_mark(lin_mark)
			buffer.apply_tag(self.command, lin, cur)
			buffer.insert(cur, "\n")
			
			cur_strip = self.current_command.rstrip()

			if cur_strip.endswith(":") \
			or (self.current_command[-2:] != "\n\n" and self.block_command):
				# Unfinished block command
				self.block_command = True
				com_mark = "... "
			elif cur_strip.endswith("\\"):
				com_mark = "... "
			else:
				# Eval the command
				self.__run(self.current_command)
				self.current_command = ''
				self.block_command = False
				com_mark = ">>> "

			# Prepare the new line
			cur = self.get_end_iter()
			buffer.move_mark(lin_mark, cur)
			buffer.insert(cur, com_mark)
			cur = self.get_end_iter()
			buffer.move_mark(inp_mark, cur)
			buffer.place_cursor(cur)
			GLib.idle_add(self.scroll_to_end)
			return True

		elif event.keyval == Gdk.KEY_KP_Down or \
		     event.keyval == Gdk.KEY_Down:
			# Next entry from history
			view.emit_stop_by_name("key_press_event")
			self.history_down()
			GLib.idle_add(self.scroll_to_end)
			return True

		elif event.keyval == Gdk.KEY_KP_Up or \
		     event.keyval == Gdk.KEY_Up:
			# Previous entry from history
			view.emit_stop_by_name("key_press_event")
			self.history_up()
			GLib.idle_add(self.scroll_to_end)
			return True

		elif event.keyval == Gdk.KEY_KP_Left or \
		     event.keyval == Gdk.KEY_Left or \
		     event.keyval == Gdk.KEY_BackSpace:
			buffer = view.get_buffer()
			inp = self.get_iter_at_mark(buffer.get_mark("input"))
			cur = self.get_iter_at_mark(buffer.get_insert())
			return inp.compare(cur) == 0

		elif event.keyval == Gdk.KEY_Home:
			# Go to the begin of the command instead of the begin of
			# the line
			buffer = view.get_buffer()
			inp = self.get_iter_at_mark(buffer.get_mark("input"))
			if event.state == Gdk.ModifierType.SHIFT_MASK:
				buffer.move_mark_by_name("insert", inp)
			else:
				buffer.place_cursor(inp)
			return True
		
	def __mark_set_cb(self, buffer, iter, name):
		input = self.get_iter_at_mark(buffer.get_mark("input"))
		pos   = self.get_iter_at_mark(buffer.get_insert())
		self.view.set_editable(pos.compare(input) != -1)

	def get_command_line(self):
		buffer = self.view.get_buffer()
		inp = self.get_iter_at_mark(buffer.get_mark("input"))
		cur = self.get_end_iter()
		return buffer.get_text(inp, cur, True)
	
	def set_command_line(self, command):
		buffer = self.view.get_buffer()
		mark = buffer.get_mark("input")
		inp = self.get_iter_at_mark(mark)
		cur = self.get_end_iter()
		buffer.delete(inp, cur)
		buffer.insert(inp, command)
		buffer.select_range(self.get_iter_at_mark(mark),
		                    self.get_end_iter())
		self.view.grab_focus()
	
	def history_add(self, line):
		if line.strip() != '':
			self.history_pos = len(self.history)
			self.history[self.history_pos - 1] = line
			self.history.append('')
	
	def history_up(self):
		if self.history_pos > 0:
			self.history[self.history_pos] = self.get_command_line()
			self.history_pos = self.history_pos - 1
			self.set_command_line(self.history[self.history_pos])
			
	def history_down(self):
		if self.history_pos < len(self.history) - 1:
			self.history[self.history_pos] = self.get_command_line()
			self.history_pos = self.history_pos + 1
			self.set_command_line(self.history[self.history_pos])
	
	def scroll_to_end(self):
		iter = self.get_end_iter()
		self.view.scroll_to_iter(iter, 0.0, False, 0.5, 0.5)
		return False

	def write(self, text, tag = None):
		buffer = self.view.get_buffer()
		if tag is None:
			buffer.insert(self.get_end_iter(), text)
		else:
			iter = self.get_end_iter()
			offset = iter.get_offset()
			buffer.insert(iter, text)

			start_iter = Gtk.TextIter()
			start_iter = buffer.get_iter_at_offset(offset)
			buffer.apply_tag(tag, start_iter, self.get_end_iter())
		GLib.idle_add(self.scroll_to_end)

	def eval(self, command, display_command = False):
		buffer = self.view.get_buffer()
		lin = buffer.get_mark("input-line")
		buffer.delete(self.get_iter_at_mark(lin),
			      self.get_end_iter())

		if isinstance(command, list) or isinstance(command, tuple):
			for c in command:
				if display_command:
					self.write(">>> " + c + "\n", self.command)
				self.__run(c)
		else:
			if display_command:
				self.write(">>> " + c + "\n", self.command)
			self.__run(command)

		cur = self.get_end_iter()
		buffer.move_mark_by_name("input-line", cur)
		buffer.insert(cur, ">>> ")
		cur = self.get_end_iter()
		buffer.move_mark_by_name("input", cur)
		self.view.scroll_to_iter(self.get_end_iter(), 0.0, False, 0.5, 0.5)
	
	def __run(self, command):
		sys.stdout, self.stdout = self.stdout, sys.stdout
		sys.stderr, self.stderr = self.stderr, sys.stderr
		
		try:
			try:
				r = eval(command, self.namespace, self.namespace)
				if r is not None:
					print(repr(r))
			except SyntaxError:
				exec(command, self.namespace)
		except:
			if hasattr(sys, 'last_type') and sys.last_type == SystemExit:
				self.destroy()
			else:
				traceback.print_exc()
		finally:
			sys.stdout, self.stdout = self.stdout, sys.stdout
			sys.stderr, self.stderr = self.stderr, sys.stderr


	def destroy(self):
		if self.destroy_cb is not None:
			self.destroy_cb()
		
class gtkoutfile:
	"""A fake output file object.  It sends output to a TK test widget,
	and if asked for a file number, returns one set on instance creation"""
	def __init__(self, console, fn, tag):
		self.fn = fn
		self.console = console
		self.tag = tag
	def close(self):         pass
	def flush(self):         pass
	def fileno(self):        return self.fn
	def isatty(self):        return 0
	def read(self, a):       return ''
	def readline(self):      return ''
	def readlines(self):     return []
	def write(self, s):      self.console.write(s, self.tag)
	def writelines(self, l): self.console.write(l, self.tag)
	def seek(self, a):       raise IOError((29, 'Illegal seek'))
	def tell(self):          raise IOError((29, 'Illegal seek'))
	truncate = tell

# ex:noet:ts=8:
