package evalop

import (
	"bytes"
	"errors"
	"fmt"
	"go/ast"
	"go/constant"
	"go/parser"
	"go/printer"
	"go/scanner"
	"go/token"
	"strconv"
	"strings"

	"github.com/go-delve/delve/pkg/dwarf/godwarf"
	"github.com/go-delve/delve/pkg/dwarf/reader"
)

var (
	ErrFuncCallNotAllowed = errors.New("function calls not allowed without using 'call'")
)

type compileCtx struct {
	evalLookup
	ops        []Op
	allowCalls bool
	curCall    int
}

type evalLookup interface {
	FindTypeExpr(ast.Expr) (godwarf.Type, error)
	HasBuiltin(string) bool
}

// CompileAST compiles the expression t into a list of instructions.
func CompileAST(lookup evalLookup, t ast.Expr) ([]Op, error) {
	ctx := &compileCtx{evalLookup: lookup, allowCalls: true}
	err := ctx.compileAST(t)
	if err != nil {
		return nil, err
	}

	err = ctx.depthCheck(1)
	if err != nil {
		return ctx.ops, err
	}
	return ctx.ops, nil
}

// Compile compiles the expression expr into a list of instructions.
// If canSet is true expressions like "x = y" are also accepted.
func Compile(lookup evalLookup, expr string, canSet bool) ([]Op, error) {
	t, err := parser.ParseExpr(expr)
	if err != nil {
		if canSet {
			eqOff, isAs := isAssignment(err)
			if isAs {
				return CompileSet(lookup, expr[:eqOff], expr[eqOff+1:])
			}
		}
		return nil, err
	}
	return CompileAST(lookup, t)
}

func isAssignment(err error) (int, bool) {
	el, isScannerErr := err.(scanner.ErrorList)
	if isScannerErr && el[0].Msg == "expected '==', found '='" {
		return el[0].Pos.Offset, true
	}
	return 0, false
}

// CompileSet compiles the expression setting lhexpr to rhexpr into a list of
// instructions.
func CompileSet(lookup evalLookup, lhexpr, rhexpr string) ([]Op, error) {
	lhe, err := parser.ParseExpr(lhexpr)
	if err != nil {
		return nil, err
	}
	rhe, err := parser.ParseExpr(rhexpr)
	if err != nil {
		return nil, err
	}

	ctx := &compileCtx{evalLookup: lookup, allowCalls: true}
	err = ctx.compileAST(rhe)
	if err != nil {
		return nil, err
	}

	if isStringLiteral(rhe) {
		ctx.compileAllocLiteralString()
	}

	err = ctx.compileAST(lhe)
	if err != nil {
		return nil, err
	}

	ctx.pushOp(&SetValue{lhe: lhe, Rhe: rhe})

	err = ctx.depthCheck(0)
	if err != nil {
		return ctx.ops, err
	}
	return ctx.ops, nil
}

func (ctx *compileCtx) compileAllocLiteralString() {
	jmp := &Jump{When: JumpIfAllocStringChecksFail}
	ctx.pushOp(jmp)

	ctx.compileSpecialCall("runtime.mallocgc", []ast.Expr{
		&ast.BasicLit{Kind: token.INT, Value: "0"},
		&ast.Ident{Name: "nil"},
		&ast.Ident{Name: "false"},
	}, []Op{
		&PushLen{},
		&PushNil{},
		&PushConst{constant.MakeBool(false)},
	})

	ctx.pushOp(&ConvertAllocToString{})
	jmp.Target = len(ctx.ops)
}

func (ctx *compileCtx) compileSpecialCall(fnname string, argAst []ast.Expr, args []Op) {
	id := ctx.curCall
	ctx.curCall++
	ctx.pushOp(&CallInjectionStartSpecial{
		id:     id,
		FnName: fnname,
		ArgAst: argAst})
	ctx.pushOp(&CallInjectionSetTarget{id: id})

	for i := range args {
		ctx.pushOp(args[i])
		ctx.pushOp(&CallInjectionCopyArg{id: id, ArgNum: i})
	}

	ctx.pushOp(&CallInjectionComplete{id: id})
}

