// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build !android

// Package glwidget provides a widget containing a GL ES framebuffer.
package glwidget

import (
	"fmt"
	"image"
	"image/draw"

	"golang.org/x/exp/shiny/driver/gldriver"
	"golang.org/x/exp/shiny/widget/node"
	"golang.org/x/mobile/gl"
)

// GL is a widget that maintains an OpenGL ES context.
//
// The Draw function is responsible for configuring the GL viewport
// and for publishing the result to the widget by calling the Publish
// method when the frame is complete. A typical draw function:
//
//	func(w *glwidget.GL) {
//		w.Ctx.Viewport(0, 0, w.Rect.Dx(), w.Rect.Dy())
//		w.Ctx.ClearColor(0, 0, 0, 1)
//		w.Ctx.Clear(gl.COLOR_BUFFER_BIT)
//		// ... draw the frame
//		w.Publish()
//	}
//
// The GL context is separate from the one used by the gldriver to
// render the window, and is only used by the glwidget package during
// initialization and for the duration of the Publish call. This means
// a glwidget user is free to use Ctx as a background GL context
// concurrently with the primary UI drawing done by the gldriver.
type GL struct {
	node.LeafEmbed

	Ctx gl.Context

	draw        func(*GL)
	framebuffer gl.Framebuffer
	tex         gl.Texture
	dst         *image.RGBA
	origin      image.Point
}

// NewGL creates a GL widget with a Draw function called when painted.
func NewGL(drawFunc func(*GL)) *GL {
	// TODO: use the size of the monitor as a bound for texture size.
	const maxWidth, maxHeight = 4096, 3072

	glctx, err := gldriver.NewContext()
	if err != nil {
		panic(fmt.Sprintf("glwidget: %v", err)) // TODO: return error?
	}
	w := &GL{
		Ctx:  glctx,
		draw: drawFunc,
	}
	w.tex = w.Ctx.CreateTexture()
	w.Ctx.BindTexture(gl.TEXTURE_2D, w.tex)

	w.Ctx.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, maxWidth, maxHeight, gl.RGBA, gl.UNSIGNED_BYTE, nil)
	w.Ctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
	w.Ctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
	w.Ctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
	w.Ctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)

	w.framebuffer = w.Ctx.CreateFramebuffer()
	w.Ctx.BindFramebuffer(gl.FRAMEBUFFER, w.framebuffer)
	w.Ctx.FramebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, w.tex, 0)

	// TODO: delete the framebuffer, texture, and gl.Context.
	// TODO: explicit or finalizer cleanup?

	w.Wrapper = w

	return w
}

func (w *GL) PaintBase(ctx *node.PaintBaseContext, origin image.Point) error {
	w.Marks.UnmarkNeedsPaintBase()
	if w.Rect.Empty() {
		return nil
	}
	w.dst = ctx.Dst
	w.origin = origin
	w.draw(w)
	w.dst = nil
	return nil
}

// Publish renders the default framebuffer of Ctx onto the area of the
// window occupied by the widget.
func (w *GL) Publish() {
	if w.dst == nil {
		panic("glwidget: no destination, Publish called outside of Draw")
	}
	// TODO: draw the widget texture directly into the window framebuffer.
	m := image.NewRGBA(image.Rect(0, 0, w.Rect.Dx(), w.Rect.Dy()))
	w.Ctx.PixelStorei(gl.PACK_ALIGNMENT, 1)
	w.Ctx.ReadPixels(m.Pix, 0, 0, w.Rect.Dx(), w.Rect.Dy(), gl.RGBA, gl.UNSIGNED_BYTE)
	draw.Draw(w.dst, w.Rect.Add(w.origin), m, image.Point{}, draw.Over)
}
