"""
Copyright (c) 2017 Eliakin Costa <eliakim170@gmail.com>

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, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
"""
from PyQt5.QtWidgets import QAction
from PyQt5.QtGui import QIcon, QKeySequence
from PyQt5.QtCore import Qt
import sys
from . import docwrapper
import importlib
from importlib.machinery import SourceFileLoader
import traceback


PYTHON33 = sys.version_info.major == 3 and sys.version_info.minor == 3
PYTHON34 = sys.version_info.major == 3 and sys.version_info.minor == 4
EXEC_NAMESPACE = "__main__"  # namespace that user scripts will run in


class RunAction(QAction):

    def __init__(self, scripter, parent=None):
        super(RunAction, self).__init__(parent)
        self.scripter = scripter

        self.editor = self.scripter.uicontroller.editor
        self.output = self.scripter.uicontroller.findTabWidget('OutPut', 'OutPutTextEdit')

        self.triggered.connect(self.run)

        self.setText('Run')
        self.setToolTip('Run Ctrl+R')
        self.setIcon(QIcon(':/icons/run.svg'))
        self.setShortcut(QKeySequence(Qt.CTRL + Qt.Key_R))

    @property
    def parent(self):
        return 'toolBar'

    def run(self):
        """ This method execute python code from an activeDocument (file) or direct
            from editor (ui_scripter/editor/pythoneditor.py). When executing code
            from a file, we use importlib to load this module/file and with
            "users_script" name. That's implementation seeks for a "main()" function in the script.
            When executing code from editor without creating a file, we compile
            this script to bytecode and we execute this in an empty scope. That's
            faster than use exec directly and cleaner, because we are using an empty scope. """

        self.scripter.uicontroller.setActiveWidget('OutPut')
        stdout = sys.stdout
        stderr = sys.stderr
        output = docwrapper.DocWrapper(self.output.document())
        if (self.editor._documentModified is True):
            output.write("==== Warning: Script not saved! ====\n")
        else:
            output.write("======================================\n")
        sys.stdout = output
        sys.stderr = output

        script = self.editor.document().toPlainText()
        document = self.scripter.documentcontroller.activeDocument

        try:
            if document and self.editor._documentModified is False:
                spec = importlib.util.spec_from_file_location(EXEC_NAMESPACE, document.filePath)
                try:
                    users_module = importlib.util.module_from_spec(spec)
                    spec.loader.exec_module(users_module)

                except AttributeError as e:  # no module from spec
                    if PYTHON34 or PYTHON33:
                        loader = SourceFileLoader(EXEC_NAMESPACE, document.filePath)
                        users_module = loader.load_module()
                    else:
                        raise e

                try:
                    # maybe script is to be execed, maybe main needs to be invoked
                    # if there is a main() then execute it, otherwise don't worry...
                    users_module.main()
                except AttributeError:
                    pass

            else:
                code = compile(script, '<string>', 'exec')
                globals_dict = {"__name__": EXEC_NAMESPACE}
                exec(code, globals_dict)

        except Exception as e:
            """Provide context (line number and text) for an error that is caught.
            Ordinarily, syntax and Indent errors are caught during initial
            compilation in exec(), and the traceback traces back to this file.
            So these need to be treated separately.
            Other errors trace back to the file/script being run.
            """
            type_, value_, traceback_ = sys.exc_info()
            if type_ == SyntaxError:
                errorMessage = "%s\n%s" % (value_.text.rstrip(), " " * (value_.offset - 1) + "^")
                # rstrip to remove trailing \n, output needs to be fixed width font for the ^ to align correctly
                errorText = "Syntax Error on line %s" % value_.lineno
            elif type_ == IndentationError:
                # (no offset is provided for an IndentationError
                errorMessage = value_.text.rstrip()
                errorText = "Unexpected Indent on line %s" % value_.lineno
            else:
                errorText = traceback.format_exception_only(type_, value_)[0]
                format_string = "In file: {0}\nIn function: {2} at line: {1}. Line with error:\n{3}"
                tbList = traceback.extract_tb(traceback_)
                tb = tbList[-1]
                errorMessage = format_string.format(*tb)
            m = "\n**********************\n%s\n%s\n**********************\n" % (errorText, errorMessage)
            output.write(m)

        sys.stdout = stdout
        sys.stderr = stderr

        # scroll to bottom of output
        bottom = self.output.verticalScrollBar().maximum()
        self.output.verticalScrollBar().setValue(bottom)
