/*==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();
}