/*
 * Copyright (c) 2009-2017 Satlab Aps <satlab@satlab.com>
 * All rights reserved. Do not distribute without permission.
 */

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <unistd.h>
#include <assert.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <time.h>
#include <locale.h>
#include <langinfo.h>
#include <errno.h>
#include <pthread.h>
#include <termios.h>
#include <dirent.h>

#include <btp/bitops.h>
#include <btp/client.h>
#include <btp/types.h>
#include <btp/error.h>
#include <btp/crc32.h>

#define MAP_SYMBOL_OK	"+"
#define MAP_SYMBOL_MISS	"-"
static char *map_symbol_ok = MAP_SYMBOL_OK;
static char *map_symbol_miss = MAP_SYMBOL_MISS;

#ifndef BITS_PER_BYTE
#define BITS_PER_BYTE	(8)
#endif

static char const * strperm(uint32_t type, uint32_t mode) {
	static char local_buff[16] = {0};
	int i = 0;
	
	/* Entry type */
	if ((type & DT_DIR) == DT_DIR) local_buff[i] = 'd';
	else local_buff[i] = '-';
	i++;

	/* user permissions */
	if ((mode & S_IRUSR) == S_IRUSR) local_buff[i] = 'r';
	else local_buff[i] = '-';
	i++;
	if ((mode & S_IWUSR) == S_IWUSR) local_buff[i] = 'w';
	else local_buff[i] = '-';
	i++;
	if ((mode & S_IXUSR) == S_IXUSR) local_buff[i] = 'x';
	else local_buff[i] = '-';
	i++;

	/* group permissions */
	if ((mode & S_IRGRP) == S_IRGRP) local_buff[i] = 'r';
	else local_buff[i] = '-';
	i++;
	if ((mode & S_IWGRP) == S_IWGRP) local_buff[i] = 'w';
	else local_buff[i] = '-';
	i++;
	if ((mode & S_IXGRP) == S_IXGRP) local_buff[i] = 'x';
	else local_buff[i] = '-';
	i++;

	/* other permissions */
	if ((mode & S_IROTH) == S_IROTH) local_buff[i] = 'r';
	else local_buff[i] = '-';
	i++;
	if ((mode & S_IWOTH) == S_IWOTH) local_buff[i] = 'w';
	else local_buff[i] = '-';
	i++;
	if ((mode & S_IXOTH) == S_IXOTH) local_buff[i] = 'x';
	else local_buff[i] = '-';
	return local_buff;
}

static inline void btp_client_update_bounds(struct btp_context *ctx, uint32_t block)
{
	/*
	 * The blocks are marked as received in a bitfield.
	 * - 'next' marks the next packet we expect to receive.
	 * - 'bits' marks the block following the largest block
	 *   we have ever seen.
	 *
	 * [X|X|X|X| | |X| |X|X| | | | | ]
	 *          |	        |
	 *          +- next     +- bits
	 *
	 * Where 'X' marks a received block. Both 'next' and 'bits' 
	 * are initialized to 0.
	 *
	 * The difference between between 'bits' and 'next' can not
	 * be larger than the number of bits in btp_statusrep.bitfield,
	 * hence the somewhat complex update bounds function.
	 */

	assert(ctx->next <= ctx->bits);

	/* Mark packet received */
	set_bit(ctx->block_status, block);
	
	/* Update maximum missing packet */
	if (block >= ctx->bits && 
			((ctx->bits - ctx->next) < (BTP_BITFIELD_LENGTH * BITS_PER_BYTE)))
		ctx->bits = block + 1;

	/* Update 'next' mark if we have received all packets so far */
	if (block == ctx->next)
		ctx->next = block + 1;
	
	/* Update 'next' mark up to next missing packet */
	while (bit_is_set(ctx->block_status, ctx->next))
		ctx->next++;

	/* Update 'bits' mark up to */
	while (bit_is_set(ctx->block_status, ctx->bits) &&
			(ctx->bits - ctx->next < BTP_BITFIELD_LENGTH * BITS_PER_BYTE || 
			ctx->next > ctx->bits))
		ctx->bits++;

	if (ctx->next == ctx->blocks)
		ctx->complete = true;

	assert(ctx->next <= ctx->bits);
}

bool btp_client_finished(struct btp_context *ctx)
{
	assert(ctx);
	assert(ctx->connected);
	assert(ctx->state == STATE_UPLOAD || ctx->state == STATE_DOWNLOAD);

	return ctx->complete;
}

