package d2records

import (
	"fmt"

	"github.com/OpenDiablo2/OpenDiablo2/d2common/d2util"

	"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
	"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2animdata"
	"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2txt"
	"github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource"
)

const (
	logPrefix = "Record Manager"
)

// NewRecordManager creates a new record manager (no loaders are bound!)
func NewRecordManager(l d2util.LogLevel) (*RecordManager, error) {
	rm := &RecordManager{
		boundLoaders: make(map[string][]recordLoader),
	}

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

	err := rm.init()
	if err != nil {
		return nil, err
	}

	return rm, nil
}

// RecordManager stores all of the records loaded from txt files
type RecordManager struct {
	*d2util.Logger
	boundLoaders map[string][]recordLoader // there can be more than one loader bound for a file
	Animation    struct {
		Data  *d2animdata.AnimationData
		Token struct {
			Player    PlayerTypes
			Composite CompositeTypes
			Armor     ArmorTypes
			Weapon    WeaponClasses
			HitClass  HitClasses
		}
	}
	BodyLocations
	Calculation struct {
		Skills   Calculations
		Missiles Calculations
	}
	Character struct {
		Classes PlayerClasses
		Events
		Experience ExperienceBreakpoints
		MaxLevel   ExperienceMaxLevels
		Modes      PlayerModes
		Stats      CharStats
	}
	ComponentCodes
	Colors
	DifficultyLevels
	ElemTypes
	Gamble
	Hireling struct {
		Details      Hirelings
		Descriptions HirelingDescriptions
	}
	Item struct {
		All CommonItems // NOTE: populated when armor, weapons, and misc items are ALL loaded

		Armors  CommonItems
		Misc    CommonItems
		Weapons CommonItems

		Equivalency         ItemEquivalenceMap      // NOTE: populated when all items are loaded
		EquivalenceByRecord ItemEquivalenceByRecord // NOTE: populated when all items are loaded

		AutoMagic
		Belts
		Books
		Gems
		Magic struct {
			Prefix MagicPrefix
			Suffix MagicSuffix
		}
		MagicPrefixGroups  ItemAffixGroups
		MagicSuffixGroups  ItemAffixGroups
		Quality            ItemQualities
		LowQualityPrefixes LowQualities
		Rare               struct {
			Prefix RarePrefixes
			Suffix RareSuffixes
		}
		Ratios ItemRatios
		Cube   struct {
			Recipes   CubeRecipes
			Modifiers CubeModifiers
			Types     CubeTypes
		}
		Runewords
		Sets
		SetItems
		Stats    ItemStatCosts
		Treasure struct {
			Normal    TreasureClass
			Expansion TreasureClass
		}
		Types  ItemTypes
		Unique UniqueItems
		StorePages
	}
	Layout struct {
		Inventory
		Overlays
	}
	Level struct {
		AutoMaps
		Details LevelDetails
		Maze    LevelMazeDetails
		Presets LevelPresets
		Sub     LevelSubstitutions
		Types   LevelTypes
		Warp    LevelWarps
	}
	Missiles
	missilesByName
	Monster struct {
		AI        MonsterAI
		Equipment MonsterEquipment
		Levels    MonsterLevels
		Modes     MonModes
		Name      struct {
			Prefix UniqueMonsterAffixes
			Suffix UniqueMonsterAffixes
		}
		Placements MonsterPlacements
		Presets    MonPresets
		Props      MonsterProperties
		Sequences  MonsterSequences
		Sounds     MonsterSounds
		Stats      MonStats
		Stats2     MonStats2
		Types      MonsterTypes
		Unique     struct {
			Appellations UniqueAppellations
			Mods         MonsterUniqueModifiers
			Constants    MonsterUniqueModifierConstants
			Super        SuperUniques
		}
	}
	NPCs
	Object struct {
		Details ObjectDetails
		Lookup  IndexedObjects
		Modes   ObjectModes
		Shrines
		Types ObjectTypes
	}
	PetTypes
	Properties
	Skill struct {
		Details      SkillDetails
		Descriptions SkillDescriptions
	}
	Sound struct {
		Details     SoundDetails
		Environment SoundEnvironments
	}
	States
}

