package d2gamescreen

import (
	"image"

	"github.com/OpenDiablo2/OpenDiablo2/d2core/d2hero"

	"github.com/OpenDiablo2/OpenDiablo2/d2core/d2inventory"

	"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
	"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
	"github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource"
	"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"
	"github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset"
	"github.com/OpenDiablo2/OpenDiablo2/d2core/d2screen"
	"github.com/OpenDiablo2/OpenDiablo2/d2core/d2ui"
	"github.com/OpenDiablo2/OpenDiablo2/d2networking/d2client/d2clientconnectiontype"
)

const (
	millisecondsPerSecond = 1000.0
)

type heroRenderConfig struct {
	idleAnimationPath               string
	idleSelectedAnimationPath       string
	forwardWalkAnimationPath        string
	forwardWalkOverlayAnimationPath string
	forwardWalkOverlayBlend         bool
	selectedAnimationPath           string
	selectedOverlayAnimationPath    string
	backWalkAnimationPath           string
	backWalkOverlayAnimationPath    string
	selectionBounds                 image.Rectangle
	selectSfx                       string
	deselectSfx                     string
	position                        image.Point
	idlePlayLengthMs                int
	forwardWalkPlayLengthMs         int
	backWalkPlayLengthMs            int
}

func point(x, y int) image.Point {
	return image.Point{X: x, Y: y}
}

func rect(x1, y1, x2, y2 int) image.Rectangle {
	return image.Rectangle{Min: point(x1, y1), Max: point(x2, y2)}
}

// animation position, selection box bound, animation play lengths in ms
const (
	barbPosX, barbPosY                                     = 400, 330
	barbRectMinX, barbRectMinY, barbRectMaxX, barbRectMaxY = 364, 201, 90, 170
	barbIdleLength, barbForwardLength, barbBackLength      = 0, 2500, 1000

	sorcPosX, sorcPosY                                     = 626, 352
	sorcRectMinX, sorcRectMinY, sorcRectMaxX, sorcRectMaxY = 580, 240, 65, 160
	sorcIdleLength, sorcForwardLength, sorcBackLength      = 2500, 2300, 1200

	necPosX, necPosY                                   = 300, 335
	necRectMinX, necRectMinY, necRectMaxX, necRectMaxY = 265, 220, 55, 175
	necIdleLength, necForwardLength, necBackLength     = 1200, 2000, 1500

	palPosX, palPosY                                   = 521, 338
	palRectMinX, palRectMinY, palRectMaxX, palRectMaxY = 490, 210, 65, 180
	palIdleLength, palForwardLength, palBackLength     = 2500, 3400, 1300

	amaPosX, amaPosY                                   = 100, 339
	amaRectMinX, amaRectMinY, amaRectMaxX, amaRectMaxY = 70, 220, 55, 200
	amaIdleLength, amaForwardLength, amaBackLength     = 2500, 2200, 1500

	assPosX, assPosY                                   = 231, 365
	assRectMinX, assRectMinY, assRectMaxX, assRectMaxY = 175, 235, 50, 180
	assIdleLength, assForwardLength, assBackLength     = 2500, 3800, 1500

	druPosX, druPosY                                   = 720, 370
	druRectMinX, druRectMinY, druRectMaxX, druRectMaxY = 680, 220, 70, 195
	druIdleLength, druForwardLength, druBackLength     = 1500, 4800, 1500

	campfirePosX, campfirePosY = 380, 335
)

// label and button positions
const (
	headingX, headingY               = 400, 17
	heroClassLabelX, heroClassLabelY = 400, 65
	heroDescLine1X, heroDescLine1Y   = 400, 100
	heroDescLine2X, heroDescLine2Y   = 400, 115
	heroDescLine3X, heroDescLine3Y   = 400, 130
	heroNameLabelX, heroNameLabelY   = 321, 475
	expansionLabelX, expansionLabelY = 339, 526
	hardcoreLabelX, hardcoreLabelY   = 339, 548

	selHeroExitBtnX, selHeroExitBtnY = 33, 537
	selHeroOkBtnX, selHeroOkBtnY     = 630, 537

	heroNameTextBoxX, heoNameTextBoxY       = 318, 493
	expandsionCheckboxX, expansionCheckboxY = 318, 526
	hardcoreCheckoxX, hardcoreCheckboxY     = 318, 548
)

