package diablo2stats

import (
	"fmt"
	"strings"

	"github.com/OpenDiablo2/OpenDiablo2/d2core/d2records"

	"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2tbl"
	"github.com/OpenDiablo2/OpenDiablo2/d2core/d2stats"
)

// static check that diablo2Stat implements Stat
var _ d2stats.Stat = &diablo2Stat{}

type descValPosition int

const (
	descValHide descValPosition = iota
	descValPrefix
	descValPostfix
)

const (
	maxSkillTabIndex = 2
	oneValue         = 1
	twoValue         = 2
	threeValue       = 3
	fourValue        = 4
)

const (
	twoComponentStr   = "%s %s"
	threeComponentStr = "%s %s %s"
	fourComponentStr  = "%s %s %s %s"
)

const (
	intVal = d2stats.StatValueInt
	sum    = d2stats.StatValueCombineSum
	static = d2stats.StatValueCombineStatic
)

// diablo2Stat is an implementation of an OpenDiablo2 Stat, with a set of values.
// It is pretty tightly coupled to the data files for d2
type diablo2Stat struct {
	factory *StatFactory
	record  *d2records.ItemStatCostRecord
	values  []d2stats.StatValue
}

// depending on the stat record, sets up the proper number of values,
// as well as set up the stat value number types, value combination types, and
// the value stringer functions used
func (s *diablo2Stat) init(numbers ...float64) { //nolint:funlen,gocyclo // can't reduce
	if s.record == nil {
		return
	}

	//nolint:gomnd // introducing a const for these would be worse
	switch s.record.DescFnID {
	case 0:
		// special case for poisonlength, or other stats, which have a
		// 0-value descfnID field but need to store values
		s.values = make([]d2stats.StatValue, len(numbers))
		for idx := range s.values {
			s.values[idx] = s.factory.NewValue(intVal, sum).SetStringer(s.factory.stringerIntSigned)
		}
	case 1:
		// +31 to Strength
		// Replenish Life +20 || Drain Life -8
		s.values = make([]d2stats.StatValue, oneValue)
		s.values[0] = s.factory.NewValue(intVal, sum).SetStringer(s.factory.stringerIntSigned)
	case 2:
		// +16% Increased Chance of Blocking
		// Lightning Absorb +10%
		s.values = make([]d2stats.StatValue, oneValue)
		s.values[0] = s.factory.NewValue(intVal,
			sum).SetStringer(s.factory.stringerIntPercentageSigned)
	case 3:
		// Damage Reduced by 25
		// Slain Monsters Rest in Peace
		s.values = make([]d2stats.StatValue, oneValue)
		s.values[0] = s.factory.NewValue(intVal, sum)
	case 4:
		// Poison Resist +25%
		// +25% Faster Run/Walk
		s.values = make([]d2stats.StatValue, oneValue)
		s.values[0] = s.factory.NewValue(intVal,
			sum).SetStringer(s.factory.stringerIntPercentageSigned)
	case 5:
		// Hit Causes Monster to Flee 25%
		s.values = make([]d2stats.StatValue, oneValue)
		s.values[0] = s.factory.NewValue(intVal, sum)
		s.values[0].SetStringer(s.factory.stringerIntPercentageUnsigned)
	case 6:
		// +25 to Life (Based on Character Level)
		s.values = make([]d2stats.StatValue, oneValue)
		s.values[0] = s.factory.NewValue(intVal, sum).SetStringer(s.factory.stringerIntSigned)
	case 7:
		// Lightning Resist +25% (Based on Character Level)
		// +25% Better Chance of Getting Magic Items (Based on Character Level)
		s.values = make([]d2stats.StatValue, oneValue)
		s.values[0] = s.factory.NewValue(intVal,
			sum).SetStringer(s.factory.stringerIntPercentageSigned)
	case 8:
		// +25% Enhanced Defense (Based on Character Level)
		// Heal Stamina Plus +25% (Based on Character Level)
		s.values = make([]d2stats.StatValue, oneValue)
		s.values[0] = s.factory.NewValue(intVal,
			sum).SetStringer(s.factory.stringerIntPercentageSigned)
	case 9:
		// Attacker Takes Damage of 25 (Based on Character Level)
		s.values = make([]d2stats.StatValue, oneValue)
		s.values[0] = s.factory.NewValue(intVal, sum)
	case 11:
		// Repairs 2 durability per second
		s.values = make([]d2stats.StatValue, oneValue)
		s.values[0] = s.factory.NewValue(intVal, sum)
	case 12:
		// Hit Blinds Target +5
		s.values = make([]d2stats.StatValue, oneValue)
		s.values[0] = s.factory.NewValue(intVal, sum).SetStringer(s.factory.stringerIntSigned)
	case 13:
		// +5 to Paladin Skill Levels
		s.values = make([]d2stats.StatValue, twoValue)
		s.values[0] = s.factory.NewValue(intVal, sum).SetStringer(s.factory.stringerIntSigned)
		s.values[1] = s.factory.NewValue(intVal, sum).SetStringer(s.factory.stringerClassAllSkills)
	case 14:
		// +5 to Combat Skills (Paladin Only)
		s.values = make([]d2stats.StatValue, threeValue)
		s.values[0] = s.factory.NewValue(intVal, sum).SetStringer(s.factory.stringerIntSigned)
		s.values[1] = s.factory.NewValue(intVal, sum).SetStringer(s.factory.stringerClassOnly)
		s.values[2] = s.factory.NewValue(intVal, static)
	case 15:
		//  5% Chance to cast level 7 Frozen Orb on attack
		s.values = make([]d2stats.StatValue, threeValue)
		s.values[0] = s.factory.NewValue(intVal, sum)
		s.values[1] = s.factory.NewValue(intVal, static)
		s.values[2] = s.factory.NewValue(intVal, static).SetStringer(s.factory.stringerSkillName)
	case 16:
		// Level 3 Warmth Aura When Equipped
		s.values = make([]d2stats.StatValue, twoValue)
		s.values[0] = s.factory.NewValue(intVal, sum)
		s.values[1] = s.factory.NewValue(intVal, static).SetStringer(s.factory.stringerSkillName)
	case 20:
		// -25% Target Defense
		s.values = make([]d2stats.StatValue, oneValue)
		s.values[0] = s.factory.NewValue(intVal,
			sum).SetStringer(s.factory.stringerIntPercentageSigned)
	case 22:
		// 25% to Attack Rating versus Specter
		s.values = make([]d2stats.StatValue, twoValue)
		s.values[0] = s.factory.NewValue(intVal,
			sum).SetStringer(s.factory.stringerIntPercentageUnsigned)
		s.values[1] = s.factory.NewValue(intVal, static).SetStringer(s.factory.stringerMonsterName)
	case 23:
		//  25% Reanimate as: Specter
		s.values = make([]d2stats.StatValue, twoValue)
		s.values[0] = s.factory.NewValue(intVal,
			sum).SetStringer(s.factory.stringerIntPercentageUnsigned)
		s.values[1] = s.factory.NewValue(intVal, static).SetStringer(s.factory.stringerMonsterName)
	case 24:
		// Level 25 Frozen Orb (19/20 Charges)
		s.values = make([]d2stats.StatValue, fourValue)
		s.values[0] = s.factory.NewValue(intVal, static)
		s.values[1] = s.factory.NewValue(intVal, static).SetStringer(s.factory.stringerSkillName)
		s.values[2] = s.factory.NewValue(intVal, static)
		s.values[3] = s.factory.NewValue(intVal, static)
	case 27:
		// +25 to Frozen Orb (Paladin Only)
		s.values = make([]d2stats.StatValue, threeValue)
		s.values[0] = s.factory.NewValue(intVal, sum).SetStringer(s.factory.stringerIntSigned)
		s.values[1] = s.factory.NewValue(intVal, static).SetStringer(s.factory.stringerSkillName)
		s.values[2] = s.factory.NewValue(intVal, static).SetStringer(s.factory.stringerClassOnly)
	case 28:
		// +25 to Frozen Orb
		s.values = make([]d2stats.StatValue, twoValue)
		s.values[0] = s.factory.NewValue(intVal, sum).SetStringer(s.factory.stringerIntSigned)
		s.values[1] = s.factory.NewValue(intVal, static).SetStringer(s.factory.stringerSkillName)
	default:
		return
	}

	for idx := range numbers {
		if idx > len(s.values)-1 {
			break
		}

		s.values[idx].SetFloat(numbers[idx])
	}
}

