/*
Copyright (c) 2010-2012 Roger Light <roger@atchoo.org>

All rights reserved. This program and the accompanying materials
are made available under the terms of the Eclipse Public License v1.0
and Eclipse Distribution License v1.0 which accompany this distribution.
 
The Eclipse Public License is available at
   http://www.eclipse.org/legal/epl-v10.html
and the Eclipse Distribution License is available at
  http://www.eclipse.org/org/documents/edl-v10.php.
 
Contributors:
   Roger Light - initial implementation and documentation.
*/

#include <arpa/inet.h>
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <time.h>

#include <mosquitto_broker_internal.h>
#include <memory_mosq.h>
#include <persist.h>

#define mosquitto__malloc(A) malloc((A))
#define mosquitto__free(A) free((A))
#define _mosquitto_malloc(A) malloc((A))
#define _mosquitto_free(A) free((A))
#include <uthash.h>

const unsigned char magic[15] = {0x00, 0xB5, 0x00, 'm','o','s','q','u','i','t','t','o',' ','d','b'};

struct client_chunk
{
	UT_hash_handle hh_id;
	char *id;
	int subscriptions;
	int subscription_size;
	int messages;
	long message_size;
};

struct msg_store_chunk
{
	UT_hash_handle hh;
	dbid_t store_id;
	uint32_t length;
};

struct db_sub
{
	char *client_id;
	char *topic;
	uint8_t qos;
};

struct db_client
{
	char *client_id;
	uint16_t last_mid;
	time_t disconnect_t;
};

struct db_client_msg
{
	char *client_id;
	uint8_t qos, retain, direction, state, dup;
	dbid_t store_id;
	uint16_t mid;
};

struct db_msg
{
	dbid_t store_id;
	uint32_t payloadlen;
	uint16_t source_mid, mid;
	uint8_t qos, retain;
	uint8_t *payload;
	char *source_id;
	char *source_username;
	char *topic;
	uint16_t source_port;
};

static uint32_t db_version;
static int stats = 0;
static int client_stats = 0;
static int do_print = 1;

struct client_chunk *clients_by_id = NULL;
struct msg_store_chunk *msgs_by_id = NULL;

static void
free__db_sub(struct db_sub *sub)
{
	free(sub->client_id);
	free(sub->topic);
}

static void
free__db_client(struct db_client *client)
{
	free(client->client_id);
}

static void
free__db_client_msg(struct db_client_msg *msg)
{
	free(msg->client_id);
}

static void
free__db_msg(struct db_msg *msg)
{
	free(msg->source_id);
	free(msg->topic);
	free(msg->payload);
}

static void
print_db_client(struct db_client *client, int length)
{
	printf("DB_CHUNK_CLIENT:\n");
	printf("\tLength: %d\n", length);
	printf("\tClient ID: %s\n", client->client_id);
	printf("\tLast MID: %d\n", client->last_mid);
	printf("\tDisconnect time: %ld\n", client->disconnect_t);
}

static void
print_db_client_msg(struct db_client_msg *msg, int length)
{
	printf("DB_CHUNK_CLIENT_MSG:\n");
	printf("\tLength: %d\n", length);
	printf("\tClient ID: %s\n", msg->client_id);
	printf("\tStore ID: %" PRIu64 "\n", msg->store_id);
	printf("\tMID: %d\n", msg->mid);
	printf("\tQoS: %d\n", msg->qos);
	printf("\tRetain: %d\n", msg->retain);
	printf("\tDirection: %d\n", msg->direction);
	printf("\tState: %d\n", msg->state);
	printf("\tDup: %d\n", msg->dup);
}

static void
print_db_sub(struct db_sub *sub, int length)
{
	printf("DB_CHUNK_SUB:\n");
	printf("\tLength: %d\n", length);
	printf("\tClient ID: %s\n", sub->client_id);
	printf("\tTopic: %s\n", sub->topic);
	printf("\tQoS: %d\n", sub->qos);
}

static void
print_db_msg(struct db_msg *msg, int length)
{
	printf("DB_CHUNK_MSG_STORE:\n");
	printf("\tLength: %d\n", length);
	printf("\tStore ID: %" PRIu64 "\n", msg->store_id);
	printf("\tSource ID: %s\n", msg->source_id);
	printf("\tSource Username: %s\n", msg->source_username);
	printf("\tSource Port: %d\n", msg->source_port);
	printf("\tSource MID: %d\n", msg->source_mid);
	printf("\tMID: %d\n", msg->mid);
	printf("\tTopic: %s\n", msg->topic);
	printf("\tQoS: %d\n", msg->qos);
	printf("\tRetain: %d\n", msg->retain);
	printf("\tPayload Length: %d\n", msg->payloadlen);

	bool binary = false;
	for(int i=0; i<msg->payloadlen; i++){
		if(msg->payload[i] == 0) binary = true;
	}
	if(binary == false && msg->payloadlen<256){
		printf("\tPayload: %s\n", msg->payload);
	}
}


