package cgo

// This file parses a fragment of C with libclang and stores the result for AST
// modification. It does not touch the AST itself.

import (
	"crypto/sha256"
	"crypto/sha512"
	"encoding/hex"
	"fmt"
	"go/ast"
	"go/scanner"
	"go/token"
	"path/filepath"
	"strconv"
	"strings"
	"unsafe"

	"tinygo.org/x/go-llvm"
)

/*
#include <clang-c/Index.h> // If this fails, libclang headers aren't available. Please take a look here: https://tinygo.org/docs/guides/build/
#include <llvm/Config/llvm-config.h>
#include <stdlib.h>
#include <stdint.h>

// This struct should be ABI-compatible on all platforms (uintptr_t has the same
// alignment etc. as void*) but does not include void* pointers that are not
// always real pointers.
// The Go garbage collector assumes that all non-nil pointer-typed integers are
// actually pointers. This is not always true, as data[1] often contains 0x1,
// which is clearly not a valid pointer. Usually the GC won't catch this issue,
// but occasionally it will leading to a crash with a vague error message.
typedef struct {
	enum CXCursorKind kind;
	int xdata;
	uintptr_t data[3];
} GoCXCursor;

// Forwarding functions. They are implemented in libclang_stubs.c and forward to
// the real functions without doing anything else, thus they are entirely
// compatible with the versions without tinygo_ prefix. The only difference is
// the CXCursor type, which has been replaced with GoCXCursor.
GoCXCursor tinygo_clang_getTranslationUnitCursor(CXTranslationUnit tu);
unsigned tinygo_clang_visitChildren(GoCXCursor parent, CXCursorVisitor visitor, CXClientData client_data);
CXString tinygo_clang_getCursorSpelling(GoCXCursor c);
CXString tinygo_clang_getCursorPrettyPrinted(GoCXCursor c, CXPrintingPolicy Policy);
CXPrintingPolicy tinygo_clang_getCursorPrintingPolicy(GoCXCursor c);
enum CXCursorKind tinygo_clang_getCursorKind(GoCXCursor c);
CXType tinygo_clang_getCursorType(GoCXCursor c);
GoCXCursor tinygo_clang_getTypeDeclaration(CXType t);
CXType tinygo_clang_getTypedefDeclUnderlyingType(GoCXCursor c);
CXType tinygo_clang_getCursorResultType(GoCXCursor c);
int tinygo_clang_Cursor_getNumArguments(GoCXCursor c);
GoCXCursor tinygo_clang_Cursor_getArgument(GoCXCursor c, unsigned i);
enum CX_StorageClass tinygo_clang_Cursor_getStorageClass(GoCXCursor c);
CXSourceLocation tinygo_clang_getCursorLocation(GoCXCursor c);
CXSourceRange tinygo_clang_getCursorExtent(GoCXCursor c);
CXTranslationUnit tinygo_clang_Cursor_getTranslationUnit(GoCXCursor c);
long long tinygo_clang_getEnumConstantDeclValue(GoCXCursor c);
CXType tinygo_clang_getEnumDeclIntegerType(GoCXCursor c);
unsigned tinygo_clang_Cursor_isAnonymous(GoCXCursor c);
unsigned tinygo_clang_Cursor_isBitField(GoCXCursor c);

int tinygo_clang_globals_visitor(GoCXCursor c, GoCXCursor parent, CXClientData client_data);
int tinygo_clang_struct_visitor(GoCXCursor c, GoCXCursor parent, CXClientData client_data);
int tinygo_clang_enum_visitor(GoCXCursor c, GoCXCursor parent, CXClientData client_data);
void tinygo_clang_inclusion_visitor(CXFile included_file, CXSourceLocation *inclusion_stack, unsigned include_len, CXClientData client_data);
*/
import "C"

// storedRefs stores references to types, used for clang_visitChildren.
var storedRefs refMap

var diagnosticSeverity = [...]string{
	C.CXDiagnostic_Ignored: "ignored",
	C.CXDiagnostic_Note:    "note",
	C.CXDiagnostic_Warning: "warning",
	C.CXDiagnostic_Error:   "error",
	C.CXDiagnostic_Fatal:   "fatal",
}

// Alias so that cgo.go (which doesn't import Clang related stuff and is in
// theory decoupled from Clang) can also use this type.
type clangCursor = C.GoCXCursor

func init() {
	// Check that we haven't messed up LLVM versioning.
	// This can happen when llvm_config_*.go files in either this or the
	// tinygo.org/x/go-llvm packages is incorrect. It should not ever happen
	// with byollvm.
	if C.LLVM_VERSION_STRING != llvm.Version {
		panic("incorrect build: using LLVM version " + llvm.Version + " in the tinygo.org/x/llvm package, and version " + C.LLVM_VERSION_STRING + " in the ./cgo package")
	}
}

