package gff

import (
	"bytes"
	"encoding/binary"
	"fmt"
	"io"
	"math"
)

const (
	headerSize = 56
)

type header struct {
	FileType           [4]byte
	FileVersion        [4]byte
	StructOffset       uint32
	StructCount        uint32
	FieldOffset        uint32
	FieldCount         uint32
	LabelOffset        uint32
	LabelCount         uint32
	FieldDataOffset    uint32
	FieldDataCount     uint32
	FieldIndicesOffset uint32
	FieldIndicesCount  uint32
	ListIndicesOffset  uint32
	ListIndicesCount   uint32
}

type rawStruct struct {
	Type         uint32
	DataOrOffset uint32
	FieldCount   uint32
}

type rawField struct {
	Type         uint32
	LabelIndex   uint32
	DataOrOffset uint32
}

func Read(r io.Reader) (Document, error) {
	data, err := io.ReadAll(r)
	if err != nil {
		return Document{}, fmt.Errorf("read gff: %w", err)
	}
	if len(data) < headerSize {
		return Document{}, fmt.Errorf("gff file too small: %d bytes", len(data))
	}

	var hdr header
	if err := binary.Read(bytes.NewReader(data[:headerSize]), binary.LittleEndian, &hdr); err != nil {
		return Document{}, fmt.Errorf("decode header: %w", err)
	}

	reader := binaryReader{data: data, hdr: hdr}
	rawStructs, err := reader.readStructs()
	if err != nil {
		return Document{}, err
	}
	rawFields, err := reader.readFields()
	if err != nil {
		return Document{}, err
	}
	labels, err := reader.readLabels()
	if err != nil {
		return Document{}, err
	}

	root, err := reader.decodeStruct(0, rawStructs, rawFields, labels)
	if err != nil {
		return Document{}, err
	}

	return Document{
		FileType:    string(hdr.FileType[:]),
		FileVersion: string(hdr.FileVersion[:]),
		Root:        root,
	}, nil
}

func Write(w io.Writer, doc Document) error {
	encoder := newBinaryEncoder(doc)
	data, err := encoder.encode()
	if err != nil {
		return err
	}
	_, err = w.Write(data)
	return err
}

type binaryReader struct {
	data []byte
	hdr  header
}

func (r binaryReader) readStructs() ([]rawStruct, error) {
	return readTable[rawStruct](r.data, r.hdr.StructOffset, r.hdr.StructCount)
}

func (r binaryReader) readFields() ([]rawField, error) {
	return readTable[rawField](r.data, r.hdr.FieldOffset, r.hdr.FieldCount)
}

func (r binaryReader) readLabels() ([]string, error) {
	start := int(r.hdr.LabelOffset)
	end := start + int(r.hdr.LabelCount)*16
	if end > len(r.data) {
		return nil, fmt.Errorf("label table exceeds file bounds")
	}

	labels := make([]string, 0, r.hdr.LabelCount)
	for offset := start; offset < end; offset += 16 {
		chunk := r.data[offset : offset+16]
		n := bytes.IndexByte(chunk, 0)
		if n == -1 {
			n = len(chunk)
		}
		labels = append(labels, string(chunk[:n]))
	}
	return labels, nil
}

func (r binaryReader) decodeStruct(index uint32, structs []rawStruct, fields []rawField, labels []string) (Struct, error) {
	if index >= uint32(len(structs)) {
		return Struct{}, fmt.Errorf("struct index %d out of range", index)
	}

	raw := structs[index]
	fieldIndices, err := r.fieldIndices(raw)
	if err != nil {
		return Struct{}, err
	}

	out := Struct{
		Type:   raw.Type,
		Fields: make([]Field, 0, len(fieldIndices)),
	}
	for _, fieldIndex := range fieldIndices {
		if fieldIndex >= uint32(len(fields)) {
			return Struct{}, fmt.Errorf("field index %d out of range", fieldIndex)
		}
		field, err := r.decodeField(fields[fieldIndex], structs, fields, labels)
		if err != nil {
			return Struct{}, err
		}
		out.Fields = append(out.Fields, field)
	}

	return out, nil
}

