/*==LICENSE==*

CyanWorlds.com Engine - MMOG client, server and tools
Copyright (C) 2011  Cyan Worlds, Inc.

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 3 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, see <http://www.gnu.org/licenses/>.

Additional permissions under GNU GPL version 3 section 7

If you modify this Program, or any covered work, by linking or
combining it with any of RAD Game Tools Bink SDK, Autodesk 3ds Max SDK,
NVIDIA PhysX SDK, Microsoft DirectX SDK, OpenSSL library, Independent
JPEG Group JPEG library, Microsoft Windows Media SDK, or Apple QuickTime SDK
(or a modified version of those libraries),
containing parts covered by the terms of the Bink SDK EULA, 3ds Max EULA,
PhysX SDK EULA, DirectX SDK EULA, OpenSSL and SSLeay licenses, IJG
JPEG Library README, Windows Media SDK EULA, or QuickTime SDK EULA, the
licensors of this Program grant you additional
permission to convey the resulting work. Corresponding Source for a
non-source form of such a combination shall include the source code for
the parts of OpenSSL and IJG JPEG Library used as well as that of the covered
work.

You can contact Cyan Worlds, Inc. by email legal@cyan.com
 or by snail mail at:
      Cyan Worlds, Inc.
      14617 N Newport Hwy
      Mead, WA   99021

*==LICENSE==*/

#include "HeadSpin.h"

#include "MaxAPI.h"

#include "MaxComponent/plComponentBase.h"
#include "MaxComponent/plComponentReg.h"
#include "plMaxNode.h"
#include "resource.h"

#include <algorithm>
#include <vector>

#include "plComponentDlg.h"
#include "plComponentPanel.h"
#include "plMaxAccelerators.h"

extern HINSTANCE hInstance;

plComponentDlg::plComponentDlg() : fhDlg(), fCompMenu(), fTypeMenu(), fCommentNode()
{
    fInterface = GetCOREInterface();

    RegisterNotification(INotify, nullptr, NOTIFY_FILE_PRE_OPEN);
    RegisterNotification(INotify, nullptr, NOTIFY_SYSTEM_PRE_NEW);
    RegisterNotification(INotify, nullptr, NOTIFY_SYSTEM_PRE_RESET);
    RegisterNotification(INotify, nullptr, NOTIFY_FILE_PRE_MERGE);
    RegisterNotification(INotify, nullptr, NOTIFY_PRE_IMPORT);
    RegisterNotification(INotify, nullptr, NOTIFY_FILE_PRE_SAVE);
    RegisterNotification(INotify, nullptr, NOTIFY_FILE_PRE_SAVE_OLD);

    RegisterNotification(INotify, nullptr, NOTIFY_FILE_POST_OPEN);
    RegisterNotification(INotify, nullptr, NOTIFY_SYSTEM_POST_NEW);
    RegisterNotification(INotify, nullptr, NOTIFY_SYSTEM_POST_RESET);
    RegisterNotification(INotify, nullptr, NOTIFY_FILE_POST_MERGE);
    RegisterNotification(INotify, nullptr, NOTIFY_POST_IMPORT);

    RegisterNotification(INotify, nullptr, NOTIFY_SYSTEM_SHUTDOWN);
}

plComponentDlg::~plComponentDlg()
{
    if (fCompMenu)
        DestroyMenu(fCompMenu);
    if (fTypeMenu)
        DestroyMenu(fTypeMenu);
}

plComponentDlg& plComponentDlg::Instance()
{
    static plComponentDlg theInstance;
    return theInstance;
}

void plComponentDlg::Open()
{
    if (!fhDlg)
    {
        fhDlg = CreateDialog(hInstance,
                            MAKEINTRESOURCE(IDD_COMP_MAIN),
                            GetCOREInterface()->GetMAXHWnd(),
                            ForwardDlgProc);

        GetWindowRect(fhDlg, &fLastRect);
        fSmallestSize.x = fLastRect.right - fLastRect.left;
        fSmallestSize.y = fLastRect.bottom - fLastRect.top;

        RECT rect;
        memcpy(&rect, &fLastRect, sizeof(RECT));
        rect.right = rect.left + 235;
        rect.bottom = rect.top + 335;
        IPositionControls(&rect, WMSZ_BOTTOM);
        SetWindowPos(fhDlg, nullptr, 0, 0, rect.right - rect.left, rect.bottom - rect.top, SWP_NOMOVE | SWP_NOZORDER);
    }
    
    ShowWindow(fhDlg, SW_SHOW);

    if (IsIconic(fhDlg))
        ShowWindow(fhDlg, SW_RESTORE);
}

