/*
 * Copyright (C) 2013 Andrea Mazzoleni
 *
 * 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.
 */

#include "internal.h"
#include "cpu.h"
#include "combo.h"
#include "memory.h"

/**
 * Binomial coefficient of n over r.
 */
static int ibc(int n, int r)
{
	if (r == 0 || n == r)
		return 1;
	else
		return ibc(n - 1, r - 1) + ibc(n - 1, r);
}

/**
 * Power n ^ r;
 */
static int ipow(int n, int r)
{
	int v = 1;

	while (r) {
		v *= n;
		--r;
	}
	return v;
}

int raid_test_combo(void)
{
	int r;
	int count;
	int p[RAID_PARITY_MAX];

	for (r = 1; r <= RAID_PARITY_MAX; ++r) {
		/* count combination (r of RAID_PARITY_MAX) elements */
		count = 0;
		combination_first(r, RAID_PARITY_MAX, p);

		do {
			++count;
		} while (combination_next(r, RAID_PARITY_MAX, p));

		if (count != ibc(RAID_PARITY_MAX, r)) {
			/* LCOV_EXCL_START */
			return -1;
			/* LCOV_EXCL_STOP */
		}
	}

	for (r = 1; r <= RAID_PARITY_MAX; ++r) {
		/* count permutation (r of RAID_PARITY_MAX) elements */
		count = 0;
		permutation_first(r, RAID_PARITY_MAX, p);

		do {
			++count;
		} while (permutation_next(r, RAID_PARITY_MAX, p));

		if (count != ipow(RAID_PARITY_MAX, r)) {
			/* LCOV_EXCL_START */
			return -1;
			/* LCOV_EXCL_STOP */
		}
	}

	return 0;
}

int raid_test_insert(void)
{
	int p[RAID_PARITY_MAX];
	int r;

	for (r = 1; r <= RAID_PARITY_MAX; ++r) {
		permutation_first(r, RAID_PARITY_MAX, p);
		do {
			int i[RAID_PARITY_MAX];
			int j;

			/* insert in order */
			for (j = 0; j < r; ++j)
				raid_insert(j, i, p[j]);

			/* check order */
			for (j = 1; j < r; ++j) {
				if (i[j - 1] > i[j]) {
					/* LCOV_EXCL_START */
					return -1;
					/* LCOV_EXCL_STOP */
				}
			}
		} while (permutation_next(r, RAID_PARITY_MAX, p));
	}

	return 0;
}

int raid_test_sort(void)
{
	int p[RAID_PARITY_MAX];
	int r;

	for (r = 1; r <= RAID_PARITY_MAX; ++r) {
		permutation_first(r, RAID_PARITY_MAX, p);
		do {
			int i[RAID_PARITY_MAX];
			int j;

			/* make a copy */
			for (j = 0; j < r; ++j)
				i[j] = p[j];

			raid_sort(r, i);

			/* check order */
			for (j = 1; j < r; ++j) {
				if (i[j - 1] > i[j]) {
					/* LCOV_EXCL_START */
					return -1;
					/* LCOV_EXCL_STOP */
				}
			}
		} while (permutation_next(r, RAID_PARITY_MAX, p));
	}

	return 0;
}

