// SPDX-License-Identifier: Unlicense OR MIT

package egl

import (
	"fmt"
	"runtime"
	"sync"
	"unsafe"

	syscall "golang.org/x/sys/windows"

	"gioui.org/app/internal/glimpl"
	gunsafe "gioui.org/internal/unsafe"
)

type (
	_EGLint           int32
	_EGLDisplay       uintptr
	_EGLConfig        uintptr
	_EGLContext       uintptr
	_EGLSurface       uintptr
	NativeDisplayType uintptr
	NativeWindowType  uintptr
)

var (
	libEGL                  = syscall.NewLazyDLL("libEGL.dll")
	_eglChooseConfig        = libEGL.NewProc("eglChooseConfig")
	_eglCreateContext       = libEGL.NewProc("eglCreateContext")
	_eglCreateWindowSurface = libEGL.NewProc("eglCreateWindowSurface")
	_eglDestroyContext      = libEGL.NewProc("eglDestroyContext")
	_eglDestroySurface      = libEGL.NewProc("eglDestroySurface")
	_eglGetConfigAttrib     = libEGL.NewProc("eglGetConfigAttrib")
	_eglGetDisplay          = libEGL.NewProc("eglGetDisplay")
	_eglGetError            = libEGL.NewProc("eglGetError")
	_eglInitialize          = libEGL.NewProc("eglInitialize")
	_eglMakeCurrent         = libEGL.NewProc("eglMakeCurrent")
	_eglReleaseThread       = libEGL.NewProc("eglReleaseThread")
	_eglSwapInterval        = libEGL.NewProc("eglSwapInterval")
	_eglSwapBuffers         = libEGL.NewProc("eglSwapBuffers")
	_eglTerminate           = libEGL.NewProc("eglTerminate")
	_eglQueryString         = libEGL.NewProc("eglQueryString")
)

var loadOnce sync.Once

func loadEGL() error {
	var err error
	loadOnce.Do(func() {
		err = loadDLLs()
	})
	return err
}

func loadDLLs() error {
	if err := loadDLL(libEGL, "libEGL.dll"); err != nil {
		return err
	}
	if err := loadDLL(glimpl.LibGLESv2, "libGLESv2.dll"); err != nil {
		return err
	}
	// d3dcompiler_47.dll is needed internally for shader compilation to function.
	return loadDLL(syscall.NewLazyDLL("d3dcompiler_47.dll"), "d3dcompiler_47.dll")
}

func loadDLL(dll *syscall.LazyDLL, name string) error {
	err := dll.Load()
	if err != nil {
		return fmt.Errorf("egl: failed to load %s: %v", name, err)
	}
	return nil
}

func eglChooseConfig(disp _EGLDisplay, attribs []_EGLint) (_EGLConfig, bool) {
	var cfg _EGLConfig
	var ncfg _EGLint
	a := &attribs[0]
	r, _, _ := _eglChooseConfig.Call(uintptr(disp), uintptr(unsafe.Pointer(a)), uintptr(unsafe.Pointer(&cfg)), 1, uintptr(unsafe.Pointer(&ncfg)))
	issue34474KeepAlive(a)
	return cfg, r != 0
}

func eglCreateContext(disp _EGLDisplay, cfg _EGLConfig, shareCtx _EGLContext, attribs []_EGLint) _EGLContext {
	a := &attribs[0]
	c, _, _ := _eglCreateContext.Call(uintptr(disp), uintptr(cfg), uintptr(shareCtx), uintptr(unsafe.Pointer(a)))
	issue34474KeepAlive(a)
	return _EGLContext(c)
}

func eglCreateWindowSurface(disp _EGLDisplay, cfg _EGLConfig, win NativeWindowType, attribs []_EGLint) _EGLSurface {
	a := &attribs[0]
	s, _, _ := _eglCreateWindowSurface.Call(uintptr(disp), uintptr(cfg), uintptr(win), uintptr(unsafe.Pointer(a)))
	issue34474KeepAlive(a)
	return _EGLSurface(s)
}

func eglDestroySurface(disp _EGLDisplay, surf _EGLSurface) bool {
	r, _, _ := _eglDestroySurface.Call(uintptr(disp), uintptr(surf))
	return r != 0
}

func eglDestroyContext(disp _EGLDisplay, ctx _EGLContext) bool {
	r, _, _ := _eglDestroyContext.Call(uintptr(disp), uintptr(ctx))
	return r != 0
}

func eglGetConfigAttrib(disp _EGLDisplay, cfg _EGLConfig, attr _EGLint) (_EGLint, bool) {
	var val uintptr
	r, _, _ := _eglGetConfigAttrib.Call(uintptr(disp), uintptr(cfg), uintptr(attr), uintptr(unsafe.Pointer(&val)))
	return _EGLint(val), r != 0
}

func eglGetDisplay(disp NativeDisplayType) _EGLDisplay {
	d, _, _ := _eglGetDisplay.Call(uintptr(disp))
	return _EGLDisplay(d)
}

func eglGetError() _EGLint {
	e, _, _ := _eglGetError.Call()
	return _EGLint(e)
}

func eglInitialize(disp _EGLDisplay) (_EGLint, _EGLint, bool) {
	var maj, min uintptr
	r, _, _ := _eglInitialize.Call(uintptr(disp), uintptr(unsafe.Pointer(&maj)), uintptr(unsafe.Pointer(&min)))
	return _EGLint(maj), _EGLint(min), r != 0
}

func eglMakeCurrent(disp _EGLDisplay, draw, read _EGLSurface, ctx _EGLContext) bool {
	r, _, _ := _eglMakeCurrent.Call(uintptr(disp), uintptr(draw), uintptr(read), uintptr(ctx))
	return r != 0
}

func eglReleaseThread() bool {
	r, _, _ := _eglReleaseThread.Call()
	return r != 0
}

func eglSwapInterval(disp _EGLDisplay, interval _EGLint) bool {
	r, _, _ := _eglSwapInterval.Call(uintptr(disp), uintptr(interval))
	return r != 0
}

func eglSwapBuffers(disp _EGLDisplay, surf _EGLSurface) bool {
	r, _, _ := _eglSwapBuffers.Call(uintptr(disp), uintptr(surf))
	return r != 0
}

func eglTerminate(disp _EGLDisplay) bool {
	r, _, _ := _eglTerminate.Call(uintptr(disp))
	return r != 0
}

func eglQueryString(disp _EGLDisplay, name _EGLint) string {
	r, _, _ := _eglQueryString.Call(uintptr(disp), uintptr(name))
	return gunsafe.GoString(gunsafe.SliceOf(r))
}

// issue34474KeepAlive calls runtime.KeepAlive as a
// workaround for golang.org/issue/34474.
func issue34474KeepAlive(v interface{}) {
	runtime.KeepAlive(v)
}
