/******************************************************************************
 jxlayout.cc

	Program to generate JX code from an fdesign .fd file.

	Copyright © 1996-99 by John Lindal. All rights reserved.

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

#include <jXGlobals.h>
#include <JPtrArray.h>
#include <JString.h>
#include <jFStreamUtil.h>
#include <jStreamUtil.h>
#include <jFileUtil.h>
#include <jDirUtil.h>
#include <jCommandLine.h>
#include <JRect.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <jAssert.h>

// Constants

static const JCharacter* kVersionStr =
	"jxlayout 1.0.7\n"
	"\n"
	"Copyright 1996-99 by John Lindal\n"
	"This program may be freely distributed at no charge.";

static const JCharacter* kBackupSuffix = "~";

static const JCharacter* kBeginCodeDelimiterPrefix = "// begin ";
static const JCharacter* kEndCodeDelimiterPrefix   = "// end ";

static const JCharacter* kDefaultDelimTag = "JXLayout";
static const JCharacter* kCustomTagMarker = "--";
const JSize kCustomTagMarkerLength        = strlen(kCustomTagMarker);

static const JCharacter* kDefTopEnclVarName = "window";

// Data files (search for at startup)

static const JCharacter* kMainConfigFileDir = "/usr/lib/jx/jxlayout/";

static const JCharacter* kEnvUserConfigFileDir = "JXLAYOUTDIR";

static JString classMapFile     = "class_map";
static JString optionMapFile    = "option_map";
static JString needFontListFile = "need_font_list";

// Form fields

static const JCharacter* kBeginFormLine      = "=============== FORM ===============";
static const JCharacter* kFormNameMarker     = "Name:";
static const JCharacter* kFormWidthMarker    = "Width:";
static const JCharacter* kFormHeightMarker   = "Height:";
static const JCharacter* kFormObjCountMarker = "Number of Objects:";

// Object fields

static const JCharacter* kBeginObjLine      = "--------------------";
static const JCharacter* kObjClassMarker    = "class:";
static const JCharacter* kObjTypeMarker     = "type:";
static const JCharacter* kObjRectMarker     = "box:";
static const JCharacter* kObjBoxTypeMarker  = "boxtype:";
static const JCharacter* kObjColorsMarker   = "colors:";
static const JCharacter* kObjLAlignMarker   = "alignment:";
static const JCharacter* kObjLStyleMarker   = "style:";
static const JCharacter* kObjLSizeMarker    = "size:";
static const JCharacter* kObjLColorMarker   = "lcol:";
static const JCharacter* kObjLabelMarker    = "label:";
static const JCharacter* kObjShortcutMarker = "shortcut:";
static const JCharacter* kObjResizeMarker   = "resize:";
static const JCharacter* kObjGravityMarker  = "gravity:";
static const JCharacter* kObjNameMarker     = "name:";
static const JCharacter* kObjCallbackMarker = "callback:";
static const JCharacter* kObjCBArgMarker    = "argument:";

// Options for each object

const JSize kOptionCount = 3;

enum
{
	kShortcutsIndex = 1,
	kCol1Index,
	kCol2Index
};

// font size conversion

struct FontSizeConversion
{
	const JCharacter* flName;
	const JCharacter* jxName;
};

static const FontSizeConversion kFontSizeTable[] =
{
	{"FL_DEFAULT_SIZE", "10"},
	{"FL_TINY_SIZE",    "8"},
	{"FL_SMALL_SIZE",   "10"},
	{"FL_NORMAL_SIZE",  "kJDefaultFontSize"},
	{"FL_MEDIUM_SIZE",  "14"},
	{"FL_LARGE_SIZE",   "18"},
	{"FL_HUGE_SIZE",    "24"}
};

const JSize kFontSizeTableSize = sizeof(kFontSizeTable)/sizeof(FontSizeConversion);

// color conversion

struct ColorConversion
{
	const JCharacter* flName;
	const JCharacter* jxName;
};

static const ColorConversion kColorTable[] =
{
	{"FL_BLACK",        "(GetColormap())->GetBlackColor()"},
	{"FL_RED",          "(GetColormap())->GetRedColor()"},
	{"FL_GREEN",        "(GetColormap())->GetGreenColor()"},
	{"FL_YELLOW",       "(GetColormap())->GetYellowColor()"},
	{"FL_BLUE",         "(GetColormap())->GetBlueColor()"},
	{"FL_MAGENTA",      "(GetColormap())->GetMagentaColor()"},
	{"FL_CYAN",         "(GetColormap())->GetCyanColor()"},
	{"FL_WHITE",        "(GetColormap())->GetWhiteColor()"},
	{"FL_LCOL",         "(GetColormap())->GetBlackColor()"},
	{"FL_COL1",         "(GetColormap())->GetDefaultBackColor()"},
	{"FL_MCOL",         "(GetColormap())->GetDefaultFocusColor()"},
	{"FL_RIGHT_BCOL",   "(GetColormap())->Get3DShadeColor()"},
	{"FL_BOTTOM_BCOL",  "(GetColormap())->Get3DShadeColor()"},
	{"FL_TOP_BCOL",     "(GetColormap())->Get3DLightColor()"},
	{"FL_LEFT_BCOL",    "(GetColormap())->Get3DLightColor()"},
	{"FL_INACTIVE",     "(GetColormap())->GetInactiveLabelColor()"},
	{"FL_INACTIVE_COL", "(GetColormap())->GetInactiveLabelColor()"}
};

const JSize kColorTableSize = sizeof(kColorTable)/sizeof(ColorConversion);

// Prototypes

void GenerateForm(istream& input, const JString& formName,
				  const JString& tagName, const JString& windArgs,
				  const JString& enclName,
				  const JString& codePath, const JString& codeSuffix,
				  const JString& headerSuffix, JPtrArray<JString>* backupList);
JBoolean ShouldGenerateForm(const JString& form, const JPtrArray<JString>& list);
JBoolean ShouldBackupForm(const JString& form, JPtrArray<JString>* list);
void GenerateCode(istream& input, ostream& output,
				  const JString& tagName, const JString& windowConstrArgs,
				  const JString& userTopEnclVarName,
				  JPtrArray<JString>* objTypes, JPtrArray<JString>* objNames);
void GenerateHeader(ostream& output, const JPtrArray<JString>& objTypes,
					const JPtrArray<JString>& objNames);

JBoolean ParseGravity(const JString& nwGravity, const JString& seGravity,
					  JString* hSizing, JString* vSizing);
void GetTempName(JString* name, const JPtrArray<JString>& objNames);
JBoolean GetEnclosure(const JArray<JRect>& rectList, const JIndex rectIndex, JIndex* enclIndex);
JBoolean GetJXClassName(const JString& className, const JString& type,
						  JString* label, JString* jxClassName);
void ApplyOptions(ostream& output, const JString& jxClassName, const JString& name,
				  const JPtrArray<JString>& values, const JString& flSize,
				  const JString& flStyle, const JString& flColor);
JBoolean AcceptsFontSpec(const JString& jxClassName);
JBoolean ConvertXFormsFontSize(const JString& flSize, JString* jxSize);
JBoolean ConvertXFormsColor(const JString& flColor, JString* jxColor);

JBoolean CopyBeforeCodeDelimiter(const JString& tag, istream& input, ostream& output);
JBoolean CopyAfterCodeDelimiter(const JString& tag, istream& input, ostream& output);
void RemoveIdentifier(const JCharacter* id, JString* line);

void GetOptions(int argc, char* argv[], JString* inputName, JString* codePath,
				JString* codeSuffix, JString* headerSuffix, JPtrArray<JString>* userFormList);
void PickForms(const JString& fileName, JPtrArray<JString>* list);
JBoolean FindConfigFile(JString* configFileName);

void PrintHelp(const JString& codeSuffix, const JString& headerSuffix);
void PrintVersion();

/******************************************************************************
 main

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

int
main
	(
	int		argc,
	char*	argv[]
	)
{
	// find the configuration files

	if (!FindConfigFile(&classMapFile) || !FindConfigFile(&optionMapFile) ||
		!FindConfigFile(&needFontListFile))
		{
		return 1;
		}

	// parse the command line options

	JString inputName, codePath, codeSuffix, headerSuffix;
	JPtrArray<JString> userFormList;		// empty => generate all forms
	JPtrArray<JString> backupList;			// forms that have been backed up

	GetOptions(argc, argv, &inputName, &codePath,
			   &codeSuffix, &headerSuffix, &userFormList);

	// generate each requested form

	ifstream input(inputName);
	while (!input.eof() && !input.fail())
		{
		const JString line = JReadLine(input);
		if (line == kBeginFormLine)
			{
			// get form name

			JString formName = JReadLine(input);
			RemoveIdentifier(kFormNameMarker, &formName);

			// look for custom tag

			const JSize formNameLength = formName.GetLength();
			JString tagName = kDefaultDelimTag;
			JString windArgs, enclName;
			JIndex tagMarkerIndex;
			if (formName.LocateSubstring(kCustomTagMarker, &tagMarkerIndex) &&
				tagMarkerIndex <= formNameLength - kCustomTagMarkerLength)
				{
				tagName = formName.GetSubstring(
					tagMarkerIndex + kCustomTagMarkerLength, formNameLength);
				formName.RemoveSubstring(tagMarkerIndex, formNameLength);

				// get enclosure name

				const JSize tagNameLength = tagName.GetLength();
				JIndex enclMarkerIndex;
				if (tagName.LocateSubstring(kCustomTagMarker, &enclMarkerIndex) &&
					enclMarkerIndex <= tagNameLength - kCustomTagMarkerLength)
					{
					enclName = tagName.GetSubstring(
						enclMarkerIndex + kCustomTagMarkerLength, tagNameLength);
					tagName.RemoveSubstring(enclMarkerIndex, tagNameLength);
					}

				// get window constructor arguments

				JIndex windArgsIndex;
				if (tagName.LocateSubstring("<", &windArgsIndex) &&
					tagName.GetLastCharacter() == '>')
					{
					const JSize tagLength = tagName.GetLength();
					if (windArgsIndex < tagLength-1)
						{
						windArgs = tagName.GetSubstring(windArgsIndex+1, tagLength-1);
						}
					tagName.RemoveSubstring(windArgsIndex, tagLength);
					}

				// report errors

				if (tagName != kDefaultDelimTag)
					{
					if (enclName.IsEmpty())
						{
						cerr << formName << ", " << tagName;
						cerr << ": no enclosure specified" << endl;
						}
					else if (!windArgs.IsEmpty())
						{
						cerr << formName << ", " << tagName;
						cerr << ": no window to which to send \"" << windArgs << '"' << endl;
						}
					}
				else if (!enclName.IsEmpty() && enclName != kDefTopEnclVarName)
					{
					cerr << formName << ", " << tagName;
					cerr << ": not allowed to specify enclosure other than ";
					cerr << kDefTopEnclVarName << endl;
					}
				}

			if (ShouldGenerateForm(formName, userFormList))
				{
				GenerateForm(input, formName, tagName, windArgs, enclName,
							 codePath, codeSuffix, headerSuffix, &backupList);
				}
			}
		}

	return 0;
}

/******************************************************************************
 ShouldGenerateForm

	Returns kTrue if form is in the list.  If the list is empty, it means
	"make all forms" so we just return kTrue.

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

JBoolean
ShouldGenerateForm
	(
	const JString&				form,
	const JPtrArray<JString>&	list
	)
{
	if (list.IsEmpty())
		{
		return kTrue;
		}

	const JSize count = list.GetElementCount();
	for (JIndex i=1; i<=count; i++)
		{
		if (form == *(list.NthElement(i)))
			{
			return kTrue;
			}
		}
	return kFalse;
}

/******************************************************************************
 GenerateForm

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

void
GenerateForm
	(
	istream&			input,
	const JString&		formName,
	const JString&		tagName,
	const JString&		windArgs,
	const JString&		enclName,
	const JString&		codePath,
	const JString&		codeSuffix,
	const JString&		headerSuffix,
	JPtrArray<JString>*	backupList
	)
{
	const JString codeFileName    = codePath + formName + codeSuffix;
	const JString codeFileBakName = codeFileName + kBackupSuffix;

	const JString headerFileName    = codePath + formName + headerSuffix;
	const JString headerFileBakName = headerFileName + kBackupSuffix;

	if (!JFileExists(codeFileName))
		{
		cerr << codeFileName << " not found" << endl;
		return;
		}
	if (!JFileExists(headerFileName))
		{
		cerr << headerFileName << " not found" << endl;
		return;
		}

	cout << "Generating: " << formName << ", " << tagName << endl;

	const JBoolean shouldBackup = ShouldBackupForm(formName, backupList);

	// copy code file contents before start delimiter

	const JString tempCodeFileName = JGetTempFileName(codePath);
	ifstream origCode(codeFileName);
	ofstream outputCode(tempCodeFileName);
	if (!outputCode.good())
		{
		cerr << "Unable to open temporary file in " << codePath << endl;
		return;
		}
	if (!CopyBeforeCodeDelimiter(tagName, origCode, outputCode))
		{
		cerr << "No starting delimiter in " << codeFileName << endl;
		outputCode.close();
		remove(tempCodeFileName);
		return;
		}

	// generate code for each object in the form

	JPtrArray<JString> objTypes, objNames;
	GenerateCode(input, outputCode, tagName, windArgs, enclName, &objTypes, &objNames);

	// copy code file contents after end delimiter

	JBoolean done = CopyAfterCodeDelimiter(tagName, origCode, outputCode);
	origCode.close();
	outputCode.close();

	if (!done)
		{
		cerr << "No ending delimiter in " << codeFileName << endl;
		remove(tempCodeFileName);
		objTypes.DeleteAll();
		objNames.DeleteAll();
		return;
		}
	else if (shouldBackup && rename(codeFileName, codeFileBakName) != 0)
		{
		cerr << "Unable to rename original " << codeFileName << endl;
		remove(tempCodeFileName);
		objTypes.DeleteAll();
		objNames.DeleteAll();
		return;
		}
	rename(tempCodeFileName, codeFileName);

	// copy header file contents before start delimiter

	const JString tempHeaderFileName = JGetTempFileName(codePath);
	ifstream origHeader(headerFileName);
	ofstream outputHeader(tempHeaderFileName);
	if (!outputHeader.good())
		{
		cerr << "Unable to open temporary file in " << codePath << endl;
		objTypes.DeleteAll();
		objNames.DeleteAll();
		return;
		}
	if (!CopyBeforeCodeDelimiter(tagName, origHeader, outputHeader))
		{
		cerr << "No starting delimiter in " << headerFileName << endl;
		outputHeader.close();
		remove(tempHeaderFileName);
		objTypes.DeleteAll();
		objNames.DeleteAll();
		return;
		}

	// generate instance variable for each object in the form

	GenerateHeader(outputHeader, objTypes, objNames);

	// copy header file contents after end delimiter

	done = CopyAfterCodeDelimiter(tagName, origHeader, outputHeader);
	origHeader.close();
	outputHeader.close();

	if (!done)
		{
		cerr << "No ending delimiter in " << headerFileName << endl;
		remove(tempHeaderFileName);
		objTypes.DeleteAll();
		objNames.DeleteAll();
		return;
		}

	// check if header file actually changed

	JString origHeaderText, newHeaderText;
	JReadFile(headerFileName, &origHeaderText);
	JReadFile(tempHeaderFileName, &newHeaderText);
	if (newHeaderText != origHeaderText)
		{
		if (shouldBackup && rename(headerFileName, headerFileBakName) != 0)
			{
			cerr << "Unable to rename original " << headerFileName << endl;
			remove(tempHeaderFileName);
			objTypes.DeleteAll();
			objNames.DeleteAll();
			return;
			}
		rename(tempHeaderFileName, headerFileName);
		}
	else
		{
		remove(tempHeaderFileName);
		}

	// clean up

	objTypes.DeleteAll();
	objNames.DeleteAll();
}

/******************************************************************************
 ShouldBackupForm

	If form is not in the list, adds it and returns kTrue.

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

JBoolean
ShouldBackupForm
	(
	const JString&		form,
	JPtrArray<JString>*	list
	)
{
	const JSize count = list->GetElementCount();
	for (JIndex i=1; i<=count; i++)
		{
		if (form == *(list->NthElement(i)))
			{
			return kFalse;
			}
		}

	// falling through means that the form hasn't been backed up yet

	JString* newForm = new JString(form);
	assert( newForm != NULL );
	list->Append(newForm);
	return kTrue;
}

/******************************************************************************
 GenerateCode

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

void
GenerateCode
	(
	istream&			input,
	ostream&			output,
	const JString&		tagName,
	const JString&		windowConstrArgs,
	const JString&		userTopEnclVarName,
	JPtrArray<JString>*	objTypes,
	JPtrArray<JString>*	objNames
	)
{
JIndex i;

	// width

	input >> ws;
	JString line = JReadUntilws(input);
	assert( line == kFormWidthMarker );
	JSize formWidth;
	input >> formWidth;

	// height

	input >> ws;
	line = JReadUntilws(input);
	assert( line == kFormHeightMarker );
	JSize formHeight;
	input >> formHeight;

	// object count (marker contains whitespace)

	input >> ws;
	line = JReadUntil(input, ':') + ":";
	assert( line == kFormObjCountMarker );
	JSize itemCount;
	input >> itemCount;

	// create window

	JString topEnclVarName;
	if (tagName == kDefaultDelimTag)
		{
		topEnclVarName = kDefTopEnclVarName;

		output << "    JXWindow* window = new JXWindow(this, ";
		output << formWidth << ',' << formHeight;
		if (!windowConstrArgs.IsEmpty())
			{
			output << ", ";
			windowConstrArgs.Print(output);
			}
		else
			{
			output << ", \"\"";
			}
		output << ");" << endl;
		output << "    assert( window != NULL );" << endl;
		output << "    SetWindow(window);" << endl;
		output << endl;
		}
	else
		{
		assert( !userTopEnclVarName.IsEmpty() );
		topEnclVarName = userTopEnclVarName;
		}

	// We need to calculate the enclosure for each object.  Since objects
	// are drawn in the order added, an object must come after its enclosure
	// in the list in order to be visible.

	JArray<JRect>    rectList(10);
	JArray<JBoolean> isInstanceVar(10);

	// This array is used to send the options to ApplyOptions.
	// It does not own the pointers that it contains.

	JPtrArray<JString> optionValues(kOptionCount);
	for (i=1; i<=kOptionCount; i++)
		{
		optionValues.Append(NULL);
		}

	// generate code for each object

	JIndex objCount = 1;
	for (i=1; i<=itemCount; i++)
		{
		// check for start-of-object

		input >> ws;
		line = JReadLine(input);
		assert( line == kBeginObjLine );

		// object class

		JString className = JReadLine(input);
		RemoveIdentifier(kObjClassMarker, &className);

		// object type

		JString type = JReadLine(input);
		RemoveIdentifier(kObjTypeMarker, &type);

		// object frame

		input >> ws;
		line = JReadUntilws(input);
		assert( line == kObjRectMarker );
		JCoordinate x,y,w,h;
		input >> x >> y >> w >> h >> ws;
		const JRect frame(y, x, y+h, x+w);
		rectList.AppendElement(frame);

		// box type

		JString boxType = JReadLine(input);
		RemoveIdentifier(kObjBoxTypeMarker, &boxType);

		// colors

		input >> ws;
		line = JReadUntilws(input);
		assert( line == kObjColorsMarker );
		JString col1 = JReadUntilws(input);
		optionValues.SetElement(kCol1Index, &col1);
		JString col2 = JReadUntilws(input);
		optionValues.SetElement(kCol2Index, &col2);

		// label info

		JString lAlign = JReadLine(input);
		RemoveIdentifier(kObjLAlignMarker, &lAlign);
		JString lStyle = JReadLine(input);
		RemoveIdentifier(kObjLStyleMarker, &lStyle);
		JString lSize  = JReadLine(input);
		RemoveIdentifier(kObjLSizeMarker, &lSize);
		JString lColor = JReadLine(input);
		RemoveIdentifier(kObjLColorMarker, &lColor);
		JString label  = JReadLine(input);
		RemoveIdentifier(kObjLabelMarker, &label);

		// shortcuts

		JString shortcuts = JReadLine(input);
		RemoveIdentifier(kObjShortcutMarker, &shortcuts);
		optionValues.SetElement(kShortcutsIndex, &shortcuts);

		// resizing (ignored)

		JIgnoreUntil(input, '\n');

		// gravity

		input >> ws;
		line = JReadUntilws(input);
		assert( line == kObjGravityMarker );
		const JString nwGravity = JReadUntilws(input);
		const JString seGravity = JReadUntilws(input);

		// name

		JBoolean isLocal = kFalse;
		JString* name = new JString(JReadLine(input));
		assert( name != NULL );
		RemoveIdentifier(kObjNameMarker, name);
		if (name->IsEmpty())
			{
			isInstanceVar.AppendElement(kFalse);
			GetTempName(name, *objNames);
			isLocal = kTrue;
			}
		else if (name->GetFirstCharacter() == '(' &&
				 name->GetLastCharacter()  == ')')
			{
			isInstanceVar.AppendElement(kFalse);
			isLocal = kTrue;
			*name = name->GetSubstring(2, name->GetLength()-1);
			}
		else if (name->GetFirstCharacter() == '<' &&
				 name->GetLastCharacter()  == '>')
			{
			isInstanceVar.AppendElement(kFalse);
			*name = name->GetSubstring(2, name->GetLength()-1);
			}
		else
			{
			isInstanceVar.AppendElement(kTrue);
			}
		objNames->Append(name);

		// callback (ignored)

		JIgnoreUntil(input, '\n');

		// callback argument

		JString cbArg = JReadLine(input);
		RemoveIdentifier(kObjCBArgMarker, &cbArg);

		// don't bother to generate code for initial box
		// if it is FL_BOX, FLAT_BOX, FL_COL1

		if (i==1 && className == "FL_BOX" && type == "FLAT_BOX" && col1 == "FL_COL1")
			{
			rectList.RemoveElement(objCount);
			isInstanceVar.RemoveElement(objCount);
			objNames->DeleteElement(objCount);
			continue;
			}

		// check for errors -- safe since we have read in entire object

		JString hSizing, vSizing;
		if (!ParseGravity(nwGravity, seGravity, &hSizing, &vSizing))
			{
			cerr << "Illegal sizing specification ";
			cerr << nwGravity << ',' << seGravity;
			cerr << " for '" << *name << '\'' << endl;
			rectList.RemoveElement(objCount);
			isInstanceVar.RemoveElement(objCount);
			objNames->DeleteElement(objCount);
			continue;
			}

		if (*name == topEnclVarName)
			{
			cerr << "Cannot use reserved name '" << topEnclVarName << '\'' << endl;
			rectList.RemoveElement(objCount);
			isInstanceVar.RemoveElement(objCount);
			objNames->DeleteElement(objCount);
			continue;
			}

		// get the objects' enclosure

		JIndex enclIndex;
		JString enclName;
		JRect localFrame = frame;
		if (GetEnclosure(rectList, objCount, &enclIndex))
			{
			enclName = *(objNames->NthElement(enclIndex));
			const JRect enclFrame = rectList.GetElement(enclIndex);
			localFrame.Shift(-enclFrame.topLeft());
			}
		else
			{
			enclName = topEnclVarName;
			}

		// generate the class name

		JString jxClassName;
		if (!GetJXClassName(className, type, &label, &jxClassName))
			{
			cerr << "Unsupported class: " << className << ", " << type << endl;
			rectList.RemoveElement(objCount);
			isInstanceVar.RemoveElement(objCount);
			objNames->DeleteElement(objCount);
			continue;
			}

		JIndex parenIndex;
		const JBoolean found = jxClassName.LocateSubstring("(", &parenIndex);
		assert( found && parenIndex > 1 );
		JString* objType = new JString(jxClassName.GetSubstring(1, parenIndex-1));
		assert( objType != NULL );
		objTypes->Append(objType);

		// generate the actual code

		output << "    ";
		if (isLocal)
			{
			objType->Print(output);
			output << "* ";
			}
		name->Print(output);
		output << " =" << endl;
		output << "        new ";
		jxClassName.Print(output);

		if (jxClassName == "JXTextRadioButton(" || jxClassName == "JXImageRadioButton(")
			{
			cbArg.Print(output);
			output << ", ";
			}

		if (jxClassName == "JXStaticText(" || jxClassName == "JXTextButton(" ||
			jxClassName == "JXTextCheckbox(" || jxClassName == "JXTextRadioButton(" ||
			jxClassName == "JXTextMenu(")
			{
			output << '\"';
			label.Print(output);
			output << "\", ";
			}

		enclName.Print(output);
		output << ',' << endl;
		output << "                    JXWidget::";
		hSizing.Print(output);
		output << ", JXWidget::";
		vSizing.Print(output);
		output << ", " << localFrame.left << ',' << localFrame.top << ", ";
		output << localFrame.width() << ',' << localFrame.height() << ");" << endl;

		output << "    assert( ";
		name->Print(output);
		output << " != NULL );" << endl;

		ApplyOptions(output, jxClassName, *name, optionValues,
					 lSize, lStyle, lColor);
		output << endl;

		// now we know the object is valid

		objCount++;
		}

	// throw away temporary variables

	objCount--;
	assert( objCount == isInstanceVar.GetElementCount() );
	assert( objCount == objTypes->GetElementCount() );
	assert( objCount == objNames->GetElementCount() );
	for (i=objCount; i>=1; i--)
		{
		if (!isInstanceVar.GetElement(i))
			{
			objTypes->DeleteElement(i);
			objNames->DeleteElement(i);
			}
		}
}

/******************************************************************************
 GenerateHeader

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

void
GenerateHeader
	(
	ostream&					output,
	const JPtrArray<JString>&	objTypes,
	const JPtrArray<JString>&	objNames
	)
{
	JIndex i;
	const JSize count = objTypes.GetElementCount();
	assert( count == objNames.GetElementCount() );

	// get width of longest type

	JSize maxLen = 0;
	for (i=1; i<=count; i++)
		{
		const JString* type = objTypes.NthElement(i);
		const JSize len     = type->GetLength();
		if (len > maxLen)
			{
			maxLen = len;
			}
		}

	// declare each object

	for (i=1; i<=count; i++)
		{
		output << "    ";
		const JString* type = objTypes.NthElement(i);
		type->Print(output);
		output << '*';
		const JSize len = type->GetLength();
		for (JIndex j=len+1; j<=maxLen+1; j++)
			{
			output << ' ';
			}
		(objNames.NthElement(i))->Print(output);
		output << ';' << endl;
		}

	// need blank line to conform to expectations of CopyAfterCodeDelimiter

	output << endl;
}

/******************************************************************************
 ParseGravity

	Convert X gravity values into JXWidget sizing values.
	Returns kFalse if user specified an invalid combination.

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

JBoolean
ParseGravity
	(
	const JString& nwGravity,
	const JString& seGravity,

	JString* hSizing,
	JString* vSizing
	)
{
	if (nwGravity == "FL_NorthWest" && seGravity == "FL_NoGravity")
		{
		*hSizing = "kFixedLeft";
		*vSizing = "kFixedTop";
		}
	else if (nwGravity == "FL_West" && seGravity == "FL_South")
		{
		*hSizing = "kFixedLeft";
		*vSizing = "kFixedBottom";
		}
	else if (nwGravity == "FL_North" && seGravity == "FL_East")
		{
		*hSizing = "kFixedRight";
		*vSizing = "kFixedTop";
		}
	else if (nwGravity == "FL_NoGravity" && seGravity == "FL_SouthEast")
		{
		*hSizing = "kFixedRight";
		*vSizing = "kFixedBottom";
		}

	else if (nwGravity == "FL_West" && seGravity == "FL_NoGravity")
		{
		*hSizing = "kFixedLeft";
		*vSizing = "kVElastic";
		}
	else if (nwGravity == "FL_NoGravity" && seGravity == "FL_East")
		{
		*hSizing = "kFixedRight";
		*vSizing = "kVElastic";
		}

	else if (nwGravity == "FL_North" && seGravity == "FL_NoGravity")
		{
		*hSizing = "kHElastic";
		*vSizing = "kFixedTop";
		}
	else if (nwGravity == "FL_NoGravity" && seGravity == "FL_South")
		{
		*hSizing = "kHElastic";
		*vSizing = "kFixedBottom";
		}

	else if (nwGravity == "FL_NoGravity" && seGravity == "FL_NoGravity")
		{
		*hSizing = "kHElastic";
		*vSizing = "kVElastic";
		}

	else
		{
		return kFalse;
		}

	// getting here means that one of the 9 valid combinations was found

	return kTrue;
}

/******************************************************************************
 GetTempName

	Return a name that is not in the given list.

	We ignore the possibility of not finding a valid name because the
	code we are writing will run out of memory long before we run out
	of possibilities.

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

void
GetTempName
	(
	JString*					name,
	const JPtrArray<JString>&	objNames
	)
{
	const JString prefix = "obj";
	const JSize count    = objNames.GetElementCount();
	for (JIndex i=1; i<=INT_MAX; i++)
		{
		*name = prefix + JString(i);
		JBoolean unique = kTrue;
		for (JIndex j=1; j<=count; j++)
			{
			const JString* usedName = objNames.NthElement(j);
			if (*name == *usedName)
				{
				unique = kFalse;
				break;
				}
			}
		if (unique)
			{
			break;
			}
		}
}

/******************************************************************************
 GetEnclosure

	Returns kTrue if it finds a rectangle that encloses the rectangle
	at the specified index.  If it finds more than one enclosing rectangle,
	it returns the smallest one.

	If no enclosure is found, returns kFalse, and sets *enclIndex to zero.

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

JBoolean
GetEnclosure
	(
	const JArray<JRect>&	rectList,
	const JIndex			rectIndex,
	JIndex*					enclIndex
	)
{
	const JRect theRect = rectList.GetElement(rectIndex);
	JBoolean found = kFalse;
	*enclIndex = 0;

	JSize minArea = 0;
	const JSize count = rectList.GetElementCount();
	for (JIndex i=1; i<=count; i++)
		{
		if (i != rectIndex)
			{
			const JRect r = rectList.GetElement(i);
			const JSize a = r.area();
			if (r.Contains(theRect) && (a < minArea || minArea == 0))
				{
				minArea    = a;
				found      = kTrue;
				*enclIndex = i;
				}
			}
		}
	return found;
}

/******************************************************************************
 GetJXClassName

	Convert the XForms type into an JX class name.
	Returns kTrue if successful.

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

JBoolean
GetJXClassName
	(
	const JString&	className,
	const JString&	type,
	JString*		label,
	JString*		jxClassName
	)
{
	if (className == "FL_BOX" && type == "NO_BOX" && !label->IsEmpty())
		{
		*jxClassName = *label;
		label->Clear();
		if (!jxClassName->Contains("("))
			{
			jxClassName->Append("(");
			}
		else if (jxClassName->GetLastCharacter() != ' ')
			{
			jxClassName->Append(" ");
			}
		return kTrue;
		}

	ifstream classMap(classMapFile);
	classMap >> ws;
	while (1)
		{
		if (classMap.peek() == '#')
			{
			JIgnoreUntil(classMap, '\n');
			}
		else
			{
			const JString aClassName = JReadUntilws(classMap);
			if (classMap.eof() || classMap.fail())
				{
				break;
				}

			const JString aType = JReadUntilws(classMap);
			if (aClassName == className && (aType == "*" || aType == type))
				{
				*jxClassName = JReadUntilws(classMap);
				jxClassName->Append("(");
				return kTrue;
				}
			else
				{
				JIgnoreUntil(classMap, '\n');
				}
			}
		classMap >> ws;
		}

	// falling through means that nothing matched

	return kFalse;
}

/******************************************************************************
 ApplyOptions

	Apply options to the specified object in an intelligent way.
	(i.e. If the value is the default value, don't explicitly set it.)

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

void
ApplyOptions
	(
	ostream&					output,
	const JString&				jxClassName,
	const JString&				name,
	const JPtrArray<JString>&	values,
	const JString&				flSize,
	const JString&				flStyle,
	const JString&				flColor
	)
{
	ifstream optionMap(optionMapFile);
	optionMap >> ws;
	while (1)
		{
		if (optionMap.peek() == '#')
			{
			JIgnoreUntil(optionMap, '\n');
			}
		else
			{
			const JString aClassName = JReadUntilws(optionMap);
			if (optionMap.eof() || optionMap.fail())
				{
				break;
				}
			else if (aClassName != jxClassName)
				{
				for (JIndex i=1; i<=kOptionCount; i++)
					{
					JIgnoreUntil(optionMap, '\n');
					}
				}
			else
				{
				JIndex i;
				JBoolean supported;

				// shortcuts

				optionMap >> ws >> supported;
				if (supported)
					{
					optionMap >> ws;
					const JString function = JReadUntilws(optionMap);
					const JString* value   = values.NthElement(kShortcutsIndex);
					if (!value->IsEmpty())
						{
						output << "    ";
						name.Print(output);
						output << "->";
						function.Print(output);
						output << "(\"";
						value->Print(output);
						output << "\");" << endl;
						}
					}
				else
					{
					JIgnoreUntil(optionMap, '\n');
					}

				// colors

				for (i=2; i<=kOptionCount; i++)
					{
					optionMap >> ws >> supported;
					if (supported)
						{
						optionMap >> ws;
						const JString defValue = JReadUntilws(optionMap);
						const JString function = JReadUntilws(optionMap);
						const JString* value   = values.NthElement(i);
						if (*value != defValue)
							{
							JString jxColor;
							if (ConvertXFormsColor(*value, &jxColor))
								{
								output << "    ";
								name.Print(output);
								output << "->";
								function.Print(output);
								output << '(';
								jxColor.Print(output);
								output << ");" << endl;
								}
							else
								{
								cerr << "Unknown color: " << *value << endl;
								}
							}
						}
					else
						{
						JIgnoreUntil(optionMap, '\n');
						}
					}
				}
			}
		optionMap >> ws;
		}

	// For some objects, we have to decode the XForms font spec.

	if (AcceptsFontSpec(jxClassName))
		{
		JString fontName = JGetDefaultFontName();
		if (flStyle.Contains("FIXED"))
			{
			output << "    ";
			name.Print(output);
			output << "->SetFontName(JXGetCourierFontName());" << endl;
			}
		else if (flStyle.Contains("TIMES"))
			{
			output << "    ";
			name.Print(output);
			output << "->SetFontName(JXGetTimesFontName());" << endl;
			}

		if (flSize != "FL_NORMAL_SIZE")
			{
			JString jxSize;
			if (ConvertXFormsFontSize(flSize, &jxSize))
				{
				output << "    ";
				name.Print(output);
				output << "->SetFontSize(";
				jxSize.Print(output);
				output << ");" << endl;
				}
			else
				{
				cerr << "Unknown font size: " << flSize << endl;
				}
			}

		JFontStyle style;
		if (flStyle.Contains("BOLD"))
			{
			style.bold = kTrue;
			}
		if (flStyle.Contains("ITALIC"))
			{
			style.italic = kTrue;
			}
		if (style.bold || style.italic || flColor != "FL_BLACK")
			{
			JString jxColor;
			if (ConvertXFormsColor(flColor, &jxColor))
				{
				output << "    const JFontStyle ";
				name.Print(output);
				output << "_style(";

				if (style.bold)
					{
					output << "kTrue, ";
					}
				else
					{
					output << "kFalse, ";
					}

				if (style.italic)
					{
					output << "kTrue, ";
					}
				else
					{
					output << "kFalse, ";
					}

				output << "0, kFalse, ";
				jxColor.Print(output);
				output << ");" << endl;

				output << "    ";
				name.Print(output);
				output << "->SetFontStyle(";
				name.Print(output);
				output << "_style);" << endl;
				}
			else
				{
				cerr << "Unknown color: " << flColor << endl;
				}
			}
		}
}

/******************************************************************************
 AcceptsFontSpec

	Returns kTrue if the given class accepts SetFontName(), SetFontSize(),
	and SetFontStyle().

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

JBoolean
AcceptsFontSpec
	(
	const JString& jxClassName
	)
{
	ifstream needFontList(needFontListFile);
	needFontList >> ws;
	while (!needFontList.eof() && !needFontList.fail())
		{
		if (needFontList.peek() == '#')
			{
			JIgnoreUntil(needFontList, '\n');
			}
		else
			{
			const JString aClassName = JReadLine(needFontList);
			if (aClassName == jxClassName)
				{
				return kTrue;
				}
			}
		needFontList >> ws;
		}

	// falling through means that nothing matched

	return kFalse;
}

/******************************************************************************
 ConvertXFormsFontSize

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

JBoolean
ConvertXFormsFontSize
	(
	const JString&	flSize,
	JString*		jxSize
	)
{
	for (JIndex i=0; i<kFontSizeTableSize; i++)
		{
		if (kFontSizeTable[i].flName == flSize)
			{
			*jxSize = kFontSizeTable[i].jxName;
			return kTrue;
			}
		}

	return kFalse;
}

/******************************************************************************
 ConvertXFormsColor

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

JBoolean
ConvertXFormsColor
	(
	const JString&	flColor,
	JString*		jxColor
	)
{
	for (JIndex i=0; i<kColorTableSize; i++)
		{
		if (kColorTable[i].flName == flColor)
			{
			*jxColor = kColorTable[i].jxName;
			return kTrue;
			}
		}

	return kFalse;
}

/******************************************************************************
 CopyBeforeCodeDelimiter

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

JBoolean
CopyBeforeCodeDelimiter
	(
	const JString&	tag,
	istream&		input,
	ostream&		output
	)
{
	const JString delim = kBeginCodeDelimiterPrefix + tag;
	while (!input.eof() && !input.fail())
		{
		const JString line = JReadLine(input);
		line.Print(output);
		output << '\n';
		if (line == delim)
			{
			output << '\n';
			break;
			}
		}

	return JI2B( !input.eof() && !input.fail() );
}

/******************************************************************************
 CopyAfterCodeDelimiter

	Skips everything before end delimiter and then copies the rest.

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

JBoolean
CopyAfterCodeDelimiter
	(
	const JString&	tag,
	istream&		input,
	ostream&		output
	)
{
	const JString delim = kEndCodeDelimiterPrefix + tag;

	// skip lines before end delimiter

	while (!input.eof() && !input.fail())
		{
		const JString line = JReadLine(input);
		if (line == delim)
			{
			break;
			}
		}

	if (input.eof() || input.fail())
		{
		return kFalse;
		}

	// include end delimiter

	delim.Print(output);
	output << endl;

	// copy lines after end delimiter

	while (1)
		{
		const JString line = JReadLine(input);
		if ((input.eof() || input.fail()) && line.IsEmpty())
			{
			break;	// avoid creating extra empty lines
			}
		line.Print(output);
		output << endl;
		}

	return kTrue;
}

/******************************************************************************
 RemoveIdentifier

	Checks that the given id is at the start of the line, and then removes it.

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

void
RemoveIdentifier
	(
	const JCharacter*	id,
	JString*			line
	)
{
	const JSize lineLength = line->GetLength();
	const JSize idLength   = strlen(id);
	assert( lineLength > idLength && line->GetSubstring(1,idLength) == id );

	line->RemoveSubstring(1,idLength);
	line->TrimWhitespace();
}

/******************************************************************************
 GetOptions

	Modify the defaults based on the command line options.

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

void
GetOptions
	(
	int					argc,
	char*				argv[],
	JString*			inputName,
	JString*			codePath,
	JString*			codeSuffix,
	JString*			headerSuffix,
	JPtrArray<JString>*	userFormList
	)
{
	*inputName    = "";
	*codePath     = "";
	*codeSuffix   = ".cc";
	*headerSuffix = ".h";

	JBoolean pickForms = kFalse;
	long index = 1;
	while (index < argc)
		{
		if (strcmp(argv[index], "-h") == 0 ||
			strcmp(argv[index], "--help") == 0)
			{
			PrintHelp(*codeSuffix, *headerSuffix);
			exit(0);
			}
		else if (strcmp(argv[index], "-v") == 0 ||
				 strcmp(argv[index], "--version") == 0)
			{
			PrintVersion();
			exit(0);
			}

		else if (strcmp(argv[index], "-cp") == 0)
			{
			index++;
			JCheckForValues(1, index, argc, argv, "invalid code path");
			*codePath = argv[index];
			JAppendDirSeparator(codePath);
			}
		else if (strcmp(argv[index], "-cs") == 0)
			{
			index++;
			JCheckForValues(1, index, argc, argv, "invalid code suffix");
			*codeSuffix = argv[index];
			}
		else if (strcmp(argv[index], "-hs") == 0)
			{
			index++;
			JCheckForValues(1, index, argc, argv, "invalid header suffix");
			*headerSuffix = argv[index];
			}

		else if (strcmp(argv[index], "-c") == 0)
			{
			pickForms = kTrue;
			}

		else if (argv[index][0] == '-')
			{
			cerr << argv[0] << ": unknown command line option: " << argv[index] << endl;
			}

		else if (inputName->IsEmpty())
			{
			*inputName = argv[index];
			const JSize length = inputName->GetLength();
			if (!inputName->EndsWith(".fd"))
				{
				cerr << argv[0] << ": fdesign file not found: " << argv[index] << endl;
				exit(1);
				}
			}

		else
			{
			JString* userForm = new JString(argv[index]);
			assert( userForm != NULL );
			userFormList->Append(userForm);
			}

		index++;
		}

	if (inputName->IsEmpty())
		{
		cerr << argv[0] << ": fdesign input file not specified" << endl;
		exit(1);
		}
	if (!JFileExists(*inputName))
		{
		cerr << argv[0] << ": fdesign input file not found" << endl;
		exit(1);
		}

	// let the user pick forms to include

	if (pickForms)
		{
		PickForms(*inputName, userFormList);
		}
}

/******************************************************************************
 PickForms

	Show the user a list of the forms in the input file and let them
	choose which ones to generate.  "All" is not an option because that
	is the default from the command line.

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

void
PickForms
	(
	const JString&		fileName,
	JPtrArray<JString>*	list
	)
{
	JPtrArray<JString> all;
	JSize count = 0;
	cout << endl;

	ifstream input(fileName);
	while (!input.eof() && !input.fail())
		{
		const JString line = JReadLine(input);
		if (line == kBeginFormLine)
			{
			JString* formName = new JString(JReadLine(input));
			assert( formName != NULL );
			RemoveIdentifier(kFormNameMarker, formName);
			all.Append(formName);
			count++;
			cout << count << ") " << *formName << endl;
			}
		}
	input.close();

	cout << endl;

	while (1)
		{
		JIndex choice;
		cout << "Form to include (0 to end): ";
		cin >> choice;
		JInputFinished();

		if (choice == 0 && list->IsEmpty())
			{
			exit(0);
			}
		else if (choice == 0)
			{
			break;
			}
		else if (choice > count)
			{
			cout << "That is not a valid choice" << endl;
			}
		else
			{
			JString* formName = new JString(*(all.NthElement(choice)));
			assert( formName != NULL );

			JIndex tagIndex;
			if (formName->LocateSubstring(kCustomTagMarker, &tagIndex))
				{
				formName->RemoveSubstring(tagIndex, formName->GetLength());
				}

			list->Append(formName);
			}
		}

	all.DeleteAll();
}

/******************************************************************************
 FindConfigFile

	Searches for the specified config file.  Returns kTrue if successful.

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

JBoolean
FindConfigFile
	(
	JString* configFileName
	)
{
	if (JFileExists(*configFileName))
		{
		return kTrue;
		}

	const JString mainFileName = kMainConfigFileDir + *configFileName;
	if (JFileExists(mainFileName))
		{
		*configFileName = mainFileName;
		return kTrue;
		}

	char* envStr = getenv(kEnvUserConfigFileDir);
	if (envStr != NULL && envStr[0] != '\0')
		{
		JString otherFileName = envStr;
		if (otherFileName.GetLastCharacter() != '/')
			{
			otherFileName.Append("/");
			}
		otherFileName += *configFileName;
		if (JFileExists(otherFileName))
			{
			*configFileName = otherFileName;
			return kTrue;
			}
		}

	cerr << "Unable to find " << *configFileName << endl;
	cerr << "  please install it in " << kMainConfigFileDir << endl;
	cerr << "  or setenv " << kEnvUserConfigFileDir << " to point to it" << endl;
	return kFalse;
}

/******************************************************************************
 PrintHelp

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

void
PrintHelp
	(
	const JString& codeSuffix,
	const JString& headerSuffix
	)
{
	cout << endl;
	cout << "This program generates subclasses of JXWindowDirector from" << endl;
	cout << "the forms stored in an fdesign .fd file." << endl;
	cout << endl;
	cout << "The source and header files are assumed to have the same" << endl;
	cout << "name as the associated form.  Everything delimited by" << endl;
	cout << endl;
	cout << "    // begin JXLayout" << endl;
	cout << "    // end JXLayout" << endl;
	cout << endl;
	cout << "will be replaced." << endl;
	cout << endl;
	cout << "FL_BOX objects with type NO_BOX count as overloaded objects." << endl;
	cout << "The label will be used as the class name.  Extra arguments" << endl;
	cout << "can be included as follows:  'MyWidget(arg1,arg2,'" << endl;
	cout << "and the standard arguments will be appended at the end." << endl;
	cout << endl;
	cout << "Named objects will be instantiated in the header file." << endl;
	cout << "To designate it as a local variable, put () around it." << endl;
	cout << "To designate it as a variable that is defined elsewhere in" << endl;
	cout << "the function, put <> around it." << endl;
	cout << endl;
	cout << "Usage:  <options> <fdesign file> <forms to generate>" << endl;
	cout << endl;
	cout << "-h  prints help" << endl;
	cout << "-v  prints version information" << endl;
	cout << "-cp <code path>     - default: current directory" << endl;
	cout << "-cs <code suffix>   - default: " << codeSuffix << endl;
	cout << "-hs <header suffix> - default: " << headerSuffix << endl;
	cout << "-c  interactively choose the forms to generate" << endl;
	cout << endl;
}

/******************************************************************************
 PrintVersion

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

void
PrintVersion()
{
	cout << endl;
	cout << kVersionStr << endl;
	cout << endl;
}
