/*
 *
 *  oFono - Open Source Telephony
 *
 *  Copyright (C) 2008-2011  Intel Corporation. All rights reserved.
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

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

#include <glib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <errno.h>
#include <unistd.h>
#include <dirent.h>

#include "ofono.h"

#include "simfs.h"
#include "simutil.h"
#include "storage.h"

#define SIM_CACHE_MODE 0600
#define SIM_CACHE_BASEPATH STORAGEDIR "/%s-%i"
#define SIM_CACHE_VERSION SIM_CACHE_BASEPATH "/version"
#define SIM_CACHE_PATH SIM_CACHE_BASEPATH "/%04x"
#define SIM_CACHE_HEADER_SIZE 39
#define SIM_FILE_INFO_SIZE 7
#define SIM_IMAGE_CACHE_BASEPATH STORAGEDIR "/%s-%i/images"
#define SIM_IMAGE_CACHE_PATH SIM_IMAGE_CACHE_BASEPATH "/%d.xpm"

#define SIM_FS_VERSION 2

static gboolean sim_fs_op_next(gpointer user_data);
static gboolean sim_fs_op_read_record(gpointer user);
static gboolean sim_fs_op_read_block(gpointer user_data);

struct sim_fs_op {
	int id;
	unsigned char *buffer;
	enum ofono_sim_file_structure structure;
	unsigned short offset;
	gboolean info_only;
	int num_bytes;
	int length;
	int record_length;
	int current;
	unsigned char path[6];
	unsigned char path_len;
	gconstpointer cb;
	gboolean is_read;
	void *userdata;
	struct ofono_sim_context *context;
};

struct ofono_sim_context {
	struct sim_fs *fs;
	struct ofono_watchlist *file_watches;
};

struct sim_fs {
	GQueue *op_q;
	gint op_source;
	unsigned char bitmap[32];
	int fd;
	struct ofono_sim *sim;
	const struct ofono_sim_driver *driver;
	GSList *contexts;
	struct ofono_sim_aid_session *session;
	int session_id;
	unsigned int watch_id;
};

static void sim_fs_op_free(gpointer pointer)
{
	struct sim_fs_op *node = pointer;

	g_free(node->buffer);
	g_free(node);
}

void sim_fs_free(struct sim_fs *fs)
{
	if (fs == NULL)
		return;

	if (fs->op_source) {
		g_source_remove(fs->op_source);
		fs->op_source = 0;
	}

	/*
	 * Note: users of sim_fs must not assume that the callback happens
	 * for operations still in progress
	 */
	if (fs->op_q) {
		g_queue_free_full(fs->op_q, sim_fs_op_free);
		fs->op_q = NULL;
	}

	while (fs->contexts)
		sim_fs_context_free(fs->contexts->data);

	if (fs->watch_id)
		__ofono_sim_remove_session_watch(fs->session, fs->watch_id);

	g_free(fs);
}

struct file_watch {
	struct ofono_watchlist_item item;
	int ef;
};

struct sim_fs *sim_fs_new(struct ofono_sim *sim,
				const struct ofono_sim_driver *driver)
{
	struct sim_fs *fs;

	fs = g_try_new0(struct sim_fs, 1);
	if (fs == NULL)
		return NULL;

	fs->sim = sim;
	fs->driver = driver;
	fs->fd = -1;

	return fs;
}

struct ofono_sim_context *sim_fs_context_new(struct sim_fs *fs)
{
	struct ofono_sim_context *context =
		g_try_new0(struct ofono_sim_context, 1);

	if (context == NULL)
		return NULL;

	context->fs = fs;
	fs->contexts = g_slist_prepend(fs->contexts, context);

	return context;
}

struct ofono_sim_context *sim_fs_context_new_with_aid(struct sim_fs *fs,
		unsigned char *aid)
{
	struct ofono_sim_context *context = sim_fs_context_new(fs);

	if (context == NULL)
		return NULL;

	context->fs->session = __ofono_sim_get_session_by_aid(fs->sim, aid);
	if (!context->fs->session) {
		sim_fs_context_free(context);
		return NULL;
	}

	return context;
}

void sim_fs_context_free(struct ofono_sim_context *context)
{
	struct sim_fs *fs = context->fs;
	int n = 0;
	struct sim_fs_op *op;

	if (fs->op_q) {
		while ((op = g_queue_peek_nth(fs->op_q, n)) != NULL) {
			if (op->context != context) {
				n += 1;
				continue;
			}

			if (n == 0) {
				op->cb = NULL;
				op->context = NULL;

				n += 1;
				continue;
			}

			sim_fs_op_free(op);
			g_queue_remove(fs->op_q, op);
		}
	}

	if (context->file_watches)
		__ofono_watchlist_free(context->file_watches);

	fs->contexts = g_slist_remove(fs->contexts, context);
	g_free(context);
}

