#include <GL/glew.h>
#include <GLFW/glfw3.h>

#include <cuda_runtime.h>
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <opencv2/imgcodecs.hpp>
#include <opencv2/imgproc.hpp>

#include "utils.h"
#include "main.h"

#define MAX_RECENT_FPS_DATA_POINTS 5

uchar4*
load_image_opencv(const char* filename, int* iw, int* ih)
{
	cv::Mat img = cv::imread(filename, cv::IMREAD_UNCHANGED);
	if (img.empty())
	{
		fprintf(stderr, "Error: could not load '%s'\n", filename);
		return NULL;
	}

	/* convert bgra to rgba like a normal person */
	if (img.channels() == 3)
	{
		cv::cvtColor(img, img, cv::COLOR_BGR2RGBA);
	}
	else if (img.channels() == 4)
	{
		cv::cvtColor(img, img, cv::COLOR_BGRA2RGBA);
	}

	*iw = img.cols;
	*ih = img.rows;

	uchar4* out = (uchar4*)malloc(img.cols * img.rows * sizeof(uchar4));
	memcpy(out, img.data, img.cols * img.rows * sizeof(uchar4));
	return out;
}

void
draw_bin_grid(int window_width, int window_height, int bin_dim_x, int bin_dim_y)
{
	float bin_w = (float)window_width / bin_dim_x;
	float bin_h = (float)window_height / bin_dim_y;

	glDisable(GL_TEXTURE_2D);
	/* green lines */
	glColor3f(0.0f, 1.0f, 0.0f);
	glBegin(GL_LINES);

	/* vertical lines */
	for (int x = 0; x <= bin_dim_x; x++)
	{
		float px = x * bin_w;
		glVertex2f(px, 0.0f);
		glVertex2f(px, (float)window_height);
	}

	/* horizontal lines */
	for (int y = 0; y <= bin_dim_y; y++)
	{
		float py = y * bin_h;
		glVertex2f(0.0f, py);
		glVertex2f((float)window_width, py);
	}

	glEnd();
	glEnable(GL_TEXTURE_2D);
}

typedef struct
{
	float recent[MAX_RECENT_FPS_DATA_POINTS];
	int idx;
	int filled;
	float total;
	float data_points;
} FPSTracker;

static FPSTracker fps_tracker = {0};

void
fps_tracker_update(float current_fps, int active_count)
{
	fps_tracker.data_points++;
	fps_tracker.total += current_fps;

	fps_tracker.recent[fps_tracker.idx] = current_fps;
	fps_tracker.idx = (fps_tracker.idx + 1) % MAX_RECENT_FPS_DATA_POINTS;

	if (fps_tracker.filled < MAX_RECENT_FPS_DATA_POINTS)
	{
		fps_tracker.filled++;
	}

	/* ignore the first data points because the data will massively skewed
	 * with the initial program startup, e.g., > 1000 fps for a few micro
	 * seconds, because there are not enough projectiles, etc.*/
	if (fps_tracker.data_points <= 2)
	{
		printf("Warming up... (%d/2)\n", (int)fps_tracker.data_points);
		return;
	}

	float rolling_sum = 0.0f;
	for (int r = 0; r < fps_tracker.filled; r++)
	{
		rolling_sum += fps_tracker.recent[r];
	}
	float rolling_avg = rolling_sum / fps_tracker.filled;

	printf("FPS: %.2f. Active: %d. Max: %d. "
	       "Avg FPS: %.2f. Avg Recent(%d) FPS: %.2f\n",
	       current_fps, active_count, MAX_PROJS,
	       fps_tracker.total / fps_tracker.data_points,
	       MAX_RECENT_FPS_DATA_POINTS, rolling_avg);
}

typedef struct
{
	float total_ms;
	int count;
} KernelTimer;

static KernelTimer kernel_timer = {0};

void
kernel_timer_record(cudaEvent_t start, cudaEvent_t stop)
{
	float ms = 0.0f;
	cudaEventElapsedTime(&ms, start, stop);
	kernel_timer.total_ms += ms;
	kernel_timer.count++;
}

void
kernel_timer_print()
{
	if (kernel_timer.count == 0)
	{
		return;
	}
	printf("Avg GPU Kernel time: %.4f ms\n",
	       kernel_timer.total_ms / kernel_timer.count);
}

void
print_kernel_occupancy(const char* name, const void* kernel, size_t shared_mem,
                       int block_size)
{
	int active_blocks;
	cudaDeviceProp prop;
	cudaGetDeviceProperties(&prop, 0);
	cudaOccupancyMaxActiveBlocksPerMultiprocessor(&active_blocks, kernel,
	                                              block_size, shared_mem);
	float occ =
		(float)(active_blocks * block_size) / prop.maxThreadsPerMultiProcessor;
	printf("%s: block=%d shared=%zu active_blocks/SM=%d occupancy=%.1f%%\n",
	       name, block_size, shared_mem, active_blocks, occ * 100.0f);
}