const heroDescCharWidth = 37

//nolint:funlen // this func returns a map of structs and the structs are big, deal with it
func getHeroRenderConfiguration() map[d2enum.Hero]*heroRenderConfig {
	configs := make(map[d2enum.Hero]*heroRenderConfig)

	configs[d2enum.HeroBarbarian] = &heroRenderConfig{
		d2resource.CharacterSelectBarbarianUnselected,
		d2resource.CharacterSelectBarbarianUnselectedH,
		d2resource.CharacterSelectBarbarianForwardWalk,
		d2resource.CharacterSelectBarbarianForwardWalkOverlay,
		false,
		d2resource.CharacterSelectBarbarianSelected,
		"",
		d2resource.CharacterSelectBarbarianBackWalk,
		"",
		rect(barbRectMinX, barbRectMinY, barbRectMaxX, barbRectMaxY),
		d2resource.SFXBarbarianSelect,
		d2resource.SFXBarbarianDeselect,
		point(barbPosX, barbPosY),
		barbIdleLength,
		barbForwardLength,
		barbBackLength,
	}

	configs[d2enum.HeroSorceress] = &heroRenderConfig{
		d2resource.CharacterSelectSorceressUnselected,
		d2resource.CharacterSelectSorceressUnselectedH,
		d2resource.CharacterSelectSorceressForwardWalk,
		d2resource.CharacterSelectSorceressForwardWalkOverlay,
		true,
		d2resource.CharacterSelectSorceressSelected,
		d2resource.CharacterSelectSorceressSelectedOverlay,
		d2resource.CharacterSelectSorceressBackWalk,
		d2resource.CharacterSelectSorceressBackWalkOverlay,
		rect(sorcRectMinX, sorcRectMinY, sorcRectMaxX, sorcRectMaxY),
		d2resource.SFXSorceressSelect,
		d2resource.SFXSorceressDeselect,
		point(sorcPosX, sorcPosY),
		sorcIdleLength,
		sorcForwardLength,
		sorcBackLength,
	}

	configs[d2enum.HeroNecromancer] = &heroRenderConfig{
		d2resource.CharacterSelectNecromancerUnselected,
		d2resource.CharacterSelectNecromancerUnselectedH,
		d2resource.CharacterSelectNecromancerForwardWalk,
		d2resource.CharacterSelectNecromancerForwardWalkOverlay,
		true,
		d2resource.CharacterSelectNecromancerSelected,
		d2resource.CharacterSelectNecromancerSelectedOverlay,
		d2resource.CharacterSelectNecromancerBackWalk,
		d2resource.CharacterSelectNecromancerBackWalkOverlay,
		rect(necRectMinX, necRectMinY, necRectMaxX, necRectMaxY),
		d2resource.SFXNecromancerSelect,
		d2resource.SFXNecromancerDeselect,
		point(necPosX, necPosY),
		necIdleLength,
		necForwardLength,
		necBackLength,
	}

	configs[d2enum.HeroPaladin] = &heroRenderConfig{
		d2resource.CharacterSelectPaladinUnselected,
		d2resource.CharacterSelectPaladinUnselectedH,
		d2resource.CharacterSelectPaladinForwardWalk,
		d2resource.CharacterSelectPaladinForwardWalkOverlay,
		false,
		d2resource.CharacterSelectPaladinSelected,
		"",
		d2resource.CharacterSelectPaladinBackWalk,
		"",
		rect(palRectMinX, palRectMinY, palRectMaxX, palRectMaxY),
		d2resource.SFXPaladinSelect,
		d2resource.SFXPaladinDeselect,
		point(palPosX, palPosY),
		palIdleLength,
		palForwardLength,
		palBackLength,
	}

	configs[d2enum.HeroAmazon] = &heroRenderConfig{
		d2resource.CharacterSelectAmazonUnselected,
		d2resource.CharacterSelectAmazonUnselectedH,
		d2resource.CharacterSelectAmazonForwardWalk,
		"",
		false,
		d2resource.CharacterSelectAmazonSelected,
		"",
		d2resource.CharacterSelectAmazonBackWalk,
		"",
		rect(amaRectMinX, amaRectMinY, amaRectMaxX, amaRectMaxY),
		d2resource.SFXAmazonSelect,
		d2resource.SFXAmazonDeselect,
		point(amaPosX, amaPosY),
		amaIdleLength,
		amaForwardLength,
		amaBackLength,
	}

	configs[d2enum.HeroAssassin] = &heroRenderConfig{
		d2resource.CharacterSelectAssassinUnselected,
		d2resource.CharacterSelectAssassinUnselectedH,
		d2resource.CharacterSelectAssassinForwardWalk,
		"",
		false,
		d2resource.CharacterSelectAssassinSelected,
		"",
		d2resource.CharacterSelectAssassinBackWalk,
		"",
		rect(assRectMinX, assRectMinY, assRectMaxX, assRectMaxY),
		d2resource.SFXAssassinSelect,
		d2resource.SFXAssassinDeselect,
		point(assPosX, assPosY),
		assIdleLength,
		assForwardLength,
		assBackLength,
	}

	configs[d2enum.HeroDruid] = &heroRenderConfig{
		d2resource.CharacterSelectDruidUnselected,
		d2resource.CharacterSelectDruidUnselectedH,
		d2resource.CharacterSelectDruidForwardWalk,
		"",
		false,
		d2resource.CharacterSelectDruidSelected,
		"",
		d2resource.CharacterSelectDruidBackWalk,
		"",
		rect(druRectMinX, druRectMinY, druRectMaxX, druRectMaxY),
		d2resource.SFXDruidSelect,
		d2resource.SFXDruidDeselect,
		point(druPosX, druPosY),
		druIdleLength,
		druForwardLength,
		druBackLength,
	}

	return configs
}

