/*
 * Copyright (C) 2006-2007 Nokia Corporation
 * Copyright (C) 2016 sigma star gmbh
 *
 * 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; see the file COPYING. If not, write to the Free Software
 * Foundation, 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 *
 * Test sub-page read and write on MTD device.
 *
 * Author: David Oberhollenzer <david.oberhollenzer@sigma-star.at>
 *
 * Based on linux subpagetest.c
 * Author: Adrian Hunter <ext-adrian.hunter@nokia.com>
 */
#define PROGRAM_NAME "nandsubpagetest"

#include <mtd/mtd-user.h>
#include <unistd.h>
#include <stdlib.h>
#include <libmtd.h>
#include <getopt.h>
#include <stdio.h>
#include <fcntl.h>
#include <time.h>

#include "common.h"

#define KEEP_CONTENTS 0x01
#define SEED_SET 0x02

static struct mtd_dev_info mtd;
static const char *mtddev;
static libmtd_t mtd_desc;

static unsigned char *bbt=NULL, *writebuf=NULL, *backup=NULL, *readbuf=NULL;
static int peb = -1, seed = -1, skip = -1, ebcnt = -1, flags = 0;
static int fd, bufsize;
static unsigned int rnd_state;

static const struct option options[] = {
	{ "help", no_argument, NULL, 'h' },
	{ "keep", no_argument, NULL, 'k' },
	{ "peb", required_argument, NULL, 'b' },
	{ "count", required_argument, NULL, 'c' },
	{ "skip", required_argument, NULL, 's' },
	{ "seed", required_argument, NULL, 'S' },
	{ NULL, 0, NULL, 0 },
};

static NORETURN void usage(int status)
{
	fputs(
	"Usage: "PROGRAM_NAME" [OPTIONS] <device>\n\n"
	"Options:\n"
	"  -h, --help         Display this help output\n"
	"  -b, --peb <num>    Index of the first erase block to use\n"
	"  -c, --count <num>  Number of erase blocks to use (default all)\n"
	"  -s, --skip <num>   Number of erase blocks to skip\n"
	"  -S, --seed <num>   Seed for pseudor random number generator\n"
	"  -k, --keep         Restore existing contents after test\n",
	status==EXIT_SUCCESS ? stdout : stderr);
	exit(status);
}

static long read_num(int opt, const char *arg)
{
	char *end;
	long num;

	num = strtol(arg, &end, 0);

	if (!end || *end != '\0') {
		fprintf(stderr, "-%c: expected integer argument\n", opt);
		exit(EXIT_FAILURE);
	}
	return num;
}

static void process_options(int argc, char **argv)
{
	int c;

	while (1) {
		c = getopt_long(argc, argv, "hb:c:s:Sk", options, NULL);
		if (c == -1)
			break;

		switch (c) {
		case 'b':
			if (peb >= 0)
				goto failmulti;
			peb = read_num(c, optarg);
			if (peb < 0)
				goto failarg;
			break;
		case 'c':
			if (ebcnt >= 0)
				goto failmulti;
			ebcnt = read_num(c, optarg);
			if (ebcnt < 0)
				goto failarg;
			break;
		case 's':
			if (skip >= 0)
				goto failmulti;
			skip = read_num(c, optarg);
			if (skip < 0)
				goto failarg;
			break;
		case 'S':
			if (flags & SEED_SET)
				goto failmulti;
			seed = read_num(c, optarg);
			flags |= SEED_SET;
			break;
		case 'k':
			if (flags & KEEP_CONTENTS)
				goto failmulti;
			flags |= KEEP_CONTENTS;
			break;
		case 'h':
			usage(EXIT_SUCCESS);
		default:
			exit(EXIT_FAILURE);
		}
	}

	if (optind < argc)
		mtddev = argv[optind++];
	else
		errmsg_die("No device specified!\n");

	if (optind < argc)
		usage(EXIT_FAILURE);
	if (peb < 0)
		peb = 0;
	if (!(flags & SEED_SET))
		seed = time(NULL);
	if (skip < 0)
		skip = 0;
	return;
failmulti:
	errmsg_die("'-%c' specified more than once!", c);
failarg:
	errmsg_die("Invalid argument for '-%c'!", c);
}

static int write_eraseblock(int ebnum)
{
	int i, j, err, off = 0;

	printf("writing first 2 sub-pages on PEB %d\n", ebnum);
	for (j = 0; j < 2; ++j) {
		for (i = 0; i < mtd.subpage_size; ++i)
			writebuf[i] = rand_r(&rnd_state);

		err = mtd_write(mtd_desc, &mtd, fd, ebnum, off, writebuf,
						mtd.subpage_size, NULL, 0, 0);
		if (err)
			return -1;

		off += mtd.subpage_size;
	}
	return 0;
}

