#include <stdint.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/queue.h>
#include <errno.h>
#include <limits.h>
#include <ctype.h>

#include "lang.h"
#include "parser.h"
#ifdef GFS2_HAS_UUID
#include <uuid.h>
#endif

const char* ast_type_string[] = {
	[AST_NONE] = "NONE",
	// Statements
	[AST_ST_SET] = "SET",
	[AST_ST_GET] = "GET",

	// Expressions
	[AST_EX_ID] = "IDENTIFIER",
	[AST_EX_NUMBER] = "NUMBER",
	[AST_EX_STRING] = "STRING",
	[AST_EX_ADDRESS] = "ADDRESS",
	[AST_EX_PATH] = "PATH",
	[AST_EX_SUBSCRIPT] = "SUBSCRIPT",
	[AST_EX_OFFSET] = "OFFSET",
	[AST_EX_BLOCKSPEC] = "BLOCKSPEC",
	[AST_EX_STRUCTSPEC] = "STRUCTSPEC",
	[AST_EX_FIELDSPEC] = "FIELDSPEC",
	[AST_EX_TYPESPEC] = "TYPESPEC",

	// Keywords
	[AST_KW_STATE] = "STATE",
};

/**
 * Initialize an expression node of the given type from a source string.
 * Currently just converts numerical values and string values where
 * appropriate. String values are duplicted into newly allocated buffers as the
 * text from the parser will go away.
 * Returns 0 on success or non-zero with errno set on failure
 */
static int ast_expr_init(struct ast_node *expr, ast_node_t type, const char *str)
{
	int ret = 0;
	switch (type) {
	case AST_EX_OFFSET:
		str++; // Cut off the +
	case AST_EX_NUMBER:
		ret = sscanf(str, "%"SCNi64, &expr->ast_num);
		if (ret != 1) {
			return 1;
		}
		break;
	case AST_EX_ID:
	case AST_EX_PATH:
	case AST_EX_STRING:
		expr->ast_str = strdup(str);
		if (expr->ast_str == NULL) {
			return 1;
		}
		break;
	case AST_EX_ADDRESS:
	case AST_EX_SUBSCRIPT:
	case AST_EX_BLOCKSPEC:
	case AST_EX_STRUCTSPEC:
	case AST_EX_FIELDSPEC:
	case AST_EX_TYPESPEC:
	case AST_KW_STATE:
		break;
	default:
		errno = EINVAL;
		return 1;
	}
	return 0;
}

/**
 * Create a new AST node of a given type from a source string.
 * Returns a pointer to the new node or NULL on failure with errno set.
 */
struct ast_node *ast_new(ast_node_t type, const char *text)
{
	struct ast_node *node;
	node = (struct ast_node *)calloc(1, sizeof(struct ast_node));
	if (node == NULL) {
		goto return_fail;
	}

	if (type > _AST_EX_START && ast_expr_init(node, type, text)) {
		goto return_free;
	}

	node->ast_text = strdup(text);
	if (node->ast_text == NULL) {
		goto return_free;
	}
	node->ast_type = type;

	return node;

return_free:
	if (node->ast_text) {
		free(node->ast_text);
	}
	if (node->ast_str) {
		free(node->ast_str);
	}
	free(node);
return_fail:
	fprintf(stderr, "Failed to create new value from %s: %s\n", text, strerror(errno));
	return NULL;
}

/**
 * Free the memory allocated for an AST node and set its pointer to NULL
 */
void ast_destroy(struct ast_node **node)
{
	if (*node == NULL) {
		return;
	}
	ast_destroy(&(*node)->ast_left);
	ast_destroy(&(*node)->ast_right);
	switch((*node)->ast_type) {
	case AST_EX_ID:
	case AST_EX_PATH:
	case AST_EX_STRING:
		free((*node)->ast_str);
		break;
	default:
		break;
	}
	free((*node)->ast_text);
	free(*node);
	*node = NULL;
}

static void ast_string_unescape(char *str)
{
	int head, tail;
	for (head = tail = 0; str[head] != '\0'; head++, tail++) {
		if (str[head] == '\\' && str[head+1] != '\0')
			head++;
		str[tail] = str[head];
	}
	str[tail] = '\0';
}

