// Copyright 2015 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.

package gldriver

import (
	"image"
	"image/color"
	"image/draw"
	"sync"

	"golang.org/x/exp/shiny/driver/internal/drawer"
	"golang.org/x/exp/shiny/driver/internal/event"
	"golang.org/x/exp/shiny/driver/internal/lifecycler"
	"golang.org/x/exp/shiny/screen"
	"golang.org/x/image/math/f64"
	"golang.org/x/mobile/event/lifecycle"
	"golang.org/x/mobile/event/size"
	"golang.org/x/mobile/gl"
)

type windowImpl struct {
	s *screenImpl

	// id is an OS-specific data structure for the window.
	//	- Cocoa:   ScreenGLView*
	//	- X11:     Window
	//	- Windows: win32.HWND
	id uintptr

	// ctx is a C data structure for the GL context.
	//	- Cocoa:   uintptr holding a NSOpenGLContext*.
	//	- X11:     uintptr holding an EGLSurface.
	//	- Windows: ctxWin32
	ctx interface{}

	lifecycler lifecycler.State
	// TODO: Delete the field below (and the useLifecycler constant), and use
	// the field above for cocoa and win32.
	lifecycleStage lifecycle.Stage // current stage

	event.Deque
	publish     chan struct{}
	publishDone chan screen.PublishResult
	drawDone    chan struct{}

	// glctxMu is a mutex that enforces the atomicity of methods like
	// Texture.Upload or Window.Draw that are conceptually one operation
	// but are implemented by multiple OpenGL calls. OpenGL is a stateful
	// API, so interleaving OpenGL calls from separate higher-level
	// operations causes inconsistencies.
	glctxMu sync.Mutex
	glctx   gl.Context
	worker  gl.Worker
	// backBufferBound is whether the default Framebuffer, with ID 0, also
	// known as the back buffer or the window's Framebuffer, is bound and its
	// viewport is known to equal the window size. It can become false when we
	// bind to a texture's Framebuffer or when the window size changes.
	backBufferBound bool

	// szMu protects only sz. If you need to hold both glctxMu and szMu, the
	// lock ordering is to lock glctxMu first (and unlock it last).
	szMu sync.Mutex
	sz   size.Event
}

// NextEvent implements the screen.EventDeque interface.
func (w *windowImpl) NextEvent() interface{} {
	e := w.Deque.NextEvent()
	if handleSizeEventsAtChannelReceive {
		if sz, ok := e.(size.Event); ok {
			w.glctxMu.Lock()
			w.backBufferBound = false
			w.szMu.Lock()
			w.sz = sz
			w.szMu.Unlock()
			w.glctxMu.Unlock()
		}
	}
	return e
}

func (w *windowImpl) Release() {
	// There are two ways a window can be closed: the Operating System or
	// Desktop Environment can initiate (e.g. in response to a user clicking a
	// red button), or the Go app can programmatically close the window (by
	// calling Window.Release).
	//
	// When the OS closes a window:
	//	- Cocoa:   Obj-C's windowWillClose calls Go's windowClosing.
	//	- X11:     the X11 server sends a WM_DELETE_WINDOW message.
	//	- Windows: TODO: implement and document this.
	//
	// This should send a lifecycle event (To: StageDead) to the Go app's event
	// loop, which should respond by calling Window.Release (this method).
	// Window.Release is where system resources are actually cleaned up.
	//
	// When Window.Release is called, the closeWindow call below:
	//	- Cocoa:   calls Obj-C's performClose, which emulates the red button
	//	           being clicked. (TODO: document how this actually cleans up
	//	           resources??)
	//	- X11:     calls C's XDestroyWindow.
	//	- Windows: TODO: implement and document this.
	//
	// On Cocoa, if these two approaches race, experiments suggest that the
	// race is won by performClose (which is called serially on the main
	// thread). Even if that isn't true, the windowWillClose handler is
	// idempotent.

	theScreen.mu.Lock()
	delete(theScreen.windows, w.id)
	theScreen.mu.Unlock()

	closeWindow(w.id)
}

func (w *windowImpl) Upload(dp image.Point, src screen.Buffer, sr image.Rectangle) {
	originalSRMin := sr.Min
	sr = sr.Intersect(src.Bounds())
	if sr.Empty() {
		return
	}
	dp = dp.Add(sr.Min.Sub(originalSRMin))
	// TODO: keep a texture around for this purpose?
	t, err := w.s.NewTexture(sr.Size())
	if err != nil {
		panic(err)
	}
	t.Upload(image.Point{}, src, sr)
	w.Draw(f64.Aff3{
		1, 0, float64(dp.X),
		0, 1, float64(dp.Y),
	}, t, t.Bounds(), draw.Src, nil)
	t.Release()
}

func useOp(glctx gl.Context, op draw.Op) {
	if op == draw.Over {
		glctx.Enable(gl.BLEND)
		glctx.BlendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA)
	} else {
		glctx.Disable(gl.BLEND)
	}
}

