/**************************************************************

   display.cpp - Display manager

   ---------------------------------------------------------

   Switchres   Modeline generation engine for emulation

   License     GPL-2.0+
   Copyright   2010-2021 Chris Kennedy, Antonio Giner,
                         Alexandre Wodarczyk, Gil Delescluse

 **************************************************************/

#include <stdio.h>
#include <ctype.h>
#include <string.h>
#include "display.h"
#if defined(_WIN32)
#include "display_windows.h"
#elif defined(__linux__)
#include "display_linux.h"
#endif
#ifdef SR_WITH_SDL2
#include "display_sdl2.h"
#endif

#include "log.h"

//============================================================
//  display_manager::make
//============================================================

display_manager *display_manager::make(display_settings *ds)
{
	display_manager *display = nullptr;

	if (!strcmp(ds->screen, "dummy"))
	{
		display = new dummy_display(ds);
		return display;
	}

#ifdef SR_WITH_SDL2
	try
	{
		display = new sdl2_display(ds);
	}
	catch (...) {};
	if (!display)
	{
#endif

#if defined(_WIN32)
	display = new windows_display(ds);
#elif defined(__linux__)
	display = new linux_display(ds);
#endif

#ifdef SR_WITH_SDL2
	}
#endif

	return display;
}

//============================================================
//  display_manager::parse_options
//============================================================

void display_manager::parse_options()
{
	log_verbose("Switchres: display[%d] options: monitor[%s] generation[%s]\n",
		m_index, m_ds.monitor, m_ds.modeline_generation?"on":"off");

	// Get user_mode as <w>x<h>@<r>
	set_user_mode(&m_ds.user_mode);

	// Get user defined modeline (overrides user_mode)
	modeline user_mode = {};
	if (m_ds.modeline_generation)
	{
		if (modeline_parse(m_ds.user_modeline, &user_mode))
		{
			memset(&range[0], 0, sizeof(struct monitor_range) * MAX_RANGES);
			user_mode.type |= MODE_USER_DEF;
			set_user_mode(&user_mode);
		}
	}

	// Get monitor specs
	if (user_mode.hactive)
	{
		modeline_to_monitor_range(range, &user_mode);
		monitor_show_range(range);
	}
	else
		set_preset(m_ds.monitor);
}

//============================================================
//  display_manager::set_preset
//============================================================

void display_manager::set_preset(const char *preset)
{
	for (size_t i = 0; i < strlen(m_ds.monitor); i++) m_ds.monitor[i] = tolower(m_ds.monitor[i]);

	memset(&range[0], 0, sizeof(struct monitor_range) * MAX_RANGES);

	if (!strcmp(preset, "custom"))
		for (int i = 0; i < MAX_RANGES; i++) monitor_fill_range(&range[i], m_ds.crt_range[i]);

	else if (!strcmp(preset, "lcd"))
		monitor_fill_lcd_range(&range[0], m_ds.lcd_range);

	else if (monitor_set_preset(preset, range) == 0)
		monitor_set_preset("generic_15", range);
}

//============================================================
//  display_manager::init
//============================================================

bool display_manager::init(void* pf_data)
{
	sprintf(m_ds.screen, "ram");
	m_pf_data = pf_data;

	return true;
}

//============================================================
//  display_manager::caps
//============================================================

int display_manager::caps()
{
	if (video() != nullptr)
		return video()->caps();
	else
		return CUSTOM_VIDEO_CAPS_ADD;
}

//============================================================
//  display_manager::add_mode
//============================================================

bool display_manager::add_mode(modeline *mode)
{
	// Add new mode
	if (video() != nullptr && !video()->add_mode(mode))
	{
		log_verbose("Switchres: error adding mode ");
		log_mode(mode);
		return false;
	}

	mode->type &= ~MODE_ADD;

	log_verbose("Switchres: added ");
	log_mode(mode);

	return true;
}

//============================================================
//  display_manager::delete_mode
//============================================================

bool display_manager::delete_mode(modeline *mode)
{
	if (video() != nullptr && !video()->delete_mode(mode))
	{
		log_verbose("Switchres: error deleting mode ");
		log_mode(mode);
		return false;
	}

	log_verbose("Switchres: deleted ");
	log_mode(mode);

	ptrdiff_t i = mode - &video_modes[0];
	video_modes.erase(video_modes.begin() + i);

	return true;
}