void plComponentDlg::IPositionControls(RECT *newRect, int edge)
{
    // Get the new width and height
    int newW = newRect->right - newRect->left;
    int newH = newRect->bottom - newRect->top;

    // If an edge we don't support is being dragged, don't allow the resize.
    if (!(edge == WMSZ_BOTTOM ||
        edge == WMSZ_BOTTOMRIGHT ||
        edge == WMSZ_RIGHT))
    {
        memcpy(newRect, &fLastRect, sizeof(RECT));
        return;
    }

    // If the width or height is too small, set it to the minimum
    if (newW < fSmallestSize.x)
        newRect->right = newRect->left + fSmallestSize.x;
    if (newH < fSmallestSize.y)
        newRect->bottom = newRect->top + fSmallestSize.y;

    // Calculate the new width and height
    int hDiff = (newRect->bottom - newRect->top) - (fLastRect.bottom - fLastRect.top);
    int wDiff = (newRect->right - newRect->left) - (fLastRect.right - fLastRect.left);

    // Copy our new rect to the last rect
    memcpy(&fLastRect, newRect, sizeof(RECT));

    // If the size has changed, reposition and resize controls
    if (hDiff != 0 || wDiff != 0)
    {
        IPositionControl(GetDlgItem(fhDlg, IDC_TREE),           hDiff, wDiff, kResizeX | kResizeY);
        IPositionControl(GetDlgItem(fhDlg, IDC_COMMENT_TEXT),   hDiff);
        IPositionControl(GetDlgItem(fhDlg, IDC_COMMENTS),       hDiff, wDiff, kResizeX | kMoveY);
        IPositionControl(GetDlgItem(fhDlg, IDC_ATTACH),         hDiff);
    }

    InvalidateRect(fhDlg, nullptr, TRUE);
}

void plComponentDlg::IPositionControl(HWND hControl, int hDiff, int wDiff, int flags)
{
    RECT rect;
    GetWindowRect(hControl, &rect);

    hsAssert(!((flags & kMoveX) && (flags & kResizeX)), "Moving AND resizing in X in IPositionControl");
    hsAssert(!((flags & kMoveY) && (flags & kResizeY)), "Moving AND resizing in Y in IPositionControl");

    if (flags & kMoveX || flags & kMoveY)
    {
        POINT pos = { rect.left, rect.top };
        ScreenToClient(fhDlg, &pos);
        if (flags & kMoveX)
            pos.x += wDiff;
        if (flags & kMoveY)
            pos.y += hDiff;

        SetWindowPos(hControl, nullptr, pos.x, pos.y, 0, 0, SWP_NOSIZE | SWP_NOZORDER | SWP_NOREDRAW);
    }

    if (flags & kResizeX || flags & kResizeY)
    {
        int w = rect.right - rect.left;
        int h = rect.bottom - rect.top;
        if (flags & kResizeX)
            w += wDiff;
        if (flags & kResizeY)
            h += hDiff;

        SetWindowPos(hControl, nullptr, 0, 0, w, h, SWP_NOMOVE | SWP_NOZORDER | SWP_NOREDRAW);
    }
}

void plComponentDlg::IGetComment()
{
    if (fCommentNode)
    {
        // Get the text from the edit and store it in the UserPropBuffer
        int len = GetWindowTextLength(GetDlgItem(fhDlg, IDC_COMMENTS))+1;
        if (len != 0)
        {
            TCHAR* buf = new TCHAR[len];
            GetDlgItemText(fhDlg, IDC_COMMENTS, buf, len);
            fCommentNode->SetUserPropBuffer(buf);
            delete [] buf;
        }
        else
            fCommentNode->SetUserPropBuffer(_M(""));
    }
}

INT_PTR plComponentDlg::ForwardDlgProc(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam)
{
    return Instance().DlgProc(hDlg, msg, wParam, lParam);
}

#define MENU_ID_START 41000

