#include "compat.h"
#include "backuppcd.h"
#include "backuppcd-common.h"
#include "net.h"

typedef enum {
        BPC_CDT_END,
        BPC_CDT_UINT8,
        BPC_CDT_UINT16,
        BPC_CDT_UINT32,
        BPC_CDT_UINT64,
        BPC_CDT_BYTEARRAY,
        BPC_CDT_STRING,
        BPC_CDT_STRING_PTR,
} backuppc_clntdt_t;

#ifndef MSG_WAITALL
#define MSG_WAITALL 0
#endif

#ifndef BPC_MAXPATH_LEN
#define BPC_MAXPATH_LEN 4096
#endif                                                                                                                                    

/*
 * Example BackupPCd client
 */

int backuppcd_client(const char *host, const int port, const char *username, const char *password, const int get);

int main(int argc, char **argv) {
	char *host, *port_str, *command, *username, *password;
	int port;
	int cmd_ret = -1;

	if (argc != 6) {
		printf("Usage: backuppcd-client <host> <port> <username> <password> {GET|LIST}\n");
		return(EXIT_FAILURE);
	}

	host = argv[1];
	port_str = argv[2];
	username = argv[3];
	password = argv[4];
	command = argv[5];

	port = strtoul(port_str, NULL, 10);

	if (strcasecmp(command, "list") == 0) {
		cmd_ret = backuppcd_client(host, port, username, password, 0);
	} else if (strcasecmp(command, "get") == 0) {
		cmd_ret = backuppcd_client(host, port, username, password, 1);
	}

	if (cmd_ret < 0) {
		fprintf(stderr, "Failed.\n");
		return(EXIT_FAILURE);
	}

	fprintf(stderr, "Done.\n");
	return(EXIT_SUCCESS);
}


int backuppcd_client_read(const int sockid, ...) {
	backuppc_clntdt_t typeid;
	va_list ap;
	uint8_t *u8v;
	uint16_t *u16v;
	uint32_t *u32v;
	uint64_t *u64v;
	char *cpv, **cppv;
	void *vpv = NULL, *vpv_s;
	size_t vpv_len;
	ssize_t read_ret;

	va_start(ap, sockid);
	while (1) {
		typeid = va_arg(ap, backuppc_clntdt_t);

		if (typeid == BPC_CDT_END) {
			break;
		}

		switch (typeid) {
			case BPC_CDT_UINT8:
				u8v = va_arg(ap, uint8_t *);
				vpv_len = sizeof(*u8v);
				vpv = u8v;
				break;
			case BPC_CDT_UINT16:
				u16v = va_arg(ap, uint16_t *);
				vpv_len = sizeof(*u16v);
				vpv = u16v;
				break;
			case BPC_CDT_UINT32:
				u32v = va_arg(ap, uint32_t *);
				vpv_len = sizeof(*u32v);
				vpv = u32v;
				break;
			case BPC_CDT_UINT64:
				u64v = va_arg(ap, uint64_t *);
				vpv_len = sizeof(*u64v);
				vpv = u64v;
				break;
			case BPC_CDT_BYTEARRAY:
				vpv_len = va_arg(ap, size_t);
				vpv = va_arg(ap, void *);
				break;
			case BPC_CDT_STRING:
				vpv_len = va_arg(ap, size_t);
				vpv_len--;
				cpv = va_arg(ap, char *);
				cpv[vpv_len] = '\0';
				vpv = cpv;
				break;
			case BPC_CDT_STRING_PTR:
				vpv_len = va_arg(ap, size_t);
				cppv = va_arg(ap, char **);
				if (*cppv == NULL) {
					cpv = *cppv = malloc(vpv_len + 1);
					cpv[vpv_len] = '\0';
				} else {
					cpv = *cppv;
				}
				vpv = cpv;
				break;
			default:
				return(0);
		}

		vpv_s = vpv;
		while (vpv_len) {
			read_ret = recv(sockid, vpv, vpv_len, MSG_WAITALL);

			if (read_ret <= 0) {
				return(0);
			}

			vpv_len -= read_ret;
			vpv += read_ret;
		}
		vpv = vpv_s;

		switch (typeid) {
			case BPC_CDT_UINT16:
				u16v = vpv;
				*u16v = ntohs(*u16v);
				break;
			case BPC_CDT_UINT32:
				u32v = vpv;
				*u32v = ntohl(*u32v);
				break;
			case BPC_CDT_UINT64:
				u64v = vpv;
				*u64v = ntohll(*u64v);
				break;
			default:
				break;
		}
	}
	va_end(ap);

	return(1);
}

