/*
 * Copyright (C) 2007 Nokia Corporation
 * Copyright (C) 2015 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 read and write speed of a MTD device.
 *
 * Author: David Oberhollenzer <david.oberhollenzer@sigma-star.at>
 *
 * Based on linux flash_speed.c
 * Author: Adrian Hunter <adrian.hunter@nokia.com>
 */
#define DESTRUCTIVE 0x01

#define PROGRAM_NAME "flash_speed"

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

#include "common.h"

static struct mtd_dev_info mtd;
static unsigned char *iobuf;
static unsigned char *bbt;
static const char *mtddev;
static libmtd_t mtd_desc;
static int fd;

static int peb=-1, count=-1, skip=-1, flags=0;
static struct timespec start, finish;
static int pgsize, pgcnt;
static int goodebcnt;

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

static NORETURN void usage(int status)
{
	fputs(
	"Usage: "PROGRAM_NAME" [OPTIONS] <device>\n\n"
	"Common options:\n"
	"  -h, --help          Display this help output\n"
	"  -b, --peb <num>     Start from this physical erase block\n"
	"  -c, --count <num>   Number of erase blocks to use (default: all)\n"
	"  -s, --skip <num>    Number of blocks to skip\n"
	"  -d, --destructive   Run destructive (erase and write speed) tests\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:d", options, NULL);
		if (c == -1)
			break;

		switch (c) {
		case 'h':
			usage(EXIT_SUCCESS);
		case 'b':
			if (peb >= 0)
				goto failmulti;
			peb = read_num(c, optarg);
			if (peb < 0)
				goto failarg;
			break;
		case 'c':
			if (count > 0)
				goto failmulti;
			count = read_num(c, optarg);
			if (count <= 0)
				goto failarg;
			break;
		case 's':
			if (skip >= 0)
				goto failmulti;
			skip = read_num(c, optarg);
			if (skip < 0)
				goto failarg;
			break;
		case 'd':
			if (flags & DESTRUCTIVE)
				goto failmulti;
			flags |= DESTRUCTIVE;
			break;
		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 (skip < 0)
		skip = 0;
	if (count < 0)
		count = 1;
	return;
failmulti:
	errmsg_die("'-%c' specified more than once!\n", c);
failarg:
	errmsg_die("Invalid argument for '-%c'!\n", c);
}

static int write_eraseblock(int ebnum)
{
	int err = mtd_write(mtd_desc, &mtd, fd, ebnum, 0,
				iobuf, mtd.eb_size, NULL, 0, 0);
	if (err)
		fprintf(stderr, "Error writing block %d!\n", ebnum);
	return err;
}

static int read_eraseblock(int ebnum)
{
	int err = mtd_read(&mtd, fd, ebnum, 0, iobuf, mtd.eb_size);
	if (err)
		fprintf(stderr, "Error writing block %d!\n", ebnum);
	return err;
}

static int write_eraseblock_by_page(int ebnum)
{
	void *buf = iobuf;
	int i, err = 0;

	for (i = 0; i < pgcnt; ++i) {
		err = mtd_write(mtd_desc, &mtd, fd, ebnum, i * pgsize,
						buf, pgsize, NULL, 0, 0);
		if (err) {
			fprintf(stderr, "Error writing block %d, page %d!\n",
					ebnum, i);
			break;
		}
		buf += pgsize;
	}

	return err;
}

static int write_eraseblock_by_2pages(int ebnum)
{
	int i, n = pgcnt / 2, err = 0;
	size_t sz = pgsize * 2;
	void *buf = iobuf;

	for (i = 0; i < n; ++i) {
		err = mtd_write(mtd_desc, &mtd, fd, ebnum, i * sz,
						buf, sz, NULL, 0, 0);
		if (err) {
			fprintf(stderr, "Error writing block %d, page %d + %d!\n",
					ebnum, i*2, i*2+1);
			return err;
		}
		buf += sz;
	}
	if (pgcnt % 2) {
		err = mtd_write(mtd_desc, &mtd, fd, ebnum, i * sz,
						buf, pgsize, NULL, 0, 0);
		if (err) {
			fprintf(stderr, "Error reading block %d, page %d!\n",
					ebnum, i*2);
		}
	}
	return err;
}

static int read_eraseblock_by_page(int ebnum)
{
	void *buf = iobuf;
	int i, err = 0;

	for (i = 0; i < pgcnt; ++i) {
		err = mtd_read(&mtd, fd, ebnum, i * pgsize, iobuf, pgsize);
		if (err) {
			fprintf(stderr, "Error reading block %d, page %d!\n",
					ebnum, i);
			break;
		}
		buf += pgsize;
	}

	return err;
}

static int read_eraseblock_by_2pages(int ebnum)
{
	int i, n = pgcnt / 2, err = 0;
	size_t sz = pgsize * 2;
	void *buf = iobuf;

	for (i = 0; i < n; ++i) {
		err = mtd_read(&mtd, fd, ebnum, i * sz, iobuf, sz);
		if (err) {
			fprintf(stderr, "Error reading block %d, page %d + %d!\n",
					ebnum, i*2, i*2+1);
			return err;
		}
		buf += sz;
	}
	if (pgcnt % 2) {
		err = mtd_read(&mtd, fd, ebnum, i * sz, iobuf, pgsize);
		if (err) {
			fprintf(stderr, "Error reading block %d, page %d!\n",
					ebnum, i*2);
		}
	}

	return err;
}

static void start_timing(void)
{
	clock_gettime(CLOCK_MONOTONIC_RAW, &start);
}

static void stop_timing(void)
{
	clock_gettime(CLOCK_MONOTONIC_RAW, &finish);
}

static long calc_speed(void)
{
	long ms;

	ms = (finish.tv_sec - start.tv_sec) * 1000L;
	ms += (finish.tv_nsec - start.tv_nsec) / 1000000L;

	if (ms <= 0)
		return 0;

	return ((long)goodebcnt * (mtd.eb_size / 1024L) * 1000L) / ms;
}

static void scan_for_bad_eraseblocks(unsigned int eb, int ebcnt, int ebskip)
{
	int i, bad = 0;

	puts("scanning for bad eraseblocks");

	for (i = 0; i < ebcnt; ++i) {
		bbt[i] = mtd_is_bad(&mtd, fd, eb + i*(ebskip+1)) ? 1 : 0;
		if (bbt[i])
			bad += 1;
	}

	printf("scanned %d eraseblocks, %d are bad\n", ebcnt, bad);
}

static int erase_good_eraseblocks(unsigned int eb, int ebcnt, int ebskip)
{
	int err = 0, block;
	unsigned int i;

	for (i = 0; i < ebcnt; ++i) {
		if (bbt[i])
			continue;
		block = eb + i*(ebskip+1);
		err = mtd_erase(mtd_desc, &mtd, fd, block);
		if (err)
			fprintf(stderr, "Error erasing block %d!\n", block);
	}

	return err;
}

#define TIME_OP_PER_PEB( op )\
		start_timing();\
		for (i = 0; i < count; ++i) {\
			if (bbt[i])\
				continue;\
			err = op(peb + i*(skip+1));\
			if (err)\
				goto out;\
		}\
		stop_timing();\
		speed = calc_speed()

int main(int argc, char **argv)
{
	int err, i, blocks, j, k, status = EXIT_FAILURE;
	long speed;

	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.subpage_size == 1) {
		puts("not NAND flash, assume page size is 512 bytes.");
		pgsize = 512;
	} else {
		pgsize = mtd.subpage_size;
	}

	pgcnt = mtd.eb_size / pgsize;

	if (count < 0)
		count = mtd.eb_size;

	if (peb >= mtd.eb_cnt)
		return errmsg("Physical erase block %d is out of range!\n", peb);

	if ((peb + (count - 1)*(skip + 1)) >= mtd.eb_cnt) {
		return errmsg("Given block range exceeds block count of %d!\n",
					mtd.eb_cnt);
	}

	iobuf = xmalloc(mtd.eb_size);
	bbt = xzalloc(count);

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

	for (i = 0; i < mtd.eb_size; ++i)
		iobuf[i] = rand();

	scan_for_bad_eraseblocks(peb, count, skip);

	for (i = 0; i < count; ++i) {
		if (!bbt[i])
			goodebcnt++;
	}

	/* Write all eraseblocks, 1 eraseblock at a time */
	if (flags & DESTRUCTIVE) {
		err = erase_good_eraseblocks(peb, count, skip);
		if (err)
			goto out;

		puts("testing eraseblock write speed");
		TIME_OP_PER_PEB(write_eraseblock);
		printf("eraseblock write speed is %ld KiB/s\n", speed);
	}

	/* Read all eraseblocks, 1 eraseblock at a time */
	puts("testing eraseblock read speed");
	TIME_OP_PER_PEB(read_eraseblock);
	printf("eraseblock read speed is %ld KiB/s\n", speed);

	/* Write all eraseblocks, 1 page at a time */
	if (flags & DESTRUCTIVE) {
		err = erase_good_eraseblocks(peb, count, skip);
		if (err)
			goto out;

		puts("testing page write speed");
		TIME_OP_PER_PEB(write_eraseblock_by_page);
		printf("page write speed is %ld KiB/s\n", speed);
	}

	/* Read all eraseblocks, 1 page at a time */
	puts("testing page read speed");
	TIME_OP_PER_PEB(read_eraseblock_by_page);
	printf("page read speed is %ld KiB/s\n", speed);

	/* Write all eraseblocks, 2 pages at a time */
	if (flags & DESTRUCTIVE) {
		err = erase_good_eraseblocks(peb, count, skip);
		if (err)
			goto out;

		puts("testing 2 page write speed");
		TIME_OP_PER_PEB(write_eraseblock_by_2pages);
		printf("2 page write speed is %ld KiB/s\n", speed);
	}

	/* Read all eraseblocks, 2 pages at a time */
	puts("testing 2 page read speed");
	TIME_OP_PER_PEB(read_eraseblock_by_2pages);
	printf("2 page read speed is %ld KiB/s\n", speed);

	/* Erase all eraseblocks */
	if (flags & DESTRUCTIVE) {
		puts("Testing erase speed");
		start_timing();
		err = erase_good_eraseblocks(peb, count, skip);
		if (err)
			goto out;
		stop_timing();
		speed = calc_speed();
		printf("erase speed is %ld KiB/s\n", speed);
	}

	/* Multi-block erase all eraseblocks */
	if (!skip) {
		for (k = 1; k < 7; ++k) {
			blocks = 1 << k;
			printf("Testing %dx multi-block erase speed\n", blocks);
			start_timing();
			for (i = 0; i < count; ) {
				for (j = 0; j < blocks && (i + j) < count; ++j)
					if (bbt[i + j])
						break;
				if (j < 1) {
					++i;
					continue;
				}
				err = mtd_erase_multi(mtd_desc, &mtd, fd, i, j);
				if (err)
					goto out;
				i += j;
			}
			stop_timing();
			speed = calc_speed();
			printf("%dx multi-block erase speed is %ld KiB/s\n",
					blocks, speed);
		}
	}

	puts("finished");
	status = EXIT_SUCCESS;
out:
	close(fd);
outfree:
	free(iobuf);
	free(bbt);
	return status;
}