/* 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 2
 * 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, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *
 * $URL$
 * $Id$
 *
 */

#if defined(__ANDROID__)

#include "backends/base-backend.h"
#include "base/main.h"
#include "graphics/surface.h"

#include "backends/platform/android/android.h"
#include "backends/platform/android/video.h"

#include <jni.h>

#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/time.h>
#include <time.h>

#include "common/archive.h"
#include "common/util.h"
#include "common/rect.h"
#include "common/queue.h"
#include "common/mutex.h"
#include "common/events.h"
#include "common/config-manager.h"

#include "backends/fs/posix/posix-fs-factory.h"
#include "backends/keymapper/keymapper.h"
#include "backends/saves/default/default-saves.h"
#include "backends/timer/default/default-timer.h"
#include "backends/plugins/posix/posix-provider.h"
#include "audio/mixer_intern.h"

#include "backends/platform/android/asset-archive.h"

const char *android_log_tag = "ScummVM";

// This replaces the bionic libc assert functions with something that
// actually prints the assertion failure before aborting.
extern "C" {
	void __assert(const char *file, int line, const char *expr) {
		__android_log_assert(expr, android_log_tag,
								"Assertion failure: '%s' in %s:%d",
								 expr, file, line);
	}

	void __assert2(const char *file, int line, const char *func, const char *expr) {
		__android_log_assert(expr, android_log_tag,
								"Assertion failure: '%s' in %s:%d (%s)",
								 expr, file, line, func);
	}
}

#ifdef ANDROID_DEBUG_GL
static const char *getGlErrStr(GLenum error) {
	switch (error) {
	case GL_INVALID_ENUM:
		return "GL_INVALID_ENUM";
	case GL_INVALID_VALUE:
		return "GL_INVALID_VALUE";
	case GL_INVALID_OPERATION:
		return "GL_INVALID_OPERATION";
	case GL_STACK_OVERFLOW:
		return "GL_STACK_OVERFLOW";
	case GL_STACK_UNDERFLOW:
		return "GL_STACK_UNDERFLOW";
	case GL_OUT_OF_MEMORY:
		return "GL_OUT_OF_MEMORY";
	}

	static char buf[40];
	snprintf(buf, sizeof(buf), "(Unknown GL error code 0x%x)", error);

	return buf;
}

void checkGlError(const char *expr, const char *file, int line) {
	GLenum error = glGetError();

	if (error != GL_NO_ERROR)
		LOGE("GL ERROR: %s on %s (%s:%d)", getGlErrStr(error), expr, file, line);
}
#endif

static JavaVM *cached_jvm;
static jfieldID FID_Event_type;
static jfieldID FID_Event_synthetic;
static jfieldID FID_Event_kbd_keycode;
static jfieldID FID_Event_kbd_ascii;
static jfieldID FID_Event_kbd_flags;
static jfieldID FID_Event_mouse_x;
static jfieldID FID_Event_mouse_y;
static jfieldID FID_Event_mouse_relative;
static jfieldID FID_ScummVM_nativeScummVM;
static jmethodID MID_Object_wait;

JNIEnv *JNU_GetEnv() {
	JNIEnv *env = 0;

	jint res = cached_jvm->GetEnv((void **)&env, JNI_VERSION_1_2);

	if (res != JNI_OK) {
		LOGE("GetEnv() failed: %d", res);
		abort();
	}

	return env;
}

static void JNU_ThrowByName(JNIEnv *env, const char *name, const char *msg) {
	jclass cls = env->FindClass(name);

	// if cls is 0, an exception has already been thrown
	if (cls != 0)
		env->ThrowNew(cls, msg);

	env->DeleteLocalRef(cls);
}

// floating point. use sparingly
template <class T>
static inline T scalef(T in, float numerator, float denominator) {
	return static_cast<float>(in) * numerator / denominator;
}

static inline GLfixed xdiv(int numerator, int denominator) {
	assert(numerator < (1 << 16));
	return (numerator << 16) / denominator;
}

#ifdef DYNAMIC_MODULES
class AndroidPluginProvider : public POSIXPluginProvider {
protected:
	virtual void addCustomDirectories(Common::FSList &dirs) const;
};
#endif

class OSystem_Android : public BaseBackend, public PaletteManager {
private:
	// back pointer to (java) peer instance
	jobject _back_ptr;

	jmethodID MID_displayMessageOnOSD;
	jmethodID MID_setWindowCaption;
	jmethodID MID_initBackend;
	jmethodID MID_audioSampleRate;
	jmethodID MID_showVirtualKeyboard;
	jmethodID MID_getSysArchives;
	jmethodID MID_getPluginDirectories;
	jmethodID MID_setupScummVMSurface;
	jmethodID MID_destroyScummVMSurface;
	jmethodID MID_swapBuffers;

	int _screen_changeid;
	int _egl_surface_width;
	int _egl_surface_height;

	bool _force_redraw;

	// Game layer
	GLESPaletteTexture *_game_texture;
	int _shake_offset;
	Common::Rect _focus_rect;

	// Overlay layer
	GLES4444Texture *_overlay_texture;
	bool _show_overlay;

	// Mouse layer
	GLESPaletteATexture *_mouse_texture;
	Common::Point _mouse_hotspot;
	int _mouse_targetscale;
	bool _show_mouse;
	bool _use_mouse_palette;

	Common::Queue<Common::Event> _event_queue;
	MutexRef _event_queue_lock;

	bool _timer_thread_exit;
	pthread_t _timer_thread;
	static void *timerThreadFunc(void *arg);

	bool _enable_zoning;
	bool _virtkeybd_on;

	Common::SaveFileManager *_savefile;
	Audio::MixerImpl *_mixer;
	Common::TimerManager *_timer;
	FilesystemFactory *_fsFactory;
	Common::Archive *_asset_archive;
	timeval _startTime;