//============================================================
//  display_manager::update_mode
//============================================================

bool display_manager::update_mode(modeline *mode)
{
	// Apply new timings
	if (video() != nullptr && !video()->update_mode(mode))
	{
		log_verbose("Switchres: error updating mode ");
		log_mode(mode);
		return false;
	}

	mode->type &= ~MODE_UPDATE;

	log_verbose("Switchres: updated ");
	log_mode(mode);
	return true;
}

//============================================================
//  display_manager::set_mode
//============================================================

bool display_manager::set_mode(modeline *)
{
	return false;
}

//============================================================
//  display_manager::log_mode
//============================================================

void display_manager::log_mode(modeline *mode)
{
	char modeline_txt[256];
	log_verbose("%s timing %s\n", video() != nullptr? video()->api_name() : "dummy", modeline_print(mode, modeline_txt, MS_FULL));
}

//============================================================
//  display_manager::restore_modes
//============================================================

bool display_manager::restore_modes()
{
	// Compare each mode in our table with its original state
	for (unsigned i = video_modes.size(); i-- > 0; )
	{
		// First, delete all modes we've added
		if (i + 1 > backup_modes.size())
			video_modes[i].type |= MODE_DELETE;

		// Now restore all modes which timings have been modified
		else if (modeline_is_different(&video_modes[i], &backup_modes[i]))
		{
			video_modes[i] = backup_modes[i];
			video_modes[i].type |= MODE_UPDATE;
		}
	}
	// Finally, flush pending changes to driver
	return flush_modes();
}

//============================================================
//  display_manager::flush_modes
//============================================================

bool display_manager::flush_modes()
{
	bool error = false;
	std::vector<modeline *> modified_modes = {};

	// Loop through our mode table to collect all pending changes
	for (auto &mode : video_modes)
		if (mode.type & (MODE_UPDATE | MODE_ADD | MODE_DELETE))
			modified_modes.push_back(&mode);

	// Flush pending changes to driver
	if (modified_modes.size() > 0)
	{
		if (video() != nullptr)
			video()->process_modelist(modified_modes);

		// Log error/success result for each mode
		for (auto &mode : modified_modes)
		{
			log_verbose("Switchres: %s %s mode ", mode->type & MODE_ERROR? "error" : "success", mode->type & MODE_DELETE? "deleting" : mode->type & MODE_ADD? "adding" : "updating");
			log_mode(mode);

			if (mode->type & MODE_ERROR)
				error = true;
		}

		// Update our internal mode table to reflect the changes
		for (unsigned i = video_modes.size(); i-- > 0; )
		{
			if (video_modes[i].type & MODE_ERROR)
				continue;

			if (video_modes[i].type & MODE_DELETE)
			{
				video_modes.erase(video_modes.begin() + i);
				m_selected_mode = 0;
			}
			else
				video_modes[i].type &= ~(MODE_UPDATE | MODE_ADD);
		}
	}

	return !error;
}

//============================================================
//  display_manager::filter_modes
//============================================================

bool display_manager::filter_modes()
{
	for (auto &mode : video_modes)
	{
		// apply options to mode type
		if (m_ds.refresh_dont_care)
			mode.type |= V_FREQ_EDITABLE;

		if ((caps() & CUSTOM_VIDEO_CAPS_UPDATE))
			mode.type |= V_FREQ_EDITABLE;

		if (caps() & CUSTOM_VIDEO_CAPS_SCAN_EDITABLE)
			mode.type |= SCAN_EDITABLE;

		if (!m_ds.modeline_generation)
			mode.type &= ~(XYV_EDITABLE | SCAN_EDITABLE);

		if ((mode.type & MODE_DESKTOP) && !(caps() & CUSTOM_VIDEO_CAPS_DESKTOP_EDITABLE))
			mode.type &= ~V_FREQ_EDITABLE;

		if (m_ds.lock_system_modes && (mode.type & CUSTOM_VIDEO_TIMING_SYSTEM))
			mode.type |= MODE_DISABLED;

		// Make sure to unlock the desktop mode as fallback
		if (mode.type & MODE_DESKTOP)
			mode.type &= ~MODE_DISABLED;

		// Lock all modes that don't match the user's -resolution rules
		if (m_user_mode.width != 0 || m_user_mode.height != 0 || m_user_mode.refresh == !0)
		{
			if (!( (mode.width == m_user_mode.width || (mode.type & X_RES_EDITABLE) || m_user_mode.width == 0)
				&& (mode.height == m_user_mode.height || (mode.type & Y_RES_EDITABLE) || m_user_mode.height == 0)
				&& (mode.refresh == m_user_mode.refresh || (mode.type & V_FREQ_EDITABLE) || m_user_mode.refresh == 0) ))
				mode.type |= MODE_DISABLED;
			else
				mode.type &= ~MODE_DISABLED;
		}
	}

	return true;
}

