/*
* libtcod
* Copyright (c) 2008-2018 Jice & Mingos & rmtew
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*     * Redistributions of source code must retain the above copyright
*       notice, this list of conditions and the following disclaimer.
*     * Redistributions in binary form must reproduce the above copyright
*       notice, this list of conditions and the following disclaimer in the
*       documentation and/or other materials provided with the distribution.
*     * The name of Jice or Mingos may not be used to endorse or promote
*       products derived from this software without specific prior written
*       permission.
*
* THIS SOFTWARE IS PROVIDED BY JICE, MINGOS AND RMTEW ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL JICE, MINGOS OR RMTEW BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef TCOD_BARE

#include <sys.h>

#include <stdio.h>
#include <string.h>

#include <SDL.h>

#include <console.h>
#include <libtcod_int.h>
#include <libtcod_utility.h>

static SDL_Surface* scale_screen=NULL;
static bool clear_screen=false;
static TCOD_console_data_t *root_console_cache; /* cache for previous values */

/* This just forces a complete redraw, bypassing the usual rendering of changes. */
void TCOD_sys_set_clear_screen(void) {
	clear_screen=true;
}

static void get_closest_mode(int *w, int *h) {
	SDL_DisplayMode wantedmode, closestmode;
	wantedmode.w = *w;
	wantedmode.h = *h;
	wantedmode.format = 0;  /* don't care for rest. */
	wantedmode.refresh_rate = 0;
	wantedmode.driverdata = 0;
	if (SDL_GetClosestDisplayMode(window?SDL_GetWindowDisplayIndex(window):0, &wantedmode, &closestmode) == &closestmode) {
		*w=closestmode.w;
		*h=closestmode.h;
	}
}

/*
 * Separate out the actual rendering, so that render to texture can be done.
 */
static void actual_rendering(void) {
	SDL_Rect srcRect, dstRect;
	SDL_Texture *texture;

	if (scale_data.min_scale_factor - 1e-3f > scale_factor) {
		/* Prepare for the unscaled and centered copy of the entire console. */
		srcRect.x=0; srcRect.y=0; srcRect.w=scale_screen->w; srcRect.h=scale_screen->h;
		if (TCOD_ctx.fullscreen) {
			dstRect.x=TCOD_ctx.fullscreen_offsetx; dstRect.y=TCOD_ctx.fullscreen_offsety;
		} else {
			dstRect.x=0; dstRect.y=0;
		}
		dstRect.w=scale_screen->w; dstRect.h=scale_screen->h;
	} else {
		/* Prepare for the scaled copy of the displayed console area. */
		srcRect.x=scale_data.src_x0; srcRect.y=scale_data.src_y0; srcRect.w=scale_data.src_copy_width; srcRect.h=scale_data.src_copy_height;
		dstRect.x=scale_data.dst_offset_x; dstRect.y=scale_data.dst_offset_y;
		dstRect.w=scale_data.dst_display_width; dstRect.h=scale_data.dst_display_height;
	}
	if ( TCOD_ctx.sdl_cbk ) {
		TCOD_ctx.sdl_cbk((void *)scale_screen);
	}
	texture = SDL_CreateTextureFromSurface(renderer, scale_screen);
	SDL_RenderCopy(renderer, texture, &srcRect, &dstRect);
	SDL_DestroyTexture(texture);
}

/* Return an up-to-date cache for the root console, create or resize the cache
   if needed */
static TCOD_console_data_t *ensure_cache(TCOD_console_data_t* root) {
	if (!root_console_cache ||
			root_console_cache->w != root->w ||
			root_console_cache->h != root->h) {
		if (root_console_cache) { TCOD_console_delete(root_console_cache); }
		root_console_cache = TCOD_console_new(root->w, root->h);
	}
	return root_console_cache;
}

/* In order to avoid rendering race conditions and the ensuing segmentation
 * faults, this should only be called when it would normally be and not
 * specifically to force screen refreshes.  To this end, and to avoid
 * threading complications it takes care of special cases internally.  */