// HeroRenderInfo stores the rendering information of a hero for the Select Hero Class screen
type HeroRenderInfo struct {
	Stance                   d2enum.HeroStance
	IdleSprite               *d2ui.Sprite
	IdleSelectedSprite       *d2ui.Sprite
	ForwardWalkSprite        *d2ui.Sprite
	ForwardWalkSpriteOverlay *d2ui.Sprite
	SelectedSprite           *d2ui.Sprite
	SelectedSpriteOverlay    *d2ui.Sprite
	BackWalkSprite           *d2ui.Sprite
	BackWalkSpriteOverlay    *d2ui.Sprite
	SelectionBounds          image.Rectangle
	SelectSfx                d2interface.SoundEffect
	DeselectSfx              d2interface.SoundEffect
	shc                      *SelectHeroClass
}

func (hri *HeroRenderInfo) advance(elapsed float64) {
	advanceSprite(hri.shc, hri.IdleSprite, elapsed)
	advanceSprite(hri.shc, hri.IdleSelectedSprite, elapsed)
	advanceSprite(hri.shc, hri.ForwardWalkSprite, elapsed)
	advanceSprite(hri.shc, hri.ForwardWalkSpriteOverlay, elapsed)
	advanceSprite(hri.shc, hri.SelectedSprite, elapsed)
	advanceSprite(hri.shc, hri.SelectedSpriteOverlay, elapsed)
	advanceSprite(hri.shc, hri.BackWalkSprite, elapsed)
	advanceSprite(hri.shc, hri.BackWalkSpriteOverlay, elapsed)
}

// CreateSelectHeroClass creates an instance of a SelectHeroClass
func CreateSelectHeroClass(
	navigator d2interface.Navigator,
	asset *d2asset.AssetManager,
	renderer d2interface.Renderer,
	audioProvider d2interface.AudioProvider,
	ui *d2ui.UIManager,
	connectionType d2clientconnectiontype.ClientConnectionType,
	l d2util.LogLevel,
	connectionHost string,
) (*SelectHeroClass, error) {
	playerStateFactory, err := d2hero.NewHeroStateFactory(asset)
	if err != nil {
		return nil, err
	}

	inventoryItemFactory, err := d2inventory.NewInventoryItemFactory(asset)
	if err != nil {
		return nil, err
	}

	selectHeroClass := &SelectHeroClass{
		asset:                asset,
		heroRenderInfo:       make(map[d2enum.Hero]*HeroRenderInfo),
		selectedHero:         d2enum.HeroNone,
		connectionType:       connectionType,
		connectionHost:       connectionHost,
		audioProvider:        audioProvider,
		renderer:             renderer,
		navigator:            navigator,
		uiManager:            ui,
		HeroStateFactory:     playerStateFactory,
		InventoryItemFactory: inventoryItemFactory,
	}

	selectHeroClass.Logger = d2util.NewLogger()
	selectHeroClass.Logger.SetLevel(l)
	selectHeroClass.Logger.SetPrefix(logPrefix)

	return selectHeroClass, nil
}

