// Package d2app contains the OpenDiablo2 application shell
package d2app

import (
	"bytes"
	"container/ring"
	"encoding/json"
	"errors"
	"fmt"
	"image"
	"image/gif"
	"image/png"
	"log"
	"os"
	"os/signal"
	"path/filepath"
	"runtime"
	"runtime/pprof"
	"strconv"
	"strings"
	"sync"
	"syscall"

	"github.com/OpenDiablo2/OpenDiablo2/d2common/d2data"

	"github.com/pkg/profile"
	"golang.org/x/image/colornames"
	"gopkg.in/alecthomas/kingpin.v2"

	"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
	"github.com/OpenDiablo2/OpenDiablo2/d2common/d2math"
	"github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource"
	"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
	"github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset"
	ebiten2 "github.com/OpenDiablo2/OpenDiablo2/d2core/d2audio/ebiten"
	"github.com/OpenDiablo2/OpenDiablo2/d2core/d2config"
	"github.com/OpenDiablo2/OpenDiablo2/d2core/d2gui"
	"github.com/OpenDiablo2/OpenDiablo2/d2core/d2input"
	"github.com/OpenDiablo2/OpenDiablo2/d2core/d2render/ebiten"
	"github.com/OpenDiablo2/OpenDiablo2/d2core/d2screen"
	"github.com/OpenDiablo2/OpenDiablo2/d2core/d2term"
	"github.com/OpenDiablo2/OpenDiablo2/d2core/d2ui"
	"github.com/OpenDiablo2/OpenDiablo2/d2game/d2gamescreen"
	"github.com/OpenDiablo2/OpenDiablo2/d2networking"
	"github.com/OpenDiablo2/OpenDiablo2/d2networking/d2client"
	"github.com/OpenDiablo2/OpenDiablo2/d2networking/d2client/d2clientconnectiontype"
	"github.com/OpenDiablo2/OpenDiablo2/d2script"
)

// these are used for debug print info
const (
	fpsX, fpsY         = 5, 565
	memInfoX, memInfoY = 670, 5
	debugLineHeight    = 16
	errMsgPadding      = 20
)

// App represents the main application for the engine
type App struct {
	lastTime          float64
	lastScreenAdvance float64
	showFPS           bool
	timeScale         float64
	captureState      captureState
	capturePath       string
	captureFrames     []*image.RGBA
	gitBranch         string
	gitCommit         string
	asset             *d2asset.AssetManager
	inputManager      d2interface.InputManager
	terminal          d2interface.Terminal
	scriptEngine      *d2script.ScriptEngine
	audio             d2interface.AudioProvider
	renderer          d2interface.Renderer
	screen            *d2screen.ScreenManager
	ui                *d2ui.UIManager
	tAllocSamples     *ring.Ring
	guiManager        *d2gui.GuiManager
	config            *d2config.Configuration
	logger            *d2util.Logger
	errorMessage      error
	*Options
}

// Options is used to store all of the app options that can be set with arguments
type Options struct {
	printVersion *bool
	Debug        *bool
	profiler     *string
	Server       *d2networking.ServerOptions
	LogLevel     *d2util.LogLevel
}

type bindTerminalEntry struct {
	name        string
	description string
	action      interface{}
}

const (
	bytesToMegabyte = 1024 * 1024
	nSamplesTAlloc  = 100
	debugPopN       = 6
)

const (
	appLoggerPrefix = "App"
)

// Create creates a new instance of the application
func Create(gitBranch, gitCommit string) *App {
	assetManager, assetError := d2asset.NewAssetManager()
	logger := d2util.NewLogger()
	logger.SetPrefix(appLoggerPrefix)
	logger.SetLevel(d2util.LogLevelNone)

	return &App{
		gitBranch: gitBranch,
		gitCommit: gitCommit,
		asset:     assetManager,
		Options: &Options{
			Server: &d2networking.ServerOptions{},
		},
		logger:       logger,
		errorMessage: assetError,
	}
}

func updateNOOP() error {
	return nil
}