	void setupScummVMSurface();
	void destroyScummVMSurface();
	void setupKeymapper();
	void _setCursorPalette(const byte *colors, uint start, uint num);

public:
	OSystem_Android(jobject am);
	virtual ~OSystem_Android();
	bool initJavaHooks(JNIEnv *env, jobject self);

	static OSystem_Android *fromJavaObject(JNIEnv *env, jobject obj);
	virtual void initBackend();
	void addPluginDirectories(Common::FSList &dirs) const;
	void enableZoning(bool enable) { _enable_zoning = enable; }
	void setSurfaceSize(int width, int height) {
		_egl_surface_width = width;
		_egl_surface_height = height;
	}

	virtual bool hasFeature(Feature f);
	virtual void setFeatureState(Feature f, bool enable);
	virtual bool getFeatureState(Feature f);
	virtual const GraphicsMode *getSupportedGraphicsModes() const;
	virtual int getDefaultGraphicsMode() const;
	bool setGraphicsMode(const char *name);
	virtual bool setGraphicsMode(int mode);
	virtual int getGraphicsMode() const;
	virtual void initSize(uint width, uint height,
							const Graphics::PixelFormat *format);

	virtual int getScreenChangeID() const {
		return _screen_changeid;
	}

	virtual int16 getHeight();
	virtual int16 getWidth();

	virtual PaletteManager *getPaletteManager() {
		return this;
	}

protected:
	// PaletteManager API
	virtual void setPalette(const byte *colors, uint start, uint num);
	virtual void grabPalette(byte *colors, uint start, uint num);

public:
	virtual void copyRectToScreen(const byte *buf, int pitch, int x, int y, int w, int h);
	virtual void updateScreen();
	virtual Graphics::Surface *lockScreen();
	virtual void unlockScreen();
	virtual void setShakePos(int shakeOffset);
	virtual void fillScreen(uint32 col);
	virtual void setFocusRectangle(const Common::Rect& rect);
	virtual void clearFocusRectangle();

	virtual void showOverlay();
	virtual void hideOverlay();
	virtual void clearOverlay();
	virtual void grabOverlay(OverlayColor *buf, int pitch);
	virtual void copyRectToOverlay(const OverlayColor *buf, int pitch, int x, int y, int w, int h);
	virtual int16 getOverlayHeight();
	virtual int16 getOverlayWidth();

	// RGBA 4444
	virtual Graphics::PixelFormat getOverlayFormat() const {
		Graphics::PixelFormat format;

		format.bytesPerPixel = 2;
		format.rLoss = 8 - 4;
		format.gLoss = 8 - 4;
		format.bLoss = 8 - 4;
		format.aLoss = 8 - 4;
		format.rShift = 3 * 4;
		format.gShift = 2 * 4;
		format.bShift = 1 * 4;
		format.aShift = 0 * 4;

		return format;
	}

	virtual bool showMouse(bool visible);

	virtual void warpMouse(int x, int y);
	virtual void setMouseCursor(const byte *buf, uint w, uint h, int hotspotX, int hotspotY, uint32 keycolor, int cursorTargetScale, const Graphics::PixelFormat *format);
	virtual void setCursorPalette(const byte *colors, uint start, uint num);
	virtual void disableCursorPalette(bool disable);

	virtual bool pollEvent(Common::Event &event);
	void pushEvent(const Common::Event& event);
	virtual uint32 getMillis();
	virtual void delayMillis(uint msecs);

	virtual MutexRef createMutex(void);
	virtual void lockMutex(MutexRef mutex);
	virtual void unlockMutex(MutexRef mutex);
	virtual void deleteMutex(MutexRef mutex);

	virtual void quit();

	virtual void setWindowCaption(const char *caption);
	virtual void displayMessageOnOSD(const char *msg);
	virtual void showVirtualKeyboard(bool enable);

	virtual Common::SaveFileManager *getSavefileManager();
	virtual Audio::Mixer *getMixer();
	virtual void getTimeAndDate(TimeDate &t) const;
	virtual Common::TimerManager *getTimerManager();
	virtual FilesystemFactory *getFilesystemFactory();
	virtual void logMessage(LogMessageType::Type type, const char *message);
	virtual void addSysArchivesToSearchSet(Common::SearchSet &s, int priority = 0);
};

OSystem_Android::OSystem_Android(jobject am) :
	_back_ptr(0),
	_screen_changeid(0),
	_force_redraw(false),
	_game_texture(0),
	_overlay_texture(0),
	_mouse_texture(0),
	_use_mouse_palette(false),
	_show_mouse(false),
	_show_overlay(false),
	_enable_zoning(false),
	_savefile(0),
	_mixer(0),
	_timer(0),
	_fsFactory(new POSIXFilesystemFactory()),
	_asset_archive(new AndroidAssetArchive(am)),
	_shake_offset(0),
	_event_queue_lock(createMutex()) {
}

OSystem_Android::~OSystem_Android() {
	ENTER();

	delete _game_texture;
	delete _overlay_texture;
	delete _mouse_texture;

	destroyScummVMSurface();

	JNIEnv *env = JNU_GetEnv();
	//env->DeleteWeakGlobalRef(_back_ptr);
	env->DeleteGlobalRef(_back_ptr);

	delete _savefile;
	delete _mixer;
	delete _timer;
	delete _fsFactory;
	delete _asset_archive;

	deleteMutex(_event_queue_lock);
}

OSystem_Android *OSystem_Android::fromJavaObject(JNIEnv *env, jobject obj) {
	jlong peer = env->GetLongField(obj, FID_ScummVM_nativeScummVM);
	return (OSystem_Android *)peer;
}

