package rule

import (
	"go/ast"

	"github.com/mgechev/revive/lint"
)

// UnreachableCodeRule lints unreachable code.
type UnreachableCodeRule struct{}

// Apply applies the rule to given file.
func (*UnreachableCodeRule) Apply(file *lint.File, _ lint.Arguments) []lint.Failure {
	var failures []lint.Failure
	onFailure := func(failure lint.Failure) {
		failures = append(failures, failure)
	}

	testingFunctions := map[string]bool{
		"Fatal":   true,
		"Fatalf":  true,
		"FailNow": true,
	}
	branchingFunctions := map[string]map[string]bool{
		"os": {"Exit": true},
		"log": {
			"Fatal":   true,
			"Fatalf":  true,
			"Fatalln": true,
			"Panic":   true,
			"Panicf":  true,
			"Panicln": true,
		},
		"t": testingFunctions,
		"b": testingFunctions,
		"f": testingFunctions,
	}

	w := lintUnreachableCode{onFailure, branchingFunctions}
	ast.Walk(w, file.AST)
	return failures
}

// Name returns the rule name.
func (*UnreachableCodeRule) Name() string {
	return "unreachable-code"
}

type lintUnreachableCode struct {
	onFailure          func(lint.Failure)
	branchingFunctions map[string]map[string]bool
}

func (w lintUnreachableCode) Visit(node ast.Node) ast.Visitor {
	blk, ok := node.(*ast.BlockStmt)
	if !ok {
		return w
	}

	if len(blk.List) < 2 {
		return w
	}
loop:
	for i, stmt := range blk.List[:len(blk.List)-1] {
		next := blk.List[i+1]
		if _, ok := next.(*ast.LabeledStmt); ok {
			continue // skip if next statement is labeled
		}

		switch s := stmt.(type) {
		case *ast.ReturnStmt:
			w.onFailure(newUnreachableCodeFailure(s))
			break loop
		case *ast.BranchStmt:
			token := s.Tok.String()
			if token != "fallthrough" {
				w.onFailure(newUnreachableCodeFailure(s))
				break loop
			}
		case *ast.ExprStmt:
			ce, ok := s.X.(*ast.CallExpr)
			if !ok {
				continue
			}
			// it's a function call
			fc, ok := ce.Fun.(*ast.SelectorExpr)
			if !ok {
				continue
			}

			id, ok := fc.X.(*ast.Ident)

			if !ok {
				continue
			}
			fn := fc.Sel.Name
			pkg := id.Name
			if !w.branchingFunctions[pkg][fn] { // it isn't a call to a branching function
				continue
			}

			if _, ok := next.(*ast.ReturnStmt); ok { // return statement needed to satisfy function signature
				continue
			}

			w.onFailure(newUnreachableCodeFailure(s))
			break loop
		}
	}

	return w
}

func newUnreachableCodeFailure(node ast.Node) lint.Failure {
	return lint.Failure{
		Confidence: 1,
		Node:       node,
		Category:   lint.FailureCategoryLogic,
		Failure:    "unreachable code after this statement",
	}
}
