// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package ssa

import (
	"go/types"

	"golang.org/x/tools/internal/typeparams"
)

// Utilities for dealing with core types.

// isBytestring returns true if T has the same terms as interface{[]byte | string}.
// These act like a core type for some operations: slice expressions, append and copy.
//
// See https://go.dev/ref/spec#Core_types for the details on bytestring.
func isBytestring(T types.Type) bool {
	U := T.Underlying()
	if _, ok := U.(*types.Interface); !ok {
		return false
	}

	tset := typeSetOf(U)
	if tset.Len() != 2 {
		return false
	}
	hasBytes, hasString := false, false
	underIs(tset, func(t types.Type) bool {
		switch {
		case isString(t):
			hasString = true
		case isByteSlice(t):
			hasBytes = true
		}
		return hasBytes || hasString
	})
	return hasBytes && hasString
}

// termList is a list of types.
type termList []*types.Term            // type terms of the type set
func (s termList) Len() int            { return len(s) }
func (s termList) At(i int) types.Type { return s[i].Type() }

// typeSetOf returns the type set of typ. Returns an empty typeset on an error.
func typeSetOf(typ types.Type) termList {
	// This is a adaptation of x/exp/typeparams.NormalTerms which x/tools cannot depend on.
	var terms []*types.Term
	var err error
	// typeSetOf(t) == typeSetOf(Unalias(t))
	switch typ := types.Unalias(typ).(type) {
	case *types.TypeParam:
		terms, err = typeparams.StructuralTerms(typ)
	case *types.Union:
		terms, err = typeparams.UnionTermSet(typ)
	case *types.Interface:
		terms, err = typeparams.InterfaceTermSet(typ)
	default:
		// Common case.
		// Specializing the len=1 case to avoid a slice
		// had no measurable space/time benefit.
		terms = []*types.Term{types.NewTerm(false, typ)}
	}

	if err != nil {
		return termList(nil)
	}
	return termList(terms)
}

// underIs calls f with the underlying types of the specific type terms
// of s and reports whether all calls to f returned true. If there are
// no specific terms, underIs returns the result of f(nil).
func underIs(s termList, f func(types.Type) bool) bool {
	if s.Len() == 0 {
		return f(nil)
	}
	for i := 0; i < s.Len(); i++ {
		u := s.At(i).Underlying()
		if !f(u) {
			return false
		}
	}
	return true
}

// indexType returns the element type and index mode of a IndexExpr over a type.
// It returns (nil, invalid) if the type is not indexable; this should never occur in a well-typed program.
func indexType(typ types.Type) (types.Type, indexMode) {
	switch U := typ.Underlying().(type) {
	case *types.Array:
		return U.Elem(), ixArrVar
	case *types.Pointer:
		if arr, ok := U.Elem().Underlying().(*types.Array); ok {
			return arr.Elem(), ixVar
		}
	case *types.Slice:
		return U.Elem(), ixVar
	case *types.Map:
		return U.Elem(), ixMap
	case *types.Basic:
		return tByte, ixValue // must be a string
	case *types.Interface:
		tset := typeSetOf(U)
		if tset.Len() == 0 {
			return nil, ixInvalid // no underlying terms or error is empty.
		}

		elem, mode := indexType(tset.At(0))
		for i := 1; i < tset.Len() && mode != ixInvalid; i++ {
			e, m := indexType(tset.At(i))
			if !types.Identical(elem, e) { // if type checked, just a sanity check
				return nil, ixInvalid
			}
			// Update the mode to the most constrained address type.
			mode = mode.meet(m)
		}
		if mode != ixInvalid {
			return elem, mode
		}
	}
	return nil, ixInvalid
}

// An indexMode specifies the (addressing) mode of an index operand.
//
// Addressing mode of an index operation is based on the set of
// underlying types.
// Hasse diagram of the indexMode meet semi-lattice:
//
//	ixVar     ixMap
//	  |          |
//	ixArrVar     |
//	  |          |
//	ixValue      |
//	   \        /
//	  ixInvalid
type indexMode byte

const (
	ixInvalid indexMode = iota // index is invalid
	ixValue                    // index is a computed value (not addressable)
	ixArrVar                   // like ixVar, but index operand contains an array
	ixVar                      // index is an addressable variable
	ixMap                      // index is a map index expression (acts like a variable on lhs, commaok on rhs of an assignment)
)

// meet is the address type that is constrained by both x and y.
func (x indexMode) meet(y indexMode) indexMode {
	if (x == ixMap || y == ixMap) && x != y {
		return ixInvalid
	}
	// Use int representation and return min.
	if x < y {
		return y
	}
	return x
}