static void render(TCOD_SDL_driver_t *sdl, void *vbitmap, TCOD_console_data_t *console) {
	if ( TCOD_ctx.renderer == TCOD_RENDERER_SDL ) {
		int console_width_p = console->w * TCOD_ctx.font_width;
		int console_height_p = console->h * TCOD_ctx.font_height;

		/* Make a bitmap of exact rendering size and correct format. */
		if (scale_screen == NULL) {
			int bpp;
			uint32_t rmask, gmask, bmask, amask;
			if (SDL_PixelFormatEnumToMasks(SDL_GetWindowPixelFormat(window), &bpp, &rmask, &gmask, &bmask, &amask) == SDL_FALSE) {
				TCOD_fatal("SDL : failed to create scaling surface : indeterminate window pixel format");
				return;
			}
			scale_screen=SDL_CreateRGBSurface(SDL_SWSURFACE,console_width_p,console_height_p,bpp,rmask,gmask,bmask,amask);
			if (scale_screen == NULL) {
				TCOD_fatal("SDL : failed to create scaling surface");
				return;
			}
		} else if (clear_screen) {
			clear_screen=false;
			SDL_FillRect(scale_screen,0,0);
			/* Implicitly do complete console redraw, not just tracked changes. */
			TCOD_console_set_dirty(0, 0, console->w, console->h);
		}

		TCOD_sys_console_to_bitmap(scale_screen, console, ensure_cache(console));

		/* Scale the rendered bitmap to the screen, preserving aspect ratio, and blit it.
		 * This data is also used for console coordinate resolution.. */
		if (scale_data.last_scale_factor != scale_factor || scale_data.last_scale_xc != sdl->scale_xc || scale_data.last_scale_yc != sdl->scale_yc ||
				scale_data.last_fullscreen != TCOD_ctx.fullscreen || scale_data.force_recalc) {
			/* Preserve old value of input variables, to enable recalculation if they change. */
			scale_data.last_scale_factor = scale_factor;
			scale_data.last_scale_xc = sdl->scale_xc;
			scale_data.last_scale_yc = sdl->scale_yc;
			scale_data.last_fullscreen = TCOD_ctx.fullscreen;
			scale_data.force_recalc = 0;

			if (scale_data.last_fullscreen) {
				scale_data.surface_width = TCOD_ctx.actual_fullscreen_width;
				scale_data.surface_height = TCOD_ctx.actual_fullscreen_height;
			} else {
				scale_data.surface_width = console_width_p;
				scale_data.surface_height = console_height_p;
			}
			scale_data.min_scale_factor = MAX((float)console_width_p/scale_data.surface_width, (float)console_height_p/scale_data.surface_height);
			if (scale_data.min_scale_factor > 1.0f)
				scale_data.min_scale_factor = 1.0f;
			/*printf("min_scale_factor %0.3f = MAX(%d/%d, %d/%d)", scale_data.min_scale_factor, console_width_p, scale_data.surface_width, console_height_p, scale_data.surface_height);*/

			scale_data.dst_height_width_ratio = (float)scale_data.surface_height/scale_data.surface_width;
			scale_data.src_proportionate_width = (int)(console_width_p / scale_factor);
			scale_data.src_proportionate_height = (int)((console_width_p * scale_data.dst_height_width_ratio) / scale_factor);

			/* Work out how much of the console to copy. */
			scale_data.src_x0 = (int)((sdl->scale_xc * console_width_p) - (0.5f * scale_data.src_proportionate_width));
			if (scale_data.src_x0 + scale_data.src_proportionate_width > console_width_p)
				scale_data.src_x0 = console_width_p - scale_data.src_proportionate_width;
			if (scale_data.src_x0 < 0)
				scale_data.src_x0 = 0;
			scale_data.src_copy_width = scale_data.src_proportionate_width;
			if (scale_data.src_x0 + scale_data.src_copy_width > console_width_p)
				scale_data.src_copy_width = console_width_p - scale_data.src_x0;

			scale_data.src_y0 = (int)((sdl->scale_yc * console_height_p) - (0.5f * scale_data.src_proportionate_height));
			if (scale_data.src_y0 + scale_data.src_proportionate_height > console_height_p)
				scale_data.src_y0 = console_height_p - scale_data.src_proportionate_height;
			if (scale_data.src_y0 < 0)
				scale_data.src_y0 = 0;
			scale_data.src_copy_height = scale_data.src_proportionate_height;
			if (scale_data.src_y0 + scale_data.src_copy_height > console_height_p)
				scale_data.src_copy_height = console_height_p - scale_data.src_y0;

			scale_data.dst_display_width = (scale_data.src_copy_width * scale_data.surface_width) / scale_data.src_proportionate_width;
			scale_data.dst_display_height = (scale_data.src_copy_height * scale_data.surface_height) / scale_data.src_proportionate_height;
			scale_data.dst_offset_x = (scale_data.surface_width - scale_data.dst_display_width)/2;
			scale_data.dst_offset_y = (scale_data.surface_height - scale_data.dst_display_height)/2;
		}

		SDL_RenderClear(renderer);
		actual_rendering();
		SDL_RenderPresent(renderer);
	}
#ifndef NO_OPENGL
	else {
		TCOD_opengl_render(oldFade, NULL, console, ensure_cache(console));
		TCOD_opengl_swap();
	}
#endif
	oldFade=(int)TCOD_console_get_fade();
}

