package room

import (
	"flag"
	"fmt"
	"hash/crc32"
	"image"
	"image/color"
	"image/draw"
	"image/png"
	"log"
	"os"
	"path/filepath"
	"runtime"
	"sync"
	"testing"
	"time"

	"github.com/giongto35/cloud-game/v3/pkg/com"
	"github.com/giongto35/cloud-game/v3/pkg/config"
	"github.com/giongto35/cloud-game/v3/pkg/encoder"
	"github.com/giongto35/cloud-game/v3/pkg/encoder/color/bgra"
	"github.com/giongto35/cloud-game/v3/pkg/encoder/color/rgb565"
	"github.com/giongto35/cloud-game/v3/pkg/encoder/color/rgba"
	"github.com/giongto35/cloud-game/v3/pkg/games"
	"github.com/giongto35/cloud-game/v3/pkg/logger"
	"github.com/giongto35/cloud-game/v3/pkg/worker/caged"
	"github.com/giongto35/cloud-game/v3/pkg/worker/caged/app"
	"github.com/giongto35/cloud-game/v3/pkg/worker/media"
	"github.com/giongto35/cloud-game/v3/pkg/worker/thread"
	"golang.org/x/image/font"
	"golang.org/x/image/font/basicfont"
	"golang.org/x/image/math/fixed"

	_ "github.com/giongto35/cloud-game/v3/test"
)

var (
	renderFrames  bool
	outputPath    string
	autoGlContext bool
)

type testRoom struct {
	*Room[*GameSession]
	started bool
}

type codec = encoder.VideoCodec

type conf struct {
	roomName      string
	game          games.GameMetadata
	codec         codec
	autoGlContext bool
	autoAppStart  bool
	noLog         bool
}

func (r testRoom) Close() {
	r.Room.Close()
	time.Sleep(2 * time.Second) // hack: wait room destruction (atm impossible to tell)
}

func (r testRoom) WaitFrame(n int) app.RawFrame {
	var wg sync.WaitGroup
	wg.Add(1)
	target := app.RawFrame{}
	WithEmulator(r.app).SetVideoCb(func(v app.Video) {
		if n == 1 {
			target = v.Frame
			target.Data = make([]byte, len(v.Frame.Data))
			copy(target.Data, v.Frame.Data)
			wg.Done()
		}
		n--
	})
	if !r.started {
		r.StartApp()
	}
	wg.Wait()
	return target
}

type testParams struct {
	system string
	game   games.GameMetadata
	codecs []codec
	frames int
	color  int
}

// Store absolute path to test games
var testTempDir = filepath.Join(os.TempDir(), "cloud-game-core-tests")

// games
var (
	alwas = games.GameMetadata{Name: "Alwa's Awakening (Demo)", Type: "nes", Path: "nes/Alwa's Awakening (Demo).nes", System: "nes"}
	sushi = games.GameMetadata{Name: "Sushi The Cat", Type: "gba", Path: "gba/Sushi The Cat.gba", System: "gba"}
	fd    = games.GameMetadata{Name: "Florian Demo", Type: "n64", Path: "n64/Sample Demo by Florian (PD).z64", System: "n64"}
	rogue = games.GameMetadata{Name: "Rogue", Type: "dos", Path: "dos/rogue.zip", System: "dos"}
)

func TestMain(m *testing.M) {
	flag.BoolVar(&renderFrames, "renderFrames", false, "Render frames for eye testing purposes")
	flag.StringVar(&outputPath, "outputPath", "./", "Output path for generated files")
	flag.BoolVar(&autoGlContext, "autoGlContext", false, "Set auto GL context choose for headless machines")

	thread.Wrap(func() { os.Exit(m.Run()) })
}

func TestRoom(t *testing.T) {
	tests := []testParams{
		{game: alwas, codecs: []codec{encoder.H264, encoder.VP8, encoder.VP9}, frames: 300},
	}

	for _, test := range tests {
		for _, codec := range test.codecs {
			room := room(conf{codec: codec, game: test.game})
			room.WaitFrame(test.frames)
			room.Close()
		}
	}
}

func TestAll(t *testing.T) {
	tests := []testParams{
		{game: sushi, frames: 150, color: 2},
		{game: alwas, frames: 50, color: 1},
		{game: fd, frames: 50, system: "gl", color: 1},
		{game: rogue, frames: 33, color: 1},
	}

	crc32q := crc32.MakeTable(0xD5828281)

	for _, test := range tests {
		var frame app.RawFrame
		room := room(conf{game: test.game, codec: encoder.VP8, autoGlContext: autoGlContext, autoAppStart: false})
		flip := test.system == "gl"
		thread.Main(func() { frame = room.WaitFrame(test.frames) })
		room.Close()

		if renderFrames {
			rect := image.Rect(0, 0, frame.W, frame.H)
			var src image.Image
			src1 := bgra.NewBGRA(rect)
			src1.Pix = frame.Data
			src1.Stride = frame.Stride
			src = src1
			if test.color == 2 {
				src2 := rgb565.NewRGB565(rect)
				src2.Pix = frame.Data
				src2.Stride = frame.Stride
				src = src2
			}
			dst := rgba.ToRGBA(src, flip)
			tag := fmt.Sprintf("%v-%v-0x%08x", runtime.GOOS, test.game.Type, crc32.Checksum(frame.Data, crc32q))
			dumpCanvas(dst, tag, fmt.Sprintf("%v [%v]", tag, test.frames), outputPath)
		}
	}
}

