# hierlist
#
# IMPORTANT - Please read before using.

# This module exposes an API for a Hierarchical Tree Control.
# Previously, a custom tree control was included in Pythonwin which
# has an API very similar to this.

# The current control used is the common "Tree Control".  This module exists now
# to provide an API similar to the old control, but for the new Tree control.

# If you need to use the Tree Control, you may still find this API a reasonable
# choice.  However, you should investigate using the tree control directly
# to provide maximum flexibility (but with extra work).
from __future__ import annotations

import commctrl
import win32api
import win32con
import win32ui
from pywin.mfc import dialog, object
from win32api import RGB


# helper to get the text of an arbitary item
def GetItemText(item):
    if isinstance(item, (tuple, list)):
        use = item[0]
    else:
        use = item
    if isinstance(use, str):
        return use
    else:
        return repr(item)


class HierDialog(dialog.Dialog):
    def __init__(
        self,
        title,
        hierList,
        bitmapID=win32ui.IDB_HIERFOLDERS,
        dlgID=win32ui.IDD_TREE,
        dll=None,
        childListBoxID=win32ui.IDC_LIST1,
    ):
        dialog.Dialog.__init__(self, dlgID, dll)  # reuse this dialog.
        self.hierList = hierList
        self.dlgID = dlgID
        self.title = title

    # 		self.childListBoxID = childListBoxID
    def OnInitDialog(self):
        self.SetWindowText(self.title)
        self.hierList.HierInit(self)
        return dialog.Dialog.OnInitDialog(self)


class HierList(object.Object):
    def __init__(
        self, root, bitmapID=win32ui.IDB_HIERFOLDERS, listBoxId=None, bitmapMask=None
    ):  # used to create object.
        self.listControl = None
        self.bitmapID = bitmapID
        self.root = root
        self.listBoxId = listBoxId
        self.itemHandleMap = {}
        self.filledItemHandlesMap = {}
        self.bitmapMask = bitmapMask

    def __getattr__(self, attr):
        try:
            return getattr(self.listControl, attr)
        except AttributeError:
            return object.Object.__getattr__(self, attr)

    def ItemFromHandle(self, handle):
        return self.itemHandleMap[handle]

    def SetStyle(self, newStyle):
        hwnd = self.listControl.GetSafeHwnd()
        style = win32api.GetWindowLong(hwnd, win32con.GWL_STYLE)
        win32api.SetWindowLong(hwnd, win32con.GWL_STYLE, (style | newStyle))

    def HierInit(self, parent, listControl=None):  # Used when window first exists.
        # this also calls "Create" on the listbox.
        # params - id of listbbox, ID of bitmap, size of bitmaps
        if self.bitmapMask is None:
            bitmapMask = RGB(0, 0, 255)
        else:
            bitmapMask = self.bitmapMask
        self.imageList = win32ui.CreateImageList(self.bitmapID, 16, 0, bitmapMask)
        if listControl is None:
            if self.listBoxId is None:
                self.listBoxId = win32ui.IDC_LIST1
            self.listControl = parent.GetDlgItem(self.listBoxId)
        else:
            self.listControl = listControl
            lbid = listControl.GetDlgCtrlID()
            assert (
                self.listBoxId is None or self.listBoxId == lbid
            ), "An invalid listbox control ID has been specified (specified as {}, but exists as {})".format(
                self.listBoxId, lbid
            )
            self.listBoxId = lbid
        self.listControl.SetImageList(self.imageList, commctrl.LVSIL_NORMAL)
        # 		self.list.AttachObject(self)

        parent.HookNotify(self.OnTreeItemExpanding, commctrl.TVN_ITEMEXPANDINGW)
        parent.HookNotify(self.OnTreeItemSelChanged, commctrl.TVN_SELCHANGEDW)
        parent.HookNotify(self.OnTreeItemDoubleClick, commctrl.NM_DBLCLK)
        self.notify_parent = parent

        if self.root:
            self.AcceptRoot(self.root)

    def DeleteAllItems(self):
        self.listControl.DeleteAllItems()
        self.root = None
        self.itemHandleMap = {}
        self.filledItemHandlesMap = {}

    def HierTerm(self):
        # Don't want notifies as we kill the list.
        parent = self.notify_parent  # GetParentFrame()
        parent.HookNotify(None, commctrl.TVN_ITEMEXPANDINGW)
        parent.HookNotify(None, commctrl.TVN_SELCHANGEDW)
        parent.HookNotify(None, commctrl.NM_DBLCLK)

        self.DeleteAllItems()
        self.listControl = None
        self.notify_parent = None  # Break a possible cycle

    def OnTreeItemDoubleClick(self, info, extra):
        (hwndFrom, idFrom, code) = info
        if idFrom != self.listBoxId:
            return None
        item = self.itemHandleMap[self.listControl.GetSelectedItem()]
        self.TakeDefaultAction(item)
        return 1

    def OnTreeItemExpanding(self, info, extra):
        (hwndFrom, idFrom, code) = info
        if idFrom != self.listBoxId:
            return None
        action, itemOld, itemNew, pt = extra
        itemHandle = itemNew[0]
        if itemHandle not in self.filledItemHandlesMap:
            item = self.itemHandleMap[itemHandle]
            self.AddSubList(itemHandle, self.GetSubList(item))
            self.filledItemHandlesMap[itemHandle] = None
        return 0

    def OnTreeItemSelChanged(self, info, extra):
        (hwndFrom, idFrom, code) = info
        if idFrom != self.listBoxId:
            return None
        action, itemOld, itemNew, pt = extra
        itemHandle = itemNew[0]
        item = self.itemHandleMap[itemHandle]
        self.PerformItemSelected(item)
        return 1

    def AddSubList(self, parentHandle, subItems):
        for item in subItems:
            self.AddItem(parentHandle, item)

    def AddItem(self, parentHandle, item, hInsertAfter=commctrl.TVI_LAST):
        text = self.GetText(item)
        if self.IsExpandable(item):
            cItems = 1  # Trick it !!
        else:
            cItems = 0
        bitmapCol = self.GetBitmapColumn(item)
        bitmapSel = self.GetSelectedBitmapColumn(item)
        if bitmapSel is None:
            bitmapSel = bitmapCol
        hitem = self.listControl.InsertItem(
            parentHandle,
            hInsertAfter,
            (None, None, None, text, bitmapCol, bitmapSel, cItems, 0),
        )
        self.itemHandleMap[hitem] = item
        return hitem

    def _GetChildHandles(self, handle):
        ret = []
        try:
            handle = self.listControl.GetChildItem(handle)
            while 1:
                ret.append(handle)
                handle = self.listControl.GetNextItem(handle, commctrl.TVGN_NEXT)
        except win32ui.error:
            # out of children
            pass
        return ret

    def Refresh(self, hparent=None):
        # Attempt to refresh the given item's sub-entries, but maintain the tree state
        # (ie, the selected item, expanded items, etc)
        if hparent is None:
            hparent = commctrl.TVI_ROOT
        if hparent not in self.filledItemHandlesMap:
            # This item has never been expanded, so no refresh can possibly be required.
            return
        root_item = self.itemHandleMap[hparent]
        old_handles = self._GetChildHandles(hparent)
        old_items = list(map(self.ItemFromHandle, old_handles))
        new_items = self.GetSubList(root_item)
        # Now an inefficient technique for synching the items.
        inew = 0
        hAfter = commctrl.TVI_FIRST
        for iold in range(len(old_items)):
            inewlook = inew
            matched = 0
            while inewlook < len(new_items):
                if old_items[iold] == new_items[inewlook]:
                    matched = 1
                    break
                inewlook += 1
            if matched:
                # Insert the new items.
                # print("Inserting after", old_items[iold], old_handles[iold])
                for i in range(inew, inewlook):
                    # print(f"Inserting index {i} ({new_items[i]})")
                    hAfter = self.AddItem(hparent, new_items[i], hAfter)

                inew = inewlook + 1
                # And recursively refresh iold
                hold = old_handles[iold]
                if hold in self.filledItemHandlesMap:
                    self.Refresh(hold)
            else:
                # Remove the deleted items.
                # print(f"Deleting {iold} ({old_items[iold]})")
                hdelete = old_handles[iold]
                # First recurse and remove the children from the map.
                for hchild in self._GetChildHandles(hdelete):
                    del self.itemHandleMap[hchild]
                    if hchild in self.filledItemHandlesMap:
                        del self.filledItemHandlesMap[hchild]
                self.listControl.DeleteItem(hdelete)
            hAfter = old_handles[iold]
        # Fill any remaining new items:
        for newItem in new_items[inew:]:
            # print("Inserting new item", newItem)
            self.AddItem(hparent, newItem)

    def AcceptRoot(self, root):
        self.listControl.DeleteAllItems()
        self.itemHandleMap = {commctrl.TVI_ROOT: root}
        self.filledItemHandlesMap = {commctrl.TVI_ROOT: root}
        subItems = self.GetSubList(root)
        self.AddSubList(0, subItems)

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

    def GetSelectedBitmapColumn(self, item) -> int | None:
        return 0

    def CheckChangedChildren(self):
        return self.listControl.CheckChangedChildren()

    def GetText(self, item):
        return GetItemText(item)

    def PerformItemSelected(self, item):
        try:
            win32ui.SetStatusText("Selected " + self.GetText(item))
        except win32ui.error:  # No status bar!
            pass

    def TakeDefaultAction(self, item):
        win32ui.MessageBox("Got item " + self.GetText(item))