bool OSystem_Android::initJavaHooks(JNIEnv *env, jobject self) {
	// weak global ref to allow class to be unloaded
	// ... except dalvik doesn't implement NewWeakGlobalRef (yet)
	//_back_ptr = env->NewWeakGlobalRef(self);
	_back_ptr = env->NewGlobalRef(self);

	jclass cls = env->GetObjectClass(_back_ptr);

#define FIND_METHOD(name, signature) do {						\
		MID_ ## name = env->GetMethodID(cls, #name, signature); \
		if (MID_ ## name == 0)									\
			return false;										\
	} while (0)

	FIND_METHOD(setWindowCaption, "(Ljava/lang/String;)V");
	FIND_METHOD(displayMessageOnOSD, "(Ljava/lang/String;)V");
	FIND_METHOD(initBackend, "()V");
	FIND_METHOD(audioSampleRate, "()I");
	FIND_METHOD(showVirtualKeyboard, "(Z)V");
	FIND_METHOD(getSysArchives, "()[Ljava/lang/String;");
	FIND_METHOD(getPluginDirectories, "()[Ljava/lang/String;");
	FIND_METHOD(setupScummVMSurface, "()V");
	FIND_METHOD(destroyScummVMSurface, "()V");
	FIND_METHOD(swapBuffers, "()Z");

#undef FIND_METHOD

	return true;
}

static void ScummVM_create(JNIEnv *env, jobject self, jobject am) {
	OSystem_Android *cpp_obj = new OSystem_Android(am);

	// Exception already thrown by initJavaHooks?
	if (!cpp_obj->initJavaHooks(env, self))
		return;

	env->SetLongField(self, FID_ScummVM_nativeScummVM, (jlong)cpp_obj);

#ifdef DYNAMIC_MODULES
	PluginManager::instance().addPluginProvider(new AndroidPluginProvider());
#endif
}

static void ScummVM_nativeDestroy(JNIEnv *env, jobject self) {
	OSystem_Android *cpp_obj = OSystem_Android::fromJavaObject(env, self);
	delete cpp_obj;
}

static void ScummVM_audioMixCallback(JNIEnv *env, jobject self,
										jbyteArray jbuf) {
	OSystem_Android *cpp_obj = OSystem_Android::fromJavaObject(env, self);
	jsize len = env->GetArrayLength(jbuf);
	jbyte *buf = env->GetByteArrayElements(jbuf, 0);

	if (buf == 0) {
		warning("Unable to get Java audio byte array. Skipping");
		return;
	}

	Audio::MixerImpl *mixer =
		static_cast<Audio::MixerImpl *>(cpp_obj->getMixer());
	assert(mixer);
	mixer->mixCallback(reinterpret_cast<byte *>(buf), len);

	env->ReleaseByteArrayElements(jbuf, buf, 0);
}

static void ScummVM_setConfManInt(JNIEnv *env, jclass cls,
									jstring key_obj, jint value) {
	ENTER("%p, %d", key_obj, (int)value);

	const char *key = env->GetStringUTFChars(key_obj, 0);

	if (key == 0)
		return;

	ConfMan.setInt(key, value);

	env->ReleaseStringUTFChars(key_obj, key);
}

static void ScummVM_setConfManString(JNIEnv *env, jclass cls, jstring key_obj,
										jstring value_obj) {
	ENTER("%p, %p", key_obj, value_obj);

	const char *key = env->GetStringUTFChars(key_obj, 0);

	if (key == 0)
		return;

	const char *value = env->GetStringUTFChars(value_obj, 0);

	if (value == 0) {
		env->ReleaseStringUTFChars(key_obj, key);
		return;
	}

	ConfMan.set(key, value);

	env->ReleaseStringUTFChars(value_obj, value);
	env->ReleaseStringUTFChars(key_obj, key);
}

void *OSystem_Android::timerThreadFunc(void *arg) {
	OSystem_Android *system = (OSystem_Android *)arg;
	DefaultTimerManager *timer = (DefaultTimerManager *)(system->_timer);

	JNIEnv *env = 0;
	jint res = cached_jvm->AttachCurrentThread(&env, 0);

	if (res != JNI_OK) {
		LOGE("AttachCurrentThread() failed: %d", res);
		abort();
	}

	struct timespec tv;
	tv.tv_sec = 0;
	tv.tv_nsec = 100 * 1000 * 1000;	// 100ms

	while (!system->_timer_thread_exit) {
		timer->handler();
		nanosleep(&tv, 0);
	}

	res = cached_jvm->DetachCurrentThread();

	if (res != JNI_OK) {
		LOGE("DetachCurrentThread() failed: %d", res);
		abort();
	}

	return 0;
}

void OSystem_Android::initBackend() {
	ENTER();

	JNIEnv *env = JNU_GetEnv();

	ConfMan.setInt("autosave_period", 0);
	ConfMan.setInt("FM_medium_quality", true);

	// must happen before creating TimerManager to avoid race in
	// creating EventManager
	setupKeymapper();

	// BUG: "transient" ConfMan settings get nuked by the options
	// screen. Passing the savepath in this way makes it stick
	// (via ConfMan.registerDefault)
	_savefile = new DefaultSaveFileManager(ConfMan.get("savepath"));
	_timer = new DefaultTimerManager();

	gettimeofday(&_startTime, 0);

	jint sample_rate = env->CallIntMethod(_back_ptr, MID_audioSampleRate);
	if (env->ExceptionCheck()) {
		warning("Error finding audio sample rate - assuming 11025HZ");

		env->ExceptionDescribe();
		env->ExceptionClear();

		sample_rate = 11025;
	}

	_mixer = new Audio::MixerImpl(this, sample_rate);
	_mixer->setReady(true);

	env->CallVoidMethod(_back_ptr, MID_initBackend);

	if (env->ExceptionCheck()) {
		error("Error in Java initBackend");

		env->ExceptionDescribe();
		env->ExceptionClear();
	}

	_timer_thread_exit = false;
	pthread_create(&_timer_thread, 0, timerThreadFunc, this);

	OSystem::initBackend();

	setupScummVMSurface();
}

