package d2player

import (
	"fmt"
	"strings"

	"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/d2ui"
)

const (
	white = 0xffffffff
)

const ( // for the dc6 frames
	questLogTopLeft = iota
	questLogTopRight
	questLogBottomLeft
	questLogBottomRight
)

const (
	questLogOffsetX, questLogOffsetY = 80, 64
)

const (
	iconOffsetY                = 88
	questOffsetX, questOffsetY = 4, 4
	socket1X                   = 100
	socket2X                   = 200
	socket3X                   = 300
	socketUpY                  = 95
	socketDownY                = 190
)

const (
	questLogCloseButtonX, questLogCloseButtonY = 358, 455
	questLogDescrButtonX, questLogDescrButtonY = 308, 457
	questNameLabelX, questNameLabelY           = 240, 297
	questDescrLabelX, questDescrLabelY         = 90, 317
)

const (
	questTabY       = 66
	questTabYOffset = 31
	questTabBaseX   = 86
	questTabXOffset = 61
)

const questCompleteAnimationDuration = 3

func getPositionForSocket(number int) (x, y int) {
	pos := []struct {
		x int
		y int
	}{
		{socket1X, socketUpY},
		{socket2X, socketUpY},
		{socket3X, socketUpY},
		{socket1X, socketDownY},
		{socket2X, socketDownY},
		{socket3X, socketDownY},
	}

	return pos[number].x, pos[number].y
}

// NewQuestLog creates a new quest log
func NewQuestLog(asset *d2asset.AssetManager,
	ui *d2ui.UIManager,
	l d2util.LogLevel,
	audioProvider d2interface.AudioProvider,
	act int) *QuestLog {
	originX := 0
	originY := 0

	//nolint:gomnd // this is only test
	qs := map[int]int{
		0:  -2,
		1:  -2,
		2:  -2,
		3:  0,
		4:  1,
		5:  4,
		6:  3,
		7:  -1,
		8:  0,
		9:  0,
		10: 0,
		11: 0,
		12: 0,
		13: 0,
		14: 0,
		15: 0,
		16: 0,
		17: 0,
		18: 0,
		19: 0,
		20: 0,
		21: 0,
		22: 0,
		23: 0,
		24: 0,
		25: 0,
		26: 0,
	}

	var quests [d2enum.ActsNumber]*questEntire
	for i := 0; i < d2enum.ActsNumber; i++ {
		quests[i] = &questEntire{WidgetGroup: ui.NewWidgetGroup(d2ui.RenderPriorityQuestLog)}
	}

	var tabs [d2enum.ActsNumber]questLogTab
	for i := 0; i < d2enum.ActsNumber; i++ {
		tabs[i] = questLogTab{}
	}

	// nolint:gomnd // this is only test, it also should come from save file
	mpa := 2

	ql := &QuestLog{
		asset:         asset,
		uiManager:     ui,
		originX:       originX,
		originY:       originY,
		act:           act,
		tab:           tabs,
		quests:        quests,
		questStatus:   qs,
		maxPlayersAct: mpa,
		audioProvider: audioProvider,
	}

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

	return ql
}

// QuestLog represents the quest log
type QuestLog struct {
	asset         *d2asset.AssetManager
	uiManager     *d2ui.UIManager
	panel         *d2ui.Sprite
	onCloseCb     func()
	panelGroup    *d2ui.WidgetGroup
	selectedTab   int
	selectedQuest int
	act           int
	tab           [d2enum.ActsNumber]questLogTab
	audioProvider d2interface.AudioProvider
	completeSound d2interface.SoundEffect

	questName     *d2ui.Label
	questDescr    *d2ui.Label
	quests        [d2enum.ActsNumber]*questEntire
	questStatus   map[int]int
	maxPlayersAct int

	originX int
	originY int
	isOpen  bool

	*d2util.Logger
}