//============================================================
//  display_manager::get_video_mode
//============================================================

modeline *display_manager::get_mode(int width, int height, float refresh, int flags)
{
	modeline s_mode = {};
	modeline t_mode = {};
	modeline best_mode = {};
	char result[256]={'\x00'};

	bool rotated = flags & SR_MODE_ROTATED;
	bool interlaced = flags & SR_MODE_INTERLACED;

	log_info("Switchres: Calculating best video mode for %dx%d@%.6f%s orientation: %s\n",
						width, height, refresh, interlaced?"i":"", rotated?"rotated":"normal");

	best_mode.result.weight |= R_OUT_OF_RANGE;

	s_mode.interlace = interlaced;
	s_mode.vfreq = refresh;

	s_mode.hactive = width;
	s_mode.vactive = height;

	if (rotated)
	{
		std::swap(s_mode.hactive, s_mode.vactive);
		s_mode.type |= MODE_ROTATED;
	}

	// Create a dummy mode entry if allowed
	if (caps() & CUSTOM_VIDEO_CAPS_ADD && m_ds.modeline_generation)
	{
		modeline new_mode = {};
		new_mode.type = XYV_EDITABLE | V_FREQ_EDITABLE | SCAN_EDITABLE | MODE_ADD | (desktop_is_rotated()? MODE_ROTATED : MODE_OK);
		video_modes.push_back(new_mode);
	}

	// Run through our mode list and find the most suitable mode
	for (auto &mode : video_modes)
	{
		log_verbose("\nSwitchres: %s%4d%sx%s%4d%s_%s%d=%.6fHz%s%s\n",
			mode.type & X_RES_EDITABLE?"(":"[", mode.width, mode.type & X_RES_EDITABLE?")":"]",
			mode.type & Y_RES_EDITABLE?"(":"[", mode.height, mode.type & Y_RES_EDITABLE?")":"]",
			mode.type & V_FREQ_EDITABLE?"(":"[", mode.refresh, mode.vfreq, mode.type & V_FREQ_EDITABLE?")":"]",
			mode.type & MODE_DISABLED?" - locked":"");

		// now get the mode if allowed
		if (mode.type & MODE_DISABLED)
			continue;

		for (int i = 0 ; i < MAX_RANGES ; i++)
		{
			if (range[i].hfreq_min == 0)
				continue;

			t_mode = mode;

			// init all editable fields with source or user values
			if (t_mode.type & X_RES_EDITABLE)
				t_mode.hactive = m_user_mode.width? m_user_mode.width : s_mode.hactive;

			if (t_mode.type & Y_RES_EDITABLE)
				t_mode.vactive = m_user_mode.height? m_user_mode.height : s_mode.vactive;

			if (t_mode.type & V_FREQ_EDITABLE)
			{
				// If user's vfreq is defined, it means we have an user modeline, so force it
				if (m_user_mode.vfreq)
					modeline_copy_timings(&t_mode, &m_user_mode);
				else
					t_mode.vfreq = s_mode.vfreq;
			}

			// lock resolution fields if required
			if (m_user_mode.width) t_mode.type &= ~X_RES_EDITABLE;
			if (m_user_mode.height) t_mode.type &= ~Y_RES_EDITABLE;
			if (m_user_mode.vfreq) t_mode.type &= ~V_FREQ_EDITABLE;

			modeline_create(&s_mode, &t_mode, &range[i], &m_ds.gs);
			t_mode.range = i;

			log_verbose("%s\n", modeline_result(&t_mode, result));

			if (modeline_compare(&t_mode, &best_mode))
			{
				best_mode = t_mode;
				m_selected_mode = &mode;
			}
		}
	}

	// If we didn't need to create a new mode, remove our dummy entry
	if (caps() & CUSTOM_VIDEO_CAPS_ADD && m_ds.modeline_generation && m_selected_mode != &video_modes.back())
		video_modes.pop_back();

	// If we didn't find a suitable mode, exit now
	if (best_mode.result.weight & R_OUT_OF_RANGE)
	{
		m_selected_mode = 0;
		log_error("Switchres: could not find a video mode that meets your specs\n");
		return nullptr;
	}

	if ((best_mode.type & V_FREQ_EDITABLE) && !(best_mode.result.weight & R_OUT_OF_RANGE))
		modeline_adjust(&best_mode, range[best_mode.range].hfreq_max, &m_ds.gs);

	log_verbose("\nSwitchres: %s (%dx%d@%.6f)->(%dx%d@%.6f)\n", rotated?"rotated":"normal",
		width, height, refresh, best_mode.hactive, best_mode.vactive, best_mode.vfreq);

	log_verbose("%s\n", modeline_result(&best_mode, result));

	// Copy the new modeline to our mode list
	if (m_ds.modeline_generation)
	{
		if (best_mode.type & MODE_ADD)
		{
			best_mode.width = best_mode.hactive;
			best_mode.height = best_mode.vactive;
			best_mode.refresh = int(best_mode.vfreq);
			// lock new mode
			best_mode.type &= ~(X_RES_EDITABLE | Y_RES_EDITABLE);
		}
		else if (modeline_is_different(&best_mode, m_selected_mode) != 0)
			best_mode.type |= MODE_UPDATE;

		char modeline[256]={'\x00'};
		log_info("Switchres: Modeline %s\n", modeline_print(&best_mode, modeline, MS_FULL));
	}

	// Check if new best mode is different than previous one
	m_switching_required = (m_current_mode != m_selected_mode || best_mode.type & MODE_UPDATE);

	// Add id to mode
	if (best_mode.id == 0)
		best_mode.id = ++m_id_counter;

	*m_selected_mode = best_mode;
	return m_selected_mode;
}

