/*==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==*/
//////////////////////////////////////////////////////////////////////////////
//																			//
//	pfJournalBook Class 													//
//	A generic, high-level, abstract method of creating various Myst-like	//
//	books within the game with very little effort, while ensuring that they	//
//	all remain consistent in appearance and operability.					//
//																			//
//////////////////////////////////////////////////////////////////////////////

#include "pfJournalBook.h"
#include "hsUtils.h"
#include "hsStlUtils.h"
#include "hsResMgr.h"
#include "pcSmallRect.h"
#include "plgDispatch.h"
#include "../pfGameGUIMgr/pfGUIDialogMod.h"
#include "../pfGameGUIMgr/pfGUIControlMod.h"
#include "../pfGameGUIMgr/pfGUICheckBoxCtrl.h"
#include "../pfGameGUIMgr/pfGUIDialogHandlers.h"
#include "../pfGameGUIMgr/pfGUIDynDisplayCtrl.h"
#include "../pfGameGUIMgr/pfGUIClickMapCtrl.h"
#include "../pfGameGUIMgr/pfGUIButtonMod.h"
#include "../pfGameGUIMgr/pfGUIProgressCtrl.h"
#include "../pfGameGUIMgr/pfGUIMultiLineEditCtrl.h"

#include "../pfMessage/pfGUINotifyMsg.h"
#include "../plGImage/plMipmap.h"
#include "../plGImage/plDynamicTextMap.h"
#include "../plPipeline/hsGDeviceRef.h"
#include "../plMessage/plAnimCmdMsg.h"
#include "../pnKeyedObject/plFixedKey.h"
#include "../pnMessage/plRefMsg.h"
#include "../pnMessage/plTimeMsg.h"
#include "../plMessage/plLayRefMsg.h"
#include "../plMessage/plMatRefMsg.h"
#include "../plSurface/plLayerInterface.h"
#include "../plSurface/plLayer.h"
#include "../plSurface/hsGMaterial.h"
#include "../plAgeLoader/plAgeLoader.h"
#include "../pfSurface/plLayerBink.h"

// So we can do image searches in our local age
#include "../plNetClient/plNetClientMgr.h"
#include "../plResMgr/plKeyFinder.h"

// For notify sends
#include "../pnMessage/plNotifyMsg.h"
#include "../pnTimer/plTimerCallbackManager.h"
#include "../plMessage/plTimerCallbackMsg.h"

// For custom cursors
#include "../plInputCore/plInputInterface.h"

// For measuring text
#include "../plGImage/plFont.h"

// For SFX
#include "hsTimer.h"



//////////////////////////////////////////////////////////////////////////////
//// pfEsHTMLChunk Class /////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////

class pfEsHTMLChunk
{
	public:

		std::wstring fText;	// Paragraph text, or face name
		plKey	fImageKey;	// Key of image
		UInt8	fFontSize;
		UInt32	fFlags;
		UInt8	fType;
		UInt32	fEventID;

		pcSmallRect	fLinkRect;		// Used only for image chunks, and only when stored in the fVisibleLinks array

		hsColorRGBA	fColor;

		UInt16		fAbsoluteX, fAbsoluteY;

		hsScalar	fCurrOpacity;	// For SFX images
		hsScalar	fSFXTime;		// For SFX images
		hsScalar	fMinOpacity, fMaxOpacity;	

		hsColorRGBA	fCurrColor;
		hsColorRGBA	fOffColor, fOnColor;

		bool	fNoResizeImg;
		Int16	fLineSpacing;
		bool	fTintDecal;
		bool	fLoopMovie;
		bool	fOnCover; // if true, the movie is on the cover
		UInt8	fMovieIndex; // the index of the movie in the source code, used for identification

		enum Flags
		{
			kFontBold	= 0x00000001,
			kFontItalic = 0x00000002,
			kFontRegular= 0x00000004,		// 'cause 0 means "style not defined"
			kFontMask	= kFontBold | kFontItalic | kFontRegular,

			kFontColor	= 0x00000008,
			kFontSpacing= 0x00000010,

			kCenter		= 0x00000001,
			kLeft		= 0x00000002,
			kRight		= 0x00000003,
			kAlignMask	= 0x00000003,

			kBlendAlpha	= 0x00000004,
			kCanLink	= 0x00000008,
			kFloating	= 0x00000010,
			kGlowing	= 0x00000020,
			kActAsCB	= 0x00000040,	// Cause the image to act in a checkbox-like fashion. 
										// Min opacity turns into "off opacity" and max opacity
										// is "on opacity"
			kChecked	= 0x00000080,	// Only for kActAsCB, set if it's currently "checked"
			kTranslucent= 0x00000100	// is the image translucent? if so, use fCurrOpacity
		};

		enum Types
		{
			kEmpty = 0,
			kParagraph,
			kImage,
			kPageBreak,
			kFontChange,
			kMargin,
			kCover,			// Just a placeholder, never actually used after compile time
			kBook,			// another placeholder
			kDecal,
			kMovie,
			kEditable		// placeholder, ver 3.0
		};

		// Paragraph constructor
		pfEsHTMLChunk( const wchar_t *text )
		{
			fType = kParagraph;
			if (text)
				fText = text;
			else
				fText = L"";
			fFlags = kLeft;
			fFontSize = 0;
			fImageKey = nil;
			fEventID = 0;
			fColor.Set( 0.f, 0.f, 0.f, 1.f );
			fAbsoluteX = fAbsoluteY = 0;
			fCurrOpacity = 1.f;
			fMinOpacity = 0.f;
			fMaxOpacity = 1.f;
			fNoResizeImg = false;
			fLineSpacing = 0;
			fTintDecal = false;
			fLoopMovie = true;
			fOnCover = false;
			fMovieIndex = -1;
		}

		// Image constructor (used for decals and movies too)
		pfEsHTMLChunk( plKey imageKey, UInt32 alignFlags )
		{
			fType = kImage;
			fText = L"";
			fFlags = alignFlags;
			fFontSize = 0;
			fImageKey = imageKey;
			fEventID = 0;
			fColor.Set( 0.f, 0.f, 0.f, 1.f );
			fAbsoluteX = fAbsoluteY = 0;
			fCurrOpacity = 1.f;
			fMinOpacity = 0.f;
			fMaxOpacity = 1.f;
			fNoResizeImg = false;
			fLineSpacing = 0;
			fTintDecal = false;
			fLoopMovie = true;
			fOnCover = false;
			fMovieIndex = -1;
		}

		// Page break constructor
		pfEsHTMLChunk()
		{
			fType = kPageBreak;
			fText = L"";
			fImageKey = nil;
			fFontSize = 0;
			fFlags = 0;
			fEventID = 0;
			fColor.Set( 0.f, 0.f, 0.f, 1.f );
			fAbsoluteX = fAbsoluteY = 0;
			fCurrOpacity = 1.f;
			fMinOpacity = 0.f;
			fMaxOpacity = 1.f;
			fNoResizeImg = false;
			fLineSpacing = 0;
			fTintDecal = false;
			fLoopMovie = true;
			fOnCover = false;
			fMovieIndex = -1;
		}

		// Font change constructor
		pfEsHTMLChunk( const wchar_t *face, UInt8 size, UInt32 fontFlags )
		{
			fType = kFontChange;
			if (face)
				fText = face;
			else
				fText = L"";
			fFontSize = size;
			fFlags = fontFlags;
			fImageKey = nil;
			fEventID = 0;
			fColor.Set( 0.f, 0.f, 0.f, 1.f );
			fAbsoluteX = fAbsoluteY = 0;
			fCurrOpacity = 1.f;
			fMinOpacity = 0.f;
			fMaxOpacity = 1.f;
			fNoResizeImg = false;
			fLineSpacing = 0;
			fTintDecal = false;
			fLoopMovie = true;
			fOnCover = false;
			fMovieIndex = -1;
		}

		~pfEsHTMLChunk() {}
};

//////////////////////////////////////////////////////////////////////////////
//// Our Template Dialog Handler /////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////

class pfJournalDlgProc : public pfGUIDialogProc
{
	protected:

		pfBookData	*fBook;

	public:

		enum TagIDs
		{
			kTagBookCover = 100,
			kTagLeftDTMap = 101,
			kTagRightDTMap = 102,
			kTagTurnFrontDTMap = 103,
			kTagTurnBackDTMap = 104,
			kTagTurnPageCtrl = 105,
			kTagLeftPageBtn = 106,
			kTagRightPageBtn = 107,
			kTagOutsideBookBtn = 108,
			kTagCoverLayer = 109,
			kTagLeftCornerBtn = 110,
			kTagRightCornerBtn = 111,
			kTagWidthCBDummy = 112,
			kTagHeightCBDummy = 113,
			kTagLeftEditCtrl = 120,
			kTagRightEditCtrl = 121,
			kTagTurnFrontEditCtrl = 122,
			kTagTurnBackEditCtrl = 123
		};

		pfJournalDlgProc( pfBookData *book ) : fBook( book )
		{
		}

		virtual ~pfJournalDlgProc()
		{
		}

		virtual void	DoSomething( pfGUIControlMod *ctrl )
		{
			if ( fBook && fBook->fCurrBook )
			{
				if( ctrl->GetTagID() == kTagBookCover )
				{
					fBook->fCurrBook->Open();
				}
				else if( ctrl->GetTagID() == kTagLeftPageBtn )
				{
					// only turn pages if the book is actually open
					if( fBook->fCurrentlyOpen )
						fBook->fCurrBook->IHandleLeftSideClick();
				}
				else if( ctrl->GetTagID() == kTagRightPageBtn )
				{
					// only turn pages if the book is actually open
					if( fBook->fCurrentlyOpen )
						fBook->fCurrBook->IHandleRightSideClick();
				}
				else if( ctrl->GetTagID() == kTagOutsideBookBtn )
				{
					if( fBook->fCurrentlyOpen )
						fBook->fCurrBook->CloseAndHide();
					else
						fBook->fCurrBook->Hide();
				}
			}
		}

		// Called on dialog init (i.e. first showing, before OnShow() is called), only ever called once
		virtual void	OnInit( void )
		{
		}

		// Called before the dialog is shown, always after OnInit()
		virtual void	OnShow( void )
		{
		}

		// Called before the dialog is hidden
		virtual void	OnHide( void )
		{
		}

		// Called on the dialog's destructor, before it's unregistered with the game GUI manager
		virtual void	OnDestroy( void )
		{
		}

		// Called when the dialog's focused control changes
		virtual void	OnCtrlFocusChange( pfGUIControlMod *oldCtrl, pfGUIControlMod *newCtrl )
		{
		}

		// Called when the key bound to a GUI event is pressed. Only called on the top modal dialog
		virtual void	OnControlEvent( ControlEvt event )
		{
			if( event == kExitMode )
			{
				if( fBook->fCurrentlyOpen )
					fBook->fCurrBook->CloseAndHide();
				else
					fBook->fCurrBook->Hide();
			}
		}

		virtual void	HandleExtendedEvent( pfGUIControlMod *ctrl, UInt32 event )
		{
			if (fBook)
			{
				if( ctrl == fBook->fLeftPageMap )
				{
					if( event == pfGUIClickMapCtrl::kMouseHovered )
					{
						if (fBook->fCurrBook)
						{
							// Update our custom cursor on the map
							Int32 idx = fBook->fCurrBook->IFindCurrVisibleLink( false, true );
							if( idx != -1 )
								fBook->fLeftPageMap->SetCustomCursor( plInputInterface::kCursorPoised/*Hand*/ );
							else if(( fBook->fCurrBook->fCurrentPage > 1 )&&( fBook->fCurrBook->fAllowTurning ))
								fBook->fLeftPageMap->SetCustomCursor( plInputInterface::kCursorLeft );
							else if ((fBook->fCurrBook->fAreEditing) && !(fBook->fLeftEditCtrl->ShowingBeginningOfBuffer())) // if we have more buffer to show
								fBook->fLeftPageMap->SetCustomCursor( plInputInterface::kCursorLeft );
							else
								fBook->fLeftPageMap->SetCustomCursor( plInputInterface::kCursorUp );
						}
					}
				}
				else if( ctrl == fBook->fRightPageMap )
				{
					if( event == pfGUIClickMapCtrl::kMouseHovered )
					{
						if (fBook->fCurrBook)
						{
							// Update our custom cursor on the map
							Int32 idx = fBook->fCurrBook->IFindCurrVisibleLink( true, true );
							if( idx != -1 )
								fBook->fRightPageMap->SetCustomCursor( plInputInterface::kCursorPoised/*Hand*/ );
							else if((fBook->fCurrBook->fAreWeShowing) && ( fBook->fCurrBook->fCurrentPage + 2 <= fBook->fCurrBook->fLastPage )&&( fBook->fCurrBook->fAllowTurning ))
								fBook->fRightPageMap->SetCustomCursor( plInputInterface::kCursorRight );
							else if((fBook->fCurrBook->fAreEditing) && !(fBook->fRightEditCtrl->ShowingEndOfBuffer())) // if we have more buffer to show
								fBook->fRightPageMap->SetCustomCursor( plInputInterface::kCursorRight );
							else
								fBook->fRightPageMap->SetCustomCursor( plInputInterface::kCursorUp );
						}
					}
				}
			}
		}
};

//// Multiline edit handler class ////////////////////////////////////////////

class pfBookMultiLineEditProc : public pfGUIMultiLineEditProc
{
private:
	pfBookData *bookData;
public:
	pfBookMultiLineEditProc(pfBookData *owner) { bookData = owner; }
	virtual ~pfBookMultiLineEditProc() {}

	virtual OnEndOfControlList(Int32 cursorPos) { bookData->HitEndOfControlList(cursorPos); }
	virtual OnBeginningOfControlList(Int32 cursorPos) { bookData->HitBeginningOfControlList(cursorPos); }
};

//// Book data class /////////////////////////////////////////////////////////

pfBookData::pfBookData(const char *guiName /* = nil */)
{
	fCurrBook = nil;
	fDialog = nil;
	fCoverButton = fTurnPageButton = nil;
	fLeftPageMap = fRightPageMap = nil;
	fCoverLayer = nil;
	fCoverMaterial = nil;
	UInt16 i;
	for (i=0; i<4; i++)
		fPageMaterials[i] = nil;
	fLeftCorner = fRightCorner = nil;
	fWidthCtrl = fHeightCtrl = nil;
	fCurrSFXPages = kNoSides;
	fBaseSFXTime = 0.f;
	fResetSFXFlag = false;
	fSFXUpdateFlip = false;
	fCurrentlyTurning = false;

	fRightEditCtrl = fLeftEditCtrl = nil;
	fTurnFrontEditCtrl = fTurnBackEditCtrl = nil;
	fEditable = false;
	fAdjustCursorTo = -1;
	
	if (guiName)
		fGUIName = guiName;
	else
		fGUIName = "BkBook";
}

pfBookData::~pfBookData()
{
	RegisterForSFX( kNoSides );
}

void pfBookData::LoadGUI()
{
	// has the dialog been loaded yet?
	if (!pfGameGUIMgr::GetInstance()->IsDialogLoaded(fGUIName.c_str()))
		// no then load and set handler
		pfGameGUIMgr::GetInstance()->LoadDialog(fGUIName.c_str(), GetKey(), "GUI");
	else
		// yes then just set the handler
		pfGameGUIMgr::GetInstance()->SetDialogToNotify(fGUIName.c_str(), GetKey());
}

hsBool pfBookData::MsgReceive(plMessage *pMsg)
{
	plGenRefMsg *ref = plGenRefMsg::ConvertNoRef(pMsg);
	if(ref != nil)
	{
		if(ref->fType == kRefDialog)
		{
			if(ref->GetContext() & (plRefMsg::kOnCreate | plRefMsg::kOnRequest | plRefMsg::kOnReplace))
			{
				pfGUIDialogMod *temp = pfGUIDialogMod::ConvertNoRef(ref->GetRef());
				if (temp != nil) // sanity check
					fDialog = temp;
			}
			/*else
			{
				fDialog = nil;
				fCoverButton = nil;
			}*/
			return true;
		}
		else if(ref->fType == kRefDefaultCover)
		{
			if(ref->GetContext() & (plRefMsg::kOnCreate | plRefMsg::kOnRequest | plRefMsg::kOnReplace))
				fDefaultCover = plMipmap::ConvertNoRef(ref->GetRef());
			else
				fDefaultCover = nil;
			return true;
		}
	}

	plEventCallbackMsg *callback = plEventCallbackMsg::ConvertNoRef( pMsg );
	if( callback != nil )
	{
		// Our callback message to tell us the page is done flipping
		if( callback->fUser & 0x08 )
		{
			// make sure that we still have a current book
			if (fCurrBook)
			{
				// Or actually maybe it's that we're done closing and should hide now,
				// produced from a CloseAndHide()
				if( callback->fEvent == kStop )
					fCurrBook->Hide();
				else
					fCurrBook->IFinishShow( ( callback->fUser & 0x01 ) ? true : false );
			}
		}
		else if( fCurrentlyTurning )
		{
			if( callback->fUser & 0x04 )
				IFillUncoveringPage( (hsBool)( callback->fUser & 0x01 ) ? true : false ); 
			else if( callback->fUser & 0x02 )
				StartTriggeredFlip( (hsBool)( callback->fUser & 0x01 ) ? true : false );
			else
				IFinishTriggeredFlip( (hsBool)( callback->fUser & 0x01 ) ? true : false );
		}
		return true;
	}

	pfGUINotifyMsg *notify = pfGUINotifyMsg::ConvertNoRef(pMsg);
	if(notify != nil)
	{
		// The only time we should get this is when the dialog loads; after that, we hijack
		// the dialog proc with our own
		IInitTemplate(pfGUIDialogMod::ConvertNoRef(notify->GetSender()->ObjectIsLoaded()));
		return true;
	}

	plTimeMsg *time = plTimeMsg::ConvertNoRef( pMsg );
	if( time != nil && fCurrSFXPages != kNoSides && !fCurrentlyTurning && fCurrentlyOpen )
	{
		IHandleSFX( (hsScalar)time->DSeconds() );
		return true;		
	}

	plTimerCallbackMsg* timerMsg = plTimerCallbackMsg::ConvertNoRef(pMsg);
	if (timerMsg)
	{
		if (timerMsg->fID == 99) // the flip animation is about to end, hide the page to prevent flickering
		{
			// the right side was uncovered, so the left side needs to be hidden
			fLeftEditCtrl->SetVisible(false);
		}
		else if (timerMsg->fID == 98)
		{
			// the left side was uncovered, so the right side needs to be hidden
			fRightEditCtrl->SetVisible(false);
		}
	}

	return hsKeyedObject::MsgReceive(pMsg);
}

