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


package android.filterfw.core;

import android.filterfw.core.Frame;
import android.filterfw.core.FrameFormat;
import android.filterfw.core.FrameManager;
import android.filterfw.core.NativeFrame;
import android.filterfw.core.StopWatchMap;
import android.graphics.Bitmap;
import android.opengl.GLES20;
import android.graphics.Rect;

import java.nio.ByteBuffer;

class GLFrameTimer {

    private static StopWatchMap mTimer = null;

    public static StopWatchMap get() {
        if (mTimer == null) {
            mTimer = new StopWatchMap();
        }
        return mTimer;
    }

}

/**
 * @hide
 */
public class GLFrame extends Frame {

    // GL-related binding types
    public final static int EXISTING_TEXTURE_BINDING = 100;
    public final static int EXISTING_FBO_BINDING     = 101;
    public final static int NEW_TEXTURE_BINDING      = 102; // TODO: REMOVE THIS
    public final static int NEW_FBO_BINDING          = 103; // TODO: REMOVE THIS
    public final static int EXTERNAL_TEXTURE         = 104;

    private int glFrameId = -1;

    /**
     * Flag whether we own the texture or not. If we do not, we must be careful when caching or
     * storing the frame, as the user may delete, and regenerate it.
     */
    private boolean mOwnsTexture = true;

    /**
     * Keep a reference to the GL environment, so that it does not get deallocated while there
     * are still frames living in it.
     */
    private GLEnvironment mGLEnvironment;

    GLFrame(FrameFormat format, FrameManager frameManager) {
        super(format, frameManager);
    }

    GLFrame(FrameFormat format, FrameManager frameManager, int bindingType, long bindingId) {
        super(format, frameManager, bindingType, bindingId);
    }

    void init(GLEnvironment glEnv) {
        FrameFormat format = getFormat();
        mGLEnvironment = glEnv;

        // Check that we have a valid format
        if (format.getBytesPerSample() != 4) {
            throw new IllegalArgumentException("GL frames must have 4 bytes per sample!");
        } else if (format.getDimensionCount() != 2) {
            throw new IllegalArgumentException("GL frames must be 2-dimensional!");
        } else if (getFormat().getSize() < 0) {
            throw new IllegalArgumentException("Initializing GL frame with zero size!");
        }

        // Create correct frame
        int bindingType = getBindingType();
        boolean reusable = true;
        if (bindingType == Frame.NO_BINDING) {
            initNew(false);
        } else if (bindingType == EXTERNAL_TEXTURE) {
            initNew(true);
            reusable = false;
        } else if (bindingType == EXISTING_TEXTURE_BINDING) {
            initWithTexture((int)getBindingId());
        } else if (bindingType == EXISTING_FBO_BINDING) {
            initWithFbo((int)getBindingId());
        } else if (bindingType == NEW_TEXTURE_BINDING) {
            initWithTexture((int)getBindingId());
        } else if (bindingType == NEW_FBO_BINDING) {
            initWithFbo((int)getBindingId());
        } else {
            throw new RuntimeException("Attempting to create GL frame with unknown binding type "
                + bindingType + "!");
        }
        setReusable(reusable);
    }

    private void initNew(boolean isExternal) {
        if (isExternal) {
            if (!nativeAllocateExternal(mGLEnvironment)) {
                throw new RuntimeException("Could not allocate external GL frame!");
            }
        } else {
            if (!nativeAllocate(mGLEnvironment, getFormat().getWidth(), getFormat().getHeight())) {
                throw new RuntimeException("Could not allocate GL frame!");
            }
        }
    }

    private void initWithTexture(int texId) {
        int width = getFormat().getWidth();
        int height = getFormat().getHeight();
        if (!nativeAllocateWithTexture(mGLEnvironment, texId, width, height)) {
            throw new RuntimeException("Could not allocate texture backed GL frame!");
        }
        mOwnsTexture = false;
        markReadOnly();
    }

