package gen

import (
	"errors"
	"fmt"
	"strings"

	"github.com/mmcloughlin/avo/internal/api"
	"github.com/mmcloughlin/avo/internal/inst"
	"github.com/mmcloughlin/avo/internal/prnt"
	"github.com/mmcloughlin/avo/printer"
)

type mov struct {
	cfg printer.Config
	prnt.Generator
}

// NewMOV generates a function that will auto-select the correct MOV instruction
// based on operand types and and sizes.
func NewMOV(cfg printer.Config) Interface {
	return GoFmt(&mov{cfg: cfg})
}

func (m *mov) Generate(is []inst.Instruction) ([]byte, error) {
	m.Printf("// %s\n\n", m.cfg.GeneratedWarning())
	m.Printf("package build\n\n")

	m.Printf("import (\n")
	m.Printf("\t\"go/types\"\n")
	m.NL()
	m.Printf("\t%q\n", api.ImportPath(api.OperandPackage))
	m.Printf(")\n\n")

	m.Printf("func (c *Context) mov(a, b operand.Op, an, bn int, t *types.Basic) {\n")
	m.Printf("switch {\n")
	for _, i := range is {
		if ismov(i) {
			m.instruction(i)
		}
	}
	m.Printf("default:\n")
	m.Printf("c.adderrormessage(\"could not deduce mov instruction\")\n")
	m.Printf("}\n")
	m.Printf("}\n")
	return m.Result()
}

func (m *mov) instruction(i inst.Instruction) {
	tcs := typecases(i)
	mfs, err := movforms(i)
	if err != nil {
		m.AddError(err)
		return
	}
	for _, mf := range mfs {
		conds := []string{
			fmt.Sprintf("an == %d", opsize[mf.A]),
			fmt.Sprintf("%s(a)", api.CheckerName(mf.A)),
			fmt.Sprintf("bn == %d", opsize[mf.B]),
			fmt.Sprintf("%s(b)", api.CheckerName(mf.B)),
		}

		for _, tc := range tcs {
			typecase := fmt.Sprintf("(t.Info() & %s) %s %s", tc.Mask, tc.Op, tc.Value)
			m.Printf("case %s && %s:\n", strings.Join(conds, " && "), typecase)
			m.Printf("c.%s(a, b)\n", i.Opcode)
		}
	}
}

// ismov decides whether the given instruction is a plain move instruction.
func ismov(i inst.Instruction) bool {
	// Ignore aliases.
	if i.AliasOf != "" {
		return false
	}

	// Accept specific move instruction prefixes.
	prefixes := []string{"MOV", "KMOV", "VMOV"}
	accept := false
	for _, prefix := range prefixes {
		accept = strings.HasPrefix(i.Opcode, prefix) || accept
	}
	if !accept {
		return false
	}

	// Exclude some cases based on instruction descriptions.
	exclude := []string{"Packed", "Duplicate", "Aligned", "Hint", "Swapping"}
	for _, substring := range exclude {
		if strings.Contains(i.Summary, substring) {
			return false
		}
	}

	return true
}

type typecase struct {
	Mask  string
	Op    string
	Value string
}

func typecases(i inst.Instruction) []typecase {
	switch {
	case strings.Contains(i.Summary, "Floating-Point"):
		return []typecase{
			{"types.IsFloat", "!=", "0"},
		}
	case strings.Contains(i.Summary, "Zero-Extend"):
		return []typecase{
			{"(types.IsInteger|types.IsUnsigned)", "==", "(types.IsInteger|types.IsUnsigned)"},
			{"types.IsBoolean", "!=", "0"},
		}
	case strings.Contains(i.Summary, "Sign-Extension"):
		return []typecase{
			{"(types.IsInteger|types.IsUnsigned)", "==", "types.IsInteger"},
		}
	default:
		return []typecase{
			{"(types.IsInteger|types.IsBoolean)", "!=", "0"},
		}
	}
}

type movform struct{ A, B string }

func movforms(i inst.Instruction) ([]movform, error) {
	var mfs []movform
	for _, f := range i.Forms {
		if f.Arity() != 2 {
			continue
		}
		mf := movform{
			A: f.Operands[0].Type,
			B: f.Operands[1].Type,
		}
		if opsize[mf.A] < 0 || opsize[mf.B] < 0 {
			continue
		}
		if opsize[mf.A] == 0 || opsize[mf.B] == 0 {
			return nil, errors.New("unknown operand type")
		}
		mfs = append(mfs, mf)
	}
	return mfs, nil
}

var opsize = map[string]int8{
	"imm8":  -1,
	"imm16": -1,
	"imm32": -1,
	"imm64": -1,
	"r8":    1,
	"r16":   2,
	"r32":   4,
	"r64":   8,
	"xmm":   16,
	"ymm":   32,
	"zmm":   64,
	"m8":    1,
	"m16":   2,
	"m32":   4,
	"m64":   8,
	"m128":  16,
	"m256":  32,
	"m512":  64,
	"k":     8,
}
