/*==LICENSE==*

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

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.

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

*==LICENSE==*/
//////////////////////////////////////////////////////////////////////////////
//                                                                          //
//  pfGUIMultiLineEditCtrl Definition                                       //
//                                                                          //
//////////////////////////////////////////////////////////////////////////////

#include "hsTypes.h"
#include "pfGUIMultiLineEditCtrl.h"
#include "pfGameGUIMgr.h"
#include "pfGUIUpDownPairMod.h"
#include "pfGUIControlHandlers.h"
#include "pfGUIDialogMod.h"
#include "pfGUIDialogHandlers.h"

#include "pnMessage/plRefMsg.h"
#include "pfMessage/pfGameGUIMsg.h"
#include "plMessage/plAnimCmdMsg.h"
#include "plAvatar/plAGModifier.h"
#include "plGImage/plDynamicTextMap.h"
#include "plgDispatch.h"
#include "hsResMgr.h"


//// Tiny Helper Class ///////////////////////////////////////////////////////

class plStringSlicer
{
    wchar_t         *fString;
    wchar_t         fTempChar;
    UInt32          fStart, fEnd;

    typedef wchar_t *CharPtr;

    public:
        plStringSlicer( wchar_t *string, UInt32 start, UInt32 end )
        {
            fString = string;
            fTempChar = string[ end ];
            string[ end ] = 0L;
            fStart = start;
            fEnd = end;
        }

        plStringSlicer( hsTArray<wchar_t> &string, UInt32 start, UInt32 end )
        {
            fString = string.AcquireArray();
            fStart = start;
            if( end < string.GetCount() )
                fEnd = end;
            else
                fEnd = fStart;

            if( fEnd > fStart )
            {
                fTempChar = fString[ end ];
                fString[ end ] = 0L;
            }
        }

        ~plStringSlicer()
        {
            if( fEnd > fStart )
                fString[ fEnd ] = fTempChar;
        }

        operator const CharPtr() const
        {
            return &fString[ fStart ];
        }
};

//// Wee Little Control Proc for scrolling ///////////////////////////////////

class pfMLScrollProc : public pfGUICtrlProcObject
{
    protected:

        pfGUIMultiLineEditCtrl  *fParent;

    public:

        pfMLScrollProc( pfGUIMultiLineEditCtrl *parent ) : fParent( parent ) {}

        virtual void    DoSomething( pfGUIControlMod *ctrl )
        {
            // Do a check here to make sure we actually changed scroll
            // positions--if not, we don't want to update, since that'll be 
            // slow as hell
            int newScrollPos = (int)fParent->fScrollControl->GetMax() - (int)fParent->fScrollControl->GetCurrValue();
            fParent->SetScrollPosition( newScrollPos );
        }
};


//// Statics /////////////////////////////////////////////////////////////////

wchar_t pfGUIMultiLineEditCtrl::fColorCodeChar = (wchar_t)1;
wchar_t pfGUIMultiLineEditCtrl::fStyleCodeChar = (wchar_t)2;
UInt32  pfGUIMultiLineEditCtrl::fColorCodeSize = (wchar_t)5;
UInt32  pfGUIMultiLineEditCtrl::fStyleCodeSize = (wchar_t)3;

//// Constructor/Destructor //////////////////////////////////////////////////

pfGUIMultiLineEditCtrl::pfGUIMultiLineEditCtrl()
{
    SetFlag( kWantsInterest );
    SetFlag( kTakesSpecialKeys );
    fCursorPos = 0;
    fLastCursorLine = 0;
    fBuffer.Append( 0L );
    fBufferLimit = -1;
    fIgnoreNextKey = false;
    fScrollControl = nil;
    fScrollProc = nil;
    fScrollPos = 0;
    fReadyToRender = false;


    fLastKeyModifiers = 0;
    fLastKeyPressed = 0;
    fLockCount = 0;

    fLastDeadKey = 0;
    SetupDeadKeyConverter();

    fNextCtrl = nil;
    fPrevCtrl = nil;

    fEventProc = nil;

    fTopMargin = fLeftMargin = 0;
    fBottomMargin = fRightMargin = 0;

    fFontFace = "";
    fFontColor.FromARGB32(0xFF000000);
    fFontSize = 0;
    fFontStyle = 0;
    fFontFlagsSet = 0;
}

pfGUIMultiLineEditCtrl::~pfGUIMultiLineEditCtrl()
{
    ClearNext(); // make sure that no one is referencing us
    ClearPrev();

    if( fScrollProc && fScrollProc->DecRef() )
        delete fScrollProc;
    if (fEventProc)
        delete fEventProc;
}

void pfGUIMultiLineEditCtrl::SetupDeadKeyConverter()
{
    int i,j;
    for (i=0; i<255; i++)
        for (j=0; j<255; j++)
            fDeadKeyConverter[i][j] = 0L;

    // we are adding 100 to the indexes because some of these chars have a negative index for some reason
    fDeadKeyConverter['^'+100]['a'] = L'�';
    fDeadKeyConverter['^'+100]['e'] = L'�';
    fDeadKeyConverter['^'+100]['i'] = L'�';
    fDeadKeyConverter['^'+100]['o'] = L'�';
    fDeadKeyConverter['^'+100]['u'] = L'�';
    fDeadKeyConverter['^'+100]['A'] = L'�';
    fDeadKeyConverter['^'+100]['E'] = L'�';
    fDeadKeyConverter['^'+100]['I'] = L'�';
    fDeadKeyConverter['^'+100]['O'] = L'�';
    fDeadKeyConverter['^'+100]['U'] = L'�';
    
    fDeadKeyConverter['�'+100]['a'] = L'�';
    fDeadKeyConverter['�'+100]['e'] = L'�';
    fDeadKeyConverter['�'+100]['i'] = L'�';
    fDeadKeyConverter['�'+100]['o'] = L'�';
    fDeadKeyConverter['�'+100]['u'] = L'�';
    fDeadKeyConverter['�'+100]['A'] = L'�';
    fDeadKeyConverter['�'+100]['E'] = L'�';
    fDeadKeyConverter['�'+100]['I'] = L'�';
    fDeadKeyConverter['�'+100]['O'] = L'�';
    fDeadKeyConverter['�'+100]['U'] = L'�';

    fDeadKeyConverter['�'+100]['a'] = L'�';
    fDeadKeyConverter['�'+100]['e'] = L'�';
    fDeadKeyConverter['�'+100]['i'] = L'�';
    fDeadKeyConverter['�'+100]['o'] = L'�';
    fDeadKeyConverter['�'+100]['u'] = L'�';
    fDeadKeyConverter['�'+100]['y'] = L'�';
    fDeadKeyConverter['�'+100]['A'] = L'�';
    fDeadKeyConverter['�'+100]['E'] = L'�';
    fDeadKeyConverter['�'+100]['I'] = L'�';
    fDeadKeyConverter['�'+100]['O'] = L'�';
    fDeadKeyConverter['�'+100]['U'] = L'�';
    fDeadKeyConverter['�'+100]['Y'] = L'�';

    fDeadKeyConverter['`'+100]['a'] = L'�';
    fDeadKeyConverter['`'+100]['e'] = L'�';
    fDeadKeyConverter['`'+100]['i'] = L'�';
    fDeadKeyConverter['`'+100]['o'] = L'�';
    fDeadKeyConverter['`'+100]['u'] = L'�';
    fDeadKeyConverter['`'+100]['A'] = L'�';
    fDeadKeyConverter['`'+100]['E'] = L'�';
    fDeadKeyConverter['`'+100]['I'] = L'�';
    fDeadKeyConverter['`'+100]['O'] = L'�';
    fDeadKeyConverter['`'+100]['U'] = L'�';
}

//// IEval ///////////////////////////////////////////////////////////////////

hsBool  pfGUIMultiLineEditCtrl::IEval( double secs, hsScalar del, UInt32 dirty )
{
    return pfGUIControlMod::IEval( secs, del, dirty );
}

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

hsBool  pfGUIMultiLineEditCtrl::MsgReceive( plMessage *msg )
{
    plGenRefMsg *refMsg = plGenRefMsg::ConvertNoRef( msg );
    if( refMsg != nil )
    {
        if( refMsg->fType == kRefScrollCtrl )
        {
            if( refMsg->GetContext() & ( plRefMsg::kOnCreate | plRefMsg::kOnRequest | plRefMsg::kOnReplace ) )
            {
                fScrollControl = pfGUIValueCtrl::ConvertNoRef( refMsg->GetRef() );
                fScrollControl->SetHandler( fScrollProc );
                fScrollControl->SetStep( 1.f );
                IUpdateScrollRange();
            }
            else
                fScrollControl = nil;
            return true;
        }
    }

    return pfGUIControlMod::MsgReceive( msg );
}

//// SetScrollPosition ///////////////////////////////////////////////////////

