package logex

import (
	"bytes"
	"errors"
	"fmt"
	"path"
	"runtime"
	"strconv"
	"strings"
)

func Define(info string) *traceError {
	return &traceError{
		error: errors.New(info),
	}
}

func NewError(info ...interface{}) *traceError {
	return TraceEx(1, errors.New(sprint(info)))
}

func NewErrorf(format string, info ...interface{}) *traceError {
	return TraceEx(1, errors.New(sprintf(format, info)))
}

func EqualAny(e error, es []error) bool {
	for i := 0; i < len(es); i++ {
		if Equal(e, es[i]) {
			return true
		}
	}
	return false
}

func Equal(e1, e2 error) bool {
	if e, ok := e1.(*traceError); ok {
		e1 = e.error
	}
	if e, ok := e2.(*traceError); ok {
		e2 = e.error
	}
	return e1 == e2
}

type traceError struct {
	error
	format []interface{}
	stack  []string
	code   *int
}

func (t *traceError) SetCode(code int) *traceError {
	if t.stack == nil {
		t = TraceEx(1, t)
	}
	t.code = &code
	return t
}

func (t *traceError) GetCode() int {
	if t.code == nil {
		return 500
	}
	return *t.code
}

func (t *traceError) Error() string {
	if t == nil {
		return "<nil>"
	}
	if t.format == nil {
		if t.error == nil {
			panic(t.stack)
		}
		return t.error.Error()
	}
	return fmt.Sprintf(t.error.Error(), t.format...)
}

func (t *traceError) Trace(info ...interface{}) *traceError {
	return TraceEx(1, t, info...)
}

func (t *traceError) Follow(err error) *traceError {
	if t == nil {
		return nil
	}
	if te, ok := err.(*traceError); ok {
		if len(te.stack) > 0 {
			te.stack[len(te.stack)-1] += ":" + err.Error()
		}
		t.stack = append(te.stack, t.stack...)
	}
	return t
}

func (t *traceError) Format(obj ...interface{}) *traceError {
	if t.stack == nil {
		t = TraceEx(1, t)
	}
	t.format = obj
	return t
}

func (t *traceError) StackError() string {
	if t == nil {
		return t.Error()
	}
	if len(t.stack) == 0 {
		return t.Error()
	}
	return fmt.Sprintf("[%s] %s", strings.Join(t.stack, ";"), t.Error())
}

func Tracefmt(layout string, objs ...interface{}) error {
	var teInfo *traceError
	for idx, obj := range objs {
		if te, ok := obj.(*traceError); ok {
			teInfo = te
			objs[idx] = te.Error()
		}
	}
	return &traceError{
		error:  fmt.Errorf(layout, objs...),
		format: teInfo.format,
		stack:  teInfo.stack,
		code:   teInfo.code,
	}
}

func Tracef(err error, obj ...interface{}) *traceError {
	e := TraceEx(1, err).Format(obj...)
	return e
}

// set runtime info to error
func TraceError(err error, info ...interface{}) *traceError {
	return TraceEx(1, err, info...)
}

func Trace(err error, info ...interface{}) error {
	if err == nil {
		return nil
	}
	return TraceEx(1, err, info...)
}

func joinInterface(info []interface{}, ch string) string {
	ret := bytes.NewBuffer(make([]byte, 0, 512))
	for idx, o := range info {
		if idx > 0 {
			ret.WriteString(ch)
		}
		ret.WriteString(fmt.Sprint(o))
	}
	return ret.String()
}

func TraceEx(depth int, err error, info ...interface{}) *traceError {
	if err == nil {
		return nil
	}
	pc, _, line, _ := runtime.Caller(1 + depth)
	name := runtime.FuncForPC(pc).Name()
	name = path.Base(name)
	stack := name + ":" + strconv.Itoa(line)
	if len(info) > 0 {
		stack += "(" + joinInterface(info, ",") + ")"
	}
	if te, ok := err.(*traceError); ok {
		if te.stack == nil { // define
			return &traceError{
				error: te.error,
				stack: []string{stack},
			}
		}
		te.stack = append(te.stack, stack)
		return te
	}
	return &traceError{err, nil, []string{stack}, nil}
}
