package tangle

import (
	"fmt"
	"strings"
)

// Debugger represents a utility that allows us to print debug messages and function calls.
type Debugger struct {
	aliases map[interface{}]string
	enabled bool
	indent  int
}

// NewDebugger is the constructor of a debugger instance.
func NewDebugger() *Debugger {
	return (&Debugger{}).ResetAliases()
}

// Enable sets the debugger to print the debug information.
func (debugger *Debugger) Enable() {
	debugger.enabled = true

	fmt.Println("[DEBUGGER::ENABLED]")
}

// Disable sets the debugger to not print any debug information.
func (debugger *Debugger) Disable() {
	fmt.Println("[DEBUGGER::DISABLED]")
	debugger.enabled = false
}

// ResetAliases removes any previously registered aliases. This can be useful if the same debugger instance is for
// example used in different tests or test cases.
func (debugger *Debugger) ResetAliases() *Debugger {
	debugger.aliases = make(map[interface{}]string)

	return debugger
}

// RegisterAlias registers a string representation for the given element. This can be used to create a string
// representation for things like ids in the form of byte slices.
func (debugger *Debugger) RegisterAlias(element interface{}, alias string) {
	debugger.aliases[element] = alias
}

// FunctionCall prints debug information about a function call. It automatically indents all following debug outputs
// until Return() is called. The best way to use this is by starting a function call with a construct like:
//
// defer debugger.FunctionCall("myFunction", param1, param2).Return()
func (debugger *Debugger) FunctionCall(identifier string, params ...interface{}) *Debugger {
	if !debugger.enabled {
		return debugger
	}

	debugger.Print(identifier + "(" + debugger.paramsAsCommaSeparatedList(params...) + ") {")
	debugger.indent++

	return debugger
}

// Return prints debug information about a FunctionCall() the was finished. It reduces the indentation for consecutive
// debug outputs.
func (debugger *Debugger) Return() *Debugger {
	if !debugger.enabled {
		return debugger
	}

	debugger.indent--
	debugger.Print("}")

	return debugger
}

// Print prints an arbitrary debug message that can for example be used to print an information when a certain part of
// the code is executed.
func (debugger *Debugger) Print(identifier string, params ...interface{}) {
	if !debugger.enabled {
		return
	}

	if len(params) >= 1 {
		debugger.print(identifier + " = " + debugger.paramsAsCommaSeparatedList(params...))
	} else {
		debugger.print(identifier)
	}
}

// print is an internal utility function that actually prints the given string to stdout.
func (debugger *Debugger) print(stringToPrint string) {
	fmt.Println("[DEBUGGER] " + strings.Repeat("    ", debugger.indent) + stringToPrint)
}

// paramsAsCommaSeparatedList creates a comma separated list of the given parameters.
func (debugger *Debugger) paramsAsCommaSeparatedList(params ...interface{}) string {
	paramsAsStrings := make([]string, len(params))
	for i, param := range params {
		paramsAsStrings[i] = debugger.paramAsString(param)
	}

	return strings.Join(paramsAsStrings, ", ")
}

// paramAsString returns a string representation of an arbitrary parameter.
func (debugger *Debugger) paramAsString(param interface{}) string {
	defer func() { recover() }()
	if alias, aliasExists := debugger.aliases[param]; aliasExists {
		return alias
	}

	return fmt.Sprint(param)
}

// debugger contains the default global debugger instance.
var debugger = NewDebugger()