package qf1001

import (
	"go/ast"
	"go/types"

	"honnef.co/go/tools/analysis/code"
	"honnef.co/go/tools/analysis/edit"
	"honnef.co/go/tools/analysis/lint"
	"honnef.co/go/tools/analysis/report"
	"honnef.co/go/tools/go/ast/astutil"
	"honnef.co/go/tools/pattern"

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

var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
	Analyzer: &analysis.Analyzer{
		Name:     "QF1001",
		Run:      CheckDeMorgan,
		Requires: []*analysis.Analyzer{inspect.Analyzer},
	},
	Doc: &lint.RawDocumentation{
		Title:    "Apply De Morgan's law",
		Since:    "2021.1",
		Severity: lint.SeverityHint,
	},
})

var Analyzer = SCAnalyzer.Analyzer

var demorganQ = pattern.MustParse(`(UnaryExpr "!" expr@(BinaryExpr _ _ _))`)

func CheckDeMorgan(pass *analysis.Pass) (any, error) {
	// TODO(dh): support going in the other direction, e.g. turning `!a && !b && !c` into `!(a || b || c)`

	// hasFloats reports whether any subexpression is of type float.
	hasFloats := func(expr ast.Expr) bool {
		found := false
		ast.Inspect(expr, func(node ast.Node) bool {
			if expr, ok := node.(ast.Expr); ok {
				if typ := pass.TypesInfo.TypeOf(expr); typ != nil {
					if basic, ok := typ.Underlying().(*types.Basic); ok {
						if (basic.Info() & types.IsFloat) != 0 {
							found = true
							return false
						}
					}
				}
			}
			return true
		})
		return found
	}

	fn := func(node ast.Node, stack []ast.Node) {
		matcher, ok := code.Match(pass, demorganQ, node)
		if !ok {
			return
		}

		expr := matcher.State["expr"].(ast.Expr)

		// be extremely conservative when it comes to floats
		if hasFloats(expr) {
			return
		}

		n := astutil.NegateDeMorgan(expr, false)
		nr := astutil.NegateDeMorgan(expr, true)
		nc, ok := astutil.CopyExpr(n)
		if !ok {
			return
		}
		ns := astutil.SimplifyParentheses(nc)
		nrc, ok := astutil.CopyExpr(nr)
		if !ok {
			return
		}
		nrs := astutil.SimplifyParentheses(nrc)

		var bn, bnr, bns, bnrs string
		switch parent := stack[len(stack)-2]; parent.(type) {
		case *ast.BinaryExpr, *ast.IfStmt, *ast.ForStmt, *ast.SwitchStmt:
			// Always add parentheses for if, for and switch. If
			// they're unnecessary, go/printer will strip them when
			// the whole file gets formatted.

			bn = report.Render(pass, &ast.ParenExpr{X: n})
			bnr = report.Render(pass, &ast.ParenExpr{X: nr})
			bns = report.Render(pass, &ast.ParenExpr{X: ns})
			bnrs = report.Render(pass, &ast.ParenExpr{X: nrs})

		default:
			// TODO are there other types where we don't want to strip parentheses?
			bn = report.Render(pass, n)
			bnr = report.Render(pass, nr)
			bns = report.Render(pass, ns)
			bnrs = report.Render(pass, nrs)
		}

		// Note: we cannot compare the ASTs directly, because
		// simplifyParentheses might have rebalanced trees without
		// affecting the rendered form.
		var fixes []analysis.SuggestedFix
		fixes = append(fixes, edit.Fix("Apply De Morgan's law", edit.ReplaceWithString(node, bn)))
		if bn != bns {
			fixes = append(fixes, edit.Fix("Apply De Morgan's law & simplify", edit.ReplaceWithString(node, bns)))
		}
		if bn != bnr {
			fixes = append(fixes, edit.Fix("Apply De Morgan's law recursively", edit.ReplaceWithString(node, bnr)))
			if bnr != bnrs {
				fixes = append(fixes, edit.Fix("Apply De Morgan's law recursively & simplify", edit.ReplaceWithString(node, bnrs)))
			}
		}

		report.Report(pass, node, "could apply De Morgan's law", report.Fixes(fixes...))
	}

	code.PreorderStack(pass, fn, (*ast.UnaryExpr)(nil))

	return nil, nil
}