void    pfGUIMultiLineEditCtrl::SetScrollPosition( Int32 topLine )
{
    if( topLine < 0 )
        topLine = 0;
    else if( topLine > fLineStarts.GetCount() - ICalcNumVisibleLines() + 1 )
        topLine = fLineStarts.GetCount() - ICalcNumVisibleLines() + 1;

    if( fScrollPos == topLine )
        return;

    if (topLine >= 0)
        fScrollPos = topLine;
    else
        fScrollPos = 0;

    IUpdate();

    if( fScrollControl != nil ) 
        // Scroll control values are reversed
        fScrollControl->SetCurrValue( fScrollControl->GetMax() - (hsScalar)fScrollPos );

    HandleExtendedEvent( pfGUIMultiLineEditCtrl::kScrollPosChanged );

    // notify thru the dialog something has changed
    if (fDialog && fDialog->GetHandler())
        fDialog->GetHandler()->DoSomething(this);
}

//// MoveCursor - by direction command////////////////////////////////////////////////
void    pfGUIMultiLineEditCtrl::MoveCursor( Direction dir )
{
    IMoveCursor(dir);
}


//// IUpdateScrollRange //////////////////////////////////////////////////////

void    pfGUIMultiLineEditCtrl::IUpdateScrollRange( void )
{
    if( fScrollControl == nil )
        return;

    if( fLineStarts.GetCount() > ICalcNumVisibleLines() - 1 )
    {
        // +1 here because the last visible line is only a partial, but we want to be able to view
        // full lines all the way to the end.
        hsScalar newMax = (hsScalar)( fLineStarts.GetCount() - ICalcNumVisibleLines() + 1 );

        if( newMax != fScrollControl->GetMax() )
        {
            fScrollControl->SetRange( 0, (hsScalar)(fLineStarts.GetCount() - ICalcNumVisibleLines() + 1) );
            fScrollControl->SetEnabled( true );
            if( fScrollPos > fLineStarts.GetCount() - ICalcNumVisibleLines() + 1 )
            {
                fScrollPos = fLineStarts.GetCount() - ICalcNumVisibleLines() + 1;
                fScrollControl->SetCurrValue( fScrollControl->GetMax() - (hsScalar)fScrollPos );
            }

            // All bets are off on scrolling, so refresh the whole area
            IUpdate();
        }
    }
    else
    {
        if( fScrollControl->GetMax() > 0 )
        {
            fScrollControl->SetRange( 0, 0 );
            fScrollControl->SetEnabled( false );
            fScrollPos = 0;
            IUpdate();
        }
    }
}

void pfGUIMultiLineEditCtrl::SetScrollEnable( hsBool state )
{
    if (fScrollControl == nil )
        return;

    if ( state )
    {
        IUpdateScrollRange();
    }
    else
    {
        fScrollControl->SetRange( 0, 0 );
        fScrollControl->SetEnabled( false );
        fScrollPos = 0;
        IUpdate();
    }
}

//// IPostSetUpDynTextMap ////////////////////////////////////////////////////

void    pfGUIMultiLineEditCtrl::IPostSetUpDynTextMap( void )
{
    pfGUIColorScheme *scheme = GetColorScheme();

    // fill in any blanks
    if (!(fFontFlagsSet & kFontFaceSet))
    {
        fFontFace = scheme->fFontFace;
        fFontFlagsSet |= kFontFaceSet;
    }
    if (!(fFontFlagsSet & kFontColorSet))
    {
        fFontColor = scheme->fForeColor;
        fFontFlagsSet |= kFontColorSet;
    }
    if (!(fFontFlagsSet & kFontSizeSet))
    {
        fFontSize = scheme->fFontSize;
        fFontFlagsSet |= kFontSizeSet;
    }
    if (!(fFontFlagsSet & kFontStyleSet))
    {
        fFontStyle = scheme->fFontFlags;
        fFontFlagsSet |= kFontStyleSet;
    }

    fDynTextMap->SetFont( fFontFace.c_str(), fFontSize, fFontStyle, 
                            HasFlag( kXparentBgnd ) ? false : true );

    // Calculate a height for each line
    fDynTextMap->CalcStringWidth( L"The quick brown fox jumped over the lazy dog.", &fLineHeight );
    fCalcedFontSize = fFontSize;

    fReadyToRender = true;
    IRecalcLineStarts( 0, true );
}

//// ICalcNumVisibleLines ////////////////////////////////////////////////////

Int32   pfGUIMultiLineEditCtrl::ICalcNumVisibleLines( void ) const
{
    if (fDynTextMap == nil || fLineHeight == 0)
        return 0;
    Int32 numLines = 0;
    numLines = (fDynTextMap->GetVisibleHeight() + fLineHeight - (fTopMargin+fBottomMargin+1))/fLineHeight;
    return numLines;
}

//// IUpdate /////////////////////////////////////////////////////////////////
//  Ranged version

void    pfGUIMultiLineEditCtrl::IUpdate( Int32 startLine, Int32 endLine )
{
    hsColorRGBA c;
    static int  testingFlip = 0;
    bool        clearEachLine = true;
    UInt32      line, x, y = 0;
    Int32       numVisibleLines, lastVisibleLine;


    if( !fReadyToRender )
        return;

    // Detect whether we need to recalc all of our dimensions entirely
    if( fFontFlagsSet & (kFontFaceSet & kFontColorSet & kFontSizeSet & kFontStyleSet) )
        IPostSetUpDynTextMap();

    if( fLineStarts.GetCount() == 0 )
    {
        // Just clear and go away
        fDynTextMap->ClearToColor( GetColorScheme()->fBackColor );
        if( IsFocused() )
            fDynTextMap->FrameRect( fLeftMargin, fTopMargin, 2, fLineHeight, GetColorScheme()->fSelForeColor );
        fDynTextMap->FlushToHost();
        return;
    }

    // Recalculate the visible range due to our visible area and the scroll range.
    // adjust the scroll position if we are linked
    if (fPrevCtrl) // the first control (which has a nil fPrevCtrl) shouldn't adjust it's scroll pos because the app may want it to start there
    {
        IUpdateBuffer(); // make sure we are rendering the correct text
        fScrollPos = GetFirstVisibleLine();
    }
    numVisibleLines = ICalcNumVisibleLines();
    if (fNextCtrl || fPrevCtrl)
        numVisibleLines--; // we don't want "partially visible" lines
    lastVisibleLine = fScrollPos + numVisibleLines - 1;
    if( lastVisibleLine > fLineStarts.GetCount() - 1 )
        lastVisibleLine = fLineStarts.GetCount() - 1;

    if( startLine < fScrollPos )
        startLine = fScrollPos;
    if( endLine > lastVisibleLine )
        endLine = lastVisibleLine;

    if( startLine == fScrollPos && endLine == lastVisibleLine )
    {
        c.Set( 0.f, 0.f, 0.f, 1.f );
        fDynTextMap->ClearToColor( GetColorScheme()->fBackColor );
        clearEachLine = false;
    }

    // Start at our line
    y = ( startLine - fScrollPos ) * fLineHeight + fTopMargin;
    // And loop!

    for( line = startLine; line <= endLine; line++ )
    {
        // Clear this line
        if( clearEachLine )
        {
            fDynTextMap->FillRect( 0, (UInt16)y, fDynTextMap->GetVisibleWidth(), fLineHeight, 
                                    GetColorScheme()->fBackColor );
        }

        UInt32 start = fLineStarts[ line ], end;
        if( line == fLineStarts.GetCount() - 1 )
            end = fBuffer.GetCount();
        else
            end = fLineStarts[ line + 1 ];

        // Render the actual text
        IRenderLine( fLeftMargin, (UInt16)y, start, end );

        // Render the cursor
        if( fCursorPos >= start && fCursorPos < end && IsFocused() )
        {
            if( fCursorPos > start )
                x = IRenderLine( fLeftMargin, (UInt16)y, start, fCursorPos, true );
            else
                x = fLeftMargin;
            
            fDynTextMap->FrameRect( (UInt16)x, (UInt16)y, 2, fLineHeight, GetColorScheme()->fSelForeColor );

            // Store the cursor X,Y pair. Go figure, the ONLY time we actually need this is
            // to move up or down one line, and even then it's only because we want to keep
            // the same approximate horizontal position (versus same character offset)
            fCurrCursorX = (UInt16)x;   
            fCurrCursorY = (UInt16)y;
        }
        y += fLineHeight;
    }
    if( clearEachLine && line >= fLineStarts.GetCount() && y < fDynTextMap->GetVisibleHeight()-fBottomMargin )
    {
        // No lines left, so clear the rest of the visible area
        fDynTextMap->FillRect( 0, (UInt16)y, fDynTextMap->GetVisibleWidth(), (UInt16)(fDynTextMap->GetVisibleHeight() - y), 
                                GetColorScheme()->fBackColor );
    }
    fDynTextMap->FlushToHost();
}

//// IReadColorCode //////////////////////////////////////////////////////////
//  Reads a color code from the given position and advances the given position
//  appropriately