static uint64_t ast_lookup_path(char *path, struct gfs2_sbd *sbd)
{
	int err = 0;
	char *c = NULL;
	struct gfs2_inode *ip, *iptmp;
	char *segment;
	uint64_t bn = 0;

	segment = strtok_r(path, "/", &c);
	ip = lgfs2_inode_read(sbd, sbd->sd_sb.sb_root_dir.no_addr);

	while (ip != NULL) {
		if (segment == NULL) { // No more segments
			bn = ip->i_di.di_num.no_addr;
			inode_put(&ip);
			return bn;
		}
		ast_string_unescape(segment);
		err = gfs2_lookupi(ip, segment, strlen(segment), &iptmp);
		inode_put(&ip);
		if (err != 0) {
			errno = -err;
			break;
		}
		ip = iptmp;
		segment = strtok_r(NULL, "/", &c);
	}

	return 0;
}

enum block_id {
	ID_SB	= 0,
	ID_MASTER,
	ID_ROOT,
	ID_RINDEX,

	ID_END
};

/**
 * Names of blocks which can be uniquely identified in the fs
 */
static const char *block_ids[] = {
	[ID_SB]		= "sb",
	[ID_MASTER]	= "master",
	[ID_ROOT]	= "root",
	[ID_RINDEX]	= "rindex",

	[ID_END]	= NULL
};

static uint64_t ast_lookup_id(const char *id, struct gfs2_sbd *sbd)
{
	uint64_t bn = 0;
	int i;
	for (i = 0; i < ID_END; i++) {
		if (!strcmp(id, block_ids[i])) {
			break;
		}
	}
	switch (i) {
	case ID_SB:
		bn = LGFS2_SB_ADDR(sbd);
		break;
	case ID_MASTER:
		bn = sbd->sd_sb.sb_master_dir.no_addr;
		break;
	case ID_ROOT:
		bn = sbd->sd_sb.sb_root_dir.no_addr;
		break;
	case ID_RINDEX:
		bn = sbd->md.riinode->i_di.di_num.no_addr;
		break;
	default:
		return 0;
	}
	return bn;
}

static uint64_t ast_lookup_rgrp(uint64_t rgnum, struct gfs2_sbd *sbd)
{
	uint64_t i = rgnum;
	struct osi_node *n;

	for (n = osi_first(&sbd->rgtree); n != NULL && i > 0; n = osi_next(n), i--);
	if (n != NULL && i == 0)
		return ((struct rgrp_tree *)n)->ri.ri_addr;
	fprintf(stderr, "Resource group number out of range: %"PRIu64"\n", rgnum);
	return 0;
}

static uint64_t ast_lookup_subscript(struct ast_node *id, struct ast_node *index,
                                     struct gfs2_sbd *sbd)
{
	uint64_t bn = 0;
	const char *name = id->ast_str;
	if (!strcmp(name, "rgrp")) {
		bn = ast_lookup_rgrp(index->ast_num, sbd);
	} else {
		fprintf(stderr, "Unrecognized identifier %s\n", name);
	}
	return bn;
}

/**
 * Look up a block and return its number. The kind of lookup depends on the
 * type of the ast node.
 */
static uint64_t ast_lookup_block_num(struct ast_node *ast, struct gfs2_sbd *sbd)
{
	uint64_t bn = 0;
	switch (ast->ast_type) {
	case AST_EX_OFFSET:
		bn = ast_lookup_block_num(ast->ast_left, sbd) + ast->ast_num;
		break;
	case AST_EX_ADDRESS:
		if (gfs2_check_range(sbd, ast->ast_num))
			break;
		bn = ast->ast_num;
		break;
	case AST_EX_PATH:
		bn = ast_lookup_path(ast->ast_str, sbd);
		break;
	case AST_EX_ID:
		bn = ast_lookup_id(ast->ast_str, sbd);
		break;
	case AST_EX_SUBSCRIPT:
		bn = ast_lookup_subscript(ast->ast_left, ast->ast_left->ast_left, sbd);
		break;
	default:
		break;
	}
	return bn;
}