static int write_eraseblock2(int ebnum)
{
	int err, off = 0, i, k;

	printf("writing with exponential offsets & sizes on PEB %d\n", ebnum);
	for (k = 1; k < 33; ++k) {
		if (off + (mtd.subpage_size * k) > mtd.eb_size)
			break;

		for (i = 0; i < (mtd.subpage_size * k); ++i)
			writebuf[i] = rand_r(&rnd_state);

		err = mtd_write(mtd_desc, &mtd, fd, ebnum, off,
						writebuf, mtd.subpage_size * k, NULL, 0, 0);
		if (err)
			return -1;

		off += mtd.subpage_size * k;
	}
	return 0;
}

static void print_subpage(unsigned char *p)
{
	int i, j;

	for (i = 0; i < mtd.subpage_size; ) {
		for (j = 0; i < mtd.subpage_size && j < 32; ++i, ++j)
			fprintf(stderr, "%02x", *p++);
		fprintf(stderr, "\n");
	}
}

static int verify_eraseblock(int ebnum)
{
	int i, j, ret = 0, off = 0;

	printf("verifying first 2 sub-pages of PEB %d\n", ebnum);
	for (j = 0; j < 2; ++j) {
		for (i = 0; i < mtd.subpage_size; ++i)
			writebuf[i] = rand_r(&rnd_state);

		memset(readbuf, 0, mtd.subpage_size);
		if (mtd_read(&mtd, fd, ebnum, off, readbuf, mtd.subpage_size))
			return -1;

		if (memcmp(readbuf, writebuf, mtd.subpage_size)) {
			fprintf(stderr, "error: verify failed at PEB %d, offset %#x\n",
					ebnum, off);
			fputs("------------- written----------------\n", stderr);
			print_subpage(writebuf);
			fputs("------------- read ------------------\n", stderr);
			print_subpage(readbuf);
			fputs("-------------------------------------\n", stderr);
			ret = -1;
		}
		off += mtd.subpage_size;
	}
	return ret;
}

static int verify_eraseblock2(int ebnum)
{
	int ret = 0, i, k, off = 0;

	printf("verifying exponential offset & size writes on PEB %d\n", ebnum);
	for (k = 1; k < 33; ++k) {
		if (off + (mtd.subpage_size * k) > mtd.eb_size)
			break;

		for (i = 0; i < (mtd.subpage_size * k); ++i)
			writebuf[i] = rand_r(&rnd_state);

		memset(readbuf, 0, mtd.subpage_size * k);
		if (mtd_read(&mtd, fd, ebnum, off, readbuf, mtd.subpage_size * k))
			return -1;

		if (memcmp(readbuf, writebuf, mtd.subpage_size * k)) {
			fprintf(stderr, "error: verify failed at PEB %d, offset %#x\n",
					ebnum, off);
			ret = -1;
		}
		off += mtd.subpage_size * k;
	}
	return ret;
}

static int verify_eraseblock_ff(int ebnum)
{
	int j, ret = 0, off = 0;

	memset(writebuf, 0xFF, mtd.subpage_size);

	for (j = 0; j < mtd.eb_size / mtd.subpage_size; ++j) {
		memset(readbuf, 0, mtd.subpage_size);
		if (mtd_read(&mtd, fd, ebnum, off, readbuf, mtd.subpage_size))
			return -1;

		if (memcmp(readbuf, writebuf, mtd.subpage_size)) {
			fprintf(stderr, "error: verify 0xff failed at PEB %d, "
					"offset %#x\n", ebnum, off);
			ret = -1;
		}
		off += mtd.subpage_size;
	}
	return ret;
}

static int verify_all_eraseblocks_ff(void)
{
	int i, eb, err;

	puts("verifying all eraseblocks for 0xff");
	for (i = 0; i < ebcnt; ++i) {
		if (bbt[i])
			continue;

		eb = peb + i * (skip + 1);
		err = verify_eraseblock_ff(eb);
		if (err)
			return err;
	}
	printf("verified %d eraseblocks\n", ebcnt);
	return 0;
}

static int erase_good_eraseblocks(void)
{
	int i, eb;

	printf("erasing good eraseblocks\n");
	for (i = 0; i < ebcnt; ++i) {
		if (bbt[i])
			continue;
		eb = peb + i * (skip + 1);
		if (mtd_erase(mtd_desc, &mtd, fd, eb))
			return -1;
	}
	return 0;
}