void OSystem_Android::addPluginDirectories(Common::FSList &dirs) const {
	ENTER();

	JNIEnv *env = JNU_GetEnv();

	jobjectArray array =
		(jobjectArray)env->CallObjectMethod(_back_ptr, MID_getPluginDirectories);
	if (env->ExceptionCheck()) {
		warning("Error finding plugin directories");

		env->ExceptionDescribe();
		env->ExceptionClear();

		return;
	}

	jsize size = env->GetArrayLength(array);
	for (jsize i = 0; i < size; ++i) {
		jstring path_obj = (jstring)env->GetObjectArrayElement(array, i);

		if (path_obj == 0)
			continue;

		const char *path = env->GetStringUTFChars(path_obj, 0);
		if (path == 0) {
			warning("Error getting string characters from plugin directory");

			env->ExceptionClear();
			env->DeleteLocalRef(path_obj);

			continue;
		}

		dirs.push_back(Common::FSNode(path));

		env->ReleaseStringUTFChars(path_obj, path);
		env->DeleteLocalRef(path_obj);
	}
}

bool OSystem_Android::hasFeature(Feature f) {
	return (f == kFeatureCursorHasPalette ||
			f == kFeatureVirtualKeyboard ||
			f == kFeatureOverlaySupportsAlpha);
}

void OSystem_Android::setFeatureState(Feature f, bool enable) {
	ENTER("%d, %d", f, enable);

	switch (f) {
	case kFeatureVirtualKeyboard:
		_virtkeybd_on = enable;
		showVirtualKeyboard(enable);
		break;
	default:
		break;
	}
}

bool OSystem_Android::getFeatureState(Feature f) {
	switch (f) {
	case kFeatureVirtualKeyboard:
		return _virtkeybd_on;
	default:
		return false;
	}
}

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

	return s_supportedGraphicsModes;
}


int OSystem_Android::getDefaultGraphicsMode() const {
	return 1;
}

bool OSystem_Android::setGraphicsMode(const char *mode) {
	ENTER("%s", mode);
	return true;
}

bool OSystem_Android::setGraphicsMode(int mode) {
	ENTER("%d", mode);
	return true;
}

int OSystem_Android::getGraphicsMode() const {
	return 1;
}

void OSystem_Android::setupScummVMSurface() {
	ENTER();

	JNIEnv *env = JNU_GetEnv();
	env->CallVoidMethod(_back_ptr, MID_setupScummVMSurface);

	if (env->ExceptionCheck())
		return;

	// EGL set up with a new surface.  Initialise OpenGLES context.
	GLESTexture::initGLExtensions();

	// Turn off anything that looks like 3D ;)
	GLCALL(glDisable(GL_CULL_FACE));
	GLCALL(glDisable(GL_DEPTH_TEST));
	GLCALL(glDisable(GL_LIGHTING));
	GLCALL(glDisable(GL_FOG));
	GLCALL(glDisable(GL_DITHER));

	GLCALL(glShadeModel(GL_FLAT));
	GLCALL(glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_FASTEST));

	GLCALL(glEnable(GL_BLEND));
	GLCALL(glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA));

	GLCALL(glEnableClientState(GL_VERTEX_ARRAY));
	GLCALL(glEnableClientState(GL_TEXTURE_COORD_ARRAY));

	GLCALL(glEnable(GL_TEXTURE_2D));

	if (!_game_texture)
		_game_texture = new GLESPaletteTexture();
	else
		_game_texture->reinitGL();

	if (!_overlay_texture)
		_overlay_texture = new GLES4444Texture();
	else
		_overlay_texture->reinitGL();

	if (!_mouse_texture)
		_mouse_texture = new GLESPaletteATexture();
	else
		_mouse_texture->reinitGL();

	GLCALL(glViewport(0, 0, _egl_surface_width, _egl_surface_height));

	GLCALL(glMatrixMode(GL_PROJECTION));
	GLCALL(glLoadIdentity());
	GLCALL(glOrthof(0, _egl_surface_width, _egl_surface_height, 0, -1, 1));
	GLCALL(glMatrixMode(GL_MODELVIEW));
	GLCALL(glLoadIdentity());

	clearFocusRectangle();
}

void OSystem_Android::destroyScummVMSurface() {
	JNIEnv *env = JNU_GetEnv();
	env->CallVoidMethod(_back_ptr, MID_destroyScummVMSurface);
	// Can't use OpenGLES functions after this
}

void OSystem_Android::initSize(uint width, uint height,
								const Graphics::PixelFormat *format) {
	ENTER("%d, %d, %p", width, height, format);

	_game_texture->allocBuffer(width, height);

	// Cap at 320x200 or the ScummVM themes abort :/
	GLuint overlay_width = MIN(_egl_surface_width, 320);
	GLuint overlay_height = MIN(_egl_surface_height, 200);
	_overlay_texture->allocBuffer(overlay_width, overlay_height);

	// 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->allocBuffer(20, 20);
}

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

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

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

	if (!_use_mouse_palette)
		_setCursorPalette(colors, start, num);

	memcpy(_game_texture->palette() + start * 3, colors, num * 3);
}

