/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <assert.h>
#include <ctype.h>
#include <dirent.h>
#include <errno.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>

#include "fatblock.h"
#include "fat.h"
#include "fdpool.h"
#include "fs.h"
#include "utils.h"

static inline int valid_char(int c)
{
	return (isalnum(c) ||
		strchr("!#$%'()-@^_`{}~", c) ||
		((c >= 128) && (c < 256)));
}

static int convert_name(char *short_name, const char *long_name)
{
	int i;

	const char *s;
	const char *dot;
	int c;

	dot = NULL;

	for (s = long_name; *s; s++) {
		if (*s == '.') {
			if (dot) {
				goto short_fail;
			} else {
				dot = s;
			}
		} else if (!valid_char(*s)) {
			goto short_fail;
		}
	}

	if (dot - long_name > 8) {
		goto short_fail;
	}

	if (dot && (s - (dot + 1) > 3)) {
		goto short_fail;
	}

	memset(short_name, ' ', 11);

	if (!dot) {
		dot = s;
	}

	for (i = 0; i < dot - long_name; i++) {
		short_name[i] = toupper(long_name[i]);
	}

	for (i = 0; i < s - dot; i++) {
		short_name[8 + i] = toupper(dot[1 + i]);
	}

	return 0;

short_fail:
	return 1;
}

struct imported {
	cluster_t first_cluster;
	uint32_t size;
	struct fat_dirent *dot_dot_dirent;
};

static int import_file(struct fs *fs, char *path, struct imported *out)
{
	struct stat st;
	struct file *f = NULL;
	char *path_copy = NULL;
	int ret;

	ret = stat(path, &st);
	if (ret < 0) {
		WARN("importing %s: stat failed: %s\n", path, strerror(errno));
		goto fail;
	}

	f = malloc(sizeof(struct file));
	if (!f) {
		WARN("importing %s: couldn't allocate file struct: "
		     "out of memory\n", path);
		ret = MALLOC_FAIL;
		goto fail;
	}

	path_copy = strdup(path);
	if (!path_copy) {
		WARN("importing %s: couldn't strdup path: out of memory\n",
		     path);
		ret = MALLOC_FAIL;
		goto fail;
	}

	f->path = path_copy;
	f->size = st.st_size;
	f->dev = st.st_dev;
	f->ino = st.st_ino;
	f->mtime = st.st_mtime;
	fdpool_init(&f->pfd);

	ret = fs_alloc_extent(fs, &f->extent,
                              f->size, EXTENT_TYPE_FILE, &out->first_cluster);
	if (ret) {
		WARN("importing %s: couldn't allocate data extent\n", path);
		goto fail;
	}

	out->size = f->size;
	out->dot_dot_dirent = NULL;

	return 0;

fail:
	if (path_copy)
		free(path_copy);
	if (f)
		free(f);
	return ret;
}

struct item {
	char name[11];
	struct imported imp;
	struct item *next;
	int is_dir;
};

static struct item *free_items_head;

static struct item *alloc_item(void)
{
	struct item *item;

	if (free_items_head) {
		item = free_items_head;
		free_items_head = item->next;
	} else {
		item = malloc(sizeof(struct item));
		/* May return NULL if item couldn't be allocated. */
	}

	return item;
}

static void free_item(struct item *item)
{
	item->next = free_items_head;
	free_items_head = item;
}

static void free_items(struct item *head)
{
	struct item *tail;

	for (tail = head; tail->next; tail = tail->next);

	tail->next = free_items_head;
	free_items_head = head;
}

/* TODO: With some work, this can be rewritten so we don't recurse
 * until all memory is allocated. */
static int import_dir(struct fs *fs, char *path, int is_root,
		      struct imported *out)
{
	struct dir *d;
	cluster_t my_first_cluster;

	DIR *dir;
	struct dirent *de;

	char ch_path[PATH_MAX];
	struct imported *ch_imp;
	cluster_t ch_first_cluster;
	struct fat_dirent *ch_dirent;

