/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package main

import (
	"bytes"
	"flag"
	"fmt"
	"go/ast"
	"go/format"
	"go/token"
	"go/types"
	"log"
	"os"
	"path/filepath"
	"strings"

	"golang.org/x/tools/go/packages"

	"github.com/apache/plc4x/plc4go/tools/common"
)

var (
	typeNames   = flag.String("type", "", "comma-separated list of type names; must be set")
	output      = flag.String("output", "", "output file name; default srcdir/<type>_plc4xgen.go")
	prefix      = flag.String("prefix", "", "prefix for all generated files")
	suffix      = flag.String("suffix", "", "suffix for all generated files")
	buildTags   = flag.String("tags", "", "comma-separated list of build tags to apply")
	licenseFile = flag.String("licenseFile", ".plc4xLicencer.header", "file containing the license (will be searched upwards)")
	tests       = flag.Bool("tests", false, "look at test files")
	pkgIndex    = flag.Int("pkgIndex", -1, "package index (can enforce a different index if multiple packages are found)")
	verbose     = flag.Bool("verbose", false, "verbosity")
)

// Usage is a replacement usage function for the flags package.
func Usage() {
	_, _ = fmt.Fprintf(os.Stderr, "Usage of plc4xGenerator:\n")
	_, _ = fmt.Fprintf(os.Stderr, "\tplc4xGenerator [flags] -type T [directory]\n")
	_, _ = fmt.Fprintf(os.Stderr, "\tplc4xGenerator [flags] -type T files... # Must be a single package\n")
	_, _ = fmt.Fprintf(os.Stderr, "Flags:\n")
	flag.PrintDefaults()
}

var outputFile string // only used for test

func main() {
	log.SetFlags(0)
	log.SetPrefix("plc4xGenerator: ")
	flag.Usage = Usage
	flag.Parse()
	if len(*typeNames) == 0 {
		flag.Usage()
		os.Exit(2)
	}
	typeList := strings.Split(*typeNames, ",")
	var tags []string
	if len(*buildTags) > 0 {
		tags = strings.Split(*buildTags, ",")
	}

	args := flag.Args()
	if len(args) == 0 {
		args = []string{"."}
	}

	// Parse the package once.
	var dir string
	generator := Generator{}
	if len(args) == 1 && isDirectory(args[0]) {
		dir = args[0]
	} else {
		if len(tags) != 0 {
			log.Fatal("-tags option applies only to directories, not when files are specified")
		}
		dir = filepath.Dir(args[0])
	}

	generator.parsePackage(args, tags)

	// Print the header and package clause.
	licenseFileName := *licenseFile
	licenceContent := common.GetLicenseFileContent(licenseFileName, *verbose)
	generator.Printf("%s", licenceContent)
	generator.Printf("\n")
	generator.Printf("// Code generated by \"plc4xGenerator %s\"; DO NOT EDIT.\n", strings.Join(os.Args[1:], " "))
	generator.Printf("\n")
	generator.Printf("package %s", generator.pkg.name)
	generator.Printf("\n")
	generator.Printf("import (\n")
	generator.Printf("\t\"context\"\n")
	generator.Printf("\t\"encoding/binary\"\n")
	generator.Printf("")
	generator.Printf("\t\"github.com/apache/plc4x/plc4go/spi/utils\"\n")
	generator.Printf("\t\"fmt\"\n")
	generator.Printf(")\n")
	generator.Printf("\n")
	generator.Printf("var _ = fmt.Printf\n")

	// Run generate for each type.
	for _, typeName := range typeList {
		generator.generate(typeName)
	}

	// Format the output.
	src := generator.format()

	// Write to file.
	outputName := *output
	if outputName == "" {
		baseName := fmt.Sprintf("%s_plc4xgen.go", typeList[0])
		outputName = filepath.Join(dir, baseName)
	}
	if *prefix != "" {
		directory, file := filepath.Split(outputName)
		outputName = filepath.Join(directory, *prefix+file)
	}

	if *suffix != "" {
		directory, file := filepath.Split(outputName)
		ext := filepath.Ext(file)
		outputName = filepath.Join(directory, strings.ReplaceAll(file, ext, "")+*suffix+ext)
	}

	log.Printf("Writing to %s\n", outputName)
	err := os.WriteFile(outputName, src, 0644)
	if err != nil {
		log.Fatalf("writing output: %s", err)
	}
	outputFile = outputName
}

