/**
 * WinPR: Windows Portable Runtime
 * Windows Registry (.reg file format)
 *
 * Copyright 2012 Marc-Andre Moreau <marcandre.moreau@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <winpr/wtypes.h>
#include <winpr/crt.h>

#include "registry_reg.h"

#include "../log.h"
#define TAG WINPR_TAG("registry")

#define WINPR_HKLM_HIVE "/etc/winpr/HKLM.reg"

struct reg_data_type
{
	char* tag;
	int length;
	DWORD type;
};

static struct reg_data_type REG_DATA_TYPE_TABLE[] = { { "\"", 1, REG_SZ },
	                                                  { "dword:", 6, REG_DWORD },
	                                                  { "str:\"", 5, REG_SZ },
	                                                  { "str(2):\"", 8, REG_EXPAND_SZ },
	                                                  { "str(7):\"", 8, REG_MULTI_SZ },
	                                                  { "hex:", 4, REG_BINARY },
	                                                  { "hex(2):\"", 8, REG_EXPAND_SZ },
	                                                  { "hex(7):\"", 8, REG_MULTI_SZ },
	                                                  { "hex(b):\"", 8, REG_QWORD },
	                                                  { NULL, 0, 0 } };

static char* REG_DATA_TYPE_STRINGS[] = { "REG_NONE",
	                                     "REG_SZ",
	                                     "REG_EXPAND_SZ",
	                                     "REG_BINARY",
	                                     "REG_DWORD",
	                                     "REG_DWORD_BIG_ENDIAN",
	                                     "REG_LINK",
	                                     "REG_MULTI_SZ",
	                                     "REG_RESOURCE_LIST",
	                                     "REG_FULL_RESOURCE_DESCRIPTOR",
	                                     "REG_RESOURCE_REQUIREMENTS_LIST",
	                                     "REG_QWORD" };

static void reg_load_start(Reg* reg)
{
	INT64 file_size;
	_fseeki64(reg->fp, 0, SEEK_END);
	file_size = _ftelli64(reg->fp);
	_fseeki64(reg->fp, 0, SEEK_SET);
	reg->line = NULL;
	reg->next_line = NULL;
	reg->buffer = NULL;

	if (file_size < 1)
		return;

	reg->buffer = (char*)malloc(file_size + 2);

	if (!reg->buffer)
		return;

	if (fread(reg->buffer, file_size, 1, reg->fp) != 1)
	{
		free(reg->buffer);
		reg->buffer = NULL;
		return;
	}

	reg->buffer[file_size] = '\n';
	reg->buffer[file_size + 1] = '\0';
	reg->next_line = strtok(reg->buffer, "\n");
}

static void reg_load_finish(Reg* reg)
{
	if (!reg)
		return;

	if (reg->buffer)
	{
		free(reg->buffer);
		reg->buffer = NULL;
	}
}

static RegVal* reg_load_value(Reg* reg, RegKey* key)
{
	int index;
	char* p[5];
	int length;
	char* name;
	char* type;
	char* data;
	RegVal* value;
	p[0] = reg->line + 1;
	p[1] = strstr(p[0], "\"=");
	p[2] = p[1] + 2;
	type = p[2];

	if (p[2][0] == '"')
		p[3] = p[2];
	else
		p[3] = strchr(p[2], ':');

	data = p[3] + 1;
	length = p[1] - p[0];
	name = (char*)malloc(length + 1);

	if (!name)
		return NULL;

	memcpy(name, p[0], length);
	name[length] = '\0';
	value = (RegVal*)malloc(sizeof(RegVal));

	if (!value)
	{
		free(name);
		return NULL;
	}

	value->name = name;
	value->type = REG_NONE;
	value->next = value->prev = NULL;

	for (index = 0; REG_DATA_TYPE_TABLE[index].length > 0; index++)
	{
		if (strncmp(type, REG_DATA_TYPE_TABLE[index].tag, REG_DATA_TYPE_TABLE[index].length) == 0)
		{
			value->type = REG_DATA_TYPE_TABLE[index].type;
			break;
		}
	}

	if (value->type == REG_DWORD)
	{
		unsigned long val;
		errno = 0;
		val = strtoul(data, NULL, 16);

		if ((errno != 0) || (val > UINT32_MAX))
		{
			free(value);
			free(name);
			return NULL;
		}

		value->data.dword = val;
	}
	else if (value->type == REG_SZ)
	{
		p[4] = strchr(data, '"');
		p[4][0] = '\0';
		value->data.string = _strdup(data);

		if (!value->data.string)
		{
			free(value);
			free(name);
			return NULL;
		}
	}
	else
	{
		WLog_ERR(TAG, "unimplemented format: %s", REG_DATA_TYPE_STRINGS[value->type]);
	}

	if (!key->values)
	{
		key->values = value;
	}
	else
	{
		RegVal* pValue = key->values;

		while (pValue->next != NULL)
		{
			pValue = pValue->next;
		}

		pValue->next = value;
		value->prev = pValue;
	}

	return value;
}

static BOOL reg_load_has_next_line(Reg* reg)
{
	if (!reg)
		return 0;

	return (reg->next_line != NULL) ? 1 : 0;
}

static char* reg_load_get_next_line(Reg* reg)
{
	if (!reg)
		return NULL;

	reg->line = reg->next_line;
	reg->next_line = strtok(NULL, "\n");
	reg->line_length = strlen(reg->line);
	return reg->line;
}

static char* reg_load_peek_next_line(Reg* reg)
{
	return reg->next_line;
}

static void reg_insert_key(Reg* reg, RegKey* key, RegKey* subkey)
{
	char* name;
	char* path;
	char* save;
	int length;
	path = _strdup(subkey->name);

	if (!path)
		return;

	name = strtok_s(path, "\\", &save);

	while (name != NULL)
	{
		if (strcmp(key->name, name) == 0)
		{
			length = strlen(name);
			name += length + 1;
			subkey->subname = _strdup(name);

			/* TODO: free allocated memory in error case */
			if (!subkey->subname)
			{
				free(path);
				return;
			}
		}

		name = strtok_s(NULL, "\\", &save);
	}

	free(path);
}

