package interp

import (
	"context"
	"errors"
	"fmt"
	"go/token"
	"reflect"
	"sort"
	"sync"
)

var (
	// ErrNotLive indicates that the specified ID does not refer to a (live) Go
	// routine.
	ErrNotLive = errors.New("not live")

	// ErrRunning indicates that the specified Go routine is running.
	ErrRunning = errors.New("running")

	// ErrNotRunning indicates that the specified Go routine is running.
	ErrNotRunning = errors.New("not running")
)

var rNodeType = reflect.TypeOf((*node)(nil)).Elem()

// A Debugger can be used to debug a Yaegi program.
type Debugger struct {
	interp  *Interpreter
	events  func(*DebugEvent)
	context context.Context
	cancel  context.CancelFunc

	gWait *sync.WaitGroup
	gLock *sync.Mutex
	gID   int
	gLive map[int]*debugRoutine

	result reflect.Value
	err    error
}

// go routine debug state.
type debugRoutine struct {
	id int

	mode    DebugEventReason
	running bool
	resume  chan struct{}

	fDepth int
	fStep  int
}

// node debug state.
type nodeDebugData struct {
	program     *Program
	breakOnLine bool
	breakOnCall bool
}

// frame debug state.
type frameDebugData struct {
	g     *debugRoutine
	node  *node
	name  string
	kind  frameKind
	scope *scope
}

// frame kind.
type frameKind int

const (
	// interpreter root frame.
	frameRoot frameKind = iota + 1

	// function call frame.
	frameCall

	// closure capture frame.
	frameClosure
)

// DebugOptions are the debugger options.
type DebugOptions struct {
	// If true, Go routine IDs start at 1 instead of 0.
	GoRoutineStartAt1 bool
}

// A DebugEvent is an event generated by a debugger.
type DebugEvent struct {
	debugger *Debugger
	reason   DebugEventReason
	frame    *frame
}

// DebugFrame provides access to stack frame information while debugging a
// program.
type DebugFrame struct {
	event  *DebugEvent
	frames []*frame
}

// DebugFrameScope provides access to scoped variables while debugging a
// program.
type DebugFrameScope struct {
	frame *frame
}

// DebugVariable is the name and value of a variable from a debug session.
type DebugVariable struct {
	Name  string
	Value reflect.Value
}

// DebugGoRoutine provides access to information about a Go routine while
// debugging a program.
type DebugGoRoutine struct {
	id int
}

// Breakpoint is the result of attempting to set a breakpoint.
type Breakpoint struct {
	// Valid indicates whether the breakpoint was successfully set.
	Valid bool

	// Position indicates the source position of the breakpoint.
	Position token.Position
}

// DebugEventReason is the reason a debug event occurred.
type DebugEventReason int

const (
	// continue execution normally.
	debugRun DebugEventReason = iota

	// DebugPause is emitted when a pause request is completed. Can be used with
	// Interrupt to request a pause.
	DebugPause

	// DebugBreak is emitted when a debug target hits a breakpoint.
	DebugBreak

	// DebugEntry is emitted when a debug target starts executing. Can be used
	// with Step to produce a corresponding event when execution starts.
	DebugEntry

	// DebugStepInto is emitted when a stepInto request is completed. Can be
	// used with Step or Interrupt to request a stepInto.
	DebugStepInto

	// DebugStepOver is emitted when a stepOver request is completed. Can be
	// used with Step or Interrupt to request a stepOver.
	DebugStepOver

	// DebugStepOut is emitted when a stepOut request is completed. Can be used
	// with Step or Interrupt to request a stepOut.
	DebugStepOut

	// DebugTerminate is emitted when a debug target terminates. Can be used
	// with Interrupt to attempt to terminate the program.
	DebugTerminate

	// DebugEnterGoRoutine is emitted when a Go routine is entered.
	DebugEnterGoRoutine

	// DebugExitGoRoutine is emitted when a Go routine is exited.
	DebugExitGoRoutine
)