/* Return the current root console cache if it exists, or NULL. */
static TCOD_console_data_t *get_root_console_cache(void){
	return root_console_cache;
}

static SDL_Surface *create_surface(int width, int height, bool with_alpha) {
	uint32_t rmask,gmask,bmask,amask;
	SDL_Surface *bitmap;
	int flags=SDL_SWSURFACE;

	if ( with_alpha ) {
		if ( SDL_BYTEORDER == SDL_LIL_ENDIAN ) {
			rmask=0x000000FF;
			gmask=0x0000FF00;
			bmask=0x00FF0000;
			amask=0xFF000000;
		} else {
			rmask=0xFF000000;
			gmask=0x00FF0000;
			bmask=0x0000FF00;
			amask=0x000000FF;
		}
	} else {
		if ( SDL_BYTEORDER == SDL_LIL_ENDIAN ) {
			rmask=0x0000FF;
			gmask=0x00FF00;
			bmask=0xFF0000;
		} else {
			rmask=0xFF0000;
			gmask=0x00FF00;
			bmask=0x0000FF;
		}
		amask=0;
	}
	bitmap=SDL_CreateRGBSurface(flags,width,height,
		with_alpha ? 32:24,
		rmask,gmask,bmask,amask);
	if ( with_alpha ) {
		SDL_SetSurfaceAlphaMod(bitmap, 255);
	}
	return (void *)bitmap;
}