func (f *cgoFile) readNames(fragment string, cflags []string, filename string, callback func(map[string]clangCursor)) {
	index := C.clang_createIndex(0, 0)
	defer C.clang_disposeIndex(index)

	// pretend to be a .c file
	filenameC := C.CString(filename + "!cgo.c")
	defer C.free(unsafe.Pointer(filenameC))

	fragmentC := C.CString(fragment)
	defer C.free(unsafe.Pointer(fragmentC))

	unsavedFile := C.struct_CXUnsavedFile{
		Filename: filenameC,
		Length:   C.ulong(len(fragment)),
		Contents: fragmentC,
	}

	// convert Go slice of strings to C array of strings.
	cmdargsC := C.malloc(C.size_t(len(cflags)) * C.size_t(unsafe.Sizeof(uintptr(0))))
	defer C.free(cmdargsC)
	cmdargs := (*[1 << 16]*C.char)(cmdargsC)
	for i, cflag := range cflags {
		s := C.CString(cflag)
		cmdargs[i] = s
		defer C.free(unsafe.Pointer(s))
	}

	var unit C.CXTranslationUnit
	errCode := C.clang_parseTranslationUnit2(
		index,
		filenameC,
		(**C.char)(cmdargsC), C.int(len(cflags)), // command line args
		&unsavedFile, 1, // unsaved files
		C.CXTranslationUnit_DetailedPreprocessingRecord,
		&unit)
	if errCode != 0 {
		// This is probably a bug in the usage of libclang.
		panic("cgo: failed to parse source with libclang")
	}
	defer C.clang_disposeTranslationUnit(unit)

	// Report parser and type errors.
	if numDiagnostics := int(C.clang_getNumDiagnostics(unit)); numDiagnostics != 0 {
		addDiagnostic := func(diagnostic C.CXDiagnostic) {
			spelling := getString(C.clang_getDiagnosticSpelling(diagnostic))
			severity := diagnosticSeverity[C.clang_getDiagnosticSeverity(diagnostic)]
			location := C.clang_getDiagnosticLocation(diagnostic)
			pos := f.getClangLocationPosition(location, unit)
			f.addError(pos, severity+": "+spelling)
		}
		for i := 0; i < numDiagnostics; i++ {
			diagnostic := C.clang_getDiagnostic(unit, C.uint(i))
			addDiagnostic(diagnostic)

			// Child diagnostics (like notes on redefinitions).
			diagnostics := C.clang_getChildDiagnostics(diagnostic)
			for j := 0; j < int(C.clang_getNumDiagnosticsInSet(diagnostics)); j++ {
				addDiagnostic(C.clang_getDiagnosticInSet(diagnostics, C.uint(j)))
			}
		}
	}

	// Extract information required by CGo.
	ref := storedRefs.Put(f)
	defer storedRefs.Remove(ref)
	cursor := C.tinygo_clang_getTranslationUnitCursor(unit)
	C.tinygo_clang_visitChildren(cursor, C.CXCursorVisitor(C.tinygo_clang_globals_visitor), C.CXClientData(ref))

	// Determine files read during CGo processing, for caching.
	inclusionCallback := func(includedFile C.CXFile) {
		// Get full file path.
		path := getString(C.clang_getFileName(includedFile))

		// Get contents of file (that should be in-memory).
		size := C.size_t(0)
		rawData := C.clang_getFileContents(unit, includedFile, &size)
		if rawData == nil {
			// Sanity check. This should (hopefully) never trigger.
			panic("libclang: file contents was not loaded")
		}
		data := (*[1 << 24]byte)(unsafe.Pointer(rawData))[:size]

		// Hash the contents if it isn't hashed yet.
		if _, ok := f.visitedFiles[path]; !ok {
			// already stored
			sum := sha512.Sum512_224(data)
			f.visitedFiles[path] = sum[:]
		}
	}
	inclusionCallbackRef := storedRefs.Put(inclusionCallback)
	defer storedRefs.Remove(inclusionCallbackRef)
	C.clang_getInclusions(unit, C.CXInclusionVisitor(C.tinygo_clang_inclusion_visitor), C.CXClientData(inclusionCallbackRef))

	// Do all the C AST operations inside a callback. This makes sure that
	// libclang related memory is only freed after it is not necessary anymore.
	callback(f.names)
}