unsigned int sim_fs_file_watch_add(struct ofono_sim_context *context, int id,
					ofono_sim_file_changed_cb_t cb,
					void *userdata,
					ofono_destroy_func destroy)
{
	struct file_watch *watch;

	if (cb == NULL)
		return 0;

	if (context->file_watches == NULL)
		context->file_watches = __ofono_watchlist_new(g_free);

	watch = g_new0(struct file_watch, 1);

	watch->ef = id;
	watch->item.notify = cb;
	watch->item.notify_data = userdata;
	watch->item.destroy = destroy;

	return __ofono_watchlist_add_item(context->file_watches,
					(struct ofono_watchlist_item *) watch);
}

void sim_fs_file_watch_remove(struct ofono_sim_context *context,
				unsigned int id)
{
	__ofono_watchlist_remove_item(context->file_watches, id);
}

void sim_fs_notify_file_watches(struct sim_fs *fs, int id)
{
	GSList *l;

	for (l = fs->contexts; l; l = l->next) {
		struct ofono_sim_context *context = l->data;
		GSList *k;

		if (context->file_watches == NULL)
			continue;

		for (k = context->file_watches->items; k; k = k->next) {
			struct file_watch *w = k->data;
			ofono_sim_file_changed_cb_t notify = w->item.notify;

			if (id == -1 || w->ef == id)
				notify(w->ef, w->item.notify_data);
		}
	}

}

static void sim_fs_end_current(struct sim_fs *fs)
{
	struct sim_fs_op *op = g_queue_pop_head(fs->op_q);

	if (g_queue_get_length(fs->op_q) > 0)
		fs->op_source = g_idle_add(sim_fs_op_next, fs);
	else if (fs->watch_id) /* release the session if no pending reads */
		__ofono_sim_remove_session_watch(fs->session, fs->watch_id);

	if (fs->fd != -1) {
		L_TFR(close(fs->fd));
		fs->fd = -1;
	}

	memset(fs->bitmap, 0, sizeof(fs->bitmap));

	sim_fs_op_free(op);
}

static void sim_fs_op_error(struct sim_fs *fs)
{
	struct sim_fs_op *op = g_queue_peek_head(fs->op_q);

	if (op->cb == NULL) {
		sim_fs_end_current(fs);
		return;
	}

	if (op->info_only == TRUE)
		((sim_fs_read_info_cb_t) op->cb)
			(0, 0, 0, 0, op->userdata);
	else if (op->is_read == TRUE)
		((ofono_sim_file_read_cb_t) op->cb)
			(0, 0, 0, 0, 0, op->userdata);
	else
		((ofono_sim_file_write_cb_t) op->cb)
			(0, op->userdata);

	sim_fs_end_current(fs);
}

static gboolean cache_block(struct sim_fs *fs, int block, int block_len,
				const unsigned char *data, int num_bytes)
{
	int offset;
	int bit;
	ssize_t r;
	unsigned char b;

	if (fs->fd == -1)
		return FALSE;

	if (lseek(fs->fd, block * block_len +
				SIM_CACHE_HEADER_SIZE, SEEK_SET) == (off_t) -1)
		return FALSE;

	r = L_TFR(write(fs->fd, data, num_bytes));

	if (r != num_bytes)
		return FALSE;

	/* update present bit for this block */
	offset = block / 8;
	bit = block % 8;

	/* lseek to correct byte (skip file info) */
	lseek(fs->fd, offset + SIM_FILE_INFO_SIZE, SEEK_SET);

	b = fs->bitmap[offset];
	b |= 1 << bit;

	r = L_TFR(write(fs->fd, &b, sizeof(b)));

	if (r != sizeof(b))
		return FALSE;

	fs->bitmap[offset] = b;

	return TRUE;
}

static void sim_fs_op_write_cb(const struct ofono_error *error, void *data)
{
	struct sim_fs *fs = data;
	struct sim_fs_op *op = g_queue_peek_head(fs->op_q);
	ofono_sim_file_write_cb_t cb = op->cb;

	if (cb == NULL) {
		sim_fs_end_current(fs);
		return;
	}

	if (error->type == OFONO_ERROR_TYPE_NO_ERROR)
		cb(1, op->userdata);
	else
		cb(0, op->userdata);

	sim_fs_end_current(fs);
}