	int ret;

	struct item *items;
	struct item *item;
	int count;

	int i;

	dir = opendir(path);
	if (!dir) {
		WARN("importing %s: opendir failed: %s\n", path,
		     strerror(errno));
		return -1;
	}

	d = malloc(sizeof(struct dir));
	if (!d) {
		WARN("importing %s: couldn't allocate dir struct: "
		     "out of memory\n", path);
		closedir(dir);
		return MALLOC_FAIL;
	}

	d->path = strdup(path);
	if (!d->path) {
		WARN("importing %s: couldn't strdup path: out of memory\n",
		     path);
		closedir(dir);
		free(d);
		return MALLOC_FAIL;
	}

	items = NULL;
	item = NULL;
	count = 0;

	while ((de = readdir(dir))) {
		if (de->d_name[0] == '.') {
			goto skip_item;
		}

		ret = snprintf(ch_path, PATH_MAX, "%s/%s", path, de->d_name);
		if (ret < 0 || ret >= PATH_MAX) {
			goto skip_item;
		}

		item = alloc_item();
		if (!item) {
			WARN("importing %s: couldn't allocate item struct: "
			     "out of memory\n", path);
			ret = MALLOC_FAIL;
			goto free_items;
		}

		if (convert_name(item->name, de->d_name)) {
			goto skip_item;
		}

		switch (de->d_type) {
			case DT_REG:
				import_file(fs, ch_path, &item->imp);
				item->is_dir = 0;
				break;
			case DT_DIR:
				import_dir(fs, ch_path, 0, &item->imp);
				item->is_dir = 1;
				break;
			default:
				goto skip_item;
		}

		item->next = items;
		items = item;

		count++;

		item = NULL;

		continue;

skip_item:
		if (item)
			free_item(item);
	}

	closedir(dir);

	d->size = sizeof(struct fat_dirent) * (count + (is_root ? 0 : 2));
	ret = fs_alloc_extent(fs, &d->extent, d->size, EXTENT_TYPE_DIR, &out->first_cluster);
	if (ret) {
		WARN("importing %s: couldn't allocate directory table extent: "
		     "out of space\n", path);
		goto free_items;
	}

	if (is_root)
		out->first_cluster = 0;

	my_first_cluster = is_root ? 0 : out->first_cluster;

	d->entries = malloc(sizeof(struct fat_dirent) * (count + (is_root ? 0 : 2)));
	assert(d->entries);
	for (i = count - 1; i >= 0; i--) {
		item = items;
		items = item->next;

		ch_dirent = &d->entries[i + (is_root ? 0 : 2)];

		fat_dirent_set(ch_dirent,
                               item->name, item->is_dir ? FAT_ATTR_SUBDIR : 0,
                               item->imp.first_cluster, item->imp.size);

		if (item->imp.dot_dot_dirent) {
			fat_dirent_set_first_cluster(item->imp.dot_dot_dirent,
						     my_first_cluster);
		}

		free_item(item);
	}

	if (!is_root) {
		fat_dirent_set(&d->entries[0],
                               "..         ", FAT_ATTR_SUBDIR,
                               (cluster_t)-1, 0);
		out->dot_dot_dirent = &d->entries[0]; /* will set first_cluster */

		fat_dirent_set(&d->entries[1],
                               ".          ", FAT_ATTR_SUBDIR,
                               my_first_cluster, 0);
	} else {
		out->dot_dot_dirent = NULL;
	}

	out->size = 0;

	return 0;

free_items:
	free_items(items);
	free(d->path);
	free(d);

	return ret;
}

int import_tree(struct fs *fs, char *path)
{
	struct imported imp;
	int ret;

	ret = import_dir(fs, path, 0, &imp);
	if (ret)
		return ret;

	fs_set_rootdir_start(fs, imp.first_cluster);
	fs_update_free_clusters(fs);

	return 0;
}