static uint64_t ast_lookup_block(struct ast_node *node, struct gfs2_sbd *sbd)
{
	uint64_t bn = ast_lookup_block_num(node, sbd);
	if (bn == 0) {
		fprintf(stderr, "Block not found: %s\n", node->ast_text);
		return 0;
	}
	return bn;
}

static const char *bitstate_strings[] = {
	[GFS2_BLKST_FREE] = "Free",
	[GFS2_BLKST_USED] = "Used",
	[GFS2_BLKST_UNLINKED] = "Unlinked",
	[GFS2_BLKST_DINODE] = "Dinode"
};

/**
 * Print a representation of an arbitrary field of an arbitrary GFS2 block to stdout
 * Returns 0 if successful, 1 otherwise
 */
static int field_print(char *buf, uint64_t addr, const struct lgfs2_metadata *mtype,
                      const struct lgfs2_metafield *field)
{
	const char *fieldp = buf + field->offset;

	printf("%s\t%"PRIu64"\t%u\t%u\t%s\t", mtype->name, addr, field->offset, field->length, field->name);
	if (field->flags & LGFS2_MFF_UUID) {
#ifdef GFS2_HAS_UUID
		char readable_uuid[36+1];
		uuid_t uuid;

		memcpy(uuid, fieldp, sizeof(uuid_t));
		uuid_unparse(uuid, readable_uuid);
		printf("'%s'\n", readable_uuid);
#endif
	} else if (field->flags & LGFS2_MFF_STRING) {
		printf("'%s'\n", fieldp);
	} else {
		switch(field->length) {
		case 1:
			printf("%"PRIu8"\n", *(uint8_t *)fieldp);
			break;
		case 2:
			printf("%"PRIu16"\n", be16_to_cpu(*(uint16_t *)fieldp));
			break;
		case 4:
			printf("%"PRIu32"\n", be32_to_cpu(*(uint32_t *)fieldp));
			break;
		case 8:
			printf("%"PRIu64"\n", be64_to_cpu(*(uint64_t *)fieldp));
			break;
		default:
			// "Reserved" field so just print 0
			printf("0\n");
			return 1;
		}
	}
	return 0;
}

/**
 * Print a representation of an arbitrary GFS2 block to stdout
 */
int lgfs2_lang_result_print(struct lgfs2_lang_result *result)
{
	int i;
	if (result->lr_mtype != NULL) {
		for (i = 0; i < result->lr_mtype->nfields; i++) {
			field_print(result->lr_buf, result->lr_blocknr,
			            result->lr_mtype, &result->lr_mtype->fields[i]);
		}
	} else {
		printf("%"PRIu64": %s\n", result->lr_blocknr, bitstate_strings[result->lr_state]);
	}
	return 0;
}

static int ast_get_bitstate(uint64_t bn, struct gfs2_sbd *sbd)
{
	int ret = 0;
	int state = 0;
	struct rgrp_tree *rgd = gfs2_blk2rgrpd(sbd, bn);
	if (rgd == NULL) {
		fprintf(stderr, "Could not find resource group for block %"PRIu64"\n", bn);
		return -1;
	}

	ret = gfs2_rgrp_read(sbd, rgd);
	if (ret != 0) {
		fprintf(stderr, "Failed to read resource group for block %"PRIu64": %d\n", bn, ret);
		return -1;
	}

	state = lgfs2_get_bitmap(sbd, bn, rgd);
	if (state == -1) {
		fprintf(stderr, "Failed to acquire bitmap state for block %"PRIu64"\n", bn);
		return -1;
	}

	gfs2_rgrp_relse(sbd, rgd);
	return state;
}

static void result_lookup_mtype(struct lgfs2_lang_result *result, int gfs1)
{
	const uint32_t mh_type = lgfs2_get_block_type(result->lr_buf);
	uint64_t addr = result->lr_blocknr;

	result->lr_mtype = NULL;
	if (mh_type == 0) {
		fprintf(stderr, "Could not determine type for block %"PRIu64"\n", addr);
		return;
	}
	result->lr_mtype = lgfs2_find_mtype(mh_type, gfs1 ? LGFS2_MD_GFS1 : LGFS2_MD_GFS2);
	if (result->lr_mtype == NULL)
		fprintf(stderr, "Could not determine meta type for block %"PRIu64"\n", addr);
}

