// Copyright 2024 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

package json

import (
	"fmt"
	"reflect"
	"strconv"
	"strings"

	"encoding/json/internal"
	"encoding/json/jsontext"
	jsonv2 "encoding/json/v2"
)

// Inject functionality into v2 to properly handle v1 types.
func init() {
	internal.TransformMarshalError = transformMarshalError
	internal.TransformUnmarshalError = transformUnmarshalError
	internal.NewMarshalerError = func(val any, err error, funcName string) error {
		return &MarshalerError{reflect.TypeOf(val), err, funcName}
	}

	internal.NewRawNumber = func() any { return new(Number) }
	internal.RawNumberOf = func(b []byte) any { return Number(b) }
}

func transformMarshalError(root any, err error) error {
	// Historically, errors returned from Marshal methods were wrapped
	// in a [MarshalerError]. This is directly performed by the v2 package
	// via the injected [internal.NewMarshalerError] constructor
	// while operating under [ReportErrorsWithLegacySemantics].
	// Note that errors from a Marshal method were always wrapped,
	// even if wrapped for multiple layers.
	if err, ok := err.(*jsonv2.SemanticError); err != nil {
		if err.Err == nil {
			// Historically, this was only reported for unserializable types
			// like complex numbers, channels, functions, and unsafe.Pointers.
			return &UnsupportedTypeError{Type: err.GoType}
		} else {
			// Historically, this was only reported for NaN or ±Inf values
			// and cycles detected in the value.
			// The Val used to be populated with the reflect.Value,
			// but this is no longer supported.
			errStr := err.Err.Error()
			if err.Err == internal.ErrCycle && err.GoType != nil {
				errStr += " via " + err.GoType.String()
			}
			errStr = strings.TrimPrefix(errStr, "unsupported value: ")
			return &UnsupportedValueError{Str: errStr}
		}
	} else if ok {
		return (*UnsupportedValueError)(nil)
	}
	if err, _ := err.(*MarshalerError); err != nil {
		err.Err = transformSyntacticError(err.Err)
		return err
	}
	return transformSyntacticError(err)
}

func transformUnmarshalError(root any, err error) error {
	// Historically, errors from Unmarshal methods were never wrapped and
	// returned verbatim while operating under [ReportErrorsWithLegacySemantics].
	if err, ok := err.(*jsonv2.SemanticError); err != nil {
		if err.Err == internal.ErrNonNilReference {
			return &InvalidUnmarshalError{err.GoType}
		}
		if err.Err == jsonv2.ErrUnknownName {
			return fmt.Errorf("json: unknown field %q", err.JSONPointer.LastToken())
		}

		// Historically, UnmarshalTypeError has always been inconsistent
		// about how it reported position information.
		//
		// The Struct field now points to the root type,
		// rather than some intermediate struct in the path.
		// This better matches the original intent of the field based
		// on how the Error message was formatted.
		//
		// For a representation closer to the historical representation,
		// we switch the '/'-delimited representation of a JSON pointer
		// to use a '.'-delimited representation. This may be ambiguous,
		// but the prior representation was always ambiguous as well.
		// Users that care about precise positions should use v2 errors
		// by disabling [ReportErrorsWithLegacySemantics].
		//
		// The introduction of a Err field is new to the v1-to-v2 migration
		// and allows us to preserve stronger error information
		// that may be surfaced by the v2 package.
		//
		// See https://go.dev/issue/43126
		var value string
		switch err.JSONKind {
		case 'n', '"', '0':
			value = err.JSONKind.String()
		case 'f', 't':
			value = "bool"
		case '[', ']':
			value = "array"
		case '{', '}':
			value = "object"
		}
		if len(err.JSONValue) > 0 {
			isStrconvError := err.Err == strconv.ErrRange || err.Err == strconv.ErrSyntax
			isNumericKind := func(t reflect.Type) bool {
				if t == nil {
					return false
				}
				switch t.Kind() {
				case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
					reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr,
					reflect.Float32, reflect.Float64:
					return true
				}
				return false
			}
			if isStrconvError && isNumericKind(err.GoType) {
				value = "number"
				if err.JSONKind == '"' {
					err.JSONValue, _ = jsontext.AppendUnquote(nil, err.JSONValue)
				}
				err.Err = nil
			}
			value += " " + string(err.JSONValue)
		}
		var rootName string
		if t := reflect.TypeOf(root); t != nil && err.JSONPointer != "" {
			if t.Kind() == reflect.Pointer {
				t = t.Elem()
			}
			rootName = t.Name()
		}
		fieldPath := string(err.JSONPointer)
		fieldPath = strings.TrimPrefix(fieldPath, "/")
		fieldPath = strings.ReplaceAll(fieldPath, "/", ".")
		return &UnmarshalTypeError{
			Value:  value,
			Type:   err.GoType,
			Offset: err.ByteOffset,
			Struct: rootName,
			Field:  fieldPath,
			Err:    transformSyntacticError(err.Err),
		}
	} else if ok {
		return (*UnmarshalTypeError)(nil)
	}
	return transformSyntacticError(err)
}
