// Copyright 2016 Hajime Hoshi
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//go:build android || ios

package ui

import (
	stdcontext "context"
	"fmt"
	"runtime"
	"runtime/debug"
	"sync"
	"sync/atomic"

	"github.com/hajimehoshi/ebiten/v2/internal/gamepad"
	"github.com/hajimehoshi/ebiten/v2/internal/graphicscommand"
	"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver"
	"github.com/hajimehoshi/ebiten/v2/internal/hook"
	"github.com/hajimehoshi/ebiten/v2/internal/restorable"
)

var (
	// renderCh receives when updating starts.
	renderCh = make(chan struct{})

	// renderEndCh receives when updating finishes.
	renderEndCh = make(chan struct{})
)

func (u *UserInterface) init() error {
	u.userInterfaceImpl = userInterfaceImpl{
		graphicsLibraryInitCh: make(chan struct{}),
		errCh:                 make(chan error),

		// Give a default outside size so that the game can start without initializing them.
		outsideWidth:  640,
		outsideHeight: 480,
	}
	u.foreground.Store(true)
	return nil
}

// Update is called from mobile/ebitenmobileview.
//
// Update must be called on the rendering thread.
func (u *UserInterface) Update() error {
	select {
	case err := <-u.errCh:
		return err
	default:
	}

	if !u.IsFocused() {
		return nil
	}

	if err := gamepad.Update(); err != nil {
		return err
	}

	ctx, cancel := stdcontext.WithCancel(stdcontext.Background())
	defer cancel()

	renderCh <- struct{}{}
	go func() {
		<-renderEndCh
		cancel()
	}()

	graphicscommand.LoopRenderThread(ctx)
	return nil
}

type userInterfaceImpl struct {
	graphicsDriver        graphicsdriver.Graphics
	graphicsLibraryInitCh chan struct{}

	outsideWidth  float64
	outsideHeight float64

	foreground atomic.Bool
	errCh      chan error

	context *context

	inputState InputState
	touches    []TouchForInput

	fpsMode  atomic.Int32
	renderer Renderer

	strictContextRestoration     atomic.Bool
	strictContextRestorationOnce sync.Once

	// uiView is used only on iOS.
	uiView atomic.Uintptr

	m sync.RWMutex
}

func (u *UserInterface) SetForeground(foreground bool) error {
	u.foreground.Store(foreground)

	if foreground {
		return hook.ResumeAudio()
	} else {
		return hook.SuspendAudio()
	}
}

func (u *UserInterface) Run(game Game, options *RunOptions) error {
	return fmt.Errorf("internal/ui: Run is not implemented for GOOS=%s", runtime.GOOS)
}

func (u *UserInterface) RunWithoutMainLoop(game Game, options *RunOptions) {
	go func() {
		if err := u.runMobile(game, options); err != nil {
			u.errCh <- err
		}
	}()
}

func (u *UserInterface) runMobile(game Game, options *RunOptions) (err error) {
	// Convert the panic to a regular error so that Java/Objective-C layer can treat this easily e.g., for
	// Crashlytics. A panic is treated as SIGABRT, and there is no way to handle this on Java/Objective-C layer
	// unfortunately.
	// TODO: Panic on other goroutines cannot be handled here.
	defer func() {
		if r := recover(); r != nil {
			err = fmt.Errorf("%v\n%s", r, string(debug.Stack()))
		}
	}()

	graphicscommand.SetOSThreadAsRenderThread()

	u.setRunning(true)
	defer u.setRunning(false)

	u.context = newContext(game)

	g, lib, err := newGraphicsDriver(&graphicsDriverCreatorImpl{
		colorSpace: options.ColorSpace,
	}, options.GraphicsLibrary)
	if err != nil {
		return err
	}
	u.graphicsDriver = g
	u.setGraphicsLibrary(lib)
	close(u.graphicsLibraryInitCh)
	if options.StrictContextRestoration {
		u.strictContextRestoration.Store(true)
	} else {
		restorable.Disable()
	}

	for {
		if err := u.update(); err != nil {
			return err
		}
	}
}

// outsideSize must be called on the same goroutine as update().
func (u *UserInterface) outsideSize() (float64, float64) {
	u.m.RLock()
	defer u.m.RUnlock()

	return u.outsideWidth, u.outsideHeight
}

func (u *UserInterface) update() error {
	<-renderCh
	defer func() {
		renderEndCh <- struct{}{}
	}()

	w, h := u.outsideSize()
	if err := u.context.updateFrame(u.graphicsDriver, w, h, theMonitor.DeviceScaleFactor(), u); err != nil {
		return err
	}
	return nil
}