int backuppcd_client_write(const int sockid, ...) {
	backuppc_clntdt_t typeid;
	va_list ap;
	uint8_t u8v;
	uint16_t u16v;
	uint32_t u32v;
	uint64_t u64v;
	void *vpv;
	char *cpv;
	size_t vpv_len;
	ssize_t write_ret;

	va_start(ap, sockid);
	while (1) {
		typeid = va_arg(ap, backuppc_clntdt_t);

		if (typeid == BPC_CDT_END) {
			break;
		}

		switch (typeid) {
			case BPC_CDT_UINT8:
				u8v = va_arg(ap, int);
				vpv_len = sizeof(u8v);
				vpv = &u8v;
				break;
			case BPC_CDT_UINT16:
				u16v = va_arg(ap, int);
				u16v = htons(u16v);
				vpv_len = sizeof(u16v);
				vpv = &u16v;
				break;
			case BPC_CDT_UINT32:
				u32v = va_arg(ap, uint32_t);
				u32v = htonl(u32v);
				vpv_len = sizeof(u32v);
				vpv = &u32v;
				break;
			case BPC_CDT_UINT64:
				u64v = va_arg(ap, uint64_t);
				u64v = htonll(u64v);
				vpv_len = sizeof(u64v);
				vpv = &u64v;
				break;
			case BPC_CDT_BYTEARRAY:
				vpv_len = va_arg(ap, size_t);
				vpv = va_arg(ap, void *);
				break;
			case BPC_CDT_STRING:
				cpv = va_arg(ap, char *);
				vpv_len = strlen(cpv);
				vpv = cpv;
				break;
			case BPC_CDT_STRING_PTR:
			default:
				return(0);
		}

		while (vpv_len) {
			write_ret = send(sockid, vpv, vpv_len, 0);

			if (write_ret < 0) {
				return(0);
			}

			vpv_len -= write_ret;
			vpv += write_ret;
		}
	}
	va_end(ap);

	return(1);
}



int backuppcd_client_auth(const int sockid, const char *username, const char *password) {
	uint8_t cmd_reply;
	uint8_t status;

	if (!backuppcd_client_write(sockid,
	                            BPC_CDT_UINT8, (uint8_t) BPC_CMD_AUTH,
	                            BPC_CDT_UINT16, (uint16_t) strlen(username),
	                            BPC_CDT_UINT16, (uint16_t) strlen(password),
	                            BPC_CDT_STRING, username,
	                            BPC_CDT_STRING, password,
	                            BPC_CDT_END)) {
		return(0);
	}

	if (!backuppcd_client_read(sockid,
	                           BPC_CDT_UINT8, (uint8_t *) &cmd_reply,
	                           BPC_CDT_UINT8, (uint8_t *) &status,
	                           BPC_CDT_END)) {
		return(0);
	}

	if (cmd_reply != BPC_CMD_AUTH_REPLY) {
		return(0);
	}

	if (status != BPC_STATUS_OKAY) {
		return(0);
	}

	return(1);
}