void    pfGUIMultiLineEditCtrl::IReadColorCode( Int32 &pos, hsColorRGBA &color ) const
{
    UInt16  *buffer = (UInt16 *)fBuffer.AcquireArray() + pos;
    UInt8   r, g, b;


    hsAssert( buffer[ 0 ] == fColorCodeChar, "Invalid position in IReadColorCode()" );
    buffer++;
    r = (UInt8)buffer[ 0 ];
    g = (UInt8)buffer[ 1 ];
    b = (UInt8)buffer[ 2 ];
    pos += fColorCodeSize;      // We have a duplicate code at the end of this block, for searching backwards
    color.Set( r / 255.f, g / 255.f, b / 255.f, fFontColor.a );
}

//// IReadStyleCode //////////////////////////////////////////////////////////
//  Reads a style code from the given position and advances the given position
//  appropriately

void    pfGUIMultiLineEditCtrl::IReadStyleCode( Int32 &pos, UInt8 &fontFlags ) const
{
    UInt16  *buffer = (UInt16 *)fBuffer.AcquireArray() + pos;


    hsAssert( buffer[ 0 ] == fStyleCodeChar, "Invalid position in IReadStyleCode()" );
    fontFlags = (UInt8)buffer[ 1 ];
    pos += fStyleCodeSize;      // We have a duplicate code at the end of this block, for searching backwards
}

inline bool pfGUIMultiLineEditCtrl::IIsRenderable( const wchar_t c )
{
    return ( !IIsCodeChar( c ) && c != L'\n' && c != L'\t' );
}

inline bool pfGUIMultiLineEditCtrl::IIsCodeChar( const wchar_t c )
{
    return ( c == fColorCodeChar || c == fStyleCodeChar );
}

//// IFindLastCode Functions /////////////////////////////////////////////////
//  Given a position, these functions start at that position and work
//  backward until they find their respective code type, then return that code
//  type. If none is found, they set the given parameter to the default value
//  and return false.

hsBool  pfGUIMultiLineEditCtrl::IFindLastColorCode( Int32 pos, hsColorRGBA &color, hsBool ignoreFirstCharacter ) const
{
    for( ; pos >= 0; pos -= IOffsetToNextCharFromPos( pos - 1 ) )
    {
        if( fBuffer[ pos ] == fColorCodeChar && !ignoreFirstCharacter )
        {
            IReadColorCode( pos, color );
            return true;
        }
        ignoreFirstCharacter = false;
    }
    color = fFontColor; // use our default color
    return false;
}

hsBool  pfGUIMultiLineEditCtrl::IFindLastStyleCode( Int32 pos, UInt8 &style, hsBool ignoreFirstCharacter ) const
{
    for( ; pos >= 0; pos -= IOffsetToNextCharFromPos( pos - 1 ) )
    {
        if( fBuffer[ pos ] == fStyleCodeChar && !ignoreFirstCharacter )
        {
            IReadStyleCode( pos, style );
            return true;
        }
        ignoreFirstCharacter = false;
    }

    style = fFontStyle; // use our default style
    return false;
}

//// IRenderLine /////////////////////////////////////////////////////////////
//  Renders a null-terminated string to the dynamic text map at the location
//  given. Takes into account style codes and special characters (like returns
//  and tabs). Returns the final X value after rendering.

UInt32  pfGUIMultiLineEditCtrl::IRenderLine( UInt16 x, UInt16 y, Int32 start, Int32 end, hsBool dontRender )
{
    Int32       pos;
    hsColorRGBA currColor = fFontColor;
    UInt8       currStyle;
    const wchar_t   *buffer = fBuffer.AcquireArray();

    // First, gotta go back from our starting position and find a color and style code to use
    IFindLastColorCode( start, currColor );
    IFindLastStyleCode( start, currStyle );

    fDynTextMap->SetTextColor( currColor, HasFlag( kXparentBgnd ) ? true : false );
    fDynTextMap->SetFont( fFontFace.c_str(), fFontSize, GetColorScheme()->fFontFlags | currStyle, 
                            HasFlag( kXparentBgnd ) ? false : true );
    
    // Now, start from our start and go to the end and keep eating up as many chunks
    // that aren't made up of control codes or carriage returns or tabs (since we render
    // those separately)
    for( pos = start; pos < end; )
    {
        start = pos;
        if( IIsRenderable( buffer[ pos ] ) )
        {
            // State 1: Render a renderable chunk
            while( pos < end && IIsRenderable( buffer[ pos ] ) )
                pos++;

            {
                plStringSlicer slicer( fBuffer, start, pos );
                if( !dontRender )
                    fDynTextMap->DrawString( x, y, slicer );
                x += fDynTextMap->CalcStringWidth( slicer );
            }
        }
        else
        {
            // State 2: Process non-renderable characters
            if( buffer[ pos ] == fColorCodeChar )
            {
                // Read color and switch to that one
                IReadColorCode( pos, currColor );
                if( !dontRender )
                    fDynTextMap->SetTextColor( currColor, HasFlag( kXparentBgnd ) ? true : false );
            }
            else if( buffer[ pos ] == fStyleCodeChar )
            {
                // Read style and switch to that one
                IReadStyleCode( pos, currStyle );
                if( !dontRender )
                    fDynTextMap->SetFont( fFontFace.c_str(), fFontSize  , GetColorScheme()->fFontFlags | currStyle, 
                                            HasFlag( kXparentBgnd ) ? false : true );
            }
            else if( buffer[ pos ] == L'\n' )
            {
                // We stop at carriage returns
                break;
            }
            else if( buffer[ pos ] == L'\t' )
            {
                // Increment by tab amount
                x += 16;
                pos++;
            }
            else
            {
                hsAssert( false, "Unknown unrenderable character, ignoring" );
                pos++;
            }
        }
    }

    return x;
}

void pfGUIMultiLineEditCtrl::PurgeDynaTextMapImage()
{
    if ( fDynTextMap != nil )
        fDynTextMap->PurgeImage();
}

//// IUpdate /////////////////////////////////////////////////////////////////

void    pfGUIMultiLineEditCtrl::IUpdate( void )
{
    // Just call the ranged one with a full range
    IUpdate( 0, fLineStarts.GetCount() - 1 );
}

//// Read/Write //////////////////////////////////////////////////////////////

void    pfGUIMultiLineEditCtrl::Read( hsStream *s, hsResMgr *mgr )
{
    pfGUIControlMod::Read(s, mgr);

    fScrollControl = nil;
    if( s->ReadBool() )
    {
        fScrollProc = TRACKED_NEW pfMLScrollProc( this );
        fScrollProc->IncRef();
        mgr->ReadKeyNotifyMe( s, TRACKED_NEW plGenRefMsg( GetKey(), plRefMsg::kOnCreate, -1, kRefScrollCtrl ), plRefFlags::kActiveRef );
    }
}

void    pfGUIMultiLineEditCtrl::Write( hsStream *s, hsResMgr *mgr )
{
    pfGUIControlMod::Write( s, mgr );

    if( fScrollControl != nil )
    {
        s->WriteBool( true );
        mgr->WriteKey( s, fScrollControl->GetKey() );
    }
    else
        s->WriteBool( false );
}

//// IPointToPosition ////////////////////////////////////////////////////////
//  Translates a 2D point on the visible texture surface to a cursor position.

Int32   pfGUIMultiLineEditCtrl::IPointToPosition( Int16 ptX, Int16 ptY, hsBool searchOutsideBounds )
{
    // Find our line
    Int32   line, start, pos, end, lastVisibleLine;
    Int16   x, y;
    
    if (fPrevCtrl)
        fScrollPos = GetFirstVisibleLine(); // update the scroll position if we are linked

    if( searchOutsideBounds )
        lastVisibleLine = fLineStarts.GetCount() - 1;
    else
    {
        lastVisibleLine = fScrollPos + ICalcNumVisibleLines() - 1;
        if( lastVisibleLine > fLineStarts.GetCount() - 1 )
            lastVisibleLine = fLineStarts.GetCount() - 1;
    }

    line = searchOutsideBounds ? 0 : fScrollPos;
    y = (Int16)(-( fScrollPos - line ) * fLineHeight);
    y += fTopMargin;
    for( ; line < lastVisibleLine; line++, y += fLineHeight )
    {
        if( ptY >= y && ptY < y + fLineHeight )
        {
            if (line < 0)
                break; // abort, and yes, this IS possible with this crappy code

            // Found the line, figure out what character
            start = fLineStarts[ line ];
            end = ( line == fLineStarts.GetCount() - 1 ) ? fBuffer.GetCount() - 1 : fLineStarts[ line + 1 ];

            for( pos = start; pos < end; pos++ )
            {
                x = (Int16)IRenderLine( fLeftMargin, 0, start, pos, true );
                if( x > ptX )
                    break;
            }

            // Go figure, our test puts it 1 past no matter what :)
            return pos - 1;
        }
    }

    // Just put us at the end of the last line
    return fBuffer.GetCount() - 1;
}