type questEntire struct {
	*d2ui.WidgetGroup
	icons   []*d2ui.Sprite
	buttons []*d2ui.Button
	sockets []*d2ui.Sprite
}

/* questIconTab returns path to quest animation using its
act and number. From d2resource:
        QuestLogAQuestAnimation = "/data/global/ui/MENU/a%dq%d.dc6"*/
func (s *QuestLog) questIconsTable(act, number int) string {
	return fmt.Sprintf(d2resource.QuestLogAQuestAnimation, act, number+1)
}

const (
	completedFrame  = 24
	inProgresFrame  = 25
	notStartedFrame = 26
)

const (
	socketNormalFrame      = 0
	socketHighlightedFrame = 1
)

const questDescriptionLenght = 30

type questLogTab struct {
	sprite          *d2ui.Sprite
	invisibleButton *d2ui.Button
}

// Load the data for the hero status panel
func (s *QuestLog) Load() {
	var err error

	s.panelGroup = s.uiManager.NewWidgetGroup(d2ui.RenderPriorityQuestLog)

	// quest completion sound.
	s.completeSound, err = s.audioProvider.LoadSound(d2resource.QuestLogDoneSfx, false, false)
	if err != nil {
		s.Error(err.Error())
	}

	frame := s.uiManager.NewUIFrame(d2ui.FrameLeft)
	s.panelGroup.AddWidget(frame)

	s.panel, err = s.uiManager.NewSprite(d2resource.QuestLogBg, d2resource.PaletteSky)
	if err != nil {
		s.Error(err.Error())
	}

	w, h := frame.GetSize()
	staticPanel := s.uiManager.NewCustomWidgetCached(s.renderStaticPanelFrames, w, h)
	s.panelGroup.AddWidget(staticPanel)

	closeButton := s.uiManager.NewButton(d2ui.ButtonTypeSquareClose, "")
	closeButton.SetVisible(false)
	closeButton.SetPosition(questLogCloseButtonX, questLogCloseButtonY)
	closeButton.OnActivated(func() { s.Close() })
	s.panelGroup.AddWidget(closeButton)

	descrButton := s.uiManager.NewButton(d2ui.ButtonTypeQuestDescr, "")
	descrButton.SetVisible(false)
	descrButton.SetPosition(questLogDescrButtonX, questLogDescrButtonY)
	descrButton.OnActivated(s.onDescrClicked)
	s.panelGroup.AddWidget(descrButton)

	s.questName = s.uiManager.NewLabel(d2resource.Font16, d2resource.PaletteStatic)
	s.questName.Alignment = d2ui.HorizontalAlignCenter
	s.questName.Color[0] = d2util.Color(white)
	s.questName.SetPosition(questNameLabelX, questNameLabelY)
	s.panelGroup.AddWidget(s.questName)

	s.questDescr = s.uiManager.NewLabel(d2resource.Font16, d2resource.PaletteStatic)
	s.questDescr.Alignment = d2ui.HorizontalAlignLeft
	s.questDescr.Color[0] = d2util.Color(white)
	s.questDescr.SetPosition(questDescrLabelX, questDescrLabelY)
	s.panelGroup.AddWidget(s.questDescr)

	s.loadTabs()

	// creates quest boards for each act
	for i := 0; i < d2enum.ActsNumber; i++ {
		item, icons, buttons, sockets := s.loadQuestBoard(i + 1)
		s.quests[i] = &questEntire{item, icons, buttons, sockets}
	}

	s.panelGroup.SetVisible(false)
}