static void sim_fs_op_read_block_cb(const struct ofono_error *error,
					const unsigned char *data, int len,
					void *user)
{
	struct sim_fs *fs = user;
	struct sim_fs_op *op = g_queue_peek_head(fs->op_q);
	int start_block;
	int end_block;
	int bufoff;
	int dataoff;
	int tocopy;

	if (error->type != OFONO_ERROR_TYPE_NO_ERROR) {
		sim_fs_op_error(fs);
		return;
	}

	start_block = op->offset / 256;
	end_block = (op->offset + (op->num_bytes - 1)) / 256;

	if (op->current == start_block) {
		bufoff = 0;
		dataoff = op->offset % 256;
		tocopy = MIN(256 - op->offset % 256,
				op->num_bytes - op->current * 256);
	} else {
		bufoff = (op->current - start_block - 1) * 256 +
				op->offset % 256;
		dataoff = 0;
		tocopy = MIN(256, op->num_bytes - op->current * 256);
	}

	DBG("bufoff: %d, dataoff: %d, tocopy: %d",
				bufoff, dataoff, tocopy);

	memcpy(op->buffer + bufoff, data + dataoff, tocopy);
	cache_block(fs, op->current, 256, data, len);

	if (op->cb == NULL) {
		sim_fs_end_current(fs);
		return;
	}

	op->current++;

	if (op->current > end_block) {
		ofono_sim_file_read_cb_t cb = op->cb;

		cb(1, op->num_bytes, 0, op->buffer,
				op->record_length, op->userdata);

		sim_fs_end_current(fs);
	} else {
		fs->op_source = g_idle_add(sim_fs_op_read_block, fs);
	}
}

static gboolean sim_fs_op_read_block(gpointer user_data)
{
	struct sim_fs *fs = user_data;
	struct sim_fs_op *op = g_queue_peek_head(fs->op_q);
	int start_block;
	int end_block;
	unsigned short read_bytes;

	fs->op_source = 0;

	if (op->cb == NULL) {
		sim_fs_end_current(fs);
		return FALSE;
	}

	start_block = op->offset / 256;
	end_block = (op->offset + (op->num_bytes - 1)) / 256;

	if (op->current == start_block) {
		op->buffer = g_try_new0(unsigned char, op->num_bytes);

		if (op->buffer == NULL) {
			sim_fs_op_error(fs);
			return FALSE;
		}
	}

	while (fs->fd != -1 && op->current <= end_block) {
		int offset = op->current / 8;
		int bit = 1 << op->current % 8;
		int bufoff;
		int seekoff;
		int toread;

		if ((fs->bitmap[offset] & bit) == 0)
			break;

		if (op->current == start_block) {
			bufoff = 0;
			seekoff = SIM_CACHE_HEADER_SIZE + op->current * 256 +
				op->offset % 256;
			toread = MIN(256 - op->offset % 256,
					op->num_bytes - op->current * 256);
		} else {
			bufoff = (op->current - start_block - 1) * 256 +
					op->offset % 256;
			seekoff = SIM_CACHE_HEADER_SIZE + op->current * 256;
			toread = MIN(256, op->num_bytes - op->current * 256);
		}

		DBG("bufoff: %d, seekoff: %d, toread: %d",
				bufoff, seekoff, toread);

		if (lseek(fs->fd, seekoff, SEEK_SET) == (off_t) -1)
			break;

		if (L_TFR(read(fs->fd, op->buffer + bufoff, toread)) != toread)
			break;

		op->current += 1;
	}

	if (op->current > end_block) {
		ofono_sim_file_read_cb_t cb = op->cb;

		cb(1, op->num_bytes, 0, op->buffer,
				op->record_length, op->userdata);

		sim_fs_end_current(fs);

		return FALSE;
	}

	if (fs->driver->read_file_transparent == NULL) {
		sim_fs_op_error(fs);
		return FALSE;
	}

	read_bytes = MIN(op->length - op->current * 256, 256);
	fs->driver->read_file_transparent(fs->sim, op->id,
						op->current * 256,
						read_bytes,
						op->path_len ? op->path : NULL,
						op->path_len,
						sim_fs_op_read_block_cb, fs);

	return FALSE;
}

static void sim_fs_op_retrieve_cb(const struct ofono_error *error,
					const unsigned char *data, int len,
					void *user)
{
	struct sim_fs *fs = user;
	struct sim_fs_op *op = g_queue_peek_head(fs->op_q);
	int total = op->length / op->record_length;
	ofono_sim_file_read_cb_t cb = op->cb;

	if (error->type != OFONO_ERROR_TYPE_NO_ERROR) {
		sim_fs_op_error(fs);
		return;
	}

	cache_block(fs, op->current - 1, op->record_length,
			data, op->record_length);

	if (cb == NULL) {
		sim_fs_end_current(fs);
		return;
	}

	cb(1, op->length, op->current, data, op->record_length, op->userdata);

	if (op->current < total) {
		op->current += 1;
		fs->op_source = g_idle_add(sim_fs_op_read_record, fs);
	} else {
		sim_fs_end_current(fs);
	}
}

