package eval

import (
	"fmt"
	"io"

	"github.com/elves/elvish/pkg/diag"
	"github.com/elves/elvish/pkg/eval/vars"
	"github.com/elves/elvish/pkg/parse"
	"github.com/elves/elvish/pkg/prog"
)

// compiler maintains the set of states needed when compiling a single source
// file.
type compiler struct {
	// Builtin namespace.
	builtin *staticNs
	// Lexical namespaces.
	scopes []*staticNs
	// Sources of captured variables.
	captures []*staticUpNs
	// Destination of warning messages. This is currently only used for
	// deprecation messages.
	warn io.Writer
	// Deprecation registry.
	deprecations deprecationRegistry
	// Information about the source.
	srcMeta parse.Source
}

type capture struct {
	name string
	// If true, the captured variable is from the immediate outer level scope,
	// i.e. the local scope the lambda is evaluated in. Otherwise the captured
	// variable is from a more outer level, i.e. the upvalue scope the lambda is
	// evaluated in.
	local bool
	// Index to the captured variable.
	index int
}

func compile(b, g *staticNs, tree parse.Tree, w io.Writer) (op nsOp, err error) {
	g = g.clone()
	cp := &compiler{
		b, []*staticNs{g}, []*staticUpNs{new(staticUpNs)},
		w, newDeprecationRegistry(), tree.Source}
	defer func() {
		r := recover()
		if r == nil {
			return
		} else if e := GetCompilationError(r); e != nil {
			// Save the compilation error and stop the panic.
			err = e
		} else {
			// Resume the panic; it is not supposed to be handled here.
			panic(r)
		}
	}()
	chunkOp := cp.chunkOp(tree.Root)
	return nsOp{chunkOp, g}, nil
}

type nsOp struct {
	inner    effectOp
	template *staticNs
}

// Prepares the local namespace, and returns the namespace and a function for
// executing the inner effectOp. Mutates fm.local.
func (op nsOp) prepare(fm *Frame) (*Ns, func() Exception) {
	if len(op.template.names) > len(fm.local.names) {
		n := len(op.template.names)
		newLocal := &Ns{make([]vars.Var, n), op.template.names, op.template.deleted}
		copy(newLocal.slots, fm.local.slots)
		for i := len(fm.local.names); i < n; i++ {
			newLocal.slots[i] = MakeVarFromName(newLocal.names[i])
		}
		fm.local = newLocal
	} else {
		// If no new has been created, there might still be some existing
		// variables deleted.
		fm.local = &Ns{fm.local.slots, fm.local.names, op.template.deleted}
	}
	return fm.local, func() Exception { return op.inner.exec(fm) }
}

const compilationErrorType = "compilation error"

func (cp *compiler) errorpf(r diag.Ranger, format string, args ...interface{}) {
	// The panic is caught by the recover in compile above.
	panic(&diag.Error{
		Type:    compilationErrorType,
		Message: fmt.Sprintf(format, args...),
		Context: *diag.NewContext(cp.srcMeta.Name, cp.srcMeta.Code, r)})
}

// GetCompilationError returns a *diag.Error if the given value is a compilation
// error. Otherwise it returns nil.
func GetCompilationError(e interface{}) *diag.Error {
	if e, ok := e.(*diag.Error); ok && e.Type == compilationErrorType {
		return e
	}
	return nil
}
func (cp *compiler) thisScope() *staticNs {
	return cp.scopes[len(cp.scopes)-1]
}

func (cp *compiler) pushScope() (*staticNs, *staticUpNs) {
	sc := new(staticNs)
	up := new(staticUpNs)
	cp.scopes = append(cp.scopes, sc)
	cp.captures = append(cp.captures, up)
	return sc, up
}

func (cp *compiler) popScope() {
	cp.scopes[len(cp.scopes)-1] = nil
	cp.scopes = cp.scopes[:len(cp.scopes)-1]
	cp.captures[len(cp.captures)-1] = nil
	cp.captures = cp.captures[:len(cp.captures)-1]
}

func (cp *compiler) checkDeprecatedBuiltin(name string, r diag.Ranger) {
	msg := ""
	minLevel := 15
	switch name {
	case "-source~":
		msg = `the "source" command is deprecated; use "eval" instead`
	case "ord~":
		msg = `the "ord" command is deprecated; use "str:to-codepoints" instead`
	case "chr~":
		msg = `the "chr" command is deprecated; use "str:from-codepoints" instead`
	case "has-prefix~":
		msg = `the "has-prefix" command is deprecated; use "str:has-prefix" instead`
	case "has-suffix~":
		msg = `the "has-suffix" command is deprecated; use "str:has-suffix" instead`
	case "esleep~":
		msg = `the "esleep" command is deprecated; use "sleep" instead`
	case "eval-symlinks~":
		msg = `the "eval-symlinks" command is deprecated; use "path:eval-symlinks" instead`
	case "path-abs~":
		msg = `the "path-abs" command is deprecated; use "path:abs" instead`
	case "path-base~":
		msg = `the "path-base" command is deprecated; use "path:base" instead`
	case "path-clean~":
		msg = `the "path-clean" command is deprecated; use "path:clean" instead`
	case "path-dir~":
		msg = `the "path-dir" command is deprecated; use "path:dir" instead`
	case "path-ext~":
		msg = `the "path-ext" command is deprecated; use "path:ext" instead`
	case "-is-dir~":
		msg = `the "-is-dir" command is deprecated; use "path:is-dir" instead`
	default:
		return
	}
	cp.deprecate(r, msg, minLevel)
}

func (cp *compiler) deprecate(r diag.Ranger, msg string, minLevel int) {
	if cp.warn == nil || r == nil {
		return
	}
	dep := deprecation{cp.srcMeta.Name, r.Range(), msg}
	if prog.DeprecationLevel >= minLevel && cp.deprecations.register(dep) {
		err := diag.Error{
			Type: "deprecation", Message: msg,
			Context: diag.Context{
				Name: cp.srcMeta.Name, Source: cp.srcMeta.Code, Ranging: r.Range()}}
		fmt.Fprintln(cp.warn, err.Show(""))
	}
}