// isDirectory reports whether the named file is a directory.
func isDirectory(name string) bool {
	info, err := os.Stat(name)
	if err != nil {
		log.Fatal(err)
	}
	return info.IsDir()
}

// Generator holds the state of the analysis. Primarily used to buffer
// the output for format.Source.
type Generator struct {
	buf bytes.Buffer // Accumulated output.
	pkg *Package     // Package we are scanning.
}

func (g *Generator) Printf(format string, args ...any) {
	_, _ = fmt.Fprintf(&g.buf, format, args...)
}

// File holds a single parsed file and associated data.
type File struct {
	pkg  *Package  // Package to which this file belongs.
	file *ast.File // Parsed AST.
	// These fields are reset for each type being generated.
	typeName string  // Name of the type.
	fields   []Field // Accumulator for fields of that type.

	trimPrefix  string
	lineComment bool
}

type Package struct {
	name  string
	defs  map[*ast.Ident]types.Object
	files []*File
}

// parsePackage analyzes the single package constructed from the patterns and tags.
// parsePackage exits if there is an error.
func (g *Generator) parsePackage(patterns []string, tags []string) {
	cfg := &packages.Config{
		Mode:       packages.NeedName | packages.NeedTypes | packages.NeedTypesInfo | packages.NeedSyntax,
		Tests:      *tests,
		BuildFlags: []string{fmt.Sprintf("-tags=%s", strings.Join(tags, " "))},
	}
	pkgs, err := packages.Load(cfg, patterns...)
	if err != nil {
		log.Fatal(err)
	}
	index := 0
	if *pkgIndex > 0 {
		index = *pkgIndex
	}
	if len(pkgs) != 1 {
		if *pkgIndex < 0 {
			log.Fatalf("error: %d packages found (%v)", len(pkgs), pkgs)
		}
	}
	g.addPackage(pkgs[index])
}

// addPackage adds a type checked Package and its syntax files to the generator.
func (g *Generator) addPackage(pkg *packages.Package) {
	g.pkg = &Package{
		name:  pkg.Name,
		defs:  pkg.TypesInfo.Defs,
		files: make([]*File, len(pkg.Syntax)),
	}

	for i, file := range pkg.Syntax {
		g.pkg.files[i] = &File{
			file: file,
			pkg:  g.pkg,
		}
	}
}

