// KEKS -- Go KEKS codec implementation
// Copyright (C) 2024-2025 Sergey Matveev <stargrave@stargrave.org>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as
// published by the Free Software Foundation, version 3 of the License.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this program.  If not, see <http://www.gnu.org/licenses/>.

package keks

import (
	"bytes"
	"fmt"
	"io"
	"math/big"
	"net"
	"reflect"
	"sort"
	"strings"
	"time"

	"github.com/google/uuid"

	keksort "go.cypherpunks.su/keks/internal/sort"
	"go.cypherpunks.su/tai64n/v4"
)

type EncodeOpts struct {
	// Do not convert time.Time to TAI, assume no conversion is needed.
	LeaveTAI bool
}

func Encode(w io.Writer, v any, opts *EncodeOpts) (written int64, err error) {
	if v == nil {
		return ByteEncode(w, byte(AtomNIL))
	}
	switch v := v.(type) {
	case []byte:
		return BinEncode(w, v)
	case *BlobChunked:
		return BlobEncode(w, v.ChunkLen, v.Reader())
	case BlobChunked:
		return BlobEncode(w, v.ChunkLen, v.Reader())
	case *BlobReader:
		return BlobEncode(w, v.ChunkLen, v.R)
	case BlobReader:
		return BlobEncode(w, v.ChunkLen, v.R)
	case time.Time:
		if opts == nil || !opts.LeaveTAI {
			v = tai64n.Leapsecs.Add(v)
		}
		if v.Nanosecond() > 0 {
			var tai tai64n.TAI64N
			tai.FromTime(v)
			return TAI64NEncode(w, &tai)
		} else {
			var tai tai64n.TAI64
			tai.FromTime(v)
			return TAI64Encode(w, &tai)
		}
	case Raw:
		return io.Copy(w, bytes.NewReader(v))
	case *big.Int:
		return BigIntEncode(w, v)
	case Float:
		return FloatEncode(w, &v)
	case *Float:
		return FloatEncode(w, v)
	case bool:
		return BoolEncode(w, v)
	case uuid.UUID:
		var h Hexlet
		copy(h[:], v[:])
		return HexletEncode(w, &h)
	case net.IP:
		var h Hexlet
		copy(h[:], v.To16()[:])
		return HexletEncode(w, &h)
	case Hexlet:
		return HexletEncode(w, &v)
	case *Hexlet:
		return HexletEncode(w, v)
	case tai64n.TAI64:
		return TAI64Encode(w, &v)
	case *tai64n.TAI64:
		return TAI64Encode(w, v)
	case tai64n.TAI64N:
		return TAI64NEncode(w, &v)
	case *tai64n.TAI64N:
		return TAI64NEncode(w, v)
	case tai64n.TAI64NA:
		return TAI64NAEncode(w, &v)
	case *tai64n.TAI64NA:
		return TAI64NAEncode(w, v)
	case uint:
		return UIntEncode(w, uint64(v))
	case uint8:
		return UIntEncode(w, uint64(v))
	case uint16:
		return UIntEncode(w, uint64(v))
	case uint32:
		return UIntEncode(w, uint64(v))
	case uint64:
		return UIntEncode(w, v)
	case int:
		return IntEncode(w, int64(v))
	case int8:
		return IntEncode(w, int64(v))
	case int16:
		return IntEncode(w, int64(v))
	case int32:
		return IntEncode(w, int64(v))
	case int64:
		return IntEncode(w, v)
	case string:
		return StrEncode(w, v)
	case Magic:
		return MagicEncode(w, v)
	}
	vv := reflect.ValueOf(v)
	switch reflect.TypeOf(v).Kind() {
	case reflect.Pointer:
		if vv.IsNil() {
			return ByteEncode(w, byte(AtomNIL))
		}
		return Encode(w, vv.Elem().Interface(), opts)
	case reflect.Slice:
		_, err = ByteEncode(w, byte(AtomList))
		if err != nil {
			return
		}
		written++
		var n64 int64
		if anys, ok := v.([]any); ok {
			for _, v := range anys {
				n64, err = Encode(w, v, opts)
				written += n64
				if err != nil {
					return
				}
			}
		} else {
			for i := range vv.Len() {
				n64, err = Encode(w, vv.Index(i).Interface(), opts)
				written += n64
				if err != nil {
					return
				}
			}
		}
		_, err = ByteEncode(w, byte(AtomEOC))
		if err != nil {
			return
		}
		written++
		return
	case reflect.Map:
		_, err = ByteEncode(w, byte(AtomMap))
		if err != nil {
			return
		}
		written++
		keys := vv.MapKeys()
		if len(keys) > 0 {
			sort.Sort(keksort.ByLenFirstRV(keys))
			if keys[0].String() == "" {
				err = ErrMapBadKey
				return
			}
			var n64 int64
			for _, k := range keys {
				if k.Kind() != reflect.String {
					err = ErrMapBadKey
					return
				}
				n64, err = StrEncode(w, k.String())
				written += n64
				if err != nil {
					return
				}
				n64, err = Encode(w, vv.MapIndex(k).Interface(), opts)
				written += n64
				if err != nil {
					return
				}
			}
		}
		_, err = ByteEncode(w, byte(AtomEOC))
		if err != nil {
			return
		}
		written++
		return
	}
	if t := vv.Type(); t.Kind() == reflect.Struct {
		fields := reflect.VisibleFields(t)
		omits := make(map[string]struct{})
		for i := range fields {
			if tag, ok := fields[i].Tag.Lookup("keks"); ok {
				opts := strings.Split(tag, ",")
				if opts[0] != "" {
					fields[i].Name = opts[0]
					if len(opts) == 2 && opts[1] == "omitempty" {
						omits[opts[0]] = struct{}{}
					}
				}
			}
		}
		sort.Sort(keksort.ByKEKSName(fields))
		_, err = ByteEncode(w, byte(AtomMap))
		if err != nil {
			return
		}
		written++
		var n64 int64
		for i := range fields {
			fv := vv.FieldByIndex(fields[i].Index)
			{
				var empty bool
				switch fv.Type().Kind() {
				case reflect.Pointer:
					if fv.IsNil() {
						empty = true
					}
				case reflect.Slice:
					if fv.Len() == 0 {
						empty = true
					}
				case reflect.Map:
					if fv.Len() == 0 {
						empty = true
					}
				}
				if empty {
					if _, ok := omits[fields[i].Name]; ok {
						continue
					}
				}
			}
			n64, err = StrEncode(w, fields[i].Name)
			written += n64
			if err != nil {
				return
			}
			n64, err = Encode(w, fv.Interface(), opts)
			written += n64
			if err != nil {
				return
			}
		}
		_, err = ByteEncode(w, byte(AtomEOC))
		if err != nil {
			return
		}
		written++
	} else {
		err = fmt.Errorf("unhandled type: %+v", v)
	}
	return
}

func EncodeBuf(v any, opts *EncodeOpts) ([]byte, error) {
	var b bytes.Buffer
	_, err := Encode(&b, v, opts)
	return b.Bytes(), err
}
