/*
 * Copyright 2013 Dolphin Emulator Project
 * SPDX-License-Identifier: GPL-2.0-or-later
 */

package org.dolphinemu.dolphinemu.utils;

import android.opengl.GLES30;

import org.dolphinemu.dolphinemu.NativeLibrary;

import javax.microedition.khronos.egl.EGL10;
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;

/**
 * Utility class that abstracts all the stuff about
 * EGL initialization out of the way if all that is
 * wanted is to query the underlying GL API for information.
 */
public final class EGLHelper
{
  private final EGL10 mEGL;
  private final EGLDisplay mDisplay;
  private EGLConfig[] mEGLConfigs;
  private EGLContext mEGLContext;
  private EGLSurface mEGLSurface;
  private GL10 mGL;

  // GL support flags
  private boolean supportGL;
  private boolean supportGLES2;
  private boolean supportGLES3;

  // Renderable type bitmasks
  public static final int EGL_OPENGL_ES_BIT = 0x0001;
  public static final int EGL_OPENGL_ES2_BIT = 0x0004;
  public static final int EGL_OPENGL_BIT = 0x0008;
  public static final int EGL_OPENGL_ES3_BIT_KHR = 0x0040;

  // API types
  public static final int EGL_OPENGL_ES_API = 0x30A0;
  public static final int EGL_OPENGL_API = 0x30A2;

  /**
   * Constructor
   * <p>
   * Initializes the underlying {@link EGLSurface} with a width and height of 1.
   * This is useful if all you need to use this class for is to query information
   * from specific API contexts.
   *
   * @param renderableType Bitmask indicating which types of client API contexts
   *                       the framebuffer config must support.
   */
  public EGLHelper(int renderableType)
  {
    this(1, 1, renderableType);
  }

  /**
   * Constructor
   *
   * @param width          Width of the underlying {@link EGLSurface}.
   * @param height         Height of the underlying {@link EGLSurface}.
   * @param renderableType Bitmask indicating which types of client API contexts
   *                       the framebuffer config must support.
   */
  public EGLHelper(int width, int height, int renderableType)
  {
    // Initialize handle to an EGL display.
    mEGL = (EGL10) EGLContext.getEGL();
    mDisplay = mEGL.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);