func (r binaryReader) fieldIndices(s rawStruct) ([]uint32, error) {
	switch s.FieldCount {
	case 0:
		return nil, nil
	case 1:
		return []uint32{s.DataOrOffset}, nil
	default:
		start := int(r.hdr.FieldIndicesOffset + s.DataOrOffset)
		end := start + int(s.FieldCount)*4
		if end > len(r.data) {
			return nil, fmt.Errorf("field indices exceed file bounds")
		}

		out := make([]uint32, s.FieldCount)
		reader := bytes.NewReader(r.data[start:end])
		if err := binary.Read(reader, binary.LittleEndian, &out); err != nil {
			return nil, fmt.Errorf("decode field indices: %w", err)
		}
		return out, nil
	}
}

func (r binaryReader) decodeField(raw rawField, structs []rawStruct, fields []rawField, labels []string) (Field, error) {
	if raw.LabelIndex >= uint32(len(labels)) {
		return Field{}, fmt.Errorf("label index %d out of range", raw.LabelIndex)
	}
	fieldType := FieldType(raw.Type)
	value, err := r.decodeValue(fieldType, raw.DataOrOffset, structs, fields, labels)
	if err != nil {
		return Field{}, fmt.Errorf("decode field %q: %w", labels[raw.LabelIndex], err)
	}

	return Field{
		Label: labels[raw.LabelIndex],
		Type:  fieldType,
		Value: value,
	}, nil
}

func (r binaryReader) decodeValue(fieldType FieldType, data uint32, structs []rawStruct, fields []rawField, labels []string) (Value, error) {
	switch fieldType {
	case TypeByte:
		return ByteValue(uint8(data)), nil
	case TypeChar:
		return CharValue(int8(data)), nil
	case TypeWord:
		return WordValue(uint16(data)), nil
	case TypeShort:
		return ShortValue(int16(data)), nil
	case TypeDWord:
		return DWordValue(data), nil
	case TypeInt:
		return IntValue(int32(data)), nil
	case TypeFloat:
		return FloatValue(math.Float32frombits(data)), nil
	case TypeDWord64:
		raw, err := r.sliceFieldData(data, 8)
		if err != nil {
			return nil, err
		}
		return DWord64Value(binary.LittleEndian.Uint64(raw)), nil
	case TypeInt64:
		raw, err := r.sliceFieldData(data, 8)
		if err != nil {
			return nil, err
		}
		return Int64Value(int64(binary.LittleEndian.Uint64(raw))), nil
	case TypeDouble:
		raw, err := r.sliceFieldData(data, 8)
		if err != nil {
			return nil, err
		}
		return DoubleValue(math.Float64frombits(binary.LittleEndian.Uint64(raw))), nil
	case TypeCExoString:
		raw, err := r.prefixedFieldData(data)
		if err != nil {
			return nil, err
		}
		return StringValue(string(raw)), nil
	case TypeResRef:
		raw, err := r.prefixedByteFieldData(data, 1)
		if err != nil {
			return nil, err
		}
		return ResRefValue(string(raw)), nil
	case TypeCExoLocString:
		raw, err := r.locStringFieldData(data)
		if err != nil {
			return nil, err
		}
		return decodeLocString(raw)
	case TypeVoid:
		raw, err := r.prefixedFieldData(data)
		if err != nil {
			return nil, err
		}
		return VoidValue(raw), nil
	case TypeStruct:
		return r.decodeStruct(data, structs, fields, labels)
	case TypeList:
		return r.decodeList(data, structs, fields, labels)
	case TypeOrientation:
		raw, err := r.sliceFieldData(data, 16)
		if err != nil {
			return nil, err
		}
		return Orientation{
			X: math.Float32frombits(binary.LittleEndian.Uint32(raw[0:4])),
			Y: math.Float32frombits(binary.LittleEndian.Uint32(raw[4:8])),
			Z: math.Float32frombits(binary.LittleEndian.Uint32(raw[8:12])),
			W: math.Float32frombits(binary.LittleEndian.Uint32(raw[12:16])),
		}, nil
	case TypeVector:
		raw, err := r.sliceFieldData(data, 12)
		if err != nil {
			return nil, err
		}
		return Vector{
			X: math.Float32frombits(binary.LittleEndian.Uint32(raw[0:4])),
			Y: math.Float32frombits(binary.LittleEndian.Uint32(raw[4:8])),
			Z: math.Float32frombits(binary.LittleEndian.Uint32(raw[8:12])),
		}, nil
	default:
		return nil, fmt.Errorf("unsupported field type %d", fieldType)
	}
}