// Name returns the name of the stat (the key in itemstatcosts)
func (s *diablo2Stat) Name() string {
	return s.record.Name
}

// Priority returns the description printing priority
func (s *diablo2Stat) Priority() int {
	return s.record.DescPriority
}

// Values returns the stat values of the stat
func (s *diablo2Stat) Values() []d2stats.StatValue {
	return s.values
}

// SetValues sets the stat values
func (s *diablo2Stat) SetValues(values ...d2stats.StatValue) {
	s.values = make([]d2stats.StatValue, len(values))
	for idx := range values {
		s.values[idx] = values[idx]
	}
}

// Clone returns a deep copy of the diablo2Stat
func (s *diablo2Stat) Clone() d2stats.Stat {
	clone := &diablo2Stat{
		record: s.record,
	}

	clone.init()

	for idx := range s.values {
		srcVal := s.values[idx]
		dstVal := &Diablo2StatValue{
			numberType:  srcVal.NumberType(),
			combineType: srcVal.CombineType(),
		}

		dstVal.SetStringer(srcVal.Stringer())

		switch srcVal.NumberType() {
		case d2stats.StatValueInt:
			dstVal.SetInt(srcVal.Int())
		case d2stats.StatValueFloat:
			dstVal.SetFloat(srcVal.Float())
		}

		if len(clone.values) < len(s.values) {
			clone.values = make([]d2stats.StatValue, len(s.values))
		}

		clone.values[idx] = dstVal
	}

	return clone
}

