package analyzer

import (
	"fmt"
	"go/ast"
	"go/token"
	"strconv"
	"strings"
	"unicode"

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

// New returns new errname analyzer.
func New() *analysis.Analyzer {
	return &analysis.Analyzer{
		Name:     "errname",
		Doc:      "Checks that sentinel errors are prefixed with the `Err` and error types are suffixed with the `Error`.",
		Run:      run,
		Requires: []*analysis.Analyzer{inspect.Analyzer},
	}
}

type stringSet = map[string]struct{}

var (
	importNodes = []ast.Node{(*ast.ImportSpec)(nil)}
	typeNodes   = []ast.Node{(*ast.TypeSpec)(nil)}
	funcNodes   = []ast.Node{(*ast.FuncDecl)(nil)}
)

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

	pkgAliases := map[string]string{}
	insp.Preorder(importNodes, func(node ast.Node) {
		i := node.(*ast.ImportSpec)
		if n := i.Name; n != nil && i.Path != nil {
			if path, err := strconv.Unquote(i.Path.Value); err == nil {
				pkgAliases[n.Name] = getPkgFromPath(path)
			}
		}
	})

	allTypes := stringSet{}
	typesSpecs := map[string]*ast.TypeSpec{}
	insp.Preorder(typeNodes, func(node ast.Node) {
		t := node.(*ast.TypeSpec)
		allTypes[t.Name.Name] = struct{}{}
		typesSpecs[t.Name.Name] = t
	})

	errorTypes := stringSet{}
	insp.Preorder(funcNodes, func(node ast.Node) {
		f := node.(*ast.FuncDecl)
		t, ok := isMethodError(f)
		if !ok {
			return
		}
		errorTypes[t] = struct{}{}

		tSpec, ok := typesSpecs[t]
		if !ok {
			panic(fmt.Sprintf("no specification for type %q", t))
		}

		if _, ok := tSpec.Type.(*ast.ArrayType); ok {
			if !isValidErrorArrayTypeName(t) {
				reportAboutErrorType(pass, tSpec.Pos(), t, true)
			}
		} else if !isValidErrorTypeName(t) {
			reportAboutErrorType(pass, tSpec.Pos(), t, false)
		}
	})

	errorFuncs := stringSet{}
	insp.Preorder(funcNodes, func(node ast.Node) {
		f := node.(*ast.FuncDecl)
		if isFuncReturningErr(f.Type, allTypes, errorTypes) {
			errorFuncs[f.Name.Name] = struct{}{}
		}
	})

	inspectPkgLevelVarsOnly := func(node ast.Node) bool {
		switch v := node.(type) {
		case *ast.FuncDecl:
			return false

		case *ast.ValueSpec:
			if name, ok := isSentinelError(v, pkgAliases, allTypes, errorTypes, errorFuncs); ok && !isValidErrorVarName(name) {
				reportAboutErrorVar(pass, v.Pos(), name)
			}
		}
		return true
	}
	for _, f := range pass.Files {
		ast.Inspect(f, inspectPkgLevelVarsOnly)
	}

	return nil, nil //nolint:nilnil
}

func reportAboutErrorType(pass *analysis.Pass, typePos token.Pos, typeName string, isArrayType bool) {
	var form string
	if unicode.IsLower([]rune(typeName)[0]) {
		form = "xxxError"
	} else {
		form = "XxxError"
	}

	if isArrayType {
		form += "s"
	}
	pass.Reportf(typePos, "the type name `%s` should conform to the `%s` format", typeName, form)
}

func reportAboutErrorVar(pass *analysis.Pass, pos token.Pos, varName string) {
	var form string
	if unicode.IsLower([]rune(varName)[0]) {
		form = "errXxx"
	} else {
		form = "ErrXxx"
	}
	pass.Reportf(pos, "the variable name `%s` should conform to the `%s` format", varName, form)
}

func getPkgFromPath(p string) string {
	idx := strings.LastIndex(p, "/")
	if idx == -1 {
		return p
	}
	return p[idx+1:]
}
