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

#define DEFAULT_MAX_DATA 512
#define DEFAULT_MAX_ROWS 100

struct Address {
	char *name;
	char *email;
};

struct TableOfContents {
	size_t n_rows;
	size_t string_field_size;
};

struct Database {
	struct Address *rows;
};

struct Connection {
	FILE *file;
	struct TableOfContents *toc;
	struct Database *db;
};

void Database_close(struct Connection *conn);
struct Address *Database_get(struct Connection *conn, const char *email);

void die(struct Connection *conn, const char *message)
{
	Database_close(conn);

	if (errno) {
		perror(message);
	} else {
		printf("ERROR: %s\n", message);
	}

	exit(1);
}

void Address_print(struct Address *addr)
{
	if (!addr) {
		return;
	}

	printf("%s %s\n", addr->name, addr->email);
}

size_t Database_addr_size(struct Connection *conn)
{
	return sizeof(struct Address) +
		/* name */ conn->toc->string_field_size +
		/* email */ conn->toc->string_field_size;
}

size_t Database_size(struct Connection *conn)
{
	return sizeof(struct Database) +
		(conn->toc->n_rows * Database_addr_size(conn));
}

size_t Database_n_rows(struct Connection *conn)
{
	return sizeof(conn->db->rows) / Database_addr_size(conn);
}

void Database_load(struct Connection *conn)
{
	int rc = 0;

	rc = fread(conn->toc, sizeof(struct TableOfContents), 1, conn->file);
	if (rc != 1) {
		die(conn, "Failed to load TableOfContents.");
	}

	rc = fread(conn->db, Database_size(conn), 1, conn->file);
	if (rc != 1) {
		die(conn, "Failed to load Database.");
	}
}

struct Connection *Database_open(const char *filename, char mode, size_t string_field_size)
{
	struct Connection *conn = malloc(sizeof(struct Connection));

	if (!conn) {
		die(conn, "Memory error allocating Connection");
	}

	conn->toc = malloc(sizeof(struct TableOfContents));
	if (!conn->toc) {
		die(conn, "Memory error allocating TableOfContents");
	}

	conn->toc->string_field_size = string_field_size;

	conn->db = malloc(sizeof(struct Database));
	if (!conn->db) {
		die(conn, "Memory error allocating Database");
	}

	if (mode == 'c') {
		conn->file = fopen(filename, "w");

		conn->toc->n_rows = 1;

		conn->db->rows = malloc(Database_addr_size(conn));
		if (!conn->db->rows) {
			die(conn, "Memory error allocating Database rows");
		}

	} else {
		conn->file = fopen(filename, "r+");

		if (conn->file) {
			Database_load(conn);
		}
	}

	if (!conn->file) {
		die(conn, "Failed to open file");
	}

	return conn;
}

void Database_close(struct Connection *conn)
{
	if (conn) {
		if (conn->file) {
			fclose(conn->file);
		}

		if (conn->toc) {
			free(conn->toc);
		}

		if (conn->db) {
			free(conn->db);
		}

		free(conn);
	}
}

void Database_write(struct Connection *conn)
{
	rewind(conn->file);

	int rc = 0;

	rc = fwrite(conn->toc, sizeof(struct TableOfContents), 1, conn->file);
	if (rc != 1) {
		die(conn, "Failed to write TableOfContents.");
	}

	rc = fwrite(conn->db, sizeof(struct Database), 1, conn->file);
	if (rc != 1) {
		die(conn, "Failed to write Database.");
	}

	rc = fflush(conn->file);
	if (rc == -1) {
		die(conn, "Cannot flush");
	}
}

void Database_set(struct Connection *conn, const char *name, const char *email)
{
	struct Address *addr = Database_get(conn, email);
	if (!addr) {
		addr = malloc(Database_addr_size(conn));
		conn->db->rows[Database_n_rows(conn)+1] = *addr;
	}

	char *res = strncpy(addr->name, name, conn->toc->string_field_size);
	if (!res) {
		die(conn, "Name copy failed");
	}

	addr->name[conn->toc->string_field_size - 1] = '\0';

	res = strncpy(addr->email, email, conn->toc->string_field_size);
	if (!res) {
		die(conn, "Email copy failed");
	}

	addr->email[conn->toc->string_field_size - 1] = '\0';
}

struct Address *Database_get(struct Connection *conn, const char *email)
{
	int i = 0;
	struct Address *addr;

	for (i = 0; i < Database_n_rows(conn); i++) {
		addr = &conn->db->rows[i];

		if (strcmp(addr->email, email) == 0) {
			return addr;
		}
	}

	return NULL;
}

void Database_delete(struct Connection *conn, const char *email)
{
	struct Address *addr = Database_get(conn, email);
	if (!addr) {
		// already deleted
		return;
	}

	addr->email = NULL;
	addr->name = NULL;
}

void Database_list(struct Connection *conn)
{
	int i = 0;
	struct Database *db = conn->db;

	for (i = 0; i < Database_n_rows(conn); i++) {
		struct Address *cur = &db->rows[i];

		Address_print(cur);
	}
}

size_t get_string_field_size()
{
	size_t string_field_size = DEFAULT_MAX_DATA;

	const char *string_field_size_string = getenv("EX17_MAX_DATA");
	if (string_field_size_string) {
		int string_field_size_int = atoi(string_field_size_string);
		string_field_size = string_field_size_int;
	}

	return string_field_size;
}

int main(int argc, char *argv[])
{
	if (argc < 3) {
		die(NULL, "USAGE: ex17 <dbfile> <action> [action params]");
	}

	char *filename = argv[1];
	char action = argv[2][0];

	size_t string_field_size = get_string_field_size();

	struct Connection *conn = Database_open(filename, action, string_field_size);

	switch (action) {
		case 'c':
			Database_write(conn);
			break;

		case 'g':
			if (argc != 4) {
				die(conn, "Need an `email` to get");
			}

			Address_print(Database_get(conn, argv[3]));
			break;

		case 's':
			if (argc != 5) {
				die(conn, "Need a `name` and `email` to set");
			}

			Database_set(conn, argv[3], argv[4]);
			Database_write(conn);
			break;

		case 'd':
			if (argc != 4) {
				die(conn, "Need an `email` to delete");
			}

			Database_delete(conn, argv[3]);
			Database_write(conn);
			break;

		case 'l':
			Database_list(conn);
			break;

		default:
			die(conn, "Invalid action: c=create, g=get, s=set, d=del, l=list");
	}

	Database_close(conn);

	return 0;
}