// Debug initializes a debugger for the given program.
//
// The program will not start running until Step or Continue has been called. If
// Step is called with DebugEntry, an entry event will be generated before the
// first statement is executed. Otherwise, the debugger will behave as usual.
func (interp *Interpreter) Debug(ctx context.Context, prog *Program, events func(*DebugEvent), opts *DebugOptions) *Debugger {
	dbg := new(Debugger)
	dbg.interp = interp
	dbg.events = events
	dbg.context, dbg.cancel = context.WithCancel(ctx)
	dbg.gWait = new(sync.WaitGroup)
	dbg.gLock = new(sync.Mutex)
	dbg.gLive = make(map[int]*debugRoutine, 1)

	if opts == nil {
		opts = new(DebugOptions)
	}
	if opts.GoRoutineStartAt1 {
		dbg.gID = 1
	}

	mainG := dbg.enterGoRoutine()
	mainG.mode = DebugEntry

	interp.debugger = dbg
	interp.frame.debug = &frameDebugData{kind: frameRoot, g: mainG}

	prog.root.Walk(func(n *node) bool {
		n.setProgram(prog)
		return true
	}, nil)

	go func() {
		defer func() { interp.debugger = nil }()
		defer events(&DebugEvent{reason: DebugTerminate})
		defer dbg.cancel()

		<-mainG.resume
		dbg.events(&DebugEvent{dbg, DebugEnterGoRoutine, interp.frame})
		dbg.result, dbg.err = interp.ExecuteWithContext(ctx, prog)
		dbg.exitGoRoutine(mainG)
		dbg.events(&DebugEvent{dbg, DebugExitGoRoutine, interp.frame})
		dbg.gWait.Wait()
	}()

	return dbg
}

// Wait blocks until all Go routines launched by the program have terminated.
// Wait returns the results of `(*Interpreter).Execute`.
func (dbg *Debugger) Wait() (reflect.Value, error) {
	<-dbg.context.Done()
	return dbg.result, dbg.err
}

// mark entry into a go routine.
func (dbg *Debugger) enterGoRoutine() *debugRoutine {
	g := new(debugRoutine)
	g.resume = make(chan struct{})

	dbg.gWait.Add(1)

	dbg.gLock.Lock()
	g.id = dbg.gID
	dbg.gID++
	dbg.gLive[g.id] = g
	dbg.gLock.Unlock()

	return g
}

// mark exit from a go routine.
func (dbg *Debugger) exitGoRoutine(g *debugRoutine) {
	dbg.gLock.Lock()
	delete(dbg.gLive, g.id)
	dbg.gLock.Unlock()

	dbg.gWait.Done()
}

// get the state for a given go routine, if it's live.
func (dbg *Debugger) getGoRoutine(id int) (*debugRoutine, bool) {
	dbg.gLock.Lock()
	g, ok := dbg.gLive[id]
	dbg.gLock.Unlock()
	return g, ok
}

// mark entry into a function call.
func (dbg *Debugger) enterCall(nFunc, nCall *node, f *frame) {
	if f.debug != nil {
		f.debug.g.fDepth++
		return
	}

	f.debug = new(frameDebugData)
	f.debug.g = f.anc.debug.g
	f.debug.scope = nFunc.scope

	switch nFunc.kind {
	case funcLit:
		f.debug.kind = frameCall
		if nFunc.frame != nil {
			nFunc.frame.debug.kind = frameClosure
			nFunc.frame.debug.node = nFunc
		}

	case funcDecl:
		f.debug.kind = frameCall
		f.debug.name = nFunc.child[1].ident
	}

	if nCall != nil && nCall.anc.kind == goStmt {
		f.debug.g = dbg.enterGoRoutine()
		dbg.events(&DebugEvent{dbg, DebugEnterGoRoutine, f})
	}

	f.debug.g.fDepth++
}

// mark exit from a function call.
func (dbg *Debugger) exitCall(nFunc, nCall *node, f *frame) {
	_ = nFunc // ignore unused, so exitCall can have the same signature as enterCall

	f.debug.g.fDepth--

	if nCall != nil && nCall.anc.kind == goStmt {
		dbg.exitGoRoutine(f.debug.g)
		dbg.events(&DebugEvent{dbg, DebugExitGoRoutine, f})
	}
}

