# -*- coding: utf-8 -*-
"""
This module gives access to the console.
"""

if __host__ is not widget:
    from rubicon.objc import *
    from _get_variables_hierarchy import get_variables_hierarchy
    from code import interact
    import stopit
    import pdb

if __platform__ is iOS:
    import _codecompletion
    from pyto import *
    from pyto import __isMainApp__
    if __host__ is not widget:
        import builtins

import importlib.util
import os
import sys
import traceback
import threading
import time

# MARK: REPL

__repl_code__ = {}

__continue_code_completion__ = True

__isREPLAskingForInput__ = False

def __repl_raw_input__(prompt=None):
    
    current_thread = threading.current_thread()
    
    ConsoleViewController.visible.suggestions = []
    ConsoleViewController.visible.completions = []
    ConsoleViewController.visible.reloadSuggestions()
    
    __isREPLAskingForInput__ = True
    Python.shared.isREPLAskingForInput = True
    
    def code_completion():
        
        prompt_ = ""
        
        while True:
            
            def continue_():
                return (not threading.current_thread() in ignoredThreads and not current_thread in ignoredThreads)
            
            if not continue_():
                break
            
            if __platform__ is iOS and __host__ is not widget and prompt_ != str(ConsoleViewController.visible.prompt):
                prompt_ = str(ConsoleViewController.visible.prompt)
            
                code = ";\n"+__repl_code__[current_thread]+"\n"+prompt_+";"
            
                suggestions = []
                completions = []
                for key, value in _codecompletion.suggestionsForCode(code).items():
                    suggestions.append(key)
                    completions.append(value)
    
                if continue_():
                    ConsoleViewController.visible.completions = completions
                    ConsoleViewController.visible.suggestions = suggestions
                else:
                    break
            else:
                continue

    if not current_thread in __repl_code__:
        __repl_code__[current_thread] = "\n"

    code_completion_thread = None
    if __platform__ is iOS and ConsoleViewController.visible != None:
        code_completion_thread = threading.Thread(target=code_completion, args=())
        code_completion_thread.start()
    
    result = ""
    exc = None
    try:
        result = input(prompt)
    except Exception as e:
        exc = e

    __isREPLAskingForInput__ = False
    Python.shared.isREPLAskingForInput = False
    if __platform__ is iOS and ConsoleViewController.visible != None:
        ConsoleViewController.visible.suggestions = []
        ConsoleViewController.visible.completions = []
        ConsoleViewController.visible.reloadSuggestions

    if exc != None:
        raise exc

    if __platform__ is iOS and ConsoleViewController.visible != None:
        ignoredThreads.append(code_completion_thread)

    __repl_code__[current_thread] += "\n"+result

    return result

def __runREPL__():
    
    if __host__ is widget:
        return
    
    os.system = Python.shared.system
    
    sys.argv = ['']
        
    Python.shared.isScriptRunning = True

    interact(readfunc=__repl_raw_input__)

# MARK: - Running

__script__ = None

__is_loop_running__ = False

__inspector_thread__ = None

__i__ = 0

__breakpoints__ = []

__are_breakpoints_set__ = True
    
__should_inspect__ = False