// loadTabs loads quest log tabs
func (s *QuestLog) loadTabs() {
	var err error

	tabsResource := d2resource.WPTabs

	// create tabs only for 'discovered' acts
	for i := 0; i < s.maxPlayersAct; i++ {
		currentValue := i

		s.tab[i].sprite, err = s.uiManager.NewSprite(tabsResource, d2resource.PaletteSky)
		if err != nil {
			s.Error(err.Error())
		}

		// nolint:gomnd // it's constant.
		// each tab has two frames (active / inactive)
		frame := 2 * currentValue

		err := s.tab[i].sprite.SetCurrentFrame(frame)
		if err != nil {
			s.Errorf("Tabs sprite (%s) hasn't frame %d. %s", tabsResource, frame, err.Error())
		}

		s.tab[i].sprite.SetPosition(questTabBaseX+i*questTabXOffset, questTabY+questTabYOffset)

		s.tab[i].invisibleButton = s.uiManager.NewButton(d2ui.ButtonTypeTabBlank, "")
		s.tab[i].invisibleButton.SetPosition(questTabBaseX+i*questTabXOffset, questTabY)
		s.tab[i].invisibleButton.OnActivated(func() { s.setTab(currentValue) })

		s.panelGroup.AddWidget(s.tab[i].sprite)
		s.panelGroup.AddWidget(s.tab[i].invisibleButton)
	}

	// sets tab to current player's act.
	s.setTab(s.act - 1)
}

// loadQuestBoard creates quest fields (socket, button, icon) for specified act
func (s *QuestLog) loadQuestBoard(act int) (wg *d2ui.WidgetGroup, icons []*d2ui.Sprite, buttons []*d2ui.Button, sockets []*d2ui.Sprite) {
	wg = s.uiManager.NewWidgetGroup(d2ui.RenderPriorityQuestLog)

	// sets number of quests in act (for act 4 it's only 3, else 6)
	var questsInAct int
	if act == d2enum.Act4 {
		questsInAct = d2enum.HalfQuestsNumber
	} else {
		questsInAct = d2enum.NormalActQuestsNumber
	}

	for n := 0; n < questsInAct; n++ {
		cw := n
		x, y := getPositionForSocket(n)

		socket, err := s.uiManager.NewSprite(d2resource.QuestLogSocket, d2resource.PaletteSky)
		if err != nil {
			s.Error(err.Error())
		}

		socket.SetPosition(x, y+iconOffsetY+questOffsetY)
		sockets = append(sockets, socket)

		icon, err := s.makeQuestIconForAct(act, n, x, y)
		if err != nil {
			s.Error(err.Error())
		}

		icons = append(icons, icon)

		button := s.uiManager.NewButton(d2ui.ButtonTypeBlankQuestBtn, "")
		button.SetPosition(x+questOffsetX, y+questOffsetY)
		button.SetEnabled(s.questStatus[s.cordsToQuestID(act, cw)] != d2enum.QuestStatusNotStarted)
		buttons = append(buttons, button)
	}

	for i := 0; i < questsInAct; i++ {
		currentQuest := i

		// creates callback for quest button
		buttons[i].OnActivated(func() {
			var err error

			// set normal (not-highlighted) frame for each quest socket
			for j := 0; j < questsInAct; j++ {
				err = sockets[j].SetCurrentFrame(socketNormalFrame)
				if err != nil {
					s.Error(err.Error())
				}
			}

			// highlights appropriate socket
			err = sockets[currentQuest].SetCurrentFrame(socketHighlightedFrame)
			if err != nil {
				s.Error(err.Error())
			}

			// sets quest labels
			s.onQuestClicked(currentQuest + 1)
		})
	}

	// adds sockets to widget group
	for _, s := range sockets {
		wg.AddWidget(s)
	}

	// adds buttons to widget group
	for _, b := range buttons {
		wg.AddWidget(b)
	}

	// adds icons to widget group
	for _, i := range icons {
		wg.AddWidget(i)
	}

	wg.SetVisible(false)

	return wg, icons, buttons, sockets
}