// called by the interpreter prior to executing the node.
func (dbg *Debugger) exec(n *node, f *frame) (stop bool) {
	f.debug.node = n

	if n != nil && n.pos == token.NoPos {
		return false
	}

	g := f.debug.g
	defer func() { g.running = true }()

	e := &DebugEvent{dbg, g.mode, f}
	switch {
	case g.mode == DebugTerminate:
		dbg.cancel()
		return true

	case n.shouldBreak():
		e.reason = DebugBreak

	case g.mode == debugRun:
		return false

	case g.mode == DebugStepOut:
		if g.fDepth >= g.fStep {
			return false
		}

	case g.mode == DebugStepOver:
		if g.fDepth > g.fStep {
			return false
		}
	}
	dbg.events(e)

	g.running = false
	select {
	case <-g.resume:
		return false
	case <-dbg.context.Done():
		return true
	}
}

// Continue continues execution of the specified Go routine. Continue returns
// ErrNotLive if there is no Go routine with the corresponding ID, or if it is not
// live.
func (dbg *Debugger) Continue(id int) error {
	g, ok := dbg.getGoRoutine(id)
	if !ok {
		return ErrNotLive
	}

	g.mode = debugRun
	g.resume <- struct{}{}
	return nil
}

// update the exec mode of this routine.
func (g *debugRoutine) setMode(reason DebugEventReason) {
	if g.mode == DebugTerminate {
		return
	}

	if g.mode == DebugEntry && reason == DebugEntry {
		return
	}

	switch reason {
	case DebugStepInto, DebugStepOver, DebugStepOut:
		g.mode, g.fStep = reason, g.fDepth
	default:
		g.mode = DebugPause
	}
}

// Step issues a stepInto, stepOver, or stepOut request to a stopped Go routine.
// Step returns ErrRunning if the Go routine is running. Step returns ErrNotLive
// if there is no Go routine with the corresponding ID, or if it is not live.
func (dbg *Debugger) Step(id int, reason DebugEventReason) error {
	g, ok := dbg.getGoRoutine(id)
	if !ok {
		return ErrNotLive
	}

	if g.running {
		return ErrRunning
	}

	g.setMode(reason)
	g.resume <- struct{}{}
	return nil
}

// Interrupt issues a stepInto, stepOver, or stepOut request to a running Go
// routine. Interrupt returns ErrRunning if the Go routine is running. Interrupt
// returns ErrNotLive if there is no Go routine with the corresponding ID, or if
// it is not live.
func (dbg *Debugger) Interrupt(id int, reason DebugEventReason) bool {
	g, ok := dbg.getGoRoutine(id)
	if !ok {
		return false
	}

	g.setMode(reason)
	return true
}

// Terminate attempts to terminate the program.
func (dbg *Debugger) Terminate() {
	dbg.gLock.Lock()
	g := dbg.gLive
	dbg.gLive = nil
	dbg.gLock.Unlock()

	for _, g := range g {
		g.mode = DebugTerminate
		close(g.resume)
	}
}

// BreakpointTarget is the target of a request to set breakpoints.
type BreakpointTarget func(*Debugger, func(*node))

// PathBreakpointTarget is used to set breapoints on compiled code by path. This
// can be used to set breakpoints on code compiled with EvalPath, or source
// packages loaded by Yaegi.
func PathBreakpointTarget(path string) BreakpointTarget {
	return func(dbg *Debugger, cb func(*node)) {
		for _, r := range dbg.interp.roots {
			f := dbg.interp.fset.File(r.pos)
			if f != nil && f.Name() == path {
				cb(r)
				return
			}
		}
	}
}

// ProgramBreakpointTarget is used to set breakpoints on a Program.
func ProgramBreakpointTarget(prog *Program) BreakpointTarget {
	return func(_ *Debugger, cb func(*node)) {
		cb(prog.root)
	}
}