int backuppcd_client_listget(const int sockid, const int get) {
	backuppc_cmd_t sendcmd;
	unsigned char *rootpath, pathbuf[8192], *outpathbuf = NULL;
	unsigned char *buf = NULL;
	uint32_t attr_sect_len, blocksize, pathnamelen, blocknum = 0;
	uint64_t filesize, byteswritten = 0;
	uint32_t bufsize = 0;
	uint32_t attr_uid, attr_gid, attr_ctime, attr_mtime;
	uint32_t attr_len;
	uint16_t attr_id;
	uint16_t attr_mode;
	uint8_t filetype, cmd;
	size_t writelen;
	mode_t modeval;
	char *attr_buf = NULL;
	char *attr_linkdest;
	char type_string[][7] = {"dir", "file", "syml", "sock", "fifo", "blk", "chr", "hrdl"};
	int fd = -1;

	if (get) {
		sendcmd = BPC_CMD_GET;
	} else {
		sendcmd = BPC_CMD_LIST;
	}

	rootpath = "/home/rkeene/devel/backuppcd/tmp/src";

	if (!backuppcd_client_write(sockid,
	                            BPC_CDT_UINT8, (uint8_t) sendcmd,
	                            BPC_CDT_UINT8, (uint8_t) 1, /* option recursive = true */
	                            BPC_CDT_UINT32, (uint32_t) 0, /* exclude section length */
	                            BPC_CDT_UINT32, (uint32_t) 0, /* include section length */
	                            BPC_CDT_UINT32, (uint32_t) strlen(rootpath),
	                            BPC_CDT_STRING, (char *) rootpath,
	                            /* Exclude section is 0 bytes */
	                            /* Include section is 0 bytes */
	                            BPC_CDT_END)) {
		CHECKPOINT;
		return(0);
	}

	if (!backuppcd_client_read(sockid,
	                           BPC_CDT_UINT8, (uint8_t *) &cmd,
	                           BPC_CDT_END)) {
		CHECKPOINT;
		return(0);
	}

	if (cmd != (sendcmd | 0x80)) {
		CHECKPOINT;
		return(0);
	}

	while (1) {
		attr_uid = 0;
		attr_gid = 0;
		attr_mtime = 0;
		attr_ctime = 0;
		attr_linkdest = NULL;

		if (!backuppcd_client_read(sockid,
		                           BPC_CDT_UINT8, (uint8_t *) &filetype,
		                           BPC_CDT_END)) {
			CHECKPOINT;
			return(0);
		}

		if (filetype == 0xff) {
			break;
		}

		if (!backuppcd_client_read(sockid,
		                           BPC_CDT_UINT32, (uint32_t *) &attr_sect_len,
		                           BPC_CDT_UINT64, (uint64_t *) &filesize,
		                           BPC_CDT_UINT32, (uint32_t *) &blocksize,
		                           BPC_CDT_UINT32, (uint32_t *) &pathnamelen,
		                           BPC_CDT_END)) {
			CHECKPOINT;
			return(0);
		}


		if (pathnamelen >= sizeof(pathbuf)) {
			CHECKPOINT;
			return(0);
		}

		if (!backuppcd_client_read(sockid,
		                           BPC_CDT_STRING, (size_t) (pathnamelen + 1), &pathbuf,
		                           BPC_CDT_END)) {
			CHECKPOINT;
			return(0);
		}

		attr_uid = 0;
		attr_gid = 0;
		attr_ctime = 0;
		attr_mtime = 0;
		attr_mode = 0;

		if (attr_sect_len != 0) {
			while (1) {
				if (!backuppcd_client_read(sockid,
				                           BPC_CDT_UINT16, (uint16_t *) &attr_id,
				                           BPC_CDT_END)) {
					CHECKPOINT;
					return(0);
				}

				if (attr_id == 0xffff) {
					break;
				}

				if (!backuppcd_client_read(sockid,
				                           BPC_CDT_UINT32, (uint32_t *) &attr_len,
				                           BPC_CDT_END)) {
					CHECKPOINT;
					return(0);
				}

				switch ((backuppc_attrid_t) attr_id) {
					case BPC_ATTRID_NOP:
						if (attr_len != 0) {
							CHECKPOINT;
							return(0);
						}
						break;
					case BPC_ATTRID_MTIME:
						if (!backuppcd_client_read(sockid,
						                           BPC_CDT_UINT32, (uint32_t *) &attr_mtime,
						                           BPC_CDT_END)) {
							CHECKPOINT;
							return(0);
						}
						break;
					case BPC_ATTRID_CTIME:
						if (!backuppcd_client_read(sockid,
						                           BPC_CDT_UINT32, (uint32_t *) &attr_ctime,
						                           BPC_CDT_END)) {
							CHECKPOINT;
							return(0);
						}
						break;
					case BPC_ATTRID_UID:
						if (!backuppcd_client_read(sockid,
						                           BPC_CDT_UINT32, (uint32_t *) &attr_uid,
						                           BPC_CDT_END)) {
							CHECKPOINT;
							return(0);
						}
						break;
					case BPC_ATTRID_GID:
						if (!backuppcd_client_read(sockid,
						                           BPC_CDT_UINT32, (uint32_t *) &attr_gid,
						                           BPC_CDT_END)) {
							CHECKPOINT;
							return(0);
						}
						break;
					case BPC_ATTRID_ACL:
						if (!backuppcd_client_read(sockid,
						                           BPC_CDT_UINT16, (uint16_t *) &attr_mode,
						                           BPC_CDT_END)) {
							CHECKPOINT;
							return(0);
						}

						break;
					case BPC_ATTRID_SYMLINKDEST:
						if (attr_len > BPC_MAXPATH_LEN) {
							CHECKPOINT;
							return(0);
						}

						if (attr_buf) {
							free(attr_buf);
						}

						attr_buf = NULL;

						if (!backuppcd_client_read(sockid,
						                           BPC_CDT_STRING_PTR, (size_t) attr_len, (char **) &attr_buf,
						                           BPC_CDT_END)) {
							CHECKPOINT;
							return(0);
						}

						attr_linkdest = strdup(attr_buf);
						break;
					case BPC_ATTRID_HRDLINKDEST:
						if (attr_len > BPC_MAXPATH_LEN) {
							CHECKPOINT;
							return(0);
						}

						if (attr_buf) {
							free(attr_buf);
						}

						attr_buf = NULL;

						if (!backuppcd_client_read(sockid,
						                           BPC_CDT_STRING_PTR, (size_t) attr_len, (char **) &attr_buf,
						                           BPC_CDT_END)) {
							CHECKPOINT;
							return(0);
						}

						attr_linkdest = strdup(attr_buf);
						break;
				}
			}
		}

		if (filetype == BPC_DIRENT_SYMLINK) {
			if (attr_linkdest == NULL) {
				CHECKPOINT;
			}
		}

		printf("[%4s] %04o %6lu %6lu %10llu %12lu %s",
		       type_string[filetype],
		       (unsigned int) attr_mode,
		       (unsigned long) attr_uid,
		       (unsigned long) attr_gid,
		       (unsigned long long) filesize,
		       (unsigned long) attr_mtime,
		       pathbuf);
		if (filetype == BPC_DIRENT_SYMLINK || filetype == BPC_DIRENT_HRDLINK) {
			printf(" -> %s", attr_linkdest);
		}
		printf("\n");

		if (cmd == BPC_CMD_GET_REPLY) {
			outpathbuf = pathbuf + 1;

			if (filetype == BPC_DIRENT_DIR) {
				backuppc_mkdir(outpathbuf);
			}

#ifdef HAVE_SYMLINK
			if (filetype == BPC_DIRENT_SYMLINK) {
				if (attr_linkdest) {
					if (attr_linkdest[0] == '/') {
						/* We must mangle attr_linkdest in the same way we did pathbuf. */
						symlink(attr_linkdest + 1, outpathbuf);
					} else {
						symlink(attr_linkdest, outpathbuf);
					}
				}
			}
#endif

			if (filetype == BPC_DIRENT_HRDLINK) {
				if (attr_linkdest) {
					if (attr_linkdest[0] == '/') {
						/* We must mangle attr_linkdest in the same way we did pathbuf. */
						link(attr_linkdest + 1, outpathbuf);
					} else {
						link(attr_linkdest, outpathbuf);
					}
				}
			}

			if (filetype == BPC_DIRENT_FILE) {
				fd = open(outpathbuf, O_WRONLY | O_CREAT | O_TRUNC, 0600);
			} else {
				fd = -1;
			}

			byteswritten = 0;

			if (bufsize != blocksize) {
				if (buf) {
					free(buf);
				}

				bufsize = blocksize;

				buf = malloc(bufsize);

				if (!buf) {
					CHECKPOINT;
					return(0);
				}
			}

			while (1) {
				if (!backuppcd_client_read(sockid,
				                          BPC_CDT_UINT32, (uint32_t *) &blocknum,
				                          BPC_CDT_END)) {
					CHECKPOINT;
					return(0);
				}

				if (blocknum == 0xffffffff) {
					break;
				}

				if (!backuppcd_client_read(sockid,
				                          BPC_CDT_BYTEARRAY, (size_t) blocksize, (void *) buf,
				                          BPC_CDT_END)) {

					CHECKPOINT;
					return(0);
				}

				if (fd >= 0) {
					if ((byteswritten + blocksize) > filesize) {
						if (filesize > byteswritten) {
							writelen = filesize - byteswritten;
						} else {
							writelen = 0;
						}
					} else {
						writelen = blocksize;
					}

					if (writelen > 0) {
						write(fd, buf, writelen);
					}

					byteswritten += blocksize;
				}
			}

			if (fd >= 0) {
				close(fd);
			}

			if (attr_mode) {
				modeval = 0;

				if ((attr_mode & BPC_ACL_XUSR) == BPC_ACL_XUSR) {
					modeval |= S_IXUSR;
				}
				if ((attr_mode & BPC_ACL_WUSR) == BPC_ACL_WUSR) {
					modeval |= S_IWUSR;
				}
				if ((attr_mode & BPC_ACL_RUSR) == BPC_ACL_RUSR) {
					modeval |= S_IRUSR;
				}
#ifndef _USE_WIN32_
				if ((attr_mode & BPC_ACL_XGRP) == BPC_ACL_XGRP) {
					modeval |= S_IXGRP;
				}
				if ((attr_mode & BPC_ACL_WGRP) == BPC_ACL_WGRP) {
					modeval |= S_IWGRP;
				}
				if ((attr_mode & BPC_ACL_RGRP) == BPC_ACL_RGRP) {
					modeval |= S_IRGRP;
				}
				if ((attr_mode & BPC_ACL_XOTH) == BPC_ACL_XOTH) {
					modeval |= S_IXOTH;
				}
				if ((attr_mode & BPC_ACL_WOTH) == BPC_ACL_WOTH) {
					modeval |= S_IWOTH;
				}
				if ((attr_mode & BPC_ACL_ROTH) == BPC_ACL_ROTH) {
					modeval |= S_IROTH;
				}
#endif

				chmod(outpathbuf, modeval);
			}

#ifdef HAVE_LCHOWN
			lchown(outpathbuf, attr_uid, attr_gid);
#else
#ifdef HAVE_CHOWN
			chown(outpathbuf, attr_uid, attr_gid);
#endif
#endif

		}

		if (attr_linkdest) {
			free(attr_linkdest);
			attr_linkdest = NULL;
		}
	}

	CHECKPOINT;
	return(1);
}

int backuppcd_client(const char *host, const int port, const char *username, const char *password, const int get) {
	int sockid;

	sockid = net_connect_tcp(host, port);

	if (sockid < 0) {
		return(-1);
	}

	if (!backuppcd_client_auth(sockid, username, password)) {
		return(-1);
	}

	if (!backuppcd_client_listget(sockid, get)) {
		return(-1);
	}

	return(0);
}
