// Copyright 2014 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 linux || darwin || windows

package glutil

import (
	"encoding/binary"
	"image"
	"runtime"
	"sync"

	"golang.org/x/mobile/event/size"
	"golang.org/x/mobile/exp/f32"
	"golang.org/x/mobile/geom"
	"golang.org/x/mobile/gl"
)

// Images maintains the shared state used by a set of *Image objects.
type Images struct {
	glctx         gl.Context
	quadXY        gl.Buffer
	quadUV        gl.Buffer
	program       gl.Program
	pos           gl.Attrib
	mvp           gl.Uniform
	uvp           gl.Uniform
	inUV          gl.Attrib
	textureSample gl.Uniform

	mu           sync.Mutex
	activeImages int
}

// NewImages creates an *Images.
func NewImages(glctx gl.Context) *Images {
	program, err := CreateProgram(glctx, vertexShader, fragmentShader)
	if err != nil {
		panic(err)
	}

	p := &Images{
		glctx:         glctx,
		quadXY:        glctx.CreateBuffer(),
		quadUV:        glctx.CreateBuffer(),
		program:       program,
		pos:           glctx.GetAttribLocation(program, "pos"),
		mvp:           glctx.GetUniformLocation(program, "mvp"),
		uvp:           glctx.GetUniformLocation(program, "uvp"),
		inUV:          glctx.GetAttribLocation(program, "inUV"),
		textureSample: glctx.GetUniformLocation(program, "textureSample"),
	}

	glctx.BindBuffer(gl.ARRAY_BUFFER, p.quadXY)
	glctx.BufferData(gl.ARRAY_BUFFER, quadXYCoords, gl.STATIC_DRAW)
	glctx.BindBuffer(gl.ARRAY_BUFFER, p.quadUV)
	glctx.BufferData(gl.ARRAY_BUFFER, quadUVCoords, gl.STATIC_DRAW)

	return p
}

// Release releases any held OpenGL resources.
// All *Image objects must be released first, or this function panics.
func (p *Images) Release() {
	if p.program == (gl.Program{}) {
		return
	}

	p.mu.Lock()
	rem := p.activeImages
	p.mu.Unlock()
	if rem > 0 {
		panic("glutil.Images.Release called, but active *Image objects remain")
	}

	p.glctx.DeleteProgram(p.program)
	p.glctx.DeleteBuffer(p.quadXY)
	p.glctx.DeleteBuffer(p.quadUV)

	p.program = gl.Program{}
}

// Image bridges between an *image.RGBA and an OpenGL texture.
//
// The contents of the *image.RGBA can be uploaded as a texture and drawn as a
// 2D quad.
//
// The number of active Images must fit in the system's OpenGL texture limit.
// The typical use of an Image is as a texture atlas.
type Image struct {
	RGBA *image.RGBA

	gltex  gl.Texture
	width  int
	height int
	images *Images
}

// NewImage creates an Image of the given size.
//
// Both a host-memory *image.RGBA and a GL texture are created.
func (p *Images) NewImage(w, h int) *Image {
	dx := roundToPower2(w)
	dy := roundToPower2(h)

	// TODO(crawshaw): Using VertexAttribPointer we can pass texture
	// data with a stride, which would let us use the exact number of
	// pixels on the host instead of the rounded up power 2 size.
	m := image.NewRGBA(image.Rect(0, 0, dx, dy))

	img := &Image{
		RGBA:   m.SubImage(image.Rect(0, 0, w, h)).(*image.RGBA),
		images: p,
		width:  dx,
		height: dy,
	}

	p.mu.Lock()
	p.activeImages++
	p.mu.Unlock()

	img.gltex = p.glctx.CreateTexture()

	p.glctx.BindTexture(gl.TEXTURE_2D, img.gltex)
	p.glctx.TexImage2D(gl.TEXTURE_2D, 0, gl.RGBA, img.width, img.height, gl.RGBA, gl.UNSIGNED_BYTE, nil)
	p.glctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
	p.glctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
	p.glctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
	p.glctx.TexParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)

	runtime.SetFinalizer(img, (*Image).Release)
	return img
}