void pfBookData::IInitTemplate(pfGUIDialogMod *templateDlg)
{
	hsAssert(templateDlg != nil, "Nil template in pfBookData::IInitTemplate()!");

	// Init and ref our fDialog pointer
	hsgResMgr::ResMgr()->SendRef(templateDlg->GetKey(), TRACKED_NEW plGenRefMsg(GetKey(), plRefMsg::kOnCreate, -1, kRefDialog), plRefFlags::kPassiveRef);  

	// Hijack the dialog proc with our own
	templateDlg->SetHandlerForAll(TRACKED_NEW pfJournalDlgProc(this));

	// Find our animation keys
																
	// And other interesting pointers
	fCoverButton = pfGUICheckBoxCtrl::ConvertNoRef(templateDlg->GetControlFromTag(pfJournalDlgProc::kTagBookCover));
	fTurnPageButton = pfGUICheckBoxCtrl::ConvertNoRef(templateDlg->GetControlFromTag(pfJournalDlgProc::kTagTurnPageCtrl));
	fLeftPageMap = pfGUIClickMapCtrl::ConvertNoRef(templateDlg->GetControlFromTag(pfJournalDlgProc::kTagLeftPageBtn));
	fRightPageMap = pfGUIClickMapCtrl::ConvertNoRef(templateDlg->GetControlFromTag(pfJournalDlgProc::kTagRightPageBtn));
	fLeftCorner = pfGUIButtonMod::ConvertNoRef(templateDlg->GetControlFromTag(pfJournalDlgProc::kTagLeftCornerBtn));
	fRightCorner = pfGUIButtonMod::ConvertNoRef(templateDlg->GetControlFromTag(pfJournalDlgProc::kTagRightCornerBtn));
	fWidthCtrl = pfGUIProgressCtrl::ConvertNoRef(templateDlg->GetControlFromTag(pfJournalDlgProc::kTagWidthCBDummy));
	fHeightCtrl = pfGUIProgressCtrl::ConvertNoRef(templateDlg->GetControlFromTag(pfJournalDlgProc::kTagHeightCBDummy));

	fTurnPageButton->SetEnabled(false);
	fCoverButton->DontPlaySounds();	// dont let checkbox play sounds, journal will take care of that.

	fCoverLayer = pfGUIDynDisplayCtrl::ConvertNoRef(templateDlg->GetControlFromTag(pfJournalDlgProc::kTagCoverLayer))->GetLayer(0);
	fCoverMaterial = pfGUIDynDisplayCtrl::ConvertNoRef(templateDlg->GetControlFromTag(pfJournalDlgProc::kTagCoverLayer))->GetMaterial(0);

	fPageMaterials[kLeftPage] = pfGUIDynDisplayCtrl::ConvertNoRef(templateDlg->GetControlFromTag(pfJournalDlgProc::kTagLeftDTMap))->GetMaterial(0);
	fPageMaterials[kRightPage] = pfGUIDynDisplayCtrl::ConvertNoRef(templateDlg->GetControlFromTag(pfJournalDlgProc::kTagRightDTMap))->GetMaterial(0);
	fPageMaterials[kTurnFrontPage] = pfGUIDynDisplayCtrl::ConvertNoRef(templateDlg->GetControlFromTag(pfJournalDlgProc::kTagTurnFrontDTMap))->GetMaterial(0);
	fPageMaterials[kTurnBackPage] = pfGUIDynDisplayCtrl::ConvertNoRef(templateDlg->GetControlFromTag(pfJournalDlgProc::kTagTurnBackDTMap))->GetMaterial(0);

	// Grab and ref the default cover mipmap
	plLayer *lay = plLayer::ConvertNoRef(fCoverLayer);
	if((lay != nil)&&(lay->GetTexture() != nil))
		hsgResMgr::ResMgr()->AddViaNotify(lay->GetTexture()->GetKey(), TRACKED_NEW plGenRefMsg(GetKey(), plRefMsg::kOnCreate, -1, kRefDefaultCover), plRefFlags::kPassiveRef);  

	fLeftPageMap->SetFlag(pfGUIClickMapCtrl::kReportHovering);
	fRightPageMap->SetFlag(pfGUIClickMapCtrl::kReportHovering);

	fLeftEditCtrl = pfGUIMultiLineEditCtrl::ConvertNoRef(templateDlg->GetControlFromTag(pfJournalDlgProc::kTagLeftEditCtrl));
	if (fLeftEditCtrl)
	{
		fLeftEditCtrl->SetEnabled(false); // disable the edit controls initially, we can turn them on later
		fLeftEditCtrl->SetVisible(false);
	}
	fRightEditCtrl = pfGUIMultiLineEditCtrl::ConvertNoRef(templateDlg->GetControlFromTag(pfJournalDlgProc::kTagRightEditCtrl));
	if (fRightEditCtrl)
	{
		fRightEditCtrl->SetEnabled(false);
		fRightEditCtrl->SetVisible(false);
	}

	fTurnFrontEditCtrl = pfGUIMultiLineEditCtrl::ConvertNoRef(templateDlg->GetControlFromTag(pfJournalDlgProc::kTagTurnFrontEditCtrl));
	if (fTurnFrontEditCtrl)
	{
		fTurnFrontEditCtrl->SetEnabled(false);
		fTurnFrontEditCtrl->SetVisible(false);
	}

	fTurnBackEditCtrl = pfGUIMultiLineEditCtrl::ConvertNoRef(templateDlg->GetControlFromTag(pfJournalDlgProc::kTagTurnBackEditCtrl));
	if (fTurnBackEditCtrl)
	{
		fTurnBackEditCtrl->SetEnabled(false);
		fTurnBackEditCtrl->SetVisible(false);
	}
	// if all the edit controls are here, we are editable, so set up the initial link
	if (fLeftEditCtrl && fRightEditCtrl && fTurnFrontEditCtrl && fTurnBackEditCtrl)
	{
		fEditable = true;
		fLeftEditCtrl->SetNext(fRightEditCtrl);
		fLeftEditCtrl->SetEventProc(TRACKED_NEW pfBookMultiLineEditProc(this));
		fTurnFrontEditCtrl->SetNext(fTurnBackEditCtrl); // these are always back to back
	}
}

//// IGetDTMap ///////////////////////////////////////////////////////////////
// Just a quick helper 

plDynamicTextMap *pfBookData::GetDTMap(UInt32 which)
{
	pfGUIDynDisplayCtrl *display = pfGUIDynDisplayCtrl::ConvertNoRef(fDialog->GetControlFromTag(which));
	return display->GetMap(0);
}

//// GetEditCtrl /////////////////////////////////////////////////////////////

pfGUIMultiLineEditCtrl *pfBookData::GetEditCtrl(UInt32 which)
{
	switch (which)
	{
	case pfJournalDlgProc::kTagLeftEditCtrl:
		return fLeftEditCtrl;
	case pfJournalDlgProc::kTagRightEditCtrl:
		return fRightEditCtrl;
	case pfJournalDlgProc::kTagTurnFrontEditCtrl:
		return fTurnFrontEditCtrl;
	case pfJournalDlgProc::kTagTurnBackEditCtrl:
		return fTurnBackEditCtrl;
	default:
		return nil;
	}
}

//// IRegisterForSFX /////////////////////////////////////////////////////////
// Registers (or unregisters) for time messages so we can process special FX 
// if we need to

void pfBookData::RegisterForSFX(WhichSide whichPages)
{
	if( whichPages == fCurrSFXPages)
		return;

	if(whichPages != kNoSides)
	{
		plgDispatch::Dispatch()->RegisterForExactType(plTimeMsg::Index(), GetKey());
	}
	else
		plgDispatch::Dispatch()->UnRegisterForExactType(plTimeMsg::Index(), GetKey());
	
	fCurrSFXPages = whichPages;
}

//// IHandleSFX //////////////////////////////////////////////////////////////
// Process SFX for this frame. Note: if ANYTHING is wrong (page starts not
// calced, pointers bad, etc) just bail, since the SFX are just for visual
// flair and not really needed.

void pfBookData::IHandleSFX(hsScalar currTime, WhichSide whichSide /*= kNoSides*/)
{
	if(fCurrBook == nil)
		return;
	if(whichSide == kNoSides)
	{
		if(fResetSFXFlag)
		{
			fBaseSFXTime=currTime;
			fResetSFXFlag=false;
		}

		fSFXUpdateFlip = !fSFXUpdateFlip;

		// Slightly recursive here to help us out a bit
		if(fSFXUpdateFlip&&(fCurrSFXPages & kLeftSide))
			IHandleSFX(currTime, kLeftSide);
		else if(!fSFXUpdateFlip &&(fCurrSFXPages & kRightSide))
			IHandleSFX(currTime, kRightSide);
		return;
	}

	// Update all SFX images for this page first
	hsScalar deltaT = currTime - fBaseSFXTime;

	UInt32 idx, inc = (whichSide == kLeftSide) ? 0 : 1;
	if(fCurrBook->fPageStarts.GetCount() <= fCurrBook->fCurrentPage + inc + 1)
		return;

	bool stillWant = false;
	for(idx = fCurrBook->fPageStarts[fCurrBook->fCurrentPage + inc]; idx < fCurrBook->fPageStarts[fCurrBook->fCurrentPage + inc + 1]; idx++)
	{
		pfEsHTMLChunk *chunk = fCurrBook->fHTMLSource[idx];
		
		if(chunk->fFlags & pfEsHTMLChunk::kGlowing)
		{
			// Glow SFX: animate opacity based on time offset
			UInt8 isOdd = 0;
			hsScalar newDelta = deltaT;
			while(newDelta > chunk->fSFXTime)
			{
				isOdd = ~isOdd;
				newDelta -= chunk->fSFXTime;
			}

			// If we're not odd, then we're decreasing in opacity, else we're increasing
			if(isOdd)
				newDelta = chunk->fSFXTime - newDelta;

			chunk->fCurrOpacity = chunk->fMaxOpacity - ((chunk->fMaxOpacity - chunk->fMinOpacity)*(newDelta / chunk->fSFXTime));
			stillWant = true;
		}
		else if(chunk->fFlags & pfEsHTMLChunk::kActAsCB)
		{
			// If our opacity doesn't match our checked state, slowly fade to it
			hsColorRGBA	inc;
			inc.Set(0.1f, 0.1f, 0.1f, 0.1f);

			hsColorRGBA &want = (chunk->fFlags & pfEsHTMLChunk::kChecked) ? chunk->fOnColor : chunk->fOffColor;
			if(want != chunk->fCurrColor)
			{
#define COMPARE_ME( wnt, curr ) \
				if( wnt > curr + 0.1f )					\
				{										\
					curr += 0.1f;	stillWant = true;	\
				}										\
				else if( wnt < curr - 0.1f )			\
				{										\
					curr -= 0.1f;	stillWant = true;	\
				}										\
				else									\
					curr = wnt;

				COMPARE_ME( want.r, chunk->fCurrColor.r )
				COMPARE_ME( want.g, chunk->fCurrColor.g )
				COMPARE_ME( want.b, chunk->fCurrColor.b )
				COMPARE_ME( want.a, chunk->fCurrColor.a )
			}
		}
	}

	// All done updating that page. Now render it!
	fCurrBook->IRenderPage( fCurrBook->fCurrentPage + inc, ( whichSide == kLeftSide ) ? pfJournalDlgProc::kTagLeftDTMap : pfJournalDlgProc::kTagRightDTMap );

	if( !stillWant )
	{
		// Done with FX for this page, so unregister for FX on this page now
		RegisterForSFX((WhichSide)(fCurrSFXPages & ~whichSide));
	}
}

//// IFillUncoveringPage /////////////////////////////////////////////////////
// Yet another step in the page flip, to make SURE we're already showing the
// turning page before we fill in the page behind it

void pfBookData::IFillUncoveringPage(hsBool rightSide)
{
	// only show the turning page if the book is open
	if ( CurrentlyOpen() )
		fTurnPageButton->SetVisible(true);
	// make sure there is a current book
	if (fCurrBook)
	{
		if (fCurrBook->fAreEditing)
		{
			int id;
			UpdatePageCorners(rightSide ? kRightSide : kLeftSide);
			if (rightSide)
			{
				fTurnBackEditCtrl->ForceUpdate(); // force everything that is changing to update
				fTurnBackEditCtrl->SetVisible(true); // and make sure everything is showing
				fTurnFrontEditCtrl->ForceUpdate();
				fTurnFrontEditCtrl->SetVisible(true);
				fRightEditCtrl->ForceUpdate();
				fRightEditCtrl->SetVisible(true);
				// The left edit ctrl doesn't update until the page flip animation is done
				id = 99;
			}
			else
			{
				fTurnFrontEditCtrl->ForceUpdate();
				fTurnFrontEditCtrl->SetVisible(true);
				fTurnBackEditCtrl->ForceUpdate();
				fTurnBackEditCtrl->SetVisible(true);
				fLeftEditCtrl->ForceUpdate();
				fLeftEditCtrl->SetVisible(true);
				// The right edit ctrl doesn't update until the page flip animation is done
				id = 98;
			}
			
			// create a timer so we can hide the old left or right turn page right before the animation finishes to prevent flicker
			plTimerCallbackMsg* pTimerMsg = TRACKED_NEW plTimerCallbackMsg(GetKey(),id);
			plgTimerCallbackMgr::NewTimer( .5, pTimerMsg ); // .5 found by trial and error
			return; // the gui controls render everything for us, so ignoring this request
		}
		if(rightSide)
			fCurrBook->IRenderPage(fCurrBook->fCurrentPage + 1, pfJournalDlgProc::kTagRightDTMap);
		else
			fCurrBook->IRenderPage(fCurrBook->fCurrentPage, pfJournalDlgProc::kTagLeftDTMap);
	}

	// Update the page corner we're flipping away from
	UpdatePageCorners(rightSide ? kRightSide : kLeftSide);
}

//// ITriggerPageFlip ////////////////////////////////////////////////////////
// Triggers the start of the page-flipping animation, as well as sets up the callback for when it's finished

void pfBookData::ITriggerPageFlip(hsBool flipBackwards, hsBool immediate)
{
	// Hack here: since we don't have an official interface to select these directly
	// in MAX, we just use a GUI check box to grab them for us, even though we never
	// actually use the functionality of the checkbox itself
	const hsTArray<plKey> &keys = fTurnPageButton->GetAnimationKeys();
	const char *animName = fTurnPageButton->GetAnimationName();

	plAnimCmdMsg *msg = TRACKED_NEW plAnimCmdMsg();
	if (immediate)
	{
		msg->SetCmd(plAnimCmdMsg::kGoToEnd);
	}
	else
	{
		msg->SetCmd(plAnimCmdMsg::kContinue);
		msg->SetCmd(plAnimCmdMsg::kSetForewards);
	}
	msg->SetAnimName(flipBackwards ? "backward" : "forward");
	msg->AddReceivers(keys);
	
	// Here's the whole reason why we're not just checking the checkbox: so we can attach a callback
	// so we know when the animation completes. Pretty sad, huh? Poor checkbox.
	plEventCallbackMsg *eventMsg = TRACKED_NEW plEventCallbackMsg;
	eventMsg->AddReceiver(GetKey());
	eventMsg->fRepeats = 0;
	if (immediate)
	{
		eventMsg->fUser = ((!flipBackwards) ? 0x01 : 0x00) | 0x02;
		eventMsg->fEvent = kSingleFrameAdjust;
	}
	else
	{
		eventMsg->fUser = (flipBackwards ? 0x01 : 0x00);
		eventMsg->fEvent = kStop;
	}
	msg->SetCmd(plAnimCmdMsg::kAddCallbacks);
	msg->AddCallback(eventMsg);
	hsRefCnt_SafeUnRef(eventMsg);
	if (!immediate)
	{
		// We want a second callback to tell us when, indeed, the page has started turning
		// and is thus visible and thus we can actually, really, honestly, safely fill in the
		// page behind it
		eventMsg = TRACKED_NEW plEventCallbackMsg;
		eventMsg->AddReceiver(GetKey());
		eventMsg->fRepeats = 0;
		eventMsg->fUser = !flipBackwards ? (0x04 | 0x01) : 0x04;
		eventMsg->fEvent = kBegin;	// Should cause it to be triggered once it seeks at the start of the command
		msg->AddCallback(eventMsg);
		hsRefCnt_SafeUnRef(eventMsg);
	}
	fCurrentlyTurning = true;

	msg->Send();
}

//// StartTriggeredFlip /////////////////////////////////////////////////////
// Finishes the start of the triggered page flip (once we're sure the 
// animation is at the new frame)

void pfBookData::StartTriggeredFlip(hsBool flipBackwards)
{
	if(flipBackwards)
	{
		ITriggerPageFlip(true, false);
	}
	else
	{
		ITriggerPageFlip(false, false);
	}
}

//// Kill the page flipping cause, we're closing the book

void pfBookData::KillPageFlip()
{
	if ( fCurrentlyTurning )
	{
		//ITriggerPageFlip(false, true);
		fTurnPageButton->SetVisible(false);
	}
}

//// IFinishTriggeredFlip ////////////////////////////////////////////////////
// Finishes the triggered page flip, on callback