func (s *QuestLog) makeQuestIconForAct(act, n, x, y int) (*d2ui.Sprite, error) {
	iconResource := s.questIconsTable(act, n)

	icon, err := s.uiManager.NewSprite(iconResource, d2resource.PaletteSky)
	if err != nil {
		s.Fatalf("during creating new quest icons for act %d (icon sprite %s doesn't exist). %s", act, iconResource, err.Error())
	}

	switch s.questStatus[s.cordsToQuestID(act, n)] {
	case d2enum.QuestStatusCompleted:
		err = icon.SetCurrentFrame(completedFrame)
	case d2enum.QuestStatusCompleting:
		// animation will be played after quest-log panel is opened (see s.playQuestAnimation)
		err = icon.SetCurrentFrame(0)
	case d2enum.QuestStatusNotStarted:
		err = icon.SetCurrentFrame(notStartedFrame)
	default:
		err = icon.SetCurrentFrame(inProgresFrame)
	}

	icon.SetPosition(x+questOffsetX, y+questOffsetY+iconOffsetY)

	return icon, err
}

// playQuestAnimations plays animations for quests (when status=questStatusCompleting)
func (s *QuestLog) playQuestAnimations() {
	for j, i := range s.quests[s.selectedTab].icons {
		questID := s.cordsToQuestID(s.selectedTab+1, j)
		if s.questStatus[questID] == d2enum.QuestStatusCompleting {
			s.completeSound.Play()

			// quest should be highlighted and it's label should be displayed
			s.quests[s.selectedTab].buttons[j].Activate()

			i.SetPlayLength(questCompleteAnimationDuration)
			i.PlayForward()
			i.SetPlayLoop(false)
		}
	}
}

// stopPlayedAnimation stops currently played animations and sets quests in
// completing state to completed (should be used, when quest log is closing)
func (s *QuestLog) stopPlayedAnimations() {
	// stops all played animations
	for j, i := range s.quests[s.selectedTab].icons {
		questID := s.cordsToQuestID(s.selectedTab+1, j)
		if s.questStatus[questID] == d2enum.QuestStatusCompleting {
			s.questStatus[questID] = d2enum.QuestStatusCompleted

			err := i.SetCurrentFrame(completedFrame)
			if err != nil {
				s.Error(err.Error())
			}
		}
	}
}

// setQuestLabel loads quest labels text (title and description)
func (s *QuestLog) setQuestLabel() {
	if s.selectedQuest == d2enum.QuestNone {
		s.questName.SetText("")
		s.questDescr.SetText("")

		return
	}

	s.questName.SetText(s.asset.TranslateString(fmt.Sprintf("qstsa%dq%d", s.selectedTab+1, s.selectedQuest)))

	status := s.questStatus[s.cordsToQuestID(s.selectedTab+1, s.selectedQuest)-1]
	switch status {
	case d2enum.QuestStatusCompleted, d2enum.QuestStatusCompleting:
		s.questDescr.SetText(
			strings.Join(
				d2util.SplitIntoLinesWithMaxWidth(
					s.asset.TranslateString("qstsprevious"),
					questDescriptionLenght),
				"\n"),
		)
	case d2enum.QuestStatusNotStarted:
		s.questDescr.SetText("")
	default:
		str := fmt.Sprintf("qstsa%dq%d%d", s.selectedTab+1, s.selectedQuest, status)
		descr := s.asset.TranslateString(str)

		// if description not found
		if str == descr {
			s.questDescr.SetText("")
		} else {
			s.questDescr.SetText(strings.Join(
				d2util.SplitIntoLinesWithMaxWidth(
					descr, questDescriptionLenght),
				"\n"),
			)
		}
	}
}

// switch all socket (in current tab) to normal state
func (s *QuestLog) clearHighlightment() {
	for _, i := range s.quests[s.selectedTab].sockets {
		err := i.SetCurrentFrame(socketNormalFrame)
		if err != nil {
			s.Error(err.Error())
		}
	}
}