void OSystem_Android::grabPalette(byte *colors, uint start, uint num) {
	ENTER("%p, %u, %u", colors, start, num);
	memcpy(colors, _game_texture->palette_const() + start * 3, num * 3);
}

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

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

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

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

	_force_redraw = false;

	GLCALL(glPushMatrix());

	if (_shake_offset != 0 ||
			(!_focus_rect.isEmpty() &&
			!Common::Rect(_game_texture->width(),
							_game_texture->height()).contains(_focus_rect))) {
		// These are the only cases where _game_texture doesn't
		// cover the entire screen.
		GLCALL(glClearColorx(0, 0, 0, 1 << 16));
		GLCALL(glClear(GL_COLOR_BUFFER_BIT));

		// Move everything up by _shake_offset (game) pixels
		GLCALL(glTranslatex(0, -_shake_offset << 16, 0));
	}

	if (_focus_rect.isEmpty()) {
		_game_texture->drawTexture(0, 0,
									_egl_surface_width, _egl_surface_height);
	} else {
		GLCALL(glPushMatrix());
		GLCALL(glScalex(xdiv(_egl_surface_width, _focus_rect.width()),
						xdiv(_egl_surface_height, _focus_rect.height()),
						1 << 16));
		GLCALL(glTranslatex(-_focus_rect.left << 16,
							-_focus_rect.top << 16, 0));
		GLCALL(glScalex(xdiv(_game_texture->width(), _egl_surface_width),
						xdiv(_game_texture->height(), _egl_surface_height),
						1 << 16));

		_game_texture->drawTexture(0, 0,
									_egl_surface_width, _egl_surface_height);
		GLCALL(glPopMatrix());
	}

	int cs = _mouse_targetscale;

	if (_show_overlay) {
		// ugly, but the modern theme sets a wacko factor, only god knows why
		cs = 1;

		GLCALL(_overlay_texture->drawTexture(0, 0,
												_egl_surface_width,
												_egl_surface_height));
	}

	if (_show_mouse) {
		GLCALL(glPushMatrix());

		// Scale up ScummVM -> OpenGL (pixel) coordinates
		int texwidth, texheight;

		if (_show_overlay) {
			texwidth = getOverlayWidth();
			texheight = getOverlayHeight();
		} else {
			texwidth = getWidth();
			texheight = getHeight();
		}

		GLCALL(glScalex(xdiv(_egl_surface_width, texwidth),
						xdiv(_egl_surface_height, texheight),
						1 << 16));

		GLCALL(glTranslatex((-_mouse_hotspot.x * cs) << 16,
							(-_mouse_hotspot.y * cs) << 16,
							0));

		// Note the extra half texel to position the mouse in
		// the middle of the x,y square:
		const Common::Point& mouse = getEventManager()->getMousePos();
		GLCALL(glTranslatex((mouse.x << 16) | 1 << 15,
							(mouse.y << 16) | 1 << 15, 0));

		GLCALL(glScalex(cs << 16, cs << 16, 1 << 16));

		_mouse_texture->drawTexture();

		GLCALL(glPopMatrix());
	}

	GLCALL(glPopMatrix());

	JNIEnv *env = JNU_GetEnv();
	if (!env->CallBooleanMethod(_back_ptr, MID_swapBuffers)) {
		// Context lost -> need to reinit GL
		destroyScummVMSurface();
		setupScummVMSurface();
	}
}

Graphics::Surface *OSystem_Android::lockScreen() {
	ENTER();

	Graphics::Surface *surface = _game_texture->surface();
	assert(surface->pixels);

	return surface;
}

void OSystem_Android::unlockScreen() {
	ENTER();

	assert(_game_texture->dirty());
}

void OSystem_Android::setShakePos(int shake_offset) {
	ENTER("%d", shake_offset);

	if (_shake_offset != shake_offset) {
		_shake_offset = shake_offset;
		_force_redraw = true;
	}
}

void OSystem_Android::fillScreen(uint32 col) {
	ENTER("%u", col);

	assert(col < 256);
	_game_texture->fillBuffer(col);
}

void OSystem_Android::setFocusRectangle(const Common::Rect& rect) {
	ENTER("%d, %d, %d, %d", rect.left, rect.top, rect.right, rect.bottom);

	if (_enable_zoning) {
		_focus_rect = rect;
		_force_redraw = true;
	}
}

void OSystem_Android::clearFocusRectangle() {
	ENTER();

	if (_enable_zoning) {
		_focus_rect = Common::Rect();
		_force_redraw = true;
	}
}

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

	_show_overlay = true;
	_force_redraw = true;
}

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

	_show_overlay = false;
	_force_redraw = true;
}

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

	_overlay_texture->fillBuffer(0);

	// Shouldn't need this, but works around a 'blank screen' bug on Nexus1
	updateScreen();
}

void OSystem_Android::grabOverlay(OverlayColor *buf, int pitch) {
	ENTER("%p, %d", buf, pitch);

	// We support overlay alpha blending, so the pixel data here
	// shouldn't actually be used.	Let's fill it with zeros, I'm sure
	// it will be fine...
	const Graphics::Surface *surface = _overlay_texture->surface_const();
	assert(surface->bytesPerPixel == sizeof(buf[0]));

	int h = surface->h;

	do {
		memset(buf, 0, surface->w * sizeof(buf[0]));

		// This 'pitch' is pixels not bytes
		buf += pitch;
	} while (--h);
}

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

	const Graphics::Surface *surface = _overlay_texture->surface_const();
	assert(surface->bytesPerPixel == sizeof(buf[0]));

	// This 'pitch' is pixels not bytes
	_overlay_texture->updateBuffer(x, y, w, h, buf, pitch * sizeof(buf[0]));

	// Shouldn't need this, but works around a 'blank screen' bug on Nexus1?
	updateScreen();
}

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

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

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

	_show_mouse = visible;

	return true;
}

void OSystem_Android::warpMouse(int x, int y) {
	ENTER("%d, %d", x, y);

	// We use only the eventmanager's idea of the current mouse
	// position, so there is nothing extra to do here.
}

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

	assert(keycolor < 256);

	_mouse_texture->allocBuffer(w, h);

	// Update palette alpha based on keycolor
	byte *palette = _mouse_texture->palette();
	int i = 256;

	do {
		palette[3] = 0xff;
		palette += 4;
	} while (--i);

	palette = _mouse_texture->palette();
	palette[keycolor * 4 + 3] = 0x00;

	_mouse_texture->updateBuffer(0, 0, w, h, buf, w);

	_mouse_hotspot = Common::Point(hotspotX, hotspotY);
	_mouse_targetscale = cursorTargetScale;
}