// AllBreakpointTarget is used to set breakpoints on all compiled code. Do not
// use with LineBreakpoint.
func AllBreakpointTarget() BreakpointTarget {
	return func(dbg *Debugger, cb func(*node)) {
		for _, r := range dbg.interp.roots {
			cb(r)
		}
	}
}

type breakpointSetup struct {
	roots []*node
	lines map[int]int
	funcs map[string]int
}

// BreakpointRequest is a request to set a breakpoint.
type BreakpointRequest func(*breakpointSetup, int)

// LineBreakpoint requests a breakpoint on the given line.
func LineBreakpoint(line int) BreakpointRequest {
	return func(b *breakpointSetup, i int) {
		b.lines[line] = i
	}
}

// FunctionBreakpoint requests a breakpoint on the named function.
func FunctionBreakpoint(name string) BreakpointRequest {
	return func(b *breakpointSetup, i int) {
		b.funcs[name] = i
	}
}

// SetBreakpoints sets breakpoints for the given target. The returned array has
// an entry for every request, in order. If a given breakpoint request cannot be
// satisfied, the corresponding entry will be marked invalid. If the target
// cannot be found, all entries will be marked invalid.
func (dbg *Debugger) SetBreakpoints(target BreakpointTarget, requests ...BreakpointRequest) []Breakpoint {
	// start with all breakpoints unverified
	results := make([]Breakpoint, len(requests))

	// prepare all the requests
	setup := new(breakpointSetup)
	target(dbg, func(root *node) {
		setup.roots = append(setup.roots, root)
		setup.lines = make(map[int]int, len(requests))
		setup.funcs = make(map[string]int, len(requests))
		for i, rq := range requests {
			rq(setup, i)
		}
	})

	// find breakpoints
	for _, root := range setup.roots {
		root.Walk(func(n *node) bool {
			// function breakpoints
			if len(setup.funcs) > 0 && n.kind == funcDecl {
				// reset stale breakpoints
				n.start.setBreakOnCall(false)

				if i, ok := setup.funcs[n.child[1].ident]; ok && !results[i].Valid {
					results[i].Valid = true
					results[i].Position = dbg.interp.fset.Position(n.start.pos)
					n.start.setBreakOnCall(true)
					return true
				}
			}

			// line breakpoints
			if len(setup.lines) > 0 && n.pos.IsValid() && n.action != aNop && getExec(n) != nil {
				// reset stale breakpoints
				n.setBreakOnLine(false)

				pos := dbg.interp.fset.Position(n.pos)
				if i, ok := setup.lines[pos.Line]; ok && !results[i].Valid {
					results[i].Valid = true
					results[i].Position = pos
					n.setBreakOnLine(true)
					return true
				}
			}

			return true
		}, nil)
	}

	return results
}

// GoRoutines returns an array of live Go routines.
func (dbg *Debugger) GoRoutines() []*DebugGoRoutine {
	dbg.gLock.Lock()
	r := make([]*DebugGoRoutine, 0, len(dbg.gLive))
	for id := range dbg.gLive {
		r = append(r, &DebugGoRoutine{id})
	}
	dbg.gLock.Unlock()
	sort.Slice(r, func(i, j int) bool { return r[i].id < r[j].id })
	return r
}

// ID returns the ID of the Go routine.
func (r *DebugGoRoutine) ID() int { return r.id }

// Name returns "Goroutine {ID}".
func (r *DebugGoRoutine) Name() string { return fmt.Sprintf("Goroutine %d", r.id) }

// GoRoutine returns the ID of the Go routine that generated the event.
func (evt *DebugEvent) GoRoutine() int {
	if evt.frame.debug == nil {
		return 0
	}
	return evt.frame.debug.g.id
}

// Reason returns the reason for the event.
func (evt *DebugEvent) Reason() DebugEventReason {
	return evt.reason
}

