// Copyright 2026 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 sgutil provides shared utilities for SIMD file
// generation across architectures.  This includes
//
// - "natural" comparison for better ordering
// - formatted-Go file saving
// - file merging for simdgenericOps.go
// - naming conventions and templates for the
//   bitwise vector reinterpretation no-op methods.

package sgutil

import (
	"bufio"
	"bytes"
	"fmt"
	"os"
	"regexp"
	"slices"
	"sort"
	"strconv"
	"strings"
	"text/template"
)

const simdGenericOpsTmpl = `
package main

func simdGenericOps() []opData {
	return []opData{
{{- range .Ops }}
		{name: "{{.OpName}}", argLength: {{.OpInLen}}{{if .Comm}}, commutative: true{{end}}}, // ARCH:{{.ArchTag}}
{{- end }}
{{- range .OpsImm }}
		{name: "{{.OpName}}", argLength: {{.OpInLen}}{{if .Comm}}, commutative: true{{end}}, aux: "UInt8"}, // ARCH:{{.ArchTag}}
{{- end }}
	}
}
`

// TemplateNamedreturns a parsed template from temp, named name.
func TemplateNamed(name, temp string) *template.Template {
	t, err := template.New(name).Parse(temp)
	if err != nil {
		panic(fmt.Errorf("failed to parse template %s: %w", name, err))
	}
	return t
}

// simdGenericOpsHeader is the arch-agnostic header for simdgenericOps.go.
const simdGenericOpsHeader = "// Code generated by 'simdgen' (merged automatically); DO NOT EDIT.\n"

// GenericOpsData holds one generic op entry for template rendering.
type GenericOpsData struct {
	OpName  string
	OpInLen int
	Comm    bool
	HasAux  bool
	Archs   []string // e.g. ["amd64","arm64"]
}

// ArchTag returns the comma-separated arch list for the template comment.
func (d GenericOpsData) ArchTag() string {
	return strings.Join(d.Archs, ",")
}

// Match lines like:
//
//	{name: "GetElemFloat32x4", argLength: 2, aux: "UInt8"}, // ARCH:amd64,arm64
//	{name: "MulInt8x16", argLength: 2, commutative: true}, // ARCH:arm64
var reEntry = regexp.MustCompile(`^\s*\{name:\s*"([^"]+)",\s*argLength:\s*(\d+)(?:,\s*commutative:\s*(true|false))?(?:,\s*aux:\s*"[^"]+")?\s*\}\s*,\s*(?://\s*ARCH:(\S+))?`)

// parseOps reads an existing simdgenericOps.go and extracts op entries along with
// their ARCH: tags. It strips currentArch from all parsed entries and drops those
// with no remaining tags (ops that belonged only to the current arch).
func parseOps(oldFile, currentArch string) ([]GenericOpsData, error) {
	f, ferr := os.Open(oldFile)
	if ferr != nil {
		if os.IsNotExist(ferr) {
			return nil, nil
		}
		return nil, ferr
	}
	defer f.Close()

	untagged := 0
	unmatched := 0
	noarch := 0

	var result []GenericOpsData
	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		line := scanner.Text()
		matches := reEntry.FindStringSubmatch(line)
		if matches == nil {
			unmatched++
			continue
		}
		name := matches[1]
		argLen, _ := strconv.Atoi(matches[2])
		comm := matches[3] == "true" // matches[3] may be empty if commutative is omitted
		hasAux := strings.Contains(line, `aux: "UInt8"`)
		archTag := matches[4]

		if archTag == "" {
			untagged++
			// Skip untagged entries (shouldn't occur after initial run).
			continue
		}

		archs := slices.DeleteFunc(strings.Split(archTag, ","), func(a string) bool { return a == currentArch })
		if len(archs) == 0 {
			noarch++
			continue
		}

		result = append(result, GenericOpsData{
			OpName:  name,
			OpInLen: argLen,
			Comm:    comm,
			HasAux:  hasAux,
			Archs:   archs,
		})
	}
	if err := scanner.Err(); err != nil {
		return nil, err
	}

	return result, nil
}