func (r *RecordManager) init() error { // nolint:funlen // can't reduce
	loaders := []struct {
		path   string
		loader recordLoader
	}{
		{d2resource.LevelType, levelTypesLoader},
		{d2resource.LevelPreset, levelPresetLoader},
		{d2resource.LevelWarp, levelWarpsLoader},
		{d2resource.ObjectType, objectTypesLoader},
		{d2resource.ObjectDetails, objectDetailsLoader},
		{d2resource.ObjectMode, objectModesLoader},
		{d2resource.Weapons, weaponsLoader},
		{d2resource.Armor, armorLoader},
		{d2resource.Misc, miscItemsLoader},
		{d2resource.Books, booksLoader},
		{d2resource.Belts, beltsLoader},
		{d2resource.Colors, colorsLoader},
		{d2resource.ItemTypes, itemTypesLoader}, // WARN: needs to be after weapons, armor, and misc
		{d2resource.UniqueItems, uniqueItemsLoader},
		{d2resource.Missiles, missilesLoader},
		{d2resource.SoundSettings, soundDetailsLoader},
		{d2resource.MonStats, monsterStatsLoader},
		{d2resource.MonStats2, monsterStats2Loader},
		{d2resource.MonPreset, monsterPresetLoader},
		{d2resource.MonProp, monsterPropertiesLoader},
		{d2resource.MonType, monsterTypesLoader},
		{d2resource.MonMode, monsterModeLoader},
		{d2resource.MagicPrefix, magicPrefixLoader},
		{d2resource.MagicSuffix, magicSuffixLoader},
		{d2resource.ItemStatCost, itemStatCostLoader},
		{d2resource.ItemRatio, itemRatioLoader},
		{d2resource.StorePage, storePagesLoader},
		{d2resource.Overlays, overlaysLoader},
		{d2resource.CharStats, charStatsLoader},
		{d2resource.Gamble, gambleLoader},
		{d2resource.Hireling, hirelingLoader},
		{d2resource.Experience, experienceLoader},
		{d2resource.Gems, gemsLoader},
		{d2resource.QualityItems, itemQualityLoader},
		{d2resource.Runes, runewordLoader},
		{d2resource.DifficultyLevels, difficultyLevelsLoader},
		{d2resource.AutoMap, autoMapLoader},
		{d2resource.LevelDetails, levelDetailsLoader},
		{d2resource.LevelMaze, levelMazeDetailsLoader},
		{d2resource.LevelSubstitutions, levelSubstitutionsLoader},
		{d2resource.CubeRecipes, cubeRecipeLoader},
		{d2resource.SuperUniques, monsterSuperUniqeLoader},
		{d2resource.Inventory, inventoryLoader},
		{d2resource.Skills, skillDetailsLoader},
		{d2resource.SkillCalc, skillCalcLoader},
		{d2resource.MissileCalc, missileCalcLoader},
		{d2resource.Properties, propertyLoader},
		{d2resource.SkillDesc, skillDescriptionLoader},
		{d2resource.BodyLocations, bodyLocationsLoader},
		{d2resource.Sets, setLoader},
		{d2resource.SetItems, setItemLoader},
		{d2resource.AutoMagic, autoMagicLoader},
		{d2resource.TreasureClass, treasureClassLoader},
		{d2resource.TreasureClassEx, treasureClassExLoader},
		{d2resource.States, statesLoader},
		{d2resource.SoundEnvirons, soundEnvironmentLoader},
		{d2resource.Shrines, shrineLoader},
		{d2resource.ElemType, elemTypesLoader},
		{d2resource.PlrMode, playerModesLoader},
		{d2resource.PetType, petTypesLoader},
		{d2resource.NPC, npcLoader},
		{d2resource.MonsterUniqueModifier, monsterUniqModifiersLoader},
		{d2resource.MonsterEquipment, monsterEquipmentLoader},
		{d2resource.UniqueAppellation, uniqueAppellationsLoader},
		{d2resource.MonsterLevel, monsterLevelsLoader},
		{d2resource.MonsterSound, monsterSoundsLoader},
		{d2resource.MonsterSequence, monsterSequencesLoader},
		{d2resource.PlayerClass, playerClassLoader},
		{d2resource.MonsterPlacement, monsterPlacementsLoader},
		{d2resource.ObjectGroup, objectGroupsLoader},
		{d2resource.CompCode, componentCodesLoader},
		{d2resource.MonsterAI, monsterAiLoader},
		{d2resource.RarePrefix, rareItemPrefixLoader},
		{d2resource.RareSuffix, rareItemSuffixLoader},
		{d2resource.Events, eventsLoader},
		{d2resource.ArmorType, armorTypesLoader},      // anim mode tokens
		{d2resource.WeaponClass, weaponClassesLoader}, // anim mode tokens
		{d2resource.PlayerType, playerTypeLoader},     // anim mode tokens
		{d2resource.Composite, compositeTypeLoader},   // anim mode tokens
		{d2resource.HitClass, hitClassLoader},         // anim mode tokens
		{d2resource.UniquePrefix, uniqueMonsterPrefixLoader},
		{d2resource.UniqueSuffix, uniqueMonsterSuffixLoader},
		{d2resource.CubeModifier, cubeModifierLoader},
		{d2resource.CubeType, cubeTypeLoader},
		{d2resource.HirelingDescription, hirelingDescriptionLoader},
		{d2resource.LowQualityItems, lowQualityLoader},
	}

	for idx := range loaders {
		err := r.AddLoader(loaders[idx].path, loaders[idx].loader)
		if err != nil {
			return err
		}
	}

	r.initObjectRecords(objectLookups)

	return nil
}

