// Copyright (C) MongoDB, Inc. 2017-present.
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License. You may obtain
// a copy of the License at http://www.apache.org/licenses/LICENSE-2.0

package bson_test

import (
	"bytes"
	"fmt"
	"math"
	"reflect"

	"go.mongodb.org/mongo-driver/v2/bson"
)

func ExampleRegistry_customEncoder() {
	// Create a custom encoder for an integer type that multiplies the input
	// value by -1 when encoding.
	type negatedInt int

	negatedIntType := reflect.TypeOf(negatedInt(0))

	negatedIntEncoder := func(
		_ bson.EncodeContext,
		vw bson.ValueWriter,
		val reflect.Value,
	) error {
		// All encoder implementations should check that val is valid and is of
		// the correct type before proceeding.
		if !val.IsValid() || val.Type() != negatedIntType {
			return bson.ValueEncoderError{
				Name:     "negatedIntEncoder",
				Types:    []reflect.Type{negatedIntType},
				Received: val,
			}
		}

		// Negate val and encode as a BSON int32 if it can fit in 32 bits and a
		// BSON int64 otherwise.
		negatedVal := val.Int() * -1
		if math.MinInt32 <= negatedVal && negatedVal <= math.MaxInt32 {
			return vw.WriteInt32(int32(negatedVal))
		}
		return vw.WriteInt64(negatedVal)
	}

	reg := bson.NewRegistry()
	reg.RegisterTypeEncoder(
		negatedIntType,
		bson.ValueEncoderFunc(negatedIntEncoder))

	// Define a document that includes both int and negatedInt fields with the
	// same value.
	type myDocument struct {
		Int        int
		NegatedInt negatedInt
	}
	doc := myDocument{
		Int:        1,
		NegatedInt: 1,
	}

	// Marshal the document as BSON. Expect that the int field is encoded to the
	// same value and that the negatedInt field is encoded as the negated value.
	buf := new(bytes.Buffer)
	vw := bson.NewDocumentWriter(buf)
	enc := bson.NewEncoder(vw)
	enc.SetRegistry(reg)
	err := enc.Encode(doc)
	if err != nil {
		panic(err)
	}
	fmt.Println(bson.Raw(buf.Bytes()).String())
	// Output: {"int": {"$numberInt":"1"},"negatedint": {"$numberInt":"-1"}}
}

func ExampleRegistry_customDecoder() {
	// Create a custom decoder for a boolean type that can be stored in the
	// database as a BSON boolean, int32, or int64. For our custom decoder, BSON
	// int32 or int64 values are considered "true" if they are non-zero.
	type lenientBool bool

	lenientBoolType := reflect.TypeOf(lenientBool(true))

	lenientBoolDecoder := func(
		_ bson.DecodeContext,
		vr bson.ValueReader,
		val reflect.Value,
	) error {
		// All decoder implementations should check that val is valid, settable,
		// and is of the correct kind before proceeding.
		if !val.IsValid() || !val.CanSet() || val.Type() != lenientBoolType {
			return bson.ValueDecoderError{
				Name:     "lenientBoolDecoder",
				Types:    []reflect.Type{lenientBoolType},
				Received: val,
			}
		}

		var result bool
		switch vr.Type() {
		case bson.TypeBoolean:
			b, err := vr.ReadBoolean()
			if err != nil {
				return err
			}
			result = b
		case bson.TypeInt32:
			i32, err := vr.ReadInt32()
			if err != nil {
				return err
			}
			result = i32 != 0
		case bson.TypeInt64:
			i64, err := vr.ReadInt64()
			if err != nil {
				return err
			}
			result = i64 != 0
		default:
			return fmt.Errorf(
				"received invalid BSON type to decode into lenientBool: %s",
				vr.Type())
		}

		val.SetBool(result)
		return nil
	}

	reg := bson.NewRegistry()
	reg.RegisterTypeDecoder(
		lenientBoolType,
		bson.ValueDecoderFunc(lenientBoolDecoder))

	// Marshal a BSON document with a single field "isOK" that is a non-zero
	// integer value.
	b, err := bson.Marshal(bson.M{"isOK": 1})
	if err != nil {
		panic(err)
	}

	// Now try to decode the BSON document to a struct with a field "IsOK" that
	// is type lenientBool. Expect that the non-zero integer value is decoded
	// as boolean true.
	type MyDocument struct {
		IsOK lenientBool `bson:"isOK"`
	}
	var doc MyDocument
	dec := bson.NewDecoder(bson.NewDocumentReader(bytes.NewReader(b)))
	dec.SetRegistry(reg)
	err = dec.Decode(&doc)
	if err != nil {
		panic(err)
	}
	fmt.Printf("%+v\n", doc)
	// Output: {IsOK:true}
}