// generate produces the String method for the named type.
func (g *Generator) generate(typeName string) {
	fields := make([]Field, 0, 100)
	for _, file := range g.pkg.files {
		// Set the state for this run of the walker.
		file.typeName = typeName
		if file.file != nil {
			ast.Inspect(file.file, file.genDecl)
			fields = append(fields, file.fields...)
		}
	}
	if len(fields) == 0 {
		if *verbose {
			log.Printf("no fields defined for type %s", typeName)
		}
	}
	// TODO: for now we remove Default from the start (maybe move that to an option)
	logicalTypeName := "\"" + strings.TrimPrefix(typeName, "Default") + "\""

	// Generate code that will fail if the constants change value.
	g.Printf("func (d *%s) Serialize() ([]byte, error) {\n", typeName)
	g.Printf("\tif d == nil {\n")
	g.Printf("\t\treturn nil, fmt.Errorf(\"(*DeviceInfoCache)(nil)\")\n")
	g.Printf("\t}\n")
	g.Printf("\twb := utils.NewWriteBufferByteBased(utils.WithByteOrderForByteBasedBuffer(binary.BigEndian))\n")
	g.Printf("\tif err := d.SerializeWithWriteBuffer(context.Background(), wb); err != nil {\n")
	g.Printf("\t\treturn nil, err\n")
	g.Printf("\t}\n")
	g.Printf("\treturn wb.GetBytes(), nil\n")
	g.Printf("}\n\n")
	g.Printf("func (d *%s) SerializeWithWriteBuffer(ctx context.Context, writeBuffer utils.WriteBuffer) error {\n", typeName)
	g.Printf("\tif d == nil {\n")
	g.Printf("\t\treturn fmt.Errorf(\"(*DeviceInfoCache)(nil)\")\n")
	g.Printf("\t}\n")
	g.Printf("\tif err := writeBuffer.PushContext(%s); err != nil {\n", logicalTypeName)
	g.Printf("\t\treturn err\n")
	g.Printf("\t}\n")
	for _, field := range fields {
		fieldType := field.fieldType
		if field.isDelegate {
			g.Printf("\t\t\tif err := d.%s.SerializeWithWriteBuffer(ctx, writeBuffer); err != nil {\n", fieldType.(*ast.Ident).Name)
			g.Printf("\t\t\t\treturn err\n")
			g.Printf("\t\t\t}\n")
			continue
		}
		fieldName := field.name
		fieldNameUntitled := "\"" + unTitle(fieldName) + "\""
		if field.hasLocker != "" {
			g.Printf("if err := func()error {\n")
			g.Printf("\td.%s.Lock()\n", field.hasLocker)
			g.Printf("\tdefer d.%s.Unlock()\n", field.hasLocker)
		}
		needsDereference := false
		if starFieldType, ok := fieldType.(*ast.StarExpr); ok {
			fieldType = starFieldType.X
			needsDereference = true
		}
		if field.isStringer {
			if needsDereference {
				g.Printf("if d.%s != nil {", field.name)
			}
			g.Printf(stringFieldSerialize, "d."+field.name+".String()", fieldNameUntitled)
			if field.hasLocker != "" {
				g.Printf("\treturn nil\n")
				g.Printf("}(); err != nil {\n")
				g.Printf("\treturn err\n")
				g.Printf("}\n")
			}
			if needsDereference {
				g.Printf("}\n")
			}
			continue
		}
		if field.asPtr {
			g.Printf(stringFieldSerialize, "fmt.Sprintf(\"%p\", d."+field.name+")", fieldNameUntitled)
			continue
		}
		if field.directSerialize {
			indentTimes := 0
			if needsDereference {
				indentTimes++
				g.Printf("if d.%s != nil {", field.name)
			}
			g.Printf(indent(indentTimes, serializableDirectFieldTemplate), "d."+field.name+"", fieldNameUntitled)
			if needsDereference {
				g.Printf("}\n")
			}
			continue
		}
		switch fieldType := fieldType.(type) {
		case *ast.SelectorExpr:
			{
				// TODO: bit hacky but not sure how else we catch those ones
				x := fieldType.X
				sel := fieldType.Sel
				xIdent, xIsIdent := x.(*ast.Ident)
				if xIsIdent {
					if xIdent.Name == "atomic" {
						if sel.Name == "Uint32" {
							g.Printf(uint32FieldSerialize, "d."+field.name+".Load()", fieldNameUntitled)
							if field.hasLocker != "" {
								g.Printf("\treturn nil\n")
								g.Printf("}(); err != nil {\n")
								g.Printf("\treturn err\n")
								g.Printf("}\n")
							}
							continue
						}
						if sel.Name == "Uint64" {
							g.Printf(uint64FieldSerialize, "d."+field.name+".Load()", fieldNameUntitled)
							if field.hasLocker != "" {
								g.Printf("\treturn nil\n")
								g.Printf("}(); err != nil {\n")
								g.Printf("\treturn err\n")
								g.Printf("}\n")
							}
							continue
						}
						if sel.Name == "Int32" {
							g.Printf(int32FieldSerialize, "d."+field.name+".Load()", fieldNameUntitled)
							if field.hasLocker != "" {
								g.Printf("\treturn nil\n")
								g.Printf("}(); err != nil {\n")
								g.Printf("\treturn err\n")
								g.Printf("}\n")
							}
							continue
						}
						if sel.Name == "Bool" {
							g.Printf(boolFieldSerialize, "d."+field.name+".Load()", fieldNameUntitled)
							if field.hasLocker != "" {
								g.Printf("\treturn nil\n")
								g.Printf("}(); err != nil {\n")
								g.Printf("\treturn err\n")
								g.Printf("}\n")
							}
							continue
						}
						if sel.Name == "Value" {
							g.Printf(serializableFieldTemplate, "d."+field.name+".Load()", fieldNameUntitled)
							if field.hasLocker != "" {
								g.Printf("\treturn nil\n")
								g.Printf("}(); err != nil {\n")
								g.Printf("\treturn err\n")
								g.Printf("}\n")
							}
							continue
						}
					}
					if xIdent.Name == "sync" {
						if *verbose {
							fmt.Printf("\t skipping field %s because it is %v.%v\n", fieldName, x, sel)
						}
						if field.hasLocker != "" {
							g.Printf("\treturn nil\n")
							g.Printf("}(); err != nil {\n")
							g.Printf("\treturn err\n")
							g.Printf("}\n")
						}
						continue
					}
					if xIdent.Name == "zerolog" {
						if *verbose {
							fmt.Printf("\t skipping field %s because it is %v.%v\n", fieldName, x, sel)
						}
						continue
					}
					if xIdent.Name == "time" {
						if sel.Name == "Time" || sel.Name == "Duration" {
							deref := ""
							indentTimes := 0
							if needsDereference {
								deref = "*"
								indentTimes++
								g.Printf("if d.%s != nil {", field.name)
							}
							g.Printf(indent(indentTimes, stringFieldSerialize), "fmt.Sprintf(\"%s\", "+deref+"d."+field.name+")", fieldNameUntitled)

							if needsDereference {
								g.Printf("}\n")
							}
							continue
						}
					}
				}
			}
			g.Printf(serializableFieldTemplate, "d."+field.name, fieldNameUntitled)
		case *ast.IndexExpr:
			x := fieldType.X
			if fieldType, isxFieldSelector := x.(*ast.SelectorExpr); isxFieldSelector { // TODO: we need to refactor this so we can reuse...
				xIdent, xIsIdent := fieldType.X.(*ast.Ident)
				sel := fieldType.Sel
				if xIsIdent && xIdent.Name == "atomic" && sel.Name == "Pointer" {
					g.Printf(atomicPointerFieldTemplate, "d."+field.name, field.name, fieldNameUntitled)
					if field.hasLocker != "" {
						g.Printf("\treturn nil\n")
						g.Printf("}(); err != nil {\n")
						g.Printf("\treturn err\n")
						g.Printf("}\n")
					}
					continue
				}
			}
			if *verbose {
				fmt.Printf("Warn: no support yet for %#q\n", fieldType)
			}
			continue
		case *ast.Ident:
			deref := ""
			indentTimes := 0
			if needsDereference {
				deref = "*"
				indentTimes++
				g.Printf("if d.%s != nil {", field.name)
			}
			switch fieldType.Name {
			case "byte":
				g.Printf(indent(indentTimes, byteFieldSerialize), deref+"d."+field.name, fieldNameUntitled)
			case "int":
				g.Printf(indent(indentTimes, int64FieldSerialize), "int64("+deref+"d."+field.name+")", fieldNameUntitled)
			case "int32":
				g.Printf(indent(indentTimes, int32FieldSerialize), "int32("+deref+"d."+field.name+")", fieldNameUntitled)
			case "uint16":
				g.Printf(indent(indentTimes, uint16FieldSerialize), deref+"d."+field.name, fieldNameUntitled)
			case "uint32":
				g.Printf(indent(indentTimes, uint32FieldSerialize), deref+"d."+field.name, fieldNameUntitled)
			case "bool":
				g.Printf(indent(indentTimes, boolFieldSerialize), deref+"d."+field.name, fieldNameUntitled)
			case "string":
				g.Printf(indent(indentTimes, stringFieldSerialize), deref+"d."+field.name, fieldNameUntitled)
			case "error":
				g.Printf(indent(indentTimes, errorFieldSerialize), deref+"d."+field.name, fieldNameUntitled)
			default:
				if *verbose {
					fmt.Printf("\t Warn: no support implemented for Ident with type %v\n", fieldType)
				}
				g.Printf("{\n")
				g.Printf("_value := fmt.Sprintf(\"%%v\", d.%s)\n", fieldName)
				g.Printf(stringFieldSerialize, "_value", fieldNameUntitled)
				g.Printf("}\n")
			}
			if needsDereference {
				g.Printf("}\n")
			}
		case *ast.ArrayType:
			if eltType, ok := fieldType.Elt.(*ast.Ident); ok && eltType.Name == "byte" {
				g.Printf("if err := writeBuffer.WriteByteArray(%s, d.%s); err != nil {\n", fieldNameUntitled, field.name)
				g.Printf("\treturn err\n")
				g.Printf("}\n")
			} else {
				g.Printf("if err := writeBuffer.PushContext(%s, utils.WithRenderAsList(true)); err != nil {\n\t\treturn err\n\t}\n", fieldNameUntitled)
				g.Printf("for _, elem := range d.%s {", field.name)
				switch eltType := fieldType.Elt.(type) {
				case *ast.SelectorExpr, *ast.StarExpr:
					g.Printf("\n\t\tvar elem any = elem\n")
					g.Printf(serializableFieldTemplate, "elem", "\"value\"")
				case *ast.Ident:
					switch eltType.Name {
					case "int":
						g.Printf(int64FieldSerialize, "int64(d."+field.name+")", fieldNameUntitled)
					case "uint32":
						g.Printf(uint32FieldSerialize, "d."+field.name, fieldNameUntitled)
					case "bool":
						g.Printf(boolFieldSerialize, "elem", "\"\"")
					case "string":
						g.Printf(stringFieldSerialize, "elem", "\"\"")
					case "error":
						g.Printf(errorFieldSerialize, "elem", "\"\"")
					default:
						if *verbose {
							fmt.Printf("\t Warn: no support implemented for Ident within ArrayType for %v\n", fieldType)
						}
						g.Printf("_value := fmt.Sprintf(\"%%v\", elem)\n")
						g.Printf(stringFieldSerialize, "_value", fieldNameUntitled)
					}
				}
				g.Printf("}\n")
				g.Printf("if err := writeBuffer.PopContext(%s, utils.WithRenderAsList(true)); err != nil {\n\t\treturn err\n\t}\n", fieldNameUntitled)
			}
		case *ast.MapType:
			g.Printf("if err := writeBuffer.PushContext(%s, utils.WithRenderAsList(true)); err != nil {\n\t\treturn err\n\t}\n", fieldNameUntitled)
			// TODO: we use serializable or strings as we don't want to over-complex this
			g.Printf("for _name, elem := range d.%s {\n", fieldName)
			switch keyType := fieldType.Key.(type) {
			case *ast.Ident:
				switch keyType.Name {
				case "uint", "uint8", "uint16", "uint32", "uint64", "int", "int8", "int16", "int32", "int64": // TODO: add other types
					g.Printf("\t\tname := fmt.Sprintf(\"%s\", _name)\n", "%v")
				case "string":
					g.Printf("\t\tname := _name\n")
				default:
					g.Printf("\t\tname := fmt.Sprintf(\"%s\", &_name)\n", "%v")
				}
			default:
				g.Printf("\t\tname := fmt.Sprintf(\"%s\", &_name)\n", "%v")
			}
			switch eltType := fieldType.Value.(type) {
			case *ast.StarExpr, *ast.SelectorExpr:
				g.Printf("\n\t\tvar elem any = elem\n")
				g.Printf("\t\tif serializable, ok := elem.(utils.Serializable); ok {\n")
				g.Printf("\t\t\tif err := writeBuffer.PushContext(name); err != nil {\n")
				g.Printf("\t\t\t\treturn err\n")
				g.Printf("\t\t\t}\n")
				g.Printf("\t\t\tif err := serializable.SerializeWithWriteBuffer(ctx, writeBuffer); err != nil {\n")
				g.Printf("\t\t\t\treturn err\n")
				g.Printf("\t\t\t}\n")
				g.Printf("\t\t\tif err := writeBuffer.PopContext(name); err != nil {\n")
				g.Printf("\t\t\t\treturn err\n")
				g.Printf("\t\t\t}\n")
				g.Printf("\t\t} else {\n")
				g.Printf("\t\t\telemAsString := fmt.Sprintf(\"%%v\", elem)\n")
				g.Printf("\t\t\tif err := writeBuffer.WriteString(name, uint32(len(elemAsString)*8), elemAsString); err != nil {\n")
				g.Printf("\t\t\t\treturn err\n")
				g.Printf("\t\t\t}\n")
				g.Printf("\t\t}\n")
			case *ast.Ident:
				switch eltType.Name {
				case "bool":
					g.Printf(boolFieldSerialize, "elem", "name")
				case "string":
					g.Printf(stringFieldSerialize, "elem", "name")
				case "error":
					g.Printf(errorFieldSerialize, "elem", "name")
				default:
					if *verbose {
						fmt.Printf("\t Warn: no support implemented for Ident within MapType for %v\n", fieldType)
					}
					g.Printf("\t\t_value := fmt.Sprintf(\"%%v\", elem)\n")
					g.Printf(stringFieldSerialize, "_value", "name")
				}
			default:
				if *verbose {
					fmt.Printf("\t Warn: no support implemented within MapType %v\n", fieldType.Value)
				}
				g.Printf("\t\t_value := fmt.Sprintf(\"%%v\", elem)\n")
				g.Printf(stringFieldSerialize, "_value", "name")
			}
			g.Printf("\t}\n")
			g.Printf("if err := writeBuffer.PopContext(%s, utils.WithRenderAsList(true)); err != nil {\n\t\treturn err\n\t}\n", fieldNameUntitled)
		case *ast.ChanType:
			g.Printf(chanFieldSerialize, "d."+field.name, fieldNameUntitled, field.name)
		case *ast.FuncType:
			g.Printf(funcFieldSerialize, "d."+field.name, fieldNameUntitled)
		default:
			if *verbose {
				fmt.Printf("Warn: no support implemented %#v\n", fieldType)
			}
		}
		if field.hasLocker != "" {
			g.Printf("\treturn nil\n")
			g.Printf("}(); err != nil {\n")
			g.Printf("\treturn err\n")
			g.Printf("}\n")
		}
	}
	g.Printf("\tif err := writeBuffer.PopContext(%s); err != nil {\n", logicalTypeName)
	g.Printf("\t\treturn err\n")
	g.Printf("\t}\n")
	g.Printf("\treturn nil\n")
	g.Printf("}\n")
	g.Printf("\n")
	g.Printf(stringerTemplate, typeName)
}

