package assigncheck

import (
	"bytes"
	"go/ast"
	"go/printer"
	"go/token"

	"golang.org/x/tools/go/analysis"
)

var Analyzer = &analysis.Analyzer{
	Name: "assigncheck",
	Doc:  "reports reassignments",
	Run:  run,
}

func run(pass *analysis.Pass) (interface{}, error) {
	for _, file := range pass.Files {
		// function assignments, if that function was just recently
		// declared, should be allowed. Anonymous functions cannot be
		// called recursively if they are not in scope yet. This means
		// that to call an anonymous function, the following pattern
		// is always needed:
		//   var x func(int) string
		//   x = func(int) string { ... x(int) }
		// To ignore that, whenever a "var x func" is encountered, we
		// save that position until the next node.
		var lastFuncDecl token.Pos

// START OMIT
ast.Inspect(file, func(n ast.Node) bool {
	switch as := n.(type) {
	case *ast.ForStmt:
		pass.Reportf(as.Pos(), "internal reassignment (for loop) in %q", renderFor(pass.Fset, as))

	case *ast.RangeStmt:
		pass.Reportf(as.Pos(), "internal reassignment (for loop) in %q", renderRange(pass.Fset, as))

	case *ast.DeclStmt:
		lastFuncDecl = functionPos(as)
		return false // important to return, as we'd reset the position if not

	case *ast.AssignStmt:
		for _, i := range exprReassigned(as, lastFuncDecl) {
			pass.Reportf(as.Pos(), "reassignment of %s", render(pass.Fset, i))
		}

	case *ast.IncDecStmt:
		pass.Reportf(as.Pos(), "inline reassignment of %s", render(pass.Fset, as.X))
	}

	lastFuncDecl = token.NoPos
	return true
})
// END OMIT
	}

	return nil, nil
}

func renderFor(fset *token.FileSet, as *ast.ForStmt) string {
	s := "for "
	if as.Init == nil && as.Cond == nil && as.Post == nil {
		return s + "{ ... }"
	}

	if as.Init == nil && as.Cond != nil && as.Post == nil {
		return s + render(fset, as.Cond) + " { ... }"
	}

	if as.Init != nil {
		s += render(fset, as.Init)
	}
	s += "; "
	if as.Cond != nil {
		s += render(fset, as.Cond)
	}
	s += "; "
	if as.Post != nil {
		s += render(fset, as.Post)
	}

	return s + " { ... }"
}

func renderRange(fset *token.FileSet, as *ast.RangeStmt) string {
	s := "for "
	switch {
	case as.Key == nil && as.Value == nil:
		// nothing
	case as.Value == nil:
		s += render(fset, as.Key) + " := "
	case as.Key == nil:
		s += "_, " + render(fset, as.Value) + " := "
	default:
		s += render(fset, as.Key) + ", " + render(fset, as.Value) + " := "
	}
	s += "range " + render(fset, as.X) + " { ... }"
	return s
}

const blankIdent = "_"

// exprReassigned returns all expressions in an assignment
// that are being reassigned. This is done by checking that the
// assignment of all identifiers is at the position of the first
// identifier. If it is not an identifier, it must be a reassignment.
// There are two exceptions to this rule:
// - Blank identifiers are ignored
// - Functions may be redeclared if the assignment position is the lastFuncPos
func exprReassigned(as *ast.AssignStmt, lastFuncPos token.Pos) (reassigned []ast.Expr) {
	type pos interface {
		Pos() token.Pos
	}

	var expectedAssignPos token.Pos

	for i, expr := range as.Lhs {
		ident, ok := expr.(*ast.Ident)
		if !ok { // if it's not an identifier, it is always reassigned.
			reassigned = append(reassigned, expr)
			continue
		}

		// we expect all assignments to be at the same position
		// as the first identifier.
		if expectedAssignPos == token.NoPos {
			expectedAssignPos = ident.Pos()
		}

		// skip blank identifiers
		if ident.Name == blankIdent {
			continue
		}

		// no object probably means that the variable has been declared
		// in a separate file, making this a reassignment.
		if ident.Obj == nil {
			reassigned = append(reassigned, ident)
			continue
		}

		// make sure the declaration has a Pos func and get it
		declPos := ident.Obj.Decl.(pos).Pos()

		// if we got a function position and the corresponding
		// Rhs expression is a function literal, check that the
		// positions match (=same declaration)
		if lastFuncPos != token.NoPos && len(as.Rhs) > i {
			if _, ok := as.Rhs[i].(*ast.FuncLit); ok {
				// the function is either declared right here or on the last
				// position that we got from the callee.
				if declPos != lastFuncPos && declPos != ident.Pos() {
					reassigned = append(reassigned, ident)
				}
				continue
			}
		}

		if declPos != expectedAssignPos {
			reassigned = append(reassigned, ident)
		}
	}

	return reassigned
}

// functionPos returns the position of the function
// declaration, if the DeclStmt has a function declaration
// at all. If not, token.NoPos is returned.
// At most, one position (the position of the last function
// declaration) is returned
func functionPos(as *ast.DeclStmt) token.Pos {
	decl, ok := as.Decl.(*ast.GenDecl)
	if !ok {
		return token.NoPos
	}

	var pos token.Pos

	// iterate over all variable specs to fetch
	// the last function declaration. Skip all declarations
	// that are not function literals.
	for i := range decl.Specs {
		val, ok := decl.Specs[i].(*ast.ValueSpec)
		if !ok {
			continue
		}

		if val.Values != nil {
			continue
		}

		_, ok = val.Type.(*ast.FuncType)
		if !ok {
			continue
		}

		// there may not be more than one function
		// declaration mapped to a single type,
		// so we just return the first one.
		pos = val.Names[0].Pos()
	}
	return pos
}

// render returns the pretty-print of the given node
func render(fset *token.FileSet, x interface{}) string {
	var buf bytes.Buffer
	if err := printer.Fprint(&buf, fset, x); err != nil {
		panic(err)
	}
	return buf.String()
}