// SetOutsideSize is called from mobile/ebitenmobileview.
//
// SetOutsideSize is concurrent safe.
func (u *UserInterface) SetOutsideSize(outsideWidth, outsideHeight float64) {
	u.m.Lock()
	defer u.m.Unlock()
	if u.outsideWidth != outsideWidth || u.outsideHeight != outsideHeight {
		u.outsideWidth = outsideWidth
		u.outsideHeight = outsideHeight
	}
}

func (u *UserInterface) CursorMode() CursorMode {
	return CursorModeHidden
}

func (u *UserInterface) SetCursorMode(mode CursorMode) {
	// Do nothing
}

func (u *UserInterface) CursorShape() CursorShape {
	return CursorShapeDefault
}

func (u *UserInterface) SetCursorShape(shape CursorShape) {
	// Do nothing
}

func (u *UserInterface) IsFullscreen() bool {
	return false
}

func (u *UserInterface) SetFullscreen(fullscreen bool) {
	// Do nothing
}

func (u *UserInterface) IsFocused() bool {
	return u.foreground.Load()
}

func (u *UserInterface) IsRunnableOnUnfocused() bool {
	return false
}

func (u *UserInterface) SetRunnableOnUnfocused(runnableOnUnfocused bool) {
	// Do nothing
}

func (u *UserInterface) FPSMode() FPSModeType {
	return FPSModeType(u.fpsMode.Load())
}

func (u *UserInterface) SetFPSMode(mode FPSModeType) {
	u.fpsMode.Store(int32(mode))
	u.updateExplicitRenderingModeIfNeeded(mode)
}

func (u *UserInterface) updateExplicitRenderingModeIfNeeded(fpsMode FPSModeType) {
	if u.renderer == nil {
		return
	}
	u.renderer.SetExplicitRenderingMode(fpsMode == FPSModeVsyncOffMinimum)
}

func (u *UserInterface) readInputState(inputState *InputState) {
	u.m.Lock()
	defer u.m.Unlock()
	u.inputState.copyAndReset(inputState)
}

func (u *UserInterface) Window() Window {
	return &nullWindow{}
}

type Monitor struct {
	width             int
	height            int
	deviceScaleFactor float64
	inited            atomic.Bool

	m sync.Mutex
}

var theMonitor = &Monitor{}

func (m *Monitor) Name() string {
	return ""
}

func (m *Monitor) ensureInit() {
	if m.inited.Load() {
		return
	}

	m.m.Lock()
	defer m.m.Unlock()
	// Re-check the state since the state might be changed while locking.
	if m.inited.Load() {
		return
	}
	width, height, scale, ok := theUI.displayInfo()
	if !ok {
		return
	}
	m.width = width
	m.height = height
	m.deviceScaleFactor = scale
	m.inited.Store(true)
}

func (m *Monitor) DeviceScaleFactor() float64 {
	m.ensureInit()
	return m.deviceScaleFactor
}

func (m *Monitor) Size() (int, int) {
	m.ensureInit()
	return m.width, m.height
}

func (u *UserInterface) AppendMonitors(mons []*Monitor) []*Monitor {
	return append(mons, theMonitor)
}

func (u *UserInterface) Monitor() *Monitor {
	return theMonitor
}

func (u *UserInterface) UpdateInput(keys map[Key]struct{}, runes []rune, touches []TouchForInput) {
	u.updateInputStateFromOutside(keys, runes, touches)
	if FPSModeType(u.fpsMode.Load()) == FPSModeVsyncOffMinimum {
		u.renderer.RequestRenderIfNeeded()
	}
}

type Renderer interface {
	SetExplicitRenderingMode(explicitRendering bool)
	RequestRenderIfNeeded()
}

func (u *UserInterface) SetRenderer(renderer Renderer) {
	u.renderer = renderer
	u.updateExplicitRenderingModeIfNeeded(FPSModeType(u.fpsMode.Load()))
}

func (u *UserInterface) ScheduleFrame() {
	if u.renderer != nil && FPSModeType(u.fpsMode.Load()) == FPSModeVsyncOffMinimum {
		u.renderer.RequestRenderIfNeeded()
	}
}

func (u *UserInterface) updateIconIfNeeded() error {
	return nil
}

func (u *UserInterface) UsesStrictContextRestoration() bool {
	return u.strictContextRestoration.Load()
}

func IsScreenTransparentAvailable() bool {
	return false
}