// format returns the gofmt-ed contents of the Generator's buffer.
func (g *Generator) format() []byte {
	src, err := format.Source(g.buf.Bytes())
	if err != nil {
		// Should never happen, but can arise when developing this code.
		// The user can compile the output to see the error.
		log.Printf("warning: internal error: invalid Go generated: %s", err)
		log.Printf("warning: compile the package to analyze the error")
		return g.buf.Bytes()
	}
	return src
}

// Field represents a declared field.
type Field struct {
	name            string
	fieldType       ast.Expr
	isDelegate      bool
	isStringer      bool
	asPtr           bool
	directSerialize bool
	hasLocker       string
}

func (f *Field) String() string {
	return f.name
}

// genDecl processes one declaration clause.
func (f *File) genDecl(node ast.Node) bool {
	decl, ok := node.(*ast.GenDecl)
	if !ok || decl.Tok != token.TYPE {
		// We only care about type declarations.
		return true
	}
	for _, spec := range decl.Specs {
		typeSpec := spec.(*ast.TypeSpec)
		structDecl, ok := typeSpec.Type.(*ast.StructType)
		if !ok {
			continue
		}
		if typeSpec.Name.Name != f.typeName {
			continue
		}
		if *verbose {
			fmt.Printf("Handling %s\n", typeSpec.Name.Name)
		}
		for _, field := range structDecl.Fields.List {
			if field.Tag != nil && field.Tag.Value == "`ignore:\"true\"`" {
				var name string
				if len(field.Names) != 0 {
					name = field.Names[0].Name
				} else {
					name = "<delegate>"
				}
				if *verbose {
					fmt.Printf("\t ignoring field %s %v\n", name, field.Type)
				}
				continue
			}
			isStringer := false
			if field.Tag != nil && field.Tag.Value == "`stringer:\"true\"`" { // TODO: Check if we do that a bit smarter
				isStringer = true
			}
			hasLocker := ""
			if field.Tag != nil && strings.HasPrefix(field.Tag.Value, "`hasLocker:\"") { // TODO: Check if we do that a bit smarter
				hasLocker = strings.TrimPrefix(field.Tag.Value, "`hasLocker:\"")
				hasLocker = strings.TrimSuffix(hasLocker, "\"`")
			}
			asPtr := false
			if field.Tag != nil && field.Tag.Value == "`asPtr:\"true\"`" { // TODO: Check if we do that a bit smarter
				asPtr = true
			}
			directSerialize := false
			if field.Tag != nil && field.Tag.Value == "`directSerialize:\"true\"`" { // TODO: Check if we do that a bit smarter
				directSerialize = true
			}
			if len(field.Names) == 0 {
				if *verbose {
					fmt.Printf("\t adding delegate\n")
				}
				switch ft := field.Type.(type) {
				case *ast.Ident:
					f.fields = append(f.fields, Field{
						fieldType:  ft,
						isDelegate: true,
						isStringer: isStringer,
						asPtr:      asPtr,
						hasLocker:  hasLocker,
					})
					continue
				case *ast.StarExpr:
					switch set := ft.X.(type) {
					case *ast.Ident:
						f.fields = append(f.fields, Field{
							fieldType:  set,
							isDelegate: true,
							isStringer: isStringer,
							asPtr:      asPtr,
							hasLocker:  hasLocker,
						})
						continue
					case *ast.SelectorExpr:
						f.fields = append(f.fields, Field{
							fieldType:  set.Sel,
							isDelegate: true,
							isStringer: isStringer,
							asPtr:      asPtr,
							hasLocker:  hasLocker,
						})
						continue
					default:
						panic(fmt.Sprintf("Only pointer to struct delegates supported now. Type %T", field.Type))
					}
				case *ast.SelectorExpr:
					f.fields = append(f.fields, Field{
						fieldType:  ft.Sel,
						isDelegate: true,
						isStringer: isStringer,
						asPtr:      asPtr,
						hasLocker:  hasLocker,
					})
					continue
				default:
					panic(fmt.Sprintf("Only struct delegates supported now. Type %T", field.Type))
				}
			}
			if *verbose {
				fmt.Printf("\t adding field %s %v\n", field.Names[0].Name, field.Type)
			}
			f.fields = append(f.fields, Field{
				name:            field.Names[0].Name,
				fieldType:       field.Type,
				isStringer:      isStringer,
				asPtr:           asPtr,
				directSerialize: directSerialize,
				hasLocker:       hasLocker,
			})
		}
	}
	return false
}