static gboolean sim_fs_op_read_record(gpointer user)
{
	struct sim_fs *fs = user;
	struct sim_fs_op *op = g_queue_peek_head(fs->op_q);
	const struct ofono_sim_driver *driver = fs->driver;
	int total = op->length / op->record_length;
	unsigned char buf[256];

	fs->op_source = 0;

	if (op->cb == NULL) {
		sim_fs_end_current(fs);
		return FALSE;
	}

	while (fs->fd != -1 && op->current <= total) {
		int offset = (op->current - 1) / 8;
		int bit = 1 << ((op->current - 1) % 8);
		ofono_sim_file_read_cb_t cb = op->cb;

		if ((fs->bitmap[offset] & bit) == 0)
			break;

		if (lseek(fs->fd, (op->current - 1) * op->record_length +
				SIM_CACHE_HEADER_SIZE, SEEK_SET) == (off_t) -1)
			break;

		if (L_TFR(read(fs->fd, buf, op->record_length)) !=
				op->record_length)
			break;

		cb(1, op->length, op->current,
				buf, op->record_length, op->userdata);

		op->current += 1;
	}

	if (op->current > total) {
		sim_fs_end_current(fs);

		return FALSE;
	}

	switch (op->structure) {
	case OFONO_SIM_FILE_STRUCTURE_FIXED:
		if (driver->read_file_linear == NULL) {
			sim_fs_op_error(fs);
			return FALSE;
		}

		driver->read_file_linear(fs->sim, op->id, op->current,
						op->record_length,
						NULL, 0,
						sim_fs_op_retrieve_cb, fs);
		break;
	case OFONO_SIM_FILE_STRUCTURE_CYCLIC:
		if (driver->read_file_cyclic == NULL) {
			sim_fs_op_error(fs);
			return FALSE;
		}

		driver->read_file_cyclic(fs->sim, op->id, op->current,
						op->record_length,
						NULL, 0,
						sim_fs_op_retrieve_cb, fs);
		break;
	default:
		ofono_error("Unrecognized file structure, this can't happen");
	}

	return FALSE;
}

static void sim_fs_op_cache_fileinfo(struct sim_fs *fs,
					const struct ofono_error *error,
					int length,
					enum ofono_sim_file_structure structure,
					int record_length,
					const unsigned char access[3],
					unsigned char file_status)
{
	struct sim_fs_op *op = g_queue_peek_head(fs->op_q);
	const char *imsi = ofono_sim_get_imsi(fs->sim);
	enum ofono_sim_phase phase = ofono_sim_get_phase(fs->sim);
	enum sim_file_access update;
	enum sim_file_access invalidate;
	enum sim_file_access rehabilitate;
	unsigned char fileinfo[SIM_CACHE_HEADER_SIZE];
	gboolean cache;
	char *path;

	/* TS 11.11, Section 9.3 */
	update = file_access_condition_decode(access[0] & 0xf);
	rehabilitate = file_access_condition_decode((access[2] >> 4) & 0xf);
	invalidate = file_access_condition_decode(access[2] & 0xf);

	/* Never cache card holder writable files */
	cache = (update == SIM_FILE_ACCESS_ADM ||
			update == SIM_FILE_ACCESS_NEVER) &&
			(invalidate == SIM_FILE_ACCESS_ADM ||
				invalidate == SIM_FILE_ACCESS_NEVER) &&
			(rehabilitate == SIM_FILE_ACCESS_ADM ||
				rehabilitate == SIM_FILE_ACCESS_NEVER);

	if (imsi == NULL || phase == OFONO_SIM_PHASE_UNKNOWN || cache == FALSE)
		return;

	memset(fileinfo, 0, SIM_CACHE_HEADER_SIZE);

	fileinfo[0] = error->type;
	fileinfo[1] = length >> 8;
	fileinfo[2] = length & 0xff;
	fileinfo[3] = structure;
	fileinfo[4] = record_length >> 8;
	fileinfo[5] = record_length & 0xff;
	fileinfo[6] = file_status;

	path = g_strdup_printf(SIM_CACHE_PATH, imsi, phase, op->id);
	fs->fd = L_TFR(open(path, O_WRONLY | O_CREAT | O_TRUNC, SIM_CACHE_MODE));
	g_free(path);

	if (fs->fd == -1)
		return;

	if (L_TFR(write(fs->fd, fileinfo, SIM_CACHE_HEADER_SIZE)) ==
			SIM_CACHE_HEADER_SIZE)
		return;

	L_TFR(close(fs->fd));
	fs->fd = -1;
}