// SelectHeroClass represents the Select Hero Class screen
type SelectHeroClass struct {
	asset           *d2asset.AssetManager
	uiManager       *d2ui.UIManager
	bgImage         *d2ui.Sprite
	campfire        *d2ui.Sprite
	headingLabel    *d2ui.Label
	heroClassLabel  *d2ui.Label
	heroDesc1Label  *d2ui.Label
	heroDesc2Label  *d2ui.Label
	heroDesc3Label  *d2ui.Label
	heroNameTextbox *d2ui.TextBox
	heroNameLabel   *d2ui.Label
	heroRenderInfo  map[d2enum.Hero]*HeroRenderInfo
	*d2inventory.InventoryItemFactory
	*d2hero.HeroStateFactory
	selectedHero       d2enum.Hero
	exitButton         *d2ui.Button
	okButton           *d2ui.Button
	expansionCheckbox  *d2ui.Checkbox
	expansionCharLabel *d2ui.Label
	hardcoreCheckbox   *d2ui.Checkbox
	hardcoreCharLabel  *d2ui.Label
	connectionType     d2clientconnectiontype.ClientConnectionType
	connectionHost     string

	audioProvider d2interface.AudioProvider
	renderer      d2interface.Renderer
	navigator     d2interface.Navigator

	*d2util.Logger
}

// OnLoad loads the resources for the Select Hero Class screen
func (v *SelectHeroClass) OnLoad(loading d2screen.LoadingState) {
	v.audioProvider.PlayBGM(d2resource.BGMTitle)
	loading.Progress(tenPercent)

	v.bgImage = v.loadSprite(
		d2resource.CharacterSelectBackground,
		point(0, 0),
		0,
		true,
		false,
	)

	loading.Progress(thirtyPercent)

	v.createLabels()
	loading.Progress(fourtyPercent)
	v.createButtons()

	v.campfire = v.loadSprite(
		d2resource.CharacterSelectCampfire,
		point(campfirePosX, campfirePosY),
		0,
		true,
		true,
	)

	v.createCheckboxes()
	loading.Progress(fiftyPercent)

	for hero, config := range getHeroRenderConfiguration() {
		position := config.position
		forwardWalkOverlaySprite := v.loadSprite(
			config.forwardWalkOverlayAnimationPath,
			position,
			config.forwardWalkPlayLengthMs,
			false,
			config.forwardWalkOverlayBlend,
		)
		v.heroRenderInfo[hero] = &HeroRenderInfo{
			Stance: d2enum.HeroStanceIdle,
			IdleSprite: v.loadSprite(config.idleAnimationPath, position,
				config.idlePlayLengthMs, true, false),
			IdleSelectedSprite: v.loadSprite(config.idleSelectedAnimationPath,
				position,
				config.idlePlayLengthMs, true, false),
			ForwardWalkSprite: v.loadSprite(config.forwardWalkAnimationPath, position,
				config.forwardWalkPlayLengthMs, false, false),
			ForwardWalkSpriteOverlay: forwardWalkOverlaySprite,
			SelectedSprite: v.loadSprite(config.selectedAnimationPath, position,
				config.idlePlayLengthMs, true, false),
			SelectedSpriteOverlay: v.loadSprite(config.selectedOverlayAnimationPath, position,
				config.idlePlayLengthMs, true, true),
			BackWalkSprite: v.loadSprite(config.backWalkAnimationPath, position,
				config.backWalkPlayLengthMs, false, false),
			BackWalkSpriteOverlay: v.loadSprite(config.backWalkOverlayAnimationPath, position,
				config.backWalkPlayLengthMs, false, true),
			SelectionBounds: config.selectionBounds,
			SelectSfx:       v.loadSoundEffect(config.selectSfx),
			DeselectSfx:     v.loadSoundEffect(config.deselectSfx),
		}
	}
}