def run_script(path, replMode=False, debug=False, breakpoints=[]):
    """
    Run the script at given path catching exceptions.
    
    This function should only be used internally by Pyto.
    
    Args:
        path: The path of the script.
        replMode: If set to `True`, errors will not be handled.
        debug: Set to `True` for debugging.
        breakpoints: Lines to break if debugging.
    """
    
    if __platform__ is iOS:
        python = Python.shared
    
        sys.argv = [path]
        for arg in python.args:
            if arg != "":
                sys.argv.append(str(arg))
    
    def run() -> None:
        
        if __platform__ is iOS:
            Python.shared.isScriptRunning = True
            os.system = Python.shared.system
            directory = os.path.expanduser(os.path.dirname(path))
            os.chdir(directory)
            sys.path.insert(0, directory)
        
        try:
            global __script__
            spec = importlib.util.spec_from_file_location("__main__", path)
            __script__ = importlib.util.module_from_spec(spec)

            if debug and __platform__ is iOS and __host__ is not widget:
                
                try:
                    console
                except:
                    import console
                
                console.__are_breakpoints_set__ = False
                console.__should_inspect__ = True
                console.__breakpoints__ = breakpoints
                
                console.__i__ = -1

                old_input = input
                
                def debugger_input(prompt):
                    
                    try:
                        console
                    except:
                        import console
                    
                    if not console.__are_breakpoints_set__:
                    
                        breakpoints = console.__breakpoints__
                        console.__i__ += 1
                    
                        if len(breakpoints) < console.__i__:
                            console.__are_breakpoints_set__ = True
                            return ""
                    
                        try:
                            breakpoints[console.__i__+1]
                        except:
                            console.__are_breakpoints_set__ = True
                    
                        return "b "+str(breakpoints[console.__i__])
                    elif not console.__should_inspect__:
                        console.__should_inspect__ = True
                        return old_input(prompt)
                    else:
                        console.__should_inspect__ = False
                        return "from pyto import ConsoleViewController; from _get_variables_hierarchy import get_variables_hierarchy; ConsoleViewController.variables = get_variables_hierarchy(locals())"
                
                if len(breakpoints) > 0:
                    builtins.input = debugger_input
                
                pdb.main(["pdb", path])
                builtins.input = old_input
            else:
                spec.loader.exec_module(__script__)
        except SystemExit:
            pass
        except KeyboardInterrupt:
            pass
        except Exception as e:
            
            if __platform__ is iOS and not __isMainApp__() or replMode:
                print(traceback.format_exc())
                if not replMode:
                    Python.shared.fatalError(str(e))
            else:
                exc_type, exc_obj, exc_tb = sys.exc_info()
            
                extracts = traceback.extract_tb(sys.exc_info()[2])
                count = len(extracts)
            
                lineNumber = -1
            
                fileName = path
                for i, extract in enumerate(extracts):
                    if extract[0] == fileName:
                        lineNumber = extract[1]
                        break
                    count -= 1
            
                if (type(e) == SyntaxError): # The last word in a `SyntaxError` exception is the line number
                    lineNumber = [int(s) for s in (str(e)[:-1]).split() if s.isdigit()][-1]

                if __platform__ is iOS:
                    Python.shared.errorType = exc_type.__name__
                    Python.shared.errorReason = str(e)
                    EditorSplitViewController.visible.editor.showErrorAtLine(lineNumber)
                elif __platform__ is macOS:
                    print("Pyto.error_at_line;"+str(lineNumber)+";")

                error = traceback.format_exc(limit=-count)

                if __platform__ is iOS:
                    PyOutputHelper.printError(error)
                    sys.path.remove(directory)
                else:
                    sys.stderr.write(error+"\n")

                if debug:
                    pdb.post_mortem(exc_tb)

        if __platform__ is iOS and __isMainApp__():
            
            EditorViewController.runningLine = 0
            
            ReviewHelper.shared.launches = ReviewHelper.shared.launches+1
            ReviewHelper.shared.requestReview()

    thread = threading.Thread(target=run, args=())
    
    def inspector_loop():
        
        time.sleep(1)
        while thread.isAlive():
            if __platform__ is iOS and not ConsoleViewController.isMainLoopRunning:
                try:
                    vars = get_variables_hierarchy(__script__)
                    if vars != ConsoleViewController.variables:
                        ConsoleViewController.variables = vars
                except:
                    pass
            time.sleep(1)

    def interruption_loop():
        while thread.isAlive():
            sys.__stdout__.write(str(Python.shared._isScriptRunning)+"\n")
            if not Python.shared._isScriptRunning or Python.shared._interrupt:
                target_tid = 0
                for tid, tobj in threading._active.items():
                    if tobj is thread:
                        found = True
                        target_tid = tid
                        break
        
                if Python.shared._interrupt:
                    stopit.async_raise(target_tid, KeyboardInterrupt)
                elif not Python.shared._isScriptRunning:
                    stopit.async_raise(target_tid, SystemExit)
        
                Python.shared._interrupt = False
        
                if thread.isAlive:
                    Python.shared._isScriptRunning = True
            
            time.sleep(1)
    
    if __platform__ is iOS:
        Python.shared.isScriptRunning = True
        Python.shared._isScriptRunning = True

    if __host__ is not widget and __platform__ is iOS and not debug:
        inspector_thread = threading.Thread(target=inspector_loop, args=())
        inspector_thread.start()

    #interruption_thread = threading.Thread(target=interruption_loop, args=())
    #interruption_thread.start()

    if __platform__ is iOS:
        thread.start()
    else:
        run()

    if __host__ is not widget and __platform__ is iOS:
        interruption_loop()

    if __platform__ is iOS:
        sys.__stdout__.write("Execution finished\n")

    try:
        vars = get_variables_hierarchy(__script__)
        if __platform__ is iOS:
            if vars != ConsoleViewController.variables:
                ConsoleViewController.variables = vars
        elif __platform__ is macOS:
            # TODO: Mac support
            pass
    except:
        pass
    
    if __platform__ is iOS:
        Python.shared._isScriptRunning = False
        Python.shared.isScriptRunning = False

    return __script__

# MARK: - I/O

ignoredThreads = []
"""
All output and input request from these threads will be ignored.
"""