static void sim_fs_op_info_cb(const struct ofono_error *error, int length,
				enum ofono_sim_file_structure structure,
				int record_length,
				const unsigned char access[3],
				unsigned char file_status,
				void *data)
{
	struct sim_fs *fs = data;
	struct sim_fs_op *op = g_queue_peek_head(fs->op_q);

	if (error->type != OFONO_ERROR_TYPE_NO_ERROR) {
		sim_fs_op_error(fs);
		return;
	}

	sim_fs_op_cache_fileinfo(fs, error, length, structure, record_length,
					access, file_status);

	if (structure != op->structure) {
		ofono_error("Requested file structure differs from SIM: %x",
				op->id);
		sim_fs_op_error(fs);
		return;
	}

	if (op->cb == NULL) {
		sim_fs_end_current(fs);
		return;
	}

	op->structure = structure;
	op->length = length;

	if (structure == OFONO_SIM_FILE_STRUCTURE_TRANSPARENT) {
		if (op->num_bytes == 0)
			op->num_bytes = op->length;

		op->record_length = length;
		op->current = op->offset / 256;

		if (op->info_only == FALSE)
			fs->op_source = g_idle_add(sim_fs_op_read_block, fs);
	} else {
		op->record_length = record_length;
		op->current = 1;

		if (op->info_only == FALSE)
			fs->op_source = g_idle_add(sim_fs_op_read_record, fs);
	}

	if (op->info_only == TRUE) {
		/*
		 * It's an info-only request, so there is no need to request
		 * actual contents of the EF. Just return the EF-info.
		 */
		sim_fs_read_info_cb_t cb = op->cb;

		cb(1, file_status, op->length,
			op->record_length, op->userdata);

		sim_fs_end_current(fs);
	}
}

static gboolean sim_fs_op_check_cached(struct sim_fs *fs)
{
	const char *imsi = ofono_sim_get_imsi(fs->sim);
	enum ofono_sim_phase phase = ofono_sim_get_phase(fs->sim);
	struct sim_fs_op *op = g_queue_peek_head(fs->op_q);
	char *path;
	int fd;
	ssize_t len;
	unsigned char fileinfo[SIM_CACHE_HEADER_SIZE];
	int error_type;
	int file_length;
	enum ofono_sim_file_structure structure;
	int record_length;
	unsigned char file_status;

	if (imsi == NULL || phase == OFONO_SIM_PHASE_UNKNOWN)
		return FALSE;

	path = g_strdup_printf(SIM_CACHE_PATH, imsi, phase, op->id);

	if (path == NULL)
		return FALSE;

	fd = L_TFR(open(path, O_RDWR));
	g_free(path);

	if (fd == -1) {
		if (errno != ENOENT)
			DBG("Error %i opening cache file for "
					"fileid %04x, IMSI %s",
					errno, op->id, imsi);

		return FALSE;
	}

	len = L_TFR(read(fd, fileinfo, SIM_CACHE_HEADER_SIZE));

	if (len != SIM_CACHE_HEADER_SIZE)
		goto error;

	error_type = fileinfo[0];
	file_length = (fileinfo[1] << 8) | fileinfo[2];
	structure = fileinfo[3];
	record_length = (fileinfo[4] << 8) | fileinfo[5];
	file_status = fileinfo[6];

	if (structure == OFONO_SIM_FILE_STRUCTURE_TRANSPARENT)
		record_length = file_length;

	if (record_length == 0 || file_length < record_length)
		goto error;

	op->length = file_length;
	op->record_length = record_length;
	memcpy(fs->bitmap, fileinfo + SIM_FILE_INFO_SIZE,
			SIM_CACHE_HEADER_SIZE - SIM_FILE_INFO_SIZE);
	fs->fd = fd;

	if (error_type != OFONO_ERROR_TYPE_NO_ERROR ||
			structure != op->structure) {
		sim_fs_op_error(fs);
		return TRUE;
	}

	if (op->info_only == TRUE) {
		/*
		 * It's an info-only request, so there is no need to request
		 * actual contents of the EF. Just return the EF-info.
		 */
		sim_fs_read_info_cb_t cb = op->cb;

		cb(1, file_status, op->length,
			op->record_length, op->userdata);

		sim_fs_end_current(fs);
	} else if (structure == OFONO_SIM_FILE_STRUCTURE_TRANSPARENT) {
		if (op->num_bytes == 0)
			op->num_bytes = op->length;

		op->current = op->offset / 256;
		fs->op_source = g_idle_add(sim_fs_op_read_block, fs);
	} else {
		op->current = 1;
		fs->op_source = g_idle_add(sim_fs_op_read_record, fs);
	}

	return TRUE;

error:
	L_TFR(close(fd));
	return FALSE;
}