struct btp_context *btp_client_connect(uint8_t host, uint8_t port, uint32_t timeout, uint32_t opts)
{
	struct btp_context *ctx;

	ctx = calloc(1, sizeof(*ctx));
	if (!ctx)
		return NULL;

	ctx->conn = csp_connect(CSP_PRIO_NORM, host, port, timeout, opts);
	if (!ctx->conn) {
		free(ctx);
		return NULL;
	}

	ctx->host = host;
	ctx->port = port;
	ctx->connected = true;
	ctx->complete = false;
	ctx->state = STATE_CONNECTED;

	return ctx;
}

int btp_client_disconnect(struct btp_context *ctx)
{
	assert(ctx);
	assert(ctx->connected);
	assert(ctx->state != STATE_IDLE);

	csp_close(ctx->conn);
	free(ctx);

	return BTP_EOK;
}

int btp_client_upload(struct btp_context *ctx, char *backend, char *filename, 
		      char *localfile, uint8_t block_size, uint32_t offset,
		      uint32_t size, uint32_t timeout, bool force, bool paranoid)
{
	struct btp_upreq req;
	struct btp_uprep rep;
	mode_t mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH;

	assert(ctx);
	assert(ctx->connected);
	assert(ctx->state == STATE_CONNECTED);

	strncpy(ctx->filename, filename, BLOB_NAME_SIZE);
	ctx->filename[BLOB_NAME_SIZE - 1] = '\0';

	req.type = BTP_UP_REQUEST;
	strncpy(req.backend, backend, BACKEND_NAME_SIZE);
	req.backend[BACKEND_NAME_SIZE - 1] = '\0';
	strncpy(req.name, filename, BLOB_NAME_SIZE);
	req.name[BLOB_NAME_SIZE - 1] = '\0';
	req.block_size = block_size;
	req.timeout = timeout * 10;
	req.force = force;
	req.paranoid = paranoid;
	
	ctx->fd = open(localfile, O_RDONLY, mode);
	if (ctx->fd < 0) {
		perror(NULL);
		return BTP_ENOENT;
	}

	ctx->size = lseek(ctx->fd, 0, SEEK_END);
	lseek(ctx->fd, 0, SEEK_SET);

	ctx->checksum = btp_crc32c_fd(ctx->fd, 0);
	ctx->block_size = block_size;
	ctx->blocks = (ctx->size + ctx->block_size - 1) / ctx->block_size;
	ctx->block_status = calloc(1, (ctx->blocks + BITS_PER_BYTE - 1) / BITS_PER_BYTE);

	if (!ctx->block_status)
		return BTP_ENOMEM;

	req.checksum = ctx->checksum;
	req.size = ctx->size;

	if (csp_transaction_persistent(ctx->conn, 30000, &req, sizeof(req), &rep, sizeof(rep)) == 0)
		return BTP_ETIMEDOUT;

	if (rep.type != BTP_UP_REPLY)
		return BTP_EPROTO;

	if (rep.err != BTP_EOK)
		return rep.err;

	ctx->state = STATE_UPLOAD;

	if (rep.size == ctx->size && rep.checksum == ctx->checksum)
		ctx->complete = true;

	ctx->next = 0;
	ctx->bits = 0;

	return BTP_EOK;
}

