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

#include <machine/endian.h>

#define UNKNOWN_COMMAND 0
#define CREATE_COMMAND	1
#define REMOVE_COMMAND	2
#define THIN_COMMAND	3
#define REPLACE_COMMAND	4
#define EXTRACT_COMMAND	5
#define INFO_COMMAND	6
#define DINFO_COMMAND	7

#define be32toh(x)	x
#define htobe32(x)	x
#define le16toh(x)	OSSwapLittleToHostInt16(x)

struct fat_header {
	uint32_t magic;
	uint32_t nfat_arch;
};

struct arch {
	uint32_t cputype;
	uint32_t cpusubtype;
	uint32_t offset;
	uint32_t size;
	uint32_t align;
};

struct elf_header{
        unsigned char ident[16];
        uint16_t      type;
        uint16_t      machine;
        uint32_t      version;
        uint32_t      entry;
        uint32_t      phoff;
        uint32_t      shoff;
        uint32_t      flags;
        uint16_t      ehsize;
        uint16_t      phentsize;
        uint16_t      phnum;
        uint16_t      shentsize;
        uint16_t      shnum;
        uint16_t      shstrndx;
};

int extract(char *ofile, char *ifile, int arch_type);
int replace(char *ofile, char *ifile, int arch_type);
int thin(char *ofile, char *ifile, int arch_type);
int remove_file(char *ofile, char *ifile, int arch_type);
int create(char *ofile, char *ifiles[]);
int info(char *ifile);
int detailed_info(char *ifile);
uint32_t copy(FILE *ofp, FILE *ifp, uint32_t offset);
int identify(FILE *fp, uint32_t *cputype, uint32_t *cpusubtype);
char *get_arch_name(uint32_t arch);

void
usage(void)
{
	printf("lipo -info <fat-filename>\n");
	printf("lipo -detailed_info <fat-filename>\n");
	printf("lipo -output <fat-filename> -create <filename> ...\n");
	printf("lipo -output <fat-filename> -remove <arch-type> <fat-filename>\n");
	printf("lipo -output <fat-filename> -thin <arch-type> <fat-filename>\n");
	printf("lipo -output <fat-filename> -replace <arch_type> <filename>\n");
	printf("lipo -output <fat-filename> -extract <arch-type> <fat-filename>\n");
	exit(1);
}

int
main(int argc, char *argv[])
{
	char *ofile = NULL;
	char *ifile = NULL;
	int arch_type = -1;
	int command = UNKNOWN_COMMAND;
	int i;

	for (i = 1; i < argc; i++) {
		if (strcmp(argv[i], "-output") == 0) {
			ofile = argv[++i];
			continue;
		} else if (strcmp(argv[i], "-info") == 0) {
			if (argc != 3)
				usage();
			command = INFO_COMMAND;
			ifile = argv[++i];
		} else if (strcmp(argv[i], "-detailed_info") == 0) {
			if (argc != 3)
				usage();
			command = DINFO_COMMAND;
			ifile = argv[++i];
		} else if (strcmp(argv[i], "-create") == 0) {
			if (command != UNKNOWN_COMMAND)
				usage();
			command = CREATE_COMMAND;
		} else if (strcmp(argv[i], "-remove") == 0) {
			if (command != UNKNOWN_COMMAND)
				usage();
			command = REMOVE_COMMAND;
			arch_type = atoi(argv[++i]);
			ifile = argv[++i];
		} else if (strcmp(argv[i], "-thin") == 0) {
			if (command != UNKNOWN_COMMAND)
				usage();
			command = THIN_COMMAND;
			arch_type = atoi(argv[++i]);
		} else if (strcmp(argv[i], "-replace") == 0) {
			if (command != UNKNOWN_COMMAND)
				usage();
			command = REPLACE_COMMAND;
			arch_type = atoi(argv[++i]);
			ifile = argv[++i];
		} else if (strcmp(argv[i], "-extract") == 0) {
			if (command != UNKNOWN_COMMAND)
				usage();
			command = EXTRACT_COMMAND;
			arch_type = atoi(argv[++i]);
		} else {
			if (command == CREATE_COMMAND)
				break;
			usage();
		}
	}

	switch (command) {
	case INFO_COMMAND:
		if (ifile == NULL)
			usage();
		return info(ifile);
		break;
	case DINFO_COMMAND:
		if (ifile == NULL)
			usage();
		return detailed_info(ifile);
		break;
	case CREATE_COMMAND:
		if (ofile == NULL)
			usage();
		return create(ofile, &argv[i]);
		break;
	case REMOVE_COMMAND:
		if (ifile == NULL || ofile == NULL)
			usage();
		return remove_file(ofile, ifile, arch_type);
		break;
	case THIN_COMMAND:
		if (ifile == NULL || ofile == NULL)
			usage();
		return thin(ofile, ifile, arch_type);
		break;
	case REPLACE_COMMAND:
		if (ifile == NULL || ofile == NULL)
			usage();
		return replace(ofile, ifile, arch_type);
		break;
	case EXTRACT_COMMAND:
		if (ifile == NULL || ofile == NULL)
			usage();
		return extract(ofile, ifile, arch_type);
		break;
	default:
		usage();
	}

	return 1;
}