func (r binaryReader) decodeList(offset uint32, structs []rawStruct, fields []rawField, labels []string) (ListValue, error) {
	start := int(r.hdr.ListIndicesOffset + offset)
	if start+4 > len(r.data) {
		return nil, fmt.Errorf("list data exceeds file bounds")
	}
	count := binary.LittleEndian.Uint32(r.data[start : start+4])
	start += 4
	end := start + int(count)*4
	if end > len(r.data) {
		return nil, fmt.Errorf("list indices exceed file bounds")
	}

	list := make(ListValue, 0, count)
	for pos := start; pos < end; pos += 4 {
		index := binary.LittleEndian.Uint32(r.data[pos : pos+4])
		child, err := r.decodeStruct(index, structs, fields, labels)
		if err != nil {
			return nil, err
		}
		list = append(list, child)
	}
	return list, nil
}

func (r binaryReader) sliceFieldData(offset uint32, size int) ([]byte, error) {
	start := int(r.hdr.FieldDataOffset + offset)
	end := start + size
	if end > len(r.data) {
		return nil, fmt.Errorf("field data exceeds file bounds")
	}
	return r.data[start:end], nil
}

func (r binaryReader) prefixedFieldData(offset uint32) ([]byte, error) {
	sizeRaw, err := r.sliceFieldData(offset, 4)
	if err != nil {
		return nil, err
	}
	size := int(binary.LittleEndian.Uint32(sizeRaw))
	return r.sliceFieldData(offset+4, size)
}

func (r binaryReader) prefixedByteFieldData(offset uint32, prefixBytes uint32) ([]byte, error) {
	header, err := r.sliceFieldData(offset, int(prefixBytes))
	if err != nil {
		return nil, err
	}
	size := int(header[0])
	return r.sliceFieldData(offset+prefixBytes, size)
}

func (r binaryReader) locStringFieldData(offset uint32) ([]byte, error) {
	sizeRaw, err := r.sliceFieldData(offset, 4)
	if err != nil {
		return nil, err
	}
	size := int(binary.LittleEndian.Uint32(sizeRaw))
	return r.sliceFieldData(offset, size+4)
}

func readTable[T any](data []byte, offset uint32, count uint32) ([]T, error) {
	if count == 0 {
		return nil, nil
	}
	var row T
	size := binary.Size(row)
	start := int(offset)
	end := start + int(count)*size
	if end > len(data) {
		return nil, fmt.Errorf("table exceeds file bounds")
	}

	out := make([]T, count)
	reader := bytes.NewReader(data[start:end])
	if err := binary.Read(reader, binary.LittleEndian, &out); err != nil {
		return nil, fmt.Errorf("decode table: %w", err)
	}
	return out, nil
}

func decodeLocString(data []byte) (LocString, error) {
	if len(data) < 12 {
		return LocString{}, fmt.Errorf("locstring too small")
	}
	totalSize := binary.LittleEndian.Uint32(data[0:4])
	if totalSize+4 != uint32(len(data)) {
		return LocString{}, fmt.Errorf("locstring size mismatch")
	}
	stringRef := binary.LittleEndian.Uint32(data[4:8])
	count := binary.LittleEndian.Uint32(data[8:12])
	pos := 12
	entries := make([]LocStringEntry, 0, count)
	for i := uint32(0); i < count; i++ {
		if pos+8 > len(data) {
			return LocString{}, fmt.Errorf("locstring entry header truncated")
		}
		id := binary.LittleEndian.Uint32(data[pos : pos+4])
		length := binary.LittleEndian.Uint32(data[pos+4 : pos+8])
		pos += 8
		if pos+int(length) > len(data) {
			return LocString{}, fmt.Errorf("locstring entry %d truncated", i)
		}
		entries = append(entries, LocStringEntry{
			ID:    id,
			Value: string(data[pos : pos+int(length)]),
		})
		pos += int(length)
	}

	return LocString{
		StringRef: stringRef,
		Entries:   entries,
	}, nil
}

type binaryEncoder struct {
	document     Document
	structs      []rawStruct
	fields       []rawField
	labels       []string
	labelIndex   map[string]uint32
	fieldData    bytes.Buffer
	fieldIndices []uint32
	listIndices  []uint32
}

func newBinaryEncoder(doc Document) *binaryEncoder {
	return &binaryEncoder{
		document:   doc,
		labelIndex: map[string]uint32{},
	}
}

