package gen

import (
	"fmt"
	"log"
	"strings"

	"github.com/clipperhouse/typewriter"
	"go/types"
)

//These are the viewmodel structs which are passed to the template
//to render the genned code
type interfaceModel struct {
	Name             string
	BackendTypeName  string
	TypePrefix       string
	AcceptErrorCheck bool
	Methods          []methodModel
}
type methodModel struct {
	Name    string
	Params  []paramModel
	Results []resultModel
	Ignore  bool
}
type paramModel struct {
	Name      string
	Type      string
	IsContext bool
}
type resultModel struct {
	Type    string
	IsError bool
}

//Generate an interfaceModel object from a type & set of tags
func buildModel(t typewriter.Type, writer typewriter.Interface, typePrefix string) (interfaceModel, error) {
	//Grab the go/types interface object for the interface we're generating code for
	intfc, err := getInterface(t)
	if err != nil {
		return interfaceModel{}, err
	}

	shortPackageName := ""
	interfaceNamedType, ok := t.Type.(*types.Named)
	if ok {
		shortPackageName = interfaceNamedType.Obj().Pkg().Name()
	}

	//Pull the gen tag data for the gen tags that are causing us to generate code
	tag, found := t.FindTag(writer)
	if !found {
		return interfaceModel{}, nil
	}

	//Check for a backend[Type] tag - backend tags specify that we should "ignore" (treat as a pass-through)
	//any method not on the backend type.  If we haven't specified one, the backend type is the interface we're
	//generating code for
	backendType := t
	hasCustomBackend, backendTypeList := hasSubtag(tag, "backend")
	if hasCustomBackend {
		if len(backendTypeList) != 1 {
			log.Fatalf("The 'backend' tag on %s's writer %s requires exactly one type, but instead it has %d types.", t.Name, writer.Name(), len(backendTypeList))
		}

		if !types.AssignableTo(t.Type, backendTypeList[0].Type) {
			log.Fatalf("The 'backend' tag on %s's writer %s is %s.  %s cannot be assigned to %s.", t.Name, writer.Name(), backendTypeList[0].Name, t.Name, backendTypeList[0].Name)
		}

		backendType = backendTypeList[0]
	}

	backendInterface, err := getInterface(backendType)
	if err != nil {
		return interfaceModel{}, nil
	}

	//Make a map of the methods on the backend type- these methods should not be "ignored" (treated as a pass-through)
	backendMethodMap := make(map[string]*types.Signature)
	for methodIndex := 0; methodIndex < backendInterface.NumMethods(); methodIndex++ {
		mthd := backendInterface.Method(methodIndex)
		sig, _ := mthd.Type().(*types.Signature)
		backendMethodMap[mthd.Name()] = sig
	}

	//Build the root of the interface model
	acceptErrorCheck, _ := hasSubtag(tag, "errcheck")
	model := interfaceModel{
		Name:             t.Name,
		BackendTypeName:  backendType.Name,
		TypePrefix:       typePrefix,
		AcceptErrorCheck: acceptErrorCheck,
	}

	untypedErrObj := types.Universe.Lookup("error")
	if untypedErrObj == nil {
		log.Fatalln("Couldn't find the error class!")
	}
	errorTypeObj, ok := untypedErrObj.Type().(*types.Named)
	if !ok {
		log.Fatalln("Couldn't find the error class!")
	}

	for methodIndex := 0; methodIndex < intfc.NumMethods(); methodIndex++ {
		mthd := intfc.Method(methodIndex)
		sig, _ := mthd.Type().(*types.Signature)

		_, methodOnBackend := backendMethodMap[mthd.Name()]

		methodModel := methodModel{
			Name:   mthd.Name(),
			Ignore: !methodOnBackend,
		}

		for paramIndex := 0; paramIndex < sig.Params().Len(); paramIndex++ {
			param := sig.Params().At(paramIndex)

			paramName := param.Name()
			if paramName == "" {
				paramName = fmt.Sprintf("param%d", paramIndex+1)
			}

			paramModel := paramModel{
				Name:      paramName,
				Type:      getTypeName(param.Type(), shortPackageName),
				IsContext: strings.HasSuffix(param.Type().String(), "golang.org/x/net/context.Context"),
			}

			methodModel.Params = append(methodModel.Params, paramModel)
		}
		for resultIndex := 0; resultIndex < sig.Results().Len(); resultIndex++ {
			result := sig.Results().At(resultIndex)

			resultModel := resultModel{
				Type:    getTypeName(result.Type(), shortPackageName),
				IsError: types.AssignableTo(result.Type(), errorTypeObj),
			}
			methodModel.Results = append(methodModel.Results, resultModel)
		}
		model.Methods = append(model.Methods, methodModel)
	}

	return model, nil
}

func hasSubtag(tag typewriter.Tag, subtag string) (bool, []typewriter.Type) {
	for _, value := range tag.Values {
		if value.Name == subtag {
			return true, value.TypeParameters
		}
	}

	return false, []typewriter.Type{}
}

func getInterface(t typewriter.Type) (*types.Interface, error) {
	underType := t.Underlying()

	intfc, ok := underType.(*types.Interface)
	if !ok {
		return nil, fmt.Errorf("Tag `migrationproducer` can only be applied to an interface- this is a %s.", underType.String())
	}

	return intfc, nil
}

func getTypeName(t types.Type, shortPackageName string) string {
	switch realType := t.(type) {
	case *types.Named:
		{
			if realType.Obj().Pkg() != nil {
				shortened := shortenedTypeName(realType.Obj().Pkg(), shortPackageName, realType.Obj().Name())
				return shortened
			}
		}
	case *types.Slice:
		{
			return "[]" + getTypeName(realType.Elem(), shortPackageName)
		}
	case *types.Pointer:
		{
			return "*" + getTypeName(realType.Elem(), shortPackageName)
		}
	}

	return t.String()
}

func shortenedTypeName(pkg *types.Package, shortPackageName, typeName string) string {
	pkgName := pkg.Name()

	if pkgName != "" && pkgName != shortPackageName {
		return fmt.Sprintf("%s.%s", pkgName, typeName)
	}
	return typeName
}