// Convert the AST node under the given Clang cursor to a Go AST node and return
// it.
func (f *cgoFile) createASTNode(name string, c clangCursor) (ast.Node, any) {
	kind := C.tinygo_clang_getCursorKind(c)
	pos := f.getCursorPosition(c)
	switch kind {
	case C.CXCursor_FunctionDecl:
		cursorType := C.tinygo_clang_getCursorType(c)
		numArgs := int(C.tinygo_clang_Cursor_getNumArguments(c))
		obj := &ast.Object{
			Kind: ast.Fun,
			Name: "C." + name,
		}
		exportName := name
		localName := name
		var stringSignature string
		if C.tinygo_clang_Cursor_getStorageClass(c) == C.CX_SC_Static {
			// A static function is assigned a globally unique symbol name based
			// on the file path (like _Cgo_static_2d09198adbf58f4f4655_foo) and
			// has a different Go name in the form of C.foo!symbols.go instead
			// of just C.foo.
			path := f.importPath + "/" + filepath.Base(f.fset.File(f.file.Pos()).Name())
			staticIDBuf := sha256.Sum256([]byte(path))
			staticID := hex.EncodeToString(staticIDBuf[:10])
			exportName = "_Cgo_static_" + staticID + "_" + name
			localName = name + "!" + filepath.Base(path)

			// Create a signature. This is necessary for MacOS to forward the
			// call, because MacOS doesn't support aliases like ELF and PE do.
			// (There is N_INDR but __attribute__((alias("..."))) doesn't work).
			policy := C.tinygo_clang_getCursorPrintingPolicy(c)
			defer C.clang_PrintingPolicy_dispose(policy)
			C.clang_PrintingPolicy_setProperty(policy, C.CXPrintingPolicy_TerseOutput, 1)
			stringSignature = getString(C.tinygo_clang_getCursorPrettyPrinted(c, policy))
			stringSignature = strings.Replace(stringSignature, " "+name+"(", " "+exportName+"(", 1)
			stringSignature = strings.TrimPrefix(stringSignature, "static ")
		}
		args := make([]*ast.Field, numArgs)
		decl := &ast.FuncDecl{
			Doc: &ast.CommentGroup{
				List: []*ast.Comment{
					{
						Slash: pos - 1,
						Text:  "//export " + exportName,
					},
				},
			},
			Name: &ast.Ident{
				NamePos: pos,
				Name:    "C." + localName,
				Obj:     obj,
			},
			Type: &ast.FuncType{
				Func: pos,
				Params: &ast.FieldList{
					Opening: pos,
					List:    args,
					Closing: pos,
				},
			},
		}
		if C.clang_isFunctionTypeVariadic(cursorType) != 0 {
			decl.Doc.List = append(decl.Doc.List, &ast.Comment{
				Slash: pos - 1,
				Text:  "//go:variadic",
			})
		}
		for i := 0; i < numArgs; i++ {
			arg := C.tinygo_clang_Cursor_getArgument(c, C.uint(i))
			argName := getString(C.tinygo_clang_getCursorSpelling(arg))
			argType := C.clang_getArgType(cursorType, C.uint(i))
			if argName == "" {
				argName = "$" + strconv.Itoa(i)
			}
			args[i] = &ast.Field{
				Names: []*ast.Ident{
					{
						NamePos: pos,
						Name:    argName,
						Obj: &ast.Object{
							Kind: ast.Var,
							Name: argName,
							Decl: decl,
						},
					},
				},
				Type: f.makeDecayingASTType(argType, pos),
			}
		}
		resultType := C.tinygo_clang_getCursorResultType(c)
		if resultType.kind != C.CXType_Void {
			decl.Type.Results = &ast.FieldList{
				List: []*ast.Field{
					{
						Type: f.makeASTType(resultType, pos),
					},
				},
			}
		}
		obj.Decl = decl
		return decl, stringSignature
	case C.CXCursor_StructDecl, C.CXCursor_UnionDecl:
		typ := f.makeASTRecordType(c, pos)
		typeName := "C." + name
		typeExpr := typ.typeExpr
		if typ.unionSize != 0 {
			// Convert to a single-field struct type.
			typeExpr = f.makeUnionField(typ)
		}
		obj := &ast.Object{
			Kind: ast.Typ,
			Name: typeName,
		}
		typeSpec := &ast.TypeSpec{
			Name: &ast.Ident{
				NamePos: typ.pos,
				Name:    typeName,
				Obj:     obj,
			},
			Type: typeExpr,
		}
		obj.Decl = typeSpec
		return typeSpec, typ
	case C.CXCursor_TypedefDecl:
		typeName := "C." + name
		underlyingType := C.tinygo_clang_getTypedefDeclUnderlyingType(c)
		obj := &ast.Object{
			Kind: ast.Typ,
			Name: typeName,
		}
		typeSpec := &ast.TypeSpec{
			Name: &ast.Ident{
				NamePos: pos,
				Name:    typeName,
				Obj:     obj,
			},
			Type: f.makeASTType(underlyingType, pos),
		}
		if underlyingType.kind != C.CXType_Enum {
			typeSpec.Assign = pos
		}
		obj.Decl = typeSpec
		return typeSpec, nil
	case C.CXCursor_VarDecl:
		cursorType := C.tinygo_clang_getCursorType(c)
		typeExpr := f.makeASTType(cursorType, pos)
		gen := &ast.GenDecl{
			TokPos: pos,
			Tok:    token.VAR,
			Lparen: token.NoPos,
			Rparen: token.NoPos,
			Doc: &ast.CommentGroup{
				List: []*ast.Comment{
					{
						Slash: pos - 1,
						Text:  "//go:extern " + name,
					},
				},
			},
		}
		obj := &ast.Object{
			Kind: ast.Var,
			Name: "C." + name,
		}
		valueSpec := &ast.ValueSpec{
			Names: []*ast.Ident{{
				NamePos: pos,
				Name:    "C." + name,
				Obj:     obj,
			}},
			Type: typeExpr,
		}
		obj.Decl = valueSpec
		gen.Specs = append(gen.Specs, valueSpec)
		return gen, nil
	case C.CXCursor_MacroDefinition:
		sourceRange := C.tinygo_clang_getCursorExtent(c)
		start := C.clang_getRangeStart(sourceRange)
		end := C.clang_getRangeEnd(sourceRange)
		var file, endFile C.CXFile
		var startOffset, endOffset C.unsigned
		C.clang_getExpansionLocation(start, &file, nil, nil, &startOffset)
		if file == nil {
			f.addError(pos, "internal error: could not find file where macro is defined")
			return nil, nil
		}
		C.clang_getExpansionLocation(end, &endFile, nil, nil, &endOffset)
		if file != endFile {
			f.addError(pos, "internal error: expected start and end location of a macro to be in the same file")
			return nil, nil
		}
		if startOffset > endOffset {
			f.addError(pos, "internal error: start offset of macro is after end offset")
			return nil, nil
		}

		// read file contents and extract the relevant byte range
		tu := C.tinygo_clang_Cursor_getTranslationUnit(c)
		var size C.size_t
		sourcePtr := C.clang_getFileContents(tu, file, &size)
		if endOffset >= C.uint(size) {
			f.addError(pos, "internal error: end offset of macro lies after end of file")
			return nil, nil
		}
		source := string(((*[1 << 28]byte)(unsafe.Pointer(sourcePtr)))[startOffset:endOffset:endOffset])
		if !strings.HasPrefix(source, name) {
			f.addError(pos, fmt.Sprintf("internal error: expected macro value to start with %#v, got %#v", name, source))
			return nil, nil
		}
		value := source[len(name):]
		// Try to convert this #define into a Go constant expression.
		expr, scannerError := parseConst(pos+token.Pos(len(name)), f.fset, value)
		if scannerError != nil {
			f.errors = append(f.errors, *scannerError)
			return nil, nil
		}

		gen := &ast.GenDecl{
			TokPos: token.NoPos,
			Tok:    token.CONST,
			Lparen: token.NoPos,
			Rparen: token.NoPos,
		}
		obj := &ast.Object{
			Kind: ast.Con,
			Name: "C." + name,
		}
		valueSpec := &ast.ValueSpec{
			Names: []*ast.Ident{{
				NamePos: pos,
				Name:    "C." + name,
				Obj:     obj,
			}},
			Values: []ast.Expr{expr},
		}
		obj.Decl = valueSpec
		gen.Specs = append(gen.Specs, valueSpec)
		return gen, nil
	case C.CXCursor_EnumDecl:
		obj := &ast.Object{
			Kind: ast.Typ,
			Name: "C." + name,
		}
		underlying := C.tinygo_clang_getEnumDeclIntegerType(c)
		// TODO: gc's CGo implementation uses types such as `uint32` for enums
		// instead of types such as C.int, which are used here.
		typeSpec := &ast.TypeSpec{
			Name: &ast.Ident{
				NamePos: pos,
				Name:    "C." + name,
				Obj:     obj,
			},
			Assign: pos,
			Type:   f.makeASTType(underlying, pos),
		}
		obj.Decl = typeSpec
		return typeSpec, nil
	case C.CXCursor_EnumConstantDecl:
		value := C.tinygo_clang_getEnumConstantDeclValue(c)
		expr := &ast.BasicLit{
			ValuePos: pos,
			Kind:     token.INT,
			Value:    strconv.FormatInt(int64(value), 10),
		}
		gen := &ast.GenDecl{
			TokPos: token.NoPos,
			Tok:    token.CONST,
			Lparen: token.NoPos,
			Rparen: token.NoPos,
		}
		obj := &ast.Object{
			Kind: ast.Con,
			Name: "C." + name,
		}
		valueSpec := &ast.ValueSpec{
			Names: []*ast.Ident{{
				NamePos: pos,
				Name:    "C." + name,
				Obj:     obj,
			}},
			Values: []ast.Expr{expr},
		}
		obj.Decl = valueSpec
		gen.Specs = append(gen.Specs, valueSpec)
		return gen, nil
	default:
		f.addError(pos, fmt.Sprintf("internal error: unknown cursor type: %d", kind))
		return nil, nil
	}
}

