// 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 writeconcern defines write concerns for MongoDB operations.
package writeconcern // import "go.mongodb.org/mongo-driver/mongo/writeconcern"

import (
	"errors"
	"time"

	"go.mongodb.org/mongo-driver/bson"
	"go.mongodb.org/mongo-driver/bson/bsontype"
	"go.mongodb.org/mongo-driver/x/bsonx/bsoncore"
)

// ErrInconsistent indicates that an inconsistent write concern was specified.
var ErrInconsistent = errors.New("a write concern cannot have both w=0 and j=true")

// ErrEmptyWriteConcern indicates that a write concern has no fields set.
var ErrEmptyWriteConcern = errors.New("a write concern must have at least one field set")

// ErrNegativeW indicates that a negative integer `w` field was specified.
var ErrNegativeW = errors.New("write concern `w` field cannot be a negative number")

// ErrNegativeWTimeout indicates that a negative WTimeout was specified.
var ErrNegativeWTimeout = errors.New("write concern `wtimeout` field cannot be negative")

// WriteConcern describes the level of acknowledgement requested from MongoDB for write operations
// to a standalone mongod or to replica sets or to sharded clusters.
type WriteConcern struct {
	w interface{}
	j bool

	// NOTE(benjirewis): wTimeout will be deprecated in a future release. The more general Timeout
	// option may be used in its place to control the amount of time that a single operation can run
	// before returning an error. Using wTimeout and setting Timeout on the client will result in
	// undefined behavior.
	wTimeout time.Duration
}

// Option is an option to provide when creating a WriteConcern.
type Option func(concern *WriteConcern)

// New constructs a new WriteConcern.
func New(options ...Option) *WriteConcern {
	concern := &WriteConcern{}

	for _, option := range options {
		option(concern)
	}

	return concern
}

// W requests acknowledgement that write operations propagate to the specified number of mongod
// instances.
func W(w int) Option {
	return func(concern *WriteConcern) {
		concern.w = w
	}
}

// WMajority requests acknowledgement that write operations propagate to the majority of mongod
// instances.
func WMajority() Option {
	return func(concern *WriteConcern) {
		concern.w = "majority"
	}
}

// WTagSet requests acknowledgement that write operations propagate to the specified mongod
// instance.
func WTagSet(tag string) Option {
	return func(concern *WriteConcern) {
		concern.w = tag
	}
}

// J requests acknowledgement from MongoDB that write operations are written to
// the journal.
func J(j bool) Option {
	return func(concern *WriteConcern) {
		concern.j = j
	}
}

// WTimeout specifies specifies a time limit for the write concern.
//
// NOTE(benjirewis): wTimeout will be deprecated in a future release. The more general Timeout
// option may be used in its place to control the amount of time that a single operation can run
// before returning an error. Using wTimeout and setting Timeout on the client will result in
// undefined behavior.
func WTimeout(d time.Duration) Option {
	return func(concern *WriteConcern) {
		concern.wTimeout = d
	}
}

// MarshalBSONValue implements the bson.ValueMarshaler interface.
func (wc *WriteConcern) MarshalBSONValue() (bsontype.Type, []byte, error) {
	if !wc.IsValid() {
		return bsontype.Type(0), nil, ErrInconsistent
	}

	var elems []byte

	if wc.w != nil {
		switch t := wc.w.(type) {
		case int:
			if t < 0 {
				return bsontype.Type(0), nil, ErrNegativeW
			}

			elems = bsoncore.AppendInt32Element(elems, "w", int32(t))
		case string:
			elems = bsoncore.AppendStringElement(elems, "w", t)
		}
	}

	if wc.j {
		elems = bsoncore.AppendBooleanElement(elems, "j", wc.j)
	}

	if wc.wTimeout < 0 {
		return bsontype.Type(0), nil, ErrNegativeWTimeout
	}

	if wc.wTimeout != 0 {
		elems = bsoncore.AppendInt64Element(elems, "wtimeout", int64(wc.wTimeout/time.Millisecond))
	}

	if len(elems) == 0 {
		return bsontype.Type(0), nil, ErrEmptyWriteConcern
	}
	return bsontype.EmbeddedDocument, bsoncore.BuildDocument(nil, elems), nil
}

// AcknowledgedValue returns true if a BSON RawValue for a write concern represents an acknowledged write concern.
// The element's value must be a document representing a write concern.
func AcknowledgedValue(rawv bson.RawValue) bool {
	doc, ok := bsoncore.Value{Type: rawv.Type, Data: rawv.Value}.DocumentOK()
	if !ok {
		return false
	}

	val, err := doc.LookupErr("w")
	if err != nil {
		// key w not found --> acknowledged
		return true
	}

	i32, ok := val.Int32OK()
	if !ok {
		return false
	}
	return i32 != 0
}

// Acknowledged indicates whether or not a write with the given write concern will be acknowledged.
func (wc *WriteConcern) Acknowledged() bool {
	if wc == nil || wc.j {
		return true
	}

	switch v := wc.w.(type) {
	case int:
		if v == 0 {
			return false
		}
	}

	return true
}

// IsValid checks whether the write concern is invalid.
func (wc *WriteConcern) IsValid() bool {
	if !wc.j {
		return true
	}

	switch v := wc.w.(type) {
	case int:
		if v == 0 {
			return false
		}
	}

	return true
}

// GetW returns the write concern w level.
func (wc *WriteConcern) GetW() interface{} {
	return wc.w
}

// GetJ returns the write concern journaling level.
func (wc *WriteConcern) GetJ() bool {
	return wc.j
}

// GetWTimeout returns the write concern timeout.
func (wc *WriteConcern) GetWTimeout() time.Duration {
	return wc.wTimeout
}

// WithOptions returns a copy of this WriteConcern with the options set.
func (wc *WriteConcern) WithOptions(options ...Option) *WriteConcern {
	if wc == nil {
		return New(options...)
	}
	newWC := &WriteConcern{}
	*newWC = *wc

	for _, option := range options {
		option(newWC)
	}

	return newWC
}

// AckWrite returns true if a write concern represents an acknowledged write
func AckWrite(wc *WriteConcern) bool {
	return wc == nil || wc.Acknowledged()
}