func (e *binaryEncoder) encode() ([]byte, error) {
	if err := e.addStruct(e.document.Root); err != nil {
		return nil, err
	}

	var buf bytes.Buffer
	hdr := header{}
	copy(hdr.FileType[:], []byte(padFour(e.document.FileType)))
	copy(hdr.FileVersion[:], []byte(padFour(defaultString(e.document.FileVersion, "V3.2"))))

	hdr.StructOffset = headerSize
	hdr.StructCount = uint32(len(e.structs))
	hdr.FieldOffset = hdr.StructOffset + uint32(len(e.structs))*12
	hdr.FieldCount = uint32(len(e.fields))
	hdr.LabelOffset = hdr.FieldOffset + uint32(len(e.fields))*12
	hdr.LabelCount = uint32(len(e.labels))
	hdr.FieldDataOffset = hdr.LabelOffset + uint32(len(e.labels))*16
	hdr.FieldDataCount = uint32(e.fieldData.Len())
	hdr.FieldIndicesOffset = hdr.FieldDataOffset + uint32(e.fieldData.Len())
	hdr.FieldIndicesCount = uint32(len(e.fieldIndices) * 4)
	hdr.ListIndicesOffset = hdr.FieldIndicesOffset + uint32(len(e.fieldIndices))*4
	hdr.ListIndicesCount = uint32(len(e.listIndices) * 4)

	if err := binary.Write(&buf, binary.LittleEndian, hdr); err != nil {
		return nil, err
	}
	if err := binary.Write(&buf, binary.LittleEndian, e.structs); err != nil {
		return nil, err
	}
	if err := binary.Write(&buf, binary.LittleEndian, e.fields); err != nil {
		return nil, err
	}
	for _, label := range e.labels {
		var raw [16]byte
		copy(raw[:], []byte(label))
		if _, err := buf.Write(raw[:]); err != nil {
			return nil, err
		}
	}
	if _, err := buf.Write(e.fieldData.Bytes()); err != nil {
		return nil, err
	}
	if err := binary.Write(&buf, binary.LittleEndian, e.fieldIndices); err != nil {
		return nil, err
	}
	if err := binary.Write(&buf, binary.LittleEndian, e.listIndices); err != nil {
		return nil, err
	}

	return buf.Bytes(), nil
}

func (e *binaryEncoder) addStruct(s Struct) error {
	index := uint32(len(e.structs))
	e.structs = append(e.structs, rawStruct{Type: s.Type})

	fieldIndices := make([]uint32, 0, len(s.Fields))
	for _, field := range s.Fields {
		fieldIndex, err := e.addField(field)
		if err != nil {
			return err
		}
		fieldIndices = append(fieldIndices, fieldIndex)
	}

	entry := &e.structs[index]
	entry.FieldCount = uint32(len(fieldIndices))
	switch len(fieldIndices) {
	case 0:
		entry.DataOrOffset = 0
	case 1:
		entry.DataOrOffset = fieldIndices[0]
	default:
		entry.DataOrOffset = uint32(len(e.fieldIndices) * 4)
		e.fieldIndices = append(e.fieldIndices, fieldIndices...)
	}
	return nil
}

func (e *binaryEncoder) addField(field Field) (uint32, error) {
	labelIndex, err := e.indexLabel(field.Label)
	if err != nil {
		return 0, err
	}
	data, err := e.encodeValue(field.Value)
	if err != nil {
		return 0, fmt.Errorf("encode field %q: %w", field.Label, err)
	}

	entry := rawField{
		Type:         uint32(field.Type),
		LabelIndex:   labelIndex,
		DataOrOffset: data,
	}
	index := uint32(len(e.fields))
	e.fields = append(e.fields, entry)
	return index, nil
}

