package interp

import (
	"context"
	"go/ast"
	"go/token"
	"io/fs"
	"path/filepath"
	"reflect"
	"runtime"
	"runtime/debug"
)

// A Program is Go code that has been parsed and compiled.
type Program struct {
	pkgName string
	root    *node
	init    []*node
}

// PackageName returns name used in a package clause.
func (p *Program) PackageName() string {
	return p.pkgName
}

// FileSet is the fileset that must be used for parsing Go that will be passed
// to interp.CompileAST().
func (interp *Interpreter) FileSet() *token.FileSet {
	return interp.fset
}

// Compile parses and compiles a Go code represented as a string.
func (interp *Interpreter) Compile(src string) (*Program, error) {
	return interp.compileSrc(src, "", true)
}

// CompilePath parses and compiles a Go code located at the given path.
func (interp *Interpreter) CompilePath(path string) (*Program, error) {
	path = filepath.ToSlash(path) // Ensure path is in Unix format. Since we work with fs.FS, we need to use Unix path.
	if !isFile(interp.filesystem, path) {
		_, err := interp.importSrc(mainID, path, NoTest)
		return nil, err
	}

	b, err := fs.ReadFile(interp.filesystem, path)
	if err != nil {
		return nil, err
	}
	return interp.compileSrc(string(b), path, false)
}

func (interp *Interpreter) compileSrc(src, name string, inc bool) (*Program, error) {
	if name != "" {
		interp.name = name
	}
	if interp.name == "" {
		interp.name = DefaultSourceName
	}

	// Parse source to AST.
	n, err := interp.parse(src, interp.name, inc)
	if err != nil {
		return nil, err
	}

	return interp.CompileAST(n)
}

// CompileAST builds a Program for the given Go code AST. Files and block
// statements can be compiled, as can most expressions. Var declaration nodes
// cannot be compiled.
//
// WARNING: The node must have been parsed using interp.FileSet(). Results are
// unpredictable otherwise.
func (interp *Interpreter) CompileAST(n ast.Node) (*Program, error) {
	// Convert AST.
	pkgName, root, err := interp.ast(n)
	if err != nil || root == nil {
		return nil, err
	}

	if interp.astDot {
		dotCmd := interp.dotCmd
		if dotCmd == "" {
			dotCmd = defaultDotCmd(interp.name, "yaegi-ast-")
		}
		root.astDot(dotWriter(dotCmd), interp.name)
		if interp.noRun {
			return nil, err
		}
	}

	// Perform global types analysis.
	if err = interp.gtaRetry([]*node{root}, pkgName, pkgName); err != nil {
		return nil, err
	}

	// Annotate AST with CFG informations.
	initNodes, err := interp.cfg(root, nil, pkgName, pkgName)
	if err != nil {
		if interp.cfgDot {
			dotCmd := interp.dotCmd
			if dotCmd == "" {
				dotCmd = defaultDotCmd(interp.name, "yaegi-cfg-")
			}
			root.cfgDot(dotWriter(dotCmd))
		}
		return nil, err
	}

	if root.kind != fileStmt {
		// REPL may skip package statement.
		setExec(root.start)
	}
	interp.mutex.Lock()
	gs := interp.scopes[pkgName]
	if interp.universe.sym[pkgName] == nil {
		// Make the package visible under a path identical to its name.
		interp.srcPkg[pkgName] = gs.sym
		interp.universe.sym[pkgName] = &symbol{kind: pkgSym, typ: &itype{cat: srcPkgT, path: pkgName}}
		interp.pkgNames[pkgName] = pkgName
	}
	interp.mutex.Unlock()

	// Add main to list of functions to run, after all inits.
	if m := gs.sym[mainID]; pkgName == mainID && m != nil {
		initNodes = append(initNodes, m.node)
	}

	if interp.cfgDot {
		dotCmd := interp.dotCmd
		if dotCmd == "" {
			dotCmd = defaultDotCmd(interp.name, "yaegi-cfg-")
		}
		root.cfgDot(dotWriter(dotCmd))
	}

	return &Program{pkgName, root, initNodes}, nil
}

// Execute executes compiled Go code.
func (interp *Interpreter) Execute(p *Program) (res reflect.Value, err error) {
	defer func() {
		r := recover()
		if r != nil {
			var pc [64]uintptr // 64 frames should be enough.
			n := runtime.Callers(1, pc[:])
			err = Panic{Value: r, Callers: pc[:n], Stack: debug.Stack()}
		}
	}()

	// Generate node exec closures.
	if err = genRun(p.root); err != nil {
		return res, err
	}

	// Init interpreter execution memory frame.
	interp.frame.setrunid(interp.runid())
	interp.frame.mutex.Lock()
	interp.resizeFrame()
	interp.frame.mutex.Unlock()

	// Execute node closures.
	interp.run(p.root, nil)

	// Wire and execute global vars.
	n, err := genGlobalVars([]*node{p.root}, interp.scopes[p.pkgName])
	if err != nil {
		return res, err
	}
	interp.run(n, nil)

	for _, n := range p.init {
		interp.run(n, interp.frame)
	}
	v := genValue(p.root)
	res = v(interp.frame)

	// If result is an interpreter node, wrap it in a runtime callable function.
	if res.IsValid() {
		if n, ok := res.Interface().(*node); ok {
			res = genFunctionWrapper(n)(interp.frame)
		}
	}

	return res, err
}

// ExecuteWithContext executes compiled Go code.
func (interp *Interpreter) ExecuteWithContext(ctx context.Context, p *Program) (res reflect.Value, err error) {
	interp.mutex.Lock()
	interp.done = make(chan struct{})
	interp.cancelChan = !interp.opt.fastChan
	interp.mutex.Unlock()

	done := make(chan struct{})
	go func() {
		defer close(done)
		res, err = interp.Execute(p)
	}()

	select {
	case <-ctx.Done():
		interp.stop()
		return reflect.Value{}, ctx.Err()
	case <-done:
	}
	return res, err
}