// Copy to this stat value the values of the given stat value
func (s *diablo2Stat) Copy(from d2stats.Stat) d2stats.Stat {
	srcValues := from.Values()
	s.values = make([]d2stats.StatValue, len(srcValues))

	for idx := range srcValues {
		src := srcValues[idx]
		valType := src.NumberType()
		dst := &Diablo2StatValue{numberType: valType}
		dst.SetStringer(src.Stringer())

		switch valType {
		case d2stats.StatValueInt:
			dst.SetInt(src.Int())
		case d2stats.StatValueFloat:
			dst.SetFloat(src.Float())
		}

		s.values[idx] = dst
	}

	return s
}

// Combine sums the other stat with this one (does not alter this stat, returns altered clone!)
func (s *diablo2Stat) Combine(other d2stats.Stat) (result d2stats.Stat, err error) {
	cantBeCombinedErr := fmt.Errorf("cannot combine %s with %s", s.Name(), other.Name())

	if !s.canBeCombinedWith(other) {
		return nil, cantBeCombinedErr
	}

	result = s.Clone()
	srcValues, dstValues := other.Values(), result.Values()

	for idx := range result.Values() {
		v1, v2 := dstValues[idx], srcValues[idx]
		combinationRule := v1.CombineType()

		if combinationRule == d2stats.StatValueCombineStatic {
			// we do not add the values, they remain the same
			// for things like monster/class/skill index or on proc stats
			// where the level of a skill isn't summed, but the
			// chance to cast values are
			continue
		}

		if combinationRule == d2stats.StatValueCombineSum {
			valType := v1.NumberType()
			switch valType {
			case d2stats.StatValueInt:
				v1.SetInt(v1.Int() + v2.Int())
			case d2stats.StatValueFloat:
				v1.SetFloat(v1.Float() + v2.Float())
			}
		}
	}

	return result, nil
}

