package xray

import (
	"fmt"
	"runtime"
	"strings"

	"github.com/pkg/errors"
)

type StackTracer interface {
	StackTrace() []uintptr
}

func Error(message string) *xrayError {
	stack := make([]uintptr, 32)
	n := runtime.Callers(2, stack)
	stack = stack[:n]

	return &xrayError{
		Type:    "Error",
		Message: message,
		Stack:   stack,
	}
}
func Errorf(formatString string, args ...interface{}) *xrayError {
	e := Error(fmt.Sprintf(formatString, args...))
	e.Stack = e.Stack[1:]
	return e
}
func Panic(message string) *xrayError {
	e := Error(message)
	e.Type = "Panic"
	e.Stack = e.Stack[4:]
	return e
}
func Panicf(formatString string, args ...interface{}) *xrayError {
	e := Panic(fmt.Sprintf(formatString, args...))
	e.Stack = e.Stack[1:]
	return e
}

type xrayError struct {
	Type    string
	Message string
	Stack   []uintptr
}

func (e *xrayError) Error() string {
	return e.Message
}
func (e *xrayError) StackTrace() []uintptr {
	return e.Stack
}

var _ = (StackTracer)(&xrayError{})

type exception struct {
	ID      string  `json:"id,omitempty"`
	Type    string  `json:"type,omitempty"`
	Message string  `json:"message,omitempty"`
	Stack   []stack `json:"stack,omitempty"`
}

type stack struct {
	Path  string `json:"path,omitempty"`
	Line  int    `json:"line,omitempty"`
	Label string `json:"label,omitempty"`
}

func exceptionFromError(err error) exception {
	e := exception{
		ID:      newSegmentID(),
		Type:    "Error",
		Message: err.Error(),
	}

	if err, ok := err.(*xrayError); ok {
		e.Type = err.Type
	}

	var s []uintptr

	// This is our publicly supported interface for passing along stack traces
	if err, ok := err.(StackTracer); ok {
		s = err.StackTrace()
	}

	// We also accept github.com/pkg/errors style stack traces for ease of use
	if err, ok := err.(interface {
		StackTrace() errors.StackTrace
	}); ok {
		for _, frame := range err.StackTrace() {
			s = append(s, uintptr(frame))
		}
	}

	if s == nil {
		s = make([]uintptr, 32)
		n := runtime.Callers(4, s)
		s = s[:n]
	}

	e.Stack = convertStack(s)
	return e
}

func convertStack(s []uintptr) []stack {
	var r []stack

	frames := runtime.CallersFrames(s)
	for frame, more := frames.Next(); more; frame, more = frames.Next() {
		f := &stack{}
		f.Path, f.Line, f.Label = parseFrame(frame)
		r = append(r, *f)
	}

	return r
}

func parseFrame(frame runtime.Frame) (string, int, string) {
	path, line, label := frame.File, frame.Line, frame.Function

	// Strip GOPATH from path by counting the number of seperators in label & path
	// For example:
	//   GOPATH = /home/user
	//   path   = /home/user/src/pkg/sub/file.go
	//   label  = pkg/sub.Type.Method
	// We want to set path to:
	//    pkg/sub/file.go
	i := len(path)
	for n, g := 0, strings.Count(label, "/")+2; n < g; n++ {
		i = strings.LastIndex(path[:i], "/")
		if i == -1 {
			// Something went wrong and path has less seperators than we expected
			// Abort and leave i as -1 to counteract the +1 below
			break
		}
	}
	path = path[i+1:] // Trim the initial /

	// Strip the path from the function name as it's already in the path
	label = label[strings.LastIndex(label, "/")+1:]
	// Likewise strip the package name
	label = label[strings.Index(label, ".")+1:]

	return path, line, label
}