void pfBookData::IFinishTriggeredFlip(hsBool wasBackwards)
{
	if (fCurrBook && fCurrBook->fAreEditing) // this is handled differently when we are editing
	{
		fLeftEditCtrl->SetNext(fRightEditCtrl); // relink the original path
		if (!wasBackwards)
		{
			// adjust the starting point of the control (not needed if we weren't backwards since that was done when we started turning
			Int32 newStart = fRightEditCtrl->GetLastVisibleLine();
			fLeftEditCtrl->SetGlobalStartLine(newStart);
		}
		if (fAdjustCursorTo >= 0)
		{
			if (wasBackwards)
			{
				fRightEditCtrl->SetCursorToLoc(fAdjustCursorTo);
				fRightEditCtrl->GetOwnerDlg()->SetFocus(fRightEditCtrl);
			}
			else
			{
				fLeftEditCtrl->SetCursorToLoc(fAdjustCursorTo);
				fLeftEditCtrl->GetOwnerDlg()->SetFocus(fLeftEditCtrl);
			}
			fAdjustCursorTo = -1;
		}

		fTurnFrontEditCtrl->SetVisible(false); // hide the controls
		fTurnBackEditCtrl->SetVisible(false);

		fLeftEditCtrl->SetVisible(true);
		fRightEditCtrl->SetVisible(true);

		fLeftEditCtrl->ForceUpdate();
		fRightEditCtrl->ForceUpdate();
	}
	else if(wasBackwards)
	{
		// Grab the DTMaps for the front of the flip page and the right page, so we can
		// copy the front into the right page
		plDynamicTextMap *turnFront = GetDTMap(pfJournalDlgProc::kTagTurnFrontDTMap);
		plDynamicTextMap *right = GetDTMap(pfJournalDlgProc::kTagRightDTMap);
//		right->Swap( turnFront );
		if ( turnFront->IsValid() && right->IsValid() )
		{
			memcpy(right->GetImage(), turnFront->GetImage(), right->GetLevelSize(0));
			if(right->GetDeviceRef() != nil)
				right->GetDeviceRef()->SetDirty(true);
		}
		// we are going to attempt to re-render the left-hand page
		// sometimes, the book stutters on a page flip and screws up the book
		if (fCurrBook)
		{
			fCurrBook->IRenderPage(fCurrBook->fCurrentPage, pfJournalDlgProc::kTagLeftDTMap);
			// move the videos over
			fCurrBook->IMoveMovies(PageMaterial(kTurnFrontPage),PageMaterial(kRightPage));
		}
	}
	else
	{
		// Grab the DTMaps for the back of the flip page and the left page, so we can
		// copy the back into the left page
		plDynamicTextMap *turnBack = GetDTMap(pfJournalDlgProc::kTagTurnBackDTMap);
		plDynamicTextMap *left = GetDTMap(pfJournalDlgProc::kTagLeftDTMap);
	//		right->Swap( turnFront );
		if ( turnBack->IsValid() && left->IsValid() )
		{
			memcpy(left->GetImage(), turnBack->GetImage(), left->GetLevelSize(0));
			if(left->GetDeviceRef() != nil)
				left->GetDeviceRef()->SetDirty(true);
		}
		// we are going to attempt to re-render the right-hand page
		// sometimes, the book stutters on a page flip and screws up the book
		if (fCurrBook)
		{
			fCurrBook->IRenderPage(fCurrBook->fCurrentPage + 1, pfJournalDlgProc::kTagRightDTMap);
			// move the videos over
			fCurrBook->IMoveMovies(PageMaterial(kTurnBackPage),PageMaterial(kLeftPage));
		}
	}

	// Hide our page flipping button/checkbox/whatever
	fTurnPageButton->SetVisible(false);

	// Update page corners
	UpdatePageCorners(kBothSides);

	fCurrentlyTurning = false;

	// Start our FX once the page is done turning
	fResetSFXFlag = true;
}

//// UpdatePageCorners //////////////////////////////////////////////////////
// Enables or disables the left and right page corners, to indicate current turnage state

void pfBookData::UpdatePageCorners(WhichSide which)
{
	// make sure there is a book to update!
	if (fCurrBook)
	{
		if (!fCurrBook->fAllowTurning || !fCurrBook->fAreWeShowing)
		{
			fLeftCorner->SetVisible(false);
			fLeftCorner->SetEnabled(false);
			fRightCorner->SetVisible(false);
			fRightCorner->SetEnabled(false);
			return;
		}
		if((which == kLeftSide)||(which == kBothSides))
		{
			if (fCurrBook->fAreEditing)
				fLeftCorner->SetVisible(!fLeftEditCtrl->ShowingBeginningOfBuffer()); // only show if the control is not viewing the beginning of the buffer
			else
				fLeftCorner->SetVisible((fCurrBook->fCurrentPage >= 2) ? true : false);
			// Note: always disabled (we just go on the page click itself)
			fLeftCorner->SetEnabled(false);
		}
		if((which == kRightSide)||(which == kBothSides))
		{
			if (fCurrBook->fAreEditing)
				fRightCorner->SetVisible(!fRightEditCtrl->ShowingEndOfBuffer()); // only show if the control is not viewing the end of the buffer
			else
				fRightCorner->SetVisible((fCurrBook->fCurrentPage + 2 <= fCurrBook->fLastPage) ? true : false);
			fRightCorner->SetEnabled(false);
		}
	}
}

//// SetCurrSize ////////////////////////////////////////////////////////////
// Seeks the width and height animations to set the desired book size. Sizes are in % across the animation

void pfBookData::SetCurrSize(hsScalar w, hsScalar h)
{
	fWidthCtrl->SetCurrValue(w);
	fHeightCtrl->SetCurrValue(h);
}

//// PlayBookCloseAnim //////////////////////////////////////////////////////
//	Triggers our animation for closing or opening the book.

void pfBookData::PlayBookCloseAnim(hsBool closeIt /*= true*/, hsBool immediate /*= false*/)
{
	// Disable the book cover button if we're opening, enable otherwise
	fCoverButton->SetEnabled(closeIt);

	// Tell our cover button to check or uncheck
	fCoverButton->SetChecked(!closeIt, immediate);

	// Trigger the open (or close) sound
	if(!immediate)
		fCoverButton->PlaySound(closeIt ? pfGUICheckBoxCtrl::kMouseUp : pfGUICheckBoxCtrl::kMouseDown);

	fCurrentlyOpen = !closeIt;
}

//// Event routines from a linked multi-line edit control ////////////////////

void pfBookData::HitEndOfControlList(Int32 cursorPos)
{
	fAdjustCursorTo = cursorPos;
	if (fCurrBook)
		fCurrBook->NextPage();
}

void pfBookData::HitBeginningOfControlList(Int32 cursorPos)
{
	fAdjustCursorTo = cursorPos;
	if (fCurrBook)
		fCurrBook->PreviousPage();
}

void pfBookData::EnableEditGUI(hsBool enable/* =true */)
{
	if (fEditable)
	{
		fLeftEditCtrl->SetEnabled(enable);
		fLeftEditCtrl->SetVisible(enable);
		fRightEditCtrl->SetEnabled(enable);
		fRightEditCtrl->SetVisible(enable);
		// we don't make these editable because they are temps used for the turning page
		fTurnFrontEditCtrl->SetVisible(false); // we don't want these visible initially either
		fTurnBackEditCtrl->SetVisible(false);
	}
}

//// Our Singleton Stuff /////////////////////////////////////////////////////

//pfJournalBook	*pfJournalBook::fInstance = nil;
std::map<std::string,pfBookData*> pfJournalBook::fBookGUIs;

void	pfJournalBook::SingletonInit( void )
{
	fBookGUIs["BkBook"] = TRACKED_NEW pfBookData(); // load the default book data object
	hsgResMgr::ResMgr()->NewKey("BkBook",fBookGUIs["BkBook"],pfGameGUIMgr::GetInstance()->GetKey()->GetUoid().GetLocation());
	fBookGUIs["BkBook"]->LoadGUI();
}

void	pfJournalBook::SingletonShutdown( void )
{
	std::map<std::string,pfBookData*>::iterator i = fBookGUIs.begin();
	while (i != fBookGUIs.end())
	{
		pfBookData *bookData = i->second;
		bookData->GetKey()->UnRefObject();
		i->second = nil;
		i++;
	}
	fBookGUIs.clear();
}

void	pfJournalBook::LoadGUI( const char *guiName )
{
	if (fBookGUIs.find(guiName) == fBookGUIs.end()) // is it already loaded?
	{ // nope, load it
		fBookGUIs[guiName] = TRACKED_NEW pfBookData(guiName);
		hsgResMgr::ResMgr()->NewKey(guiName,fBookGUIs[guiName],pfGameGUIMgr::GetInstance()->GetKey()->GetUoid().GetLocation());
		fBookGUIs[guiName]->LoadGUI();
	}
}

void	pfJournalBook::UnloadGUI( const char *guiName )
{
	if (strcmp(guiName,"BkBook")==0)
		return; // do not allow people to unload the default book gui
	std::map<std::string,pfBookData*>::iterator loc = fBookGUIs.find(guiName);
	if (loc != fBookGUIs.end()) // make sure it's loaded
	{
		fBookGUIs[guiName]->GetKey()->UnRefObject();
		fBookGUIs[guiName] = nil;
		fBookGUIs.erase(loc);
	}
}

void	pfJournalBook::UnloadAllGUIs()
{
	std::map<std::string,pfBookData*>::iterator i = fBookGUIs.begin();
	std::vector<std::string> names;
	while (i != fBookGUIs.end())
	{
		std::string name = i->first;
		names.push_back(name); // store a list of keys
		i++;
	}
	int idx;
	for (idx = 0; idx < names.size(); idx++)
		UnloadGUI(names[idx].c_str()); // UnloadGUI won't unload BkBook
}

//// Constructor /////////////////////////////////////////////////////////////
// The constructor takes in the esHTML source for the journal, along with
// the name of the mipmap to use as the cover of the book. The callback
// key is the keyed object to send event messages to (see <img> tag).

pfJournalBook::pfJournalBook( const char *esHTMLSource, plKey coverImageKey, plKey callbackKey /*= nil*/, 
								const plLocation &hintLoc /* = plLocation::kGlobalFixedLoc */, const char *guiName /* = nil */ )
{
	if (guiName && (strcmp(guiName,"") != 0))
		fCurBookGUI = guiName;
	else
		fCurBookGUI = "BkBook";
	if (fBookGUIs.find(fCurBookGUI) == fBookGUIs.end())
	{
		fBookGUIs[fCurBookGUI] = TRACKED_NEW pfBookData(fCurBookGUI.c_str());
		hsgResMgr::ResMgr()->NewKey(fCurBookGUI.c_str(),fBookGUIs[fCurBookGUI],pfGameGUIMgr::GetInstance()->GetKey()->GetUoid().GetLocation());
		fBookGUIs[fCurBookGUI]->LoadGUI();
	}
	
	fCurrentPage = 0;
	fLastPage = -1;
	fCoverMipKey = coverImageKey;
	fCoverFromHTML = false;
	fCallbackKey = callbackKey;
	fWidthScale = fHeightScale = 0.f;
	fPageTMargin = fPageLMargin = fPageBMargin = fPageRMargin = 16;
	fAllowTurning = true;
	fAreWeShowing = false;
	fCoverTint.Set( 0.f, 0.f, 0.f, 1.f );
	fTintFirst = true;
	fTintCover = false;
	fAreEditing = false;
	fWantEditing = false;
	fDefLoc = hintLoc;

	wchar_t *wESHTMLSource = hsStringToWString(esHTMLSource);
	fUncompiledSource = wESHTMLSource;
	ICompileSource( wESHTMLSource, hintLoc );
	delete [] wESHTMLSource;
}

pfJournalBook::pfJournalBook( const wchar_t *esHTMLSource, plKey coverImageKey, plKey callbackKey /*= nil*/, 
								const plLocation &hintLoc /* = plLocation::kGlobalFixedLoc */, const char *guiName /* = nil */ )
{
	if (guiName && (strcmp(guiName,"") != 0))
		fCurBookGUI = guiName;
	else
		fCurBookGUI = "BkBook";
	if (fBookGUIs.find(fCurBookGUI) == fBookGUIs.end())
	{
		fBookGUIs[fCurBookGUI] = TRACKED_NEW pfBookData(fCurBookGUI.c_str());
		hsgResMgr::ResMgr()->NewKey(fCurBookGUI.c_str(),fBookGUIs[fCurBookGUI],pfGameGUIMgr::GetInstance()->GetKey()->GetUoid().GetLocation());
		fBookGUIs[fCurBookGUI]->LoadGUI();
	}
	
	fCurrentPage = 0;
	fLastPage = -1;
	fCoverMipKey = coverImageKey;
	fCoverFromHTML = false;
	fCallbackKey = callbackKey;
	fWidthScale = fHeightScale = 0.f;
	fPageTMargin = fPageLMargin = fPageBMargin = fPageRMargin = 16;
	fAllowTurning = true;
	fAreWeShowing = false;
	fCoverTint.Set( 1.f, 1.f, 1.f, 1.f );
	fTintFirst = true;
	fTintCover = false;
	fAreEditing = false;
	fWantEditing = false;
	fDefLoc = hintLoc;
	fUncompiledSource = esHTMLSource;

	ICompileSource( esHTMLSource, hintLoc );
}

pfJournalBook::~pfJournalBook()
{
	if (fBookGUIs.find(fCurBookGUI) != fBookGUIs.end()) // it might have been deleted before we got here
		if( fBookGUIs[fCurBookGUI] && fBookGUIs[fCurBookGUI]->CurBook() == this )
			Hide();

	IFreeSource();
}

//// MsgReceive //////////////////////////////////////////////////////////////

hsBool	pfJournalBook::MsgReceive( plMessage *pMsg )
{
	return hsKeyedObject::MsgReceive( pMsg );
}

void	pfJournalBook::SetGUI( const char *guiName )
{
	if (guiName && (strcmp(guiName,"") != 0))
		fCurBookGUI = guiName;
	if (fBookGUIs.find(fCurBookGUI) == fBookGUIs.end())
		fCurBookGUI = "BkBook"; // requested GUI isn't loaded, so use default GUI
	SetEditable(fWantEditing); // make sure that if we want editing, to set it
	ICompileSource(fUncompiledSource.c_str(), fDefLoc); // recompile the source to be safe
}

//// Show ////////////////////////////////////////////////////////////////////
// Shows the book, optionally starting open or closed

void	pfJournalBook::Show( hsBool startOpened /*= false */)
{
	fBookGUIs[fCurBookGUI]->StartedOpen(startOpened);
	fBookGUIs[fCurBookGUI]->CurBook(this);
	fBookGUIs[fCurBookGUI]->SetCurrSize(fWidthScale, fHeightScale);
	ILoadAllImages( false );

	hsGMaterial *cover = fBookGUIs[fCurBookGUI]->CoverMaterial();
	if( cover != nil )
	{
		hsTArray<plLayerInterface*> layers;
		plMipmap *mip = fCoverMipKey ? plMipmap::ConvertNoRef( fCoverMipKey->ObjectIsLoaded() ) : nil;
		if( mip != nil )
		{
			layers.Append(IMakeBaseLayer(mip));

			int i;
			for (i=0; i<fCoverDecals.GetCount(); i++)
			{
				if (fCoverDecals[i]->fType == pfEsHTMLChunk::kDecal)
				{
					plMipmap *decal = plMipmap::ConvertNoRef( fCoverDecals[i]->fImageKey != nil ? fCoverDecals[i]->fImageKey->ObjectIsLoaded() : nil );
					if (decal != nil)
						layers.Append(IMakeDecalLayer(fCoverDecals[i],decal,mip));
				}
				else
				{
					// it's a cover movie, not a decal, so we make a layer, thinking it's at 0,0 and a left map (which gives us the results we want)
					plLayerBink *movieLayer = IMakeMovieLayer(fCoverDecals[i],0,0,mip,pfJournalDlgProc::kTagLeftDTMap,false);
					loadedMovie *movie = TRACKED_NEW loadedMovie;
					movie->movieLayer = movieLayer;
					movie->movieChunk = fCoverDecals[i];
					fLoadedMovies.Append(movie);
					layers.Append(plLayerInterface::ConvertNoRef(movieLayer));
					fVisibleLinks.Reset(); // remove any links that the make movie layer might have added, since a cover movie can't link
				}
			}
			ISetDecalLayers(cover,layers);
		}
		else
		{
			layers.Append(IMakeBaseLayer(fBookGUIs[fCurBookGUI]->DefaultCover()));
			ISetDecalLayers(cover,layers);
		}
		// release our ref on the cover layers since the material will take care of them now
		int i;
		for (i=0; i<layers.GetCount(); i++)
			GetKey()->Release(layers[i]->GetKey());
	}

//	fInstance->IPlayBookCloseAnim( !startOpened, true );
	fBookGUIs[fCurBookGUI]->TurnPageButton()->SetVisible( false );
	ITriggerCloseWithNotify( !startOpened, true );
}

//// IFinishShow /////////////////////////////////////////////////////////////
// Finish showing the book, due to the animation being done seeking

void	pfJournalBook::IFinishShow( hsBool startOpened )
{
	fBookGUIs[fCurBookGUI]->Dialog()->Show();

	fAreWeShowing = true;

	if( startOpened )
	{
		// Render initial pages
		fCurrentPage = 0;
		fVisibleLinks.Reset();
		IRenderPage( 0, pfJournalDlgProc::kTagLeftDTMap );
		IRenderPage( 1, pfJournalDlgProc::kTagRightDTMap );

		fBookGUIs[fCurBookGUI]->UpdatePageCorners( pfBookData::kBothSides );
	}

	ISendNotify( kNotifyShow );
}

//// Hide ////////////////////////////////////////////////////////////////////

void	pfJournalBook::Hide( void )
{
	if (fBookGUIs[fCurBookGUI])
	{
		// we can only hide our own dialog
		if (fBookGUIs[fCurBookGUI]->CurBook() == this )
		{
			if (fBookGUIs[fCurBookGUI]->Dialog())
				fBookGUIs[fCurBookGUI]->Dialog()->Hide();
			fBookGUIs[fCurBookGUI]->CurBook(nil);
			ISendNotify( kNotifyHide );
			ILoadAllImages( true );
			// purge the dynaTextMaps, we're done with them for now
			IPurgeDynaTextMaps();
			// nuke the movies so they don't stay in memory (they're big!)
			int i;
			for( i = 0; i < fLoadedMovies.GetCount(); i++ )
			{
				plLayerBink *movie = fLoadedMovies[ i ]->movieLayer;
				movie->GetKey()->UnRefObject();
				delete fLoadedMovies[ i ];
			}
			fLoadedMovies.Reset();
		}
	}

}

//// Open ////////////////////////////////////////////////////////////////////
// Opens the book, optionally to the given page

