package exceptions

import (
	"fmt"
	"io"
	"os"
	"strings"
)

type Exception interface {
	Cause() Exception
	Error() string
	StackTrace() []StackTrace
	PrintStackTrace()
	PrintStackTraceTo(writer io.Writer)
	BuildPrintStackTrace(builder *strings.Builder)
}

func PrintStackTraceByArray(writer io.Writer, trace []StackTrace) {
	if trace == nil {
		return
	}
	builder := &strings.Builder{}
	for _, stackTrace := range trace {
		stackTrace.WriteTo(builder)
	}
	bytes := []byte(builder.String())
	writeBytes := 0
	for writeBytes < len(bytes) {
		write, err := writer.Write(bytes[writeBytes:])
		if err != nil {
			Print(err)
			return
		}
		writeBytes += write
	}
}

func BuildStackTraceByArray(builder *strings.Builder, trace []StackTrace) {
	if trace == nil {
		return
	}
	for _, stackTrace := range trace {
		stackTrace.WriteTo(builder)
	}
}

func BuildStackTrace(builder *strings.Builder, e Exception, exceptionMsg string) {
	builder.WriteString(fmt.Sprintln(exceptionMsg, e.Error()))
	if e.StackTrace() == nil {
		return
	}
	for _, stackTrace := range e.StackTrace() {
		stackTrace.WriteTo(builder)
	}
}

func Try[R any](
	f func() (ret R, err Exception),
	catch func(panic any) (ret R, err Exception),
) (ret R, err Exception) {
	defer func() {
		if r := recover(); r != nil {
			ret, err = catch(r)
		}
	}()
	ret, err = f()
	if err != nil {
		ret, err = catch(err)
	}
	return
}

func Print(err error) {
	if err == nil {
		return
	}
	switch err.(type) {
	case Exception:
		err.(Exception).PrintStackTrace()
	default:
		_, _ = fmt.Fprintln(os.Stderr, err)
		PrintStackTraceByArray(os.Stderr, GetStackTrace())
	}
}

func Package(err error) Exception {
	if err == nil {
		return nil
	}
	switch err.(type) {
	case Exception:
		return err.(Exception)
	}
	return NewRuntimeException(err, "", DefaultExceptionConfig().SetCause(err))
}

func PackageAny(err any) Exception {
	if err == nil {
		return nil
	}
	switch err.(type) {
	case error:
		return Package(err.(error))
	default:
		return NewRuntimeException(err, "", DefaultExceptionConfig())
	}
}

func PackagePanic(panic any, exceptionMessage string) Exception {
	if panic == nil {
		return nil
	}
	switch panic.(type) {
	case error:
		return NewRuntimeException(
			panic,
			exceptionMessage,
			DefaultExceptionConfig().SetCause(panic),
		)
	default:
		return NewRuntimeException(
			panic,
			exceptionMessage,
			DefaultExceptionConfig(),
		)
	}
}