package s1001

import (
	"fmt"
	"go/ast"
	"go/token"
	"go/types"

	"honnef.co/go/tools/analysis/code"
	"honnef.co/go/tools/analysis/edit"
	"honnef.co/go/tools/analysis/facts/generated"
	"honnef.co/go/tools/analysis/lint"
	"honnef.co/go/tools/analysis/report"
	"honnef.co/go/tools/pattern"

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

var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
	Analyzer: &analysis.Analyzer{
		Name:     "S1001",
		Run:      run,
		Requires: []*analysis.Analyzer{inspect.Analyzer, generated.Analyzer},
	},
	Doc: &lint.RawDocumentation{
		Title: `Replace for loop with call to copy`,
		Text: `
Use \'copy()\' for copying elements from one slice to another. For
arrays of identical size, you can use simple assignment.`,
		Before: `
for i, x := range src {
    dst[i] = x
}`,
		After: `copy(dst, src)`,
		Since: "2017.1",
		// MergeIfAll because the types of src and dst might be different under different build tags.
		// You shouldn't write code like that…
		MergeIf: lint.MergeIfAll,
	},
})

var Analyzer = SCAnalyzer.Analyzer

var (
	checkLoopCopyQ = pattern.MustParse(`
		(Or
			(RangeStmt
				key@(Ident _) value@(Ident _) ":=" src
				[(AssignStmt (IndexExpr dst key) "=" value)])
			(RangeStmt
				key@(Ident _) nil ":=" src
				[(AssignStmt (IndexExpr dst key) "=" (IndexExpr src key))])
			(ForStmt
				(AssignStmt key@(Ident _) ":=" (IntegerLiteral "0"))
				(BinaryExpr key "<" (CallExpr (Symbol "len") [src]))
				(IncDecStmt key "++")
				[(AssignStmt (IndexExpr dst key) "=" (IndexExpr src key))]))`)
)

func run(pass *analysis.Pass) (interface{}, error) {
	// TODO revisit once range doesn't require a structural type

	isInvariant := func(k, v types.Object, node ast.Expr) bool {
		if code.MayHaveSideEffects(pass, node, nil) {
			return false
		}
		invariant := true
		ast.Inspect(node, func(node ast.Node) bool {
			if node, ok := node.(*ast.Ident); ok {
				obj := pass.TypesInfo.ObjectOf(node)
				if obj == k || obj == v {
					// don't allow loop bodies like 'a[i][i] = v'
					invariant = false
					return false
				}
			}
			return true
		})
		return invariant
	}

	var elType func(T types.Type) (el types.Type, isArray bool, isArrayPointer bool, ok bool)
	elType = func(T types.Type) (el types.Type, isArray bool, isArrayPointer bool, ok bool) {
		switch typ := T.Underlying().(type) {
		case *types.Slice:
			return typ.Elem(), false, false, true
		case *types.Array:
			return typ.Elem(), true, false, true
		case *types.Pointer:
			el, isArray, _, ok = elType(typ.Elem())
			return el, isArray, true, ok
		default:
			return nil, false, false, false
		}
	}

	fn := func(node ast.Node) {
		m, ok := code.Match(pass, checkLoopCopyQ, node)
		if !ok {
			return
		}

		src := m.State["src"].(ast.Expr)
		dst := m.State["dst"].(ast.Expr)

		k := pass.TypesInfo.ObjectOf(m.State["key"].(*ast.Ident))
		var v types.Object
		if value, ok := m.State["value"]; ok {
			v = pass.TypesInfo.ObjectOf(value.(*ast.Ident))
		}
		if !isInvariant(k, v, dst) {
			return
		}
		if !isInvariant(k, v, src) {
			// For example: 'for i := range foo()'
			return
		}

		Tsrc := pass.TypesInfo.TypeOf(src)
		Tdst := pass.TypesInfo.TypeOf(dst)
		TsrcElem, TsrcArray, TsrcPointer, ok := elType(Tsrc)
		if !ok {
			return
		}
		if TsrcPointer {
			Tsrc = Tsrc.Underlying().(*types.Pointer).Elem()
		}
		TdstElem, TdstArray, TdstPointer, ok := elType(Tdst)
		if !ok {
			return
		}
		if TdstPointer {
			Tdst = Tdst.Underlying().(*types.Pointer).Elem()
		}

		if !types.Identical(TsrcElem, TdstElem) {
			return
		}

		if TsrcArray && TdstArray && types.Identical(Tsrc, Tdst) {
			if TsrcPointer {
				src = &ast.StarExpr{
					X: src,
				}
			}
			if TdstPointer {
				dst = &ast.StarExpr{
					X: dst,
				}
			}
			r := &ast.AssignStmt{
				Lhs: []ast.Expr{dst},
				Rhs: []ast.Expr{src},
				Tok: token.ASSIGN,
			}

			report.Report(pass, node, "should copy arrays using assignment instead of using a loop",
				report.FilterGenerated(),
				report.ShortRange(),
				report.Fixes(edit.Fix("replace loop with assignment", edit.ReplaceWithNode(pass.Fset, node, r))))
		} else {
			tv, err := types.Eval(pass.Fset, pass.Pkg, node.Pos(), "copy")
			if err == nil && tv.IsBuiltin() {
				to := "to"
				from := "from"
				src := m.State["src"].(ast.Expr)
				if TsrcArray {
					from = "from[:]"
					src = &ast.SliceExpr{
						X: src,
					}
				}
				dst := m.State["dst"].(ast.Expr)
				if TdstArray {
					to = "to[:]"
					dst = &ast.SliceExpr{
						X: dst,
					}
				}

				r := &ast.CallExpr{
					Fun:  &ast.Ident{Name: "copy"},
					Args: []ast.Expr{dst, src},
				}
				opts := []report.Option{
					report.ShortRange(),
					report.FilterGenerated(),
					report.Fixes(edit.Fix("replace loop with call to copy()", edit.ReplaceWithNode(pass.Fset, node, r))),
				}
				report.Report(pass, node, fmt.Sprintf("should use copy(%s, %s) instead of a loop", to, from), opts...)
			}
		}
	}
	code.Preorder(pass, fn, (*ast.ForStmt)(nil), (*ast.RangeStmt)(nil))
	return nil, nil
}
