package code

import (
	"bytes"
	"fmt"
	"go/ast"
	"go/format"
	"go/types"
	"iter"
	"slices"

	typeindexanalyzer "honnef.co/go/tools/internal/analysisinternal/typeindex"
	"honnef.co/go/tools/internal/typesinternal/typeindex"
	"honnef.co/go/tools/pattern"

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

var RequiredAnalyzers = []*analysis.Analyzer{inspect.Analyzer, typeindexanalyzer.Analyzer}

func Preorder(pass *analysis.Pass, fn func(ast.Node), types ...ast.Node) {
	pass.ResultOf[inspect.Analyzer].(*inspector.Inspector).Preorder(types, fn)
}

func PreorderStack(pass *analysis.Pass, fn func(ast.Node, []ast.Node), types ...ast.Node) {
	pass.ResultOf[inspect.Analyzer].(*inspector.Inspector).WithStack(types, func(n ast.Node, push bool, stack []ast.Node) (proceed bool) {
		if push {
			fn(n, stack)
		}
		return true
	})
}

func Matches(pass *analysis.Pass, qs ...pattern.Pattern) iter.Seq2[ast.Node, *pattern.Matcher] {
	return func(yield func(ast.Node, *pattern.Matcher) bool) {
		for _, q := range qs {
			if !CouldMatchAny(pass, q) {
				continue
			}

			if len(q.RootCallSymbols) != 0 {
				index := pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
				for _, isym := range q.RootCallSymbols {
					var obj types.Object
					if isym.Type == "" {
						obj = index.Object(isym.Path, isym.Ident)
					} else {
						obj = index.Selection(isym.Path, isym.Type, isym.Ident)
					}
					for c := range index.Calls(obj) {
						node := c.Node()
						if m, ok := Match(pass, q, node); ok {
							if !yield(node, m) {
								return
							}
						}
					}
				}
			} else {
				ins := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
				fn := func(node ast.Node, push bool) bool {
					if !push {
						return true
					}

					if m, ok := Match(pass, q, node); ok {
						return yield(node, m)
					}
					return true
				}
				ins.Nodes(q.EntryNodes, fn)
			}
		}
	}
}

func Match(pass *analysis.Pass, q pattern.Pattern, node ast.Node) (*pattern.Matcher, bool) {
	// Note that we ignore q.Relevant – callers of Match usually use
	// AST inspectors that already filter on nodes we're interested
	// in.
	m := &pattern.Matcher{TypesInfo: pass.TypesInfo}
	ok := m.Match(q, node)
	return m, ok
}

func CouldMatchAny(pass *analysis.Pass, qs ...pattern.Pattern) bool {
	index := pass.ResultOf[typeindexanalyzer.Analyzer].(*typeindex.Index)
	var do func(node pattern.Node) bool
	do = func(node pattern.Node) bool {
		switch node := node.(type) {
		case pattern.Any:
			return true
		case pattern.Or:
			return slices.ContainsFunc(node.Nodes, do)
		case pattern.And:
			for _, child := range node.Nodes {
				if !do(child) {
					return false
				}
			}
			return true
		case pattern.IndexSymbol:
			if node.Type == "" {
				return index.Object(node.Path, node.Ident) != nil
			} else {
				return index.Selection(node.Path, node.Type, node.Ident) != nil
			}
		default:
			panic(fmt.Sprintf("internal error: unexpected type %T", node))
		}
	}

	for _, q := range qs {
		if do(q.SymbolsPattern) {
			return true
		}
	}
	return false
}

func MatchAndEdit(pass *analysis.Pass, before, after pattern.Pattern, node ast.Node) (*pattern.Matcher, []analysis.TextEdit, bool) {
	m, ok := Match(pass, before, node)
	if !ok {
		return m, nil, false
	}
	r := pattern.NodeToAST(after.Root, m.State)
	buf := &bytes.Buffer{}
	format.Node(buf, pass.Fset, r)
	edit := []analysis.TextEdit{{
		Pos:     node.Pos(),
		End:     node.End(),
		NewText: buf.Bytes(),
	}}
	return m, edit, true
}

func EditMatch(pass *analysis.Pass, node ast.Node, m *pattern.Matcher, after pattern.Pattern) []analysis.TextEdit {
	r := pattern.NodeToAST(after.Root, m.State)
	buf := &bytes.Buffer{}
	format.Node(buf, pass.Fset, r)
	edit := []analysis.TextEdit{{
		Pos:     node.Pos(),
		End:     node.End(),
		NewText: buf.Bytes(),
	}}
	return edit
}