func (a *App) startDedicatedServer() error {
	min, max := d2networking.ServerMinPlayers, d2networking.ServerMaxPlayersDefault
	maxPlayers := d2math.ClampInt(*a.Options.Server.MaxPlayers, min, max)

	srvChanIn := make(chan int)
	srvChanLog := make(chan string)

	srvErr := d2networking.StartDedicatedServer(a.asset, srvChanIn, srvChanLog, maxPlayers)
	if srvErr != nil {
		return srvErr
	}

	c := make(chan os.Signal)
	signal.Notify(c, os.Interrupt, syscall.SIGTERM) // This traps Control-c to safely shut down the server

	go func() {
		<-c
		srvChanIn <- d2networking.ServerEventStop
	}()

	for {
		for data := range srvChanLog {
			log.Println(data)
		}
	}
}

func (a *App) loadEngine() error {
	// Create our renderer
	renderer, err := ebiten.CreateRenderer(a.config)
	if err != nil {
		return err
	}

	a.renderer = renderer

	if a.errorMessage != nil {
		return a.renderer.Run(a.updateInitError, updateNOOP, 800, 600, "OpenDiablo2")
	}

	// if the log level was specified at the command line, use it
	logLevel := *a.Options.LogLevel
	if logLevel == d2util.LogLevelUnspecified {
		logLevel = a.config.LogLevel
	}

	a.asset.SetLogLevel(logLevel)

	audio := ebiten2.CreateAudio(a.asset)

	inputManager := d2input.NewInputManager()

	term, err := d2term.New(inputManager)
	if err != nil {
		return err
	}

	err = a.asset.BindTerminalCommands(term)
	if err != nil {
		return err
	}

	scriptEngine := d2script.CreateScriptEngine()

	uiManager := d2ui.NewUIManager(a.asset, renderer, inputManager, audio)

	a.inputManager = inputManager
	a.terminal = term
	a.scriptEngine = scriptEngine
	a.audio = audio
	a.ui = uiManager
	a.tAllocSamples = createZeroedRing(nSamplesTAlloc)

	if a.gitBranch == "" {
		a.gitBranch = "Local Build"
	}

	return nil
}

func (a *App) parseArguments() {
	const (
		versionArg   = "version"
		versionShort = 'v'
		versionDesc  = "Prints the version of the app"

		profilerArg  = "profile"
		profilerDesc = "Profiles the program, one of (cpu, mem, block, goroutine, trace, thread, mutex)"

		serverArg   = "dedicated"
		serverShort = 'd'
		serverDesc  = "Starts a dedicated server"

		playersArg  = "players"
		playersDesc = "Sets the number of max players for the dedicated server"

		loggingArg   = "loglevel"
		loggingShort = 'l'
		loggingDesc  = "Enables verbose logging. Log levels will include those below it. " +
			"0 disables log messages, " +
			"1 shows errors, " +
			"2 shows warnings, " +
			"3 shows info, " +
			"4 shows debug" +
			"5 uses value from config file (default)"
	)

	a.Options.profiler = kingpin.Flag(profilerArg, profilerDesc).String()
	a.Options.Server.Dedicated = kingpin.Flag(serverArg, serverDesc).Short(serverShort).Bool()
	a.Options.printVersion = kingpin.Flag(versionArg, versionDesc).Short(versionShort).Bool()
	a.Options.Server.MaxPlayers = kingpin.Flag(playersArg, playersDesc).Int()
	a.Options.LogLevel = kingpin.Flag(loggingArg, loggingDesc).
		Short(loggingShort).
		Default(strconv.Itoa(d2util.LogLevelUnspecified)).
		Int()

	kingpin.Parse()
}

// LoadConfig loads the OpenDiablo2 config file
func (a *App) LoadConfig() (*d2config.Configuration, error) {
	// by now the, the loader has initialized and added our config dirs as sources...
	configBaseName := filepath.Base(d2config.DefaultConfigPath())

	configAsset, _ := a.asset.LoadAsset(configBaseName)

	config := &d2config.Configuration{}

	// create the default if not found
	if configAsset == nil {
		config = d2config.DefaultConfig()

		fullPath := filepath.Join(config.Dir(), config.Base())
		config.SetPath(fullPath)

		a.logger.Infof("creating default configuration file at %s...", fullPath)

		saveErr := config.Save()

		return config, saveErr
	}

	if err := json.NewDecoder(configAsset).Decode(config); err != nil {
		return nil, err
	}

	config.SetPath(filepath.Join(configAsset.Source().Path(), configAsset.Path()))

	a.logger.Infof("loaded configuration file from %s", config.Path())

	return config, nil
}

