/* ScummVM - Graphic Adventure Engine
 *
 * ScummVM is the legal property of its developers, whose names
 * are too numerous to list here. Please refer to the COPYRIGHT
 * file distributed with this source distribution.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

// Allow use of stuff in <time.h>
#define FORBIDDEN_SYMBOL_EXCEPTION_time_h

// Disable printf override in common/forbidden.h to avoid
// clashes with log.h from the Android SDK.
// That header file uses
//   __attribute__ ((format(printf, 3, 4)))
// which gets messed up by our override mechanism; this could
// be avoided by either changing the Android SDK to use the equally
// legal and valid
//   __attribute__ ((format(printf, 3, 4)))
// or by refining our printf override to use a varadic macro
// (which then wouldn't be portable, though).
// Anyway, for now we just disable the printf override globally
// for the Android port
#define FORBIDDEN_SYMBOL_EXCEPTION_printf

#include "common/tokenizer.h"
#include "graphics/conversion.h"
#include "graphics/opengl/shader.h"
#include "graphics/opengl/context.h"

#include "backends/graphics3d/android/android-graphics3d.h"
#include "backends/platform/android/android.h"
#include "backends/platform/android/jni-android.h"

// These helper macros let us setup our context only when the game has different settings than us
#define CONTEXT_SAVE_STATE(gl_param) GLboolean saved ## gl_param; GLCALL(saved ## gl_param = glIsEnabled(gl_param))
#define CONTEXT_SET_ENABLE(gl_param) if (!(saved ## gl_param)) { GLCALL(glEnable(gl_param)); }
#define CONTEXT_SET_DISABLE(gl_param) if (saved ## gl_param) { GLCALL(glDisable(gl_param)); }
// These helper macros do the opposite to get back what the game expected
#define CONTEXT_RESET_ENABLE(gl_param) if (!(saved ## gl_param)) { GLCALL(glDisable(gl_param)); }
#define CONTEXT_RESET_DISABLE(gl_param) if (saved ## gl_param) { GLCALL(glEnable(gl_param)); }

static GLES8888Texture *loadBuiltinTexture(JNI::BitmapResources resource) {
	const Graphics::Surface *src = JNI::getBitmapResource(JNI::BitmapResources::TOUCH_ARROWS_BITMAP);
	if (!src) {
		error("Failed to fetch touch arrows bitmap");
	}

	GLES8888Texture *ret = new GLES8888Texture();
	ret->allocBuffer(src->w, src->h);
	Graphics::Surface *dst = ret->surface();

	Graphics::crossBlit(
			(byte *)dst->getPixels(), (const byte *)src->getPixels(),
			dst->pitch, src->pitch,
			src->w, src->h,
			src->format, dst->format);

	delete src;
	return ret;
}

AndroidGraphics3dManager::AndroidGraphics3dManager() :
	_screenChangeID(0),
	_graphicsMode(0),
	_fullscreen(true),
	_ar_correction(true),
	_force_redraw(false),
	_game_texture(0),
	_frame_buffer(0),
	_cursorX(0),
	_cursorY(0),
	_overlay_texture(0),
	_overlay_background(nullptr),
	_show_overlay(false),
	_mouse_texture(nullptr),
	_mouse_texture_palette(nullptr),
	_mouse_texture_rgb(nullptr),
	_mouse_hotspot(),
	_mouse_dont_scale(false),
	_show_mouse(false),
	_touchcontrols_texture(nullptr),
	_old_touch_mode(OSystem_Android::TOUCH_MODE_TOUCHPAD) {

	if (JNI::egl_bits_per_pixel == 16) {
		// We default to RGB565 and RGBA5551 which is closest to what we setup in Java side
		_game_texture = new GLES565Texture();
		_overlay_texture = new GLES5551Texture();
		_overlay_background = new GLES565Texture();
		_mouse_texture_palette = new GLESFakePalette5551Texture();
	} else {
		// If not 16, this must be 24 or 32 bpp so make use of them
		_game_texture = new GLES888Texture();
		_overlay_texture = new GLES8888Texture();
		_overlay_background = new GLES888Texture();
		_mouse_texture_palette = new GLESFakePalette8888Texture();
	}
	_mouse_texture = _mouse_texture_palette;

	_touchcontrols_texture = loadBuiltinTexture(JNI::BitmapResources::TOUCH_ARROWS_BITMAP);

	initSurface();

	// in 3D, not in overlay
	dynamic_cast<OSystem_Android *>(g_system)->applyTouchSettings(true, false);
}

AndroidGraphics3dManager::~AndroidGraphics3dManager() {
	// Reinitialize OpenGL for other manager
	glBindBuffer(GL_ARRAY_BUFFER, 0);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
	glBindFramebuffer(GL_FRAMEBUFFER, 0);
	glBindRenderbuffer(GL_RENDERBUFFER, 0);
	glUseProgram(0);

	deinitSurface();

	delete _frame_buffer;
	delete _game_texture;
	delete _overlay_texture;
	delete _overlay_background;
	delete _mouse_texture_palette;
	delete _mouse_texture_rgb;
	delete _touchcontrols_texture;
}

static void logExtensions() {
	const char *ext_string =
	    reinterpret_cast<const char *>(glGetString(GL_EXTENSIONS));

	LOGI("Extensions:");

	Common::String exts;
	Common::StringTokenizer tokenizer(ext_string, " ");
	while (!tokenizer.empty()) {
		Common::String token = tokenizer.nextToken();

		exts += token + " ";
		if (exts.size() > 100) {
			LOGI("\t%s", exts.c_str());
			exts = "";
		}
	}

	if (exts.size() > 0) {
		LOGI("\t%s", exts.c_str());
	}
}


void AndroidGraphics3dManager::initSurface() {
	LOGD("initializing 3D surface");

	assert(!JNI::haveSurface());

	JNI::initSurface();

	_screenChangeID = JNI::surface_changeid;

	// Initialize OpenGLES context.
	OpenGLContext.initialize(OpenGL::kContextGLES2);
	logExtensions();
	GLESTexture::initGL();

	if (_game_texture) {
		_game_texture->reinit();
		// We had a frame buffer initialized, we must renew it as the game textured got renewed
		if (_frame_buffer) {
			delete _frame_buffer;
			_frame_buffer = new OpenGL::FrameBuffer(_game_texture->getTextureName(),
	                                        _game_texture->width(), _game_texture->height(),
	                                        _game_texture->texWidth(), _game_texture->texHeight());

		}
	}

	// We don't have any content to display: just make sure surface is clean
	if (_overlay_background) {
		_overlay_background->release();
	}

	if (_overlay_texture) {
		_overlay_texture->reinit();
		initOverlay();
	}

	if (_mouse_texture) {
		_mouse_texture->reinit();
	}

	if (_touchcontrols_texture) {
		_touchcontrols_texture->reinit();
	}
	dynamic_cast<OSystem_Android *>(g_system)->getTouchControls().init(
	    this, JNI::egl_surface_width, JNI::egl_surface_height);

	updateScreenRect();
	// double buffered, flip twice
	clearScreen(kClearUpdate, 2);
}

void AndroidGraphics3dManager::deinitSurface() {
	if (!JNI::haveSurface()) {
		return;
	}

	LOGD("deinitializing 3D surface");

	_screenChangeID = JNI::surface_changeid;

	// release texture resources
	if (_game_texture) {
		_game_texture->release();
	}

	if (_overlay_texture) {
		_overlay_texture->release();
	}

	if (_overlay_background) {
		_overlay_background->release();
	}

	if (_mouse_texture) {
		_mouse_texture->release();
	}

	dynamic_cast<OSystem_Android *>(g_system)->getTouchControls().init(
	    nullptr, 0, 0);
	if (_touchcontrols_texture) {
		_touchcontrols_texture->release();
	}

	OpenGLContext.reset();

	JNI::deinitSurface();
}

void AndroidGraphics3dManager::updateScreen() {
	//ENTER();

	GLTHREADCHECK;

	if (!JNI::haveSurface()) {
		return;
	}

	if (!_force_redraw &&
	        !_game_texture->dirty() &&
	        !_overlay_texture->dirty() &&
	        !_mouse_texture->dirty()) {
		return;
	}

	_force_redraw = false;

	// Save the game state
	GLint savedBlendSrcRGB, savedBlendDstRGB, savedBlendSrcAlpha, savedBlendDstAlpha,
	      savedBlendEqRGB, savedBlendEqAlpha;
	GLint savedViewport[4];
	CONTEXT_SAVE_STATE(GL_BLEND);
	GLCALL(glGetIntegerv(GL_BLEND_SRC_RGB, &savedBlendSrcRGB));
	GLCALL(glGetIntegerv(GL_BLEND_DST_RGB, &savedBlendDstRGB));
	GLCALL(glGetIntegerv(GL_BLEND_SRC_ALPHA, &savedBlendSrcAlpha));
	GLCALL(glGetIntegerv(GL_BLEND_DST_ALPHA, &savedBlendDstAlpha));
	GLCALL(glGetIntegerv(GL_BLEND_EQUATION_RGB, &savedBlendEqRGB));
	GLCALL(glGetIntegerv(GL_BLEND_EQUATION_ALPHA, &savedBlendEqAlpha));
	CONTEXT_SAVE_STATE(GL_CULL_FACE);
	CONTEXT_SAVE_STATE(GL_DEPTH_TEST);
	CONTEXT_SAVE_STATE(GL_DITHER);
	CONTEXT_SAVE_STATE(GL_POLYGON_OFFSET_FILL);
	CONTEXT_SAVE_STATE(GL_SAMPLE_ALPHA_TO_COVERAGE);
	CONTEXT_SAVE_STATE(GL_SAMPLE_COVERAGE);
	CONTEXT_SAVE_STATE(GL_SCISSOR_TEST);
	CONTEXT_SAVE_STATE(GL_STENCIL_TEST);
	GLCALL(glGetIntegerv(GL_VIEWPORT, savedViewport));

	if (_frame_buffer) {
		_frame_buffer->detach();
	}

	// Make sure everything we need is correctly set up
	// Enable what we need and disable the other if it is not already
	CONTEXT_SET_ENABLE(GL_BLEND);
	if (savedBlendSrcRGB != GL_SRC_ALPHA ||
	    savedBlendDstRGB != GL_ONE_MINUS_SRC_ALPHA ||
	    savedBlendSrcAlpha != GL_SRC_ALPHA ||
	    savedBlendDstAlpha != GL_ONE_MINUS_SRC_ALPHA) {
		GLCALL(glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));
	}
	if (savedBlendEqRGB != GL_FUNC_ADD ||
	    savedBlendEqAlpha != GL_FUNC_ADD) {
		GLCALL(glBlendEquation(GL_FUNC_ADD));
	}
	CONTEXT_SET_DISABLE(GL_CULL_FACE);
	CONTEXT_SET_DISABLE(GL_DEPTH_TEST);
	CONTEXT_SET_DISABLE(GL_DITHER);
	CONTEXT_SET_DISABLE(GL_POLYGON_OFFSET_FILL);
	CONTEXT_SET_DISABLE(GL_SAMPLE_ALPHA_TO_COVERAGE);
	CONTEXT_SET_DISABLE(GL_SAMPLE_COVERAGE);
	CONTEXT_SET_DISABLE(GL_SCISSOR_TEST);
	CONTEXT_SET_DISABLE(GL_STENCIL_TEST);

	glViewport(0, 0, JNI::egl_surface_width, JNI::egl_surface_height);

	// clear pointer leftovers in dead areas
	clearScreen(kClear);

	_game_texture->drawTextureRect();

	if (_show_overlay) {
		if (_overlay_background && _overlay_background->getTextureName() != 0) {
			GLCALL(_overlay_background->drawTextureRect());
		}
		GLCALL(_overlay_texture->drawTextureRect());

		if (_show_mouse && !_mouse_texture->isEmpty()) {
			_mouse_texture->drawTexture(_cursorX - _mouse_hotspot_scaled.x, _cursorY - _mouse_hotspot_scaled.y,
						    _mouse_width_scaled, _mouse_width_scaled);
		}
	}

	dynamic_cast<OSystem_Android *>(g_system)->getTouchControls().draw();

	if (!JNI::swapBuffers()) {
		LOGW("swapBuffers failed: 0x%x", glGetError());
	}

	// Here we restore back the GLES state so if we enabled something we disable it back if it needs too and vice versa

	CONTEXT_RESET_ENABLE(GL_BLEND);
	if (savedGL_BLEND && (
	    savedBlendSrcRGB != GL_SRC_ALPHA ||
	    savedBlendDstRGB != GL_ONE_MINUS_SRC_ALPHA ||
	    savedBlendSrcAlpha != GL_SRC_ALPHA ||
	    savedBlendDstAlpha != GL_ONE_MINUS_SRC_ALPHA)) {
		GLCALL(glBlendFuncSeparate(savedBlendSrcRGB, savedBlendDstRGB,
		                           savedBlendSrcAlpha, savedBlendDstAlpha));
	}
	if (savedGL_BLEND && (
	    savedBlendEqRGB != GL_FUNC_ADD ||
	    savedBlendEqAlpha != GL_FUNC_ADD)) {
		GLCALL(glBlendEquationSeparate(savedBlendEqRGB, savedBlendEqAlpha));
	}
	CONTEXT_RESET_DISABLE(GL_CULL_FACE);
	CONTEXT_RESET_DISABLE(GL_DEPTH_TEST);
	CONTEXT_RESET_DISABLE(GL_DITHER);
	CONTEXT_RESET_DISABLE(GL_POLYGON_OFFSET_FILL);
	CONTEXT_RESET_DISABLE(GL_SAMPLE_ALPHA_TO_COVERAGE);
	CONTEXT_RESET_DISABLE(GL_SAMPLE_COVERAGE);
	CONTEXT_RESET_DISABLE(GL_SCISSOR_TEST);
	CONTEXT_RESET_DISABLE(GL_STENCIL_TEST);

	// Restore game viewport
	GLCALL(glViewport(savedViewport[0], savedViewport[1], savedViewport[2], savedViewport[3]));

	// Don't keep our texture attached to avoid the engine writing on it if it forgets to setup its own texture
	GLCALL(glBindTexture(GL_TEXTURE_2D, 0));
	// Unload our program to make sure engine will use its own
	GLESBaseTexture::unbindShader();

	if (_frame_buffer) {
		_frame_buffer->attach();
	}
}

void AndroidGraphics3dManager::displayMessageOnOSD(const Common::U32String &msg) {
	ENTER("%s", msg.encode().c_str());

	JNI::displayMessageOnOSD(msg);
}

Common::Point AndroidGraphics3dManager::convertScreenToVirtual(int &x, int &y) const {
	const GLESBaseTexture *tex = getActiveTexture();
	const Common::Rect &screenRect = tex->getDrawRect();

	// Clip in place the coordinates that comes handy to call setMousePosition
	x = CLIP<int>(x, screenRect.left, screenRect.right - 1);
	y = CLIP<int>(y, screenRect.top, screenRect.bottom - 1);

	// Now convert this to virtual coordinates using texture virtual size
	const uint16 virtualWidth = tex->width();
	const uint16 virtualHeight = tex->height();

	int virtualX = ((x - screenRect.left) * virtualWidth + screenRect.width() / 2) / screenRect.width();
	int virtualY = ((y - screenRect.top) * virtualHeight + screenRect.height() / 2) / screenRect.height();

	return Common::Point(CLIP<int>(virtualX, 0, virtualWidth - 1),
	                     CLIP<int>(virtualY, 0, virtualHeight - 1));
}

Common::Point AndroidGraphics3dManager::convertVirtualToScreen(int x, int y) const {
	const GLESBaseTexture *tex = getActiveTexture();
	const uint16 virtualWidth = tex->width();
	const uint16 virtualHeight = tex->height();
	const Common::Rect &screenRect = tex->getDrawRect();

	int screenX = screenRect.left + (x * screenRect.width() + virtualWidth / 2) / virtualWidth;
	int screenY = screenRect.top + (y * screenRect.height() + virtualHeight / 2) / virtualHeight;

	return Common::Point(CLIP<int>(screenX, screenRect.left, screenRect.right - 1),
	                     CLIP<int>(screenY, screenRect.top, screenRect.bottom - 1));
}

bool AndroidGraphics3dManager::notifyMousePosition(Common::Point &mouse) {
	// At entry, mouse is in screen coordinates like the texture draw rectangle
	int x = mouse.x, y = mouse.y;
	Common::Point vMouse = convertScreenToVirtual(x, y);

	// Our internal mouse position is in screen coordinates
	// convertScreenToVirtual just clipped coordinates so we are safe
	setMousePosition(x, y);

	// Now modify mouse to translate to virtual coordinates for the caller
	mouse = vMouse;

	return true;
}

const OSystem::GraphicsMode *AndroidGraphics3dManager::getSupportedGraphicsModes() const {
	static const OSystem::GraphicsMode s_supportedGraphicsModes[] = {
		{ "default", "Default", 0 },
		{ 0, 0, 0 },
	};

	return s_supportedGraphicsModes;
}

int AndroidGraphics3dManager::getDefaultGraphicsMode() const {
	return 0;
}

bool AndroidGraphics3dManager::setGraphicsMode(int mode, uint flags) {
	return true;
}

int AndroidGraphics3dManager::getGraphicsMode() const {
	return _graphicsMode;
}

bool AndroidGraphics3dManager::hasFeature(OSystem::Feature f) const {
	if (f == OSystem::kFeatureCursorPalette ||
	        f == OSystem::kFeatureOpenGLForGame ||
	        f == OSystem::kFeatureAspectRatioCorrection) {
		return true;
	}
	if (f == OSystem::kFeatureOverlaySupportsAlpha) {
		return _overlay_texture->getPixelFormat().aBits() > 3;
	}
	return false;
}

void AndroidGraphics3dManager::setFeatureState(OSystem::Feature f, bool enable) {
	switch (f) {
	case OSystem::kFeatureFullscreenMode:
		_fullscreen = enable;
		updateScreenRect();
		break;
	case OSystem::kFeatureAspectRatioCorrection:
		_ar_correction = enable;
		updateScreenRect();
		break;
	default:
		break;
	}
}

bool AndroidGraphics3dManager::getFeatureState(OSystem::Feature f) const {
	switch (f) {
	case OSystem::kFeatureCursorPalette:
		return true;
	case OSystem::kFeatureFullscreenMode:
		return _fullscreen;
	case OSystem::kFeatureAspectRatioCorrection:
		return _ar_correction;
	default:
		return false;
	}
}

void AndroidGraphics3dManager::showOverlay() {
	ENTER();

	if (_show_overlay) {
		return;
	}

	_old_touch_mode = JNI::getTouchMode();
	// in 3D, in overlay
	dynamic_cast<OSystem_Android *>(g_system)->applyTouchSettings(true, true);

	_show_overlay = true;
	_force_redraw = true;

	// If there is a game running capture the screen, so that it can be shown "below" the overlay.
	if (_overlay_background) {
		_overlay_background->release();

		if (g_engine) {
			GLint savedViewport[4];
			GLCALL(glGetIntegerv(GL_VIEWPORT, savedViewport));

			if (_frame_buffer) {
				_frame_buffer->detach();
			}

			GLCALL(glViewport(0, 0, JNI::egl_surface_width, JNI::egl_surface_height));
			_overlay_background->allocBuffer(_overlay_texture->width(), _overlay_texture->height());
			_overlay_background->setDrawRect(0, 0,
			                                 JNI::egl_surface_width, JNI::egl_surface_height);
			_overlay_background->readPixels();

			// Restore game viewport
			GLCALL(glViewport(savedViewport[0], savedViewport[1], savedViewport[2], savedViewport[3]));

			if (_frame_buffer) {
				_frame_buffer->attach();
			}
		}
	}

	warpMouse(_overlay_texture->width() / 2, _overlay_texture->height() / 2);
}

void AndroidGraphics3dManager::hideOverlay() {
	ENTER();

	if (!_show_overlay) {
		return;
	}

	_show_overlay = false;

	_overlay_background->release();

	// Restore touch mode active before overlay was shown
	JNI::setTouchMode(_old_touch_mode);

	warpMouse(_game_texture->width() / 2, _game_texture->height() / 2);

	// double buffered, flip twice
	clearScreen(kClearUpdate, 2);
}

void AndroidGraphics3dManager::clearOverlay() {
	ENTER();

	GLTHREADCHECK;

	_overlay_texture->fillBuffer(0);
}

void AndroidGraphics3dManager::grabOverlay(Graphics::Surface &surface) const {
	ENTER("%p", &surface);

	GLTHREADCHECK;

	const Graphics::Surface *overlaySurface = _overlay_texture->surface_const();

	assert(surface.w >= overlaySurface->w);
	assert(surface.h >= overlaySurface->h);
	assert(surface.format.bytesPerPixel == overlaySurface->format.bytesPerPixel);

	const byte *src = (const byte *)overlaySurface->getPixels();
	byte *dst = (byte *)surface.getPixels();
	Graphics::copyBlit(dst, src, surface.pitch, overlaySurface->pitch,
	                   overlaySurface->w, overlaySurface->h, overlaySurface->format.bytesPerPixel);
}

void AndroidGraphics3dManager::copyRectToOverlay(const void *buf, int pitch,
        int x, int y, int w, int h) {
	ENTER("%p, %d, %d, %d, %d, %d", buf, pitch, x, y, w, h);

	GLTHREADCHECK;

	_overlay_texture->updateBuffer(x, y, w, h, buf, pitch);
}

int16 AndroidGraphics3dManager::getOverlayHeight() const {
	return _overlay_texture->height();
}

int16 AndroidGraphics3dManager::getOverlayWidth() const {
	return _overlay_texture->width();
}

Graphics::PixelFormat AndroidGraphics3dManager::getOverlayFormat() const {
	return _overlay_texture->getPixelFormat();
}

int16 AndroidGraphics3dManager::getHeight() const {
	return _game_texture->height();
}

int16 AndroidGraphics3dManager::getWidth() const {
	return _game_texture->width();
}

void AndroidGraphics3dManager::setPalette(const byte *colors, uint start, uint num) {
	// We should never end up here in 3D
	assert(false);
}

void AndroidGraphics3dManager::grabPalette(byte *colors, uint start, uint num) const {
	// We should never end up here in 3D
	assert(false);
}

Graphics::Surface *AndroidGraphics3dManager::lockScreen() {
	// We should never end up here in 3D
	assert(false);

	return nullptr;
}

void AndroidGraphics3dManager::unlockScreen() {
	// We should never end up here in 3D
	assert(false);
}

void AndroidGraphics3dManager::fillScreen(uint32 col) {
	// We should never end up here in 3D
	assert(false);
}

void AndroidGraphics3dManager::copyRectToScreen(const void *buf, int pitch,
        int x, int y, int w, int h) {
	// We should never end up here in 3D
	assert(false);
}

void AndroidGraphics3dManager::initSize(uint width, uint height,
                                        const Graphics::PixelFormat *format) {
	// resize game texture
	ENTER("%d, %d, %p", width, height, format);

	// We do only 3D with this manager and in 3D there is no format
	assert(format == nullptr);

	bool engineSupportsArbitraryResolutions = !g_engine ||
	        g_engine->hasFeature(Engine::kSupportsArbitraryResolutions);
	if (engineSupportsArbitraryResolutions) {
		width = JNI::egl_surface_width;
		height = JNI::egl_surface_height;
	}

	GLTHREADCHECK;

	_game_texture->allocBuffer(width, height);

	delete _frame_buffer;
	_frame_buffer = new OpenGL::FrameBuffer(_game_texture->getTextureName(),
	                                        _game_texture->width(), _game_texture->height(),
	                                        _game_texture->texWidth(), _game_texture->texHeight());
	_frame_buffer->attach();

	// Don't know mouse size yet - it gets reallocated in
	// setMouseCursor.  We need the palette allocated before
	// setMouseCursor however, so just take a guess at the desired
	// size (it's small).
	_mouse_texture_palette->allocBuffer(20, 20);

	updateScreenRect();

	clearScreen(kClear);

	_game_texture->setGameTexture();
}

int AndroidGraphics3dManager::getScreenChangeID() const {
	return _screenChangeID;
}

bool AndroidGraphics3dManager::showMouse(bool visible) {
	ENTER("%d", visible);

	_show_mouse = visible;

	return true;
}

void AndroidGraphics3dManager::warpMouse(int x, int y) {
	// x and y are in virtual coordinates
	ENTER("%d, %d", x, y);

	// Check active coordinate instead of screen coordinate to avoid warping
	// the mouse if it is still within the same virtual pixel
	// Don't take the risk of modifying _cursorX and _cursorY
	int cx = _cursorX, cy = _cursorY;
	const Common::Point currentMouse = convertScreenToVirtual(cx, cy);
	if (currentMouse.x == x && currentMouse.y == y) {
		// Same virtual coordinates: nothing to do
		return;
	}

	const Common::Point sMouse = convertVirtualToScreen(x, y);

	// Our internal mouse position is in screen coordinates
	// convertVirtualToScreen just clipped coordinates so we are safe
	setMousePosition(sMouse.x, sMouse.y);

	// Events pushed to Android system are in screen coordinates too
	// They are converted back by notifyMousePosition later
	Common::Event e;
	e.type = Common::EVENT_MOUSEMOVE;
	e.mouse = sMouse;

	dynamic_cast<OSystem_Android *>(g_system)->pushEvent(e);
}

void AndroidGraphics3dManager::updateCursorScaling() {
	// By default we use the unscaled versions.
	_mouse_hotspot_scaled = _mouse_hotspot;
	_mouse_width_scaled = _mouse_texture->width();
	_mouse_height_scaled = _mouse_texture->height();

	// In case scaling is actually enabled we will scale the cursor according
	// to the game screen.
	uint16 w = _game_texture->width();
	uint16 h = _game_texture->height();

	if (!_mouse_dont_scale && w && h) {
		const frac_t screen_scale_factor_x = intToFrac(_game_texture->getDrawRect().width()) / w;
		const frac_t screen_scale_factor_y = intToFrac(_game_texture->getDrawRect().height()) / h;

		_mouse_hotspot_scaled = Common::Point(
			fracToInt(_mouse_hotspot_scaled.x * screen_scale_factor_x),
			fracToInt(_mouse_hotspot_scaled.y * screen_scale_factor_y));

		_mouse_width_scaled  = fracToInt(_mouse_width_scaled * screen_scale_factor_x);
		_mouse_height_scaled = fracToInt(_mouse_height_scaled * screen_scale_factor_y);
	}
}

void AndroidGraphics3dManager::setMouseCursor(const void *buf, uint w, uint h,
        int hotspotX, int hotspotY,
        uint32 keycolor, bool dontScale,
        const Graphics::PixelFormat *format) {
	ENTER("%p, %u, %u, %d, %d, %u, %d, %p", buf, w, h, hotspotX, hotspotY,
	      keycolor, dontScale, format);

	GLTHREADCHECK;

#ifdef USE_RGB_COLOR
	if (format && format->bytesPerPixel > 1) {
		if (_mouse_texture != _mouse_texture_rgb) {
			LOGD("switching to rgb mouse cursor");

			assert(!_mouse_texture_rgb);
			if (JNI::egl_bits_per_pixel == 16) {
				_mouse_texture_rgb = new GLES5551Texture();
			} else {
				_mouse_texture_rgb = new GLES8888Texture();
			}
			_mouse_texture_rgb->setLinearFilter(_graphicsMode == 1);
		}

		_mouse_texture = _mouse_texture_rgb;
	} else {
		if (_mouse_texture != _mouse_texture_palette) {
			LOGD("switching to paletted mouse cursor");
		}

		_mouse_texture = _mouse_texture_palette;

		delete _mouse_texture_rgb;
		_mouse_texture_rgb = 0;
	}
#endif

	_mouse_texture->allocBuffer(w, h);

	if (_mouse_texture == _mouse_texture_palette) {
		assert(keycolor < 256);
		_mouse_texture->setKeycolor(keycolor);
	}

	if (w == 0 || h == 0) {
		return;
	}

	if (_mouse_texture == _mouse_texture_palette) {
		_mouse_texture->updateBuffer(0, 0, w, h, buf, w);
	} else {
		uint16 pitch = _mouse_texture->pitch();
		uint16 bpp = _mouse_texture->getPixelFormat().bytesPerPixel;

		byte *tmp = new byte[pitch * h];

		// meh, a n-bit cursor without alpha bits... this is so silly
		if (!crossBlit(tmp, (const byte *)buf, pitch, w * format->bytesPerPixel, w, h,
		               _mouse_texture->getPixelFormat(),
		               *format)) {
			LOGE("crossblit failed");

			delete[] tmp;

			_mouse_texture->allocBuffer(0, 0);

			return;
		}

		if (format->bytesPerPixel == 2) {
			const uint16 *s = (const uint16 *)buf;
			byte *d = tmp;
			for (uint16 y = 0; y < h; ++y, d += pitch / 2 - w)
				for (uint16 x = 0; x < w; ++x, d++)
					if (*s++ == (keycolor & 0xffff)) {
						memset(d, 0, bpp);
					}
		} else if (format->bytesPerPixel == 4) {
			const uint32 *s = (const uint32 *)buf;
			byte *d = tmp;
			for (uint16 y = 0; y < h; ++y, d += pitch / 2 - w)
				for (uint16 x = 0; x < w; ++x, d++)
					if (*s++ == (keycolor & 0xffffffff)) {
						memset(d, 0, bpp);
					}
		} else {
			error("AndroidGraphics3dManager::setMouseCursor: invalid bytesPerPixel %d", format->bytesPerPixel);
		}

		_mouse_texture->updateBuffer(0, 0, w, h, tmp, pitch);

		delete[] tmp;
	}

	_mouse_hotspot = Common::Point(hotspotX, hotspotY);
	_mouse_dont_scale = dontScale;

	updateCursorScaling();
}

void AndroidGraphics3dManager::setCursorPalette(const byte *colors,
        uint start, uint num) {
	ENTER("%p, %u, %u", colors, start, num);

	GLTHREADCHECK;

	if (!_mouse_texture->hasPalette()) {
		LOGD("switching to paletted mouse cursor");

		_mouse_texture = _mouse_texture_palette;

		delete _mouse_texture_rgb;
		_mouse_texture_rgb = 0;
	}

	_mouse_texture->setPalette(colors, start, num);
}

bool AndroidGraphics3dManager::lockMouse(bool lock) {
	_show_mouse = lock;
	return true;
}

#ifdef USE_RGB_COLOR
Graphics::PixelFormat AndroidGraphics3dManager::getScreenFormat() const {
	return _game_texture->getPixelFormat();
}

Common::List<Graphics::PixelFormat> AndroidGraphics3dManager::getSupportedFormats() const {
	Common::List<Graphics::PixelFormat> res;

	// empty list

	return res;
}
#endif

void AndroidGraphics3dManager::updateScreenRect() {
	Common::Rect rect(0, 0, JNI::egl_surface_width, JNI::egl_surface_height);

	_overlay_texture->setDrawRect(rect);

	// Clear the overlay background so it is not displayed distorted while resizing
	_overlay_background->release();

	uint16 w = _game_texture->width();
	uint16 h = _game_texture->height();

	if (w && h && _ar_correction) {

		float dpi[2];
		JNI::getDPI(dpi);

		float screen_ar;
		if (dpi[0] != 0.0 && dpi[1] != 0.0) {
			// horizontal orientation
			screen_ar = (dpi[1] * JNI::egl_surface_width) /
			            (dpi[0] * JNI::egl_surface_height);
		} else {
			screen_ar = float(JNI::egl_surface_width) / float(JNI::egl_surface_height);
		}

		float game_ar = float(w) / float(h);

		if (screen_ar > game_ar) {
			rect.setWidth(round(JNI::egl_surface_height * game_ar));
			rect.moveTo((JNI::egl_surface_width - rect.width()) / 2, 0);
		} else {
			rect.setHeight(round(JNI::egl_surface_width / game_ar));
			rect.moveTo((JNI::egl_surface_height - rect.height()) / 2, 0);
		}
	}

	_game_texture->setDrawRect(rect);

	updateCursorScaling();
}

const GLESBaseTexture *AndroidGraphics3dManager::getActiveTexture() const {
	if (_show_overlay) {
		return _overlay_texture;
	} else {
		return _game_texture;
	}
}

void AndroidGraphics3dManager::initOverlay() {
	// minimum of 320x200
	// (surface can get smaller when opening the virtual keyboard on *QVGA*)
	int overlay_width = MAX(JNI::egl_surface_width, 320);
	int overlay_height = MAX(JNI::egl_surface_height, 200);

	LOGI("overlay size is %ux%u", overlay_width, overlay_height);

	_overlay_texture->allocBuffer(overlay_width, overlay_height);
	_overlay_texture->setDrawRect(0, 0,
	                              JNI::egl_surface_width, JNI::egl_surface_height);
}

void AndroidGraphics3dManager::clearScreen(FixupType type, byte count) {
	assert(count > 0);

	bool sm = _show_mouse;
	_show_mouse = false;

	CONTEXT_SAVE_STATE(GL_SCISSOR_TEST);
	CONTEXT_SET_DISABLE(GL_SCISSOR_TEST);

	for (byte i = 0; i < count; ++i) {
		// clear screen
		GLCALL(glClearColor(0, 0, 0, 1 << 16));
		GLCALL(glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT));

		switch (type) {
		case kClear:
			break;

		case kClearSwap:
			JNI::swapBuffers();
			break;

		case kClearUpdate:
			_force_redraw = true;
			updateScreen();
			break;
		}
	}

	CONTEXT_RESET_DISABLE(GL_SCISSOR_TEST);

	_show_mouse = sm;
	_force_redraw = true;
}

float AndroidGraphics3dManager::getHiDPIScreenFactor() const {
	// TODO: Use JNI to get DisplayMetrics.density, which according to the documentation
	// seems to be what we want.
	// "On a medium-density screen, DisplayMetrics.density equals 1.0; on a high-density
	//  screen it equals 1.5; on an extra-high-density screen, it equals 2.0; and on a
	//  low-density screen, it equals 0.75. This figure is the factor by which you should
	//  multiply the dp units in order to get the actual pixel count for the current screen."

	return 2.f;
}

AndroidCommonGraphics::State AndroidGraphics3dManager::getState() const {
	AndroidCommonGraphics::State state;

	state.screenWidth   = getWidth();
	state.screenHeight  = getHeight();
	state.aspectRatio   = getFeatureState(OSystem::kFeatureAspectRatioCorrection);
	state.fullscreen    = getFeatureState(OSystem::kFeatureFullscreenMode);
	state.cursorPalette = getFeatureState(OSystem::kFeatureCursorPalette);
#ifdef USE_RGB_COLOR
	state.pixelFormat   = _2d_pixel_format;
#endif
	return state;
}

bool AndroidGraphics3dManager::setState(const AndroidCommonGraphics::State &state) {
	// In 3d we don't have a pixel format so we ignore it but store it for when leaving 3d mode
	initSize(state.screenWidth, state.screenHeight, nullptr);
#ifdef USE_RGB_COLOR
	_2d_pixel_format = state.pixelFormat;
#endif
	setFeatureState(OSystem::kFeatureAspectRatioCorrection, state.aspectRatio);
	setFeatureState(OSystem::kFeatureFullscreenMode, state.fullscreen);
	setFeatureState(OSystem::kFeatureCursorPalette, state.cursorPalette);

	return true;
}

void AndroidGraphics3dManager::touchControlNotifyChanged() {
	// Make sure we redraw the screen
	_force_redraw = true;
}

void AndroidGraphics3dManager::touchControlDraw(int16 x, int16 y, int16 w, int16 h, const Common::Rect &clip) {
	_touchcontrols_texture->drawTexture(x, y, w, h, clip);
}