//============================================================
//  display_manager::auto_specs
//============================================================

bool display_manager::auto_specs()
{
	// Make sure we have a valid mode
	if (desktop_mode.width == 0 || desktop_mode.height == 0 || desktop_mode.refresh == 0)
	{
		log_error("Switchres: Invalid desktop mode %dx%d@%d\n", desktop_mode.width, desktop_mode.height, desktop_mode.refresh);
		return false;
	}

	log_verbose("Switchres: Creating automatic specs for LCD based on %s\n", (desktop_mode.type & CUSTOM_VIDEO_TIMING_SYSTEM)? "VESA GTF" : "current timings");

	// Make sure our current refresh is within range if set to auto
	if (!strcmp(m_ds.lcd_range, "auto"))
	{
		sprintf(m_ds.lcd_range, "%d-%d", desktop_mode.refresh - 1, desktop_mode.refresh + 1);
		monitor_fill_lcd_range(range, m_ds.lcd_range);
	}

	// Create a working range with the best possible information
	if (desktop_mode.type & CUSTOM_VIDEO_TIMING_SYSTEM) modeline_vesa_gtf(&desktop_mode);
	modeline_to_monitor_range(range, &desktop_mode);
	monitor_show_range(range);

	// Force our resolution to LCD's native one
	modeline user_mode = {};
	user_mode.width = desktop_mode.width;
	user_mode.height = desktop_mode.height;
	user_mode.refresh = desktop_mode.refresh;
	set_user_mode(&user_mode);

	return true;
}

//============================================================
//  display_manager::get_aspect
//============================================================

double display_manager::get_aspect(const char* aspect)
{
	int num, den;
	if (sscanf(aspect, "%d:%d", &num, &den) == 2)
	{
		if (den == 0)
		{
			log_error("Error: denominator can't be zero\n");
			return STANDARD_CRT_ASPECT;
		}
		return (double(num)/double(den));
	}

	log_error("Error: use format --aspect <num:den>\n");
	return STANDARD_CRT_ASPECT;
}