int btp_client_download(struct btp_context *ctx, char *backend, char *filename,
			char *localfile, uint8_t block_size, uint32_t offset, 
			uint32_t size, uint32_t timeout, bool force, bool paranoid)
{
	int i;
	char c;
	struct btp_dwreq req;
	struct btp_dwrep rep;
	uint32_t cursize, checksum;
	mode_t mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH;

	assert(ctx);
	assert(ctx->connected);
	assert(ctx->state == STATE_CONNECTED);

	strncpy(ctx->filename, filename, BLOB_NAME_SIZE);
	ctx->filename[BLOB_NAME_SIZE - 1] = '\0';

	req.type = BTP_DW_REQUEST;
	strncpy(req.backend, backend, BACKEND_NAME_SIZE);
	req.backend[BACKEND_NAME_SIZE - 1] = '\0';
	strncpy(req.name, filename, BLOB_NAME_SIZE);
	req.name[BLOB_NAME_SIZE - 1] = '\0';
	req.block_size = block_size;
	req.offset = offset;
	req.size = size;
	req.timeout = timeout * 10;

	if (csp_transaction_persistent(ctx->conn, 10000, &req, sizeof(req), &rep, sizeof(rep)) == 0)
		return BTP_ETIMEDOUT;

	if (rep.type != BTP_DW_REPLY)
		return BTP_EPROTO;

	if (rep.err != BTP_EOK)
		return rep.err;

	ctx->state = STATE_DOWNLOAD;
	ctx->size = rep.size;
	ctx->checksum = rep.checksum;
	ctx->block_size = block_size;
	ctx->blocks = (ctx->size + ctx->block_size - 1) / ctx->block_size;

	ctx->block_status = calloc(1, (ctx->blocks + BITS_PER_BYTE - 1) / BITS_PER_BYTE);
	if (!ctx->block_status)
		return BTP_ENOMEM;

	snprintf(ctx->mapname, sizeof(ctx->mapname), MAP_FORMAT, localfile);
	ctx->mapname[sizeof(ctx->mapname) - 1] = '\0';
	
	if (force) {
		unlink(localfile);
		unlink(ctx->mapname);
	}
	
	ctx->paranoid = paranoid;
	ctx->fd = open(localfile, O_RDWR | O_CREAT, mode);
	if (ctx->fd < 0)
		return BTP_EIO;

	checksum = btp_crc32c_fd(ctx->fd, 0);
	cursize = lseek(ctx->fd, 0, SEEK_END);
	lseek(ctx->fd, 0, SEEK_SET);

	if (ctx->size == cursize && ctx->checksum == checksum)
		ctx->complete = true;

	ctx->next = 0;
	ctx->bits = 0;
	
	ctx->fd_map = open(ctx->mapname, O_RDWR, mode);
	if (ctx->fd_map >= 0) {
		if (lseek(ctx->fd_map, 0, SEEK_END) != ctx->blocks) {
			printf("%s has wrong size for this block size (expected %"PRIu32" bytes)\r\n", ctx->mapname, ctx->blocks);
			close(ctx->fd_map);
			return BTP_EINVAL;
		} else {
			lseek(ctx->fd_map, 0, SEEK_SET);
		}

		for (i = 0; i < ctx->blocks; i++) {
			if (read(ctx->fd_map, &c, 1) != 1 || (c != '+' && c != '-')) {
				printf("%s has wrong format\r\n", ctx->mapname);
				close(ctx->fd_map);
				return BTP_EINVAL;
			} else {
				if (c == '+')
					btp_client_update_bounds(ctx, i);
			}
		}
	}

	if (ctx->fd_map < 0) {
		ctx->next = 0;
		ctx->bits = 0;
		ctx->fd_map = open(ctx->mapname, O_RDWR | O_CREAT, mode);
		if (ctx->fd < 0)
			return BTP_EIO;
		for (i = 0; i < ctx->blocks; i++)
			if (write(ctx->fd_map, map_symbol_miss, 1) != 1)
				return BTP_EIO;
	}

	if (!ctx->block_status)
		return BTP_ENOMEM;

	return BTP_EOK;
}

int btp_client_request_status(struct btp_context *ctx, uint32_t timeout)
{
	struct btp_stat_pushrequest req;
	struct btp_stat_pushreply rep;

	assert(ctx);
	assert(ctx->connected);
	assert(ctx->state == STATE_UPLOAD);

	req.type = BTP_STAT_PUSH_REQUEST;

	if (csp_transaction_persistent(ctx->conn, timeout, &req, sizeof(req), &rep, sizeof(rep)) == 0)
		return BTP_ETIMEDOUT;

	if (rep.type != BTP_STAT_PUSH_REPLY)
		return BTP_EPROTO;

	if (rep.err != BTP_EOK)
		return rep.err;	

	if (ctx->next > 0)
		ctx->progress += (rep.next - ctx->next) * ctx->block_size;

	ctx->next = rep.next;
	ctx->bits = rep.bits;
	copy_bits(ctx->block_status, ctx->next, rep.bitfield, 0, ctx->bits - ctx->next);

#ifdef DEBUG
	int i;
	printf("Received status: G=%"PRIu32" B=%"PRIu32" I=[", ctx->next, ctx->bits);
	for (i = 0; i < (ctx->bits - ctx->next); i++)
		printf("%c", get_bit(ctx->block_status, ctx->next + i) ? 'X' : '.');
	printf("]\r\n");
#endif

	if (rep.next == ctx->blocks)
		ctx->complete = true;
	
	return BTP_EOK;
}