void	pfJournalBook::Open( UInt32 startingPage /*= 0 */)
{
	if( !fBookGUIs[fCurBookGUI]->CurrentlyOpen() )
	{
		fBookGUIs[fCurBookGUI]->PlayBookCloseAnim( false );

		// Render initial pages
		fCurrentPage = startingPage;
		fVisibleLinks.Reset();
		IRenderPage( startingPage, pfJournalDlgProc::kTagLeftDTMap );
		IRenderPage( startingPage + 1, pfJournalDlgProc::kTagRightDTMap );

		fBookGUIs[fCurBookGUI]->UpdatePageCorners( pfBookData::kBothSides );
	}
}

//// Close ///////////////////////////////////////////////////////////////////
// Closes the book.

void	pfJournalBook::Close( void )
{
	// don't allow them to close the book if the book started open
	if( !fBookGUIs[fCurBookGUI]->StartedOpen() && fBookGUIs[fCurBookGUI]->CurrentlyOpen() )
	{
		ISendNotify( kNotifyClose );
		fBookGUIs[fCurBookGUI]->PlayBookCloseAnim( true );
	}
}

//// CloseAndHide ////////////////////////////////////////////////////////////
// Closes the book, then calls Hide() once it's done closing

void	pfJournalBook::CloseAndHide( void )
{
	// if they start with the book open, then don't allow them to close it
	if( !fBookGUIs[fCurBookGUI]->StartedOpen() && fBookGUIs[fCurBookGUI]->CurrentlyOpen() )
	{
		// if we are flipping a book then kill the page flipping animation
		if ( fBookGUIs[fCurBookGUI]->CurrentlyTurning() )
			fBookGUIs[fCurBookGUI]->KillPageFlip();
		ISendNotify( kNotifyClose );

		ITriggerCloseWithNotify( true, false );

		// Don't hide until we get the callback!
	}
	else
		// Already closed, just hide
		Hide();
}

//// ITriggerCloseWithNotify /////////////////////////////////////////////////
// Close with a notify

void	pfJournalBook::ITriggerCloseWithNotify( hsBool closeNotOpen, hsBool immediate )
{
	// Disable the book cover button if we're opening, enable otherwise
	fBookGUIs[fCurBookGUI]->CoverButton()->SetEnabled( closeNotOpen );

	// Do the animation manually so we can get a callback
	fBookGUIs[fCurBookGUI]->CurrentlyOpen(!closeNotOpen);

	const hsTArray<plKey> &keys = fBookGUIs[fCurBookGUI]->CoverButton()->GetAnimationKeys();
	const char *animName = fBookGUIs[fCurBookGUI]->CoverButton()->GetAnimationName();

	plAnimCmdMsg *msg = TRACKED_NEW plAnimCmdMsg();
	if( !immediate )
	{
		msg->SetCmd( plAnimCmdMsg::kContinue );
		msg->SetCmd( closeNotOpen ? plAnimCmdMsg::kSetBackwards : plAnimCmdMsg::kSetForewards );
	}
	else
	{
		msg->SetCmd( plAnimCmdMsg::kStop );
		msg->SetCmd( closeNotOpen ? plAnimCmdMsg::kGoToBegin : plAnimCmdMsg::kGoToEnd );
	}
	msg->SetAnimName( animName );
	msg->AddReceivers( keys );
	
	plEventCallbackMsg *eventMsg = TRACKED_NEW plEventCallbackMsg;
	eventMsg->AddReceiver( fBookGUIs[fCurBookGUI]->GetKey() );
	eventMsg->fRepeats = 0;
	eventMsg->fUser = 0x08 | ( closeNotOpen ? 0x00 : 0x01 );	// So we know which this is for
	eventMsg->fEvent = immediate ? ( kSingleFrameEval ) : kStop;
	msg->SetCmd( plAnimCmdMsg::kAddCallbacks );
	msg->AddCallback( eventMsg );
	hsRefCnt_SafeUnRef( eventMsg );

	msg->Send();

	// Trigger the open (or close) sound
	if( !immediate )
		fBookGUIs[fCurBookGUI]->CoverButton()->PlaySound(closeNotOpen ? pfGUICheckBoxCtrl::kMouseUp : pfGUICheckBoxCtrl::kMouseDown);
}

//// NextPage ////////////////////////////////////////////////////////////////
// Advances forward one page

void	pfJournalBook::NextPage( void )
{
	if( (fBookGUIs[fCurBookGUI]->CurrentlyTurning()) || (!fAllowTurning) || (!fAreWeShowing) )
		return;

	if ((fAreEditing)&&!(fBookGUIs[fCurBookGUI]->GetEditCtrl(pfJournalDlgProc::kTagRightEditCtrl)->ShowingEndOfBuffer())) // we're editing the book, so page turning is different here
	{
		fCurrentPage += 2; // we just go to the next page, an editable book has "infinite" pages anyway

		pfGUIMultiLineEditCtrl *right = fBookGUIs[fCurBookGUI]->GetEditCtrl(pfJournalDlgProc::kTagRightEditCtrl);
		pfGUIMultiLineEditCtrl *left = fBookGUIs[fCurBookGUI]->GetEditCtrl(pfJournalDlgProc::kTagLeftEditCtrl);
		pfGUIMultiLineEditCtrl *turnFront = fBookGUIs[fCurBookGUI]->GetEditCtrl(pfJournalDlgProc::kTagTurnFrontEditCtrl);
		pfGUIMultiLineEditCtrl *turnBack = fBookGUIs[fCurBookGUI]->GetEditCtrl(pfJournalDlgProc::kTagTurnBackEditCtrl);
		// re-link the controls with the turn page in the middle
		left->SetNext(turnFront);
		turnBack->SetNext(right);

		// At this point, only the left and right pages are visible, we don't want to actually update anything until the animation
		// starts so that nothing flashes or overdraws.

		fBookGUIs[fCurBookGUI]->StartTriggeredFlip( false );
		fBookGUIs[fCurBookGUI]->TurnPageButton()->PlaySound( pfGUICheckBoxCtrl::kMouseUp );
		ISendNotify(kNotifyNextPage);
	}
	else if( fCurrentPage + 2 <= fLastPage )
	{
		fCurrentPage += 2;
		fVisibleLinks.Reset();

		// Swap the right DT map into the turn page front DTMap, then render
		// the new current page into turn page back and currPage+1 into 
		// the right DTMap
		plDynamicTextMap *turnFront = fBookGUIs[fCurBookGUI]->GetDTMap( pfJournalDlgProc::kTagTurnFrontDTMap );
		plDynamicTextMap *right = fBookGUIs[fCurBookGUI]->GetDTMap( pfJournalDlgProc::kTagRightDTMap );
		if ( turnFront->IsValid() && right->IsValid() )
		{
			memcpy( turnFront->GetImage(), right->GetImage(), right->GetLevelSize( 0 ) );
			if( turnFront->GetDeviceRef() != nil )
				turnFront->GetDeviceRef()->SetDirty( true );
		}
		// copy the videos over
		IMoveMovies( fBookGUIs[fCurBookGUI]->PageMaterial(pfBookData::kRightPage), fBookGUIs[fCurBookGUI]->PageMaterial(pfBookData::kTurnFrontPage) );
		IRenderPage( fCurrentPage, pfJournalDlgProc::kTagTurnBackDTMap );

		// This will fire a callback when it's done that'll let us continue the setup
		fBookGUIs[fCurBookGUI]->StartTriggeredFlip( false );

		// Play us a sound too, if defined on our button
		fBookGUIs[fCurBookGUI]->TurnPageButton()->PlaySound( pfGUICheckBoxCtrl::kMouseUp );

		ISendNotify( kNotifyNextPage );
	}
}

//// PreviousPage ////////////////////////////////////////////////////////////
// Same, only back

void	pfJournalBook::PreviousPage( void )
{
	if(( fBookGUIs[fCurBookGUI]->CurrentlyTurning() )||( !fAllowTurning ))
		return;

	if (fAreEditing) // we're editing the book, so page turning is different here
	{
		if (!(fBookGUIs[fCurBookGUI]->GetEditCtrl(pfJournalDlgProc::kTagLeftEditCtrl)->ShowingBeginningOfBuffer())) // make sure we don't flip past the beginning
		{
			if (fCurrentPage >= 2) // this variable can get out of whack if we open the book to a page in the middle
				fCurrentPage -= 2; // just making sure that this doesn't go below zero (and therefore wrap around)
			
			pfGUIMultiLineEditCtrl *right = fBookGUIs[fCurBookGUI]->GetEditCtrl(pfJournalDlgProc::kTagRightEditCtrl);
			pfGUIMultiLineEditCtrl *left = fBookGUIs[fCurBookGUI]->GetEditCtrl(pfJournalDlgProc::kTagLeftEditCtrl);
			pfGUIMultiLineEditCtrl *turnFront = fBookGUIs[fCurBookGUI]->GetEditCtrl(pfJournalDlgProc::kTagTurnFrontEditCtrl);
			pfGUIMultiLineEditCtrl *turnBack = fBookGUIs[fCurBookGUI]->GetEditCtrl(pfJournalDlgProc::kTagTurnBackEditCtrl);
			// adjust the starting position of the left control to the new start
			Int32 newStartLine = left->GetFirstVisibleLine() - ((left->GetNumVisibleLines()-1)*2);
			left->SetGlobalStartLine(newStartLine);
			// re-link the controls with the turn page in the middle
			left->SetNext(turnFront);
			turnBack->SetNext(right);

			// At this point, only the left and right pages are visible, we don't want to actually update anything until the animation
			// starts so that nothing flashes or overdraws.
			
			fBookGUIs[fCurBookGUI]->StartTriggeredFlip( true );
			fBookGUIs[fCurBookGUI]->TurnPageButton()->PlaySound( pfGUICheckBoxCtrl::kMouseUp );
			ISendNotify(kNotifyPreviousPage);
		}
		else
		{
			Close();
		}
	}
	else if( fCurrentPage > 1 )
	{
		fCurrentPage -= 2;
		fVisibleLinks.Reset();

		// Swap the left DT map into the turn page back DTMap, then render
		// the new current page into the left and currPage+1 into 
		// the turn page front DTMap
		plDynamicTextMap *turnBack = fBookGUIs[fCurBookGUI]->GetDTMap( pfJournalDlgProc::kTagTurnBackDTMap );
		plDynamicTextMap *left = fBookGUIs[fCurBookGUI]->GetDTMap( pfJournalDlgProc::kTagLeftDTMap );
		if ( turnBack->IsValid() && left->IsValid() )
		{
			memcpy( turnBack->GetImage(), left->GetImage(), left->GetLevelSize( 0 ) );
			if( turnBack->GetDeviceRef() != nil )
				turnBack->GetDeviceRef()->SetDirty( true );
		}
		// copy the videos over
		IMoveMovies( fBookGUIs[fCurBookGUI]->PageMaterial(pfBookData::kLeftPage), fBookGUIs[fCurBookGUI]->PageMaterial(pfBookData::kTurnBackPage) );
		IRenderPage( fCurrentPage + 1, pfJournalDlgProc::kTagTurnFrontDTMap );

		// This will fire a callback when it's done that'll let us continue the setup
		fBookGUIs[fCurBookGUI]->StartTriggeredFlip( true );

		// Play us a sound too, if defined on our button
		fBookGUIs[fCurBookGUI]->TurnPageButton()->PlaySound( pfGUICheckBoxCtrl::kMouseUp );

		ISendNotify( kNotifyPreviousPage );
	}
	else
	{
		Close();
	}
}

//// IFindCurrVisibleLink ////////////////////////////////////////////////////
// Find the current moused link, if any

Int32	pfJournalBook::IFindCurrVisibleLink( hsBool rightNotLeft, hsBool hoverNotUp )
{
	pfGUIClickMapCtrl *ctrl = ( rightNotLeft ) ? fBookGUIs[fCurBookGUI]->RightPageMap() : fBookGUIs[fCurBookGUI]->LeftPageMap();

	hsPoint3 pt = hoverNotUp ? ctrl->GetLastMousePt() : ctrl->GetLastMouseUpPt();

	// This should be 0-1 in the context of the control, so scale to our DTMap size
	plDynamicTextMap *dtMap = fBookGUIs[fCurBookGUI]->GetDTMap( rightNotLeft ? pfJournalDlgProc::kTagRightDTMap : pfJournalDlgProc::kTagLeftDTMap );
	pt.fX *= (hsScalar)dtMap->GetWidth();
	pt.fY *= (hsScalar)dtMap->GetHeight();
	if( rightNotLeft )
	{
		// Clicks on the right side are offsetted in x by the left side's width
		dtMap = fBookGUIs[fCurBookGUI]->GetDTMap( pfJournalDlgProc::kTagLeftDTMap );
		pt.fX += dtMap->GetWidth();
	}

	// Search through the list of visible hotspots
	UInt32 i;
	for( i = 0; i < fVisibleLinks.GetCount(); i++ )
	{
		if( fVisibleLinks[ i ]->fLinkRect.Contains( (Int16)pt.fX, (Int16)pt.fY ) )
		{
			// Found a visible link
			return (Int32)i;
		}
	}

	return -1;
}

//// IHandleLeftSideClick ////////////////////////////////////////////////////

void	pfJournalBook::IHandleLeftSideClick( void )
{
	if( fBookGUIs[fCurBookGUI]->CurrentlyTurning() )
		return;

	Int32 idx = IFindCurrVisibleLink( false, false );
	if( idx != -1 )
	{
		if( fVisibleLinks[ idx ]->fFlags & pfEsHTMLChunk::kActAsCB )
			IHandleCheckClick( idx, pfBookData::kLeftSide );
		else
			ISendNotify( kNotifyImageLink, fVisibleLinks[ idx ]->fEventID );
		return;
	}

	// No link found that we're inside of, so just do the default behavior of turning the page
	PreviousPage();
}

void	pfJournalBook::IHandleRightSideClick( void )
{
	if( fBookGUIs[fCurBookGUI]->CurrentlyTurning() )
		return;

	Int32 idx = IFindCurrVisibleLink( true, false );
	if( idx != -1 )
	{
		if( fVisibleLinks[ idx ]->fFlags & pfEsHTMLChunk::kActAsCB )
			IHandleCheckClick( idx, pfBookData::kRightSide );
		else
			ISendNotify( kNotifyImageLink, fVisibleLinks[ idx ]->fEventID );
		return;
	}

	// No link found that we're inside of, so just do the default behavior of turning the page
	NextPage();
}

//// IHandleCheckClick ///////////////////////////////////////////////////////
// Process a click on the given "check box" image

void	pfJournalBook::IHandleCheckClick( UInt32 idx, pfBookData::WhichSide which )
{
	// Special processing for checkboxes--toggle our state, switch our opacity
	// and then send a notify about our new state
	hsBool check = ( fVisibleLinks[ idx ]->fFlags & pfEsHTMLChunk::kChecked ) ? false : true;
	if( check )
	{
		fVisibleLinks[ idx ]->fFlags |= pfEsHTMLChunk::kChecked;
//		fVisibleLinks[ idx ]->fCurrOpacity = fVisibleLinks[ idx ]->fMaxOpacity;
	}
	else
	{
		fVisibleLinks[ idx ]->fFlags &= ~pfEsHTMLChunk::kChecked;
//		fVisibleLinks[ idx ]->fCurrOpacity = fVisibleLinks[ idx ]->fMinOpacity;
	}

	// Re-render the page we're on, to show the change in state
	IRenderPage( fCurrentPage + ( ( which == pfBookData::kLeftSide ) ? 0 : 1 ), ( which == pfBookData::kLeftSide ) ? pfJournalDlgProc::kTagLeftDTMap: pfJournalDlgProc::kTagRightDTMap );

	ISendNotify( check ? kNotifyImageLink : kNotifyCheckUnchecked, fVisibleLinks[ idx ]->fEventID );

	// Register for FX processing, so we can fade the checkbox in
	fBookGUIs[fCurBookGUI]->RegisterForSFX( (pfBookData::WhichSide)( fBookGUIs[fCurBookGUI]->CurSFXPages() | which ) );
}

//// GoToPage ////////////////////////////////////////////////////////////////
// For completeness...

void	pfJournalBook::GoToPage( UInt32 pageNumber )
{
	// Put us here, but only on an even page (odd pages go on the right, y'know)
	// (no need for a range check, going past the end simply puts you on a blank page, able to go backward but not forward)
	fCurrentPage = pageNumber & ~0x00000001;
	fVisibleLinks.Reset();
	IRenderPage( fCurrentPage, pfJournalDlgProc::kTagLeftDTMap );
	IRenderPage( fCurrentPage + 1, pfJournalDlgProc::kTagRightDTMap );
	fBookGUIs[fCurBookGUI]->UpdatePageCorners( pfBookData::kBothSides );
}

//// SetEditable /////////////////////////////////////////////////////////////

void	pfJournalBook::SetEditable(hsBool editable)
{
	if (fBookGUIs[fCurBookGUI]->IsEditable()) // make sure this GUI supports editing
	{
		fBookGUIs[fCurBookGUI]->EnableEditGUI(editable);
		fAreEditing = editable; // we may be editing the book, so change rendering/page flipping methods
		if (editable)
			fLastPage = 0; // setting this to 0 since editable books don't know what the last page is (it always changes)
	}
	else
		fWantEditing = editable; // we want to edit, but the gui doesn't support it, check again if the GUI changes
};

//// ForceCacheCalculations //////////////////////////////////////////////////
// Just forces a full calc of the cached info

void	pfJournalBook::ForceCacheCalculations( void )
{
	// Make sure our page starts are up-to-snuff, at least to this point
	IRecalcPageStarts( -1 );
}

// Tiny helper to convert hex values the *right* way
static UInt32	IConvertHex( const wchar_t *str )
{
	UInt32 value = 0;
	while( *str != 0 )
	{
		value <<= 4;
		switch( *str )
		{
			case L'0':			value |= 0x0;	break;
			case L'1':			value |= 0x1;	break;
			case L'2':			value |= 0x2;	break;
			case L'3':			value |= 0x3;	break;
			case L'4':			value |= 0x4;	break;
			case L'5':			value |= 0x5;	break;
			case L'6':			value |= 0x6;	break;
			case L'7':			value |= 0x7;	break;
			case L'8':			value |= 0x8;	break;
			case L'9':			value |= 0x9;	break;
			case L'a': case L'A':	value |= 0xa;	break;
			case L'b': case L'B':	value |= 0xb;	break;
			case L'c': case L'C':	value |= 0xc;	break;
			case L'd': case L'D':	value |= 0xd;	break;
			case L'e': case L'E':	value |= 0xe;	break;
			case L'f': case L'F':	value |= 0xf;	break;
		}
		str++;
	}

	return value;
}

