// SPDX-License-Identifier: Unlicense OR MIT

// +build linux windows freebsd openbsd

package egl

import (
	"errors"
	"fmt"
	"runtime"
	"strings"

	"gioui.org/gpu"
	"gioui.org/internal/gl"
	"gioui.org/internal/srgb"
)

type Context struct {
	c             *gl.Functions
	disp          _EGLDisplay
	eglCtx        *eglContext
	eglSurf       _EGLSurface
	width, height int
	refreshFBO    bool
	// For sRGB emulation.
	srgbFBO *srgb.FBO
}

type eglContext struct {
	config      _EGLConfig
	ctx         _EGLContext
	visualID    int
	srgb        bool
	surfaceless bool
}

var (
	nilEGLDisplay       _EGLDisplay
	nilEGLSurface       _EGLSurface
	nilEGLContext       _EGLContext
	nilEGLConfig        _EGLConfig
	EGL_DEFAULT_DISPLAY NativeDisplayType
)

const (
	_EGL_ALPHA_SIZE             = 0x3021
	_EGL_BLUE_SIZE              = 0x3022
	_EGL_CONFIG_CAVEAT          = 0x3027
	_EGL_CONTEXT_CLIENT_VERSION = 0x3098
	_EGL_DEPTH_SIZE             = 0x3025
	_EGL_GL_COLORSPACE_KHR      = 0x309d
	_EGL_GL_COLORSPACE_SRGB_KHR = 0x3089
	_EGL_GREEN_SIZE             = 0x3023
	_EGL_EXTENSIONS             = 0x3055
	_EGL_NATIVE_VISUAL_ID       = 0x302e
	_EGL_NONE                   = 0x3038
	_EGL_OPENGL_ES2_BIT         = 0x4
	_EGL_RED_SIZE               = 0x3024
	_EGL_RENDERABLE_TYPE        = 0x3040
	_EGL_SURFACE_TYPE           = 0x3033
	_EGL_WINDOW_BIT             = 0x4
)

func (c *Context) Release() {
	if c.srgbFBO != nil {
		c.srgbFBO.Release()
		c.srgbFBO = nil
	}
	c.ReleaseSurface()
	if c.eglCtx != nil {
		eglDestroyContext(c.disp, c.eglCtx.ctx)
		c.eglCtx = nil
	}
	c.disp = nilEGLDisplay
}

func (c *Context) Present() error {
	if c.srgbFBO != nil {
		c.srgbFBO.Blit()
	}
	if !eglSwapBuffers(c.disp, c.eglSurf) {
		return fmt.Errorf("eglSwapBuffers failed (%x)", eglGetError())
	}
	if c.srgbFBO != nil {
		c.srgbFBO.AfterPresent()
	}
	return nil
}

func NewContext(disp NativeDisplayType) (*Context, error) {
	if err := loadEGL(); err != nil {
		return nil, err
	}
	eglDisp := eglGetDisplay(disp)
	// eglGetDisplay can return EGL_NO_DISPLAY yet no error
	// (EGL_SUCCESS), in which case a default EGL display might be
	// available.
	if eglDisp == nilEGLDisplay {
		eglDisp = eglGetDisplay(EGL_DEFAULT_DISPLAY)
	}
	if eglDisp == nilEGLDisplay {
		return nil, fmt.Errorf("eglGetDisplay failed: 0x%x", eglGetError())
	}
	eglCtx, err := createContext(eglDisp)
	if err != nil {
		return nil, err
	}
	f, err := gl.NewFunctions(nil)
	if err != nil {
		return nil, err
	}
	c := &Context{
		disp:   eglDisp,
		eglCtx: eglCtx,
		c:      f,
	}
	return c, nil
}

func (c *Context) API() gpu.API {
	return gpu.OpenGL{}
}

func (c *Context) ReleaseSurface() {
	if c.eglSurf == nilEGLSurface {
		return
	}
	// Make sure any in-flight GL commands are complete.
	c.c.Finish()
	c.ReleaseCurrent()
	eglDestroySurface(c.disp, c.eglSurf)
	c.eglSurf = nilEGLSurface
}

func (c *Context) VisualID() int {
	return c.eglCtx.visualID
}

func (c *Context) CreateSurface(win NativeWindowType, width, height int) error {
	eglSurf, err := createSurface(c.disp, c.eglCtx, win)
	c.eglSurf = eglSurf
	c.width = width
	c.height = height
	c.refreshFBO = true
	return err
}

func (c *Context) ReleaseCurrent() {
	if c.disp != nilEGLDisplay {
		eglMakeCurrent(c.disp, nilEGLSurface, nilEGLSurface, nilEGLContext)
	}
}

func (c *Context) MakeCurrent() error {
	if c.eglSurf == nilEGLSurface && !c.eglCtx.surfaceless {
		return errors.New("no surface created yet EGL_KHR_surfaceless_context is not supported")
	}
	if !eglMakeCurrent(c.disp, c.eglSurf, c.eglSurf, c.eglCtx.ctx) {
		return fmt.Errorf("eglMakeCurrent error 0x%x", eglGetError())
	}
	if c.eglCtx.srgb || c.eglSurf == nilEGLSurface {
		return nil
	}
	if c.srgbFBO == nil {
		var err error
		c.srgbFBO, err = srgb.New(nil)
		if err != nil {
			c.ReleaseCurrent()
			return err
		}
	}
	if c.refreshFBO {
		c.refreshFBO = false
		if err := c.srgbFBO.Refresh(c.width, c.height); err != nil {
			c.ReleaseCurrent()
			return err
		}
	}
	return nil
}