INT_PTR plComponentDlg::DlgProc(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch (msg)
    {
    case WM_INITDIALOG:
        fhDlg = hDlg;
        fInterface->RegisterDlgWnd(fhDlg);
        ICreateComponentsTree();
        ICreateMenu();
        ICreateRightClickMenu();
        return TRUE;

    case WM_CLOSE:
        DestroyWindow(fhDlg);
        return TRUE;

    case WM_DESTROY:
        fInterface->UnRegisterDlgWnd(fhDlg);
        fhDlg = nullptr;
        return TRUE;

    case WM_SIZING:
        IPositionControls((RECT*)lParam, (int)wParam);
        return TRUE;

    case WM_ACTIVATE:
        if (LOWORD(wParam) == WA_INACTIVE)
            plMaxAccelerators::Enable();
        else
            plMaxAccelerators::Disable();
        return TRUE;

    case WM_COMMAND:
        if (HIWORD(wParam) == BN_CLICKED && LOWORD(wParam) == IDCANCEL)
        {
            ShowWindow(hDlg, SW_HIDE);
            fInterface->UnRegisterDlgWnd(hDlg);
            return TRUE;
        }
        else if (HIWORD(wParam) == BN_CLICKED && LOWORD(wParam) == IDC_ATTACH)
        {
            IAttachTreeSelection();
            return TRUE;
        }
        else if (HIWORD(wParam) == EN_KILLFOCUS && LOWORD(wParam) == IDC_COMMENTS)
        {
            IGetComment();
            return TRUE;
        }
        // "Refresh" menu item
        else if (HIWORD(wParam) == BN_CLICKED && LOWORD(wParam) == ID_REFRESH)
        {
            IRefreshTree();
            return TRUE;
        }
        // "Remove unused components" menu item
        else if (HIWORD(wParam) == BN_CLICKED && LOWORD(wParam) == ID_REMOVE_UNUSED)
        {
            IRemoveUnusedComps();
            return TRUE;
        }
        // Item selected from 'New' menu
        else if (HIWORD(wParam) == BN_CLICKED && LOWORD(wParam) >= MENU_ID_START)
        {
            ClassDesc *desc = plComponentMgr::Inst().Get(LOWORD(wParam)-MENU_ID_START);
            // If this is a component type (not a category)
            if (desc)
            {
                // Create an object of that type and a node to reference it
                Object *obj = (Object*)GetCOREInterface()->CreateInstance(desc->SuperClassID(), desc->ClassID());
                INode *node = GetCOREInterface()->CreateObjectNode(obj);

                plComponentBase *comp = (plComponentBase*)obj;
                node->Hide(!comp->AllowUnhide());
                node->Freeze(TRUE);

                // Add the new component to the tree
                HWND hTree = GetDlgItem(hDlg, IDC_TREE);
                HTREEITEM item = IAddComponent(hTree, (plMaxNode*)node);
                TreeView_SelectItem(hTree, item);
                TreeView_EnsureVisible(hTree, item);
            }
        }
        break;

    case WM_NOTIFY:
        NMHDR *nmhdr = (NMHDR*)lParam;
        if (nmhdr->idFrom == IDC_TREE)
        {
            switch (nmhdr->code)
            {
            case TVN_SELCHANGED:
                {
                    NMTREEVIEW *tv = (NMTREEVIEW*)lParam;

                    IGetComment();

                    bool isComponent = IIsComponent(tv->itemNew.lParam);

                    // If the new selection is a component, enable the attach button and comment field
                    EnableWindow(GetDlgItem(hDlg, IDC_ATTACH), isComponent);
                    SendDlgItemMessage(hDlg, IDC_COMMENTS, EM_SETREADONLY, !isComponent, 0);

                    if (isComponent)
                    {
                        fCommentNode = (plMaxNode*)tv->itemNew.lParam;
                        
                        TSTR buf;
                        fCommentNode->GetUserPropBuffer(buf);
                        SetDlgItemText(hDlg, IDC_COMMENTS, buf);
                    }
                    else
                    {
                        fCommentNode = nullptr;
                        SetDlgItemText(hDlg, IDC_COMMENTS, _T(""));
                    }

                    return TRUE;
                }
                break;

            case TVN_BEGINLABELEDIT:
                // If this isn't a component, don't allow the edit
                if (!IIsComponent(((NMTVDISPINFO*)lParam)->item.lParam))
                {
                    SetWindowLongPtr(hDlg, DWLP_MSGRESULT, TRUE);
                    return TRUE;
                }

                // The edit box this creates kills the focus on our window, causing
                // accelerators to be enabled.  Add an extra disable to counteract that.
                plMaxAccelerators::Disable();

                return TRUE;

            // Finishing changing the name of a component
            case TVN_ENDLABELEDIT:
                {
                    NMTVDISPINFO *di = (NMTVDISPINFO*)lParam;
                    TCHAR* text = di->item.pszText;
                    // If the name was changed...
                    if (text && *text != _T('\0'))
                    {
                        // Update the name of the node
                        plMaxNode *node = IGetTreeSelection();
                        node->SetName(text);

                        // Update the name in the panel too
                        if (plComponentUtil::Instance().IsOpen())
                            plComponentUtil::Instance().IUpdateNodeName(node);

                        // Make sure Max knows the file was changed
                        SetSaveRequiredFlag();

                        // Return true to keep the changes
                        SetWindowLongPtr(hDlg, DWLP_MSGRESULT, TRUE);
                    }

                    plMaxAccelerators::Enable();
                }
                return TRUE;

            // User double-clicked.  Select the objects the selected component is attached to.
            case NM_DBLCLK:
                ISelectTreeSelection();
                return TRUE;

            case NM_RCLICK:
                IOpenRightClickMenu();
                return TRUE;
                
            case TVN_KEYDOWN:
                // User pressed delete
                if (((NMTVKEYDOWN*)lParam)->wVKey == VK_DELETE)
                {
                    IDeleteComponent(IGetTreeSelection());
                    return TRUE;
                }
                break;
            }
        }
        break;
    }

    return FALSE;
}