// Run executes the application and kicks off the entire game process
func (a *App) Run() error {
	a.parseArguments()

	// add our possible config directories
	_, _ = a.asset.AddSource(filepath.Dir(d2config.LocalConfigPath()))
	_, _ = a.asset.AddSource(filepath.Dir(d2config.DefaultConfigPath()))

	config, err := a.LoadConfig()
	if err != nil {
		return err
	}

	a.config = config

	a.asset.SetLogLevel(config.LogLevel)

	// print version and exit if `--version` was supplied
	if *a.Options.printVersion {
		fmtVersion := "OpenDiablo2 (%s %s)"

		if a.gitBranch == "" {
			a.gitBranch = "local"
		}

		if a.gitCommit == "" {
			a.gitCommit = "build"
		}

		fmt.Printf(fmtVersion, a.gitBranch, a.gitCommit)
		os.Exit(0)
	}

	logLevel := *a.Options.LogLevel
	if logLevel == d2util.LogLevelUnspecified {
		logLevel = a.config.LogLevel
	}

	a.asset.SetLogLevel(logLevel)

	// start profiler if argument was supplied
	if len(*a.Options.profiler) > 0 {
		profiler := enableProfiler(*a.Options.profiler)
		if profiler != nil {
			defer profiler.Stop()
		}
	}

	// start the server if `--listen` option was supplied
	if *a.Options.Server.Dedicated {
		if err := a.startDedicatedServer(); err != nil {
			return err
		}
	}

	if err := a.loadEngine(); err != nil {
		a.renderer.ShowPanicScreen(err.Error())
		return err
	}

	windowTitle := fmt.Sprintf("OpenDiablo2 (%s)", a.gitBranch)

	// If we fail to initialize, we will show the error screen
	if err := a.initialize(); err != nil {
		if a.errorMessage == nil {
			a.errorMessage = err // if there was an error during init, don't clobber it
		}

		gameErr := a.renderer.Run(a.updateInitError, updateNOOP, 800, 600, windowTitle)
		if gameErr != nil {
			return gameErr
		}

		return err
	}

	a.ToMainMenu()

	if err := a.renderer.Run(a.update, a.advance, 800, 600, windowTitle); err != nil {
		return err
	}

	return nil
}

func (a *App) initialize() error {
	if err := a.initConfig(a.config); err != nil {
		return err
	}

	if err := a.initDataDictionaries(); err != nil {
		return err
	}

	a.timeScale = 1.0
	a.lastTime = d2util.Now()
	a.lastScreenAdvance = a.lastTime

	a.renderer.SetWindowIcon("d2logo.png")
	a.terminal.BindLogger()

	terminalActions := [...]bindTerminalEntry{
		{"dumpheap", "dumps the heap to pprof/heap.pprof", a.dumpHeap},
		{"fullscreen", "toggles fullscreen", a.toggleFullScreen},
		{"capframe", "captures a still frame", a.setupCaptureFrame},
		{"capgifstart", "captures an animation (start)", a.startAnimationCapture},
		{"capgifstop", "captures an animation (stop)", a.stopAnimationCapture},
		{"vsync", "toggles vsync", a.toggleVsync},
		{"fps", "toggle fps counter", a.toggleFpsCounter},
		{"timescale", "set scalar for elapsed time", a.setTimeScale},
		{"quit", "exits the game", a.quitGame},
		{"screen-gui", "enters the gui playground screen", a.enterGuiPlayground},
		{"js", "eval JS scripts", a.evalJS},
	}

	for idx := range terminalActions {
		action := &terminalActions[idx]

		if err := a.terminal.BindAction(action.name, action.description, action.action); err != nil {
			log.Fatal(err)
		}
	}

	gui, err := d2gui.CreateGuiManager(a.asset, a.inputManager)
	if err != nil {
		return err
	}

	a.guiManager = gui

	a.screen = d2screen.NewScreenManager(a.ui, a.guiManager)

	a.audio.SetVolumes(a.config.BgmVolume, a.config.SfxVolume)

	if err := a.loadStrings(); err != nil {
		return err
	}

	a.ui.Initialize()

	return nil
}

const (
	fmtErrSourceNotFound = `file not found: %s

Please check your config file at %s

Also, verify that the MPQ files exist at %s

Capitalization in the file name matters.
`
)