func (w *windowImpl) bindBackBuffer() {
	w.szMu.Lock()
	sz := w.sz
	w.szMu.Unlock()

	w.backBufferBound = true
	w.glctx.BindFramebuffer(gl.FRAMEBUFFER, gl.Framebuffer{Value: 0})
	w.glctx.Viewport(0, 0, sz.WidthPx, sz.HeightPx)
}

func (w *windowImpl) fill(mvp f64.Aff3, src color.Color, op draw.Op) {
	w.glctxMu.Lock()
	defer w.glctxMu.Unlock()

	if !w.backBufferBound {
		w.bindBackBuffer()
	}

	doFill(w.s, w.glctx, mvp, src, op)
}

func doFill(s *screenImpl, glctx gl.Context, mvp f64.Aff3, src color.Color, op draw.Op) {
	useOp(glctx, op)
	if !glctx.IsProgram(s.fill.program) {
		p, err := compileProgram(glctx, fillVertexSrc, fillFragmentSrc)
		if err != nil {
			// TODO: initialize this somewhere else we can better handle the error.
			panic(err.Error())
		}
		s.fill.program = p
		s.fill.pos = glctx.GetAttribLocation(p, "pos")
		s.fill.mvp = glctx.GetUniformLocation(p, "mvp")
		s.fill.color = glctx.GetUniformLocation(p, "color")
		s.fill.quad = glctx.CreateBuffer()

		glctx.BindBuffer(gl.ARRAY_BUFFER, s.fill.quad)
		glctx.BufferData(gl.ARRAY_BUFFER, quadCoords, gl.STATIC_DRAW)
	}
	glctx.UseProgram(s.fill.program)

	writeAff3(glctx, s.fill.mvp, mvp)

	r, g, b, a := src.RGBA()
	glctx.Uniform4f(
		s.fill.color,
		float32(r)/65535,
		float32(g)/65535,
		float32(b)/65535,
		float32(a)/65535,
	)

	glctx.BindBuffer(gl.ARRAY_BUFFER, s.fill.quad)
	glctx.EnableVertexAttribArray(s.fill.pos)
	glctx.VertexAttribPointer(s.fill.pos, 2, gl.FLOAT, false, 0, 0)

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

	glctx.DisableVertexAttribArray(s.fill.pos)
}

func (w *windowImpl) Fill(dr image.Rectangle, src color.Color, op draw.Op) {
	minX := float64(dr.Min.X)
	minY := float64(dr.Min.Y)
	maxX := float64(dr.Max.X)
	maxY := float64(dr.Max.Y)
	w.fill(w.mvp(
		minX, minY,
		maxX, minY,
		minX, maxY,
	), src, op)
}

func (w *windowImpl) DrawUniform(src2dst f64.Aff3, src color.Color, sr image.Rectangle, op draw.Op, opts *screen.DrawOptions) {
	minX := float64(sr.Min.X)
	minY := float64(sr.Min.Y)
	maxX := float64(sr.Max.X)
	maxY := float64(sr.Max.Y)
	w.fill(w.mvp(
		src2dst[0]*minX+src2dst[1]*minY+src2dst[2],
		src2dst[3]*minX+src2dst[4]*minY+src2dst[5],
		src2dst[0]*maxX+src2dst[1]*minY+src2dst[2],
		src2dst[3]*maxX+src2dst[4]*minY+src2dst[5],
		src2dst[0]*minX+src2dst[1]*maxY+src2dst[2],
		src2dst[3]*minX+src2dst[4]*maxY+src2dst[5],
	), src, op)
}