static void create_window(int w, int h, bool fullscreen) {
	uint32_t winflags = SDL_WINDOW_SHOWN | SDL_WINDOW_OPENGL;
#if defined(TCOD_ANDROID)
	/* Android should always be fullscreen. */
	TCOD_ctx.fullscreen = fullscreen = true;
#endif
	if ( fullscreen  ) {
		find_resolution();
#ifndef NO_OPENGL
		if (TCOD_ctx.renderer != TCOD_RENDERER_SDL ) {
			TCOD_opengl_init_attributes();
			winflags |= SDL_WINDOW_FULLSCREEN | SDL_WINDOW_BORDERLESS | SDL_WINDOW_OPENGL;
#	if defined(TCOD_ANDROID) && defined(FUTURE_SUPPORT)
			winflags |= SDL_WINDOW_RESIZABLE;
#	endif
			window = SDL_CreateWindow(TCOD_ctx.window_title,SDL_WINDOWPOS_CENTERED,SDL_WINDOWPOS_CENTERED,TCOD_ctx.actual_fullscreen_width,TCOD_ctx.actual_fullscreen_height,winflags);
			if ( window && TCOD_opengl_init_state(w, h, charmap) && TCOD_opengl_init_shaders() ) {
				TCOD_LOG(("Using %s renderer...\n",TCOD_ctx.renderer == TCOD_RENDERER_GLSL ? "GLSL" : "OPENGL"));
			} else {
				TCOD_LOG(("Fallback to SDL renderer...\n"));
				TCOD_ctx.renderer = TCOD_RENDERER_SDL;
			}
		}
#endif
		if (TCOD_ctx.renderer == TCOD_RENDERER_SDL ) {
			winflags |= SDL_WINDOW_FULLSCREEN | SDL_WINDOW_BORDERLESS;
#	if defined(TCOD_ANDROID) && defined(FUTURE_SUPPORT)
			winflags |= SDL_WINDOW_RESIZABLE;
#	endif
			window = SDL_CreateWindow(TCOD_ctx.window_title,SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, TCOD_ctx.actual_fullscreen_width,TCOD_ctx.actual_fullscreen_height,winflags);
			if ( window == NULL ) TCOD_fatal("SDL : cannot set fullscreen video mode: %s", SDL_GetError());
		}
		SDL_ShowCursor(0);
		SDL_GetWindowSize(window,&TCOD_ctx.actual_fullscreen_width,&TCOD_ctx.actual_fullscreen_height);
		TCOD_sys_init_screen_offset();
	} else {
#ifndef NO_OPENGL
		if (TCOD_ctx.renderer != TCOD_RENDERER_SDL ) {
			TCOD_opengl_init_attributes();
			winflags |= SDL_WINDOW_OPENGL;
			window = SDL_CreateWindow(TCOD_ctx.window_title,SDL_WINDOWPOS_CENTERED,SDL_WINDOWPOS_CENTERED,w*TCOD_ctx.font_width,h*TCOD_ctx.font_height,winflags);
			if ( window && TCOD_opengl_init_state(w, h, charmap) && TCOD_opengl_init_shaders() ) {
				TCOD_LOG(("Using %s renderer...\n",TCOD_ctx.renderer == TCOD_RENDERER_GLSL ? "GLSL" : "OPENGL"));
			} else {
				TCOD_LOG(("Fallback to SDL renderer...\n"));
				TCOD_ctx.renderer = TCOD_RENDERER_SDL;
			}
		}
#endif
		if (TCOD_ctx.renderer == TCOD_RENDERER_SDL ) {
			window = SDL_CreateWindow(TCOD_ctx.window_title,SDL_WINDOWPOS_CENTERED,SDL_WINDOWPOS_CENTERED,w*TCOD_ctx.font_width,h*TCOD_ctx.font_height,winflags);
			TCOD_LOG(("Using SDL renderer...\n"));
		}
		if ( window == NULL ) TCOD_fatal("Cannot create SDL window: %s", SDL_GetError());
	}
	if (TCOD_ctx.renderer == TCOD_RENDERER_SDL ) {
		renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
		if ( renderer == NULL ) TCOD_fatal("Cannot create SDL renderer: %s", SDL_GetError());
		SDL_SetRenderDrawColor(renderer, 0, 0, 0, SDL_ALPHA_OPAQUE);
	}
}

static void destroy_window(void) {
#ifndef NO_OPENGL
	if (TCOD_ctx.renderer == TCOD_RENDERER_OPENGL || TCOD_ctx.renderer == TCOD_RENDERER_GLSL) {
		TCOD_opengl_uninit_state();
	}
#endif
	if (scale_screen) {
		SDL_FreeSurface(scale_screen);
		scale_screen = NULL;
	}
	if (renderer) {
		SDL_DestroyRenderer(renderer);
		renderer = NULL;
	}
	if (window) {
		SDL_DestroyWindow(window);
		window = NULL;
	}
}