//// IIsWordBreaker //////////////////////////////////////////////////////////
//  Returns whether the given character is one that can break a line

inline bool IIsWordBreaker( const wchar_t c )
{
    return ( wcschr( L" \t,.;\n", c ) != nil ) ? true : false;
}

//// IOffsetToNextChar ///////////////////////////////////////////////////////

inline  Int32   pfGUIMultiLineEditCtrl::IOffsetToNextChar( wchar_t stringChar )
{
    if( stringChar == fColorCodeChar )
        return fColorCodeSize;
    else if( stringChar == fStyleCodeChar )
        return fStyleCodeSize;
    else
        return 1;
}

inline  Int32   pfGUIMultiLineEditCtrl::IOffsetToNextCharFromPos( Int32 position ) const
{
    if( position >= 0 )
        return IOffsetToNextChar( fBuffer[ position ] );
    else
        return 1;
}

//// IRecalcLineStarts ///////////////////////////////////////////////////////
//  Recalculates all the word wrapping/line start values, starting at the
//  given line. If not forced, recalc will stop once a calculated line start
//  matches one already stored (this implying that everything after will be
//  the same as well, assuming contents are the same). If this assumption can't
//  be made, force recalc of all the lines.
//  Returns the last line recalced.
//  The starting line is basically the first line whose start might have
//  changed, so we assume as a hint that every line before the starting line
//  is still valid. If startingLine = 0, we recalc 'em all.

Int32   pfGUIMultiLineEditCtrl::IRecalcLineStarts( Int32 startingLine, hsBool force, hsBool dontUpdate )
{
    UInt16      wrapWidth, widthCounter;
    UInt32      charPos = 0, nextPos, startPos, lastStartPos;
    Int32       currLine, realStartingLine;
    bool        firstLine;
    wchar_t     *buffer;
    const wchar_t   wordBreaks[] = L" \t,.";
    const wchar_t   wordSeparators[] = L" \t,.\n";

    if( fPrevCtrl )
        IUpdateBuffer(); // make sure our buffer is correct if we are linked
    
    if( fDynTextMap == nil )
    {
        // Can't calculate anything. Just return invalid
        fLineStarts.Reset();
        IUpdateScrollRange();
        return -1;
    }

    // Figure out our starting character
    if( startingLine > 0 )
    {
        if( startingLine >= fLineStarts.GetCount() )
        {
            // Must be a problem, force full recalc
            hsStatusMessage( "Invalid starting line in IRecalcLineStarts(), forcing full recalc" );
            currLine = 0;
            force = true;
        }
        else
        {
            // Start 1 line before, since we assume that line start is still valid
            currLine = startingLine - 1;
            charPos = fLineStarts[ currLine ];
        }
    }
    else
        currLine = 0;

    realStartingLine = currLine;    // For the IUpdate call later

    // Precalculate some helper values
    wrapWidth = fDynTextMap->GetVisibleWidth() - fRightMargin;
    buffer = fBuffer.AcquireArray();
    firstLine = true;
    lastStartPos = (UInt32)-1;

    for( ; charPos < fBuffer.GetCount(); currLine++ )
    {
        //// Store this line start
        startPos = charPos;
        if( IStoreLineStart( currLine, startPos ) )
        {
            if( currLine > startingLine )
            {
                // We just stored a line that didn't change its starting position (first line doesn't count)
                if( !force )
                    break;
            }
            else if( currLine > realStartingLine )
            {
                // Line start didn't change, but we're at the start. See, we always go one line back in case
                // something changed that would cause the line before to update. But if we're here, both the
                // line before and this line have the same starting position, which means that the line before
                // didn't change, so we can increment realStartingLine and skip re-drawing that line
                realStartingLine++;
            }
        }

        firstLine = false;

        //// We do a walk where we find the start of the next word (i.e. the end of this word plus
        //// any "white space"), and then see if we can fit everything up to that point. If we can,
        //// keep walking, if not, stop at whatever we had as a starting point.
        nextPos = charPos;
        for( widthCounter = 0; widthCounter < wrapWidth; )
        {
            charPos = nextPos;

            // Are we on a line break?
            if( nextPos >= fBuffer.GetCount() || buffer[ nextPos ] == L'\n' || buffer[ nextPos ] == 0L )
            {
                charPos++;
                break;  // Yup, so do so
            }

            // Find the end of this word
            while( nextPos < fBuffer.GetCount() && !IIsWordBreaker( buffer[ nextPos ] ) )
                nextPos += IOffsetToNextChar( buffer[ nextPos ] );

            // Now we're at some white space, keep going until we hit the next word
            while( nextPos < fBuffer.GetCount() && IIsWordBreaker( buffer[ nextPos ] ) && buffer[ nextPos ] != L'\n' )
                nextPos += IOffsetToNextChar( buffer[ nextPos ] );

            // Now see how much width this is
            widthCounter = (UInt16)IRenderLine( fLeftMargin, 0, startPos, nextPos, true );
            
            // Now we loop. If wrapWidth is too much, we'll break the loop with charPos pointing to the
            // end of our line. If not, charPos will advance to start the search again
        }

        // Check to see if the line was just too long to wrap at all
        if( startPos == charPos )
        {
            // OK, so just try again and break as soon as necessary. nextPos should be 
            // already advanced too far, so start from there and go back
            while( widthCounter >= wrapWidth && nextPos > startPos )
            {
                nextPos -= IOffsetToNextChar( buffer[ nextPos - 1 ] );
                widthCounter = (UInt16)IRenderLine( fLeftMargin, 0, startPos, nextPos, true );
            }

            charPos = nextPos;
        }

        // Continue on!     
    }

    if( charPos >= fBuffer.GetCount() )
    {
        // Make sure there are no lines stored after this one
        fLineStarts.SetCount( currLine );
    }

    IUpdateScrollRange();

    if( !dontUpdate )
        // We just changed some of the line starts, so time to redraw
        IUpdate( realStartingLine, currLine - 1 );

    // we just updated the line starts, so set all following controls to the same set
    if (fNextCtrl)
        fNextCtrl->ISetLineStarts(fLineStarts);

    return currLine;
}

//// IStoreLineStart /////////////////////////////////////////////////////////
//  Stores a single line start, expanding the array if necessary.

hsBool  pfGUIMultiLineEditCtrl::IStoreLineStart( UInt32 line, Int32 start )
{
    if( fLineStarts.GetCount() <= line )
    {
        hsAssert( line == fLineStarts.GetCount(), "Trying to store a line way past the end of line starts!" );
        fLineStarts.Expand( line + 1 );
        fLineStarts.SetCount( line + 1 );
        fLineStarts[ line ] = -1;
    }

    hsBool same = ( fLineStarts[ line ] == start ) ? true : false;
    fLineStarts[ line ] = start;
    return same;
}

//// IFindCursorLine /////////////////////////////////////////////////////////
//  Calculates the line the cursor is sitting on

Int32   pfGUIMultiLineEditCtrl::IFindCursorLine( Int32 cursorPos ) const
{
    Int32   line;


    if( cursorPos == -1 )
        cursorPos = fCursorPos;

    for( line = 0; line < fLineStarts.GetCount() - 1; line++ )
    {
        if( fLineStarts[ line + 1 ] > cursorPos )
            break;
    }
    return line;
}

//// IRecalcFromCursor ///////////////////////////////////////////////////////
//  Recalcs starting with the line that the cursor is sitting on

void    pfGUIMultiLineEditCtrl::IRecalcFromCursor( hsBool force )
{
    IRecalcLineStarts( IFindCursorLine(), force );
}

//// IOffsetLineStarts ///////////////////////////////////////////////////////
//  Given the position and offset, offsets all line starts >= that position
//  by the given amount. Used to insert a character and you know that all the
//  line offsets after that character should just bump up one, for example.
//  Also offsets the end of the current selection if desired and in range.
//  Why only the end of the selection? Because in the only cases where we're
//  doing this, we are inserting into (and offseting inside) a selection,
//  so we don't want the start moving around.

void    pfGUIMultiLineEditCtrl::IOffsetLineStarts( UInt32 position, Int32 offset, hsBool offsetSelectionEnd )
{
    Int32   line;

    // Check our first line and make sure offsetting it won't make it invalid.
    // If it will, we need to recalc the line starts entirely (which is fine,
    // since this function is just called to try to optimize out doing so, but
    // when you gotta, you gotta...)
    line = IFindCursorLine( position );
    if( line < fLineStarts.GetCount() - 1 )
    {
        if( fLineStarts[ line + 1 ] + offset <= fLineStarts[ line ] )
        {
            // We want to force the line starts to recalc. Otherwise, IRecalc() will attempt
            // to stop once it detects a line start that hasn't changed. Which doesn't work
            // because our line starts just got offsetted, hence why this function was called.
            // Even then, it isn't necessarily a bad thing, since that line will be OK, but
            // the assumption IRecalc() makes is that all the lines *after* that must be OK too
            // b/c it just found the same line start, which in this case it didn't.
            // Just trust me.
            IRecalcLineStarts( line, true );
            return;
        }
    }

    // Offset all lines past our given position
    for( line = 0; line < fLineStarts.GetCount(); line++ )
    {
        if( fLineStarts[ line ] > position )
            fLineStarts[ line ] += offset;
    }

    // now update all following controls
    if (fNextCtrl)
        fNextCtrl->ISetLineStarts(fLineStarts);
}