static char *lang_read_block(int fd, unsigned bsize, uint64_t addr)
{
	off_t off = addr * bsize;
	char *buf;

	buf = calloc(1, bsize);
	if (buf == NULL) {
		perror("Failed to read block");
		return NULL;
	}
	if (pread(fd, buf, bsize, off) != bsize) {
		fprintf(stderr, "Failed to read block %"PRIu64": %s\n", addr, strerror(errno));
		free(buf);
		return NULL;
	}
	return buf;
}

/**
 * Interpret the get statement.
 */
static struct lgfs2_lang_result *ast_interp_get(struct lgfs2_lang_state *state,
                                     struct ast_node *ast, struct gfs2_sbd *sbd)
{
	struct lgfs2_lang_result *result = calloc(1, sizeof(struct lgfs2_lang_result));
	if (result == NULL) {
		fprintf(stderr, "Failed to allocate memory for result\n");
		return NULL;
	}

	if (ast->ast_right->ast_right == NULL) {
		result->lr_blocknr = ast_lookup_block(ast->ast_right, sbd);
		if (result->lr_blocknr == 0) {
			free(result);
			return NULL;
		}
		result->lr_buf = lang_read_block(sbd->device_fd, sbd->bsize, result->lr_blocknr);
		if (result->lr_buf == NULL) {
			free(result);
			return NULL;
		}
		result_lookup_mtype(result, sbd->gfs1);

	} else if (ast->ast_right->ast_right->ast_type == AST_KW_STATE) {
		result->lr_blocknr = ast_lookup_block_num(ast->ast_right, sbd);
		if (result->lr_blocknr == 0) {
			free(result);
			return NULL;
		}
		result->lr_state = ast_get_bitstate(result->lr_blocknr, sbd);
	}

	return result;
}

/**
 * Set a field of a gfs2 block of a given type to a given value.
 * Returns AST_INTERP_* to signal success, an invalid field/value or an error.
 */
static int ast_field_set(char *buf, const struct lgfs2_metafield *field,
                         struct ast_node *val)
{
	int err = 0;

	if (field->flags & LGFS2_MFF_UUID) {
#ifdef GFS2_HAS_UUID
		uuid_t uuid;

		if (uuid_parse(val->ast_str, uuid) != 0) {
			fprintf(stderr, "Invalid UUID\n");
			return AST_INTERP_INVAL;
		}
		err = lgfs2_field_assign(buf, field, uuid);
#else
		fprintf(stderr, "No UUID support\n");
		err = 1;
#endif
	} else if (field->flags & LGFS2_MFF_STRING) {
		err = lgfs2_field_assign(buf, field, val->ast_str);
	} else {
		err = lgfs2_field_assign(buf, field, &val->ast_num);
	}

	if (err) {
		fprintf(stderr, "Invalid field assignment: %s (size %d) = %s\n",
	                field->name, field->length, val->ast_text);
		return AST_INTERP_INVAL;
	}
	return AST_INTERP_SUCCESS;
}

static const struct lgfs2_metadata *lang_find_mtype(struct ast_node *node, const char *buf, unsigned ver)
{
	const struct lgfs2_metadata *mtype = NULL;

	if (node->ast_type == AST_EX_TYPESPEC) {
		mtype = lgfs2_find_mtype_name(node->ast_str, ver);
		if (mtype == NULL)
			fprintf(stderr, "Invalid block type: %s\n", node->ast_text);
	} else {
		mtype = lgfs2_find_mtype(lgfs2_get_block_type(buf), ver);
		if (mtype == NULL)
			fprintf(stderr, "Unrecognised block at: %s\n", node->ast_text);
	}

	return mtype;
}

static int lang_write_result(int fd, unsigned bsize, struct lgfs2_lang_result *result)
{
	off_t off = bsize * result->lr_blocknr;

	if (pwrite(fd, result->lr_buf, bsize, off) != bsize) {
		fprintf(stderr, "Failed to write modified block %"PRIu64": %s\n",
		                result->lr_blocknr, strerror(errno));
		return -1;
	}
	return 0;
}