// mergeOps merges existing and new op slices using a merge-sort walk.
// Both inputs must be sorted by OpName (natural sort); the output is verified
// to be sorted.
//
// existing must contain only tags from other architectures.
// new entries must be tagged with currentArch.
//
// Overlapping names have their arch tags merged and properties verified for
// consistency (argLength, commutativity, HasAux).
func mergeOps(currentArch string, existing, new []GenericOpsData) ([]GenericOpsData, error) {
	var (
		result []GenericOpsData
		i, j   int
	)
	for i < len(existing) || j < len(new) {
		var cmp int
		if i < len(existing) && j < len(new) {
			cmp = CompareNatural(existing[i].OpName, new[j].OpName)
		} else if i < len(existing) {
			cmp = -1
		} else {
			cmp = 1
		}

		var entry GenericOpsData
		switch cmp {
		case -1:
			entry = existing[i]
			i++

		case 1:
			entry = new[j]
			j++

		case 0:
			e, n := existing[i], new[j]
			if e.OpInLen != n.OpInLen {
				return nil, fmt.Errorf("simdgen: op %q has inconsistent argLength: existing=%d, new=%d", e.OpName, e.OpInLen, n.OpInLen)
			}
			if e.Comm != n.Comm {
				return nil, fmt.Errorf("simdgen: op %q has inconsistent commutativity: existing=%v, new=%v", e.OpName, e.Comm, n.Comm)
			}
			if e.HasAux != n.HasAux {
				return nil, fmt.Errorf("simdgen: op %q has inconsistent HasAux: existing=%v, new=%v", e.OpName, e.HasAux, n.HasAux)
			}
			entry = e
			entry.Archs = append(entry.Archs, currentArch)
			sort.Strings(entry.Archs)
			i++
			j++
		}
		if len(result) > 0 && CompareNatural(result[len(result)-1].OpName, entry.OpName) >= 0 {
			return nil, fmt.Errorf("simdgen: mergeOps: sort invariant violated between %q and  %q",
				result[len(result)-1].OpName, entry.OpName)
		}
		result = append(result, entry)
	}

	return result, nil
}

// MergeSIMDGenericOps merges a new set of generic ops with the existing ones read
// from goroot "/" file.
func MergeSIMDGenericOps(newOps []GenericOpsData, oldFile, currentArch string) *bytes.Buffer {
	thisArch := []string{currentArch}
	for i := range newOps {
		newOps[i].Archs = thisArch
	}

	sortNatural := func(s []GenericOpsData) []GenericOpsData {
		sort.Slice(s, func(i, j int) bool {
			return CompareNatural(s[i].OpName, s[j].OpName) < 0
		})
		return s
	}

	existing, err := parseOps(oldFile, currentArch)
	if err != nil {
		panic(fmt.Errorf("failed to parse existing %s: %w", oldFile, err))
	}

	merged, err := mergeOps(currentArch, sortNatural(existing), sortNatural(newOps))
	if err != nil {
		panic(err)
	}

	// Split into ops/opsImm.
	type opData struct {
		Ops    []GenericOpsData
		OpsImm []GenericOpsData
	}
	var opsData opData
	for _, gOp := range merged {
		if gOp.HasAux {
			opsData.OpsImm = append(opsData.OpsImm, gOp)
		} else {
			opsData.Ops = append(opsData.Ops, gOp)
		}
	}

	t := TemplateNamed("simdgenericOps", simdGenericOpsTmpl)
	buffer := new(bytes.Buffer)
	buffer.WriteString(simdGenericOpsHeader)

	err = t.Execute(buffer, opsData)
	if err != nil {
		panic(fmt.Errorf("failed to execute template: %w", err))
	}

	return buffer
}