##########################################################################
#
# Classes for use with seperate HierListItems.
#
#
class HierListWithItems(HierList):
    def __init__(
        self, root, bitmapID=win32ui.IDB_HIERFOLDERS, listBoxID=None, bitmapMask=None
    ):  # used to create object.
        HierList.__init__(self, root, bitmapID, listBoxID, bitmapMask)

    def DelegateCall(self, fn):
        return fn()

    def GetBitmapColumn(self, item):
        rc = self.DelegateCall(item.GetBitmapColumn)
        if rc is None:
            rc = HierList.GetBitmapColumn(self, item)
        return rc

    def GetSelectedBitmapColumn(self, item):
        return self.DelegateCall(item.GetSelectedBitmapColumn)

    def IsExpandable(self, item):
        return self.DelegateCall(item.IsExpandable)

    def GetText(self, item):
        return self.DelegateCall(item.GetText)

    def GetSubList(self, item):
        return self.DelegateCall(item.GetSubList)

    def PerformItemSelected(self, item):
        func = getattr(item, "PerformItemSelected", None)
        if func is None:
            return HierList.PerformItemSelected(self, item)
        else:
            return self.DelegateCall(func)

    def TakeDefaultAction(self, item):
        func = getattr(item, "TakeDefaultAction", None)
        if func is None:
            return HierList.TakeDefaultAction(self, item)
        else:
            return self.DelegateCall(func)


# A hier list item - for use with a HierListWithItems
class HierListItem:
    def __init__(self):
        pass

    def GetText(self):
        pass

    def GetSubList(self):
        pass

    def IsExpandable(self):
        pass

    def GetBitmapColumn(self):
        return None  # indicate he should do it.

    def GetSelectedBitmapColumn(self):
        return None  # same as other

    def __lt__(self, other):
        # we want unrelated items to be sortable...
        return id(self) < id(other)

    def __eq__(self, other):
        return False