func roundToPower2(x int) int {
	x2 := 1
	for x2 < x {
		x2 *= 2
	}
	return x2
}

// Upload copies the host image data to the GL device.
func (img *Image) Upload() {
	img.images.glctx.BindTexture(gl.TEXTURE_2D, img.gltex)
	img.images.glctx.TexSubImage2D(gl.TEXTURE_2D, 0, 0, 0, img.width, img.height, gl.RGBA, gl.UNSIGNED_BYTE, img.RGBA.Pix)
}

// Release invalidates the Image and removes any underlying data structures.
// The Image cannot be used after being deleted.
func (img *Image) Release() {
	if img.gltex == (gl.Texture{}) {
		return
	}

	img.images.glctx.DeleteTexture(img.gltex)
	img.gltex = gl.Texture{}

	img.images.mu.Lock()
	img.images.activeImages--
	img.images.mu.Unlock()
}

// Draw draws the srcBounds part of the image onto a parallelogram, defined by
// three of its corners, in the current GL framebuffer.
func (img *Image) Draw(sz size.Event, topLeft, topRight, bottomLeft geom.Point, srcBounds image.Rectangle) {
	glimage := img.images
	glctx := img.images.glctx

	glctx.BlendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA)
	glctx.Enable(gl.BLEND)

	// TODO(crawshaw): Adjust viewport for the top bar on android?
	glctx.UseProgram(glimage.program)
	{
		// We are drawing a parallelogram PQRS, defined by three of its
		// corners, onto the entire GL framebuffer ABCD. The two quads may
		// actually be equal, but in the general case, PQRS can be smaller,
		// and PQRS is not necessarily axis-aligned.
		//
		//	A +---------------+ B
		//	  |  P +-----+ Q  |
		//	  |    |     |    |
		//	  |  S +-----+ R  |
		//	D +---------------+ C
		//
		// There are two co-ordinate spaces: geom space and framebuffer space.
		// In geom space, the ABCD rectangle is:
		//
		//	(0, 0)           (geom.Width, 0)
		//	(0, geom.Height) (geom.Width, geom.Height)
		//
		// and the PQRS quad is:
		//
		//	(topLeft.X,    topLeft.Y)    (topRight.X, topRight.Y)
		//	(bottomLeft.X, bottomLeft.Y) (implicit,   implicit)
		//
		// In framebuffer space, the ABCD rectangle is:
		//
		//	(-1, +1) (+1, +1)
		//	(-1, -1) (+1, -1)
		//
		// First of all, convert from geom space to framebuffer space. For
		// later convenience, we divide everything by 2 here: px2 is half of
		// the P.X co-ordinate (in framebuffer space).
		px2 := -0.5 + float32(topLeft.X/sz.WidthPt)
		py2 := +0.5 - float32(topLeft.Y/sz.HeightPt)
		qx2 := -0.5 + float32(topRight.X/sz.WidthPt)
		qy2 := +0.5 - float32(topRight.Y/sz.HeightPt)
		sx2 := -0.5 + float32(bottomLeft.X/sz.WidthPt)
		sy2 := +0.5 - float32(bottomLeft.Y/sz.HeightPt)
		// Next, solve for the affine transformation matrix
		//	    [ a00 a01 a02 ]
		//	a = [ a10 a11 a12 ]
		//	    [   0   0   1 ]
		// that maps A to P:
		//	a × [ -1 +1 1 ]' = [ 2*px2 2*py2 1 ]'
		// and likewise maps B to Q and D to S. Solving those three constraints
		// implies that C maps to R, since affine transformations keep parallel
		// lines parallel. This gives 6 equations in 6 unknowns:
		//	-a00 + a01 + a02 = 2*px2
		//	-a10 + a11 + a12 = 2*py2
		//	+a00 + a01 + a02 = 2*qx2
		//	+a10 + a11 + a12 = 2*qy2
		//	-a00 - a01 + a02 = 2*sx2
		//	-a10 - a11 + a12 = 2*sy2
		// which gives:
		//	a00 = (2*qx2 - 2*px2) / 2 = qx2 - px2
		// and similarly for the other elements of a.
		writeAffine(glctx, glimage.mvp, &f32.Affine{{
			qx2 - px2,
			px2 - sx2,
			qx2 + sx2,
		}, {
			qy2 - py2,
			py2 - sy2,
			qy2 + sy2,
		}})
	}

	{
		// Mapping texture co-ordinates is similar, except that in texture
		// space, the ABCD rectangle is:
		//
		//	(0,0) (1,0)
		//	(0,1) (1,1)
		//
		// and the PQRS quad is always axis-aligned. First of all, convert
		// from pixel space to texture space.
		w := float32(img.width)
		h := float32(img.height)
		px := float32(srcBounds.Min.X-img.RGBA.Rect.Min.X) / w
		py := float32(srcBounds.Min.Y-img.RGBA.Rect.Min.Y) / h
		qx := float32(srcBounds.Max.X-img.RGBA.Rect.Min.X) / w
		sy := float32(srcBounds.Max.Y-img.RGBA.Rect.Min.Y) / h
		// Due to axis alignment, qy = py and sx = px.
		//
		// The simultaneous equations are:
		//	  0 +   0 + a02 = px
		//	  0 +   0 + a12 = py
		//	a00 +   0 + a02 = qx
		//	a10 +   0 + a12 = qy = py
		//	  0 + a01 + a02 = sx = px
		//	  0 + a11 + a12 = sy
		writeAffine(glctx, glimage.uvp, &f32.Affine{{
			qx - px,
			0,
			px,
		}, {
			0,
			sy - py,
			py,
		}})
	}

	glctx.ActiveTexture(gl.TEXTURE0)
	glctx.BindTexture(gl.TEXTURE_2D, img.gltex)
	glctx.Uniform1i(glimage.textureSample, 0)

	glctx.BindBuffer(gl.ARRAY_BUFFER, glimage.quadXY)
	glctx.EnableVertexAttribArray(glimage.pos)
	glctx.VertexAttribPointer(glimage.pos, 2, gl.FLOAT, false, 0, 0)

	glctx.BindBuffer(gl.ARRAY_BUFFER, glimage.quadUV)
	glctx.EnableVertexAttribArray(glimage.inUV)
	glctx.VertexAttribPointer(glimage.inUV, 2, gl.FLOAT, false, 0, 0)

	glctx.DrawArrays(gl.TRIANGLE_STRIP, 0, 4)

	glctx.DisableVertexAttribArray(glimage.pos)
	glctx.DisableVertexAttribArray(glimage.inUV)

	glctx.Disable(gl.BLEND)
}

var quadXYCoords = f32.Bytes(binary.LittleEndian,
	-1, +1, // top left
	+1, +1, // top right
	-1, -1, // bottom left
	+1, -1, // bottom right
)

var quadUVCoords = f32.Bytes(binary.LittleEndian,
	0, 0, // top left
	1, 0, // top right
	0, 1, // bottom left
	1, 1, // bottom right
)

const vertexShader = `#version 100
uniform mat3 mvp;
uniform mat3 uvp;
attribute vec3 pos;
attribute vec2 inUV;
varying vec2 UV;
void main() {
	vec3 p = pos;
	p.z = 1.0;
	gl_Position = vec4(mvp * p, 1);
	UV = (uvp * vec3(inUV, 1)).xy;
}
`

const fragmentShader = `#version 100
precision mediump float;
varying vec2 UV;
uniform sampler2D textureSample;
void main(){
	gl_FragColor = texture2D(textureSample, UV);
}
`