func ExampleRegistry_RegisterKindEncoder() {
	// Create a custom encoder that writes any Go type that has underlying type
	// int32 as an a BSON int64. To do that, we register the encoder as a "kind"
	// encoder for kind reflect.Int32. That way, even user-defined types with
	// underlying type int32 will be encoded as a BSON int64.
	int32To64Encoder := func(
		_ bson.EncodeContext,
		vw bson.ValueWriter,
		val reflect.Value,
	) error {
		// All encoder implementations should check that val is valid and is of
		// the correct type or kind before proceeding.
		if !val.IsValid() || val.Kind() != reflect.Int32 {
			return bson.ValueEncoderError{
				Name:     "int32To64Encoder",
				Kinds:    []reflect.Kind{reflect.Int32},
				Received: val,
			}
		}

		return vw.WriteInt64(val.Int())
	}

	// Create a default registry and register our int32-to-int64 encoder for
	// kind reflect.Int32.
	reg := bson.NewRegistry()
	reg.RegisterKindEncoder(
		reflect.Int32,
		bson.ValueEncoderFunc(int32To64Encoder))

	// Define a document that includes an int32, an int64, and a user-defined
	// type "myInt" that has underlying type int32.
	type myInt int32
	type myDocument struct {
		MyInt myInt
		Int32 int32
		Int64 int64
	}
	doc := myDocument{
		Int32: 1,
		Int64: 1,
		MyInt: 1,
	}

	// Marshal the document as BSON. Expect that all fields are encoded as BSON
	// int64 (represented as "$numberLong" when encoded as Extended JSON).
	buf := new(bytes.Buffer)
	vw := bson.NewDocumentWriter(buf)
	enc := bson.NewEncoder(vw)
	enc.SetRegistry(reg)
	err := enc.Encode(doc)
	if err != nil {
		panic(err)
	}
	fmt.Println(bson.Raw(buf.Bytes()).String())
	// Output: {"myint": {"$numberLong":"1"},"int32": {"$numberLong":"1"},"int64": {"$numberLong":"1"}}
}

func ExampleRegistry_RegisterKindDecoder() {
	// Create a custom decoder that can decode any integer value, including
	// integer values encoded as floating point numbers, to any Go type
	// with underlying type int64. To do that, we register the decoder as a
	// "kind" decoder for kind reflect.Int64. That way, we can even decode to
	// user-defined types with underlying type int64.
	flexibleInt64KindDecoder := func(
		_ bson.DecodeContext,
		vr bson.ValueReader,
		val reflect.Value,
	) error {
		// All decoder implementations should check that val is valid, settable,
		// and is of the correct kind before proceeding.
		if !val.IsValid() || !val.CanSet() || val.Kind() != reflect.Int64 {
			return bson.ValueDecoderError{
				Name:     "flexibleInt64KindDecoder",
				Kinds:    []reflect.Kind{reflect.Int64},
				Received: val,
			}
		}

		var result int64
		switch vr.Type() {
		case bson.TypeInt64:
			i64, err := vr.ReadInt64()
			if err != nil {
				return err
			}
			result = i64
		case bson.TypeInt32:
			i32, err := vr.ReadInt32()
			if err != nil {
				return err
			}
			result = int64(i32)
		case bson.TypeDouble:
			d, err := vr.ReadDouble()
			if err != nil {
				return err
			}
			i64 := int64(d)
			// Make sure the double field is an integer value.
			if d != float64(i64) {
				return fmt.Errorf("double %f is not an integer value", d)
			}
			result = i64
		default:
			return fmt.Errorf(
				"received invalid BSON type to decode into int64: %s",
				vr.Type())
		}

		val.SetInt(result)
		return nil
	}

	reg := bson.NewRegistry()
	reg.RegisterKindDecoder(
		reflect.Int64,
		bson.ValueDecoderFunc(flexibleInt64KindDecoder))

	// Marshal a BSON document with fields that are mixed numeric types but all
	// hold integer values (i.e. values with no fractional part).
	b, err := bson.Marshal(bson.M{"myInt": float64(8), "int64": int32(9)})
	if err != nil {
		panic(err)
	}

	// Now try to decode the BSON document to a struct with fields
	// that is type int32. Expect that the float value is successfully decoded.
	type myInt int64
	type myDocument struct {
		MyInt myInt
		Int64 int64
	}
	var doc myDocument
	dec := bson.NewDecoder(bson.NewDocumentReader(bytes.NewReader(b)))
	dec.SetRegistry(reg)
	err = dec.Decode(&doc)
	if err != nil {
		panic(err)
	}
	fmt.Printf("%+v\n", doc)
	// Output: {MyInt:8 Int64:9}
}