//// ICompileSource //////////////////////////////////////////////////////////
// Compiles the given string of esHTML source into our compiled chunk list

hsBool	pfJournalBook::ICompileSource( const wchar_t *source, const plLocation &hintLoc )
{
	IFreeSource();


	pfEsHTMLChunk *chunk, *lastParChunk = TRACKED_NEW pfEsHTMLChunk( nil );
	const wchar_t	*c, *start;
	wchar_t	name[ 128 ], option[ 256 ];
	float bookWidth=1.0, bookHeight=1.0;
	UInt8 movieIndex = 0; // the index of a movie in the source (used for id purposes)

	plKey anotherKey;


	// Parse our source!
	for( start = c = source; *c != 0; )
	{
		// Are we on a tag?
		UInt8 type = IGetTagType( c );
		if( type != pfEsHTMLChunk::kEmpty )
		{
			// First, end the current paragraph chunk, which is a special case 'cause its 
			// text is defined outside the tag
			if( start == c )
			{
				// No actual text, just delete
				delete lastParChunk;
				lastParChunk = nil;
			}
			else if( lastParChunk != nil )
			{
				UInt32 count = ((UInt32)c - (UInt32)start)/2; // wchar_t is 2 bytes
				
				wchar_t *temp = TRACKED_NEW wchar_t[ count + 1 ];
				wcsncpy( temp, start, count );
				temp[count] = L'\0';
				lastParChunk->fText = temp;
				delete [] temp;

				// Special case to remove any last trailing carriage return
//				if( count > 1 && lastParChunk->fText[ count - 1 ] == '\n' )
//					lastParChunk->fText[ count - 1 ] = 0;

				fHTMLSource.Append( lastParChunk );
			}

			// What chunk are we making now?
			switch( type )
			{
				case pfEsHTMLChunk::kParagraph:
					c += 2;
					chunk = TRACKED_NEW pfEsHTMLChunk( nil );
					chunk->fFlags = IFindLastAlignment();
					while( IGetNextOption( c, name, option ) )
					{
						if( wcsicmp( name, L"align" ) == 0 )
						{
							if( wcsicmp( option, L"left" ) == 0 )
								chunk->fFlags = pfEsHTMLChunk::kLeft;
							else if( wcsicmp( option, L"center" ) == 0 )
								chunk->fFlags = pfEsHTMLChunk::kCenter;
							else if( wcsicmp( option, L"right" ) == 0 )
								chunk->fFlags = pfEsHTMLChunk::kRight;
						}
					}
					// Append text to this one (don't add to source just yet)
					lastParChunk = chunk;
					break;

				case pfEsHTMLChunk::kImage:
					c += 4;
					chunk = TRACKED_NEW pfEsHTMLChunk( nil, 0 );
					while( IGetNextOption( c, name, option ) )
					{
						if( wcsicmp( name, L"align" ) == 0 )
						{
							chunk->fFlags &= ~pfEsHTMLChunk::kAlignMask;
							if( wcsicmp( option, L"left" ) == 0 )
								chunk->fFlags |= pfEsHTMLChunk::kLeft;
							else if( wcsicmp( option, L"center" ) == 0 )
								chunk->fFlags |= pfEsHTMLChunk::kCenter;
							else if( wcsicmp( option, L"right" ) == 0 )
								chunk->fFlags |= pfEsHTMLChunk::kRight;
						}
						else if( wcsicmp( name, L"src" ) == 0 )
						{
							// Name of mipmap source
							chunk->fImageKey = IGetMipmapKey( option, hintLoc );
						}
						else if( wcsicmp( name, L"link" ) == 0 )
						{
							chunk->fEventID = _wtoi( option );
							chunk->fFlags |= pfEsHTMLChunk::kCanLink;
						}
						else if( wcsicmp( name, L"blend" ) == 0 )
						{
							if( wcsicmp( option, L"alpha" ) == 0 )
								chunk->fFlags |= pfEsHTMLChunk::kBlendAlpha;
						}
						else if( wcsicmp( name, L"pos" ) == 0 )
						{
							chunk->fFlags |= pfEsHTMLChunk::kFloating;

							wchar_t *comma = wcschr( option, L',' );
							if( comma != nil )
							{

								chunk->fAbsoluteY = _wtoi( comma + 1 );
								*comma = 0;
							}
							chunk->fAbsoluteX = _wtoi( option );
						}
						else if( wcsicmp( name, L"glow" ) == 0 )
						{
							chunk->fFlags |= pfEsHTMLChunk::kGlowing;
							chunk->fFlags &= ~pfEsHTMLChunk::kActAsCB;

							char *cOption = hsWStringToString(option);
							char *comma = strchr( cOption, ',' );
							if( comma != nil )
							{
								char *comma2 = strchr( comma + 1, ',' );
								if( comma2 != nil )
								{
									chunk->fMaxOpacity = (hsScalar)atof( comma2 + 1 );
									*comma2 = 0;
								}
								chunk->fMinOpacity = (hsScalar)atof( comma + 1 );
								*comma = 0;
							}
							chunk->fSFXTime = (hsScalar)atof( cOption );
							delete [] cOption;
						}
						else if( wcsicmp( name, L"opacity" ) == 0 )
						{
							chunk->fFlags |= pfEsHTMLChunk::kTranslucent;
							char *cOption = hsWStringToString(option);
							chunk->fCurrOpacity = (hsScalar)atof( cOption );
							delete [] cOption;
						}
						else if( wcsicmp( name, L"check" ) == 0 )
						{
							chunk->fFlags |= pfEsHTMLChunk::kActAsCB;
							chunk->fFlags &= ~pfEsHTMLChunk::kGlowing;

							wchar_t *comma = wcschr( option, L',' );
							if( comma != nil )
							{
								wchar_t *comma2 = wcschr( comma + 1, L',' );
								if( comma2 != nil )
								{
									if( _wtoi( comma2 + 1 ) != 0 )
										chunk->fFlags |= pfEsHTMLChunk::kChecked;
									*comma2 = 0;
								}
								UInt32 c = IConvertHex( comma + 1 );
								if( wcslen( comma + 1 ) <= 6 )
									c |= 0xff000000;	// Add in full alpha if none specified
								chunk->fOffColor.FromARGB32( c );
								*comma = 0;
							}
							UInt32 c = IConvertHex( option );
							if( wcslen( option ) <= 6 )
								c |= 0xff000000;	// Add in full alpha if none specified
							chunk->fOnColor.FromARGB32( c );

							if( chunk->fFlags & pfEsHTMLChunk::kChecked )
								chunk->fCurrColor = chunk->fOnColor;
							else
								chunk->fCurrColor = chunk->fOffColor;
						}
						else if (wcsicmp(name,L"resize")==0)
						{
							if (wcsicmp(option,L"no")==0)
								chunk->fNoResizeImg = true;
						}
					}
					if( chunk->fImageKey != nil )
						fHTMLSource.Append( chunk );
					else
						delete chunk;
					// Start new paragraph chunk after this one
					lastParChunk = TRACKED_NEW pfEsHTMLChunk( nil );
					lastParChunk->fFlags = IFindLastAlignment();
					break;

				case pfEsHTMLChunk::kCover:
					// Don't create an actual chunk for this one, just use the "src" and 
					// grab the mipmap key for our cover
					c += 6;
					while( IGetNextOption( c, name, option ) )
					{
						if( wcsicmp( name, L"src" ) == 0 )
						{
							// Name of mipmap source
							anotherKey = IGetMipmapKey( option, hintLoc );
							if( anotherKey != nil )
							{
								fCoverMipKey = anotherKey;
								fCoverFromHTML = true;
							}
						}
						if( wcsicmp( name, L"tint" ) == 0 )
						{
							fTintCover = true;
							fCoverTint.FromARGB32( wcstol( option, nil, 16 ) | 0xff000000 );
						}
						if( wcsicmp( name, L"tintfirst" ) == 0 )
						{
							if (wcsicmp(option,L"no")==0)
								fTintFirst = false;
						}
					}
					// Still gotta create a new par chunk
					lastParChunk = TRACKED_NEW pfEsHTMLChunk( nil );
					lastParChunk->fFlags = IFindLastAlignment();
					break;

				case pfEsHTMLChunk::kPageBreak:
					c += 3;
					chunk = TRACKED_NEW pfEsHTMLChunk();
					while( IGetNextOption( c, name, option ) )
					{
					}
					fHTMLSource.Append( chunk );
					// Start new paragraph chunk after this one
					lastParChunk = TRACKED_NEW pfEsHTMLChunk( nil );
					lastParChunk->fFlags = IFindLastAlignment();
					break;

				case pfEsHTMLChunk::kFontChange:
					c += 5;
					chunk = TRACKED_NEW pfEsHTMLChunk( nil, 0, 0 );
					while( IGetNextOption( c, name, option ) )
					{
						if( wcsicmp( name, L"style" ) == 0 )
						{
							UInt8 guiFlags = 0;
							if( wcsicmp( option, L"b" ) == 0 )
							{
								chunk->fFlags = pfEsHTMLChunk::kFontBold;
								guiFlags = plDynamicTextMap::kFontBold;
							}
							else if( wcsicmp( option, L"i" ) == 0 )
							{
								chunk->fFlags = pfEsHTMLChunk::kFontItalic;
								guiFlags = plDynamicTextMap::kFontItalic;
							}
							else if( wcsicmp( option, L"bi" ) == 0 )
							{
								chunk->fFlags = pfEsHTMLChunk::kFontBold | pfEsHTMLChunk::kFontItalic;
								guiFlags = plDynamicTextMap::kFontBold | plDynamicTextMap::kFontItalic;
							}
							else
								chunk->fFlags = pfEsHTMLChunk::kFontRegular;
							if (fBookGUIs[fCurBookGUI]->IsEditable())
							{
								fBookGUIs[fCurBookGUI]->GetEditCtrl(pfJournalDlgProc::kTagRightEditCtrl)->SetFontStyle(guiFlags);
								fBookGUIs[fCurBookGUI]->GetEditCtrl(pfJournalDlgProc::kTagLeftEditCtrl)->SetFontStyle(guiFlags);
								fBookGUIs[fCurBookGUI]->GetEditCtrl(pfJournalDlgProc::kTagTurnFrontEditCtrl)->SetFontStyle(guiFlags);
								fBookGUIs[fCurBookGUI]->GetEditCtrl(pfJournalDlgProc::kTagTurnBackEditCtrl)->SetFontStyle(guiFlags);
							}
						}
						else if( wcsicmp( name, L"face" ) == 0 )
						{
							// Name of mipmap source
							chunk->fText = option;
							if (fBookGUIs[fCurBookGUI]->IsEditable())
							{
								char *fontFace = hsWStringToString(option);
								fBookGUIs[fCurBookGUI]->GetEditCtrl(pfJournalDlgProc::kTagRightEditCtrl)->SetFontFace(fontFace);
								fBookGUIs[fCurBookGUI]->GetEditCtrl(pfJournalDlgProc::kTagLeftEditCtrl)->SetFontFace(fontFace);
								fBookGUIs[fCurBookGUI]->GetEditCtrl(pfJournalDlgProc::kTagTurnFrontEditCtrl)->SetFontFace(fontFace);
								fBookGUIs[fCurBookGUI]->GetEditCtrl(pfJournalDlgProc::kTagTurnBackEditCtrl)->SetFontFace(fontFace);
								delete [] fontFace;
							}
						}
						else if( wcsicmp( name, L"size" ) == 0 )
						{
							chunk->fFontSize = _wtoi( option );
							if (fBookGUIs[fCurBookGUI]->IsEditable())
							{
								fBookGUIs[fCurBookGUI]->GetEditCtrl(pfJournalDlgProc::kTagRightEditCtrl)->SetFontSize(chunk->fFontSize);
								fBookGUIs[fCurBookGUI]->GetEditCtrl(pfJournalDlgProc::kTagLeftEditCtrl)->SetFontSize(chunk->fFontSize);
								fBookGUIs[fCurBookGUI]->GetEditCtrl(pfJournalDlgProc::kTagTurnFrontEditCtrl)->SetFontSize(chunk->fFontSize);
								fBookGUIs[fCurBookGUI]->GetEditCtrl(pfJournalDlgProc::kTagTurnBackEditCtrl)->SetFontSize(chunk->fFontSize);
							}
						}
						else if( wcsicmp( name, L"color" ) == 0 )
						{
							chunk->fColor.FromARGB32( wcstol( option, nil, 16 ) | 0xff000000 );
							chunk->fFlags |= pfEsHTMLChunk::kFontColor;
							if (fBookGUIs[fCurBookGUI]->IsEditable())
							{
								fBookGUIs[fCurBookGUI]->GetEditCtrl(pfJournalDlgProc::kTagRightEditCtrl)->SetFontColor(chunk->fColor);
								fBookGUIs[fCurBookGUI]->GetEditCtrl(pfJournalDlgProc::kTagLeftEditCtrl)->SetFontColor(chunk->fColor);
								fBookGUIs[fCurBookGUI]->GetEditCtrl(pfJournalDlgProc::kTagTurnFrontEditCtrl)->SetFontColor(chunk->fColor);
								fBookGUIs[fCurBookGUI]->GetEditCtrl(pfJournalDlgProc::kTagTurnBackEditCtrl)->SetFontColor(chunk->fColor);
							}
						}
						else if( wcsicmp( name, L"spacing" ) == 0 )
						{
							chunk->fLineSpacing = _wtoi(option);
							chunk->fFlags |= pfEsHTMLChunk::kFontSpacing;
						}
					}
					fHTMLSource.Append( chunk );
					// Start new paragraph chunk after this one
					lastParChunk = TRACKED_NEW pfEsHTMLChunk( nil );
					lastParChunk->fFlags = IFindLastAlignment();
					break;

				case pfEsHTMLChunk::kMargin:
					c += 7;
					while(IGetNextOption(c,name,option))
					{
						if (wcsicmp(name,L"top") == 0)
							fPageTMargin = _wtoi(option);
						else if (wcsicmp(name,L"left") == 0)
							fPageLMargin = _wtoi(option);
						else if (wcsicmp(name,L"bottom") == 0)
							fPageBMargin = _wtoi(option);
						else if (wcsicmp(name,L"right") == 0)
							fPageRMargin = _wtoi(option);
					}
					// set the edit controls to the margins we just set
					if (fBookGUIs[fCurBookGUI]->IsEditable())
					{
						fBookGUIs[fCurBookGUI]->GetEditCtrl(pfJournalDlgProc::kTagRightEditCtrl)->SetMargins(fPageTMargin,fPageLMargin,fPageBMargin,fPageRMargin);
						fBookGUIs[fCurBookGUI]->GetEditCtrl(pfJournalDlgProc::kTagLeftEditCtrl)->SetMargins(fPageTMargin,fPageLMargin,fPageBMargin,fPageRMargin);
						fBookGUIs[fCurBookGUI]->GetEditCtrl(pfJournalDlgProc::kTagTurnFrontEditCtrl)->SetMargins(fPageTMargin,fPageLMargin,fPageBMargin,fPageRMargin);
						fBookGUIs[fCurBookGUI]->GetEditCtrl(pfJournalDlgProc::kTagTurnBackEditCtrl)->SetMargins(fPageTMargin,fPageLMargin,fPageBMargin,fPageRMargin);
					}
					// Start a new paragraph chunk after this one
					lastParChunk = TRACKED_NEW pfEsHTMLChunk(nil);
					lastParChunk->fFlags = IFindLastAlignment();
					break;

				case pfEsHTMLChunk::kBook:
					c += 5;
					// don't actually create a chunk, just set the book size
					while (IGetNextOption(c,name,option))
					{
						if (wcsicmp(name,L"height") == 0)
						{
							char *temp = hsWStringToString(option);
							bookHeight = (float)atof(temp);
							delete [] temp;
						}
						else if (wcsicmp(name,L"width") == 0)
						{
							char *temp = hsWStringToString(option);
							bookWidth = (float)atof(temp);
							delete [] temp;
						}
					}
					fHeightScale = 1.f - bookHeight;
					fWidthScale = 1.f - bookWidth;
					
					// Still gotta create a new par chunk
					lastParChunk = TRACKED_NEW pfEsHTMLChunk( nil );
					lastParChunk->fFlags = IFindLastAlignment();
					break;

				case pfEsHTMLChunk::kDecal:
					c += 6;
					chunk = TRACKED_NEW pfEsHTMLChunk( nil, 0 );
					chunk->fType = pfEsHTMLChunk::kDecal;
					while( IGetNextOption( c, name, option ) )
					{
						if( wcsicmp( name, L"align" ) == 0 )
						{
							chunk->fFlags &= ~pfEsHTMLChunk::kAlignMask;
							if( wcsicmp( option, L"left" ) == 0 )
								chunk->fFlags |= pfEsHTMLChunk::kLeft;
							else if( wcsicmp( option, L"center" ) == 0 )
								chunk->fFlags |= pfEsHTMLChunk::kCenter;
							else if( wcsicmp( option, L"right" ) == 0 )
								chunk->fFlags |= pfEsHTMLChunk::kRight;
						}
						else if( wcsicmp( name, L"src" ) == 0 )
						{
							// Name of mipmap source
							chunk->fImageKey = IGetMipmapKey( option, hintLoc );
						}
						else if( wcsicmp( name, L"pos" ) == 0 )
						{
							chunk->fFlags |= pfEsHTMLChunk::kFloating;

							wchar_t *comma = wcschr( option, L',' );
							if( comma != nil )
							{

								chunk->fAbsoluteY = _wtoi( comma + 1 );
								*comma = 0;
							}
							chunk->fAbsoluteX = _wtoi( option );
						}
						else if (wcsicmp(name,L"resize")==0)
						{
							if (wcsicmp(option,L"no")==0)
								chunk->fNoResizeImg = true;
						}
						else if (wcsicmp(name,L"tint")==0)
						{
							if (wcsicmp(option,L"yes")==0)
								chunk->fTintDecal = true;
						}
					}
					// add it to our cover decals list (this is tag is essentially thrown away as far as the parser cares)
					if( chunk->fImageKey != nil )
						fCoverDecals.Append( chunk );
					else
						delete chunk;
					// Start new paragraph chunk after this one
					lastParChunk = TRACKED_NEW pfEsHTMLChunk( nil );
					lastParChunk->fFlags = IFindLastAlignment();
					break;

				case pfEsHTMLChunk::kMovie:
					c += 6;
					chunk = TRACKED_NEW pfEsHTMLChunk( nil, 0 );
					chunk->fType = pfEsHTMLChunk::kMovie;
					while( IGetNextOption( c, name, option ) )
					{
						if( wcsicmp( name, L"align" ) == 0 )
						{
							chunk->fFlags &= ~pfEsHTMLChunk::kAlignMask;
							if( wcsicmp( option, L"left" ) == 0 )
								chunk->fFlags |= pfEsHTMLChunk::kLeft;
							else if( wcsicmp( option, L"center" ) == 0 )
								chunk->fFlags |= pfEsHTMLChunk::kCenter;
							else if( wcsicmp( option, L"right" ) == 0 )
								chunk->fFlags |= pfEsHTMLChunk::kRight;
						}
						else if( wcsicmp( name, L"src" ) == 0 )
						{
							chunk->fText = option;
						}
						else if( wcsicmp( name, L"link" ) == 0 )
						{
							chunk->fEventID = _wtoi( option );
							chunk->fFlags |= pfEsHTMLChunk::kCanLink;
						}
						else if( wcsicmp( name, L"pos" ) == 0 )
						{
							chunk->fFlags |= pfEsHTMLChunk::kFloating;

							wchar_t *comma = wcschr( option, L',' );
							if( comma != nil )
							{

								chunk->fAbsoluteY = _wtoi( comma + 1 );
								*comma = 0;
							}
							chunk->fAbsoluteX = _wtoi( option );
						}
						else if (wcsicmp(name,L"resize")==0)
						{
							if (wcsicmp(option,L"no")==0)
								chunk->fNoResizeImg = true;
						}
						else if (wcsicmp(name,L"oncover")==0)
						{
							if (wcsicmp(option,L"yes")==0)
								chunk->fOnCover = true;
						}
						else if (wcsicmp(name,L"loop")==0)
						{
							if (wcsicmp(option,L"no")==0)
								chunk->fLoopMovie = false;
						}
					}
					chunk->fMovieIndex = movieIndex;
					movieIndex++;
					if (chunk->fOnCover)
					{
						if( chunk->fText != L"" )
							fCoverDecals.Append( chunk );
						else
							delete chunk;
					}
					else
					{
						if( chunk->fText != L"" )
							fHTMLSource.Append( chunk );
						else
							delete chunk;
					}
					// Start new paragraph chunk after this one
					lastParChunk = TRACKED_NEW pfEsHTMLChunk( nil );
					lastParChunk->fFlags = IFindLastAlignment();
					break;
					
				case pfEsHTMLChunk::kEditable:
					c += 9;
					SetEditable(true);
					chunk = TRACKED_NEW pfEsHTMLChunk();
					while( IGetNextOption( c, name, option ) )
					{
					}
					fHTMLSource.Append( chunk );
					// Start new paragraph chunk after this one
					lastParChunk = TRACKED_NEW pfEsHTMLChunk( nil );
					lastParChunk->fFlags = IFindLastAlignment();
					break;
			}

			start = c;
		}
		else
		{
			// Keep looking
			c++;
		}
	}

	// Final bit goes into the last paragraph chunk we had
	if( start == c )
	{
		// No actual text, just delete
		delete lastParChunk;
		lastParChunk = nil;
	}
	else if( lastParChunk != nil )
	{
		UInt32 count = (UInt32)c - (UInt32)start;
		
		wchar_t *temp = TRACKED_NEW wchar_t[ count + 1 ];
		wcsncpy( temp, start, count + 1 );
		lastParChunk->fText = temp;
		delete [] temp;
		
		// Special case to remove any last trailing carriage return
//		if( count > 1 && lastParChunk->fText[ count - 1 ] == '\n' )
//			lastParChunk->fText[ count - 1 ] = 0;

		fHTMLSource.Append( lastParChunk );
	}

	// Reset a few
	fPageStarts.Reset();
	fPageStarts.Append( 0 );
	if (fAreEditing)
		fLastPage = 0;
	else
		fLastPage = -1;
	
	return true;
}