HTREEITEM plComponentDlg::IAddLeaf(HWND hTree, HTREEITEM hParent, const TCHAR* text, LPARAM lParam)
{
    TVITEM tvi = {0};
    tvi.mask       = TVIF_TEXT | TVIF_PARAM;
    tvi.pszText    = const_cast<TCHAR*>(text);
    tvi.cchTextMax = _tcslen(text);
    tvi.lParam     = lParam;

    TVINSERTSTRUCT tvins = {};
    tvins.item         = tvi;
    tvins.hParent      = hParent;
    tvins.hInsertAfter = TVI_SORT;

    return TreeView_InsertItem(hTree, &tvins);
}

HTREEITEM plComponentDlg::IFindTreeItem(HWND hTree, const TCHAR* name, HTREEITEM hParent)
{
    HTREEITEM hChild = TreeView_GetChild(hTree, hParent);

    while (hChild)
    {
        TCHAR buf[256];
        TVITEM tvi;
        tvi.mask  = TVIF_TEXT;
        tvi.hItem = hChild;
        tvi.pszText = buf;
        tvi.cchTextMax = sizeof(buf);
        TreeView_GetItem(hTree, &tvi);

        if (_tcscmp(name, tvi.pszText) == 0)
            return hChild;

        hChild = TreeView_GetNextSibling(hTree, hChild);
    }

    return nullptr;
}

HTREEITEM plComponentDlg::IAddComponent(HWND hTree, plMaxNode *node)
{
    plComponentBase *comp = node->ConvertToComponent();

    // Try and find the component category in the tree
    const TCHAR* category = comp->GetCategory();
    HTREEITEM hCat = IFindTreeItem(hTree, category, TVI_ROOT);
    // If it isn't there yet, add it
    if (!hCat)
        hCat = IAddLeaf(hTree, TVI_ROOT, category, 0);

    // Try and find the component type in the tree
    int idx = plComponentMgr::Inst().FindClassID(comp->ClassID());
    HTREEITEM hType = ISearchTree(hTree, idx+1, hCat);
    if (!hType)
    {
        // If it isn't there yet, add it
        TSTR type;
        comp->GetClassName(type, true);

        if (IIsHidden(comp->ClassID()))
            type.Append(_T(" (Hidden)"));

        hType = IAddLeaf(hTree, hCat, type, idx+1);
    }

    // Add the name of this component to this type
    return IAddLeaf(hTree, hType, node->GetName(), (LPARAM)node);
}

void plComponentDlg::IAddComponentsRecur(HWND hTree, plMaxNode *node)
{
    if (node->IsComponent())
        IAddComponent(hTree, node);

    for (int i = 0; i < node->NumberOfChildren(); i++)
    {
        plMaxNode *child = (plMaxNode*)node->GetChildNode(i);
        IAddComponentsRecur(hTree, child);
    }
}

void plComponentDlg::ICreateComponentsTree()
{
    HWND tree = GetDlgItem(fhDlg, IDC_TREE);

#if MAX_VERSION_MAJOR >= 14 // Max 2012
    TreeView_SetBkColor(tree, GetColorManager()->GetColor(kWindow));
    TreeView_SetTextColor(tree, GetColorManager()->GetColor(kText));
#endif

    IAddComponentsRecur(tree, (plMaxNode*)GetCOREInterface()->GetRootNode());
}

void plComponentDlg::ICreateMenu()
{
    // Add a refresh option to the system menu, for those rare cases where the manager gets out of sync
    HMENU hMenu = GetMenu(fhDlg);

    HMENU hNew = CreatePopupMenu();
    InsertMenu(hMenu, 0, MF_POPUP | MF_STRING | MF_BYPOSITION, (UINT_PTR)hNew, _T("New"));

    const TCHAR* lastCat = nullptr;
    HMENU hCurType = nullptr;

    uint32_t count = plComponentMgr::Inst().Count();
    for (uint32_t i = 0; i < count; i++)
    {
        plComponentClassDesc *desc = (plComponentClassDesc*)plComponentMgr::Inst().Get(i);

        // Don't put in the create menu if obsolete
        if (desc->IsObsolete())
            continue;

        if (!lastCat || _tcscmp(lastCat, desc->Category()))
        {
            lastCat = desc->Category();

            hCurType = CreatePopupMenu();
            AppendMenu(hNew, MF_POPUP | MF_STRING, (UINT_PTR)hCurType, lastCat);
        }

        AppendMenu(hCurType, MF_STRING, MENU_ID_START+i, desc->ClassName());
    }
}