void OSystem_Android::_setCursorPalette(const byte *colors,
										uint start, uint num) {
	byte *palette = _mouse_texture->palette() + start * 4;

	do {
		for (int i = 0; i < 3; ++i)
			palette[i] = colors[i];

		// Leave alpha untouched to preserve keycolor

		palette += 4;
		colors += 3;
	} while (--num);
}

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

	_setCursorPalette(colors, start, num);
	_use_mouse_palette = true;
}

void OSystem_Android::disableCursorPalette(bool disable) {
	ENTER("%d", disable);

	_use_mouse_palette = !disable;
}

void OSystem_Android::setupKeymapper() {
#ifdef ENABLE_KEYMAPPER
	using namespace Common;

	Keymapper *mapper = getEventManager()->getKeymapper();

	HardwareKeySet *keySet = new HardwareKeySet();

	keySet->addHardwareKey(
		new HardwareKey("n", KeyState(KEYCODE_n), "n (vk)",
				kTriggerLeftKeyType,
				kVirtualKeyboardActionType));

	mapper->registerHardwareKeySet(keySet);

	Keymap *globalMap = new Keymap("global");
	Action *act;

	act = new Action(globalMap, "VIRT", "Display keyboard",
						kVirtualKeyboardActionType);
	act->addKeyEvent(KeyState(KEYCODE_F7, ASCII_F7, 0));

	mapper->addGlobalKeymap(globalMap);

	mapper->pushKeymap("global");
#endif
}

bool OSystem_Android::pollEvent(Common::Event &event) {
	//ENTER();

	lockMutex(_event_queue_lock);

	if (_event_queue.empty()) {
		unlockMutex(_event_queue_lock);
		return false;
	}

	event = _event_queue.pop();
	unlockMutex(_event_queue_lock);

	switch (event.type) {
	case Common::EVENT_MOUSEMOVE:
		// TODO: only dirty/redraw move bounds
		_force_redraw = true;
		// fallthrough
	case Common::EVENT_LBUTTONDOWN:
	case Common::EVENT_LBUTTONUP:
	case Common::EVENT_RBUTTONDOWN:
	case Common::EVENT_RBUTTONUP:
	case Common::EVENT_WHEELUP:
	case Common::EVENT_WHEELDOWN:
	case Common::EVENT_MBUTTONDOWN:
	case Common::EVENT_MBUTTONUP: {
		// relative mouse hack
		if (event.kbd.flags == 1) {
			// Relative (trackball) mouse hack.
			const Common::Point& mouse_pos =
				getEventManager()->getMousePos();
			event.mouse.x += mouse_pos.x;
			event.mouse.y += mouse_pos.y;
			event.mouse.x = CLIP(event.mouse.x, (int16)0, _show_overlay ?
									getOverlayWidth() : getWidth());
			event.mouse.y = CLIP(event.mouse.y, (int16)0, _show_overlay ?
									getOverlayHeight() : getHeight());
		} else {
			// Touchscreen events need to be converted
			// from device to game coords first.
			const GLESTexture *tex = _show_overlay
				? static_cast<GLESTexture *>(_overlay_texture)
				: static_cast<GLESTexture *>(_game_texture);
			event.mouse.x = scalef(event.mouse.x, tex->width(),
									_egl_surface_width);
			event.mouse.y = scalef(event.mouse.y, tex->height(),
									_egl_surface_height);
			event.mouse.x -= _shake_offset;
		}
		break;
	}
	case Common::EVENT_SCREEN_CHANGED:
		debug("EVENT_SCREEN_CHANGED");
		_screen_changeid++;
		destroyScummVMSurface();
		setupScummVMSurface();
		break;
	default:
		break;
	}

	return true;
}

void OSystem_Android::pushEvent(const Common::Event& event) {
	lockMutex(_event_queue_lock);

	// Try to combine multiple queued mouse move events
	if (event.type == Common::EVENT_MOUSEMOVE &&
			!_event_queue.empty() &&
			_event_queue.back().type == Common::EVENT_MOUSEMOVE) {
		Common::Event tail = _event_queue.back();
		if (event.kbd.flags) {
			// relative movement hack
			tail.mouse.x += event.mouse.x;
			tail.mouse.y += event.mouse.y;
		} else {
			// absolute position, clear relative flag
			tail.kbd.flags = 0;
			tail.mouse.x = event.mouse.x;
			tail.mouse.y = event.mouse.y;
		}
	} else {
	  _event_queue.push(event);
	}

	unlockMutex(_event_queue_lock);
}

static void ScummVM_pushEvent(JNIEnv *env, jobject self, jobject java_event) {
	OSystem_Android *cpp_obj = OSystem_Android::fromJavaObject(env, self);

	Common::Event event;
	event.type = (Common::EventType)env->GetIntField(java_event,
														FID_Event_type);

	event.synthetic =
		env->GetBooleanField(java_event, FID_Event_synthetic);

	switch (event.type) {
	case Common::EVENT_KEYDOWN:
	case Common::EVENT_KEYUP:
		event.kbd.keycode = (Common::KeyCode)env->GetIntField(
			java_event, FID_Event_kbd_keycode);
		event.kbd.ascii = static_cast<int>(env->GetIntField(
			java_event, FID_Event_kbd_ascii));
		event.kbd.flags = static_cast<int>(env->GetIntField(
			java_event, FID_Event_kbd_flags));
		break;
	case Common::EVENT_MOUSEMOVE:
	case Common::EVENT_LBUTTONDOWN:
	case Common::EVENT_LBUTTONUP:
	case Common::EVENT_RBUTTONDOWN:
	case Common::EVENT_RBUTTONUP:
	case Common::EVENT_WHEELUP:
	case Common::EVENT_WHEELDOWN:
	case Common::EVENT_MBUTTONDOWN:
	case Common::EVENT_MBUTTONUP:
		event.mouse.x =
			env->GetIntField(java_event, FID_Event_mouse_x);
		event.mouse.y =
			env->GetIntField(java_event, FID_Event_mouse_y);
		// This is a terrible hack.	 We stash "relativeness"
		// in the kbd.flags field until pollEvent() can work
		// it out.
		event.kbd.flags = env->GetBooleanField(
			java_event, FID_Event_mouse_relative) ? 1 : 0;
		break;
	default:
		break;
	}

	cpp_obj->pushEvent(event);
}

