package d2tbl

import (
	"fmt"
	"strconv"

	"github.com/OpenDiablo2/OpenDiablo2/d2common/d2datautils"
)

// TextDictionary is a string map
type TextDictionary map[string]string

func (td TextDictionary) loadHashEntries(hashEntries []*textDictionaryHashEntry, br *d2datautils.StreamReader) error {
	for i := 0; i < len(hashEntries); i++ {
		entry := textDictionaryHashEntry{}

		active, err := br.ReadByte()
		if err != nil {
			return fmt.Errorf("reading active: %v", err)
		}

		entry.IsActive = active > 0

		entry.Index, err = br.ReadUInt16()
		if err != nil {
			return fmt.Errorf("reading Index: %v", err)
		}

		entry.HashValue, err = br.ReadUInt32()
		if err != nil {
			return fmt.Errorf("reading hash value: %v", err)
		}

		entry.IndexString, err = br.ReadUInt32()
		if err != nil {
			return fmt.Errorf("reading index string pos: %v", err)
		}

		entry.NameString, err = br.ReadUInt32()
		if err != nil {
			return fmt.Errorf("reading name string pos: %v", err)
		}

		entry.NameLength, err = br.ReadUInt16()
		if err != nil {
			return fmt.Errorf("reading name length: %v", err)
		}

		hashEntries[i] = &entry
	}

	for idx := range hashEntries {
		if !hashEntries[idx].IsActive {
			continue
		}

		if err := td.loadHashEntry(idx, hashEntries[idx], br); err != nil {
			return fmt.Errorf("loading entry %d: %v", idx, err)
		}
	}

	return nil
}

func (td TextDictionary) loadHashEntry(idx int, hashEntry *textDictionaryHashEntry, br *d2datautils.StreamReader) error {
	br.SetPosition(uint64(hashEntry.NameString))

	nameVal, err := br.ReadBytes(int(hashEntry.NameLength - 1))
	if err != nil {
		return fmt.Errorf("reading name value: %v", err)
	}

	value := string(nameVal)

	br.SetPosition(uint64(hashEntry.IndexString))

	key := ""

	for {
		b, err := br.ReadByte()
		if b == 0 {
			break
		}

		if err != nil {
			return fmt.Errorf("reading kay char: %v", err)
		}

		key += string(b)
	}

	if key == "x" || key == "X" {
		key = "#" + strconv.Itoa(idx)
	}

	_, exists := td[key]
	if !exists {
		td[key] = value
	}

	return nil
}

type textDictionaryHashEntry struct {
	IsActive    bool
	Index       uint16
	HashValue   uint32
	IndexString uint32
	NameString  uint32
	NameLength  uint16
}

const (
	crcByteCount = 2
)

// LoadTextDictionary loads the text dictionary from the given data
func LoadTextDictionary(dictionaryData []byte) (TextDictionary, error) {
	lookupTable := make(TextDictionary)

	br := d2datautils.CreateStreamReader(dictionaryData)

	// skip past the CRC
	_, _ = br.ReadBytes(crcByteCount)

	var err error

	/*
		number of indicates
		(https://d2mods.info/forum/viewtopic.php?p=202077#p202077)
		Indices ...
		An array of WORD. Each entry is an index into the hash table.
		The actual string key index in the .bin file is an index into this table.
		So to get a string from a key index ...
	*/
	numberOfElements, err := br.ReadUInt16()
	if err != nil {
		return nil, fmt.Errorf("reading number of elements: %v", err)
	}

	hashTableSize, err := br.ReadUInt32()
	if err != nil {
		return nil, fmt.Errorf("reading hash table size: %v", err)
	}

	// Version
	_, err = br.ReadByte()
	if err != nil {
		return nil, fmt.Errorf("reading version: %v", err)
	}

	_, _ = br.ReadUInt32() // StringOffset

	// When the number of times you have missed a match with a
	// hash key equals this value, you give up because it is not there.
	_, _ = br.ReadUInt32()

	_, _ = br.ReadUInt32() // FileSize

	elementIndex := make([]uint16, numberOfElements)
	for i := 0; i < int(numberOfElements); i++ {
		elementIndex[i], err = br.ReadUInt16()
		if err != nil {
			return nil, fmt.Errorf("reading element index %d: %v", i, err)
		}
	}

	hashEntries := make([]*textDictionaryHashEntry, hashTableSize)

	err = lookupTable.loadHashEntries(hashEntries, br)
	if err != nil {
		return nil, fmt.Errorf("loading has entries: %v", err)
	}

	return lookupTable, nil
}

// Marshal encodes text dictionary back into byte slice
func (td *TextDictionary) Marshal() []byte {
	sw := d2datautils.CreateStreamWriter()

	// https://github.com/OpenDiablo2/OpenDiablo2/issues/1043
	sw.PushBytes(0, 0)

	sw.PushUint16(0)

	keys := make([]string, 0)
	for key := range *td {
		keys = append(keys, key)
	}

	sw.PushUint32(uint32(len(keys)))

	// version (always 0)
	sw.PushBytes(0)

	// offset of start of data (unnecessary for our decoder)
	sw.PushUint32(0)

	// Max retry count for a hash hit.
	sw.PushUint32(0)

	// offset to end of data (noop)
	sw.PushUint32(0)

	// indicates (len = 0, so nothing here)

	// nolint:gomnd // 17 comes from the size of one "data-header index"
	// dataPos is a position, when we're placing data stream
	dataPos := len(sw.GetBytes()) + 17*len(*td)

	for _, key := range keys {
		value := (*td)[key]
		// non-zero if record is used (for us, every record is used ;-)
		sw.PushBytes(1)

		// generally unused;
		// 	string key index (used in .bin)
		sw.PushUint16(0)

		// also unused in our decoder
		// 	calculated hash of the string.
		sw.PushUint32(0)

		sw.PushUint32(uint32(dataPos))

		if key[0] == '#' {
			// 1 for X, and 1 for separator
			dataPos += 2
		} else {
			dataPos += len(key) + 1
		}

		sw.PushUint32(uint32(dataPos))
		dataPos += len(value) + 1

		sw.PushUint16(uint16(len(value) + 1))
	}

	// data stream: put all data in appropriate order
	for _, key := range keys {
		value := (*td)[key]

		if key[0] == '#' {
			key = "x"
		}

		sw.PushBytes([]byte(key)...)

		// 0 as separator
		sw.PushBytes(0)

		sw.PushBytes([]byte(value)...)

		// 0 as separator
		sw.PushBytes(0)
	}

	return sw.GetBytes()
}
