// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build !disable_events

package event

import (
	"reflect"
	"runtime"
	"sort"
	"strings"
)

const (
	// this is the maximum amount of helpers we scan past to find a non helper
	helperDepthLimit = 5
)

type sources struct {
	entries []caller
}

type Source struct {
	Space string
	Owner string
	Name  string
}

type caller struct {
	helper bool
	pc     uintptr
	source Source
}

var globalCallers chan *sources

// RegisterHelper records a function as being an event helper that should not
// be used when capturing the source information on events.
// v should be either a string or a function pointer.
// If v is a string it is of the form
//
//	Space.Owner.Name
//
// where Owner and Name cannot contain '/' and Name also cannot contain '.'
func RegisterHelper(v interface{}) {
	g := <-globalCallers
	defer func() { globalCallers <- g }()
	switch v := v.(type) {
	case string:
		g.entries = append(g.entries, caller{source: splitName(v), helper: true})
	default:
		g.helperFunction(v)
	}
}

func init() {
	g := &sources{}
	// make all entries in the event package helpers
	globalCallers = make(chan *sources, 1)
	globalCallers <- g
	RegisterHelper("golang.org/x/exp/event")
}

func newCallers() sources {
	g := <-globalCallers
	defer func() { globalCallers <- g }()
	c := sources{}
	c.entries = make([]caller, len(g.entries))
	copy(c.entries, g.entries)
	return c
}

func (c *sources) addCaller(entry caller) {
	i := sort.Search(len(c.entries), func(i int) bool {
		return c.entries[i].pc >= entry.pc
	})
	if i >= len(c.entries) {
		// add to end
		c.entries = append(c.entries, entry)
		return
	}
	if c.entries[i].pc == entry.pc {
		// already present
		return
	}
	//expand the array
	c.entries = append(c.entries, caller{})
	//make a space
	copy(c.entries[i+1:], c.entries[i:])
	// insert the entry
	c.entries[i] = entry
}

func (c *sources) getCaller(pc uintptr) (caller, bool) {
	i := sort.Search(len(c.entries), func(i int) bool {
		return c.entries[i].pc >= pc
	})
	if i == len(c.entries) || c.entries[i].pc != pc {
		return caller{}, false
	}
	return c.entries[i], true
}

func scanStack() Source {
	g := <-globalCallers
	defer func() { globalCallers <- g }()
	return g.scanStack()
}

func (c *sources) scanStack() Source {
	// first capture the caller stack
	var stack [helperDepthLimit]uintptr
	// we can skip the first three entries
	//   runtime.Callers
	//   event.(*sources).scanStack (this function)
	//   another function in this package (because scanStack is private)
	depth := runtime.Callers(3, stack[:]) // start at 2 to skip Callers and this function
	// do a cheap first pass to see if we have an entry for this stack
	for i := 0; i < depth; i++ {
		pc := stack[i]
		e, found := c.getCaller(pc)
		if found {
			if !e.helper {
				// exact non helper match found, return it
				return e.source
			}
			// helper found, keep scanning
			continue
		}
		// stack entry not found, we need to fill one in
		f := runtime.FuncForPC(stack[i])
		if f == nil {
			// symtab lookup failed, pretend it does not exist
			continue
		}
		e = caller{
			source: splitName(f.Name()),
			pc:     pc,
		}
		e.helper = c.isHelper(e)
		c.addCaller(e)
		if !e.helper {
			// found a non helper entry, add it and return it
			return e.source
		}
	}
	// ran out of stack, was all helpers
	return Source{}
}

// we do helper matching by name, if the pc matched we would have already found
// that, but helper registration does not know the call stack pcs
func (c *sources) isHelper(entry caller) bool {
	// scan to see if it matches any of the helpers
	// we match by name in case of inlining
	for _, e := range c.entries {
		if !e.helper {
			// ignore all the non helper entries
			continue
		}
		if isMatch(entry.source.Space, e.source.Space) &&
			isMatch(entry.source.Owner, e.source.Owner) &&
			isMatch(entry.source.Name, e.source.Name) {
			return true
		}
	}
	return false
}

func isMatch(value, against string) bool {
	return len(against) == 0 || value == against
}

func (c *sources) helperFunction(v interface{}) {
	r := reflect.ValueOf(v)
	pc := r.Pointer()
	f := runtime.FuncForPC(pc)
	entry := caller{
		source: splitName(f.Name()),
		pc:     f.Entry(),
		helper: true,
	}
	c.addCaller(entry)
	if entry.pc != pc {
		entry.pc = pc
		c.addCaller(entry)
	}
}

func splitName(full string) Source {
	// Function is the fully-qualified function name. The name itself may
	// have dots (for a closure, for instance), but it can't have slashes.
	// So the package path ends at the first dot after the last slash.
	entry := Source{Space: full}
	slash := strings.LastIndexByte(full, '/')
	if slash < 0 {
		slash = 0
	}
	if dot := strings.IndexByte(full[slash:], '.'); dot >= 0 {
		entry.Space = full[:slash+dot]
		entry.Name = full[slash+dot+1:]
		if dot = strings.LastIndexByte(entry.Name, '.'); dot >= 0 {
			entry.Owner = entry.Name[:dot]
			entry.Name = entry.Name[dot+1:]
		}
	}
	return entry
}