func indent(times int, input string) string {
	if times == 0 {
		return input
	}
	lines := strings.Split(input, "\n")
	indents := strings.Repeat("\t", times)
	return indents + strings.Join(lines, "\n"+indents)
}

var stringerTemplate = `
func (d *%s) String() string {
	if alternateStringer, ok := any(d).(utils.AlternateStringer); ok {
		if alternateString, use := alternateStringer.AlternateString(); use {
			return alternateString
		}
	}
	wb := utils.NewWriteBufferBoxBased(utils.WithWriteBufferBoxBasedMergeSingleBoxes(), utils.WithWriteBufferBoxBasedOmitEmptyBoxes())
	if err := wb.WriteSerializable(context.Background(), d); err != nil {
		return err.Error()
	}
	return wb.GetBox().String()
}
`

var serializableDirectFieldTemplate = `
	if err := writeBuffer.PushContext(%[2]s); err != nil {
		return err
	}
	if err := %[1]s.SerializeWithWriteBuffer(ctx, writeBuffer); err != nil {
		return err
	}
	if err := writeBuffer.PopContext(%[2]s); err != nil {
		return err
	}
`

var serializableFieldTemplate = `
	if %[1]s != nil {
		if serializableField, ok := any(%[1]s).(utils.Serializable); ok {
			if err := writeBuffer.PushContext(%[2]s); err != nil {
				return err
			}
			if err := serializableField.SerializeWithWriteBuffer(ctx, writeBuffer); err != nil {
				return err
			}
			if err := writeBuffer.PopContext(%[2]s); err != nil {
				return err
			}
		} else {
			stringValue := fmt.Sprintf("%%v", %[1]s)
			if err := writeBuffer.WriteString(%[2]s, uint32(len(stringValue)*8), stringValue); err != nil {
				return err
			}
		}
	}
`