static RegKey* reg_load_key(Reg* reg, RegKey* key)
{
	char* p[2];
	int length;
	char* line;
	RegKey* subkey;
	p[0] = reg->line + 1;
	p[1] = strrchr(p[0], ']');
	subkey = (RegKey*)malloc(sizeof(RegKey));

	if (!subkey)
		return NULL;

	subkey->values = NULL;
	subkey->prev = subkey->next = NULL;
	length = p[1] - p[0];
	subkey->name = (char*)malloc(length + 1);

	if (!subkey->name)
	{
		free(subkey);
		return NULL;
	}

	memcpy(subkey->name, p[0], length);
	subkey->name[length] = '\0';

	while (reg_load_has_next_line(reg))
	{
		line = reg_load_peek_next_line(reg);

		if (line[0] == '[')
			break;

		reg_load_get_next_line(reg);

		if (reg->line[0] == '"')
		{
			reg_load_value(reg, subkey);
		}
	}

	reg_insert_key(reg, key, subkey);

	if (!key->subkeys)
	{
		key->subkeys = subkey;
	}
	else
	{
		RegKey* pKey = key->subkeys;

		while (pKey->next != NULL)
		{
			pKey = pKey->next;
		}

		pKey->next = subkey;
		subkey->prev = pKey;
	}

	return subkey;
}

static void reg_load(Reg* reg)
{
	reg_load_start(reg);

	while (reg_load_has_next_line(reg))
	{
		reg_load_get_next_line(reg);

		if (reg->line[0] == '[')
		{
			reg_load_key(reg, reg->root_key);
		}
	}

	reg_load_finish(reg);
}

static void reg_unload_value(Reg* reg, RegVal* value)
{
	if (value->type == REG_DWORD)
	{
	}
	else if (value->type == REG_SZ)
	{
		free(value->data.string);
	}
	else
	{
		WLog_ERR(TAG, "unimplemented format: %s", REG_DATA_TYPE_STRINGS[value->type]);
	}

	free(value);
}

static void reg_unload_key(Reg* reg, RegKey* key)
{
	RegVal* pValue;
	RegVal* pValueNext;
	pValue = key->values;

	while (pValue != NULL)
	{
		pValueNext = pValue->next;
		reg_unload_value(reg, pValue);
		pValue = pValueNext;
	}

	free(key->name);
	free(key);
}

static void reg_unload(Reg* reg)
{
	RegKey* pKey;
	RegKey* pKeyNext;
	pKey = reg->root_key->subkeys;

	while (pKey != NULL)
	{
		pKeyNext = pKey->next;
		reg_unload_key(reg, pKey);
		pKey = pKeyNext;
	}

	free(reg->root_key);
}

Reg* reg_open(BOOL read_only)
{
	Reg* reg;
	reg = (Reg*)malloc(sizeof(Reg));

	if (!reg)
		return NULL;

	reg->read_only = read_only;
	reg->filename = WINPR_HKLM_HIVE;

	if (reg->read_only)
	{
		reg->fp = fopen(reg->filename, "r");
	}
	else
	{
		reg->fp = fopen(reg->filename, "r+");

		if (!reg->fp)
			reg->fp = fopen(reg->filename, "w+");
	}

	if (!reg->fp)
	{
		free(reg);
		return NULL;
	}

	reg->root_key = (RegKey*)malloc(sizeof(RegKey));

	if (!reg->root_key)
	{
		fclose(reg->fp);
		free(reg);
		return NULL;
	}

	reg->root_key->values = NULL;
	reg->root_key->subkeys = NULL;
	reg->root_key->name = "HKEY_LOCAL_MACHINE";
	reg_load(reg);
	return reg;
}

void reg_close(Reg* reg)
{
	if (reg)
	{
		reg_unload(reg);
		fclose(reg->fp);
		free(reg);
	}
}
