/*******************************************************************************

Copyright (c) 2007 Adobe Systems Incorporated

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

********************************************************************************/

#include "ScintillaMacOSX.h"
#include "ExtInput.h"

using namespace Scintilla;

// uncomment this for a log to /dev/console
// #define LOG_TSM 1

#if LOG_TSM
FILE* logFile = NULL;
#endif

static EventHandlerUPP tsmHandler;

static EventTypeSpec	tsmSpecs[] = {
	{ kEventClassTextInput, kEventTextInputUpdateActiveInputArea },
//	{ kEventClassTextInput, kEventTextInputUnicodeForKeyEvent },
	{ kEventClassTextInput, kEventTextInputOffsetToPos },
	{ kEventClassTextInput, kEventTextInputPosToOffset },
	{ kEventClassTextInput, kEventTextInputGetSelectedText }
};

#define kScintillaTSM	'ScTs'

// The following structure is attached to the HIViewRef as property kScintillaTSM

struct TSMData
{
	HIViewRef			view;				// this view
	TSMDocumentID		docid;				// the TSM document ID
	EventHandlerRef		handler;			// the event handler
	ScintillaMacOSX*	scintilla;			// the Scintilla pointer
	int					styleMask;			// the document style mask
	int					indicStyle [3];		// indicator styles save
	int					indicColor [3];		// indicator colors save
	int					selStart;			// starting position of selection (Scintilla offset)
	int					selLength;			// UTF-8 number of characters
	int					selCur;				// current position (Scintilla offset)
	int					inhibitRecursion;	// true to stop recursion
	bool				active;				// true if this is active
};

static const int numSpecs = 5;


// Fetch a range of text as UTF-16; delete the buffer after use

static char* getTextPortion (TSMData* data, UInt32 start, UInt32 size)
{
	Scintilla::TextRange range;
	range.chrg.cpMin = start;
	range.chrg.cpMax = start + size;
	range.lpstrText = new char [size + 1];
	range.lpstrText [size] = 0;
	data->scintilla->WndProc (SCI_GETTEXTRANGE, 0, (uptr_t) &range);
	return range.lpstrText;
}

static pascal OSStatus doHandleTSM (EventHandlerCallRef, EventRef inEvent, void* userData);

void ExtInput::attach (HIViewRef viewRef)
{
	if (NULL == tsmHandler)
		tsmHandler = NewEventHandlerUPP (doHandleTSM);
	::UseInputWindow (NULL, FALSE);

#ifdef LOG_TSM
	if (NULL == logFile)
		logFile = fopen ("/dev/console", "a");
#endif

	// create and attach the TSM data
	TSMData* data = new TSMData;

	data->view				= viewRef;
	data->active			= false;
	data->inhibitRecursion	= 0;
	
	::GetControlProperty (viewRef, scintillaMacOSType, 0, sizeof( data->scintilla ), NULL, &data->scintilla);
	
	if (NULL != data->scintilla)
	{
		// create the TSM document ref
		InterfaceTypeList interfaceTypes;
		interfaceTypes[0] = kUnicodeDocumentInterfaceType;
		::NewTSMDocument (1, interfaceTypes, &data->docid, (long) viewRef);
		// install my event handler
		::InstallControlEventHandler (viewRef, tsmHandler, numSpecs, tsmSpecs, data, &data->handler);

		::SetControlProperty (viewRef, kScintillaTSM, 0, sizeof (data), &data);
	}
	else
		delete data;
}

static TSMData* getTSMData (HIViewRef viewRef)
{
	TSMData* data = NULL;
	UInt32 n;
	::GetControlProperty (viewRef, kScintillaTSM, 0, sizeof (data), &n, (UInt32*) &data);
	return data;
}

void ExtInput::detach (HIViewRef viewRef)
{
	TSMData* data = getTSMData (viewRef);
	if (NULL != data)
	{
		::DeleteTSMDocument (data->docid);
		::RemoveEventHandler (data->handler);
		delete data;
	}
}

