// Copyright 2018 PingCAP, Inc.
//
// 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
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// See the License for the specific language governing permissions and
// limitations under the License.

package errcode

// HasOperation is an interface to retrieve the operation that occurred during an error.
// The end goal is to be able to see a trace of operations in a distributed system to quickly have a good understanding of what occurred.
// Inspiration is taken from upspin error handling: https://commandcenter.blogspot.com/2017/12/error-handling-in-upspin.html
// The relationship to error codes is not one-to-one.
// A given error code can be triggered by multiple different operations,
// just as a given operation could result in multiple different error codes.
//
// GetOperation is defined, but generally the operation should be retrieved with Operation().
// Operation() will check if a HasOperation interface exists.
// As an alternative to defining this interface
// you can use an existing wrapper (OpErrCode via AddOp) or embedding (EmbedOp) that has already defined it.
type HasOperation interface {
	GetOperation() string
}

// Operation will return an operation string if it exists.
// It checks for the HasOperation interface.
// Otherwise it will return the zero value (empty) string.
func Operation(v interface{}) string {
	var operation string
	if hasOp, ok := v.(HasOperation); ok {
		operation = hasOp.GetOperation()
	}
	return operation
}

// EmbedOp is designed to be embedded into your existing error structs.
// It provides the HasOperation interface already, which can reduce your boilerplate.
type EmbedOp struct{ Op string }

// GetOperation satisfies the HasOperation interface
func (e EmbedOp) GetOperation() string {
	return e.Op
}

// OpErrCode is an ErrorCode with an Operation field attached.
// This can be conveniently constructed with Op() and AddTo() to record the operation information for the error.
// However, it isn't required to be used, see the HasOperation documentation for alternatives.
type OpErrCode struct {
	Operation string
	Err       ErrorCode
}

// Cause satisfies the Causer interface
func (e OpErrCode) Cause() error {
	return e.Err
}

// Error prefixes the operation to the underlying Err Error.
func (e OpErrCode) Error() string {
	return e.Operation + ": " + e.Err.Error()
}

// GetOperation satisfies the HasOperation interface.
func (e OpErrCode) GetOperation() string {
	return e.Operation
}

// Code returns the underlying Code of Err.
func (e OpErrCode) Code() Code {
	return e.Err.Code()
}

// GetClientData returns the ClientData of the underlying Err.
func (e OpErrCode) GetClientData() interface{} {
	return ClientData(e.Err)
}

var _ ErrorCode = (*OpErrCode)(nil)     // assert implements interface
var _ HasClientData = (*OpErrCode)(nil) // assert implements interface
var _ HasOperation = (*OpErrCode)(nil)  // assert implements interface
var _ Causer = (*OpErrCode)(nil)        // assert implements interface

// AddOp is constructed by Op. It allows method chaining with AddTo.
type AddOp func(ErrorCode) OpErrCode

// AddTo adds the operation from Op to the ErrorCode
func (addOp AddOp) AddTo(err ErrorCode) OpErrCode {
	return addOp(err)
}

// Op adds an operation to an ErrorCode with AddTo.
// This converts the error to the type OpErrCode.
//
//	op := errcode.Op("path.move.x")
//	if start < obstable && obstacle < end  {
//		return op.AddTo(PathBlocked{start, end, obstacle})
//	}
//
func Op(operation string) AddOp {
	return func(err ErrorCode) OpErrCode {
		if err == nil {
			panic("Op error is nil")
		}
		return OpErrCode{Operation: operation, Err: err}
	}
}