func getString(clangString C.CXString) (s string) {
	rawString := C.clang_getCString(clangString)
	s = C.GoString(rawString)
	C.clang_disposeString(clangString)
	return
}

//export tinygo_clang_globals_visitor
func tinygo_clang_globals_visitor(c, parent C.GoCXCursor, client_data C.CXClientData) C.int {
	f := storedRefs.Get(unsafe.Pointer(client_data)).(*cgoFile)
	switch C.tinygo_clang_getCursorKind(c) {
	case C.CXCursor_FunctionDecl:
		name := getString(C.tinygo_clang_getCursorSpelling(c))
		f.names[name] = c
	case C.CXCursor_StructDecl:
		name := getString(C.tinygo_clang_getCursorSpelling(c))
		if name != "" {
			f.names["struct_"+name] = c
		}
	case C.CXCursor_UnionDecl:
		name := getString(C.tinygo_clang_getCursorSpelling(c))
		if name != "" {
			f.names["union_"+name] = c
		}
	case C.CXCursor_TypedefDecl:
		typedefType := C.tinygo_clang_getCursorType(c)
		name := getString(C.clang_getTypedefName(typedefType))
		f.names[name] = c
	case C.CXCursor_VarDecl:
		name := getString(C.tinygo_clang_getCursorSpelling(c))
		f.names[name] = c
	case C.CXCursor_MacroDefinition:
		name := getString(C.tinygo_clang_getCursorSpelling(c))
		f.names[name] = c
	case C.CXCursor_EnumDecl:
		name := getString(C.tinygo_clang_getCursorSpelling(c))
		if name != "" {
			// Named enum, which can be referenced from Go using C.enum_foo.
			f.names["enum_"+name] = c
		}
		// The enum fields are in global scope, so recurse to visit them.
		return C.CXChildVisit_Recurse
	case C.CXCursor_EnumConstantDecl:
		// We arrive here because of the "Recurse" above.
		name := getString(C.tinygo_clang_getCursorSpelling(c))
		f.names[name] = c
	}
	return C.CXChildVisit_Continue
}