/**
 * Interpret an assignment (set)
 */
static struct lgfs2_lang_result *ast_interp_set(struct lgfs2_lang_state *state,
                                    struct ast_node *ast, struct gfs2_sbd *sbd)
{
	struct ast_node *lookup = ast->ast_right;
	struct ast_node *fieldspec;
	struct ast_node *fieldname;
	struct ast_node *fieldval;
	int ret = 0;
	unsigned ver = sbd->gfs1 ? LGFS2_MD_GFS1 : LGFS2_MD_GFS2;

	struct lgfs2_lang_result *result = calloc(1, sizeof(struct lgfs2_lang_result));
	if (result == NULL) {
		fprintf(stderr, "Failed to allocate memory for result\n");
		return NULL;
	}

	result->lr_blocknr = ast_lookup_block(lookup, sbd);
	if (result->lr_blocknr == 0)
		goto out_err;
	result->lr_buf = lang_read_block(sbd->device_fd, sbd->bsize, result->lr_blocknr);
	if (result->lr_buf == NULL)
		goto out_err;

	result->lr_mtype = lang_find_mtype(lookup->ast_right, result->lr_buf, ver);
	if (result->lr_mtype == NULL) {
		fprintf(stderr, "Unrecognised block at: %s\n", lookup->ast_str);
		goto out_err;
	}

	if (lookup->ast_right->ast_type == AST_EX_TYPESPEC) {
		struct gfs2_meta_header mh = {
			.mh_magic = GFS2_MAGIC,
			.mh_type = result->lr_mtype->mh_type,
			.mh_format = result->lr_mtype->mh_format,
		};
		gfs2_meta_header_out(&mh, result->lr_buf);
		lookup = lookup->ast_right;
	}

	for (fieldspec = lookup->ast_right;
	     fieldspec != NULL && fieldspec->ast_type == AST_EX_FIELDSPEC;
	     fieldspec = fieldspec->ast_left) {
		const struct lgfs2_metafield *mfield;

		fieldname = fieldspec->ast_right;
		fieldval = fieldname->ast_right;

		mfield = lgfs2_find_mfield_name(fieldname->ast_str, result->lr_mtype);
		if (mfield == NULL) {
			fprintf(stderr, "No field '%s' found in '%s'\n",
			        fieldname->ast_str, result->lr_mtype->name);
			goto out_err;
		}

		ret = ast_field_set(result->lr_buf, mfield, fieldval);
		if (ret != AST_INTERP_SUCCESS) {
			goto out_err;
		}
	}

	ret = lang_write_result(sbd->device_fd, sbd->bsize, result);
	if (ret != 0)
		goto out_err;

	return result;
out_err:
	lgfs2_lang_result_free(&result);
	return NULL;
}

static struct lgfs2_lang_result *ast_interpret_node(struct lgfs2_lang_state *state,
                                        struct ast_node *ast, struct gfs2_sbd *sbd)
{
	struct lgfs2_lang_result *result = NULL;

	if (ast->ast_type == AST_ST_SET) {
		result = ast_interp_set(state, ast, sbd);
	} else if (ast->ast_type == AST_ST_GET) {
		result = ast_interp_get(state, ast, sbd);
	} else {
		fprintf(stderr, "Invalid AST node type: %d\n", ast->ast_type);
	}
	return result;
}

struct lgfs2_lang_result *lgfs2_lang_result_next(struct lgfs2_lang_state *state,
                                                           struct gfs2_sbd *sbd)
{
	struct lgfs2_lang_result *result;
	if (state->ls_interp_curr == NULL) {
		return NULL;
	}
	result = ast_interpret_node(state, state->ls_interp_curr, sbd);
	if (result == NULL) {
		return NULL;
	}
	state->ls_interp_curr = state->ls_interp_curr->ast_left;
	return result;
}

void lgfs2_lang_result_free(struct lgfs2_lang_result **result)
{
	if (*result == NULL) {
		fprintf(stderr, "Warning: attempted to free a null result\n");
		return;
	}
	free((*result)->lr_buf);
	free(*result);
	*result = NULL;
}
