// Copyright 2019 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 darwin && cgo

// Package mtldriver provides a Metal driver for accessing a screen.
//
// At this time, the Metal API is used only to present the final pixels
// to the screen. All rendering is performed on the CPU via the image/draw
// algorithms. Future work is to use mtl.Buffer, mtl.Texture, etc., and
// do more of the rendering work on the GPU.
package mtldriver

import (
	"runtime"
	"unsafe"

	"dmitri.shuralyov.com/gpu/mtl"
	"github.com/go-gl/glfw/v3.3/glfw"
	"golang.org/x/exp/shiny/driver/internal/errscreen"
	"golang.org/x/exp/shiny/driver/mtldriver/internal/appkit"
	"golang.org/x/exp/shiny/driver/mtldriver/internal/coreanim"
	"golang.org/x/exp/shiny/screen"
	"golang.org/x/mobile/event/key"
	"golang.org/x/mobile/event/mouse"
	"golang.org/x/mobile/event/paint"
	"golang.org/x/mobile/event/size"
)

func init() {
	runtime.LockOSThread()
}

// Main is called by the program's main function to run the graphical
// application.
//
// It calls f on the Screen, possibly in a separate goroutine, as some OS-
// specific libraries require being on 'the main thread'. It returns when f
// returns.
func Main(f func(screen.Screen)) {
	if err := main(f); err != nil {
		f(errscreen.Stub(err))
	}
}

func main(f func(screen.Screen)) error {
	device, err := mtl.CreateSystemDefaultDevice()
	if err != nil {
		return err
	}
	err = glfw.Init()
	if err != nil {
		return err
	}
	defer glfw.Terminate()
	glfw.WindowHint(glfw.ClientAPI, glfw.NoAPI)
	{
		// TODO(dmitshur): Delete this when https://github.com/go-gl/glfw/issues/272 is resolved.
		// Post an empty event from the main thread before it can happen in a non-main thread,
		// to work around https://github.com/glfw/glfw/issues/1649.
		glfw.PostEmptyEvent()
	}
	var (
		done            = make(chan struct{})
		newWindowCh     = make(chan newWindowReq, 1)
		releaseWindowCh = make(chan releaseWindowReq, 1)
	)
	go func() {
		f(&screenImpl{
			newWindowCh: newWindowCh,
		})
		close(done)
		glfw.PostEmptyEvent() // Break main loop out of glfw.WaitEvents so it can receive on done.
	}()
	for {
		select {
		case <-done:
			return nil
		case req := <-newWindowCh:
			w, err := newWindow(device, releaseWindowCh, req.opts)
			req.respCh <- newWindowResp{w, err}
		case req := <-releaseWindowCh:
			req.window.Destroy()
			req.respCh <- struct{}{}
		default:
			glfw.WaitEvents()
		}
	}
}

type newWindowReq struct {
	opts   *screen.NewWindowOptions
	respCh chan newWindowResp
}

type newWindowResp struct {
	w   screen.Window
	err error
}

type releaseWindowReq struct {
	window *glfw.Window
	respCh chan struct{}
}