// Taking advantage of the fact that the node pointers we store in the lParam
// will certainly be higher than the number of component types
bool plComponentDlg::IIsComponent(LPARAM lParam)
{
    return (lParam > plComponentMgr::Inst().Count()+1);
}

bool plComponentDlg::IIsType(LPARAM lParam)
{
    return (lParam > 0 && lParam <= plComponentMgr::Inst().Count()+1);
}

void plComponentDlg::IAttachTreeSelection()
{
    HWND hTree = GetDlgItem(fhDlg, IDC_TREE);

    // Get the current selection from the tree
    HTREEITEM hSelected = TreeView_GetSelection(hTree);
    TVITEM item;
    item.mask = TVIF_PARAM;
    item.hItem = hSelected;
    TreeView_GetItem(hTree, &item);

    // If the item has a lParam it is a component
    if (IIsComponent(item.lParam))
    {
        plMaxNode *node = (plMaxNode*)item.lParam;
        plComponentBase *comp = node->ConvertToComponent();

        // Add each of the selected nodes that is not a component to the targets list
        int count = fInterface->GetSelNodeCount();
        for (int i = 0; i < count; i++)
        {
            plMaxNode *target = (plMaxNode*)fInterface->GetSelNode(i);
            if (!target->IsComponent())
                comp->AddTarget(target);
        }

        // Update the rollups to reflect the new component
        if (plComponentUtil::Instance().IsOpen())
            plComponentUtil::Instance().IUpdateRollups();
    }
}

// Wow, this INodeTab class is very thorough
bool FindNodeInTab(INode *node, INodeTab& nodes)
{
    for (int i = 0; i < nodes.Count(); i++)
    {
        if (node == nodes[i])
            return true;
    }

    return false;
}

void plComponentDlg::SelectComponentTargs(INodeTab& nodes)
{
    // Make an INode tab with all the targets in it
    INodeTab targets;
    for (int i = 0; i < nodes.Count(); i++)
    {
        plComponentBase *comp = ((plMaxNode*)nodes[i])->ConvertToComponent();

        for (int j = 0; j < comp->NumTargets(); j++)
        {
            INode *node = comp->GetTarget(j);
            if (node && !FindNodeInTab(node, targets))
                targets.Append(1, &node);
        }
    }

    // If the user is selecting a single component, make sure it is selected in the rollup too
    if (plComponentUtil::Instance().IsOpen() && nodes.Count() == 1)
        plComponentUtil::Instance().fLastComponent = ((plMaxNode*)nodes[0])->ConvertToComponent();
    
    theHold.Begin();
    fInterface->RedrawViews(fInterface->GetTime(), REDRAW_BEGIN);
    fInterface->ClearNodeSelection(FALSE);              // Deselect current nodes

    // If there is at least one valid target, select it
    if (targets.Count() > 0)
        fInterface->SelectNodeTab(targets, TRUE, FALSE);

    fInterface->RedrawViews(fInterface->GetTime(), REDRAW_END);
    theHold.Accept(_M("Select"));
}

void plComponentDlg::ISelectTreeSelection()
{
    INodeTab nodes;

    INode *curComponent = (INode*)IGetTreeSelection();
    if (curComponent)
    {
        nodes.Append(1, &curComponent);
    }
    else
    {
        HWND hTree = GetDlgItem(fhDlg, IDC_TREE);
        HTREEITEM hRoot = TreeView_GetSelection(hTree);

        IGetComponentsRecur(hTree, hRoot, nodes);
    }

    SelectComponentTargs(nodes);
}

void plComponentDlg::IGetComponentsRecur(HWND hTree, HTREEITEM hItem, INodeTab& nodes)
{
    if (hItem)
    {
        INode *node = (INode*)ITreeItemToNode(hTree, hItem);
        if (node)
            nodes.Append(1, &node);
        else
        {
            HTREEITEM hChild = TreeView_GetChild(hTree, hItem);
            IGetComponentsRecur(hTree, hChild, nodes);

            while (hChild = TreeView_GetNextSibling(hTree, hChild))
            {
                IGetComponentsRecur(hTree, hChild, nodes);
            }
        }
    }
}