int
extract(char *ofile, char *ifile, int arch_type)
{
	printf("NOT IMPLEMENTED\n");
	return 1;
}

int
replace(char *ofile, char *ifile, int arch_type)
{
	printf("NOT IMPLEMENTED\n");
	return 1;
}

int
thin(char *ofile, char *ifile, int arch_type)
{
	printf("NOT IMPLEMENTED\n");
	return 1;
}

int
remove_file(char *ofile, char *ifile, int arch_type)
{
	printf("NOT IMPLEMENTED\n");
	return 1;
}

int
create(char *ofile, char *ifiles[])
{
	char **p = ifiles;
	struct fat_header hdr;
	struct arch arch;
	uint32_t offset = 4096;
	uint32_t narch = 0;
	int n;
	FILE *ofp;

	ofp = fopen(ofile, "w+");
	if (ofp == NULL)
		errx(1, "Cannot open '%s' for writing", ofile);

	while (*p)  {
		uint32_t cputype, cpusubtype;
		uint32_t size;
		int rv;
		FILE *ifp;

		ifp = fopen(*p, "rb");
		if (ifp == NULL)
			errx(1, "Cannot open '%s' for reading", *p);

		if (identify(ifp, &cputype, &cpusubtype) != 0)
			errx(1, "unable to determine cpu type");

		size = copy(ofp, ifp, offset);
		if (size == 0)
			errx(1, "unable to copy contents");

		arch.cputype = htobe32(cputype);
		arch.cpusubtype = htobe32(cpusubtype);
		arch.offset = htobe32(offset);
		arch.size = htobe32(size);
		arch.align = htobe32(12);

		rv = fseek(ofp, narch * sizeof(struct arch) + sizeof(struct fat_header), SEEK_SET);
		if (rv != 0)
			errx(1, "fseek");
		n = fwrite(&arch, sizeof(struct arch), 1, ofp);
		if (n != 1)
			errx(1, "Failed to write arch record");

		fclose(ifp);

		size += 4096;
		size &= ~(4096-1);
	
		offset += size;
		narch++;
		p++;
	}
	
	hdr.magic = htobe32(0xcafebabe);
	hdr.nfat_arch = htobe32(narch);
	fseek(ofp, 0, SEEK_SET);
	n = fwrite(&hdr, sizeof(struct fat_header), 1, ofp);
	if (n != 1)
		errx(1, "Cannot write fat header");

	fclose(ofp);

	return 0;
}

