package gen

import (
	"errors"
	"fmt"
	"sort"
	"strings"

	"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\"%s/operand\"\n", pkg)
	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) {
	f := flags(i)
	sizes, err := formsizes(i)
	if err != nil {
		m.AddError(err)
		return
	}
	for _, size := range sizes {
		conds := []string{
			fmt.Sprintf("an == %d", size.A),
			fmt.Sprintf("bn == %d", size.B),
		}
		for c, on := range f {
			cmp := map[bool]string{true: "!=", false: "=="}
			cond := fmt.Sprintf("(t.Info() & %s) %s 0", c, cmp[on])
			conds = append(conds, cond)
		}
		sort.Strings(conds)
		m.Printf("case %s:\n", strings.Join(conds, " && "))
		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 {
	if i.AliasOf != "" || !strings.HasPrefix(i.Opcode, "MOV") {
		return false
	}
	exclude := []string{"Packed", "Duplicate", "Aligned", "Hint", "Swapping"}
	for _, substring := range exclude {
		if strings.Contains(i.Summary, substring) {
			return false
		}
	}
	return true
}

func flags(i inst.Instruction) map[string]bool {
	f := map[string]bool{}
	switch {
	case strings.Contains(i.Summary, "Floating-Point"):
		f["types.IsFloat"] = true
	case strings.Contains(i.Summary, "Zero-Extend"):
		f["types.IsInteger"] = true
		f["types.IsUnsigned"] = true
	case strings.Contains(i.Summary, "Sign-Extension"):
		f["types.IsInteger"] = true
		f["types.IsUnsigned"] = false
	default:
		f["types.IsInteger"] = true
	}
	return f
}

type movsize struct{ A, B int8 }

func (s movsize) sortkey() uint16 { return (uint16(s.A) << 8) | uint16(s.B) }

func formsizes(i inst.Instruction) ([]movsize, error) {
	set := map[movsize]bool{}
	for _, f := range i.Forms {
		if f.Arity() != 2 {
			continue
		}
		s := movsize{
			A: opsize[f.Operands[0].Type],
			B: opsize[f.Operands[1].Type],
		}
		if s.A < 0 || s.B < 0 {
			continue
		}
		if s.A == 0 || s.B == 0 {
			return nil, errors.New("unknown operand type")
		}
		set[s] = true
	}
	var ss []movsize
	for s := range set {
		ss = append(ss, s)
	}
	sort.Slice(ss, func(i, j int) bool { return ss[i].sortkey() < ss[j].sortkey() })
	return ss, nil
}

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