/*
 * Copyright (C) 2008 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

// This string is autogenerated by ChangeAppSettings.sh, do not change spaces amount
package org.renpy.android;

import javax.microedition.khronos.egl.EGL10;
import javax.microedition.khronos.egl.EGL11;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.egl.EGLContext;
import javax.microedition.khronos.egl.EGLDisplay;
import javax.microedition.khronos.egl.EGLSurface;
import javax.microedition.khronos.opengles.GL10;
import android.opengl.GLES20;
import android.opengl.Matrix;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.MotionEvent;
import android.view.KeyEvent;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.ExtractedText;
import android.view.inputmethod.ExtractedTextRequest;
import android.view.inputmethod.CompletionInfo;
//API 11 only
//import android.view.inputmethod.CorrectionInfo;
import android.opengl.GLSurfaceView;
import android.net.Uri;
import android.os.PowerManager;
import android.os.Handler;
import android.content.pm.PackageManager;
import android.content.pm.ApplicationInfo;
import android.graphics.PixelFormat;

import java.io.IOException;
import java.io.InputStream;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.opengl.GLUtils;
import java.lang.Math;
import java.nio.FloatBuffer;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import android.graphics.Color;
import android.content.res.Resources;


public class SDLSurfaceView extends SurfaceView implements SurfaceHolder.Callback, Runnable {
    static private final String TAG = "SDLSurface";
    static private final boolean DEBUG = false;
    static private final String mVertexShader =
        "uniform mat4 uMVPMatrix;\n" +
        "attribute vec4 aPosition;\n" +
        "attribute vec2 aTextureCoord;\n" +
        "varying vec2 vTextureCoord;\n" +
        "void main() {\n" +
        "  gl_Position = uMVPMatrix * aPosition;\n" +
        "  vTextureCoord = aTextureCoord;\n" +
        "}\n";

    static private final String mFragmentShader =
        "precision mediump float;\n" +
        "varying vec2 vTextureCoord;\n" +
        "uniform sampler2D sTexture;\n" +
        "void main() {\n" +
        "  gl_FragColor = texture2D(sTexture, vTextureCoord);\n" +

        "}\n";
    private static class ConfigChooser implements GLSurfaceView.EGLConfigChooser {

        public ConfigChooser(int r, int g, int b, int a, int depth, int stencil) {
            mRedSize = r;
            mGreenSize = g;
            mBlueSize = b;
            mAlphaSize = a;
            mDepthSize = depth;
            mStencilSize = stencil;
        }

        /* This EGL config specification is used to specify 2.0 rendering.
         * We use a minimum size of 4 bits for red/green/blue, but will
         * perform actual matching in chooseConfig() below.
         */
        private static int EGL_OPENGL_ES2_BIT = 4;
        private static int[] s_configAttribs2 =
        {
            EGL10.EGL_RED_SIZE, 4,
            EGL10.EGL_GREEN_SIZE, 4,
            EGL10.EGL_BLUE_SIZE, 4,
            EGL10.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
            EGL10.EGL_NONE
        };

        public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) {

            /* Get the number of minimally matching EGL configurations
            */
            int[] num_config = new int[1];
            egl.eglChooseConfig(display, s_configAttribs2, null, 0, num_config);

            int numConfigs = num_config[0];

            if (numConfigs <= 0) {
                throw new IllegalArgumentException("No configs match configSpec");
            }

            /* Allocate then read the array of minimally matching EGL configs
            */
            EGLConfig[] configs = new EGLConfig[numConfigs];
            egl.eglChooseConfig(display, s_configAttribs2, configs, numConfigs, num_config);

            /* Now return the "best" one
            */
            //printConfigs(egl, display, configs);
            return chooseConfig(egl, display, configs);
        }

        public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display,
                EGLConfig[] configs) {
            for(EGLConfig config : configs) {
                int d = findConfigAttrib(egl, display, config,
                        EGL10.EGL_DEPTH_SIZE, 0);
                int s = findConfigAttrib(egl, display, config,
                        EGL10.EGL_STENCIL_SIZE, 0);

                // We need at least mDepthSize and mStencilSize bits
                if (d < mDepthSize || s < mStencilSize)
                    continue;

                // We want an *exact* match for red/green/blue/alpha
                int r = findConfigAttrib(egl, display, config,
                        EGL10.EGL_RED_SIZE, 0);
                int g = findConfigAttrib(egl, display, config,
                        EGL10.EGL_GREEN_SIZE, 0);
                int b = findConfigAttrib(egl, display, config,
                        EGL10.EGL_BLUE_SIZE, 0);
                int a = findConfigAttrib(egl, display, config,
                        EGL10.EGL_ALPHA_SIZE, 0);

                if (r == mRedSize && g == mGreenSize && b == mBlueSize && a == mAlphaSize)
                    return config;
            }
            return null;
        }

        private int findConfigAttrib(EGL10 egl, EGLDisplay display,
                EGLConfig config, int attribute, int defaultValue) {

            if (egl.eglGetConfigAttrib(display, config, attribute, mValue)) {
                return mValue[0];
            }
            return defaultValue;
        }

        private void printConfigs(EGL10 egl, EGLDisplay display,
                EGLConfig[] configs) {
            int numConfigs = configs.length;
            Log.w(TAG, String.format("%d configurations", numConfigs));
            for (int i = 0; i < numConfigs; i++) {
                Log.w(TAG, String.format("Configuration %d:\n", i));
                printConfig(egl, display, configs[i]);
            }
        }

        private void printConfig(EGL10 egl, EGLDisplay display,
                EGLConfig config) {
            int[] attributes = {
                EGL10.EGL_BUFFER_SIZE,
                EGL10.EGL_ALPHA_SIZE,
                EGL10.EGL_BLUE_SIZE,
                EGL10.EGL_GREEN_SIZE,
                EGL10.EGL_RED_SIZE,
                EGL10.EGL_DEPTH_SIZE,
                EGL10.EGL_STENCIL_SIZE,
                EGL10.EGL_CONFIG_CAVEAT,
                EGL10.EGL_CONFIG_ID,
                EGL10.EGL_LEVEL,
                EGL10.EGL_MAX_PBUFFER_HEIGHT,
                EGL10.EGL_MAX_PBUFFER_PIXELS,
                EGL10.EGL_MAX_PBUFFER_WIDTH,
                EGL10.EGL_NATIVE_RENDERABLE,
                EGL10.EGL_NATIVE_VISUAL_ID,
                EGL10.EGL_NATIVE_VISUAL_TYPE,
                0x3030, // EGL10.EGL_PRESERVED_RESOURCES,
                EGL10.EGL_SAMPLES,
                EGL10.EGL_SAMPLE_BUFFERS,
                EGL10.EGL_SURFACE_TYPE,
                EGL10.EGL_TRANSPARENT_TYPE,
                EGL10.EGL_TRANSPARENT_RED_VALUE,
                EGL10.EGL_TRANSPARENT_GREEN_VALUE,
                EGL10.EGL_TRANSPARENT_BLUE_VALUE,
                0x3039, // EGL10.EGL_BIND_TO_TEXTURE_RGB,
                0x303A, // EGL10.EGL_BIND_TO_TEXTURE_RGBA,
                0x303B, // EGL10.EGL_MIN_SWAP_INTERVAL,
                0x303C, // EGL10.EGL_MAX_SWAP_INTERVAL,
                EGL10.EGL_LUMINANCE_SIZE,
                EGL10.EGL_ALPHA_MASK_SIZE,
                EGL10.EGL_COLOR_BUFFER_TYPE,
                EGL10.EGL_RENDERABLE_TYPE,
                0x3042 // EGL10.EGL_CONFORMANT
            };
            String[] names = {
                "EGL_BUFFER_SIZE",
                "EGL_ALPHA_SIZE",
                "EGL_BLUE_SIZE",
                "EGL_GREEN_SIZE",
                "EGL_RED_SIZE",
                "EGL_DEPTH_SIZE",
                "EGL_STENCIL_SIZE",
                "EGL_CONFIG_CAVEAT",
                "EGL_CONFIG_ID",
                "EGL_LEVEL",
                "EGL_MAX_PBUFFER_HEIGHT",
                "EGL_MAX_PBUFFER_PIXELS",
                "EGL_MAX_PBUFFER_WIDTH",
                "EGL_NATIVE_RENDERABLE",
                "EGL_NATIVE_VISUAL_ID",
                "EGL_NATIVE_VISUAL_TYPE",
                "EGL_PRESERVED_RESOURCES",
                "EGL_SAMPLES",
                "EGL_SAMPLE_BUFFERS",
                "EGL_SURFACE_TYPE",
                "EGL_TRANSPARENT_TYPE",
                "EGL_TRANSPARENT_RED_VALUE",
                "EGL_TRANSPARENT_GREEN_VALUE",
                "EGL_TRANSPARENT_BLUE_VALUE",
                "EGL_BIND_TO_TEXTURE_RGB",
                "EGL_BIND_TO_TEXTURE_RGBA",
                "EGL_MIN_SWAP_INTERVAL",
                "EGL_MAX_SWAP_INTERVAL",
                "EGL_LUMINANCE_SIZE",
                "EGL_ALPHA_MASK_SIZE",
                "EGL_COLOR_BUFFER_TYPE",
                "EGL_RENDERABLE_TYPE",
                "EGL_CONFORMANT"
            };
            int[] value = new int[1];
            for (int i = 0; i < attributes.length; i++) {
                int attribute = attributes[i];
                String name = names[i];
                if ( egl.eglGetConfigAttrib(display, config, attribute, value)) {
                    Log.w(TAG, String.format("  %s: %d\n", name, value[0]));
                } else {
                    // Log.w(TAG, String.format("  %s: failed\n", name));
                    while (egl.eglGetError() != EGL10.EGL_SUCCESS);
                }
            }
        }

        // Subclasses can adjust these values:
        protected int mRedSize;
        protected int mGreenSize;
        protected int mBlueSize;
        protected int mAlphaSize;
        protected int mDepthSize;
        protected int mStencilSize;
        private int[] mValue = new int[1];
    }

    public interface OnInterceptTouchListener {
        boolean onTouch(MotionEvent ev);
    }

    private OnInterceptTouchListener mOnInterceptTouchListener = null;

    // The activity we're a part of.
    private static PythonActivity mActivity;

    // Have we started yet?
    public boolean mStarted = false;

    // what is the textinput type while calling the keyboard
    public int inputType = EditorInfo.TYPE_CLASS_TEXT;

    // Is Python ready to receive input events?
    static boolean mInputActivated = false;

    // The number of times we should clear the screen after swap.
    private int mClears = 2;

    // Has the display been changed?
    private boolean mChanged = false;

    // Are we running yet?
    private boolean mRunning = false;

    // The EGL used by our thread.
    private EGL10 mEgl = null;

    // The EGL Display used.
    private EGLDisplay mEglDisplay = null;

    // The EGL Context used.
    private EGLContext mEglContext = null;

    // The EGL Surface used.
    private EGLSurface mEglSurface = null;

    // The EGL Config used.
    private EGLConfig mEglConfig = null;

    // The user program is not participating in the pause protocol.
    static int PAUSE_NOT_PARTICIPATING = 0;

    // A pause has not been requested by the OS.
    static int PAUSE_NONE = 1;

    // A pause has been requested by Android, but the user program has
    // not bothered responding yet.
    static int PAUSE_REQUEST = 2;

    // The user program is waiting in waitForResume.
    static int PAUSE_WAIT_FOR_RESUME = 3;

    static int PAUSE_STOP_REQUEST = 4;
    static int PAUSE_STOP_ACK = 5;

    // This stores the state of the pause system.
    static int mPause = PAUSE_NOT_PARTICIPATING;

    private PowerManager.WakeLock wakeLock;

    // This stores the length of the text in pridiction/swype buffer
    private int mDelLen = 0;

    // The width and height. (This should be set at startup time -
    // these values just prevent segfaults and divide by zero, etc.)
    int mWidth = 100;
    int mHeight = 100;

    // The name of the directory where the context stores its files.
    String mFilesDirectory = null;

    // The value of the argument passed in.
    String mArgument = null;

    // The resource manager we use.
    ResourceManager mResourceManager;

    // Access to our meta-data
    private ApplicationInfo ai;

    // Text before/after cursor
    static String mTbf = "";
    static String mTaf = "";

    public static void updateTextFromCursor(String bef, String aft){
        mTbf = bef;
        mTaf = aft;
        if (DEBUG) Log.d(TAG, String.format("mtbf: %s mtaf:%s <<<<<<<<<", mTbf, mTaf));
        }

    // Our own view
    static SDLSurfaceView instance = null;

    public SDLSurfaceView(PythonActivity act, String argument) {
        super(act);
        SDLSurfaceView.instance = this;

        mActivity = act;
        mResourceManager = new ResourceManager(act);

        SurfaceHolder holder = getHolder();
        holder.addCallback(this);
        holder.setType(SurfaceHolder.SURFACE_TYPE_GPU);

        mFilesDirectory = mActivity.getFilesDir().getAbsolutePath();
        mArgument = argument;

        PowerManager pm = (PowerManager) act.getSystemService(Context.POWER_SERVICE);

        wakeLock = null;
        try {
            ai = act.getPackageManager().getApplicationInfo(
                    act.getPackageName(), PackageManager.GET_META_DATA);
            if ( (Integer)ai.metaData.get("wakelock") == 1 ) {
                wakeLock = pm.newWakeLock(PowerManager.SCREEN_BRIGHT_WAKE_LOCK, "Screen On");
            }
        } catch (PackageManager.NameNotFoundException e) {
        }

        if ( ai.metaData.getInt("surface.transparent") != 0 ) {
            Log.d(TAG, "Surface will be transparent.");
            setZOrderOnTop(true);
            getHolder().setFormat(PixelFormat.TRANSPARENT);
        } else {
            Log.i(TAG, "Surface will NOT be transparent");
        }

    }


    /**
     * The user program should call this frequently to check if a
     * pause has been requested by android. If this ever returns
     * true, the user program should clean up and call waitForResume.
     */
    public int checkPause() {
        if (mPause == PAUSE_NOT_PARTICIPATING) {
            mPause = PAUSE_NONE;
        }

        if (mPause == PAUSE_REQUEST) {
            return 1;
        } else {
            return 0;
        }
    }


    /**
     * The user program should call this quickly after checkPause
     * returns true. This causes the android application to sleep,
     * waiting for resume. While sleeping, it should not have any
     * activity. (Notably, it should stop all timers.)
     *
     * While we're waiting in this method, android is allowed to
     * kill us to reclaim memory, without any further warning.
     */
    public void waitForResume() {
        synchronized (this) {
            mPause = PAUSE_WAIT_FOR_RESUME;

            // Notify any threads waiting in onPause.
            this.notifyAll();

            while (mPause == PAUSE_WAIT_FOR_RESUME) {
                try {
                    this.wait();
                } catch (InterruptedException e) {
                }
            }
        }
        setOpenFile();
    }

    /**
     * if the activity was called with a file parameter, put it in the
     * 'PYTHON_OPENFILE' env var
     */
    public static void setOpenFile(){
        final android.content.Intent intent = mActivity.getIntent();
        if (intent != null) {
            final android.net.Uri data = intent.getData();
            if (data != null && data.getEncodedPath() != null){
                nativeSetEnv("PYTHON_OPENFILE", data.getEncodedPath());
            }
        }
    }


    public void closeSoftKeyboard(){
        // close the IME overlay(keyboard)
        Hardware.hideKeyboard();
    }

    /**
     * Inform the view that the activity is paused. The owner of this view must
     * call this method when the activity is paused. Calling this method will
     * pause the rendering thread.
     * Must not be called before a renderer has been set.
     */
    public void onPause() {

        this.closeSoftKeyboard();
        synchronized (this) {
            if (mPause == PAUSE_NONE) {
                mPause = PAUSE_REQUEST;

                while (mPause == PAUSE_REQUEST) {
                    try {
                        this.wait();
                    } catch (InterruptedException e) {
                        // pass
                    }
                }
            }
        }

        if ( wakeLock != null )
            wakeLock.release();

    }

    /**
     * Inform the view that the activity is resumed. The owner of this view must
     * call this method when the activity is resumed. Calling this method will
     * recreate the OpenGL display and resume the rendering
     * thread.
     * Must not be called before a renderer has been set.
     */
    public void onResume() {
        synchronized (this) {
            if (mPause == PAUSE_WAIT_FOR_RESUME) {
                mPause = PAUSE_NONE;
                this.notifyAll();
            }
        }
        if ( wakeLock != null )
            wakeLock.acquire();
    }

    public void onDestroy() {
        Log.w(TAG, "onDestroy() called");
        this.closeSoftKeyboard();
        synchronized (this) {
            this.notifyAll();

            if ( mPause == PAUSE_STOP_ACK ) {
                Log.d(TAG, "onDestroy() app already leaved.");
                return;
            }


            // application didn't leave, give 10s before closing.
            // hopefully, this could be enough for launching the on_stop() trigger within the app.
            mPause = PAUSE_STOP_REQUEST;
            int i = 50;

            Log.d(TAG, "onDestroy() stop requested, wait for an event from the app");
            for (; i >= 0 && mPause == PAUSE_STOP_REQUEST; i--) {
                try {
                    this.wait(200);
                } catch (InterruptedException e) {
                    break;
                }
            }
            Log.d(TAG, "onDestroy() stop finished waiting.");
        }
    }

    static int checkStop() {
        if (mPause == PAUSE_STOP_REQUEST)
            return 1;
        return 0;
    }

    static void ackStop() {
        Log.d(TAG, "ackStop() notify");
        synchronized (instance) {
            mPause = PAUSE_STOP_ACK;
            instance.notifyAll();
        }
    }


    /**
     * This method is part of the SurfaceHolder.Callback interface, and is
     * not normally called or subclassed by clients of GLSurfaceView.
     */
    public void surfaceCreated(SurfaceHolder holder) {
        if (DEBUG) Log.d(TAG, "surfaceCreated()");
        synchronized (this) {
            if (!mStarted) {
                this.notifyAll();
            }
        }
    }

    /**
     * This method is part of the SurfaceHolder.Callback interface, and is
     * not normally called or subclassed by clients of GLSurfaceView.
     */
    public void surfaceDestroyed(SurfaceHolder holder) {
        if (DEBUG) Log.d(TAG, "surfaceDestroyed()");
    }

    /**
     * This method is part of the SurfaceHolder.Callback interface, and is
     * not normally called or subclassed by clients of GLSurfaceView.
     */
    public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) {
        if (DEBUG) Log.i(TAG, String.format("surfaceChanged() fmt=%d size=%dx%d", format, w, h));

        mWidth = w;
        mHeight = h;

        if (mActivity.getRequestedOrientation() == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE &&
                mWidth < mHeight) {
            return;
                }

        if (mActivity.getRequestedOrientation() == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT &&
                mWidth > mHeight) {
            return;
                }

        if (!mRunning) {
            mRunning = true;
            new Thread(this).start();
        } else {
            mChanged = true;
            if (mStarted) {
                nativeExpose();
            }
        }
    }


    public void run() {
        mEgl = (EGL10) EGLContext.getEGL();

        mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);

        int[] version = new int[2];
        mEgl.eglInitialize(mEglDisplay, version);

        // Pick an appropriate config. We just take the first config
        // the system offers to us, because anything more complicated
        // than that stands a really good chance of not working.
        int[] configSpec = {
            // RENDERABLE_TYPE = OpenGL ES is the default.
            EGL10.EGL_NONE
        };

        EGLConfig[] configs = new EGLConfig[1];
        int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
        int[] num_config = new int[1];
        int[] attrib_list = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL10.EGL_NONE };

        // Create an opengl es 2.0 surface
        Log.i(TAG, "Choose egl configuration");
        int configToTest = 0;
        boolean configFound = false;

        while (true) {
            try {
                if (configToTest == 0) {
                    Log.i(TAG, "Try to use graphics config R8G8B8A8S8");
                    ConfigChooser chooser = new ConfigChooser(
                            // rgba
                            8, 8, 8, 8,
                            // depth
                            ai.metaData.getInt("surface.depth", 0),
                            // stencil
                            ai.metaData.getInt("surface.stencil", 8));
                    mEglConfig = chooser.chooseConfig(mEgl, mEglDisplay);
                } else if (configToTest == 1) {
                    Log.i(TAG, "Try to use graphics config R5G6B5S8");
                    ConfigChooser chooser = new ConfigChooser(
                            // rgba
                            5, 6, 5, 0,
                            // depth
                            ai.metaData.getInt("surface.depth", 0),
                            // stencil
                            ai.metaData.getInt("surface.stencil", 8));
                    mEglConfig = chooser.chooseConfig(mEgl, mEglDisplay);
                } else {
                    Log.e(TAG, "Unable to find a correct surface for this device !");
                    break;
                }

            } catch (IllegalArgumentException e) {
                configToTest++;
                continue;
            }

            if (DEBUG) Log.d(TAG, "Create egl context");
            mEglContext = mEgl.eglCreateContext(mEglDisplay, mEglConfig, EGL10.EGL_NO_CONTEXT, attrib_list);
            if (mEglContext == null) {
                Log.w(TAG, "Unable to create egl context with this configuration, try the next one.");
                configToTest++;
                continue;
            }

            Log.w(TAG, "Create egl surface");
            if (!createSurface()) {
                Log.w(TAG, "Unable to create egl surface with this configuration, try the next one.");
                configToTest++;
                continue;
            }

            configFound = true;
            break;
        }

        if (!configFound) {
            System.exit(0);
            return;
        }

        if (DEBUG) Log.d(TAG, "Done egl");
        waitForStart();

        nativeResize(mWidth, mHeight);
        nativeInitJavaCallbacks();
        nativeSetEnv("ANDROID_PRIVATE", mFilesDirectory);
        nativeSetEnv("ANDROID_UNPACK", mFilesDirectory);
        nativeSetEnv("ANDROID_ARGUMENT", mArgument);
        nativeSetEnv("ANDROID_APP_PATH", mArgument);
        nativeSetEnv("PYTHONOPTIMIZE", "2");
        nativeSetEnv("PYTHONHOME", mFilesDirectory);
        nativeSetEnv("PYTHONPATH", mArgument + ":" + mFilesDirectory + "/lib");

        // XXX Using SetOpenFile make a crash in nativeSetEnv. I don't
        // understand why, maybe because the method is static or something.
        // Anyway, if you remove that part of the code, ensure the Laucher
        // (ProjectChooser) is still working.
        final android.content.Intent intent = mActivity.getIntent();
        if (intent != null) {
            final android.net.Uri data = intent.getData();
            if (data != null && data.getEncodedPath() != null)
                nativeSetEnv("PYTHON_OPENFILE", data.getEncodedPath());
        }

        nativeSetMultitouchUsed();
        nativeInit();

        mPause = PAUSE_STOP_ACK;

        //Log.i(TAG, "End of native init, stop everything (exit0)");
        System.exit(0);
    }

    private void waitForStart() {

        int presplashId = mResourceManager.getIdentifier("presplash", "drawable");
        InputStream is = mActivity.getResources().openRawResource(presplashId);

        Bitmap bitmap = null;
        try {
            bitmap = BitmapFactory.decodeStream(is);
            bitmap = bitmap.copy(Bitmap.Config.ARGB_8888, false);
        } finally {
            try {
                is.close();
            } catch (IOException e) { }
        }

        mTriangleVertices = ByteBuffer.allocateDirect(mTriangleVerticesData.length
                * FLOAT_SIZE_BYTES).order(ByteOrder.nativeOrder()).asFloatBuffer();
        mTriangleVertices.put(mTriangleVerticesData).position(0);

        mProgram = createProgram(mVertexShader, mFragmentShader);
        if (mProgram == 0) {
            synchronized (this) {
                while (!mStarted) {
                    try {
                        this.wait(250);
                    } catch (InterruptedException e) {
                        continue;
                    }
                }
            }
            return;
        }

        maPositionHandle = GLES20.glGetAttribLocation(mProgram, "aPosition");
        checkGlError("glGetAttribLocation aPosition");
        if (maPositionHandle == -1) {
            throw new RuntimeException("Could not get attrib location for aPosition");
        }
        maTextureHandle = GLES20.glGetAttribLocation(mProgram, "aTextureCoord");
        checkGlError("glGetAttribLocation aTextureCoord");
        if (maTextureHandle == -1) {
            throw new RuntimeException("Could not get attrib location for aTextureCoord");
        }

        muMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix");
        checkGlError("glGetUniformLocation uMVPMatrix");
        if (muMVPMatrixHandle == -1) {
            throw new RuntimeException("Could not get attrib location for uMVPMatrix");
        }

        // Create our texture. This has to be done each time the
        // surface is created.

        int[] textures = new int[1];
        GLES20.glGenTextures(1, textures, 0);

        mTextureID = textures[0];
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureID);

        GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER,
                GLES20.GL_LINEAR);
        GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
                GLES20.GL_TEXTURE_MAG_FILTER,
                GLES20.GL_LINEAR);

        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S,
                GLES20.GL_CLAMP_TO_EDGE);
        GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T,
                GLES20.GL_CLAMP_TO_EDGE);

        GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);

        Matrix.setLookAtM(mVMatrix, 0, 0, 0, -5, 0f, 0f, 0f, 0f, 1.0f, 0.0f);

        GLES20.glViewport(0, 0, mWidth, mHeight);

        if (bitmap != null) {
            float mx = ((float)mWidth / bitmap.getWidth()) / 2.0f;
            float my = ((float)mHeight / bitmap.getHeight()) / 2.0f;
            Matrix.orthoM(mProjMatrix, 0, -mx, mx, my, -my, 0, 10);
            int value = bitmap.getPixel(0, 0);
            Color color = new Color();
            GLES20.glClearColor(
                    (float)color.red(value) / 255.0f,
                    (float)color.green(value) / 255.0f,
                    (float)color.blue(value) / 255.0f,
                    0.0f);
        } else {
            Matrix.orthoM(mProjMatrix, 0, -1, 1, -1, 1, 0, 10);
            GLES20.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
        }

        GLES20.glClear(GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);
        GLES20.glUseProgram(mProgram);
        checkGlError("glUseProgram");

        GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureID);

        mTriangleVertices.position(TRIANGLE_VERTICES_DATA_POS_OFFSET);
        GLES20.glVertexAttribPointer(maPositionHandle, 3, GLES20.GL_FLOAT, false,
                TRIANGLE_VERTICES_DATA_STRIDE_BYTES, mTriangleVertices);
        checkGlError("glVertexAttribPointer maPosition");
        mTriangleVertices.position(TRIANGLE_VERTICES_DATA_UV_OFFSET);
        GLES20.glEnableVertexAttribArray(maPositionHandle);
        checkGlError("glEnableVertexAttribArray maPositionHandle");
        GLES20.glVertexAttribPointer(maTextureHandle, 2, GLES20.GL_FLOAT, false,
                TRIANGLE_VERTICES_DATA_STRIDE_BYTES, mTriangleVertices);
        checkGlError("glVertexAttribPointer maTextureHandle");
        GLES20.glEnableVertexAttribArray(maTextureHandle);
        checkGlError("glEnableVertexAttribArray maTextureHandle");

        Matrix.setRotateM(mMMatrix, 0, 0, 0, 0, 1.0f);
        Matrix.multiplyMM(mMVPMatrix, 0, mVMatrix, 0, mMMatrix, 0);
        Matrix.multiplyMM(mMVPMatrix, 0, mProjMatrix, 0, mMVPMatrix, 0);

        GLES20.glUniformMatrix4fv(muMVPMatrixHandle, 1, false, mMVPMatrix, 0);

        // Ensure that, even with double buffer, or if we lost one buffer (like
        // BufferQueue has been abandoned!), it will work.
        for ( int i = 0; i < 2; i++ ) {
            GLES20.glClear(GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);
            GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 6);
            checkGlError("glDrawArrays");
            swapBuffers();
        }

        // Wait to be notified it's okay to start Python.
        synchronized (this) {
            while (!mStarted) {
                // Draw & Flip.
                GLES20.glClear(GLES20.GL_DEPTH_BUFFER_BIT | GLES20.GL_COLOR_BUFFER_BIT);
                GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 6);
                swapBuffers();

                try {
                    this.wait(250);
                } catch (InterruptedException e) {
                }
            }
        }

        GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
        GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);

        // Delete texture.
        GLES20.glDeleteTextures(1, textures, 0);
        if (bitmap != null)
            bitmap.recycle();

        // Delete program
        GLES20.glDeleteProgram(mProgram);
    }


    public void start() {
        this.setFocusableInTouchMode(true);
        this.setFocusable(true);
        this.requestFocus();

        synchronized (this) {
            mStarted = true;
            this.notify();
        }

    }

    public boolean createSurface() {
        mChanged = false;

        // Destroy the old surface.
        if (mEglSurface != null) {

            /*
             * Unbind and destroy the old EGL surface, if
             * there is one.
             */
            mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE,
                    EGL10.EGL_NO_SURFACE, EGL10.EGL_NO_CONTEXT);
            mEgl.eglDestroySurface(mEglDisplay, mEglSurface);
        }

        // Create a new surface.
        mEglSurface = mEgl.eglCreateWindowSurface(
                mEglDisplay, mEglConfig, getHolder(), null);

        // Make the new surface current.
        boolean rv = mEgl.eglMakeCurrent(
                mEglDisplay, mEglSurface, mEglSurface, mEglContext);
        if (!rv) {
            mEglSurface = null;
            return false;
        }

        if (mStarted) {
            if (mInputActivated)
                nativeResizeEvent(mWidth, mHeight);
            else
                nativeResize(mWidth, mHeight);

            // If the size didn't change, kivy might no rerender the scene. Force it.
            nativeExpose();
        }

        return true;

    }

    public int swapBuffers() {
        // If the display has been changed, then disregard all the
        // rendering we've done to it, and make a new surface.
        //
        // Otherwise, swap the buffers.
        if (mChanged) {
            createSurface();
            mClears = 2;
            return 0;

        } else {
            mEgl.eglSwapBuffers(mEglDisplay, mEglSurface);
            if (mClears-- > 0)
                GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
            return 1;
        }

    }

    private static final int INVALID_POINTER_ID = -1;
    private int mActivePointerId = INVALID_POINTER_ID;

    public void setInterceptTouchListener(OnInterceptTouchListener listener) {
        this.mOnInterceptTouchListener = listener;
    }

    @Override
    public boolean onTouchEvent(final MotionEvent event) {

        if (mInputActivated == false)
            return true;

        if (mOnInterceptTouchListener != null)
            if (mOnInterceptTouchListener.onTouch(event))
                return false;

        int action = event.getAction() & MotionEvent.ACTION_MASK;
        int sdlAction = -1;
        int pointerId = -1;
        int pointerIndex = -1;

        switch ( action ) {
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_POINTER_DOWN:
                sdlAction = 0;
                break;
            case MotionEvent.ACTION_MOVE:
                sdlAction = 2;
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_POINTER_UP:
                sdlAction = 1;
                break;
        }

        // http://android-developers.blogspot.com/2010/06/making-sense-of-multitouch.html
        switch ( action  & MotionEvent.ACTION_MASK ) {
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_MOVE:
            case MotionEvent.ACTION_UP:
                pointerIndex = event.findPointerIndex(mActivePointerId);
                break;
            case MotionEvent.ACTION_POINTER_DOWN:
            case MotionEvent.ACTION_POINTER_UP:
                pointerIndex = (event.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK)
                    >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
                if ( action == MotionEvent.ACTION_POINTER_UP ) {
                    pointerId = event.getPointerId(pointerIndex);
                    if ( pointerId == mActivePointerId )
                        mActivePointerId = event.getPointerId(pointerIndex == 0 ? 1 : 0);
                }
                break;
        }

        if ( sdlAction >= 0 ) {

            for ( int i = 0; i < event.getPointerCount(); i++ ) {

                if ( pointerIndex == -1 || pointerIndex == i ) {

                    /**
                      Log.i("python", String.format("mouse id=%d action=%d x=%f y=%f",
                      event.getPointerId(i),
                      sdlAction,
                      event.getX(i),
                      event.getY(i)
                      ));
                     **/
                    SDLSurfaceView.nativeMouse(
                            (int)event.getX(i),
                            (int)event.getY(i),
                            sdlAction,
                            event.getPointerId(i),
                            (int)(event.getPressure(i) * 1000.0),
                            (int)(event.getSize(i) * 1000.0));
                }

            }

        }

        return true;
    };

    @Override
    public boolean onKeyDown(int keyCode, final KeyEvent event) {
        if (DEBUG) Log.d(TAG, String.format("onKeyDown() keyCode=%d", keyCode));
        if (mDelLen > 0){
            mDelLen = 0;
            return true;
        }
        if (mInputActivated && nativeKey(keyCode, 1, event.getUnicodeChar())) {
            return true;
        } else {
            return super.onKeyDown(keyCode, event);
        }
    }

    @Override
    public boolean onKeyUp(int keyCode, final KeyEvent event) {
        if (DEBUG) Log.d(TAG, String.format("onKeyUp() keyCode=%d", keyCode));
        if (mDelLen > 0){
            mDelLen = 0;
            return true;
        }
        if (mInputActivated && nativeKey(keyCode, 0, event.getUnicodeChar())) {
            return true;
        } else {
            return super.onKeyUp(keyCode, event);
        }
    }

    @Override
    public boolean onKeyMultiple(int keyCode, int count, KeyEvent event){
        String keys = event.getCharacters();
        if (DEBUG)
            Log.d(TAG, String.format(
                "onKeyMultiple() keyCode=%d count=%d, keys=%s", keyCode, count, keys));
        char[] keysBuffer = new char[keys.length()];
        if (mDelLen > 0){
            mDelLen = 0;
            return true;
        }
        if (keyCode == 0){
            // FIXME: here is hardcoded value of "q" key
            // on hacker's keyboard. It is passed to
            // nativeKey function to get it worked if
            // we get 9 and some non-ascii characters
            // but it my cause some odd behaviour
            keyCode = 45;
        }
        if (mInputActivated && event.getAction() == KeyEvent.ACTION_MULTIPLE){
            String message = String.format("INSERTN:%s", keys);
            dispatchCommand(message);
            return true;
        }else {
            return super.onKeyMultiple(keyCode, count, event);
        }
    }

    @Override
    public boolean onKeyPreIme(int keyCode, final KeyEvent event){
        if (DEBUG) Log.d(TAG, String.format("onKeyPreIme() keyCode=%d", keyCode));
        if (mInputActivated){
            switch (event.getAction()) {
                case KeyEvent.ACTION_DOWN:
                    return onKeyDown(keyCode, event);
                case KeyEvent.ACTION_UP:
                    return onKeyUp(keyCode, event);
                case KeyEvent.ACTION_MULTIPLE:
                    return onKeyMultiple(
                            keyCode,
                            event.getRepeatCount(),
                            event);
            }
            return false;
        }
        return super.onKeyPreIme(keyCode, event);
    }

    public void delayed_message(String message, int delay){
        Handler handler = new Handler();
        final String msg = message;
        final int d = delay;
        handler.postDelayed(new Runnable(){
        @Override
            public void run(){
                dispatchCommand(msg);
        }
        }, d);
    }

    public void dispatchCommand(String message){

        int delay = 0;
        while (message.length() > 50){
            delayed_message(message.substring(0, 50), delay);
            delay += 100;
            message = String.format("INSERTN:%s", message.substring(50, message.length()));
            if (message.length() <= 50){
                delayed_message(message, delay+50);
                return;
            }
        }

        if (DEBUG) Log.d(TAG, String.format("dispatch :%s", message));
        int keyCode = 45;
        //send control sequence start
        nativeKey(keyCode, 1, 1);
        nativeKey(keyCode, 0, 1);

        for(int i=0; i < message.length(); i++){
            //Calls both up/down events to emulate key pressing
            nativeKey(keyCode, 1, (int) message.charAt(i));
            nativeKey(keyCode, 0, (int) message.charAt(i));
        }

        //send control sequence end \x02
        nativeKey(keyCode, 1, 2);
        nativeKey(keyCode, 0, 2);

    }

    @Override
    public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
        outAttrs.inputType = inputType;
        // ask IME to avoid taking full screen on landscape mode
        outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI;

        // add a listener for the layout chnages to the IME view
        final android.view.View activityRootView = mActivity.getWindow().getDecorView();
        activityRootView.getViewTreeObserver().addOnGlobalLayoutListener(new android.view.ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                //send control sequence start /x04 == kayboard layout changed
                nativeKey(45, 1, 4);
                nativeKey(45, 0, 4);
                }
            });
        return new BaseInputConnection(this, false){

            private void deleteLastText(){
                // send back space keys
                if (DEBUG) Log.i("Python:", String.format("delete text%s", mDelLen));

                if (mDelLen == 0){
                    return;
                }

                String message = String.format("DEL:%s", mDelLen);
                dispatchCommand(message);
            }

            @Override
            public boolean endBatchEdit() {
                if (DEBUG) Log.i("Python:", "endBatchEdit");
                return super.endBatchEdit();
            }

            @Override
            public boolean beginBatchEdit() {
                if (DEBUG) Log.i("Python:", "beginBatchEdit");
                return super.beginBatchEdit();
            }

            @Override
            public boolean commitCompletion(CompletionInfo text){
                if (DEBUG) Log.i("Python:", String.format("Commit Completion %s", text));
                return super.commitCompletion(text);
            }

            /*API11 only
            @Override
            public boolean commitCorrection(CorrectionInfo correctionInfo){
                if (DEBUG) Log.i("Python:", String.format("Commit Correction"));
                return super.commitCorrection(correctionInfo);
            }*/

            @Override
            public boolean commitText(CharSequence text, int newCursorPosition) {
                // some code which takes the input and manipulates it and calls editText.getText().replace() afterwards
                this.deleteLastText();
                if (DEBUG) Log.i("Python:", String.format("Commit Text %s", text));
                mDelLen = 0;
                return super.commitText(text, newCursorPosition);
            }

            @Override
            public boolean sendKeyEvent(KeyEvent event){
                if (DEBUG) Log.d("Python:", String.format("sendKeyEvent %s", event.getKeyCode()));
                return super.sendKeyEvent(event);
            }

            @Override
            public boolean setComposingRegion(int start, int end){
                if (DEBUG) Log.d("Python:", String.format("Set Composing Region %s %s", start, end));
                finishComposingText();
                if (start < 0 || start > end)
                    return true;
                //dispatchCommand(String.format("SEL:%s,%s,%s", mTbf.length(), start, end));
                return true;
                //return super.setComposingRegion(start, end);
            }

            @Override
            public boolean setComposingText(CharSequence text,
                int newCursorPosition){
                this.deleteLastText();
                if (DEBUG) Log.i("Python:", String.format("set Composing Text %s", text));
                // send text
                String message = String.format("INSERT:%s", text);
                dispatchCommand(message);
                // store len to be deleted for next time
                mDelLen = text.length();
                return super.setComposingText(text, newCursorPosition);
            }

            @Override
            public boolean finishComposingText(){
                if (DEBUG) Log.i("Python:", String.format("finish Composing Text"));
                return super.finishComposingText();
            }

            @Override
            public boolean deleteSurroundingText (int beforeLength, int afterLength){
                if (DEBUG) Log.d("Python:", String.format("delete surrounding Text %s %s", beforeLength, afterLength));
                // move cursor to place from where to start deleting
                // send right arrow keys
                for (int i = 0; i < afterLength; i++){
                    nativeKey(45, 1, 39);
                    nativeKey(45, 0, 39);
                }
                // remove text before cursor
                mDelLen = beforeLength + afterLength;
                this.deleteLastText();
                mDelLen = 0;

                return super.deleteSurroundingText(beforeLength, afterLength);
            }

            @Override
            public ExtractedText getExtractedText (ExtractedTextRequest request, int flags){
                if (DEBUG) Log.d("Python:", String.format("getExtractedText %s %s", request.describeContents(), flags));
                    return super.getExtractedText(request, flags);
            }

            @Override
            public CharSequence getSelectedText(int flags){
                if (DEBUG) Log.d("Python:", String.format("getSelectedText %s", flags));
                    return super.getSelectedText(flags);
            }

            @Override
            public CharSequence getTextBeforeCursor(int n, int flags){
                if (DEBUG) Log.d("Python:", String.format("getTextBeforeCursor %s %s", n, flags));
                /*int len = mTbf.length();
                int len_n = Math.min(len, n);
                int start = Math.max(len - n, 0);
                String tbf = mTbf.substring(start,  start + len_n);
                return tbf;*/
                return super.getTextBeforeCursor(n, flags);
            }

            @Override
            public CharSequence getTextAfterCursor(int n, int flags){
                if (DEBUG) Log.d("Python:", String.format("getTextAfterCursor %s %s", n, flags));
                Log.d("Python:", String.format("TextAfterCursor %s", mTaf));
                //return mTaf.substring(0, Math.min(mTaf.length(), n));
                return super.getTextAfterCursor(n, flags);
            }

            @Override
            public boolean setSelection(int start, int end){
                if (DEBUG) Log.d("Python:", String.format("Set Selection %s %s", start, end));
                return super.setSelection(start, end);
            }
        };
    }

    static void activateInput() {
        mInputActivated = true;
    }

    static void openUrl(String url) {
        Log.i("python", "Opening URL: " + url);

        Intent i = new Intent(Intent.ACTION_VIEW);
        i.setData(Uri.parse(url));
        mActivity.startActivity(i);
    }

    // Taken from the "GLES20TriangleRenderer" in Android SDK
    private int loadShader(int shaderType, String source) {
        int shader = GLES20.glCreateShader(shaderType);
        if (shader != 0) {
            GLES20.glShaderSource(shader, source);
            GLES20.glCompileShader(shader);
            int[] compiled = new int[1];
            GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compiled, 0);
            if (compiled[0] == 0) {
                Log.e(TAG, "Could not compile shader " + shaderType + ":");
                Log.e(TAG, GLES20.glGetShaderInfoLog(shader));
                GLES20.glDeleteShader(shader);
                shader = 0;
            }
        }
        return shader;
    }

    private int createProgram(String vertexSource, String fragmentSource) {
        int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource);
        if (vertexShader == 0) {
            return 0;
        }

        int pixelShader = loadShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource);
        if (pixelShader == 0) {
            return 0;
        }

        int program = GLES20.glCreateProgram();
        if (program != 0) {
            GLES20.glAttachShader(program, vertexShader);
            checkGlError("glAttachShader");
            GLES20.glAttachShader(program, pixelShader);
            checkGlError("glAttachShader");
            GLES20.glLinkProgram(program);
            int[] linkStatus = new int[1];
            GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0);
            if (linkStatus[0] != GLES20.GL_TRUE) {
                Log.e(TAG, "Could not link program: ");
                Log.e(TAG, GLES20.glGetProgramInfoLog(program));
                GLES20.glDeleteProgram(program);
                program = 0;
            }
        }
        return program;
    }

    private void checkGlError(String op) {
        int error;
        while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) {
            Log.e(TAG, op + ": glError " + error);
            //throw new RuntimeException(op + ": glError " + error);
        }
    }
    private static final int FLOAT_SIZE_BYTES = 4;
    private static final int TRIANGLE_VERTICES_DATA_STRIDE_BYTES = 5 * FLOAT_SIZE_BYTES;
    private static final int TRIANGLE_VERTICES_DATA_POS_OFFSET = 0;
    private static final int TRIANGLE_VERTICES_DATA_UV_OFFSET = 3;
    private final float[] mTriangleVerticesData = {
        // X, Y, Z, U, V
        -0.5f, -0.5f, 0, 1.0f, 0.0f,
        0.5f, -0.5f, 0, 0.0f, 0.0f,
        0.5f, 0.5f, 0, 0.0f, 1.0f,
        -0.5f, -0.5f, 0, 1.0f, 0.0f,
        0.5f, 0.5f, 0, 0.0f, 1.0f,
        -0.5f, 0.5f, 0, 1.0f, 1.0f,
    };

    private FloatBuffer mTriangleVertices;

    private float[] mMVPMatrix = new float[16];
    private float[] mProjMatrix = new float[16];
    private float[] mMMatrix = new float[16];
    private float[] mVMatrix = new float[16];
    private int mProgram;
    private int mTextureID;
    private int muMVPMatrixHandle;
    private int maPositionHandle;
    private int maTextureHandle;

    // Native part

    public static native void nativeSetEnv(String name, String value);
    public static native void nativeInit();

    public static native void nativeMouse( int x, int y, int action, int pointerId, int pressure, int radius );
    public static native boolean nativeKey(int keyCode, int down, int unicode);
    public static native void nativeSetMouseUsed();
    public static native void nativeSetMultitouchUsed();

    public native void nativeResize(int width, int height);
    public native void nativeResizeEvent(int width, int height);
    public native void nativeExpose();
    public native void nativeInitJavaCallbacks();

}