    // If a display is present, initialize EGL.
    if (mDisplay != EGL10.EGL_NO_DISPLAY)
    {
      int[] version = new int[2];
      if (mEGL.eglInitialize(mDisplay, version))
      {
        // Detect supported GL APIs, initialize configs, etc.
        detect();

        // Create context and surface
        create(width, height, renderableType);
      }
      else
      {
        Log.error("[EGLHelper] Error initializing EGL.");
      }
    }
    else
    {
      Log.error("[EGLHelper] Error initializing EGL display.");
    }
  }

  /**
   * Releases all resources associated with this helper.
   * <p>
   * This should be called whenever this helper is no longer needed.
   */
  public void closeHelper()
  {
    mEGL.eglTerminate(mDisplay);
  }

  /**
   * Gets information through EGL.<br/>
   * <p>
   * Index 0: Vendor     <br/>
   * Index 1: Version    <br/>
   * Index 2: Renderer   <br/>
   * Index 3: Extensions <br/>
   *
   * @return information retrieved through EGL.
   */
  public String[] getEGLInfo()
  {
    return new String[]{
            mGL.glGetString(GL10.GL_VENDOR),
            mGL.glGetString(GL10.GL_VERSION),
            mGL.glGetString(GL10.GL_RENDERER),
            mGL.glGetString(GL10.GL_EXTENSIONS),
    };
  }

  /**
   * Whether or not this device supports OpenGL.
   *
   * @return true if this device supports OpenGL; false otherwise.
   */
  public boolean supportsOpenGL()
  {
    return supportGL;
  }

  /**
   * Whether or not this device supports OpenGL ES 2.
   * <br/>
   * Note that if this returns true, then OpenGL ES 1 is also supported.
   *
   * @return true if this device supports OpenGL ES 2; false otherwise.
   */
  public boolean supportsGLES2()
  {
    return supportGLES2;
  }

  /**
   * Whether or not this device supports OpenGL ES 3.
   * <br/>
   * Note that if this returns true, then OpenGL ES 1 and 2 are also supported.
   *
   * @return true if this device supports OpenGL ES 3; false otherwise.
   */
  public boolean supportsGLES3()
  {
    return supportGLES3;
  }

  /**
   * Gets the underlying {@link EGL10} instance.
   *
   * @return the underlying {@link EGL10} instance.
   */
  public EGL10 getEGL()
  {
    return mEGL;
  }

  /**
   * Gets the underlying {@link GL10} instance.
   *
   * @return the underlying {@link GL10} instance.
   */
  public GL10 getGL()
  {
    return mGL;
  }

  /**
   * Gets the underlying {@link EGLDisplay}.
   *
   * @return the underlying {@link EGLDisplay}
   */
  public EGLDisplay getDisplay()
  {
    return mDisplay;
  }

  /**
   * Gets all supported framebuffer configurations for this device.
   *
   * @return all supported framebuffer configurations for this device.
   */
  public EGLConfig[] getConfigs()
  {
    return mEGLConfigs;
  }

  /**
   * Gets the underlying {@link EGLContext}.
   *
   * @return the underlying {@link EGLContext}.
   */
  public EGLContext getContext()
  {
    return mEGLContext;
  }

  /**
   * Gets the underlying {@link EGLSurface}.
   *
   * @return the underlying {@link EGLSurface}.
   */
  public EGLSurface getSurface()
  {
    return mEGLSurface;
  }

  // Detects the specific kind of GL modes that are supported
  private void detect()
  {
    // Get total number of configs available.
    int[] numConfigs = new int[1];
    if (!mEGL.eglGetConfigs(mDisplay, null, 0, numConfigs))
    {
      Log.error("[EGLHelper] Error retrieving number of EGL configs available.");
      return;
    }

    // Now get all the configurations
    mEGLConfigs = new EGLConfig[numConfigs[0]];
    if (!mEGL.eglGetConfigs(mDisplay, mEGLConfigs, mEGLConfigs.length, numConfigs))
    {
      Log.error("[EGLHelper] Error retrieving all EGL configs.");
      return;
    }

    for (EGLConfig mEGLConfig : mEGLConfigs)
    {
      int[] attribVal = new int[1];
      boolean ret =
              mEGL.eglGetConfigAttrib(mDisplay, mEGLConfig, EGL10.EGL_RENDERABLE_TYPE, attribVal);
      if (ret)
      {
        if ((attribVal[0] & EGL_OPENGL_BIT) != 0)
          supportGL = true;

        if ((attribVal[0] & EGL_OPENGL_ES2_BIT) != 0)
          supportGLES2 = true;

        if ((attribVal[0] & EGL_OPENGL_ES3_BIT_KHR) != 0)
          supportGLES3 = true;
      }
    }
  }

  // Creates the context and surface.
  private void create(int width, int height, int renderableType)
  {
    int[] attribs = {
            EGL10.EGL_WIDTH, width,
            EGL10.EGL_HEIGHT, height,
            EGL10.EGL_NONE
    };

    // Initially we just assume GLES2 will be the default context.
    int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
    int[] ctx_attribs = {
            EGL_CONTEXT_CLIENT_VERSION, 2,
            EGL10.EGL_NONE
    };

    // Determine the type of context that will be created
    // and change the attribute arrays accordingly.
    switch (renderableType)
    {
      case EGL_OPENGL_ES_BIT:
        ctx_attribs[1] = 1;
        break;

      case EGL_OPENGL_BIT:
        ctx_attribs[0] = EGL10.EGL_NONE;
        break;

      case EGL_OPENGL_ES3_BIT_KHR:
        ctx_attribs[1] = 3;
        break;

      case EGL_OPENGL_ES2_BIT:
      default: // Fall-back to GLES 2.
        ctx_attribs[1] = 2;
        break;
    }
    if (renderableType == EGL_OPENGL_BIT)
      NativeLibrary.eglBindAPI(EGL_OPENGL_API);
    else
      NativeLibrary.eglBindAPI(EGL_OPENGL_ES_API);

    mEGLContext =
            mEGL.eglCreateContext(mDisplay, mEGLConfigs[0], EGL10.EGL_NO_CONTEXT, ctx_attribs);
    mEGLSurface = mEGL.eglCreatePbufferSurface(mDisplay, mEGLConfigs[0], attribs);
    mEGL.eglMakeCurrent(mDisplay, mEGLSurface, mEGLSurface, mEGLContext);
    mGL = (GL10) mEGLContext.getGL();
  }

  /**
   * Simplified call to {@link GL10#glGetString(int)}
   * <p>
   * Accepts the following constants:
   * <ul>
   * <li>GL_VENDOR - Company responsible for the GL implementation.</li>
   * <li>GL_VERSION - Version or release number.</li>
   * <li>GL_RENDERER - Name of the renderer</li>
   * <li>GL_SHADING_LANGUAGE_VERSION - Version or release number of the shading language </li>
   * </ul>
   *
   * @param glEnum A symbolic constant within {@link GL10}.
   * @return the string information represented by {@code glEnum}.
   */
  public String glGetString(int glEnum)
  {
    return mGL.glGetString(glEnum);
  }

  /**
   * Simplified call to {@link GLES30#glGetStringi(int, int)}
   * <p>
   * Accepts the following constants:
   * <ul>
   * <li>GL_VENDOR - Company responsible for the GL implementation.</li>
   * <li>GL_VERSION - Version or release number.</li>
   * <li>GL_RENDERER - Name of the renderer</li>
   * <li>GL_SHADING_LANGUAGE_VERSION - Version or release number of the shading language </li>
   * <li>GL_EXTENSIONS - Extension string supported by the implementation at {@code index}.</li>
   * </ul>
   *
   * @param glEnum A symbolic GL constant
   * @param index  The index of the string to return.
   * @return the string information represented by {@code glEnum} and {@code index}.
   */
  public String glGetStringi(int glEnum, int index)
  {
    return GLES30.glGetStringi(glEnum, index);
  }

  public boolean SupportsExtension(String extension)
  {
    int[] num_ext = new int[1];
    GLES30.glGetIntegerv(GLES30.GL_NUM_EXTENSIONS, num_ext, 0);

    for (int i = 0; i < num_ext[0]; ++i)
    {
      String ext = GLES30.glGetStringi(GLES30.GL_EXTENSIONS, i);
      if (ext.equals(extension))
        return true;
    }
    return false;
  }

  public int GetVersion()
  {
    int[] major = new int[1];
    int[] minor = new int[1];
    GLES30.glGetIntegerv(GLES30.GL_MAJOR_VERSION, major, 0);
    GLES30.glGetIntegerv(GLES30.GL_MINOR_VERSION, minor, 0);
    return major[0] * 100 + minor[0] * 10;
  }

  /**
   * Simplified call to {@link GL10#glGetIntegerv(int, int[], int)
   *
   * @param glEnum A symbolic GL constant.
   * @return the integer information represented by {@code glEnum}.
   */
  public int glGetInteger(int glEnum)
  {
    int[] val = new int[1];
    mGL.glGetIntegerv(glEnum, val, 0);
    return val[0];
  }
}