// AddLoader associates a file path with a record loader
func (r *RecordManager) AddLoader(path string, loader recordLoader) error {
	if _, found := r.boundLoaders[path]; !found {
		r.boundLoaders[path] = make([]recordLoader, 0)
	}

	r.boundLoaders[path] = append(r.boundLoaders[path], loader)

	return nil
}

// Load will pass the dictionary to any bound loaders and populate the record entries
func (r *RecordManager) Load(path string, dict *d2txt.DataDictionary) error {
	loaders, found := r.boundLoaders[path]
	if !found {
		return fmt.Errorf("no loader bound for `%s`", path)
	}

	for idx := range loaders {
		err := loaders[idx](r, dict)
		if err != nil {
			return err
		}
	}

	// as soon as Armor, Weapons, and Misc items are loaded, we merge into r.Item.All
	if r.Item.All == nil && r.Item.Armors != nil && r.Item.Weapons != nil && r.Item.Misc != nil {
		r.Item.All = make(CommonItems)

		for code := range r.Item.Armors {
			r.Item.All[code] = r.Item.Armors[code]
		}

		for code := range r.Item.Weapons {
			r.Item.All[code] = r.Item.Weapons[code]
		}

		for code := range r.Item.Misc {
			r.Item.All[code] = r.Item.Misc[code]
		}
	}

	return nil
}

// GetMaxLevelByHero returns the highest level attainable for a hero type
func (r *RecordManager) GetMaxLevelByHero(heroType d2enum.Hero) int {
	return r.Character.MaxLevel[heroType]
}

// GetExperienceBreakpoint given a hero type and a level, returns the experience required for the level
func (r *RecordManager) GetExperienceBreakpoint(heroType d2enum.Hero, level int) int {
	return r.Character.Experience[level].HeroBreakpoints[heroType]
}

