// EditCase is FAR manager plugin. It allows to change the case of selected text
// or current (nearest) word in the internal editor.
// This plugin can change case to: lower case, Title Case, UPPER CASE and tOGGLE cASE
// Besides, it has ability of cyclic case change like MS Word by ShiftF3
#if defined(__GNUC__) || defined(_MSC_VER)
#include <limits.h>
#undef MAXINT
#define MAXINT INT_MAX
#else
#include <values.h>		//MAXINT
#endif

#include <farplug-wide.h>
#include "EditLng.hpp"
#include "EditCase.hpp"
// Registry operations
#include <functional>
#include <algorithm>

#include <utils.h>
#include <KeyFileHelper.h>

#define INI_LOCATION InMyConfig("plugins/editcase/config.ini")
#define INI_SECTION  "Settings"

// This chars aren't letters
static std::wstring WordDiv = L" \n\r\t";

SHAREDSYMBOL int WINAPI EXP_NAME(GetMinFarVersion)()
{
	return FARMANAGERVERSION;
}

SHAREDSYMBOL void WINAPI EXP_NAME(SetStartupInfo)(const struct PluginStartupInfo *Info)
{
	::Info = *Info;
	::FSF = *Info->FSF;
	::Info.FSF = &::FSF;

	TCHAR SysWordDiv[0x100];
	int SysWordDivLen = (int)::Info.AdvControl(::Info.ModuleNumber, ACTL_GETSYSWORDDIV, SysWordDiv, NULL);
	if (SysWordDivLen > 0) {
		ASSERT(SysWordDivLen <= (int)ARRAYSIZE(SysWordDiv));
		WordDiv.append(SysWordDiv, SysWordDivLen);
	}
	WordDiv+= KeyFileReadSection(INI_LOCATION, INI_SECTION).GetString("AddWordDiv", L"#");
}

SHAREDSYMBOL HANDLE WINAPI EXP_NAME(OpenPlugin)(int OpenFrom, INT_PTR Item)
{
	size_t i;
	struct FarMenuItem MenuItems[5], *MenuItem;
	memset(MenuItems, 0, sizeof(MenuItems));
	int Msgs[] = {MCaseLower, MCaseTitle, MCaseUpper, MCaseToggle, MCaseCyclic};

	for (MenuItem = MenuItems, i = 0; i < ARRAYSIZE(MenuItems); ++i, ++MenuItem) {
		MenuItem->Selected = MenuItem->Checked = MenuItem->Separator = 0;
#ifndef UNICODE
		FSF.sprintf(MenuItem->Text, _T("%s"), GetMsg(Msgs[i]));		// Text in menu
#else
		MenuItem->Text = GetMsg(Msgs[i]);	// Text in menu
#endif
	};

	// First item is selected
	MenuItems[0].Selected = TRUE;

	// Show menu
	int MenuCode = Info.Menu(Info.ModuleNumber, -1, -1, 0, FMENU_AUTOHIGHLIGHT | FMENU_WRAPMODE,
			GetMsg(MCaseConversion), NULL, _T("Contents"), NULL, NULL, MenuItems, ARRAYSIZE(MenuItems));
	switch (MenuCode) {
		// If menu Escaped
		case -1:
			break;

		default:
			EditorInfo ei;
			Info.EditorControl(ECTL_GETINFO, &ei);

			// Current line number
			int CurLine = ei.CurLine;
			// Is anything selected
			BOOL IsBlock = FALSE;

			// Nothing selected?
			if (ei.BlockType != BTYPE_NONE) {
				IsBlock = TRUE;
				CurLine = ei.BlockStartLine;
			}

			// Type of Case Change
			int CCType = MenuCode;

			// Temporary string
			TCHAR *NewString = 0;

			// Forever :-) (Line processing loop)
			for (;;) {
				if (IsBlock) {
					if (CurLine >= ei.TotalLines)
						break;
					struct EditorSetPosition esp = {CurLine++, -1, -1, -1, -1, -1};
					Info.EditorControl(ECTL_SETPOSITION, &esp);
				}

				struct EditorGetString egs;

				egs.StringNumber = -1;

				// If can't get line
				if (!Info.EditorControl(ECTL_GETSTRING, &egs))
					break;	// Exit

				// If last selected line was processed or
				// nothing selected and line is empty
				if ((IsBlock && egs.SelStart == -1) || (!IsBlock && egs.StringLength <= 0))
					break;	// Exit

				// If something selected, but line is empty
				if (egs.StringLength <= 0)
					continue;	// Get next line

				// If whole line (with EOL) is selected
				if (egs.SelEnd == -1 || egs.SelEnd > egs.StringLength) {
					egs.SelEnd = egs.StringLength;
					if (egs.SelEnd < egs.SelStart)
						egs.SelEnd = egs.SelStart;
				}

				// Memory allocation
				NewString = (TCHAR *)malloc((egs.StringLength + 1) * sizeof(TCHAR));
				// If memory couldn't be allocated
				if (!NewString)
					break;

#ifndef UNICODE
				struct EditorConvertText ect;
#endif

				// If nothing selected - finding word bounds (what'll be converted)
				if (!IsBlock) {
					// Making NewString
					_tmemcpy(NewString, egs.StringText, egs.StringLength);
					NewString[egs.StringLength] = 0;
#ifndef UNICODE
					ect.Text = NewString;
					ect.TextLength = egs.StringLength;
					// Convert to OEM
					Info.EditorControl(ECTL_EDITORTOOEM, &ect);
#endif

					// Like whole line is selected
					egs.SelStart = 0;
					egs.SelEnd = egs.StringLength;

					// Finding word bounds (what'll be converted)
					FindBounds(NewString, egs.StringLength, ei.CurPos, egs.SelStart, egs.SelEnd);
				};

				// Making NewString
				_tmemcpy(NewString, egs.StringText, egs.StringLength);
				NewString[egs.StringLength] = 0;
#ifndef UNICODE
				ect.Text = &NewString[egs.SelStart];
				ect.TextLength = egs.SelEnd - egs.SelStart;
				// Convert to OEM
				Info.EditorControl(ECTL_EDITORTOOEM, &ect);
#endif

				// If Conversion Type is unknown or Cyclic
				if (CCType == CCCyclic)
					// Define Conversion Type
					CCType = GetNextCCType(NewString, egs.StringLength, egs.SelStart, egs.SelEnd);

				// NewString contains no words
				if (CCType != CCCyclic) {
					// Do the conversion
					ChangeCase(NewString, egs.SelStart, egs.SelEnd, CCType);

#ifndef UNICODE
					// Back to editor charset
					Info.EditorControl(ECTL_OEMTOEDITOR, &ect);
#endif

					// Put converted string to editor
					struct EditorSetString ess;
					ess.StringNumber = -1;
					ess.StringText = NewString;
					ess.StringEOL = (TCHAR *)egs.StringEOL;
					ess.StringLength = egs.StringLength;
					Info.EditorControl(ECTL_SETSTRING, &ess);
				};

#if 0
         if (!IsBlock)
         {
           struct EditorSelect esel;
           esel.BlockType=BTYPE_STREAM;
           esel.BlockStartLine=-1;
           esel.BlockStartPos=egs.SelStart;
           esel.BlockWidth=egs.SelEnd-egs.SelStart;
           esel.BlockHeight=1;
           Info.EditorControl(ECTL_SELECT,&esel);
         }
#endif
				// Free memory
				free(NewString);

				// Exit if nothing was selected (single word was converted)
				if (!IsBlock)
					break;
			}
			if (IsBlock) {
				struct EditorSetPosition esp = {ei.CurLine, ei.CurPos, -1, ei.TopScreenLine, ei.LeftPos,
						ei.Overtype};
				Info.EditorControl(ECTL_SETPOSITION, &esp);
			}

	};	// switch

	return (INVALID_HANDLE_VALUE);
}