void ExtInput::activate (HIViewRef viewRef, bool on)
{
	TSMData* data = getTSMData (viewRef);
	if (NULL == data)
		return;

	if (on)
	{
		::ActivateTSMDocument (data->docid);
		HIRect bounds;
		::HIViewGetBounds (viewRef, &bounds);
		::HIViewConvertRect (&bounds, viewRef, NULL);
		RgnHandle hRgn = ::NewRgn();
		::SetRectRgn (hRgn, (short) bounds.origin.x, (short) bounds.origin.y, 
							(short) (bounds.origin.x + bounds.size.width), 
							(short) (bounds.origin.y + bounds.size.height));
#if LOG_TSM
		fprintf (logFile, "TSMSetInlineInputRegion (%08lX, %ld:%ld-%ld:%ld)\n", 
						 (long) data->docid, (long) bounds.origin.x, (long) bounds.origin.y,
						 (long) (bounds.origin.x + bounds.size.width), (long) (bounds.origin.y + bounds.size.height));
		fflush (logFile);
#endif
		::TSMSetInlineInputRegion (data->docid, HIViewGetWindow (data->view), hRgn);
		::DisposeRgn (hRgn);
		::UseInputWindow (NULL, FALSE);
	}
	else
	{
#if LOG_TSM
		fprintf (logFile, "DeactivateTSMDocument (%08lX)\n", (long) data->docid);
		fflush (logFile);
#endif
		::DeactivateTSMDocument (data->docid);
	}
}

static void startInput (TSMData* data, bool delSelection = true)
{
	if (!data->active && 0 == data->inhibitRecursion)
	{
		data->active = true;
		
		// Delete any selection
		if( delSelection )
			data->scintilla->WndProc (SCI_REPLACESEL, 0, reinterpret_cast<sptr_t>(""));
		 
		// need all style bits because we do indicators
		data->styleMask = data->scintilla->WndProc (SCI_GETSTYLEBITS, 0, 0);
		data->scintilla->WndProc (SCI_SETSTYLEBITS, 5, 0);
		
		// Set the target range for successive replacements
		data->selStart	=
		data->selCur	= data->scintilla->WndProc (SCI_GETCURRENTPOS, 0, 0);
		data->selLength	= 0;
		
		// save needed styles
		for (int i = 0; i < 2; i++)
		{
			data->indicStyle [i] = data->scintilla->WndProc (SCI_INDICGETSTYLE, i, 0);
			data->indicColor [i] = data->scintilla->WndProc (SCI_INDICGETFORE, i, 0);
		}
		// set styles and colors
		data->scintilla->WndProc (SCI_INDICSETSTYLE, 0, INDIC_SQUIGGLE);
		data->scintilla->WndProc (SCI_INDICSETFORE,  0, 0x808080);
		data->scintilla->WndProc (SCI_INDICSETSTYLE, 1, INDIC_PLAIN);	// selected converted
		data->scintilla->WndProc (SCI_INDICSETFORE,  1, 0x808080);
		data->scintilla->WndProc (SCI_INDICSETSTYLE, 2, INDIC_PLAIN);	// selected raw
		data->scintilla->WndProc (SCI_INDICSETFORE,  2, 0x0000FF);
		// stop Undo
		data->scintilla->WndProc (SCI_BEGINUNDOACTION, 0, 0);
	}
}

static void stopInput (TSMData* data, int pos)
{
	if (data->active && 0 == data->inhibitRecursion)
	{
		// First fix the doc - this may cause more messages
		// but do not fall into recursion
		data->inhibitRecursion++;
		::FixTSMDocument (data->docid);
		data->inhibitRecursion--;
		data->active = false;
		
		// Remove indicator styles
		data->scintilla->WndProc (SCI_STARTSTYLING, data->selStart, INDICS_MASK);
		data->scintilla->WndProc (SCI_SETSTYLING, pos - data->selStart, 0);
		// Restore old indicator styles and colors
		data->scintilla->WndProc (SCI_SETSTYLEBITS, data->styleMask, 0);
		for (int i = 0; i < 2; i++)
		{
			data->scintilla->WndProc (SCI_INDICSETSTYLE, i, data->indicStyle [i]);
			data->scintilla->WndProc (SCI_INDICSETFORE, i, data->indicColor [i]);
		}

		// remove selection and re-allow selections to display
		data->scintilla->WndProc (SCI_SETSEL, pos, pos);
		data->scintilla->WndProc (SCI_TARGETFROMSELECTION, 0, 0);
		data->scintilla->WndProc (SCI_HIDESELECTION, 0, 0);

		// move the caret behind the current area
		data->scintilla->WndProc (SCI_SETCURRENTPOS, pos, 0);
		// re-enable Undo
		data->scintilla->WndProc (SCI_ENDUNDOACTION, 0, 0);
		// re-colorize
		int32_t startLine = data->scintilla->WndProc (SCI_LINEFROMPOSITION, data->selStart, 0);
		int32_t startPos  = data->scintilla->WndProc (SCI_POSITIONFROMLINE, startLine, 0);
		int32_t endLine   = data->scintilla->WndProc (SCI_LINEFROMPOSITION, pos, 0);
		if (endLine == startLine)
			endLine++;
		int32_t endPos    = data->scintilla->WndProc (SCI_POSITIONFROMLINE, endLine, 0);
		
		data->scintilla->WndProc (SCI_COLOURISE, startPos, endPos);
	}
}