static void sim_fs_read_session_cb(const struct ofono_error *error,
		const unsigned char *sdata, int length, void *data)
{
	struct sim_fs *fs = data;
	struct sim_fs_op *op = g_queue_peek_head(fs->op_q);
	ofono_sim_file_read_cb_t cb;

	if (error->type != OFONO_ERROR_TYPE_NO_ERROR) {
		sim_fs_op_error(fs);
		return;
	}

	cb = op->cb;
	cb(TRUE, length, 0, sdata, length, op->userdata);

	sim_fs_end_current(fs);
}

static void session_read_info_cb(const struct ofono_error *error,
					int filelength,
					enum ofono_sim_file_structure structure,
					int recordlength,
					const unsigned char access[3],
					unsigned char file_status,
					void *data)
{
	struct sim_fs *fs = data;
	struct sim_fs_op *op = g_queue_peek_head(fs->op_q);

	if (error->type != OFONO_ERROR_TYPE_NO_ERROR) {
		sim_fs_op_error(fs);
		return;
	}

	sim_fs_op_cache_fileinfo(fs, error, filelength, structure, recordlength,
			access, file_status);

	if (op->info_only) {
		sim_fs_read_info_cb_t cb = op->cb;

		cb(1, file_status, filelength, recordlength, op->userdata);

		sim_fs_end_current(fs);
		return;
	}

	if (op->structure == OFONO_SIM_FILE_STRUCTURE_TRANSPARENT) {
		if (!fs->driver->session_read_binary) {
			sim_fs_op_error(fs);
			return;
		}

		fs->driver->session_read_binary(fs->sim, fs->session_id,
				op->id, op->offset, filelength, op->path,
				op->path_len, sim_fs_read_session_cb, fs);
	} else {
		if (!fs->driver->session_read_record) {
			sim_fs_op_error(fs);
			return;
		}

		fs->driver->session_read_record(fs->sim, fs->session_id,
				op->id, op->offset, recordlength, op->path,
				op->path_len, sim_fs_read_session_cb, fs);
	}
}

static void session_destroy_cb(void *userdata)
{
	struct sim_fs *fs = userdata;

	fs->watch_id = 0;
}

static void get_session_cb(ofono_bool_t active, int session_id,
		void *data)
{
	struct sim_fs *fs = data;
	struct sim_fs_op *op;

	if (!active) {
		sim_fs_op_error(fs);
		return;
	}

	op = g_queue_peek_head(fs->op_q);

	fs->session_id = session_id;

	fs->driver->session_read_info(fs->sim, session_id, op->id, op->path,
			op->path_len, session_read_info_cb, fs);
}

static gboolean sim_fs_op_next(gpointer user_data)
{
	struct sim_fs *fs = user_data;
	const struct ofono_sim_driver *driver = fs->driver;
	struct sim_fs_op *op;

	fs->op_source = 0;

	if (fs->op_q == NULL)
		return FALSE;

	op = g_queue_peek_head(fs->op_q);

	if (op->cb == NULL) {
		sim_fs_end_current(fs);
		return FALSE;
	}

	if (op->is_read == TRUE) {
		if (sim_fs_op_check_cached(fs))
			return FALSE;

		if (!fs->session) {
			driver->read_file_info(fs->sim, op->id,
						op->path_len ? op->path : NULL,
						op->path_len,
						sim_fs_op_info_cb, fs);
		} else {
			if (fs->watch_id)
				fs->driver->session_read_info(fs->sim,
						fs->session_id, op->id,
						op->path, op->path_len,
						session_read_info_cb, fs);
			else
				fs->watch_id = __ofono_sim_add_session_watch(
						fs->session, get_session_cb,
						fs, session_destroy_cb);
		}
	} else {
		switch (op->structure) {
		case OFONO_SIM_FILE_STRUCTURE_TRANSPARENT:
			driver->write_file_transparent(fs->sim, op->id, 0,
					op->length, op->buffer,
					NULL, 0, sim_fs_op_write_cb, fs);
			break;
		case OFONO_SIM_FILE_STRUCTURE_FIXED:
			driver->write_file_linear(fs->sim, op->id, op->current,
					op->length, op->buffer,
					NULL, 0, sim_fs_op_write_cb, fs);
			break;
		case OFONO_SIM_FILE_STRUCTURE_CYCLIC:
			driver->write_file_cyclic(fs->sim, op->id,
					op->length, op->buffer,
					NULL, 0, sim_fs_op_write_cb, fs);
			break;
		default:
			ofono_error("Unrecognized file structure, "
					"this can't happen");
		}

		g_free(op->buffer);
		op->buffer = NULL;
	}

	return FALSE;
}