func (a *App) initConfig(config *d2config.Configuration) error {
	a.config = config

	for _, mpqName := range a.config.MpqLoadOrder {
		cleanDir := filepath.Clean(a.config.MpqPath)
		srcPath := filepath.Join(cleanDir, mpqName)

		_, err := a.asset.AddSource(srcPath)
		if err != nil {
			// nolint:stylecheck // we want a multiline error message here..
			return fmt.Errorf(fmtErrSourceNotFound, srcPath, a.config.Path(), a.config.MpqPath)
		}
	}

	return nil
}

func (a *App) initDataDictionaries() error {
	dictPaths := []string{
		d2resource.LevelType, d2resource.LevelPreset, d2resource.LevelWarp,
		d2resource.ObjectType, d2resource.ObjectDetails, d2resource.Weapons,
		d2resource.Armor, d2resource.Misc, d2resource.Books, d2resource.ItemTypes,
		d2resource.UniqueItems, d2resource.Missiles, d2resource.SoundSettings,
		d2resource.MonStats, d2resource.MonStats2, d2resource.MonPreset,
		d2resource.MonProp, d2resource.MonType, d2resource.MonMode,
		d2resource.MagicPrefix, d2resource.MagicSuffix, d2resource.ItemStatCost,
		d2resource.ItemRatio, d2resource.StorePage, d2resource.Overlays,
		d2resource.CharStats, d2resource.Hireling, d2resource.Experience,
		d2resource.Gems, d2resource.QualityItems, d2resource.Runes,
		d2resource.DifficultyLevels, d2resource.AutoMap, d2resource.LevelDetails,
		d2resource.LevelMaze, d2resource.LevelSubstitutions, d2resource.CubeRecipes,
		d2resource.SuperUniques, d2resource.Inventory, d2resource.Skills,
		d2resource.SkillCalc, d2resource.MissileCalc, d2resource.Properties,
		d2resource.SkillDesc, d2resource.BodyLocations, d2resource.Sets,
		d2resource.SetItems, d2resource.AutoMagic, d2resource.TreasureClass,
		d2resource.TreasureClassEx, d2resource.States, d2resource.SoundEnvirons,
		d2resource.Shrines, d2resource.ElemType, d2resource.PlrMode,
		d2resource.PetType, d2resource.NPC, d2resource.MonsterUniqueModifier,
		d2resource.MonsterEquipment, d2resource.UniqueAppellation, d2resource.MonsterLevel,
		d2resource.MonsterSound, d2resource.MonsterSequence, d2resource.PlayerClass,
		d2resource.MonsterPlacement, d2resource.ObjectGroup, d2resource.CompCode,
		d2resource.MonsterAI, d2resource.RarePrefix, d2resource.RareSuffix,
		d2resource.Events, d2resource.Colors, d2resource.ArmorType,
		d2resource.WeaponClass, d2resource.PlayerType, d2resource.Composite,
		d2resource.HitClass, d2resource.UniquePrefix, d2resource.UniqueSuffix,
		d2resource.CubeModifier, d2resource.CubeType, d2resource.HirelingDescription,
		d2resource.LowQualityItems,
	}

	a.logger.Info("Initializing asset manager")

	for _, path := range dictPaths {
		err := a.asset.LoadRecords(path)
		if err != nil {
			return err
		}
	}

	err := a.initAnimationData(d2resource.AnimationData)
	if err != nil {
		return err
	}

	return nil
}

const (
	fmtLoadAnimData = "loading animation data from: %s"
)

func (a *App) initAnimationData(path string) error {
	animDataBytes, err := a.asset.LoadFile(path)
	if err != nil {
		return err
	}

	a.logger.Debug(fmt.Sprintf(fmtLoadAnimData, path))

	animData := d2data.LoadAnimationData(animDataBytes)

	a.logger.Infof("Loaded %d animation data records", len(animData))

	a.asset.Records.Animation.Data = animData

	return nil
}

func (a *App) loadStrings() error {
	tablePaths := []string{
		d2resource.PatchStringTable,
		d2resource.ExpansionStringTable,
		d2resource.StringTable,
	}

	for _, tablePath := range tablePaths {
		_, err := a.asset.LoadStringTable(tablePath)
		if err != nil {
			return err
		}
	}

	return nil
}