SHAREDSYMBOL void WINAPI EXP_NAME(GetPluginInfo)(struct PluginInfo *Info)
{
	Info->StructSize = sizeof(*Info);
	Info->Flags = PF_EDITOR | PF_DISABLEPANELS;
	static const TCHAR *PluginMenuStrings[1];
	// Text in Plugins menu
	PluginMenuStrings[0] = GetMsg(MCaseConversion);
	Info->PluginMenuStrings = PluginMenuStrings;
	Info->PluginMenuStringsNumber = ARRAYSIZE(PluginMenuStrings);
};

const TCHAR *GetMsg(int MsgId)
{
	return (Info.GetMsg(Info.ModuleNumber, MsgId));
}

// What we consider as letter
bool MyIsAlpha(TCHAR c)
{
	return (WordDiv.find(c) == std::wstring::npos);
}

// Finding word bounds (what'll be converted) (Str is in OEM)
BOOL FindBounds(TCHAR *Str, int Len, int Pos, int &Start, int &End)
{
	int i = 1;
	BOOL ret = FALSE;

	// If line isn't empty
	if (Len > Start) {
		End = std::min(End, Len);

		// Pos between [Start, End] ?
		Pos = std::max(Pos, Start);
		Pos = std::min(End, Pos);

		// If current character is non letter
		if (!MyIsAlpha(Str[Pos])) {
			// Looking for letter on the left and counting radius
			while ((Start <= Pos - i) && (!MyIsAlpha(Str[Pos - i])))
				i++;

			// Radius
			int r = MAXINT;

			// Letter was found on the left
			if (Start <= Pos - i)
				r = i;	// Storing radius

			i = 1;
			// Looking for letter on the right and counting radius
			while ((Pos + i <= End) && (!MyIsAlpha(Str[Pos + i])))
				i++;

			// Letter was not found
			if (Pos + i > End)
				i = MAXINT;

			// Here r is left radius and i is right radius

			// If no letters was found
			if (std::min(r, i) != MAXINT) {
				// What radius is less? Left?
				if (r <= i) {
					End = Pos - r + 1;
					Start = FindStart(Str, Start, End);
				} else		// Right!
				{
					Start = Pos + i;
					End = FindEnd(Str, Start, End);
				};
				ret = TRUE;
			};
		} else		// Current character is letter!
		{
			Start = FindStart(Str, Start, Pos);
			End = FindEnd(Str, Pos, End);
			ret = TRUE;
		};
	};

	if (!ret)
		Start = End = -1;

	return ret;
};