void plComponentDlg::IDeleteComponent(plMaxNode *component)
{
    if (!component)
        return;

    // Make sure this components interface isn't showing
    if (plComponentUtil::Instance().IsOpen())
        plComponentUtil::Instance().IComponentPreDelete(component->ConvertToComponent());
    
    // Delete the component from the scene
    theHold.Begin();
    fInterface->DeleteNode(component);
    theHold.Accept(_M("Delete Component"));

    // Delete the component from the tree
    HWND hTree = GetDlgItem(fhDlg, IDC_TREE);
    HTREEITEM hItem = TreeView_GetSelection(hTree);
    HTREEITEM hParent = TreeView_GetParent(hTree, hItem);
    TreeView_DeleteItem(hTree, hItem);

    // If that was the only component of this type, delete the type too
    if (!TreeView_GetChild(hTree, hParent))
    {
        HTREEITEM hCategory = TreeView_GetParent(hTree, hParent);
        TreeView_DeleteItem(hTree, hParent);

        // If this is the only type in this category, delete the category too!
        // Sadly, this is the most we can delete.
        if (!TreeView_GetChild(hTree, hCategory))
            TreeView_DeleteItem(hTree, hCategory);
    }

    // Update the rollups in case the selected object had this component attached
    if (plComponentUtil::Instance().IsOpen())
        plComponentUtil::Instance().IUpdateRollups();
}

plMaxNode *plComponentDlg::IGetTreeSelection()
{
    HWND hTree = GetDlgItem(fhDlg, IDC_TREE);

    HTREEITEM hItem = TreeView_GetSelection(hTree);
    return ITreeItemToNode(hTree, hItem);
}

plMaxNode *plComponentDlg::ITreeItemToNode(HWND hTree, HTREEITEM hItem)
{
    if (hItem)
    {
        TVITEM item;
        item.mask = TVIF_PARAM;
        item.hItem = hItem;
        TreeView_GetItem(hTree, &item);

        if (IIsComponent(item.lParam))
            return (plMaxNode*)item.lParam;
    }

    return nullptr;
}

enum
{
    // Comp menu
    kMenuDelete = 1,
    kMenuRename,
    kMenuCopy,

    // Type menu
    kMenuHide
};

void plComponentDlg::ICreateRightClickMenu()
{
    fCompMenu = CreatePopupMenu();
    AppendMenu(fCompMenu, MF_STRING, kMenuDelete, _T("Delete"));
    AppendMenu(fCompMenu, MF_STRING, kMenuRename, _T("Rename"));
    AppendMenu(fCompMenu, MF_STRING, kMenuCopy, _T("Copy"));

    fTypeMenu = CreatePopupMenu();
    AppendMenu(fTypeMenu, MF_STRING, kMenuHide, _T("Hide/Show"));
}

void plComponentDlg::IOpenRightClickMenu()
{
    HWND hTree = GetDlgItem(fhDlg, IDC_TREE);

    // Get the position of the cursor in screen and tree client coords
    POINT point, localPoint;
    GetCursorPos(&point);
    localPoint = point;
    ScreenToClient(hTree, &localPoint);

    // Check if there is a tree item at that point
    TVHITTESTINFO hitTest;
    hitTest.pt = localPoint;
    TreeView_HitTest(hTree, &hitTest);
    if (!(hitTest.flags & TVHT_ONITEMLABEL))
        return;

    // Check if the tree item has an lParam (is a component)
    TVITEM item;
    item.mask = TVIF_PARAM;
    item.hItem = hitTest.hItem;
    TreeView_GetItem(hTree, &item);

    HMENU menu = nullptr;
    if (IIsComponent(item.lParam))
        menu = fCompMenu;
    else if (IIsType(item.lParam))
        menu = fTypeMenu;
    else
        return;

    // Select the item we're working with, so the user isn't confused
    TreeView_SelectItem(hTree, item.hItem);

    // Create the popup menu and get the option the user selects
    SetForegroundWindow(fhDlg);
    int sel = TrackPopupMenu(menu, TPM_NONOTIFY | TPM_RETURNCMD, point.x, point.y, 0, fhDlg, nullptr);
    switch(sel)
    {
    case kMenuDelete:
        IDeleteComponent((plMaxNode*)item.lParam);
        break;

    case kMenuRename:
        TreeView_EditLabel(hTree, hitTest.hItem);
        break;

    case kMenuCopy:
        {
            // Component to copy
            INode *node = (INode*)item.lParam;
            INodeTab tab;
            tab.Append(1, &node);

            // Copy
            INodeTab copy;

            // Make the copy
            fInterface->CloneNodes(tab, Point3(0,0,0), true, NODE_COPY, nullptr, &copy);

            // Delete the targets for the copy and add it to the tree
            plMaxNode *newNode = (plMaxNode*)copy[0];
            newNode->ConvertToComponent()->DeleteAllTargets();
            HTREEITEM hItem = IAddComponent(GetDlgItem(fhDlg, IDC_TREE), newNode);
            TreeView_SelectItem(GetDlgItem(fhDlg, IDC_TREE), hItem);
        }
        break;

    case kMenuHide:
        {
            ClassDesc *desc = plComponentMgr::Inst().Get((uint32_t)(item.lParam-1));

            std::vector<Class_ID>::iterator it;
            it = std::find(fHiddenComps.begin(), fHiddenComps.end(), desc->ClassID());

            MSTR name = desc->ClassName();
            if (it == fHiddenComps.end())
            {
                fHiddenComps.push_back(desc->ClassID());
                name.Append(_M(" (Hidden)"));
            }
            else
                fHiddenComps.erase(it);

            item.mask = TVIF_TEXT;
            item.pszText = const_cast<MCHAR*>(name.data());
            TreeView_SetItem(GetDlgItem(fhDlg, IDC_TREE), &item);

            plComponentUtil::Instance().IUpdateRollups();
        }
        break;
    }

    PostMessage(fhDlg, WM_USER, 0, 0);
}

