/******************************************************************************
 JXFileInput.cc

	Input field for entering a path + file.

	BASE CLASS = JXInputField

	Copyright © 1999 by John Lindal. All rights reserved.

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

#include <JXFileInput.h>
#include <JXSelectionManager.h>
#include <JXDNDManager.h>
#include <JXColormap.h>
#include <jXGlobals.h>
#include <jXUtil.h>
#include <jFileUtil.h>
#include <jDirUtil.h>
#include <jAssert.h>

/******************************************************************************
 Constructor

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

JXFileInput::JXFileInput
	(
	JXContainer*		enclosure,
	const HSizingOption	hSizing,
	const VSizingOption	vSizing,
	const JCoordinate	x,
	const JCoordinate	y,
	const JCoordinate	w,
	const JCoordinate	h
	)
	:
	JXInputField(enclosure, hSizing, vSizing, x,y, w,h)
{
	itsAllowInvalidFileFlag = kFalse;
	itsRequireReadFlag      = kTrue;
	itsRequireWriteFlag     = kTrue;
	itsRequireExecFlag      = kFalse;
	itsExpectURLDropFlag    = kFalse;
	SetIsRequired();
	ListenTo(this);
}

/******************************************************************************
 Destructor

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

JXFileInput::~JXFileInput()
{
}

/******************************************************************************
 HandleUnfocusEvent (virtual protected)

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

void
JXFileInput::HandleUnfocusEvent()
{
	JXInputField::HandleUnfocusEvent();
	SetCaretLocation(GetTextLength() + 1);
}

/******************************************************************************
 ApertureResized (virtual protected)

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

void
JXFileInput::ApertureResized
	(
	const JCoordinate dw,
	const JCoordinate dh
	)
{
	JXInputField::ApertureResized(dw,dh);
	if (!HasFocus())
		{
		SetCaretLocation(GetTextLength() + 1);
		}
}

/******************************************************************************
 Receive (virtual protected)

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

void
JXFileInput::Receive
	(
	JBroadcaster*	sender,
	const Message&	message
	)
{
	if (sender == this && message.Is(JTextEditor::kTextSet))
		{
		SetCaretLocation(GetTextLength() + 1);
		}

	JXInputField::Receive(sender, message);
}

/******************************************************************************
 SetBasePath

	This is used if a relative path is entered.  It must be an absolute path.

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

void
JXFileInput::SetBasePath
	(
	const JCharacter* path
	)
{
	assert( path != NULL && path[0] == '/' );
	itsBasePath = path;
	RecalcAll(kTrue);
}

/******************************************************************************
 GetTextForChooseFile

	Returns a string that can safely be passed to JChooseSaveFile::ChooseFile().

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

JString
JXFileInput::GetTextForChooseFile()
	const
{
	JString text = GetText();
	if (text.EndsWith("/"))
		{
		text.AppendCharacter('*');
		}
	return text;
}

/******************************************************************************
 GetFile

	Returns kTrue if the current file name is valid.  In this case, *fullName
	is set to the full path + name, relative to the root directory.

	Use this instead of GetText(), because that may return a relative path.

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

JBoolean
JXFileInput::GetFile
	(
	JString* fullName
	)
	const
{
	const JString& text = GetText();
	return JI2B(!text.IsEmpty() &&
				JConvertToAbsolutePath(text, itsBasePath, fullName) &&
				JFileExists(*fullName) &&
				(!itsRequireReadFlag  || JFileReadable(*fullName)) &&
				(!itsRequireWriteFlag || JFileWritable(*fullName)) &&
				(!itsRequireExecFlag  || JFileExecutable(*fullName)));
}

/******************************************************************************
 InputValid (virtual)

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

JBoolean
JXFileInput::InputValid()
{
	if (itsAllowInvalidFileFlag)
		{
		return kTrue;
		}
	else if (!JXInputField::InputValid())
		{
		return kFalse;
		}

	const JString& text = GetText();
	if (text.IsEmpty())
		{
		return JNegate(IsRequired());
		}

	JString fullName;
	if (!JConvertToAbsolutePath(text, itsBasePath, &fullName) ||
		!JFileExists(fullName))
		{
		(JGetUserNotification())->ReportError("This file does not exist.");
		return kFalse;
		}
	else if (itsRequireReadFlag && !JFileReadable(fullName))
		{
		(JGetUserNotification())->ReportError("This file is unreadable.");
		return kFalse;
		}
	else if (itsRequireWriteFlag && !JFileWritable(fullName))
		{
		(JGetUserNotification())->ReportError(
			"You don't have permission to write to this file.");
		return kFalse;
		}
	else if (itsRequireExecFlag && !JFileExecutable(fullName))
		{
		(JGetUserNotification())->ReportError("This file is not executable.");
		return kFalse;
		}
	else
		{
		return kTrue;
		}
}

/******************************************************************************
 AdjustStylesBeforeRecalc (virtual protected)

	Draw the invalid portion of the path in red.

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

void
JXFileInput::AdjustStylesBeforeRecalc
	(
	const JString&		buffer,
	JRunArray<Font>*	styles,
	JIndexRange*		recalcRange,
	JIndexRange*		redrawRange,
	const JBoolean		deletion
	)
{
	const JColormap* colormap = GetColormap();
	const JSize totalLength   = buffer.GetLength();

	JString fullName = buffer;
	if (buffer.GetFirstCharacter() != '/' &&
		buffer.GetFirstCharacter() != '~' &&
		!JConvertToAbsolutePath(buffer, itsBasePath, &fullName))
		{
		if (!itsBasePath.IsEmpty())
			{
			fullName = JCombinePathAndName(itsBasePath, buffer);
			}
		else
			{
			fullName.Clear();
			}
		}

	JSize errLength;
	if (fullName.IsEmpty())
		{
		errLength = totalLength;
		}
	else if (!JFileExists(fullName) ||
			 (itsRequireReadFlag  && !JFileReadable(fullName)) ||
			 (itsRequireWriteFlag && !JFileWritable(fullName)) ||
			 (itsRequireExecFlag  && !JFileExecutable(fullName)))
		{
		const JString closestDir = JGetClosestDirectory(fullName, kFalse);
		if (fullName.BeginsWith(closestDir))
			{
			errLength = fullName.GetLength() - closestDir.GetLength();
			}
		else
			{
			errLength = totalLength;
			}
		}
	else
		{
		errLength = 0;
		}

	if (errLength > 0 && buffer.EndsWith("/."))
		{
		errLength++;	// trailing . is trimmed
		}

	Font f = styles->GetFirstElement();

	styles->RemoveAll();
	if (errLength >= totalLength)
		{
		f.style.color = colormap->GetRedColor();
		styles->AppendElements(f, totalLength);
		}
	else
		{
		f.style.color = colormap->GetBlackColor();
		styles->AppendElements(f, totalLength - errLength);

		if (errLength > 0)
			{
			f.style.color = colormap->GetRedColor();
			styles->AppendElements(f, errLength);
			}
		}

	*redrawRange += JIndexRange(1, totalLength);
}

/******************************************************************************
 GetTextColor (static)

	Draw the entire text red if the file is invalid.  This is provided
	so tables can draw the text the same way as the input field.

	base can be NULL.  If you use JXFileInput for relative paths, basePath
	should be the path passed to SetBasePath().

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

JColorIndex
JXFileInput::GetTextColor
	(
	const JCharacter*	fileName,
	const JCharacter*	basePath,
	const JBoolean		requireRead,
	const JBoolean		requireWrite,
	const JBoolean		requireExec,
	const JColormap*	colormap
	)
{
	JString fullName;
	if (JConvertToAbsolutePath(fileName, basePath, &fullName) &&
		(!requireRead  || JFileReadable(fullName)) &&
		(!requireWrite || JFileWritable(fullName)) &&
		(!requireExec  || JFileExecutable(fullName)))
		{
		return colormap->GetBlackColor();
		}
	else
		{
		return colormap->GetRedColor();
		}
}

/******************************************************************************
 WillAcceptDrop (virtual protected)

	Accept text/uri-list.

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

JBoolean
JXFileInput::WillAcceptDrop
	(
	const JArray<Atom>&	typeList,
	Atom*				action,
	const Time			time,
	const JXWidget*		source
	)
{
	itsExpectURLDropFlag = kFalse;

	const Atom urlXAtom = (GetSelectionManager())->GetURLXAtom();

	JString fileName;
	const JSize typeCount = typeList.GetElementCount();
	for (JIndex i=1; i<=typeCount; i++)
		{
		if (typeList.GetElement(i) == urlXAtom &&
			GetDroppedFileName(time, &fileName))
			{
			*action = (GetDNDManager())->GetDNDActionPrivateXAtom();
			itsExpectURLDropFlag = kTrue;
			return kTrue;
			}
		}

	return JXInputField::WillAcceptDrop(typeList, action, time, source);
}

/******************************************************************************
 HandleDNDEnter (virtual protected)

	This is called when the mouse enters the widget.

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

void
JXFileInput::HandleDNDEnter()
{
	if (!itsExpectURLDropFlag)
		{
		JXInputField::HandleDNDEnter();
		}
}

/******************************************************************************
 HandleDNDHere (virtual protected)

	This is called while the mouse is inside the widget.

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

void
JXFileInput::HandleDNDHere
	(
	const JPoint&	pt,
	const JXWidget*	source
	)
{
	if (!itsExpectURLDropFlag)
		{
		JXInputField::HandleDNDHere(pt, source);
		}
}

/******************************************************************************
 HandleDNDLeave (virtual protected)

	This is called when the mouse leaves the widget without dropping data.

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

void
JXFileInput::HandleDNDLeave()
{
	if (!itsExpectURLDropFlag)
		{
		JXInputField::HandleDNDLeave();
		}
}

/******************************************************************************
 HandleDNDDrop (virtual protected)

	This is called when the data is dropped.  The data is accessed via
	the selection manager, just like Paste.

	Since we only accept text/uri-list, we don't bother to check typeList.

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

void
JXFileInput::HandleDNDDrop
	(
	const JPoint&		pt,
	const JArray<Atom>&	typeList,
	const Atom			action,
	const Time			time,
	const JXWidget*		source
	)
{
	JString fileName;
	if (!itsExpectURLDropFlag)
		{
		JXInputField::HandleDNDDrop(pt, typeList, action, time, source);
		}
	else if (Focus() && GetDroppedFileName(time, &fileName))
		{
		SetText(fileName);
		}
}

/******************************************************************************
 GetDroppedFileName (private)

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

JBoolean
JXFileInput::GetDroppedFileName
	(
	const Time	time,
	JString*	fileName
	)
{
	fileName->Clear();

	JXSelectionManager* selMgr = GetSelectionManager();

	Atom returnType;
	unsigned char* data;
	JSize dataLength;
	JXSelectionManager::DeleteMethod delMethod;
	if (selMgr->GetSelectionData((GetDNDManager())->GetDNDSelectionName(),
								 time, GetWindow(), selMgr->GetURLXAtom(),
								 &returnType, &data, &dataLength, &delMethod))
		{
		if (returnType == selMgr->GetURLXAtom())
			{
			JPtrArray<JString> fileNameList, urlList;
			JXUnpackFileNames((char*) data, dataLength, &fileNameList, &urlList);
			if (fileNameList.GetElementCount() == 1 &&
				JFileExists(*(fileNameList.FirstElement())))
				{
				*fileName = *(fileNameList.FirstElement());
				}
			fileNameList.DeleteAll();
			urlList.DeleteAll();
			}

		selMgr->DeleteSelectionData(&data, delMethod);
		}

	return JNegate( fileName->IsEmpty() );
}