int btp_client_send_status(struct btp_context *ctx, uint32_t timeout)
{
	struct btp_stat_pullrequest req;
	struct btp_stat_pullreply rep;

	assert(ctx);
	assert(ctx->connected);
	assert(ctx->state == STATE_DOWNLOAD);

	req.type = BTP_STAT_PULL_REQUEST;
	req.next = ctx->next;
	req.bits = ctx->bits;
	
	memset(req.bitfield, 0, sizeof(req.bitfield));
	copy_bits(req.bitfield, 0, ctx->block_status, ctx->next, ctx->bits - ctx->next);

#ifdef DEBUG
	int i;
	//pr_debug("Sending status: G=%"PRIu32" B=%"PRIu32" I=[", ctx->next, ctx->bits);
	for (i = 0; i < (ctx->bits - ctx->next); i++)
		printf("%c", get_bit(ctx->block_status, ctx->next + i) ? 'X' : '.');
	//pr_debug("]\r\n");
#endif

	if (csp_transaction_persistent(ctx->conn, timeout, &req, sizeof(req), &rep, sizeof(rep)) == 0)
		return BTP_ETIMEDOUT;

	return rep.err;
}

int btp_client_get_blocks(struct btp_context *ctx, uint32_t offset, uint32_t count, uint32_t timeout, progress_cb cb, void *cbarg)
{
	int i, received = 0;
	uint32_t remain, bytes;
	csp_packet_t *packet;
	struct btp_blockreq req;
	struct btp_blockrep *rep;

	assert(ctx);
	assert(ctx->connected);
	assert(ctx->state == STATE_DOWNLOAD);

	req.type = BTP_BLOCK_REQUEST;
	req.offset = offset;
	req.count = count;

	if (csp_transaction_persistent(ctx->conn, timeout, &req, sizeof(req), NULL, 0) == 0)
		return BTP_ETIMEDOUT;
	
	for (i = 0; i < count; i++) {
		packet = csp_read(ctx->conn, timeout);

		if (!packet) {
			//pr_debug("waiting for more packets\r\n");
			if (i >= 2 && !received) {
				//pr_debug("Fast retransmit\r\n");
				return BTP_ETIMEDOUT;
			}
			continue;
		} else {
			received++;
		}

		rep = (struct btp_blockrep *) packet->data;

		if (rep->type != BTP_BLOCK_REPLY) {
			//pr_debug("not block reply\r\n");
			csp_buffer_free(packet);
			return BTP_EPROTO;
		}

		if (rep->err != BTP_EOK) {
			//pr_debug("not EOK\r\n");
			csp_buffer_free(packet);
			return rep->err;
		}

		if (rep->block < ctx->next) {
			//pr_debug("block (%"PRIu32") < next (%"PRIu32"\r\n", rep->block, ctx->next);
			csp_buffer_free(packet);
			continue;
		}

		if (lseek(ctx->fd, rep->block * ctx->block_size, SEEK_SET) < 0) {
			printf("lseek failed\r\n");
			return BTP_EIO;
		}

		if (lseek(ctx->fd_map, rep->block, SEEK_SET) < 0) {
			printf("lseek map failed\r\n");
			return BTP_EIO;
		}

		remain = ctx->size - rep->block * ctx->block_size;
		bytes = remain >= ctx->block_size ? ctx->block_size : remain;

		if (write(ctx->fd, rep->data, bytes) < bytes) {
			printf("Failed to write data\r\n");
			return BTP_EIO;
		} else if (ctx->paranoid)
			fsync(ctx->fd);

		if (write(ctx->fd_map, map_symbol_ok, 1) != 1) {
			printf("Failed to write map\r\n");
			return BTP_EIO;
		} else if (ctx->paranoid)
			fsync(ctx->fd_map);

		ctx->progress += bytes;

		//pr_debug("received block %"PRIu32"\r\n", rep->block);
		btp_client_update_bounds(ctx, rep->block);

		if (rep->seq == count - 1) {
			csp_buffer_free(packet);
			break;
		}

		csp_buffer_free(packet);

		if (cb != NULL)
			cb(ctx, cbarg);

		if (btp_client_finished(ctx))
			break;
	}

	return BTP_EOK;
}

