#define FUSE_USE_VERSION 26
#define __STDC_FORMAT_MACROS

#include <stdlib.h>
#include <memory.h>
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
#include <errno.h>
#include <sys/mman.h>
#include <syslog.h>

#include <fuse/fuse.h>
#define SBF_MAX_CGS 256
#define SBF_MAX_FILE_HEADERS 258
#define SBFFS(p) ((struct sbffs*)(p))
#define PACKED __attribute__((__packed__))

#define CHANGE_ENDIANNESS(x, to)		\
do {						\
	typeof(x) tmp = 0;			\
	int sz = sizeof(x);			\
	while (sz-- > 0) {			\
		tmp <<= 8;			\
		tmp += (x & 0xff);		\
		x >>= 8;			\
	}					\
	to = tmp;				\
} while (0)

#ifndef MACH_BIG_ENDIAN
  #define FROM_BE(x, y) CHANGE_ENDIANNESS(x, y)
  #define FROM_LE(x, y) y = x
#else
  #define FROM_BE(x, y) y = x
  #define FROM_LE(x, y) CHANGE_ENDIANNESS(x, y)
#endif // BIG_ENDIAN

#define INPLACE_BE(x) FROM_BE(x, x)
#define INPLACE_LE(x) FROM_LE(x, x)


static void *sbffs_init(struct fuse_conn_info *conn);
static void sbffs_destroy(void *handle);
static int sbffs_readdir(const char *path, void *buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi);
static int sbffs_getattr(const char *path, struct stat *statbuf);
static int sbffs_open(const char *path, struct fuse_file_info *fi);
static int sbffs_read(const char *path, char *buf, size_t size, off_t offset, struct fuse_file_info *fi);

struct shx_header {
	uint8_t  type[256];
	uint8_t  v1[32];
	uint8_t  v2[32];
	uint8_t  date[128];
	uint8_t  info[256];
	uint8_t  unkn1[80];
	uint8_t  v3[160];
	uint32_t cgcount;
} PACKED;

struct shx_file_header {
	uint32_t startofs;
	uint32_t endofs;
	uint32_t desc[4];
	uint16_t checksum;
	uint16_t code;
} PACKED;

struct sbf_cg_descriptor {
	uint32_t exists;
	uint32_t start;
	uint32_t size;
	off_t    offset;
	uint32_t checksum;
};

struct sbf_ctx {
	char *mapping;
	int fd;
	off_t filesize;
	struct sbf_cg_descriptor ramloader;
	struct sbf_cg_descriptor cgs[SBF_MAX_CGS];
};
		
struct sbffs {
	const char *sbffile;
	const char *mountpoint;
	struct sbf_ctx sbf_ctx;
};

struct fuse_opt fuse_opts[] = {
	FUSE_OPT_END
};

static struct fuse_operations sbffs_ops = {
	.init		=	sbffs_init,
	.destroy	=	sbffs_destroy,
	.readdir	=	sbffs_readdir,
	.getattr	=	sbffs_getattr,
	.open		=	sbffs_open,
	.read		=	sbffs_read
};

static struct sbffs *get_sbffs() {
	return SBFFS(fuse_get_context()->private_data);
}

static void *sbffs_init(struct fuse_conn_info *conn) {
	return (void*)get_sbffs();
}

static void sbffs_destroy(void *handle) {
	struct sbffs *pSbffs = SBFFS(handle);
	munmap(pSbffs->sbf_ctx.mapping, pSbffs->sbf_ctx.filesize);
	close(pSbffs->sbf_ctx.fd);
	closelog();
}

static void sbffs_add_direntry(struct sbf_cg_descriptor *desc, const char *name, void *buf, fuse_fill_dir_t filler) {
	char *fullname;
	size_t fullsize;

	fullsize = strlen(name) + 1 + 8 + 1; // name + @ + xxxxxxxx + \0
	fullname = (char*)alloca(fullsize);
	snprintf(fullname, fullsize, "%s@%08"PRIx32, name, desc->start);
	filler(buf, fullname, NULL, 0);
}

static int sbffs_readdir(const char *path, void *buf, fuse_fill_dir_t filler, off_t offset, struct fuse_file_info *fi) {
	struct sbffs *pSbffs = get_sbffs();
	uint8_t i;

	if (strcmp(path, "/") != 0) {
		return -ENOENT;
	}
	filler(buf, ".", NULL, 0);
	filler(buf, "..", NULL, 0);
	if (pSbffs->sbf_ctx.ramloader.exists) {
		sbffs_add_direntry(&pSbffs->sbf_ctx.ramloader, "ramloader", buf, filler);
	}
	for (i = 0; i != SBF_MAX_CGS-1; ++i) {
		char numbuf[3+1];
		if (pSbffs->sbf_ctx.cgs[i].exists) {
			snprintf(numbuf, sizeof(numbuf), "%"PRIu8, i);
			numbuf[sizeof(numbuf)-1] = '\0';
			sbffs_add_direntry(&pSbffs->sbf_ctx.cgs[i], numbuf, buf, filler);
		}
	}
	return 0;
}

