# Copyright (c) 2011-2015 Rusty Wagner
#
# 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 of the License, or
# (at your option) any later 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, see <http://www.gnu.org/licenses/>.

from PySide.QtCore import *
from PySide.QtGui import *
from Fonts import *
import code
import sys
import threading
import Threads

class PythonConsoleOutput():
	def __init__(self, orig, error):
		self.stdout = orig
		self.error = error

	def write(self, data):
		console = None
		if "value" in dir(_python_console):
			console = _python_console.value

		if console is None:
			self.stdout.write(data)
		else:
			if self.error:
				Threads.run_on_gui_thread(lambda: console.write_stderr(data))
			else:
				Threads.run_on_gui_thread(lambda: console.write_stdout(data))

class PythonConsoleInput():
	def __init__(self, orig):
		self.stdin = orig

	def read(self, size):
		console = None
		if "value" in dir(_python_console):
			console = _python_console.value

		if console is None:
			return self.stdin.read(size)
		else:
			return console.read_stdin(size)

	def readline(self):
		console = None
		if "value" in dir(_python_console):
			console = _python_console.value

		if console is None:
			return self.stdin.readline()
		else:
			return console.readline_stdin()

class PythonConsoleThread(threading.Thread):
	def __init__(self, console):
		threading.Thread.__init__(self)
		self.console = console
		self.globals = {"__name__":"__console__", "__doc__":None}
		self.code = None
		self.event = threading.Event()
		self.done = threading.Event()
		self.exit = False
		self.interpreter = code.InteractiveInterpreter(self.globals)

		# There is no way to interrupt a thread that isn't the main thread, so
		# to avoid not being able to close the app, create the thread as
		# as daemon thread.
		self.daemon = True

		# Set up environment with useful variables and functions
		self.globals["data"] = Threads.GuiObjectProxy(self.console.view.data)
		self.globals["exe"] = Threads.GuiObjectProxy(self.console.view.exe)
		self.globals["view"] = Threads.GuiObjectProxy(self.console.view)

		self.globals["current_view"] = Threads.GuiObjectProxy(lambda: self.console.view.view)
		self.globals["change_view"] = Threads.GuiObjectProxy(lambda type: self.console.view.setViewType(type))
		self.globals["navigate"] = Threads.GuiObjectProxy(lambda type, pos: self.console.view.navigate(type, pos))
		self.globals["create_file"] = Threads.GuiObjectProxy(lambda data: Threads.create_file(data))

		self.globals["cursor"] = Threads.GuiObjectProxy(lambda: self.console.view.view.get_cursor_pos())
		self.globals["set_cursor"] = Threads.GuiObjectProxy(lambda pos: self.console.view.view.set_cursor_pos(pos))
		self.globals["selection_range"] = Threads.GuiObjectProxy(lambda: self.console.view.view.get_selection_range())
		self.globals["set_selection_range"] = Threads.GuiObjectProxy(lambda start, end: self.console.view.view.set_selection_range(start, end))
		self.globals["selection"] = Threads.GuiObjectProxy(lambda: self.get_selection())
		self.globals["replace_selection"] = Threads.GuiObjectProxy(lambda value: self.replace_selection(value))
		self.globals["write_at_cursor"] = Threads.GuiObjectProxy(lambda value: self.write_at_cursor(value))

		self.globals["undo"] = Threads.GuiObjectProxy(lambda: self.console.view.undo())
		self.globals["redo"] = Threads.GuiObjectProxy(lambda: self.console.view.redo())
		self.globals["commit"] = Threads.GuiObjectProxy(lambda: self.console.view.commit_undo())

		self.globals["copy"] = Threads.GuiObjectProxy(lambda value: self.copy(value))
		self.globals["paste"] = Threads.GuiObjectProxy(lambda: self.console.view.view.paste())
		self.globals["clipboard"] = Threads.GuiObjectProxy(lambda: self.get_clipboard())

	# Helper APIs
	def get_selection(self):
		data = self.console.view.view.data
		range = self.console.view.view.get_selection_range()
		return data.read(range[0], range[1] - range[0])

	def replace_selection(self, value):
		data = self.console.view.view.data
		range = self.console.view.view.get_selection_range()
		if (range[1] - range[0]) == len(value):
			result = data.write(range[0], value)
		else:
			data.remove(range[0], range[1] - range[0])
			result = data.insert(range[0], value)
		self.console.view.view.set_cursor_pos(range[0] + result)
		return result

	def write_at_cursor(self, value):
		data = self.console.view.view.data
		pos = self.console.view.view.get_cursor_pos()
		result = data.write(pos, value)
		self.console.view.view.set_cursor_pos(pos + result)
		return result

	def copy(self, data):
		if type(data) != str:
			data = str(data)
		clipboard = QApplication.clipboard()
		clipboard.clear()
		mime = QMimeData()
		mime.setText(data.encode("string_escape").replace("\"", "\\\""))
		mime.setData("application/octet-stream", QByteArray(data))
		clipboard.setMimeData(mime)

	def get_clipboard(self):
		clipboard = QApplication.clipboard()
		mime = clipboard.mimeData()
		if mime.hasFormat("application/octet-stream"):
			return mime.data("application/octet-stream").data()
		elif mime.hasText():
			return mime.text().encode("utf8")
		else:
			return None

	# Thread run loop
	def run(self):
		_python_console.value = self.console
		while not self.exit:
			self.event.wait()
			self.event.clear()
			if self.exit:
				break
			if self.code:
				self.interpreter.runsource(self.code)
				self.code = None
				self.done.set()