static int btp_client_read_block(struct btp_context *ctx, uint32_t block, uint8_t *buf, uint32_t bytes)
{
	if (block > ctx->blocks)
		return BTP_EINVAL;

	if (lseek(ctx->fd, block * ctx->block_size, SEEK_SET) < 0)
		return BTP_EIO;	

	if (read(ctx->fd, buf, bytes) != bytes)
		return BTP_EIO;

	return BTP_EOK;
}

int btp_client_send_blocks(struct btp_context *ctx, uint32_t offset, uint32_t count, uint32_t timeout, progress_cb cb, void *cbarg)
{
	int i;
	bool only_missing = false;
	uint32_t remain, bytes;
	struct btp_blockrep *rep;
	csp_packet_t *packet;

	assert(ctx);

	if (ctx->state != STATE_UPLOAD)
		return BTP_EPROTO;

	if (offset == BTP_OFFSET_NEXT) {
		offset = ctx->next;
		only_missing = true;
	}

	for (i = 0; i < count && offset < ctx->blocks; i++) {
		packet = csp_buffer_get(sizeof(*rep) + ctx->block_size);
		if (!packet) {
			//pr_debug("Failed to get buffer\r\n");
			return BTP_ENOMEM;
		}

		rep = (struct btp_blockrep *) packet->data;
		rep->type = BTP_BLOCK_REPLY;
		rep->err = BTP_EOK;
		rep->seq = i;
		rep->total = count;
		rep->block = offset++;

		/* Skip forward to next missing block */
		while (only_missing && bit_is_set(ctx->block_status, offset) &&
		       offset + 1 < ctx->blocks)
			offset++;

		remain = ctx->size - rep->block * ctx->block_size;
		bytes = remain >= ctx->block_size ? ctx->block_size : remain;

		if (btp_client_read_block(ctx, rep->block, rep->data, bytes) == BTP_EOK) {
			packet->length = sizeof(*rep) + bytes;
			//pr_debug("Sending block %"PRIu32"\r\n", rep->block);
			if (!csp_send(ctx->conn, packet, timeout)) {
				//pr_debug("Failed to send block %"PRIu32"\r\n", rep->block);
				csp_buffer_free(packet);
				return BTP_ETIMEDOUT;
			}
		} else {
			//pr_debug("Failed to read block %"PRIu32"\r\n", rep->block);
			csp_buffer_free(packet);
			return BTP_EIO;
		}
	}
	
	return BTP_EOK;
}

int btp_client_list(struct btp_context *ctx, uint16_t count, char *backend, char *name, uint32_t timeout)
{
	int i;
	struct btp_listreq req;
	struct btp_listrep rep;
	struct btp_entryreq *ereq;
	struct btp_entryrep *erep;
	csp_packet_t *packet;
	char datestring[256];
	struct tm *tm;

	assert(ctx);
	assert(ctx->connected);

	req.type = BTP_LIST_REQUEST;
	strncpy(req.backend, backend, BACKEND_NAME_SIZE);
	req.backend[BACKEND_NAME_SIZE - 1] = '\0';
	strncpy(req.name, name, BLOB_NAME_SIZE);
	req.name[BLOB_NAME_SIZE - 1] = '\0';
	req.count = count;

	if (csp_transaction_persistent(ctx->conn, timeout, &req, sizeof(req), &rep, sizeof(rep)) == 0)
		return BTP_ETIMEDOUT;

	if (rep.err != BTP_EOK)
		return rep.err;

	printf("total %d\r\n", rep.entries);

	for (i = 0; i < rep.entries; i++) {
		packet = csp_read(ctx->conn, timeout);
		if (!packet)
			return BTP_ETIMEDOUT;

		erep = (struct btp_entryrep *) packet->data;

		if (erep->type != BTP_ENTRY_REPLY) {
			csp_buffer_free(packet);
			return BTP_EPROTO;
		}

		time_t ttt = erep->mtime;
		tm = localtime(&ttt);
		strftime(datestring, sizeof(datestring) - 1, "%Y-%m-%d %H:%M", tm);
		datestring[sizeof(datestring) - 1] = '\0';

		printf("%s", strperm(erep->etype, erep->mode));
		printf(" %3d", erep->nlink);
		printf(" %-4d", erep->uid);
		printf(" %-4d", erep->gid);
		printf(" %10"PRIu32"", erep->size);
		printf(" %s %s\r\n", datestring, erep->name);

		csp_buffer_free(packet);

		if (i && (i + 1) % count == 0) {
			packet = csp_buffer_get(sizeof(*ereq));
			if (!packet)
				return BTP_ENOMEM;

			ereq = (struct btp_entryreq *) packet->data;

			ereq->type = BTP_ENTRY_REQUEST;
			ereq->count = count;

			packet->length = sizeof(*ereq);
			
			if (!csp_send(ctx->conn, packet, timeout)) {
				csp_buffer_free(packet);
				return BTP_ETIMEDOUT;
			}
		}
	}

	return rep.err;
}

