#include <dv-sdk/module.hpp>

#include <SDL2/SDL.h>

class Visualizer : public dv::ModuleBase {
private:
	SDL_Window *window{nullptr};
	SDL_Renderer *renderer{nullptr};
	SDL_Texture *texture{nullptr};
	SDL_Event event{};
	int displayWidth{0};
	int displayHeight{0};

	static int getColorHex(const std::string &hexColor) {
		auto c = std::stoi(hexColor, nullptr, 16);
		if ((c < 0) || (c > 255)) {
			c = 0;
		}
		return c;
	}

public:
	static void initInputs(dv::InputDefinitionList &in) {
		in.addFrameInput("frames");
	}

	static const char *initDescription() {
		return ("Simple, lightweight frame visualizer using SDL2.");
	}

	static void initConfigOptions(dv::RuntimeConfig &config) {
		config.add("fullscreen", dv::ConfigOption::boolOption("Enable fullscreen mode.", false));

		config.add("windowPositionX",
			dv::ConfigOption::floatOption("Window position X-axis coordinate in % of screen width.", 0, 0, 100));
		config.add("windowPositionY",
			dv::ConfigOption::floatOption("Window position Y-axis coordinate in % of screen height.", 0, 0, 100));
		config.add("windowWidth", dv::ConfigOption::floatOption("Window width in % of screen width.", 20, 2, 100));
		config.add("windowHeight", dv::ConfigOption::floatOption("Window height in % of screen height.", 20, 2, 100));

		config.add("backgroundColor",
			dv::ConfigOption::stringOption("Background color in hex format #RRGGBB.", "FFFFFF", 6, 6));
	}

	Visualizer() {
		if (SDL_InitSubSystem(SDL_INIT_VIDEO) < 0) {
			throw std::runtime_error("Failed to initialize SDL video subsystem.");
		}

		window = SDL_CreateWindow(moduleNode.getName().c_str(), SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 640,
			480, SDL_WINDOW_BORDERLESS);
		if (window == nullptr) {
			SDL_QuitSubSystem(SDL_INIT_VIDEO);
			throw std::runtime_error("Failed to create window.");
		}

		renderer = SDL_CreateRenderer(window, -1, 0); // Priority to hardware accelerated renderers.
		if (renderer == nullptr) {
			SDL_DestroyWindow(window);

			SDL_QuitSubSystem(SDL_INIT_VIDEO);
			throw std::runtime_error("Failed to create renderer.");
		}

		texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_BGR24, SDL_TEXTUREACCESS_STREAMING,
			inputs.getFrameInput("frames").sizeX(), inputs.getFrameInput("frames").sizeY());
		if (texture == nullptr) {
			SDL_DestroyRenderer(renderer);
			SDL_DestroyWindow(window);

			SDL_QuitSubSystem(SDL_INIT_VIDEO);
			throw std::runtime_error("Failed to create texture.");
		}

		SDL_SetTextureBlendMode(texture, SDL_BLENDMODE_NONE);

