// Copyright (c) 2025 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

//go:build ignore

package main

import (
	"fmt"
	"go/ast"
	"go/parser"
	"go/token"
	"os"
	"strings"

	"go.mau.fi/util/exerrors"
)

const header = `// GENERATED BY internals_generate.go; DO NOT EDIT

//go:generate go run internals_generate.go
//go:generate goimports -local go.mau.fi/whatsmeow -w internals.go

package whatsmeow

`
const postImportHeader = `
type DangerousInternalClient struct {
        c *Client
}

// DangerousInternals allows access to all unexported methods in Client.
//
// Deprecated: dangerous
func (cli *Client) DangerousInternals() *DangerousInternalClient {
        return &DangerousInternalClient{cli}
}

type DangerousInfoQuery = infoQuery
type DangerousInfoQueryType = infoQueryType

`

func getTypeName(expr ast.Expr) string {
	switch e := expr.(type) {
	case *ast.Ident:
		return e.Name
	case *ast.StarExpr:
		return "*" + getTypeName(e.X)
	case *ast.ArrayType:
		if e.Len != nil {
			return fmt.Sprintf("[%s]%s", getTypeName(e.Len), getTypeName(e.Elt))
		}
		return "[]" + getTypeName(e.Elt)
	case *ast.MapType:
		return fmt.Sprintf("map[%s]%s", getTypeName(e.Key), getTypeName(e.Value))
	case *ast.ChanType:
		if e.Dir == ast.SEND {
			return fmt.Sprintf("chan<- %s", getTypeName(e.Value))
		} else if e.Dir == ast.RECV {
			return fmt.Sprintf("<-chan %s", getTypeName(e.Value))
		}
		return fmt.Sprintf("chan %s", getTypeName(e.Value))
	case *ast.FuncType:
		var params []string
		for _, param := range e.Params.List {
			params = append(params, getTypeName(param.Type))
		}
		var results []string
		if e.Results != nil {
			for _, result := range e.Results.List {
				results = append(results, getTypeName(result.Type))
			}
		}
		retVals := strings.Join(results, ", ")
		if len(results) > 1 {
			retVals = fmt.Sprintf("(%s)", retVals)
		}
		return fmt.Sprintf("func(%s) %s", strings.Join(params, ", "), retVals)
	case *ast.SelectorExpr:
		return fmt.Sprintf("%s.%s", getTypeName(e.X), e.Sel.Name)
	case *ast.StructType:
		// This isn't technically correct, but struct literals shouldn't be used for anything else
		return "struct{}"
	case *ast.Ellipsis:
		return fmt.Sprintf("...%s", getTypeName(e.Elt))
	case *ast.BasicLit:
		return e.Value
	default:
		panic(fmt.Errorf("unknown type %T", e))
	}
}

var write func(str string)
var writef func(format string, args ...any)

func main() {
	fset := token.NewFileSet()
	fileNames := []string{
		"appstate.go", "armadillomessage.go", "broadcast.go", "call.go", "client.go",
		"connectionevents.go", "download.go", "download-to-file.go", "group.go", "handshake.go",
		"keepalive.go", "mediaconn.go", "mediaretry.go", "message.go", "msgsecret.go",
		"newsletter.go", "notification.go", "pair-code.go", "pair.go", "prekeys.go",
		"presence.go", "privacysettings.go", "push.go", "qrchan.go", "receipt.go", "request.go",
		"retry.go", "sendfb.go", "send.go", "upload.go", "user.go", "reportingtoken.go",
	}
	files := make([]*ast.File, len(fileNames))
	for i, name := range fileNames {
		files[i] = exerrors.Must(parser.ParseFile(fset, name, nil, parser.SkipObjectResolution))
	}
	file := exerrors.Must(os.OpenFile("internals.go", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644))
	write = func(str string) {
		exerrors.Must(file.WriteString(str))
	}
	writef = func(format string, args ...any) {
		exerrors.Must(fmt.Fprintf(file, format, args...))
	}
	write(header)
	write("import (\n")
	for _, i := range files[0].Imports {
		write("\t")
		if i.Name != nil {
			writef("%s ", i.Name.Name)
		}
		writef("%s\n", i.Path.Value)
	}
	write(")\n")
	write(postImportHeader)
	for _, f := range files {
		processFile(f)
	}
	exerrors.PanicIfNotNil(file.Close())
}

func processFile(f *ast.File) {
	ast.Inspect(f, func(node ast.Node) (retVal bool) {
		retVal = true
		funcDecl, ok := node.(*ast.FuncDecl)
		if !ok || funcDecl.Name.IsExported() {
			return
		}
		if funcDecl.Recv == nil || len(funcDecl.Recv.List) == 0 || len(funcDecl.Recv.List[0].Names) == 0 ||
			funcDecl.Recv.List[0].Names[0].Name != "cli" {
			return
		}
		writef("\nfunc (int *DangerousInternalClient) %s%s(", strings.ToUpper(funcDecl.Name.Name[0:1]), funcDecl.Name.Name[1:])
		for i, param := range funcDecl.Type.Params.List {
			if i != 0 {
				write(", ")
			}
			for j, name := range param.Names {
				if j != 0 {
					write(", ")
				}
				write(name.Name)
			}
			if len(param.Names) > 0 {
				write(" ")
			}
			write(getTypeName(param.Type))
		}
		write(") ")
		if funcDecl.Type.Results != nil && len(funcDecl.Type.Results.List) > 0 {
			needsParentheses := len(funcDecl.Type.Results.List) > 1 || len(funcDecl.Type.Results.List[0].Names) > 0
			if needsParentheses {
				write("(")
			}
			for i, result := range funcDecl.Type.Results.List {
				if i != 0 {
					write(", ")
				}
				for j, name := range result.Names {
					if j != 0 {
						write(", ")
					}
					write(name.Name)
				}
				if len(result.Names) > 0 {
					write(" ")
				}
				write(getTypeName(result.Type))
			}
			if needsParentheses {
				write(")")
			}
			write(" ")
		}
		write("{\n\t")
		if funcDecl.Type.Results != nil {
			write("return ")
		}
		writef("int.c.%s(", funcDecl.Name.Name)
		for i, param := range funcDecl.Type.Params.List {
			for j, name := range param.Names {
				if i != 0 || j != 0 {
					write(", ")
				}
				write(name.Name)
				_, isEllipsis := param.Type.(*ast.Ellipsis)
				if isEllipsis {
					write("...")
				}
			}
		}
		write(")\n}\n")
		return
	})
}