func (s *diablo2Stat) canBeCombinedWith(other d2stats.Stat) bool {
	if s.Name() != other.Name() {
		return false
	}

	values1, values2 := s.Values(), other.Values()
	if len(values1) != len(values2) {
		return false
	}

	for idx := range values1 {
		if values1[idx].NumberType() != values2[idx].NumberType() {
			return false
		}

		if values1[idx].CombineType() != values2[idx].CombineType() {
			return false
		}

		// in the case that we are trying to combine stats like:
		// 		+1 to Paladin Skills
		// 		+1 to Sorceress Skills
		// the numeric value (an index) that denotes the class skill type knows not to be summed
		// with the other index, even though the format of the stat and stat value is pretty much
		// the same.
		if values1[idx].CombineType() == d2stats.StatValueCombineStatic {
			if values1[idx].Float() != values2[idx].Float() {
				return false
			}
		}
	}

	return true
}

// String returns the formatted description string
func (s *diablo2Stat) String() string { //nolint:gocyclo // switch statement is not so bad
	var result string

	for idx := range s.values {
		if s.values[idx].Stringer() == nil {
			s.values[idx].SetStringer(s.factory.stringerUnsignedInt)
		}
	}

	//nolint:gomnd // introducing a const for these would be worse
	switch s.record.DescFnID {
	case 1, 2, 3, 4, 5, 12, 20:
		result = s.descFn1()
	case 6, 7, 8:
		result = s.descFn6()
	case 9:
		result = s.descFn9()
	case 11:
		result = s.descFn11()
	case 13:
		result = s.descFn13()
	case 14:
		result = s.descFn14()
	case 15:
		result = s.descFn15()
	case 16:
		result = s.descFn16()
	case 22, 23:
		result = s.descFn22()
	case 24:
		result = s.descFn24()
	case 27:
		result = s.descFn27()
	case 28:
		result = s.descFn28()
	default:
		result = ""
	}

	return result
}

func (s *diablo2Stat) descFn1() string {
	var stringTableKey, result string

	value := s.values[0]

	formatString := twoComponentStr

	if value.Int() < 0 {
		stringTableKey = s.record.DescStrNeg
	} else {
		stringTableKey = s.record.DescStrPos
	}

	stringTableString := d2tbl.TranslateString(stringTableKey)

	switch descValPosition(s.record.DescVal) {
	case descValPrefix:
		result = fmt.Sprintf(formatString, value, stringTableString)
	case descValPostfix:
		result = fmt.Sprintf(formatString, stringTableString, value)
	case descValHide:
		result = stringTableString
	default:
		result = ""
	}

	return result
}

func (s *diablo2Stat) descFn6() string {
	var stringTableKey, result string

	value := s.values[0]

	formatString := threeComponentStr

	if value.Int() < 0 {
		stringTableKey = s.record.DescStrNeg
	} else {
		stringTableKey = s.record.DescStrPos
	}

	str1 := d2tbl.TranslateString(stringTableKey)
	str2 := d2tbl.TranslateString(s.record.DescStr2)

	switch descValPosition(s.record.DescVal) {
	case descValPrefix:
		result = fmt.Sprintf(formatString, value, str1, str2)
	case descValPostfix:
		result = fmt.Sprintf(formatString, str1, value, str2)
	case descValHide:
		formatString = twoComponentStr
		result = fmt.Sprintf(formatString, value, str2)
	default:
		result = ""
	}

	return result
}

func (s *diablo2Stat) descFn9() string {
	var stringTableKey, result string

	value := s.values[0]

	formatString := threeComponentStr

	if value.Int() < 0 {
		stringTableKey = s.record.DescStrNeg
	} else {
		stringTableKey = s.record.DescStrPos
	}

	str1 := d2tbl.TranslateString(stringTableKey)
	str2 := d2tbl.TranslateString(s.record.DescStr2)

	switch descValPosition(s.record.DescVal) {
	case descValPrefix:
		result = fmt.Sprintf(formatString, value, str1, str2)
	case descValPostfix:
		result = fmt.Sprintf(formatString, str1, value, str2)
	case descValHide:
		result = fmt.Sprintf(twoComponentStr, value, str2)
	default:
		result = ""
	}

	return result
}

func (s *diablo2Stat) descFn11() string {
	var stringTableKey string

	value := s.values[0]

	if value.Int() < 0 {
		stringTableKey = s.record.DescStrNeg
	} else {
		stringTableKey = s.record.DescStrPos
	}

	str1 := d2tbl.TranslateString(stringTableKey)

	formatString := str1

	return fmt.Sprintf(formatString, value)
}