// GetLevelDetails gets a LevelDetailRecord by the record Id
func (r *RecordManager) GetLevelDetails(id int) *LevelDetailRecord {
	for i := 0; i < len(r.Level.Details); i++ {
		if r.Level.Details[i].ID == id {
			return r.Level.Details[i]
		}
	}

	return nil
}

// LevelPreset looks up a LevelPresetRecord by ID
func (r *RecordManager) LevelPreset(id int) LevelPresetRecord {
	for i := 0; i < len(r.Level.Presets); i++ {
		if r.Level.Presets[i].DefinitionID == id {
			return r.Level.Presets[i]
		}
	}

	panic("Unknown level preset")
}

// FindEquivalentTypesByItemCommonRecord returns itemtype codes that are equivalent
// to the given item common record
func (r *RecordManager) FindEquivalentTypesByItemCommonRecord(
	icr *ItemCommonRecord,
) []string {
	if r.Item.EquivalenceByRecord == nil {
		r.Item.EquivalenceByRecord = make(map[*ItemCommonRecord][]string)
	}

	// the first lookup generates the lookup table entry, next time will just use the table
	if r.Item.EquivalenceByRecord[icr] == nil {
		r.Item.EquivalenceByRecord[icr] = make([]string, 0)

		for code := range r.Item.Equivalency {
			icrList := r.Item.Equivalency[code]
			for idx := range icrList {
				if icr == icrList[idx] {
					r.Item.EquivalenceByRecord[icr] = append(r.Item.EquivalenceByRecord[icr], code)
					break
				}
			}
		}
	}

	return r.Item.EquivalenceByRecord[icr]
}

func (r *RecordManager) initObjectRecords(lookups []ObjectLookupRecord) {
	// Allocating 6 to allow Acts 1-5 without requiring a -1 at every read.
	records := make(IndexedObjects, 6)

	for i := range lookups {
		record := &lookups[i]
		if records[record.Act] == nil {
			// Likewise allocating 3 so a -1 isn't necessary.
			records[record.Act] = make([][]*ObjectLookupRecord, 3)
		}

		if records[record.Act][record.Type] == nil {
			// For simplicity, allocating with length 1000 then filling the values in by index.
			// If ids in the dictionary ever surpass 1000, raise this number.
			records[record.Act][record.Type] = make([]*ObjectLookupRecord, 1000)
		}

		records[record.Act][record.Type][record.Id] = record
	}

	r.Object.Lookup = records
}

// LookupObject looks up an object record
func (r *RecordManager) LookupObject(act, typ, id int) *ObjectLookupRecord {
	object := r.lookupObject(act, typ, id)
	if object == nil {
		r.Fatalf("Failed to look up object Act: %d, Type: %d, ID: %d", act, typ, id)
	}

	return object
}

func (r *RecordManager) lookupObject(act, typ, id int) *ObjectLookupRecord {
	if len(r.Object.Lookup) < act {
		return nil
	}

	if len(r.Object.Lookup[act]) < typ {
		return nil
	}

	if len(r.Object.Lookup[act][typ]) < id {
		return nil
	}

	return r.Object.Lookup[act][typ][id]
}

// SelectSoundByIndex selects a sound by its ID
func (r *RecordManager) SelectSoundByIndex(index int) *SoundDetailRecord {
	for idx := range r.Sound.Details {
		if r.Sound.Details[idx].Index == index {
			return r.Sound.Details[idx]
		}
	}

	return nil
}

// GetSkillByName returns the skill record for the given Skill name.
func (r *RecordManager) GetSkillByName(skillName string) *SkillRecord {
	for idx := range r.Skill.Details {
		if r.Skill.Details[idx].Skill == skillName {
			return r.Skill.Details[idx]
		}
	}

	return nil
}

// GetMissileByName allows lookup of a MissileRecord by a given name. The name will be lowercased and stripped of whitespaces.
func (r *RecordManager) GetMissileByName(missileName string) *MissileRecord {
	return r.missilesByName[sanitizeMissilesKey(missileName)]
}