void ExtInput::stop (HIViewRef viewRef)
{
	TSMData* data = getTSMData (viewRef);
	if (NULL != data)
		stopInput (data, data->selStart + data->selLength);
}

static char* UTF16toUTF8 (const UniChar* buf, int len, int& utf8len)
{
	CFStringRef str = CFStringCreateWithCharactersNoCopy (NULL, buf, (UInt32) len, kCFAllocatorNull);
	CFRange range = { 0, len };
	CFIndex bufLen;
	CFStringGetBytes (str, range, kCFStringEncodingUTF8, '?', false, NULL, 0, &bufLen);
	UInt8* utf8buf = new UInt8 [bufLen+1];
	CFStringGetBytes (str, range, kCFStringEncodingUTF8, '?', false, utf8buf, bufLen, NULL);
	utf8buf [bufLen] = 0;
	CFRelease (str);
	utf8len = (int) bufLen;
	return (char*) utf8buf;
}

static int UCS2Length (const char* buf, int len)
{
	int n = 0;
	while (len > 0)
	{
		int bytes = 0;
		char ch = *buf;
		while (ch & 0x80)
			bytes++, ch <<= 1;
		len -= bytes;
		n += bytes;
	}
	return n;
}

static int UTF8Length (const UniChar* buf, int len)
{
	int n = 0;
	while (len > 0)
	{
		UInt32 uch = *buf++;
		len--;
		if (uch >= 0xD800 && uch <= 0xDBFF && len > 0)
		{
			UInt32 uch2 = *buf;
			if (uch2 >= 0xDC00 && uch2 <= 0xDFFF)
			{
				buf++;
				len--;
				uch = ((uch & 0x3FF) << 10) + (uch2 & 0x3FF);
			}
		}
		n++;
		if (uch > 0x7F)
			n++;
		if (uch > 0x7FF)
			n++;
		if (uch > 0xFFFF)
			n++;
		if (uch > 0x1FFFFF)
			n++;
		if (uch > 0x3FFFFFF)
			n++;
	}
	return n;
}