func (s *diablo2Stat) descFn13() string {
	value := s.values[0]
	allSkills := s.values[1]

	formatString := twoComponentStr

	switch descValPosition(s.record.DescVal) {
	case descValPrefix:
		return fmt.Sprintf(formatString, value, allSkills)
	case descValPostfix:
		return fmt.Sprintf(formatString, allSkills, value)
	case descValHide:
		return allSkills.String()
	default:
		return ""
	}
}

func (s *diablo2Stat) descFn14() string {
	// strings come out like `+5 to Combat Skills (Paladin Only)`
	numSkills, hero, skillTab := s.values[0], s.values[1], s.values[2]
	heroMap := s.factory.getHeroMap()
	heroIndex := hero.Int()
	classRecord := s.factory.asset.Records.Character.Stats[heroMap[heroIndex]]

	// diablo 2 is hardcoded to have only 3 skill tabs
	skillTabIndex := skillTab.Int()
	if skillTabIndex < 0 || skillTabIndex > maxSkillTabIndex {
		skillTabIndex = 0
	}

	// `+5`
	numSkillsStr := numSkills.String()

	// `to Combat Skills`
	skillTabKey := classRecord.SkillStrTab[skillTabIndex]
	skillTabStr := d2tbl.TranslateString(skillTabKey)
	skillTabStr = strings.ReplaceAll(skillTabStr, "+%d ", "") // has a token we dont need

	// `(Paladin Only)`
	classOnlyStr := hero.String()

	return fmt.Sprintf(threeComponentStr, numSkillsStr, skillTabStr, classOnlyStr)
}

func (s *diablo2Stat) descFn15() string {
	chance, lvl, skill := s.values[0], s.values[1], s.values[2]

	// Special case, `chance to cast` format is actually in the string table!
	chanceToCastStr := d2tbl.TranslateString(s.record.DescStrPos)

	return fmt.Sprintf(chanceToCastStr, chance.Int(), lvl.Int(), skill)
}

func (s *diablo2Stat) descFn16() string {
	skillLevel, skillIndex := s.values[0], s.values[1]

	// Special case, `Level # XYZ Aura When Equipped`, format is actually in the string table!
	format := d2tbl.TranslateString(s.record.DescStrPos)

	return fmt.Sprintf(format, skillLevel.Int(), skillIndex)
}

func (s *diablo2Stat) descFn22() string {
	arBonus, monsterIndex := s.values[0], s.values[1]
	arVersus := d2tbl.TranslateString(s.record.DescStrPos)

	return fmt.Sprintf(threeComponentStr, arBonus, arVersus, monsterIndex)
}

func (s *diablo2Stat) descFn24() string {
	// Special case formatting
	format := "Level " + threeComponentStr

	lvl, skill, chargeMax, chargeCurrent := s.values[0],
		s.values[1],
		s.values[2].Int(),
		s.values[3].Int()

	chargeStr := d2tbl.TranslateString(s.record.DescStrPos)
	chargeStr = fmt.Sprintf(chargeStr, chargeCurrent, chargeMax)

	return fmt.Sprintf(format, lvl, skill, chargeStr)
}

func (s *diablo2Stat) descFn27() string {
	// property "skill-rand" will try to make an instance with an invalid hero index
	// in this case, we use descfn 28
	if s.values[2].Int() == -1 {
		return s.descFn28()
	}

	amount, skill, hero := s.values[0], s.values[1], s.values[2]

	return fmt.Sprintf(fourComponentStr, amount, "to", skill, hero)
}

func (s *diablo2Stat) descFn28() string {
	amount, skill := s.values[0], s.values[1]

	return fmt.Sprintf(threeComponentStr, amount, "to", skill)
}

// DescGroupString return a string based on the DescGroupFuncID
func (s *diablo2Stat) DescGroupString(a ...interface{}) string {
	if s.record.DescGroupFuncID < 0 {
		return ""
	}

	format := ""
	for range a {
		format += "%s "
	}

	format = strings.Trim(format, " ")

	return fmt.Sprintf(format, a...)
}