uint32 OSystem_Android::getMillis() {
	timeval curTime;

	gettimeofday(&curTime, 0);

	return (uint32)(((curTime.tv_sec - _startTime.tv_sec) * 1000) + \
			((curTime.tv_usec - _startTime.tv_usec) / 1000));
}

void OSystem_Android::delayMillis(uint msecs) {
	usleep(msecs * 1000);
}

OSystem::MutexRef OSystem_Android::createMutex() {
	pthread_mutexattr_t attr;
	pthread_mutexattr_init(&attr);
	pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);

	pthread_mutex_t *mutex = new pthread_mutex_t;

	if (pthread_mutex_init(mutex, &attr) != 0) {
		warning("pthread_mutex_init() failed");

		delete mutex;

		return 0;
	}

	return (MutexRef)mutex;
}

void OSystem_Android::lockMutex(MutexRef mutex) {
	if (pthread_mutex_lock((pthread_mutex_t *)mutex) != 0)
		warning("pthread_mutex_lock() failed");
}

void OSystem_Android::unlockMutex(MutexRef mutex) {
	if (pthread_mutex_unlock((pthread_mutex_t *)mutex) != 0)
		warning("pthread_mutex_unlock() failed");
}

void OSystem_Android::deleteMutex(MutexRef mutex) {
	pthread_mutex_t *m = (pthread_mutex_t *)mutex;

	if (pthread_mutex_destroy(m) != 0)
		warning("pthread_mutex_destroy() failed");
	else
		delete m;
}

void OSystem_Android::quit() {
	ENTER();

	_timer_thread_exit = true;
	pthread_join(_timer_thread, 0);
}

void OSystem_Android::setWindowCaption(const char *caption) {
	ENTER("%s", caption);

	JNIEnv *env = JNU_GetEnv();
	jstring java_caption = env->NewStringUTF(caption);
	env->CallVoidMethod(_back_ptr, MID_setWindowCaption, java_caption);

	if (env->ExceptionCheck()) {
		warning("Failed to set window caption");

		env->ExceptionDescribe();
		env->ExceptionClear();
	}

	env->DeleteLocalRef(java_caption);
}

void OSystem_Android::displayMessageOnOSD(const char *msg) {
	ENTER("%s", msg);

	JNIEnv *env = JNU_GetEnv();
	jstring java_msg = env->NewStringUTF(msg);

	env->CallVoidMethod(_back_ptr, MID_displayMessageOnOSD, java_msg);

	if (env->ExceptionCheck()) {
		warning("Failed to display OSD message");

		env->ExceptionDescribe();
		env->ExceptionClear();
	}

	env->DeleteLocalRef(java_msg);
}

void OSystem_Android::showVirtualKeyboard(bool enable) {
	ENTER("%d", enable);

	JNIEnv *env = JNU_GetEnv();

	env->CallVoidMethod(_back_ptr, MID_showVirtualKeyboard, enable);

	if (env->ExceptionCheck()) {
		error("Error trying to show virtual keyboard");

		env->ExceptionDescribe();
		env->ExceptionClear();
	}
}

Common::SaveFileManager *OSystem_Android::getSavefileManager() {
	assert(_savefile);
	return _savefile;
}

Audio::Mixer *OSystem_Android::getMixer() {
	assert(_mixer);
	return _mixer;
}

Common::TimerManager *OSystem_Android::getTimerManager() {
	assert(_timer);
	return _timer;
}

void OSystem_Android::getTimeAndDate(TimeDate &td) const {
	struct tm tm;
	const time_t curTime = time(0);

	localtime_r(&curTime, &tm);
	td.tm_sec = tm.tm_sec;
	td.tm_min = tm.tm_min;
	td.tm_hour = tm.tm_hour;
	td.tm_mday = tm.tm_mday;
	td.tm_mon = tm.tm_mon;
	td.tm_year = tm.tm_year;
}

FilesystemFactory *OSystem_Android::getFilesystemFactory() {
	return _fsFactory;
}

void OSystem_Android::addSysArchivesToSearchSet(Common::SearchSet &s,
												int priority) {
	s.add("ASSET", _asset_archive, priority, false);

	JNIEnv *env = JNU_GetEnv();

	jobjectArray array =
		(jobjectArray)env->CallObjectMethod(_back_ptr, MID_getSysArchives);

	if (env->ExceptionCheck()) {
		warning("Error finding system archive path");

		env->ExceptionDescribe();
		env->ExceptionClear();

		return;
	}

	jsize size = env->GetArrayLength(array);
	for (jsize i = 0; i < size; ++i) {
		jstring path_obj = (jstring)env->GetObjectArrayElement(array, i);
		const char *path = env->GetStringUTFChars(path_obj, 0);

		if (path != 0) {
			s.addDirectory(path, path, priority);
			env->ReleaseStringUTFChars(path_obj, path);
		}

		env->DeleteLocalRef(path_obj);
	}
}

void OSystem_Android::logMessage(LogMessageType::Type type, const char *message) {
	switch (type) {
	case LogMessageType::kDebug:
		__android_log_write(ANDROID_LOG_DEBUG, android_log_tag, message);
		break;

	case LogMessageType::kWarning:
		__android_log_write(ANDROID_LOG_WARN, android_log_tag, message);
		break;

	case LogMessageType::kError:
		__android_log_write(ANDROID_LOG_ERROR, android_log_tag, message);
		break;
	}
}