static int remove_test_data(void)
{
	if (erase_good_eraseblocks())
		return -1;
	if (verify_all_eraseblocks_ff())
		return -1;
	return 0;
}

int main(int argc, char **argv)
{
	int i, eb, err = 0, status = EXIT_FAILURE;
	unsigned char *backupptr;

	process_options(argc, argv);

	mtd_desc = libmtd_open();
	if (!mtd_desc)
		return errmsg("can't initialize libmtd");
	if (mtd_get_dev_info(mtd_desc, mtddev, &mtd) < 0)
		return errmsg("mtd_get_dev_info failed");
	if (mtd.type!=MTD_MLCNANDFLASH && mtd.type!=MTD_NANDFLASH)
		return errmsg("%s is not a NAND flash!", mtddev);
	if (ebcnt < 0)
		ebcnt = (mtd.eb_cnt - peb) / (skip + 1);
	if (peb >= mtd.eb_cnt)
		return errmsg("physical erase block %d is out of range!", peb);
	eb = peb + (ebcnt - 1)*(skip + 1);
	if (eb >= mtd.eb_cnt)
		return errmsg("last physical erase block %d is out of range!", eb);

	bufsize = mtd.subpage_size * 32;
	writebuf = xmalloc(bufsize);
	readbuf = xmalloc(bufsize);
	bbt = xzalloc(ebcnt);

	if ((fd = open(mtddev, O_RDWR)) == -1) {
		perror(mtddev);
		goto out_cleanup;
	}

	/* find bad blocks */
	for (i = 0; i < ebcnt; ++i) {
		eb = peb + i * (skip + 1);
		bbt[i] = mtd_is_bad(&mtd, fd, eb);

		if (bbt[i])
			printf("ignoring bad erase block %d\n", eb);
	}

	/* create block backup */
	if (flags & KEEP_CONTENTS) {
		eb = 0;
		for (i = 0; i < ebcnt; ++i) {
			if (!bbt[i])
				++eb;
		}
		backup = malloc(mtd.eb_size * eb);
		if (!backup) {
			fprintf(stderr, "not enough memory to keep block contents!\n");
			goto out_cleanup;
		}
		printf("reading %d blocks into memory\n", eb);
		backupptr = backup;
		for (i = 0; i < ebcnt; ++i) {
			if (bbt[i])
				continue;
			eb = peb + i*(skip+1);
			err = mtd_read(&mtd, fd, eb, 0, backupptr, mtd.eb_size);
			if (err) {
				fprintf(stderr, "error reading block %d!\n", eb);
				goto out_cleanup;
			}
			backupptr += mtd.eb_size;
		}
	}

	/* write first 2 sub-pages of each block */
	if (remove_test_data())
		goto out;

	rnd_state = seed;
	for (i = 0; i < ebcnt; ++i) {
		if (bbt[i])
			continue;
		err = write_eraseblock(i);
		if (err)
			goto out;
	}

	rnd_state = seed;
	for (i = 0; i < ebcnt; ++i) {
		if (bbt[i])
			continue;
		err = verify_eraseblock(i);
		if (err)
			goto out;
	}

	/* write with exponential offset & size */
	if (remove_test_data())
		goto out;

	rnd_state = seed;
	for (i = 0; i < ebcnt; ++i) {
		if (bbt[i])
			continue;
		err = write_eraseblock2(i);
		if (err)
			goto out;
	}

	rnd_state = seed;
	for (i = 0; i < ebcnt; ++i) {
		if (bbt[i])
			continue;
		err = verify_eraseblock2(i);
		if (err)
			goto out;
	}

	if (remove_test_data())
		goto out;

	status = EXIT_SUCCESS;
out:
	if (flags & KEEP_CONTENTS) {
		puts("restoring original contents");
		backupptr = backup;
		for (i = 0; i < ebcnt; ++i) {
			if (bbt[i])
				continue;
			eb = peb + i*(skip+1);
			if (status != EXIT_SUCCESS && mtd_erase(mtd_desc, &mtd, fd, eb))
				fprintf(stderr, "error erasing block %d!\n", eb);

			err = mtd_write(mtd_desc, &mtd, fd, eb, 0,
							backupptr, mtd.eb_size, NULL, 0, 0);
			if (err) {
				fprintf(stderr, "error restoring block %d!\n", eb);
				status = EXIT_FAILURE;
			}
			backupptr += mtd.eb_size;
		}
	}
out_cleanup:
	free(bbt);
	free(readbuf);
	free(writebuf);
	free(backup);
	close(fd);
	return status;
}