package org.emdev.ui.gl;

/*
 * Copyright (C) 2010 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.
 */

import android.graphics.Paint;
import android.graphics.PointF;
import android.graphics.RectF;
import android.opengl.GLU;
import android.opengl.Matrix;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.util.ArrayList;

import javax.microedition.khronos.opengles.GL10;
import javax.microedition.khronos.opengles.GL11;

import org.emdev.utils.MathUtils;
import org.emdev.utils.collections.IntArray;

public class GLCanvasImpl implements GLCanvas {

    private static final float OPAQUE_ALPHA = 0.95f;

    private static final int OFFSET_FILL_RECT = 0;
    private static final int OFFSET_DRAW_LINE = 4;
    private static final int OFFSET_DRAW_RECT = 6;
    private static final float[] BOX_COORDINATES = { 0, 0, 1, 0, 0, 1, 1, 1, // used for filling a rectangle
            0, 0, 1, 1, // used for drawing a line
            0, 0, 0, 1, 1, 1, 1, 0 }; // used for drawing the outline of a rectangle

    final GL11 mGL;

    private final float mMatrixValues[] = new float[16];
    private final float mTextureMatrixValues[] = new float[16];

    private int mBoxCoords;

    private final GLState mGLState;

    private float mAlpha;
    private final ArrayList<ConfigState> mRestoreStack = new ArrayList<ConfigState>();
    private ConfigState mRecycledRestoreAction;

    private final RectF mDrawTextureSourceRect = new RectF();
    private final RectF mDrawTextureTargetRect = new RectF();
    private final float[] mTempMatrix = new float[32];
    private final IntArray mUnboundTextures = new IntArray();
    private final IntArray mDeleteBuffers = new IntArray();
    private int mScreenWidth;
    private int mScreenHeight;
    private final boolean mBlendEnabled = true;

    private final GLClipHelper mClipper;

    public GLCanvasImpl(final GL11 gl) {
        mGL = gl;
        mGLState = new GLState(gl);
        mClipper = new GLClipHelper(this);
        initialize();
    }

    @Override
    public void setSize(final int width, final int height) {
        mScreenWidth = width;
        mScreenHeight = height;
        mAlpha = 1.0f;

        final GL11 gl = mGL;
        gl.glViewport(0, 0, width, height);
        gl.glMatrixMode(GL10.GL_PROJECTION);
        gl.glLoadIdentity();
        GLU.gluOrtho2D(gl, 0, width, 0, height);

        gl.glMatrixMode(GL10.GL_MODELVIEW);
        gl.glLoadIdentity();

        final float matrix[] = mMatrixValues;
        Matrix.setIdentityM(matrix, 0);
        // to match the graphic coordinate system in android, we flip it vertically.
        Matrix.translateM(matrix, 0, 0, height, 0);
        Matrix.scaleM(matrix, 0, 1, -1, 1);
    }

    public int getScreenHeight() {
        return mScreenHeight;
    }

    public int getScreenWidth() {
        return mScreenWidth;
    }

    @Override
    public void setAlpha(final float alpha) {
        mAlpha = alpha;
    }

    @Override
    public float getAlpha() {
        return mAlpha;
    }

    @Override
    public void multiplyAlpha(final float alpha) {
        mAlpha *= alpha;
    }