func (v *SelectHeroClass) createLabels() {
	v.headingLabel = v.uiManager.NewLabel(d2resource.Font30, d2resource.PaletteUnits)
	fontWidth, _ := v.headingLabel.GetSize()
	half := 2
	halfFontWidth := fontWidth / half

	v.headingLabel.SetPosition(headingX-halfFontWidth, headingY)
	v.headingLabel.SetText(v.asset.TranslateString(d2enum.SelectHeroClassLabel))
	v.headingLabel.Alignment = d2ui.HorizontalAlignCenter

	v.heroClassLabel = v.uiManager.NewLabel(d2resource.Font30, d2resource.PaletteUnits)
	v.heroClassLabel.Alignment = d2ui.HorizontalAlignCenter
	v.heroClassLabel.SetPosition(heroClassLabelX, heroClassLabelY)

	v.heroDesc1Label = v.uiManager.NewLabel(d2resource.Font16, d2resource.PaletteUnits)
	v.heroDesc1Label.Alignment = d2ui.HorizontalAlignCenter
	v.heroDesc1Label.SetPosition(heroDescLine1X, heroDescLine1Y)

	v.heroDesc2Label = v.uiManager.NewLabel(d2resource.Font16, d2resource.PaletteUnits)
	v.heroDesc2Label.Alignment = d2ui.HorizontalAlignCenter
	v.heroDesc2Label.SetPosition(heroDescLine2X, heroDescLine2Y)

	v.heroDesc3Label = v.uiManager.NewLabel(d2resource.Font16, d2resource.PaletteUnits)
	v.heroDesc3Label.Alignment = d2ui.HorizontalAlignCenter
	v.heroDesc3Label.SetPosition(heroDescLine3X, heroDescLine3Y)

	v.heroNameLabel = v.uiManager.NewLabel(d2resource.Font16, d2resource.PaletteUnits)
	v.heroNameLabel.Alignment = d2ui.HorizontalAlignLeft
	v.heroNameLabel.SetText(d2ui.ColorTokenize(v.asset.TranslateString(d2enum.CharNameLabel), d2ui.ColorTokenGold))
	v.heroNameLabel.SetPosition(heroNameLabelX, heroNameLabelY)

	v.expansionCharLabel = v.uiManager.NewLabel(d2resource.Font16, d2resource.PaletteUnits)
	v.expansionCharLabel.Alignment = d2ui.HorizontalAlignLeft
	v.expansionCharLabel.SetText(d2ui.ColorTokenize(v.asset.TranslateString("#803"), d2ui.ColorTokenGold))
	v.expansionCharLabel.SetPosition(expansionLabelX, expansionLabelY)

	v.hardcoreCharLabel = v.uiManager.NewLabel(d2resource.Font16, d2resource.PaletteUnits)
	v.hardcoreCharLabel.Alignment = d2ui.HorizontalAlignLeft
	v.hardcoreCharLabel.SetText(d2ui.ColorTokenize(v.asset.TranslateString(d2enum.HardCoreLabel), d2ui.ColorTokenGold))
	v.hardcoreCharLabel.SetPosition(hardcoreLabelX, hardcoreLabelY)
}

func (v *SelectHeroClass) createButtons() {
	v.exitButton = v.uiManager.NewButton(d2ui.ButtonTypeMedium, v.asset.TranslateString(d2enum.ExitLabel))
	v.exitButton.SetPosition(selHeroExitBtnX, selHeroExitBtnY)
	v.exitButton.OnActivated(func() { v.onExitButtonClicked() })

	v.okButton = v.uiManager.NewButton(d2ui.ButtonTypeMedium, v.asset.TranslateString(d2enum.OKLabel))
	v.okButton.SetPosition(selHeroOkBtnX, selHeroOkBtnY)
	v.okButton.OnActivated(func() { v.onOkButtonClicked() })
	v.okButton.SetVisible(false)
	v.okButton.SetEnabled(false)
}

func (v *SelectHeroClass) createCheckboxes() {
	v.heroNameTextbox = v.uiManager.NewTextbox()
	v.heroNameTextbox.SetPosition(heroNameTextBoxX, heoNameTextBoxY)
	v.heroNameTextbox.SetVisible(false)

	v.expansionCheckbox = v.uiManager.NewCheckbox(true)
	v.expansionCheckbox.SetPosition(expandsionCheckboxX, expansionCheckboxY)
	v.expansionCheckbox.SetVisible(false)

	v.hardcoreCheckbox = v.uiManager.NewCheckbox(false)
	v.hardcoreCheckbox.SetPosition(hardcoreCheckoxX, hardcoreCheckboxY)
	v.hardcoreCheckbox.SetVisible(false)
}