// Get the precise location in the source code. Used for uniquely identifying
// source locations.
func (f *cgoFile) getUniqueLocationID(pos token.Pos, cursor C.GoCXCursor) interface{} {
	clangLocation := C.tinygo_clang_getCursorLocation(cursor)
	var file C.CXFile
	var line C.unsigned
	var column C.unsigned
	C.clang_getFileLocation(clangLocation, &file, &line, &column, nil)
	location := token.Position{
		Filename: getString(C.clang_getFileName(file)),
		Line:     int(line),
		Column:   int(column),
	}
	if location.Filename == "" || location.Line == 0 {
		// Not sure when this would happen, but protect from it anyway.
		f.addError(pos, "could not find file/line information")
	}
	return location
}

// getCursorPosition returns a usable token.Pos from a libclang cursor.
func (p *cgoPackage) getCursorPosition(cursor C.GoCXCursor) token.Pos {
	return p.getClangLocationPosition(C.tinygo_clang_getCursorLocation(cursor), C.tinygo_clang_Cursor_getTranslationUnit(cursor))
}

// getClangLocationPosition returns a usable token.Pos based on a libclang
// location and translation unit. If the file for this cursor has not been seen
// before, it is read from libclang (which already has the file in memory) and
// added to the token.FileSet.
func (p *cgoPackage) getClangLocationPosition(location C.CXSourceLocation, tu C.CXTranslationUnit) token.Pos {
	var file C.CXFile
	var line C.unsigned
	var column C.unsigned
	var offset C.unsigned
	C.clang_getExpansionLocation(location, &file, &line, &column, &offset)
	if line == 0 || file == nil {
		// Invalid token.
		return token.NoPos
	}
	filename := getString(C.clang_getFileName(file))
	if _, ok := p.tokenFiles[filename]; !ok {
		// File has not been seen before in this package, add line information
		// now by reading the file from libclang.
		var size C.size_t
		sourcePtr := C.clang_getFileContents(tu, file, &size)
		source := ((*[1 << 28]byte)(unsafe.Pointer(sourcePtr)))[:size:size]
		lines := []int{0}
		for i := 0; i < len(source)-1; i++ {
			if source[i] == '\n' {
				lines = append(lines, i+1)
			}
		}
		f := p.fset.AddFile(filename, -1, int(size))
		f.SetLines(lines)
		p.tokenFiles[filename] = f
		// Add dummy file AST, to satisfy the type checker.
		astFile := &ast.File{
			Package: f.Pos(0),
			Name:    ast.NewIdent(p.packageName),
		}
		setASTFileFields(astFile, f.Pos(0), f.Pos(int(size)))
		p.cgoFiles = append(p.cgoFiles, astFile)
	}
	positionFile := p.tokenFiles[filename]

	// Check for alternative line/column information (set with a line directive).
	var filename2String C.CXString
	var line2 C.unsigned
	var column2 C.unsigned
	C.clang_getPresumedLocation(location, &filename2String, &line2, &column2)
	filename2 := getString(filename2String)
	if filename2 != filename || line2 != line || column2 != column {
		// The location was changed with a preprocessor directive.
		// TODO: this only works for locations that are added in order. Adding
		// line/column info to a file that already has line/column info after
		// the given offset is ignored.
		positionFile.AddLineColumnInfo(int(offset), filename2, int(line2), int(column2))
	}

	return positionFile.Pos(int(offset))
}

// addError is a utility function to add an error to the list of errors. It will
// convert the token position to a line/column position first, and call
// addErrorAt.
func (p *cgoPackage) addError(pos token.Pos, msg string) {
	p.addErrorAt(p.fset.PositionFor(pos, true), msg)
}

// addErrorAfter is like addError, but adds the text `after` to the source
// location.
func (p *cgoPackage) addErrorAfter(pos token.Pos, after, msg string) {
	position := p.fset.PositionFor(pos, true)
	lines := strings.Split(after, "\n")
	if len(lines) != 1 {
		// Adjust lines.
		// For why we can't just do pos+token.Pos(len(after)), see:
		// https://github.com/golang/go/issues/35803
		position.Line += len(lines) - 1
		position.Column = len(lines[len(lines)-1]) + 1
	} else {
		position.Column += len(after)
	}
	p.addErrorAt(position, msg)
}

// addErrorAt is a utility function to add an error to the list of errors.
func (p *cgoPackage) addErrorAt(position token.Position, msg string) {
	if filepath.IsAbs(position.Filename) {
		// Relative paths for readability, like other Go parser errors.
		relpath, err := filepath.Rel(p.currentDir, position.Filename)
		if err == nil {
			position.Filename = relpath
		}
	}
	p.errors = append(p.errors, scanner.Error{
		Pos: position,
		Msg: msg,
	})
}

