/*
 * Copyright (c) International Business Machines Corp., 2006
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * 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., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <memory.h>
#include <limits.h>
#include <fcntl.h>

#include <libubi.h>
#include "ubimirror.h"

#define COMPARE_BUF_SIZE    (128 * 1024)

#define DEFAULT_DEV_PATTERN    "/dev/ubi%d"
#define DEFAULT_VOL_PATTERN    "/dev/ubi%d_%d"

#define EBUF(fmt...) do {			\
	snprintf(err_buf, err_buf_size, fmt);	\
} while (0)

enum {
	compare_error = -1,
	seek_error = -2,
	write_error = -3,
	read_error = -4,
	update_error = -5,
	ubi_error = -6,
	open_error = -7,
	close_error = -8,
	compare_equal = 0,
	compare_different = 1
};

/*
 * Read len number of bytes from fd.
 * Return 0 on EOF, -1 on error.
 */
static ssize_t fill_buffer(int fd, unsigned char *buf, ssize_t len)
{
	ssize_t got, have = 0;

	do {
		got = read(fd, buf + have, len - have);
		if (got == -1 && errno != EINTR)
			return -1;
		have += got;
	} while (got > 0 && have < len);
	return have;
}

/*
 * Write len number of bytes to fd.
 * Return bytes written (>= 0), -1 on error.
 */
static ssize_t flush_buffer(int fd, unsigned char *buf, ssize_t len)
{
	ssize_t done, have = 0;

	do {
		done = write(fd, buf + have, len - have);
		if (done == -1 && errno != EINTR)
			return -1;
		have += done;
	} while (done > 0 && have < len);
	return have;
}

/*
 *  Compare two files.  Return 0, 1, or -1, depending on whether the
 *  files are equal, different, or an error occured.
 *  Return compare-different when target volume can not be read. Might be
 *  an interrupted volume update and then the target device returns -EIO but
 *  can be updated.
 *
 *  fd_a is source
 *  fd_b is destination
 */
static int compare_files(int fd_a, int fd_b)
{
	unsigned char buf_a[COMPARE_BUF_SIZE], buf_b[COMPARE_BUF_SIZE];
	ssize_t len_a, len_b;
	int rc;

	for (;;) {
		len_a = fill_buffer(fd_a, buf_a, sizeof(buf_a));
		if (len_a == -1) {
			rc = compare_error;
			break;
		}
		len_b = fill_buffer(fd_b, buf_b, sizeof(buf_b));
		if (len_b == -1) {
			rc = compare_different;
			break;
		}
		if (len_a != len_b) {
			rc = compare_different;
			break;
		}
		if (len_a == 0) {	/* Size on both files equal and EOF */
			rc = compare_equal;
			break;
		}
		if (memcmp(buf_a, buf_b, len_a) != 0 ) {
			rc = compare_different;
			break;
		}
	}
	/* Position both files at the beginning */
	if (lseek(fd_a, 0, SEEK_SET) == -1 ||
	   lseek(fd_b, 0, SEEK_SET) == -1)
		rc = seek_error;
	return rc;
}

int vol_get_used_bytes(int vol_fd, unsigned long long *bytes)
{
	off_t res;

	res = lseek(vol_fd, 0, SEEK_END);
	if (res == (off_t)-1)
		return -1;
	*bytes = (unsigned long long) res;
	res = lseek(vol_fd, 0, SEEK_SET);
	return res == (off_t)-1 ? -1 : 0;
}

static int copy_files(libubi_t ulib, int fd_in, int fd_out)
{
	unsigned char buf_a[COMPARE_BUF_SIZE];
	ssize_t len_a, len_b;
	unsigned long long update_size, copied;

	if (vol_get_used_bytes(fd_in, &update_size) == -1 ||
	    ubi_update_start(ulib, fd_out, update_size) == -1)
		return update_error;
	for (copied = 0; copied < update_size; copied += len_b ) {
		len_a = fill_buffer(fd_in, buf_a, sizeof(buf_a));
		if (len_a == -1)
			return read_error;
		if (len_a == 0)		/* Reach EOF */
			return 0;
		len_b = flush_buffer(fd_out, buf_a, len_a);
		if (len_b != len_a)
			return write_error;
	}
	return 0;
}

int ubimirror(uint32_t devno, int seqnum, uint32_t *ids, ssize_t ids_size,
		char *err_buf, size_t err_buf_size)
{
	int rc = 0;
	uint32_t src_id;
	char path[PATH_MAX];
	libubi_t ulib;
	int fd_in = -1, i = 0, fd_out = -1;

	if (ids_size == 0)
		return 0;
	else {
		if ((seqnum < 0) || (seqnum > (ids_size - 1))) {
			EBUF("volume id %d out of range", seqnum);
			return EUBIMIRROR_NO_SRC;
		}
		src_id = ids[seqnum];
	}

	ulib = libubi_open();
	if (ulib == NULL)
		return ubi_error;

	snprintf(path, PATH_MAX, DEFAULT_VOL_PATTERN, devno, src_id);

	fd_in = open(path, O_RDONLY);
	if (fd_in == -1) {
		EBUF("open error source volume %d", ids[i]);
		rc = open_error;
		goto err;
	}

	for (i = 0; i < ids_size; i++) {
		if (ids[i] == src_id)		/* skip self-mirror */
			continue;

		snprintf(path, PATH_MAX, DEFAULT_VOL_PATTERN, devno, ids[i]);

		fd_out = open(path, O_RDWR);
		if (fd_out < 0){
			EBUF("open error destination volume %d", ids[i]);
			rc = open_error;
			goto err;
		}
		rc = compare_files(fd_in, fd_out);
		if (rc < 0) {
			EBUF("compare error volume %d and %d", src_id, ids[i]);
			goto err;
		} else if (rc == compare_different) {
			rc = copy_files(ulib, fd_in, fd_out);
			if (rc != 0) {
				EBUF("mirror error volume %d to %d", src_id,
						ids[i]);
				goto err;
			}
		}
		if ((rc = close(fd_out)) == -1) {
			EBUF("close error volume %d", ids[i]);
			rc = close_error;
			goto err;
		} else
			fd_out = -1;
	}
err:
	if (fd_out != -1)
		close(fd_out);
	if (fd_in != -1)
		close(fd_in);
	if (ulib != NULL)
		libubi_close(ulib);
	return rc;
}