//// HandleMouseDown /////////////////////////////////////////////////////////

void    pfGUIMultiLineEditCtrl::HandleMouseDown( hsPoint3 &mousePt, UInt8 modifiers )
{
    if( fDynTextMap == nil || !fBounds.IsInside( &mousePt ) )
        return;

    IScreenToLocalPt( mousePt );
    mousePt.fX *= fDynTextMap->GetVisibleWidth();
    mousePt.fY *= fDynTextMap->GetVisibleHeight();

    IMoveCursorTo( IPointToPosition( (Int16)(mousePt.fX), (Int16)(mousePt.fY) ) );
}

//// HandleMouseUp ///////////////////////////////////////////////////////////

void    pfGUIMultiLineEditCtrl::HandleMouseUp( hsPoint3 &mousePt, UInt8 modifiers )
{
    if( fDynTextMap == nil || !fBounds.IsInside( &mousePt ) )
        return;

    IScreenToLocalPt( mousePt );
    mousePt.fX *= fDynTextMap->GetVisibleWidth();
    mousePt.fY *= fDynTextMap->GetVisibleHeight();

    IMoveCursorTo( IPointToPosition( (Int16)(mousePt.fX), (Int16)(mousePt.fY) ) );
}

//// HandleMouseDrag /////////////////////////////////////////////////////////

void    pfGUIMultiLineEditCtrl::HandleMouseDrag( hsPoint3 &mousePt, UInt8 modifiers )
{
    if( fDynTextMap == nil || !fBounds.IsInside( &mousePt ) )
        return;

    IScreenToLocalPt( mousePt );
    mousePt.fX *= fDynTextMap->GetVisibleWidth();
    mousePt.fY *= fDynTextMap->GetVisibleHeight();

    IMoveCursorTo( IPointToPosition( (Int16)(mousePt.fX), (Int16)(mousePt.fY) ) );
}

hsBool  pfGUIMultiLineEditCtrl::HandleKeyPress( char keyIn, UInt8 modifiers )
{
    wchar_t key = (wchar_t)keyIn;

    if ((fPrevCtrl || fNextCtrl) && (fLineStarts.GetCount() <= GetFirstVisibleLine()))
        return true; // we're ignoring if we can't actually edit our visible frame (and we're linked)

    if (modifiers & pfGameGUIMgr::kCtrlDown)
        return true; // we're ignoring ctrl key events

    if( fIgnoreNextKey )
    {
        // So we don't process keys that already got handled by HandleKeyEvent()
        fIgnoreNextKey = false;
        return true;
    }

    // Store info for the event we're about to send out
    fLastKeyModifiers = modifiers;
    fLastKeyPressed = key;
    
    // Send out a key pressed event. 
    HandleExtendedEvent( kKeyPressedEvent );

    // We discard keys when locked only after we give our handler the key
    if( IsLocked() )
        return true;

    if (plKeyboardDevice::KeyIsDeadKey())
    {
        if (fLastDeadKey != 0)
        {
            wchar_t temp = key; // we have two dead keys in a row, print out the old one and store the new one
            key = fLastDeadKey;
            fLastDeadKey = temp;
        }
        else
        {
            fLastDeadKey = key; // store the dead key and don't print it until we get the next char
            return true;
        }
    }

    if (fLastDeadKey != 0) // we have a dead key that needs to be added in
    {
        wchar_t translatedKey = fDeadKeyConverter[(char)fLastDeadKey+100][(char)key];
        if (translatedKey == 0) // no translation possible?
        {
            // so we need to print the dead key, followed by the typed key
            // unless key is a space, then we just type the dead key
            if (key == ' ')
            {
                // Insert character at the current cursor position, then inc the cursor by one
                // Note: we always want selection mode off when we're typing
                InsertChar( fLastDeadKey );
                fLastDeadKey = 0;
                return true;
            }
            // Insert characters at the current cursor position, then inc the cursor by one
            // Note: we always want selection mode off when we're typing
            InsertChar( fLastDeadKey );
            InsertChar( key );
            fLastDeadKey = 0;
            return true;
        }
        // ok, so we have a translated key now, so assign it to our key and print it normally
        key = translatedKey;
        fLastDeadKey = 0;
    }

    // Insert character at the current cursor position, then inc the cursor by one
    // Note: we always want selection mode off when we're typing
    InsertChar( key );
    return true;
}

hsBool  pfGUIMultiLineEditCtrl::HandleKeyEvent( pfGameGUIMgr::EventType event, plKeyDef key, UInt8 modifiers )
{
    if( key == KEY_CAPSLOCK )
        return false;

    if ((fPrevCtrl || fNextCtrl) && (fLineStarts.GetCount() <= GetFirstVisibleLine()))
        return true; // we're ignoring if we can't actually edit our visible frame (and we're linked)

    if (modifiers & pfGameGUIMgr::kCtrlDown)
        return true; // we're ignoring ctrl key events

    if( event == pfGameGUIMgr::kKeyDown || event == pfGameGUIMgr::kKeyRepeat )
    {
        // Use arrow keys to do our dirty work
        if( key == KEY_UP )
            IMoveCursor( kOneLineUp );
        else if( key == KEY_DOWN )
            IMoveCursor( kOneLineDown );
        else if( key == KEY_HOME )
            IMoveCursor( ( modifiers & pfGameGUIMgr::kCtrlDown ) ? kBufferStart : kLineStart );
        else if( key == KEY_END )
            IMoveCursor( ( modifiers & pfGameGUIMgr::kCtrlDown ) ? kBufferEnd : kLineEnd );
        else if( key == KEY_PAGEUP )
            IMoveCursor( kPageUp );
        else if( key == KEY_PAGEDOWN )
            IMoveCursor( kPageDown );
        
        else if( key == KEY_LEFT )
            IMoveCursor( ( modifiers & pfGameGUIMgr::kCtrlDown ) ? kOneWordBack : kOneBack );
        else if( key == KEY_RIGHT )
            IMoveCursor( ( modifiers & pfGameGUIMgr::kCtrlDown ) ? kOneWordForward : kOneForward );

        else if( key == KEY_BACKSPACE )
        {
            if( IsLocked() )
                return true;

            if( fCursorPos > 0 )
            {
                IMoveCursor(kOneBack);
                DeleteChar();
            }
        }
        else if( key == KEY_DELETE )
        {
            if( IsLocked() )
                return true;

            DeleteChar();
        }
        else if( key == KEY_ESCAPE )
        {
//          fEscapedFlag = true;
            DoSomething();      // Query WasEscaped() to see if it was escape vs enter
        }
        else
        {
            fIgnoreNextKey = false;
            return true;
        }

        fIgnoreNextKey = true;
        return true;
    }
    else
        // We don't process them, but we don't want anybody else processing them either
        return true;
}

//// ISetCursor //////////////////////////////////////////////////////////////
//  Only moves the cursor and redraws, doesn't bother with selections. You
//  should probably call IMoveCursorTo() unless you really know what you're
//  doing and don't want the current selection updated.

void    pfGUIMultiLineEditCtrl::ISetCursor( Int32 newPosition )
{
    fCursorPos = newPosition;

    Int32 newLine = IFindCursorLine();

    // Rescroll if necessary
    if( fLastCursorLine != newLine )
    {
        if( newLine < fScrollPos )
        {
            if (fPrevCtrl) // we are linked
            {
                fPrevCtrl->ISetCursor(newPosition); // so tell the prev control to set its cursor
                fPrevCtrl->GetOwnerDlg()->SetFocus(fPrevCtrl); // give control to the prev control (since we just moved our cursor there)
            }
            else if (!fPrevCtrl && fNextCtrl) // we are linked, but there isn't anyone before us
                IHitBeginningOfControlList(newPosition); // send an event to the person that wanted it
            else
                SetScrollPosition( newLine );
        }
        else
        {
            // -2 here for a reason: 1 because we want the last fully visible line, not partially visible,
            // and 1 because we want the actual last visible line index, which is of course start + len - 1
            Int32 delta = newLine - ( fScrollPos + ICalcNumVisibleLines() - 2 );
            if( delta > 0 )
            {
                if (fNextCtrl) // we are linked
                {
                    fNextCtrl->ISetCursor(newPosition); // so tell the next control to set its cursor
                    fNextCtrl->GetOwnerDlg()->SetFocus(fNextCtrl); // give control to the next control (since we just moved our cursor there)
                }
                else if (!fNextCtrl && fPrevCtrl) // we are linked, but there isn't anyone after us
                    IHitEndOfControlList(newPosition); // send an event to the person that wanted it
                else
                    SetScrollPosition( fScrollPos + delta );
            }
        }
    }

    if( fLastCursorLine < newLine )
        IUpdate( fLastCursorLine, newLine );
    else
        IUpdate( newLine, fLastCursorLine );

    fLastCursorLine = newLine;
}