// makeDecayingASTType does the same as makeASTType but takes care of decaying
// types (arrays in function parameters, etc). It is otherwise identical to
// makeASTType.
func (f *cgoFile) makeDecayingASTType(typ C.CXType, pos token.Pos) ast.Expr {
	// Strip typedefs, if any.
	underlyingType := typ
	if underlyingType.kind == C.CXType_Elaborated {
		// Starting with LLVM 16, the elaborated type is used for more types.
		// According to the Clang documentation, the elaborated type has no
		// semantic meaning so can be stripped (it is used to better convey type
		// name information).
		// Source:
		// https://clang.llvm.org/doxygen/classclang_1_1ElaboratedType.html#details
		// > The type itself is always "sugar", used to express what was written
		// > in the source code but containing no additional semantic information.
		underlyingType = C.clang_Type_getNamedType(underlyingType)
	}
	if underlyingType.kind == C.CXType_Typedef {
		c := C.tinygo_clang_getTypeDeclaration(underlyingType)
		underlyingType = C.tinygo_clang_getTypedefDeclUnderlyingType(c)
		// TODO: support a chain of typedefs. At the moment, it seems to get
		// stuck in an endless loop when trying to get to the most underlying
		// type.
	}
	// Check for decaying type. An example would be an array type in a
	// parameter. This declaration:
	//   void foo(char buf[6]);
	// is the same as this one:
	//   void foo(char *buf);
	// But this one:
	//   void bar(char buf[6][4]);
	// equals this:
	//   void bar(char *buf[4]);
	// so not all array dimensions should be stripped, just the first one.
	// TODO: there are more kinds of decaying types.
	if underlyingType.kind == C.CXType_ConstantArray {
		// Apply type decaying.
		pointeeType := C.clang_getElementType(underlyingType)
		return &ast.StarExpr{
			Star: pos,
			X:    f.makeASTType(pointeeType, pos),
		}
	}
	return f.makeASTType(typ, pos)
}

// makeASTType return the ast.Expr for the given libclang type. In other words,
// it converts a libclang type to a type in the Go AST.
func (f *cgoFile) makeASTType(typ C.CXType, pos token.Pos) ast.Expr {
	var typeName string
	switch typ.kind {
	case C.CXType_Char_S, C.CXType_Char_U:
		typeName = "C.char"
	case C.CXType_SChar:
		typeName = "C.schar"
	case C.CXType_UChar:
		typeName = "C.uchar"
	case C.CXType_Short:
		typeName = "C.short"
	case C.CXType_UShort:
		typeName = "C.ushort"
	case C.CXType_Int:
		typeName = "C.int"
	case C.CXType_UInt:
		typeName = "C.uint"
	case C.CXType_Long:
		typeName = "C.long"
	case C.CXType_ULong:
		typeName = "C.ulong"
	case C.CXType_LongLong:
		typeName = "C.longlong"
	case C.CXType_ULongLong:
		typeName = "C.ulonglong"
	case C.CXType_Bool:
		typeName = "bool"
	case C.CXType_Float, C.CXType_Double, C.CXType_LongDouble:
		switch C.clang_Type_getSizeOf(typ) {
		case 4:
			typeName = "float32"
		case 8:
			typeName = "float64"
		default:
			// Don't do anything, rely on the fallback code to show a somewhat
			// sensible error message like "undeclared name: C.long double".
		}
	case C.CXType_Complex:
		switch C.clang_Type_getSizeOf(typ) {
		case 8:
			typeName = "complex64"
		case 16:
			typeName = "complex128"
		}
	case C.CXType_Pointer:
		pointeeType := C.clang_getPointeeType(typ)
		if pointeeType.kind == C.CXType_Void {
			// void* type is translated to Go as unsafe.Pointer
			return &ast.SelectorExpr{
				X: &ast.Ident{
					NamePos: pos,
					Name:    "unsafe",
				},
				Sel: &ast.Ident{
					NamePos: pos,
					Name:    "Pointer",
				},
			}
		}
		return &ast.StarExpr{
			Star: pos,
			X:    f.makeASTType(pointeeType, pos),
		}
	case C.CXType_ConstantArray:
		return &ast.ArrayType{
			Lbrack: pos,
			Len: &ast.BasicLit{
				ValuePos: pos,
				Kind:     token.INT,
				Value:    strconv.FormatInt(int64(C.clang_getArraySize(typ)), 10),
			},
			Elt: f.makeASTType(C.clang_getElementType(typ), pos),
		}
	case C.CXType_FunctionProto:
		// Be compatible with gc, which uses the *[0]byte type for function
		// pointer types.
		// Return type [0]byte because this is a function type, not a pointer to
		// this function type.
		return &ast.ArrayType{
			Lbrack: pos,
			Len: &ast.BasicLit{
				ValuePos: pos,
				Kind:     token.INT,
				Value:    "0",
			},
			Elt: &ast.Ident{
				NamePos: pos,
				Name:    "byte",
			},
		}
	case C.CXType_Typedef:
		name := getString(C.clang_getTypedefName(typ))
		c := C.tinygo_clang_getTypeDeclaration(typ)
		return &ast.Ident{
			NamePos: pos,
			Name:    f.getASTDeclName(name, c, false),
		}
	case C.CXType_Elaborated:
		underlying := C.clang_Type_getNamedType(typ)
		switch underlying.kind {
		case C.CXType_Record:
			return f.makeASTType(underlying, pos)
		case C.CXType_Enum:
			return f.makeASTType(underlying, pos)
		case C.CXType_Typedef:
			return f.makeASTType(underlying, pos)
		default:
			typeKindSpelling := getString(C.clang_getTypeKindSpelling(underlying.kind))
			f.addError(pos, fmt.Sprintf("unknown elaborated type (libclang type kind %s)", typeKindSpelling))
			typeName = "<unknown>"
		}
	case C.CXType_Record:
		cursor := C.tinygo_clang_getTypeDeclaration(typ)
		name := getString(C.tinygo_clang_getCursorSpelling(cursor))
		var cgoRecordPrefix string
		switch C.tinygo_clang_getCursorKind(cursor) {
		case C.CXCursor_StructDecl:
			cgoRecordPrefix = "struct_"
		case C.CXCursor_UnionDecl:
			cgoRecordPrefix = "union_"
		default:
			// makeASTRecordType will create an appropriate error.
			cgoRecordPrefix = "record_"
		}
		if name == "" || C.tinygo_clang_Cursor_isAnonymous(cursor) != 0 {
			// Anonymous record, probably inside a typedef.
			location := f.getUniqueLocationID(pos, cursor)
			name = f.getUnnamedDeclName("_Ctype_"+cgoRecordPrefix+"__", location)
		} else {
			name = cgoRecordPrefix + name
		}
		return &ast.Ident{
			NamePos: pos,
			Name:    f.getASTDeclName(name, cursor, false),
		}
	case C.CXType_Enum:
		cursor := C.tinygo_clang_getTypeDeclaration(typ)
		name := getString(C.tinygo_clang_getCursorSpelling(cursor))
		if name == "" {
			// Anonymous enum, probably inside a typedef.
			location := f.getUniqueLocationID(pos, cursor)
			name = f.getUnnamedDeclName("_Ctype_enum___", location)
		} else {
			name = "enum_" + name
		}
		return &ast.Ident{
			NamePos: pos,
			Name:    f.getASTDeclName(name, cursor, false),
		}
	}
	if typeName == "" {
		// Report this as an error.
		typeSpelling := getString(C.clang_getTypeSpelling(typ))
		typeKindSpelling := getString(C.clang_getTypeKindSpelling(typ.kind))
		f.addError(pos, fmt.Sprintf("unknown C type: %v (libclang type kind %s)", typeSpelling, typeKindSpelling))
		typeName = "C.<unknown>"
	}
	return &ast.Ident{
		NamePos: pos,
		Name:    typeName,
	}
}