int raid_test_rec(int mode, int nd, size_t size)
{
	void (*f[RAID_PARITY_MAX][4])(
		int nr, int *id, int *ip, int nd, size_t size, void **vbuf);
	void *v_alloc;
	void **v;
	void **data;
	void **parity;
	void **test;
	void *data_save[RAID_PARITY_MAX];
	void *parity_save[RAID_PARITY_MAX];
	void *waste;
	int nv;
	int id[RAID_PARITY_MAX];
	int ip[RAID_PARITY_MAX];
	int i;
	int j;
	int nr;
	int nf[RAID_PARITY_MAX];
	int np;

	raid_mode(mode);
	if (mode == RAID_MODE_CAUCHY)
		np = RAID_PARITY_MAX;
	else
		np = 3;

	nv = nd + np * 2 + 2;

	v = raid_malloc_vector(nd, nv, size, &v_alloc);
	if (!v) {
		/* LCOV_EXCL_START */
		return -1;
		/* LCOV_EXCL_STOP */
	}

	data = v;
	parity = v + nd;
	test = v + nd + np;

	for (i = 0; i < np; ++i)
		parity_save[i] = parity[i];

	memset(v[nv - 2], 0, size);
	raid_zero(v[nv - 2]);

	waste = v[nv - 1];

	/* fill with pseudo-random data with the arbitrary seed "1" */
	raid_mrand_vector(1, nd, size, v);

	/* setup recov functions */
	for (i = 0; i < np; ++i) {
		nf[i] = 0;
		if (i == 0) {
			f[i][nf[i]++] = raid_rec1_int8;
#ifdef CONFIG_X86
#ifdef CONFIG_SSSE3
			if (raid_cpu_has_ssse3())
				f[i][nf[i]++] = raid_rec1_ssse3;
#endif
#ifdef CONFIG_AVX2
			if (raid_cpu_has_avx2())
				f[i][nf[i]++] = raid_rec1_avx2;
#endif
#endif
		} else if (i == 1) {
			f[i][nf[i]++] = raid_rec2_int8;
#ifdef CONFIG_X86
#ifdef CONFIG_SSSE3
			if (raid_cpu_has_ssse3())
				f[i][nf[i]++] = raid_rec2_ssse3;
#endif
#ifdef CONFIG_AVX2
			if (raid_cpu_has_avx2())
				f[i][nf[i]++] = raid_rec2_avx2;
#endif
#endif
		} else {
			f[i][nf[i]++] = raid_recX_int8;
#ifdef CONFIG_X86
#ifdef CONFIG_SSSE3
			if (raid_cpu_has_ssse3())
				f[i][nf[i]++] = raid_recX_ssse3;
#endif
#ifdef CONFIG_AVX2
			if (raid_cpu_has_avx2())
				f[i][nf[i]++] = raid_recX_avx2;
#endif
#endif
		}
	}

	/* compute the parity */
	raid_gen_ref(nd, np, size, v);

	/* set all the parity to the waste v */
	for (i = 0; i < np; ++i)
		parity[i] = waste;

	/* all parity levels */
	for (nr = 1; nr <= np; ++nr) {
		/* all combinations (nr of nd) disks */
		combination_first(nr, nd, id);
		do {
			/* all combinations (nr of np) parities */
			combination_first(nr, np, ip);
			do {
				/* for each recover function */
				for (j = 0; j < nf[nr - 1]; ++j) {
					/* set */
					for (i = 0; i < nr; ++i) {
						/* remove the missing data */
						data_save[i] = data[id[i]];
						data[id[i]] = test[i];
						/* set the parity to use */
						parity[ip[i]] = parity_save[ip[i]];
					}

					/* recover */
					f[nr - 1][j](nr, id, ip, nd, size, v);

					/* check */
					for (i = 0; i < nr; ++i) {
						if (memcmp(test[i], data_save[i], size) != 0) {
							/* LCOV_EXCL_START */
							goto bail;
							/* LCOV_EXCL_STOP */
						}
					}

					/* restore */
					for (i = 0; i < nr; ++i) {
						/* restore the data */
						data[id[i]] = data_save[i];
						/* restore the parity */
						parity[ip[i]] = waste;
					}
				}
			} while (combination_next(nr, np, ip));
		} while (combination_next(nr, nd, id));
	}

	free(v_alloc);
	free(v);
	return 0;

bail:
	/* LCOV_EXCL_START */
	free(v_alloc);
	free(v);
	return -1;
	/* LCOV_EXCL_STOP */
}

