/*==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/>.

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 "plResponderComponentPriv.h"
#include "resource.h"
#include "plComponent.h"
#include "plComponentReg.h"

#include "../pnSceneObject/plSceneObject.h"
#include "../pnKeyedObject/hsKeyedObject.h"
#include "../pnKeyedObject/plKey.h"
#include "../pnNetCommon/plSDLTypes.h"

#include "../plModifier/plResponderModifier.h"
#include "../plModifier/plLogicModifier.h"
#include "../plModifier/plAxisAnimModifier.h"
#include "../pfConditional/plActivatorConditionalObject.h"
#include "../pfConditional/plORConditionalObject.h"

#include "../pnMessage/plObjRefMsg.h"
#include "../pnMessage/plNotifyMsg.h"

#include "hsResMgr.h"
#include "../MaxMain/plMaxNode.h"

#include "plPickNode.h"

#include "../MaxMain/plPlasmaRefMsgs.h"

#include "plResponderLink.h"
#include "plResponderAnim.h"
#include "plResponderMtl.h"
#include "plResponderWait.h"

#include "../MaxMain/plMaxAccelerators.h"

IParamBlock2 *CreateWaitBlk();

int ResponderGetActivatorCount(plComponentBase *comp)
{
    if (comp->ClassID() == RESPONDER_CID)
        return comp->GetParamBlockByID(plComponentBase::kBlkComp)->Count(kResponderActivators);

    return -1;
}

plComponentBase *ResponderGetActivator(plComponentBase *comp, int idx)
{
    if (comp->ClassID() == RESPONDER_CID)
    {
        IParamBlock2 *pb = comp->GetParamBlockByID(plComponentBase::kBlkComp);
        plMaxNode *activatorNode = (plMaxNode*)pb->GetINode(kResponderActivators, 0, idx);
        return activatorNode->ConvertToComponent();
    }

    return nil;
}

plKey Responder::GetKey(plComponentBase *comp, plMaxNodeBase *node)
{
    if (comp->ClassID() != RESPONDER_CID)
        return nil;

    plResponderComponent *responder = (plResponderComponent*)comp;
    if (responder->fModKeys.find((plMaxNode*)node) != responder->fModKeys.end())
        return responder->fModKeys[(plMaxNode*)node];

    return nil; 
}

CLASS_DESC(plResponderComponent, gResponderDesc, "Responder", "Responder", COMP_TYPE_LOGIC, RESPONDER_CID)

class plResponderProc;
extern plResponderProc gResponderComponentProc;

// When one of our parameters that is a ref changes, send out the component ref
// changed message.  Normally, messages from component refs are ignored since
// they pass along all the messages of the ref, which generates a lot of false
// converts.
class plResponderAccessor : public PBAccessor
{
public:
    void Set(PB2Value& v, ReferenceMaker* owner, ParamID id, int tabIndex, TimeValue t)
    {
        if (id == kResponderActivators || id == kResponderState)
        {
            plResponderComponent *comp = (plResponderComponent*)owner;
            comp->NotifyDependents(FOREVER, PART_ALL, REFMSG_USER_COMP_REF_CHANGED);
        }
    }
};
static plResponderAccessor gResponderAccessor;

ParamBlockDesc2 gResponderBlock
(
    plComponent::kBlkComp, _T("responderComp"), 0, &gResponderDesc, P_AUTO_CONSTRUCT + P_AUTO_UI, plComponent::kRefComp,

    IDD_COMP_RESPOND, IDS_COMP_RESPOND, 0, 0, &gResponderComponentProc,

    kResponderState,    _T("state"),        TYPE_REFTARG_TAB, 0,        0, 0,
        p_accessor,     &gResponderAccessor,
        end,
    kResponderStateName, _T("stateName"),   TYPE_STRING_TAB, 0,         0, 0,
        end,

    kResponderActivators,   _T("activators"),   TYPE_INODE_TAB, 0,      0, 0,
        p_ui,           TYPE_NODELISTBOX, IDC_LIST_TARGS, 0, 0, IDC_DEL_TARGS,
        p_accessor,     &gResponderAccessor,
        end,

    kResponderStateDef, _T("defState"),     TYPE_INT,                   0, 0,
        end,

    kResponderEnabled, _T("enabled"),   TYPE_BOOL,                  0, 0,
        p_default,  TRUE,
        p_ui,       TYPE_SINGLECHEKBOX, IDC_ENABLED,
        end,

    kResponderTrigger, _T("trigger"),   TYPE_BOOL,                  0, 0,
        p_default,  TRUE,
        p_ui,       TYPE_SINGLECHEKBOX, IDC_CHECK_TRIGGER,
        end,

    kResponderUnTrigger, _T("unTrigger"),   TYPE_BOOL,              0, 0,
        p_default,  FALSE,
        p_ui,       TYPE_SINGLECHEKBOX, IDC_CHECK_UNTRIGGER,
        end,

    kResponderLocalDetect, _T("localDetect"),   TYPE_BOOL,          0, 0,
        p_default,  FALSE,
        p_ui,       TYPE_SINGLECHEKBOX, IDC_DETECT_LOCAL_CHECK,
        end,

    kResponderSkipFFSound, _T("skipFFSound"),   TYPE_BOOL,                  0, 0,
        p_default,  FALSE,
        p_ui,       TYPE_SINGLECHEKBOX, IDC_CHECK_SKIPFFSOUND,
        end,

    end
);

ParamBlockDesc2 gStateBlock
(
    kResponderStateBlk, _T("responderState"), 0, &gResponderDesc, 0,

    kStateCmdParams, _T("cmdParam"),    TYPE_REFTARG_TAB, 0,        0, 0,
        end,

    kStateCmdWait,  _T("cmdWait"),      TYPE_REFTARG_TAB, 0,        0, 0,
        end,

    kStateCmdSwitch, _T("cmdSwitch"),   TYPE_INT,                   0, 0,
        end,

    kStateCmdEnabled, _T("enabled"),    TYPE_BOOL_TAB, 0,           0, 0,
        p_default,  TRUE,
        end,

    end
);

std::vector<plResponderCmd*> gResponderCmds;

plResponderCmd *plResponderCmd::Find(IParamBlock2 *pb)
{
    if (!pb)
        return nil;

    ParamBlockDesc2 *pbDesc = pb->GetDesc();

    for (int i = 0; i < gResponderCmds.size(); i++)
    {
        if (gResponderCmds[i]->GetDesc() == pbDesc)
            return gResponderCmds[i];
    }

    return nil;
}

IParamBlock2* plResponderCmd::CreatePB(int idx)
{
    hsAssert(NumTypes() == 1, "Can't auto-create the pb for a cmd with multiple types");
    IParamBlock2 *pb = CreateParameterBlock2(GetDesc(), nil);
    return pb;
}

void DummyCodeIncludeFuncResponder()
{
    gResponderCmds.push_back(&(plResponderCmdVisibility::Instance()));
    gResponderCmds.push_back(&(plResponderCmdLink::Instance()));
    gResponderCmds.push_back(&(plResponderCmdEnable::Instance()));
    gResponderCmds.push_back(&(plResponderCmdXRegion::Instance()));
    gResponderCmds.push_back(&(plResponderCmdOneShot::Instance()));
    gResponderCmds.push_back(&(plResponderCmdNotify::Instance()));
    gResponderCmds.push_back(&(plResponderCmdDetectorEnable::Instance()));
    gResponderCmds.push_back(&(plResponderCmdCamTransition::Instance()));
    gResponderCmds.push_back(&(plResponderCmdCamForce::Instance()));
    gResponderCmds.push_back(&(plResponderCmdAnim::Instance()));
    gResponderCmds.push_back(&(plResponderCmdMtl::Instance()));
    gResponderCmds.push_back(&(plResponderCmdDelay::Instance()));
    gResponderCmds.push_back(&(plResponderCmdFootSurface::Instance()));
    gResponderCmds.push_back(&(plResponderCmdMultistage::Instance()));
    gResponderCmds.push_back(&(plResponderCmdPhysEnable::Instance()));
    gResponderCmds.push_back(&(plResponderCmdSubWorld::Instance()));

    for (int i = 0; i < gResponderCmds.size(); i++)
        gResponderCmds[i]->GetDesc()->SetClassDesc(&gResponderDesc);

    ResponderWait::SetDesc(&gResponderDesc);
}

plResponderComponent::plResponderComponent()
{
    fClassDesc = &gResponderDesc;
    fClassDesc->MakeAutoParamBlocks(this);
}

hsBool plResponderComponent::SetupProperties(plMaxNode* node, plErrorMsg* pErrMsg)
{
    int numStates = fCompPB->Count(kResponderState);
    for (int i = 0; i < numStates; i++)
    {
        IParamBlock2 *statePB = (IParamBlock2*)fCompPB->GetReferenceTarget(kResponderState, 0, i);

        for (int j = 0; j < statePB->Count(kStateCmdParams); j++)
        {
            IParamBlock2 *cmdPB = (IParamBlock2*)statePB->GetReferenceTarget(kStateCmdParams, 0, j);
            plResponderCmd *cmd = plResponderCmd::Find(cmdPB);
            cmd->SetupProperties(node, pErrMsg, cmdPB);
        }
    }

    return true;
}

hsBool plResponderComponent::PreConvert(plMaxNode *node,plErrorMsg *pErrMsg)
{
    plSceneObject* rObj = node->GetSceneObject();
    plLocation loc = node->GetLocation();

    // Create and register the RESPONDER's logic component
    plResponderModifier *responder = TRACKED_NEW plResponderModifier;
    plKey responderKey = hsgResMgr::ResMgr()->NewKey(IGetUniqueName(node), responder, loc);
    hsgResMgr::ResMgr()->AddViaNotify(responderKey, TRACKED_NEW plObjRefMsg(rObj->GetKey(), plRefMsg::kOnCreate, -1, plObjRefMsg::kModifier), plRefFlags::kActiveRef);

    // Tell all the activators to notify us
    for (int i = 0; i < fCompPB->Count(kResponderActivators); i++)
    {
        plMaxNode *activatorNode = (plMaxNode*)fCompPB->GetINode(kResponderActivators, 0, i);
        plComponentBase *comp = activatorNode ? activatorNode->ConvertToComponent() : nil;
        if (comp)
        {
            if (fCompPB->GetInt(kResponderLocalDetect))
                comp->AddReceiverKey(responderKey, node);
            else
                comp->AddReceiverKey(responderKey);
        }
    }

    fModKeys[node] = responderKey;

    return true;
}

plResponderModifier* plResponderComponent::IGetResponderMod(plMaxNode* node)
{
    plKey responderKey = fModKeys[node];
    return plResponderModifier::ConvertNoRef(responderKey->GetObjectPtr());
}

hsBool plResponderComponent::Convert(plMaxNode* node, plErrorMsg* pErrMsg)
{
    IFixOldPB();

    // Create the commands for each state
    int numStates = fCompPB->Count(kResponderState);

    plResponderModifier *responder = IGetResponderMod(node);
    responder->fStates.SetCount(numStates);

    for (int i = 0; i < numStates; i++)
    {
        CmdIdxs cmdIdxs;

        IConvertCmds(node, pErrMsg, i, cmdIdxs);

        int numCallbacks = 0;
        ISetupDefaultWait(node, pErrMsg, i, cmdIdxs, numCallbacks);
        IConvertCmdWaits(node, pErrMsg, i, cmdIdxs, numCallbacks);

        IParamBlock2 *statePB = (IParamBlock2*)fCompPB->GetReferenceTarget(kResponderState, 0, i);
        responder->fStates[i].fNumCallbacks = numCallbacks;
        responder->fStates[i].fSwitchToState = statePB->GetInt(kStateCmdSwitch);
    }

    // Set the initial state
    responder->fCurState = fCompPB->GetInt(kResponderStateDef);
    responder->fEnabled = fCompPB->GetInt(kResponderEnabled) != 0;

    if (fCompPB->GetInt(kResponderTrigger))
        responder->fFlags |= plResponderModifier::kDetectTrigger;
    if (fCompPB->GetInt(kResponderUnTrigger))
        responder->fFlags |= plResponderModifier::kDetectUnTrigger;
    if (fCompPB->GetInt(kResponderSkipFFSound))
        responder->fFlags |= plResponderModifier::kSkipFFSound;

    // Unless it's been overridden somewhere else, don't save our state on the server
    if (!node->GetOverrideHighLevelSDL())
        responder->AddToSDLExcludeList(kSDLResponder);

    return true;
}

hsBool plResponderComponent::DeInit(plMaxNode *node, plErrorMsg *pErrMsg)
{
    fModKeys.clear();
    return true;
}

void plResponderComponent::IConvertCmds(plMaxNode* node, plErrorMsg* pErrMsg, int state, CmdIdxs& cmdIdxs)
{
    IParamBlock2 *statePB = (IParamBlock2*)fCompPB->GetReferenceTarget(kResponderState, 0, state);
    plResponderModifier *responder = IGetResponderMod(node);

    // Add the messages to the logic modifier
    for (int i = 0; i < statePB->Count(kStateCmdParams); i++)
    {
        plMessage *msg = nil;

        BOOL enabled = statePB->GetInt(kStateCmdEnabled, 0, i);
        if (!enabled)
            continue;

        IParamBlock2 *cmdPB = (IParamBlock2*)statePB->GetReferenceTarget(kStateCmdParams, 0, i);

        try
        {
            plResponderCmd *cmd = plResponderCmd::Find(cmdPB);
            if (cmd)
                msg = cmd->CreateMsg(node, pErrMsg, cmdPB);
        }
        catch (char *reason)
        {
            char buf[512];

            char stateName[128];
            const char *curStateName = fCompPB->GetStr(kResponderStateName, 0, state);
            if (curStateName && *curStateName != '\0')
                strcpy(stateName, fCompPB->GetStr(kResponderStateName, 0, state));
            else
                sprintf(stateName, "State %d", state+1);

            sprintf(buf,
                "A responder command failed to export.\n\nResponder:\t%s\nState:\t\t%s\nCommand:\t%d\n\nReason: %s",
                GetINode()->GetName(), stateName, i+1, reason);

            pErrMsg->Set(true, "Responder Warning", buf).Show();
            pErrMsg->Set(false);
        }

        if (msg)
        {
            msg->SetSender(responder->GetKey());
            responder->AddCommand(msg, state);
            int idx = responder->fStates[state].fCmds.Count()-1;
            cmdIdxs[i] = idx;
        }
    }
}

static IParamBlock2 *GetWaitBlk(IParamBlock2 *state, int idx)
{
    return (IParamBlock2*)state->GetReferenceTarget(kStateCmdWait, 0, idx);
}

void plResponderComponent::ISetupDefaultWait(plMaxNode* node, plErrorMsg* pErrMsg,
                                             int state, CmdIdxs& cmdIdxs, int &numCallbacks)
{
    IParamBlock2 *statePB = (IParamBlock2*)fCompPB->GetReferenceTarget(kResponderState, 0, state);
    plResponderModifier *responder = IGetResponderMod(node);
    hsTArray<plResponderModifier::plResponderCmd>& cmds = responder->fStates[state].fCmds;

    int numCmds = cmds.Count();
    for (int i = 0; i < numCmds; i++)
    {
        IParamBlock2 *waitPB = GetWaitBlk(statePB, i);
        ResponderWait::FixupWaitBlock(waitPB);

        // If we're supposed to wait for this command, and it converted, create a callback
        if (ResponderWait::GetWaitOnMe(waitPB) && cmdIdxs.find(i) != cmdIdxs.end())
        {
            int convertedIdx = cmdIdxs[i];

            ResponderWaitInfo waitInfo;
            waitInfo.responderName = GetINode()->GetName();
            waitInfo.receiver = responder->GetKey();
            waitInfo.callbackUser = numCallbacks++;
            waitInfo.msg = cmds[convertedIdx].fMsg;
            waitInfo.point = nil;

            IParamBlock2 *pb = (IParamBlock2*)statePB->GetReferenceTarget(kStateCmdParams, 0, i);
            plResponderCmd *cmd = plResponderCmd::Find(pb);

            cmd->CreateWait(node, pErrMsg, pb, waitInfo);
        }
    }
}

void plResponderComponent::IConvertCmdWaits(plMaxNode* node, plErrorMsg* pErrMsg,
                                            int state, CmdIdxs& cmdIdxs, int &numCallbacks)
{
    IParamBlock2 *statePB = (IParamBlock2*)fCompPB->GetReferenceTarget(kResponderState, 0, state);
    plResponderModifier *responder = IGetResponderMod(node);
    hsTArray<plResponderModifier::plResponderCmd>& cmds = responder->fStates[state].fCmds;

    int numWaits = statePB->Count(kStateCmdWait);
    for (int i = 0; i < numWaits; i++)
    {
        IParamBlock2 *waitPB = GetWaitBlk(statePB, i);

        int wait = ResponderWait::GetWaitingOn(waitPB);

        // If the waiter and waitee both converted, create the callback
        if (cmdIdxs.find(wait) != cmdIdxs.end() && cmdIdxs.find(i) != cmdIdxs.end())
        {
            int convertedIdx = cmdIdxs[wait];

            ResponderWaitInfo waitInfo;
            waitInfo.responderName = GetINode()->GetName();
            waitInfo.receiver = responder->GetKey();
            waitInfo.callbackUser = numCallbacks++;
            waitInfo.msg = cmds[convertedIdx].fMsg;
            waitInfo.point = ResponderWait::GetWaitPoint(waitPB);

            responder->AddCallback(state, convertedIdx, waitInfo.callbackUser);
            cmds[cmdIdxs[i]].fWaitOn = waitInfo.callbackUser;

            IParamBlock2 *pb = (IParamBlock2*)statePB->GetReferenceTarget(kStateCmdParams, 0, wait);
            plResponderCmd *cmd = plResponderCmd::Find(pb);

            cmd->CreateWait(node, pErrMsg, pb, waitInfo);
        }
    }
}

void plResponderComponent::IFixOldPB()
{
    if (fCompPB)
    {
        if (fCompPB->Count(kResponderState) == 0)
        {
            IParamBlock2 *pb = CreateParameterBlock2(&gStateBlock, nil);
            int idx = fCompPB->Append(kResponderState, 1, (ReferenceTarget**)&pb);
            pb->SetValue(kStateCmdSwitch, 0, idx);
        }
        if (fCompPB->Count(kResponderStateName) == 0)
        {
            char *name = "";
            fCompPB->Append(kResponderStateName, 1, &name);
        }

        // Make sure there is an enabled value for each command in the state
        for (int i = 0; i < fCompPB->Count(kResponderState); i++)
        {
            IParamBlock2* pb = (IParamBlock2*)fCompPB->GetReferenceTarget(kResponderState, 0, i);
            if (pb->Count(kStateCmdEnabled) != pb->Count(kStateCmdParams))
                pb->SetCount(kStateCmdEnabled, pb->Count(kStateCmdParams));
        }
    }
}

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

#define CUSTOM_DRAW

enum
{
    kStateName,
    kStateAdd,
    kStateRemove,
    kStateDefault,
    kStateCopy,
};

class plResponderProc : public ParamMap2UserDlgProc
{
protected:
    HWND fhDlg;
    IParamBlock2 *fPB;
    IParamBlock2 *fStatePB;
    int fCurState;
    
    plResponderComponent *fComp;

    IParamMap2 *fCmdMap;
    IParamMap2 *fWaitMap;
    
    int fCmdIdx;

    typedef std::map<int, const char*> NameID;
    NameID fNames;

    HMENU fhMenu;
    typedef std::pair<plResponderCmd*, int> CmdID;
    typedef std::map<int, CmdID> MenuCmd;
    MenuCmd fMenuCmds;

    HWND fhList;

    bool fIgnoreNextDrop;

public:
    plResponderProc();

    BOOL DlgProc(TimeValue t, IParamMap2 *pm, HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);
    void DeleteThis() { IRemoveCmdRollups(); }

protected:
    void ICreateMenu();
    void IAddMenuItem(HMENU hMenu, int id);

    void ICmdRightClick(HWND hCmdList);

    // Add and remove command rollups
    void ICreateCmdRollups();
    void IRemoveCmdRollups();
    IParamMap2 *ICreateMap(IParamBlock2 *pb);   // Helper

    const char* GetCommandName(int cmdIdx);
    void LoadList();

    BOOL DragListProc(HWND hWnd, DRAGLISTINFO *info);

#ifdef CUSTOM_DRAW
    void IDrawComboItem(DRAWITEMSTRUCT *dis);
#endif

    void LoadState();
    
    void AddCommand();
    void RemoveCurCommand();
    void MoveCommand(int oldIdx, int newIdx);

    // Takes a freshly created state PB and adds it as a new state, then returns its index
    int AddState(IParamBlock2 *pb);
};
static plResponderProc gResponderComponentProc;

void plResponderProc::IAddMenuItem(HMENU hMenu, int id)
{
    AppendMenu(hMenu, MF_STRING, id+1, fNames[id]);
}

#include "hsSTLSortUtils.h"

void plResponderProc::ICreateMenu()
{
    fhMenu = CreatePopupMenu();

    std::map<const char *, HMENU, stringSorter> menus;
    int cmdID = 0;

    for (int i = 0; i < gResponderCmds.size(); i++)
    {
        plResponderCmd *cmd = gResponderCmds[i];
        for (int j = 0; j < cmd->NumTypes(); j++)
        {
            HMENU hParent = fhMenu;

            const char *category = cmd->GetCategory(j);
            if (category)
            {
                // Menu for this category hasn't been created yet, make one
                if (menus.find(category) == menus.end())
                {
                    hParent = CreatePopupMenu();
                    menus[category] = hParent;
                    InsertMenu(fhMenu, 0, MF_BYPOSITION | MF_POPUP, (UINT)hParent, category);
                }
                else
                    hParent = menus[category];
            }

            const char *name = cmd->GetName(j);
            
            cmdID++;
            fMenuCmds[cmdID] = CmdID(cmd, j);
            AppendMenu(hParent, MF_STRING, cmdID, name);
        }
    }
}

plResponderProc::plResponderProc() : fCmdMap(nil), fCmdIdx(-1), fCurState(0), fhMenu(nil), fIgnoreNextDrop(false)
{
}

const char* plResponderProc::GetCommandName(int cmdIdx)
{
    static char buf[256];

    if (fStatePB->Count(kStateCmdParams) > cmdIdx)
    {
        buf[0] = '\0';

        BOOL enabled = fStatePB->GetInt(kStateCmdEnabled, 0, cmdIdx);
        if (!enabled)
            strcat(buf, "[D]");

        IParamBlock2 *cmdPB = (IParamBlock2*)fStatePB->GetReferenceTarget(kStateCmdParams, 0, cmdIdx);
        plResponderCmd *cmd = plResponderCmd::Find(cmdPB);

        IParamBlock2 *waitPB = (IParamBlock2*)fStatePB->GetReferenceTarget(kStateCmdWait, 0, cmdIdx);
        int waitingOn = ResponderWait::GetWaitingOn(waitPB);
        if (waitingOn != -1)
        {
            char num[10];
            sprintf(num, "(%d)", waitingOn+1);
            strcat(buf, num);
        }

        strcat(buf, cmd->GetInstanceName(cmdPB));

        return buf;
    }

    hsAssert(0, "Bad index to GetCommandName");
    return nil;
}

void plResponderProc::LoadList()
{
    ListBox_ResetContent(fhList);

    for (int i = 0; i < fStatePB->Count(kStateCmdParams); i++)
    {
        const char* name = GetCommandName(i);
        ListBox_AddString(fhList, name);
    }

    ListBox_SetCurSel(fhList, -1);
}

void plResponderProc::AddCommand()
{
    RECT rect;
    GetWindowRect(GetDlgItem(fhDlg, IDC_ADD_CMD), &rect);

    // Create the popup menu and get the option the user selects
    SetForegroundWindow(fhDlg);
    int type = TrackPopupMenu(fhMenu, TPM_RIGHTALIGN | TPM_NONOTIFY | TPM_RETURNCMD, rect.left, rect.top, 0, fhDlg, NULL);
    PostMessage(fhDlg, WM_USER, 0, 0);

    if (type == 0)
        return;

    CmdID& cmdID = fMenuCmds[type];
    plResponderCmd *cmd = cmdID.first;
    int cmdIdx = cmdID.second;

    IParamBlock2 *cmdPB = cmd->CreatePB(cmdIdx);
    fStatePB->Append(kStateCmdParams, 1, (ReferenceTarget**)&cmdPB);

    IParamBlock2 *waitPB = ResponderWait::CreatePB();
    fStatePB->Append(kStateCmdWait, 1, (ReferenceTarget**)&waitPB);

    BOOL enabled = TRUE;
    fStatePB->Append(kStateCmdEnabled, 1, &enabled);

    const char* name = GetCommandName(fStatePB->Count(kStateCmdParams)-1);
    int idx = ListBox_AddString(fhList, name);
    ListBox_SetCurSel(fhList, idx);

    ICreateCmdRollups();
}

void plResponderProc::RemoveCurCommand()
{
    int idx = ListBox_GetCurSel(fhList);
    if (idx == LB_ERR)
        return;

    // Destroy the current rollup, since it's this guy
    IRemoveCmdRollups();

    // Delete all traces of this command
    fStatePB->Delete(kStateCmdParams, idx, 1);
    fStatePB->Delete(kStateCmdWait, idx, 1);
    fStatePB->Delete(kStateCmdEnabled, idx, 1);
    ListBox_DeleteString(fhList, idx);

    // Patch the wait commands
    ResponderWait::CmdRemoved(fStatePB, idx);
    
    fCmdIdx = -1;
}

void plResponderProc::IRemoveCmdRollups()
{
    if (fCmdMap)
    {
        DestroyCPParamMap2(fCmdMap);
        fCmdMap = nil;
    }
    if (fWaitMap)
    {
        DestroyCPParamMap2(fWaitMap);
        fWaitMap = nil;
    }
}

IParamMap2 *plResponderProc::ICreateMap(IParamBlock2 *pb)
{
    ParamBlockDesc2 *pd = pb->GetDesc();

    // Don't show anything if there isn't a UI
    if (pd->Count() < 1)
    {
        pb->ReleaseDesc();
        return nil;
    }

    // Create the rollout
    IParamMap2 *map = CreateCPParamMap2(0,
                                        pb,
                                        GetCOREInterface(),
                                        hInstance,
                                        MAKEINTRESOURCE(pd->dlg_template),
                                        GetString(pd->title),
                                        pd->flags,
                                        pd->dlgProc,
                                        NULL,
                                        ROLLUP_CAT_STANDARD);

    // Save the rollout in the paramblock
    pb->SetMap(map);
    pb->ReleaseDesc();

    return map;
}

void plResponderProc::ICreateCmdRollups()
{
    // Get the index of the current command
    HWND hCmds = GetDlgItem(fhDlg, IDC_CMD_LIST);
    int cmdIdx = ListBox_GetCurSel(hCmds);

    if (cmdIdx != LB_ERR && cmdIdx != fCmdIdx)
    {
        fCmdIdx = cmdIdx;
        fIgnoreNextDrop = true;

        // Save the current scroll position and reset it at the end, so the panels
        // won't always jerk back up to the top
        IRollupWindow *rollup = GetCOREInterface()->GetCommandPanelRollup();
        int scrollPos = rollup->GetScrollPos();

        // Destroy the last command's rollups
        IRemoveCmdRollups();

        // Create the rollup for the current command
        IParamBlock2 *pb = (IParamBlock2*)fStatePB->GetReferenceTarget(kStateCmdParams, 0, fCmdIdx);
        fCmdMap = ICreateMap(pb);

        ResponderWait::InitDlg(fStatePB, fCmdIdx, GetDlgItem(fhDlg, IDC_CMD_LIST));
        pb = (IParamBlock2*)fStatePB->GetReferenceTarget(kStateCmdWait, 0, fCmdIdx);
        fWaitMap = ICreateMap(pb);

        rollup->SetScrollPos(scrollPos);
    }
}

BOOL plResponderProc::DragListProc(HWND hWnd, DRAGLISTINFO *info)
{
    static int oldIdx = -1;

    int curIdx = LBItemFromPt(info->hWnd, info->ptCursor, TRUE);

    switch (info->uNotification)
    {
        // Allow the drag
        case DL_BEGINDRAG:
            // When you click on an item in the listbox, the rollups are changed and Max can
            // shift the position of dialog you were just clicking in.  Since this happens
            // before you let go of the mouse button, the listbox thinks you are dragging.
            // To get around it, we don't allow a selection change and a drag in the same click.
            if (fIgnoreNextDrop)
            {
                SetWindowLong(hWnd, DWL_MSGRESULT, FALSE);
            }
            else
            {
                oldIdx = curIdx;
                SetWindowLong(hWnd, DWL_MSGRESULT, TRUE);
            }
            return TRUE;

        case DL_DRAGGING:
            {
                if (curIdx < oldIdx)
                    DrawInsert(hWnd, info->hWnd, curIdx);
                else if (curIdx > oldIdx && ListBox_GetCount(info->hWnd) > curIdx+1)
                    DrawInsert(hWnd, info->hWnd, curIdx+1);
                else
                    DrawInsert(hWnd, info->hWnd, -1);
            }
            return TRUE;

        case DL_CANCELDRAG:
            // Clear drag arrow
            DrawInsert(hWnd, info->hWnd, -1);
            return TRUE;

        case DL_DROPPED:
        {
            if (fIgnoreNextDrop)
            {
                fIgnoreNextDrop = false;
                return TRUE;
            }

            // Clear drag arrow
            DrawInsert(hWnd, info->hWnd, -1);

            if (curIdx != -1 && oldIdx != -1 && curIdx != oldIdx)
            {
                // Make sure this won't mess up any wait commands, or at least
                // that the user approves if it does.
                if (!ResponderWait::ValidateCmdMove(fStatePB, oldIdx, curIdx))
                    return TRUE;

                MoveCommand(oldIdx, curIdx);
            }

            return TRUE;
        }
    }

    return FALSE;
}

#ifdef CUSTOM_DRAW
void plResponderProc::IDrawComboItem(DRAWITEMSTRUCT *dis)
{
    if (dis->itemID == -1)          // empty item
        return; 

    // The colors depend on whether the item is selected. 
    COLORREF clrForeground = SetTextColor(dis->hDC, 
        GetSysColor(dis->itemState & ODS_SELECTED ? COLOR_HIGHLIGHTTEXT : COLOR_WINDOWTEXT)); 

    COLORREF clrBackground = SetBkColor(dis->hDC, 
        GetSysColor(dis->itemState & ODS_SELECTED ? COLOR_HIGHLIGHT : COLOR_WINDOW)); 

    // Calculate the vertical and horizontal position.
    TEXTMETRIC tm;
    GetTextMetrics(dis->hDC, &tm);
    int y = (dis->rcItem.bottom + dis->rcItem.top - tm.tmHeight) / 2;
    int x = LOWORD(GetDialogBaseUnits()) / 4;

    // If this is a command, not a state, make it bold
    HFONT oldFont = nil;
    if (dis->itemData != kStateName)
    {
        LOGFONT lf;
        memset(&lf, 0, sizeof(lf));
        lf.lfHeight = tm.tmHeight;
        lf.lfWeight = FW_BOLD;
        GetTextFace(dis->hDC, LF_FACESIZE, lf.lfFaceName);
        HFONT boldFont = CreateFontIndirect(&lf);
        oldFont = SelectFont(dis->hDC, boldFont);
    }

    // Get and display the text for the list item.
    char buf[256];
    ComboBox_GetLBText(dis->hwndItem, dis->itemID, buf);
    if (fPB->GetInt(kResponderStateDef) == dis->itemID)
    {
        char buf2[256];
        sprintf(buf2, "* %s", buf);
        strcpy(buf, buf2);
    }

    ExtTextOut(dis->hDC, x, y, ETO_CLIPPED | ETO_OPAQUE, &dis->rcItem, buf, strlen(buf), NULL); 

    // Restore the previous colors. 
    SetTextColor(dis->hDC, clrForeground); 
    SetBkColor(dis->hDC, clrBackground); 

    if (oldFont)
        DeleteFont(SelectFont(dis->hDC, oldFont));

    // If the item has the focus, draw focus rectangle. 
    if (dis->itemState & ODS_FOCUS) 
        DrawFocusRect(dis->hDC, &dis->rcItem); 
}
#endif

void plResponderProc::LoadState()
{
    fStatePB = (IParamBlock2*)fPB->GetReferenceTarget(kResponderState, 0, fCurState);

    IRemoveCmdRollups();
    LoadList();

    HWND hSwitchCombo = GetDlgItem(fhDlg, IDC_SWITCH_COMBO);
    ComboBox_SetCurSel(hSwitchCombo, fStatePB->GetInt(kStateCmdSwitch));
}

// THE MAGICAL TURDFEST.  Max's default RemapDir tries to always clone your referenced
// objects.  So when we reference an INode it wants to make a clone of that INode
// (and fails for some reason).  To get around this I just check if the object to be cloned
// is a paramblock, and actually clone it if it is.  Otherwise, I just return the object.
//
// UPDATE: Looks like it's only with ResponderComponents.  Probably the parentless PB's (which
// exist due to another bug).  Who cares, this works.
class MyRemapDir : public RemapDir
{
public:
    RefTargetHandle CloneRef(RefTargetHandle oldTarg)
    {
        if (oldTarg == NULL)
            return NULL;
        else if (oldTarg->SuperClassID() == PARAMETER_BLOCK2_CLASS_ID)
            return oldTarg->Clone(*this);
        else
            return oldTarg;
    }

    void PatchPointer(RefTargetHandle* patchThis, RefTargetHandle oldTarg) { hsAssert(0, "shit"); }
    RefTargetHandle FindMapping(RefTargetHandle from) { hsAssert(0, "shit"); return NULL; }
    void AddEntry(RefTargetHandle hfrom, RefTargetHandle hto) { hsAssert(0, "shit"); }
    void Backpatch() { hsAssert(0, "shit"); }
    void Clear() { hsAssert(0, "shit"); }
    void DeleteThis() { hsAssert(0, "shit"); }
    void AddPostPatchProc(PostPatchProc* proc, bool toDelete) { hsAssert(0, "shit"); }
};
// Even turdier - I had to define this to compile
RefTargetHandle RemapDir::CloneRef(RefTargetHandle oldTarg) { return NULL; }
static MyRemapDir gMyRemapDir;

RefTargetHandle plResponderComponent::Clone(RemapDir &remap)
{
    plComponentBase *obj = (plComponentBase*)fClassDesc->Create(false);
    // Do the base clone
    BaseClone(this, obj, remap);
    // Copy our references
    obj->ReplaceReference(kRefComp, fCompPB->Clone(gMyRemapDir));
    obj->ReplaceReference(kRefTargs, fTargsPB->Clone(remap));

    return obj;
}

BOOL plResponderProc::DlgProc(TimeValue t, IParamMap2 *pm, HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    static UINT dragListMsg = 0;

    if (dragListMsg != 0 && msg == dragListMsg)
        if (DragListProc(hWnd, (DRAGLISTINFO*)lParam))
            return TRUE;

    switch (msg)
    {
    case WM_INITDIALOG:
        {
            if (!fhMenu)
                ICreateMenu();

            fhDlg = hWnd;
            fhList = GetDlgItem(fhDlg, IDC_CMD_LIST);
            fCurState = 0;
            fCmdIdx = -1;
            
            fPB = pm->GetParamBlock();
            fComp = (plResponderComponent*)fPB->GetOwner();

            fComp->IFixOldPB();

            LoadState();
            
            // Make it so the user can drag commands to different positions
            dragListMsg = RegisterWindowMessage(DRAGLISTMSGSTRING);
            MakeDragList(GetDlgItem(hWnd, IDC_CMD_LIST));

            // Setup the State Name combo
            HWND hStateName = GetDlgItem(hWnd, IDC_STATE_COMBO);
            ComboBox_LimitText(hStateName, 256);

// I give up, Windows doesn't want to tell me the real font size
#if 0//def CUSTOM_DRAW
            // TEMP
            HDC hDC = GetDC(hStateName);
            HFONT sysFont = (HFONT)GetStockObject(DEFAULT_GUI_FONT);
            HFONT oldFont = SelectFont(hDC, sysFont);

            TEXTMETRIC tm;
            GetTextMetrics(hDC, &tm);
            ComboBox_SetItemHeight(hStateName, 0, tm.tmHeight+2);

            DeleteFont(SelectFont(hDC, oldFont));
            ReleaseDC(hStateName, hDC);
#endif

            // Add the commands
            int idx = ComboBox_AddString(hStateName, "Add State");
            ComboBox_SetItemData(hStateName, idx, kStateAdd);
            idx = ComboBox_AddString(hStateName, "Remove Current State");
            ComboBox_SetItemData(hStateName, idx, kStateRemove);
            idx = ComboBox_AddString(hStateName, "Set Current as Default");
            ComboBox_SetItemData(hStateName, idx, kStateDefault);
            idx = ComboBox_AddString(hStateName, "Copy Current State");
            ComboBox_SetItemData(hStateName, idx, kStateCopy);

            HWND hSwitchCombo = GetDlgItem(hWnd, IDC_SWITCH_COMBO);

            int numStates = fPB->Count(kResponderStateName);
            for (int i = 0; i < numStates; i++)
            {
                const char *stateName = fPB->GetStr(kResponderStateName, 0, i);
                char buf[128];
                if (!stateName || *stateName == '\0')
                {
                    sprintf(buf, "State %d", i+1);
                    stateName = buf;
                }
                ComboBox_InsertString(hStateName, i, stateName);
                ComboBox_AddString(hSwitchCombo, stateName);
            }

            ComboBox_SetCurSel(hStateName, fCurState);

            ComboBox_SetCurSel(hSwitchCombo, fStatePB->GetInt(kStateCmdSwitch));
        }
        return TRUE;

#ifdef CUSTOM_DRAW
    case WM_DRAWITEM:
        if (wParam == IDC_STATE_COMBO)
        {
            IDrawComboItem((DRAWITEMSTRUCT*)lParam);
            return TRUE;
        }
        break;
#endif

    case WM_SETCURSOR:
        {
            if (HIWORD(lParam) == WM_RBUTTONDOWN && HWND(wParam) == GetDlgItem(hWnd, IDC_CMD_LIST))
            {
                ICmdRightClick(HWND(wParam));
                return TRUE;
            }
        }
        break;
    
    case WM_COMMAND:
        if (HIWORD(wParam) == BN_CLICKED)
        {
            if (LOWORD(wParam) == IDC_ADD_ACTIVATOR)
            {
                // Adding an activator.  Set it and refresh the UI to show it in our list.
                plPick::Activator(fPB, kResponderActivators, false);
                pm->Invalidate(kResponderActivators);
                return TRUE;
            }
            else if (LOWORD(wParam) == IDC_ADD_CMD)
            {
                AddCommand();
                return TRUE;
            }
            // Remove the currently selected condition
            else if (LOWORD(wParam) == IDC_REMOVE_CMD)
            {
                RemoveCurCommand();
                return TRUE;
            }
        }
        else if (HIWORD(wParam) == LBN_SELCHANGE && LOWORD(wParam) == IDC_CMD_LIST)
        {
            ICreateCmdRollups();
            return TRUE;
        }
        else if (HIWORD(wParam) == CBN_SELCHANGE && LOWORD(wParam) == IDC_SWITCH_COMBO)
        {
            int sel = ComboBox_GetCurSel((HWND)lParam);
            if (sel != CB_ERR)
                fStatePB->SetValue(kStateCmdSwitch, 0, sel);
        }
        else if (LOWORD(wParam) == IDC_STATE_COMBO)
        {
            HWND hCombo = (HWND)lParam;
            int code = HIWORD(wParam);

            // Disable accelerators when the combo has focus, so that new names can be typed in
            if (code == CBN_SETFOCUS)
            {
                plMaxAccelerators::Disable();
                return TRUE;
            }
            else if (code == CBN_KILLFOCUS)
            {
                plMaxAccelerators::Enable();
                return TRUE;
            }
            // State name changed, save it in the PB
            else if (code == CBN_EDITCHANGE)
            {
                char buf[256];
                ComboBox_GetText(hCombo, buf, sizeof(buf));
                const char *curName = fPB->GetStr(kResponderStateName, 0, fCurState);
                if (!curName || strcmp(buf, curName))
                {
                    HWND hSwitch = GetDlgItem(hWnd, IDC_SWITCH_COMBO);
                    int sel = ComboBox_GetCurSel(hSwitch);
                    ComboBox_DeleteString(hSwitch, fCurState);
                    ComboBox_InsertString(hSwitch, fCurState, buf);
                    ComboBox_SetCurSel(hSwitch, sel);
                    
                    fPB->SetValue(kResponderStateName, 0, buf, fCurState);
                    ComboBox_DeleteString(hCombo, fCurState);
                    ComboBox_InsertString(hCombo, fCurState, buf);
//                  ComboBox_SetCurSel(hCombo, fCurState);
                }

                return TRUE;
            }
            else if (code == CBN_SELCHANGE)
            {
                int sel = ComboBox_GetCurSel(hCombo);
                int type = ComboBox_GetItemData(hCombo, sel);

                if (type == kStateAdd)
                {
                    IParamBlock2 *pb = CreateParameterBlock2(&gStateBlock, nil);
                    fCurState = AddState(pb);
                    fCmdIdx = -1;
                }
                else if (type == kStateRemove)
                {
                    int count = fPB->Count(kResponderState);
                    // Don't let the user remove the last state
                    if (count == 1)
                    {
                        hsMessageBox("You must have at least one state.", "Error", hsMessageBoxNormal);
                        ComboBox_SetCurSel(hCombo, fCurState);
                        return TRUE;
                    }
                    // Verify that the user really wants to delete the state
                    else
                    {
                        int ret = hsMessageBox("Are you sure you want to remove this state?", "Verify Remove", hsMessageBoxYesNo);
                        if (ret == hsMBoxNo)
                        {
                            ComboBox_SetCurSel(hCombo, fCurState);
                            return TRUE;
                        }
                    }

                    fPB->Delete(kResponderState, fCurState, 1);
                    fPB->Delete(kResponderStateName, fCurState, 1);

                    ComboBox_DeleteString(hCombo, fCurState);
                    ComboBox_SetCurSel(hCombo, 0);

                    HWND hSwitch = GetDlgItem(hWnd, IDC_SWITCH_COMBO);
                    ComboBox_DeleteString(hSwitch, fCurState);

                    // If the deleted state was the default, set the default to the first
                    int defState = fPB->GetInt(kResponderStateDef);
                    if (fCurState == defState)
                        fPB->SetValue(kResponderStateDef, 0, 0);
                    else if (fCurState < defState)
                        fPB->SetValue(kResponderStateDef, 0, defState-1);

                    // Patch up the switch commands
                    for (int i = fCurState; i < fPB->Count(kResponderState); i++)
                    {
                        IParamBlock2 *pb = (IParamBlock2*)fPB->GetReferenceTarget(kResponderState, 0, i);

                        int switchState = pb->GetInt(kStateCmdSwitch);
                        // TODO: might want to warn about this
                        if (switchState == fCurState)
                            pb->SetValue(kStateCmdSwitch, 0, 0);
                        else if (switchState > fCurState)
                            pb->SetValue(kStateCmdSwitch, 0, switchState-1);
                    }

                    fCurState = 0;
                    fCmdIdx = -1;
                }
                else if (type == kStateDefault)
                {
                    // Set the current state as the default
                    fPB->SetValue(kResponderStateDef, 0, fCurState);
                    ComboBox_SetCurSel(hCombo, fCurState);
                }
                else if (type == kStateCopy)
                {
                    // Clone the state PB
                    IParamBlock2 *origPB = (IParamBlock2*)fPB->GetReferenceTarget(kResponderState, 0, fCurState);
                    IParamBlock2 *copyPB = (IParamBlock2*)origPB->Clone(gMyRemapDir);
                    fCurState = AddState(copyPB);
                    fCmdIdx = -1;
                }
                else
                {
                    fCurState = sel;
                    fCmdIdx = -1;
                }

                LoadState();

                return TRUE;
            }
        }
    }

    return FALSE;
}

void plResponderProc::ICmdRightClick(HWND hCmdList)
{
    // Get the position of the cursor in screen and tree client coords
    POINT point, localPoint;
    GetCursorPos(&point);
    localPoint = point;
    ScreenToClient(hCmdList, &localPoint);

    LRESULT res = SendMessage(hCmdList, LB_ITEMFROMPOINT, 0, MAKELPARAM(localPoint.x, localPoint.y));
    WORD index = LOWORD(res);
    if (index == WORD(LB_ERR))
        return;

    RECT rect;
    SendMessage(hCmdList, LB_GETITEMRECT, index, (LPARAM)&rect);

    // Make sure we're actually ON an item, LB_ITEMFROMPOINT get the closest instead of exact
    if (localPoint.y >= rect.top && localPoint.y <= rect.bottom)
    {
        BOOL enabled = fStatePB->GetInt(kStateCmdEnabled, 0, index);

        HMENU hMenu = CreatePopupMenu();
        AppendMenu(hMenu, MF_STRING, 1, enabled ? "Disable" : "Enable");

        SetForegroundWindow(fhDlg);
        int sel = TrackPopupMenu(hMenu, TPM_NONOTIFY | TPM_RETURNCMD, point.x, point.y, 0, fhDlg, NULL);
        if (sel == 1)
        {
            fStatePB->SetValue(kStateCmdEnabled, 0, !enabled, index);

            ListBox_DeleteString(hCmdList, index);
            ListBox_InsertString(hCmdList, index, GetCommandName(index));
        }

        DestroyMenu(hMenu);
    }
}

int plResponderProc::AddState(IParamBlock2 *pb)
{
    int idx = fPB->Append(kResponderState, 1, (ReferenceTarget**)&pb);
    pb->SetValue(kStateCmdSwitch, 0, idx);

    char *name = "";
    fPB->Append(kResponderStateName, 1, &name);

    HWND hCombo = GetDlgItem(fhDlg, IDC_STATE_COMBO);
    char buf[128];
    sprintf(buf, "State %d", idx+1);
    ComboBox_InsertString(hCombo, idx, buf);
    ComboBox_SetCurSel(hCombo, idx);

    HWND hSwitch = GetDlgItem(fhDlg, IDC_SWITCH_COMBO);
    ComboBox_AddString(hSwitch, buf);

    return idx;
}

void plResponderProc::MoveCommand(int oldIdx, int newIdx)
{
    // Move data
    int insertIdx = (newIdx > oldIdx) ? newIdx+1 : newIdx;
    int deleteIdx = (newIdx < oldIdx) ? oldIdx+1 : oldIdx;

    ReferenceTarget *targ = fStatePB->GetReferenceTarget(kStateCmdParams, 0, oldIdx);
    fStatePB->Insert(kStateCmdParams, insertIdx, 1, &targ);
    fStatePB->Delete(kStateCmdParams, deleteIdx, 1);

    ReferenceTarget *wait = fStatePB->GetReferenceTarget(kStateCmdWait, 0, oldIdx);
    fStatePB->Insert(kStateCmdWait, insertIdx, 1, &wait);
    fStatePB->Delete(kStateCmdWait, deleteIdx, 1);

    BOOL oldEnabled = fStatePB->GetInt(kStateCmdEnabled, 0, oldIdx);
    BOOL newEnabled = fStatePB->GetInt(kStateCmdEnabled, 0, newIdx);
    fStatePB->SetValue(kStateCmdEnabled, 0, oldEnabled, newIdx);
    fStatePB->SetValue(kStateCmdEnabled, 0, newEnabled, oldIdx);

    ResponderWait::CmdMoved(fStatePB, oldIdx, newIdx);

    LoadList();

    // Reselect item
    // (This doesn't send the LBN_SELCHANGE message so we do that manually)
    ListBox_SetCurSel(fhList, newIdx);
    ICreateCmdRollups();
}