int sim_fs_read_info(struct ofono_sim_context *context, int id,
			enum ofono_sim_file_structure expected_type,
			sim_fs_read_info_cb_t cb, void *data)
{
	struct sim_fs *fs = context->fs;
	struct sim_fs_op *op;

	if (cb == NULL)
		return -EINVAL;

	if (fs->driver == NULL)
		return -EINVAL;

	if (fs->driver->read_file_info == NULL)
		return -ENOSYS;

	if (fs->op_q == NULL)
		fs->op_q = g_queue_new();

	op = g_try_new0(struct sim_fs_op, 1);
	if (op == NULL)
		return -ENOMEM;

	op->id = id;
	op->structure = expected_type;
	op->cb = cb;
	op->userdata = data;
	op->is_read = TRUE;
	op->info_only = TRUE;
	op->context = context;

	g_queue_push_tail(fs->op_q, op);

	if (g_queue_get_length(fs->op_q) == 1)
		fs->op_source = g_idle_add(sim_fs_op_next, fs);

	return 0;
}

int sim_fs_read(struct ofono_sim_context *context, int id,
		enum ofono_sim_file_structure expected_type,
		unsigned short offset, unsigned short num_bytes,
		const unsigned char *path, unsigned int path_len,
		ofono_sim_file_read_cb_t cb, void *data)
{
	struct sim_fs *fs = context->fs;
	struct sim_fs_op *op;

	if (cb == NULL)
		return -EINVAL;

	if (fs->driver == NULL)
		return -EINVAL;

	/* check driver support for session based read */
	if (fs->session) {
		if (!fs->driver->session_read_info) {
			cb(0, 0, 0, NULL, 0, data);
			return -ENOSYS;
		}
	} else {
		if (!fs->driver->read_file_info) {
			cb(0, 0, 0, NULL, 0, data);
			return -ENOSYS;
		}
	}

	if (fs->op_q == NULL)
		fs->op_q = g_queue_new();

	op = g_try_new0(struct sim_fs_op, 1);
	if (op == NULL)
		return -ENOMEM;

	op->id = id;
	op->structure = expected_type;
	op->cb = cb;
	op->userdata = data;
	op->is_read = TRUE;
	op->offset = offset;
	op->num_bytes = num_bytes;
	op->info_only = FALSE;
	op->context = context;
	memcpy(op->path, path, path_len);
	op->path_len = path_len;

	g_queue_push_tail(fs->op_q, op);

	if (g_queue_get_length(fs->op_q) == 1)
		fs->op_source = g_idle_add(sim_fs_op_next, fs);

	return 0;
}

int sim_fs_write(struct ofono_sim_context *context, int id,
			ofono_sim_file_write_cb_t cb,
			enum ofono_sim_file_structure structure, int record,
			const unsigned char *data, int length, void *userdata)
{
	struct sim_fs *fs = context->fs;
	struct sim_fs_op *op;
	gconstpointer fn = NULL;

	if (cb == NULL)
		return -EINVAL;

	if (fs->driver == NULL)
		return -EINVAL;

	switch (structure) {
	case OFONO_SIM_FILE_STRUCTURE_TRANSPARENT:
		fn = fs->driver->write_file_transparent;
		break;
	case OFONO_SIM_FILE_STRUCTURE_FIXED:
		fn = fs->driver->write_file_linear;
		break;
	case OFONO_SIM_FILE_STRUCTURE_CYCLIC:
		fn = fs->driver->write_file_cyclic;
		break;
	default:
		ofono_error("Unrecognized file structure, this can't happen");
	}

	if (fn == NULL)
		return -ENOSYS;

	if (fs->op_q == NULL)
		fs->op_q = g_queue_new();

	op = g_try_new0(struct sim_fs_op, 1);
	if (op == NULL)
		return -ENOMEM;

	op->id = id;
	op->cb = cb;
	op->userdata = userdata;
	op->is_read = FALSE;
	op->buffer = g_memdup(data, length);
	op->structure = structure;
	op->length = length;
	op->current = record;
	op->context = context;

	g_queue_push_tail(fs->op_q, op);

	if (g_queue_get_length(fs->op_q) == 1)
		fs->op_source = g_idle_add(sim_fs_op_next, fs);

	return 0;
}

void sim_fs_cache_image(struct sim_fs *fs, const char *image, int id)
{
	const char *imsi;
	enum ofono_sim_phase phase;

	if (fs == NULL || image == NULL)
		return;

	imsi = ofono_sim_get_imsi(fs->sim);
	if (imsi == NULL)
		return;

	phase = ofono_sim_get_phase(fs->sim);
	if (phase == OFONO_SIM_PHASE_UNKNOWN)
		return;

	write_file((const unsigned char *) image, strlen(image),
			SIM_CACHE_MODE, SIM_IMAGE_CACHE_PATH, imsi,
			phase, id);
}

