package analyzer

import (
	"go/ast"

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

const (
	name = "nilnil"
	doc  = "Checks that there is no simultaneous return of `nil` error and an invalid value."

	reportMsg = "return both the `nil` error and invalid value: use a sentinel error instead"
)

// New returns new nilnil analyzer.
func New() *analysis.Analyzer {
	n := newNilNil()

	a := &analysis.Analyzer{
		Name:     name,
		Doc:      doc,
		Run:      n.run,
		Requires: []*analysis.Analyzer{inspect.Analyzer},
	}
	a.Flags.Var(&n.checkedTypes, "checked-types", "coma separated list")

	return a
}

type nilNil struct {
	checkedTypes checkedTypes
}

func newNilNil() *nilNil {
	return &nilNil{
		checkedTypes: newDefaultCheckedTypes(),
	}
}

var (
	types = []ast.Node{(*ast.TypeSpec)(nil)}

	funcAndReturns = []ast.Node{
		(*ast.FuncDecl)(nil),
		(*ast.FuncLit)(nil),
		(*ast.ReturnStmt)(nil),
	}
)

type typeSpecByName map[string]*ast.TypeSpec

func (n *nilNil) run(pass *analysis.Pass) (interface{}, error) {
	insp := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)

	typeSpecs := typeSpecByName{}
	insp.Preorder(types, func(node ast.Node) {
		t := node.(*ast.TypeSpec)
		typeSpecs[t.Name.Name] = t
	})

	var fs funcTypeStack
	insp.Nodes(funcAndReturns, func(node ast.Node, push bool) (proceed bool) {
		switch v := node.(type) {
		case *ast.FuncLit:
			if push {
				fs.Push(v.Type)
			} else {
				fs.Pop()
			}

		case *ast.FuncDecl:
			if push {
				fs.Push(v.Type)
			} else {
				fs.Pop()
			}

		case *ast.ReturnStmt:
			ft := fs.Top() // Current function.

			if !push || len(v.Results) != 2 || ft == nil || ft.Results == nil || len(ft.Results.List) != 2 {
				return false
			}

			fRes1, fRes2 := ft.Results.List[0], ft.Results.List[1]
			if !(n.isDangerNilField(fRes1, typeSpecs) && n.isErrorField(fRes2)) {
				return
			}

			rRes1, rRes2 := v.Results[0], v.Results[1]
			if isNil(rRes1) && isNil(rRes2) {
				pass.Reportf(v.Pos(), reportMsg)
			}
		}

		return true
	})

	return nil, nil //nolint:nilnil
}

func (n *nilNil) isDangerNilField(f *ast.Field, typeSpecs typeSpecByName) bool {
	return n.isDangerNilType(f.Type, typeSpecs)
}

func (n *nilNil) isDangerNilType(t ast.Expr, typeSpecs typeSpecByName) bool {
	switch v := t.(type) {
	case *ast.StarExpr:
		return n.checkedTypes.Contains(ptrType)

	case *ast.FuncType:
		return n.checkedTypes.Contains(funcType)

	case *ast.InterfaceType:
		return n.checkedTypes.Contains(ifaceType)

	case *ast.MapType:
		return n.checkedTypes.Contains(mapType)

	case *ast.ChanType:
		return n.checkedTypes.Contains(chanType)

	case *ast.Ident:
		if t, ok := typeSpecs[v.Name]; ok {
			return n.isDangerNilType(t.Type, nil)
		}
	}
	return false
}

func (n *nilNil) isErrorField(f *ast.Field) bool {
	return isIdent(f.Type, "error")
}

func isNil(e ast.Expr) bool {
	return isIdent(e, "nil")
}

func isIdent(n ast.Node, name string) bool {
	i, ok := n.(*ast.Ident)
	if !ok {
		return false
	}
	return i.Name == name
}
