# basic module browser.

# usage:
# >>> import browser
# >>> browser.Browse()
# or
# >>> browser.Browse(your_module)
import sys
import types

import __main__
import win32ui
from pywin.mfc import dialog

from . import hierlist

special_names = ["__doc__", "__name__", "__self__"]


#
# HierList items
class HLIPythonObject(hierlist.HierListItem):
    def __init__(self, myobject=None, name=None):
        hierlist.HierListItem.__init__(self)
        self.myobject = myobject
        self.knownExpandable = None
        if name:
            self.name = name
        else:
            try:
                self.name = myobject.__name__
            except (AttributeError, TypeError):
                try:
                    r = repr(myobject)
                    if len(r) > 20:
                        r = r[:20] + "..."
                    self.name = r
                except (AttributeError, TypeError):
                    self.name = "???"

    def __lt__(self, other):
        return self.name < other.name

    def __eq__(self, other):
        return self.name == other.name

    def __repr__(self):
        return (
            f"{self.__class__.__name__}(name={self.name!r}, object={self.myobject!r})"
        )

    def GetText(self):
        try:
            return f"{self.name} ({self.GetHLIType()})"
        except AttributeError:
            return f"{self.name} = {self.myobject!r}"

    def InsertDocString(self, lst):
        ob = None
        try:
            ob = self.myobject.__doc__
        except (AttributeError, TypeError):
            pass
        # I don't quite grok descriptors enough to know how to
        # best hook them up. Eg:
        # >>> object.__getattribute__.__class__.__doc__
        # <attribute '__doc__' of 'wrapper_descriptor' objects>
        if ob and isinstance(ob, str):
            lst.insert(0, HLIDocString(ob, "Doc"))

    def GetSubList(self):
        ret = []
        try:
            for key, ob in self.myobject.__dict__.items():
                if key not in special_names:
                    ret.append(MakeHLI(ob, key))
        except (AttributeError, TypeError):
            pass
        try:
            for name in self.myobject.__methods__:
                ret.append(HLIMethod(name))  # no MakeHLI, as can't auto detect
        except (AttributeError, TypeError):
            pass
        try:
            for member in self.myobject.__members__:
                if not member in special_names:
                    ret.append(MakeHLI(getattr(self.myobject, member), member))
        except (AttributeError, TypeError):
            pass
        ret.sort()
        self.InsertDocString(ret)
        return ret

    # if the has a dict, it is expandable.
    def IsExpandable(self):
        if self.knownExpandable is None:
            self.knownExpandable = self.CalculateIsExpandable()
        return self.knownExpandable

    def CalculateIsExpandable(self):
        if hasattr(self.myobject, "__doc__"):
            return 1
        try:
            for key in self.myobject.__dict__:
                if key not in special_names:
                    return 1
        except (AttributeError, TypeError):
            pass
        try:
            self.myobject.__methods__
            return 1
        except (AttributeError, TypeError):
            pass
        try:
            for item in self.myobject.__members__:
                if item not in special_names:
                    return 1
        except (AttributeError, TypeError):
            pass
        return 0

    def GetBitmapColumn(self):
        if self.IsExpandable():
            return 0
        else:
            return 4

    def TakeDefaultAction(self):
        ShowObject(self.myobject, self.name)


class HLIDocString(HLIPythonObject):
    def GetHLIType(self):
        return "DocString"

    def GetText(self):
        return self.myobject.strip()

    def IsExpandable(self):
        return 0

    def GetBitmapColumn(self):
        return 6


class HLIModule(HLIPythonObject):
    def GetHLIType(self):
        return "Module"


class HLIFrame(HLIPythonObject):
    def GetHLIType(self):
        return "Stack Frame"


class HLITraceback(HLIPythonObject):
    def GetHLIType(self):
        return "Traceback"


class HLIClass(HLIPythonObject):
    def GetHLIType(self):
        return "Class"

    def GetSubList(self):
        ret = []
        for base in self.myobject.__bases__:
            ret.append(MakeHLI(base, "Base class: " + base.__name__))
        ret.extend(HLIPythonObject.GetSubList(self))
        return ret


class HLIMethod(HLIPythonObject):
    # myobject is just a string for methods.
    def GetHLIType(self):
        return "Method"

    def GetText(self):
        return "Method: " + self.myobject + "()"


