//go:build ignore

package main

import (
	"bytes"
	"cmp"
	"flag"
	"fmt"
	"go/format"
	"go/token"
	"log"
	"maps"
	"os"
	"path/filepath"
	"regexp"
	"runtime"
	"slices"
	"strconv"
	"strings"
	"unicode"

	"github.com/go-json-experiment/json"
	"efprojects.com/kitten-ipc/kitcom/internal/tsgo/repo"
)

type diagnosticMessage struct {
	Category           string `json:"category"`
	Code               int    `json:"code"`
	ReportsUnnecessary bool   `json:"reportsUnnecessary"`
	ReportsDeprecated  bool   `json:"reportsDeprecated"`
	// spelling error here is [sic] in Strada
	ElidedInCompatibilityPyramid bool `json:"elidedInCompatabilityPyramid"`

	key string
}

func main() {
	log.SetFlags(log.LstdFlags | log.Lshortfile)

	output := flag.String("output", "", "path to the output diagnostics_generated.go file")
	flag.Parse()

	if *output == "" {
		flag.Usage()
		return
	}

	rawDiagnosticMessages := readRawMessages(filepath.Join(repo.TypeScriptSubmodulePath, "src", "compiler", "diagnosticMessages.json"))

	_, filename, _, ok := runtime.Caller(0)
	if !ok {
		panic("could not get current filename")
	}
	filename = filepath.FromSlash(filename) // runtime.Caller always returns forward slashes; https://go.dev/issues/3335, https://go.dev/cl/603275

	rawExtraMessages := readRawMessages(filepath.Join(filepath.Dir(filename), "extraDiagnosticMessages.json"))

	maps.Copy(rawDiagnosticMessages, rawExtraMessages)
	diagnosticMessages := slices.Collect(maps.Values(rawDiagnosticMessages))

	slices.SortFunc(diagnosticMessages, func(a *diagnosticMessage, b *diagnosticMessage) int {
		return cmp.Compare(a.Code, b.Code)
	})

	var buf bytes.Buffer

	buf.WriteString("// Code generated by generate.go; DO NOT EDIT.\n")
	buf.WriteString("\n")
	buf.WriteString("package diagnostics\n")

	for _, m := range diagnosticMessages {
		varName, key := convertPropertyName(m.key, m.Code)

		fmt.Fprintf(&buf, "var %s = &Message{code: %d, category: Category%s, key: %q, text: %q", varName, m.Code, m.Category, key, m.key)

		if m.ReportsUnnecessary {
			buf.WriteString(`, reportsUnnecessary: true`)
		}
		if m.ElidedInCompatibilityPyramid {
			buf.WriteString(`, elidedInCompatibilityPyramid: true`)
		}
		if m.ReportsDeprecated {
			buf.WriteString(`, reportsDeprecated: true`)
		}

		buf.WriteString("}\n\n")
	}

	formatted, err := format.Source(buf.Bytes())
	if err != nil {
		log.Fatalf("failed to format output: %v", err)
		return
	}

	if err := os.WriteFile(*output, formatted, 0o666); err != nil {
		log.Fatalf("failed to write output: %v", err)
		return
	}
}

func readRawMessages(p string) map[int]*diagnosticMessage {
	file, err := os.Open(p)
	if err != nil {
		log.Fatalf("failed to open file: %v", err)
		return nil
	}
	defer file.Close()

	var rawMessages map[string]*diagnosticMessage
	if err := json.UnmarshalRead(file, &rawMessages); err != nil {
		log.Fatalf("failed to decode file: %v", err)
		return nil
	}

	codeToMessage := make(map[int]*diagnosticMessage, len(rawMessages))
	for k, m := range rawMessages {
		m.key = k
		codeToMessage[m.Code] = m
	}

	return codeToMessage
}

var (
	multipleUnderscoreRegexp           = regexp.MustCompile(`_+`)
	leadingUnderscoreUnlessDigitRegexp = regexp.MustCompile(`^_+(\D)`)
	trailingUnderscoreRegexp           = regexp.MustCompile(`_$`)
)

func convertPropertyName(origName string, code int) (varName string, key string) {
	var b strings.Builder
	b.Grow(len(origName))

	for _, r := range origName {
		switch r {
		case '*':
			b.WriteString("_Asterisk")
		case '/':
			b.WriteString("_Slash")
		case ':':
			b.WriteString("_Colon")
		default:
			if !unicode.IsLetter(r) && !unicode.IsDigit(r) {
				b.WriteRune('_')
			} else {
				b.WriteRune(r)
			}
		}
	}

	varName = b.String()
	// get rid of all multi-underscores
	varName = multipleUnderscoreRegexp.ReplaceAllString(varName, "_")
	// remove any leading underscore, unless it is followed by a number.
	varName = leadingUnderscoreUnlessDigitRegexp.ReplaceAllString(varName, "$1")
	// get rid of all trailing underscores.
	varName = trailingUnderscoreRegexp.ReplaceAllString(varName, "")

	key = varName
	if len(key) > 100 {
		key = key[:100]
	}
	key = key + "_" + strconv.Itoa(code)

	if !token.IsExported(varName) {
		var b strings.Builder
		b.Grow(len(varName) + 2)
		if varName[0] == '_' {
			b.WriteString("X")
		} else {
			b.WriteString("X_")
		}
		b.WriteString(varName)
		varName = b.String()
	}

	if !token.IsIdentifier(varName) || !token.IsExported(varName) {
		log.Fatalf("failed to convert property name to exported identifier: %q", origName)
	}

	return varName, key
}
