# -*- coding: utf-8 -*-
# Copyright (C) 2016, the Pyzo development team
#
# Pyzo is distributed under the terms of the 2-Clause BSD License.
# The full license can be found in 'license.txt'.

import sys
import yoton
import inspect  # noqa - used in eval()


try:
    import thread  # Python 2
except ImportError:
    import _thread as thread  # Python 3


class PyzoIntrospector(yoton.RepChannel):
    """This is a RepChannel object that runs a thread to respond to
    requests from the IDE.
    """

    def _getNameSpace(self, name=""):
        """_getNameSpace(name='')

        Get the namespace to apply introspection in.
        If name is given, will find that name. For example sys.stdin.

        """

        # Get namespace
        NS1 = sys._pyzoInterpreter.locals
        NS2 = sys._pyzoInterpreter.globals
        if not NS2:
            NS = NS1
        else:
            NS = NS2.copy()
            NS.update(NS1)

        # Look up a name?
        if not name:
            return NS
        else:
            try:
                # Get object
                ob = eval(name, None, NS)

                # Get namespace for this object
                if isinstance(ob, dict):
                    NS = {}
                    for el in ob:
                        NS["[" + repr(el) + "]"] = ob[el]
                elif isinstance(ob, (list, tuple)):
                    NS = {}
                    count = -1
                    for el in ob:
                        count += 1
                        NS["[%i]" % count] = el
                else:
                    keys = dir(ob)
                    NS = {}
                    for key in keys:
                        try:
                            NS[key] = getattr(ob, key)
                        except Exception:
                            NS[key] = "<unknown>"

                # Done
                return NS

            except Exception:
                return {}

    def _getSignature(self, objectName):
        """_getSignature(objectName)

        Get the signature of builtin, function or method.
        Returns a tuple (signature_string, kind), where kind is a string
        of one of the above. When none of the above, both elements in
        the tuple are an empty string.

        """

        # if a class, get init
        # not if an instance! -> try __call__ instead
        # what about self?

        # Get valid object names
        parts = objectName.rsplit(".")
        objectNames = [".".join(parts[-i:]) for i in range(1, len(parts) + 1)]

        # find out what kind of function, or if a function at all!
        NS = self._getNameSpace()
        fun1 = eval("inspect.isbuiltin(%s)" % (objectName), None, NS)
        fun2 = eval("inspect.isfunction(%s)" % (objectName), None, NS)
        fun3 = eval("inspect.ismethod(%s)" % (objectName), None, NS)
        fun4 = False
        fun5 = False
        if not (fun1 or fun2 or fun3):
            # Maybe it's a class with an init?
            if eval("hasattr(%s,'__init__')" % (objectName), None, NS):
                objectName += ".__init__"
                fun4 = eval("inspect.ismethod(%s)" % (objectName), None, NS)
            #  Or a callable object?
            elif eval("hasattr(%s,'__call__')" % (objectName), None, NS):
                objectName += ".__call__"
                fun5 = eval("inspect.ismethod(%s)" % (objectName), None, NS)

        sigs = ""
        if True:
            # the first line in the docstring is usually the signature
            tmp = eval("%s.__doc__" % (objectNames[-1]), {}, NS)
            sigs = ""
            if tmp:
                sigs = tmp.splitlines()[0].strip()
            # Test if doc has signature
            hasSig = False
            for name in objectNames:  # list.append -> L.apend(objec) -- blabla
                name += "("
                if name in sigs:
                    hasSig = True
            # If not a valid signature, do not bother ...
            if (not hasSig) or (sigs.count("(") != sigs.count(")")):
                sigs = ""

        if fun1 or fun2 or fun3 or fun4 or fun5:
            if fun1:
                kind = "builtin"
            elif fun2:
                kind = "function"
            elif fun3:
                kind = "method"
            elif fun4:
                kind = "class"
            elif fun5:
                kind = "callable"

            if not sigs:
                # Use intospection

                funname = objectName.split(".")[-1]

                try:
                    tmp = eval(
                        "inspect.signature(%s)" % (objectName), None, NS
                    )  # py3.3
                    sigs = funname + str(tmp)
                except Exception:
                    try:
                        tmp = eval(
                            "inspect.getargspec(%s)" % (objectName), None, NS
                        )  # py2
                    except Exception:  # the above fails on 2.4 (+?) for builtins
                        tmp = None
                        kind = ""

                    if tmp is not None:
                        args, varargs, varkw, defaults = tmp[:4]
                        # prepare defaults
                        if defaults is None:
                            defaults = ()
                        defaults = list(defaults)
                        defaults.reverse()
                        # make list (back to forth)
                        args2 = []
                        for i in range(len(args) - fun4):
                            arg = args.pop()
                            if i < len(defaults):
                                args2.insert(0, "%s=%s" % (arg, defaults[i]))
                            else:
                                args2.insert(0, arg)
                        # append varargs and kwargs
                        if varargs:
                            args2.append("*" + varargs)
                        if varkw:
                            args2.append("**" + varkw)
                        # append the lot to our  string
                        sigs = "%s(%s)" % (funname, ", ".join(args2))

        elif sigs:
            kind = "function"
        else:
            sigs = ""
            kind = ""

        return sigs, kind

    # todo: variant that also says whether it's a property/function/class/other
    def dir(self, objectName):
        """dir(objectName)

        Get list of attributes for the given name.

        """
        # sys.__stdout__.write('handling '+objectName+'\n')
        # sys.__stdout__.flush()

        # Get namespace
        NS = self._getNameSpace()

        # Init names
        names = set()

        # Obtain all attributes of the class
        try:
            command = "dir(%s.__class__)" % (objectName)
            d = eval(command, {}, NS)
        except Exception:
            pass
        else:
            names.update(d)

        # Obtain instance attributes
        try:
            command = "%s.__dict__.keys()" % (objectName)
            d = eval(command, {}, NS)
        except Exception:
            pass
        else:
            names.update(d)

        # That should be enough, but in case __dir__ is overloaded,
        # query that as well
        try:
            command = "dir(%s)" % (objectName)
            d = eval(command, {}, NS)
        except Exception:
            pass
        else:
            names.update(d)

        # Respond
        return list(names)

    def dir2(self, objectName):
        """dir2(objectName)

        Get variable names in currently active namespace plus extra information.
        Returns a list of tuple of strings: name, type, kind, repr.

        """
        try:
            name = ""
            names = []

            def storeInfo(name, val):
                # Determine type
                typeName = type(val).__name__
                # Determine kind
                kind = typeName
                if typeName != "type":
                    if (
                        hasattr(val, "__array__")
                        and hasattr(val, "dtype")
                        and hasattr(val, "shape")
                        and not hasattr(
                            val,
                            "if_this_is_an_attribute_then_there_are_likely_inf_attributes",
                        )
                    ):
                        kind = "array"
                    elif isinstance(val, list):
                        kind = "list"
                    elif isinstance(val, tuple):
                        kind = "tuple"
                # Determine representation
                if kind == "array":
                    tmp = "x".join([str(s) for s in val.shape])
                    if tmp:
                        values_repr = ""
                        if hasattr(val, "flat"):
                            for el in val.flat:
                                values_repr += ", " + repr(el)
                                if len(values_repr) > 70:
                                    values_repr = values_repr[:67] + ", …"
                                    break
                        repres = "<array %s %s: %s>" % (
                            tmp,
                            val.dtype.name,
                            values_repr,
                        )
                    elif val.size:
                        tmp = str(float(val))
                        if "int" in val.dtype.name:
                            tmp = str(int(val))
                        repres = "<array scalar %s (%s)>" % (val.dtype.name, tmp)
                    else:
                        repres = "<array empty %s>" % (val.dtype.name)
                elif kind == "list":
                    values_repr = ""
                    for el in val:
                        values_repr += ", " + repr(el)
                        if len(values_repr) > 70:
                            values_repr = values_repr[:67] + ", …"
                            break
                    repres = "<%i-element list: %s>" % (len(val), values_repr)
                elif kind == "tuple":
                    values_repr = ""
                    for el in val:
                        values_repr += ", " + repr(el)
                        if len(values_repr) > 70:
                            values_repr = values_repr[:67] + ", …"
                            break
                    repres = "<%i-element tuple: %s>" % (len(val), values_repr)
                elif kind == "dict":
                    values_repr = ""
                    for k, v in val.items():
                        values_repr += ", " + repr(k) + ": " + repr(v)
                        if len(values_repr) > 70:
                            values_repr = values_repr[:67] + ", …"
                            break
                    repres = "<%i-item dict: %s>" % (len(val), values_repr)
                else:
                    repres = repr(val)
                    if len(repres) > 80:
                        repres = repres[:77] + "…"
                # Store
                tmp = (name, typeName, kind, repres)
                names.append(tmp)

            # Get locals
            NS = self._getNameSpace(objectName)
            for name in NS.keys():  # name can be a key in a dict, i.e. not str
                if hasattr(name, "startswith") and name.startswith("__"):
                    continue
                try:
                    storeInfo(str(name), NS[name])
                except Exception:
                    pass

            return names

        except Exception:
            return []

    def signature(self, objectName):
        """signature(objectName)

        Get signature.

        """
        try:
            text, kind = self._getSignature(objectName)
            return text
        except Exception:
            return None

    def doc(self, objectName):
        """doc(objectName)

        Get documentation for an object.

        """

        # Get namespace
        NS = self._getNameSpace()

        try:
            # collect docstring
            h_text = ""
            # Try using the class (for properties)
            try:
                className = eval("%s.__class__.__name__" % (objectName), {}, NS)
                if "." in objectName:
                    tmp = objectName.rsplit(".", 1)
                    tmp[1] += "."
                else:
                    tmp = [objectName, ""]
                if className not in [
                    "type",
                    "module",
                    "builtin_function_or_method",
                    "function",
                ]:
                    cmd = "%s.__class__.%s__doc__"
                    h_text = eval(cmd % (tmp[0], tmp[1]), {}, NS)
            except Exception:
                pass

            # Normal doc
            if not h_text:
                h_text = eval("%s.__doc__" % (objectName), {}, NS)

            # collect more data
            h_repr = eval("repr(%s)" % (objectName), {}, NS)
            try:
                h_class = eval("%s.__class__.__name__" % (objectName), {}, NS)
            except Exception:
                h_class = "unknown"

            # docstring can be None, but should be empty then
            if not h_text:
                h_text = ""

            # get and correct signature
            h_fun, kind = self._getSignature(objectName)

            if not h_fun:
                h_fun = ""  # signature not available

            # cut repr if too long
            if len(h_repr) > 200:
                h_repr = h_repr[:200] + "..."
            # replace newlines so we can separates the different parts
            h_repr = h_repr.replace("\n", "\r")

            # build final text
            text = "\n".join([objectName, h_class, h_fun, h_repr, h_text])

        except Exception:
            type, value, tb = sys.exc_info()
            del tb
            text = "\n".join(
                [objectName, "", "", "", "No help available. ", str(value)]
            )

        # Done
        return text

    def eval(self, command):
        """eval(command)

        Evaluate a command and return result.

        """

        # Get namespace
        NS = self._getNameSpace()

        try:
            # here globals is None, so we can look into sys, time, etc...
            return eval(command, None, NS)
        except Exception:
            return "Error evaluating: " + command

    def interrupt(self, command=None):
        """interrupt()

        Interrupt the main thread. This does not work if the main thread
        is running extension code.

        A bit of a hack to do this in the introspector, but it's the
        easeast way and prevents having to launch another thread just
        to wait for an interrupt/terminare command.

        Note that on POSIX we can send an OS INT signal, which is faster
        and maybe more effective in some situations.

        """
        thread.interrupt_main()

    def terminate(self, command=None):
        """terminate()

        Ask the kernel to terminate by closing the stdin.

        """
        sys.stdin._channel.close()