char *sim_fs_get_cached_image(struct sim_fs *fs, int id)
{
	const char *imsi;
	enum ofono_sim_phase phase;
	unsigned short image_length;
	int fd;
	char *buffer;
	char *path;
	int len;
	struct stat st_buf;

	if (fs == NULL)
		return NULL;

	imsi = ofono_sim_get_imsi(fs->sim);
	if (imsi == NULL)
		return NULL;

	phase = ofono_sim_get_phase(fs->sim);
	if (phase == OFONO_SIM_PHASE_UNKNOWN)
		return NULL;

	path = g_strdup_printf(SIM_IMAGE_CACHE_PATH, imsi, phase, id);

	L_TFR(stat(path, &st_buf));
	fd = L_TFR(open(path, O_RDONLY));
	g_free(path);

	if (fd < 0)
		return NULL;

	image_length = st_buf.st_size;
	buffer = g_try_new0(char, image_length + 1);

	if (buffer == NULL) {
		L_TFR(close(fd));
		return NULL;
	}

	len = L_TFR(read(fd, buffer, image_length));
	L_TFR(close(fd));

	if (len != image_length) {
		g_free(buffer);
		return NULL;
	}

	return buffer;
}

static void remove_cachefile(const char *imsi, enum ofono_sim_phase phase,
				const struct dirent *file)
{
	int id;
	char *path;

	if (file->d_type != DT_REG)
		return;

	if (sscanf(file->d_name, "%4x", &id) != 1)
		return;

	path = g_strdup_printf(SIM_CACHE_PATH, imsi, phase, id);
	remove(path);
	g_free(path);
}

static void remove_imagefile(const char *imsi, enum ofono_sim_phase phase,
				const struct dirent *file)
{
	int id;
	char *path;

	if (file->d_type != DT_REG)
		return;

	if (sscanf(file->d_name, "%d", &id) != 1)
		return;

	path = g_strdup_printf(SIM_IMAGE_CACHE_PATH, imsi, phase, id);
	remove(path);
	g_free(path);
}

void sim_fs_check_version(struct sim_fs *fs)
{
	const char *imsi = ofono_sim_get_imsi(fs->sim);
	enum ofono_sim_phase phase = ofono_sim_get_phase(fs->sim);
	unsigned char version;

	if (imsi == NULL || phase == OFONO_SIM_PHASE_UNKNOWN)
		return;

	if (read_file(&version, 1, SIM_CACHE_VERSION, imsi, phase) == 1)
		if (version == SIM_FS_VERSION)
			return;

	sim_fs_cache_flush(fs);

	version = SIM_FS_VERSION;
	write_file(&version, 1, SIM_CACHE_MODE, SIM_CACHE_VERSION, imsi, phase);
}

void sim_fs_cache_flush(struct sim_fs *fs)
{
	const char *imsi = ofono_sim_get_imsi(fs->sim);
	enum ofono_sim_phase phase = ofono_sim_get_phase(fs->sim);
	char *path = g_strdup_printf(SIM_CACHE_BASEPATH, imsi, phase);
	struct dirent **entries;
	int len = scandir(path, &entries, NULL, alphasort);

	g_free(path);

	if (len > 0) {
		/* Remove all file ids */
		while (len--) {
			remove_cachefile(imsi, phase, entries[len]);
			g_free(entries[len]);
		}

		g_free(entries);
	}

	sim_fs_image_cache_flush(fs);
}

void sim_fs_cache_flush_file(struct sim_fs *fs, int id)
{
	const char *imsi = ofono_sim_get_imsi(fs->sim);
	enum ofono_sim_phase phase = ofono_sim_get_phase(fs->sim);
	char *path = g_strdup_printf(SIM_CACHE_PATH, imsi, phase, id);

	remove(path);
	g_free(path);
}

void sim_fs_image_cache_flush(struct sim_fs *fs)
{
	const char *imsi = ofono_sim_get_imsi(fs->sim);
	enum ofono_sim_phase phase = ofono_sim_get_phase(fs->sim);
	char *path = g_strdup_printf(SIM_IMAGE_CACHE_BASEPATH, imsi, phase);
	struct dirent **entries;
	int len = scandir(path, &entries, NULL, alphasort);

	g_free(path);

	if (len <= 0)
		return;

	/* Remove everything */
	while (len--) {
		remove_imagefile(imsi, phase, entries[len]);
		g_free(entries[len]);
	}

	g_free(entries);
}

void sim_fs_image_cache_flush_file(struct sim_fs *fs, int id)
{
	const char *imsi = ofono_sim_get_imsi(fs->sim);
	enum ofono_sim_phase phase = ofono_sim_get_phase(fs->sim);
	char *path = g_strdup_printf(SIM_IMAGE_CACHE_PATH, imsi, phase, id);

	remove(path);
	g_free(path);
}