int btp_client_remove(struct btp_context *ctx, char *backend, char *path, uint32_t timeout)
{
	struct btp_removereq req;
	struct btp_removerep rep;

	assert(ctx);
	assert(ctx->connected);
	
	req.type = BTP_REMOVE_REQUEST;
	strncpy(req.backend, backend, BACKEND_NAME_SIZE);
	req.backend[BACKEND_NAME_SIZE - 1] = '\0';
	strncpy(req.name, path, BLOB_NAME_SIZE);
	req.name[BLOB_NAME_SIZE - 1] = '\0';

	if (csp_transaction_persistent(ctx->conn, timeout, &req, sizeof(req), &rep, sizeof(rep)) == 0)
		return BTP_ETIMEDOUT;

	return rep.err;
}

int btp_client_move(struct btp_context *ctx, char *backend, char *frompath, char *topath, uint32_t timeout)
{
	struct btp_movereq req;
	struct btp_moverep rep;

	assert(ctx);
	assert(ctx->connected);

	req.type = BTP_MOVE_REQUEST;
	strncpy(req.backend, backend, BACKEND_NAME_SIZE);
	req.backend[BACKEND_NAME_SIZE - 1] = '\0';
	strncpy(req.from, frompath, BLOB_NAME_SIZE);
	req.from[BLOB_NAME_SIZE - 1] = '\0';
	strncpy(req.to, topath, BLOB_NAME_SIZE);
	req.to[BLOB_NAME_SIZE - 1] = '\0';

	if (csp_transaction_persistent(ctx->conn, timeout, &req, sizeof(req), &rep, sizeof(rep)) == 0)
		return BTP_ETIMEDOUT;

	return rep.err;
}

int btp_client_copy(struct btp_context *ctx, char *backend, char *frompath, char *topath, uint32_t timeout)
{
	struct btp_copyreq req;
	struct btp_copyrep rep;

	assert(ctx);
	assert(ctx->connected);

	req.type = BTP_COPY_REQUEST;
	strncpy(req.backend, backend, BACKEND_NAME_SIZE);
	req.backend[BACKEND_NAME_SIZE - 1] = '\0';
	strncpy(req.from, frompath, BLOB_NAME_SIZE);
	req.from[BLOB_NAME_SIZE - 1] = '\0';
	strncpy(req.to, topath, BLOB_NAME_SIZE);
	req.to[BLOB_NAME_SIZE - 1] = '\0';

	if (csp_transaction_persistent(ctx->conn, timeout, &req, sizeof(req), &rep, sizeof(rep)) == 0)
		return BTP_ETIMEDOUT;

	return rep.err;
}

struct thread_args {
	struct btp_context *ctx;
	uint32_t timeout;
	uint32_t count;
};

static void tty_mode(struct btp_context *ctx, bool raw)
{
	struct termios current;

	if (ctx->raw == raw)
		return;

	if (tcgetattr(STDIN_FILENO, &current) < 0)
		perror("tcgetattr");

	current.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
	current.c_oflag &= ~(OPOST);
	current.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG);

	current.c_cc[VMIN] = 20;
	current.c_cc[VTIME] = 2;

	/* put terminal in raw mode after flushing */
	if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &current) < 0)
		perror("tcsetattr");

	ctx->raw = true;
}

