package rule

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

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

// Based on https://github.com/fzipp/gocyclo

// CyclomaticRule lints given else constructs.
type CyclomaticRule struct {
	maxComplexity int

	configureOnce sync.Once
}

const defaultMaxCyclomaticComplexity = 10

func (r *CyclomaticRule) configure(arguments lint.Arguments) {
	if len(arguments) < 1 {
		r.maxComplexity = defaultMaxCyclomaticComplexity
		return
	}

	complexity, ok := arguments[0].(int64) // Alt. non panicking version
	if !ok {
		panic(fmt.Sprintf("invalid argument for cyclomatic complexity; expected int but got %T", arguments[0]))
	}
	r.maxComplexity = int(complexity)
}

// Apply applies the rule to given file.
func (r *CyclomaticRule) Apply(file *lint.File, arguments lint.Arguments) []lint.Failure {
	r.configureOnce.Do(func() { r.configure(arguments) })

	var failures []lint.Failure
	fileAst := file.AST

	walker := lintCyclomatic{
		file:       file,
		complexity: r.maxComplexity,
		onFailure: func(failure lint.Failure) {
			failures = append(failures, failure)
		},
	}

	ast.Walk(walker, fileAst)

	return failures
}

// Name returns the rule name.
func (*CyclomaticRule) Name() string {
	return "cyclomatic"
}

type lintCyclomatic struct {
	file       *lint.File
	complexity int
	onFailure  func(lint.Failure)
}

func (w lintCyclomatic) Visit(_ ast.Node) ast.Visitor {
	f := w.file
	for _, decl := range f.AST.Decls {
		fn, ok := decl.(*ast.FuncDecl)
		if !ok {
			continue
		}

		c := complexity(fn)
		if c > w.complexity {
			w.onFailure(lint.Failure{
				Confidence: 1,
				Category:   "maintenance",
				Failure: fmt.Sprintf("function %s has cyclomatic complexity %d (> max enabled %d)",
					funcName(fn), c, w.complexity),
				Node: fn,
			})
		}
	}

	return nil
}

// funcName returns the name representation of a function or method:
// "(Type).Name" for methods or simply "Name" for functions.
func funcName(fn *ast.FuncDecl) string {
	declarationHasReceiver := fn.Recv != nil && fn.Recv.NumFields() > 0
	if declarationHasReceiver {
		typ := fn.Recv.List[0].Type
		return fmt.Sprintf("(%s).%s", recvString(typ), fn.Name)
	}

	return fn.Name.Name
}

// recvString returns a string representation of recv of the
// form "T", "*T", or "BADRECV" (if not a proper receiver type).
func recvString(recv ast.Expr) string {
	switch t := recv.(type) {
	case *ast.Ident:
		return t.Name
	case *ast.StarExpr:
		return "*" + recvString(t.X)
	}
	return "BADRECV"
}

// complexity calculates the cyclomatic complexity of a function.
func complexity(fn *ast.FuncDecl) int {
	v := complexityVisitor{}
	ast.Walk(&v, fn)
	return v.Complexity
}

type complexityVisitor struct {
	// Complexity is the cyclomatic complexity
	Complexity int
}

// Visit implements the ast.Visitor interface.
func (v *complexityVisitor) Visit(n ast.Node) ast.Visitor {
	switch n := n.(type) {
	case *ast.FuncDecl, *ast.IfStmt, *ast.ForStmt, *ast.RangeStmt, *ast.CaseClause, *ast.CommClause:
		v.Complexity++
	case *ast.BinaryExpr:
		if n.Op == token.LAND || n.Op == token.LOR {
			v.Complexity++
		}
	}
	return v
}