class HLICode(HLIPythonObject):
    def GetHLIType(self):
        return "Code"

    def IsExpandable(self):
        return self.myobject

    def GetSubList(self):
        ret = []
        ret.append(MakeHLI(self.myobject.co_consts, "Constants (co_consts)"))
        ret.append(MakeHLI(self.myobject.co_names, "Names (co_names)"))
        ret.append(MakeHLI(self.myobject.co_filename, "Filename (co_filename)"))
        ret.append(MakeHLI(self.myobject.co_argcount, "Number of args (co_argcount)"))
        ret.append(MakeHLI(self.myobject.co_varnames, "Param names (co_varnames)"))

        return ret


class HLIInstance(HLIPythonObject):
    def GetHLIType(self):
        return "Instance"

    def GetText(self):
        return (
            str(self.name)
            + " (Instance of class "
            + str(self.myobject.__class__.__name__)
            + ")"
        )

    def IsExpandable(self):
        return 1

    def GetSubList(self):
        ret = []
        ret.append(MakeHLI(self.myobject.__class__))
        ret.extend(HLIPythonObject.GetSubList(self))
        return ret


class HLIBuiltinFunction(HLIPythonObject):
    def GetHLIType(self):
        return "Builtin Function"


class HLIFunction(HLIPythonObject):
    def GetHLIType(self):
        return "Function"

    def IsExpandable(self):
        return 1

    def GetSubList(self):
        ret = [
            MakeHLI(self.myobject.__code__, "Code"),
            MakeHLI(self.myobject.__globals__, "Globals"),
        ]
        self.InsertDocString(ret)
        return ret


class HLISeq(HLIPythonObject):
    def GetHLIType(self):
        return "Sequence (abstract!)"

    def IsExpandable(self):
        return len(self.myobject) > 0

    def GetSubList(self):
        ret = []
        pos = 0
        for item in self.myobject:
            ret.append(MakeHLI(item, f"[{pos}]"))
            pos += 1
        self.InsertDocString(ret)
        return ret


class HLIList(HLISeq):
    def GetHLIType(self):
        return "List"


class HLITuple(HLISeq):
    def GetHLIType(self):
        return "Tuple"


class HLIDict(HLIPythonObject):
    def GetHLIType(self):
        return "Dict"

    def IsExpandable(self):
        try:
            self.myobject.__doc__
            return 1
        except (AttributeError, TypeError):
            return len(self.myobject) > 0

    def GetSubList(self):
        ret = [MakeHLI(self.myobject[key], str(key)) for key in sorted(self.myobject)]
        self.InsertDocString(ret)
        return ret


# strings and Unicode have builtin methods, but we don't really want to see these
class HLIString(HLIPythonObject):
    def IsExpandable(self):
        return 0


TypeMap = {
    type: HLIClass,
    types.FunctionType: HLIFunction,
    tuple: HLITuple,
    dict: HLIDict,
    list: HLIList,
    types.ModuleType: HLIModule,
    types.CodeType: HLICode,
    types.BuiltinFunctionType: HLIBuiltinFunction,
    types.FrameType: HLIFrame,
    types.TracebackType: HLITraceback,
    str: HLIString,
    int: HLIPythonObject,
    bool: HLIPythonObject,
    float: HLIPythonObject,
}


def MakeHLI(ob, name=None):
    try:
        cls = TypeMap[type(ob)]
    except KeyError:
        # hrmph - this check gets more and more bogus as Python
        # improves.  It's possible we should just *always* use
        # HLIInstance?
        if hasattr(ob, "__class__"):  # 'new style' class
            cls = HLIInstance
        else:
            cls = HLIPythonObject
    return cls(ob, name)


#########################################
#
# Dialog related.


class DialogShowObject(dialog.Dialog):
    def __init__(self, object, title):
        self.object = object
        self.title = title
        dialog.Dialog.__init__(self, win32ui.IDD_LARGE_EDIT)

    def OnInitDialog(self):
        import re

        self.SetWindowText(self.title)
        self.edit = self.GetDlgItem(win32ui.IDC_EDIT1)
        try:
            strval = str(self.object)
        except:
            t, v, tb = sys.exc_info()
            strval = f"Exception getting object value\n\n{t}:{v}"
            tb = None
        strval = re.sub(r"\n", "\r\n", strval)
        self.edit.ReplaceSel(strval)


