package dsd

// dynamic structured data
// check here for some benchmarks: https://github.com/alecthomas/go_serialization_benchmarks

import (
	"encoding/json"
	"errors"
	"fmt"
	"io"

	"github.com/fxamacker/cbor/v2"
	"github.com/ghodss/yaml"
	"github.com/vmihailenco/msgpack/v5"

	"github.com/safing/portbase/formats/varint"
	"github.com/safing/portbase/utils"
)

// Load loads an dsd structured data blob into the given interface.
func Load(data []byte, t interface{}) (format uint8, err error) {
	format, read, err := loadFormat(data)
	if err != nil {
		return 0, err
	}

	_, ok := ValidateSerializationFormat(format)
	if ok {
		return format, LoadAsFormat(data[read:], format, t)
	}
	return DecompressAndLoad(data[read:], format, t)
}

// LoadAsFormat loads a data blob into the interface using the specified format.
func LoadAsFormat(data []byte, format uint8, t interface{}) (err error) {
	switch format {
	case RAW:
		return ErrIsRaw
	case JSON:
		err = json.Unmarshal(data, t)
		if err != nil {
			return fmt.Errorf("dsd: failed to unpack json: %w, data: %s", err, utils.SafeFirst16Bytes(data))
		}
		return nil
	case YAML:
		err = yaml.Unmarshal(data, t)
		if err != nil {
			return fmt.Errorf("dsd: failed to unpack yaml: %w, data: %s", err, utils.SafeFirst16Bytes(data))
		}
		return nil
	case CBOR:
		err = cbor.Unmarshal(data, t)
		if err != nil {
			return fmt.Errorf("dsd: failed to unpack cbor: %w, data: %s", err, utils.SafeFirst16Bytes(data))
		}
		return nil
	case MsgPack:
		err = msgpack.Unmarshal(data, t)
		if err != nil {
			return fmt.Errorf("dsd: failed to unpack msgpack: %w, data: %s", err, utils.SafeFirst16Bytes(data))
		}
		return nil
	case GenCode:
		genCodeStruct, ok := t.(GenCodeCompatible)
		if !ok {
			return errors.New("dsd: gencode is not supported by the given data structure")
		}
		_, err = genCodeStruct.GenCodeUnmarshal(data)
		if err != nil {
			return fmt.Errorf("dsd: failed to unpack gencode: %w, data: %s", err, utils.SafeFirst16Bytes(data))
		}
		return nil
	default:
		return ErrIncompatibleFormat
	}
}

func loadFormat(data []byte) (format uint8, read int, err error) {
	format, read, err = varint.Unpack8(data)
	if err != nil {
		return 0, 0, err
	}
	if len(data) <= read {
		return 0, 0, io.ErrUnexpectedEOF
	}

	return format, read, nil
}

// Dump stores the interface as a dsd formatted data structure.
func Dump(t interface{}, format uint8) ([]byte, error) {
	return DumpIndent(t, format, "")
}

// DumpIndent stores the interface as a dsd formatted data structure with indentation, if available.
func DumpIndent(t interface{}, format uint8, indent string) ([]byte, error) {
	data, err := dumpWithoutIdentifier(t, format, indent)
	if err != nil {
		return nil, err
	}

	// TODO: Find a better way to do this.
	return append(varint.Pack8(format), data...), nil
}

func dumpWithoutIdentifier(t interface{}, format uint8, indent string) ([]byte, error) {
	format, ok := ValidateSerializationFormat(format)
	if !ok {
		return nil, ErrIncompatibleFormat
	}

	var data []byte
	var err error
	switch format {
	case RAW:
		var ok bool
		data, ok = t.([]byte)
		if !ok {
			return nil, ErrIncompatibleFormat
		}
	case JSON:
		// TODO: use SetEscapeHTML(false)
		if indent != "" {
			data, err = json.MarshalIndent(t, "", indent)
		} else {
			data, err = json.Marshal(t)
		}
		if err != nil {
			return nil, err
		}
	case YAML:
		data, err = yaml.Marshal(t)
		if err != nil {
			return nil, err
		}
	case CBOR:
		data, err = cbor.Marshal(t)
		if err != nil {
			return nil, err
		}
	case MsgPack:
		data, err = msgpack.Marshal(t)
		if err != nil {
			return nil, err
		}
	case GenCode:
		genCodeStruct, ok := t.(GenCodeCompatible)
		if !ok {
			return nil, errors.New("dsd: gencode is not supported by the given data structure")
		}
		data, err = genCodeStruct.GenCodeMarshal(nil)
		if err != nil {
			return nil, fmt.Errorf("dsd: failed to pack gencode struct: %w", err)
		}
	default:
		return nil, ErrIncompatibleFormat
	}

	return data, nil
}