func (ctx *compileCtx) pushOp(op Op) {
	ctx.ops = append(ctx.ops, op)
}

// depthCheck validates the list of instructions produced by Compile and
// CompileSet by performing a stack depth check.
// It calculates the depth of the stack at every instruction in ctx.ops and
// checks that they have enough arguments to execute. For instructions that
// can be reached through multiple paths (because of a jump) it checks that
// all paths reach the instruction with the same stack depth.
// Finally it checks that the stack depth after all instructions have
// executed is equal to endDepth.
func (ctx *compileCtx) depthCheck(endDepth int) error {
	depth := make([]int, len(ctx.ops)+1) // depth[i] is the depth of the stack before i-th instruction
	for i := range depth {
		depth[i] = -1
	}
	depth[0] = 0

	var err error
	checkAndSet := func(j, d int) { // sets depth[j] to d after checking that we can
		if depth[j] < 0 {
			depth[j] = d
		}
		if d != depth[j] {
			err = fmt.Errorf("internal debugger error: depth check error at instruction %d: expected depth %d have %d (jump target)\n%s", j, d, depth[j], Listing(depth, ctx.ops))
		}
	}

	for i, op := range ctx.ops {
		npop, npush := op.depthCheck()
		if depth[i] < npop {
			return fmt.Errorf("internal debugger error: depth check error at instruction %d: expected at least %d have %d\n%s", i, npop, depth[i], Listing(depth, ctx.ops))
		}
		d := depth[i] - npop + npush
		checkAndSet(i+1, d)
		if jmp, _ := op.(*Jump); jmp != nil {
			checkAndSet(jmp.Target, d)
		}
		if err != nil {
			return err
		}
	}

	if depth[len(ctx.ops)] != endDepth {
		return fmt.Errorf("internal debugger error: depth check failed: depth at the end is not %d (got %d)\n%s", depth[len(ctx.ops)], endDepth, Listing(depth, ctx.ops))
	}
	return nil
}

