#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <time.h>
#include <stdlib.h>
#include <math.h>
#include <stdbool.h>
#include <limits.h>
#include <stdarg.h>
#include <string.h>

int A = (float)(7 * 6 * 8);

// --------------------------
// Functions
// --------------------------
bool debug = false;
// Function for get out debug information if debug mode enabled
void debugPrint(const char *format, ...) {
	if (debug) {
		va_list args;
		va_start(args, format);
		vprintf(format, args);
		va_end(args);
	}
}

// Random float from min to max
float randFloat(int min, int max, unsigned int *seed) {
    float scale = rand_r(seed) / (float) RAND_MAX;
    return min + scale * (max - min);
}

void printNum(double *num) {
	if (*num > 10000) {
		debugPrint("%.3e ", *num);
		// 0.000
	}
	else {
		debugPrint("%.3f ", *num);
		// 0.000e+0
	}
}

void outM(double *M1_ptr, int M1_N, double *M2_ptr, int M2_N) {
	debugPrint("M1:\n");
	for (int i = 0; i < M1_N; i++) {
		printNum(&M1_ptr[i]);
	}
	debugPrint("\nM2:\n");
	for (int i = 0; i < M2_N; i++) {
		printNum(&M2_ptr[i]);
	}
}
// ---------- END------------

// --------------------------
// STEP 1 - Generation
// --------------------------
unsigned int seed;
void gen_M1(double *M_ptr, int N) {
	float min = 1.0f;
	float max = A;
	for (int i = 0; i < N; i++) {
		M_ptr[i] = randFloat(min, max, &seed);
	}	
}
void gen_M2(double *M_ptr, int N) {
	float min = A;
	float max = A*10;
	for (int i = 0; i < N; i++) {
		M_ptr[i] = randFloat(min, max, &seed);
	}
}
// ---------- END------------


// --------------------------
// STEP 2 - Map
// --------------------------
void applyMapM1(double *M_ptr, int N) {
	for (int i = 0; i < N; i++) {
		M_ptr[i] = cosh(M_ptr[i]);
	}
}

void applyMapM2(double *M_ptr, int N) {
	bool first = true;
	double M[N];
	for (int i = 0; i < N; i++) {
		if (first) {
			M[i] = fabs(sin(M_ptr[i]));
		}
		else {
			M[i] = fabs(sin(M_ptr[i] + M[i-1]));
		}
	}
	// Write changes
	for (int i = 0; i < N; i++) {
		M_ptr[i] = M[i];
	}
}
// ---------- END------------


// --------------------------
// STEP 3 - Merge
// --------------------------
void applyMerge(double *M1_ptr, double *M2_ptr, int M2_N) {
	for (int i = 0; i < M2_N; i++) {
		M2_ptr[i] = M1_ptr[i]*M2_ptr[i];
	}
}
// ---------- END------------


// --------------------------
// STEP 4 - Sort
// --------------------------
void selectionSort(double *M_ptr, int N) {
	double max = 0;
	int max_it = 0;
	int it = N;
	for (int n = 0; n < N; n++) {
		max = NAN;
		for (int i = 0; i < it; i++) {
			if (isnan(max) || M_ptr[i] > max) {
				max = M_ptr[i];
				max_it = i;
			}
		}
		M_ptr[max_it] = M_ptr[it - 1];
		M_ptr[it - 1] = max;
		it--;
	}
}
// ---------- END------------


// --------------------------
// STEP 5 - Reduce
// --------------------------
void applyReduce(double *M_ptr, int N) {
	float sum = 0;
	int min = INT_MIN;
	// Find min non-zero
	for (int i = 0; i < N; i++) {
		if (min == INT_MIN || trunc(M_ptr[i]) < min) {
			if (trunc(M_ptr[i]) != 0) {
				min = (int)trunc(M_ptr[i]);
			}
		}
	}
	// Sum of all sin(i) where i%min == 0
	for (int i = 0; i < N; i++) {
		if ((int)trunc(M_ptr[i]) % min == 0) {
			sum += sin( (int)trunc(M_ptr[i]) );
			debugPrint("sum: %f\n", sum);
		}
	}
}
// ---------- END------------


int main(int argc, char* argv[]) {
	if (argc < 2) {
		debugPrint("Usage: %s <N> [debug]\n", argv[0]);
		exit(1);
	}
	if (argc >= 3 && strcmp(argv[2], "debug") == 0) {
		debug = true;
	}
	// Number of numbers
	int N;
	N = atoi(argv[1]);

	// Set custom seed, because need to check different progs
	//seed = time(0);
	seed = 10;

	// First array
	double* M1_ptr;
	M1_ptr = (double*)malloc(N * sizeof(double));
	int M1_N = N;
	// Second array
	double* M2_ptr;
	M2_ptr = (double*)malloc(N/2 * sizeof(double));
	int M2_N = N/2;

	// Remember start time
	struct timeval T1, T2;
	long delta_ms;
	gettimeofday(&T1, NULL);

	// STEP 1 - generate
	gen_M1(M1_ptr, M1_N);
	gen_M2(M2_ptr, M2_N);

	debugPrint("---------- Generated arrays ----------\n");
	outM(M1_ptr, M1_N, M2_ptr, M2_N);
	debugPrint("\n---------------- END -----------------\n");

	// STEP 2 - map
	applyMapM1(M1_ptr, M1_N);
	applyMapM2(M2_ptr, M2_N);

	debugPrint("\n--------------- STEP 2 ---------------\n");
	outM(M1_ptr, M1_N, M2_ptr, M2_N);
	debugPrint("\n---------------- END -----------------\n");

	// STEP 3 - merge
	applyMerge(M1_ptr, M2_ptr, M2_N);

	debugPrint("\n--------------- STEP 3 ---------------\n");
	outM(M1_ptr, M1_N, M2_ptr, M2_N);
	debugPrint("\n---------------- END -----------------\n");

	// STEP 4 - sort
	selectionSort(M1_ptr, M1_N);
	selectionSort(M2_ptr, M2_N);
	
	debugPrint("\n--------------- STEP 4 ---------------\n");
	outM(M1_ptr, M1_N, M2_ptr, M2_N);
	debugPrint("\n---------------- END -----------------\n");

	// STEP 5 - reduce
	debugPrint("\n--------------- STEP 5 ---------------\n");
	applyReduce(M2_ptr, M2_N);
	debugPrint("\n---------------- END -----------------\n");

	// Remember time of end and get out it
	gettimeofday(&T2, NULL);
	delta_ms = (T2.tv_sec - T1.tv_sec) * 1000 + (T2.tv_usec - T1.tv_usec) / 1000;
	printf("\nN=%d. Milliseconds passed: %ld\n", N, delta_ms);

	free(M1_ptr);
	free(M2_ptr);

	return 0;
}