		log.debug.format("Initialized SDL visualizer with video driver: {}", SDL_GetCurrentVideoDriver());
	}

	~Visualizer() override {
		SDL_DestroyTexture(texture);
		SDL_DestroyRenderer(renderer);
		SDL_DestroyWindow(window);

		SDL_QuitSubSystem(SDL_INIT_VIDEO);
	}

	void run() override {
		while (SDL_PollEvent(&event)) {
			if (event.type == SDL_QUIT) {
				// User wants to quit window, let's stop the module.
				throw std::runtime_error{"Requested window closing."};
			}

			// Ignore other events for now.
		}

		const auto frame = inputs.getFrameInput("frames").frame();
		if (!frame) {
			return;
		}

		// Ensure cv::Mat is BGR24 format.
		cv::Mat toCopy;

		switch (frame.getMatPointer()->channels()) {
			case 1:
				// Grayscale to BGR.
				cv::cvtColor(*frame.getMatPointer(), toCopy, cv::COLOR_GRAY2BGR);
				break;

			case 3:
				// Already BGR.
				toCopy = *frame.getMatPointer();
				break;

			case 4:
				// BGRA to BGR.
				cv::cvtColor(*frame.getMatPointer(), toCopy, cv::COLOR_BGRA2BGR);
				break;

			default:
				throw std::runtime_error{"Invalid color channel number."};
		}

		// cv::Mat to texture copy.
		unsigned char *textureData{nullptr};
		int texturePitch{0};

		SDL_LockTexture(texture, nullptr, reinterpret_cast<void **>(&textureData), &texturePitch);
		// Go row-by-row to handle texture pitch alignment properly.
		for (int r = 0; r < toCopy.rows; r++) {
			memcpy(textureData + (r * texturePitch), toCopy.ptr(r), toCopy.cols * 3); // BGR24 format.
		}
		SDL_UnlockTexture(texture);

		SDL_RenderClear(renderer);

		SDL_Rect source{.x = 0, .y = 0, .w = toCopy.cols, .h = toCopy.rows};
		SDL_Rect destination{};

		// We try to maximize size while preserving aspect ratio, so we first check
		// if maximizing in the horizontal direction will result in too big vertical.
		const auto frameWidth            = static_cast<float>(toCopy.cols);
		const auto frameHeight           = static_cast<float>(toCopy.rows);
		const auto horizontalGrowthRatio = static_cast<float>(displayWidth) / frameWidth;

		if (static_cast<int>(frameHeight * horizontalGrowthRatio) <= displayHeight) {
			// Maximizing horizontal growth results in a vertical size that still fits!
			destination.w = displayWidth;
			destination.h = static_cast<int>(frameHeight * horizontalGrowthRatio);
			destination.x = 0;
			destination.y = (displayHeight - destination.h) / 2;
		}
		else {
			// Vertical size too great, so we need to go the other way and apply
			// maximal vertical growth, with horizontal being the smaller axis.
			const auto verticalGrowthRatio = static_cast<float>(displayHeight) / frameHeight;

			destination.w = static_cast<int>(frameWidth * verticalGrowthRatio);
			destination.h = displayHeight;
			destination.x = (displayWidth - destination.w) / 2;
			destination.y = 0;
		}

		SDL_RenderCopy(renderer, texture, &source, &destination);

		SDL_RenderPresent(renderer);
	}

	void configUpdate() override {
		// Update background color.
		SDL_SetRenderDrawColor(renderer, getColorHex(config.getString("backgroundColor").substr(0, 2)),
			getColorHex(config.getString("backgroundColor").substr(2, 2)),
			getColorHex(config.getString("backgroundColor").substr(4, 2)), SDL_ALPHA_OPAQUE);

		if (config.getBool("fullscreen")) {
			// Fullscreen mode.
			SDL_SetWindowFullscreen(window, SDL_WINDOW_FULLSCREEN);

			SDL_DisplayMode mode{};
			SDL_GetCurrentDisplayMode(0, &mode);
			displayWidth  = mode.w;
			displayHeight = mode.h;
		}
		else {
			// Windowed mode.
			SDL_SetWindowFullscreen(window, 0);

			SDL_DisplayMode mode{};
			SDL_GetCurrentDisplayMode(0, &mode);
			const auto screenWidth  = mode.w;
			const auto screenHeight = mode.h;

			const auto windowPositionX
				= static_cast<int>(static_cast<float>(screenWidth) * config.getFloat("windowPositionX") / 100.0f);
			const auto windowPositionY
				= static_cast<int>(static_cast<float>(screenHeight) * config.getFloat("windowPositionY") / 100.0f);

			const auto windowWidth
				= static_cast<int>(static_cast<float>(screenWidth) * config.getFloat("windowWidth") / 100.0f);
			const auto windowHeight
				= static_cast<int>(static_cast<float>(screenHeight) * config.getFloat("windowHeight") / 100.0f);

			SDL_SetWindowPosition(window, windowPositionX, windowPositionY);
			SDL_SetWindowSize(window, windowWidth, windowHeight);

			displayWidth  = windowWidth;
			displayHeight = windowHeight;
		}
	}
};

registerModuleClass(Visualizer)