func (ctx *compileCtx) compileAST(t ast.Expr) error {
	switch node := t.(type) {
	case *ast.CallExpr:
		return ctx.compileTypeCastOrFuncCall(node)

	case *ast.Ident:
		return ctx.compileIdent(node)

	case *ast.ParenExpr:
		// otherwise just eval recursively
		return ctx.compileAST(node.X)

	case *ast.SelectorExpr: // <expression>.<identifier>
		switch x := node.X.(type) {
		case *ast.Ident:
			switch {
			case x.Name == "runtime" && node.Sel.Name == "curg":
				ctx.pushOp(&PushCurg{})

			case x.Name == "runtime" && node.Sel.Name == "frameoff":
				ctx.pushOp(&PushFrameoff{})

			case x.Name == "runtime" && node.Sel.Name == "threadid":
				ctx.pushOp(&PushThreadID{})

			case x.Name == "runtime" && node.Sel.Name == "rangeParentOffset":
				ctx.pushOp(&PushRangeParentOffset{})

			default:
				ctx.pushOp(&PushPackageVarOrSelect{Name: x.Name, Sel: node.Sel.Name})
			}

		case *ast.CallExpr:
			ident, ok := x.Fun.(*ast.SelectorExpr)
			if ok {
				f, ok := ident.X.(*ast.Ident)
				if ok && f.Name == "runtime" && ident.Sel.Name == "frame" {
					switch arg := x.Args[0].(type) {
					case *ast.BasicLit:
						fr, err := strconv.ParseInt(arg.Value, 10, 8)
						if err != nil {
							return err
						}
						// Push local onto the stack to be evaluated in the new frame context.
						ctx.pushOp(&PushLocal{Name: node.Sel.Name, Frame: fr})
						return nil
					default:
						return fmt.Errorf("expected integer value for frame, got %v", arg)
					}
				}
			}
			return ctx.compileUnary(node.X, &Select{node.Sel.Name})

		case *ast.BasicLit: // try to accept "package/path".varname syntax for package variables
			s, err := strconv.Unquote(x.Value)
			if err != nil {
				return err
			}
			ctx.pushOp(&PushPackageVarOrSelect{Name: s, Sel: node.Sel.Name, NameIsString: true})

		default:
			return ctx.compileUnary(node.X, &Select{node.Sel.Name})
		}

	case *ast.TypeAssertExpr: // <expression>.(<type>)
		return ctx.compileTypeAssert(node)

	case *ast.IndexExpr:
		return ctx.compileBinary(node.X, node.Index, nil, &Index{node})

	case *ast.SliceExpr:
		if node.Slice3 {
			return errors.New("3-index slice expressions not supported")
		}
		return ctx.compileReslice(node)

	case *ast.StarExpr:
		// pointer dereferencing *<expression>
		return ctx.compileUnary(node.X, &PointerDeref{node})

	case *ast.UnaryExpr:
		// The unary operators we support are +, - and & (note that unary * is parsed as ast.StarExpr)
		switch node.Op {
		case token.AND:
			return ctx.compileUnary(node.X, &AddrOf{node})
		default:
			return ctx.compileUnary(node.X, &Unary{node})
		}

	case *ast.BinaryExpr:
		switch node.Op {
		case token.INC, token.DEC, token.ARROW:
			return fmt.Errorf("operator %s not supported", node.Op.String())
		}
		// short circuits logical operators
		var sop *Jump
		switch node.Op {
		case token.LAND:
			sop = &Jump{When: JumpIfFalse, Node: node.X}
		case token.LOR:
			sop = &Jump{When: JumpIfTrue, Node: node.X}
		}
		err := ctx.compileBinary(node.X, node.Y, sop, &Binary{node})
		if err != nil {
			return err
		}
		if sop != nil {
			sop.Target = len(ctx.ops)
			ctx.pushOp(&BoolToConst{})
		}

	case *ast.BasicLit:
		ctx.pushOp(&PushConst{constant.MakeFromLiteral(node.Value, node.Kind, 0)})

	default:
		return fmt.Errorf("expression %T not implemented", t)
	}
	return nil
}

func (ctx *compileCtx) compileTypeCastOrFuncCall(node *ast.CallExpr) error {
	if len(node.Args) != 1 {
		// Things that have more or less than one argument are always function calls.
		return ctx.compileFunctionCall(node)
	}

	ambiguous := func() error {
		// Ambiguous, could be a function call or a type cast, if node.Fun can be
		// evaluated then try to treat it as a function call, otherwise try the
		// type cast.
		ctx2 := &compileCtx{evalLookup: ctx.evalLookup}
		err0 := ctx2.compileAST(node.Fun)
		if err0 == nil {
			return ctx.compileFunctionCall(node)
		}
		return ctx.compileTypeCast(node, err0)
	}

	fnnode := node.Fun
	for {
		fnnode = removeParen(fnnode)
		n, _ := fnnode.(*ast.StarExpr)
		if n == nil {
			break
		}
		fnnode = n.X
	}

	switch n := fnnode.(type) {
	case *ast.BasicLit:
		// It can only be a ("type string")(x) type cast
		return ctx.compileTypeCast(node, nil)
	case *ast.ArrayType, *ast.StructType, *ast.FuncType, *ast.InterfaceType, *ast.MapType, *ast.ChanType:
		return ctx.compileTypeCast(node, nil)
	case *ast.SelectorExpr:
		if _, isident := n.X.(*ast.Ident); isident {
			if typ, _ := ctx.FindTypeExpr(n); typ != nil {
				return ctx.compileTypeCast(node, nil)
			}
			return ambiguous()
		}
		return ctx.compileFunctionCall(node)
	case *ast.Ident:
		if typ, _ := ctx.FindTypeExpr(n); typ != nil {
			return ctx.compileTypeCast(node, fmt.Errorf("could not find symbol value for %s", n.Name))
		}
		return ctx.compileFunctionCall(node)
	case *ast.IndexExpr:
		// Ambiguous, could be a parametric type
		switch n.X.(type) {
		case *ast.Ident, *ast.SelectorExpr:
			// Do the type-cast first since evaluating node.Fun could be expensive.
			err := ctx.compileTypeCast(node, nil)
			if err == nil || err != reader.ErrTypeNotFound {
				return err
			}
			return ctx.compileFunctionCall(node)
		default:
			return ctx.compileFunctionCall(node)
		}
	case *ast.IndexListExpr:
		return ctx.compileTypeCast(node, nil)
	default:
		// All other expressions must be function calls
		return ctx.compileFunctionCall(node)
	}
}