static void set_fullscreen(bool fullscreen) {
	bool mouseOn=SDL_ShowCursor(-1);
	if ( fullscreen ) {
		find_resolution();
		SDL_SetWindowFullscreen(window, fullscreen);
		SDL_ShowCursor(mouseOn ? 1:0);
		SDL_GetWindowSize(window,&TCOD_ctx.actual_fullscreen_width,&TCOD_ctx.actual_fullscreen_height);
		TCOD_sys_init_screen_offset();
	} else {
		SDL_SetWindowFullscreen(window, fullscreen);
		SDL_ShowCursor(mouseOn ? 1:0);
		TCOD_ctx.fullscreen_offsetx=0;
		TCOD_ctx.fullscreen_offsety=0;
	}
	TCOD_ctx.fullscreen=fullscreen;
	oldFade=-1; /* to redraw the whole screen */
}

static void set_window_title(const char *title) {
	SDL_SetWindowTitle(window, title);
}

static void save_screenshot(const char *filename) {
	if ( TCOD_ctx.renderer == TCOD_RENDERER_SDL ) {
		/* This would be a lot easier if image saving could do textures. */
	    SDL_Rect rect;
		uint32_t format;
		SDL_Texture *texture;
		SDL_RenderGetViewport(renderer, &rect);
		format = SDL_GetWindowPixelFormat(window);
		texture = SDL_CreateTexture(renderer, format, SDL_TEXTUREACCESS_TARGET, rect.w, rect.h);
		if (0 != texture) {
			if (SDL_SetRenderTarget(renderer, texture)) {
				void *pixels;
				int pitch, access;

				actual_rendering();
				SDL_SetRenderTarget(renderer, NULL);

				rect.x = rect.y = rect.w = rect.h = 0;
				if (-1 != SDL_QueryTexture(texture, &format, &access, &rect.w, &rect.h) &&
						-1 != SDL_LockTexture(texture, NULL, &pixels, &pitch)) {
					int depth;
					uint32_t rmask, gmask, bmask, amask;
					if (SDL_TRUE == SDL_PixelFormatEnumToMasks(format, &depth, &rmask, &gmask, &bmask, &amask)) {
						SDL_Surface *surface = SDL_CreateRGBSurfaceFrom(pixels, rect.w, rect.h, depth, pitch, rmask, gmask, bmask, amask);
						TCOD_sys_save_bitmap((void *)surface,filename);
						SDL_FreeSurface(surface);
					} else
						TCOD_LOG(("TCOD_sys_save_screenshot - failed call to SDL_PixelFormatEnumToMasks"));

					SDL_UnlockTexture(texture);
				} else
					TCOD_LOG(("TCOD_sys_save_screenshot - failed call to SDL_QueryTexture or SDL_LockTexture"));
			} else
				TCOD_LOG(("TCOD_sys_save_screenshot - failed call to SDL_SetRenderTarget"));
			SDL_DestroyTexture(texture);
		} else
			TCOD_LOG(("TCOD_sys_save_screenshot - failed call to SDL_CreateTexture"));
#ifndef NO_OPENGL
	} else {
		SDL_Surface *screenshot=(SDL_Surface *)TCOD_opengl_get_screen();
		TCOD_sys_save_bitmap((void *)screenshot,filename);
		SDL_FreeSurface(screenshot);
#endif
	}
}
/* get desktop resolution */
static void get_current_resolution(int *w, int *h) {
	int displayidx;
	SDL_Rect rect = { 0, 0, 0, 0 };
	if (window) {
		TCOD_IFNOT(window) return;
		displayidx = SDL_GetWindowDisplayIndex(window);
		TCOD_IFNOT(displayidx >= 0) return;
	} else {
		/* No window if no console, but user can want to know res before opening one. */
		TCOD_IFNOT(SDL_GetNumVideoDisplays() > 0) return;
		displayidx = 0;
	}
	TCOD_IFNOT(SDL_GetDisplayBounds(displayidx, &rect) == 0) return;
	*w=rect.w;
	*h=rect.h;
}

