// Copyright 2016 Google Inc. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.

package main

import (
	"go/ast"
	"path"
	"strconv"
	"strings"
)

const (
	ctxPackage = "golang.org/x/net/context"

	newPackageBase = "google.golang.org/"
	stutterPackage = false
)

func init() {
	register(fix{
		"ae",
		"2016-04-15",
		aeFn,
		`Update old App Engine APIs to new App Engine APIs`,
	})
}

// logMethod is the set of methods on appengine.Context used for logging.
var logMethod = map[string]bool{
	"Debugf":    true,
	"Infof":     true,
	"Warningf":  true,
	"Errorf":    true,
	"Criticalf": true,
}

// mapPackage turns "appengine" into "google.golang.org/appengine", etc.
func mapPackage(s string) string {
	if stutterPackage {
		s += "/" + path.Base(s)
	}
	return newPackageBase + s
}

func aeFn(f *ast.File) bool {
	// During the walk, we track the last thing seen that looks like
	// an appengine.Context, and reset it once the walk leaves a func.
	var lastContext *ast.Ident

	fixed := false

	// Update imports.
	mainImp := "appengine"
	for _, imp := range f.Imports {
		pth, _ := strconv.Unquote(imp.Path.Value)
		if pth == "appengine" || strings.HasPrefix(pth, "appengine/") {
			newPth := mapPackage(pth)
			imp.Path.Value = strconv.Quote(newPth)
			fixed = true

			if pth == "appengine" {
				mainImp = newPth
			}
		}
	}

	// Update any API changes.
	walk(f, func(n interface{}) {
		if ft, ok := n.(*ast.FuncType); ok && ft.Params != nil {
			// See if this func has an `appengine.Context arg`.
			// If so, remember its identifier.
			for _, param := range ft.Params.List {
				if !isPkgDot(param.Type, "appengine", "Context") {
					continue
				}
				if len(param.Names) == 1 {
					lastContext = param.Names[0]
					break
				}
			}
			return
		}

		if as, ok := n.(*ast.AssignStmt); ok {
			if len(as.Lhs) == 1 && len(as.Rhs) == 1 {
				// If this node is an assignment from an appengine.NewContext invocation,
				// remember the identifier on the LHS.
				if isCall(as.Rhs[0], "appengine", "NewContext") {
					if ident, ok := as.Lhs[0].(*ast.Ident); ok {
						lastContext = ident
						return
					}
				}
				// x (=|:=) appengine.Timeout(y, z)
				//   should become
				// x, _ (=|:=) context.WithTimeout(y, z)
				if isCall(as.Rhs[0], "appengine", "Timeout") {
					addImport(f, ctxPackage)
					as.Lhs = append(as.Lhs, ast.NewIdent("_"))
					// isCall already did the type checking.
					sel := as.Rhs[0].(*ast.CallExpr).Fun.(*ast.SelectorExpr)
					sel.X = ast.NewIdent("context")
					sel.Sel = ast.NewIdent("WithTimeout")
					fixed = true
					return
				}
			}
			return
		}

		// If this node is a FuncDecl, we've finished the function, so reset lastContext.
		if _, ok := n.(*ast.FuncDecl); ok {
			lastContext = nil
			return
		}

		if call, ok := n.(*ast.CallExpr); ok {
			if isPkgDot(call.Fun, "appengine", "Datacenter") && len(call.Args) == 0 {
				insertContext(f, call, lastContext)
				fixed = true
				return
			}
			if isPkgDot(call.Fun, "taskqueue", "QueueStats") && len(call.Args) == 3 {
				call.Args = call.Args[:2] // drop last arg
				fixed = true
				return
			}

			sel, ok := call.Fun.(*ast.SelectorExpr)
			if !ok {
				return
			}
			if lastContext != nil && refersTo(sel.X, lastContext) && logMethod[sel.Sel.Name] {
				// c.Errorf(...)
				//   should become
				// log.Errorf(c, ...)
				addImport(f, mapPackage("appengine/log"))
				sel.X = &ast.Ident{ // ast.NewIdent doesn't preserve the position.
					NamePos: sel.X.Pos(),
					Name:    "log",
				}
				insertContext(f, call, lastContext)
				fixed = true
				return
			}
		}
	})

	// Change any `appengine.Context` to `context.Context`.
	// Do this in a separate walk because the previous walk
	// wants to identify "appengine.Context".
	walk(f, func(n interface{}) {
		expr, ok := n.(ast.Expr)
		if ok && isPkgDot(expr, "appengine", "Context") {
			addImport(f, ctxPackage)
			// isPkgDot did the type checking.
			n.(*ast.SelectorExpr).X.(*ast.Ident).Name = "context"
			fixed = true
			return
		}
	})

	// The changes above might remove the need to import "appengine".
	// Check if it's used, and drop it if it isn't.
	if fixed && !usesImport(f, mainImp) {
		deleteImport(f, mainImp)
	}

	return fixed
}

// ctx may be nil.
func insertContext(f *ast.File, call *ast.CallExpr, ctx *ast.Ident) {
	if ctx == nil {
		// context is unknown, so use a plain "ctx".
		ctx = ast.NewIdent("ctx")
	} else {
		// Create a fresh *ast.Ident so we drop the position information.
		ctx = ast.NewIdent(ctx.Name)
	}

	call.Args = append([]ast.Expr{ctx}, call.Args...)
}