func (c *Context) EnableVSync(enable bool) {
	if enable {
		eglSwapInterval(c.disp, 1)
	} else {
		eglSwapInterval(c.disp, 0)
	}
}

func hasExtension(exts []string, ext string) bool {
	for _, e := range exts {
		if ext == e {
			return true
		}
	}
	return false
}

func createContext(disp _EGLDisplay) (*eglContext, error) {
	major, minor, ret := eglInitialize(disp)
	if !ret {
		return nil, fmt.Errorf("eglInitialize failed: 0x%x", eglGetError())
	}
	// sRGB framebuffer support on EGL 1.5 or if EGL_KHR_gl_colorspace is supported.
	exts := strings.Split(eglQueryString(disp, _EGL_EXTENSIONS), " ")
	srgb := major > 1 || minor >= 5 || hasExtension(exts, "EGL_KHR_gl_colorspace")
	attribs := []_EGLint{
		_EGL_RENDERABLE_TYPE, _EGL_OPENGL_ES2_BIT,
		_EGL_SURFACE_TYPE, _EGL_WINDOW_BIT,
		_EGL_BLUE_SIZE, 8,
		_EGL_GREEN_SIZE, 8,
		_EGL_RED_SIZE, 8,
		_EGL_CONFIG_CAVEAT, _EGL_NONE,
	}
	if srgb {
		if runtime.GOOS == "linux" || runtime.GOOS == "android" {
			// Some Mesa drivers crash if an sRGB framebuffer is requested without alpha.
			// https://bugs.freedesktop.org/show_bug.cgi?id=107782.
			//
			// Also, some Android devices (Samsung S9) needs alpha for sRGB to work.
			attribs = append(attribs, _EGL_ALPHA_SIZE, 8)
		}
		// Only request a depth buffer if we're going to render directly to the framebuffer.
		attribs = append(attribs, _EGL_DEPTH_SIZE, 16)
	}
	attribs = append(attribs, _EGL_NONE)
	eglCfg, ret := eglChooseConfig(disp, attribs)
	if !ret {
		return nil, fmt.Errorf("eglChooseConfig failed: 0x%x", eglGetError())
	}
	if eglCfg == nilEGLConfig {
		supportsNoCfg := hasExtension(exts, "EGL_KHR_no_config_context")
		if !supportsNoCfg {
			return nil, errors.New("eglChooseConfig returned no configs")
		}
	}
	var visID _EGLint
	if eglCfg != nilEGLConfig {
		var ok bool
		visID, ok = eglGetConfigAttrib(disp, eglCfg, _EGL_NATIVE_VISUAL_ID)
		if !ok {
			return nil, errors.New("newContext: eglGetConfigAttrib for _EGL_NATIVE_VISUAL_ID failed")
		}
	}
	ctxAttribs := []_EGLint{
		_EGL_CONTEXT_CLIENT_VERSION, 3,
		_EGL_NONE,
	}
	eglCtx := eglCreateContext(disp, eglCfg, nilEGLContext, ctxAttribs)
	if eglCtx == nilEGLContext {
		// Fall back to OpenGL ES 2 and rely on extensions.
		ctxAttribs := []_EGLint{
			_EGL_CONTEXT_CLIENT_VERSION, 2,
			_EGL_NONE,
		}
		eglCtx = eglCreateContext(disp, eglCfg, nilEGLContext, ctxAttribs)
		if eglCtx == nilEGLContext {
			return nil, fmt.Errorf("eglCreateContext failed: 0x%x", eglGetError())
		}
	}
	return &eglContext{
		config:      _EGLConfig(eglCfg),
		ctx:         _EGLContext(eglCtx),
		visualID:    int(visID),
		srgb:        srgb,
		surfaceless: hasExtension(exts, "EGL_KHR_surfaceless_context"),
	}, nil
}

func createSurface(disp _EGLDisplay, eglCtx *eglContext, win NativeWindowType) (_EGLSurface, error) {
	var surfAttribs []_EGLint
	if eglCtx.srgb {
		surfAttribs = append(surfAttribs, _EGL_GL_COLORSPACE_KHR, _EGL_GL_COLORSPACE_SRGB_KHR)
	}
	surfAttribs = append(surfAttribs, _EGL_NONE)
	eglSurf := eglCreateWindowSurface(disp, eglCtx.config, win, surfAttribs)
	if eglSurf == nilEGLSurface && eglCtx.srgb {
		// Try again without sRGB
		eglCtx.srgb = false
		surfAttribs = []_EGLint{_EGL_NONE}
		eglSurf = eglCreateWindowSurface(disp, eglCtx.config, win, surfAttribs)
	}
	if eglSurf == nilEGLSurface {
		return nilEGLSurface, fmt.Errorf("newContext: eglCreateWindowSurface failed 0x%x (sRGB=%v)", eglGetError(), eglCtx.srgb)
	}
	return eglSurf, nil
}