var atomicPointerFieldTemplate = `
	if %[2]sLoaded :=%[1]s.Load(); %[2]sLoaded != nil && *%[2]sLoaded != nil {
		%[2]s := *%[2]sLoaded
		if serializableField, ok := %[2]s.(utils.Serializable); ok {
			if err := writeBuffer.PushContext(%[3]s); err != nil {
				return err
			}
			if err := serializableField.SerializeWithWriteBuffer(ctx, writeBuffer); err != nil {
				return err
			}
			if err := writeBuffer.PopContext(%[3]s); err != nil {
				return err
			}
		} else {
			stringValue := fmt.Sprintf("%%v", %[2]s)
			if err := writeBuffer.WriteString(%[3]s, uint32(len(stringValue)*8), stringValue); err != nil {
				return err
			}
		}
	}
`

var byteFieldSerialize = `
	if err := writeBuffer.WriteByte(%[2]s, %[1]s); err != nil {
		return err
	}
`

var int32FieldSerialize = `
	if err := writeBuffer.WriteInt32(%[2]s, 32, %[1]s); err != nil {
		return err
	}
`

var int64FieldSerialize = `
	if err := writeBuffer.WriteInt64(%[2]s, 64, %[1]s); err != nil {
		return err
	}
`

var uint16FieldSerialize = `
	if err := writeBuffer.WriteUint16(%[2]s, 16, %[1]s); err != nil {
		return err
	}
`