    private void initWithFbo(int fboId) {
        int width = getFormat().getWidth();
        int height = getFormat().getHeight();
        if (!nativeAllocateWithFbo(mGLEnvironment, fboId, width, height)) {
            throw new RuntimeException("Could not allocate FBO backed GL frame!");
        }
    }

    void flushGPU(String message) {
        StopWatchMap timer = GLFrameTimer.get();
        if (timer.LOG_MFF_RUNNING_TIMES) {
          timer.start("glFinish " + message);
          GLES20.glFinish();
          timer.stop("glFinish " + message);
        }
    }

    @Override
    protected synchronized boolean hasNativeAllocation() {
        return glFrameId != -1;
    }

    @Override
    protected synchronized void releaseNativeAllocation() {
        nativeDeallocate();
        glFrameId = -1;
    }

    public GLEnvironment getGLEnvironment() {
        return mGLEnvironment;
    }

    @Override
    public Object getObjectValue() {
        assertGLEnvValid();
        return ByteBuffer.wrap(getNativeData());
    }

    @Override
    public void setInts(int[] ints) {
        assertFrameMutable();
        assertGLEnvValid();
        if (!setNativeInts(ints)) {
            throw new RuntimeException("Could not set int values for GL frame!");
        }
    }

    @Override
    public int[] getInts() {
        assertGLEnvValid();
        flushGPU("getInts");
        return getNativeInts();
    }

    @Override
    public void setFloats(float[] floats) {
        assertFrameMutable();
        assertGLEnvValid();
        if (!setNativeFloats(floats)) {
            throw new RuntimeException("Could not set int values for GL frame!");
        }
    }

    @Override
    public float[] getFloats() {
        assertGLEnvValid();
        flushGPU("getFloats");
        return getNativeFloats();
    }

    @Override
    public void setData(ByteBuffer buffer, int offset, int length) {
        assertFrameMutable();
        assertGLEnvValid();
        byte[] bytes = buffer.array();
        if (getFormat().getSize() != bytes.length) {
            throw new RuntimeException("Data size in setData does not match GL frame size!");
        } else if (!setNativeData(bytes, offset, length)) {
            throw new RuntimeException("Could not set GL frame data!");
        }
    }

    @Override
    public ByteBuffer getData() {
        assertGLEnvValid();
        flushGPU("getData");
        return ByteBuffer.wrap(getNativeData());
    }

    @Override
    public void setBitmap(Bitmap bitmap) {
        assertFrameMutable();
        assertGLEnvValid();
        if (getFormat().getWidth()  != bitmap.getWidth() ||
            getFormat().getHeight() != bitmap.getHeight()) {
            throw new RuntimeException("Bitmap dimensions do not match GL frame dimensions!");
        } else {
            Bitmap rgbaBitmap = convertBitmapToRGBA(bitmap);
            if (!setNativeBitmap(rgbaBitmap, rgbaBitmap.getByteCount())) {
                throw new RuntimeException("Could not set GL frame bitmap data!");
            }
        }
    }

    @Override
    public Bitmap getBitmap() {
        assertGLEnvValid();
        flushGPU("getBitmap");
        Bitmap result = Bitmap.createBitmap(getFormat().getWidth(),
                                            getFormat().getHeight(),
                                            Bitmap.Config.ARGB_8888);
        if (!getNativeBitmap(result)) {
            throw new RuntimeException("Could not get bitmap data from GL frame!");
        }
        return result;
    }

    @Override
    public void setDataFromFrame(Frame frame) {
        assertGLEnvValid();

        // Make sure frame fits
        if (getFormat().getSize() < frame.getFormat().getSize()) {
            throw new RuntimeException(
                "Attempting to assign frame of size " + frame.getFormat().getSize() + " to " +
                "smaller GL frame of size " + getFormat().getSize() + "!");
        }

        // Invoke optimized implementations if possible
        if (frame instanceof NativeFrame) {
            nativeCopyFromNative((NativeFrame)frame);
        } else if (frame instanceof GLFrame) {
            nativeCopyFromGL((GLFrame)frame);
        } else if (frame instanceof SimpleFrame) {
            setObjectValue(frame.getObjectValue());
        } else {
            super.setDataFromFrame(frame);
        }
    }

