import code, sys, traceback
from ..Qt import QtWidgets, QtGui, QtCore
from ..functions import mkBrush
from .CmdInput import CmdInput


class ReplWidget(QtWidgets.QWidget):
    sigCommandEntered = QtCore.Signal(object, object)  # self, command
    sigCommandRaisedException = QtCore.Signal(object, object)  # self, exc

    def __init__(self, globals, locals, parent=None):
        self.globals = globals
        self.locals = locals
        self._lastCommandRow = None
        self._commandBuffer = []  # buffer to hold multiple lines of input
        self.stdoutInterceptor = StdoutInterceptor(self.write)
        self.ps1 = ">>> "
        self.ps2 = "... "

        QtWidgets.QWidget.__init__(self, parent=parent)

        self._setupUi()

        # define text styles
        isDark = self.output.palette().color(QtGui.QPalette.ColorRole.Base).value() < 128
        outputBlockFormat = QtGui.QTextBlockFormat()
        outputFirstLineBlockFormat = QtGui.QTextBlockFormat(outputBlockFormat)
        outputFirstLineBlockFormat.setTopMargin(5)
        outputCharFormat = QtGui.QTextCharFormat()
        outputCharFormat.setFontWeight(QtGui.QFont.Weight.Normal)
        cmdBlockFormat = QtGui.QTextBlockFormat()
        cmdBlockFormat.setBackground(mkBrush("#335" if isDark else "#CCF"))
        cmdCharFormat = QtGui.QTextCharFormat()
        cmdCharFormat.setFontWeight(QtGui.QFont.Weight.Bold)
        self.textStyles = {
            'command': (cmdCharFormat, cmdBlockFormat),
            'output': (outputCharFormat, outputBlockFormat),
            'output_first_line': (outputCharFormat, outputFirstLineBlockFormat),
        }

        self.input.ps1 = self.ps1
        self.input.ps2 = self.ps2

    def _setupUi(self):
        self.layout = QtWidgets.QVBoxLayout(self)
        self.layout.setContentsMargins(0, 0, 0, 0)
        self.layout.setSpacing(0)
        self.setLayout(self.layout)

        self.output = QtWidgets.QTextEdit(self)
        font = QtGui.QFont()
        font.setFamily("Courier New")
        font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferAntialias)
        self.output.setFont(font)
        self.output.setReadOnly(True)
        self.layout.addWidget(self.output)
        
        # put input box in a horizontal layout so we can easily place buttons at the end
        self.inputWidget = QtWidgets.QWidget(self)
        self.layout.addWidget(self.inputWidget)
        self.inputLayout = QtWidgets.QHBoxLayout()
        self.inputWidget.setLayout(self.inputLayout)
        self.inputLayout.setContentsMargins(0, 0, 0, 0)

        self.input = CmdInput(parent=self)
        self.inputLayout.addWidget(self.input)

        self.input.sigExecuteCmd.connect(self.runCmd)

    def runCmd(self, cmd):
        if '\n' in cmd:
            for line in cmd.split('\n'):
                self.runCmd(line)
            return

        if len(self._commandBuffer) == 0:
            self.write(f"{self.ps1}{cmd}\n", style='command')
        else:
            self.write(f"{self.ps2}{cmd}\n", style='command')
        
        self.sigCommandEntered.emit(self, cmd)
        self._commandBuffer.append(cmd)

        fullcmd = '\n'.join(self._commandBuffer)
        try:
            cmdCode = code.compile_command(fullcmd)
            self.input.setMultiline(False)
        except Exception:
            # cannot continue processing this command; reset and print exception
            self._commandBuffer = []
            self.displayException()
            self.input.setMultiline(False)
        else:
            if cmdCode is None:
                # incomplete input; wait for next line
                self.input.setMultiline(True)
                return

            self._commandBuffer = []

            # run command
            try:
                with self.stdoutInterceptor:
                    exec(cmdCode, self.globals(), self.locals())  # nosec B102
            except Exception as exc:
                self.displayException()
                self.sigCommandRaisedException.emit(self, exc)

            # Add a newline if the output did not
            cursor = self.output.textCursor()
            if cursor.columnNumber() > 0:
                self.write('\n')

    def write(self, strn, style='output', scrollToBottom='auto'):
        """Write a string into the console.

        If scrollToBottom is 'auto', then the console is automatically scrolled
        to fit the new text only if it was already at the bottom.
        """
        isGuiThread = QtCore.QThread.currentThread() == QtCore.QCoreApplication.instance().thread()
        if not isGuiThread:
            sys.__stdout__.write(strn)
            return

        cursor = self.output.textCursor()
        cursor.movePosition(QtGui.QTextCursor.MoveOperation.End)
        self.output.setTextCursor(cursor)

        sb = self.output.verticalScrollBar()
        scroll = sb.value()
        if scrollToBottom == 'auto':
            atBottom = scroll == sb.maximum()
            scrollToBottom = atBottom

        row = cursor.blockNumber()
        if style == 'command':
            self._lastCommandRow = row

        if style == 'output' and row == self._lastCommandRow + 1:
            # adjust style for first line of output
            firstLine, endl, strn = strn.partition('\n')
            self._setTextStyle('output_first_line')
            self.output.insertPlainText(firstLine + endl)

        if len(strn) > 0:
            self._setTextStyle(style)
            self.output.insertPlainText(strn)
            # return to output style immediately to avoid seeing an extra line of command style
            if style != 'output':
                self._setTextStyle('output')

        if scrollToBottom:
            sb.setValue(sb.maximum())
        else:
            sb.setValue(scroll)

    def displayException(self):
        """
        Display the current exception and stack.
        """
        tb = traceback.format_exc()
        lines = []
        indent = 4
        prefix = '' 
        for l in tb.split('\n'):
            lines.append(" "*indent + prefix + l)
        self.write('\n'.join(lines))
        
    def _setTextStyle(self, style):
        charFormat, blockFormat = self.textStyles[style]
        cursor = self.output.textCursor()
        cursor.setBlockFormat(blockFormat)
        self.output.setCurrentCharFormat(charFormat)


class StdoutInterceptor:
    """Used to temporarily redirect writes meant for sys.stdout and sys.stderr to a new location
    """
    def __init__(self, writeFn):
        self._orig_stdout = None
        self._orig_stderr = None
        self.writeFn = writeFn

    def realOutputFiles(self):
        """Return the real sys.stdout and stderr (which are sometimes masked while running commands)
        """
        return (
            self._orig_stdout or sys.stdout,
            self._orig_stderr or sys.stderr
        )

    def print(self, *args):
        """Print to real stdout (for debugging)
        """
        self.realOutputFiles()[0].write(' '.join(map(str, args)) + "\n")

    def flush(self):
        # Need to implement this since we temporarily occlude sys.stdout
        pass

    def fileno(self):
        # Need to implement this since we temporarily occlude sys.stdout, and someone may be looking for it (faulthandler, for example)
        return 1

    def write(self, strn):
        self.writeFn(strn)

    def __enter__(self):
        self._orig_stdout = sys.stdout
        self._orig_stderr = sys.stderr
        sys.stdout = self
        sys.stderr = self

    def __exit__(self, exc_type, exc_val, exc_tb):
        sys.stdout = self._orig_stdout
        sys.stderr = self._orig_stderr
        self._orig_stdout = None
        self._orig_stderr = None