int FindStart(TCHAR *Str, int Start, int End)
{
	// Current pos in Str
	int CurPos = End - 1;

	// While current character is letter
	while (CurPos >= Start && MyIsAlpha(Str[CurPos]))
		CurPos--;	// Moving to left

	return CurPos + 1;
};

int FindEnd(TCHAR *Str, int Start, int End)
{
	// Current pos in Str
	int CurPos = Start;

	// While current character is letter
	while (CurPos < End && MyIsAlpha(Str[CurPos]))
		CurPos++;	// Moving to right

	return CurPos;
};

// Changes Case of NewString from position Start till End
// to CCType and returns amount of changes
int ChangeCase(TCHAR *NewString, int Start, int End, int CCType)
{
	// If previous symbol is letter, then IsPrevSymbAlpha!=0
	BOOL IsPrevSymbAlpha = FALSE;
	// Amount of changes
	int ChangeCount = 0;

	// Main loop (position inside line)
	for (int i = Start; i < End; i++) {
		if (MyIsAlpha(NewString[i]))	// && ReverseOem==NewString[i])
		{
			switch (CCType) {
				case CCLower:
					NewString[i] = (TCHAR)FSF.LLower(NewString[i]);
					break;

				case CCTitle:
					if (IsPrevSymbAlpha)
						NewString[i] = (TCHAR)FSF.LLower(NewString[i]);
					else
						NewString[i] = (TCHAR)FSF.LUpper(NewString[i]);
					break;

				case CCUpper:
					NewString[i] = (TCHAR)FSF.LUpper(NewString[i]);
					break;

				case CCToggle:
					if (FSF.LIsLower(NewString[i]))
						NewString[i] = (TCHAR)FSF.LUpper(NewString[i]);
					else
						NewString[i] = (TCHAR)FSF.LLower(NewString[i]);
					break;
			};
			// Put converted letter back to string
			IsPrevSymbAlpha = TRUE;
			ChangeCount++;
		} else
			IsPrevSymbAlpha = FALSE;
	};

	return ChangeCount;
};

// Return CCType by rule: lower->Title->UPPER
// If Str contains no letters, then return CCCyclic
int GetNextCCType(TCHAR *Str, int StrLen, int Start, int End)
{
	int SignalWordStart = Start, SignalWordEnd = End;
	int SignalWordLen = std::max(Start, End);
	// Default conversion is to lower case
	int CCType = CCLower;

	Start = std::min(Start, End);
	End = SignalWordLen;

	if (StrLen < Start)
		return CCCyclic;

	// Looking for SignalWord (the 1-st word)
	if (!FindBounds(Str, StrLen, SignalWordStart, SignalWordStart, SignalWordEnd))
		return CCCyclic;

	SignalWordLen = SignalWordEnd - SignalWordStart;

	TCHAR *SignalWord = (TCHAR *)malloc((SignalWordLen + 1) * sizeof(TCHAR));

	if (SignalWord != NULL) {
		TCHAR *WrappedWord = (TCHAR *)malloc((SignalWordLen + 1) * sizeof(TCHAR));

		if (WrappedWord != NULL) {

			FSF.snprintf(SignalWord, SignalWordLen, _T("%.*s"), SignalWordLen, &Str[SignalWordStart]);
			FSF.snprintf(WrappedWord, SignalWordLen, _T("%s"), SignalWord);

			// if UPPER then Title
			FSF.LUpperBuf(WrappedWord, SignalWordLen);

			if (SignalWordLen == 1 && lstrcmp(SignalWord, WrappedWord) == 0)
				CCType = CCLower;
			else {
				if (SignalWordLen == 1)
					CCType = CCUpper;
				else {
					if (lstrcmp(SignalWord, WrappedWord) == 0)
						CCType = CCTitle;
					else {
						// if lower then UPPER
						FSF.LLowerBuf(WrappedWord, SignalWordLen);
						if (lstrcmp(SignalWord, WrappedWord) == 0)
							CCType = CCUpper;
						else {
							// if Title then lower
							WrappedWord[0] = FSF.LUpper(WrappedWord[0]);
							if (lstrcmp(SignalWord, WrappedWord) == 0)
								CCType = CCLower;
							else {
								// if upper case letters amount more than lower case letters
								// then tOGGLE
								FSF.LUpperBuf(WrappedWord, SignalWordLen);
								int Counter = SignalWordLen / 2 + 1;
								for (int i = 0; i < SignalWordLen && Counter; i++)
									if (SignalWord[i] == WrappedWord[i])
										Counter--;
								if (!Counter)
									CCType = CCToggle;
							}
						};
					};
				}
			}
			free(WrappedWord);
		};
		free(SignalWord);
	};

	return CCType;
};
