// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build !goexperiment.jsonv2 || !go1.25

package jsonopts

import (
	"github.com/go-json-experiment/json/internal"
	"github.com/go-json-experiment/json/internal/jsonflags"
)

// Options is the common options type shared across json packages.
type Options interface {
	// JSONOptions is exported so related json packages can implement Options.
	JSONOptions(internal.NotForPublicUse)
}

// experimentalFormatTagSupporter is a special option method that allows
// [github.com/go-json-experiment/json.ExperimentalSupportFormatTag]
// to enable experimental `format` tag functionality at runtime.
type experimentalFormatTagSupporter interface{ ExperimentalSupportFormatTag() bool }

// Struct is the combination of all options in struct form.
// This is efficient to pass down the call stack and to query.
type Struct struct {
	Flags jsonflags.Flags

	CoderValues
	ArshalValues
}

type CoderValues struct {
	Indent       string // jsonflags.Indent
	IndentPrefix string // jsonflags.IndentPrefix
	ByteLimit    int64  // jsonflags.ByteLimit
	DepthLimit   int    // jsonflags.DepthLimit
}

type ArshalValues struct {
	// The Marshalers and Unmarshalers fields use the any type to avoid a
	// concrete dependency on *json.Marshalers and *json.Unmarshalers,
	// which would in turn create a dependency on the "reflect" package.

	Marshalers   any // jsonflags.Marshalers
	Unmarshalers any // jsonflags.Unmarshalers

	Format string // valid if jsonflags.FormatTag is set
}

// DefaultOptionsV2 is the set of all options that define default v2 behavior.
var DefaultOptionsV2 = Struct{
	Flags: jsonflags.Flags{
		Presence: uint64(jsonflags.DefaultV1Flags),
		Values:   uint64(0), // all flags in DefaultV1Flags are false
	},
}

// DefaultOptionsV1 is the set of all options that define default v1 behavior.
var DefaultOptionsV1 = Struct{
	Flags: jsonflags.Flags{
		Presence: uint64(jsonflags.DefaultV1Flags),
		Values:   uint64(jsonflags.DefaultV1Flags), // all flags in DefaultV1Flags are true
	},
}

func (*Struct) JSONOptions(internal.NotForPublicUse) {}

// GetUnknownOption is injected by the "json" package to handle Options
// declared in that package so that "jsonopts" can handle them.
var GetUnknownOption = func(Struct, Options) (any, bool) { panic("unknown option") }

func GetOption[T any](opts Options, setter func(T) Options) (T, bool) {
	// Collapse the options to *Struct to simplify lookup.
	structOpts, ok := opts.(*Struct)
	if !ok {
		var structOpts2 Struct
		structOpts2.Join(opts)
		structOpts = &structOpts2
	}

	// Lookup the option based on the return value of the setter.
	var zero T
	switch opt := setter(zero).(type) {
	case jsonflags.Bools:
		v := structOpts.Flags.Get(opt)
		ok := structOpts.Flags.Has(opt)
		if !ok && opt == jsonflags.StringifyNumbers && structOpts.Flags.Get(jsonflags.StringTag) {
			return any(true).(T), true // check also whether the option is specified via a `string` tag
		}
		return any(v).(T), ok
	case experimentalFormatTagSupporter:
		v := structOpts.Flags.Get(jsonflags.FormatTagSupported)
		ok := structOpts.Flags.Has(jsonflags.FormatTagSupported)
		return any(v).(T), ok
	case Indent:
		if !structOpts.Flags.Has(jsonflags.Indent) {
			return zero, false
		}
		return any(structOpts.Indent).(T), true
	case IndentPrefix:
		if !structOpts.Flags.Has(jsonflags.IndentPrefix) {
			return zero, false
		}
		return any(structOpts.IndentPrefix).(T), true
	case ByteLimit:
		if !structOpts.Flags.Has(jsonflags.ByteLimit) {
			return zero, false
		}
		return any(structOpts.ByteLimit).(T), true
	case DepthLimit:
		if !structOpts.Flags.Has(jsonflags.DepthLimit) {
			return zero, false
		}
		return any(structOpts.DepthLimit).(T), true
	default:
		v, ok := GetUnknownOption(*structOpts, opt)
		return v.(T), ok
	}
}

