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

package opt

import (
	"fmt"
	"reflect"

	jsonv2 "github.com/go-json-experiment/json"
	"github.com/go-json-experiment/json/jsontext"
)

// Value is an optional value to be JSON-encoded.
// With [encoding/json], a zero Value is marshaled as a JSON null.
// With [github.com/go-json-experiment/json], a zero Value is omitted from the
// JSON object if the Go struct field specified with omitzero.
// The omitempty tag option should never be used with Value fields.
type Value[T any] struct {
	value T
	set   bool
}

// Equal reports whether the receiver and the other value are equal.
// If the template type T in Value[T] implements an Equal method, it will be used
// instead of the == operator for comparing values.
type equatable[T any] interface {
	// Equal reports whether the receiver and the other values are equal.
	Equal(other T) bool
}

// ValueOf returns an optional Value containing the specified value.
// It treats nil slices and maps as empty slices and maps.
func ValueOf[T any](v T) Value[T] {
	return Value[T]{value: v, set: true}
}

// String implements [fmt.Stringer].
func (o Value[T]) String() string {
	if !o.set {
		return fmt.Sprintf("(empty[%T])", o.value)
	}
	return fmt.Sprint(o.value)
}

// Set assigns the specified value to the optional value o.
func (o *Value[T]) Set(v T) {
	*o = ValueOf(v)
}

// Clear resets o to an empty state.
func (o *Value[T]) Clear() {
	*o = Value[T]{}
}

// IsSet reports whether o has a value set.
func (o *Value[T]) IsSet() bool {
	return o.set
}

// Get returns the value of o.
// If a value hasn't been set, a zero value of T will be returned.
func (o Value[T]) Get() T {
	return o.value
}

// GetOr returns the value of o or def if a value hasn't been set.
func (o Value[T]) GetOr(def T) T {
	if o.set {
		return o.value
	}
	return def
}

// Get returns the value and a flag indicating whether the value is set.
func (o Value[T]) GetOk() (v T, ok bool) {
	return o.value, o.set
}

// Equal reports whether o is equal to v.
// Two optional values are equal if both are empty,
// or if both are set and the underlying values are equal.
// If the template type T implements an Equal(T) bool method, it will be used
// instead of the == operator for value comparison.
// If T is not comparable, it returns false.
func (o Value[T]) Equal(v Value[T]) bool {
	if o.set != v.set {
		return false
	}
	if !o.set {
		return true
	}
	ov := any(o.value)
	if eq, ok := ov.(equatable[T]); ok {
		return eq.Equal(v.value)
	}
	if reflect.TypeFor[T]().Comparable() {
		return ov == any(v.value)
	}
	return false
}

// MarshalJSONTo implements [jsonv2.MarshalerTo].
func (o Value[T]) MarshalJSONTo(enc *jsontext.Encoder) error {
	if !o.set {
		return enc.WriteToken(jsontext.Null)
	}
	return jsonv2.MarshalEncode(enc, &o.value)
}

// UnmarshalJSONFrom implements [jsonv2.UnmarshalerFrom].
func (o *Value[T]) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
	if dec.PeekKind() == 'n' {
		*o = Value[T]{}
		_, err := dec.ReadToken() // read null
		return err
	}
	o.set = true
	return jsonv2.UnmarshalDecode(dec, &o.value)
}

// MarshalJSON implements [json.Marshaler].
func (o Value[T]) MarshalJSON() ([]byte, error) {
	return jsonv2.Marshal(o) // uses MarshalJSONTo
}

// UnmarshalJSON implements [json.Unmarshaler].
func (o *Value[T]) UnmarshalJSON(b []byte) error {
	return jsonv2.Unmarshal(b, o) // uses UnmarshalJSONFrom
}