func (a *App) renderDebug(target d2interface.Surface) {
	if !a.showFPS {
		return
	}

	vsyncEnabled := a.renderer.GetVSyncEnabled()
	fps := a.renderer.CurrentFPS()
	cx, cy := a.renderer.GetCursorPos()

	target.PushTranslation(fpsX, fpsY)
	target.DrawTextf("vsync:" + strconv.FormatBool(vsyncEnabled) + "\nFPS:" + strconv.Itoa(int(fps)))
	target.Pop()

	var m runtime.MemStats

	runtime.ReadMemStats(&m)
	target.PushTranslation(memInfoX, memInfoY)
	target.DrawTextf("Alloc    " + strconv.FormatInt(int64(m.Alloc)/bytesToMegabyte, 10))
	target.PushTranslation(0, debugLineHeight)
	target.DrawTextf("TAlloc/s " + strconv.FormatFloat(a.allocRate(m.TotalAlloc, fps), 'f', 2, 64))
	target.PushTranslation(0, debugLineHeight)
	target.DrawTextf("Pause    " + strconv.FormatInt(int64(m.PauseTotalNs/bytesToMegabyte), 10))
	target.PushTranslation(0, debugLineHeight)
	target.DrawTextf("HeapSys  " + strconv.FormatInt(int64(m.HeapSys/bytesToMegabyte), 10))
	target.PushTranslation(0, debugLineHeight)
	target.DrawTextf("NumGC    " + strconv.FormatInt(int64(m.NumGC), 10))
	target.PushTranslation(0, debugLineHeight)
	target.DrawTextf("Coords   " + strconv.FormatInt(int64(cx), 10) + "," + strconv.FormatInt(int64(cy), 10))
	target.PopN(debugPopN)
}

func (a *App) renderCapture(target d2interface.Surface) error {
	cleanupCapture := func() {
		a.captureState = captureStateNone
		a.capturePath = ""
		a.captureFrames = nil
	}

	switch a.captureState {
	case captureStateFrame:
		defer cleanupCapture()

		if err := a.doCaptureFrame(target); err != nil {
			return err
		}
	case captureStateGif:
		a.doCaptureGif(target)
	case captureStateNone:
		if len(a.captureFrames) > 0 {
			defer cleanupCapture()

			if err := a.convertFramesToGif(); err != nil {
				return err
			}
		}
	}

	return nil
}

func (a *App) render(target d2interface.Surface) {
	a.screen.Render(target)
	a.ui.Render(target)

	if err := a.guiManager.Render(target); err != nil {
		return
	}

	a.renderDebug(target)

	if err := a.renderCapture(target); err != nil {
		return
	}

	if err := a.terminal.Render(target); err != nil {
		return
	}
}

func (a *App) advance() error {
	current := d2util.Now()
	elapsedUnscaled := current - a.lastTime
	elapsed := elapsedUnscaled * a.timeScale

	a.lastTime = current

	elapsedLastScreenAdvance := (current - a.lastScreenAdvance) * a.timeScale
	a.lastScreenAdvance = current

	if err := a.screen.Advance(elapsedLastScreenAdvance); err != nil {
		return err
	}

	a.ui.Advance(elapsed)

	if err := a.inputManager.Advance(elapsed, current); err != nil {
		return err
	}

	if err := a.guiManager.Advance(elapsed); err != nil {
		return err
	}

	if err := a.terminal.Advance(elapsedUnscaled); err != nil {
		return err
	}

	return nil
}

func (a *App) update(target d2interface.Surface) error {
	a.render(target)

	if target.GetDepth() > 0 {
		return errors.New("detected surface stack leak")
	}

	return nil
}

func (a *App) allocRate(totalAlloc uint64, fps float64) float64 {
	a.tAllocSamples.Value = totalAlloc
	a.tAllocSamples = a.tAllocSamples.Next()
	deltaAllocPerFrame := float64(totalAlloc-a.tAllocSamples.Value.(uint64)) / nSamplesTAlloc

	return deltaAllocPerFrame * fps / bytesToMegabyte
}

