// 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.

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)
}

// 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
	FormatDepth int
}

// DefaultOptionsV2 is the set of all options that define default v2 behavior.
var DefaultOptionsV2 = Struct{
	Flags: jsonflags.Flags{
		Presence: uint64(jsonflags.AllFlags & ^jsonflags.WhitespaceFlags),
		Values:   uint64(0),
	},
}

// DefaultOptionsV1 is the set of all options that define default v1 behavior.
var DefaultOptionsV1 = Struct{
	Flags: jsonflags.Flags{
		Presence: uint64(jsonflags.AllFlags & ^jsonflags.WhitespaceFlags),
		Values:   uint64(jsonflags.DefaultV1Flags),
	},
}

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)
		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) {
	dst.join(false, srcs...)
}

func (dst *Struct) JoinWithoutCoderOptions(srcs ...Options) {
	dst.join(true, srcs...)
}

func (dst *Struct) join(excludeCoderOptions bool, srcs ...Options) {
	for _, src := range srcs {
		switch src := src.(type) {
		case nil:
			continue
		case jsonflags.Bools:
			if excludeCoderOptions {
				src &= ^jsonflags.AllCoderFlags
			}
			dst.Flags.Set(src)
		case Indent:
			if excludeCoderOptions {
				continue
			}
			dst.Flags.Set(jsonflags.Multiline | jsonflags.Indent | 1)
			dst.Indent = string(src)
		case IndentPrefix:
			if excludeCoderOptions {
				continue
			}
			dst.Flags.Set(jsonflags.Multiline | jsonflags.IndentPrefix | 1)
			dst.IndentPrefix = string(src)
		case ByteLimit:
			if excludeCoderOptions {
				continue
			}
			dst.Flags.Set(jsonflags.ByteLimit | 1)
			dst.ByteLimit = int64(src)
		case DepthLimit:
			if excludeCoderOptions {
				continue
			}
			dst.Flags.Set(jsonflags.DepthLimit | 1)
			dst.DepthLimit = int(src)
		case *Struct:
			srcFlags := src.Flags // shallow copy the flags
			if excludeCoderOptions {
				srcFlags.Clear(jsonflags.AllCoderFlags)
			}
			dst.Flags.Join(srcFlags)
			if srcFlags.Has(jsonflags.NonBooleanFlags) {
				if srcFlags.Has(jsonflags.Indent) {
					dst.Indent = src.Indent
				}
				if srcFlags.Has(jsonflags.IndentPrefix) {
					dst.IndentPrefix = src.IndentPrefix
				}
				if srcFlags.Has(jsonflags.ByteLimit) {
					dst.ByteLimit = src.ByteLimit
				}
				if srcFlags.Has(jsonflags.DepthLimit) {
					dst.DepthLimit = src.DepthLimit
				}
				if srcFlags.Has(jsonflags.Marshalers) {
					dst.Marshalers = src.Marshalers
				}
				if srcFlags.Has(jsonflags.Unmarshalers) {
					dst.Unmarshalers = src.Unmarshalers
				}
			}
		default:
			*dst = JoinUnknownOption(*dst, src)
		}
	}
}

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)   {}