HTREEITEM plComponentDlg::ISearchTree(HWND hTree, LPARAM lParam, HTREEITEM hCur)
{
    // Get the param for the current item
    TVITEM tvi;
    tvi.mask  = TVIF_PARAM;
    tvi.hItem = hCur;
    TreeView_GetItem(hTree, &tvi);

    // If the lParam matches the one searching for, return the handle
    if (tvi.lParam == lParam)
        return hCur;

    // Do a recursive search on the items children
    HTREEITEM hChild = TreeView_GetChild(hTree, hCur);
    while (hChild)
    {
        HTREEITEM hResult = ISearchTree(hTree, lParam, hChild);
        if (hResult)
            return hResult;

        hChild = TreeView_GetNextSibling(hTree, hChild);
    }

    return nullptr;
}

void plComponentDlg::IRefreshTree()
{
    if (fhDlg)
    {
        fCommentNode = nullptr;

        HWND hTree = GetDlgItem(fhDlg, IDC_TREE);
        TreeView_DeleteAllItems(hTree);
        IAddComponentsRecur(hTree, (plMaxNode*)GetCOREInterface()->GetRootNode());
    }
}

void plComponentDlg::INotify(void *param, NotifyInfo *info)
{
    if (info->intcode == NOTIFY_SYSTEM_SHUTDOWN)
    {
        UnRegisterNotification(INotify, nullptr, NOTIFY_FILE_PRE_OPEN);
        UnRegisterNotification(INotify, nullptr, NOTIFY_SYSTEM_PRE_NEW);
        UnRegisterNotification(INotify, nullptr, NOTIFY_SYSTEM_PRE_RESET);
        UnRegisterNotification(INotify, nullptr, NOTIFY_FILE_PRE_MERGE);
        UnRegisterNotification(INotify, nullptr, NOTIFY_PRE_IMPORT);
        UnRegisterNotification(INotify, nullptr, NOTIFY_FILE_PRE_SAVE);
        UnRegisterNotification(INotify, nullptr, NOTIFY_FILE_PRE_SAVE_OLD);

        UnRegisterNotification(INotify, nullptr, NOTIFY_FILE_POST_OPEN);
        UnRegisterNotification(INotify, nullptr, NOTIFY_SYSTEM_POST_NEW);
        UnRegisterNotification(INotify, nullptr, NOTIFY_SYSTEM_POST_RESET);
        UnRegisterNotification(INotify, nullptr, NOTIFY_FILE_POST_MERGE);
        UnRegisterNotification(INotify, nullptr, NOTIFY_POST_IMPORT);

        UnRegisterNotification(INotify, nullptr, NOTIFY_SYSTEM_SHUTDOWN);
    }
    // New nodes are coming in, refresh the scene component list
    else if (info->intcode == NOTIFY_FILE_POST_OPEN ||
             info->intcode == NOTIFY_SYSTEM_POST_NEW ||
             info->intcode == NOTIFY_SYSTEM_POST_RESET ||
             info->intcode == NOTIFY_FILE_POST_MERGE ||
             info->intcode == NOTIFY_POST_IMPORT)
    {
        Instance().IRefreshTree();
    }
    // Nodes may be going away, save the comment now
    else if (info->intcode == NOTIFY_FILE_PRE_OPEN ||
             info->intcode == NOTIFY_SYSTEM_PRE_NEW ||
             info->intcode == NOTIFY_SYSTEM_PRE_RESET ||
             info->intcode == NOTIFY_FILE_PRE_MERGE ||
             info->intcode == NOTIFY_PRE_IMPORT ||
             info->intcode == NOTIFY_FILE_PRE_SAVE ||
             info->intcode == NOTIFY_FILE_PRE_SAVE_OLD)
    {
        // This is causing a crash, so for now if you add a comment and don't
        // pick another component or close the manager before closing the file,
        // you lose the comment -Colin
//      Instance().IGetComment();
    }
}