func (ctx *compileCtx) compileTypeCast(node *ast.CallExpr, ambiguousErr error) error {
	err := ctx.compileAST(node.Args[0])
	if err != nil {
		return err
	}

	fnnode := node.Fun

	// remove all enclosing parenthesis from the type name
	fnnode = removeParen(fnnode)

	targetTypeStr := exprToString(removeParen(node.Fun))
	styp, err := ctx.FindTypeExpr(fnnode)
	if err != nil {
		switch targetTypeStr {
		case "[]byte", "[]uint8":
			styp = godwarf.FakeSliceType(godwarf.FakeBasicType("uint", 8))
		case "[]int32", "[]rune":
			styp = godwarf.FakeSliceType(godwarf.FakeBasicType("int", 32))
		default:
			if ambiguousErr != nil && err == reader.ErrTypeNotFound {
				return fmt.Errorf("could not evaluate function or type %s: %v", exprToString(node.Fun), ambiguousErr)
			}
			return err
		}
	}

	ctx.pushOp(&TypeCast{DwarfType: styp, Node: node})
	return nil
}

func (ctx *compileCtx) compileBuiltinCall(builtin string, args []ast.Expr) error {
	for _, arg := range args {
		err := ctx.compileAST(arg)
		if err != nil {
			return err
		}
	}
	ctx.pushOp(&BuiltinCall{builtin, args})
	return nil
}

func (ctx *compileCtx) compileIdent(node *ast.Ident) error {
	ctx.pushOp(&PushIdent{node.Name})
	return nil
}

func (ctx *compileCtx) compileUnary(expr ast.Expr, op Op) error {
	err := ctx.compileAST(expr)
	if err != nil {
		return err
	}
	ctx.pushOp(op)
	return nil
}

func (ctx *compileCtx) compileTypeAssert(node *ast.TypeAssertExpr) error {
	err := ctx.compileAST(node.X)
	if err != nil {
		return err
	}
	// Accept .(data) as a type assertion that always succeeds, so that users
	// can access the data field of an interface without actually having to
	// type the concrete type.
	if idtyp, isident := node.Type.(*ast.Ident); !isident || idtyp.Name != "data" {
		typ, err := ctx.FindTypeExpr(node.Type)
		if err != nil {
			return err
		}
		ctx.pushOp(&TypeAssert{typ, node})
		return nil
	}
	ctx.pushOp(&TypeAssert{nil, node})
	return nil
}

func (ctx *compileCtx) compileBinary(a, b ast.Expr, sop *Jump, op Op) error {
	err := ctx.compileAST(a)
	if err != nil {
		return err
	}
	if sop != nil {
		ctx.pushOp(sop)
	}
	err = ctx.compileAST(b)
	if err != nil {
		return err
	}
	ctx.pushOp(op)
	return nil
}