int persist__read_string(FILE *db_fptr, char **str)
{
	uint16_t i16temp;
	uint16_t slen;
	char *s = NULL;

	if(fread(&i16temp, 1, sizeof(uint16_t), db_fptr) != sizeof(uint16_t)){
		return MOSQ_ERR_INVAL;
	}

	slen = ntohs(i16temp);
	if(slen){
		s = mosquitto__malloc(slen+1);
		if(!s){
			fclose(db_fptr);
			fprintf(stderr, "Error: Out of memory.\n");
			return MOSQ_ERR_NOMEM;
		}
		if(fread(s, 1, slen, db_fptr) != slen){
			mosquitto__free(s);
			fprintf(stderr, "Error: %s.\n", strerror(errno));
			return MOSQ_ERR_INVAL;
		}
		s[slen] = '\0';
	}

	*str = s;
	return MOSQ_ERR_SUCCESS;
}


static int db__client_chunk_restore(struct mosquitto_db *db, FILE *db_fd, struct db_client *client)
{
	uint16_t i16temp;
	int rc = 0;
	struct client_chunk *cc;

	rc = persist__read_string(db_fd, &client->client_id);
	if(rc){
		fprintf(stderr, "Error: Corrupt persistent database.");
		fclose(db_fd);
		return rc;
	}

	read_e(db_fd, &i16temp, sizeof(uint16_t));
	client->last_mid = ntohs(i16temp);

	if(db_version == 2){
		client->disconnect_t = time(NULL);
	}else{
		read_e(db_fd, &client->disconnect_t, sizeof(time_t));
	}

	if(client_stats){
		cc = calloc(1, sizeof(struct client_chunk));
		if(!cc){
			errno = ENOMEM;
			goto error;
		}
		cc->id = strdup(client->client_id);
		HASH_ADD_KEYPTR(hh_id, clients_by_id, cc->id, strlen(cc->id), cc);
	}

	return rc;
error:
	fprintf(stderr, "Error: %s.", strerror(errno));
	if(db_fd) fclose(db_fd);
	free(client->client_id);
	return 1;
}

static int db__client_msg_chunk_restore(struct mosquitto_db *db, FILE *db_fd, uint32_t length, struct db_client_msg *msg)
{
	dbid_t i64temp;
	uint16_t i16temp;
	struct client_chunk *cc;
	struct msg_store_chunk *msc;
	int rc;

	rc = persist__read_string(db_fd, &msg->client_id);
	if(rc){
		fprintf(stderr, "Error: Corrupt persistent database.");
		fclose(db_fd);
		return rc;
	}

	read_e(db_fd, &i64temp, sizeof(dbid_t));
	msg->store_id = i64temp;

	read_e(db_fd, &i16temp, sizeof(uint16_t));
	msg->mid = ntohs(i16temp);

	read_e(db_fd, &msg->qos, sizeof(uint8_t));
	read_e(db_fd, &msg->retain, sizeof(uint8_t));
	read_e(db_fd, &msg->direction, sizeof(uint8_t));
	read_e(db_fd, &msg->state, sizeof(uint8_t));
	read_e(db_fd, &msg->dup, sizeof(uint8_t));

	if(client_stats){
		HASH_FIND(hh_id, clients_by_id, msg->client_id, strlen(msg->client_id), cc);
		if(cc){
			cc->messages++;
			cc->message_size += length;

			HASH_FIND(hh, msgs_by_id, &msg->store_id, sizeof(dbid_t), msc);
			if(msc){
				cc->message_size += msc->length;
			}
		}
	}

	return 0;
error:
	fprintf(stderr, "Error: %s.", strerror(errno));
	if(db_fd) fclose(db_fd);
	free(msg->client_id);
	return 1;
}