func (a *App) dumpHeap() {
	if _, err := os.Stat("./pprof/"); os.IsNotExist(err) {
		if err := os.Mkdir("./pprof/", 0750); err != nil {
			log.Fatal(err)
		}
	}

	fileOut, err := os.Create("./pprof/heap.pprof")
	if err != nil {
		log.Print(err)
	}

	if err := pprof.WriteHeapProfile(fileOut); err != nil {
		log.Fatal(err)
	}

	if err := fileOut.Close(); err != nil {
		log.Fatal(err)
	}
}

func (a *App) evalJS(code string) {
	val, err := a.scriptEngine.Eval(code)
	if err != nil {
		a.terminal.OutputErrorf("%s", err)
		return
	}

	log.Printf("%s", val)
}

func (a *App) toggleFullScreen() {
	fullscreen := !a.renderer.IsFullScreen()
	a.renderer.SetFullScreen(fullscreen)
	a.terminal.OutputInfof("fullscreen is now: %v", fullscreen)
}

func (a *App) setupCaptureFrame(path string) {
	a.captureState = captureStateFrame
	a.capturePath = path
	a.captureFrames = nil
}

func (a *App) doCaptureFrame(target d2interface.Surface) error {
	fp, err := os.Create(a.capturePath)
	if err != nil {
		return err
	}

	defer func() {
		if err := fp.Close(); err != nil {
			log.Fatal(err)
		}
	}()

	screenshot := target.Screenshot()
	if err := png.Encode(fp, screenshot); err != nil {
		return err
	}

	log.Printf("saved frame to %s", a.capturePath)

	return nil
}

func (a *App) doCaptureGif(target d2interface.Surface) {
	screenshot := target.Screenshot()
	a.captureFrames = append(a.captureFrames, screenshot)
}

func (a *App) convertFramesToGif() error {
	fp, err := os.Create(a.capturePath)
	if err != nil {
		return err
	}

	defer func() {
		if err := fp.Close(); err != nil {
			log.Fatal(err)
		}
	}()

	var (
		framesTotal  = len(a.captureFrames)
		framesPal    = make([]*image.Paletted, framesTotal)
		frameDelays  = make([]int, framesTotal)
		framesPerCPU = framesTotal / runtime.NumCPU()
	)

	var waitGroup sync.WaitGroup

	for i := 0; i < framesTotal; i += framesPerCPU {
		waitGroup.Add(1)

		go func(start, end int) {
			defer waitGroup.Done()

			for j := start; j < end; j++ {
				var buffer bytes.Buffer
				if err := gif.Encode(&buffer, a.captureFrames[j], nil); err != nil {
					panic(err)
				}

				framePal, err := gif.Decode(&buffer)
				if err != nil {
					panic(err)
				}

				framesPal[j] = framePal.(*image.Paletted)
				frameDelays[j] = 5
			}
		}(i, d2math.MinInt(i+framesPerCPU, framesTotal))
	}

	waitGroup.Wait()

	if err := gif.EncodeAll(fp, &gif.GIF{Image: framesPal, Delay: frameDelays}); err != nil {
		return err
	}

	log.Printf("saved animation to %s", a.capturePath)

	return nil
}

func (a *App) startAnimationCapture(path string) {
	a.captureState = captureStateGif
	a.capturePath = path
	a.captureFrames = nil
}

func (a *App) stopAnimationCapture() {
	a.captureState = captureStateNone
}

func (a *App) toggleVsync() {
	vsync := !a.renderer.GetVSyncEnabled()
	a.renderer.SetVSyncEnabled(vsync)
	a.terminal.OutputInfof("vsync is now: %v", vsync)
}

func (a *App) toggleFpsCounter() {
	a.showFPS = !a.showFPS
	a.terminal.OutputInfof("fps counter is now: %v", a.showFPS)
}

func (a *App) setTimeScale(timeScale float64) {
	if timeScale <= 0 {
		a.terminal.OutputErrorf("invalid time scale value")
	} else {
		a.terminal.OutputInfof("timescale changed from %f to %f", a.timeScale, timeScale)
		a.timeScale = timeScale
	}
}

func (a *App) quitGame() {
	os.Exit(0)
}

func (a *App) enterGuiPlayground() {
	a.screen.SetNextScreen(d2gamescreen.CreateGuiTestMain(a.renderer, a.guiManager, a.asset))
}

func createZeroedRing(n int) *ring.Ring {
	r := ring.New(n)
	for i := 0; i < n; i++ {
		r.Value = uint64(0)
		r = r.Next()
	}

	return r
}