static jint ScummVM_scummVMMain(JNIEnv *env, jobject self, jobjectArray args) {
	OSystem_Android *cpp_obj = OSystem_Android::fromJavaObject(env, self);

	const int MAX_NARGS = 32;
	int res = -1;

	int argc = env->GetArrayLength(args);
	if (argc > MAX_NARGS) {
		JNU_ThrowByName(env, "java/lang/IllegalArgumentException",
						"too many arguments");
		return 0;
	}

	char *argv[MAX_NARGS];

	// note use in cleanup loop below
	int nargs;

	for (nargs = 0; nargs < argc; ++nargs) {
		jstring arg = (jstring)env->GetObjectArrayElement(args, nargs);

		if (arg == 0) {
			argv[nargs] = 0;
		} else {
			const char *cstr = env->GetStringUTFChars(arg, 0);

			argv[nargs] = const_cast<char *>(cstr);

			// exception already thrown?
			if (cstr == 0)
				goto cleanup;
		}

		env->DeleteLocalRef(arg);
	}

	g_system = cpp_obj;
	assert(g_system);

	LOGI("Entering scummvm_main with %d args", argc);

	res = scummvm_main(argc, argv);

	LOGI("Exiting scummvm_main");

	g_system->quit();

cleanup:
	nargs--;

	for (int i = 0; i < nargs; ++i) {
		if (argv[i] == 0)
			continue;

		jstring arg = (jstring)env->GetObjectArrayElement(args, nargs);

		// Exception already thrown?
		if (arg == 0)
			return res;

		env->ReleaseStringUTFChars(arg, argv[i]);
		env->DeleteLocalRef(arg);
	}

	return res;
}

#ifdef DYNAMIC_MODULES
void AndroidPluginProvider::addCustomDirectories(Common::FSList &dirs) const {
	OSystem_Android *g_system_android = (OSystem_Android *)g_system;
	g_system_android->addPluginDirectories(dirs);
}
#endif

static void ScummVM_enableZoning(JNIEnv *env, jobject self, jboolean enable) {
	OSystem_Android *cpp_obj = OSystem_Android::fromJavaObject(env, self);
	cpp_obj->enableZoning(enable);
}

static void ScummVM_setSurfaceSize(JNIEnv *env, jobject self,
									jint width, jint height) {
	OSystem_Android *cpp_obj = OSystem_Android::fromJavaObject(env, self);
	cpp_obj->setSurfaceSize(width, height);
}

const static JNINativeMethod gMethods[] = {
	{ "create", "(Landroid/content/res/AssetManager;)V",
		(void *)ScummVM_create },
	{ "nativeDestroy", "()V",
		(void *)ScummVM_nativeDestroy },
	{ "scummVMMain", "([Ljava/lang/String;)I",
	 	(void *)ScummVM_scummVMMain },
	{ "pushEvent", "(Lorg/inodes/gus/scummvm/Event;)V",
		(void *)ScummVM_pushEvent },
	{ "audioMixCallback", "([B)V",
		(void *)ScummVM_audioMixCallback },
	{ "setConfMan", "(Ljava/lang/String;I)V",
		(void *)ScummVM_setConfManInt },
	{ "setConfMan", "(Ljava/lang/String;Ljava/lang/String;)V",
		(void *)ScummVM_setConfManString },
	{ "enableZoning", "(Z)V",
		(void *)ScummVM_enableZoning },
	{ "setSurfaceSize", "(II)V",
		(void *)ScummVM_setSurfaceSize },
};

JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *jvm, void *reserved) {
	cached_jvm = jvm;

	JNIEnv *env;

	if (jvm->GetEnv((void **)&env, JNI_VERSION_1_2))
		return JNI_ERR;

	jclass cls = env->FindClass("org/inodes/gus/scummvm/ScummVM");
	if (cls == 0)
		return JNI_ERR;

	if (env->RegisterNatives(cls, gMethods, ARRAYSIZE(gMethods)) < 0)
		return JNI_ERR;

	FID_ScummVM_nativeScummVM = env->GetFieldID(cls, "nativeScummVM", "J");
	if (FID_ScummVM_nativeScummVM == 0)
		return JNI_ERR;

	jclass event = env->FindClass("org/inodes/gus/scummvm/Event");
	if (event == 0)
		return JNI_ERR;

	FID_Event_type = env->GetFieldID(event, "type", "I");
	if (FID_Event_type == 0)
		return JNI_ERR;

	FID_Event_synthetic = env->GetFieldID(event, "synthetic", "Z");
	if (FID_Event_synthetic == 0)
		return JNI_ERR;

	FID_Event_kbd_keycode = env->GetFieldID(event, "kbd_keycode", "I");
	if (FID_Event_kbd_keycode == 0)
		return JNI_ERR;

	FID_Event_kbd_ascii = env->GetFieldID(event, "kbd_ascii", "I");
	if (FID_Event_kbd_ascii == 0)
		return JNI_ERR;

	FID_Event_kbd_flags = env->GetFieldID(event, "kbd_flags", "I");
	if (FID_Event_kbd_flags == 0)
		return JNI_ERR;

	FID_Event_mouse_x = env->GetFieldID(event, "mouse_x", "I");
	if (FID_Event_mouse_x == 0)
		return JNI_ERR;

	FID_Event_mouse_y = env->GetFieldID(event, "mouse_y", "I");
	if (FID_Event_mouse_y == 0)
		return JNI_ERR;

	FID_Event_mouse_relative = env->GetFieldID(event, "mouse_relative", "Z");
	if (FID_Event_mouse_relative == 0)
		return JNI_ERR;

	cls = env->FindClass("java/lang/Object");
	if (cls == 0)
		return JNI_ERR;

	MID_Object_wait = env->GetMethodID(cls, "wait", "()V");
	if (MID_Object_wait == 0)
		return JNI_ERR;

	return JNI_VERSION_1_2;
}

#endif