// getIntegerType returns an AST node that defines types such as C.int.
func (p *cgoPackage) getIntegerType(name string, cursor clangCursor) *ast.TypeSpec {
	pos := p.getCursorPosition(cursor)

	// Find a Go type that matches the size and signedness of the given C type.
	underlyingType := C.tinygo_clang_getTypedefDeclUnderlyingType(cursor)
	var goName string
	typeSize := C.clang_Type_getSizeOf(underlyingType)
	switch name {
	case "C.char":
		if typeSize != 1 {
			// This happens for some very special purpose architectures
			// (DSPs etc.) that are not currently targeted.
			// https://www.embecosm.com/2017/04/18/non-8-bit-char-support-in-clang-and-llvm/
			p.addError(pos, fmt.Sprintf("unknown char width: %d", typeSize))
		}
		switch underlyingType.kind {
		case C.CXType_Char_S:
			goName = "int8"
		case C.CXType_Char_U:
			goName = "uint8"
		}
	case "C.schar", "C.short", "C.int", "C.long", "C.longlong":
		switch typeSize {
		case 1:
			goName = "int8"
		case 2:
			goName = "int16"
		case 4:
			goName = "int32"
		case 8:
			goName = "int64"
		}
	case "C.uchar", "C.ushort", "C.uint", "C.ulong", "C.ulonglong":
		switch typeSize {
		case 1:
			goName = "uint8"
		case 2:
			goName = "uint16"
		case 4:
			goName = "uint32"
		case 8:
			goName = "uint64"
		}
	}

	if goName == "" { // should not happen
		p.addError(pos, "internal error: did not find Go type for C type "+name)
		goName = "int"
	}

	// Construct an *ast.TypeSpec for this type.
	obj := &ast.Object{
		Kind: ast.Typ,
		Name: name,
	}
	spec := &ast.TypeSpec{
		Name: &ast.Ident{
			NamePos: pos,
			Name:    name,
			Obj:     obj,
		},
		Type: &ast.Ident{
			NamePos: pos,
			Name:    goName,
		},
	}
	obj.Decl = spec
	return spec
}