func enableProfiler(profileOption string) interface{ Stop() } {
	var options []func(*profile.Profile)

	switch strings.ToLower(strings.Trim(profileOption, " ")) {
	case "cpu":
		log.Printf("CPU profiling is enabled.")

		options = append(options, profile.CPUProfile)
	case "mem":
		log.Printf("Memory profiling is enabled.")

		options = append(options, profile.MemProfile)
	case "block":
		log.Printf("Block profiling is enabled.")

		options = append(options, profile.BlockProfile)
	case "goroutine":
		log.Printf("Goroutine profiling is enabled.")

		options = append(options, profile.GoroutineProfile)
	case "trace":
		log.Printf("Trace profiling is enabled.")

		options = append(options, profile.TraceProfile)
	case "thread":
		log.Printf("Thread creation profiling is enabled.")

		options = append(options, profile.ThreadcreationProfile)
	case "mutex":
		log.Printf("Mutex profiling is enabled.")

		options = append(options, profile.MutexProfile)
	}

	options = append(options, profile.ProfilePath("./pprof/"))

	if len(options) > 1 {
		return profile.Start(options...)
	}

	return nil
}

func (a *App) updateInitError(target d2interface.Surface) error {
	target.Clear(colornames.Darkred)
	target.PushTranslation(errMsgPadding, errMsgPadding)
	target.DrawTextf(a.errorMessage.Error())

	return nil
}

// ToMainMenu forces the game to transition to the Main Menu
func (a *App) ToMainMenu(errorMessageOptional ...string) {
	buildInfo := d2gamescreen.BuildInfo{Branch: a.gitBranch, Commit: a.gitCommit}

	mainMenu, err := d2gamescreen.CreateMainMenu(a, a.asset, a.renderer, a.inputManager, a.audio, a.ui, buildInfo, errorMessageOptional...)
	if err != nil {
		log.Print(err)
		return
	}

	a.screen.SetNextScreen(mainMenu)
}

// ToSelectHero forces the game to transition to the Select Hero (create character) screen
func (a *App) ToSelectHero(connType d2clientconnectiontype.ClientConnectionType, host string) {
	selectHero, err := d2gamescreen.CreateSelectHeroClass(a, a.asset, a.renderer, a.audio, a.ui, connType, host)
	if err != nil {
		log.Print(err)
		return
	}

	a.screen.SetNextScreen(selectHero)
}

// ToCreateGame forces the game to transition to the Create Game screen
func (a *App) ToCreateGame(filePath string, connType d2clientconnectiontype.ClientConnectionType, host string) {
	gameClient, err := d2client.Create(connType, a.asset, a.scriptEngine)
	if err != nil {
		log.Print(err)
	}

	if err = gameClient.Open(host, filePath); err != nil {
		errorMessage := fmt.Sprintf("can not connect to the host: %s", host)
		fmt.Println(errorMessage)
		a.ToMainMenu(errorMessage)
	} else {
		a.screen.SetNextScreen(d2gamescreen.CreateGame(
			a, a.asset, a.ui, a.renderer, a.inputManager, a.audio, gameClient, a.terminal, a.guiManager,
		))
	}
}

// ToCharacterSelect forces the game to transition to the Character Select (load character) screen
func (a *App) ToCharacterSelect(connType d2clientconnectiontype.ClientConnectionType, connHost string) {
	characterSelect, err := d2gamescreen.CreateCharacterSelect(a, a.asset, a.renderer, a.inputManager,
		a.audio, a.ui, connType, connHost)
	if err != nil {
		fmt.Printf("unable to create character select screen: %s", err)
	}

	a.screen.SetNextScreen(characterSelect)
}

// ToMapEngineTest forces the game to transition to the map engine test screen
func (a *App) ToMapEngineTest(region, level int) {
	met, err := d2gamescreen.CreateMapEngineTest(region, level, a.asset, a.terminal, a.renderer, a.inputManager, a.audio, a.screen)
	if err != nil {
		log.Print(err)
		return
	}

	a.screen.SetNextScreen(met)
}

// ToCredits forces the game to transition to the credits screen
func (a *App) ToCredits() {
	a.screen.SetNextScreen(d2gamescreen.CreateCredits(a, a.asset, a.renderer, a.ui))
}