def __console__():
    if __platform__ is iOS:
        return ConsoleViewController.visible
    else:
        return

def clear():
    """
    Clears the console.
    """
    
    if threading.current_thread() in ignoredThreads:
        return
    
    if __platform__ is macOS:
        print("Pyto.console.clear")
    else:
        __console__().clear()
    time.sleep(0.1)

if __platform__ is iOS:
    __PyInputHelper__ = PyInputHelper

def input(prompt=""):
    """
    Requests input with given prompt.
        
    Args:
        prompt: Text printed before the user's input without a newline.
    """
    
    if __host__ is widget:
        return None
    
    NSBundle = ObjCClass("NSBundle")
    if NSBundle.mainBundle.bundlePath.pathExtension == "appex":
        return None
    
    if __platform__ is iOS:
        if not __isREPLAskingForInput__ and ConsoleViewController.visible != None:
            ConsoleViewController.visible.suggestions = []
            ConsoleViewController.visible.completions = []
    
    __PyInputHelper__.userInput = None
    
    __PyInputHelper__.showAlertWithPrompt(prompt)
    
    while __PyInputHelper__.userInput == None or threading.currentThread() in ignoredThreads:
        continue
    
    userInput = __PyInputHelper__.userInput
    __PyInputHelper__.userInput = None
    
    return str(userInput)

if __platform__ is iOS:
    __PyOutputHelper__ = PyOutputHelper

    def print(*objects, sep=None, end=None):
        """
        Prints to the Pyto console, not to the stdout. Works as the builtin `print` function but does not support printing to a custom file. Pyto catches by default the stdout and the stderr, so use the builtin function instead. This function is mainly for internal use.
        """
        if sep is None:
            sep = ' '
        if end is None:
            end = '\n'
        array = map(str, objects)
        __PyOutputHelper__.print(sep.join(array)+end)

# MARK: - Alerts

if __host__ is not widget:

    if __platform__ is iOS:
        PyAlert = PyAlert
        """
        A class representing an alert.

        Example:

            alert = console.Alert.alertWithTitle("Hello", message="Hello World!")
            alert.addAction("Ok")
            alert.addCancelAction("Cancel")
            if (alert.show() == "Ok"):
            print("Good Bye!")
        """

    class Alert:
        """
        A wrapper of `PyAlert` Objective-C class on iOS and `NSAlert` on macOS that represents an UI alert.
        """
        
        if __platform__ is iOS:
            pyAlert = None
            """
            The Objective-C representation.
            """
        elif __platform__ is macOS:
            nsAlert = None
            """
            The Objective-C representation.
            """
        
        def __init__(self):
            
            if __platform__ is iOS:
                self.pyAlert = PyAlert.alloc().init()
            elif __platform__ is macOS:
                from ctypes import cdll
                cdll.LoadLibrary("/System/Library/Frameworks/Cocoa.framework/Versions/Current/Cocoa")
                self.nsAlert = ObjCClass("NSAlert").alloc().init()
                
        @staticmethod
        def alertWithTitle(title, message):
            """
            Creates an alert.
            
            Args:
                title: The title of the alert.
                message: The message of the alert.
            """
            
            alert = Alert()
            if __platform__ is iOS:
                alert.pyAlert.title = title
                alert.pyAlert.message = message
            elif __platform__ is macOS:
                alert.nsAlert.informativeText = title
                alert.nsAlert.messageText = message
            return alert
                
        __actions__ = []

        def addAction(self, title):
            """
            Add an action with given title.
            
            Args:
                title: The title of the action.
            """
            
            if __platform__ is iOS:
                self.pyAlert.addAction(title)
            else:
                self.nsAlert.addButtonWithTitle(title)
                self.__actions__.append(title)
        
        def addDestructiveAction(self, title):
            """
            Add a destructive action with given title.
                
            Args:
                title: The title of the action.
            """
            
            if __platform__ is macOS:
                raise NotImplementedError("`addDestructiveAction` cannot be used on macOS. Use `addAction`.")
            
            self.pyAlert.addDestructiveAction(title)

        def addCancelAction(self, title):
            """
            Add a cancel action with given title. Can only added once.
                
            Args:
                title: The title of the action.
            """
            
            if __platform__ is macOS:
                raise NotImplementedError("`addCancelAction` cannot be used on macOS. Use `addAction`.")
                    
            if not self.pyAlert.addCancelAction(title):
                raise ValueError("There is already a cancel action.")
                
        def show(self):
            """
            Shows alert.
            
            Returns the title of the selected action.
            """
            
            if __platform__ is iOS:
                return self.pyAlert._show()
            elif __platform__ is macOS:
                return self.__actions__[self.nsAlert.runModal()-1000]

else:
    PyAlert = None
    Alert = None