func (w *windowImpl) Draw(src2dst f64.Aff3, src screen.Texture, sr image.Rectangle, op draw.Op, opts *screen.DrawOptions) {
	t := src.(*textureImpl)
	sr = sr.Intersect(t.Bounds())
	if sr.Empty() {
		return
	}

	w.glctxMu.Lock()
	defer w.glctxMu.Unlock()

	if !w.backBufferBound {
		w.bindBackBuffer()
	}

	useOp(w.glctx, op)
	w.glctx.UseProgram(w.s.texture.program)

	// Start with src-space left, top, right and bottom.
	srcL := float64(sr.Min.X)
	srcT := float64(sr.Min.Y)
	srcR := float64(sr.Max.X)
	srcB := float64(sr.Max.Y)
	// Transform to dst-space via the src2dst matrix, then to a MVP matrix.
	writeAff3(w.glctx, w.s.texture.mvp, w.mvp(
		src2dst[0]*srcL+src2dst[1]*srcT+src2dst[2],
		src2dst[3]*srcL+src2dst[4]*srcT+src2dst[5],
		src2dst[0]*srcR+src2dst[1]*srcT+src2dst[2],
		src2dst[3]*srcR+src2dst[4]*srcT+src2dst[5],
		src2dst[0]*srcL+src2dst[1]*srcB+src2dst[2],
		src2dst[3]*srcL+src2dst[4]*srcB+src2dst[5],
	))

	// OpenGL's fragment shaders' UV coordinates run from (0,0)-(1,1),
	// unlike vertex shaders' XY coordinates running from (-1,+1)-(+1,-1).
	//
	// We are drawing a rectangle PQRS, defined by two of its
	// corners, onto the entire texture. The two quads may actually
	// be equal, but in the general case, PQRS can be smaller.
	//
	//	(0,0) +---------------+ (1,0)
	//	      |  P +-----+ Q  |
	//	      |    |     |    |
	//	      |  S +-----+ R  |
	//	(0,1) +---------------+ (1,1)
	//
	// The PQRS quad is always axis-aligned. First of all, convert
	// from pixel space to texture space.
	tw := float64(t.size.X)
	th := float64(t.size.Y)
	px := float64(sr.Min.X-0) / tw
	py := float64(sr.Min.Y-0) / th
	qx := float64(sr.Max.X-0) / tw
	sy := float64(sr.Max.Y-0) / th
	// 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
	writeAff3(w.glctx, w.s.texture.uvp, f64.Aff3{
		qx - px, 0, px,
		0, sy - py, py,
	})

	w.glctx.ActiveTexture(gl.TEXTURE0)
	w.glctx.BindTexture(gl.TEXTURE_2D, t.id)
	w.glctx.Uniform1i(w.s.texture.sample, 0)

	w.glctx.BindBuffer(gl.ARRAY_BUFFER, w.s.texture.quad)
	w.glctx.EnableVertexAttribArray(w.s.texture.pos)
	w.glctx.VertexAttribPointer(w.s.texture.pos, 2, gl.FLOAT, false, 0, 0)

	w.glctx.BindBuffer(gl.ARRAY_BUFFER, w.s.texture.quad)
	w.glctx.EnableVertexAttribArray(w.s.texture.inUV)
	w.glctx.VertexAttribPointer(w.s.texture.inUV, 2, gl.FLOAT, false, 0, 0)

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

	w.glctx.DisableVertexAttribArray(w.s.texture.pos)
	w.glctx.DisableVertexAttribArray(w.s.texture.inUV)
}

func (w *windowImpl) Copy(dp image.Point, src screen.Texture, sr image.Rectangle, op draw.Op, opts *screen.DrawOptions) {
	drawer.Copy(w, dp, src, sr, op, opts)
}

func (w *windowImpl) Scale(dr image.Rectangle, src screen.Texture, sr image.Rectangle, op draw.Op, opts *screen.DrawOptions) {
	drawer.Scale(w, dr, src, sr, op, opts)
}

func (w *windowImpl) mvp(tlx, tly, trx, try, blx, bly float64) f64.Aff3 {
	w.szMu.Lock()
	sz := w.sz
	w.szMu.Unlock()

	return calcMVP(sz.WidthPx, sz.HeightPx, tlx, tly, trx, try, blx, bly)
}

// calcMVP returns the Model View Projection matrix that maps the quadCoords
// unit square, (0, 0) to (1, 1), to a quad QV, such that QV in vertex shader
// space corresponds to the quad QP in pixel space, where QP is defined by
// three of its four corners - the arguments to this function. The three
// corners are nominally the top-left, top-right and bottom-left, but there is
// no constraint that e.g. tlx < trx.
//
// In pixel space, the window ranges from (0, 0) to (widthPx, heightPx). The
// Y-axis points downwards.
//
// In vertex shader space, the window ranges from (-1, +1) to (+1, -1), which
// is a 2-unit by 2-unit square. The Y-axis points upwards.
func calcMVP(widthPx, heightPx int, tlx, tly, trx, try, blx, bly float64) f64.Aff3 {
	// Convert from pixel coords to vertex shader coords.
	invHalfWidth := +2 / float64(widthPx)
	invHalfHeight := -2 / float64(heightPx)
	tlx = tlx*invHalfWidth - 1
	tly = tly*invHalfHeight + 1
	trx = trx*invHalfWidth - 1
	try = try*invHalfHeight + 1
	blx = blx*invHalfWidth - 1
	bly = bly*invHalfHeight + 1

	// The resultant affine matrix:
	//	- maps (0, 0) to (tlx, tly).
	//	- maps (1, 0) to (trx, try).
	//	- maps (0, 1) to (blx, bly).
	return f64.Aff3{
		trx - tlx, blx - tlx, tlx,
		try - tly, bly - tly, tly,
	}
}

func (w *windowImpl) Publish() screen.PublishResult {
	// gl.Flush is a lightweight (on modern GL drivers) blocking call
	// that ensures all GL functions pending in the gl package have
	// been passed onto the GL driver before the app package attempts
	// to swap the screen buffer.
	//
	// This enforces that the final receive (for this paint cycle) on
	// gl.WorkAvailable happens before the send on publish.
	w.glctxMu.Lock()
	w.glctx.Flush()
	w.glctxMu.Unlock()

	w.publish <- struct{}{}
	res := <-w.publishDone

	select {
	case w.drawDone <- struct{}{}:
	default:
	}

	return res
}