struct sbf_cg_descriptor *sbffs_parse_fullname(const char *path) {
	struct sbffs *pSbffs = get_sbffs();
	struct sbf_cg_descriptor *ret = NULL;
	char name[10+1]; // 10 characters should be enogh for name
	uint32_t addr = 0xdeadbeaf;
	char c;
	
	memset(name, 0, sizeof(name));
	sscanf(path, "/%10[^@]%c%08"SCNx32, name, &c, &addr);
	if (c != '@') {
		return NULL;
	}
	if (strcmp(name, "ramloader") == 0) {
		ret = &pSbffs->sbf_ctx.ramloader;
	} else if ((strspn(name, "0123456789") == strlen(name)) && (name[0] != '0') && (strlen(name) <= 3)) {
		int num = atoi(name);
		if (num < SBF_MAX_CGS) {
			ret = &pSbffs->sbf_ctx.cgs[num];
		}
	}
	if (ret && ret->exists) {
		if (addr == ret->start) {
			return ret;
		}
	}
	return NULL;
}

static int sbffs_getattr(const char *path, struct stat *statbuf) {
	struct sbf_cg_descriptor *desc;

	memset(statbuf, 0, sizeof(struct stat));

	if (strcmp(path, "/") == 0) {
		statbuf->st_mode = S_IFDIR | 0755;
		statbuf->st_nlink = 2;
	} else if ((desc = sbffs_parse_fullname(path)) != NULL) {
		statbuf->st_mode = S_IFREG | 0444;
		statbuf->st_nlink = 1;
		statbuf->st_size = desc->size;
	} else {
		return -ENOENT;
	}

	return 0;
}

static int sbffs_open(const char *path, struct fuse_file_info *fi) {
	struct sbf_cg_descriptor *desc;

	if (fi->flags & 3 != O_RDONLY) {
		return -EACCES;
	}

	desc = sbffs_parse_fullname(path);
	if (desc == NULL) {
		return -ENOENT;
	}

	fi->fh = (uint64_t)(void*)desc;
	return 0;
}

static int sbffs_read(const char *path, char *buf, size_t size, off_t offset, struct fuse_file_info *fi) {
	struct sbffs *pSbffs = get_sbffs();
	struct sbf_cg_descriptor *desc = (struct sbf_cg_descriptor*)fi->fh;

	if (offset < desc->size) {
		if (offset + size > desc->size) {
			size = desc->size - offset;
		}
		memcpy(buf, pSbffs->sbf_ctx.mapping + desc->offset + offset, size);
	} else {
		size = 0;
	}

	return size;
}
static int sbffs_opt_proc(void *data, const char *arg, int key, struct fuse_args *outargs) {
	struct sbffs *pSbffs = SBFFS(data);
	switch (key) {
		case FUSE_OPT_KEY_OPT:
			return -1;
		case FUSE_OPT_KEY_NONOPT:
			if (pSbffs->sbffile == NULL) {
				pSbffs->sbffile = arg;
				return 0;
			} else if (pSbffs->mountpoint == NULL) {
				pSbffs->mountpoint = arg;
				return 1;
			}
			fprintf(stderr, "unknown opt `%s'\n", arg);
			return -1;
#if 0
		case FUSE_KEY_HELP:
			sbffs_usage();
			return 0;
		case KEY_VERBOSE:
			pSbffs->verbose = 1;
			return 0;
		case KEY_VERSION:
			fprintf(stderr, "sbffs version unknown\n");
			exit(1);
#endif
		default:
			exit(1);
	}
}

off_t filesize(int fd) {
	off_t size;
	size = lseek(fd, 0, SEEK_END);
	lseek(fd, 0, SEEK_SET);
	return size;
}

void sbf_finder_init_ctx(off_t *fctx) {
	*fctx = sizeof(struct shx_header) + SBF_MAX_FILE_HEADERS*sizeof(struct shx_file_header);
}