static int db__msg_store_chunk_restore(struct mosquitto_db *db, FILE *db_fd, uint32_t length, struct db_msg *msg)
{
	dbid_t i64temp;
	uint32_t i32temp;
	uint16_t i16temp;
	int rc = 0;
	struct msg_store_chunk *mcs;

	read_e(db_fd, &i64temp, sizeof(dbid_t));
	msg->store_id = i64temp;

	rc = persist__read_string(db_fd, &msg->source_id);
	if(rc){
		fprintf(stderr, "Error: Corrupt persistent database.");
		fclose(db_fd);
		return rc;
	}
	if(db_version == 4){
		rc = persist__read_string(db_fd, &msg->source_username);
		if(rc){
			fprintf(stderr, "Error: %s.", strerror(errno));
			fclose(db_fd);
			free(msg->source_id);
			free(msg->topic);
			free(msg->payload);
			free(msg->source_id);
			return 1;
		}
		read_e(db_fd, &i16temp, sizeof(uint16_t));
		msg->source_port = ntohs(i16temp);
	}


	read_e(db_fd, &i16temp, sizeof(uint16_t));
	msg->source_mid = ntohs(i16temp);

	read_e(db_fd, &i16temp, sizeof(uint16_t));
	msg->mid = ntohs(i16temp);

	rc = persist__read_string(db_fd, &msg->topic);
	if(rc){
		fprintf(stderr, "Error: Corrupt persistent database.");
		fclose(db_fd);
		return rc;
	}

	read_e(db_fd, &msg->qos, sizeof(uint8_t));
	read_e(db_fd, &msg->retain, sizeof(uint8_t));
	
	read_e(db_fd, &i32temp, sizeof(uint32_t));
	msg->payloadlen = ntohl(i32temp);

	if(msg->payloadlen){
		msg->payload = malloc(msg->payloadlen+1);
		if(!msg->payload){
			fclose(db_fd);
			free(msg->source_id);
			free(msg->topic);
			fprintf(stderr, "Error: Out of memory.");
			return 1;
		}
		memset(msg->payload, 0, msg->payloadlen+1);
		if(fread(msg->payload, 1, msg->payloadlen, db_fd) != msg->payloadlen){
			fprintf(stderr, "Error: %s.", strerror(errno));
			fclose(db_fd);
			free(msg->source_id);
			free(msg->topic);
			free(msg->payload);
			return 1;
		}
	}

	if(client_stats){
		mcs = calloc(1, sizeof(struct msg_store_chunk));
		if(!mcs){
			errno = ENOMEM;
			goto error;
		}
		mcs->store_id = msg->store_id;
		mcs->length = length;
		HASH_ADD(hh, msgs_by_id, store_id, sizeof(dbid_t), mcs);
	}

	return rc;
error:
	fprintf(stderr, "Error: %s.", strerror(errno));
	if(db_fd) fclose(db_fd);
	free(msg->source_id);
	free(msg->topic);
	return 1;
}

static int db__retain_chunk_restore(struct mosquitto_db *db, FILE *db_fd)
{
	dbid_t i64temp, store_id;

	if(fread(&i64temp, sizeof(dbid_t), 1, db_fd) != 1){
		fprintf(stderr, "Error: %s.", strerror(errno));
		fclose(db_fd);
		return 1;
	}
	store_id = i64temp;
	if(do_print) printf("\tStore ID: %" PRIu64 "\n", store_id);
	return 0;
}

static int db__sub_chunk_restore(struct mosquitto_db *db, FILE *db_fd, uint32_t length, struct db_sub *sub)
{
	int rc = 0;
	struct client_chunk *cc;

	rc = persist__read_string(db_fd, &sub->client_id);
	if(rc){
		fprintf(stderr, "Error: Corrupt persistent database.");
		fclose(db_fd);
		return rc;
	}

	rc = persist__read_string(db_fd, &sub->topic);
	if(rc){
		fprintf(stderr, "Error: Corrupt persistent database.");
		fclose(db_fd);
		return rc;
	}

	read_e(db_fd, &sub->qos, sizeof(uint8_t));

	if(client_stats){
		HASH_FIND(hh_id, clients_by_id, sub->client_id, strlen(sub->client_id), cc);
		if(cc){
			cc->subscriptions++;
			cc->subscription_size += length;
		}
	}

	return rc;
error:
	fprintf(stderr, "Error: %s.", strerror(errno));
	if(db_fd >= 0) fclose(db_fd);
	return 1;
}