UInt8	pfJournalBook::IGetTagType( const wchar_t *string )
{
	if( string[ 0 ] != '<' )
		return pfEsHTMLChunk::kEmpty;

	struct TagRec
	{
		const wchar_t *fTag;
		UInt8		fType;
	} tags[] = { { L"p", pfEsHTMLChunk::kParagraph },
				{ L"img", pfEsHTMLChunk::kImage },
				{ L"pb", pfEsHTMLChunk::kPageBreak },
				{ L"font", pfEsHTMLChunk::kFontChange },
				{ L"margin", pfEsHTMLChunk::kMargin },
				{ L"cover", pfEsHTMLChunk::kCover },
				{ L"book", pfEsHTMLChunk::kBook },
				{ L"decal", pfEsHTMLChunk::kDecal },
				{ L"movie", pfEsHTMLChunk::kMovie },
				{ L"editable", pfEsHTMLChunk::kEditable },
				{ nil, pfEsHTMLChunk::kEmpty } };
				

	UInt32 i;
	for( i = 0; tags[ i ].fTag != nil; i++ )
	{
		if( wcsnicmp( string + 1, tags[ i ].fTag, wcslen( tags[ i ].fTag ) ) == 0 )
		{
			// Found tag--but only space or end tag marker allowed afterwards
			char end = (char)string[ wcslen( tags[ i ].fTag ) + 1 ];
			if( end == '>' || end == ' ' )
				return tags[ i ].fType;
		}
	}

	return pfEsHTMLChunk::kEmpty;
}

hsBool	pfJournalBook::IGetNextOption( const wchar_t *&string, wchar_t *name, wchar_t *option )
{
	const wchar_t *c;


	// Advance past any white space
	while( *string == L' ' )
		string++;

	if( *string == L'>' )
	{
		string++;
		return false;
	}

	// Advance to =
	c = string;
	while( *string != L'>' && *string != L' ' && *string != L'=' && *string != L'\0' )
		string++;

	if( *string != L'=' )
		return false;

	// Copy name
	UInt32 len = ((UInt32)string - (UInt32)c)/2; // divide length by 2 because each character is two bytes
	wcsncpy( name, c, len );
	name[len] = L'\0';

	// Find start of option value
	string++;
	while( *string == L' ' )
		string++;

	if( *string == L'\0' || *string == L'>' )
		return false;

	if( *string == L'\"' )
	{
		// Search for other quote
		string++;
		c = string;
		while( *string != L'>' && *string != L'\"' && *string != L'\0' )
			string++;

		len = ((UInt32)string - (UInt32)c)/2; // divide length by 2 because each character is two bytes
		wcsncpy( option, c, len );
		option[len] = L'\0';
		
		if( *string == L'\"' )
			string++;

		return true;
	}

	// Non-quoted token
	c = string;
	while( *string != L' ' && *string != L'>' && *string != L'\0' )
		string++;

	len = ((UInt32)string - (UInt32)c)/2; // divide length by 2 because each character is two bytes
	wcsncpy( option, c, len );
	option[len] = L'\0';
	
	return true;
}

void	pfJournalBook::IFreeSource( void )
{
	UInt32 i;
	
	for( i = 0; i < fHTMLSource.GetCount(); i++ )
		delete fHTMLSource[ i ];
	fHTMLSource.Reset();

	for( i = 0; i < fCoverDecals.GetCount(); i++ )
		delete fCoverDecals[ i ];
	fCoverDecals.Reset();

	for( i = 0; i < fLoadedMovies.GetCount(); i++ )
	{
		plLayerBink *movie = fLoadedMovies[ i ]->movieLayer;
		movie->GetKey()->UnRefObject();
		delete fLoadedMovies[ i ];
	}
	fLoadedMovies.Reset();
}

//// IGetMipmapKey ///////////////////////////////////////////////////////////
// Looks up the key for a mipmap given the image name. Note that the given
// location is treated as a hint; if the image isn't found in that location,
// the code will attempt to look in the currently loaded age for a matching
// image name.

#ifndef PLASMA_EXTERNAL_RELEASE
#include "../plJPEG/plJPEG.h"
#endif

plKey	pfJournalBook::IGetMipmapKey( const wchar_t *name, const plLocation &loc )
{
	char *cName = hsWStringToString(name);
#ifndef PLASMA_EXTERNAL_RELEASE
	if( strchr( cName, '/' ) != nil || strchr( cName, '\\' ) != nil )
	{
		// For internal use only--we allow local path names of JPEG images, to
		// facilitate fast prototyping
		plMipmap *mip = plJPEG::Instance().ReadFromFile( cName );
		hsgResMgr::ResMgr()->NewKey( cName, mip, loc );
		delete [] cName;
		return mip->GetKey();
	}
#endif

	// Try first to find in the given location
	plUoid myUoid( loc, plMipmap::Index(), cName );
	plKey key = hsgResMgr::ResMgr()->FindKey( myUoid );
	if( key != nil )
	{
		delete [] cName;
		return key;
	}


	// Next, try our "global" pre-defined age
	const plLocation &globLoc = plKeyFinder::Instance().FindLocation( "GUI", "BkBookImages" );
	myUoid = plUoid( globLoc, plMipmap::Index(), cName );
	key = hsgResMgr::ResMgr()->FindKey( myUoid );
	if( key != nil )
	{
		delete [] cName;
		return key;
	}

	// Do a search through our current age with just the name given
	if( plNetClientMgr::GetInstance() != nil )
	{
		const char *thisAge = plAgeLoader::GetInstance()->GetCurrAgeDesc().GetAgeName();
		if( thisAge != nil )
		{
			key = plKeyFinder::Instance().StupidSearch( thisAge, nil, plMipmap::Index(), cName, true );
			if( key != nil )
			{
				delete [] cName;
				return key;
			}
		}
	}

	delete [] cName;
	return nil;
}

//// IRenderPage /////////////////////////////////////////////////////////////
//	Takes the given page out of the source and renders it into the specified
//	DTMap (by GUI tag ID). If no page is cached after this one, also updates
//	the various cached info about page endings, etc.

void	pfJournalBook::IRenderPage( UInt32 page, UInt32 whichDTMap, hsBool suppressRendering /*= false*/ )
{
	if (fAreEditing)
		return; // we don't render if we are editing the book
	
	// Grab the DTMap via the GUI system
	plDynamicTextMap *dtMap = fBookGUIs[fCurBookGUI]->GetDTMap( whichDTMap );
	hsAssert( dtMap != nil, "Invalid DT map in IRenderPage()" );

	loadedMovie *movie = nil;
	bool movieAlreadyLoaded = false;

	// Make sure our page starts are up-to-snuff, at least to this point
	IRecalcPageStarts( page );

	// Render!
	hsColorRGBA color;
	color.Set( 0, 0, 0, 0 );
	if( !suppressRendering )
		dtMap->ClearToColor( color );

	hsGMaterial *material = nil;
	if (whichDTMap == pfJournalDlgProc::kTagLeftDTMap)
		material = fBookGUIs[fCurBookGUI]->PageMaterial(pfBookData::kLeftPage);
	else if (whichDTMap == pfJournalDlgProc::kTagRightDTMap)
		material = fBookGUIs[fCurBookGUI]->PageMaterial(pfBookData::kRightPage);
	else if (whichDTMap == pfJournalDlgProc::kTagTurnFrontDTMap)
		material = fBookGUIs[fCurBookGUI]->PageMaterial(pfBookData::kTurnFrontPage);
	else if (whichDTMap == pfJournalDlgProc::kTagTurnBackDTMap)
		material = fBookGUIs[fCurBookGUI]->PageMaterial(pfBookData::kTurnBackPage);

	if (material)
	{
		// clear any exiting layers (movies) from the material
		int i;
		for( i = 0; i < material->GetNumLayers(); i++ ) // remove all plLayerBink layers
		{
			plLayerInterface *matLayer = material->GetLayer(i);
			plLayerBink *bink = plLayerBink::ConvertNoRef(matLayer);
			if (bink) // if it was a bink layer
			{
				plMatRefMsg* refMsg = TRACKED_NEW plMatRefMsg(material->GetKey(), plRefMsg::kOnRemove, i, plMatRefMsg::kLayer); // remove it
				hsgResMgr::ResMgr()->SendRef(material->GetLayer(i)->GetKey(), refMsg, plRefFlags::kActiveRef);
			}
		}
	}

	hsAssert(page < fPageStarts.GetCount() || page > fLastPage, "UnInitialized page start!");
	if( page <= fLastPage 
		&& page < fPageStarts.GetCount())	// Added this as a crash-prevention bandaid - MT
	{
		UInt32 idx;
		UInt16 width, height, y, x, ascent, lastX, lastY;
		
		UInt8		fontFlags, fontSize;
		const wchar_t *fontFace;
		hsColorRGBA	fontColor;
		Int16		fontSpacing;
		hsBool		needSFX = false;

		// Find and set initial font properties
		IFindFontProps( fPageStarts[ page ], fontFace, fontSize, fontFlags, fontColor, fontSpacing );
		dtMap->SetFont( fontFace, fontSize, fontFlags, false );
		dtMap->SetTextColor( fontColor, true );
		dtMap->SetLineSpacing(fontSpacing);

		for( idx = fPageStarts[ page ], x = (UInt16)fPageLMargin, y = (UInt16)fPageTMargin;
			y < (UInt16)(512 - fPageTMargin - fPageBMargin) && idx < fHTMLSource.GetCount(); idx++ )
		{
			if( fPageStarts.GetCount() > page + 1 && idx == fPageStarts[ page + 1 ] )
				break;	// Just go ahead and break at the start of the next page, since we already found it

			pfEsHTMLChunk *chunk = fHTMLSource[ idx ];

			switch( chunk->fType )
			{
				case pfEsHTMLChunk::kParagraph:				
					if( ( chunk->fFlags & pfEsHTMLChunk::kAlignMask ) == pfEsHTMLChunk::kLeft )
					{
						dtMap->SetJustify( plDynamicTextMap::kLeftJustify );
						x = (UInt16)fPageLMargin; // reset X if our justification changes
					}
					else if( ( chunk->fFlags & pfEsHTMLChunk::kAlignMask ) == pfEsHTMLChunk::kRight )
					{
						dtMap->SetJustify( plDynamicTextMap::kRightJustify );
						x = (UInt16)fPageLMargin; // reset X if our justification changes
					}
					else if( ( chunk->fFlags & pfEsHTMLChunk::kAlignMask ) == pfEsHTMLChunk::kCenter )
					{
						dtMap->SetJustify( plDynamicTextMap::kCenter );
						x = (UInt16)fPageLMargin; // reset X if our justification changes
					}

					dtMap->SetFirstLineIndent( (Int16)(x - fPageLMargin) );
					width = (UInt16)(512 - fPageLMargin - fPageRMargin);
					height = (UInt16)(512 - fPageBMargin - y);
					UInt32 lastChar;
					dtMap->CalcWrappedStringSize( chunk->fText.c_str(), &width, &height, &lastChar, &ascent, &lastX, &lastY );
					width = (UInt16)(512 - fPageLMargin - fPageRMargin);
					if( !suppressRendering )
						dtMap->DrawWrappedString( (UInt16)fPageLMargin, y, chunk->fText.c_str(), width, (UInt16)(512 - fPageBMargin - y), &lastX, &lastY );

					if( lastChar == 0 )
					{
						// This paragraph didn't fit on this page at *all*, so just bump it to the next
						// one artificially (the -- is to account for the for loop; see image handling below)
						y += 512;
						if( idx > fPageStarts[ page ] )
							idx--;
						break;
					}
					if( chunk->fText[ lastChar ] != 0 )
					{
						// Didn't get to render the whole paragraph in this go, so we're going to cheat
						// and split the paragraph up into two so that we can handle it properly. Note:
						// this changes the chunk array beyond this point, so we need to invalidate the
						// cache, but that's ok 'cause if we're doing this, it's probably invalid (or empty)
						// anyway
						int fTextLen = chunk->fText.length();
						wchar_t *s = TRACKED_NEW wchar_t[fTextLen+1];
						wcscpy(s,chunk->fText.c_str());
						s[fTextLen] = L'\0';

						// Note: Makes a copy of the string
						pfEsHTMLChunk *c2 = TRACKED_NEW pfEsHTMLChunk( &s[ lastChar ] );
						c2->fFlags = chunk->fFlags;
						fHTMLSource.Insert( idx + 1, c2 );

						// Clip and reallocate so we don't have two copies laying around
						s[ lastChar ] = L'\0';
						chunk->fText = s;
						delete [] s;

						// Invalidate our cache starting with the next page
						if( fPageStarts.GetCount() > page + 1 )
							fPageStarts.SetCount( page + 1 );

						y += 512;
						break;
					}
					
					x = lastX;
					y = (UInt16)(lastY - dtMap->GetCurrFont()->GetAscent());	// Since our text is top-justified

					if( ( chunk->fFlags & pfEsHTMLChunk::kAlignMask ) != pfEsHTMLChunk::kLeft )
					{
						// Ending X is not guaranteed to be anything useful if we're not left justified
						x = (UInt16)fPageLMargin;
					}

					break;

				case pfEsHTMLChunk::kImage:
					{
						plMipmap *mip = plMipmap::ConvertNoRef( chunk->fImageKey != nil ? chunk->fImageKey->ObjectIsLoaded() : nil );
						if( mip != nil )
						{
							// First, determine if we need to be processing FX messages
							if( chunk->fFlags & pfEsHTMLChunk::kGlowing )
								needSFX = true;
							else if( chunk->fFlags & pfEsHTMLChunk::kActAsCB )
							{
								// If our color doesn't match our checked state, we want to be fading it in
								hsColorRGBA &want = ( chunk->fFlags & pfEsHTMLChunk::kChecked ) ? chunk->fOnColor : chunk->fOffColor;
								if( want != chunk->fCurrColor )
									needSFX = true;
							}

							if( chunk->fFlags & pfEsHTMLChunk::kFloating )
							{
								// Floating image, ignore the text flow completely and just splat the image on!
								IDrawMipmap( chunk, chunk->fAbsoluteX, chunk->fAbsoluteY, mip, dtMap, whichDTMap, suppressRendering );
							}
							else
							{
								if( y + mip->GetHeight() >= 512 - fPageBMargin )
								{
									// Mipmap overlaps the bottom of this page, so forcibly break so we'll
									// end up marking the page break here (note that, unlike paragraphs, we
									// can't really break the mipmap into two...well, OK, we could, but it 
									// wouldn't make much sense :)
									y += (UInt16)(mip->GetHeight());

									// Wonderful, the break breaks us from the switch(), which means the for()
									// loops runs once more and increments idx. So this is to counter that.
									// (We better check tho, just to make sure nobody feeds us an extra-large 
									// image and sends us on an infinite loop)
									if( idx > fPageStarts[ page ] )
										idx--;
									break;
								}
								if( ( chunk->fFlags & pfEsHTMLChunk::kAlignMask ) == pfEsHTMLChunk::kLeft )
									x = (UInt16)fPageLMargin;
								else if( ( chunk->fFlags & pfEsHTMLChunk::kAlignMask ) == pfEsHTMLChunk::kRight )
									x = (UInt16)(512 - fPageRMargin - mip->GetWidth());
								else if( ( chunk->fFlags & pfEsHTMLChunk::kAlignMask ) == pfEsHTMLChunk::kCenter )
									x = (UInt16)(256 - ( mip->GetWidth() >> 1 ));

								IDrawMipmap( chunk, x, y, mip, dtMap, whichDTMap, suppressRendering );

								y += (UInt16)(mip->GetHeight());
								x = (UInt16)fPageLMargin;
							}
						}
					}
					break;

				case pfEsHTMLChunk::kPageBreak:
					// Time for some creative guesswork. See, if the user put a <pb> in at the end of a page,
					// but coincidentally we already broke to a new page, then the explicit <pb> is redundant,
					// so we don't want it. However, if we're at a new page *because* of a <pb>, then a new <pb>
					// is intentional, so we DO want to process it.
					if( idx == fPageStarts[ page ] )
					{
						// Did we get here b/c the last chunk was a pb?
						if( idx > 0 && fHTMLSource[ idx - 1 ]->fType != pfEsHTMLChunk::kPageBreak )
						{
							// Nope, so we DO want to ignore it!
							continue;
						}
					}
					y = (UInt16)(512 - fPageTMargin - fPageBMargin);
					x = (UInt16)fPageLMargin;
					break;

				case pfEsHTMLChunk::kFontChange:				
					IFindFontProps( idx, fontFace, fontSize, fontFlags, fontColor, fontSpacing );
					dtMap->SetFont( fontFace, fontSize, fontFlags, false );
					dtMap->SetTextColor( fontColor, true );
					dtMap->SetLineSpacing(fontSpacing);
					break;

				case pfEsHTMLChunk::kMovie:
					movieAlreadyLoaded = (IMovieAlreadyLoaded(chunk) != nil); // have we already cached it?
					plLayerBink *movieLayer = IMakeMovieLayer(chunk, x, y, (plMipmap*)dtMap, whichDTMap, suppressRendering);
					if (movieLayer)
					{
						// adjust the starting height of the movie if we are keeping it inline with the text
						UInt32 movieHeight = 0, movieWidth = 0;
						movieHeight = movieLayer->GetHeight();
						movieWidth = movieLayer->GetWidth();
						if(!(chunk->fFlags & pfEsHTMLChunk::kFloating ))
						{
							if( y + movieHeight >= 512 - fPageBMargin )
							{
								// Movie overlaps the bottom of this page, so forcibly break so we'll
								// end up marking the page break here (note that, unlike paragraphs, we
								// can't really break the Movie into two)
								y += (UInt16)movieHeight;

								// Wonderful, the break breaks us from the switch(), which means the for()
								// loops runs once more and increments idx. So this is to counter that.
								// (We better check tho, just to make sure nobody feeds us an extra-large 
								// image and sends us on an infinite loop)
								if( idx > fPageStarts[ page ] )
									idx--;
								break;
							}
							y += (UInt16)movieHeight;
							x = (UInt16)fPageLMargin;
						}
						if (!movieAlreadyLoaded) // if the movie wasn't already cached, cache it
						{
							movie = TRACKED_NEW loadedMovie;
							movie->movieLayer = movieLayer; // save the layer and chunk data
							movie->movieChunk = chunk;
							fLoadedMovies.Append(movie);
							movie = nil;
							movieAlreadyLoaded = false;
						}
						if (material && !suppressRendering)
							material->AddLayerViaNotify(movieLayer);
					}
					break;
			}
		}

		if( fPageStarts.GetCount() <= page + 1 )
			fPageStarts.ExpandAndZero( page + 2 );
		fPageStarts[ page + 1 ] = idx;

		if( idx == fHTMLSource.GetCount() )
			fLastPage = page;

		pfBookData::WhichSide thisWhich = ( whichDTMap == pfJournalDlgProc::kTagRightDTMap ) ? pfBookData::kRightSide : ( whichDTMap == pfJournalDlgProc::kTagLeftDTMap )  ? pfBookData::kLeftSide : pfBookData::kNoSides;
		if( needSFX )
			fBookGUIs[fCurBookGUI]->RegisterForSFX( (pfBookData::WhichSide)( fBookGUIs[fCurBookGUI]->CurSFXPages() | thisWhich ) );
		else
			fBookGUIs[fCurBookGUI]->RegisterForSFX( (pfBookData::WhichSide)( fBookGUIs[fCurBookGUI]->CurSFXPages() & ~thisWhich ) );
	}

	if( !suppressRendering )
		dtMap->FlushToHost();
}