static void *reader(void *params)
{
	struct thread_args *args;
	csp_packet_t *packet;
	struct btp_shellreq req;
	struct btp_shellrep *rep;
	int count;
	
	args = (struct thread_args *) params;

	count = args->count;

	while (count--) {
		packet = csp_read(args->ctx->conn, args->timeout);
		if (!packet) {
			printf("\r\nconnection timed out\r\n");
			break;
		}

		rep = (struct btp_shellrep *) packet->data;

		if (rep->err != BTP_EOK) {
			csp_buffer_free(packet);
			break;
		}

		tty_mode(args->ctx, rep->flags & SHELL_FLAG_CHAR);

		printf("%s", rep->output);
		fflush(stdout);

		csp_buffer_free(packet);
	}

	if (count < 0)
		printf("\r\n\r\n... (remaing output truncated)\r\n");

	req.type = BTP_SHELL_REQUEST;
	req.count = 0;
	req.flags = SHELL_FLAG_DONE;
	req.input[0] = '\0';
	if (csp_transaction_persistent(args->ctx->conn, args->timeout, &req, shell_request_size(1), NULL, 0) == 0)
		printf("could not send DONE packet\r\n");

	pthread_exit(NULL);
}

static int readinput(char *line, int max)
{
	int r;
	r = read(STDIN_FILENO, line, max);
	if (r < 1)
		r = 0;

	line[r] = '\0';

	return r+1;
}

static void *writer(void *params)
{
	int r;
	struct thread_args *args;
	struct btp_shellreq req;

	args = (struct thread_args *) params;

	while (1) {
		r = readinput(req.input, sizeof(req.input));

		if (!r)
			req.flags = SHELL_FLAG_DONE;
		req.type = BTP_SHELL_REQUEST;
		req.count = args->count;
		if (csp_transaction_persistent(args->ctx->conn, args->timeout, 
					       &req, shell_request_size(r), NULL, 2) == 0)
			break;
	}

	pthread_exit(NULL);
}

int btp_client_shell(struct btp_context *ctx, uint16_t count, char *cmd, bool raw, uint32_t timeout)
{
	int ret = BTP_ETIMEDOUT, tret = 0;
	struct btp_shellreq req;
	pthread_t treader, twriter;
	struct thread_args threadargs;
	struct termios orig_termios[3];

	tret += tcgetattr(STDIN_FILENO, &orig_termios[0]);
	tret += tcgetattr(STDOUT_FILENO, &orig_termios[1]);
	tret += tcgetattr(STDERR_FILENO, &orig_termios[2]);

	if (tret < 0) {
		perror ("ERROR getting current terminal's attributes");
		return BTP_EINVAL;
	}

	req.flags = 0;
	if (raw) {
		tty_mode(ctx, true);
		req.flags = SHELL_FLAG_CHAR;
	}

	strncpy(req.input, cmd, SHELL_INP_SIZE);
	req.type = BTP_SHELL_REQUEST;
	req.count = count;
	if (csp_transaction_persistent(ctx->conn, timeout, &req, shell_request_size(strlen(req.input)+3), NULL, 0) == 0)
		goto out;

	threadargs.ctx = ctx;
	threadargs.timeout = timeout;
	threadargs.count = count;

	if (pthread_create(&treader, NULL, reader, &threadargs) < 0)
		goto out;

	if (pthread_create(&twriter, NULL, writer, &threadargs) < 0)
		goto out;

	pthread_join(treader, NULL);
	pthread_cancel(twriter);

	ret = BTP_EOK;

out: 
	tret += tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios[0]);
	tret += tcsetattr(STDOUT_FILENO, TCSAFLUSH, &orig_termios[1]);
	tret += tcsetattr(STDERR_FILENO, TCSAFLUSH, &orig_termios[2]);

	if (tret < 0) {
		perror("ERROR restoring current terminal's attributes");
		return BTP_EINVAL;
	}

	return ret;
}

int btp_client_complete(struct btp_context *ctx, uint32_t timeout)
{
	struct btp_completerequest req;
	struct btp_completereply rep;

	assert(ctx);
	assert(ctx->connected);

	req.type = BTP_COMPLETE_REQUEST;

	/* Remove map file */
	if (ctx->state == STATE_DOWNLOAD && ctx->complete) {
		close(ctx->fd_map);
		unlink(ctx->mapname);
		if (ctx->checksum != btp_crc32c_fd(ctx->fd, 0))
			return BTP_ESTALE;
	}

	if (csp_transaction_persistent(ctx->conn, timeout, &req, sizeof(req), &rep, sizeof(rep)) == 0)
		return BTP_EOK;

	return BTP_EOK;
}
