// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause

// Package jsonutil provides utilities to improve JSON performance.
// It includes an Unmarshal wrapper that amortizes allocated garbage over subsequent runs
// and a Bytes type to reduce allocations when unmarshalling a non-hex-encoded string into a []byte.
package jsonutil

import (
	"bytes"
	"encoding/json"
	"sync"
)

// decoder is a re-usable json decoder.
type decoder struct {
	dec *json.Decoder
	r   *bytes.Reader
}

var readerPool = sync.Pool{
	New: func() any {
		return bytes.NewReader(nil)
	},
}

var decoderPool = sync.Pool{
	New: func() any {
		var d decoder
		d.r = readerPool.Get().(*bytes.Reader)
		d.dec = json.NewDecoder(d.r)
		return &d
	},
}

// Unmarshal is similar to encoding/json.Unmarshal.
// There are three major differences:
//
// On error, encoding/json.Unmarshal zeros v.
// This Unmarshal may leave partial data in v.
// Always check the error before using v!
// (Future improvements may remove this bug.)
//
// The errors they return don't always match perfectly.
// If you do error matching more precise than err != nil,
// don't use this Unmarshal.
//
// This Unmarshal allocates considerably less memory.
func Unmarshal(b []byte, v any) error {
	d := decoderPool.Get().(*decoder)
	d.r.Reset(b)
	off := d.dec.InputOffset()
	err := d.dec.Decode(v)
	d.r.Reset(nil) // don't keep a reference to b
	// In case of error, report the offset in this byte slice,
	// instead of in the totality of all bytes this decoder has processed.
	// It is not possible to make all errors match json.Unmarshal exactly,
	// but we can at least try.
	switch jsonerr := err.(type) {
	case *json.SyntaxError:
		jsonerr.Offset -= off
	case *json.UnmarshalTypeError:
		jsonerr.Offset -= off
	case nil:
		// json.Unmarshal fails if there's any extra junk in the input.
		// json.Decoder does not; see https://github.com/golang/go/issues/36225.
		// We need to check for anything left over in the buffer.
		if d.dec.More() {
			// TODO: Provide a better error message.
			// Unfortunately, we can't set the msg field.
			// The offset doesn't perfectly match json:
			// Ours is at the end of the valid data,
			// and theirs is at the beginning of the extra data after whitespace.
			// Close enough, though.
			err = &json.SyntaxError{Offset: d.dec.InputOffset() - off}

			// TODO: zero v. This is hard; see encoding/json.indirect.
		}
	}
	if err == nil {
		decoderPool.Put(d)
	} else {
		// There might be junk left in the decoder's buffer.
		// There's no way to flush it, no Reset method.
		// Abandoned the decoder but reuse the reader.
		readerPool.Put(d.r)
	}
	return err
}