static void set_mouse_position(int x, int y) {
  SDL_WarpMouseInWindow(window, (uint16_t)x,(uint16_t)y);
}

static char *get_clipboard_text(void) {
#ifdef TCOD_LINUX
	/*
		X11 clipboard is inaccessible without an open window.
		https://bugzilla.libsdl.org/show_bug.cgi?id=3222
	*/
	if (!window)
		return "";
#endif

	/*
	We hold onto the last clipboard text pointer SDL gave us.
	For C API callers it can be considered a borrowed reference.
	For Python ctypes API callers, the contents are copied into the Python string that is constructed from it.
	*/
	if (last_clipboard_text) {
		SDL_free(last_clipboard_text);
		last_clipboard_text = NULL;
	}
	last_clipboard_text = SDL_GetClipboardText();
	return last_clipboard_text;
}

static bool set_clipboard_text(const char *text) {
#ifdef TCOD_LINUX
	/*
	X11 clipboard is inaccessible without an open window.
	https://bugzilla.libsdl.org/show_bug.cgi?id=3222
	*/
	if (!window)
		return false;
#endif

	return SDL_SetClipboardText(text) == 0;
}

/* android compatible file access functions */
static bool file_read(const char *filename, unsigned char **buf, size_t *size) {
	int64_t filesize;
	/* get file size */
	SDL_RWops *rwops= SDL_RWFromFile(filename,"rb");
	if (!rwops) return false;
	SDL_RWseek(rwops,0,RW_SEEK_END);
	filesize=SDL_RWtell(rwops);
	SDL_RWseek(rwops,0,RW_SEEK_SET);
	/* allocate buffer */
	*buf = (unsigned char *)malloc(sizeof(unsigned char)*filesize);
	/* read from file */
	if (SDL_RWread(rwops,*buf,sizeof(unsigned char),filesize) != filesize) {
		SDL_RWclose(rwops);
		free(*buf);
		return false;
	}
	SDL_RWclose(rwops);
	*size=filesize;
	return true;
}

static bool file_exists(const char * filename) {
	SDL_RWops *rwops;
	rwops = SDL_RWFromFile(filename,"rb");
	if (rwops) {
		SDL_RWclose(rwops);
		return true;
	}
	return false;
}

static bool file_write(const char *filename, unsigned char *buf, uint32_t size) {
	SDL_RWops *rwops= SDL_RWFromFile(filename,"wb");
	if (!rwops) return false;
	SDL_RWwrite(rwops,buf,sizeof(unsigned char),size);
	SDL_RWclose(rwops);
	return true;
}

static void shutdown_(void) {
	if (last_clipboard_text) {
		SDL_free(last_clipboard_text);
		last_clipboard_text = NULL;
	}
	if (root_console_cache) {
		TCOD_console_delete(root_console_cache);
		root_console_cache = NULL;
		}
}

TCOD_SDL_driver_t *SDL_implementation_factory(void) {
	TCOD_SDL_driver_t *ret=(TCOD_SDL_driver_t *)calloc(1,sizeof(TCOD_SDL_driver_t));
    ret->scale_xc = 0.5f;
    ret->scale_yc = 0.5f;

    ret->get_closest_mode = get_closest_mode;
	ret->render = render;
	ret->create_surface = create_surface;
	ret->create_window = create_window;
	ret->destroy_window = destroy_window;
	ret->set_fullscreen = set_fullscreen;
	ret->set_window_title = set_window_title;
	ret->save_screenshot = save_screenshot;
	ret->get_current_resolution = get_current_resolution;
	ret->set_mouse_position = set_mouse_position;
	ret->get_clipboard_text = get_clipboard_text;
	ret->set_clipboard_text = set_clipboard_text;
	ret->file_read = file_read;
	ret->file_exists = file_exists;
	ret->file_write = file_write;
	ret->shutdown = shutdown_;
	ret->get_root_console_cache = get_root_console_cache;
	return ret;
}

#endif /* TCOD_BARE */