// newWindow creates a new GLFW window.
// It must be called on the main thread.
func newWindow(device mtl.Device, releaseWindowCh chan releaseWindowReq, opts *screen.NewWindowOptions) (screen.Window, error) {
	width, height := optsSize(opts)
	window, err := glfw.CreateWindow(width, height, opts.GetTitle(), nil, nil)
	if err != nil {
		return nil, err
	}

	ml := coreanim.MakeMetalLayer()
	ml.SetDevice(device)
	ml.SetPixelFormat(mtl.PixelFormatBGRA8UNorm)
	ml.SetMaximumDrawableCount(3)
	ml.SetDisplaySyncEnabled(true)
	cv := appkit.NewWindow(unsafe.Pointer(window.GetCocoaWindow())).ContentView()
	cv.SetLayer(ml)
	cv.SetWantsLayer(true)

	w := &windowImpl{
		device:          device,
		window:          window,
		releaseWindowCh: releaseWindowCh,
		ml:              ml,
		cq:              device.MakeCommandQueue(),
	}

	// Set callbacks.
	framebufferSizeCallback := func(_ *glfw.Window, width, height int) {
		w.Send(size.Event{
			WidthPx:  width,
			HeightPx: height,
			// TODO(dmitshur): ppp,
		})
		w.Send(paint.Event{External: true})
	}
	window.SetFramebufferSizeCallback(framebufferSizeCallback)
	window.SetCursorPosCallback(func(_ *glfw.Window, x, y float64) {
		const scale = 2 // TODO(dmitshur): compute dynamically
		w.Send(mouse.Event{X: float32(x * scale), Y: float32(y * scale)})
	})
	window.SetMouseButtonCallback(func(_ *glfw.Window, b glfw.MouseButton, a glfw.Action, mods glfw.ModifierKey) {
		btn := glfwMouseButton(b)
		if btn == mouse.ButtonNone {
			return
		}
		const scale = 2 // TODO(dmitshur): compute dynamically
		x, y := window.GetCursorPos()
		w.Send(mouse.Event{
			X: float32(x * scale), Y: float32(y * scale),
			Button:    btn,
			Direction: glfwMouseDirection(a),
			// TODO(dmitshur): set Modifiers
		})
	})
	window.SetKeyCallback(func(_ *glfw.Window, k glfw.Key, _ int, a glfw.Action, mods glfw.ModifierKey) {
		code := glfwKeyCode(k)
		if code == key.CodeUnknown {
			return
		}
		w.Send(key.Event{
			Code:      code,
			Direction: glfwKeyDirection(a),
			// TODO(dmitshur): set Modifiers
		})
	})
	// TODO(dmitshur): set CharModsCallback to catch text (runes) that are typed,
	//                 and perhaps try to unify key pressed + character typed into single event
	window.SetCloseCallback(func(*glfw.Window) {
		w.lifecycler.SetDead(true)
		w.lifecycler.SendEvent(w, nil)
	})

	// TODO(dmitshur): more fine-grained tracking of whether window is visible and/or focused
	w.lifecycler.SetDead(false)
	w.lifecycler.SetVisible(true)
	w.lifecycler.SetFocused(true)
	w.lifecycler.SendEvent(w, nil)

	// Send the initial size and paint events.
	width, height = window.GetFramebufferSize()
	framebufferSizeCallback(window, width, height)

	return w, nil
}

func optsSize(opts *screen.NewWindowOptions) (width, height int) {
	width, height = 1024/2, 768/2
	if opts != nil {
		if opts.Width > 0 {
			width = opts.Width
		}
		if opts.Height > 0 {
			height = opts.Height
		}
	}
	return width, height
}

func glfwMouseButton(button glfw.MouseButton) mouse.Button {
	switch button {
	case glfw.MouseButtonLeft:
		return mouse.ButtonLeft
	case glfw.MouseButtonRight:
		return mouse.ButtonRight
	case glfw.MouseButtonMiddle:
		return mouse.ButtonMiddle
	default:
		return mouse.ButtonNone
	}
}

func glfwMouseDirection(action glfw.Action) mouse.Direction {
	switch action {
	case glfw.Press:
		return mouse.DirPress
	case glfw.Release:
		return mouse.DirRelease
	default:
		panic("unreachable")
	}
}

func glfwKeyCode(k glfw.Key) key.Code {
	// TODO(dmitshur): support more keys
	switch k {
	case glfw.KeyEnter:
		return key.CodeReturnEnter
	case glfw.KeyEscape:
		return key.CodeEscape
	default:
		return key.CodeUnknown
	}
}

func glfwKeyDirection(action glfw.Action) key.Direction {
	switch action {
	case glfw.Press:
		return key.DirPress
	case glfw.Release:
		return key.DirRelease
	case glfw.Repeat:
		return key.DirNone
	default:
		panic("unreachable")
	}
}