func (ctx *compileCtx) compileReslice(node *ast.SliceExpr) error {
	err := ctx.compileAST(node.X)
	if err != nil {
		return err
	}

	trustLen := true
	hasHigh := false
	if node.High != nil {
		hasHigh = true
		err = ctx.compileAST(node.High)
		if err != nil {
			return err
		}
		_, isbasiclit := node.High.(*ast.BasicLit)
		trustLen = trustLen && isbasiclit
	} else {
		trustLen = false
	}

	if node.Low != nil {
		err = ctx.compileAST(node.Low)
		if err != nil {
			return err
		}
		_, isbasiclit := node.Low.(*ast.BasicLit)
		trustLen = trustLen && isbasiclit
	} else {
		ctx.pushOp(&PushConst{constant.MakeInt64(0)})
	}

	ctx.pushOp(&Reslice{Node: node, HasHigh: hasHigh, TrustLen: trustLen})
	return nil
}

func (ctx *compileCtx) compileFunctionCall(node *ast.CallExpr) error {
	if fnnode, ok := node.Fun.(*ast.Ident); ok {
		if ctx.HasBuiltin(fnnode.Name) {
			return ctx.compileBuiltinCall(fnnode.Name, node.Args)
		}
	}
	if !ctx.allowCalls {
		return ErrFuncCallNotAllowed
	}

	id := ctx.curCall
	ctx.curCall++

	oldAllowCalls := ctx.allowCalls
	oldOps := ctx.ops
	ctx.allowCalls = false
	err := ctx.compileAST(node.Fun)
	ctx.allowCalls = oldAllowCalls
	hasFunc := false
	if err != nil {
		ctx.ops = oldOps
		if err != ErrFuncCallNotAllowed {
			return err
		}
	} else {
		hasFunc = true
	}
	ctx.pushOp(&CallInjectionStart{HasFunc: hasFunc, id: id, Node: node})

	// CallInjectionStart pushes true on the stack if it needs the function argument re-evaluated
	var jmpif *Jump
	if hasFunc {
		jmpif = &Jump{When: JumpIfFalse, Pop: true}
		ctx.pushOp(jmpif)
	}
	ctx.pushOp(&Pop{})
	err = ctx.compileAST(node.Fun)
	if err != nil {
		return err
	}
	if jmpif != nil {
		jmpif.Target = len(ctx.ops)
	}

	ctx.pushOp(&CallInjectionSetTarget{id: id})

	for i, arg := range node.Args {
		err := ctx.compileAST(arg)
		if err != nil {
			return fmt.Errorf("error evaluating %q as argument %d in function %s: %v", exprToString(arg), i+1, exprToString(node.Fun), err)
		}
		if isStringLiteral(arg) {
			ctx.compileAllocLiteralString()
		}
		ctx.pushOp(&CallInjectionCopyArg{id: id, ArgNum: i, ArgExpr: arg})
	}

	ctx.pushOp(&CallInjectionComplete{id: id})

	return nil
}

func Listing(depth []int, ops []Op) string {
	if depth == nil {
		depth = make([]int, len(ops)+1)
	}
	buf := new(strings.Builder)
	for i, op := range ops {
		fmt.Fprintf(buf, " %3d  (%2d->%2d) %#v\n", i, depth[i], depth[i+1], op)
	}
	return buf.String()
}

func isStringLiteral(expr ast.Expr) bool {
	switch expr := expr.(type) {
	case *ast.BasicLit:
		return expr.Kind == token.STRING
	case *ast.BinaryExpr:
		if expr.Op == token.ADD {
			return isStringLiteral(expr.X) && isStringLiteral(expr.Y)
		}
	case *ast.ParenExpr:
		return isStringLiteral(expr.X)
	}
	return false
}

func removeParen(n ast.Expr) ast.Expr {
	for {
		p, ok := n.(*ast.ParenExpr)
		if !ok {
			break
		}
		n = p.X
	}
	return n
}

func exprToString(t ast.Expr) string {
	var buf bytes.Buffer
	printer.Fprint(&buf, token.NewFileSet(), t)
	return buf.String()
}