static OSStatus handleTSMUpdateActiveInputArea (TSMData* data, EventRef inEvent)
{
	UInt32				fixLength;
	int					caretPos = -1;
	UInt32				actualSize;
	::TextRangeArray*	hiliteRanges = NULL;
	char*				hiliteBuffer = NULL;
	bool				done;
	
	// extract the text
	UniChar* buffer = NULL;
	UniChar temp [128];
	UniChar* text = temp;
	
	// get the fix length (in bytes)
	OSStatus err = ::GetEventParameter (inEvent, kEventParamTextInputSendFixLen,
								   typeLongInteger, NULL, sizeof (long), NULL, &fixLength);
	// need the size (in bytes)
	if (noErr == err)
		err = ::GetEventParameter (inEvent, kEventParamTextInputSendText,
										typeUnicodeText, NULL, 256, &actualSize, temp);
										
	// then allocate and fetch if necessary
	UInt32 textLength = actualSize / sizeof (UniChar);
	fixLength /= sizeof (UniChar);

	if (noErr == err)
	{
		// this indicates that we are completely done
		done = (fixLength == textLength || fixLength < 0);
		if (textLength >= 128)
		{
			buffer = text = new UniChar [textLength];
			err = ::GetEventParameter (inEvent, kEventParamTextInputSendText,
									   typeUnicodeText, NULL, actualSize, NULL, (void*) text);
		}
		
		// set the text now, but convert it to UTF-8 first
		int utf8len;
		char* utf8 = UTF16toUTF8 (text, textLength, utf8len);
		data->scintilla->WndProc (SCI_SETTARGETSTART, data->selStart, 0);
		data->scintilla->WndProc (SCI_SETTARGETEND, data->selStart + data->selLength, 0);
		data->scintilla->WndProc (SCI_HIDESELECTION, 1, 0);
		data->scintilla->WndProc (SCI_REPLACETARGET, utf8len, (sptr_t) utf8);
		data->selLength = utf8len;
		delete [] utf8;
	}
	
	// attempt to extract the array of hilite ranges
	if (noErr == err)
	{
		::TextRangeArray tempTextRangeArray;
		OSStatus tempErr = ::GetEventParameter (inEvent, kEventParamTextInputSendHiliteRng,
											 typeTextRangeArray, NULL, sizeof (::TextRangeArray), &actualSize, &tempTextRangeArray);
		if (noErr == tempErr)
		{
			// allocate memory and get the stuff!
			hiliteBuffer = new char [actualSize];
			hiliteRanges = (::TextRangeArray*) hiliteBuffer;
			err = ::GetEventParameter (inEvent, kEventParamTextInputSendHiliteRng,
									   typeTextRangeArray, NULL, actualSize, NULL, hiliteRanges);
			if (noErr != err)
			{
				delete [] hiliteBuffer;
				hiliteBuffer = NULL;
				hiliteRanges = NULL;
			}
		}
	}
#if LOG_TSM
	fprintf (logFile, "kEventTextInputUpdateActiveInputArea:\n"
					 "  TextLength = %ld\n"
					 "  FixLength = %ld\n",
					 (long) textLength, (long) fixLength);
	fflush (logFile);
#endif

	if (NULL != hiliteRanges)
	{
		for (int i = 0; i < hiliteRanges->fNumOfRanges; i++)
		{
#if LOG_TSM
			fprintf (logFile, "  Range #%d: %ld-%ld (%d)\n",
								i+1,
								hiliteRanges->fRange[i].fStart,
								hiliteRanges->fRange[i].fEnd,
								hiliteRanges->fRange[i].fHiliteStyle);
			fflush (logFile);
#endif
			// start and end of range, zero based
			long bgn = long (hiliteRanges->fRange[i].fStart) / sizeof (UniChar);
			long end = long (hiliteRanges->fRange[i].fEnd) / sizeof (UniChar);
			if (bgn >= 0 && end >= 0)
			{
				// move the caret if this is requested
				if (hiliteRanges->fRange[i].fHiliteStyle == kTSMHiliteCaretPosition)
					caretPos = bgn;
				else
				{
					// determine which style to use
					int style;
					switch (hiliteRanges->fRange[i].fHiliteStyle)
					{
						case kTSMHiliteRawText:					style = INDIC0_MASK; break;
						case kTSMHiliteSelectedRawText:			style = INDIC0_MASK; break;
						case kTSMHiliteConvertedText:			style = INDIC1_MASK; break;
						case kTSMHiliteSelectedConvertedText:	style = INDIC2_MASK; break;
						default:								style = INDIC0_MASK;
					}
					// bgn and end are Unicode offsets from the starting pos
					// use the text buffer to determine the UTF-8 offsets
					long utf8bgn  = data->selStart + UTF8Length (text, bgn);
					long utf8size = UTF8Length (text + bgn, end - bgn);
					// set indicators
					int oldEnd = data->scintilla->WndProc (SCI_GETENDSTYLED, 0, 0);
					data->scintilla->WndProc (SCI_STARTSTYLING, utf8bgn, INDICS_MASK);
					data->scintilla->WndProc (SCI_SETSTYLING, utf8size, style & ~1);
					data->scintilla->WndProc (SCI_STARTSTYLING, oldEnd, 31);
				}
			}
		}
	}
	if (noErr == err)
	{
		// if the fixed length is == to the new text, we are done
		if (done)
			stopInput (data, data->selStart + UTF8Length (text, textLength));
		else if (caretPos >= 0)
		{
			data->selCur = data->selStart + UTF8Length (text, caretPos);
			data->scintilla->WndProc (SCI_SETCURRENTPOS, data->selCur, 0);
		}
	}
	
	delete [] hiliteBuffer;
	delete [] buffer;
	return err;
}

struct MacPoint {
  short               v;
  short               h;
};

static OSErr handleTSMOffset2Pos (TSMData* data, EventRef inEvent)
{
	long offset;
	
	// get the offfset to convert
	OSStatus err = ::GetEventParameter (inEvent, kEventParamTextInputSendTextOffset,
										typeLongInteger, NULL, sizeof (long), NULL, &offset);
	if (noErr == err)
	{
		// where is the caret now?
		HIPoint where;

		int line = (int) data->scintilla->WndProc (SCI_LINEFROMPOSITION, data->selCur, 0);
		where.x = data->scintilla->WndProc (SCI_POINTXFROMPOSITION, 0, data->selCur);
		where.y = data->scintilla->WndProc (SCI_POINTYFROMPOSITION, 0, data->selCur)
				+ data->scintilla->WndProc (SCI_TEXTHEIGHT, line, 0);
		// convert to window coords
		::HIViewConvertPoint (&where, data->view, NULL);
		// convert to screen coords
		Rect global;
		GetWindowBounds (HIViewGetWindow (data->view), kWindowStructureRgn, &global);
		MacPoint pt;
		pt.h = (short) where.x + global.left;
		pt.v = (short) where.y + global.top;

		// set the result
		err = ::SetEventParameter (inEvent, kEventParamTextInputReplyPoint, typeQDPoint, sizeof (MacPoint), &pt);
#if LOG_TSM
		fprintf (logFile, "kEventTextInputOffsetToPos:\n"
						 "  Offset: %ld\n"
						 "  Pos: %ld:%ld (orig = %ld:%ld)\n", offset, 
						 (long) pt.h, (long) pt.v,
						 (long) where.x, (long) where.y);
		fflush (logFile);
#endif
	}
	return err;
}