func dumpCanvas(frame *image.RGBA, name string, caption string, path string) {
	// slap 'em caption
	if caption != "" {
		draw.Draw(frame, image.Rect(8, 8, 8+len(caption)*7+3, 24), &image.Uniform{C: color.RGBA{}}, image.Point{}, draw.Src)
		(&font.Drawer{
			Dst:  frame,
			Src:  image.NewUniform(color.RGBA{R: 255, G: 255, B: 255, A: 255}),
			Face: basicfont.Face7x13,
			Dot:  fixed.Point26_6{X: fixed.Int26_6(10 * 64), Y: fixed.Int26_6(20 * 64)},
		}).DrawString(caption)
	}

	outPath := testTempDir
	if path != "" {
		outPath = path
	}

	if err := os.MkdirAll(outPath, 0770); err != nil {
		log.Printf("Couldn't create target dir for the output images, %v", err)
		return
	}

	if f, err := os.Create(filepath.Join(outPath, name+".png")); err == nil {
		if err = png.Encode(f, frame); err != nil {
			log.Printf("Couldn't encode the image, %v", err)
		}
		_ = f.Close()
	} else {
		log.Printf("Couldn't create the image, %v", err)
	}
}

// room returns mocked Room struct.
func room(cfg conf) testRoom {
	var conf config.WorkerConfig
	if _, err := config.LoadConfig(&conf, ""); err != nil {
		panic(err)
	}

	conf.Emulator.Libretro.Cores.Repo.ExtLock = expand("tests", ".cr", "cloud-game.lock")
	conf.Emulator.LocalPath = expand("tests", conf.Emulator.LocalPath)
	conf.Emulator.Storage = expand("tests", "storage")

	conf.Encoder.Video.Codec = string(cfg.codec)

	l := logger.NewConsole(conf.Worker.Debug, "w", false)
	if cfg.noLog {
		logger.SetGlobalLevel(logger.Disabled)
	}

	id := cfg.roomName
	if id == "" {
		id = games.GenerateRoomID(cfg.game.Name)
	}

	manager := caged.NewManager(l)
	if err := manager.Load(caged.Libretro, conf); err != nil {
		l.Fatal().Msgf("couldn't cage libretro: %v", err)
	}

	emu := WithEmulator(manager.Get(caged.Libretro))
	emu.ReloadFrontend()
	emu.SetSessionId(id)
	if err := emu.Load(cfg.game, conf.Library.BasePath); err != nil {
		l.Fatal().Err(err).Msgf("couldn't load the game %v", cfg.game)
	}

	m := media.NewWebRtcMediaPipe(conf.Encoder.Audio, conf.Encoder.Video, l)
	m.AudioSrcHz = emu.AudioSampleRate()
	m.AudioFrames = conf.Encoder.Audio.Frames
	m.VideoW, m.VideoH = emu.ViewportSize()
	m.VideoScale = emu.Scale()
	if err := m.Init(); err != nil {
		l.Fatal().Err(err).Msgf("no init")
	}

	room := NewRoom[*GameSession](id, emu, &com.NetMap[SessionKey, *GameSession]{}, m)
	if cfg.autoAppStart {
		room.StartApp()
	}

	return testRoom{Room: room, started: cfg.autoAppStart}
}

// Measures emulation performance of various
// emulators and encoding options.
func BenchmarkRoom(b *testing.B) {
	benches := []testParams{
		// warm up
		{system: "gba", game: sushi, codecs: []codec{encoder.VP8, encoder.VP9}, frames: 50},
		{system: "gba", game: sushi, codecs: []codec{encoder.VP8, encoder.H264}, frames: 100},
		{system: "nes", game: alwas, codecs: []codec{encoder.VP8, encoder.H264}, frames: 100},
	}

	for _, bench := range benches {
		for _, cod := range bench.codecs {
			b.Run(fmt.Sprintf("%s-%v-%d", bench.system, cod, bench.frames), func(b *testing.B) {
				for i := 0; i < b.N; i++ {
					b.StopTimer()
					room := room(conf{game: bench.game, codec: cod, noLog: true})
					b.StartTimer()
					room.WaitFrame(bench.frames)
					b.StopTimer()
					room.Room.Close()
				}
			})
		}
	}
}

// expand joins a list of file path elements.
func expand(p ...string) string {
	ph, _ := filepath.Abs(filepath.FromSlash(filepath.Join(p...)))
	return ph
}