int
info(char *ifile)
{
	struct fat_header hdr;
	struct arch arch;
	int i, n;
	FILE *fp = fopen(ifile, "rb");

	if (fp == NULL)
		errx(1, "unable to open file '%s'", ifile);

	n = fread(&hdr, sizeof(struct fat_header), 1, fp);
	if (n != 1)
		errx(1, "unable to read fat header");

	if (be32toh(hdr.magic) != 0xcafebabe)
		errx(1, "Non-fat file");

	printf("Architectures in the fat file: %s are: ", ifile);

	for (i = 0; i < be32toh(hdr.nfat_arch); i++) {
		n = fread(&arch, sizeof(struct arch), 1, fp);
		if (n != 1)
			break;
		printf("%s ", get_arch_name(be32toh(arch.cputype)));
	}
	printf("\n");

	fclose(fp);

	return 0;
}

int
detailed_info(char *ifile)
{
	struct fat_header hdr;
	struct arch arch;
	int i, n;
	FILE *fp = fopen(ifile, "rb");

	if (fp == NULL)
		errx(1, "unable to open file '%s'", ifile);

	n = fread(&hdr, sizeof(struct fat_header), 1, fp);
	if (n != 1)
		errx(1, "unable to read fat header");

	if (be32toh(hdr.magic) != 0xcafebabe)
		errx(1, "Non-fat file");

	printf("Fat header in: %s\n", ifile);
	printf("fat_magic 0x%08x\n", be32toh(hdr.magic));
	printf("nfat_arch %d\n", be32toh(hdr.nfat_arch));

	for (i = 0; i < be32toh(hdr.nfat_arch); i++) {
		n = fread(&arch, sizeof(struct arch), 1, fp);
		if (n != 1)
			break;
		printf("architecture %s\n", get_arch_name(be32toh(arch.cputype)));
		printf("\tcputype 0x%x\n", be32toh(arch.cputype));
		printf("\tcpusubtype 0x%x\n", be32toh(arch.cpusubtype));
		printf("\toffset %d\n", be32toh(arch.offset));
		printf("\tsize %d\n", be32toh(arch.size));
		printf("\talign 2^%d (%d)\n", be32toh(arch.align), 1 << be32toh(arch.align));
	}

	fclose(fp);

	return 0;
}

uint32_t
copy(FILE *ofp, FILE *ifp, uint32_t offset)
{
	char buf[4096];
	char zero = 0;
	uint32_t size = 0;
	int padding = 0;

	fseek(ifp, 0, SEEK_SET);
	fseek(ofp, offset, SEEK_SET);

	for (;;) {
		int nr = fread(buf, 1, 4096, ifp);
		int nw = fwrite(buf, 1, nr, ofp);
		if (nw != nr)
			errx(1, "Unable to copy block");
		size += nw;
		if (nw != 4096)
			break;
	}

	padding = 4096 - (size & (4096-1));
	printf("padding=%d\n", padding);
	while (padding-- > 0)
		if (fwrite(&zero, 1, 1, ofp) != 1)
			errx(1, "cannot write padding");

	return size;
}


int
identify(FILE *fp, uint32_t *cputype, uint32_t *cpusubtype)
{
	struct elf_header hdr;
	int n;

	fseek(fp, 0, SEEK_SET);
	n = fread(&hdr, sizeof(struct elf_header), 1, fp);
	if (n != 1)
		return 1;

	printf("machine=%d (0x%x)\n", le16toh(hdr.machine), le16toh(hdr.machine));

#define LP64 0x1000000
	switch (le16toh(hdr.machine)) {
	case 20:	/* ppc */
		*cputype = 18; *cpusubtype = 0; break;
	case 3:		/* i386 */
		*cputype = 7; *cpusubtype = 3; break;
	case 62:	/* amd642 */
		*cputype = LP64 | 7; *cpusubtype = 3; break;
	case 40:
		*cputype = 12; *cpusubtype = 0; break;
	default:
		return 1;
	}

	return 0;
}

char *
get_arch_name(uint32_t arch)
{
	switch (arch) {
	case 7: return "i386";
	case 18: return "ppc";
	case 0x1000007: return "x86_64";
	case 12: return "arm";
	}

	return "unknown";
}
