package rule

import (
	"fmt"
	"go/ast"
	"strings"

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

// UseFmtPrintRule lints calls to print and println.
type UseFmtPrintRule struct{}

// Apply applies the rule to given file.
func (r *UseFmtPrintRule) Apply(file *lint.File, _ lint.Arguments) []lint.Failure {
	redefinesPrint, redefinesPrintln := r.analyzeRedefinitions(file.AST.Decls)
	// Here we could check if both are redefined and if it's the case return nil
	// but the check being false 99.9999999% of the cases we don't

	var failures []lint.Failure
	onFailure := func(failure lint.Failure) {
		failures = append(failures, failure)
	}

	w := lintUseFmtPrint{onFailure, redefinesPrint, redefinesPrintln}
	ast.Walk(w, file.AST)

	return failures
}

// Name returns the rule name.
func (*UseFmtPrintRule) Name() string {
	return "use-fmt-print"
}

type lintUseFmtPrint struct {
	onFailure        func(lint.Failure)
	redefinesPrint   bool
	redefinesPrintln bool
}

func (w lintUseFmtPrint) Visit(node ast.Node) ast.Visitor {
	ce, ok := node.(*ast.CallExpr)
	if !ok {
		return w // nothing to do, the node is not a call
	}

	id, ok := (ce.Fun).(*ast.Ident)
	if !ok {
		return nil
	}

	name := id.Name
	switch name {
	case "print":
		if w.redefinesPrint {
			return nil // it's a call to user-defined print
		}
	case "println":
		if w.redefinesPrintln {
			return nil // it's a call to user-defined println
		}
	default:
		return nil // nothing to do, the call is not println(...) nor print(...)
	}

	callArgs := w.callArgsAsStr(ce.Args)
	w.onFailure(lint.Failure{
		Confidence: 1,
		Node:       node,
		Category:   lint.FailureCategoryBadPractice,
		Failure:    fmt.Sprintf(`avoid using built-in function %q, replace it by "fmt.F%s(os.Stderr, %s)"`, name, name, callArgs),
	})

	return w
}

func (lintUseFmtPrint) callArgsAsStr(args []ast.Expr) string {
	strs := []string{}
	for _, expr := range args {
		strs = append(strs, astutils.GoFmt(expr))
	}

	return strings.Join(strs, ", ")
}

func (UseFmtPrintRule) analyzeRedefinitions(decls []ast.Decl) (redefinesPrint, redefinesPrintln bool) {
	for _, decl := range decls {
		fnDecl, ok := decl.(*ast.FuncDecl)
		if !ok {
			continue // not a function declaration
		}

		if fnDecl.Recv != nil {
			continue // it´s a method (not function) declaration
		}

		switch fnDecl.Name.Name {
		case "print":
			redefinesPrint = true
		case "println":
			redefinesPrintln = true
		}
	}
	return redefinesPrint, redefinesPrintln
}