    private static ByteBuffer allocateDirectNativeOrderBuffer(final int size) {
        return ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder());
    }

    private void initialize() {
        final GL11 gl = mGL;

        // First create an nio buffer, then create a VBO from it.
        final int size = BOX_COORDINATES.length * Float.SIZE / Byte.SIZE;
        final FloatBuffer xyBuffer = allocateDirectNativeOrderBuffer(size).asFloatBuffer();
        xyBuffer.put(BOX_COORDINATES, 0, BOX_COORDINATES.length).position(0);

        final int[] name = new int[1];
        GLId.glGenBuffers(1, name, 0);
        mBoxCoords = name[0];

        gl.glBindBuffer(GL11.GL_ARRAY_BUFFER, mBoxCoords);
        gl.glBufferData(GL11.GL_ARRAY_BUFFER, xyBuffer.capacity() * (Float.SIZE / Byte.SIZE), xyBuffer,
                GL11.GL_STATIC_DRAW);

        gl.glVertexPointer(2, GL10.GL_FLOAT, 0, 0);
        gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, 0);

        // Enable the texture coordinate array for Texture 1
        gl.glClientActiveTexture(GL10.GL_TEXTURE1);
        gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, 0);
        gl.glClientActiveTexture(GL10.GL_TEXTURE0);
        gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);

        gl.glHint(GL10.GL_LINE_SMOOTH_HINT, GL10.GL_NICEST);
        gl.glHint(GL10.GL_POLYGON_SMOOTH_HINT, GL10.GL_NICEST);

        gl.glEnable(GL10.GL_LINE_SMOOTH);
        gl.glLineWidth(1.5f);
        gl.glEnable(GL10.GL_BLEND);
        gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
        // mMatrixValues and mAlpha will be initialized in setSize()
    }

    @Override
    public void setClipRect(final RectF bounds) {
        mClipper.setClipRect(bounds);
    }

    @Override
    public void setClipRect(final float left, final float top, final float width, final float height) {
        mClipper.setClipRect(left, top, width, height);
    }

    @Override
    public void setClipPath(final PointF... path) {
        mClipper.setClipPath(path);
    }

    @Override
    public void clearClipRect() {
        mClipper.clearClipRect();
    }

    @Override
    public void drawRect(final float x, final float y, final float width, final float height, final Paint paint) {
        final GL11 gl = mGL;

        mGLState.setColorMode(paint.getColor(), mAlpha);
        mGLState.setLineWidth(paint.getStrokeWidth());

        saveTransform();
        translate(x, y);
        scale(width, height, 1);

        gl.glLoadMatrixf(mMatrixValues, 0);
        gl.glDrawArrays(GL10.GL_LINE_LOOP, OFFSET_DRAW_RECT, 4);

        restoreTransform();
    }

    @Override
    public void drawRect(final RectF r, final Paint paint) {
        final GL11 gl = mGL;

        mGLState.setColorMode(paint.getColor(), mAlpha);
        mGLState.setLineWidth(paint.getStrokeWidth());

        saveTransform();
        translate(r.left, r.top);
        scale(r.width(), r.height(), 1);

        gl.glLoadMatrixf(mMatrixValues, 0);
        gl.glDrawArrays(GL10.GL_LINE_LOOP, OFFSET_DRAW_RECT, 4);

        restoreTransform();
    }

    @Override
    public void drawLine(final float x1, final float y1, final float x2, final float y2, final Paint paint) {
        final GL11 gl = mGL;

        mGLState.setColorMode(paint.getColor(), mAlpha);
        mGLState.setLineWidth(paint.getStrokeWidth());

        saveTransform();
        translate(x1, y1);
        scale(x2 - x1, y2 - y1, 1);

        gl.glLoadMatrixf(mMatrixValues, 0);
        gl.glDrawArrays(GL10.GL_LINE_STRIP, OFFSET_DRAW_LINE, 2);

        restoreTransform();
    }

    @Override
    public void fillRect(final float x, final float y, final float width, final float height, final int color) {
        mGLState.setColorMode(color, mAlpha);
        final GL11 gl = mGL;

        saveTransform();
        translate(x, y);
        scale(width, height, 1);

        gl.glLoadMatrixf(mMatrixValues, 0);
        gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, OFFSET_FILL_RECT, 4);

        restoreTransform();
    }

    @Override
    public void fillRect(final RectF r, final Paint p) {
        final int color = p.getColor();
        fillRect(r, color);
    }

    public void fillRect(final RectF r, final int color) {
        mGLState.setColorMode(color, mAlpha);

        final GL11 gl = mGL;

        saveTransform();
        translate(r.left, r.top);
        scale(r.width(), r.height(), 1);

        gl.glLoadMatrixf(mMatrixValues, 0);
        gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, OFFSET_FILL_RECT, 4);

        restoreTransform();
    }

    @Override
    public void fillPoly(final int color, final PointF... path) {
        mGLState.setColorMode(color, mAlpha);
        final GL11 gl = mGL;

        final int bytes = 2 * path.length * Float.SIZE;
        final FloatBuffer xyBuffer = allocateDirectNativeOrderBuffer(bytes).asFloatBuffer();
        for (int i = 0; i < path.length; i++) {
            xyBuffer.put(path[i].x / mScreenWidth);
            xyBuffer.put(path[i].y / mScreenHeight);
        }
        xyBuffer.position(0);

        saveTransform();
        translate(0, 0);
        scale(mScreenWidth, mScreenHeight, 1);
        gl.glLoadMatrixf(mMatrixValues, 0);

        final int[] name = new int[1];
        GLId.glGenBuffers(1, name, 0);

        gl.glBindBuffer(GL11.GL_ARRAY_BUFFER, name[0]);
        gl.glBufferData(GL11.GL_ARRAY_BUFFER, bytes, xyBuffer, GL11.GL_DYNAMIC_DRAW);
        gl.glVertexPointer(2, GL10.GL_FLOAT, 0, 0);
        gl.glDrawArrays(GL10.GL_TRIANGLE_FAN, 0, path.length);

        restoreTransform();

        gl.glBindBuffer(GL11.GL_ARRAY_BUFFER, mBoxCoords);
        gl.glVertexPointer(2, GL10.GL_FLOAT, 0, 0);
        gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, 0);
    }

    @Override
    public void drawPoly(final int color, final PointF... path) {
        mGLState.setColorMode(color, mAlpha);
        mGLState.setLineWidth(1.5f);
        final GL11 gl = mGL;

        final int bytes = 2 * path.length * Float.SIZE;
        final FloatBuffer xyBuffer = allocateDirectNativeOrderBuffer(bytes).asFloatBuffer();
        for (int i = 0; i < path.length; i++) {
            xyBuffer.put(path[i].x / mScreenWidth);
            xyBuffer.put(path[i].y / mScreenHeight);
        }
        xyBuffer.position(0);

        saveTransform();
        translate(0, 0);
        scale(mScreenWidth, mScreenHeight, 1);
        gl.glLoadMatrixf(mMatrixValues, 0);

        final int[] name = new int[1];
        GLId.glGenBuffers(1, name, 0);

        gl.glBindBuffer(GL11.GL_ARRAY_BUFFER, name[0]);
        gl.glBufferData(GL11.GL_ARRAY_BUFFER, bytes, xyBuffer, GL11.GL_DYNAMIC_DRAW);
        gl.glVertexPointer(2, GL10.GL_FLOAT, 0, 0);
        gl.glDrawArrays(GL10.GL_LINE_LOOP, 0, path.length);

        restoreTransform();

        gl.glBindBuffer(GL11.GL_ARRAY_BUFFER, mBoxCoords);
        gl.glVertexPointer(2, GL10.GL_FLOAT, 0, 0);
        gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, 0);
    }

    @Override
    public void translate(final float x, final float y, final float z) {
        Matrix.translateM(mMatrixValues, 0, x, y, z);
    }

    // This is a faster version of translate(x, y, z) because
    // (1) we knows z = 0, (2) we inline the Matrix.translateM call,
    // (3) we unroll the loop
    @Override
    public void translate(final float x, final float y) {
        final float[] m = mMatrixValues;
        m[12] += m[0] * x + m[4] * y;
        m[13] += m[1] * x + m[5] * y;
        m[14] += m[2] * x + m[6] * y;
        m[15] += m[3] * x + m[7] * y;
    }

    @Override
    public void scale(final float sx, final float sy, final float sz) {
        Matrix.scaleM(mMatrixValues, 0, sx, sy, sz);
    }

    @Override
    public void rotate(final float angle, final float x, final float y, final float z) {
        if (angle == 0) {
            return;
        }
        final float[] temp = mTempMatrix;
        Matrix.setRotateM(temp, 0, angle, x, y, z);
        Matrix.multiplyMM(temp, 16, mMatrixValues, 0, temp, 0);
        System.arraycopy(temp, 16, mMatrixValues, 0, 16);
    }

    @Override
    public void multiplyMatrix(final float matrix[], final int offset) {
        final float[] temp = mTempMatrix;
        Matrix.multiplyMM(temp, 0, mMatrixValues, 0, matrix, offset);
        System.arraycopy(temp, 0, mMatrixValues, 0, 16);
    }

    private void textureRect(final float x, final float y, final float width, final float height) {
        final GL11 gl = mGL;

        saveTransform();
        translate(x, y);
        scale(width, height, 1);
        gl.glColor4f(1, 1, 1, 1);
        gl.glLoadMatrixf(mMatrixValues, 0);
        gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, OFFSET_FILL_RECT, 4);

        restoreTransform();
    }

    private void drawBoundTexture(final BasicTexture texture, final int x, final int y, final int width,
            final int height) {
        final float w = (float) texture.getWidth() / texture.getTextureWidth();
        final float h = (float) texture.getHeight() / texture.getTextureHeight();
        setTextureCoords(0, 0, w, h);
        textureRect(x, y, width, height);
    }

    @Override
    public void drawTexture(final BasicTexture texture, final int x, final int y, final int width, final int height) {
        drawTexture(texture, x, y, width, height, mAlpha);
    }

    private void drawTexture(final BasicTexture texture, final int x, final int y, final int width, final int height,
            final float alpha) {
        if (width <= 0 || height <= 0) {
            return;
        }

        mGLState.setBlendEnabled(mBlendEnabled && (!texture.isOpaque() || alpha < OPAQUE_ALPHA));
        if (!bindTexture(texture)) {
            return;
        }
        mGLState.setTextureAlpha(alpha);
        drawBoundTexture(texture, x, y, width, height);
    }

    @Override
    public boolean drawTexture(final BasicTexture texture, RectF source, RectF target) {
        if (target.width() <= 0 || target.height() <= 0) {
            return false;
        }

        // Copy the input to avoid changing it.
        mDrawTextureSourceRect.set(source);
        mDrawTextureTargetRect.set(target);
        source = mDrawTextureSourceRect;
        target = mDrawTextureTargetRect;

        mGLState.setBlendEnabled(mBlendEnabled && (!texture.isOpaque() || mAlpha < OPAQUE_ALPHA));
        if (!bindTexture(texture)) {
            return false;
        }
        convertCoordinate(source, target, texture);
        setTextureCoords(source);
        mGLState.setTextureAlpha(mAlpha);
        textureRect(target.left, target.top, target.width(), target.height());

        return true;
    }

    @Override
    public void drawTexture(final BasicTexture texture, final float[] mTextureTransform, final int x, final int y,
            final int w, final int h) {
        mGLState.setBlendEnabled(mBlendEnabled && (!texture.isOpaque() || mAlpha < OPAQUE_ALPHA));
        if (!bindTexture(texture)) {
            return;
        }
        setTextureCoords(mTextureTransform);
        mGLState.setTextureAlpha(mAlpha);
        textureRect(x, y, w, h);
    }

    // This function changes the source coordinate to the texture coordinates.
    // It also clips the source and target coordinates if it is beyond the
    // bound of the texture.
    private void convertCoordinate(final RectF source, final RectF target, final BasicTexture texture) {

        final int width = texture.getWidth();
        final int height = texture.getHeight();
        final int texWidth = texture.getTextureWidth();
        final int texHeight = texture.getTextureHeight();
        // Convert to texture coordinates
        source.left /= texWidth;
        source.right /= texWidth;
        source.top /= texHeight;
        source.bottom /= texHeight;

        // Clip if the rendering range is beyond the bound of the texture.
        final float xBound = (float) width / texWidth;
        if (source.right > xBound) {
            target.right = target.left + target.width() * (xBound - source.left) / source.width();
            source.right = xBound;
        }
        final float yBound = (float) height / texHeight;
        if (source.bottom > yBound) {
            target.bottom = target.top + target.height() * (yBound - source.top) / source.height();
            source.bottom = yBound;
        }
    }

    private boolean bindTexture(final BasicTexture texture) {
        if (!texture.onBind(this)) {
            return false;
        }
        final int target = texture.getTarget();
        mGLState.setTextureTarget(target);
        mGL.glBindTexture(target, texture.getId());
        return true;
    }

    private static class GLState {

        private final GL11 mGL;

        private int mTexEnvMode = GL11.GL_REPLACE;
        private float mTextureAlpha = 1.0f;
        private int mTextureTarget = GL11.GL_TEXTURE_2D;
        private boolean mBlendEnabled = true;
        private float mLineWidth = 1.0f;

        public GLState(final GL11 gl) {
            mGL = gl;

            // Disable unused state
            gl.glDisable(GL10.GL_LIGHTING);

            // Enable used features
            gl.glEnable(GL10.GL_DITHER);

            gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
            gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
            gl.glEnable(GL10.GL_TEXTURE_2D);

            gl.glTexEnvf(GL10.GL_TEXTURE_ENV, GL10.GL_TEXTURE_ENV_MODE, GL10.GL_REPLACE);

            // Set the background color
            gl.glClearColor(0f, 0f, 0f, 0f);
            gl.glClearStencil(0);

            gl.glEnable(GL10.GL_BLEND);
            gl.glBlendFunc(GL10.GL_ONE, GL10.GL_ONE_MINUS_SRC_ALPHA);

            // We use 565 or 8888 format, so set the alignment to 2 bytes/pixel.
            gl.glPixelStorei(GL10.GL_UNPACK_ALIGNMENT, 2);
        }

        public void setTexEnvMode(final int mode) {
            if (mTexEnvMode == mode) {
                return;
            }
            mTexEnvMode = mode;
            mGL.glTexEnvf(GL10.GL_TEXTURE_ENV, GL10.GL_TEXTURE_ENV_MODE, mode);
        }

        public void setLineWidth(final float width) {
            if (mLineWidth == width) {
                return;
            }
            mLineWidth = width;
            mGL.glLineWidth(width);
        }

        public void setTextureAlpha(final float alpha) {
            if (mTextureAlpha == alpha) {
                return;
            }
            mTextureAlpha = alpha;
            if (alpha >= OPAQUE_ALPHA) {
                // The alpha is need for those texture without alpha channel
                mGL.glColor4f(1, 1, 1, 1);
                setTexEnvMode(GL10.GL_REPLACE);
            } else {
                mGL.glColor4f(alpha, alpha, alpha, alpha);
                setTexEnvMode(GL10.GL_MODULATE);
            }
        }

        public void setColorMode(final int color, final float alpha) {
            setBlendEnabled(!MathUtils.isOpaque(color) || alpha < OPAQUE_ALPHA);

            // Set mTextureAlpha to an invalid value, so that it will reset
            // again in setTextureAlpha(float) later.
            mTextureAlpha = -1.0f;

            setTextureTarget(0);

            final float prealpha = (color >>> 24) * alpha * 65535f / 255f / 255f;
            mGL.glColor4x(Math.round(((color >> 16) & 0xFF) * prealpha), Math.round(((color >> 8) & 0xFF) * prealpha),
                    Math.round((color & 0xFF) * prealpha), Math.round(255 * prealpha));
        }

        // target is a value like GL_TEXTURE_2D. If target = 0, texturing is disabled.
        public void setTextureTarget(final int target) {
            if (mTextureTarget == target) {
                return;
            }
            if (mTextureTarget != 0) {
                mGL.glDisable(mTextureTarget);
            }
            mTextureTarget = target;
            if (mTextureTarget != 0) {
                mGL.glEnable(mTextureTarget);
            }
        }

        public void setBlendEnabled(final boolean enabled) {
            if (mBlendEnabled == enabled) {
                return;
            }
            mBlendEnabled = enabled;
            if (enabled) {
                mGL.glEnable(GL10.GL_BLEND);
            } else {
                mGL.glDisable(GL10.GL_BLEND);
            }
        }
    }

    @Override
    public GL11 getGLInstance() {
        return mGL;
    }

    @Override
    public void clearBuffer() {
        mGL.glClear(GL10.GL_COLOR_BUFFER_BIT);
    }

    @Override
    public void clearBuffer(final Paint p) {
        clearBuffer(p.getColor());
    }

    @Override
    public void clearBuffer(final int color) {
        final GL11 gl = mGL;
        final float prealpha = (color >>> 24) / 255f;
        final float red = ((color >> 16) & 0xFF) * prealpha;
        final float green = ((color >> 8) & 0xFF) * prealpha;
        final float blue = (color & 0xFF) * prealpha;
        final float alpha = 255 * prealpha;

        gl.glClearColor(red, green, blue, alpha);
        gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
    }

    private void setTextureCoords(final RectF source) {
        setTextureCoords(source.left, source.top, source.right, source.bottom);
    }

    private void setTextureCoords(final float left, final float top, final float right, final float bottom) {
        mGL.glMatrixMode(GL10.GL_TEXTURE);
        mTextureMatrixValues[0] = right - left;
        mTextureMatrixValues[5] = bottom - top;
        mTextureMatrixValues[10] = 1;
        mTextureMatrixValues[12] = left;
        mTextureMatrixValues[13] = top;
        mTextureMatrixValues[15] = 1;
        mGL.glLoadMatrixf(mTextureMatrixValues, 0);
        mGL.glMatrixMode(GL10.GL_MODELVIEW);
    }

    private void setTextureCoords(final float[] mTextureTransform) {
        mGL.glMatrixMode(GL10.GL_TEXTURE);
        mGL.glLoadMatrixf(mTextureTransform, 0);
        mGL.glMatrixMode(GL10.GL_MODELVIEW);
    }

    // unloadTexture and deleteBuffer can be called from the finalizer thread,
    // so we synchronized on the mUnboundTextures object.
    @Override
    public boolean unloadTexture(final BasicTexture t) {
        synchronized (mUnboundTextures) {
            if (!t.isLoaded()) {
                return false;
            }
            mUnboundTextures.add(t.mId);
            return true;
        }
    }

    @Override
    public void deleteBuffer(final int bufferId) {
        synchronized (mUnboundTextures) {
            mDeleteBuffers.add(bufferId);
        }
    }

    @Override
    public void deleteRecycledResources() {
        synchronized (mUnboundTextures) {
            IntArray ids = mUnboundTextures;
            if (ids.size() > 0) {
                GLId.glDeleteTextures(mGL, ids.size(), ids.getInternalArray(), 0);
                ids.clear();
            }

            ids = mDeleteBuffers;
            if (ids.size() > 0) {
                GLId.glDeleteBuffers(mGL, ids.size(), ids.getInternalArray(), 0);
                ids.clear();
            }
        }
    }

    @Override
    public void save() {
        save(SAVE_FLAG_ALL);
    }

    @Override
    public void save(final int saveFlags) {
        final ConfigState config = obtainRestoreConfig();

        if ((saveFlags & SAVE_FLAG_ALPHA) != 0) {
            config.mAlpha = mAlpha;
        } else {
            config.mAlpha = -1;
        }

        if ((saveFlags & SAVE_FLAG_MATRIX) != 0) {
            System.arraycopy(mMatrixValues, 0, config.mMatrix, 0, 16);
        } else {
            config.mMatrix[0] = Float.NEGATIVE_INFINITY;
        }

        mRestoreStack.add(config);
    }

    @Override
    public void restore() {
        if (mRestoreStack.isEmpty()) {
            throw new IllegalStateException();
        }
        final ConfigState config = mRestoreStack.remove(mRestoreStack.size() - 1);
        config.restore(this);
        freeRestoreConfig(config);
    }

    private void freeRestoreConfig(final ConfigState action) {
        action.mNextFree = mRecycledRestoreAction;
        mRecycledRestoreAction = action;
    }

    private ConfigState obtainRestoreConfig() {
        if (mRecycledRestoreAction != null) {
            final ConfigState result = mRecycledRestoreAction;
            mRecycledRestoreAction = result.mNextFree;
            return result;
        }
        return new ConfigState();
    }

    private static class ConfigState {

        float mAlpha;
        float mMatrix[] = new float[16];
        ConfigState mNextFree;

        public void restore(final GLCanvasImpl canvas) {
            if (mAlpha >= 0) {
                canvas.setAlpha(mAlpha);
            }
            if (mMatrix[0] != Float.NEGATIVE_INFINITY) {
                System.arraycopy(mMatrix, 0, canvas.mMatrixValues, 0, 16);
            }
        }
    }

    private void saveTransform() {
        System.arraycopy(mMatrixValues, 0, mTempMatrix, 0, 16);
    }

    private void restoreTransform() {
        System.arraycopy(mTempMatrix, 0, mMatrixValues, 0, 16);
    }
}