func (e *binaryEncoder) encodeValue(value Value) (uint32, error) {
	switch typed := value.(type) {
	case ByteValue:
		return uint32(uint8(typed)), nil
	case CharValue:
		return uint32(uint8(typed)), nil
	case WordValue:
		return uint32(uint16(typed)), nil
	case ShortValue:
		return uint32(uint16(typed)), nil
	case DWordValue:
		return uint32(typed), nil
	case IntValue:
		return uint32(int32(typed)), nil
	case FloatValue:
		return math.Float32bits(float32(typed)), nil
	case DWord64Value:
		return e.writeFieldData(func(buf *bytes.Buffer) error {
			return binary.Write(buf, binary.LittleEndian, uint64(typed))
		}), nil
	case Int64Value:
		return e.writeFieldData(func(buf *bytes.Buffer) error {
			return binary.Write(buf, binary.LittleEndian, int64(typed))
		}), nil
	case DoubleValue:
		return e.writeFieldData(func(buf *bytes.Buffer) error {
			return binary.Write(buf, binary.LittleEndian, math.Float64bits(float64(typed)))
		}), nil
	case StringValue:
		return e.writeLengthPrefixed([]byte(typed)), nil
	case ResRefValue:
		if len(typed) > 255 {
			return 0, fmt.Errorf("resref exceeds 255 bytes")
		}
		return e.writeFieldData(func(buf *bytes.Buffer) error {
			if err := buf.WriteByte(byte(len(typed))); err != nil {
				return err
			}
			_, err := buf.Write([]byte(typed))
			return err
		}), nil
	case LocString:
		return e.writeFieldData(func(buf *bytes.Buffer) error {
			payload := bytes.Buffer{}
			if err := binary.Write(&payload, binary.LittleEndian, typed.StringRef); err != nil {
				return err
			}
			if err := binary.Write(&payload, binary.LittleEndian, uint32(len(typed.Entries))); err != nil {
				return err
			}
			for _, entry := range typed.Entries {
				if err := binary.Write(&payload, binary.LittleEndian, entry.ID); err != nil {
					return err
				}
				if err := binary.Write(&payload, binary.LittleEndian, uint32(len(entry.Value))); err != nil {
					return err
				}
				if _, err := payload.Write([]byte(entry.Value)); err != nil {
					return err
				}
			}
			if err := binary.Write(buf, binary.LittleEndian, uint32(payload.Len())); err != nil {
				return err
			}
			if _, err := buf.Write(payload.Bytes()); err != nil {
				return err
			}
			return nil
		}), nil
	case VoidValue:
		return e.writeLengthPrefixed([]byte(typed)), nil
	case Struct:
		index := uint32(len(e.structs))
		if err := e.addStruct(typed); err != nil {
			return 0, err
		}
		return index, nil
	case ListValue:
		return e.writeList(typed)
	case Orientation:
		return e.writeFieldData(func(buf *bytes.Buffer) error {
			for _, component := range []float32{typed.X, typed.Y, typed.Z, typed.W} {
				if err := binary.Write(buf, binary.LittleEndian, math.Float32bits(component)); err != nil {
					return err
				}
			}
			return nil
		}), nil
	case Vector:
		return e.writeFieldData(func(buf *bytes.Buffer) error {
			for _, component := range []float32{typed.X, typed.Y, typed.Z} {
				if err := binary.Write(buf, binary.LittleEndian, math.Float32bits(component)); err != nil {
					return err
				}
			}
			return nil
		}), nil
	default:
		return 0, fmt.Errorf("unsupported value type %T", value)
	}
}

func (e *binaryEncoder) writeList(list ListValue) (uint32, error) {
	indices := make([]uint32, 0, len(list))
	for _, item := range list {
		index := uint32(len(e.structs))
		if err := e.addStruct(item); err != nil {
			return 0, err
		}
		indices = append(indices, index)
	}

	offset := uint32(len(e.listIndices) * 4)
	e.listIndices = append(e.listIndices, uint32(len(list)))
	e.listIndices = append(e.listIndices, indices...)
	return offset, nil
}

func (e *binaryEncoder) writeFieldData(write func(*bytes.Buffer) error) uint32 {
	offset := uint32(e.fieldData.Len())
	if err := write(&e.fieldData); err != nil {
		panic(err)
	}
	return offset
}

func (e *binaryEncoder) writeLengthPrefixed(data []byte) uint32 {
	return e.writeFieldData(func(buf *bytes.Buffer) error {
		if err := binary.Write(buf, binary.LittleEndian, uint32(len(data))); err != nil {
			return err
		}
		_, err := buf.Write(data)
		return err
	})
}

func (e *binaryEncoder) indexLabel(label string) (uint32, error) {
	if len(label) > 16 {
		return 0, fmt.Errorf("label %q exceeds 16 bytes", label)
	}
	if index, ok := e.labelIndex[label]; ok {
		return index, nil
	}
	index := uint32(len(e.labels))
	e.labels = append(e.labels, label)
	e.labelIndex[label] = index
	return index, nil
}

func padFour(value string) string {
	if len(value) >= 4 {
		return value[:4]
	}
	return value + string(bytes.Repeat([]byte(" "), 4-len(value)))
}

func defaultString(value, fallback string) string {
	if value == "" {
		return fallback
	}
	return value
}
