// 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 (
	"errors"
	"math/big"
	"strings"
	"unicode/utf8"
	"unsafe"

	"go.cypherpunks.su/keks/be"
	"go.cypherpunks.su/keks/types"
	"go.cypherpunks.su/tai64n/v4"
)

var (
	ErrLenTooBig       = errors.New("too big string")
	ErrIntNonMinimal   = errors.New("int non minimal")
	ErrUnknownType     = errors.New("unknown type")
	ErrUTF8Bad         = errors.New("invalid UTF-8")
	ErrIntBad          = errors.New("bad int value")
	ErrMagicBad        = errors.New("bad magic value")
	ErrTAIReserved     = errors.New("reserved TAI64 values in use")
	ErrTAINonMinimal   = errors.New("non-minimal TAI64")
	ErrTAITooManyNsecs = errors.New("too many nanoseconds")
	ErrTAITooManyAsecs = errors.New("too many attoseconds")
)

func (ctx *Decoder) DecodeAtom() (t types.Type, err error) {
	if ctx.opts != nil && ctx.opts.SaveOffsets {
		ctx.offsets = append(ctx.offsets, ctx.Read)
	}
	var tag byte
	tag, err = ctx.getByte()
	if err != nil {
		return
	}
	if (tag & AtomStrings) > 0 {
		if (tag & AtomIsUTF8) == 0 {
			t = types.Bin
		} else {
			t = types.Str
		}
		var s string
		s, err = ctx.getStr(tag)
		if err != nil {
			return
		}
		ctx.types = append(ctx.types, t)
		ctx.depths = append(ctx.depths, ctx.depth)
		ctx.strs = append(ctx.strs, s)
		if t == types.Str && !(ctx.opts != nil && ctx.opts.DisableUTF8Check) {
			if !utf8.ValidString(s) {
				err = ErrUTF8Bad
			}
			if strings.Contains(s, "\x00") {
				err = ErrUTF8Bad
			}
		}
		return
	}
	switch AtomType(tag) {
	case AtomEOC:
		t = types.EOC
	case AtomNIL:
		t = types.NIL
	case AtomFalse:
		t = types.Bool
		ctx.bools = append(ctx.bools, false)
	case AtomTrue:
		t = types.Bool
		ctx.bools = append(ctx.bools, true)
	case AtomHexlet:
		var s string
		s, err = ctx.getBytes(16)
		if err != nil {
			return
		}
		t = types.Hexlet
		v := Hexlet([]byte(s))
		ctx.hexlets = append(ctx.hexlets, &v)
	case AtomList:
		t = types.List
	case AtomMap:
		t = types.Map
	case AtomBLOB:
		var s string
		s, err = ctx.getBytes(8)
		if err != nil {
			return
		}
		chunkLen := be.Get([]byte(s))
		if chunkLen >= (1<<63)-1 {
			err = ErrLenTooBig
			return
		}
		chunkLen++
		t = types.Blob
		ctx.blobChunkLens = append(ctx.blobChunkLens, int64(chunkLen))
	case AtomPInt, AtomNInt:
		if AtomType(tag) == AtomPInt {
			t = types.UInt
		} else {
			t = types.Int
		}
		tag, err = ctx.getByte()
		if err != nil {
			return
		}
		if tag&AtomStrings == 0 || tag&AtomIsUTF8 != 0 {
			err = ErrIntBad
			return
		}
		var s string
		s, err = ctx.getStr(tag)
		if err != nil {
			return
		}
		if len(s) == 0 {
			if t == types.UInt {
				ctx.uints = append(ctx.uints, 0)
			} else {
				ctx.ints = append(ctx.ints, -1)
			}
			break
		}
		if s[0] == 0 {
			err = ErrIntNonMinimal
			return
		}
		if len(s) > 8 {
			bi := new(big.Int).SetBytes([]byte(s))
			if t == types.Int {
				bi = bi.Add(bi, big.NewInt(1))
				bi = bi.Neg(bi)
			}
			t = types.BigInt
			ctx.bigints = append(ctx.bigints, bi)
			break
		}
		i := be.Get([]byte(s))
		if t == types.UInt {
			ctx.uints = append(ctx.uints, i)
		} else {
			if i >= (1 << 63) {
				bi := new(big.Int).SetBytes([]byte(s))
				bi = bi.Add(bi, big.NewInt(1))
				bi = bi.Neg(bi)
				ctx.bigints = append(ctx.bigints, bi)
				t = types.BigInt
			} else {
				ctx.ints = append(ctx.ints, -1-int64(i))
			}
		}
	case AtomFloatNaN:
		t = types.Float
		ctx.floats = append(ctx.floats, &Float{NaN: true})
	case AtomFloatPinf:
		t = types.Float
		ctx.floats = append(ctx.floats, &Float{Pinf: true})
	case AtomFloatNinf:
		t = types.Float
		ctx.floats = append(ctx.floats, &Float{Ninf: true})
	case AtomFloat:
		t = types.Float
		ctx.floats = append(ctx.floats, &Float{})
	case AtomTAI64, AtomTAI64N, AtomTAI64NA:
		var l int
		switch AtomType(tag) {
		case AtomTAI64:
			t = types.TAI64
			l = 8
		case AtomTAI64N:
			t = types.TAI64N
			l = 12
		case AtomTAI64NA:
			t = types.TAI64NA
			l = 16
		}
		var s string
		s, err = ctx.getBytes(l)
		if err != nil {
			return
		}
		if be.Get([]byte(s)[:8]) > (1 << 63) {
			err = ErrTAIReserved
			return
		}
		if l > 8 {
			nsecs := be.Get([]byte(s)[8 : 8+4])
			if l == 12 && nsecs == 0 {
				err = ErrTAINonMinimal
				return
			}
			if nsecs > 999999999 {
				err = ErrTAITooManyNsecs
				return
			}
		}
		if l > 12 {
			asecs := be.Get([]byte(s)[8+4 : 8+4+4])
			if asecs == 0 {
				err = ErrTAINonMinimal
				return
			}
			if asecs > 999999999 {
				err = ErrTAITooManyAsecs
				return
			}
		}
		switch t {
		case types.TAI64:
			tai := tai64n.TAI64(unsafe.Slice(unsafe.StringData(s), 8))
			ctx.tai64s = append(ctx.tai64s, tai)
		case types.TAI64N:
			tai := tai64n.TAI64N(unsafe.Slice(unsafe.StringData(s), 12))
			ctx.tai64ns = append(ctx.tai64ns, tai)
		case types.TAI64NA:
			tai := tai64n.TAI64NA(unsafe.Slice(unsafe.StringData(s), 16))
			ctx.tai64nas = append(ctx.tai64nas, tai)
		}
	case AtomMagic:
		var s string
		s, err = ctx.getBytes(15)
		if err != nil {
			return
		}
		if s[:3] != "EKS" {
			err = ErrMagicBad
			return
		}
		t = types.Magic
		ctx.strs = append(ctx.strs, strings.TrimRight(s[3:], "\x00"))
	default:
		err = ErrUnknownType
		return
	}
	ctx.types = append(ctx.types, t)
	ctx.depths = append(ctx.depths, ctx.depth)
	return
}