//// IMoveMovies /////////////////////////////////////////////////////////////

void	pfJournalBook::IMoveMovies( hsGMaterial *source, hsGMaterial *dest )
{
	hsTArray<plLayerBink*> moviesOnPage;
	if (source && dest)
	{
		// clear any exiting layers (movies) from the material and save them to our local array
		int i;
		for( i = 0; i < source->GetNumLayers(); i++ ) // remove all plLayerBink layers
		{
			plLayerInterface *matLayer = source->GetLayer(i);
			plLayerBink *bink = plLayerBink::ConvertNoRef(matLayer);
			if (bink) // if it was a bink layer
			{
				plMatRefMsg* refMsg = TRACKED_NEW plMatRefMsg(source->GetKey(), plRefMsg::kOnRemove, i, plMatRefMsg::kLayer); // remove it
				hsgResMgr::ResMgr()->SendRef(source->GetLayer(i)->GetKey(), refMsg, plRefFlags::kActiveRef);
				moviesOnPage.Append(bink);
			}
		}
		// clear the destination's movies (if it has any)
		for( i = 0; i < dest->GetNumLayers(); i++ ) // remove all plLayerBink layers
		{
			plLayerInterface *matLayer = dest->GetLayer(i);
			plLayerBink *bink = plLayerBink::ConvertNoRef(matLayer);
			if (bink) // if it was a bink layer
			{
				plMatRefMsg* refMsg = TRACKED_NEW plMatRefMsg(dest->GetKey(), plRefMsg::kOnRemove, i, plMatRefMsg::kLayer); // remove it
				hsgResMgr::ResMgr()->SendRef(dest->GetLayer(i)->GetKey(), refMsg, plRefFlags::kActiveRef);
			}
		}
		// put the movies we ripped off the old page onto the new one
		for( i = 0; i < moviesOnPage.GetCount(); i++ )
		{
			dest->AddLayerViaNotify(moviesOnPage[i]);
		}
	}
}

//// IDrawMipmap /////////////////////////////////////////////////////////////

void	pfJournalBook::IDrawMipmap( pfEsHTMLChunk *chunk, UInt16 x, UInt16 y, plMipmap *mip, plDynamicTextMap *dtMap, UInt32 whichDTMap, hsBool dontRender )
{
	plMipmap *copy = TRACKED_NEW plMipmap();
	copy->CopyFrom(mip);
	if (chunk->fNoResizeImg)
	{
		// book is NOT square, there is a h/w ratio of 1/0.7
		// calc new size based on how the book has been skewed
		float xScale = (fWidthScale == 0) ? 1 : 1/(1-fWidthScale);
		float yScale = (fHeightScale == 0) ? 1 : 1/(1-fHeightScale);
		yScale *= 0.7; // adjust because the book isn't square
		UInt32 width = (UInt32)(mip->GetWidth()*xScale);
		UInt32 height = (UInt32)(mip->GetHeight()*yScale);
		UInt16 xShift;
		UInt16 yShift;

		if (dtMap->GetWidth() < width) width = dtMap->GetWidth();
		if (dtMap->GetHeight() < height) height = dtMap->GetHeight();

		if (height < mip->GetHeight())
		{
			yShift = (UInt16)((mip->GetHeight()-height)/2);
			if (y+yShift+height > dtMap->GetHeight())
				y = (UInt16)(dtMap->GetHeight()-height);
			else
				y += yShift;
		}
		else
		{
			yShift = (UInt16)((height-mip->GetHeight())/2);
			if (yShift > y)
				y = 0;
			else
				y -= yShift;
		}

		if (width < mip->GetWidth())
		{
			xShift = (UInt16)((mip->GetWidth()-width)/2);
			if (x+xShift+width > dtMap->GetWidth())
				x = (UInt16)(dtMap->GetWidth()-width);
			else
				x += xShift;
		}
		else
		{
			xShift = (UInt16)((width-mip->GetWidth())/2);
			if (xShift > x)
				x = 0;
			else
				x -= xShift;
		}
		
		copy->SetCurrLevel(0); // resize the image so it will look unchanged when rendered on the altered book
		copy->ResizeNicely((UInt16)width,(UInt16)height,plMipmap::kDefaultFilter);
	}
	if( !dontRender )
	{
		plMipmap::CompositeOptions	opts;
		if( chunk->fFlags & pfEsHTMLChunk::kActAsCB )
		{
			opts.fFlags = ( chunk->fFlags & pfEsHTMLChunk::kBlendAlpha ) ? 0 : plMipmap::kBlendWriteAlpha;
			opts.fRedTint = chunk->fCurrColor.r;
			opts.fGreenTint = chunk->fCurrColor.g;
			opts.fBlueTint = chunk->fCurrColor.b;
			opts.fOpacity = (UInt8)(chunk->fCurrColor.a * 255.f);
		}
		else 
		{
			if( chunk->fFlags & pfEsHTMLChunk::kGlowing )
				opts.fFlags = ( chunk->fFlags & pfEsHTMLChunk::kBlendAlpha ) ? 0 : plMipmap::kMaskSrcAlpha;
			else if (chunk->fFlags & pfEsHTMLChunk::kTranslucent)
				opts.fFlags = plMipmap::kMaskSrcAlpha;
			else
				opts.fFlags = ( chunk->fFlags & pfEsHTMLChunk::kBlendAlpha ) ? plMipmap::kCopySrcAlpha : plMipmap::kForceOpaque;
			opts.fOpacity = (UInt8)(chunk->fCurrOpacity * 255.f);
		}
		dtMap->Composite( copy, x, y, &opts );
	}

	if( chunk->fFlags & pfEsHTMLChunk::kCanLink )
	{
		if( whichDTMap == pfJournalDlgProc::kTagRightDTMap || whichDTMap == pfJournalDlgProc::kTagTurnFrontDTMap )
			x += (UInt16)(dtMap->GetWidth());	// Right page rects are offsetted to differentiate

		if (dontRender) // if we aren't rendering then this link isn't visible, but the index still needs to be valid, so give it a rect of 0,0,0,0
			chunk->fLinkRect.Set(0,0,0,0);
		else
			chunk->fLinkRect.Set( x, y, (Int16)(copy->GetWidth()), (Int16)(copy->GetHeight()) );
		fVisibleLinks.Append( chunk );
	}
	delete copy;
}

pfJournalBook::loadedMovie *pfJournalBook::IMovieAlreadyLoaded(pfEsHTMLChunk *chunk)
{
	int i;
	for (i=0; i<fLoadedMovies.GetCount(); i++) // filename and id# must both match
	{
		if ((chunk->fText == fLoadedMovies[i]->movieChunk->fText)&&(chunk->fMovieIndex == fLoadedMovies[i]->movieChunk->fMovieIndex))
			return fLoadedMovies[i];
	}
	return nil;
}

plKey pfJournalBook::GetMovie(UInt8 index)
{
	loadedMovie *movie = IGetMovieByIndex(index);
	if (movie)
		return movie->movieLayer->GetKey();
	return plKey(nil);
}

pfJournalBook::loadedMovie *pfJournalBook::IGetMovieByIndex(UInt8 index)
{
	int i;
	for (i=0; i<fLoadedMovies.GetCount(); i++)
	{
		if (fLoadedMovies[i]->movieChunk->fMovieIndex == index)
			return fLoadedMovies[i];
	}
	return nil;
}

plLayerBink *pfJournalBook::IMakeMovieLayer(pfEsHTMLChunk *chunk, UInt16 x, UInt16 y, plMipmap *baseMipmap, UInt32 whichDTMap, hsBool dontRender)
{
	// see if it's already loaded
	loadedMovie *movie = IMovieAlreadyLoaded(chunk);
	plLayer* layer = nil;
	plLayerBink* movieLayer = nil;
	UInt16 movieWidth=0,movieHeight=0;
	if (movie)
	{
		movieLayer = movie->movieLayer;
		layer = plLayer::ConvertNoRef(movieLayer->BottomOfStack());
		movieWidth = movieLayer->GetWidth();
		movieHeight = movieLayer->GetHeight();
	}
	else
	{
		// Create the layer and register it.

		// We'll need a unique name. This is a hack, but an effective hack.
		static int uniqueSuffix = 0;
		char buff[256];

		sprintf(buff, "%s_%d_ml", GetKey()->GetName(), uniqueSuffix);
		layer = TRACKED_NEW plLayer;
		hsgResMgr::ResMgr()->NewKey(buff, layer, GetKey()->GetUoid().GetLocation());

		sprintf(buff, "%s_%d_m", GetKey()->GetName(), uniqueSuffix++);
		movieLayer = TRACKED_NEW plLayerBink;
		hsgResMgr::ResMgr()->NewKey(buff, movieLayer, GetKey()->GetUoid().GetLocation());
		movieLayer->GetKey()->RefObject(); // we want to own a ref so we can nuke it at will

		movieLayer->AttachViaNotify(layer);

		// Initialize it.
		char *name = hsWStringToString(chunk->fText.c_str());
		movieLayer->SetMovieName(name);
		delete [] name;
		movieLayer->Eval(0,0,0); // set up the movie

		movieWidth = movieLayer->GetWidth();
		movieHeight = movieLayer->GetHeight();

		if (movieHeight == 0 || movieWidth == 0) // problem loading the file
		{
			movieLayer->GetKey()->UnRefObject();
			return nil;
		}
	}

	if (layer)
	{
		layer->InitToDefault();

		layer->SetAmbientColor(hsColorRGBA().Set(0.f, 0.f, 0.f, 1.0f));
		layer->SetOpacity(1.0f);

		// Blend flags, movies are opaque, but they don't take the whole page, so alphamapping them
		layer->SetBlendFlags(hsGMatState::kBlendAlpha);

		// Movie shouldn't have to ZWrite
		layer->SetZFlags(hsGMatState::kZNoZWrite);

		// No special shading.
		layer->SetShadeFlags(0);

		// Clamp all textures.
		layer->SetClampFlags(hsGMatState::kClampTexture);

		// Draw passes individually.
		layer->SetMiscFlags(hsGMatState::kMiscRestartPassHere);

		// Shared UV coordinates.
		layer->SetUVWSrc(0);

		if( chunk->fFlags & pfEsHTMLChunk::kFloating )
		{
			// Floating movie, ignore the text flow completely and just splat the movie on!
			x = chunk->fAbsoluteX;
			y = chunk->fAbsoluteY;
		}

		if( ( chunk->fFlags & pfEsHTMLChunk::kAlignMask ) == pfEsHTMLChunk::kLeft )
			x = (UInt16)fPageLMargin;
		else if( ( chunk->fFlags & pfEsHTMLChunk::kAlignMask ) == pfEsHTMLChunk::kRight )
			x = (UInt16)(baseMipmap->GetWidth() - fPageRMargin - movieWidth);
		else if( ( chunk->fFlags & pfEsHTMLChunk::kAlignMask ) == pfEsHTMLChunk::kCenter )
			x = (UInt16)((baseMipmap->GetWidth() >> 1) - (movieWidth >> 1));

		// x and y are in pixels, need to convert to a range of 0 to 1
		float xRel = (float)x/(float)baseMipmap->GetWidth();
		float yRel = (float)y/(float)baseMipmap->GetHeight();

		// Need to convert the scaling to texture space
		float xScale = (float)baseMipmap->GetWidth()/(float)movieWidth;
		float yScale = (float)baseMipmap->GetHeight()/(float)movieHeight;

		if (chunk->fNoResizeImg)
		{
			// book is NOT square, there is a h/w ratio of 1/0.7
			// calc new size based on how the book has been skewed
			xScale *= (fWidthScale == 0) ? 1 : (1-fWidthScale);
			yScale *= (fHeightScale == 0) ? 1 : (1-fHeightScale);
			yScale *= 1/0.7; // adjust because the book isn't square
		}

		hsVector3 scaleVec(xScale,yScale,1), translateVec(-(xRel*xScale),-(yRel*yScale),0);
		hsMatrix44 scaleMat, translateMat;
		scaleMat.MakeScaleMat(&scaleVec);
		translateMat.MakeTranslateMat(&translateVec);

		hsMatrix44 flipMat;
		if (chunk->fOnCover) // cover movies need to be y flipped
		{
			hsVector3 yTransVec(0,-1,0), invertYVec(1,-1,1);
			hsMatrix44 invertY, transY;
			invertY.MakeScaleMat(&invertYVec);
			transY.MakeTranslateMat(&yTransVec);
			flipMat = invertY * transY;
		}
		else // left page movies need to be x flipped
		{
			if ((whichDTMap == pfJournalDlgProc::kTagLeftDTMap) || (whichDTMap == pfJournalDlgProc::kTagTurnBackDTMap))
			{
				hsVector3 xTransVec(-1,0,0), invertXVec(-1,1,1);
				hsMatrix44 invertX, transX;
				invertX.MakeScaleMat(&invertXVec);
				transX.MakeTranslateMat(&xTransVec);
				flipMat = invertX * transX;
			}
			else
				flipMat = hsMatrix44::IdentityMatrix();
		}
		
		hsMatrix44 xfm;
		xfm = translateMat * scaleMat * flipMat;

		layer->SetTransform(xfm);
	}
	if( chunk->fFlags & pfEsHTMLChunk::kCanLink )
	{
		if( whichDTMap == pfJournalDlgProc::kTagRightDTMap || whichDTMap == pfJournalDlgProc::kTagTurnFrontDTMap )
			x += (UInt16)(baseMipmap->GetWidth());	// Right page rects are offsetted to differentiate

		if (dontRender) // if we aren't rendering then this link isn't visible, but the index still needs to be valid, so give it a rect of 0,0,0,0
			chunk->fLinkRect.Set(0,0,0,0);
		else
			chunk->fLinkRect.Set( x, y, movieWidth, movieHeight );
		fVisibleLinks.Append( chunk );
	}

	plAnimTimeConvert &timeConvert = movieLayer->GetTimeConvert();
	timeConvert.SetBegin(0);
	timeConvert.SetEnd(movieLayer->GetLength());
	if (chunk->fLoopMovie)
	{
		timeConvert.SetLoopPoints(0,movieLayer->GetLength());
		timeConvert.Loop();
	}
	timeConvert.Start(); // start the show!

	return movieLayer;
}