void plComponentDlg::IUpdateNodeName(plMaxNode *node)
{
    if (!fhDlg)
        return;

    // Update the name in the tree too
    HWND hTree = GetDlgItem(fhDlg, IDC_TREE);
    TVITEM tvi = {0};
    tvi.hItem = ISearchTree(hTree, (LPARAM)node);
    tvi.mask = TVIF_TEXT;
    tvi.pszText = const_cast<TCHAR*>(node->GetName());
    TreeView_SetItem(hTree, &tvi);
}

void FindUnusedCompsRecur(plMaxNode *node, std::vector<plMaxNode*>& unused)
{
    plComponentBase *comp = node->ConvertToComponent();
    if (comp)
    {
        bool isAttached = false;

        int num = comp->NumTargets();
        for (int i = 0; i < num; i++)
        {
            if (comp->GetTarget(i))
            {
                isAttached = true;
                break;
            }
        }

        if (!isAttached)
            unused.push_back(node);
    }
    
    for (int i = 0; i < node->NumberOfChildren(); i++)
        FindUnusedCompsRecur((plMaxNode*)node->GetChildNode(i), unused);
}

void plComponentDlg::IRemoveUnusedComps()
{
    std::vector<plMaxNode*> unused;
    FindUnusedCompsRecur((plMaxNode*)GetCOREInterface()->GetRootNode(), unused);

    for (int i = 0; i < unused.size(); i++)
        GetCOREInterface()->DeleteNode(unused[i], FALSE);

    IRefreshTree();
}

bool plComponentDlg::IIsHidden(Class_ID& cid)
{
    return (std::find(fHiddenComps.begin(), fHiddenComps.end(), cid) != fHiddenComps.end());
}

////////////////////////////////////////////////////////////////////////////////



class plCopyCompCallback : public HitByNameDlgCallback
{
protected:
    Tab<plComponentBase*> fSharedComps;
    INodeTab fSelectedNodes;

public:
    bool GetComponents()
    {
        fSelectedNodes.ZeroCount();
        fSharedComps.ZeroCount();

        Interface *ip = GetCOREInterface();

        int nodeCount = ip->GetSelNodeCount();
        if (nodeCount == 0)
            return false;
        
        // Get the components shared among the selected nodes
        int i;
        fSelectedNodes.SetCount(nodeCount);
        for (i = 0; i < nodeCount; i++)
            fSelectedNodes[i] = ip->GetSelNode(i);

        INodeTab sharedComps;
        if (plSharedComponents(fSelectedNodes, sharedComps) == 0)
            return false;

        // Put the shared components in a list
        fSharedComps.SetCount(sharedComps.Count());
        for (i = 0; i < sharedComps.Count(); i++)
            fSharedComps[i] = ((plMaxNode*)sharedComps[i])->ConvertToComponent();

        return true;
    }

    GETDLGTEXT_RETURN_TYPE dialogTitle() override { return _M("Select Nodes"); }
    GETDLGTEXT_RETURN_TYPE buttonText() override { return _M("Copy"); }

    int filter(INode *node) override
    {
        // Make sure this node doesn't already have the components
        for (int i = 0; i < fSelectedNodes.Count(); i++)
        {
            if (fSelectedNodes[i] == node)
                return FALSE;
        }

        return TRUE;
    }

    void proc(INodeTab &nodeTab) override
    {
        for (int i = 0; i < nodeTab.Count(); i++)
        {
            for (int j = 0; j < fSharedComps.Count(); j++)
            {
                fSharedComps[j]->AddTarget((plMaxNodeBase*)nodeTab[i]);
            }
        }
    }
};
static plCopyCompCallback copyCompCallback;

void CopyComponents()
{
    if (copyCompCallback.GetComponents())
        GetCOREInterface()->DoHitByNameDialog(&copyCompCallback);
    else
    {
        const TCHAR* msg;
        switch (GetCOREInterface()->GetSelNodeCount()) {
        case 0:
            msg = _T("No object(s) selected");
            break;
        case 1:
            msg = _T("No components on the selected object");
            break;
        default:
            msg = _T("No components are shared among the selected objects");
            break;
        }
        plMaxMessageBox(
            nullptr,
            msg,
            _T("Component Copy"),
            MB_OK | MB_ICONASTERISK
        );
    }
}