// OnUnload releases the resources of the Select Hero Class screen
func (v *SelectHeroClass) OnUnload() error {
	for i := range v.heroRenderInfo {
		v.heroRenderInfo[i].SelectSfx.Stop()
		v.heroRenderInfo[i].DeselectSfx.Stop()
	}

	v.heroRenderInfo = nil

	return nil
}

func (v *SelectHeroClass) onExitButtonClicked() {
	v.navigator.ToCharacterSelect(v.connectionType, v.connectionHost)
}

func (v *SelectHeroClass) onOkButtonClicked() {
	heroName := v.heroNameTextbox.GetText()
	defaultStats := v.asset.Records.Character.Stats[v.selectedHero]
	statsState := v.CreateHeroStatsState(v.selectedHero, defaultStats)

	playerState, err := v.CreateHeroState(heroName, v.selectedHero, statsState)
	if err != nil {
		v.Errorf("failed to create hero state!, err: %v", err.Error())
		return
	}

	err = v.Save(playerState)
	if err != nil {
		v.Errorf("failed to save game state!, err: %v", err.Error())
		return
	}

	playerState.Equipment = v.InventoryItemFactory.DefaultHeroItems[v.selectedHero]
	v.navigator.ToCreateGame(playerState.FilePath, v.connectionType, v.connectionHost)
}

// Render renders the Select Hero Class screen
func (v *SelectHeroClass) Render(screen d2interface.Surface) {
	v.bgImage.RenderSegmented(screen, 4, 3, 0)
	v.headingLabel.Render(screen)

	for heroClass, heroInfo := range v.heroRenderInfo {
		if heroInfo.Stance == d2enum.HeroStanceIdle || heroInfo.Stance == d2enum.HeroStanceIdleSelected {
			v.renderHero(screen, heroClass)
		}
	}

	for heroClass, heroInfo := range v.heroRenderInfo {
		if heroInfo.Stance != d2enum.HeroStanceIdle && heroInfo.Stance != d2enum.HeroStanceIdleSelected {
			v.renderHero(screen, heroClass)
		}
	}

	if v.selectedHero != d2enum.HeroNone {
		v.heroClassLabel.Render(screen)
		v.heroDesc1Label.Render(screen)
		v.heroDesc2Label.Render(screen)
		v.heroDesc3Label.Render(screen)
	}

	v.campfire.Render(screen)

	if v.heroNameTextbox.GetVisible() {
		v.heroNameLabel.Render(screen)
		v.expansionCharLabel.Render(screen)
		v.hardcoreCharLabel.Render(screen)
	}
}

// Advance runs the update logic on the Select Hero Class screen
func (v *SelectHeroClass) Advance(tickTime float64) error {
	canSelect := true

	if err := v.campfire.Advance(tickTime); err != nil {
		return err
	}

	for infoIdx := range v.heroRenderInfo {
		v.heroRenderInfo[infoIdx].advance(tickTime)

		if v.heroRenderInfo[infoIdx].Stance != d2enum.HeroStanceIdle &&
			v.heroRenderInfo[infoIdx].Stance != d2enum.HeroStanceIdleSelected &&
			v.heroRenderInfo[infoIdx].Stance != d2enum.HeroStanceSelected {
			canSelect = false
		}
	}

	for heroType := range v.heroRenderInfo {
		v.updateHeroSelectionHover(heroType, canSelect)
	}

	v.okButton.SetEnabled(len(v.heroNameTextbox.GetText()) >= 2 && v.selectedHero != d2enum.HeroNone)

	return nil
}