//// IMoveCursor /////////////////////////////////////////////////////////////
//  Moves the cursor in a relative direction.

void    pfGUIMultiLineEditCtrl::IMoveCursor( pfGUIMultiLineEditCtrl::Direction dir )
{
    Int32   cursor = fCursorPos, line, offset, end;


    switch( dir )
    {
        case kLineStart:
            while( cursor > 0 && fBuffer[ cursor - 1 ] != L'\n' )
                cursor--;
            break;

        case kLineEnd:
            while( cursor < fBuffer.GetCount() - 1 && fBuffer[ cursor ] != L'\n' )
                cursor++;
            break;

        case kBufferStart:
            cursor = 0;
            break;

        case kBufferEnd:
            cursor = fBuffer.GetCount() - 1;
            break;

        case kOneBack:
            if( cursor > 0 )
            {
                cursor--;
                while( cursor > 0 && ( fBuffer[ cursor ] == fColorCodeChar || fBuffer[ cursor ] == fStyleCodeChar ) )
                    cursor -= IOffsetToNextChar( fBuffer[ cursor ] );
            }
            break;

        case kOneForward:
            if( cursor < fBuffer.GetCount() - 1 )
            {
                cursor++;
                while( cursor < fBuffer.GetCount() - 1 && ( fBuffer[ cursor ] == fColorCodeChar || fBuffer[ cursor ] == fStyleCodeChar ) )
                    cursor += IOffsetToNextChar( fBuffer[ cursor ] );
            }
            break;

        case kOneWordBack:
            if( cursor > 0 )
            {
                for( ; cursor > 0 && IIsWordBreaker( fBuffer[ cursor - 1 ] ); cursor-- );
                for( ; cursor > 0 && !IIsWordBreaker( fBuffer[ cursor - 1 ] ); cursor-- );
            }
            break;

        case kOneWordForward:
            if( cursor < fBuffer.GetCount() - 1 )
            {
                for( ; cursor < fBuffer.GetCount() - 1 && !IIsWordBreaker( fBuffer[ cursor ] ); cursor++ );
                for( ; cursor < fBuffer.GetCount() - 1 && IIsWordBreaker( fBuffer[ cursor ] ); cursor++ );
            }
            break;

        case kOneLineUp:
            // The wonderful thing is, since we keep going on the position of the cursor (which magically
            // is the left side of the character we're on), we end up drifting left as we keep going up
            // or down. So to compensate, we put a little fudge factor in that lets us attempt to stay 
            // "on course", as it were.
            if( IFindCursorLine( cursor ) > 0 )
                cursor = IPointToPosition( fCurrCursorX + ( fLineHeight >> 2 ), fCurrCursorY - fLineHeight, true );
            break;

        case kOneLineDown:
            if( IFindCursorLine( cursor ) < fLineStarts.GetCount() - 1 )
                cursor = IPointToPosition( fCurrCursorX + ( fLineHeight >> 2 ), fCurrCursorY + fLineHeight, true );
            break;

        case kPageUp:
            line = IFindCursorLine( cursor );
            offset = cursor - fLineStarts[ line ];

            line -= ( ICalcNumVisibleLines() - 1 );
            if( line < 0 )
                line = 0;

            end = ( line < fLineStarts.GetCount() - 1 ) ? fLineStarts[ line + 1 ] : fBuffer.GetCount() - 1;
            if( fLineStarts[ line ] + offset > end )
                offset = end - fLineStarts[ line ] - 1;

            cursor = fLineStarts[ line ] + offset;
            break;

        case kPageDown:
            line = IFindCursorLine( cursor );
            offset = cursor - fLineStarts[ line ];

            line += ( ICalcNumVisibleLines() - 1 );
            if( line > fLineStarts.GetCount() - 1 )
                line = fLineStarts.GetCount() - 1;

            end = ( line < fLineStarts.GetCount() - 1 ) ? fLineStarts[ line + 1 ] : fBuffer.GetCount() - 1;
            if( fLineStarts[ line ] + offset > end )
                offset = end - fLineStarts[ line ] - 1;

            cursor = fLineStarts[ line ] + offset;
            break;
    }

    IMoveCursorTo( cursor );
}

//// IMoveCursorTo ///////////////////////////////////////////////////////////
//  Moves the cursor to the given absolute position and updates the selection
//  area accordingly and if necessary.

void    pfGUIMultiLineEditCtrl::IMoveCursorTo( Int32 position )
{
    ISetCursor( position );
}

//////////////////////////////////////////////////////////////////////////////
//// Buffer Modification /////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////

//// InsertChar //////////////////////////////////////////////////////////////
//  Inserts a character at the cursor, or if there is a selection, replaces
//  the selection with the given character. Either way, at the end the cursor
//  is after the inserted character.

void    pfGUIMultiLineEditCtrl::InsertChar( char c )
{
    InsertChar((wchar_t)c); // a simple cast should be fine
}

void    pfGUIMultiLineEditCtrl::InsertChar( wchar_t c )
{
    // Error checking--make sure our actual character isn't in the range of our
    // code characters, or chaos could erupt
    //if( c < 3 )
    //return;
    if (c == 0)
        return;

    if ( fBufferLimit == -1 || fBuffer.GetCount()+1 < fBufferLimit-1)
    {
        fBuffer.Insert( fCursorPos, c );

        ISetGlobalBuffer(); // update the global buffer

        IOffsetLineStarts( fCursorPos, 1 );
        IMoveCursor( kOneForward );
        IRecalcFromCursor();
    }
}

//// InsertString ////////////////////////////////////////////////////////////
//  Same as InsertChar, only with a null-terminated string of characters
//  instead of just one.

void    pfGUIMultiLineEditCtrl::InsertString( const char *string )
{
    wchar_t* temp = hsStringToWString(string);
    InsertString(temp);
    delete [] temp;
}

void    pfGUIMultiLineEditCtrl::InsertString( const wchar_t *string )
{
    int numChars = wcslen( string );

    if ( fBufferLimit == -1 || fBuffer.GetCount() + numChars < fBufferLimit-1)
    {
        // Don't freak out, the cast is OK here. Insert() wants an array of chars.
        // It should really take a const array, since it's just copying them,
        // but since it doesn't and we know it's just copying them, we cast and
        // be happy for now.
        fBuffer.Insert( fCursorPos, numChars, (wchar_t *)string );

        ISetGlobalBuffer(); // update the global buffer

        IOffsetLineStarts( fCursorPos, numChars );
        IMoveCursorTo( fCursorPos + numChars );
        IRecalcFromCursor();
    }
}

//// InsertColor /////////////////////////////////////////////////////////////
//  Inserts a color control code into the buffer, or if there is a selection,
//  places TWO color codes in: one before the selection with the given color
//  and the other after the given selection with the previous color.
//  There is one catch to all of this, though: if we have a selection AND
//  we're inserting a code that conflicts with any code contained in the
//  selection, we remove the conflicting codes.

void    pfGUIMultiLineEditCtrl::InsertColor( hsColorRGBA &color )
{
    IActuallyInsertColor( fCursorPos, color );
    IOffsetLineStarts( fCursorPos, fColorCodeSize );
    fCursorPos += fColorCodeSize;
    IRecalcFromCursor( true );  // Force update of all following lines, since 
                                // insertion of this code changes appearance of following characters
}

void    pfGUIMultiLineEditCtrl::IActuallyInsertColor( Int32 pos, hsColorRGBA &color )
{
    if ( fBufferLimit == -1 || fBuffer.GetCount()+4 < fBufferLimit-1 )
    {
        fBuffer.Insert( pos, fColorCodeChar );
        fBuffer.Insert( pos + 1, (UInt8)( color.r * 255.f ) );
        fBuffer.Insert( pos + 2, (UInt8)( color.g * 255.f ) );
        fBuffer.Insert( pos + 3, (UInt8)( color.b * 255.f ) );
        fBuffer.Insert( pos + 4, fColorCodeChar );
    }
}

//// InsertStyle /////////////////////////////////////////////////////////////
//  Same thing as InsertColor(), only with a style code (or two).

void    pfGUIMultiLineEditCtrl::InsertStyle( UInt8 fontStyle )
{
    IActuallyInsertStyle( fCursorPos, fontStyle );

    IOffsetLineStarts( fCursorPos, fStyleCodeSize );
    fCursorPos += fStyleCodeSize;
    IRecalcFromCursor( true );  // Force update of all following lines, since 
                                // insertion of this code changes appearance of following characters
}