var uint32FieldSerialize = `
	if err := writeBuffer.WriteUint32(%[2]s, 32, %[1]s); err != nil {
		return err
	}
`

var uint64FieldSerialize = `
	if err := writeBuffer.WriteUint64(%[2]s, 64, %[1]s); err != nil {
		return err
	}
`

var boolFieldSerialize = `
	if err := writeBuffer.WriteBit(%[2]s, %[1]s); err != nil {
		return err
	}
`

var stringFieldSerialize = `
	if err := writeBuffer.WriteString(%[2]s, uint32(len(%[1]s)*8), %[1]s); err != nil {
		return err
	}
`

var errorFieldSerialize = `
	if %[1]s != nil {
		_errString := %[1]s.Error()
		if err := writeBuffer.WriteString(%[2]s, uint32(len(_errString)*8), _errString); err != nil {
			return err
		}
	}
`

var chanFieldSerialize = `
	_%[3]s_plx4gen_description := fmt.Sprintf("%%d element(s)", len(%[1]s))
    if err := writeBuffer.WriteString(%[2]s, uint32(len(_%[3]s_plx4gen_description)*8), _%[3]s_plx4gen_description); err != nil {
		return err
	}
`

var funcFieldSerialize = `
	if err := writeBuffer.WriteBit(%[2]s, %[1]s != nil); err != nil {
		return err
	}
`

func unTitle(value string) string {
	return strings.ToLower(string(value[0])) + value[1:]
}
