/*
 * CiderPress
 * Copyright (C) 2007 by faddenSoft, LLC.  All Rights Reserved.
 * See the file LICENSE for distribution terms.
 */
/*
 * Windows Registry operations.
 */
#include "stdafx.h"
#include "Registry.h"
#include "Main.h"
#include "MyApp.h"

#define kRegAppName		"CiderPress"
#define kRegExeName		"CiderPress.exe"
#define kCompanyName	"faddenSoft"

static const char* kRegKeyCPKVersions = _T("vrs");
static const char* kRegKeyCPKExpire = _T("epr");

/*
 * Application path.  Add two keys:
 *
 *	(default) = FullPathName
 *		Full pathname of the executable file.
 *	Path = Path
 *		The $PATH that will be in effect when the program starts (but only if
 *		launched from the Windows explorer).
 */
static const char* kAppKeyBase =
	_T("HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\" kRegExeName);

/*
 * Local settings.  App stuff goes in the per-user key, registration info is
 * in the per-machine key.
 */
static const char* kMachineSettingsBaseKey =
	_T("HKEY_LOCAL_MACHINE\\SOFTWARE\\" kCompanyName "\\" kRegAppName);
static const char* kUserSettingsBaseKey =
	_T("HKEY_CURRENT_USER\\Software\\" kCompanyName "\\" kRegAppName);

/*
 * Set this key + ".XXX" to (Default)=AppID.  This associates the file
 * type with kRegAppID.
 */
//static const char* kFileExtensionBase = _T("HKEY_CLASSES_ROOT");

/*
 * Description of data files.  Set this key + AppID to 40-char string, e.g.
 * (Default)=CompanyName AppName Version DataType
 *
 * Can also set DefaultIcon = Pathname [,Index]
 */
//static const char* kAppIDBase = _T("HKEY_CLASSES_ROOT");

/*
 * Put one of these under the AppID to specify the icon for a file type.
 */
static const char* kDefaultIcon = _T("DefaultIcon");

static const char* kRegKeyCPKStr = "CPK";

/*
 * Table of file type associations.  They will appear in the UI in the same
 * order that they appear here, so try to maintain alphabetical order.
 */
#define kAppIDNuFX		_T("CiderPress.NuFX")
#define kAppIDDiskImage	_T("CiderPress.DiskImage")
#define kAppIDBinaryII	_T("CiderPress.BinaryII")
#define kNoAssociation	_T("(no association)")
const MyRegistry::FileTypeAssoc MyRegistry::kFileTypeAssoc[] = {
	{ _T(".2MG"),	kAppIDDiskImage },
	{ _T(".APP"),	kAppIDDiskImage },
	{ _T(".BNY"),	kAppIDBinaryII },
	{ _T(".BQY"),	kAppIDBinaryII },
	{ _T(".BSE"),	kAppIDNuFX },
	{ _T(".BXY"),	kAppIDNuFX },
	{ _T(".D13"),	kAppIDDiskImage },
	{ _T(".DDD"),	kAppIDDiskImage },
	{ _T(".DO"),	kAppIDDiskImage },
	{ _T(".DSK"),	kAppIDDiskImage },
	{ _T(".FDI"),	kAppIDDiskImage },
	{ _T(".HDV"),	kAppIDDiskImage },
	{ _T(".IMG"),	kAppIDDiskImage },
	{ _T(".NIB"),	kAppIDDiskImage },
	{ _T(".PO"),	kAppIDDiskImage },
	{ _T(".SDK"),	kAppIDDiskImage },
	{ _T(".SEA"),	kAppIDNuFX },
	{ _T(".SHK"),	kAppIDNuFX },
//	{ _T(".DC"),	kAppIDDiskImage },
//	{ _T(".DC6"),	kAppIDDiskImage },
//	{ _T(".GZ"),	kAppIDDiskImage },
//	{ _T(".NB2"),	kAppIDDiskImage },
//	{ _T(".RAW"),	kAppIDDiskImage },
//	{ _T(".ZIP"),	kAppIDDiskImage },
};

static const struct {
	const char* user;
	const char* reg;
} gBadKeys[] = {
	{ "Nimrod Bonehead", "CP1-68C069-62CC9444" },
	{ "Connie Tan", "CP1-877B2C-A428FFD6" },
};


/*
 * ==========================================================================
 *		One-time install/uninstall
 * ==========================================================================
 */

/*
 * This is called immediately after installation finishes.
 *
 * We want to snatch up any unused file type associations.  We define them
 * as "unused" if the entry does not exist in the registry at all.  A more
 * thorough installer would also verify that the appID actually existed
 * and "steal" any apparent orphans, but we can let the user do that manually.
 */
void
MyRegistry::OneTimeInstall(void) const
{
	/* start by stomping on our appIDs */
	WMSG0(" Removing appIDs\n");
	RegDeleteKeyNT(HKEY_CLASSES_ROOT, kAppIDNuFX);
	RegDeleteKeyNT(HKEY_CLASSES_ROOT, kAppIDDiskImage);
	RegDeleteKeyNT(HKEY_CLASSES_ROOT, kAppIDBinaryII);

	/* configure the appIDs */
	FixBasicSettings();

	/* configure extensions */
	int i, res;
	for (i = 0; i < NELEM(kFileTypeAssoc); i++) {
		HKEY hExtKey;
		res = RegOpenKeyEx(HKEY_CLASSES_ROOT, kFileTypeAssoc[i].ext, 0,
				KEY_READ, &hExtKey);
		if (res == ERROR_SUCCESS) {
			WMSG1(" Found existing HKCR\\'%s', leaving alone\n",
				kFileTypeAssoc[i].ext);
			RegCloseKey(hExtKey);
		} else if (res == ERROR_FILE_NOT_FOUND) {
			OwnExtension(kFileTypeAssoc[i].ext, kFileTypeAssoc[i].appID);
		} else {
			WMSG2(" Got error %ld opening HKCR\\'%s', leaving alone\n",
				res, kFileTypeAssoc[i].ext);
		}
	}
}

/*
 * Remove things that the standard uninstall script won't.
 *
 * We want to un-set any of our file associations.  We don't really need to
 * clean up the ".xxx" entries, because removing their appID entries is enough
 * to fry their little brains, but it's probably the right thing to do.
 *
 * We definitely want to strip out our appIDs.
 */
void
MyRegistry::OneTimeUninstall(void) const
{
	/* drop any associations we hold */
	int i;
	for (i = 0; i < NELEM(kFileTypeAssoc); i++) {
		CString ext, handler;
		bool ours;

		GetFileAssoc(i, &ext, &handler, &ours);
		if (ours) {
			DisownExtension(ext);
		}
	}

	/* remove our appIDs */
	WMSG0(" Removing appIDs\n");
	RegDeleteKeyNT(HKEY_CLASSES_ROOT, kAppIDNuFX);
	RegDeleteKeyNT(HKEY_CLASSES_ROOT, kAppIDDiskImage);
	RegDeleteKeyNT(HKEY_CLASSES_ROOT, kAppIDBinaryII);
}


/*
 * ==========================================================================
 *		Shareware registration logic
 * ==========================================================================
 */

/* [removed] */

/*
 * ==========================================================================
 *		Windows shell game
 * ==========================================================================
 */

/*
 * Return the application's registry key.  This is used as the argument to
 * CWinApp::SetRegistryKey().  The GetProfile{Int,String} calls combine this
 * (in m_pszRegistryKey) with the app name (in m_pszProfileName) and prepend
 * "HKEY_CURRENT_USER\Software\".
 */
const char*
MyRegistry::GetAppRegistryKey(void) const
{
	return kCompanyName;
}

/*
 * See if an AppID is one we recognize.
 */
bool
MyRegistry::IsOurAppID(const char* id) const
{
	return (strcasecmp(id, kAppIDNuFX) == 0 ||
			strcasecmp(id, kAppIDDiskImage) == 0 ||
			strcasecmp(id, kAppIDBinaryII) == 0);
}

/*
 * Fix the basic registry settings, e.g. our AppID classes.
 *
 * We don't overwrite values that already exist.  We want to hold on to the
 * installer's settings, which should get whacked if the program is
 * uninstalled or reinstalled.  This is here for "installer-less" environments
 * and to cope with registry damage.
 */
void
MyRegistry::FixBasicSettings(void) const
{
	const char* exeName = gMyApp.GetExeFileName();
	ASSERT(exeName != nil && strlen(exeName) > 0);

	WMSG0("Fixing any missing file type AppID entries in registry\n");

	ConfigureAppID(kAppIDNuFX, "NuFX Archive (CiderPress)", exeName, 1);
	ConfigureAppID(kAppIDBinaryII, "Binary II (CiderPress)", exeName, 2);
	ConfigureAppID(kAppIDDiskImage, "Disk Image (CiderPress)", exeName, 3);
}

/*
 * Set up the registry goodies for one appID.
 */
void
MyRegistry::ConfigureAppID(const char* appID, const char* descr,
	const char* exeName, int iconIdx) const
{
	WMSG2(" Configuring '%s' for '%s'\n", appID, exeName);

	HKEY hAppKey = nil;
	HKEY hIconKey = nil;

	DWORD dw;
	if (RegCreateKeyEx(HKEY_CLASSES_ROOT, appID, 0, REG_NONE,
		REG_OPTION_NON_VOLATILE, KEY_WRITE|KEY_READ, NULL,
		&hAppKey, &dw) == ERROR_SUCCESS)
	{
		ConfigureAppIDSubFields(hAppKey, descr, exeName);

		if (RegCreateKeyEx(hAppKey, kDefaultIcon, 0, REG_NONE,
			REG_OPTION_NON_VOLATILE, KEY_WRITE|KEY_READ, NULL,
			&hIconKey, &dw) == ERROR_SUCCESS)
		{
			DWORD type, size;
			unsigned char buf[256];
			long res;

			size = sizeof(buf);
			res = RegQueryValueEx(hIconKey, "", nil, &type, buf, &size);
			if (res == ERROR_SUCCESS && size > 1) {
				WMSG1("  Icon for '%s' already exists, not altering\n", appID);
			} else {
				CString iconStr;
				iconStr.Format("%s,%d", exeName, iconIdx);

				if (RegSetValueEx(hIconKey, "", 0, REG_SZ, (const unsigned char*)
					(const char*) iconStr, strlen(iconStr)) == ERROR_SUCCESS)
				{
					WMSG2("  Set icon for '%s' to '%s'\n", appID, (LPCTSTR) iconStr);
				} else {
					WMSG2("  WARNING: unable to set DefaultIcon  for '%s' to '%s'\n",
						appID, (LPCTSTR) iconStr);
				}
			}
		} else {
			WMSG1("WARNING: couldn't set up DefaultIcon for '%s'\n", appID);
		}
	} else {
		WMSG1("WARNING: couldn't create AppID='%s'\n", appID);
	}

	RegCloseKey(hIconKey);
	RegCloseKey(hAppKey);
}

/*
 * Set up the current key's default (which is used as the explorer
 * description) and put the "Open" command in "...\shell\open\command".
 */
void
MyRegistry::ConfigureAppIDSubFields(HKEY hAppKey, const char* descr,
	const char* exeName) const
{
	HKEY hShellKey, hOpenKey, hCommandKey;
	DWORD dw;

	ASSERT(hAppKey != nil);
	ASSERT(descr != nil);
	ASSERT(exeName != nil);
	hShellKey = hOpenKey = hCommandKey = nil;
	
	if (RegSetValueEx(hAppKey, "", 0, REG_SZ, (const unsigned char*) descr,
		strlen(descr)) != ERROR_SUCCESS)
	{
		WMSG1("  WARNING: unable to set description to '%s'\n", descr);
	}

	if (RegCreateKeyEx(hAppKey, _T("shell"), 0, REG_NONE,
		REG_OPTION_NON_VOLATILE, KEY_WRITE|KEY_READ, NULL,
		&hShellKey, &dw) == ERROR_SUCCESS)
	{
		if (RegCreateKeyEx(hShellKey, _T("open"), 0, REG_NONE,
			REG_OPTION_NON_VOLATILE, KEY_WRITE|KEY_READ, NULL,
			&hOpenKey, &dw) == ERROR_SUCCESS)
		{
			if (RegCreateKeyEx(hOpenKey, _T("command"), 0, REG_NONE,
				REG_OPTION_NON_VOLATILE, KEY_WRITE|KEY_READ, NULL,
				&hCommandKey, &dw) == ERROR_SUCCESS)
			{
				DWORD type, size;
				unsigned char buf[MAX_PATH+8];
				long res;

				size = sizeof(buf);
				res = RegQueryValueEx(hCommandKey, "", nil, &type, buf, &size);
				if (res == ERROR_SUCCESS && size > 1) {
					WMSG1("  Command already exists, not altering ('%s')\n", buf);
				} else {
					CString openCmd;

					openCmd.Format("\"%s\" \"%%1\"", exeName);
					if (RegSetValueEx(hCommandKey, "", 0, REG_SZ,
						(const unsigned char*) (const char*) openCmd,
						strlen(openCmd)) == ERROR_SUCCESS)
					{
						WMSG1("  Set command to '%s'\n", openCmd);
					} else {
						WMSG1("  WARNING: unable to set open cmd '%s'\n", openCmd);
					}
				}
			}
		}
	}

	RegCloseKey(hCommandKey);
	RegCloseKey(hOpenKey);
	RegCloseKey(hShellKey);
}


/*
 * Return the number of file type associations.
 */
int
MyRegistry::GetNumFileAssocs(void) const
{
	return NELEM(kFileTypeAssoc);
}

#if 0
/*
 * Return information on a file association.
 *
 * Check to see if we're the application that will be launched.
 *
 * Problem: the file must *actually exist* for this to work.
 */
void
MyRegistry::GetFileAssoc(int idx, CString* pExt, CString* pHandler,
	bool* pOurs) const
{
	char buf[MAX_PATH];

	*pExt = kFileTypeAssoc[idx].ext;
	*pHandler = "";
	*pOurs = false;

	HINSTANCE res = FindExecutable(*pExt, "\\", buf);
	if ((long) res > 32) {
		WMSG1("Executable is '%s'\n", buf);
		*pHandler = buf;
	} else {
		WMSG1("FindExecutable failed (err=%d)\n", res);
		*pHandler = kNoAssociation;
	}
}
#endif


/*
 * Return information on a file association.
 *
 * We check to see if the file extension is associated with one of our
 * application ID strings.  We don't bother to check whether the appID
 * strings are still associated with CiderPress, since nobody should be
 * messing with those.
 *
 * BUG: we should be checking to see what the shell actually does to
 * take into account the overrides that users can set.
 */
void
MyRegistry::GetFileAssoc(int idx, CString* pExt, CString* pHandler,
	bool* pOurs) const
{
	ASSERT(idx >= 0 && idx < NELEM(kFileTypeAssoc));
	long res;

	*pExt = kFileTypeAssoc[idx].ext;
	*pHandler = "";

	CString appID;
	HKEY hExtKey = nil;

	res = RegOpenKeyEx(HKEY_CLASSES_ROOT, *pExt, 0, KEY_READ, &hExtKey);
	if (res == ERROR_SUCCESS) {
		unsigned char buf[260];
		DWORD type, size;

		size = sizeof(buf);
		res = RegQueryValueEx(hExtKey, "", nil, &type, buf, &size);
		if (res == ERROR_SUCCESS) {
			WMSG1("  Got '%s'\n", buf);
			appID = buf;

			if (GetAssocAppName(appID, pHandler) != 0)
				*pHandler = appID;
		} else {
			WMSG1("RegQueryValueEx failed on '%s'\n", (LPCTSTR) *pExt);
		}
	} else {
		WMSG1("  RegOpenKeyEx failed on '%s'\n", *pExt);
	}

	*pOurs = false;
	if (pHandler->IsEmpty()) {
		*pHandler = kNoAssociation;
	} else {
		*pOurs = IsOurAppID(appID);
	}

	RegCloseKey(hExtKey);
}

/*
 * Given an application ID, determine the application's name.
 *
 * This requires burrowing down into HKEY_CLASSES_ROOT\<appID>\shell\open\.
 */
int
MyRegistry::GetAssocAppName(const CString& appID, CString* pCmd) const
{
	CString keyName;
	unsigned char buf[260];
	DWORD type, size = sizeof(buf);
	HKEY hAppKey = nil;
	long res;
	int result = -1;

	keyName = appID + "\\shell\\open\\command";

	res = RegOpenKeyEx(HKEY_CLASSES_ROOT, keyName, 0, KEY_READ, &hAppKey);
	if (res == ERROR_SUCCESS) {
		res = RegQueryValueEx(hAppKey, "", nil, &type, buf, &size);
		if (res == ERROR_SUCCESS) {
			CString cmd(buf);
			int pos;

			/* cut it down to just the EXE name */
			ReduceToToken(&cmd);

			pos = cmd.ReverseFind('\\');
			if (pos != -1 && pos != cmd.GetLength()-1) {
				cmd = cmd.Right(cmd.GetLength() - pos -1);
			}

			*pCmd = cmd;
			result = 0;
		} else {
			WMSG1("Unable to open shell\\open\\command for '%s'\n", appID);
		}
	} else {
		CString errBuf;
		GetWin32ErrorString(res, &errBuf);

		WMSG2("Unable to open AppID key '%s' (%s)\n",
			keyName, (LPCTSTR) errBuf);
	}

	RegCloseKey(hAppKey);
	return result;
}

/*
 * Reduce a compound string to just its first token.
 */
void
MyRegistry::ReduceToToken(CString* pStr) const
{
	char* argv[1];
	int argc = 1;
	char* mangle = strdup(*pStr);

	VectorizeString(mangle, argv, &argc);

	if (argc == 1)
		*pStr = argv[0];

	free(mangle);
}

/*
 * Set the state of a file association.  There are four possibilities:
 *
 *	- We own it, we want to keep owning it: do nothing.
 *	- We don't own it, we want to keep not owning it: do nothing.
 *	- We own it, we don't want it anymore: remove ".xxx" entry.
 *	- We don't own it, we want to own it: remove ".xxx" entry and replace it.
 *
 * Returns 0 on success, nonzero on failure.
 */
int
MyRegistry::SetFileAssoc(int idx, bool wantIt) const
{
	const char* ext;
	bool weOwnIt;
	int result = 0;

	ASSERT(idx >= 0 && idx < NELEM(kFileTypeAssoc));

	ext = kFileTypeAssoc[idx].ext;
	weOwnIt = GetAssocState(ext);
	WMSG3("SetFileAssoc: ext='%s' own=%d want=%d\n", ext, weOwnIt, wantIt);

	if (weOwnIt && !wantIt) {
		/* reset it */
		WMSG1(" SetFileAssoc: clearing '%s'\n", ext);
		result = DisownExtension(ext);
	} else if (!weOwnIt && wantIt) {
		/* take it */
		WMSG1(" SetFileAssoc: taking '%s'\n", ext);
		result = OwnExtension(ext, kFileTypeAssoc[idx].appID);
	} else {
		WMSG1(" SetFileAssoc: do nothing with '%s'\n", ext);
		/* do nothing */
	}

	return 0;
}

/*
 * Determine whether or not the filetype described by "ext" is one that we
 * currently manage.
 *
 * Returns "true" if so, "false" if not.  Returns "false" on any errors
 * encountered.
 */
bool
MyRegistry::GetAssocState(const char* ext) const
{
	unsigned char buf[260];
	HKEY hExtKey = nil;
	DWORD type, size;
	int res;
	bool result = false;

	res = RegOpenKeyEx(HKEY_CLASSES_ROOT, ext, 0, KEY_READ, &hExtKey);
	if (res == ERROR_SUCCESS) {
		size = sizeof(buf);
		res = RegQueryValueEx(hExtKey, "", nil, &type, buf, &size);
		if (res == ERROR_SUCCESS && type == REG_SZ) {
			/* compare it to known appID values */
			WMSG2("  Found '%s', testing '%s'\n", ext, buf);
			if (IsOurAppID((char*)buf))
				result = true;
		}
	}

	RegCloseKey(hExtKey);
	return result;
}

/*
 * Drop ownership of a file extension.
 *
 * We assume we own it.
 *
 * Returns 0 on success, -1 on error.
 */
int
MyRegistry::DisownExtension(const char* ext) const
{
	ASSERT(ext != nil);
	ASSERT(ext[0] == '.');
	if (ext == nil || strlen(ext) < 2)
		return -1;

	if (RegDeleteKeyNT(HKEY_CLASSES_ROOT, ext) == ERROR_SUCCESS) {
		WMSG1("    HKCR\\%s subtree deleted\n", ext);
	} else {
		WMSG1("    Failed deleting HKCR\\'%s'\n", ext);
		return -1;
	}

	return 0;
}

/*
 * Take ownership of a file extension.
 *
 * Returns 0 on success, -1 on error.
 */
int
MyRegistry::OwnExtension(const char* ext, const char* appID) const
{
	ASSERT(ext != nil);
	ASSERT(ext[0] == '.');
	if (ext == nil || strlen(ext) < 2)
		return -1;

	HKEY hExtKey = nil;
	DWORD dw;
	int res, result = -1;

	/* delete the old key (which might be a hierarchy) */
	res = RegDeleteKeyNT(HKEY_CLASSES_ROOT, ext);
	if (res == ERROR_SUCCESS) {
		WMSG1("    HKCR\\%s subtree deleted\n", ext);
	} else if (res == ERROR_FILE_NOT_FOUND) {
		WMSG1("    No HKCR\\%s subtree to delete\n", ext);
	} else {
		WMSG1("    Failed deleting HKCR\\'%s'\n", ext);
		goto bail;
	}

	/* set the new key */
	if (RegCreateKeyEx(HKEY_CLASSES_ROOT, ext, 0, REG_NONE,
		REG_OPTION_NON_VOLATILE, KEY_WRITE|KEY_READ, NULL,
		&hExtKey, &dw) == ERROR_SUCCESS)
	{
		res = RegSetValueEx(hExtKey, "", 0, REG_SZ,
				(const unsigned char*) appID, strlen(appID));
		if (res == ERROR_SUCCESS) {
			WMSG2("    Set '%s' to '%s'\n", ext, appID);
			result = 0;
		} else {
			WMSG3("Failed setting '%s' to '%s' (res=%d)\n", ext, appID, res);
			goto bail;
		}
	}

bail:
	RegCloseKey(hExtKey);
	return result;
}


// (This comes from the MSDN sample sources.)
//
// The sample code makes no attempt to check or recover from partial
// deletions.
//
// A registry key that is opened by an application can be deleted
// without error by another application in both Windows 95 and
// Windows NT. This is by design.
//
#define MAX_KEY_LENGTH	256		// not in any header I can find ++ATM
DWORD
MyRegistry::RegDeleteKeyNT(HKEY hStartKey, LPCTSTR pKeyName) const
{
	DWORD	dwRtn, dwSubKeyLength;
	LPTSTR	pSubKey = NULL;
	TCHAR	szSubKey[MAX_KEY_LENGTH]; // (256) this should be dynamic.
	HKEY	hKey;
	
	// Do not allow NULL or empty key name
	if ( pKeyName &&  lstrlen(pKeyName))
	{
		if( (dwRtn=RegOpenKeyEx(hStartKey,pKeyName,
			0, KEY_ENUMERATE_SUB_KEYS | DELETE, &hKey )) == ERROR_SUCCESS)
		{
			while (dwRtn == ERROR_SUCCESS )
			{
				dwSubKeyLength = MAX_KEY_LENGTH;
				dwRtn=RegEnumKeyEx(
					hKey,
					0,		 // always index zero, because we're deleting it
					szSubKey,
					&dwSubKeyLength,
					NULL,
					NULL,
					NULL,
					NULL
					);
				
				if(dwRtn == ERROR_NO_MORE_ITEMS)
				{
					dwRtn = RegDeleteKey(hStartKey, pKeyName);
					break;
				}
				else if(dwRtn == ERROR_SUCCESS)
					dwRtn=RegDeleteKeyNT(hKey, szSubKey);
			}
			RegCloseKey(hKey);
			// Do not save return code because error
			// has already occurred
		}
	}
	else
		dwRtn = ERROR_BADKEY;
	
	return dwRtn;
}
