// SPDX-License-Identifier: Unlicense OR MIT

//go:build (linux || freebsd) && !novulkan
// +build linux freebsd
// +build !novulkan

package app

import (
	"errors"
	"unsafe"

	"gioui.org/gpu"
	"gioui.org/internal/vk"
)

type vkContext struct {
	physDev    vk.PhysicalDevice
	inst       vk.Instance
	dev        vk.Device
	queueFam   int
	queue      vk.Queue
	acquireSem vk.Semaphore
	presentSem vk.Semaphore
	fence      vk.Fence

	swchain    vk.Swapchain
	imgs       []vk.Image
	views      []vk.ImageView
	fbos       []vk.Framebuffer
	format     vk.Format
	presentIdx int
}

func newVulkanContext(inst vk.Instance, surf vk.Surface) (*vkContext, error) {
	physDev, qFam, err := vk.ChoosePhysicalDevice(inst, surf)
	if err != nil {
		return nil, err
	}
	dev, err := vk.CreateDeviceAndQueue(physDev, qFam, "VK_KHR_swapchain")
	if err != nil {
		return nil, err
	}
	acquireSem, err := vk.CreateSemaphore(dev)
	if err != nil {
		vk.DestroyDevice(dev)
		return nil, err
	}
	presentSem, err := vk.CreateSemaphore(dev)
	if err != nil {
		vk.DestroySemaphore(dev, acquireSem)
		vk.DestroyDevice(dev)
		return nil, err
	}
	fence, err := vk.CreateFence(dev, vk.FENCE_CREATE_SIGNALED_BIT)
	if err != nil {
		vk.DestroySemaphore(dev, presentSem)
		vk.DestroySemaphore(dev, acquireSem)
		vk.DestroyDevice(dev)
		return nil, err
	}
	c := &vkContext{
		physDev:    physDev,
		inst:       inst,
		dev:        dev,
		queueFam:   qFam,
		queue:      vk.GetDeviceQueue(dev, qFam, 0),
		acquireSem: acquireSem,
		presentSem: presentSem,
		fence:      fence,
	}
	return c, nil
}

func (c *vkContext) RenderTarget() (gpu.RenderTarget, error) {
	vk.WaitForFences(c.dev, c.fence)
	vk.ResetFences(c.dev, c.fence)

	imgIdx, err := vk.AcquireNextImage(c.dev, c.swchain, c.acquireSem, 0)
	if err := mapSurfaceErr(err); err != nil {
		return nil, err
	}
	c.presentIdx = imgIdx
	return gpu.VulkanRenderTarget{
		WaitSem:     uint64(c.acquireSem),
		SignalSem:   uint64(c.presentSem),
		Fence:       uint64(c.fence),
		Framebuffer: uint64(c.fbos[imgIdx]),
		Image:       uint64(c.imgs[imgIdx]),
	}, nil
}

func (c *vkContext) api() gpu.API {
	return gpu.Vulkan{
		PhysDevice:  unsafe.Pointer(c.physDev),
		Device:      unsafe.Pointer(c.dev),
		Format:      int(c.format),
		QueueFamily: c.queueFam,
		QueueIndex:  0,
	}
}

func mapErr(err error) error {
	var vkErr vk.Error
	if errors.As(err, &vkErr) && vkErr == vk.ERROR_DEVICE_LOST {
		return gpu.ErrDeviceLost
	}
	return err
}

func mapSurfaceErr(err error) error {
	var vkErr vk.Error
	if !errors.As(err, &vkErr) {
		return err
	}
	switch {
	case vkErr == vk.SUBOPTIMAL_KHR:
		// Android reports VK_SUBOPTIMAL_KHR when presenting to a rotated
		// swapchain (preTransform != currentTransform). However, we don't
		// support transforming the output ourselves, so we'll live with it.
		return nil
	case vkErr == vk.ERROR_OUT_OF_DATE_KHR:
		return errOutOfDate
	case vkErr == vk.ERROR_SURFACE_LOST_KHR:
		// Treating a lost surface as a lost device isn't accurate, but
		// probably not worth optimizing.
		return gpu.ErrDeviceLost
	}
	return mapErr(err)
}

func (c *vkContext) release() {
	vk.DeviceWaitIdle(c.dev)

	c.destroySwapchain()
	vk.DestroyFence(c.dev, c.fence)
	vk.DestroySemaphore(c.dev, c.acquireSem)
	vk.DestroySemaphore(c.dev, c.presentSem)
	vk.DestroyDevice(c.dev)
	*c = vkContext{}
}

func (c *vkContext) present() error {
	return mapSurfaceErr(vk.PresentQueue(c.queue, c.swchain, c.presentSem, c.presentIdx))
}

func (c *vkContext) destroyImageViews() {
	for _, f := range c.fbos {
		vk.DestroyFramebuffer(c.dev, f)
	}
	c.fbos = nil
	for _, view := range c.views {
		vk.DestroyImageView(c.dev, view)
	}
	c.views = nil
}

func (c *vkContext) destroySwapchain() {
	vk.DeviceWaitIdle(c.dev)

	c.destroyImageViews()
	if c.swchain != 0 {
		vk.DestroySwapchain(c.dev, c.swchain)
		c.swchain = 0
	}
}

func (c *vkContext) refresh(surf vk.Surface, width, height int) error {
	vk.DeviceWaitIdle(c.dev)

	c.destroyImageViews()
	// Check whether size is valid. That's needed on X11, where ConfigureNotify
	// is not always synchronized with the window extent.
	caps, err := vk.GetPhysicalDeviceSurfaceCapabilities(c.physDev, surf)
	if err != nil {
		return err
	}
	minExt, maxExt := vk.SurfaceCapabilitiesMinExtent(caps), vk.SurfaceCapabilitiesMaxExtent(caps)
	if width < minExt.X || maxExt.X < width || height < minExt.Y || maxExt.Y < height {
		return errOutOfDate
	}
	swchain, imgs, format, err := vk.CreateSwapchain(c.physDev, c.dev, surf, width, height, c.swchain)
	if c.swchain != 0 {
		vk.DestroySwapchain(c.dev, c.swchain)
		c.swchain = 0
	}
	if err := mapSurfaceErr(err); err != nil {
		return err
	}
	c.swchain = swchain
	c.imgs = imgs
	c.format = format
	pass, err := vk.CreateRenderPass(
		c.dev,
		format,
		vk.ATTACHMENT_LOAD_OP_CLEAR,
		vk.IMAGE_LAYOUT_UNDEFINED,
		vk.IMAGE_LAYOUT_PRESENT_SRC_KHR,
		nil,
	)
	if err := mapErr(err); err != nil {
		return err
	}
	defer vk.DestroyRenderPass(c.dev, pass)
	for _, img := range imgs {
		view, err := vk.CreateImageView(c.dev, img, format)
		if err := mapErr(err); err != nil {
			return err
		}
		c.views = append(c.views, view)
		fbo, err := vk.CreateFramebuffer(c.dev, pass, view, width, height)
		if err := mapErr(err); err != nil {
			return err
		}
		c.fbos = append(c.fbos, fbo)
	}
	return nil
}