    public void setViewport(int x, int y, int width, int height) {
        assertFrameMutable();
        setNativeViewport(x, y, width, height);
    }

    public void setViewport(Rect rect) {
        assertFrameMutable();
        setNativeViewport(rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top);
    }

    public void generateMipMap() {
        assertFrameMutable();
        assertGLEnvValid();
        if (!generateNativeMipMap()) {
            throw new RuntimeException("Could not generate mip-map for GL frame!");
        }
    }

    public void setTextureParameter(int param, int value) {
        assertFrameMutable();
        assertGLEnvValid();
        if (!setNativeTextureParam(param, value)) {
            throw new RuntimeException("Could not set texture value " + param + " = " + value + " " +
                                       "for GLFrame!");
        }
    }

    public int getTextureId() {
        return getNativeTextureId();
    }

    public int getFboId() {
        return getNativeFboId();
    }

    public void focus() {
        if (!nativeFocus()) {
            throw new RuntimeException("Could not focus on GLFrame for drawing!");
        }
    }

    @Override
    public String toString() {
        return "GLFrame id: " + glFrameId + " (" + getFormat() + ") with texture ID "
            + getTextureId() + ", FBO ID " + getFboId();
    }

    @Override
    protected void reset(FrameFormat newFormat) {
        if (!nativeResetParams()) {
            throw new RuntimeException("Could not reset GLFrame texture parameters!");
        }
        super.reset(newFormat);
    }

    @Override
    protected void onFrameStore() {
        if (!mOwnsTexture) {
            // Detach texture from FBO in case user manipulates it.
            nativeDetachTexFromFbo();
        }
    }

    @Override
    protected void onFrameFetch() {
        if (!mOwnsTexture) {
            // Reattach texture to FBO when using frame again. This may reallocate the texture
            // in case it has become invalid.
            nativeReattachTexToFbo();
        }
    }

    private void assertGLEnvValid() {
        if (!mGLEnvironment.isContextActive()) {
            if (GLEnvironment.isAnyContextActive()) {
                throw new RuntimeException("Attempting to access " + this + " with foreign GL " +
                    "context active!");
            } else {
                throw new RuntimeException("Attempting to access " + this + " with no GL context " +
                    " active!");
            }
        }
    }

    static {
        System.loadLibrary("filterfw");
    }

    private native boolean nativeAllocate(GLEnvironment env, int width, int height);

    private native boolean nativeAllocateWithTexture(GLEnvironment env,
                                               int textureId,
                                               int width,
                                               int height);

    private native boolean nativeAllocateWithFbo(GLEnvironment env,
                                           int fboId,
                                           int width,
                                           int height);

    private native boolean nativeAllocateExternal(GLEnvironment env);

    private native boolean nativeDeallocate();

    private native boolean setNativeData(byte[] data, int offset, int length);

    private native byte[] getNativeData();

    private native boolean setNativeInts(int[] ints);

    private native boolean setNativeFloats(float[] floats);

    private native int[] getNativeInts();

    private native float[] getNativeFloats();

    private native boolean setNativeBitmap(Bitmap bitmap, int size);

    private native boolean getNativeBitmap(Bitmap bitmap);

    private native boolean setNativeViewport(int x, int y, int width, int height);

    private native int getNativeTextureId();

    private native int getNativeFboId();

    private native boolean generateNativeMipMap();

    private native boolean setNativeTextureParam(int param, int value);

    private native boolean nativeResetParams();

    private native boolean nativeCopyFromNative(NativeFrame frame);

    private native boolean nativeCopyFromGL(GLFrame frame);

    private native boolean nativeFocus();

    private native boolean nativeReattachTexToFbo();

    private native boolean nativeDetachTexFromFbo();
}