class PythonConsoleLineEdit(QLineEdit):
	prevHistory = Signal(())
	nextHistory = Signal(())

	def __init__(self, *args):
		super(PythonConsoleLineEdit, self).__init__(*args)

	def event(self, event):
		if (event.type() == QEvent.KeyPress) and (event.key() == Qt.Key_Tab):
			self.insert("\t")
			return True
		if (event.type() == QEvent.KeyPress) and (event.key() == Qt.Key_Up):
			self.prevHistory.emit()
			return True
		if (event.type() == QEvent.KeyPress) and (event.key() == Qt.Key_Down):
			self.nextHistory.emit()
			return True
		return QLineEdit.event(self, event)

class PythonConsole(QWidget):
	def __init__(self, view):
		super(PythonConsole, self).__init__(view)
		self.view = view

		font = getMonospaceFont()

		layout = QVBoxLayout()
		layout.setContentsMargins(0, 0, 0, 0)
		layout.setSpacing(0)

		self.output = QTextEdit()
		self.output.setFont(font)
		self.output.setReadOnly(True)
		layout.addWidget(self.output, 1)

		input_layout = QHBoxLayout()
		input_layout.setContentsMargins(4, 4, 4, 4)
		input_layout.setSpacing(4)

		self.prompt = QLabel(">>>")
		self.prompt.setFont(font)
		input_layout.addWidget(self.prompt)
		self.input = PythonConsoleLineEdit()
		self.input.setFont(font)
		self.input.returnPressed.connect(self.process_input)
		self.input.prevHistory.connect(self.prev_history)
		self.input.nextHistory.connect(self.next_history)
		input_layout.addWidget(self.input)

		layout.addLayout(input_layout, 0)

		self.setLayout(layout)
		self.setFocusPolicy(Qt.NoFocus)
		self.setMinimumSize(100, 100)

		size_policy = self.sizePolicy()
		size_policy.setVerticalStretch(1)
		self.setSizePolicy(size_policy)

		self.thread = PythonConsoleThread(self)
		self.thread.start()

		self.completion_timer = QTimer()
		self.completion_timer.setInterval(100)
		self.completion_timer.setSingleShot(False)
		self.completion_timer.timeout.connect(self.completion_timer_event)
		self.completion_timer.start()

		self.source = None
		self.running = False

		self.input_requested = False
		self.input_result = ""
		self.input_event = threading.Event()

		self.input_history = []
		self.input_history_pos = None

	def stop(self):
		self.thread.exit = True
		self.thread.event.set()
		# Can't join here, as it might be stuck in user code

	def process_input(self):
		self.input_history += [str(self.input.text())]
		self.input_history_pos = None

		input = str(self.input.text()) + "\n"
		self.input.setText("")

		self.output.textCursor().movePosition(QTextCursor.End)
		fmt = QTextCharFormat()
		fmt.setForeground(QBrush(Qt.black))
		if len(self.prompt.text()) > 0:
			self.output.textCursor().insertText(self.prompt.text() + " " + input, fmt)
		else:
			self.output.textCursor().insertText(input, fmt)
		self.output.ensureCursorVisible()

		if self.input_requested:
			# Request for data from stdin
			self.input_requested = False
			self.input.setEnabled(False)
			self.input_result = input
			self.input_event.set()
			return

		if self.source is not None:
			self.source = self.source + input
			if input != "\n":
				# Don't end multiline input until a blank line
				return
			input = self.source

		try:
			result = code.compile_command(input)
		except:
			result = False

		if result is None:
			if self.source is None:
				self.source = input
			else:
				self.source += input
			self.prompt.setText("...")
			return

		self.source = None
		self.prompt.setText(">>>")

		self.thread.code = input
		self.thread.event.set()
		self.running = True

		self.thread.done.wait(0.05)
		if self.thread.done.is_set():
			self.thread.done.clear()
			self.running = False
		else:
			self.input.setEnabled(False)

	def prev_history(self):
		if len(self.input_history) == 0:
			return
		if self.input_history_pos is None:
			self.input_history_pos = len(self.input_history)
		if self.input_history_pos == 0:
			return
		self.input_history_pos -= 1
		self.input.setText(self.input_history[self.input_history_pos])

	def next_history(self):
		if self.input_history_pos is None:
			return
		if (self.input_history_pos + 1) >= len(self.input_history):
			self.input_history_pos = None
			self.input.setText("")
			return
		self.input_history_pos += 1
		self.input.setText(self.input_history[self.input_history_pos])

	def completion_timer_event(self):
		if self.thread.done.is_set():
			self.thread.done.clear()
			self.running = False
			self.input.setEnabled(True)
			self.input.setFocus(Qt.OtherFocusReason)
			self.prompt.setText(">>>")

	def request_input(self):
		self.input_requested = True
		self.input.setEnabled(True)
		self.input.setFocus(Qt.OtherFocusReason)
		self.prompt.setText("")

	def write_stdout(self, data):
		self.output.textCursor().movePosition(QTextCursor.End)
		fmt = QTextCharFormat()
		fmt.setForeground(QBrush(Qt.blue))
		self.output.textCursor().insertText(data, fmt)
		self.output.ensureCursorVisible()

	def write_stderr(self, data):
		self.output.textCursor().movePosition(QTextCursor.End)
		fmt = QTextCharFormat()
		fmt.setForeground(QBrush(Qt.red))
		self.output.textCursor().insertText(data, fmt)
		self.output.ensureCursorVisible()

	def read_stdin(self, size):
		if Threads.is_gui_thread():
			raise RuntimeError, "Cannot call read_stdin from GUI thread"

		if len(self.input_result) == 0:
			Threads.run_on_gui_thread(self.request_input)
			self.input_event.wait()
			self.input_event.clear()

		if len(self.input_result) > size:
			result = self.input_result[0:size]
			self.input_result = self.input_result[size:]
			return result

		result = self.input_result
		self.input_result = ""
		return 

	def readline_stdin(self):
		if Threads.is_gui_thread():
			raise RuntimeError, "Cannot call readline_stdin from GUI thread"

		if len(self.input_result) == 0:
			Threads.run_on_gui_thread(self.request_input)
			self.input_event.wait()
			self.input_event.clear()

		result = self.input_result
		self.input_result = ""
		return result


sys.stderr = PythonConsoleOutput(sys.stderr, True)
sys.stdout = PythonConsoleOutput(sys.stdout, False)
sys.stdin = PythonConsoleInput(sys.stdin)
_python_console = threading.local()