void    pfGUIMultiLineEditCtrl::IActuallyInsertStyle( Int32 pos, UInt8 style )
{
    if ( fBufferLimit == -1 || fBuffer.GetCount() + 3 < fBufferLimit-1 )
    {
        fBuffer.Insert( pos, fStyleCodeChar );
        fBuffer.Insert( pos + 1, (wchar_t)style );
        fBuffer.Insert( pos + 2, fStyleCodeChar );
    }
}

//// DeleteChar //////////////////////////////////////////////////////////////
//  If there is no selection, deletes the single character that the cursor
//  is in front of. Otherwise, deletes the current selection and places the
//  cursor where the selection used to be.

void    pfGUIMultiLineEditCtrl::DeleteChar( void )
{
    if( fCursorPos < fBuffer.GetCount() - 1 )
    {
        Int32 offset = IOffsetToNextChar( fBuffer[ fCursorPos ] );
        bool forceUpdate = IIsCodeChar( fBuffer[ fCursorPos ] );
        fBuffer.Remove( fCursorPos, offset );

        ISetGlobalBuffer(); // update the global buffer

        IOffsetLineStarts( fCursorPos, -offset );
        IRecalcFromCursor( forceUpdate );
    }
}

//// ICopyRange //////////////////////////////////////////////////////////////
//  Generic coded-to-non-coded conversion. Returns a copy of the string that
//  the caller must free.

wchar_t *pfGUIMultiLineEditCtrl::ICopyRange( Int32 start, Int32 end ) const
{
    Int32   stringSize, pos;
    wchar_t *string;


    // First loop, just count how much space we need
    for( stringSize = 0, pos = start; pos < end; pos = pos + IOffsetToNextChar( fBuffer[ pos ] ) )
    {
        if( !IIsCodeChar( fBuffer[ pos ] ) )
            stringSize++;
    }

    // Our string...
    string = TRACKED_NEW wchar_t[ stringSize + 1 ];

    // Now actually copy the characters
    for( stringSize = 0, pos = start; pos < end; pos = pos + IOffsetToNextChar( fBuffer[ pos ] ) )
    {
        if( !IIsCodeChar( fBuffer[ pos ] ) )
            string[ stringSize++ ] = fBuffer[ pos ];
    }
    string[ stringSize++ ] = 0;

    // All done!
    return string;
}

//// ClearBuffer /////////////////////////////////////////////////////////////
//  Clears everything, including the undo list.

void    pfGUIMultiLineEditCtrl::ClearBuffer( void )
{
    fBuffer.Reset();
    fBuffer.Append( 0 );
    fCursorPos = 0;
    fLastCursorLine = 0;
    fScrollPos = 0;
    IRecalcLineStarts( 0, true );
}

//// SetBuffer ///////////////////////////////////////////////////////////////
//  Replaces the entire contents of the buffer with the given text. Also
//  clears the undo list.

void    pfGUIMultiLineEditCtrl::SetBuffer( const char *asciiText )
{
    SetBuffer( (const UInt8 *)asciiText, (UInt32)strlen( asciiText ) );
}

void    pfGUIMultiLineEditCtrl::SetBuffer( const wchar_t *asciiText )
{
    SetBuffer( (const UInt16 *)asciiText, (UInt32)wcslen( asciiText ) );
}

//// SetBuffer ///////////////////////////////////////////////////////////////
//  The non-0-terminated-string version that can handle buffers with style
//  codes in them.

void    pfGUIMultiLineEditCtrl::SetBuffer( const UInt8 *codedText, UInt32 length )
{
    // convert to UInt16 and set
    UInt16 *convertedText = TRACKED_NEW UInt16[ length ];
    for( Int32 curChar = 0; curChar < length; curChar++ )
        convertedText[ curChar ] = (UInt16)codedText[ curChar ];
    SetBuffer(convertedText,length);
    delete [] convertedText;
}

void    pfGUIMultiLineEditCtrl::SetBuffer( const UInt16 *codedText, UInt32 length )
{
    // recursively call back to the first control and set it
    if (fPrevCtrl)
    {
        fPrevCtrl->SetBuffer(codedText,length);
        IUpdateBuffer();
    }
    else // we are the first control, so set our buffer
    {
        fBuffer.Reset();
        // Why o why doesn't Insert() take a const array....
        fBuffer.Insert( 0, length, (wchar_t *)codedText );
        fBuffer.Append( 0 );
        IRecalcLineStarts( 0, true ); // only the first control will recalc, this function recurses down to handle all following controls
    }

    fScrollPos = GetFirstVisibleLine();
    fCursorPos = 0;
    fLastCursorLine = 0;
}

//// GetNonCodedBuffer ///////////////////////////////////////////////////////
//  Copies the entire buffer into an ASCII string (i.e. strips formatting)
//  and returns it. The caller is responsible for freeing it.
//  To avoid code duplication, we'll just cheat and use CopySelection()...

char    *pfGUIMultiLineEditCtrl::GetNonCodedBuffer( void ) const
{
    // recursively search back to the first control in the linked list and grab its buffer
    if (fPrevCtrl)
        return fPrevCtrl->GetNonCodedBuffer();
    else
    {
        // -1 for the null terminator
        wchar_t *buffer = ICopyRange( 0, fBuffer.GetCount() - 1 );
        char *retVal = hsWStringToString(buffer);
        delete [] buffer;

        return retVal;
    }
}

wchar_t *pfGUIMultiLineEditCtrl::GetNonCodedBufferW( void ) const
{
    // recursively search back to the first control in the linked list and grab its buffer
    if (fPrevCtrl)
        return fPrevCtrl->GetNonCodedBufferW();
    else
    {
        // -1 for the null terminator
        return ICopyRange( 0, fBuffer.GetCount() - 1 );
    }
}

//// GetCodedBuffer //////////////////////////////////////////////////////////
//  Basically does a blanket copy of the entire buffer and returns it and
//  the length. The caller is responsible for freeing the buffer.

UInt8   *pfGUIMultiLineEditCtrl::GetCodedBuffer( UInt32 &length ) const
{
    // recursively search back to the first control in the linked list and grab its buffer
    if (fPrevCtrl)
        return fPrevCtrl->GetCodedBuffer(length);
    else
    {
        length = fBuffer.GetCount() - 1;

        // convert to UInt8 and return
        UInt8 *buffer = TRACKED_NEW UInt8[ length ];

        for (Int32 curChar = 0; curChar < length; curChar++)
        {
            if (fBuffer[ curChar ] > (wchar_t)0xFF)
            {
                // char doesn't fit, fake it with a space
                buffer[ curChar ] = (UInt8)(L' ');
            }
            else
                buffer[ curChar ] = (UInt8)fBuffer[ curChar ];
        }
        return buffer;
    }
}

UInt16  *pfGUIMultiLineEditCtrl::GetCodedBufferW( UInt32 &length ) const
{
    // recursively search back to the first control in the linked list and grab its buffer
    if (fPrevCtrl)
        return fPrevCtrl->GetCodedBufferW(length);
    else
    {
        length = fBuffer.GetCount() - 1;

        UInt16 *buffer = TRACKED_NEW UInt16[ length ];

        // AcquireArray() isn't const...
        memcpy( buffer, &fBuffer[ 0 ], length * sizeof(UInt16) );

        return buffer;
    }
}

UInt32  pfGUIMultiLineEditCtrl::GetBufferSize()
{
    return fBuffer.GetCount() - 1;
}

//// ICharPosToBufferPos /////////////////////////////////////////////////////
//  Given a character position (i.e. a buffer position if we didn't have
//  control codes), returns the actual buffer pos.

Int32   pfGUIMultiLineEditCtrl::ICharPosToBufferPos( Int32 charPos ) const
{
    Int32   pos;


    for( pos = 0; charPos > 0 && pos < fBuffer.GetCount() - 1; pos += IOffsetToNextCharFromPos( pos ), charPos-- );

    return pos;
}

//// Locking /////////////////////////////////////////////////////////////////

void    pfGUIMultiLineEditCtrl::Lock( void )
{
    fLockCount++;
    IUpdate();  
}

void    pfGUIMultiLineEditCtrl::Unlock( void )
{
    fLockCount--;
    //hsAssert( fLockCount >= 0, "Too many unlocks for pfGUIMultiLineEditCtrl" );
    if (fLockCount < 0)
        fLockCount = 0; // instead of asserting, hande it nicely
    IUpdate();  
}

// linking functions, for linking multiple multi-line edit controls together

void    pfGUIMultiLineEditCtrl::SetNext( pfGUIMultiLineEditCtrl *newNext )
{
    ClearNext(); // clear the existing link (if there is one)
    if (newNext)
    {
        newNext->ClearPrev(); // clear any existing prev link
        fNextCtrl = newNext;
        fNextCtrl->fPrevCtrl = this;
        fNextCtrl->IUpdateBuffer();
        fNextCtrl->fScrollPos = fNextCtrl->GetFirstVisibleLine();
    }
}