func (v *SelectHeroClass) updateHeroSelectionHover(hero d2enum.Hero, canSelect bool) {
	renderInfo := v.heroRenderInfo[hero]
	switch renderInfo.Stance {
	case d2enum.HeroStanceApproaching:
		if renderInfo.ForwardWalkSprite.IsOnLastFrame() {
			renderInfo.Stance = d2enum.HeroStanceSelected
			setSpriteToFirstFrame(renderInfo.SelectedSprite)
			setSpriteToFirstFrame(renderInfo.SelectedSpriteOverlay)
		}

		return
	case d2enum.HeroStanceRetreating:
		if renderInfo.BackWalkSprite.IsOnLastFrame() {
			renderInfo.Stance = d2enum.HeroStanceIdle
			setSpriteToFirstFrame(renderInfo.IdleSprite)
		}

		return
	}

	if !canSelect || renderInfo.Stance == d2enum.HeroStanceSelected {
		return
	}

	mouseX, mouseY := v.uiManager.CursorPosition()
	b := renderInfo.SelectionBounds
	mouseHover := (mouseX >= b.Min.X) && (mouseX <= b.Min.X+b.Max.X) && (mouseY >= b.Min.Y) && (mouseY <= b.Min.Y+b.Max.Y)

	if mouseHover && v.uiManager.CursorButtonPressed(d2ui.CursorButtonLeft) {
		v.handleCursorButtonPress(hero, renderInfo)
		return
	}

	v.setCurrentFrame(mouseHover, renderInfo)

	if v.selectedHero == d2enum.HeroNone && mouseHover {
		v.selectedHero = hero
		v.updateHeroText()
	}
}

func (v *SelectHeroClass) handleCursorButtonPress(hero d2enum.Hero, renderInfo *HeroRenderInfo) {
	v.heroNameTextbox.SetVisible(true)
	v.heroNameTextbox.Activate()
	v.okButton.SetVisible(true)
	v.expansionCheckbox.SetVisible(true)
	v.hardcoreCheckbox.SetVisible(true)

	renderInfo.Stance = d2enum.HeroStanceApproaching
	setSpriteToFirstFrame(renderInfo.ForwardWalkSprite)
	setSpriteToFirstFrame(renderInfo.ForwardWalkSpriteOverlay)

	for _, heroInfo := range v.heroRenderInfo {
		if heroInfo.Stance != d2enum.HeroStanceSelected {
			continue
		}

		heroInfo.SelectSfx.Stop()
		heroInfo.DeselectSfx.Play()
		heroInfo.Stance = d2enum.HeroStanceRetreating
		setSpriteToFirstFrame(heroInfo.BackWalkSprite)
		setSpriteToFirstFrame(heroInfo.BackWalkSpriteOverlay)
	}

	v.selectedHero = hero
	v.updateHeroText()
	renderInfo.SelectSfx.Play()
}

func (v *SelectHeroClass) setCurrentFrame(mouseHover bool, renderInfo *HeroRenderInfo) {
	if mouseHover && renderInfo.Stance != d2enum.HeroStanceIdleSelected {
		if err := renderInfo.IdleSelectedSprite.SetCurrentFrame(renderInfo.IdleSprite.GetCurrentFrame()); err != nil {
			v.Errorf("could not set current frame to: %d\n", renderInfo.IdleSprite.GetCurrentFrame())
		}

		renderInfo.Stance = d2enum.HeroStanceIdleSelected
	} else if !mouseHover && renderInfo.Stance != d2enum.HeroStanceIdle {
		if err := renderInfo.IdleSprite.SetCurrentFrame(renderInfo.IdleSelectedSprite.GetCurrentFrame()); err != nil {
			v.Errorf("could not set current frame to: %d\n", renderInfo.IdleSelectedSprite.GetCurrentFrame())
		}

		renderInfo.Stance = d2enum.HeroStanceIdle
	}
}

func (v *SelectHeroClass) renderHero(screen d2interface.Surface, hero d2enum.Hero) {
	renderInfo := v.heroRenderInfo[hero]
	switch renderInfo.Stance {
	case d2enum.HeroStanceIdle:
		drawSprite(renderInfo.IdleSprite, screen)
	case d2enum.HeroStanceIdleSelected:
		drawSprite(renderInfo.IdleSelectedSprite, screen)
	case d2enum.HeroStanceApproaching:
		drawSprite(renderInfo.ForwardWalkSprite, screen)
		drawSprite(renderInfo.ForwardWalkSpriteOverlay, screen)
	case d2enum.HeroStanceSelected:
		drawSprite(renderInfo.SelectedSprite, screen)
		drawSprite(renderInfo.SelectedSpriteOverlay, screen)
	case d2enum.HeroStanceRetreating:
		drawSprite(renderInfo.BackWalkSprite, screen)
		drawSprite(renderInfo.BackWalkSpriteOverlay, screen)
	}
}