int raid_test_par(int mode, int nd, size_t size)
{
	void (*f[64])(int nd, size_t size, void **vbuf);
	void *v_alloc;
	void **v;
	int nv;
	int i, j;
	int nf;
	int np;

	raid_mode(mode);
	if (mode == RAID_MODE_CAUCHY)
		np = RAID_PARITY_MAX;
	else
		np = 3;

	nv = nd + np * 2;

	v = raid_malloc_vector(nd, nv, size, &v_alloc);
	if (!v) {
		/* LCOV_EXCL_START */
		return -1;
		/* LCOV_EXCL_STOP */
	}

	/* check memory */
	if (raid_mtest_vector(nv, size, v) != 0) {
		/* LCOV_EXCL_START */
		goto bail;
		/* LCOV_EXCL_STOP */
	}

	/* fill with pseudo-random data with the arbitrary seed "2" */
	raid_mrand_vector(2, nv, size, v);

	/* compute the parity */
	raid_gen_ref(nd, np, size, v);

	/* copy in back buffers */
	for (i = 0; i < np; ++i)
		memcpy(v[nd + np + i], v[nd + i], size);

	/* load all the available functions */
	nf = 0;

	f[nf++] = raid_gen1_int32;
	f[nf++] = raid_gen1_int64;
	f[nf++] = raid_gen2_int32;
	f[nf++] = raid_gen2_int64;

#ifdef CONFIG_X86
#ifdef CONFIG_SSE2
	if (raid_cpu_has_sse2()) {
		f[nf++] = raid_gen1_sse2;
		f[nf++] = raid_gen2_sse2;
#ifdef CONFIG_X86_64
		f[nf++] = raid_gen2_sse2ext;
#endif
	}
#endif

#ifdef CONFIG_AVX2
	if (raid_cpu_has_avx2()) {
		f[nf++] = raid_gen1_avx2;
		f[nf++] = raid_gen2_avx2;
	}
#endif
#endif /* CONFIG_X86 */

	if (mode == RAID_MODE_CAUCHY) {
		f[nf++] = raid_gen3_int8;
		f[nf++] = raid_gen4_int8;
		f[nf++] = raid_gen5_int8;
		f[nf++] = raid_gen6_int8;

#ifdef CONFIG_X86
#ifdef CONFIG_SSSE3
		if (raid_cpu_has_ssse3()) {
			f[nf++] = raid_gen3_ssse3;
			f[nf++] = raid_gen4_ssse3;
			f[nf++] = raid_gen5_ssse3;
			f[nf++] = raid_gen6_ssse3;
#ifdef CONFIG_X86_64
			f[nf++] = raid_gen3_ssse3ext;
			f[nf++] = raid_gen4_ssse3ext;
			f[nf++] = raid_gen5_ssse3ext;
			f[nf++] = raid_gen6_ssse3ext;
#endif
		}
#endif

#ifdef CONFIG_AVX2
#ifdef CONFIG_X86_64
		if (raid_cpu_has_avx2()) {
			f[nf++] = raid_gen3_avx2ext;
			f[nf++] = raid_gen4_avx2ext;
			f[nf++] = raid_gen5_avx2ext;
			f[nf++] = raid_gen6_avx2ext;
		}
#endif
#endif
#endif /* CONFIG_X86 */
	} else {
		f[nf++] = raid_genz_int32;
		f[nf++] = raid_genz_int64;

#ifdef CONFIG_X86
#ifdef CONFIG_SSE2
		if (raid_cpu_has_sse2()) {
			f[nf++] = raid_genz_sse2;
#ifdef CONFIG_X86_64
			f[nf++] = raid_genz_sse2ext;
#endif
		}
#endif

#ifdef CONFIG_AVX2
#ifdef CONFIG_X86_64
		if (raid_cpu_has_avx2())
			f[nf++] = raid_genz_avx2ext;
#endif
#endif
#endif /* CONFIG_X86 */
	}

	/* check all the functions */
	for (j = 0; j < nf; ++j) {
		/* compute parity */
		f[j](nd, size, v);

		/* check it */
		for (i = 0; i < np; ++i) {
			if (memcmp(v[nd + np + i], v[nd + i], size) != 0) {
				/* LCOV_EXCL_START */
				goto bail;
				/* LCOV_EXCL_STOP */
			}
		}
	}

	free(v_alloc);
	free(v);
	return 0;

bail:
	/* LCOV_EXCL_START */
	free(v_alloc);
	free(v);
	return -1;
	/* LCOV_EXCL_STOP */
}