package gotypes

import (
	"bytes"
	"errors"
	"fmt"
	"go/token"
	"go/types"
	"strconv"

	"github.com/mmcloughlin/avo/operand"
)

// Signature represents a Go function signature.
type Signature struct {
	pkg     *types.Package
	sig     *types.Signature
	params  *Tuple
	results *Tuple
}

// NewSignature constructs a Signature.
func NewSignature(pkg *types.Package, sig *types.Signature) *Signature {
	s := &Signature{
		pkg: pkg,
		sig: sig,
	}
	s.init()
	return s
}

// NewSignatureVoid builds the void signature "func()".
func NewSignatureVoid() *Signature {
	return NewSignature(nil, types.NewSignature(nil, nil, nil, false))
}

// LookupSignature returns the signature of the named function in the provided package.
func LookupSignature(pkg *types.Package, name string) (*Signature, error) {
	scope := pkg.Scope()
	obj := scope.Lookup(name)
	if obj == nil {
		return nil, fmt.Errorf("could not find function \"%s\"", name)
	}
	s, ok := obj.Type().(*types.Signature)
	if !ok {
		return nil, fmt.Errorf("object \"%s\" does not have signature type", name)
	}
	return NewSignature(pkg, s), nil
}

// ParseSignature builds a Signature by parsing a Go function type expression.
// The function type must reference builtin types only; see
// ParseSignatureInPackage if custom types are required.
func ParseSignature(expr string) (*Signature, error) {
	return ParseSignatureInPackage(nil, expr)
}

// ParseSignatureInPackage builds a Signature by parsing a Go function type
// expression. The expression may reference types in the provided package.
func ParseSignatureInPackage(pkg *types.Package, expr string) (*Signature, error) {
	tv, err := types.Eval(token.NewFileSet(), pkg, token.NoPos, expr)
	if err != nil {
		return nil, err
	}
	if tv.Value != nil {
		return nil, errors.New("signature expression should have nil value")
	}
	s, ok := tv.Type.(*types.Signature)
	if !ok {
		return nil, errors.New("provided type is not a function signature")
	}
	return NewSignature(pkg, s), nil
}

// Params returns the function signature argument types.
func (s *Signature) Params() *Tuple { return s.params }

// Results returns the function return types.
func (s *Signature) Results() *Tuple { return s.results }

// Bytes returns the total size of the function arguments and return values.
func (s *Signature) Bytes() int { return s.Params().Bytes() + s.Results().Bytes() }

// String writes Signature as a string. This does not include the "func" keyword.
func (s *Signature) String() string {
	var buf bytes.Buffer
	types.WriteSignature(&buf, s.sig, func(pkg *types.Package) string {
		if pkg == s.pkg {
			return ""
		}
		return pkg.Name()
	})
	return buf.String()
}

func (s *Signature) init() {
	p := s.sig.Params()
	r := s.sig.Results()

	// Compute parameter offsets.
	vs := tuplevars(p)
	vs = append(vs, types.NewParam(token.NoPos, nil, "sentinel", types.Typ[types.Uint64]))
	paramsoffsets := Sizes.Offsetsof(vs)
	paramssize := paramsoffsets[p.Len()]
	s.params = newTuple(p, paramsoffsets, paramssize, "arg")

	// Result offsets.
	vs = tuplevars(r)
	resultsoffsets := Sizes.Offsetsof(vs)
	var resultssize int64
	if n := len(vs); n > 0 {
		resultssize = resultsoffsets[n-1] + Sizes.Sizeof(vs[n-1].Type())
	}
	for i := range resultsoffsets {
		resultsoffsets[i] += paramssize
	}
	s.results = newTuple(r, resultsoffsets, resultssize, "ret")
}

// Tuple represents a tuple of variables, such as function arguments or results.
type Tuple struct {
	components []Component
	byname     map[string]Component
	size       int
}

func newTuple(t *types.Tuple, offsets []int64, size int64, defaultprefix string) *Tuple {
	tuple := &Tuple{
		byname: map[string]Component{},
		size:   int(size),
	}
	for i := 0; i < t.Len(); i++ {
		v := t.At(i)
		name := v.Name()
		if name == "" {
			name = defaultprefix
			if i > 0 {
				name += strconv.Itoa(i)
			}
		}
		addr := operand.NewParamAddr(name, int(offsets[i]))
		c := NewComponent(v.Type(), addr)
		tuple.components = append(tuple.components, c)
		if v.Name() != "" {
			tuple.byname[v.Name()] = c
		}
	}
	return tuple
}

// Lookup returns the variable with the given name.
func (t *Tuple) Lookup(name string) Component {
	e := t.byname[name]
	if e == nil {
		return errorf("unknown variable \"%s\"", name)
	}
	return e
}

// At returns the variable at index i.
func (t *Tuple) At(i int) Component {
	if i >= len(t.components) {
		return errorf("index out of range")
	}
	return t.components[i]
}

// Bytes returns the size of the Tuple. This may include additional padding.
func (t *Tuple) Bytes() int { return t.size }

func tuplevars(t *types.Tuple) []*types.Var {
	vs := make([]*types.Var, t.Len())
	for i := 0; i < t.Len(); i++ {
		vs[i] = t.At(i)
	}
	return vs
}