static OSErr handleTSMPos2Offset (TSMData* data, EventRef inEvent)
{
	MacPoint		qdPosition;
	long			offset;
	short			regionClass;
	
	// retrieve the global point to convert
	OSStatus err = ::GetEventParameter (inEvent, kEventParamTextInputSendCurrentPoint,
										typeQDPoint, NULL, sizeof (MacPoint), NULL, &qdPosition);
	if (noErr == err)
	{
#if LOG_TSM
		fprintf (logFile, "kEventTextInputPosToOffset:\n"
						 "  Pos: %ld:%ld\n", (long) qdPosition.v, (long) qdPosition.h);
		fflush (logFile);
#endif
		// convert to local coordinates
		HIRect rect;
		rect.origin.x = qdPosition.h;
		rect.origin.y = qdPosition.v;
		rect.size.width =
		rect.size.height = 1;
		::HIViewConvertRect (&rect, NULL, data->view);

		// we always report the position to be within the composition;
		// coords inside the same pane are clipped to the composition,
		// and if the position is outside, then we deactivate this instance
		// this leaves the edit open and active so we can edit multiple panes
		regionClass = kTSMInsideOfActiveInputArea;
		
		// compute the offset (relative value)
		offset = data->scintilla->WndProc (SCI_POSITIONFROMPOINTCLOSE, (uptr_t) rect.origin.x, (sptr_t) rect.origin.y);
		if (offset >= 0)
		{
			// convert to a UTF-16 offset (Brute Force)
			char* buf = getTextPortion (data, 0, offset);
			offset = UCS2Length (buf, offset);
			delete [] buf;
			
#if LOG_TSM
			fprintf (logFile, "  Offset: %ld (class %ld)\n", offset, (long) regionClass);
			fflush (logFile);
#endif
			// store the offset
			err = ::SetEventParameter (inEvent, kEventParamTextInputReplyTextOffset, typeLongInteger, sizeof (long), &offset);
			if (noErr == err)
				err = ::SetEventParameter (inEvent, kEventParamTextInputReplyRegionClass, typeShortInteger, sizeof (short), &regionClass);
		}
		else
		{
			// not this pane!
			err = eventNotHandledErr;
			ExtInput::activate (data->view, false);
		}
			
	}
	return err;	
}

static OSErr handleTSMGetText (TSMData* data, EventRef inEvent)
{
	char* buf = getTextPortion (data, data->selStart, data->selLength);
		
#if LOG_TSM
		fprintf (logFile, "kEventTextInputGetSelectedText:\n"
						  "  Text: \"%s\"\n", buf);
		fflush (logFile);
#endif
	OSStatus status = ::SetEventParameter (inEvent, kEventParamTextInputReplyText, typeUTF8Text, data->selLength, buf);
	delete [] buf;
	return status;
}		

static pascal OSStatus doHandleTSM (EventHandlerCallRef, EventRef inEvent, void* userData)
{
	TSMData* data = (TSMData*) userData;
	
	OSStatus err = eventNotHandledErr;

	switch (::GetEventKind (inEvent))
	{
		case kEventTextInputUpdateActiveInputArea:
			// Make sure that input has been started
			startInput (data);
			err = handleTSMUpdateActiveInputArea (data, inEvent);
			break;
//		case kEventTextInputUnicodeForKeyEvent:
//			err = handleTSMUnicodeInput (inEvent);
//			break;
		case kEventTextInputOffsetToPos:
			err = handleTSMOffset2Pos (data, inEvent);
			break;
		case kEventTextInputPosToOffset:
			err = handleTSMPos2Offset (data, inEvent);
			break;
		case kEventTextInputGetSelectedText:
			// Make sure that input has been started
			startInput (data, false);
			err = handleTSMGetText (data, inEvent);
			break;
	}
	return err;
}