// Walk the stack trace frames. The root frame is included if and only if it is
// the only frame. Closure frames are rolled up into the following call frame.
func (evt *DebugEvent) walkFrames(fn func([]*frame) bool) {
	if evt.frame == evt.frame.root {
		fn([]*frame{evt.frame})
		return
	}

	var g *debugRoutine
	if evt.frame.debug != nil {
		g = evt.frame.debug.g
	}

	var frames []*frame
	for f := evt.frame; f != nil && f != f.root && (f.debug == nil || f.debug.g == g); f = f.anc {
		if f.debug == nil || f.debug.kind != frameCall {
			frames = append(frames, f)
			continue
		}

		if len(frames) > 0 {
			if !fn(frames) {
				return
			}
		}

		frames = frames[:0]
		frames = append(frames, f)
	}

	if len(frames) > 0 {
		fn(frames)
	}
}

// FrameDepth returns the number of call frames in the stack trace.
func (evt *DebugEvent) FrameDepth() int {
	if evt.frame == evt.frame.root {
		return 1
	}

	var n int
	evt.walkFrames(func([]*frame) bool { n++; return true })
	return n
}

// Frames returns the call frames in the range [start, end).
func (evt *DebugEvent) Frames(start, end int) []*DebugFrame {
	count := end - start
	if count < 0 {
		return nil
	}

	frames := []*DebugFrame{}
	evt.walkFrames(func(f []*frame) bool {
		df := &DebugFrame{evt, make([]*frame, len(f))}
		copy(df.frames, f)
		frames = append(frames, df)
		return len(frames) < count
	})
	return frames
}

// Name returns the name of the stack frame. For function calls to named
// functions, this is the function name.
func (f *DebugFrame) Name() string {
	d := f.frames[0].debug
	if d == nil {
		return "<unknown>"
	}
	switch d.kind {
	case frameRoot:
		return "<init>"
	case frameClosure:
		return "<closure>"
	case frameCall:
		if d.name == "" {
			return "<anonymous>"
		}
		return d.name
	default:
		return "<unknown>"
	}
}

// Position returns the current position of the frame. This is effectively the
// program counter/link register. May return `Position{}`.
func (f *DebugFrame) Position() token.Position {
	d := f.frames[0].debug
	if d == nil || d.node == nil {
		return token.Position{}
	}
	return f.event.debugger.interp.fset.Position(d.node.pos)
}

// Program returns the program associated with the current position of the
// frame. May return nil.
func (f *DebugFrame) Program() *Program {
	d := f.frames[0].debug
	if d == nil || d.node == nil {
		return nil
	}

	return d.node.debug.program
}

// Scopes returns the variable scopes of the frame.
func (f *DebugFrame) Scopes() []*DebugFrameScope {
	s := make([]*DebugFrameScope, len(f.frames))
	for i, f := range f.frames {
		s[i] = &DebugFrameScope{f}
	}
	return s
}

// IsClosure returns true if this is the capture scope of a closure.
func (f *DebugFrameScope) IsClosure() bool {
	return f.frame.debug != nil && f.frame.debug.kind == frameClosure
}

// Variables returns the names and values of the variables of the scope.
func (f *DebugFrameScope) Variables() []*DebugVariable {
	d := f.frame.debug
	if d == nil || d.scope == nil {
		return nil
	}

	index := map[int]string{}
	scanScope(d.scope, index)

	m := make([]*DebugVariable, 0, len(f.frame.data))
	for i, v := range f.frame.data {
		if typ := v.Type(); typ.AssignableTo(rNodeType) || typ.Kind() == reflect.Ptr && typ.Elem().AssignableTo(rNodeType) {
			continue
		}
		name, ok := index[i]
		if !ok {
			continue
		}

		m = append(m, &DebugVariable{name, v})
	}
	return m
}

func scanScope(sc *scope, index map[int]string) {
	for name, sym := range sc.sym {
		if _, ok := index[sym.index]; ok {
			continue
		}
		index[sym.index] = name
	}

	for _, ch := range sc.child {
		if ch.def != sc.def {
			continue
		}
		scanScope(ch, index)
	}
}