func (v *SelectHeroClass) updateHeroText() {
	// v.setDescLabels("") really takes a string translation key, but temporarily disabled.
	switch v.selectedHero {
	case d2enum.HeroNone:
		return
	case d2enum.HeroBarbarian:
		v.heroClassLabel.SetText(v.asset.TranslateString("partycharbar"))
		v.setDescLabels(d2enum.BarbarianDescr, "")
	case d2enum.HeroNecromancer:
		v.heroClassLabel.SetText(v.asset.TranslateString("partycharnec"))
		v.setDescLabels(d2enum.NecromancerDescr, "")
	case d2enum.HeroPaladin:
		v.heroClassLabel.SetText(v.asset.TranslateString("partycharpal"))
		v.setDescLabels(d2enum.PaladinDescr, "")
	case d2enum.HeroAssassin:
		v.heroClassLabel.SetText(v.asset.TranslateString("partycharass"))
		v.setDescLabels(0, "#305")
	case d2enum.HeroSorceress:
		v.heroClassLabel.SetText(v.asset.TranslateString("partycharsor"))
		v.setDescLabels(d2enum.SorceressDescr, "")
	case d2enum.HeroAmazon:
		v.heroClassLabel.SetText(v.asset.TranslateString("partycharama"))
		v.setDescLabels(d2enum.AmazonDescr, "")
	case d2enum.HeroDruid:
		v.heroClassLabel.SetText(v.asset.TranslateString("partychardru"))
		// here is a problem with polish language: in polish string table, there are two items with key "#304"
		v.setDescLabels(0, "#304")
	}
}

const (
	oneLine = 1
	twoLine = 2
)

func (v *SelectHeroClass) setDescLabels(descKey int, key string) {
	var heroDesc string

	if key != "" {
		heroDesc = v.asset.TranslateString(key)
	} else {
		heroDesc = v.asset.TranslateString(descKey)
	}

	parts := d2util.SplitIntoLinesWithMaxWidth(heroDesc, heroDescCharWidth)

	numLines := len(parts)

	if numLines > oneLine {
		v.heroDesc1Label.SetText(parts[0])
		v.heroDesc2Label.SetText(parts[1])
	} else {
		v.heroDesc1Label.SetText("")
		v.heroDesc2Label.SetText("")
	}

	if numLines > twoLine {
		v.heroDesc3Label.SetText(parts[2])
	} else {
		v.heroDesc3Label.SetText("")
	}
}

func setSpriteToFirstFrame(sprite *d2ui.Sprite) {
	if sprite != nil {
		sprite.Rewind()
	}
}

func drawSprite(sprite *d2ui.Sprite, target d2interface.Surface) {
	if sprite != nil {
		sprite.Render(target)
	}
}

func advanceSprite(v *SelectHeroClass, sprite *d2ui.Sprite, elapsed float64) {
	if sprite != nil {
		if err := sprite.Advance(elapsed); err != nil {
			v.Error("could not advance the sprite:" + err.Error())
		}
	}
}

func (v *SelectHeroClass) loadSprite(animationPath string, position image.Point,
	playLength int,
	playLoop,
	blend bool) *d2ui.Sprite {
	if animationPath == "" {
		return nil
	}

	sprite, err := v.uiManager.NewSprite(animationPath, d2resource.PaletteFechar)
	if err != nil {
		v.Error("could not load sprite for the animation: %s\n" + animationPath + "with error: " + err.Error())
		return nil
	}

	sprite.PlayForward()
	sprite.SetPlayLoop(playLoop)

	if blend {
		sprite.SetEffect(d2enum.DrawEffectModulate)
	}

	if playLength != 0 {
		sprite.SetPlayLength(float64(playLength) / millisecondsPerSecond)
	}

	sprite.SetPosition(position.X, position.Y)

	return sprite
}

func (v *SelectHeroClass) loadSoundEffect(sfx string) d2interface.SoundEffect {
	result, err := v.audioProvider.LoadSound(sfx, false, false)
	if err != nil {
		v.Error(err.Error())
		return nil
	}

	return result
}