off_t sbf_finder_append_descriptor(off_t *fctx, struct sbf_ctx *sctx, struct sbf_cg_descriptor *desc) {
	off_t full_cg_size, offset;
	uint32_t addr, size;
	char *cgbase;

	full_cg_size = 9 + 4 + 4 + desc->size + 12; // pre-junk, size, addr, CG, post-junk

	if (*fctx + full_cg_size > sctx->filesize) {
		fprintf(stderr, "SBF file is truncated. Sad but true.\n");
		return -1;
	}

	cgbase = sctx->mapping + *fctx;
	size = *((uint32_t*)(cgbase + 9));
	INPLACE_BE(size);
	addr = *((uint32_t*)(cgbase + 9 + 4));
	INPLACE_BE(addr);
	if ((addr != desc->start) || (size != desc->size)) {
		fprintf(stderr, "Disagreements about CG addr/size.\n");
		return -1;
	}
	offset = *fctx + 9 + 4 + 4;
	*fctx += full_cg_size;
	return offset;
}

int sbf_verify_header(struct sbf_ctx *ctx) {
	struct shx_header *hdr;
	static const uint8_t valid_type[] = {'P', '2', 'K', ' ', 'S', 'u', 'p', 'e', 'r', 'f', 'i', 'l', 'e'};
	
	if (ctx->filesize < sizeof(struct shx_header)) {
		fprintf(stderr, "SBF header is truncated.\n");
		return -1;
	}

	hdr = (struct shx_header*)ctx->mapping;
	if (memcmp(hdr->type, valid_type, sizeof(valid_type)) != 0) {
		fprintf(stderr, "SBF header is invalid. You gave me a junk instead of SBF file, didn't you?\n");
		return -1;
	}
	return 0;
}

int sbf_fill_cgs(struct sbf_ctx *ctx) {
	struct shx_file_header *fhdr;
	off_t finder_ctx;
	int i;

	if (ctx->filesize < sizeof(struct shx_header) + SBF_MAX_FILE_HEADERS * sizeof(struct shx_file_header)) {
		fprintf(stderr, "SBF fhdrs are truncated.\n");
		return -1;
	}
	fhdr = (struct shx_file_header*)(ctx->mapping + sizeof(struct shx_header));
	sbf_finder_init_ctx(&finder_ctx);
	for (i = 0; i < SBF_MAX_FILE_HEADERS; ++i, ++fhdr) {
		struct sbf_cg_descriptor *descriptor;

		if (fhdr->startofs == 0) {
			continue;
		}
		switch (i) {
			case 0:				// ramloader
				descriptor = &ctx->ramloader;
				break;
			case 1:				// don't know what is it
				descriptor = NULL;
				break;
			default:
				descriptor = &ctx->cgs[i-2];
				break;
		}
		if (descriptor == NULL) {
			continue;
		}
		descriptor->exists = 1;
		descriptor->start = fhdr->startofs;
		descriptor->size = fhdr->endofs - fhdr->startofs + 1;
		descriptor->offset = sbf_finder_append_descriptor(&finder_ctx, ctx, descriptor);
		if (descriptor->offset == (off_t)-1) {
			return -1;
		}
		printf("start %08x, size %08x, offset %08llx, cg %d\n", descriptor->start, descriptor->size, descriptor->offset, i);
	}
	return 0;
}
			
int sbf_context_init(const char *sbffile, struct sbf_ctx *ctx) {
	memset(ctx, 0, sizeof(struct sbf_ctx));
	
	ctx->fd = open(sbffile, O_RDONLY);
	if (ctx->fd == -1) {
		perror("open");
		goto out;
	}

	ctx->filesize = filesize(ctx->fd);
	if (ctx->filesize == (off_t)-1) {
		perror("lseek");
		goto out;
	}

	ctx->mapping = mmap(NULL, (size_t)ctx->filesize, PROT_READ, MAP_PRIVATE, ctx->fd, 0);
	if (ctx->mapping == (void*)-1) {
		perror("mmap");
		goto out;
	}

	if (sbf_verify_header(ctx) != 0) {
		goto out;
	}
	
	if (sbf_fill_cgs(ctx) != 0) {
		goto out;
	}

	return 0;
out:
	close(ctx->fd);
	if (ctx->mapping) munmap(ctx->mapping, ctx->filesize);
	return -1;
}

int main(int argc, char **argv) {
	struct sbffs sbffs;
	struct fuse_args args = FUSE_ARGS_INIT(argc, argv);

	memset(&sbffs, 0, sizeof(struct sbffs));
	if (fuse_opt_parse(&args, &sbffs, fuse_opts, sbffs_opt_proc) == -1) {
		return 1;
	}
	if (sbf_context_init(sbffs.sbffile, &sbffs.sbf_ctx) == -1) {
		return 1;
	}
	fprintf(stderr, "%s %s\n", sbffs.sbffile, sbffs.mountpoint);
	openlog("sbffs", 0, LOG_KERN);
	fuse_main(args.argc, args.argv, &sbffs_ops, (void*)&sbffs);
	return 0;
}
