// SPDX-License-Identifier: Unlicense OR MIT

package egl

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

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

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

var (
	libEGL                  = syscall.DLL{}
	_eglChooseConfig        *syscall.Proc
	_eglCreateContext       *syscall.Proc
	_eglCreateWindowSurface *syscall.Proc
	_eglDestroyContext      *syscall.Proc
	_eglDestroySurface      *syscall.Proc
	_eglGetConfigAttrib     *syscall.Proc
	_eglGetDisplay          *syscall.Proc
	_eglGetError            *syscall.Proc
	_eglInitialize          *syscall.Proc
	_eglMakeCurrent         *syscall.Proc
	_eglReleaseThread       *syscall.Proc
	_eglSwapInterval        *syscall.Proc
	_eglSwapBuffers         *syscall.Proc
	_eglTerminate           *syscall.Proc
	_eglQueryString         *syscall.Proc
	_eglWaitClient          *syscall.Proc
)

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
	}

	procs := map[string]**syscall.Proc{
		"eglChooseConfig":        &_eglChooseConfig,
		"eglCreateContext":       &_eglCreateContext,
		"eglCreateWindowSurface": &_eglCreateWindowSurface,
		"eglDestroyContext":      &_eglDestroyContext,
		"eglDestroySurface":      &_eglDestroySurface,
		"eglGetConfigAttrib":     &_eglGetConfigAttrib,
		"eglGetDisplay":          &_eglGetDisplay,
		"eglGetError":            &_eglGetError,
		"eglInitialize":          &_eglInitialize,
		"eglMakeCurrent":         &_eglMakeCurrent,
		"eglReleaseThread":       &_eglReleaseThread,
		"eglSwapInterval":        &_eglSwapInterval,
		"eglSwapBuffers":         &_eglSwapBuffers,
		"eglTerminate":           &_eglTerminate,
		"eglQueryString":         &_eglQueryString,
		"eglWaitClient":          &_eglWaitClient,
	}
	for name, proc := range procs {
		p, err := libEGL.FindProc(name)
		if err != nil {
			return fmt.Errorf("failed to locate %s in %s: %w", name, libEGL.Name, err)
		}
		*proc = p
	}
	return nil
}

func loadDLL(dll *syscall.DLL, name string) error {
	handle, err := syscall.LoadLibraryEx(name, 0, syscall.LOAD_LIBRARY_SEARCH_DEFAULT_DIRS)
	if err != nil {
		return fmt.Errorf("egl: failed to load %s: %v", name, err)
	}
	dll.Handle = handle
	dll.Name = name
	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 syscall.BytePtrToString((*byte)(unsafe.Pointer(r)))
}

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

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