def ShowObject(object, title):
    dlg = DialogShowObject(object, title)
    dlg.DoModal()


# And some mods for a sizable dialog from Sam Rushing!
import commctrl
import win32api
import win32con


class dynamic_browser(dialog.Dialog):
    style = win32con.WS_OVERLAPPEDWINDOW | win32con.WS_VISIBLE
    cs = (
        win32con.WS_CHILD
        | win32con.WS_VISIBLE
        | commctrl.TVS_HASLINES
        | commctrl.TVS_LINESATROOT
        | commctrl.TVS_HASBUTTONS
    )

    dt = [
        ["Python Object Browser", (0, 0, 200, 200), style, None, (8, "MS Sans Serif")],
        ["SysTreeView32", None, win32ui.IDC_LIST1, (0, 0, 200, 200), cs],
    ]

    def __init__(self, hli_root):
        dialog.Dialog.__init__(self, self.dt)
        self.hier_list = hierlist.HierListWithItems(hli_root, win32ui.IDB_BROWSER_HIER)
        self.HookMessage(self.on_size, win32con.WM_SIZE)

    def OnInitDialog(self):
        self.hier_list.HierInit(self)
        return dialog.Dialog.OnInitDialog(self)

    def OnOK(self):
        self.hier_list.HierTerm()
        self.hier_list = None
        return self._obj_.OnOK()

    def OnCancel(self):
        self.hier_list.HierTerm()
        self.hier_list = None
        return self._obj_.OnCancel()

    def on_size(self, params):
        lparam = params[3]
        w = win32api.LOWORD(lparam)
        h = win32api.HIWORD(lparam)
        self.GetDlgItem(win32ui.IDC_LIST1).MoveWindow((0, 0, w, h))


def Browse(ob=__main__):
    "Browse the argument, or the main dictionary"
    root = MakeHLI(ob, "root")
    if not root.IsExpandable():
        raise TypeError(
            "Browse() argument must have __dict__ attribute, or be a Browser supported type"
        )

    dlg = dynamic_browser(root)
    dlg.CreateWindow()
    return dlg


#
#
# Classes for using the browser in an MDI window, rather than a dialog
#
from pywin.mfc import docview


class BrowserTemplate(docview.DocTemplate):
    def __init__(self):
        docview.DocTemplate.__init__(
            self, win32ui.IDR_PYTHONTYPE, BrowserDocument, None, BrowserView
        )

    def OpenObject(self, root):  # Use this instead of OpenDocumentFile.
        # Look for existing open document
        for doc in self.GetDocumentList():
            if doc.root == root:
                doc.GetFirstView().ActivateFrame()
                return doc
        # not found - new one.
        doc = BrowserDocument(self, root)
        frame = self.CreateNewFrame(doc)
        doc.OnNewDocument()
        self.InitialUpdateFrame(frame, doc, 1)
        return doc


class BrowserDocument(docview.Document):
    def __init__(self, template, root):
        docview.Document.__init__(self, template)
        self.root = root
        self.SetTitle("Browser: " + root.name)

    def OnOpenDocument(self, name):
        raise TypeError("This template can not open files")
        return 0


class BrowserView(docview.TreeView):
    def OnInitialUpdate(self):
        import commctrl

        rc = self._obj_.OnInitialUpdate()
        list = hierlist.HierListWithItems(
            self.GetDocument().root,
            win32ui.IDB_BROWSER_HIER,
            win32ui.AFX_IDW_PANE_FIRST,
        )
        list.HierInit(self.GetParent())
        list.SetStyle(
            commctrl.TVS_HASLINES | commctrl.TVS_LINESATROOT | commctrl.TVS_HASBUTTONS
        )
        return rc


template = None


def MakeTemplate():
    global template
    if template is None:
        template = (
            BrowserTemplate()
        )  # win32ui.IDR_PYTHONTYPE, BrowserDocument, None, BrowserView)


def BrowseMDI(ob=__main__):
    """Browse an object using an MDI window."""

    MakeTemplate()
    root = MakeHLI(ob, repr(ob))
    if not root.IsExpandable():
        raise TypeError(
            "Browse() argument must have __dict__ attribute, or be a Browser supported type"
        )

    template.OpenObject(root)