// makeASTRecordType parses a C record (struct or union) and translates it into
// a Go struct type.
func (f *cgoFile) makeASTRecordType(cursor C.GoCXCursor, pos token.Pos) *elaboratedTypeInfo {
	fieldList := &ast.FieldList{
		Opening: pos,
		Closing: pos,
	}
	var bitfieldList []bitfieldInfo
	inBitfield := false
	bitfieldNum := 0
	ref := storedRefs.Put(struct {
		fieldList    *ast.FieldList
		file         *cgoFile
		inBitfield   *bool
		bitfieldNum  *int
		bitfieldList *[]bitfieldInfo
	}{fieldList, f, &inBitfield, &bitfieldNum, &bitfieldList})
	defer storedRefs.Remove(ref)
	C.tinygo_clang_visitChildren(cursor, C.CXCursorVisitor(C.tinygo_clang_struct_visitor), C.CXClientData(ref))
	renameFieldKeywords(fieldList)
	switch C.tinygo_clang_getCursorKind(cursor) {
	case C.CXCursor_StructDecl:
		return &elaboratedTypeInfo{
			typeExpr: &ast.StructType{
				Struct: pos,
				Fields: fieldList,
			},
			pos:       pos,
			bitfields: bitfieldList,
		}
	case C.CXCursor_UnionDecl:
		typeInfo := &elaboratedTypeInfo{
			typeExpr: &ast.StructType{
				Struct: pos,
				Fields: fieldList,
			},
			pos:       pos,
			bitfields: bitfieldList,
		}
		if len(fieldList.List) <= 1 {
			// Useless union, treat it as a regular struct.
			return typeInfo
		}
		if bitfieldList != nil {
			// This is valid C... but please don't do this.
			f.addError(pos, "bitfield in a union is not supported")
		}
		typ := C.tinygo_clang_getCursorType(cursor)
		alignInBytes := int64(C.clang_Type_getAlignOf(typ))
		sizeInBytes := int64(C.clang_Type_getSizeOf(typ))
		if sizeInBytes == 0 {
			f.addError(pos, "zero-length union is not supported")
		}
		typeInfo.unionSize = sizeInBytes
		typeInfo.unionAlign = alignInBytes
		return typeInfo
	default:
		cursorKind := C.tinygo_clang_getCursorKind(cursor)
		cursorKindSpelling := getString(C.clang_getCursorKindSpelling(cursorKind))
		f.addError(pos, fmt.Sprintf("expected StructDecl or UnionDecl, not %s", cursorKindSpelling))
		return &elaboratedTypeInfo{
			typeExpr: &ast.StructType{
				Struct: pos,
			},
			pos: pos,
		}
	}
}

//export tinygo_clang_struct_visitor
func tinygo_clang_struct_visitor(c, parent C.GoCXCursor, client_data C.CXClientData) C.int {
	passed := storedRefs.Get(unsafe.Pointer(client_data)).(struct {
		fieldList    *ast.FieldList
		file         *cgoFile
		inBitfield   *bool
		bitfieldNum  *int
		bitfieldList *[]bitfieldInfo
	})
	fieldList := passed.fieldList
	f := passed.file
	inBitfield := passed.inBitfield
	bitfieldNum := passed.bitfieldNum
	bitfieldList := passed.bitfieldList
	pos := f.getCursorPosition(c)
	switch cursorKind := C.tinygo_clang_getCursorKind(c); cursorKind {
	case C.CXCursor_FieldDecl:
		// Expected. This is a regular field.
	case C.CXCursor_StructDecl, C.CXCursor_UnionDecl:
		// Ignore. The next field will be the struct/union itself.
		return C.CXChildVisit_Continue
	default:
		cursorKindSpelling := getString(C.clang_getCursorKindSpelling(cursorKind))
		f.addError(pos, fmt.Sprintf("expected FieldDecl in struct or union, not %s", cursorKindSpelling))
		return C.CXChildVisit_Continue
	}
	name := getString(C.tinygo_clang_getCursorSpelling(c))
	if name == "" {
		// Assume this is a bitfield of 0 bits.
		// Warning: this is not necessarily true!
		return C.CXChildVisit_Continue
	}
	typ := C.tinygo_clang_getCursorType(c)
	field := &ast.Field{
		Type: f.makeASTType(typ, f.getCursorPosition(c)),
	}
	offsetof := int64(C.clang_Type_getOffsetOf(C.tinygo_clang_getCursorType(parent), C.CString(name)))
	alignOf := int64(C.clang_Type_getAlignOf(typ) * 8)
	bitfieldOffset := offsetof % alignOf
	if bitfieldOffset != 0 {
		if C.tinygo_clang_Cursor_isBitField(c) != 1 {
			f.addError(pos, "expected a bitfield")
			return C.CXChildVisit_Continue
		}
		if !*inBitfield {
			*bitfieldNum++
		}
		bitfieldName := "__bitfield_" + strconv.Itoa(*bitfieldNum)
		prevField := fieldList.List[len(fieldList.List)-1]
		if !*inBitfield {
			// The previous element also was a bitfield, but wasn't noticed
			// then. Add it now.
			*inBitfield = true
			*bitfieldList = append(*bitfieldList, bitfieldInfo{
				field:    prevField,
				name:     prevField.Names[0].Name,
				startBit: 0,
				pos:      prevField.Names[0].NamePos,
			})
			prevField.Names[0].Name = bitfieldName
			prevField.Names[0].Obj.Name = bitfieldName
		}
		prevBitfield := &(*bitfieldList)[len(*bitfieldList)-1]
		prevBitfield.endBit = bitfieldOffset
		*bitfieldList = append(*bitfieldList, bitfieldInfo{
			field:    prevField,
			name:     name,
			startBit: bitfieldOffset,
			pos:      pos,
		})
		return C.CXChildVisit_Continue
	}
	*inBitfield = false
	field.Names = []*ast.Ident{
		{
			NamePos: pos,
			Name:    name,
			Obj: &ast.Object{
				Kind: ast.Var,
				Name: name,
				Decl: field,
			},
		},
	}
	fieldList.List = append(fieldList.List, field)
	return C.CXChildVisit_Continue
}

//export tinygo_clang_inclusion_visitor
func tinygo_clang_inclusion_visitor(includedFile C.CXFile, inclusionStack *C.CXSourceLocation, includeLen C.unsigned, clientData C.CXClientData) {
	callback := storedRefs.Get(unsafe.Pointer(clientData)).(func(C.CXFile))
	callback(includedFile)
}