void    pfGUIMultiLineEditCtrl::ClearNext()
{
    if (fNextCtrl)
        fNextCtrl->fPrevCtrl = nil; // unlink ourselves from the next control
    fNextCtrl = nil;
}

void    pfGUIMultiLineEditCtrl::SetPrev( pfGUIMultiLineEditCtrl *newPrev )
{
    ClearPrev(); // clear existing link
    if (newPrev)
    {
        newPrev->ClearNext(); // clear any existing next link
        fPrevCtrl = newPrev;
        fPrevCtrl->fNextCtrl = this;
        IUpdateBuffer();
        fScrollPos = GetFirstVisibleLine();
    }
}

void    pfGUIMultiLineEditCtrl::ClearPrev()
{
    if (fPrevCtrl)
        fPrevCtrl->fNextCtrl = nil; // unlink ourselves from the prev control
    fPrevCtrl = nil;
}

void    pfGUIMultiLineEditCtrl::SetEventProc(pfGUIMultiLineEditProc *eventProc)
{
    if (fPrevCtrl)
        fPrevCtrl->SetEventProc(eventProc);
    else
    {
        if (fEventProc)
            delete fEventProc;
        fEventProc = eventProc;
    }
}

void    pfGUIMultiLineEditCtrl::ClearEventProc()
{
    if (fPrevCtrl)
        fPrevCtrl->ClearEventProc();
    else
    {
        if (fEventProc)
            delete fEventProc;
        fEventProc = nil;
    }
}

Int32   pfGUIMultiLineEditCtrl::GetFirstVisibleLine()
{
    // recursively search back to the first control and work our way forwards to where we are supposed to be
    if (fPrevCtrl)
        return fPrevCtrl->GetLastVisibleLine();
    return fScrollPos; // we're the first control, so we show the first part of the buffer
}

Int32   pfGUIMultiLineEditCtrl::GetLastVisibleLine()
{
    // simply add our number of lines to our first visible line
    return GetFirstVisibleLine()+ICalcNumVisibleLines()-1;
}

void    pfGUIMultiLineEditCtrl::SetGlobalStartLine(Int32 line)
{
    // recursively call back to the first control and set it
    if (fPrevCtrl)
        fPrevCtrl->SetGlobalStartLine(line);
    else
    {
        fScrollPos = line;
        IRecalcLineStarts(fScrollPos,true);
    }
}

void    pfGUIMultiLineEditCtrl::IUpdateLineStarts()
{
    // make sure our line starts array matches the gloabl one
    if (fPrevCtrl)
    {
        fPrevCtrl->IUpdateLineStarts();
        fLineStarts = fPrevCtrl->fLineStarts;
    }
}

void    pfGUIMultiLineEditCtrl::IUpdateBuffer()
{
    // make sure our local buffer matches the global one
    if (fPrevCtrl)
    {
        // copy the buffer from our global one
        UInt32 length;
        UInt16 *codedText = GetCodedBufferW(length);
        fBuffer.Reset();
        fBuffer.Insert( 0, length, (wchar_t *)codedText );
        fBuffer.Append( 0 );
        delete [] codedText;
    }
}

void pfGUIMultiLineEditCtrl::ISetGlobalBuffer()
{
    if (fPrevCtrl)
    {
        fPrevCtrl->fBuffer.Reset();
        int i;
        for (i=0; i<fBuffer.GetCount(); i++)
            fPrevCtrl->fBuffer.Append(fBuffer[i]);
        fPrevCtrl->ISetGlobalBuffer(); // pass the update backwards
    }
}

void pfGUIMultiLineEditCtrl::ISetLineStarts(hsTArray<Int32> lineStarts)
{
    if (fNextCtrl)
        fNextCtrl->ISetLineStarts(lineStarts); // pass it on down

    IUpdateBuffer(); // make sure the buffer is correct
    fLineStarts = lineStarts;

    IUpdate(); // make sure everything looks right
}

void pfGUIMultiLineEditCtrl::SetMargins(int top, int left, int bottom, int right)
{
    fTopMargin = top;
    fLeftMargin = left;
    fBottomMargin = bottom;
    fRightMargin = right;
    IRecalcLineStarts(0,false);
}

void pfGUIMultiLineEditCtrl::IHitEndOfControlList(Int32 cursorPos)
{
    if (fPrevCtrl)
        fPrevCtrl->IHitEndOfControlList(cursorPos);
    else
    {
        if (fEventProc)
            fEventProc->OnEndOfControlList(cursorPos);
    }
}

void pfGUIMultiLineEditCtrl::IHitBeginningOfControlList(Int32 cursorPos)
{
    if (fPrevCtrl)
        fPrevCtrl->IHitBeginningOfControlList(cursorPos);
    else
    {
        if (fEventProc)
            fEventProc->OnBeginningOfControlList(cursorPos);
    }
}

void pfGUIMultiLineEditCtrl::SetFontFace(std::string fontFace)
{
    fFontFace = fontFace;
    fFontFlagsSet |= kFontFaceSet;
    fDynTextMap->SetFont( fFontFace.c_str(), fFontSize, fFontStyle, 
                            HasFlag( kXparentBgnd ) ? false : true );
    fDynTextMap->CalcStringWidth( "The quick brown fox jumped over the lazy dog.", &fLineHeight );
}

void pfGUIMultiLineEditCtrl::SetFontSize(UInt8 fontSize)
{
    fFontSize = fontSize;
    fFontFlagsSet |= kFontSizeSet;
    fCalcedFontSize = fontSize;
    fDynTextMap->SetFont( fFontFace.c_str(), fFontSize, fFontStyle, 
                            HasFlag( kXparentBgnd ) ? false : true );
    fDynTextMap->CalcStringWidth( "The quick brown fox jumped over the lazy dog.", &fLineHeight );
}

// are we showing the beginning of the buffer? (controls before us are too far up)
hsBool pfGUIMultiLineEditCtrl::ShowingBeginningOfBuffer()
{
    if (fScrollPos == 0)
        return true;
    return false;
}

// are we showing the end of the buffer? (controls after us are too far down)
hsBool pfGUIMultiLineEditCtrl::ShowingEndOfBuffer()
{
    //IRecalcLineStarts(0,true); // This function gets called a lot from the journal book, so IRecalcLineStarts() REALLY slows things
    // down if we're looking at a large amount of text, hopefully we can mess with the existing line starts for now without issue
    if (GetLastVisibleLine() >= fLineStarts.GetCount())
        return true;
    return false;
}

void pfGUIMultiLineEditCtrl::DeleteLinesFromTop(int numLines)
{
    if (fPrevCtrl || fNextCtrl)
        return; // don't do anything

    UInt32 bufferLen = 0;
    UInt16* buffer = GetCodedBufferW(bufferLen);

    if (bufferLen == 0)
    {
        delete [] buffer;
        return;
    }

    for (int curLine = 0; curLine < numLines; ++curLine)
    {
        bool hitEnd = true; // did we hit the end of the buffer before we hit a newline?

        // search for the first newline and nuke it and everything before it
        bool skippingColor = false, skippingStyle = false;
        int curColorPos = 0, curStylePos = 0;
        for (UInt32 curChar = 0; curChar < bufferLen - 1; ++curChar)
        {
            // we need to skip the crappy color and style "tags" so non-character values inside them
            // don't trigger our newline check
            if (!skippingColor && !skippingStyle)
            {
                if (buffer[curChar] == fColorCodeChar)
                {
                    curColorPos = 0;
                    skippingColor = true;
                    continue;
                }
                else if (buffer[curChar] == fStyleCodeChar)
                {
                    curStylePos = 0;
                    skippingStyle = true;
                    continue;
                }
            }

            if (skippingColor)
            {
                ++curColorPos;
                if (curColorPos == fColorCodeSize)
                    skippingColor = false;
                else
                    continue;
            }

            if (skippingStyle)
            {
                ++curStylePos;
                if (curStylePos == fStyleCodeSize)
                    skippingStyle = false;
                else
                    continue;
            }

            // if it's a newline, erase it and everything before it
            if ((buffer[curChar] == L'\n') || (buffer[curChar] == L'\r'))
            {
                hitEnd = false;
                UInt32 newBufferStart = curChar + 1; // +1 so we eat the newline as well
                UInt32 newBufferLen = bufferLen - newBufferStart;
                MemCopy(buffer, buffer + newBufferStart, newBufferLen * sizeof(UInt16)); // copy all bytes after the newline to the beginning
                MemSet(buffer + newBufferLen, 0, (bufferLen - newBufferLen) * sizeof(UInt16)); // fill out the rest of the buffer with null chars
                bufferLen = newBufferLen;
                break;
            }
        }

        if (hitEnd)
        {
            delete [] buffer;
            SetBuffer(L""); // we are removing too many (or all) lines, just clear it
            return;
        }

        // still got lines to go, start looking for the next line
    }

    // we got here, so buffer is now our new buffer
    SetBuffer(buffer, bufferLen);
    delete [] buffer;
    return;
}