func (s *QuestLog) setTab(tab int) {
	var mod int

	// before we leafe current tab, we need to switch highlighted
	// quest socket to normal frame
	s.clearHighlightment()

	s.selectedTab = tab
	s.selectedQuest = d2enum.QuestNone
	s.setQuestLabel()
	s.playQuestAnimations()

	// displays appropriate quests board
	for i := 0; i < s.maxPlayersAct; i++ {
		s.quests[i].SetVisible(tab == i)
	}

	// "highlights" appropriate tab
	for i := 0; i < s.maxPlayersAct; i++ {
		cv := i

		// converts bool to 1/0
		if cv == s.selectedTab {
			mod = 0
		} else {
			mod = 1
		}

		// sets tab sprite to highlighted/non-highlighted
		err := s.tab[cv].sprite.SetCurrentFrame(2*cv + mod)
		if err != nil {
			s.Error(err.Error())
		}
	}
}

func (s *QuestLog) onQuestClicked(number int) {
	s.selectedQuest = number
	s.setQuestLabel()
	s.Infof("Quest number %d in tab %d clicked", number, s.selectedTab)
}

//
func (s *QuestLog) onDescrClicked() {
	s.Info("Quest description button clicked")
}

// IsOpen returns true if the hero status panel is open
func (s *QuestLog) IsOpen() bool {
	return s.isOpen
}

// Toggle toggles the visibility of the hero status panel
func (s *QuestLog) Toggle() {
	if s.isOpen {
		s.Close()
	} else {
		s.Open()
	}
}

// Open opens the hero status panel
func (s *QuestLog) Open() {
	s.isOpen = true
	s.panelGroup.SetVisible(true)
	s.setTab(s.selectedTab)
	s.playQuestAnimations()
}

// Close closed the hero status panel
func (s *QuestLog) Close() {
	s.isOpen = false
	s.panelGroup.SetVisible(false)

	for i := 0; i < s.maxPlayersAct; i++ {
		s.quests[i].SetVisible(false)
	}

	s.stopPlayedAnimations()

	s.onCloseCb()
}

// SetOnCloseCb the callback run on closing the HeroStatsPanel
func (s *QuestLog) SetOnCloseCb(cb func()) {
	s.onCloseCb = cb
}

// Advance updates labels on the panel
func (s *QuestLog) Advance(elapsed float64) {
	if !s.IsOpen() {
		return
	}

	for j, i := range s.quests[s.selectedTab].icons {
		questID := s.cordsToQuestID(s.selectedTab+1, j)
		if s.questStatus[questID] == d2enum.QuestStatusCompleting {
			if err := i.Advance(elapsed); err != nil {
				s.Error(err.Error())
			}

			if i.GetCurrentFrame() == completedFrame {
				s.questStatus[questID] = d2enum.QuestStatusCompleted
			}
		}
	}
}

// nolint:dupl // I think it is OK, to duplicate this function
func (s *QuestLog) renderStaticPanelFrames(target d2interface.Surface) {
	frames := []int{
		questLogTopLeft,
		questLogTopRight,
		questLogBottomRight,
		questLogBottomLeft,
	}

	currentX := s.originX + questLogOffsetX
	currentY := s.originY + questLogOffsetY

	for _, frameIndex := range frames {
		if err := s.panel.SetCurrentFrame(frameIndex); err != nil {
			s.Error(err.Error())
		}

		w, h := s.panel.GetCurrentFrameSize()

		switch frameIndex {
		case questLogTopLeft:
			s.panel.SetPosition(currentX, currentY+h)
			currentX += w
		case questLogTopRight:
			s.panel.SetPosition(currentX, currentY+h)
			currentY += h
		case questLogBottomRight:
			s.panel.SetPosition(currentX, currentY+h)
		case questLogBottomLeft:
			s.panel.SetPosition(currentX-w, currentY+h)
		}

		s.panel.Render(target)
	}
}

func (s *QuestLog) cordsToQuestID(act, number int) int {
	key := (act-1)*d2enum.NormalActQuestsNumber + number
	if act > d2enum.Act4 {
		key -= d2enum.HalfQuestsNumber
	}

	return key
}
