// Package ints is an optimised encoder for decimal numbers in ASCII format,
// that simplifies and accelerates encoding and decoding decimal strings. It is
// faster than strconv in part because it uses a base of 10000 and a lookup
// table.
package ints

import (
	_ "embed"
	"io"

	"golang.org/x/exp/constraints"
	"lol.mleku.dev/errorf"
)

// run this to regenerate (pointlessly) the base 10 array of 4 places per entry
//go:generate go run ./gen/.

//go:embed base10k.txt
var base10k []byte

const base = 10000

// T is an integer with a fast codec to decimal ASCII.
type T struct {
	N uint64
}

func New[V constraints.Integer](n V) *T {
	return &T{uint64(n)}
}

// Uint64 returns the int.T as a uint64 (the base type)
func (n *T) Uint64() uint64 { return n.N }

// Int64 returns an int64 from the base number (may cause truncation)
func (n *T) Int64() int64 { return int64(n.N) }

// Uint16 returns an uint16 from the base number (may cause truncation)
func (n *T) Uint16() uint16 { return uint16(n.N) }

var powers = []*T{
	{1},
	{1_0000},
	{1_0000_0000},
	{1_0000_0000_0000},
	{1_0000_0000_0000_0000},
}

const zero = '0'
const nine = '9'

// Marshal the int.T into a byte string.
func (n *T) Marshal(dst []byte) (b []byte) {
	nn := n.N
	b = dst
	if n.N == 0 {
		b = append(b, '0')
		return
	}
	var i int
	var trimmed bool
	k := len(powers)
	for k > 0 {
		k--
		q := n.N / powers[k].N
		if !trimmed && q == 0 {
			continue
		}
		offset := q * 4
		bb := base10k[offset : offset+4]
		if !trimmed {
			for i = range bb {
				if bb[i] != '0' {
					bb = bb[i:]
					trimmed = true
					break
				}
			}
		}
		b = append(b, bb...)
		n.N = n.N - q*powers[k].N
	}
	n.N = nn
	return
}

// Unmarshal reads a string, which must be a positive integer no larger than math.MaxUint64,
// skipping any non-numeric content before it.
//
// Note that leading zeros are not considered valid, but basically no such thing as machine
// generated JSON integers with leading zeroes. Until this is disproven, this is the fastest way
// to read a positive json integer, and a leading zero is decoded as a zero, and the remainder
// returned.
func (n *T) Unmarshal(b []byte) (r []byte, err error) {
	if len(b) < 1 {
		err = errorf.E("zero length number")
		return
	}
	var sLen int
	if b[0] == zero {
		r = b[1:]
		n.N = 0
		return
	}
	// skip non-number characters
	for i, v := range b {
		if v >= '0' && v <= '9' {
			b = b[i:]
			break
		}
	}
	// log.I.F("%s", b)
	if len(b) == 0 {
		err = io.EOF
		return
	}
	// count the digits
	for ; sLen < len(b) && b[sLen] >= zero && b[sLen] <= nine && b[sLen] != ','; sLen++ {
	}
	// log.I.F("%s", b[:sLen])
	if sLen == 0 {
		err = errorf.E("zero length number")
		return
	}
	if sLen > 20 {
		err = errorf.E("too big number for uint64")
		return
	}
	// the length of the string found
	r = b[sLen:]
	b = b[:sLen]
	// log.I.F("\n%s\n%s", b, r)
	n.N = uint64(b[0]) - zero
	b = b[1:]
	for _, ch := range b {
		ch -= zero
		n.N = n.N*10 + uint64(ch)
	}
	// log.I.F("%d", n.N)
	return
}