int main(int argc, char *argv[])
{
	FILE *fd;
	char header[15];
	int rc = 0;
	uint32_t crc;
	dbid_t i64temp;
	uint32_t i32temp, length;
	uint16_t i16temp, chunk;
	uint8_t i8temp;
	ssize_t rlen;
	struct mosquitto_db db;
	char *filename;
	long cfg_count = 0;
	long msg_store_count = 0;
	long client_msg_count = 0;
	long retain_count = 0;
	long sub_count = 0;
	long client_count = 0;
	struct client_chunk *cc, *cc_tmp;

	if(argc == 2){
		filename = argv[1];
	}else if(argc == 3 && !strcmp(argv[1], "--stats")){
		stats = 1;
		do_print = 0;
		filename = argv[2];
	}else if(argc == 3 && !strcmp(argv[1], "--client-stats")){
		client_stats = 1;
		do_print = 0;
		filename = argv[2];
	}else{
		fprintf(stderr, "Usage: db_dump [--stats | --client-stats] <mosquitto db filename>\n");
		return 1;
	}
	memset(&db, 0, sizeof(struct mosquitto_db));
	fd = fopen(filename, "rb");
	if(!fd) return 0;
	read_e(fd, &header, 15);
	if(!memcmp(header, magic, 15)){
		if(do_print) printf("Mosquitto DB dump\n");
		// Restore DB as normal
		read_e(fd, &crc, sizeof(uint32_t));
		if(do_print) printf("CRC: %d\n", crc);
		read_e(fd, &i32temp, sizeof(uint32_t));
		db_version = ntohl(i32temp);
		if(do_print) printf("DB version: %d\n", db_version);

		while(rlen = fread(&i16temp, sizeof(uint16_t), 1, fd), rlen == 1){
			chunk = ntohs(i16temp);
			read_e(fd, &i32temp, sizeof(uint32_t));
			length = ntohl(i32temp);
			switch(chunk){
				case DB_CHUNK_CFG:
					cfg_count++;
					if(do_print) printf("DB_CHUNK_CFG:\n");
					if(do_print) printf("\tLength: %d\n", length);
					read_e(fd, &i8temp, sizeof(uint8_t)); // shutdown
					if(do_print) printf("\tShutdown: %d\n", i8temp);
					read_e(fd, &i8temp, sizeof(uint8_t)); // sizeof(dbid_t)
					if(do_print) printf("\tDB ID size: %d\n", i8temp);
					if(i8temp != sizeof(dbid_t)){
						fprintf(stderr, "Error: Incompatible database configuration (dbid size is %d bytes, expected %ld)",
								i8temp, sizeof(dbid_t));
						fclose(fd);
						return 1;
					}
					read_e(fd, &i64temp, sizeof(dbid_t));
					if(do_print) printf("\tLast DB ID: %ld\n", (long)i64temp);
					break;

				case DB_CHUNK_MSG_STORE:
					msg_store_count++;
					struct db_msg msg = {0};
					if(db__msg_store_chunk_restore(&db, fd, length, &msg)) return 1;
					if(do_print) {
						print_db_msg(&msg, length);
					}
					free__db_msg(&msg);
					break;

				case DB_CHUNK_CLIENT_MSG:
					client_msg_count++;
					struct db_client_msg cmsg = {0};
					if(db__client_msg_chunk_restore(&db, fd, length, &cmsg)) return 1;
					if(do_print) {
						print_db_client_msg(&cmsg, length);
					}
					free__db_client_msg(&cmsg);
					break;

				case DB_CHUNK_RETAIN:
					retain_count++;
					if(do_print) printf("DB_CHUNK_RETAIN:\n");
					if(do_print) printf("\tLength: %d\n", length);
					if(db__retain_chunk_restore(&db, fd)) return 1;
					break;

				case DB_CHUNK_SUB:
					sub_count++;
					struct db_sub sub = {0};
					if(db__sub_chunk_restore(&db, fd, length, &sub)) return 1;
					if(do_print) {
						print_db_sub(&sub, length);
					}
					free__db_sub(&sub);
					break;

				case DB_CHUNK_CLIENT:
					client_count++;
					struct db_client client = {0};
					if(db__client_chunk_restore(&db, fd, &client)) return 1;
					if(do_print) {
						print_db_client(&client, length);
					}
					free__db_client(&client);
					break;

				default:
					fprintf(stderr, "Warning: Unsupported chunk \"%d\" in persistent database file. Ignoring.\n", chunk);
					fseek(fd, length, SEEK_CUR);
					break;
			}
		}
		if(rlen < 0) goto error;
	}else{
		fprintf(stderr, "Error: Unrecognised file format.");
		rc = 1;
	}

	fclose(fd);

	if(stats){
		printf("DB_CHUNK_CFG:        %ld\n", cfg_count);
		printf("DB_CHUNK_MSG_STORE:  %ld\n", msg_store_count);
		printf("DB_CHUNK_CLIENT_MSG: %ld\n", client_msg_count);
		printf("DB_CHUNK_RETAIN:     %ld\n", retain_count);
		printf("DB_CHUNK_SUB:        %ld\n", sub_count);
		printf("DB_CHUNK_CLIENT:     %ld\n", client_count);
	}

	if(client_stats){
		HASH_ITER(hh_id, clients_by_id, cc, cc_tmp){
			printf("SC: %d SS: %d MC: %d MS: %ld   ", cc->subscriptions, cc->subscription_size, cc->messages, cc->message_size);
			printf("%s\n", cc->id);
			free(cc->id);
		}
	}

	return rc;
error:
	fprintf(stderr, "Error: %s.", strerror(errno));
	if(fd >= 0) fclose(fd);
	return 1;
}