plLayerInterface *pfJournalBook::IMakeBaseLayer(plMipmap *image)
{
	// Create the layer and register it.

	// We'll need a unique name. This is a hack, but an effective hack.
	static int uniqueSuffix = 0;
	char buff[256];
	sprintf(buff, "%s_%d", GetKey()->GetName(), uniqueSuffix++);

	plLayer* layer = TRACKED_NEW plLayer;
	hsgResMgr::ResMgr()->NewKey(buff, layer, GetKey()->GetUoid().GetLocation());

	// Initialize it.
	layer->InitToDefault();

	layer->SetAmbientColor(hsColorRGBA().Set(0.f, 0.f, 0.f, 1.f));
	if (fTintCover)
		layer->SetRuntimeColor(hsColorRGBA().Set(fCoverTint.r, fCoverTint.g, fCoverTint.b, 1.f));
	layer->SetOpacity(1.f);

	// Blend flags, opaque for the bottom layer.
	layer->SetBlendFlags(0);

	// Only the bottom layer writes it's Z value.
	layer->SetZFlags(0);

	// No special shading.
	layer->SetShadeFlags(hsGMatState::kShadeReallyNoFog);

	// Clamp all textures.
	layer->SetClampFlags(hsGMatState::kClampTexture);

	// Draw passes individually.
	layer->SetMiscFlags(hsGMatState::kMiscRestartPassHere);

	// Shared UV coordinates.
	layer->SetUVWSrc(0);

	// Set up the transform.
	hsVector3 yTransVec(0,-1,0), invertYVec(1,-1,1);
	hsMatrix44 xfm, invertY, transY, flipY;
	invertY.MakeScaleMat(&invertYVec);
	transY.MakeTranslateMat(&yTransVec);
	flipY = invertY * transY;
	xfm = flipY;

	layer->SetTransform(xfm);

	// Set the texture (assumes mipmap is non-nil).
	plLayRefMsg* refMsg = TRACKED_NEW plLayRefMsg(layer->GetKey(), plRefMsg::kOnCreate, 0, plLayRefMsg::kTexture);
	hsgResMgr::ResMgr()->SendRef(image->GetKey(), refMsg, plRefFlags::kActiveRef);

	return plLayerInterface::ConvertNoRef(layer);
}

plLayerInterface *pfJournalBook::IMakeDecalLayer(pfEsHTMLChunk *decalChunk, plMipmap *decal, plMipmap *baseMipmap)
{
	// Create the layer and register it.

	// We'll need a unique name. This is a hack, but an effective hack.
	static int uniqueSuffix = 0;
	char buff[256];
	sprintf(buff, "%s_%d_d", GetKey()->GetName(), uniqueSuffix++);

	plLayer* layer = TRACKED_NEW plLayer;
	hsgResMgr::ResMgr()->NewKey(buff, layer, GetKey()->GetUoid().GetLocation());

	// Initialize it.
	layer->InitToDefault();

	// tint the layer only if the decal wants to be tinted
	layer->SetAmbientColor(hsColorRGBA().Set(0.f, 0.f, 0.f, 1.f));
	if (decalChunk->fTintDecal)
		layer->SetRuntimeColor(hsColorRGBA().Set(fCoverTint.r, fCoverTint.g, fCoverTint.b, 1.f));
	layer->SetOpacity(1.f);

	// Blend flags, decals are alpha blended.
	layer->SetBlendFlags(hsGMatState::kBlendAlpha);

	// Only the bottom layer writes it's Z value.
	layer->SetZFlags(hsGMatState::kZNoZWrite);

	// No special shading.
	layer->SetShadeFlags(hsGMatState::kShadeReallyNoFog);

	// Clamp all textures.
	layer->SetClampFlags(hsGMatState::kClampTexture);

	// Draw passes individually.
	layer->SetMiscFlags(hsGMatState::kMiscRestartPassHere);

	// Shared UV coordinates.
	layer->SetUVWSrc(0);

	// Set up the transform.
	UInt16 x,y;
	x = decalChunk->fAbsoluteX;
	y = decalChunk->fAbsoluteY;

	if((decalChunk->fFlags & pfEsHTMLChunk::kAlignMask) == pfEsHTMLChunk::kLeft)
		x = (UInt16)fPageLMargin;
	else if((decalChunk->fFlags & pfEsHTMLChunk::kAlignMask) == pfEsHTMLChunk::kRight)
		x = (UInt16)(baseMipmap->GetWidth() - decal->GetWidth());
	else if((decalChunk->fFlags & pfEsHTMLChunk::kAlignMask) == pfEsHTMLChunk::kCenter)
		x = (UInt16)((baseMipmap->GetWidth() >> 1) - (decal->GetWidth() >> 1));

	// x and y are in pixels, need to convert to a range of 0 to 1
	float xRel = (float)x/(float)baseMipmap->GetWidth();
	float yRel = (float)y/(float)baseMipmap->GetHeight();

	// Need to convert the scaling to texture space
	float xScale = (float)baseMipmap->GetWidth()/(float)decal->GetWidth();
	float yScale = (float)baseMipmap->GetHeight()/(float)decal->GetHeight();

	if (decalChunk->fNoResizeImg)
	{
		// book is NOT square, there is a h/w ratio of 1/0.7
		// calc new size based on how the book has been skewed
		xScale *= (fWidthScale == 0) ? 1 : (1-fWidthScale);
		yScale *= (fHeightScale == 0) ? 1 : (1-fHeightScale);
		yScale *= 1/0.7; // adjust because the book isn't square
	}

	hsVector3 scaleVec(xScale,yScale,1), translateVec(-(xRel*xScale),-(yRel*yScale),0), yTransVec(0,-1,0), invertYVec(1,-1,1);
	hsMatrix44 scaleMat, translateMat, invertY, transY;
	scaleMat.MakeScaleMat(&scaleVec);
	translateMat.MakeTranslateMat(&translateVec);
	invertY.MakeScaleMat(&invertYVec);
	transY.MakeTranslateMat(&yTransVec);

	hsMatrix44 xfm, flipY;
	flipY = invertY * transY;
	xfm = translateMat * scaleMat * flipY;

	layer->SetTransform(xfm);

	// Set the texture (assumes mipmap is non-nil).
	plLayRefMsg* refMsg = TRACKED_NEW plLayRefMsg(layer->GetKey(), plRefMsg::kOnCreate, 0, plLayRefMsg::kTexture);
	hsgResMgr::ResMgr()->SendRef(decal->GetKey(), refMsg, plRefFlags::kActiveRef);

	return plLayerInterface::ConvertNoRef(layer);
}

void pfJournalBook::ISetDecalLayers(hsGMaterial *material,hsTArray<plLayerInterface*> layers)
{
	// First, clear out the existing layers.
	int i;
	for( i = material->GetNumLayers()-1; i >= 0; i-- )
	{
		plMatRefMsg* refMsg = TRACKED_NEW plMatRefMsg(material->GetKey(), plRefMsg::kOnRemove, i, plMatRefMsg::kLayer);
		hsgResMgr::ResMgr()->SendRef(material->GetLayer(i)->GetKey(), refMsg, plRefFlags::kActiveRef);
	}

	// Now append our new layers in order.
	for( i = 0; i < layers.GetCount(); i++ )
	{
		material->AddLayerViaNotify(layers[i]);
	}
}

//// IFindFontProps //////////////////////////////////////////////////////////
// Starting at the given chunk, works backwards to determine the full set of current
// font properties at that point, or assigns defaults if none were specified

void	pfJournalBook::IFindFontProps( UInt32 chunkIdx, const wchar_t *&face, UInt8 &size, UInt8 &flags, hsColorRGBA &color, Int16 &spacing )
{
	enum Which
	{
		kFace	= 0x01,
		kSize	= 0x02,
		kFlags	= 0x04,
		kColor	= 0x08,
		kSpacing= 0x10,

		kAllFound = kFace | kSize | kFlags | kColor | kSpacing
	};

	// Start empty
	UInt8	found = 0;

	// Work backwards and fill in our properties
	chunkIdx++;
	do
	{
		chunkIdx--;
		if (fHTMLSource.Count() <= chunkIdx)
			break; // apparently it's sometimes possible for fHTMLSource to be empty (parse errors?)
		pfEsHTMLChunk *chunk = fHTMLSource[ chunkIdx ];

		if( chunk->fType == pfEsHTMLChunk::kFontChange )
		{
			// What do we (still) need?
			if( !( found & kFace ) && chunk->fText != L"" )
			{
				face = chunk->fText.c_str();
				found |= kFace;
			}
			if( !( found & kSize ) && chunk->fFontSize > 0 )
			{
				size = chunk->fFontSize;
				found |= kSize;
			}
			if( !( found & kFlags ) && ( chunk->fFlags & pfEsHTMLChunk::kFontMask ) != 0 )
			{
				flags = 0;
				if( chunk->fFlags & pfEsHTMLChunk::kFontBold )
					flags |= plDynamicTextMap::kFontBold;
				if( chunk->fFlags & pfEsHTMLChunk::kFontItalic )
					flags |= plDynamicTextMap::kFontItalic;

				found |= kFlags;
			}
			if( !( found & kColor ) && ( chunk->fFlags & pfEsHTMLChunk::kFontColor ) )
			{
				color = chunk->fColor;
				found |= kColor;
			}
			if( !( found & kSpacing ) && ( chunk->fFlags & pfEsHTMLChunk::kFontSpacing ) )
			{
				spacing = chunk->fLineSpacing;
				found |= kSpacing;
			}
		}

	} while( chunkIdx != 0 && found != kAllFound );

	// Set any un-found defaults
	if( !( found & kFace ) )
		face = L"Arial";
	if( !( found & kSize ) )
		size = 24;
	if( !( found & kFlags ) )
		flags = 0;
	if( !( found & kColor ) )
		color.Set( 0.f, 0.f, 0.f, 1.f );
	if( !( found & kSpacing ) )
		spacing = 0;
}

//// IFindLastAlignment //////////////////////////////////////////////////////
// Find the last paragraph chunk and thus the last par alignment settings

UInt8	pfJournalBook::IFindLastAlignment( void ) const
{
	Int32 idx;


	for( idx = fHTMLSource.GetCount() - 1; idx >= 0; idx-- )
	{
		if( fHTMLSource[ idx ]->fType == pfEsHTMLChunk::kParagraph && fHTMLSource[ idx ]->fFlags != 0 )
			return (UInt8)(fHTMLSource[ idx ]->fFlags);
	}

	return pfEsHTMLChunk::kLeft;
}

//// IRecalcPageStarts ///////////////////////////////////////////////////////
// Ensures that all the page starts are calced up to the given page (but not including it)

void	pfJournalBook::IRecalcPageStarts( UInt32 upToPage )
{
	UInt32 page;


	// Well, sadly, we can't really calc the page starts without at least a DTMap
	// we can change things on...so we just pick one and render. Note: this WILL
	// trash the font settings on the given DTMap!

	// We assume that the stored page starts we already have are accurate, so
	// just start from there and calc onward

	for( page = fPageStarts.GetCount()-1; page < upToPage && page <= fLastPage; page++ )
	{
		// normally we would surpress rendering the pages, but that seems to have a bug in it
		// that causes lost text that the rendering doesn't have. Since it isn't very costly to
		// actually draw them all (even in large journals), we're just going to do it
		IRenderPage( page, pfJournalDlgProc::kTagTurnBackDTMap, false );
		// Reset any "visible" links since they aren't really visible
		UInt16 i;
		for (i=0; i<fVisibleLinks.Count(); i++)
		{
			fVisibleLinks[i]->fLinkRect.Set(0,0,0,0);
		}
	}
}

//// ISendNotify /////////////////////////////////////////////////////////////
// Just sends out a notify to our currently set receiver key

void	pfJournalBook::ISendNotify( UInt32 type, UInt32 linkID )
{
	if( fCallbackKey != nil )
	{
		plNotifyMsg *pMsg = TRACKED_NEW plNotifyMsg;
		pMsg->AddBookEvent( type, linkID );
		pMsg->SetBCastFlag( plMessage::kNetPropagate, false ); // don't deliver networked!
		pMsg->Send( fCallbackKey );
	}
}

//// SetBookSize /////////////////////////////////////////////////////////////
// Sets the book size scaling. 1,1 would be full size, 0,0 is the smallest size possible
// Note: internally we store these as the seek positions on our animations,
// so the incoming parameters are actually inverse of what we finally want

void	pfJournalBook::SetBookSize( hsScalar width, hsScalar height )
{
	fWidthScale = 1.f - width;
	fHeightScale = 1.f - height;

	if( fBookGUIs[fCurBookGUI]->CurBook() == this )
		fBookGUIs[fCurBookGUI]->SetCurrSize( fWidthScale, fHeightScale );
}

//// ILoadAllImages //////////////////////////////////////////////////////////
// Load (or unload) all the images for the book

void	pfJournalBook::ILoadAllImages( hsBool unload )
{
	UInt32	i;

	// load the cover
	if( fCoverFromHTML && fCoverMipKey != nil )
	{
		if( unload )
			fBookGUIs[fCurBookGUI]->GetKey()->Release( fCoverMipKey );
		else
		{
			plGenRefMsg *ref = TRACKED_NEW plGenRefMsg( fBookGUIs[fCurBookGUI]->GetKey(), plRefMsg::kOnCreate, -1, kRefImage );
			hsgResMgr::ResMgr()->AddViaNotify( fCoverMipKey, ref, plRefFlags::kActiveRef );
		}
	}

	for( i = 0; i < fHTMLSource.GetCount(); i++ )
	{
		if( fHTMLSource[ i ]->fType == pfEsHTMLChunk::kImage && fHTMLSource[ i ]->fImageKey != nil )
		{
			if( unload )
				fBookGUIs[fCurBookGUI]->GetKey()->Release( fHTMLSource[ i ]->fImageKey );
			else
			{
				plGenRefMsg *ref = TRACKED_NEW plGenRefMsg( fBookGUIs[fCurBookGUI]->GetKey(), plRefMsg::kOnCreate, -1, kRefImage );  
				hsgResMgr::ResMgr()->AddViaNotify( fHTMLSource[ i ]->fImageKey, ref, plRefFlags::kActiveRef );
			}
		}
	}
}

void pfJournalBook::IPurgeDynaTextMaps( )
{
	plDynamicTextMap* turnFront = fBookGUIs[fCurBookGUI]->GetDTMap( pfJournalDlgProc::kTagTurnFrontDTMap );
	if (turnFront)
		turnFront->PurgeImage();

	plDynamicTextMap* right = fBookGUIs[fCurBookGUI]->GetDTMap( pfJournalDlgProc::kTagRightDTMap );
	if (right)
		right->PurgeImage();

	plDynamicTextMap* turnBack = fBookGUIs[fCurBookGUI]->GetDTMap( pfJournalDlgProc::kTagTurnBackDTMap );
	if (turnBack)
		turnBack->PurgeImage();

	plDynamicTextMap* left = fBookGUIs[fCurBookGUI]->GetDTMap( pfJournalDlgProc::kTagLeftDTMap );
	if (left)
		left->PurgeImage();

	pfGUIMultiLineEditCtrl *leftEdit = fBookGUIs[fCurBookGUI]->GetEditCtrl( pfJournalDlgProc::kTagLeftEditCtrl );
	if (leftEdit)
		leftEdit->PurgeDynaTextMapImage();

	pfGUIMultiLineEditCtrl *rightEdit = fBookGUIs[fCurBookGUI]->GetEditCtrl( pfJournalDlgProc::kTagRightEditCtrl );
	if (rightEdit)
		rightEdit->PurgeDynaTextMapImage();

	pfGUIMultiLineEditCtrl *turnFrontEdit = fBookGUIs[fCurBookGUI]->GetEditCtrl( pfJournalDlgProc::kTagTurnFrontEditCtrl );
	if (turnFrontEdit)
		turnFrontEdit->PurgeDynaTextMapImage();

	pfGUIMultiLineEditCtrl *turnBackEdit = fBookGUIs[fCurBookGUI]->GetEditCtrl( pfJournalDlgProc::kTagTurnBackEditCtrl );
	if (turnBackEdit)
		turnBackEdit->PurgeDynaTextMapImage();
}

std::string pfJournalBook::GetEditableText()
{
	pfGUIMultiLineEditCtrl *left = fBookGUIs[fCurBookGUI]->GetEditCtrl( pfJournalDlgProc::kTagLeftEditCtrl );
	if (left)
	{
		char *temp = left->GetNonCodedBuffer();
		std::string retVal = temp;
		delete [] temp;
		return retVal;
	}
	return "";
}

void pfJournalBook::SetEditableText(std::string text)
{
	pfGUIMultiLineEditCtrl *left = fBookGUIs[fCurBookGUI]->GetEditCtrl( pfJournalDlgProc::kTagLeftEditCtrl );
	if (left)
	{
		left->SetBuffer(text.c_str());
		left->ForceUpdate();
	}
}