package unused

import (
	"go/types"

	"honnef.co/go/tools/go/types/typeutil"
)

// lookupMethod returns the index of and method with matching package and name, or (-1, nil).
func lookupMethod(T *types.Interface, pkg *types.Package, name string) (int, *types.Func) {
	if name != "_" {
		for i := 0; i < T.NumMethods(); i++ {
			m := T.Method(i)
			if sameId(m, pkg, name) {
				return i, m
			}
		}
	}
	return -1, nil
}

func sameId(obj types.Object, pkg *types.Package, name string) bool {
	// spec:
	// "Two identifiers are different if they are spelled differently,
	// or if they appear in different packages and are not exported.
	// Otherwise, they are the same."
	if name != obj.Name() {
		return false
	}
	// obj.Name == name
	if obj.Exported() {
		return true
	}
	// not exported, so packages must be the same (pkg == nil for
	// fields in Universe scope; this can only happen for types
	// introduced via Eval)
	if pkg == nil || obj.Pkg() == nil {
		return pkg == obj.Pkg()
	}
	// pkg != nil && obj.pkg != nil
	return pkg.Path() == obj.Pkg().Path()
}

func implements(V types.Type, T *types.Interface, msV *types.MethodSet) ([]*types.Selection, bool) {
	// fast path for common case
	if T.Empty() {
		return nil, true
	}

	if ityp, _ := V.Underlying().(*types.Interface); ityp != nil {
		// TODO(dh): is this code reachable?
		for m := range T.Methods() {
			_, obj := lookupMethod(ityp, m.Pkg(), m.Name())
			switch {
			case obj == nil:
				return nil, false
			case !types.Identical(obj.Type(), m.Type()):
				return nil, false
			}
		}
		return nil, true
	}

	// A concrete type implements T if it implements all methods of T.
	var sels []*types.Selection

	mapping := map[*types.TypeParam]types.Type{}
	for m := range T.Methods() {
		sel := msV.Lookup(m.Pkg(), m.Name())
		if sel == nil {
			return nil, false
		}

		f, _ := sel.Obj().(*types.Func)
		if f == nil {
			return nil, false
		}

		if ok := typeutil.Unify(f.Type(), m.Type(), mapping); !ok {
			return nil, false
		}

		sels = append(sels, sel)
	}
	for tparam, targ := range mapping {
		// This checks constraints on a best-effort basis, erring on the side
		// of accepting too many types.
		if !satisfiesConstraint(targ, tparam) {
			return nil, false
		}
	}
	return sels, true
}

func satisfiesConstraint(t types.Type, tp *types.TypeParam) bool {
	if t == nil {
		// t is nil when we unify two type parameters.
		return true
	}
	bound := tp.Constraint().Underlying().(*types.Interface)
	return types.Satisfies(t, bound)
}
