#include <sys/types.h>
#include <sys/mman.h>
#include <sys/stat.h>

#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <openssl/sha.h>

#include "benc.h"
#include "metainfo.h"
#include "subr.h"

/*
 * d
 * announce = url
 * info = d
 *   name = advisory file/dir save name
 *   piece length = power of two length of each block
 *   pieces = 20b of sha1-hash * num of pieces
 *   length = length of file in bytes in single file download
 *   files = l d
 *     length = length of file in bytes
 *     path = l path components
 *
 */

#ifndef PRId64
#define PRId64 "lld"
#endif
#ifndef PRIu32
#define PRIu32 "u"
#endif

void
print_metainfo(struct metainfo *tp)
{
    unsigned i;

    printf("Info hash: ");
    for (i = 0; i < 20; i++)
	printf("%.2x", tp->info_hash[i]);
    printf("\n");
    printf("Tracker URL: %s\n", tp->announce);
    printf("Piece length: %" PRId64 "\n", (int64_t)tp->piece_length);
    printf("Number of pieces: %" PRIu32 "\n", tp->npieces);
    printf("Number of files: %u\n", tp->nfiles);
    printf("Advisory name: %s\n", tp->name);
    printf("Files:\n");
    for (i = 0; i < tp->nfiles; i++) {
	printf("%s (%" PRId64 ")\n",
	    tp->files[i].path, (int64_t)tp->files[i].length);
    }
    printf("Total length: %" PRId64 "\n\n", (int64_t)tp->total_length);
}

static int
check_path(const char *path, size_t len)
{
    if (len == 0)
	return 0;
    else if (len == 1 && path[0] == '.')
	return 0;
    else if (len == 2 && path[0] == '.' && path[1] == '.')
	return 0;
    else if (memchr(path, '/', len) != NULL)
	return 0;
    return 1;
}

int
fill_fileinfo(const char *fdct, struct fileinfo *tfp)
{
    int err;
    size_t npath, plen, len;
    const char *plst, *iter, *str;

    if ((err = benc_dget_off(fdct, "length", &tfp->length)) != 0)
	return err;

    if ((err = benc_dget_lst(fdct, "path", &plst)) != 0)
	return err;

    npath = plen = 0;
    iter = benc_first(plst);
    while (iter != NULL) {
	if (!benc_isstr(iter))
	    return EINVAL;
	benc_str(iter, &str, &len, &iter);
	if (!check_path(str, len))
	    return EINVAL;
	npath++;
	plen += len;
    }
    if (npath == 0)
	return EINVAL;

    if ((tfp->path = malloc(plen + (npath - 1) + 1)) == NULL)
	return ENOMEM;

    iter = benc_first(plst);
    benc_str(iter, &str, &len, &iter);
    memcpy(tfp->path, str, len);
    plen = len;
    npath--;
    while (npath > 0) {
	tfp->path[plen++] = '/';
	benc_str(iter, &str, &len, &iter);
	memcpy(tfp->path + plen, str, len);
	plen += len;
	npath--;
    }
    tfp->path[plen] = '\0';
    return 0;
}

void
clear_metainfo(struct metainfo *mip)
{
    int i;
    if (mip->piece_hash != NULL)
	free(mip->piece_hash);
    if (mip->announce != NULL)
	free(mip->announce);
    if (mip->files != NULL) {
	for (i = 0; i < mip->nfiles; i++) {
	    if (mip->files[i].path != NULL)
		free(mip->files[i].path);
	}
	free(mip->files);
    }
    if (mip->name != NULL)
	free(mip->name);
}

int
fill_metainfo(const char *bep, struct metainfo *tp, int mem_hashes)
{
    size_t len;
    int err;
    const char *base_addr = bep;
    const char *hash_addr;

    if (!benc_isdct(bep))
	return EINVAL;

    if ((err = benc_dget_strz(bep, "announce", &tp->announce, NULL)) != 0)
	goto out;

    if ((err = benc_dget_dct(bep, "info", &bep)) != 0)
	goto out;

    SHA1(bep, benc_length(bep), tp->info_hash);

    if ((err = benc_dget_off(bep, "piece length", &tp->piece_length)) != 0)
	goto out;

    if ((err = benc_dget_str(bep, "pieces", &hash_addr, &len)) != 0)
	goto out;

    if (len % 20 != 0) {
	err = EINVAL;
	goto out;
    }
    tp->npieces = len / 20;

    tp->pieces_off = hash_addr - base_addr;
    
    if (mem_hashes) {
	if ((tp->piece_hash = malloc(len)) == NULL) {
	    err = ENOMEM;
	    goto out;
	}
	bcopy(hash_addr, tp->piece_hash, len);
    }

    if ((err = benc_dget_strz(bep, "name", &tp->name, NULL)) != 0)
	goto out;

    err = benc_dget_off(bep, "length", &tp->total_length);
    if (err == 0) {
	tp->nfiles = 1;
	tp->files = calloc(1, sizeof(struct fileinfo));
	if (tp->files != NULL) {
	    tp->files[0].length = tp->total_length;
	    tp->files[0].path = strdup(tp->name);
	    if (tp->files[0].path == NULL) {
		err = ENOMEM;
		goto out;
	    }
	} else {
	    err = ENOMEM;
	    goto out;
	}
    }
    else if (err == ENOENT) {
	int i;
	const char *flst, *fdct;

	if ((err = benc_dget_lst(bep, "files", &flst)) != 0)
	    goto out;

	tp->nfiles = benc_nelems(flst);
	if (tp->nfiles < 1) {
	    err = EINVAL;
	    goto out;
	}
	tp->files = calloc(tp->nfiles, sizeof(struct fileinfo));

	tp->total_length = 0;
	i = 0;
	for (fdct = benc_first(flst); fdct != NULL; fdct = benc_next(fdct)) {
	    if (!benc_isdct(fdct)) {
		err = EINVAL;
		goto out;
	    }

	    if ((err = fill_fileinfo(fdct, &tp->files[i])) != 0)
		goto out;

	    tp->total_length += tp->files[i].length;
	    i++;
	}
    }
    else
	goto out;
out:
    if (err != 0)
	clear_metainfo(tp);

    return err;
}

int
load_metainfo(const char *path, off_t size, int mem_hashes,
              struct metainfo **res)
{
    char *buf;
    int fd, err = 0;

    if ((fd = open(path, O_RDONLY)) == -1)
	return errno;

    if (size <= 0) {
	struct stat sb;
	if (fstat(fd, &sb) == -1) {
	    close(fd);
	    return errno;
	} else
	    size = sb.st_size;
    }

    if ((buf = mmap(0, size, PROT_READ, MAP_PRIVATE, fd, 0)) == MAP_FAILED)
	err = errno;
    close(fd);

    if (err == 0)
	err = benc_validate(buf, size);

    if (err == 0)
	if ((*res = calloc(1, sizeof(**res))) == NULL)
	    err = ENOMEM;

    if (err == 0)
	if ((err = fill_metainfo(buf, *res, mem_hashes)) != 0)
	    free(*res);

    munmap(buf, size);
    return err;
}