package common

import (
	"fmt"
	"runtime"
)

//----------------------------------------
// Error & cmnError

type Error interface {
	Error() string
	Trace(format string, a ...interface{}) Error
	TraceCause(cause error, format string, a ...interface{}) Error
	Cause() error
	Type() interface{}
	WithType(t interface{}) Error
}

// New Error with no cause where the type is the format string of the message..
func NewError(format string, a ...interface{}) Error {
	msg := Fmt(format, a...)
	return newError(msg, nil, format)

}

// New Error with cause where the type is the cause, with message..
func NewErrorWithCause(cause error, format string, a ...interface{}) Error {
	msg := Fmt(format, a...)
	return newError(msg, cause, cause)
}

// New Error with specified type and message.
func NewErrorWithType(type_ interface{}, format string, a ...interface{}) Error {
	msg := Fmt(format, a...)
	return newError(msg, nil, type_)
}

type traceItem struct {
	msg      string
	filename string
	lineno   int
}

func (ti traceItem) String() string {
	return fmt.Sprintf("%v:%v %v", ti.filename, ti.lineno, ti.msg)
}

type cmnError struct {
	msg    string
	cause  error
	type_  interface{}
	traces []traceItem
}

// NOTE: Do not expose, it's not very friendly.
func newError(msg string, cause error, type_ interface{}) *cmnError {
	return &cmnError{
		msg:    msg,
		cause:  cause,
		type_:  type_,
		traces: nil,
	}
}

func (err *cmnError) Error() string {
	return fmt.Sprintf("Error{%v:%s,%v,%v}", err.type_, err.msg, err.cause, len(err.traces))
}

// Add tracing information with msg.
func (err *cmnError) Trace(format string, a ...interface{}) Error {
	msg := Fmt(format, a...)
	return err.doTrace(msg, 2)
}

// Add tracing information with cause and msg.
// If a cause was already set before, it is overwritten.
func (err *cmnError) TraceCause(cause error, format string, a ...interface{}) Error {
	msg := Fmt(format, a...)
	err.cause = cause
	return err.doTrace(msg, 2)
}

// Return the "type" of this message, primarily for switching
// to handle this error.
func (err *cmnError) Type() interface{} {
	return err.type_
}

// Overwrites the error's type.
func (err *cmnError) WithType(type_ interface{}) Error {
	err.type_ = type_
	return err
}

func (err *cmnError) doTrace(msg string, n int) Error {
	_, fn, line, ok := runtime.Caller(n)
	if !ok {
		if fn == "" {
			fn = "<unknown>"
		}
		if line <= 0 {
			line = -1
		}
	}
	// Include file & line number & msg.
	// Do not include the whole stack trace.
	err.traces = append(err.traces, traceItem{
		filename: fn,
		lineno:   line,
		msg:      msg,
	})
	return err
}

// Return last known cause.
// NOTE: The meaning of "cause" is left for the caller to define.
// There exists to canonical definition of "cause".
// Instead of blaming, try to handle-or-organize it.
func (err *cmnError) Cause() error {
	return err.cause
}

//----------------------------------------
// StackError

// NOTE: Used by Tendermint p2p upon recovery.
// Err could be "Reason", since it isn't an error type.
type StackError struct {
	Err   interface{}
	Stack []byte
}

func (se StackError) String() string {
	return fmt.Sprintf("Error: %v\nStack: %s", se.Err, se.Stack)
}

func (se StackError) Error() string {
	return se.String()
}

//----------------------------------------
// Panic wrappers
// XXX DEPRECATED

// A panic resulting from a sanity check means there is a programmer error
// and some guarantee is not satisfied.
// XXX DEPRECATED
func PanicSanity(v interface{}) {
	panic(Fmt("Panicked on a Sanity Check: %v", v))
}

// A panic here means something has gone horribly wrong, in the form of data corruption or
// failure of the operating system. In a correct/healthy system, these should never fire.
// If they do, it's indicative of a much more serious problem.
// XXX DEPRECATED
func PanicCrisis(v interface{}) {
	panic(Fmt("Panicked on a Crisis: %v", v))
}

// Indicates a failure of consensus. Someone was malicious or something has
// gone horribly wrong. These should really boot us into an "emergency-recover" mode
// XXX DEPRECATED
func PanicConsensus(v interface{}) {
	panic(Fmt("Panicked on a Consensus Failure: %v", v))
}

// For those times when we're not sure if we should panic
// XXX DEPRECATED
func PanicQ(v interface{}) {
	panic(Fmt("Panicked questionably: %v", v))
}