// JoinUnknownOption is injected by the "json" package to handle Options
// declared in that package so that "jsonopts" can handle them.
var JoinUnknownOption = func(Struct, Options) Struct { panic("unknown option") }

func (dst *Struct) Join(srcs ...Options) {
	for _, src := range srcs {
		switch src := src.(type) {
		case nil:
			continue
		case jsonflags.Bools:
			dst.Flags.Set(src)
		case experimentalFormatTagSupporter:
			if src.ExperimentalSupportFormatTag() {
				dst.Flags.Set(jsonflags.FormatTagSupported | 1)
			} else {
				dst.Flags.Set(jsonflags.FormatTagSupported | 0)
			}
		case Indent:
			dst.Flags.Set(jsonflags.Multiline | jsonflags.Indent | 1)
			dst.Indent = string(src)
		case IndentPrefix:
			dst.Flags.Set(jsonflags.Multiline | jsonflags.IndentPrefix | 1)
			dst.IndentPrefix = string(src)
		case ByteLimit:
			dst.Flags.Set(jsonflags.ByteLimit | 1)
			dst.ByteLimit = int64(src)
		case DepthLimit:
			dst.Flags.Set(jsonflags.DepthLimit | 1)
			dst.DepthLimit = int(src)
		case *Struct:
			dst.Flags.Join(src.Flags)
			if src.Flags.Has(jsonflags.NonBooleanFlags) {
				if src.Flags.Has(jsonflags.Indent) {
					dst.Indent = src.Indent
				}
				if src.Flags.Has(jsonflags.IndentPrefix) {
					dst.IndentPrefix = src.IndentPrefix
				}
				if src.Flags.Has(jsonflags.ByteLimit) {
					dst.ByteLimit = src.ByteLimit
				}
				if src.Flags.Has(jsonflags.DepthLimit) {
					dst.DepthLimit = src.DepthLimit
				}
				if src.Flags.Has(jsonflags.Marshalers) {
					dst.Marshalers = src.Marshalers
				}
				if src.Flags.Has(jsonflags.Unmarshalers) {
					dst.Unmarshalers = src.Unmarshalers
				}
				if src.Flags.Has(jsonflags.FormatTag) {
					dst.Format = src.Format
				}
			}
		default:
			*dst = JoinUnknownOption(*dst, src)
		}
	}
}

// InitializeMultiline sets default options implied by Multiline.
func (s *Struct) InitializeMultiline() {
	if !s.Flags.Has(jsonflags.SpaceAfterColon) {
		s.Flags.Set(jsonflags.SpaceAfterColon | 1)
	}
	if !s.Flags.Has(jsonflags.SpaceAfterComma) {
		s.Flags.Set(jsonflags.SpaceAfterComma | 0)
	}
	if !s.Flags.Has(jsonflags.Indent) {
		s.Flags.Set(jsonflags.Indent | 1)
		s.Indent = "\t"
	}
}

// ChangedWhitespace reports whether whitespace values have changed.
func ChangedWhitespace(s1, s2 Struct) bool {
	return s1.Flags.Get(jsonflags.Multiline) != s2.Flags.Get(jsonflags.Multiline) ||
		s1.Flags.Get(jsonflags.SpaceAfterColon) != s2.Flags.Get(jsonflags.SpaceAfterColon) ||
		s1.Flags.Get(jsonflags.SpaceAfterComma) != s2.Flags.Get(jsonflags.SpaceAfterComma) ||
		(s2.Flags.Get(jsonflags.Multiline) && (s1.Indent != s2.Indent || s1.IndentPrefix != s2.IndentPrefix))
}

type (
	Indent       string // jsontext.WithIndent
	IndentPrefix string // jsontext.WithIndentPrefix
	ByteLimit    int64  // jsontext.WithByteLimit
	DepthLimit   int    // jsontext.WithDepthLimit
	// type for jsonflags.Marshalers declared in "json" package
	// type for jsonflags.Unmarshalers declared in "json" package
)

func (Indent) JSONOptions(internal.NotForPublicUse)       {}
func (IndentPrefix) JSONOptions(internal.NotForPublicUse) {}
func (ByteLimit) JSONOptions(internal.NotForPublicUse)    {}
func (DepthLimit) JSONOptions(internal.NotForPublicUse)   {}
